summaryrefslogtreecommitdiffstats
path: root/src/shell_func.rs
blob: 0b18e68e7b515ed132d08a8e22542b929e1c8eb1 (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
224
225
226
227
228
229
230
231
232
233
//! 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.
//!
//! if we start to handle several shell families, we'll make the appropriate
//! structure and versions will be per shell function. Right now it would be
//! useless as we only know how to handle bash and zsh.

use std::fs;
use std::fs::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;

const BASH_FUNC_VERSION: usize = 1;

// This script has been tested on bash and zsh.
// It's installed under the bash name (~/.config/broot
// but linked from both the .bashrc and the .zshrc files
const BASH_FUNC: &str = r#"
# This script was automatically generated by the broot function
# More information can be found in https://github.com/Canop/broot/blob/master/documentation.md

# This function starts broot and executes the command
# it produces, if any.
# It's needed because some shell commands, like `cd`,
# have no useful effect if executed in a subshell.
function br {
    f=$(mktemp)
    (
	set +e
	broot --outcmd "$f" "$@"
	code=$?
	if [ "$code" != 0 ]; then
	    rm -f "$f"
	    exit "$code"
	fi
    )
    code=$?
    if [ "$code" != 0 ]; then
	return "$code"
    fi
    d=$(cat "$f")
    rm -f "$f"
    eval "$d"
}
"#;

const BASH_COMPATIBLE_RC_FILES: &[&str] = &[".bashrc", ".zshrc"];

// make sure the bash 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_bash_script_installed(launcher_dir: &Path) -> io::Result<()> {
    let shellfamily = "bash";
    let dir = launcher_dir.join(&shellfamily);
    let link_path = dir.join("br");
    let link_present = link_path.exists();
    let script_path = dir.join(BASH_FUNC_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, BASH_FUNC)?;
        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(())
}

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)
}

/// return true if the application should quit
fn maybe_patch_all_rcfiles(
    launcher_dir: &Path,
    installation_required: 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<_> = BASH_COMPATIBLE_RC_FILES
        .iter()
        .map(|name| (name, homedir_path.join(name)))
        .filter(|t| t.1.exists())
        .collect();
    if rc_files.is_empty() {
        warn!("no bash compatible rc file found, no installation possible");
        if installation_required {
            println!( "no rcfile found, we can't install the br function");
        }
        return Ok(installation_required);
    }
    if !installation_required {
        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 to {:?} ? [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("bash").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)
}

/// 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");
    ensure_bash_script_installed(&launcher_dir)?;
    maybe_patch_all_rcfiles(&launcher_dir, launch_args.install)
}