From d9496689e29d9aaa1d55ec9ca822d36ef895381d Mon Sep 17 00:00:00 2001 From: Christoph Jabs <98587286+chrjabs@users.noreply.github.com> Date: Wed, 3 Apr 2024 21:28:18 +0300 Subject: Zoxide related features (#506) * make `:z` act more like zoxide before checking the zoxide database, check if the query is a directory that can be navigated to as by `:cd` also interpret `~` and `-` correctly * optionally track all movement in zoxide Add new config option `zoxide_update` with default false. When manually enabled, all directory navigation (via manually navigating or `:cd` and `:z`) will update the `zoxide` database. Since navigation via `:z` always updates the database, ensure that it doesn't update it twice. * support arguments for zoxide interactive support for `:zi ` where arguments are used to prefilter the matches provided in the interactive zoxide prompt * allow command aliases with arguments note: only aliases that do not contain spaces can be used with arguments since either the entire command or only the first word are checked against the aliases --- config/joshuto.toml | 1 + docs/configuration/joshuto.toml.md | 3 +++ src/commands/change_directory.rs | 6 +++++- src/commands/command_line.rs | 10 +++++++++- src/commands/zoxide.rs | 33 ++++++++++++++++++++++++++++----- src/config/clean/app/config.rs | 2 ++ src/config/raw/app/config.rs | 2 ++ src/key_command/command.rs | 2 +- src/key_command/impl_appcommand.rs | 2 +- src/key_command/impl_appexecute.rs | 4 +++- src/key_command/impl_comment.rs | 2 +- src/key_command/impl_from_str.rs | 29 +++++++++++++++++++++++++++-- src/util/unix.rs | 10 ++++++++++ 13 files changed, 93 insertions(+), 13 deletions(-) diff --git a/config/joshuto.toml b/config/joshuto.toml index d6a4235..64e6462 100644 --- a/config/joshuto.toml +++ b/config/joshuto.toml @@ -7,6 +7,7 @@ watch_files = true xdg_open = false xdg_open_fork = false case_insensitive_ext = false +zoxide_update = false custom_commands = [] diff --git a/docs/configuration/joshuto.toml.md b/docs/configuration/joshuto.toml.md index d7af43a..5a30863 100644 --- a/docs/configuration/joshuto.toml.md +++ b/docs/configuration/joshuto.toml.md @@ -36,6 +36,9 @@ focus_on_create = true # The maximum file size to show a preview for max_preview_size = 2097152 # 2MB +# Update the zoxide database with every navigation type instead of only with the z command +zoxide_update = false + # Define custom commands (using shell) with parameters like %text, %s etc. custom_commands = [ { name = "rgfzf", command = "/home//.config/joshuto/rgfzf '%text' %s" }, diff --git a/src/commands/change_directory.rs b/src/commands/change_directory.rs index ebf471f..84d8fdb 100644 --- a/src/commands/change_directory.rs +++ b/src/commands/change_directory.rs @@ -1,6 +1,6 @@ use std::path; -use crate::commands::reload; +use crate::commands::{reload, zoxide}; use crate::context::AppContext; use crate::error::AppResult; use crate::history::{generate_entries_to_root, DirectoryHistory}; @@ -10,6 +10,10 @@ use crate::util::cwd; pub fn cd(path: &path::Path, context: &mut AppContext) -> std::io::Result<()> { cwd::set_current_dir(path)?; context.tab_context_mut().curr_tab_mut().set_cwd(path); + if context.config_ref().zoxide_update { + debug_assert!(path.is_absolute()); + zoxide::zoxide_add(path.to_str().expect("cannot convert path to string"))?; + } Ok(()) } diff --git a/src/commands/command_line.rs b/src/commands/command_line.rs index d423835..5f9d0de 100644 --- a/src/commands/command_line.rs +++ b/src/commands/command_line.rs @@ -22,12 +22,20 @@ pub fn read_and_execute( .suffix(suffix) .get_input(backend, context, &mut listener); - if let Some(s) = user_input { + if let Some(mut s) = user_input { let mut trimmed = s.trim_start(); let _ = context.commandline_context_mut().history_mut().add(trimmed); + let (command, arg) = match trimmed.find(' ') { + Some(i) => (&trimmed[..i], &trimmed[i..]), + None => (trimmed, ""), + }; + if let Some(alias) = context.config_ref().cmd_aliases.get(trimmed) { trimmed = alias; + } else if let Some(alias) = context.config_ref().cmd_aliases.get(command) { + s.replace_range(..s.len() - arg.len(), alias); + trimmed = &s; } let command = Command::from_str(trimmed)?; diff --git a/src/commands/zoxide.rs b/src/commands/zoxide.rs index 222bb9d..4ebddff 100644 --- a/src/commands/zoxide.rs +++ b/src/commands/zoxide.rs @@ -10,18 +10,34 @@ use crate::ui::AppBackend; pub fn zoxide_query(context: &mut AppContext, args: &str) -> AppResult { let cwd = std::env::current_dir()?; + let path = Path::new(args); + if change_directory::change_directory(context, path).is_ok() { + if !context.config_ref().zoxide_update { + let cwd = context + .tab_context_ref() + .curr_tab_ref() + .cwd() + .to_str() + .expect("path cannot be converted to string"); + zoxide_add(cwd)?; + } + return Ok(()); + } + let zoxide_output = Command::new("zoxide") .arg("query") .arg("--exclude") .arg(&cwd) .arg("--") - .args(args.split(' ').collect::>()) + .args(args.split(' ')) .output()?; if zoxide_output.status.success() { if let Ok(zoxide_str) = std::str::from_utf8(&zoxide_output.stdout) { let zoxide_path = &zoxide_str[..zoxide_str.len() - 1]; - zoxide_add(zoxide_path)?; + if !context.config_ref().zoxide_update { + zoxide_add(zoxide_path)?; + } let path = Path::new(zoxide_path); context @@ -37,13 +53,18 @@ pub fn zoxide_query(context: &mut AppContext, args: &str) -> AppResult { Ok(()) } -pub fn zoxide_query_interactive(context: &mut AppContext, backend: &mut AppBackend) -> AppResult { +pub fn zoxide_query_interactive( + context: &mut AppContext, + backend: &mut AppBackend, + args: &str, +) -> AppResult { backend.terminal_drop(); let zoxide_process = Command::new("zoxide") .arg("query") .arg("-i") .arg("--") + .args(args.split(' ')) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .spawn()?; @@ -54,7 +75,9 @@ pub fn zoxide_query_interactive(context: &mut AppContext, backend: &mut AppBacke if zoxide_output.status.success() { if let Ok(zoxide_str) = std::str::from_utf8(&zoxide_output.stdout) { let zoxide_path = &zoxide_str[..zoxide_str.len() - 1]; - zoxide_add(zoxide_path)?; + if !context.config_ref().zoxide_update { + zoxide_add(zoxide_path)?; + } let path = Path::new(zoxide_path); context @@ -70,7 +93,7 @@ pub fn zoxide_query_interactive(context: &mut AppContext, backend: &mut AppBacke Ok(()) } -fn zoxide_add(s: &str) -> io::Result<()> { +pub fn zoxide_add(s: &str) -> io::Result<()> { Command::new("zoxide").arg("add").arg(s).output()?; Ok(()) } diff --git a/src/config/clean/app/config.rs b/src/config/clean/app/config.rs index 3d0be5a..740430a 100644 --- a/src/config/clean/app/config.rs +++ b/src/config/clean/app/config.rs @@ -24,6 +24,7 @@ pub struct AppConfig { pub focus_on_create: bool, pub mouse_support: bool, pub cmd_aliases: HashMap, + pub zoxide_update: bool, pub _display_options: DisplayOption, pub _preview_options: PreviewOption, pub _search_options: SearchOption, @@ -90,6 +91,7 @@ impl From for AppConfig { cmd_aliases: raw.cmd_aliases, focus_on_create: raw.focus_on_create, mouse_support: raw.mouse_support, + zoxide_update: raw.zoxide_update, _display_options: DisplayOption::from(raw.display_options), _preview_options: PreviewOption::from(raw.preview_options), _search_options: SearchOption::from(raw.search_options), diff --git a/src/config/raw/app/config.rs b/src/config/raw/app/config.rs index 2980e4c..9c18ff8 100644 --- a/src/config/raw/app/config.rs +++ b/src/config/raw/app/config.rs @@ -39,6 +39,8 @@ pub struct AppConfigRaw { #[serde(default = "default_true")] pub mouse_support: bool, #[serde(default)] + pub zoxide_update: bool, + #[serde(default)] pub cmd_aliases: HashMap, #[serde(default, rename = "display")] pub display_options: DisplayOptionRaw, diff --git a/src/key_command/command.rs b/src/key_command/command.rs index 87ab74b..84e407e 100644 --- a/src/key_command/command.rs +++ b/src/key_command/command.rs @@ -191,7 +191,7 @@ pub enum Command { processor: PostProcessor, }, Zoxide(String), - ZoxideInteractive, + ZoxideInteractive(String), CustomSearch(Vec), CustomSearchInteractive(Vec), diff --git a/src/key_command/impl_appcommand.rs b/src/key_command/impl_appcommand.rs index df9ffcf..fe75dc5 100644 --- a/src/key_command/impl_appcommand.rs +++ b/src/key_command/impl_appcommand.rs @@ -111,7 +111,7 @@ impl AppCommand for Command { Self::SubdirFzf => CMD_SUBDIR_FZF, Self::SelectFzf { .. } => CMD_SELECT_FZF, Self::Zoxide(_) => CMD_ZOXIDE, - Self::ZoxideInteractive => CMD_ZOXIDE_INTERACTIVE, + Self::ZoxideInteractive(_) => CMD_ZOXIDE_INTERACTIVE, Self::CustomSearch(_) => CMD_CUSTOM_SEARCH, Self::CustomSearchInteractive(_) => CMD_CUSTOM_SEARCH_INTERACTIVE, diff --git a/src/key_command/impl_appexecute.rs b/src/key_command/impl_appexecute.rs index 5ab59b7..25d7041 100644 --- a/src/key_command/impl_appexecute.rs +++ b/src/key_command/impl_appexecute.rs @@ -167,7 +167,9 @@ impl AppExecute for Command { Self::SubdirFzf => subdir_fzf::subdir_fzf(context, backend), Self::SelectFzf { options } => select_fzf::select_fzf(context, backend, options), Self::Zoxide(arg) => zoxide::zoxide_query(context, arg), - Self::ZoxideInteractive => zoxide::zoxide_query_interactive(context, backend), + Self::ZoxideInteractive(args) => { + zoxide::zoxide_query_interactive(context, backend, args) + } Self::BookmarkAdd => bookmark::add_bookmark(context, backend), Self::BookmarkChangeDirectory => bookmark::change_directory_bookmark(context, backend), diff --git a/src/key_command/impl_comment.rs b/src/key_command/impl_comment.rs index 6fa744d..e22618c 100644 --- a/src/key_command/impl_comment.rs +++ b/src/key_command/impl_comment.rs @@ -140,7 +140,7 @@ impl CommandComment for Command { Self::SubdirFzf => "Switch to a child directory via fzf", Self::SelectFzf { .. } => "Select via fzf", Self::Zoxide(_) => "Zoxide", - Self::ZoxideInteractive => "Zoxide interactive", + Self::ZoxideInteractive(_) => "Zoxide interactive", Self::BookmarkAdd => "Add a bookmark", Self::BookmarkChangeDirectory => "Navigate to a bookmark", diff --git a/src/key_command/impl_from_str.rs b/src/key_command/impl_from_str.rs index 55a7fd7..74bac5d 100644 --- a/src/key_command/impl_from_str.rs +++ b/src/key_command/impl_from_str.rs @@ -113,8 +113,11 @@ impl std::str::FromStr for Command { Self::CustomSearchInteractive(arg.split(' ').map(|x| x.to_string()).collect()) ); simple_command_conversion_case!(command, CMD_SUBDIR_FZF, Self::SubdirFzf); - simple_command_conversion_case!(command, CMD_ZOXIDE, Self::Zoxide(arg.to_string())); - simple_command_conversion_case!(command, CMD_ZOXIDE_INTERACTIVE, Self::ZoxideInteractive); + simple_command_conversion_case!( + command, + CMD_ZOXIDE_INTERACTIVE, + Self::ZoxideInteractive(arg.to_string()) + ); if command == CMD_QUIT { match arg { @@ -602,6 +605,28 @@ impl std::str::FromStr for Command { Ok(Self::FilterString { pattern: arg.to_string(), }) + } else if command == CMD_ZOXIDE { + match arg { + "" => match HOME_DIR.as_ref() { + Some(s) => Ok(Self::ChangeDirectory { path: s.clone() }), + None => Err(AppError::new( + AppErrorKind::EnvVarNotPresent, + format!("{}: Cannot find home directory", command), + )), + }, + ".." => Ok(Self::ParentDirectory), + "-" => Ok(Self::PreviousDirectory), + arg => { + let (head, tail) = match arg.find(' ') { + Some(i) => (&arg[..i], &arg[i..]), + None => (arg, ""), + }; + let head = unix::expand_shell_string_cow(head); + let mut args = String::from(head); + args.push_str(tail); + Ok(Self::Zoxide(args)) + } + } } else { Err(AppError::new( AppErrorKind::UnrecognizedCommand, diff --git a/src/util/unix.rs b/src/util/unix.rs index 61d41fb..2d71359 100644 --- a/src/util/unix.rs +++ b/src/util/unix.rs @@ -56,6 +56,16 @@ pub fn mode_to_string(mode: u32) -> String { mode_str } +pub fn expand_shell_string_cow(s: &str) -> std::borrow::Cow<'_, str> { + let dir = dirs_next::home_dir(); + let os_str = dir.map(|s| s.as_os_str().to_owned()); + let context_func = || { + let cow_str = os_str.as_ref().map(|s| s.to_string_lossy()); + cow_str + }; + shellexpand::tilde_with_context(s, context_func) +} + pub fn expand_shell_string(s: &str) -> path::PathBuf { let dir = dirs_next::home_dir(); let os_str = dir.map(|s| s.as_os_str().to_owned()); -- cgit v1.2.3