diff options
author | Canop <cano.petrole@gmail.com> | 2020-09-22 18:56:47 +0200 |
---|---|---|
committer | Canop <cano.petrole@gmail.com> | 2020-09-22 18:56:47 +0200 |
commit | 60b742fceda175bd1b357280535b27ba5e915d94 (patch) | |
tree | 30c48e578a42db9428b82effaead61be4ce87af1 /src | |
parent | 5edab5fd73ac38df12ff4ce7db21091f6bd8acbb (diff) |
refactor: move the invocation pattern from execution to verb
Diffstat (limited to 'src')
-rw-r--r-- | src/app/selection.rs | 63 | ||||
-rw-r--r-- | src/app/state.rs | 104 | ||||
-rw-r--r-- | src/browser/browser_state.rs | 62 | ||||
-rw-r--r-- | src/cli.rs | 1 | ||||
-rw-r--r-- | src/command/sequence.rs | 2 | ||||
-rw-r--r-- | src/conf/default_conf.rs | 7 | ||||
-rw-r--r-- | src/help/help_state.rs | 1 | ||||
-rw-r--r-- | src/image/image_view.rs | 1 | ||||
-rw-r--r-- | src/preview/preview_state.rs | 1 | ||||
-rw-r--r-- | src/tree/tree_line.rs | 1 | ||||
-rw-r--r-- | src/verb/builtin.rs | 185 | ||||
-rw-r--r-- | src/verb/cd.rs | 10 | ||||
-rw-r--r-- | src/verb/execution_builder.rs | 158 | ||||
-rw-r--r-- | src/verb/external_execution.rs | 258 | ||||
-rw-r--r-- | src/verb/internal.rs | 21 | ||||
-rw-r--r-- | src/verb/internal_execution.rs | 17 | ||||
-rw-r--r-- | src/verb/internal_focus.rs | 4 | ||||
-rw-r--r-- | src/verb/invocation_parser.rs | 126 | ||||
-rw-r--r-- | src/verb/mod.rs | 16 | ||||
-rw-r--r-- | src/verb/verb.rs | 177 | ||||
-rw-r--r-- | src/verb/verb_conf.rs | 36 |
21 files changed, 682 insertions, 569 deletions
diff --git a/src/app/selection.rs b/src/app/selection.rs index 9cbb3f6..4a106f0 100644 --- a/src/app/selection.rs +++ b/src/app/selection.rs @@ -1,21 +1,24 @@ use { - std::path::Path, + super::{ + AppContext, + AppStateCmdResult, + }, + crate::{ + errors::ProgramError, + launchable::Launchable, + }, + std::{ + fs::OpenOptions, + io::Write, + path::Path, + }, }; /// the id of a line, starting at 1 /// (0 if not specified) pub type LineNumber = usize; -/// light information about the currently selected -/// file and maybe line number -#[derive(Debug, Clone, Copy)] -pub struct Selection<'s> { - pub path: &'s Path, - pub line: LineNumber, // the line number in the file (0 if none selected) - pub stype: SelectionType, -} - #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum SelectionType { File, @@ -28,3 +31,43 @@ impl SelectionType { constraint == Self::Any || self == constraint } } + +/// light information about the currently selected +/// file and maybe line number +#[derive(Debug, Clone, Copy)] +pub struct Selection<'s> { + pub path: &'s Path, + pub line: LineNumber, // the line number in the file (0 if none selected) + pub stype: SelectionType, + pub is_exe: bool, +} + +impl Selection<'_> { + + /// build a AppStateCmdResult with a launchable which will be used to + /// 1/ quit broot + /// 2/ open the relevant file the best possible way + pub fn to_opener( + self, + con: &AppContext, + ) -> Result<AppStateCmdResult, ProgramError> { + Ok(if self.is_exe { + let path = self.path.to_string_lossy().to_string(); + if let Some(export_path) = &con.launch_args.cmd_export_path { + // broot was launched as br, we can launch the executable from the shell + let f = OpenOptions::new().append(true).open(export_path)?; + writeln!(&f, "{}", path)?; + AppStateCmdResult::Quit + } else { + AppStateCmdResult::from(Launchable::program( + vec![path], + None, // we don't set the working directory + )?) + } + } else { + AppStateCmdResult::from(Launchable::opener(self.path.to_path_buf())) + }) + } + +} + diff --git a/src/app/state.rs b/src/app/state.rs index e1b30d7..686f8c3 100644 --- a/src/app/state.rs +++ b/src/app/state.rs @@ -13,7 +13,9 @@ use { task_sync::Dam, verb::*, }, - std::path::{Path, PathBuf}, + std::{ + path::{Path, PathBuf}, + }, termimad::Area, }; @@ -63,7 +65,7 @@ pub trait AppState { input_invocation: Option<&VerbInvocation>, trigger_type: TriggerType, cc: &CmdContext, - screen: &mut Screen, + screen: &mut Screen, // TODO remove (seeems to be used only for page_height) ) -> Result<AppStateCmdResult, ProgramError>; /// a generic implementation of on_internal which may be @@ -121,6 +123,7 @@ pub trait AppState { AppStateCmdResult::NewState(Box::new(HelpState::new(screen, con))) } } + Internal::open_leave => self.selection().to_opener(con)?, Internal::open_preview => self.open_preview(None, false, cc), Internal::preview_image => self.open_preview(Some(PreviewMode::Image), false, cc), Internal::preview_text => self.open_preview(Some(PreviewMode::Text), false, cc), @@ -164,6 +167,44 @@ pub trait AppState { }) } + fn execute_verb( + &mut self, + w: &mut W, // needed because we may want to switch from alternate in some externals + verb: &Verb, + invocation: Option<&VerbInvocation>, + trigger_type: TriggerType, + cc: &CmdContext, + screen: &mut Screen, + ) -> Result<AppStateCmdResult, ProgramError> { + match &verb.execution { + VerbExecution::Internal(internal_exec) => self.on_internal( + w, + internal_exec, + invocation, + trigger_type, + cc, + screen, + ), + VerbExecution::External(external) => { + let exec_builder = ExecutionStringBuilder::from_invocation( + &verb.invocation_parser, + self.selection(), + &cc.other_path, + if let Some(inv) = invocation { + &inv.args + } else { + &None + }, + ); + external.to_cmd_result( + w, + exec_builder, + &cc.con, + ) + } + } + } + /// change the state, does no rendering fn on_command( &mut self, @@ -185,30 +226,14 @@ pub trait AppState { Command::VerbTrigger { index, input_invocation, - } => { - let verb = &con.verb_store.verbs[*index]; - match &verb.execution { - VerbExecution::Internal(internal_exec) => self.on_internal( - w, - internal_exec, - input_invocation.as_ref(), - TriggerType::Other, - cc, - screen, - ), - VerbExecution::External(external) => external.to_cmd_result( - w, - self.selection(), - &cc.other_path, - if let Some(inv) = &input_invocation { - &inv.args - } else { - &None - }, - con, - ), - } - } + } => self.execute_verb( + w, + &con.verb_store.verbs[*index], + input_invocation.as_ref(), + TriggerType::Other, + cc, + screen, + ), Command::Internal { internal, input_invocation, @@ -225,25 +250,14 @@ pub trait AppState { if let Some(err) = verb.check_args(invocation, &cc.other_path) { Ok(AppStateCmdResult::DisplayError(err)) } else { - match &verb.execution { - VerbExecution::Internal(internal_exec) => self.on_internal( - w, - internal_exec, - Some(invocation), - TriggerType::Input, - cc, - screen, - ), - VerbExecution::External(external) => { - external.to_cmd_result( - w, - self.selection(), - &cc.other_path, - &invocation.args, - con, - ) - } - } + self.execute_verb( + w, + verb, + Some(invocation), + TriggerType::Input, + cc, + screen, + ) } } _ => Ok(AppStateCmdResult::verb_not_found(&invocation.name)), diff --git a/src/browser/browser_state.rs b/src/browser/browser_state.rs index 9cd833b..591d2d6 100644 --- a/src/browser/browser_state.rs +++ b/src/browser/browser_state.rs @@ -6,7 +6,6 @@ use { errors::{ProgramError, TreeBuildError}, flag::Flag, git, - launchable::Launchable, pattern::*, path, path_anchor::PathAnchor, @@ -19,8 +18,6 @@ use { }, open, std::{ - fs::OpenOptions, - io::Write, path::{Path, PathBuf}, }, termimad::Area, @@ -155,37 +152,6 @@ impl BrowserState { } } - pub fn open_selection_quit_broot( - &mut self, - w: &mut W, - con: &AppContext, - ) -> Result<AppStateCmdResult, ProgramError> { - let tree = self.displayed_tree(); - let line = tree.selected_line(); - match &line.line_type { - TreeLineType::File => make_opener(line.path.clone(), line.is_exe(), con), - TreeLineType::Dir | TreeLineType::SymLinkToDir(_) => { - Ok(if con.launch_args.cmd_export_path.is_some() { - CD.to_cmd_result(w, line.as_selection(), &None, &None, con)? - } else { - AppStateCmdResult::DisplayError( - "This feature needs broot to be launched with the `br` script".to_owned(), - ) - }) - } - TreeLineType::SymLinkToFile(target) => { - make_opener( - PathBuf::from(target), - line.is_exe(), // today this always return false - con, - ) - } - _ => { - unreachable!(); - } - } - } - pub fn go_to_parent( &mut self, screen: &mut Screen, @@ -209,32 +175,6 @@ impl BrowserState { } -/// build a AppStateCmdResult with a launchable which will be used to -/// 1/ quit broot -/// 2/ open the relevant file the best possible way -fn make_opener( - path: PathBuf, - is_exe: bool, - con: &AppContext, -) -> Result<AppStateCmdResult, ProgramError> { - Ok(if is_exe { - let path = path.to_string_lossy().to_string(); - if let Some(export_path) = &con.launch_args.cmd_export_path { - // broot was launched as br, we can launch the executable from the shell - let f = OpenOptions::new().append(true).open(export_path)?; - writeln!(&f, "{}", path)?; - AppStateCmdResult::Quit - } else { - AppStateCmdResult::from(Launchable::program( - vec![path], - None, // we don't set the working directory - )?) - } - } else { - AppStateCmdResult::from(Launchable::opener(path)) - }) -} - impl AppState for BrowserState { fn get_pending_task(&self) -> Option<&'static str> { @@ -249,7 +189,6 @@ impl AppState for BrowserState { } } - fn selected_path(&self) -> &Path { &self.displayed_tree().selected_line().path } @@ -352,7 +291,6 @@ impl AppState for BrowserState { }, Internal::open_stay => self.open_selection_stay_in_broot(screen, con, bang, false)?, Internal::open_stay_filter => self.open_selection_stay_in_broot(screen, con, bang, true)?, - Internal::open_leave => self.open_selection_quit_broot(w, con)?, Internal::line_down => { self.displayed_tree_mut().move_selection(1, page_height); AppStateCmdResult::Keep @@ -27,7 +27,6 @@ use { }, }; - /// launch arguments related to installation /// (not used by the application after the first step) struct InstallLaunchArgs { diff --git a/src/command/sequence.rs b/src/command/sequence.rs index a8d8fe0..7856abb 100644 --- a/src/command/sequence.rs +++ b/src/command/sequence.rs @@ -75,11 +75,9 @@ fn add_commands( let raw_parts = CommandParts::from(input.to_string()); let (pattern, verb_invocation) = raw_parts.split(); if let Some(pattern) = pattern { - debug!("adding pattern: {:?}", pattern); commands.push((input.to_string(), Command::from_parts(pattern, false))); } if let Some(verb_invocation) = verb_invocation { - debug!("adding verb_invocation: {:?}", verb_invocation); let command = Command::from_parts(verb_invocation, true); if let Command::VerbInvocate(invocation) = &command { // we check that the verb exists to avoid running a sequence diff --git a/src/conf/default_conf.rs b/src/conf/default_conf.rs index 1350b83..6375e83 100644 --- a/src/conf/default_conf.rs +++ b/src/conf/default_conf.rs @@ -133,11 +133,18 @@ leave_broot = false # uncomment if you want to launch a terminal on ctrl-T # (on exit you'll be back in broot) # [[verbs]] +# invocation = "terminal" # key = "ctrl-t" # execution = "$SHELL" # set_working_dir = true # leave_broot = false +# Here's an example of a shorctut bringing you to your home directory +# [[verbs]] +# invocation = "home" +# key = "ctrl-home" +# execution = ":focus ~" + # A popular set of shorctuts for going up and down: # # [[verbs]] diff --git a/src/help/help_state.rs b/src/help/help_state.rs index 1a58ec6..aa19572 100644 --- a/src/help/help_state.rs +++ b/src/help/help_state.rs @@ -42,6 +42,7 @@ impl AppState for HelpState { Selection { path: Conf::default_location(), stype: SelectionType::File, + is_exe: false, line: 0, } } diff --git a/src/image/image_view.rs b/src/image/image_view.rs index 30b7c3d..74053f4 100644 --- a/src/image/image_view.rs +++ b/src/image/image_view.rs @@ -68,7 +68,6 @@ impl ImageView { let bg = styles.preview.get_bg() .or_else(|| styles.default.get_bg()) .unwrap_or(Color::AnsiValue(238)); - debug!("true colors: {:?}", con.true_colors); let mut double_line = DoubleLine::new(width as usize, con.true_colors); let mut y = area.top; let margin = area.width as usize - width as usize; diff --git a/src/preview/preview_state.rs b/src/preview/preview_state.rs index dfa3544..86a1468 100644 --- a/src/preview/preview_state.rs +++ b/src/preview/preview_state.rs @@ -155,6 +155,7 @@ impl AppState for PreviewState { Selection { path: &self.path, stype: SelectionType::File, + is_exe: false, // not always true. It means :open_leave won't execute it line: self.preview.get_selected_line_number().unwrap_or(0), } } diff --git a/src/tree/tree_line.rs b/src/tree/tree_line.rs index ab1c76c..5e0ae19 100644 --- a/src/tree/tree_line.rs +++ b/src/tree/tree_line.rs @@ -85,6 +85,7 @@ impl TreeLine { Selection { path: &self.path, stype: self.selection_type(), + is_exe: self.is_exe(), line: 0, } } diff --git a/src/verb/builtin.rs b/src/verb/builtin.rs index 90375ed..0c7092f 100644 --- a/src/verb/builtin.rs +++ b/src/verb/builtin.rs @@ -1,132 +1,185 @@ use { - super::Verb, - crate::keys::*, + super::*, + crate::{ + app::SelectionType, + keys::*, + }, crossterm::event::{KeyCode, KeyEvent, KeyModifiers}, }; +fn build_internal( + internal: Internal, + bang: bool, +) -> Verb { + let invocation = internal.invocation_pattern(); + let execution = VerbExecution::Internal( + InternalExecution::from_internal_bang(internal, bang) + ); + let description = VerbDescription::from_text(internal.description().to_string()); + Verb::new(Some(invocation), execution, description).unwrap() +} + +fn internal( + internal: Internal, +) -> Verb { + build_internal(internal, false) +} + +fn internal_bang( + internal: Internal, +) -> Verb { + build_internal(internal, true) +} + +fn external( + invocation_str: &str, + execution_str: &str, + exec_mode: ExternalExecutionMode, +) -> Verb { + let execution = VerbExecution::External(ExternalExecution::new( + execution_str, + exec_mode, + )); + Verb::new( + Some(invocation_str), + execution, + VerbDescription::from_code(execution_str.to_string()), + ).unwrap() +} + /// declare the built_in verbs, the ones which are available /// in standard (they still may be overriden by configuration) pub fn builtin_verbs() -> Vec<Verb> { use super::{ExternalExecutionMode::*, Internal::*}; vec![ - Verb::internal(back), - Verb::from(super::cd::CD.clone()) - .with_description("change directory and quit (mapped to *alt*-*enter*)"), + internal(back), + + // those two operations are mapped on ALT-ENTER, one + // for directories and the other one for the other files + external( + "cd", + "cd {directory}", + FromParentShell, + ) + .with_stype(SelectionType::Directory) + .with_key(ALT_ENTER) + .with_description("change directory and quit"), + internal(open_leave) // calls the system open + .with_key(ALT_ENTER) + .with_shortcut("ol"), + #[cfg(unix)] - Verb::external( + external( "chmod {args}", "chmod {args} {file}", StayInBroot, - ).unwrap(), - Verb::internal(open_preview), - Verb::internal(close_preview), - Verb::internal(toggle_preview), - Verb::internal(preview_image), - Verb::internal(preview_text), - Verb::internal(preview_binary), - Verb::internal(close_panel_ok), - Verb::internal(close_panel_cancel) + ), + internal(open_preview), + internal(close_preview), + internal(toggle_preview), + internal(preview_image), + internal(preview_text), + internal(preview_binary), + internal(close_panel_ok), + internal(close_panel_cancel) .with_key(BACK_TAB) .with_control_key('w'), - Verb::external( + external( "copy {newpath:path-from-parent}", "/bin/cp -r {file} {newpath:path-from-parent}", StayInBroot, - ).unwrap() + ) .with_shortcut("cp"), #[cfg(feature="clipboard")] - Verb::internal(copy_path) + internal(copy_path) .with_alt_key('c'), - Verb::external( + external( "copy_to_panel", "/bin/cp -r {file} {other-panel-directory}", StayInBroot, - ).unwrap() + ) .with_shortcut("cpp"), // :focus is also hardcoded on Enter on directories // but ctrl-f is useful for focusing on a file's parent // (and keep the filter) - Verb::internal(focus) + internal(focus) .with_control_key('f'), - Verb::internal(help).with_key(F1).with_shortcut("?"), + internal(help).with_key(F1).with_shortcut("?"), #[cfg(feature="clipboard")] - Verb::internal(input_paste) + internal(input_paste) .with_control_key('v'), - Verb::internal(line_down).with_key(DOWN), - Verb::internal(line_up).with_key(UP), - Verb::external( + internal(line_down).with_key(DOWN), + internal(line_up).with_key(UP), + external( "mkdir {subpath}", "/bin/mkdir -p {subpath:path-from-directory}", StayInBroot, - ).unwrap() + ) .with_shortcut("md"), - Verb::external( + external( "move {newpath:path-from-parent}", "/bin/mv {file} {newpath:path-from-parent}", StayInBroot, - ).unwrap() + ) .with_shortcut("mv"), - Verb::external( + external( "move_to_panel", "/bin/mv {file} {other-panel-directory}", StayInBroot, - ).unwrap() + ) .with_shortcut("mvp"), - Verb::internal_bang(start_end_panel) + internal_bang(start_end_panel) .with_control_key('p'), - Verb::internal(next_match) + internal(next_match) .with_key(TAB), - Verb::internal(no_sort) + internal(no_sort) .with_shortcut("ns"), - Verb::internal(open_stay) + internal(open_stay) .with_key(ENTER) .with_shortcut("os"), - Verb::internal(open_stay_filter) + internal(open_stay_filter) .with_shortcut("osf"), - Verb::internal(open_leave) - .with_key(ALT_ENTER) - .with_shortcut("ol"), - Verb::internal(parent).with_shortcut("p"), - Verb::internal(page_down).with_key(PAGE_DOWN), - Verb::internal(page_up).with_key(PAGE_UP), - Verb::internal(panel_left) + internal(parent).with_shortcut("p"), + internal(page_down).with_key(PAGE_DOWN), + internal(page_up).with_key(PAGE_UP), + internal(panel_left) .with_key(KeyEvent { code: KeyCode::Left, modifiers: KeyModifiers::CONTROL, }), - Verb::internal(panel_right) + internal(panel_right) .with_key(KeyEvent { code: KeyCode::Right, modifiers: KeyModifiers::CONTROL, }), - Verb::internal(print_path).with_shortcut("pp"), - Verb::internal(print_relative_path).with_shortcut("prp"), - Verb::internal(print_tree).with_shortcut("pt"), - Verb::internal(quit) + internal(print_path).with_shortcut("pp"), + internal(print_relative_path).with_shortcut("prp"), + internal(print_tree).with_shortcut("pt"), + internal(quit) .with_control_key('c') .with_control_key('q') .with_shortcut("q"), - Verb::internal(refresh).with_key(F5), - Verb::internal(sort_by_count).with_shortcut("sc"), - Verb::internal(sort_by_date).with_shortcut("sd"), - Verb::internal(sort_by_size).with_shortcut("ss"), - Verb::external( + internal(refresh).with_key(F5), + internal(sort_by_count).with_shortcut("sc"), + internal(sort_by_date).with_shortcut("sd"), + internal(sort_by_size).with_shortcut("ss"), + external( "rm", "/bin/rm -rf {file}", StayInBroot, - ).unwrap(), - Verb::internal(toggle_counts).with_shortcut("counts"), - Verb::internal(toggle_dates).with_shortcut("dates"), - Verb::internal(toggle_files).with_shortcut("files"), - Verb::internal(toggle_git_ignore).with_shortcut("gi"), - Verb::internal(toggle_git_file_info).with_shortcut("gf"), - Verb::internal(toggle_git_status).with_shortcut("gs"), - Verb::internal(toggle_hidden).with_shortcut("h"), + ), + internal(toggle_counts).with_shortcut("counts"), + internal(toggle_dates).with_shortcut("dates"), + internal(toggle_files).with_shortcut("files"), + internal(toggle_git_ignore).with_shortcut("gi"), + internal(toggle_git_file_info).with_shortcut("gf"), + internal(toggle_git_status).with_shortcut("gs"), + internal(toggle_hidden).with_shortcut("h"), #[cfg(unix)] - Verb::internal(toggle_perm).with_shortcut("perm"), - Verb::internal(toggle_sizes).with_shortcut("sizes"), - Verb::internal(toggle_trim_root), - Verb::internal(total_search).with_control_key('s'), - Verb::internal(up_tree).with_shortcut("up"), + internal(toggle_perm).with_shortcut("perm"), + internal(toggle_sizes).with_shortcut("sizes"), + internal(toggle_trim_root), + internal(total_search).with_control_key('s'), + internal(up_tree).with_shortcut("up"), ] } diff --git a/src/verb/cd.rs b/src/verb/cd.rs deleted file mode 100644 index ab08dbd..0000000 --- a/src/verb/cd.rs +++ /dev/null @@ -1,10 +0,0 @@ -use super::{ExternalExecution, ExternalExecutionMode}; - -lazy_static! { - pub static ref CD: ExternalExecution = ExternalExecution::new( - "cd", - "cd {directory}", - ExternalExecutionMode::FromParentShell, - ) - .unwrap(); -} diff --git a/src/verb/execution_builder.rs b/src/verb/execution_builder.rs new file mode 100644 index 0000000..c1882bd --- /dev/null +++ b/src/verb/execution_builder.rs @@ -0,0 +1,158 @@ + +use { + super::{ + GROUP, + InvocationParser, + }, + crate::{ + app::{Selection}, + path, + }, + regex::{Captures}, + std::{ + collections::HashMap, + path::{Path, PathBuf}, + }, +}; + +/// a temporary structure gathering selection and invocation +/// parameters and able to generate an executable string from +/// a verb's execution pattern +pub struct ExecutionStringBuilder<'b> { + /// the current file selection + pub sel: Selection<'b>, + + /// the selection in the other panel, when there exactly two + other_file: Option<&'b PathBuf>, + + /// parsed arguments + invocation_values: Option<HashMap<String, String>>, +} + +impl<'b> ExecutionStringBuilder<'b> { + pub fn from_selection( + sel: Selection<'b>, + ) -> Self { + Self { + sel, + other_file: None, + invocation_values: None, + } + } + pub fn from_invocation( + invocation_parser: &Option<InvocationParser>, + sel: Selection<'b>, + other_file: &'b Option<PathBuf>, + invocation_args: & Option<String>, + ) -> Self { + let invocation_values = invocation_parser.as_ref().zip(invocation_args.as_ref()) + .and_then(|(parser, args)| parser.parse(args)); + Self { + sel, + other_file: other_file.as_ref(), + invocation_values, + } + } + fn get_file(&self) -> &Path { + &self.sel.path + } + fn get_directory(&self) -> PathBuf { + path::closest_dir(self.sel.path) + } + fn get_parent(&self) -> &Path { + let file = &self.sel.path; + file.parent().unwrap_or(file) + } + fn path_to_string(&self, path: &Path, escape: bool) -> String { + if escape { + path::escape_for_shell(path) + } else { + path.to_string_lossy().to_string() + } + } + fn get_raw_capture_replacement(&self, ec: &Captures<'_>, escape: bool) -> Option<String> { + let name = ec.get(1).unwrap().as_str(); + match name { + "line" => Some(self.sel.line.to_string()), + "file" => Some(self.path_to_string(self.get_file(), escape)), + "directory" => Some(self.path_to_string(&self.get_directory(), escape)), + "parent" => Some(self.path_to_string(self.get_parent(), escape)), + "other-panel-file" => self.other_file.map(|p| self.path_to_string(p, escape)), + "other-panel-directory" => { + self.other_file + .map(|p| path::closest_dir(p)) + .as_ref() + .map(|p| self.path_to_string(p, escape)) + } + "other-panel-parent" => { + self.other_file + .and_then(|p| p.parent()) + .map(|p| self.path_to_string(p, escape)) + } + _ => { + // it's not one of the standard group names, so we'll look + // into the ones provided by the invocation pattern + self.invocation_values.as_ref() + .and_then(|map| map.get(name) + .map(|value| { + if let Some(fmt) = ec.get(2) { + match fmt.as_str() { + "path-from-directory" => path::path_str_from(self.get_directory(), value), + "path-from-parent" => path::path_str_from(self.get_parent(), value), + _ => format!("invalid format: {:?}", fmt.as_str()), + } + } else { + value.to_string() + } + }) + ) + } + } + } + fn get_capture_replacement(&self, ec: &Captures<'_>, escape: bool) -> String { + self.get_raw_capture_replacement(ec, escape) + .unwrap_or_else(|| ec[0].to_string()) + } + /// build a shell compatible command, with escapings + pub fn shell_exec_string( + &self, + exec_pattern: &str, + ) -> String { + GROUP + .replace_all( + |