summaryrefslogtreecommitdiffstats
path: root/src/shell_install.rs
blob: dd7ad3df6c733db9fd091d8c706b347acd9a2eb9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
//! The goal of this mod is to ensure the launcher shell function
//! is available i.e. the `br` shell function can be used to launch
//! broot (and thus make it possible to execute some commands, like `cd`,
//! from the starting shell.
//!
//! When everybody's OK, the resulting config dir looks like this:
//!
//!    ~/.config/broot
//!    ├──conf.toml
//!    └──launcher
//!       ├──bash
//!       │  ├──1
//!       │  └──br -> /home/dys/.config/broot/launcher/bash/1
//!       └──installed
//!
//! and a "source .config/broot/launcher/bash/br" line is written in
//! the .bashrc file (and the .zshrc file if found)
//!
//!
//! If the user refused the installation, a "refused" file takes the
//! place of the "installed" one.
//!

use std::fs::{self, OpenOptions};
use std::io::{self, BufRead, BufReader, Write};
use std::os::unix::fs::symlink;
use std::path::Path;

use directories::UserDirs;
use termion::style;

use crate::cli::{self, AppLaunchArgs};
use crate::conf;
use crate::shell_bash::BASH;
use crate::shell_fish::FISH;

const SHELL_FAMILIES: &[ShellFamily<'static>] = &[ BASH, FISH ];

pub struct ShellFamily<'a> {
    pub name: &'a str,
    pub sourcing_files: &'a[&'a str],
    pub version: usize,
    pub script: &'a str,
}

impl ShellFamily<'static> {

    // make sure the script and symlink are installed
    // but don't touch the shellrc files
    // (i.e. this isn't enough to make the function available)
    fn ensure_script_installed(&self, launcher_dir: &Path) -> io::Result<()> {
        let dir = launcher_dir.join(self.name);
        let link_path = dir.join("br");
        let link_present = link_path.exists();
        let script_path = dir.join(self.version.to_string());
        let func_present = script_path.exists();
        if !func_present {
            info!("script_path not present: writing it");
            fs::create_dir_all(dir)?;
            fs::write(&script_path, self.script)?;
            if link_present {
                fs::remove_file(&link_path)?;
            }
        }
        if !func_present || !link_present {
            info!("creating link from {:?} to {:?}", &link_path, &script_path);
            symlink(&script_path, &link_path)?;
        }
        Ok(())
    }

    /// return true if the application should quit
    fn maybe_patch_all_sourcing_files(
        &self,
        launcher_dir: &Path,
        installation_required: bool,
        motivation_already_explained: bool,
    ) -> io::Result<bool> {
        let installed_path = launcher_dir.join("installed");
        if installed_path.exists() {
            debug!("*installed* file found");
            // everything seems OK
            // Note that if a new shell has been installed, we don't
            // look again at all the .shellrc files, by design.
            // This means the user having installed a new shell after
            // broot should run `broot --install`
            if !installation_required {
                return Ok(false);
            }
        }
        let refused_path = launcher_dir.join("refused");
        if refused_path.exists() {
            debug!("*refused* file found :(");
            if installation_required {
                fs::remove_file(&refused_path)?;
            } else {
                // user doesn't seem to want the shell function
                return Ok(false);
            }
        }
        // it looks like the shell function is neither installed nor refused
        let homedir_path = match UserDirs::new() {
            Some(user_dirs) => user_dirs.home_dir().to_path_buf(),
            None => {
                warn!("no home directory found!");
                return Ok(false);
            }
        };
        let rc_files: Vec<_> = self.sourcing_files
            .iter()
            .map(|name| (name, homedir_path.join(name)))
            .filter(|t| t.1.exists())
            .collect();
        if rc_files.is_empty() {
            warn!("no {} compatible shell config file found, no installation possible", self.name);
            if installation_required {
                println!("No shell config found, we can't install the br function.");
                println!("We were looking for the following file(s):");
                for name in self.sourcing_files {
                    println!(" - {:?}", homedir_path.join(name));
                }
            }
            return Ok(installation_required);
        }
        if !installation_required {
            if !motivation_already_explained {
                println!(
                    "{}Broot{} should be launched using a shell function",
                    style::Bold,
                    style::Reset
                );
                println!("(see https://github.com/Canop/broot for explanations).");
                println!("The function is either missing, old or badly installed.");
            }
            let proceed = cli::ask_authorization(&format!(
                "Can I add a line in {:?} ? [Y n]",
                rc_files
                    .iter()
                    .map(|f| *f.0)
                    .collect::<Vec<&str>>()
                    .join(" and "),
            ))?;
            debug!("proceed: {:?}", proceed);
            if !proceed {
                // user doesn't want the shell function, let's remember it
                fs::write(
                    &refused_path,
                    "to install the br function, run broot --install\n",
                )?;
                println!("Okey. If you change your mind, use ̀ broot --install`.");
                return Ok(false);
            }
        }

        let br_path = launcher_dir.join(self.name).join("br");
        let source_line = format!("source {}", br_path.to_string_lossy());
        let mut changes_made = false;
        for rc_file in rc_files {
            if file_contains_line(&rc_file.1, &source_line)? {
                println!("{} already patched, no change made.", rc_file.0);
            } else {
                let mut shellrc = OpenOptions::new()
                    .write(true)
                    .append(true)
                    .open(&rc_file.1)?;
                shellrc.write_all(b"\n")?;
                shellrc.write_all(source_line.as_bytes())?;
                println!(
                    "{} successfully patched, you should now refresh it with",
                    rc_file.0
                );
                println!("  source {}", rc_file.1.to_string_lossy());
                changes_made = true;
            }
            // signal if there's an old br function declared in the shellrc file
            // (which was the normal way to install before broot 0.6)
            if file_contains_line(&rc_file.1, "function br {")? {
                println!(
                    "Your {} contains another br function, maybe dating from an old version of broot.",
                    rc_file.0
                );
                println!("You should remove it.");
            }
        }
        if changes_made {
            println!(
                "You should afterwards start broot with just {}br{}.",
                style::Bold,
                style::Reset
            );
        }
        // and remember we did it
        fs::write(
            &installed_path,
            "to reinstall the br function, run broot --install\n",
        )?;
        Ok(changes_made)
    }
}

fn file_contains_line(path: &Path, searched_line: &str) -> io::Result<bool> {
    for line in BufReader::new(fs::File::open(path)?).lines() {
        if line? == searched_line {
            return Ok(true);
        }
    }
    Ok(false)
}

/// check whether the shell function is installed, install
/// it if it wasn't refused before or if broot is launched
/// with --install.
/// returns true if the app should quit
pub fn init(launch_args: &AppLaunchArgs) -> io::Result<bool> {
    let launcher_dir = conf::dir().join("launcher");
    let mut should_quit = false;
    for family in SHELL_FAMILIES {
        family.ensure_script_installed(&launcher_dir)?;
        let done = family.maybe_patch_all_sourcing_files(&launcher_dir, launch_args.install, should_quit)?;
        should_quit |= done;
    }
    Ok(should_quit)
}