diff options
author | Tomasz Durda <tomekdur@wp.pl> | 2023-11-04 16:30:23 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-11-04 11:30:23 -0400 |
commit | 960decfcb89de5601f483d559096afe3481ae10c (patch) | |
tree | 83b708cf0897aee98144274cc8ab6ae1c3c4674a | |
parent | ee50d175d8c14cc7420e922864e6c5289c2785f3 (diff) |
custom_commands + fallback for the older configurations (#446)
* custom_commands + fallback for the older configurations
1. Added custom_commands
2. Implemented custom_search
3. Implemented custom_serach_interactive
4. Added fallback for the command in the keymaps
* Docs + missing file
* Added two more joshuto scripts
---------
Co-authored-by: Tomasz Durda <edotdurda@e-science.pl>
Co-authored-by: Jeff Zhao <jeff.no.zhao@gmail.com>
22 files changed, 292 insertions, 29 deletions
diff --git a/config/joshuto.toml b/config/joshuto.toml index 0543241..b7d4626 100644 --- a/config/joshuto.toml +++ b/config/joshuto.toml @@ -6,6 +6,7 @@ watch_files = true xdg_open = false xdg_open_fork = false +custom_commands = [] [display] # default, hsplit diff --git a/docs/configuration/custom_commands/git_ignored b/docs/configuration/custom_commands/git_ignored new file mode 100755 index 0000000..fa35764 --- /dev/null +++ b/docs/configuration/custom_commands/git_ignored @@ -0,0 +1,21 @@ +#!/bin/bash + +CURRENT_PATH="$PWD" +GIT_PATH="$(git rev-parse --show-toplevel)" + +cd $GIT_PATH +GIT_PATH="$PWD" + +IFS=$'\n' FILES=($(git ls-files . --ignored --exclude-standard --others)) + + +cnt=${#FILES[@]} +for ((i=0;i<cnt;i++)); do + FILES[i]=$(realpath --relative-to "$CURRENT_PATH" "${GIT_PATH}/${FILES[i]}") +done + +cd $CURRENT_PATH + +echo "${FILES[*]}" \ + | fzf --ansi --preview 'bat -n $(echo {})' \ + | cut -d ":" -f1 diff --git a/docs/configuration/custom_commands/git_untracked b/docs/configuration/custom_commands/git_untracked new file mode 100755 index 0000000..fedae42 --- /dev/null +++ b/docs/configuration/custom_commands/git_untracked @@ -0,0 +1,20 @@ +#!/bin/bash + +CURRENT_PATH="$PWD" +GIT_PATH="$(git rev-parse --show-toplevel)" + +cd $GIT_PATH +GIT_PATH="$PWD" + +IFS=$'\n' FILES=($(git ls-files . --exclude-standard --others)) + +cnt=${#FILES[@]} +for ((i=0;i<cnt;i++)); do + FILES[i]=$(realpath --relative-to "$CURRENT_PATH" "${GIT_PATH}/${FILES[i]}") +done + +cd $CURRENT_PATH + +echo "${FILES[*]}" \ + | fzf --ansi --preview 'bat -n $(echo {})' \ + | cut -d ":" -f1 diff --git a/docs/configuration/custom_commands/joshuto_git_conflicts b/docs/configuration/custom_commands/joshuto_git_conflicts new file mode 100755 index 0000000..7721e88 --- /dev/null +++ b/docs/configuration/custom_commands/joshuto_git_conflicts @@ -0,0 +1,20 @@ +#!/bin/bash + +CURRENT_PATH="$PWD" +GIT_PATH="$(git rev-parse --show-toplevel)" + +cd $GIT_PATH +GIT_PATH="$PWD" + +IFS=$'\n' FILES=($(git diff --name-only --diff-filter=U --relative)) + +cnt=${#FILES[@]} +for ((i=0;i<cnt;i++)); do + FILES[i]=$(realpath --relative-to "$CURRENT_PATH" "${GIT_PATH}/${FILES[i]}") +done + +cd $CURRENT_PATH + +echo "${FILES[*]}" \ + | fzf --ansi --preview 'bat -n $(echo {})' \ + | cut -d ":" -f1 diff --git a/docs/configuration/custom_commands/joshuto_git_root b/docs/configuration/custom_commands/joshuto_git_root new file mode 100755 index 0000000..0b230ca --- /dev/null +++ b/docs/configuration/custom_commands/joshuto_git_root @@ -0,0 +1,2 @@ +#!/bin/bash +echo "$(git rev-parse --show-toplevel)/.git" diff --git a/docs/configuration/custom_commands/joshuto_rg b/docs/configuration/custom_commands/joshuto_rg new file mode 100755 index 0000000..4562acc --- /dev/null +++ b/docs/configuration/custom_commands/joshuto_rg @@ -0,0 +1,2 @@ +#!/bin/bash +rg -l "$@" | tail -n 1 diff --git a/docs/configuration/custom_commands/joshuto_rgfzf b/docs/configuration/custom_commands/joshuto_rgfzf new file mode 100755 index 0000000..1c0b7e0 --- /dev/null +++ b/docs/configuration/custom_commands/joshuto_rgfzf @@ -0,0 +1,5 @@ +#!/bin/bash + +rg -n -H --color=never "$@" \ + | fzf --ansi --preview 'bat -n $(echo {} | cut -d ":" -f1) --line-range="$(echo {} | cut -d ":" -f2):"' \ + | cut -d ":" -f1 diff --git a/docs/configuration/joshuto.toml.md b/docs/configuration/joshuto.toml.md index 1fc6e6e..7c90fc2 100644 --- a/docs/configuration/joshuto.toml.md +++ b/docs/configuration/joshuto.toml.md @@ -30,6 +30,12 @@ focus_on_create = true # The maximum file size to show a preview for max_preview_size = 2097152 # 2MB +# Define custom commands (using shell) with parameters like %text, %s etc. +custom_commands = [ + { name = "rgfzf", command = "/home/<USER>/.config/joshuto/rgfzf '%text' %s" }, + { name = "rg", command = "/home/<USER>/.config/joshuto/rg '%text' %s" } +] + # Configurations related to the display [display] # Different view layouts @@ -128,4 +134,5 @@ fzf_case_sensitivity = "insensitive" [tab] # inherit, home, root home_page = "home" + ``` diff --git a/docs/configuration/keymap.toml.md b/docs/configuration/keymap.toml.md index c83ebf7..0a3b791 100644 --- a/docs/configuration/keymap.toml.md +++ b/docs/configuration/keymap.toml.md @@ -405,6 +405,15 @@ An example: :set_case_sensitivity --type=fzf sensitive ``` +### `custom_search` + +Define search command using [`custom_command`]() + +### `custom_search_interactive` + +Similar to `select` and `custom_search`. Allows user to execute `custom_command` and +then interactively operate on the results. + ## Bookmarks ### `add_bookmark`: adds a bookmark to the `bookmarks.toml` file diff --git a/src/commands/custom_search.rs b/src/commands/custom_search.rs new file mode 100644 index 0000000..422143e --- /dev/null +++ b/src/commands/custom_search.rs @@ -0,0 +1,110 @@ +use super::change_directory::change_directory; +use super::sub_process::current_filenames; +use crate::commands::cursor_move; +use crate::context::AppContext; +use crate::error::{AppError, AppErrorKind, AppResult}; +use crate::ui::AppBackend; +use shell_words::split; +use std::process::{Command, Stdio}; + +pub fn custom_search( + context: &mut AppContext, + backend: &mut AppBackend, + words: &[String], + interactive: bool, +) -> AppResult { + let custom_command = context + .config_ref() + .custom_commands + .as_slice() + .iter() + .find(|x| x.name == words[0]) + .ok_or(AppError::new( + AppErrorKind::InvalidParameters, + "No custom command with given name".into(), + ))? + .command + .clone(); + + let current_filenames = current_filenames(context); + + let text = custom_command.replace("%s", ¤t_filenames.join(" ")); + let text = text.replace( + "%text", + &words + .iter() + .skip(1) + .cloned() + .collect::<Vec<String>>() + .join(" "), + ); + let mut command_with_args: Vec<String> = split(&text).map_err(|_| { + AppError::new( + AppErrorKind::InvalidParameters, + "Command cannot be splitted".into(), + ) + })?; + + let mut cmd = Command::new(command_with_args.remove(0)); + command_with_args.into_iter().for_each(|x| { + cmd.arg(x); + }); + + let cmd_result = if interactive { + backend.terminal_drop(); + let cmd_result = cmd + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn()? + .wait_with_output()?; + backend.terminal_restore()?; + cmd_result + } else { + cmd.output()? + }; + + if cmd_result.status.success() { + let returned_text = std::str::from_utf8(&cmd_result.stdout) + .map_err(|_| { + AppError::new( + AppErrorKind::ParseError, + "Could not get command result as utf8".into(), + ) + })? + .trim_end(); + + let path = std::path::Path::new(returned_text); + change_directory( + context, + path.parent().ok_or(AppError::new( + AppErrorKind::ParseError, + "Could not get parent directory".into(), + ))?, + )?; + + if let Some(current_dir_items) = context.tab_context_ref().curr_tab_ref().curr_list_ref() { + let position = current_dir_items + .iter() + .enumerate() + .find(|x| x.1.file_name() == path.file_name().unwrap_or_default()) + .map(|x| x.0) + .unwrap_or_default(); + + cursor_move::cursor_move(context, position); + } + + Ok(()) + } else { + let returned_text = std::str::from_utf8(&cmd_result.stderr).map_err(|_| { + AppError::new( + AppErrorKind::ParseError, + "Could not get command result as utf8".into(), + ) + })?; + + Err(AppError::new( + AppErrorKind::ParseError, + format!("Command failed: {}", returned_text), + )) + } +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 8b56f5a..da39d43 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -4,6 +4,7 @@ pub mod case_sensitivity; pub mod change_directory; pub mod command_line; pub mod cursor_move; +pub mod custom_search; pub mod delete_files; pub mod escape; pub mod file_ops; diff --git a/src/commands/sub_process.rs b/src/commands/sub_process.rs index fb1afb8..9f97af1 100644 --- a/src/commands/sub_process.rs +++ b/src/commands/sub_process.rs @@ -5,6 +5,27 @@ use std::process::{Command, Stdio}; use super::reload; +pub fn current_filenames(context: &AppContext) -> Vec<&str> { + let mut result = Vec::new(); + if let Some(curr_list) = context.tab_context_ref().curr_tab_ref().curr_list_ref() { + let mut i = 0; + curr_list + .iter_selected() + .map(|e| e.file_name()) + .for_each(|file_name| { + result.push(file_name); + i += 1; + }); + if i == 0 { + if let Some(entry) = curr_list.curr_entry_ref() { + result.push(entry.file_name()); + } + } + } + + result +} + fn execute_sub_process( context: &mut AppContext, words: &[String], @@ -14,21 +35,9 @@ fn execute_sub_process( for word in words.iter().skip(1) { match (*word).as_str() { "%s" => { - if let Some(curr_list) = context.tab_context_ref().curr_tab_ref().curr_list_ref() { - let mut i = 0; - curr_list - .iter_selected() - .map(|e| e.file_name()) - .for_each(|file_name| { - command.arg(file_name); - i += 1; - }); - if i == 0 { - if let Some(entry) = curr_list.curr_entry_ref() { - command.arg(entry.file_name()); - } - } - } + current_filenames(context).into_iter().for_each(|x| { + command.arg(x); + }); } s => { command.arg(s); diff --git a/src/config/clean/app/config.rs b/src/config/clean/app/config.rs index fb8ceda..0995e8d 100644 --- a/src/config/clean/app/config.rs +++ b/src/config/clean/app/config.rs @@ -1,7 +1,11 @@ use std::collections::HashMap; use crate::{ - config::{parse_config_or_default, raw::app::AppConfigRaw, TomlConfigFile}, + config::{ + parse_config_or_default, + raw::app::{AppConfigRaw, CustomCommand}, + TomlConfigFile, + }, error::AppResult, }; @@ -16,6 +20,7 @@ pub struct AppConfig { pub xdg_open: bool, pub xdg_open_fork: bool, pub watch_files: bool, + pub custom_commands: Vec<CustomCommand>, pub focus_on_create: bool, pub cmd_aliases: HashMap<String, String>, pub _display_options: DisplayOption, @@ -84,6 +89,7 @@ impl From<AppConfigRaw> for AppConfig { _preview_options: PreviewOption::from(raw.preview_options), _search_options: SearchOption::from(raw.search_options), _tab_options: TabOption::from(raw.tab_options), + custom_commands: raw.custom_commands, } } } diff --git a/src/config/clean/keymap/config.rs b/src/config/clean/keymap/config.rs index 23d7b06..55e1bf9 100644 --- a/src/config/clean/keymap/config.rs +++ b/src/config/clean/keymap/config.rs @@ -46,23 +46,31 @@ fn command_keymaps_vec_to_map(keymaps: &[CommandKeymap]) -> HashMap<Event, Comma let mut hashmap = HashMap::new(); for keymap in keymaps { - if keymap.commands.is_empty() { + if keymap.commands.is_empty() && keymap.command.is_none() { eprintln!("Keymap `commands` cannot be empty"); continue; } - let commands: Vec<Command> = keymap - .commands - .iter() - .filter_map(|cmd_str| match Command::from_str(cmd_str) { - Ok(s) => Some(s), - Err(err) => { - eprintln!("Keymap error: {}", err); - None - } - }) - .collect(); + let commands: Vec<Command> = match &keymap.command { + Some(command) => vec![command.clone()], + None => keymap.commands.clone(), + } + .iter() + .filter_map(|cmd_str| match Command::from_str(cmd_str) { + Ok(s) => Some(s), + Err(err) => { + eprintln!("Keymap error: {}", err); + None + } + }) + .collect(); + + let expected_len = if keymap.command.is_none() { + keymap.commands.len() + } else { + 1 + }; - if commands.len() != keymap.commands.len() { + if commands.len() != expected_len { eprintln!("Failed to parse commands: {:?}", keymap.commands); continue; } diff --git a/src/config/raw/app/config.rs b/src/config/raw/app/config.rs index feac4f2..cf2b758 100644 --- a/src/config/raw/app/config.rs +++ b/src/config/raw/app/config.rs @@ -14,6 +14,12 @@ const fn default_scroll_offset() -> usize { 6 } +#[derive(Debug, Deserialize, Clone)] +pub struct CustomCommand { + pub name: String, + pub command: String, +} + #[derive(Clone, Debug, Deserialize)] pub struct AppConfigRaw { #[serde(default = "default_scroll_offset")] @@ -38,4 +44,6 @@ pub struct AppConfigRaw { pub search_options: SearchOptionRaw, #[serde(default, rename = "tab")] pub tab_options: TabOptionRaw, + #[serde(default)] + pub custom_commands: Vec<CustomCommand>, } diff --git a/src/config/raw/keymap/config.rs b/src/config/raw/keymap/config.rs index 3eae82d..8d958d6 100644 --- a/src/config/raw/keymap/config.rs +++ b/src/config/raw/keymap/config.rs @@ -3,7 +3,12 @@ use serde::Deserialize; #[derive(Clone, Debug, Deserialize)] pub struct CommandKeymap { pub keys: Vec<String>, + + #[serde(default)] pub commands: Vec<String>, + #[serde(default)] + pub command: Option<String>, + pub description: Option<String>, } diff --git a/src/key_command/command.rs b/src/key_command/command.rs index d218632..fad1530 100644 --- a/src/key_command/command.rs +++ b/src/key_command/command.rs @@ -184,6 +184,9 @@ pub enum Command { Zoxide(String), ZoxideInteractive, + CustomSearch(Vec<String>), + CustomSearchInteractive(Vec<String>), + BookmarkAdd, BookmarkChangeDirectory, } diff --git a/src/key_command/constants.rs b/src/key_command/constants.rs index 1edfb42..0656a58 100644 --- a/src/key_command/constants.rs +++ b/src/key_command/constants.rs @@ -93,6 +93,8 @@ cmd_constants![ (CMD_FILTER_STRING, "filter"), (CMD_BOOKMARK_ADD, "add_bookmark"), (CMD_BOOKMARK_CHANGE_DIRECTORY, "cd_bookmark"), + (CMD_CUSTOM_SEARCH, "custom_search"), + (CMD_CUSTOM_SEARCH_INTERACTIVE, "custom_search_interactive"), ]; pub fn complete_command(partial_command: &str) -> Vec<Pair> { diff --git a/src/key_command/impl_appcommand.rs b/src/key_command/impl_appcommand.rs index 4b86608..dd7ab84 100644 --- a/src/key_command/impl_appcommand.rs +++ b/src/key_command/impl_appcommand.rs @@ -101,6 +101,9 @@ impl AppCommand for Command { Self::Zoxide(_) => CMD_ZOXIDE, Self::ZoxideInteractive => CMD_ZOXIDE_INTERACTIVE, + Self::CustomSearch(_) => CMD_CUSTOM_SEARCH, + Self::CustomSearchInteractive(_) => CMD_CUSTOM_SEARCH_INTERACTIVE, + Self::BookmarkAdd => CMD_BOOKMARK_ADD, Self::BookmarkChangeDirectory => CMD_BOOKMARK_CHANGE_DIRECTORY, } diff --git a/src/key_command/impl_appexecute.rs b/src/key_command/impl_appexecute.rs index b4033c2..c90fc2b 100644 --- a/src/key_command/impl_appexecute.rs +++ b/src/key_command/impl_appexecute.rs @@ -169,6 +169,13 @@ impl AppExecute for Command { Self::BookmarkAdd => bookmark::add_bookmark(context, backend), Self::BookmarkChangeDirectory => bookmark::change_directory_bookmark(context, backend), + + Self::CustomSearch(words) => { + custom_search::custom_search(context, backend, words.as_slice(), false) + } + Self::CustomSearchInteractive(words) => { + custom_search::custom_search(context, backend, words.as_slice(), true) + } } } } diff --git a/src/key_command/impl_comment.rs b/src/key_command/impl_comment.rs index 3922b32..0483e85 100644 --- a/src/key_command/impl_comment.rs +++ b/src/key_command/impl_comment.rs @@ -140,6 +140,10 @@ impl CommandComment for Command { Self::BookmarkAdd => "Add a bookmark", Self::BookmarkChangeDirectory => "Navigate to a bookmark", + Self::CustomSearch(_) => "Find file based on the custom command", + Self::CustomSearchInteractive(_) => { + "Interactively find file based on the custom command" + } } } } diff --git a/src/key_command/impl_from_str.rs b/src/key_command/impl_from_str.rs index 33fdfef..325003f 100644 --- a/src/key_command/impl_from_str.rs +++ b/src/key_command/impl_from_str.rs @@ -100,6 +100,16 @@ impl std::str::FromStr for Command { simple_command_conversion_case!(command, CMD_BULK_RENAME, Self::BulkRename); simple_command_conversion_case!(command, CMD_SEARCH_FZF, Self::SearchFzf); + simple_command_conversion_case!( + command, + CMD_CUSTOM_SEARCH, + Self::CustomSearch(arg.split(' ').map(|x| x.to_string()).collect()) + ); + simple_command_conversion_case!( + command, + CMD_CUSTOM_SEARCH_INTERACTIVE, + 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); |