From 829a67ffa87f6f4abf9f22661ccdabf64b90d341 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denys=20S=C3=A9guret?= Date: Wed, 21 Feb 2024 09:59:04 +0100 Subject: :write_output & :clear_output, --verb_output (#834) Fix: #825 Add a `--verb-output` launch argument which takes a path to a file (which will be created if necessary) Add a `:write_output` internal which allows adding a line to that file. No escaping is done (contrary to what happens with `--outcmd`). Add a `:clear_output` internal which clears the file. Here are 2 examples of verbs: ``` { invocation: "wc {cmd}" execution: ":write_output wc:{cmd} {file-stem}.bro" } { key: alt-w cmd: ":clear_output;:write_output {directory};:quit" } ``` The first one is called with an input like `:wc hop` which appends to the output a line like `wc:hop main.bro`. The second one makes the content of the output file the directory closest to the selection then quits. It could for example be used for a new version of the `br` shell function. Note: Semantics isn't pretty. If you have a better idea than "output", please tell me. --- src/app/panel_state.rs | 50 +++++++++++++++++++++++++------ src/browser/browser_state.rs | 2 ++ src/cli/args.rs | 4 +++ src/cli/mod.rs | 1 + src/filesystems/filesystems_state.rs | 2 ++ src/help/help_state.rs | 2 ++ src/preview/preview_state.rs | 2 ++ src/stage/stage_state.rs | 2 ++ src/verb/execution_builder.rs | 58 ++++++++++++++++++++++++++++++++++-- src/verb/internal.rs | 4 +++ src/verb/internal_focus.rs | 4 +-- src/verb/internal_select.rs | 2 +- src/verb/mod.rs | 2 ++ src/verb/verb.rs | 2 +- src/verb/verb_store.rs | 3 ++ src/verb/write.rs | 42 ++++++++++++++++++++++++++ 16 files changed, 167 insertions(+), 15 deletions(-) create mode 100644 src/verb/write.rs diff --git a/src/app/panel_state.rs b/src/app/panel_state.rs index dfa97d9..fd00aef 100644 --- a/src/app/panel_state.rs +++ b/src/app/panel_state.rs @@ -79,9 +79,11 @@ pub trait PanelState { /// The invocation comes from the input and may be related /// to a different verb (the verb may have been triggered /// by a key shortcut) + #[allow(clippy::too_many_arguments)] fn on_internal( &mut self, w: &mut W, + invocation_parser: Option<&InvocationParser>, internal_exec: &InternalExecution, input_invocation: Option<&VerbInvocation>, trigger_type: TriggerType, @@ -92,9 +94,11 @@ pub trait PanelState { /// a generic implementation of on_internal which may be /// called by states when they don't have a specific /// behavior to execute + #[allow(clippy::too_many_arguments)] fn on_internal_generic( &mut self, _w: &mut W, + invocation_parser: Option<&InvocationParser>, internal_exec: &InternalExecution, input_invocation: Option<&VerbInvocation>, _trigger_type: TriggerType, @@ -571,6 +575,37 @@ pub trait PanelState { Internal::print_relative_path => print::print_relative_paths(self.sel_info(app_state), con)?, Internal::refresh => CmdResult::RefreshState { clear_cache: true }, Internal::quit => CmdResult::Quit, + Internal::clear_output => { + verb_clear_output(con) + .unwrap_or_else(|e| CmdResult::DisplayError(format!("{e}"))) + } + Internal::write_output => { + let sel_info = self.sel_info(app_state); + let exec_builder = match input_invocation { + Some(inv) => { + ExecutionStringBuilder::with_invocation( + invocation_parser, + sel_info, + app_state, + inv.args.as_ref(), + ) + } + None => { + ExecutionStringBuilder::without_invocation(sel_info, app_state) + } + }; + if let Some(pattern) = internal_exec.arg.as_ref() { + let line = exec_builder.string(pattern); + verb_write(con, &line)?; + } else { + let line = input_invocation + .and_then(|inv| inv.args.as_ref()) + .map(|s| s.as_str()) + .unwrap_or(""); + verb_write(con, line)?; + } + CmdResult::Keep + } _ => CmdResult::Keep, }) } @@ -653,6 +688,7 @@ pub trait PanelState { VerbExecution::Internal(internal_exec) => { self.on_internal( w, + verb.invocation_parser.as_ref(), internal_exec, invocation, trigger_type, @@ -700,7 +736,7 @@ pub trait PanelState { } } let exec_builder = ExecutionStringBuilder::with_invocation( - &verb.invocation_parser, + verb.invocation_parser.as_ref(), sel_info, app_state, if let Some(inv) = invocation { @@ -719,7 +755,7 @@ pub trait PanelState { seq_ex: &SequenceExecution, invocation: Option<&VerbInvocation>, app_state: &mut AppState, - _cc: &CmdContext, + cc: &CmdContext, ) -> Result { let sel_info = self.sel_info(app_state); if matches!(sel_info, SelInfo::More(_)) { @@ -729,7 +765,7 @@ pub trait PanelState { return Ok(CmdResult::error("sequences can't be executed on multiple selections")); } let exec_builder = ExecutionStringBuilder::with_invocation( - &verb.invocation_parser, + verb.invocation_parser.as_ref(), sel_info, app_state, if let Some(inv) = invocation { @@ -738,12 +774,7 @@ pub trait PanelState { None }, ); - // TODO what follows is dangerous: if an inserted group value contains the separator, - // the parsing will cut on this separator - let sequence = Sequence { - raw: exec_builder.shell_exec_string(&ExecPattern::from_string(&seq_ex.sequence.raw)), - separator: seq_ex.sequence.separator.clone(), - }; + let sequence = exec_builder.sequence(&seq_ex.sequence, &cc.app.con.verb_store); Ok(CmdResult::ExecuteSequence { sequence }) } @@ -782,6 +813,7 @@ pub trait PanelState { input_invocation, } => self.on_internal( w, + None, &InternalExecution::from_internal(*internal), input_invocation.as_ref(), TriggerType::Other, diff --git a/src/browser/browser_state.rs b/src/browser/browser_state.rs index 1a6e6db..e84788e 100644 --- a/src/browser/browser_state.rs +++ b/src/browser/browser_state.rs @@ -315,6 +315,7 @@ impl PanelState for BrowserState { fn on_internal( &mut self, w: &mut W, + invocation_parser: Option<&InvocationParser>, internal_exec: &InternalExecution, input_invocation: Option<&VerbInvocation>, trigger_type: TriggerType, @@ -612,6 +613,7 @@ impl PanelState for BrowserState { Internal::quit => CmdResult::Quit, _ => self.on_internal_generic( w, + invocation_parser, internal_exec, input_invocation, trigger_type, diff --git a/src/cli/args.rs b/src/cli/args.rs index 6b45cc1..0dd7e86 100644 --- a/src/cli/args.rs +++ b/src/cli/args.rs @@ -140,6 +140,10 @@ pub struct Args { #[arg(long, value_name = "path")] pub outcmd: Option, + /// An optional path where to write when a verb uses `:write_output` + #[arg(long, value_name = "verb-output")] + pub verb_output: Option, + /// Semicolon separated commands to execute #[arg(short, long, value_name = "cmd")] pub cmd: Option, diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 408ad2d..f7cfc22 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -158,6 +158,7 @@ pub fn run() -> Result, ProgramError> { w.queue(EnableMouseCapture)?; } let r = app.run(&mut w, &mut context, &config); + w.flush()?; if context.capture_mouse { w.queue(DisableMouseCapture)?; } diff --git a/src/filesystems/filesystems_state.rs b/src/filesystems/filesystems_state.rs index bbeb26d..1a11582 100644 --- a/src/filesystems/filesystems_state.rs +++ b/src/filesystems/filesystems_state.rs @@ -474,6 +474,7 @@ impl PanelState for FilesystemState { fn on_internal( &mut self, w: &mut W, + invocation_parser: Option<&InvocationParser>, internal_exec: &InternalExecution, input_invocation: Option<&VerbInvocation>, trigger_type: TriggerType, @@ -578,6 +579,7 @@ impl PanelState for FilesystemState { open_leave => CmdResult::PopStateAndReapply, _ => self.on_internal_generic( w, + invocation_parser, internal_exec, input_invocation, trigger_type, diff --git a/src/help/help_state.rs b/src/help/help_state.rs index ac50fdc..9425b57 100644 --- a/src/help/help_state.rs +++ b/src/help/help_state.rs @@ -202,6 +202,7 @@ impl PanelState for HelpState { fn on_internal( &mut self, w: &mut W, + invocation_parser: Option<&InvocationParser>, internal_exec: &InternalExecution, input_invocation: Option<&VerbInvocation>, trigger_type: TriggerType, @@ -260,6 +261,7 @@ impl PanelState for HelpState { } _ => self.on_internal_generic( w, + invocation_parser, internal_exec, input_invocation, trigger_type, diff --git a/src/preview/preview_state.rs b/src/preview/preview_state.rs index 4640cf7..ca27d95 100644 --- a/src/preview/preview_state.rs +++ b/src/preview/preview_state.rs @@ -304,6 +304,7 @@ impl PanelState for PreviewState { fn on_internal( &mut self, w: &mut W, + invocation_parser: Option<&InvocationParser>, internal_exec: &InternalExecution, input_invocation: Option<&VerbInvocation>, trigger_type: TriggerType, @@ -397,6 +398,7 @@ impl PanelState for PreviewState { Internal::preview_binary => self.set_mode(PreviewMode::Hex, con), _ => self.on_internal_generic( w, + invocation_parser, internal_exec, input_invocation, trigger_type, diff --git a/src/stage/stage_state.rs b/src/stage/stage_state.rs index 7c277a3..0c70418 100644 --- a/src/stage/stage_state.rs +++ b/src/stage/stage_state.rs @@ -443,6 +443,7 @@ impl PanelState for StageState { fn on_internal( &mut self, w: &mut W, + invocation_parser: Option<&InvocationParser>, internal_exec: &InternalExecution, input_invocation: Option<&VerbInvocation>, trigger_type: TriggerType, @@ -505,6 +506,7 @@ impl PanelState for StageState { } _ => self.on_internal_generic( w, + invocation_parser, internal_exec, input_invocation, trigger_type, diff --git a/src/verb/execution_builder.rs b/src/verb/execution_builder.rs index c212793..984f2e7 100644 --- a/src/verb/execution_builder.rs +++ b/src/verb/execution_builder.rs @@ -2,6 +2,7 @@ use { super::*, crate::{ app::*, + command::*, path, }, ahash::AHashMap, @@ -46,7 +47,7 @@ impl<'b> ExecutionStringBuilder<'b> { } } pub fn with_invocation( - invocation_parser: &Option, + invocation_parser: Option<&InvocationParser>, sel_info: SelInfo<'b>, app_state: &'b AppState, invocation_args: Option<&String>, @@ -253,7 +254,60 @@ impl<'b> ExecutionStringBuilder<'b> { .one_sel() .map_or(self.root, |sel| sel.path) } - + /// replace groups in a sequence + /// + /// Replacing escapes for the shell for externals, and without + /// escaping for internals. + /// + /// Note that this is *before* asking the (local or remote) panel + /// state the sequential execution of the different commands. In + /// this secondary execution, new replacements are expected too, + /// depending on the verbs. + pub fn sequence( + &self, + sequence: &Sequence, + verb_store: &VerbStore, + ) -> Sequence { + let mut inputs = Vec::new(); + for input in sequence.raw.split(&sequence.separator) { + let raw_parts = CommandParts::from(input.to_string()); + let (_, verb_invocation) = raw_parts.split(); + let verb_is_external = verb_invocation + .and_then(|vi| { + let command = Command::from_parts(vi, true); + if let Command::VerbInvocate(invocation) = &command { + let search = verb_store.search_prefix(&invocation.name); + if let PrefixSearchResult::Match(_, verb) = search { + return Some(verb); + } + } + None + }) + .map_or(false, |verb| verb.get_internal().is_none()); + let input = if verb_is_external { + self.shell_exec_string(&ExecPattern::from_string(input)) + } else { + self.string(&input) + }; + inputs.push(input); + } + Sequence { + raw: inputs.join(&sequence.separator), + separator: sequence.separator.clone(), + } + } + /// build a raw string, without escapings + pub fn string( + &self, + pattern: &str, + ) -> String { + GROUP + .replace_all( + pattern, + |ec: &Captures<'_>| self.get_capture_replacement(ec), + ) + .to_string() + } /// build a path pub fn path( &self, diff --git a/src/verb/internal.rs b/src/verb/internal.rs index ff56bd0..d15c774 100644 --- a/src/verb/internal.rs +++ b/src/verb/internal.rs @@ -148,6 +148,8 @@ Internals! { trash: "move file to system trash" true, unstage: "remove selection from staging area" true, up_tree: "focus the parent of the current root" true, + write_output: "write the argument to the --verb-output file" false, + clear_output: "clear the --verb-output file" false, //restore_pattern: "restore a pattern which was just removed" false, } @@ -161,6 +163,7 @@ impl Internal { Internal::line_down_no_cycle => r"line_down_no_cycle (?P\d*)?", Internal::line_up_no_cycle => r"line_up_no_cycle (?P\d*)?", Internal::set_syntax_theme => r"set_syntax_theme {theme:theme}", + Internal::write_output => r"write_output (?P.*)", _ => self.name(), } } @@ -171,6 +174,7 @@ impl Internal { Internal::line_up => r"line_up {count}", Internal::line_down_no_cycle => r"line_down_no_cycle {count}", Internal::line_up_no_cycle => r"line_up_no_cycle {count}", + Internal::write_output => r"write_output {line}", _ => self.name(), } } diff --git a/src/verb/internal_focus.rs b/src/verb/internal_focus.rs index eaa939b..95b8862 100644 --- a/src/verb/internal_focus.rs +++ b/src/verb/internal_focus.rs @@ -114,7 +114,7 @@ fn path_from_input( // } // (or that input is useless) let path_builder = ExecutionStringBuilder::with_invocation( - &verb.invocation_parser, + verb.invocation_parser.as_ref(), SelInfo::from_path(base_path), app_state, Some(input_arg), @@ -137,7 +137,7 @@ fn path_from_input( // state's selection // (we assume a check before ensured it doesn't need an input) let path_builder = ExecutionStringBuilder::with_invocation( - &verb.invocation_parser, + verb.invocation_parser.as_ref(), SelInfo::from_path(base_path), app_state, None, diff --git a/src/verb/internal_select.rs b/src/verb/internal_select.rs index 43db6ee..383e281 100644 --- a/src/verb/internal_select.rs +++ b/src/verb/internal_select.rs @@ -95,7 +95,7 @@ fn path_from_input( // } // (or that input is useless) let path_builder = ExecutionStringBuilder::with_invocation( - &verb.invocation_parser, + verb.invocation_parser.as_ref(), SelInfo::from_path(base_path), app_state, Some(input_arg), diff --git a/src/verb/mod.rs b/src/verb/mod.rs index 01476d3..f90d05d 100644 --- a/src/verb/mod.rs +++ b/src/verb/mod.rs @@ -15,6 +15,7 @@ mod verb_description; mod verb_execution; mod verb_invocation; mod verb_store; +mod write; pub use { arg_def::*, @@ -33,6 +34,7 @@ pub use { verb_execution::VerbExecution, verb_invocation::*, verb_store::{PrefixSearchResult, VerbStore}, + write::*, }; use { lazy_regex::*, diff --git a/src/verb/verb.rs b/src/verb/verb.rs index a96e0da..b316161 100644 --- a/src/verb/verb.rs +++ b/src/verb/verb.rs @@ -242,7 +242,7 @@ impl Verb { let builder = || { ExecutionStringBuilder::with_invocation( - &self.invocation_parser, + self.invocation_parser.as_ref(), sel_info, app_state, invocation.args.as_ref(), diff --git a/src/verb/verb_store.rs b/src/verb/verb_store.rs index a748e17..ce2f5ad 100644 --- a/src/verb/verb_store.rs +++ b/src/verb/verb_store.rs @@ -311,6 +311,9 @@ impl VerbStore { self.add_internal(trash); self.add_internal(total_search).with_key(key!(ctrl-s)); self.add_internal(up_tree).with_shortcut("up"); + + self.add_internal(clear_output); + self.add_internal(write_output); } fn build_add_internal( diff --git a/src/verb/write.rs b/src/verb/write.rs new file mode 100644 index 0000000..2c489e4 --- /dev/null +++ b/src/verb/write.rs @@ -0,0 +1,42 @@ +use { + crate::{ + app::*, + errors::ProgramError, + }, + std::{ + fs::{File, OpenOptions}, + io::Write, + }, +}; + +/// Intended to verbs, this function writes the passed string to the file +/// provided to broot with `--verb-output`, creating a new line if the +/// file is not empty. +pub fn verb_write( + con: &AppContext, + line: &str, +) -> Result { + let Some(path) = &con.launch_args.verb_output else { + return Ok(CmdResult::error("No --verb-output provided".to_string())); + }; + let mut file = OpenOptions::new() + .create(true) + .append(true) + .open(path)?; + if file.metadata().map(|m| m.len() > 0).unwrap_or(false) { + writeln!(file)?; + } + write!(file, "{}", line)?; + Ok(CmdResult::Keep) +} + +/// Remove the content of the file provided to broot with `--verb-output`. +pub fn verb_clear_output( + con: &AppContext, +) -> Result { + let Some(path) = &con.launch_args.verb_output else { + return Ok(CmdResult::error("No --verb-output provided".to_string())); + }; + File::create(path)?; + Ok(CmdResult::Keep) +} -- cgit v1.2.3