From fe01c907bf7523fc117dbf7bdad69a363602bb49 Mon Sep 17 00:00:00 2001 From: Canop Date: Thu, 21 Feb 2019 20:54:02 +0100 Subject: [PATCH 1/2] br installer for fish Fix #31 --- src/shell_bash.rs | 40 ++++++++ src/shell_fish.rs | 29 ++++++ src/shell_install.rs | 224 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 293 insertions(+) create mode 100644 src/shell_bash.rs create mode 100644 src/shell_fish.rs create mode 100644 src/shell_install.rs diff --git a/src/shell_bash.rs b/src/shell_bash.rs new file mode 100644 index 00000000..6362549c --- /dev/null +++ b/src/shell_bash.rs @@ -0,0 +1,40 @@ +use crate::shell_install::ShellFamily; + +pub const BASH: ShellFamily<'static> = ShellFamily { + name: "bash", + sourcing_files: &[".bashrc", ".zshrc"], + version: 1, + script: BASH_FUNC +}; + +// 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" +} +"#; diff --git a/src/shell_fish.rs b/src/shell_fish.rs new file mode 100644 index 00000000..e0febc66 --- /dev/null +++ b/src/shell_fish.rs @@ -0,0 +1,29 @@ +use crate::shell_install::ShellFamily; + +pub const FISH: ShellFamily<'static> = ShellFamily { + name: "fish", + sourcing_files: &[".config/fish/config.fish"], // idealy we should probably use XDG here... + version: 1, + script: FISH_FUNC +}; + +const FISH_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 + set f (mktemp) + broot --outcmd $f $argv + if test $status -ne 0 + rm -f "$f" + return "$code" + end + set d (cat "$f") + rm -f "$f" + eval "$d" +end +"#; diff --git a/src/shell_install.rs b/src/shell_install.rs new file mode 100644 index 00000000..7a55a2bf --- /dev/null +++ b/src/shell_install.rs @@ -0,0 +1,224 @@ +//! 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; +use std::fs::OpenOptions; +use std::io::{self, BufRead, BufReader, Write}; +use std::os::unix::fs::symlink; +use std::path::{Path, PathBuf}; + +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: &'static[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 { + 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::>() + .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 { + 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 { + 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 = should_quit | done; + } + Ok(should_quit) +} From 7dc198810127b93893a403940fca963e7abbb2bc Mon Sep 17 00:00:00 2001 From: Canop Date: Fri, 22 Feb 2019 16:33:30 +0100 Subject: [PATCH 2/2] minor code cleaning --- src/cli.rs | 2 +- src/main.rs | 6 +- src/shell_func.rs | 234 ------------------------------------------- src/shell_install.rs | 9 +- 4 files changed, 9 insertions(+), 242 deletions(-) delete mode 100644 src/shell_func.rs diff --git a/src/cli.rs b/src/cli.rs index c0155ab5..32c99013 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -62,7 +62,7 @@ fn get_cli_args<'a>() -> clap::ArgMatches<'a> { .arg( clap::Arg::with_name("install") .long("install") - .help("install the br shell function"), + .help("install or reinstall the br shell function"), ) .arg( clap::Arg::with_name("only-folders") diff --git a/src/main.rs b/src/main.rs index 75ab4e34..7cb2cdc3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,7 +26,9 @@ mod patterns; mod regex_patterns; mod screen_text; mod screens; -mod shell_func; +mod shell_bash; +mod shell_fish; +mod shell_install; mod skin; mod skin_conf; mod spinner; @@ -81,7 +83,7 @@ fn configure_log() { fn run() -> Result, ProgramError> { configure_log(); let launch_args = cli::read_lauch_args()?; - let should_quit = shell_func::init(&launch_args)?; + let should_quit = shell_install::init(&launch_args)?; if should_quit { return Ok(None); } diff --git a/src/shell_func.rs b/src/shell_func.rs deleted file mode 100644 index 1fd6647f..00000000 --- a/src/shell_func.rs +++ /dev/null @@ -1,234 +0,0 @@ -//! 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 { - 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 { - 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::>() - .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 { - let launcher_dir = conf::dir().join("launcher"); - ensure_bash_script_installed(&launcher_dir)?; - maybe_patch_all_rcfiles(&launcher_dir, launch_args.install) -} diff --git a/src/shell_install.rs b/src/shell_install.rs index 7a55a2bf..dd7ad3df 100644 --- a/src/shell_install.rs +++ b/src/shell_install.rs @@ -21,11 +21,10 @@ //! place of the "installed" one. //! -use std::fs; -use std::fs::OpenOptions; +use std::fs::{self, OpenOptions}; use std::io::{self, BufRead, BufReader, Write}; use std::os::unix::fs::symlink; -use std::path::{Path, PathBuf}; +use std::path::Path; use directories::UserDirs; use termion::style; @@ -35,7 +34,7 @@ use crate::conf; use crate::shell_bash::BASH; use crate::shell_fish::FISH; -const SHELL_FAMILIES: &'static[ShellFamily<'static>] = &[ BASH, FISH ]; +const SHELL_FAMILIES: &[ShellFamily<'static>] = &[ BASH, FISH ]; pub struct ShellFamily<'a> { pub name: &'a str, @@ -218,7 +217,7 @@ pub fn init(launch_args: &AppLaunchArgs) -> io::Result { 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 = should_quit | done; + should_quit |= done; } Ok(should_quit) }