summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDenys Séguret <cano.petrole@gmail.com>2024-02-21 09:59:04 +0100
committerGitHub <noreply@github.com>2024-02-21 09:59:04 +0100
commit829a67ffa87f6f4abf9f22661ccdabf64b90d341 (patch)
treee601a9550647b060529e787d20c31ee9517dd4a3
parent43ddc796162e5554623e1fd88555153e5ec360f1 (diff)
: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.
-rw-r--r--src/app/panel_state.rs50
-rw-r--r--src/browser/browser_state.rs2
-rw-r--r--src/cli/args.rs4
-rw-r--r--src/cli/mod.rs1
-rw-r--r--src/filesystems/filesystems_state.rs2
-rw-r--r--src/help/help_state.rs2
-rw-r--r--src/preview/preview_state.rs2
-rw-r--r--src/stage/stage_state.rs2
-rw-r--r--src/verb/execution_builder.rs58
-rw-r--r--src/verb/internal.rs4
-rw-r--r--src/verb/internal_focus.rs4
-rw-r--r--src/verb/internal_select.rs2
-rw-r--r--src/verb/mod.rs2
-rw-r--r--src/verb/verb.rs2
-rw-r--r--src/verb/verb_store.rs3
-rw-r--r--src/verb/write.rs42
16 files changed, 167 insertions, 15 deletions
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<CmdResult, ProgramError> {
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<PathBuf>,
+ /// An optional path where to write when a verb uses `:write_output`
+ #[arg(long, value_name = "verb-output")]
+ pub verb_output: Option<PathBuf>,
+
/// Semicolon separated commands to execute
#[arg(short, long, value_name = "cmd")]
pub cmd: Option<String>,
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<Option<Launchable>, 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<InvocationParser>,
+ 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<count>\d*)?",
Internal::line_up_no_cycle => r"line_up_no_cycle (?P<count>\d*)?",
Internal::set_syntax_theme => r"set_syntax_theme {theme:theme}",
+ Internal::write_output => r"write_output (?P<line>.*)",
_ => 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<CmdResult, ProgramError> {
+ 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<CmdResult, ProgramError> {
+ 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)
+}