diff options
author | Canop <cano.petrole@gmail.com> | 2021-04-19 20:59:15 +0200 |
---|---|---|
committer | Canop <cano.petrole@gmail.com> | 2021-04-19 20:59:15 +0200 |
commit | 6da21634bf925b9e3bce497b5d8e6218786f2628 (patch) | |
tree | 00a1d150a8f8847933266791b86672a050481818 /src/verb | |
parent | aa1dc4a944ad1f11cf8292c49cb771dd1384c6c7 (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.rs | 111 | ||||
-rw-r--r-- | src/verb/external_execution.rs | 116 | ||||
-rw-r--r-- | src/verb/verb.rs | 38 |
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, ) |