summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/cli/mod.rs9
-rw-r--r--src/errors.rs61
-rw-r--r--src/shell_install/bash.rs14
-rw-r--r--src/shell_install/fish.rs4
-rw-r--r--src/shell_install/mod.rs76
-rw-r--r--src/shell_install/nushell.rs123
-rw-r--r--src/shell_install/util.rs32
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(())
+}