diff --git a/CHANGELOG.md b/CHANGELOG.md index 86b6b895..5f4bba04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ ### next +- allow rebinding of the 'tab' and 'esc' keys with the `:next_match` and `:escape` internals - Fix #740 - fix fuzzy patterns not case insensitive on some characters - Fix #746 - when given a path to a file at launch, broot now selects it in the tree and opens it in preview - Fix #729 diff --git a/src/app/app.rs b/src/app/app.rs index d163e755..9c326d62 100644 --- a/src/app/app.rs +++ b/src/app/app.rs @@ -365,6 +365,12 @@ impl App { HandleInApp(internal) => { debug!("handling internal {internal:?} at app level"); match internal { + Internal::escape => { + let mode = self.panel().state().get_mode(); + let cmd = self.mut_panel().input.escape(con, mode); + debug!("cmd on escape: {cmd:?}"); + self.apply_command(w, cmd, panel_skin, app_state, con)?; + } Internal::panel_left_no_open | Internal::panel_right_no_open => { let new_active_panel_idx = if internal == Internal::panel_left_no_open { // we're here because the state wants us to either move to the panel diff --git a/src/app/panel.rs b/src/app/panel.rs index 688a9381..0c11b791 100644 --- a/src/app/panel.rs +++ b/src/app/panel.rs @@ -30,7 +30,7 @@ pub struct Panel { pub areas: Areas, status: Status, pub purpose: PanelPurpose, - input: PanelInput, + pub input: PanelInput, } impl Panel { @@ -251,7 +251,7 @@ impl Panel { if let Some(area) = &self.areas.purpose { let shortcut = con .verb_store - .verbs + .verbs() .iter() .filter(|v| match &v.execution { VerbExecution::Internal(exec) => exec.internal == Internal::start_end_panel, diff --git a/src/app/panel_state.rs b/src/app/panel_state.rs index 17a05754..ec3e5304 100644 --- a/src/app/panel_state.rs +++ b/src/app/panel_state.rs @@ -490,6 +490,9 @@ pub trait PanelState { CmdResult::Keep } } + Internal::escape => { + CmdResult::HandleInApp(Internal::escape) + } Internal::panel_left | Internal::panel_left_no_open => { CmdResult::HandleInApp(Internal::panel_left_no_open) } @@ -749,11 +752,11 @@ pub trait PanelState { } } Command::VerbTrigger { - index, + verb_id, input_invocation, } => self.execute_verb( w, - &con.verb_store.verbs[*index], + con.verb_store.verb(*verb_id), input_invocation.as_ref(), TriggerType::Other, app_state, diff --git a/src/command/command.rs b/src/command/command.rs index 3785ef67..439c17ea 100644 --- a/src/command/command.rs +++ b/src/command/command.rs @@ -2,7 +2,7 @@ use { super::*, crate::{ pattern::*, - verb::{Internal, VerbInvocation}, + verb::{Internal, VerbInvocation, VerbId}, }, bet::BeTree, }; @@ -35,7 +35,7 @@ pub enum Command { /// call of a verb done without the input /// (using a trigger key for example) VerbTrigger { - index: usize, + verb_id: VerbId, input_invocation: Option, }, diff --git a/src/command/panel_input.rs b/src/command/panel_input.rs index 6aac4b35..5fa3b25e 100644 --- a/src/command/panel_input.rs +++ b/src/command/panel_input.rs @@ -78,14 +78,22 @@ impl PanelInput { pub fn on_event( &mut self, w: &mut W, - event: TimedEvent, + timed_event: TimedEvent, con: &AppContext, sel_info: SelInfo<'_>, app_state: &AppState, mode: Mode, panel_state_type: PanelStateType, ) -> Result { - let cmd = self.get_command(event, con, sel_info, app_state, mode, panel_state_type); + let cmd = match timed_event.event { + Event::Mouse(MouseEvent { kind, column, row, modifiers: KeyModifiers::NONE }) => { + self.on_mouse(timed_event, kind, column, row) + } + Event::Key(key) => { + self.on_key(timed_event, key, con, sel_info, app_state, mode, panel_state_type) + } + _ => Command::None, + }; self.input_field.display_on(w)?; Ok(cmd) } @@ -181,13 +189,15 @@ impl PanelInput { /// escape (bound to the 'esc' key) /// - /// Can't be done as a standard internal for the moment because of - /// the necessary position before the 'tab' if/else handling - fn escape( + /// This function is better called from the on_key method of + /// panel input, when a key triggers it, because then it + /// can also properly deal with completion sequence. + /// When ':escape' is called from a verb's cmd sequence, then + /// it's not called on on_key but by the app. + pub fn escape( &mut self, con: &AppContext, mode: Mode, - parts: CommandParts, ) -> Command { self.tab_cycle_count = 0; if let Some(raw) = self.input_before_cycle.take() { @@ -203,6 +213,8 @@ impl PanelInput { } } else { // general back command + let raw = self.input_field.get_content(); + let parts = CommandParts::from(raw.clone()); self.input_field.clear(); let internal = Internal::back; Command::Internal { @@ -268,8 +280,8 @@ impl PanelInput { con: &'c AppContext, sel_info: SelInfo<'_>, panel_state_type: PanelStateType, - ) -> Option<(usize, &'c Verb)> { - for (index, verb) in con.verb_store.verbs.iter().enumerate() { + ) -> Option<&'c Verb> { + for verb in con.verb_store.verbs().iter() { // note that there can be several verbs with the same key and // not all of them can apply if !verb.keys.contains(&key) { @@ -288,135 +300,149 @@ impl PanelInput { } } debug!("verb for key: {}", &verb.execution); - return Some((index, verb)); + return Some(verb); } None } - /// Consume the event to - /// - maybe change the input - /// - build a command - fn get_command( + /// Consume the event, maybe change the input, return a command + fn on_mouse( + &mut self, + timed_event: TimedEvent, + kind: MouseEventKind, + column: u16, + row: u16, + ) -> Command { + if self.input_field.apply_timed_event(timed_event) { + Command::empty() + } else { + match kind { + MouseEventKind::Up(MouseButton::Left) => { + if timed_event.double_click { + Command::DoubleClick(column, row) + } else { + Command::Click(column, row) + } + } + MouseEventKind::ScrollDown => { + Command::Internal { + internal: Internal::line_down, + input_invocation: None, + } + } + MouseEventKind::ScrollUp => { + Command::Internal { + internal: Internal::line_up, + input_invocation: None, + } + } + _ => Command::None, + } + } + } + + /// Consume the event, maybe change the input, return a command + #[allow(clippy::too_many_arguments)] + fn on_key( &mut self, timed_event: TimedEvent, + key: KeyEvent, con: &AppContext, sel_info: SelInfo<'_>, app_state: &AppState, mode: Mode, panel_state_type: PanelStateType, ) -> Command { - match timed_event.event { - Event::Mouse(MouseEvent { kind, column, row, modifiers: KeyModifiers::NONE }) => { - if self.input_field.apply_timed_event(timed_event) { - Command::empty() - } else { - match kind { - MouseEventKind::Up(MouseButton::Left) => { - if timed_event.double_click { - Command::DoubleClick(column, row) - } else { - Command::Click(column, row) - } - } - MouseEventKind::ScrollDown => { - Command::Internal { - internal: Internal::line_down, - input_invocation: None, - } - } - MouseEventKind::ScrollUp => { - Command::Internal { - internal: Internal::line_up, - input_invocation: None, - } - } - _ => Command::None, - } - } - } - Event::Key(key) => { - // value of raw and parts before any key related change - let raw = self.input_field.get_content(); - let mut parts = CommandParts::from(raw.clone()); + // value of raw and parts before any key related change + let raw = self.input_field.get_content(); + let parts = CommandParts::from(raw.clone()); - // We first handle the cases that MUST absolutely - // not be overridden by configuration. - // Those handlings must also be done in order: escaping - // a cycling must be done before the 'tab' handling (and - // its else which resets the cycle) + let verb = if keys::is_key_allowed_for_verb(key, mode, raw.is_empty()) { + self.find_key_verb( + key, + con, + sel_info, + panel_state_type, + ) + } else { + None + }; - if key == key!(esc) { - return self.escape(con, mode, parts); - } + // WARNINGS: + // - beware the execution order below: we must execute + // escape before the else clause of next_match, and we must + // be sure this else clause (which ends cycling) is always + // executed when neither next_match or escape is triggered + // - some behaviors can't really be handled as normally + // triggered internals because of the interactions with + // the input - // tab completion - if key == key!(tab) { - if parts.verb_invocation.is_some() { - return self.auto_complete_verb(con, sel_info, raw, parts); - } - // if no verb is being edited, other bindings may apply (eg :next_match) - } else { - self.tab_cycle_count = 0; - self.input_before_cycle = None; - } + // usually 'esc' key + if Verb::is_some_internal(verb, Internal::escape) { + return self.escape(con, mode); + } - if key == key!(enter) && parts.has_not_empty_verb_invocation() { - return Command::from_parts(parts, true); - } + // 'tab' completion of a verb or one of its arguments + if Verb::is_some_internal(verb, Internal::next_match) { + if parts.verb_invocation.is_some() { + return self.auto_complete_verb(con, sel_info, raw, parts); + } + // if no verb is being edited, the state may handle this internal + // in a specific way + } else { + self.tab_cycle_count = 0; + self.input_before_cycle = None; + } - if (key == key!('?') || key == key!(shift-'?')) - && (raw.is_empty() || parts.verb_invocation.is_some()) { - // a '?' opens the help when it's the first char - // or when it's part of the verb invocation - return Command::Internal { - internal: Internal::help, - input_invocation: parts.verb_invocation, - }; - } + // 'enter': trigger the verb if any on the input. If none, then may be + // used as trigger of another verb + if key == key!(enter) && parts.has_not_empty_verb_invocation() { + return Command::from_parts(parts, true); + } - // we now check if the key is the trigger key of one of the verbs - if keys::is_key_allowed_for_verb(key, mode, raw.is_empty()) { - if let Some((index, verb)) = self.find_key_verb( - key, - con, - sel_info, - panel_state_type, - ) { - if self.handle_input_related_verb(verb, con) { - return Command::from_raw(self.input_field.get_content(), false); - } - if mode != Mode::Input && verb.is_internal(Internal::mode_input) { - self.enter_input_mode_with_key(key, &parts); - } - if verb.auto_exec { - return Command::VerbTrigger { - index, - input_invocation: parts.verb_invocation, - }; - } - if let Some(invocation_parser) = &verb.invocation_parser { - let exec_builder = ExecutionStringBuilder::without_invocation( - sel_info, - app_state, - ); - let verb_invocation = exec_builder.invocation_with_default( - &invocation_parser.invocation_pattern - ); - parts.verb_invocation = Some(verb_invocation); - self.set_content(&parts.to_string()); - return Command::VerbEdit(parts.verb_invocation.unwrap()); - } - } - } + // a '?' opens the help when it's the first char or when it's part + // of the verb invocation. It may be used as a verb name in other cases + if (key == key!('?') || key == key!(shift-'?')) + && (raw.is_empty() || parts.verb_invocation.is_some()) { + return Command::Internal { + internal: Internal::help, + input_invocation: parts.verb_invocation, + }; + } - // input field management - if mode == Mode::Input && self.input_field.apply_timed_event(timed_event) { - return Command::from_raw(self.input_field.get_content(), false); - } - Command::None + if let Some(verb) = verb { + if self.handle_input_related_verb(verb, con) { + return Command::from_raw(self.input_field.get_content(), false); } - _ => Command::None, + if mode != Mode::Input && verb.is_internal(Internal::mode_input) { + self.enter_input_mode_with_key(key, &parts); + } + if verb.auto_exec { + return Command::VerbTrigger { + verb_id: verb.id, + input_invocation: parts.verb_invocation, + }; + } + if let Some(invocation_parser) = &verb.invocation_parser { + let exec_builder = ExecutionStringBuilder::without_invocation( + sel_info, + app_state, + ); + let verb_invocation = exec_builder.invocation_with_default( + &invocation_parser.invocation_pattern + ); + let mut parts = parts; + parts.verb_invocation = Some(verb_invocation); + self.set_content(&parts.to_string()); + return Command::VerbEdit(parts.verb_invocation.unwrap()); + } + } + + // input field management + if mode == Mode::Input && self.input_field.apply_timed_event(timed_event) { + return Command::from_raw(self.input_field.get_content(), false); } + Command::None } } diff --git a/src/conf/verb_conf.rs b/src/conf/verb_conf.rs index 1b214720..33787d57 100644 --- a/src/conf/verb_conf.rs +++ b/src/conf/verb_conf.rs @@ -2,11 +2,7 @@ use { crate::{ app::{ PanelStateType, - SelectionType, }, - command::Sequence, - errors::ConfError, - keys, verb::*, }, serde::Deserialize, @@ -16,171 +12,45 @@ use { #[derive(Default, Debug, Clone, Deserialize)] pub struct VerbConf { - invocation: Option, + pub invocation: Option, - internal: Option, + pub internal: Option, - external: Option, + pub external: Option, - execution: Option, + pub execution: Option, - cmd: Option, + pub cmd: Option, - cmd_separator: Option, + pub cmd_separator: Option, - key: Option, + pub key: Option, #[serde(default)] - keys: Vec, + pub keys: Vec, #[serde(default)] - extensions: Vec, + pub extensions: Vec, - shortcut: Option, + pub shortcut: Option, - leave_broot: Option, + pub leave_broot: Option, - from_shell: Option, + pub from_shell: Option, - apply_to: Option, + pub apply_to: Option, - set_working_dir: Option, + pub set_working_dir: Option, - working_dir: Option, + pub working_dir: Option, - description: Option, + pub description: Option, - auto_exec: Option, + pub auto_exec: Option, - switch_terminal: Option, + pub switch_terminal: Option, #[serde(default)] - panels: Vec, -} - -/// read a deserialized verb conf item into a verb, -/// checking a few basic things in the process -impl VerbConf { - /// the verb_store is provided to allow a verb to be built from other ones - /// already defined - pub fn make_verb(&self, previous_verbs: &[Verb]) -> Result { - let vc = self; - if vc.leave_broot == Some(false) && vc.from_shell == Some(true) { - return Err(ConfError::InvalidVerbConf { - details: "You can't simultaneously have leave_broot=false and from_shell=true".to_string(), - }); - } - let invocation = vc.invocation.clone().filter(|i| !i.is_empty()); - let internal = vc.internal.as_ref().filter(|i| !i.is_empty()); - let external = vc.external.as_ref().filter(|i| !i.is_empty()); - let cmd = vc.cmd.as_ref().filter(|i| !i.is_empty()); - let cmd_separator = vc.cmd_separator.as_ref().filter(|i| !i.is_empty()); - let execution = vc.execution.as_ref().filter(|i| !i.is_empty()); - let make_external_execution = |s| { - let working_dir = match (vc.set_working_dir, &vc.working_dir) { - (Some(false), _) => None, - (_, Some(s)) => Some(s.clone()), - (Some(true), None) => Some("{directory}".to_owned()), - (None, None) => None, - }; - let mut external_execution = ExternalExecution::new( - s, - ExternalExecutionMode::from_conf(vc.from_shell, vc.leave_broot), - ) - .with_working_dir(working_dir); - if let Some(b) = self.switch_terminal { - external_execution.switch_terminal = b; - } - external_execution - }; - let execution = match (execution, internal, external, cmd) { - // old definition with "execution": we guess whether it's an internal or - // an external - (Some(ep), None, None, None) => { - if let Some(internal_pattern) = ep.as_internal_pattern() { - if let Some(previous_verb) = previous_verbs.iter().find(|&v| v.has_name(internal_pattern)) { - previous_verb.execution.clone() - } else { - VerbExecution::Internal(InternalExecution::try_from(internal_pattern)?) - } - } else { - VerbExecution::External(make_external_execution(ep.clone())) - } - } - // "internal": the leading `:` or ` ` is optional - (None, Some(s), None, None) => { - VerbExecution::Internal(if s.starts_with(':') || s.starts_with(' ') { - InternalExecution::try_from(&s[1..])? - } else { - InternalExecution::try_from(s)? - }) - } - // "external": it can be about any form - (None, None, Some(ep), None) => { - VerbExecution::External(make_external_execution(ep.clone())) - } - // "cmd": it's a sequence - (None, None, None, Some(s)) => VerbExecution::Sequence(SequenceExecution { - sequence: Sequence::new(s, cmd_separator), - }), - _ => { - return Err(ConfError::InvalidVerbConf { - details: "You must define either internal, external or cmd".to_string(), - }); - } - }; - let description = vc - .description - .clone() - .map(VerbDescription::from_text) - .unwrap_or_else(|| VerbDescription::from_code(execution.to_string())); - let mut verb = Verb::new( - invocation.as_deref(), - execution, - description, - )?; - // we accept both key and keys. We merge both here - let mut unchecked_keys = vc.keys.clone(); - if let Some(key) = &vc.key { - unchecked_keys.push(key.clone()); - } - let mut checked_keys = Vec::new(); - for key in &unchecked_keys { - let key = crokey::parse(key)?; - if keys::is_reserved(key) { - return Err(ConfError::ReservedKey { - key: keys::KEY_FORMAT.to_string(key) - }); - } - checked_keys.push(key); - } - for extension in &self.extensions { - verb.file_extensions.push(extension.clone()); - } - if !checked_keys.is_empty() { - verb.add_keys(checked_keys); - } - if let Some(shortcut) = &vc.shortcut { - verb.names.push(shortcut.clone()); - } - if vc.auto_exec == Some(false) { - verb.auto_exec = false; - } - if !vc.panels.is_empty() { - verb.panels = vc.panels.clone(); - } - verb.selection_condition = match vc.apply_to.as_deref() { - Some("file") => SelectionType::File, - Some("directory") => SelectionType::Directory, - Some("any") => SelectionType::Any, - None => SelectionType::Any, - Some(s) => { - return Err(ConfError::InvalidVerbConf { - details: format!("{s:?} isn't a valid value of apply_to"), - }); - } - }; - Ok(verb) - } + pub panels: Vec, } diff --git a/src/help/help_verbs.rs b/src/help/help_verbs.rs index 2fe9b7a0..f0d3e78f 100644 --- a/src/help/help_verbs.rs +++ b/src/help/help_verbs.rs @@ -46,7 +46,7 @@ pub fn matching_verb_rows<'v>( con: &'v AppContext, ) -> Vec> { let mut rows = Vec::new(); - for verb in &con.verb_store.verbs { + for verb in con.verb_store.verbs().iter() { if !verb.show_in_doc { continue; } diff --git a/src/verb/builtin.rs b/src/verb/builtin.rs deleted file mode 100644 index d45c6c9f..00000000 --- a/src/verb/builtin.rs +++ /dev/null @@ -1,307 +0,0 @@ -use { - super::*, - crate::{ - app::SelectionType, - }, - crokey::*, -}; - -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(ExecPattern::from_string(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 overridden by configuration) -pub fn builtin_verbs() -> Vec { - use super::{ExternalExecutionMode::*, Internal::*}; - vec![ - internal(back), - - // input actions, not visible in doc, but available for - // example in remote control - internal(input_clear).no_doc(), - internal(input_del_char_left).no_doc(), - internal(input_del_char_below).no_doc(), - internal(input_del_word_left).no_doc(), - internal(input_del_word_right).no_doc(), - internal(input_go_to_end).with_key(key!(end)).no_doc(), - internal(input_go_left).no_doc(), - internal(input_go_right).no_doc(), - internal(input_go_to_start).with_key(key!(home)).no_doc(), - internal(input_go_word_left).no_doc(), - internal(input_go_word_right).no_doc(), - - // arrow keys bindings - internal(back).with_key(key!(left)), - internal(open_stay).with_key(key!(right)), - internal(line_down).with_key(key!(down)).with_key(key!('j')), - internal(line_up).with_key(key!(up)).with_key(key!('k')), - - // - internal(set_syntax_theme), - - // those two operations are mapped on ALT-ENTER, one - // for directories and the other one for the other files - internal(open_leave) // calls the system open - .with_stype(SelectionType::File) - .with_key(key!(alt-enter)) - .with_shortcut("ol"), - external("cd", "cd {directory}", FromParentShell) - .with_stype(SelectionType::Directory) - .with_key(key!(alt-enter)) - .with_shortcut("ol") - .with_description("change directory and quit"), - - #[cfg(unix)] - external("chmod {args}", "chmod {args} {file}", StayInBroot) - .with_stype(SelectionType::File), - #[cfg(unix)] - external("chmod {args}", "chmod -R {args} {file}", StayInBroot) - .with_stype(SelectionType::Directory), - internal(open_preview), - internal(close_preview), - internal(toggle_preview), - internal(preview_image) - .with_shortcut("img"), - internal(preview_text) - .with_shortcut("txt"), - internal(preview_binary) - .with_shortcut("hex"), - internal(close_panel_ok), - internal(close_panel_cancel) - .with_key(key!(ctrl-w)), - #[cfg(unix)] - external( - "copy {newpath}", - "cp -r {file} {newpath:path-from-parent}", - StayInBroot, - ) - .with_shortcut("cp"), - #[cfg(windows)] - external( - "copy {newpath}", - "xcopy /Q /H /Y /I {file} {newpath:path-from-parent}", - StayInBroot, - ) - .with_shortcut("cp"), - #[cfg(feature = "clipboard")] - internal(copy_line) - .with_key(key!(alt-c)), - #[cfg(feature = "clipboard")] - internal(copy_path), - #[cfg(unix)] - external( - "copy_to_panel", - "cp -r {file} {other-panel-directory}", - StayInBroot, - ) - .with_shortcut("cpp"), - #[cfg(windows)] - external( - "copy_to_panel", - "xcopy /Q /H /Y /I {file} {other-panel-directory}", - StayInBroot, - ) - .with_shortcut("cpp"), - #[cfg(unix)] - internal(filesystems) - .with_shortcut("fs"), - // :focus is also hardcoded on Enter on directories - // but ctrl-f is useful for focusing on a file's parent - // (and keep the filter) - internal(focus) - .with_key(key!(L)) // hum... why this one ? - .with_key(key!(ctrl-f)), - internal(help) - .with_key(key!(F1)) - .with_shortcut("?"), - #[cfg(feature="clipboard")] - internal(input_paste) - .with_key(key!(ctrl-v)), - #[cfg(unix)] - external( - "mkdir {subpath}", - "mkdir -p {subpath:path-from-directory}", - StayInBroot, - ) - .with_shortcut("md"), - #[cfg(windows)] - external( - "mkdir {subpath}", - "cmd /c mkdir {subpath:path-from-directory}", - StayInBroot, - ) - .with_shortcut("md"), - #[cfg(unix)] - external( - "move {newpath}", - "mv {file} {newpath:path-from-parent}", - StayInBroot, - ) - .with_shortcut("mv"), - #[cfg(windows)] - external( - "move {newpath}", - "cmd /c move /Y {file} {newpath:path-from-parent}", - StayInBroot, - ) - .with_shortcut("mv"), - #[cfg(unix)] - external( - "move_to_panel", - "mv {file} {other-panel-directory}", - StayInBroot, - ) - .with_shortcut("mvp"), - #[cfg(windows)] - external( - "move_to_panel", - "cmd /c move /Y {file} {other-panel-directory}", - StayInBroot, - ) - .with_shortcut("mvp"), - #[cfg(unix)] - external( - "rename {new_filename:file-name}", - "mv {file} {parent}/{new_filename}", - StayInBroot, - ) - .with_auto_exec(false) - .with_key(key!(f2)), - #[cfg(windows)] - external( - "rename {new_filename:file-name}", - "cmd /c move /Y {file} {parent}/{new_filename}", - StayInBroot, - ) - .with_auto_exec(false) - .with_key(key!(f2)), - internal_bang(start_end_panel) - .with_key(key!(ctrl-p)), - // the char keys for mode_input are handled differently as they're not - // consumed by the command - internal(mode_input) - .with_key(key!(' ')) - .with_key(key!(':')) - .with_key(key!('/')), - internal(previous_match) - .with_key(key!(shift-backtab)) - .with_key(key!(backtab)), - internal(next_match) - .with_key(key!(tab)), - internal(no_sort) - .with_shortcut("ns"), - internal(open_stay) - .with_key(key!(enter)) - .with_shortcut("os"), - internal(open_stay_filter) - .with_shortcut("osf"), - internal(parent) - .with_key(key!(h)) - .with_shortcut("p"), - internal(page_down) - .with_key(key!(ctrl-d)) - .with_key(key!(pagedown)), - internal(page_up) - .with_key(key!(ctrl-u)) - .with_key(key!(pageup)), - internal(panel_left_no_open) - .with_key(key!(ctrl-left)), - internal(panel_right) - .with_key(key!(ctrl-right)), - internal(print_path).with_shortcut("pp"), - internal(print_relative_path).with_shortcut("prp"), - internal(print_tree).with_shortcut("pt"), - internal(quit) - .with_key(key!(ctrl-c)) - .with_key(key!(ctrl-q)) - .with_shortcut("q"), - internal(refresh).with_key(key!(f5)), - internal(root_up) - .with_key(key!(ctrl-up)), - internal(root_down) - .with_key(key!(ctrl-down)), - internal(select_first), - internal(select_last), - internal(select), - internal(clear_stage).with_shortcut("cls"), - internal(stage) - .with_key(key!('+')), - internal(unstage) - .with_key(key!('-')), - internal(stage_all_files) - .with_key(key!(ctrl-a)), - internal(toggle_stage) - .with_key(key!(ctrl-g)), - internal(open_staging_area).with_shortcut("osa"), - internal(close_staging_area).with_shortcut("csa"), - internal(toggle_staging_area).with_shortcut("tsa"), - internal(sort_by_count).with_shortcut("sc"), - internal(sort_by_date).with_shortcut("sd"), - internal(sort_by_size).with_shortcut("ss"), - internal(sort_by_type).with_shortcut("st"), - #[cfg(unix)] - external("rm", "rm -rf {file}", StayInBroot), - #[cfg(windows)] - external("rm", "cmd /c rmdir /Q /S {file}", StayInBroot) - .with_stype(SelectionType::Directory), - #[cfg(windows)] - external("rm", "cmd /c del /Q {file}", StayInBroot) - .with_stype(SelectionType::File), - internal(toggle_counts).with_shortcut("counts"), - internal(toggle_dates).with_shortcut("dates"), - internal(toggle_device_id).with_shortcut("dev"), - internal(toggle_files).with_shortcut("files"), - internal(toggle_git_ignore) - .with_key(key!(alt-i)) - .with_shortcut("gi"), - internal(toggle_git_file_info).with_shortcut("gf"), - internal(toggle_git_status).with_shortcut("gs"), - internal(toggle_root_fs).with_shortcut("rfs"), - internal(toggle_hidden) - .with_key(key!(alt-h)) - .with_shortcut("h"), - #[cfg(unix)] - internal(toggle_perm).with_shortcut("perm"), - internal(toggle_sizes).with_shortcut("sizes"), - internal(toggle_trim_root), - internal(total_search).with_key(key!(ctrl-s)), - internal(up_tree).with_shortcut("up"), - - ] -} diff --git a/src/verb/internal.rs b/src/verb/internal.rs index 21214e14..33f905d1 100644 --- a/src/verb/internal.rs +++ b/src/verb/internal.rs @@ -54,6 +54,7 @@ macro_rules! Internals { // name: "description" needs_a_path Internals! { back: "revert to the previous state (mapped to *esc*)" false, + escape: "escape from edition, completion, page, etc." false, close_panel_ok: "close the panel, validating the selected path" false, close_panel_cancel: "close the panel, not using the selected path" false, copy_line: "copy selected line (in tree or preview)" true, diff --git a/src/verb/mod.rs b/src/verb/mod.rs index 7ea25eaf..0dc0b6ba 100644 --- a/src/verb/mod.rs +++ b/src/verb/mod.rs @@ -1,5 +1,4 @@ mod arg_def; -mod builtin; mod exec_pattern; mod execution_builder; mod external_execution; @@ -40,6 +39,8 @@ use { /// the group you find in invocation patterns and execution patterns pub static GROUP: Lazy = lazy_regex!(r"\{([^{}:]+)(?::([^{}:]+))?\}"); +pub type VerbId = usize; + pub fn str_has_selection_group(s: &str) -> bool { GROUP.find_iter(s) .any(|group| matches!( diff --git a/src/verb/verb.rs b/src/verb/verb.rs index 6f226b47..aaaf046c 100644 --- a/src/verb/verb.rs +++ b/src/verb/verb.rs @@ -13,6 +13,7 @@ use { }, }; + /// what makes a verb. /// /// Verbs are the engines of broot commands, and apply @@ -28,6 +29,9 @@ use { /// in memory. #[derive(Debug)] pub struct Verb { + + pub id: VerbId, + /// names (like "cd", "focus", "focus_tab", "c") by which /// a verb can be called. /// Can be empty if the verb is only called with a key shortcut. @@ -81,6 +85,7 @@ impl PartialEq for Verb { impl Verb { pub fn new( + id: VerbId, invocation_str: Option<&str>, execution: VerbExecution, description: VerbDescription, @@ -108,6 +113,7 @@ impl Verb { ) }; Ok(Self { + id, names, keys: Vec::new(), invocation_parser, @@ -122,7 +128,7 @@ impl Verb { panels: Vec::new(), }) } - pub fn with_key(mut self, key: KeyEvent) -> Self { + pub fn with_key(&mut self, key: KeyEvent) -> &mut Self { self.keys.push(key); self } @@ -131,27 +137,27 @@ impl Verb { self.keys.push(key); } } - pub fn no_doc(mut self) -> Self { + pub fn no_doc(&mut self) -> &mut Self { self.show_in_doc = false; self } - pub fn with_description(mut self, description: &str) -> Self { + pub fn with_description(&mut self, description: &str) -> &mut Self { self.description = VerbDescription::from_text(description.to_string()); self } - pub fn with_shortcut(mut self, shortcut: &str) -> Self { + pub fn with_shortcut(&mut self, shortcut: &str) -> &mut Self { self.names.push(shortcut.to_string()); self } - pub fn with_stype(mut self, stype: SelectionType) -> Self { + pub fn with_stype(&mut self, stype: SelectionType) -> &mut Self { self.selection_condition = stype; self } - pub fn needing_another_panel(mut self) -> Self { + pub fn needing_another_panel(&mut self) -> &mut Self { self.needs_another_panel = true; self } - pub fn with_auto_exec(mut self, b: bool) -> Self { + pub fn with_auto_exec(&mut self, b: bool) -> &mut Self { self.auto_exec = b; self } @@ -272,6 +278,10 @@ impl Verb { self.get_internal() == Some(internal) } + pub fn is_some_internal(v: Option<&Verb>, internal: Internal) -> bool { + v.map_or(false, |v| v.is_internal(internal)) + } + pub fn is_sequence(&self) -> bool { matches!(self.execution, VerbExecution::Sequence(_)) } diff --git a/src/verb/verb_store.rs b/src/verb/verb_store.rs index 339f4f3f..e9cee1a2 100644 --- a/src/verb/verb_store.rs +++ b/src/verb/verb_store.rs @@ -1,15 +1,19 @@ use { super::{ - builtin::builtin_verbs, Internal, Verb, + VerbId, }, crate::{ app::*, - conf::Conf, + command::Sequence, + conf::{Conf, VerbConf}, errors::ConfError, keys::KEY_FORMAT, + keys, + verb::*, }, + crokey::*, }; /// Provide access to the verbs: @@ -20,7 +24,7 @@ use { /// - if the input exactly matches a shortcut or the name /// - if only one verb name starts with the input pub struct VerbStore { - pub verbs: Vec, + verbs: Vec, } #[derive(Debug, Clone, PartialEq)] @@ -32,13 +36,451 @@ pub enum PrefixSearchResult<'v, T> { impl VerbStore { pub fn new(conf: &mut Conf) -> Result { - let mut verbs = Vec::new(); + let mut store = Self { verbs: Vec::new() }; for vc in &conf.verbs { - let verb = vc.make_verb(&verbs)?; - verbs.push(verb); + store.add_from_conf(vc)?; } - verbs.append(&mut builtin_verbs()); // at the end so that we can override them - Ok(Self { verbs }) + store.add_builtin_verbs(); // at the end so that we can override them + Ok(store) + } + + fn add_builtin_verbs( + &mut self, + ) { + use super::{ExternalExecutionMode::*, Internal::*}; + self.add_internal(escape).with_key(key!(esc)); + + // input actions, not visible in doc, but available for + // example in remote control + self.add_internal(input_clear).no_doc(); + self.add_internal(input_del_char_left).no_doc(); + self.add_internal(input_del_char_below).no_doc(); + self.add_internal(input_del_word_left).no_doc(); + self.add_internal(input_del_word_right).no_doc(); + self.add_internal(input_go_to_end).with_key(key!(end)).no_doc(); + self.add_internal(input_go_left).no_doc(); + self.add_internal(input_go_right).no_doc(); + self.add_internal(input_go_to_start).with_key(key!(home)).no_doc(); + self.add_internal(input_go_word_left).no_doc(); + self.add_internal(input_go_word_right).no_doc(); + + // arrow keys bindings + self.add_internal(back).with_key(key!(left)); + self.add_internal(open_stay).with_key(key!(right)); + self.add_internal(line_down).with_key(key!(down)).with_key(key!('j')); + self.add_internal(line_up).with_key(key!(up)).with_key(key!('k')); + + self.add_internal(set_syntax_theme); + + // those two operations are mapped on ALT-ENTER, one + // for directories and the other one for the other files + self.add_internal(open_leave) // calls the system open + .with_stype(SelectionType::File) + .with_key(key!(alt-enter)) + .with_shortcut("ol"); + self.add_external("cd", "cd {directory}", FromParentShell) + .with_stype(SelectionType::Directory) + .with_key(key!(alt-enter)) + .with_shortcut("ol") + .with_description("change directory and quit"); + + #[cfg(unix)] + self.add_external("chmod {args}", "chmod {args} {file}", StayInBroot) + .with_stype(SelectionType::File); + #[cfg(unix)] + self.add_external("chmod {args}", "chmod -R {args} {file}", StayInBroot) + .with_stype(SelectionType::Directory); + self.add_internal(open_preview); + self.add_internal(close_preview); + self.add_internal(toggle_preview); + self.add_internal(preview_image) + .with_shortcut("img"); + self.add_internal(preview_text) + .with_shortcut("txt"); + self.add_internal(preview_binary) + .with_shortcut("hex"); + self.add_internal(close_panel_ok); + self.add_internal(close_panel_cancel) + .with_key(key!(ctrl-w)); + #[cfg(unix)] + self.add_external( + "copy {newpath}", + "cp -r {file} {newpath:path-from-parent}", + StayInBroot, + ) + .with_shortcut("cp"); + #[cfg(windows)] + self.add_external( + "copy {newpath}", + "xcopy /Q /H /Y /I {file} {newpath:path-from-parent}", + StayInBroot, + ) + .with_shortcut("cp"); + #[cfg(feature = "clipboard")] + self.add_internal(copy_line) + .with_key(key!(alt-c)); + #[cfg(feature = "clipboard")] + self.add_internal(copy_path); + #[cfg(unix)] + self.add_external( + "copy_to_panel", + "cp -r {file} {other-panel-directory}", + StayInBroot, + ) + .with_shortcut("cpp"); + #[cfg(windows)] + self.add_external( + "copy_to_panel", + "xcopy /Q /H /Y /I {file} {other-panel-directory}", + StayInBroot, + ) + .with_shortcut("cpp"); + #[cfg(unix)] + self.add_internal(filesystems) + .with_shortcut("fs"); + // :focus is also hardcoded on Enter on directories + // but ctrl-f is useful for focusing on a file's parent + // (and keep the filter) + self.add_internal(focus) + .with_key(key!(L)) // hum... why this one ? + .with_key(key!(ctrl-f)); + self.add_internal(help) + .with_key(key!(F1)) + .with_shortcut("?"); + #[cfg(feature="clipboard")] + self.add_internal(input_paste) + .with_key(key!(ctrl-v)); + #[cfg(unix)] + self.add_external( + "mkdir {subpath}", + "mkdir -p {subpath:path-from-directory}", + StayInBroot, + ) + .with_shortcut("md"); + #[cfg(windows)] + self.add_external( + "mkdir {subpath}", + "cmd /c mkdir {subpath:path-from-directory}", + StayInBroot, + ) + .with_shortcut("md"); + #[cfg(unix)] + self.add_external( + "move {newpath}", + "mv {file} {newpath:path-from-parent}", + StayInBroot, + ) + .with_shortcut("mv"); + #[cfg(windows)] + self.add_external( + "move {newpath}", + "cmd /c move /Y {file} {newpath:path-from-parent}", + StayInBroot, + ) + .with_shortcut("mv"); + #[cfg(unix)] + self.add_external( + "move_to_panel", + "mv {file} {other-panel-directory}", + StayInBroot, + ) + .with_shortcut("mvp"); + #[cfg(windows)] + self.add_external( + "move_to_panel", + "cmd /c move /Y {file} {other-panel-directory}", + StayInBroot, + ) + .with_shortcut("mvp"); + #[cfg(unix)] + self.add_external( + "rename {new_filename:file-name}", + "mv {file} {parent}/{new_filename}", + StayInBroot, + ) + .with_auto_exec(false) + .with_key(key!(f2)); + #[cfg(windows)] + self.add_external( + "rename {new_filename:file-name}", + "cmd /c move /Y {file} {parent}/{new_filename}", + StayInBroot, + ) + .with_auto_exec(false) + .with_key(key!(f2)); + self.add_internal_bang(start_end_panel) + .with_key(key!(ctrl-p)); + // the char keys for mode_input are handled differently as they're not + // consumed by the command + self.add_internal(mode_input) + .with_key(key!(' ')) + .with_key(key!(':')) + .with_key(key!('/')); + self.add_internal(previous_match) + .with_key(key!(shift-backtab)) + .with_key(key!(backtab)); + self.add_internal(next_match) + .with_key(key!(tab)); + self.add_internal(no_sort) + .with_shortcut("ns"); + self.add_internal(open_stay) + .with_key(key!(enter)) + .with_shortcut("os"); + self.add_internal(open_stay_filter) + .with_shortcut("osf"); + self.add_internal(parent) + .with_key(key!(h)) + .with_shortcut("p"); + self.add_internal(page_down) + .with_key(key!(ctrl-d)) + .with_key(key!(pagedown)); + self.add_internal(page_up) + .with_key(key!(ctrl-u)) + .with_key(key!(pageup)); + self.add_internal(panel_left_no_open) + .with_key(key!(ctrl-left)); + self.add_internal(panel_right) + .with_key(key!(ctrl-right)); + self.add_internal(print_path).with_shortcut("pp"); + self.add_internal(print_relative_path).with_shortcut("prp"); + self.add_internal(print_tree).with_shortcut("pt"); + self.add_internal(quit) + .with_key(key!(ctrl-c)) + .with_key(key!(ctrl-q)) + .with_shortcut("q"); + self.add_internal(refresh).with_key(key!(f5)); + self.add_internal(root_up) + .with_key(key!(ctrl-up)); + self.add_internal(root_down) + .with_key(key!(ctrl-down)); + self.add_internal(select_first); + self.add_internal(select_last); + self.add_internal(select); + self.add_internal(clear_stage).with_shortcut("cls"); + self.add_internal(stage) + .with_key(key!('+')); + self.add_internal(unstage) + .with_key(key!('-')); + self.add_internal(stage_all_files) + .with_key(key!(ctrl-a)); + self.add_internal(toggle_stage) + .with_key(key!(ctrl-g)); + self.add_internal(open_staging_area).with_shortcut("osa"); + self.add_internal(close_staging_area).with_shortcut("csa"); + self.add_internal(toggle_staging_area).with_shortcut("tsa"); + self.add_internal(sort_by_count).with_shortcut("sc"); + self.add_internal(sort_by_date).with_shortcut("sd"); + self.add_internal(sort_by_size).with_shortcut("ss"); + self.add_internal(sort_by_type).with_shortcut("st"); + #[cfg(unix)] + self.add_external("rm", "rm -rf {file}", StayInBroot); + #[cfg(windows)] + add_external("rm", "cmd /c rmdir /Q /S {file}", StayInBroot) + .with_stype(SelectionType::Directory); + #[cfg(windows)] + self.add_external("rm", "cmd /c del /Q {file}", StayInBroot) + .with_stype(SelectionType::File); + self.add_internal(toggle_counts).with_shortcut("counts"); + self.add_internal(toggle_dates).with_shortcut("dates"); + self.add_internal(toggle_device_id).with_shortcut("dev"); + self.add_internal(toggle_files).with_shortcut("files"); + self.add_internal(toggle_git_ignore) + .with_key(key!(alt-i)) + .with_shortcut("gi"); + self.add_internal(toggle_git_file_info).with_shortcut("gf"); + self.add_internal(toggle_git_status).with_shortcut("gs"); + self.add_internal(toggle_root_fs).with_shortcut("rfs"); + self.add_internal(toggle_hidden) + .with_key(key!(alt-h)) + .with_shortcut("h"); + #[cfg(unix)] + self.add_internal(toggle_perm).with_shortcut("perm"); + self.add_internal(toggle_sizes).with_shortcut("sizes"); + self.add_internal(toggle_trim_root); + self.add_internal(total_search).with_key(key!(ctrl-s)); + self.add_internal(up_tree).with_shortcut("up"); + } + + fn build_add_internal( + &mut self, + internal: Internal, + bang: bool, + ) -> &mut 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()); + self.add_verb(Some(invocation), execution, description).unwrap() + } + + fn add_internal( + &mut self, + internal: Internal, + ) -> &mut Verb { + self.build_add_internal(internal, false) + } + + fn add_internal_bang( + &mut self, + internal: Internal, + ) -> &mut Verb { + self.build_add_internal(internal, true) + } + + fn add_external( + &mut self, + invocation_str: &str, + execution_str: &str, + exec_mode: ExternalExecutionMode, + ) -> &mut Verb { + let execution = VerbExecution::External( + ExternalExecution::new(ExecPattern::from_string(execution_str), exec_mode) + ); + self.add_verb( + Some(invocation_str), + execution, + VerbDescription::from_code(execution_str.to_string()), + ).unwrap() + } + + pub fn add_verb( + &mut self, + invocation_str: Option<&str>, + execution: VerbExecution, + description: VerbDescription, + ) -> Result<&mut Verb, ConfError> { + let id = self.verbs.len(); + self.verbs.push(Verb::new( + id, + invocation_str, + execution, + description, + )?); + Ok(&mut self.verbs[id]) + } + + /// Create a verb from its configuration, adding it to its store + pub fn add_from_conf( + &mut self, + vc: &VerbConf, + ) -> Result<(), ConfError> { + if vc.leave_broot == Some(false) && vc.from_shell == Some(true) { + return Err(ConfError::InvalidVerbConf { + details: "You can't simultaneously have leave_broot=false and from_shell=true".to_string(), + }); + } + let invocation = vc.invocation.clone().filter(|i| !i.is_empty()); + let internal = vc.internal.as_ref().filter(|i| !i.is_empty()); + let external = vc.external.as_ref().filter(|i| !i.is_empty()); + let cmd = vc.cmd.as_ref().filter(|i| !i.is_empty()); + let cmd_separator = vc.cmd_separator.as_ref().filter(|i| !i.is_empty()); + let execution = vc.execution.as_ref().filter(|i| !i.is_empty()); + let make_external_execution = |s| { + let working_dir = match (vc.set_working_dir, &vc.working_dir) { + (Some(false), _) => None, + (_, Some(s)) => Some(s.clone()), + (Some(true), None) => Some("{directory}".to_owned()), + (None, None) => None, + }; + let mut external_execution = ExternalExecution::new( + s, + ExternalExecutionMode::from_conf(vc.from_shell, vc.leave_broot), + ) + .with_working_dir(working_dir); + if let Some(b) = vc.switch_terminal { + external_execution.switch_terminal = b; + } + external_execution + }; + let execution = match (execution, internal, external, cmd) { + // old definition with "execution": we guess whether it's an internal or + // an external + (Some(ep), None, None, None) => { + if let Some(internal_pattern) = ep.as_internal_pattern() { + if let Some(previous_verb) = self.verbs.iter().find(|&v| v.has_name(internal_pattern)) { + previous_verb.execution.clone() + } else { + VerbExecution::Internal(InternalExecution::try_from(internal_pattern)?) + } + } else { + VerbExecution::External(make_external_execution(ep.clone())) + } + } + // "internal": the leading `:` or ` ` is optional + (None, Some(s), None, None) => { + VerbExecution::Internal(if s.starts_with(':') || s.starts_with(' ') { + InternalExecution::try_from(&s[1..])? + } else { + InternalExecution::try_from(s)? + }) + } + // "external": it can be about any form + (None, None, Some(ep), None) => { + VerbExecution::External(make_external_execution(ep.clone())) + } + // "cmd": it's a sequence + (None, None, None, Some(s)) => VerbExecution::Sequence(SequenceExecution { + sequence: Sequence::new(s, cmd_separator), + }), + _ => { + return Err(ConfError::InvalidVerbConf { + details: "You must define either internal, external or cmd".to_string(), + }); + } + }; + let description = vc + .description + .clone() + .map(VerbDescription::from_text) + .unwrap_or_else(|| VerbDescription::from_code(execution.to_string())); + let verb = self.add_verb( + invocation.as_deref(), + execution, + description, + )?; + // we accept both key and keys. We merge both here + let mut unchecked_keys = vc.keys.clone(); + if let Some(key) = &vc.key { + unchecked_keys.push(key.clone()); + } + let mut checked_keys = Vec::new(); + for key in &unchecked_keys { + let key = crokey::parse(key)?; + if keys::is_reserved(key) { + return Err(ConfError::ReservedKey { + key: keys::KEY_FORMAT.to_string(key) + }); + } + checked_keys.push(key); + } + for extension in &vc.extensions { + verb.file_extensions.push(extension.clone()); + } + if !checked_keys.is_empty() { + verb.add_keys(checked_keys); + } + if let Some(shortcut) = &vc.shortcut { + verb.names.push(shortcut.clone()); + } + if vc.auto_exec == Some(false) { + verb.auto_exec = false; + } + if !vc.panels.is_empty() { + verb.panels = vc.panels.clone(); + } + verb.selection_condition = match vc.apply_to.as_deref() { + Some("file") => SelectionType::File, + Some("directory") => SelectionType::Directory, + Some("any") => SelectionType::Any, + None => SelectionType::Any, + Some(s) => { + return Err(ConfError::InvalidVerbConf { + details: format!("{s:?} isn't a valid value of apply_to"), + }); + } + }; + Ok(()) } pub fn search_sel_info<'v>( @@ -142,4 +584,12 @@ impl VerbStore { None } + pub fn verbs(&self) -> &[Verb] { + &self.verbs + } + + pub fn verb(&self, id: VerbId) -> &Verb { + &self.verbs[id] + } + } diff --git a/website/docs/conf_verbs.md b/website/docs/conf_verbs.md index 9e4e3c42..b4a2ea02 100644 --- a/website/docs/conf_verbs.md +++ b/website/docs/conf_verbs.md @@ -379,7 +379,8 @@ Here's a list of internals: builtin actions you can add an alternate shortcut or invocation | default key | default shortcut | behavior / details -|-|-|- -:back | Esc | - | back to previous app state (see Usage page) | +:back | left | - | back to previous app state (see Usage page) | +:escape | esc | - | escape from completions, current input, page, etc. (this internal can be bound to another key but should not be used in command sequences) :chmod {args} | - | - | execute a chmod :clear_stage | - | cls | empty the staging area :close_preview | - | - | close the preview panel @@ -396,7 +397,7 @@ invocation | default key | default shortcut | behavior / details :mv {newpath} | - | - | move the file or directory to the provided path :no_sort | - | ns | remove all sorts :next_dir | - | - | select the next directory -:next_match | tab | - | select the next matching file +:next_match | tab | - | select the next matching file, or matching verb or path in auto-completion :open_leave | altenter | - | open the selected file in the default OS opener and leave broot :open_preview | - | - | open the preview panel :open_staging_area | - | osa | open the staging area