diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-07-08 22:31:09 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-07-08 22:31:40 +0300 |
| commit | b708f85e7d17d8109bef70ea3f595cea0ae40c46 (patch) | |
| tree | ed2e7948824cd9069fc63d4dea35ba592d31f101 /src/app/input | |
| parent | 552aa8ca223908a2ee3b1058587d4b5a84828e71 (diff) | |
fix: match shifted indexed number keybinds
refs #1184
Diffstat (limited to 'src/app/input')
| -rw-r--r-- | src/app/input/mod.rs | 4 | ||||
| -rw-r--r-- | src/app/input/navigate.rs | 218 | ||||
| -rw-r--r-- | src/app/input/terminal.rs | 15 |
3 files changed, 212 insertions, 25 deletions
diff --git a/src/app/input/mod.rs b/src/app/input/mod.rs index 00bbf3cf..450afab8 100644 --- a/src/app/input/mod.rs +++ b/src/app/input/mod.rs @@ -49,7 +49,9 @@ pub(crate) use self::{ handle_global_menu_key, handle_keybind_help_key, handle_navigator_key, insert_navigator_search_text, insert_rename_input_text, }, - navigate::terminal_direct_navigation_action, + navigate::{ + terminal_direct_indexed_navigation_action, terminal_direct_non_indexed_navigation_action, + }, settings::open_settings_at, }; use self::{ diff --git a/src/app/input/navigate.rs b/src/app/input/navigate.rs index 426864e6..a1597669 100644 --- a/src/app/input/navigate.rs +++ b/src/app/input/navigate.rs @@ -21,6 +21,7 @@ use crate::{ terminal::TerminalRuntimeRegistry, }; +#[cfg(test)] pub(crate) fn terminal_direct_navigation_action( state: &AppState, key: TerminalKey, @@ -28,6 +29,20 @@ pub(crate) fn terminal_direct_navigation_action( action_for_key(state, key, BindingDispatch::Direct) } +pub(crate) fn terminal_direct_non_indexed_navigation_action( + state: &AppState, + key: TerminalKey, +) -> Option<NavigateAction> { + non_indexed_action_for_key(state, key, BindingDispatch::Direct) +} + +pub(crate) fn terminal_direct_indexed_navigation_action( + state: &AppState, + key: TerminalKey, +) -> Option<NavigateAction> { + indexed_navigation_action(state, key, BindingDispatch::Direct) +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum ActionContext { Direct, @@ -61,25 +76,10 @@ impl App { return; } - if let Some(action) = action_for_key(&self.state, raw_key, BindingDispatch::Prefix) { - if action == NavigateAction::EditScrollback { - let previous_mode = self.state.mode; - self.cancel_copy_mode_if_active(); - self.launch_focused_scrollback_editor(); - finish_action_context(&mut self.state, ActionContext::Prefix, previous_mode); - } else if action == NavigateAction::CopyMode { - self.cancel_copy_mode_if_active(); - self.execute_tui_navigate_action(action, ActionContext::Prefix); - } else if copy_mode_survives_prefix_action(action) { - self.execute_tui_navigate_action(action, ActionContext::Prefix); - if self.state.copy_mode.is_some() { - self.state.sync_copy_mode_with_focus(); - } - } else { - self.cancel_copy_mode_if_active(); - self.execute_tui_navigate_action(action, ActionContext::Prefix); - } - self.selection_autoscroll_deadline = None; + if let Some(action) = + non_indexed_action_for_key(&self.state, raw_key, BindingDispatch::Prefix) + { + self.execute_prefix_key_action(action); return; } @@ -89,9 +89,37 @@ impl App { return; } + if let Some(action) = + indexed_navigation_action(&self.state, raw_key, BindingDispatch::Prefix) + { + self.execute_prefix_key_action(action); + return; + } + leave_command_mode(&mut self.state); } + fn execute_prefix_key_action(&mut self, action: NavigateAction) { + if action == NavigateAction::EditScrollback { + let previous_mode = self.state.mode; + self.cancel_copy_mode_if_active(); + self.launch_focused_scrollback_editor(); + finish_action_context(&mut self.state, ActionContext::Prefix, previous_mode); + } else if action == NavigateAction::CopyMode { + self.cancel_copy_mode_if_active(); + self.execute_tui_navigate_action(action, ActionContext::Prefix); + } else if copy_mode_survives_prefix_action(action) { + self.execute_tui_navigate_action(action, ActionContext::Prefix); + if self.state.copy_mode.is_some() { + self.state.sync_copy_mode_with_focus(); + } + } else { + self.cancel_copy_mode_if_active(); + self.execute_tui_navigate_action(action, ActionContext::Prefix); + } + self.selection_autoscroll_deadline = None; + } + pub(crate) fn handle_navigate_key(&mut self, raw_key: TerminalKey) { let key = raw_key.as_key_event(); self.state.update_dismissed = true; @@ -127,7 +155,7 @@ impl App { return; } - if let Some(action) = navigate_mode_action_for_key(&self.state, raw_key) { + if let Some(action) = navigate_mode_non_indexed_action_for_key(&self.state, raw_key) { if action == NavigateAction::EditScrollback { self.launch_focused_scrollback_editor(); } else { @@ -139,6 +167,12 @@ impl App { if let Some(binding) = command_for_key(&self.state, raw_key, BindingDispatch::Prefix) { self.launch_custom_command(binding, ActionContext::Navigate); + return; + } + + if let Some(action) = navigate_mode_indexed_action_for_key(&self.state, raw_key) { + self.execute_tui_navigate_action(action, ActionContext::Navigate); + self.selection_autoscroll_deadline = None; } } @@ -1360,15 +1394,21 @@ fn action_matches( } } +#[cfg(test)] fn action_for_key( state: &AppState, key: TerminalKey, dispatch: BindingDispatch, ) -> Option<NavigateAction> { - if let Some(action) = indexed_navigation_action(state, key, dispatch) { - return Some(action); - } + non_indexed_action_for_key(state, key, dispatch) + .or_else(|| indexed_navigation_action(state, key, dispatch)) +} +fn non_indexed_action_for_key( + state: &AppState, + key: TerminalKey, + dispatch: BindingDispatch, +) -> Option<NavigateAction> { let kb = &state.keybinds; for (bindings, action) in [ (&kb.help, NavigateAction::Help), @@ -1424,6 +1464,7 @@ fn action_for_key( None } +#[cfg(test)] fn navigate_mode_action_for_key(state: &AppState, key: TerminalKey) -> Option<NavigateAction> { let action = action_for_key(state, key, BindingDispatch::Prefix)?; if matches!( @@ -1438,6 +1479,30 @@ fn navigate_mode_action_for_key(state: &AppState, key: TerminalKey) -> Option<Na Some(action) } +fn navigate_mode_non_indexed_action_for_key( + state: &AppState, + key: TerminalKey, +) -> Option<NavigateAction> { + let action = non_indexed_action_for_key(state, key, BindingDispatch::Prefix)?; + if matches!( + action, + NavigateAction::FocusPaneLeft + | NavigateAction::FocusPaneDown + | NavigateAction::FocusPaneUp + | NavigateAction::FocusPaneRight + ) { + return None; + } + Some(action) +} + +fn navigate_mode_indexed_action_for_key( + state: &AppState, + key: TerminalKey, +) -> Option<NavigateAction> { + indexed_navigation_action(state, key, BindingDispatch::Prefix) +} + #[cfg(test)] pub(super) fn execute_navigate_action(state: &mut AppState, action: NavigateAction) { let mut terminal_runtimes = TerminalRuntimeRegistry::new(); @@ -2383,6 +2448,113 @@ last_pane = "prefix+tab" assert_eq!(action, Some(NavigateAction::SwitchTab(2))); } + #[test] + fn prefix_shift_indexed_workspace_shortcut_maps_shifted_symbol_key() { + let mut state = state_with_workspaces(&["one", "two"]); + let config: Config = + toml::from_str("[keys]\nswitch_workspace = \"prefix+shift+1..9\"\n").unwrap(); + state.keybinds.switch_workspace = config.keybinds().switch_workspace; + + let action = action_for_key( + &state, + TerminalKey::new(KeyCode::Char('@'), KeyModifiers::empty()), + BindingDispatch::Prefix, + ); + + assert_eq!(action, Some(NavigateAction::SwitchWorkspace(1))); + } + + #[test] + fn literal_symbol_binding_takes_precedence_over_shifted_indexed_alias() { + let mut state = state_with_workspaces(&["one", "two"]); + let config: Config = toml::from_str( + r#" +[keys] +help = "prefix+!" +switch_workspace = "prefix+shift+1..9" +"#, + ) + .unwrap(); + state.keybinds = config.keybinds(); + + let action = action_for_key( + &state, + TerminalKey::new(KeyCode::Char('!'), KeyModifiers::empty()), + BindingDispatch::Prefix, + ); + + assert_eq!(action, Some(NavigateAction::Help)); + } + + #[test] + fn literal_symbol_custom_command_is_visible_before_shifted_indexed_alias() { + let mut state = state_with_workspaces(&["one", "two"]); + let config: Config = toml::from_str( + r#" +[keys] +switch_workspace = "prefix+shift+1..9" + +[[keys.command]] +key = "prefix+!" +command = "echo literal" +"#, + ) + .unwrap(); + state.keybinds = config.keybinds(); + + let key = TerminalKey::new(KeyCode::Char('!'), KeyModifiers::empty()); + assert!(command_for_key(&state, key, BindingDispatch::Prefix).is_some()); + assert_eq!( + indexed_navigation_action(&state, key, BindingDispatch::Prefix), + Some(NavigateAction::SwitchWorkspace(0)) + ); + } + + #[cfg(unix)] + #[tokio::test] + async fn literal_symbol_custom_command_runs_before_shifted_indexed_alias() { + let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel(); + let mut app = App::new( + &Config::default(), + true, + None, + api_rx, + crate::api::EventHub::default(), + ); + app.state.workspaces = vec![Workspace::test_new("one"), Workspace::test_new("two")]; + app.state.active = Some(1); + app.state.selected = 1; + app.state.mode = Mode::Terminal; + + let output_path = unique_temp_path("literal-symbol-custom-command"); + let config: Config = toml::from_str(&format!( + r#" +[keys] +switch_workspace = "prefix+shift+1..9" + +[[keys.command]] +key = "prefix+!" +command = "printf literal > '{}'" +"#, + output_path.display() + )) + .unwrap(); + app.state.keybinds = config.keybinds(); + + app.handle_key(TerminalKey::new( + app.state.prefix_code, + app.state.prefix_mods, + )) + .await; + app.handle_key(TerminalKey::new(KeyCode::Char('!'), KeyModifiers::empty())) + .await; + + assert_eq!(wait_for_file(&output_path), "literal"); + assert_eq!(app.state.active, Some(1)); + assert_eq!(app.state.mode, Mode::Terminal); + let _ = std::fs::remove_file(output_path); + } + #[tokio::test] async fn navigate_mode_runs_prefix_action_rhs_without_pressing_prefix_again() { let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel(); diff --git a/src/app/input/terminal.rs b/src/app/input/terminal.rs index a8e364ba..0fc35bd3 100644 --- a/src/app/input/terminal.rs +++ b/src/app/input/terminal.rs @@ -34,7 +34,8 @@ impl App { let key_event = key.as_key_event(); - if let Some(action) = super::terminal_direct_navigation_action(&self.state, key) { + if let Some(action) = super::terminal_direct_non_indexed_navigation_action(&self.state, key) + { debug!( code = ?key_event.code, modifiers = ?key_event.modifiers, @@ -66,6 +67,18 @@ impl App { return None; } + if let Some(action) = super::terminal_direct_indexed_navigation_action(&self.state, key) { + debug!( + code = ?key_event.code, + modifiers = ?key_event.modifiers, + kind = ?key_event.kind, + action = ?action, + "intercepted terminal direct indexed keybinding before forwarding to pane" + ); + self.execute_tui_navigate_action(action, super::navigate::ActionContext::Direct); + return None; + } + if self.state.is_prefix_key(key) { self.state.mode = Mode::Prefix; return None; |
