diff options
author | DLFW <daniel@llin.info> | 2024-03-10 20:07:10 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-03-10 15:07:10 -0400 |
commit | 2de86e369748b5f62e08ea1abb133f9816d60b8b (patch) | |
tree | 473787eab309916befb44c220e1aaac6b9905009 | |
parent | dfbf093611d77e5d95c34f11a496f7f2001273ab (diff) |
Add `capture` and `stdout` commands (#495)
This adds two new commands as a base to enable users to use the output
of scripts to do certain actions in Joshuto.
The first command this adds is a third command to start a sub-process
beside `shell` and `spawn`, called `capture`. Like `shell`, `capture` is
running blocking but unlike `shell`, it does not release the terminal
but captures the `stdout` of the sub-process and stores it in an
`AppContext` attribute.
The second command added by this commit is `stdout`. This command takes
the output from the last `capture` run, stored in the `AppContext`
attribute, and uses it for some action. The action has to be specified
as a sub-command. As of now, only `stdout cd` is implemented. This
checks that the last output of `capture` is a single line of an existing
file or directory and then changes the working directory to that.
To get significant value from these new commands, `capture` needs to be
equipped with more variables to feed more information about Joshuto's
state into external scripts, and `stdout` needs to get some more
sub-commands.
-rw-r--r-- | src/commands/mod.rs | 1 | ||||
-rw-r--r-- | src/commands/stdout.rs | 64 | ||||
-rw-r--r-- | src/commands/sub_process.rs | 94 | ||||
-rw-r--r-- | src/context/app_context.rs | 3 | ||||
-rw-r--r-- | src/error/error_kind.rs | 3 | ||||
-rw-r--r-- | src/key_command/command.rs | 7 | ||||
-rw-r--r-- | src/key_command/constants.rs | 6 | ||||
-rw-r--r-- | src/key_command/impl_appcommand.rs | 16 | ||||
-rw-r--r-- | src/key_command/impl_appexecute.rs | 6 | ||||
-rw-r--r-- | src/key_command/impl_comment.rs | 8 | ||||
-rw-r--r-- | src/key_command/impl_from_str.rs | 26 |
11 files changed, 203 insertions, 31 deletions
diff --git a/src/commands/mod.rs b/src/commands/mod.rs index da39d43..02900a7 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -39,6 +39,7 @@ pub mod show_help; pub mod show_hidden; pub mod show_tasks; pub mod sort; +pub mod stdout; pub mod sub_process; pub mod subdir_fzf; pub mod tab_ops; diff --git a/src/commands/stdout.rs b/src/commands/stdout.rs new file mode 100644 index 0000000..cfa214f --- /dev/null +++ b/src/commands/stdout.rs @@ -0,0 +1,64 @@ +use crate::commands::change_directory::change_directory; +use crate::context::AppContext; +use crate::error::{AppError, AppErrorKind, AppResult}; +use crate::util::unix::expand_shell_string; +use std::path::PathBuf; + +#[derive(Debug, Clone)] +pub enum PostProcessor { + ChangeDirectory, +} + +impl PostProcessor { + pub fn from_str(args: &str) -> Option<Self> { + match args { + "cd" => Some(PostProcessor::ChangeDirectory), + _ => None, + } + } +} + +fn assert_one_line(stdout: &str) -> AppResult { + match stdout.lines().count() { + 1 => Ok(()), + _ => Err(AppError::new(AppErrorKind::StateError, "The last `capture` stdout does not have exactly one line as expected for this stdout processor".to_string())) + } +} + +fn as_one_existing_directory(stdout: &str) -> AppResult<PathBuf> { + assert_one_line(stdout)?; + let path = expand_shell_string(stdout); + if path.exists() { + if path.is_file() { + if let Some(parent) = path.parent() { + Ok(parent.to_path_buf()) + } else { + Err(AppError::new(AppErrorKind::StateError, "The last `capture` output is a file but without a valid directory as parent in the file system".to_string())) + } + } else { + Ok(path.to_path_buf()) + } + } else { + Err(AppError::new( + AppErrorKind::StateError, + "The last `capture` output line is not an existing path".to_string(), + )) + } +} + +pub fn post_process_std_out(processor: &PostProcessor, context: &mut AppContext) -> AppResult { + let last_stdout = &context.last_stdout; + if let Some(stdout) = last_stdout { + let stdout = stdout.trim(); + match processor { + PostProcessor::ChangeDirectory => { + change_directory(context, as_one_existing_directory(stdout)?.as_path()) + } + } + } else { + Err(AppError::new( + AppErrorKind::StateError, + "No result from a former `shell` available".to_string(), + )) + } +} diff --git a/src/commands/sub_process.rs b/src/commands/sub_process.rs index 58bfdc1..e1a5262 100644 --- a/src/commands/sub_process.rs +++ b/src/commands/sub_process.rs @@ -5,6 +5,13 @@ use std::process::{Command, Stdio}; use super::reload; +#[derive(Debug, Clone)] +pub enum SubprocessCallMode { + Interactive, + Spawn, + Capture, +} + 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() { @@ -29,7 +36,7 @@ pub fn current_filenames(context: &AppContext) -> Vec<&str> { fn execute_sub_process( context: &mut AppContext, words: &[String], - spawn: bool, + mode: SubprocessCallMode, ) -> std::io::Result<()> { let mut command = Command::new(words[0].clone()); for word in words.iter().skip(1) { @@ -61,15 +68,49 @@ fn execute_sub_process( } }; } - if spawn { - command - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn()?; - } else { - command.status()?; + match mode { + SubprocessCallMode::Interactive => { + let status = command.status(); + match status { + Ok(status) => { + if status.code() == Some(0) { + Ok(()) + } else { + Err(std::io::Error::new( + std::io::ErrorKind::Other, + format!("Command failed with {:?}", status), + )) + } + } + Err(err) => Err(std::io::Error::new( + std::io::ErrorKind::Other, + format!("Shell execution failed: {:?}", err), + )), + } + } + SubprocessCallMode::Spawn => { + command + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn()?; + Ok(()) + } + SubprocessCallMode::Capture => { + let output = command + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output()?; + if output.status.code() == Some(0) { + context.last_stdout = Some(String::from_utf8_lossy(&output.stdout).to_string()); + Ok(()) + } else { + Err(std::io::Error::new( + std::io::ErrorKind::Other, + format!("Command failed with {:?}", output.status), + )) + } + } } - Ok(()) } /// Handler for Joshuto's `shell` and `spawn` commands. @@ -77,17 +118,30 @@ pub fn sub_process( context: &mut AppContext, backend: &mut AppBackend, words: &[String], - spawn: bool, + mode: SubprocessCallMode, ) -> AppResult { - backend.terminal_drop(); - let res = execute_sub_process(context, words, spawn); - backend.terminal_restore(context.config_ref().mouse_support)?; - let _ = reload::soft_reload_curr_tab(context); - context.message_queue_mut().push_info(format!( - "{}: {}", - if spawn { "Spawned" } else { "Finished" }, - words.join(" ") - )); - res?; + match mode { + SubprocessCallMode::Interactive => { + // Joshuto needs to release the terminal when handing it over to some interactive + // shell command and restore it afterwards + backend.terminal_drop(); + execute_sub_process(context, words, mode)?; + backend.terminal_restore(context.config_ref().mouse_support)?; + let _ = reload::soft_reload_curr_tab(context); + context + .message_queue_mut() + .push_info(format!("Finished: {}", words.join(" "))); + } + SubprocessCallMode::Spawn => { + execute_sub_process(context, words, mode)?; + context + .message_queue_mut() + .push_info(format!("Spawned: {}", words.join(" "))); + } + SubprocessCallMode::Capture => { + execute_sub_process(context, words, mode)?; + let _ = reload::soft_reload_curr_tab(context); + } + }; Ok(()) } diff --git a/src/context/app_context.rs b/src/context/app_context.rs index 0026a52..a524c6e 100644 --- a/src/context/app_context.rs +++ b/src/context/app_context.rs @@ -49,6 +49,8 @@ pub struct AppContext { // the last preview area (or None if now preview shown) to check if a preview hook script needs // to be called preview_area: Option<PreviewArea>, + // the stdout of the last `shell` command + pub last_stdout: Option<String>, } impl AppContext { @@ -108,6 +110,7 @@ impl AppContext { watcher, watched_paths, preview_area: None, + last_stdout: None, } } diff --git a/src/error/error_kind.rs b/src/error/error_kind.rs index 1d68d29..915244b 100644 --- a/src/error/error_kind.rs +++ b/src/error/error_kind.rs @@ -21,11 +21,14 @@ pub enum AppErrorKind { Regex, InvalidParameters, + StateError, UnrecognizedArgument, UnrecognizedCommand, UnknownError, + + InternalError, } impl From<io::ErrorKind> for AppErrorKind { diff --git a/src/key_command/command.rs b/src/key_command/command.rs index fad1530..0a4736f 100644 --- a/src/key_command/command.rs +++ b/src/key_command/command.rs @@ -3,6 +3,8 @@ use std::path; use crate::commands::case_sensitivity::SetType; use crate::commands::quit::QuitAction; use crate::commands::select::SelectOption; +use crate::commands::stdout::PostProcessor; +use crate::commands::sub_process::SubprocessCallMode; use crate::config::clean::app::display::line_mode::LineMode; use crate::config::clean::app::display::line_number::LineNumberStyle; use crate::config::clean::app::display::new_tab::NewTabMode; @@ -137,7 +139,7 @@ pub enum Command { SetMode, SubProcess { words: Vec<String>, - spawn: bool, + mode: SubprocessCallMode, }, ShowTasks, @@ -181,6 +183,9 @@ pub enum Command { SelectFzf { options: SelectOption, }, + StdOutPostProcess { + processor: PostProcessor, + }, Zoxide(String), ZoxideInteractive, diff --git a/src/key_command/constants.rs b/src/key_command/constants.rs index 0656a58..a7d4d9a 100644 --- a/src/key_command/constants.rs +++ b/src/key_command/constants.rs @@ -69,8 +69,10 @@ cmd_constants![ (CMD_SET_MODE, "set_mode"), (CMD_SORT, "sort"), (CMD_SORT_REVERSE, "sort reverse"), - (CMD_SUBPROCESS_FOREGROUND, "shell"), - (CMD_SUBPROCESS_BACKGROUND, "spawn"), + (CMD_SUBPROCESS_INTERACTIVE, "shell"), + (CMD_SUBPROCESS_SPAWN, "spawn"), + (CMD_SUBPROCESS_CAPTURE, "capture"), + (CMD_STDOUT_POST_PROCESS, "stdout"), (CMD_SHOW_TASKS, "show_tasks"), (CMD_TAB_SWITCH, "tab_switch"), (CMD_TAB_SWITCH_INDEX, "tab_switch_index"), diff --git a/src/key_command/impl_appcommand.rs b/src/key_command/impl_appcommand.rs index dd7ab84..e261391 100644 --- a/src/key_command/impl_appcommand.rs +++ b/src/key_command/impl_appcommand.rs @@ -1,5 +1,6 @@ use super::constants::*; use super::{AppCommand, Command}; +use crate::commands::sub_process::SubprocessCallMode; impl AppCommand for Command { fn command(&self) -> &'static str { @@ -85,8 +86,19 @@ impl AppCommand for Command { Self::FilterRegex { .. } => CMD_FILTER_REGEX, Self::FilterString { .. } => CMD_FILTER_STRING, - Self::SubProcess { spawn: false, .. } => CMD_SUBPROCESS_FOREGROUND, - Self::SubProcess { spawn: true, .. } => CMD_SUBPROCESS_BACKGROUND, + Self::SubProcess { + mode: SubprocessCallMode::Interactive, + .. + } => CMD_SUBPROCESS_INTERACTIVE, + Self::SubProcess { + mode: SubprocessCallMode::Spawn, + .. + } => CMD_SUBPROCESS_SPAWN, + Self::SubProcess { + mode: SubprocessCallMode::Capture, + .. + } => CMD_SUBPROCESS_CAPTURE, + Self::StdOutPostProcess { .. } => CMD_STDOUT_POST_PROCESS, Self::SwitchLineNums(_) => CMD_SWITCH_LINE_NUMBERS, Self::SetLineMode(_) => CMD_SET_LINEMODE, diff --git a/src/key_command/impl_appexecute.rs b/src/key_command/impl_appexecute.rs index c90fc2b..10ab5e9 100644 --- a/src/key_command/impl_appexecute.rs +++ b/src/key_command/impl_appexecute.rs @@ -1,3 +1,4 @@ +use crate::commands::stdout::post_process_std_out; use crate::context::AppContext; use crate::error::AppResult; use crate::ui::AppBackend; @@ -137,9 +138,10 @@ impl AppExecute for Command { Self::Sort(t) => sort::set_sort(context, *t), Self::SetLineMode(mode) => linemode::set_linemode(context, *mode), Self::SortReverse => sort::toggle_reverse(context), - Self::SubProcess { words, spawn } => { - sub_process::sub_process(context, backend, words.as_slice(), *spawn) + Self::SubProcess { words, mode } => { + sub_process::sub_process(context, backend, words.as_slice(), mode.clone()) } + Self::StdOutPostProcess { processor } => post_process_std_out(processor, context), Self::SwitchLineNums(d) => line_nums::switch_line_numbering(context, *d), Self::Flat { depth } => flat::flatten(context, *depth), diff --git a/src/key_command/impl_comment.rs b/src/key_command/impl_comment.rs index 0483e85..bcfd3c9 100644 --- a/src/key_command/impl_comment.rs +++ b/src/key_command/impl_comment.rs @@ -3,6 +3,8 @@ use crate::{ io::FileOperationOptions, }; +use crate::commands::sub_process::SubprocessCallMode; + use super::{Command, CommandComment}; impl CommandComment for Command { @@ -104,8 +106,10 @@ impl CommandComment for Command { Self::SetCaseSensitivity { .. } => "Set case sensitivity", Self::SetMode => "Set file permissions", - Self::SubProcess { spawn: false, .. } => "Run a shell command", - Self::SubProcess { spawn: true, .. } => "Run command in background", + Self::SubProcess { mode: SubprocessCallMode::Interactive, .. } => "Run a shell command (blocking) and hand over shell temporarily", + Self::SubProcess { mode: SubprocessCallMode::Spawn, .. } => "Spawn a shell command", + Self::SubProcess { mode: SubprocessCallMode::Capture, .. } => "Run a shell command (blocking), do not hand over shall but capture stdout for post-processing", + Self::StdOutPostProcess { .. } => "Post process stdout of last `shell` command", Self::ShowTasks => "Show running background tasks", Self::ToggleHiddenFiles => "Toggle hidden files displaying", diff --git a/src/key_command/impl_from_str.rs b/src/key_command/impl_from_str.rs index 325003f..79a7164 100644 --- a/src/key_command/impl_from_str.rs +++ b/src/key_command/impl_from_str.rs @@ -3,6 +3,8 @@ use std::path; use crate::commands::case_sensitivity::SetType; use crate::commands::quit::QuitAction; use crate::commands::select::SelectOption; +use crate::commands::stdout::PostProcessor; +use crate::commands::sub_process::SubprocessCallMode; use crate::config::clean::app::display::line_mode::LineMode; use crate::config::clean::app::display::line_number::LineNumberStyle; use crate::config::clean::app::display::new_tab::NewTabMode; @@ -470,11 +472,22 @@ impl std::str::FromStr for Command { format!("{}: {}", arg, e), )), } - } else if command == CMD_SUBPROCESS_FOREGROUND || command == CMD_SUBPROCESS_BACKGROUND { + } else if command == CMD_SUBPROCESS_INTERACTIVE + || command == CMD_SUBPROCESS_SPAWN + || command == CMD_SUBPROCESS_CAPTURE + { match shell_words::split(arg) { Ok(s) if !s.is_empty() => Ok(Self::SubProcess { words: s, - spawn: command == "spawn", + mode: match command { + CMD_SUBPROCESS_CAPTURE => SubprocessCallMode::Capture, + CMD_SUBPROCESS_SPAWN => SubprocessCallMode::Spawn, + CMD_SUBPROCESS_INTERACTIVE => SubprocessCallMode::Interactive, + c => Err(AppError::new( + AppErrorKind::InternalError, + format!("Joshuto internal error: command {} unexpected in sub-process handling", c), + ))? + } }), Ok(_) => Err(AppError::new( AppErrorKind::InvalidParameters, @@ -485,6 +498,15 @@ impl std::str::FromStr for Command { format!("{}: {}", arg, e), )), } + } else if command == CMD_STDOUT_POST_PROCESS { + if let Some(processor) = PostProcessor::from_str(arg) { + Ok(Self::StdOutPostProcess { processor }) + } else { + Err(AppError::new( + AppErrorKind::InvalidParameters, + format!("{} is not a valid argument for stdout post-processing", arg), + )) + } } else if command == CMD_SORT { match arg { "reverse" => Ok(Self::SortReverse), |