summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDLFW <daniel@llin.info>2024-03-10 20:07:10 +0100
committerGitHub <noreply@github.com>2024-03-10 15:07:10 -0400
commit2de86e369748b5f62e08ea1abb133f9816d60b8b (patch)
tree473787eab309916befb44c220e1aaac6b9905009
parentdfbf093611d77e5d95c34f11a496f7f2001273ab (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.rs1
-rw-r--r--src/commands/stdout.rs64
-rw-r--r--src/commands/sub_process.rs94
-rw-r--r--src/context/app_context.rs3
-rw-r--r--src/error/error_kind.rs3
-rw-r--r--src/key_command/command.rs7
-rw-r--r--src/key_command/constants.rs6
-rw-r--r--src/key_command/impl_appcommand.rs16
-rw-r--r--src/key_command/impl_appexecute.rs6
-rw-r--r--src/key_command/impl_comment.rs8
-rw-r--r--src/key_command/impl_from_str.rs26
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),