Skip to main content

broot/shell_install/
bash.rs

1//! The goal of this mod is to ensure the launcher shell function
2//! is available for bash and zsh i.e. the `br` shell function can
3//! be used to launch broot (and thus make it possible to execute
4//! some commands, like `cd`, from the starting shell.
5//!
6//! In a correct installation, we have:
7//! - a function declaration script in ~/.local/share/broot/launcher/bash/br/1
8//! - a link to that script in ~/.config/broot/launcher/bash/br/1
9//! - a line to source the link in ~/.bashrc and ~/.zshrc
10//!
11//! (exact paths depend on XDG variables)
12
13use {
14    super::{
15        ShellInstall,
16        util,
17    },
18    crate::{
19        conf,
20        errors::*,
21    },
22    directories::UserDirs,
23    lazy_regex::regex,
24    regex::Captures,
25    std::{
26        env,
27        path::PathBuf,
28    },
29    termimad::mad_print_inline,
30};
31
32const NAME: &str = "bash";
33const SOURCING_FILES: &[&str] = &[".bashrc", ".bash_profile", ".zshrc", "$ZDOTDIR/.zshrc"];
34const VERSION: &str = "1";
35
36// This script has been tested on bash and zsh.
37// It's installed under the bash name (~/.config/broot
38// but linked from both the .bashrc and the .zshrc files
39const BASH_FUNC: &str = r#"
40# This script was automatically generated by the broot program
41# More information can be found in https://github.com/Canop/broot
42# This function starts broot and executes the command
43# it produces, if any.
44# It's needed because some shell commands, like `cd`,
45# have no useful effect if executed in a subshell.
46function br {
47    local cmd cmd_file code
48    cmd_file=$(mktemp)
49    if broot --outcmd "$cmd_file" "$@"; then
50        cmd=$(<"$cmd_file")
51        command rm -f "$cmd_file"
52        eval "$cmd"
53    else
54        code=$?
55        command rm -f "$cmd_file"
56        return "$code"
57    fi
58}
59"#;
60
61const MD_NO_SOURCING: &str = r"
62I found no sourcing file for the bash/zsh family.
63If you're using bash or zsh, then installation isn't complete:
64the br function initialization script won't be sourced unless you source it yourself.
65";
66
67pub fn get_script() -> &'static str {
68    BASH_FUNC
69}
70
71/// return the path to the link to the function script
72fn get_link_path() -> PathBuf {
73    conf::dir().join("launcher").join(NAME).join("br")
74}
75
76/// return the path to the script containing the function.
77///
78/// At version 0.10.4 we change the location of the script:
79/// It was previously with the link, but it's now in
80/// XDG_DATA_HOME (typically ~/.local/share on linux)
81fn get_script_path() -> PathBuf {
82    conf::app_dirs()
83        .data_dir()
84        .join("launcher")
85        .join(NAME)
86        .join(VERSION)
87}
88
89/// return the paths to the files in which the br function is sourced.
90/// Paths in SOURCING_FILES can be absolute or relative to the home
91/// directory. Environment variables designed as $NAME are interpolated.
92fn get_sourcing_paths() -> Vec<PathBuf> {
93    let homedir_path = UserDirs::new()
94        .expect("no home directory!")
95        .home_dir()
96        .to_path_buf();
97    SOURCING_FILES
98        .iter()
99        .map(|name| {
100            regex!(r#"\$(\w+)"#)
101                .replace(name, |c: &Captures<'_>| {
102                    env::var(&c[1]).unwrap_or_else(|_| (*name).to_string())
103                })
104                .to_string()
105        })
106        .map(PathBuf::from)
107        .map(|path| {
108            if path.is_absolute() {
109                path
110            } else {
111                homedir_path.join(path)
112            }
113        })
114        .filter(|path| {
115            debug!("considering path: {:?}", &path);
116            path.exists()
117        })
118        .collect()
119}
120
121/// check for bash and zsh shells.
122/// check whether the shell function is installed, install
123/// it if it wasn't refused before or if broot is launched
124/// with --install.
125pub fn install(si: &mut ShellInstall) -> Result<(), ShellInstallError> {
126    let script_path = get_script_path();
127    si.write_script(&script_path, BASH_FUNC)?;
128    let link_path = get_link_path();
129    si.create_link(&link_path, &script_path)?;
130    let sourcing_paths = get_sourcing_paths();
131    if sourcing_paths.is_empty() {
132        warn!("no sourcing path for bash/zsh!");
133        si.skin.print_text(MD_NO_SOURCING);
134        return Ok(());
135    }
136    let escaped_path = link_path.to_string_lossy().replace(' ', "\\ ");
137    let source_line = format!("source {}", &escaped_path);
138    for sourcing_path in &sourcing_paths {
139        let sourcing_path_str = sourcing_path.to_string_lossy();
140        if util::file_contains_line(sourcing_path, &source_line)? {
141            mad_print_inline!(
142                &si.skin,
143                "`$0` already patched, no change made.\n",
144                &sourcing_path_str,
145            );
146        } else {
147            util::append_to_file(sourcing_path, format!("\n{source_line}\n"))?;
148            let is_zsh = sourcing_path_str.contains(".zshrc");
149            if is_zsh {
150                mad_print_inline!(
151                    &si.skin,
152                    "`$0` successfully patched, you can make the function immediately available with `exec zsh`\n",
153                    &sourcing_path_str,
154                );
155            } else {
156                mad_print_inline!(
157                    &si.skin,
158                    "`$0` successfully patched, you can make the function immediately available with `source $0`\n",
159                    &sourcing_path_str,
160                );
161            }
162        }
163    }
164    si.done = true;
165    Ok(())
166}