Skip to main content

broot/shell_install/
mod.rs

1mod bash;
2mod fish;
3mod nushell;
4mod powershell;
5mod state;
6mod util;
7
8use {
9    crate::{
10        cli,
11        errors::*,
12        skin,
13    },
14    std::{
15        fs,
16        os,
17        path::Path,
18    },
19    termimad::{
20        MadSkin,
21        mad_print_inline,
22    },
23};
24
25pub use state::ShellInstallState;
26
27const MD_INSTALL_REQUEST: &str = r"
28**Broot** should be launched using a shell function.
29This function most notably makes it possible to `cd` from inside broot
30(see *https://dystroy.org/broot/install-br/* for explanations).
31
32Can I install it now? [**Y**/n]
33";
34
35const MD_UPGRADE_REQUEST: &str = r"
36Broot's shell function should be upgraded.
37
38Can I proceed? [**Y**/n]
39";
40
41const MD_INSTALL_CANCELLED: &str = r"
42You refused the installation (for now).
43You can still used `broot` but some features won't be available.
44If you want the `br` shell function, you may either
45* do `broot --install`
46* install the various pieces yourself
47(see *https://dystroy.org/broot/install-br/* for details).
48
49";
50
51const MD_PERMISSION_DENIED: &str = r"
52Installation check resulted in **Permission Denied**.
53Please relaunch with elevated privilege.
54This is typically only needed once.
55Error details:
56";
57
58const MD_INSTALL_DONE: &str = r"
59The **br** function has been successfully installed.
60You may have to restart your shell or source your shell init files.
61Afterwards, you should start broot with `br` in order to use its full power.
62
63";
64
65pub struct ShellInstall {
66    force_install: bool, // when the program was launched with --install
67    skin: MadSkin,
68    pub should_quit: bool,
69    authorization: Option<bool>,
70    done: bool, // true if the installation was just made
71}
72
73impl ShellInstall {
74    pub fn new(force_install: bool) -> Self {
75        Self {
76            force_install,
77            skin: skin::make_cli_mad_skin(),
78            should_quit: false,
79            authorization: if force_install { Some(true) } else { None },
80            done: false,
81        }
82    }
83
84    /// write on stdout the script building the function for
85    /// the given shell
86    pub fn print(shell: &str) -> Result<(), ProgramError> {
87        match shell {
88            "bash" | "zsh" => println!("{}", bash::get_script()),
89            "fish" => println!("{}", fish::get_script()),
90            "nushell" => println!("{}", nushell::get_script()),
91            "powershell" => println!("{}", powershell::get_script()),
92            _ => {
93                return Err(ProgramError::UnknownShell {
94                    shell: shell.to_string(),
95                });
96            }
97        }
98        Ok(())
99    }
100
101    /// check whether the shell function is installed an up to date,
102    /// install it if it wasn't refused before or if broot is launched
103    /// with --install.
104    pub fn check(&mut self) -> Result<(), ShellInstallError> {
105        let install_state = ShellInstallState::detect();
106        info!("Shell installation state: {install_state:?}");
107        if self.force_install {
108            self.skin.print_text("You requested a clean (re)install.");
109            ShellInstallState::remove(self)?;
110        } else {
111            match install_state {
112                ShellInstallState::Refused => {
113                    return Ok(());
114                }
115                ShellInstallState::UpToDate => {
116                    return Ok(());
117                }
118                ShellInstallState::Obsolete => {
119                    if !self.can_upgrade()? {
120                        debug!("User refuses the upgrade. Doing nothing.");
121                        return Ok(());
122                    }
123                }
124                ShellInstallState::NotInstalled => {
125                    if !self.can_install()? {
126                        debug!("User refuses the installation. Doing nothing.");
127                        return Ok(());
128                    }
129                }
130            }
131        }
132        // even if the installation isn't really complete (for example
133        // when no bash file was found), we don't want to ask the user
134        // again, we'll assume it's done
135        ShellInstallState::UpToDate.write(self)?;
136        debug!("Starting install");
137        bash::install(self)?;
138        fish::install(self)?;
139        nushell::install(self)?;
140        powershell::install(self)?;
141        self.should_quit = true;
142        if self.done {
143            self.skin.print_text(MD_INSTALL_DONE);
144        }
145        Ok(())
146    }
147
148    /// print some additional information on the error (typically before
149    /// the error itself is dumped)
150    pub fn comment_error(
151        &self,
152        err: &ShellInstallError,
153    ) {
154        if err.is_permission_denied() {
155            self.skin.print_text(MD_PERMISSION_DENIED);
156        }
157    }
158
159    pub fn remove(
160        &self,
161        path: &Path,
162    ) -> Result<(), ShellInstallError> {
163        // path.exists() doesn't work when the file is a link (it checks whether
164        // the link destination exists instead of checking the link exists
165        // so we first check whether the link exists
166        if fs::read_link(path).is_ok() || path.exists() {
167            mad_print_inline!(self.skin, "Removing `$0`.\n", path.to_string_lossy());
168            fs::remove_file(path).context(&|| format!("removing {path:?}"))?;
169        }
170        Ok(())
171    }
172
173    /// check whether we're allowed to install.
174    fn can_install(&mut self) -> Result<bool, ShellInstallError> {
175        self.can_do(false)
176    }
177    fn can_upgrade(&mut self) -> Result<bool, ShellInstallError> {
178        self.can_do(true)
179    }
180    fn can_do(
181        &mut self,
182        upgrade: bool,
183    ) -> Result<bool, ShellInstallError> {
184        if let Some(authorization) = self.authorization {
185            return Ok(authorization);
186        }
187        let refused_path = ShellInstallState::get_refused_path();
188        if refused_path.exists() {
189            debug!("User already refused the installation");
190            return Ok(false);
191        }
192        self.skin.print_text(if upgrade {
193            MD_UPGRADE_REQUEST
194        } else {
195            MD_INSTALL_REQUEST
196        });
197        let proceed = cli::ask_authorization().context(&|| "asking user".to_string())?; // read_line failure
198        debug!("proceed: {:?}", proceed);
199        self.authorization = Some(proceed);
200        if !proceed {
201            ShellInstallState::Refused.write(self)?;
202            self.skin.print_text(MD_INSTALL_CANCELLED);
203        }
204        Ok(proceed)
205    }
206
207    /// write the script at the given path
208    fn write_script(
209        &self,
210        script_path: &Path,
211        content: &str,
212    ) -> Result<(), ShellInstallError> {
213        self.remove(script_path)?;
214        info!("Writing `br` shell function in `{:?}`", &script_path);
215        mad_print_inline!(
216            &self.skin,
217            "Writing *br* shell function in `$0`.\n",
218            script_path.to_string_lossy(),
219        );
220        fs::create_dir_all(script_path.parent().unwrap())
221            .context(&|| format!("creating parent dirs to {script_path:?}"))?;
222        fs::write(script_path, content)
223            .context(&|| format!("writing script in {script_path:?}"))?;
224        Ok(())
225    }
226
227    /// create a link
228    fn create_link(
229        &self,
230        link_path: &Path,
231        script_path: &Path,
232    ) -> Result<(), ShellInstallError> {
233        info!("Creating link from {:?} to {:?}", &link_path, &script_path);
234        self.remove(link_path)?;
235        let link_path_str = link_path.to_string_lossy();
236        let script_path_str = script_path.to_string_lossy();
237        mad_print_inline!(
238            &self.skin,
239            "Creating link from `$0` to `$1`.\n",
240            &link_path_str,
241            &script_path_str,
242        );
243        let parent = link_path.parent().unwrap();
244        fs::create_dir_all(parent).context(&|| format!("creating directory {parent:?}"))?;
245        #[cfg(unix)]
246        os::unix::fs::symlink(script_path, link_path)
247            .context(&|| format!("linking from {link_path:?} to {script_path:?}"))?;
248        #[cfg(windows)]
249        os::windows::fs::symlink_file(&script_path, &link_path)
250            .context(&|| format!("linking from {link_path:?} to {script_path:?}"))?;
251        Ok(())
252    }
253}