summaryrefslogtreecommitdiffstats
path: root/src/verb
diff options
context:
space:
mode:
authorCanop <cano.petrole@gmail.com>2021-04-19 20:59:15 +0200
committerCanop <cano.petrole@gmail.com>2021-04-19 20:59:15 +0200
commit6da21634bf925b9e3bce497b5d8e6218786f2628 (patch)
tree00a1d150a8f8847933266791b86672a050481818 /src/verb
parentaa1dc4a944ad1f11cf8292c49cb771dd1384c6c7 (diff)
improve command previsualization in status for multiselection
If a group is common to all paths, it will be kept
Diffstat (limited to 'src/verb')
-rw-r--r--src/verb/execution_builder.rs111
-rw-r--r--src/verb/external_execution.rs116
-rw-r--r--src/verb/verb.rs38
3 files changed, 209 insertions, 56 deletions
diff --git a/src/verb/execution_builder.rs b/src/verb/execution_builder.rs
index 24cec0e..2721ce4 100644
--- a/src/verb/execution_builder.rs
+++ b/src/verb/execution_builder.rs
@@ -1,7 +1,7 @@
use {
super::*,
crate::{
- app::Selection,
+ app::{Selection, SelInfo, SelectionType},
path,
},
ahash::AHashMap,
@@ -14,9 +14,9 @@ use {
/// a verb's execution pattern
pub struct ExecutionStringBuilder<'b> {
/// the current file selection
- pub sel: Option<Selection<'b>>,
+ pub sel_info: SelInfo<'b>,
- /// the selection in the other panel, when there exactly two
+ /// the selection in the other panel, when there are exactly two
other_file: Option<&'b PathBuf>,
/// parsed arguments
@@ -24,18 +24,18 @@ pub struct ExecutionStringBuilder<'b> {
}
impl<'b> ExecutionStringBuilder<'b> {
- pub fn from_selection(
- sel: Option<Selection<'b>>,
+ pub fn from_sel_info(
+ sel_info: SelInfo<'b>,
) -> Self {
Self {
- sel,
+ sel_info,
other_file: None,
invocation_values: None,
}
}
pub fn from_invocation(
invocation_parser: &Option<InvocationParser>,
- sel: Option<Selection<'b>>,
+ sel_info: SelInfo<'b>,
other_file: &'b Option<PathBuf>,
invocation_args: &Option<String>,
) -> Self {
@@ -44,27 +44,47 @@ impl<'b> ExecutionStringBuilder<'b> {
.zip(invocation_args.as_ref())
.and_then(|(parser, args)| parser.parse(args));
Self {
- sel,
+ sel_info,
other_file: other_file.as_ref(),
invocation_values,
}
}
- fn get_file(&self) -> Option<&Path> {
- self.sel.map(|s| s.path)
- }
- fn get_directory(&self) -> Option<PathBuf> {
- self.sel.map(|s| path::closest_dir(s.path))
- }
- fn get_parent(&self) -> Option<&Path> {
- self.sel.and_then(|s| s.path.parent())
- }
fn get_raw_capture_replacement(&self, ec: &Captures<'_>) -> Option<String> {
+ match self.sel_info {
+ SelInfo::None => self.get_raw_sel_capture_replacement(ec, None),
+ SelInfo::One(sel) => self.get_raw_sel_capture_replacement(ec, Some(sel)),
+ SelInfo::More(stage) => {
+ let mut sels = stage.paths.iter()
+ .map(|path| Selection {
+ path,
+ line: 0,
+ stype: SelectionType::from(path),
+ is_exe: false,
+ });
+ self.get_raw_sel_capture_replacement(ec, sels.next())
+ .filter(|first_rcr| {
+ for sel in sels {
+ let rcr = self.get_raw_sel_capture_replacement(ec, Some(sel));
+ if rcr.as_ref() != Some(first_rcr) {
+ return false;
+ }
+ }
+ true
+ })
+ }
+ }
+ }
+ fn get_raw_sel_capture_replacement(
+ &self,
+ ec: &Captures<'_>,
+ sel: Option<Selection<'_>>,
+ ) -> Option<String> {
let name = ec.get(1).unwrap().as_str();
match name {
- "line" => self.sel.map(|s| s.line.to_string()),
- "file" => self.get_file().map(path_to_string),
- "directory" => self.get_directory().map(path_to_string),
- "parent" => self.get_parent().map(path_to_string),
+ "line" => sel.map(|s| s.line.to_string()),
+ "file" => sel.map(|s| s.path).map(path_to_string),
+ "directory" => sel.map(|s| path::closest_dir(s.path)).map(path_to_string),
+ "parent" => sel.and_then(|s| s.path.parent()).map(path_to_string),
"other-panel-file" => self.other_file.map(path_to_string),
"other-panel-directory" => self
.other_file
@@ -84,11 +104,11 @@ impl<'b> ExecutionStringBuilder<'b> {
if let Some(fmt) = ec.get(2) {
match fmt.as_str() {
"path-from-directory" => {
- self.get_directory()
+ sel.map(|s| path::closest_dir(s.path))
.map(|dir| path::path_str_from(dir, value))
}
"path-from-parent" => {
- self.get_parent()
+ sel.and_then(|s| s.path.parent())
.map(|dir| path::path_str_from(dir, value))
}
_ => Some(format!("invalid format: {:?}", fmt.as_str())),
@@ -104,6 +124,14 @@ impl<'b> ExecutionStringBuilder<'b> {
self.get_raw_capture_replacement(ec)
.unwrap_or_else(|| ec[0].to_string())
}
+ fn get_sel_capture_replacement(
+ &self,
+ ec: &Captures<'_>,
+ sel: Option<Selection<'_>>,
+ ) -> String {
+ self.get_raw_sel_capture_replacement(ec, sel)
+ .unwrap_or_else(|| ec[0].to_string())
+ }
/// build a shell compatible command, with escapings
pub fn shell_exec_string(
&self,
@@ -119,6 +147,24 @@ impl<'b> ExecutionStringBuilder<'b> {
.fix_paths()
.to_string()
}
+ /// build a shell compatible command, with escapings, for a specific
+ /// selection (this is intended for execution on all selections of a
+ /// stage)
+ pub fn sel_shell_exec_string(
+ &self,
+ exec_pattern: &ExecPattern,
+ sel: Option<Selection<'_>>,
+ ) -> String {
+ exec_pattern
+ .apply(&|s| {
+ GROUP.replace_all(
+ s,
+ |ec: &Captures<'_>| self.get_sel_capture_replacement(ec, sel),
+ ).to_string()
+ })
+ .fix_paths()
+ .to_string()
+ }
/// build a vec of tokens which can be passed to Command to
/// launch an executable
pub fn exec_token(
@@ -135,6 +181,23 @@ impl<'b> ExecutionStringBuilder<'b> {
.fix_paths()
.into_array()
}
+ /// build a vec of tokens which can be passed to Command to
+ /// launch an executable
+ pub fn sel_exec_token(
+ &self,
+ exec_pattern: &ExecPattern,
+ sel: Option<Selection<'_>>,
+ ) -> Vec<String> {
+ exec_pattern
+ .apply(&|s| {
+ GROUP.replace_all(
+ s,
+ |ec: &Captures<'_>| self.get_sel_capture_replacement(ec, sel),
+ ).to_string()
+ })
+ .fix_paths()
+ .into_array()
+ }
}
#[cfg(test)]
@@ -170,7 +233,7 @@ mod execution_builder_test {
stype: SelectionType::File,
is_exe: false,
};
- let mut builder = ExecutionStringBuilder::from_selection(Some(sel));
+ let mut builder = ExecutionStringBuilder::from_sel_info(SelInfo::One(sel));
let mut map = AHashMap::default();
for (k, v) in replacements {
map.insert(k.to_owned(), v.to_owned());
diff --git a/src/verb/external_execution.rs b/src/verb/external_execution.rs
index 3ec6cdd..a8ebe1e 100644
--- a/src/verb/external_execution.rs
+++ b/src/verb/external_execution.rs
@@ -58,25 +58,44 @@ impl ExternalExecution {
self
}
+ /// goes from the external execution command to the CmdResult:
+ /// - by executing the command if it can be executed from a subprocess
+ /// - by building a command to be executed in parent shell in other cases
pub fn to_cmd_result(
&self,
w: &mut W,
builder: ExecutionStringBuilder<'_>,
con: &AppContext,
) -> Result<CmdResult, ProgramError> {
- if self.exec_mode.is_from_shell() {
- self.exec_from_shell_cmd_result(builder, con)
- } else {
- self.exec_cmd_result(w, builder, con)
+ match self.exec_mode {
+ ExternalExecutionMode::FromParentShell => self.cmd_result_exec_from_parent_shell(
+ builder,
+ con,
+ ),
+ ExternalExecutionMode::LeaveBroot => self.cmd_result_exec_leave_broot(
+ builder,
+ con,
+ ),
+ ExternalExecutionMode::StayInBroot => self.cmd_result_exec_stay_in_broot(
+ w,
+ builder,
+ con,
+ ),
}
}
- /// build the cmd result as an executable which will be called from shell
- fn exec_from_shell_cmd_result(
+ /// build the cmd result as an executable which will be called
+ /// from the parent shell (meaning broot must quit)
+ fn cmd_result_exec_from_parent_shell(
&self,
builder: ExecutionStringBuilder<'_>,
con: &AppContext,
) -> Result<CmdResult, ProgramError> {
+ if builder.sel_info.count_paths() > 1 {
+ return Ok(CmdResult::error(
+ "only verbs returning to broot on end can be executed on a multi-selection"
+ ));
+ }
if let Some(ref export_path) = con.launch_args.cmd_export_path {
// Broot was probably launched as br.
// the whole command is exported in the passed file
@@ -84,7 +103,7 @@ impl ExternalExecution {
writeln!(&f, "{}", builder.shell_exec_string(&self.exec_pattern))?;
Ok(CmdResult::Quit)
} else if let Some(ref export_path) = con.launch_args.file_export_path {
- if let Some(sel) = builder.sel {
+ if let Some(sel) = builder.sel_info.as_one_sel() {
// old version of the br function: only the file is exported
// in the passed file
let f = OpenOptions::new().append(true).open(export_path)?;
@@ -102,37 +121,80 @@ impl ExternalExecution {
}
/// build the cmd result as an executable which will be called in a process
- /// launched by broot
- fn exec_cmd_result(
+ /// launched by broot at end of broot
+ fn cmd_result_exec_leave_broot(
&self,
- w: &mut W,
builder: ExecutionStringBuilder<'_>,
con: &AppContext,
) -> Result<CmdResult, ProgramError> {
+ if builder.sel_info.count_paths() > 1 {
+ return Ok(CmdResult::error(
+ "only verbs returning to broot on end can be executed on a multi-selection"
+ ));
+ }
let launchable = Launchable::program(
builder.exec_token(&self.exec_pattern),
- if self.set_working_dir {
- builder.sel.map(|sel| path::closest_dir(sel.path))
- } else {
- None
- },
+ builder.sel_info
+ .as_one_sel()
+ .filter(|_| self.set_working_dir)
+ .map(|sel| path::closest_dir(sel.path)),
con,
)?;
- if self.exec_mode.is_leave_broot() {
- Ok(CmdResult::from(launchable))
- } else {
- info!("Executing not leaving, launchable {:?}", launchable);
- let execution = launchable.execute(Some(w));
- match execution {
- Ok(()) => {
- debug!("ok");
- Ok(CmdResult::RefreshState { clear_cache: true })
- }
- Err(e) => {
+ Ok(CmdResult::from(launchable))
+ }
+
+ /// build the cmd result as an executable which will be called in a process
+ /// launched by broot
+ fn cmd_result_exec_stay_in_broot(
+ &self,
+ w: &mut W,
+ builder: ExecutionStringBuilder<'_>,
+ con: &AppContext,
+ ) -> Result<CmdResult, ProgramError> {
+ match &builder.sel_info {
+ SelInfo::None | SelInfo::One(_) => {
+ // zero or one selection -> only one execution
+ let launchable = Launchable::program(
+ builder.exec_token(&self.exec_pattern),
+ builder.sel_info
+ .as_one_sel()
+ .filter(|_| self.set_working_dir)
+ .map(|sel| path::closest_dir(sel.path)),
+ con,
+ )?;
+ info!("Executing not leaving, launchable {:?}", launchable);
+ if let Err(e) = launchable.execute(Some(w)) {
warn!("launchable failed : {:?}", e);
- Ok(CmdResult::error(e.to_string()))
+ return Ok(CmdResult::error(e.to_string()));
+ }
+ }
+ SelInfo::More(stage) => {
+ // multiselection -> we must execute on all paths
+ let sels = stage.paths.iter()
+ .map(|path| Selection {
+ path,
+ line: 0,
+ stype: SelectionType::from(path),
+ is_exe: false,
+ });
+ for sel in sels {
+ debug!("executing on path {:?}", &sel.path);
+ let launchable = Launchable::program(
+ builder.sel_exec_token(&self.exec_pattern, Some(sel)),
+ if self.set_working_dir {
+ Some(path::closest_dir(sel.path))
+ } else {
+ None
+ },
+ con,
+ )?;
+ if let Err(e) = launchable.execute(Some(w)) {
+ warn!("launchable failed : {:?}", e);
+ return Ok(CmdResult::error(e.to_string()));
+ }
}
}
}
+ Ok(CmdResult::RefreshState { clear_cache: true })
}
}
diff --git a/src/verb/verb.rs b/src/verb/verb.rs
index 5c8c25d..1ec079e 100644
--- a/src/verb/verb.rs
+++ b/src/verb/verb.rs
@@ -1,7 +1,7 @@
use {
super::*,
crate::{
- app::{Selection, SelectionType},
+ app::{Selection, SelInfo, SelectionType},
errors::ConfError,
keys,
path::{self, PathAnchor},
@@ -154,7 +154,32 @@ impl Verb {
/// and return the error to display if arguments don't match.
pub fn check_args(
&self,
- sel: &Option<Selection>,
+ sel_info: &SelInfo<'_>,
+ invocation: &VerbInvocation,
+ other_path: &Option<PathBuf>,
+ ) -> Option<String> {
+ match sel_info {
+ SelInfo::None => self.check_sel_args(None, invocation, other_path),
+ SelInfo::One(sel) => self.check_sel_args(Some(*sel), invocation, other_path),
+ SelInfo::More(stage) => {
+ stage.paths.iter()
+ .filter_map(|path| {
+ let sel = Selection {
+ path,
+ line: 0,
+ stype: SelectionType::from(path),
+ is_exe: false,
+ };
+ self.check_sel_args(Some(sel), invocation, other_path)
+ })
+ .next()
+ }
+ }
+ }
+
+ fn check_sel_args(
+ &self,
+ sel: Option<Selection<'_>>,
invocation: &VerbInvocation,
other_path: &Option<PathBuf>,
) -> Option<String> {
@@ -173,7 +198,7 @@ impl Verb {
pub fn get_status_markdown(
&self,
- sel: Option<Selection<'_>>,
+ sel_info: SelInfo<'_>,
other_path: &Option<PathBuf>,
invocation: &VerbInvocation,
) -> String {
@@ -186,7 +211,7 @@ impl Verb {
// thus I hardcode the test here.
if let VerbExecution::Internal(internal_exec) = &self.execution {
if internal_exec.internal == Internal::focus {
- if let Some(sel) = sel { // this is normally checked prior to this method
+ if let Some(sel) = sel_info.as_one_sel() {
let arg = invocation.args.as_ref().or_else(|| internal_exec.arg.as_ref());
let pb;
let arg_path = if let Some(arg) = arg {
@@ -196,14 +221,17 @@ impl Verb {
sel.path
};
return format!("Hit *enter* to {} `{}`", name, arg_path.to_string_lossy());
+ } else {
+ return "You can't focus without selection".to_string();
}
}
+ // TODO check that before
}
let builder = || {
ExecutionStringBuilder::from_invocation(
&self.invocation_parser,
- sel,
+ sel_info,
other_path,
&invocation.args,
)