pacaptr/pm/
brew.rs

1#![doc = doc_self!()]
2
3use std::sync::LazyLock;
4
5use async_trait::async_trait;
6use indoc::indoc;
7use tap::prelude::*;
8
9use super::{DryRunStrategy, NoCacheStrategy, Pm, PmHelper, PromptStrategy, Strategy};
10use crate::{config::Config, error::Result, exec::Cmd};
11
12macro_rules! doc_self {
13    () => {
14        indoc! {"
15            The [Homebrew Package Manager](https://brew.sh/).
16        "}
17    };
18}
19use doc_self;
20
21#[doc = doc_self!()]
22#[derive(Debug)]
23pub struct Brew {
24    cfg: Config,
25}
26
27static STRAT_PROMPT: LazyLock<Strategy> = LazyLock::new(|| Strategy {
28    prompt: PromptStrategy::CustomPrompt,
29    ..Strategy::default()
30});
31
32static STRAT_INSTALL: LazyLock<Strategy> = LazyLock::new(|| Strategy {
33    prompt: PromptStrategy::CustomPrompt,
34    no_cache: NoCacheStrategy::Scc,
35    ..Strategy::default()
36});
37
38impl Brew {
39    #[must_use]
40    #[allow(missing_docs)]
41    pub const fn new(cfg: Config) -> Self {
42        Self { cfg }
43    }
44}
45
46#[async_trait]
47impl Pm for Brew {
48    /// Gets the name of the package manager.
49    fn name(&self) -> &'static str {
50        "brew"
51    }
52
53    fn cfg(&self) -> &Config {
54        &self.cfg
55    }
56
57    /// Q generates a list of installed packages.
58    async fn q(&self, kws: &[&str], flags: &[&str]) -> Result<()> {
59        if kws.is_empty() {
60            self.run(Cmd::new(["brew", "list"]).flags(flags)).await
61        } else {
62            self.qs(kws, flags).await
63        }
64    }
65
66    /// Qc shows the changelog of a package.
67    async fn qc(&self, kws: &[&str], flags: &[&str]) -> Result<()> {
68        self.run(Cmd::new(["brew", "log"]).kws(kws).flags(flags))
69            .await
70    }
71
72    /// Qi displays local package information: name, version, description, etc.
73    async fn qi(&self, kws: &[&str], flags: &[&str]) -> Result<()> {
74        self.si(kws, flags).await
75    }
76
77    /// Qii displays local packages which require X to be installed, aka local
78    /// reverse dependencies.
79    async fn qii(&self, kws: &[&str], flags: &[&str]) -> Result<()> {
80        Cmd::new(["brew", "uses", "--installed"])
81            .kws(kws)
82            .flags(flags)
83            .pipe(|cmd| self.run(cmd))
84            .await
85    }
86
87    /// Ql displays files provided by local package.
88    async fn ql(&self, kws: &[&str], flags: &[&str]) -> Result<()> {
89        // TODO: it seems that the output of `brew list python` in fish has a mechanism
90        // against duplication: /usr/local/Cellar/python/3.6.0/Frameworks/
91        // Python.framework/ (1234 files)
92        self.run(Cmd::new(["brew", "list"]).kws(kws).flags(flags))
93            .await
94    }
95
96    /// Qs searches locally installed package for names or descriptions.
97    // According to https://www.archlinux.org/pacman/pacman.8.html#_query_options_apply_to_em_q_em_a_id_qo_a,
98    // when including multiple search terms, only packages with descriptions
99    // matching ALL of those terms are returned.
100    async fn qs(&self, kws: &[&str], flags: &[&str]) -> Result<()> {
101        // ! `brew list` lists all formulae and casks only when using tty.
102        self.search_regex(Cmd::new(["brew", "list", "--formula"]).flags(flags), kws)
103            .await?;
104        if cfg!(target_os = "macos") {
105            self.search_regex(Cmd::new(["brew", "list", "--cask"]).flags(flags), kws)
106                .await?;
107        }
108
109        Ok(())
110    }
111
112    /// Qu lists packages which have an update available.
113    async fn qu(&self, kws: &[&str], flags: &[&str]) -> Result<()> {
114        self.run(Cmd::new(["brew", "outdated"]).kws(kws).flags(flags))
115            .await
116    }
117
118    /// R removes a single package, leaving all of its dependencies installed.
119    async fn r(&self, kws: &[&str], flags: &[&str]) -> Result<()> {
120        Cmd::new(["brew", "uninstall"])
121            .kws(kws)
122            .flags(flags)
123            .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_PROMPT))
124            .await
125    }
126
127    /// Rn removes a package and skips the generation of configuration backup
128    /// files.
129    async fn rn(&self, kws: &[&str], flags: &[&str]) -> Result<()> {
130        Cmd::new(["brew", "uninstall", "--zap", "-f"])
131            .kws(kws)
132            .flags(flags)
133            .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_PROMPT))
134            .await
135    }
136
137    /// Rns removes a package and its dependencies which are not required by any
138    /// other installed package, and skips the generation of configuration
139    /// backup files.
140    async fn rns(&self, kws: &[&str], flags: &[&str]) -> Result<()> {
141        self.rn(kws, flags).await?;
142        Cmd::new(["brew", "autoremove"])
143            .flags(flags)
144            .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_PROMPT))
145            .await
146    }
147
148    /// Rs removes a package and its dependencies which are not required by any
149    /// other installed package, and not explicitly installed by the user.
150    async fn rs(&self, kws: &[&str], flags: &[&str]) -> Result<()> {
151        self.r(kws, flags).await?;
152        Cmd::new(["brew", "autoremove"])
153            .flags(flags)
154            .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_PROMPT))
155            .await
156    }
157
158    /// S installs one or more packages by name.
159    async fn s(&self, kws: &[&str], flags: &[&str]) -> Result<()> {
160        Cmd::new(if self.cfg.needed {
161            ["brew", "install"]
162        } else {
163            // If the package is not installed, `brew reinstall` behaves just like `brew
164            // install`, so `brew reinstall` matches perfectly the behavior of
165            // `pacman -S`.
166            ["brew", "reinstall"]
167        })
168        .kws(kws)
169        .flags(flags)
170        .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_INSTALL))
171        .await
172    }
173
174    /// Sc removes all the cached packages that are not currently installed, and
175    /// the unused sync database.
176    async fn sc(&self, kws: &[&str], flags: &[&str]) -> Result<()> {
177        let strat = Strategy {
178            dry_run: DryRunStrategy::with_flags(["--dry-run"]),
179            prompt: PromptStrategy::CustomPrompt,
180            ..Strategy::default()
181        };
182        Cmd::new(["brew", "cleanup"])
183            .kws(kws)
184            .flags(flags)
185            .pipe(|cmd| self.run_with(cmd, self.default_mode(), &strat))
186            .await
187    }
188
189    /// Scc removes all files from the cache.
190    async fn scc(&self, kws: &[&str], flags: &[&str]) -> Result<()> {
191        let strat = Strategy {
192            dry_run: DryRunStrategy::with_flags(["--dry-run"]),
193            prompt: PromptStrategy::CustomPrompt,
194            ..Strategy::default()
195        };
196        Cmd::new(["brew", "cleanup", "-s"])
197            .kws(kws)
198            .flags(flags)
199            .pipe(|cmd| self.run_with(cmd, self.default_mode(), &strat))
200            .await
201    }
202
203    /// Sccc performs a deeper cleaning of the cache than `Scc` (if applicable).
204    async fn sccc(&self, kws: &[&str], flags: &[&str]) -> Result<()> {
205        let strat = Strategy {
206            dry_run: DryRunStrategy::with_flags(["--dry-run"]),
207            prompt: PromptStrategy::CustomPrompt,
208            ..Strategy::default()
209        };
210        Cmd::new(["brew", "cleanup", "--prune=all"])
211            .kws(kws)
212            .flags(flags)
213            .pipe(|cmd| self.run_with(cmd, self.default_mode(), &strat))
214            .await
215    }
216
217    /// Si displays remote package information: name, version, description, etc.
218    async fn si(&self, kws: &[&str], flags: &[&str]) -> Result<()> {
219        self.run(Cmd::new(["brew", "info"]).kws(kws).flags(flags))
220            .await
221    }
222
223    /// Sii displays packages which require X to be installed, aka reverse
224    /// dependencies.
225    async fn sii(&self, kws: &[&str], flags: &[&str]) -> Result<()> {
226        Cmd::new(["brew", "uses", "--eval-all"])
227            .kws(kws)
228            .flags(flags)
229            .pipe(|cmd| self.run(cmd))
230            .await
231    }
232
233    /// Ss searches for package(s) by searching the expression in name,
234    /// description, short description.
235    async fn ss(&self, kws: &[&str], flags: &[&str]) -> Result<()> {
236        self.run(Cmd::new(["brew", "search"]).kws(kws).flags(flags))
237            .await
238    }
239
240    /// Su updates outdated packages.
241    async fn su(&self, kws: &[&str], flags: &[&str]) -> Result<()> {
242        Cmd::new(["brew", "upgrade"])
243            .kws(kws)
244            .flags(flags)
245            .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_INSTALL))
246            .await
247    }
248
249    /// Suy refreshes the local package database, then updates outdated
250    /// packages.
251    async fn suy(&self, kws: &[&str], flags: &[&str]) -> Result<()> {
252        self.sy(&[], flags).await?;
253        self.su(kws, flags).await
254    }
255
256    /// Sw retrieves all packages from the server, but does not install/upgrade
257    /// anything.
258    async fn sw(&self, kws: &[&str], flags: &[&str]) -> Result<()> {
259        Cmd::new(["brew", "fetch"])
260            .kws(kws)
261            .flags(flags)
262            .pipe(|cmd| self.run_with(cmd, self.default_mode(), &STRAT_PROMPT))
263            .await
264    }
265
266    /// Sy refreshes the local package database.
267    async fn sy(&self, kws: &[&str], flags: &[&str]) -> Result<()> {
268        self.run(Cmd::new(["brew", "update"]).flags(flags)).await?;
269        if !kws.is_empty() {
270            self.s(kws, flags).await?;
271        }
272        Ok(())
273    }
274}