From 1d55a8f4d933e072ff4eec563c529e20acb2652f Mon Sep 17 00:00:00 2001 From: Komsan Date: Tue, 4 Aug 2026 16:29:42 +0700 Subject: [PATCH 1/2] fix(input): leave navigate mode on ctrl+[ refs #1431 --- src/app/input/navigate.rs | 44 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/src/app/input/navigate.rs b/src/app/input/navigate.rs index 68941e663f..d95cdb1dee 100644 --- a/src/app/input/navigate.rs +++ b/src/app/input/navigate.rs @@ -125,10 +125,9 @@ impl App { } pub(crate) fn handle_navigate_key(&mut self, raw_key: TerminalKey) { - let key = raw_key.as_key_event(); self.state.update_dismissed = true; - if key.code == KeyCode::Esc || self.state.is_prefix_key(&raw_key) { + if is_navigate_cancel_key(&raw_key) || self.state.is_prefix_key(&raw_key) { leave_navigate_mode(&mut self.state); return; } @@ -1310,7 +1309,7 @@ pub(crate) fn handle_navigate_key(state: &mut AppState, key: KeyEvent) { state.update_dismissed = true; let terminal_key = TerminalKey::from(key); - if state.is_prefix_key(&terminal_key) || key.code == KeyCode::Esc { + if state.is_prefix_key(&terminal_key) || is_navigate_cancel_key(&terminal_key) { leave_navigate_mode(state); return; } @@ -1809,6 +1808,19 @@ fn workspace_can_start_worktree_action( !git_space.is_some_and(|space| space.is_linked_worktree) } +/// True for Esc and for Ctrl+[, its terminal-level equivalent. +/// +/// A legacy terminal sends Ctrl+[ as 0x1b, the same byte as Esc, so it already +/// arrives as `KeyCode::Esc`. Under the kitty keyboard protocol, which Herdr +/// negotiates, the modified key is reported on its own and reaches us as +/// `Char('[')` with CONTROL. Ctrl+Shift+[ stays distinct because it carries +/// SHIFT. +fn is_navigate_cancel_key(key: &TerminalKey) -> bool { + key.code == KeyCode::Esc + || (key.code == KeyCode::Char('[') + && key.modifiers == crossterm::event::KeyModifiers::CONTROL) +} + fn leave_navigate_mode(state: &mut AppState) { if state.active.is_some() { state.mode = Mode::Terminal; @@ -2892,6 +2904,32 @@ command = "printf literal > '{}'" assert_eq!(app.state.mode, Mode::Navigate); } + #[test] + fn app_navigate_mode_ctrl_bracket_leaves_like_esc() { + let mut app = app_with_test_workspaces(&["one", "two"]); + + app.state.mode = Mode::Navigate; + app.handle_navigate_key(TerminalKey::new(KeyCode::Char('['), KeyModifiers::CONTROL)); + assert_eq!(app.state.mode, Mode::Terminal); + + app.state.mode = Mode::Navigate; + app.handle_navigate_key(TerminalKey::new(KeyCode::Esc, KeyModifiers::empty())); + assert_eq!(app.state.mode, Mode::Terminal); + } + + #[test] + fn app_navigate_mode_ctrl_shift_bracket_stays_open() { + let mut app = app_with_test_workspaces(&["one", "two"]); + app.state.mode = Mode::Navigate; + + app.handle_navigate_key(TerminalKey::new( + KeyCode::Char('['), + KeyModifiers::CONTROL | KeyModifiers::SHIFT, + )); + + assert_eq!(app.state.mode, Mode::Navigate); + } + #[test] fn app_navigate_mode_maps_french_number_row_to_workspace() { let mut app = app_with_test_workspaces(&["one", "two"]); From 647415372f5b15105468b159e93baa590a13d48b Mon Sep 17 00:00:00 2001 From: Komsan Date: Thu, 6 Aug 2026 09:13:28 +0700 Subject: [PATCH 2/2] fix(input): check ctrl+[ after navigate keybindings refs #1431 --- src/app/input/navigate.rs | 41 +++++++++++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/src/app/input/navigate.rs b/src/app/input/navigate.rs index d95cdb1dee..7ab1bc0a53 100644 --- a/src/app/input/navigate.rs +++ b/src/app/input/navigate.rs @@ -127,7 +127,7 @@ impl App { pub(crate) fn handle_navigate_key(&mut self, raw_key: TerminalKey) { self.state.update_dismissed = true; - if is_navigate_cancel_key(&raw_key) || self.state.is_prefix_key(&raw_key) { + if raw_key.code == KeyCode::Esc || self.state.is_prefix_key(&raw_key) { leave_navigate_mode(&mut self.state); return; } @@ -176,6 +176,11 @@ impl App { 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; + return; + } + + if is_ctrl_bracket_key(&raw_key) { + leave_navigate_mode(&mut self.state); } } @@ -1309,7 +1314,7 @@ pub(crate) fn handle_navigate_key(state: &mut AppState, key: KeyEvent) { state.update_dismissed = true; let terminal_key = TerminalKey::from(key); - if state.is_prefix_key(&terminal_key) || is_navigate_cancel_key(&terminal_key) { + if state.is_prefix_key(&terminal_key) || key.code == KeyCode::Esc { leave_navigate_mode(state); return; } @@ -1318,13 +1323,18 @@ pub(crate) fn handle_navigate_key(state: &mut AppState, key: KeyEvent) { return; } - if let Some(action) = navigate_mode_action_for_key(state, terminal_key) { + if let Some(action) = navigate_mode_action_for_key(state, terminal_key.clone()) { execute_navigate_action_in_context( state, &mut terminal_runtimes, action, ActionContext::Navigate, ); + return; + } + + if is_ctrl_bracket_key(&terminal_key) { + leave_navigate_mode(state); } } @@ -1808,17 +1818,18 @@ fn workspace_can_start_worktree_action( !git_space.is_some_and(|space| space.is_linked_worktree) } -/// True for Esc and for Ctrl+[, its terminal-level equivalent. +/// True for Ctrl+[, the terminal-level equivalent of Esc. /// /// A legacy terminal sends Ctrl+[ as 0x1b, the same byte as Esc, so it already /// arrives as `KeyCode::Esc`. Under the kitty keyboard protocol, which Herdr /// negotiates, the modified key is reported on its own and reaches us as /// `Char('[')` with CONTROL. Ctrl+Shift+[ stays distinct because it carries /// SHIFT. -fn is_navigate_cancel_key(key: &TerminalKey) -> bool { - key.code == KeyCode::Esc - || (key.code == KeyCode::Char('[') - && key.modifiers == crossterm::event::KeyModifiers::CONTROL) +/// +/// Callers check this only after navigate-mode keybinding dispatch, so a +/// configured Ctrl+[ binding keeps working and this stays a fallback cancel. +fn is_ctrl_bracket_key(key: &TerminalKey) -> bool { + key.code == KeyCode::Char('[') && key.modifiers == crossterm::event::KeyModifiers::CONTROL } fn leave_navigate_mode(state: &mut AppState) { @@ -2917,6 +2928,20 @@ command = "printf literal > '{}'" assert_eq!(app.state.mode, Mode::Terminal); } + #[test] + fn app_navigate_mode_configured_ctrl_bracket_binding_wins_over_cancel() { + let mut app = app_with_test_workspaces(&["one", "two"]); + let config: Config = + toml::from_str("[keys]\nnavigate_workspace_down = \"ctrl+[\"\n").unwrap(); + app.state.keybinds = config.keybinds(); + app.state.mode = Mode::Navigate; + + app.handle_navigate_key(TerminalKey::new(KeyCode::Char('['), KeyModifiers::CONTROL)); + + assert_eq!(app.state.selected, 1); + assert_eq!(app.state.mode, Mode::Navigate); + } + #[test] fn app_navigate_mode_ctrl_shift_bracket_stays_open() { let mut app = app_with_test_workspaces(&["one", "two"]);