diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/cli/mod.rs | 9 | ||||
-rw-r--r-- | src/errors.rs | 61 | ||||
-rw-r--r-- | src/shell_install/bash.rs | 14 | ||||
-rw-r--r-- | src/shell_install/fish.rs | 4 | ||||
-rw-r--r-- | src/shell_install/mod.rs | 76 | ||||
-rw-r--r-- | src/shell_install/nushell.rs | 123 | ||||
-rw-r--r-- | src/shell_install/util.rs | 32 |
7 files changed, 261 insertions, 58 deletions
diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 6b56c36..7456337 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -72,7 +72,12 @@ pub fn run() -> Result<Option<Launchable>, ProgramError> { // configuration if specific_conf.is_none() && install_args.install != Some(false) { let mut shell_install = ShellInstall::new(install_args.install == Some(true)); - shell_install.check()?; + // TODO clean the next few lines when inspect_err is stable + let res = shell_install.check(); + if let Err(e) = &res { + shell_install.comment_error(e); + } + res?; if shell_install.should_quit { return Ok(None); } @@ -137,7 +142,7 @@ pub fn run() -> Result<Option<Launchable>, ProgramError> { } /// wait for user input, return `true` if they didn't answer 'n' -pub fn ask_authorization() -> Result<bool, ProgramError> { +pub fn ask_authorization() -> io::Result<bool> { let mut answer = String::new(); io::stdin().read_line(&mut answer)?; let answer = answer.trim(); diff --git a/src/errors.rs b/src/errors.rs index e1571bb..5ae94d5 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -8,28 +8,59 @@ use { }; custom_error! {pub ProgramError - Io {source: io::Error} = "IO Error : {source}", - Termimad {source: termimad::Error} = "Termimad Error : {source}", + AmbiguousVerbName {name: String} = "Ambiguous name: More than one verb matches {name:?}", + ArgParse {bad: String, valid: String} = "{bad:?} can't be parsed (valid values: {valid:?})", Conf {source: ConfError} = "Bad configuration: {source}", ConfFile {path:String, details: ConfError} = "Bad configuration file {path:?} : {details}", - ArgParse {bad: String, valid: String} = "{bad:?} can't be parsed (valid values: {valid:?})", - UnknownVerb {name: String} = "No verb matches {name:?}", - AmbiguousVerbName {name: String} = "Ambiguous name: More than one verb matches {name:?}", - UnmatchingVerbArgs {name: String} = "No matching argument found for verb {name:?}", - TreeBuild {source: TreeBuildError} = "{source}", - LaunchError {program: String, source: io::Error} = "Unable to launch {program}: {source}", - UnknowShell {shell: String} = "Unknown shell: {shell}", + ImageError {source: ImageError } = "{source}", InternalError {details: String} = "Internal error: {details}", // should not happen InvalidGlobError {pattern: String} = "Invalid glob: {pattern}", - Unrecognized {token: String} = "Unrecognized: {token}", - NetError {source: NetError} = "{source}", - ImageError {source: ImageError } = "{source}", + Io {source: io::Error} = "IO Error : {source}", + LaunchError {program: String, source: io::Error} = "Unable to launch {program}: {source}", Lfs {details: String} = "Failed to fetch mounts: {details}", - ZeroLenFile = "File seems empty", + NetError {source: NetError} = "{source}", + OpenError { source: opener::OpenError } = "Open error: {source}", + ShelInstall { source: ShellInstallError } = "{source}", + SyntectCrashed { details: String } = "Syntect crashed on {details:?}", + Termimad {source: termimad::Error} = "Termimad Error : {source}", + TreeBuild {source: TreeBuildError} = "{source}", + UnknowShell {shell: String} = "Unknown shell: {shell}", + UnknownVerb {name: String} = "No verb matches {name:?}", UnmappableFile = "File can't be mapped", + UnmatchingVerbArgs {name: String} = "No matching argument found for verb {name:?}", UnprintableFile = "File can't be printed", // has characters that can't be printed without escaping - SyntectCrashed { details: String } = "Syntect crashed on {details:?}", - OpenError { source: opener::OpenError } = "Open error: {source}", + Unrecognized {token: String} = "Unrecognized: {token}", + ZeroLenFile = "File seems empty", +} + +custom_error!{pub ShellInstallError + Io {source: io::Error, when: String} = "IO Error {source} on {when}", +} +impl ShellInstallError { + pub fn is_permission_denied(&self) -> bool { + match self { + Self::Io { source, .. } => { + if source.kind() == io::ErrorKind::PermissionDenied { + true + } else if cfg!(windows) && source.raw_os_error().unwrap_or(0) == 1314 { + // https://learn.microsoft.com/en-us/windows/win32/debug/system-error-codes--1300-1699- + true + } else { + false + } + } + } + } +} +pub trait IoToShellInstallError<Ok> { + fn context(self, f: &dyn Fn() -> String) -> Result<Ok, ShellInstallError>; +} +impl<Ok> IoToShellInstallError<Ok> for Result<Ok, io::Error> { + fn context(self, f: &dyn Fn() -> String) -> Result<Ok, ShellInstallError> { + self.map_err(|source| ShellInstallError::Io { + source, when: f() + }) + } } custom_error! {pub TreeBuildError diff --git a/src/shell_install/bash.rs b/src/shell_install/bash.rs index 1ebae33..e730e03 100644 --- a/src/shell_install/bash.rs +++ b/src/shell_install/bash.rs @@ -13,12 +13,12 @@ use { super::{util, ShellInstall}, crate::{ conf, - errors::ProgramError, + errors::*, }, directories::UserDirs, lazy_regex::regex, regex::Captures, - std::{env, fs::OpenOptions, io::Write, path::PathBuf}, + std::{env, path::PathBuf}, termimad::{ mad_print_inline, }, @@ -117,7 +117,7 @@ fn get_sourcing_paths() -> Vec<PathBuf> { /// check whether the shell function is installed, install /// it if it wasn't refused before or if broot is launched /// with --install. -pub fn install(si: &mut ShellInstall) -> Result<(), ProgramError> { +pub fn install(si: &mut ShellInstall) -> Result<(), ShellInstallError> { let script_path = get_script_path(); si.write_script(&script_path, BASH_FUNC)?; let link_path = get_link_path(); @@ -139,13 +139,7 @@ pub fn install(si: &mut ShellInstall) -> Result<(), ProgramError> { &sourcing_path_str, ); } else { - let mut shellrc = OpenOptions::new() - .write(true) - .append(true) - .open(sourcing_path)?; - shellrc.write_all(b"\n")?; - shellrc.write_all(source_line.as_bytes())?; - shellrc.write_all(b"\n")?; + util::append_to_file(sourcing_path, format!("\n{source_line}\n"))?; let is_zsh = sourcing_path_str.contains(".zshrc"); if is_zsh { mad_print_inline!( diff --git a/src/shell_install/fish.rs b/src/shell_install/fish.rs index e56343e..720db20 100644 --- a/src/shell_install/fish.rs +++ b/src/shell_install/fish.rs @@ -15,7 +15,7 @@ use { super::ShellInstall, - crate::{conf, errors::ProgramError}, + crate::{conf, errors::*}, directories::BaseDirs, directories::ProjectDirs, std::path::PathBuf, @@ -94,7 +94,7 @@ fn get_script_path() -> PathBuf { /// /// As fish isn't frequently used, we first check that it seems /// to be installed. If not, we just do nothing. -pub fn install(si: &mut ShellInstall) -> Result<(), ProgramError> { +pub fn install(si: &mut ShellInstall) -> Result<(), ShellInstallError> { let fish_dir = get_fish_dir(); if !fish_dir.exists() { debug!("no fish config directory. Assuming fish isn't used."); diff --git a/src/shell_install/mod.rs b/src/shell_install/mod.rs index f3de0ac..356c714 100644 --- a/src/shell_install/mod.rs +++ b/src/shell_install/mod.rs @@ -2,11 +2,12 @@ use { crate::{ cli::{self, ShellInstallState}, conf, - errors::ProgramError, + errors::*, skin, }, std::{ - fs, io, os, + fs, + os, path::{Path, PathBuf}, }, termimad::{mad_print_inline, MadSkin}, @@ -14,6 +15,7 @@ use { mod bash; mod fish; +mod nushell; mod util; const MD_INSTALL_REQUEST: &str = r#" @@ -34,6 +36,13 @@ If you want the `br` shell function, you may either "#; +const MD_PERMISSION_DENIED: &str = r#" +Installation check resulted in **Permission Denied**. +Please relaunch with elevated privilege. +This is typically only needed once. +Error details: +"#; + const MD_INSTALL_DONE: &str = r#" The **br** function has been successfully installed. You may have to restart your shell or source your shell init files. @@ -68,23 +77,29 @@ pub struct ShellInstall { /// or test scripts when we don't want the user to be prompted /// to install the function, or in case something doesn't properly /// work in shell detections -pub fn write_state(state: ShellInstallState) -> Result<(), ProgramError> { +pub fn write_state(state: ShellInstallState) -> Result<(), ShellInstallError> { let refused_path = get_refused_path(); let installed_path = get_installed_path(); if installed_path.exists() { - fs::remove_file(&installed_path)?; + fs::remove_file(&installed_path) + .context(&|| format!("removing {:?}", &installed_path))?; } if refused_path.exists() { - fs::remove_file(&refused_path)?; + fs::remove_file(&refused_path) + .context(&|| format!("removing {:?}", &refused_path))?; } match state { ShellInstallState::Refused => { - fs::create_dir_all(refused_path.parent().unwrap())?; - fs::write(&refused_path, REFUSED_FILE_CONTENT)?; + fs::create_dir_all(refused_path.parent().unwrap()) + .context(&|| format!("creating parents of {refused_path:?}"))?; + fs::write(&refused_path, REFUSED_FILE_CONTENT) + .context(&|| format!("writing in {refused_path:?}"))?; } ShellInstallState::Installed => { - fs::create_dir_all(installed_path.parent().unwrap())?; - fs::write(&installed_path, INSTALLED_FILE_CONTENT)?; + fs::create_dir_all(installed_path.parent().unwrap()) + .context(&|| format!("creating parents of {installed_path:?}"))?; + fs::write(&installed_path, INSTALLED_FILE_CONTENT) + .context(&|| format!("writing in {installed_path:?}"))?; } _ => {} } @@ -116,6 +131,7 @@ impl ShellInstall { match shell { "bash" | "zsh" => println!("{}", bash::get_script()), "fish" => println!("{}", fish::get_script()), + "nushell" => println!("{}", nushell::get_script()), _ => { return Err(ProgramError::UnknowShell { shell: shell.to_string(), @@ -128,7 +144,7 @@ impl ShellInstall { /// check whether the shell function is installed, install /// it if it wasn't refused before or if broot is launched /// with --install. - pub fn check(&mut self) -> Result<(), ProgramError> { + pub fn check(&mut self) -> Result<(), ShellInstallError> { let installed_path = get_installed_path(); if self.force_install { self.skin.print_text("You requested a clean (re)install."); @@ -152,6 +168,7 @@ impl ShellInstall { debug!("Starting install"); bash::install(self)?; fish::install(self)?; + nushell::install(self)?; self.should_quit = true; if self.done { self.skin.print_text(MD_INSTALL_DONE); @@ -159,19 +176,28 @@ impl ShellInstall { Ok(()) } - pub fn remove(&self, path: &Path) -> io::Result<()> { + /// print some additional information on the error (typically before + /// the error itself is dumped) + pub fn comment_error(&self, err: &ShellInstallError) { + if err.is_permission_denied() { + self.skin.print_text(MD_PERMISSION_DENIED); + } + } + + pub fn remove(&self, path: &Path) -> Result<(), ShellInstallError> { // path.exists() doesn't work when the file is a link (it checks whether // the link destination exists instead of checking the link exists // so we first check whether the link exists if fs::read_link(path).is_ok() || path.exists() { mad_print_inline!(self.skin, "Removing `$0`.\n", path.to_string_lossy()); - fs::remove_file(path)?; + fs::remove_file(path) + .context(&|| format!("removing {path:?}"))?; } Ok(()) } /// check whether we're allowed to install. - fn can_do(&mut self) -> Result<bool, ProgramError> { + fn can_do(&mut self) -> Result<bool, ShellInstallError> { if let Some(authorization) = self.authorization { return Ok(authorization); } @@ -181,7 +207,8 @@ impl ShellInstall { return Ok(false); } self.skin.print_text(MD_INSTALL_REQUEST); - let proceed = cli::ask_authorization()?; + let proceed = cli::ask_authorization() + .context(&|| "asking user".to_string())?; // read_line failure debug!("proceed: {:?}", proceed); self.authorization = Some(proceed); if !proceed { @@ -192,7 +219,7 @@ impl ShellInstall { } /// write the script at the given path - fn write_script(&self, script_path: &Path, content: &str) -> Result<(), ProgramError> { + fn write_script(&self, script_path: &Path, content: &str) -> Result<(), ShellInstallError> { self.remove(script_path)?; info!("Writing `br` shell function in `{:?}`", &script_path); mad_print_inline!( @@ -200,13 +227,16 @@ impl ShellInstall { "Writing *br* shell function in `$0`.\n", script_path.to_string_lossy(), ); - fs::create_dir_all(script_path.parent().unwrap())?; - fs::write(script_path, content)?; + fs::create_dir_all(script_path.parent().unwrap()) + .context(&|| format!("creating parent dirs to {script_path:?}"))?; + fs::write(script_path, content) + .context(&|| format!("writing script in {script_path:?}"))?; Ok(()) } + /// create a link - fn create_link(&self, link_path: &Path, script_path: &Path) -> Result<(), ProgramError> { + fn create_link(&self, link_path: &Path, script_path: &Path) -> Result<(), ShellInstallError> { info!("Creating link from {:?} to {:?}", &link_path, &script_path); self.remove(link_path)?; let link_path_str = link_path.to_string_lossy(); @@ -217,11 +247,15 @@ impl ShellInstall { &link_path_str, &script_path_str, ); - fs::create_dir_all(link_path.parent().unwrap())?; + let parent = link_path.parent().unwrap(); + fs::create_dir_all(parent) + .context(&|| format!("creating directory {parent:?}"))?; #[cfg(unix)] - os::unix::fs::symlink(script_path, link_path)?; + os::unix::fs::symlink(script_path, link_path) + .context(&|| format!("linking from {link_path:?} to {script_path:?}"))?; #[cfg(windows)] - os::windows::fs::symlink_file(&script_path, &link_path)?; + os::windows::fs::symlink_file(&script_path, &link_path) + .context(&|| format!("linking from {link_path:?} to {script_path:?}"))?; Ok(()) } } diff --git a/src/shell_install/nushell.rs b/src/shell_install/nushell.rs new file mode 100644 index 0000000..228a28f --- /dev/null +++ b/src/shell_install/nushell.rs @@ -0,0 +1,123 @@ +//! The goal of this mod is to ensure the launcher shell function +//! is available for nushell 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. +//! +//! In a correct installation, we have: +//! - a function declaration script in ~/.local/share/broot/launcher/nushell/br/1 +//! - a link to that script in ~/.config/broot/launcher/nushell/br/1 +//! - a line to source the link in ~/.config/nushell/config.nu +//! (exact paths depend on XDG variables) +//! +//! More info at +//! https://github.com/Canop/broot/issues/375 + +use { + super::{util, ShellInstall}, + crate::{ + conf, + errors::*, + }, + directories::BaseDirs, + std::path::PathBuf, + termimad::{ + mad_print_inline, + }, +}; + +const NAME: &str = "nushell"; +const VERSION: &str = "1"; + +const NU_FUNC: &str = r#" +# This script was automatically generated by the broot program +# More information can be found in https://github.com/Canop/broot +# 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. +def _br_cmd [] { + let cmd_file = ([ $nu.temp-path, $"broot-(random chars).tmp" ] | path join) + touch $cmd_file + ^broot --outcmd $cmd_file + let target_dir = (open $cmd_file | to text | str replace "^cd\\s+" "" | str trim) + rm -p -f $cmd_file + + $target_dir +} +alias br = cd (_br_cmd) +"#; + +pub fn get_script() -> &'static str { + NU_FUNC +} + +/// return the path to the link to the function script +fn get_link_path() -> PathBuf { + conf::dir().join("launcher").join(NAME).join("br") +} + +/// return the root of +fn get_nushell_dir() -> Option<PathBuf> { + BaseDirs::new() + .map(|base_dirs| base_dirs.config_dir().join("nushell")) + .filter(|dir| dir.exists()) +} + +/// return the path to the script containing the function. +/// +/// In XDG_DATA_HOME (typically ~/.local/share on linux) +fn get_script_path() -> PathBuf { + conf::app_dirs() + .data_dir() + .join("launcher") + .join(NAME) + .join(VERSION) +} + +/// Check for nushell. +/// +/// Check whether the shell function is installed, install +/// it if it wasn't refused before or if broot is launched +/// with --install. +pub fn install(si: &mut ShellInstall) -> Result<(), ShellInstallError> { + debug!("install {NAME}"); + let Some(nushell_dir) = get_nushell_dir() else { + debug!("no nushell config directory. Assuming nushell isn't used."); + return Ok(()); + }; + info!("nushell seems to be installed"); + let script_path = get_script_path(); + si.write_script(&script_path, NU_FUNC)?; + let link_path = get_link_path(); + si.create_link(&link_path, &script_path)?; + + let escaped_path = link_path.to_string_lossy().replace(' ', "\\ "); + let source_line = format!("source {}", &escaped_path); + + let sourcing_path = nushell_dir.join("config.nu"); + if !sourcing_path.exists() { + warn!("Unexpected lack of config.nu file"); + return Ok(()); + } + if sourcing_path.is_dir() { + warn!("config.nu file"); + return Ok(()); + } + let sourcing_path_str = sourcing_path.to_string_lossy(); + if util::file_contains_line(&sourcing_path, &source_line)? { + mad_print_inline!( + &si.skin, + "`$0` already patched, no change made.\n", + &sourcing_path_str, + ); + } else { + util::append_to_file(&sourcing_path, format!("\n{source_line}\n"))?; + mad_print_inline!( + &si.skin, + "`$0` successfully patched, you can make the function immediately available with `source $0`\n", + &sourcing_path_str, + ); + } + si.done = true; + Ok(()) +} diff --git a/src/shell_install/util.rs b/src/shell_install/util.rs index 13ad5d0..34d11d1 100644 --- a/src/shell_install/util.rs +++ b/src/shell_install/util.rs @@ -1,14 +1,30 @@ -use std::{ - fs, - io::{self, BufRead, BufReader}, - path::Path, +use { + crate::errors::*, + std::{ + fs::{self, OpenOptions}, + io::{BufRead, BufReader, Write}, + path::Path, + }, }; - -pub 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 { +pub fn file_contains_line(path: &Path, searched_line: &str) -> Result<bool, ShellInstallError> { + let file = fs::File::open(path) + .context(&|| format!("opening {path:?}"))?; + for line in BufReader::new(file).lines() { + let line = line.context(&|| format!("reading line in {path:?}"))?; + if line == searched_line { return Ok(true); } } Ok(false) } + +pub fn append_to_file<S: AsRef<str>>(path: &Path, content: S) -> Result<(), ShellInstallError> { + let mut shellrc = OpenOptions::new() + .write(true) + .append(true) + .open(path) + .context(&|| format!("opening {path:?} for append"))?; + shellrc.write_all(content.as_ref().as_bytes()) + .context(&|| format!("writing in {path:?}"))?; + Ok(()) +} |