From 7def57203f493c38719802fc13b5a26b88ac01aa Mon Sep 17 00:00:00 2001 From: Hunter Bown Date: Wed, 6 May 2026 10:13:00 -0500 Subject: [PATCH] fix(tui): stop stealing Ctrl+E from composer fix(tui): stop stealing Ctrl+E from composer --- crates/tui/src/tui/file_tree.rs | 2 +- crates/tui/src/tui/ui.rs | 35 +++++++++++++++++++++++++++------ crates/tui/src/tui/ui/tests.rs | 20 +++++++++++++++++++ docs/KEYBINDINGS.md | 2 +- 4 files changed, 51 insertions(+), 8 deletions(-) diff --git a/crates/tui/src/tui/file_tree.rs b/crates/tui/src/tui/file_tree.rs index c47b3683..c15c36d4 100644 --- a/crates/tui/src/tui/file_tree.rs +++ b/crates/tui/src/tui/file_tree.rs @@ -1,4 +1,4 @@ -//! File-tree pane — Ctrl+E toggles a left-side workspace file navigator. +//! File-tree pane — Ctrl+Shift+E toggles a left-side workspace file navigator. //! //! Shows the workspace directory tree with expandable directories. Up/Down //! navigate, Enter expands/collapses directories or inserts `@path` for files, diff --git a/crates/tui/src/tui/ui.rs b/crates/tui/src/tui/ui.rs index dcebeac5..dfa12094 100644 --- a/crates/tui/src/tui/ui.rs +++ b/crates/tui/src/tui/ui.rs @@ -1805,10 +1805,9 @@ async fn run_event_loop( continue; } - // Ctrl+E toggles the file-tree pane. Visible even when other - // modals are open (the file tree is part of the body layout, - // not a modal overlay). - if key.code == KeyCode::Char('e') && key.modifiers.contains(KeyModifiers::CONTROL) { + // Shifted shortcuts toggle the file-tree pane. Keep plain Ctrl+E + // reserved for the composer end-of-line binding used by shells. + if is_file_tree_toggle_shortcut(&key) { if let Some(_state) = app.file_tree.as_mut() { // File tree visible → hide it. app.file_tree = None; @@ -2003,7 +2002,9 @@ async fn run_event_loop( continue; } KeyCode::Char('o') - if key.modifiers == KeyModifiers::CONTROL && open_thinking_pager(app) => + if key.modifiers == KeyModifiers::CONTROL + && app.input.is_empty() + && open_thinking_pager(app) => { continue; } @@ -2562,7 +2563,10 @@ async fn run_event_loop( app.move_cursor_end(); } KeyCode::Char('e') if key.modifiers.contains(KeyModifiers::CONTROL) => { - // Ctrl+E: spawn $EDITOR on the composer contents (#91). + app.move_cursor_end(); + } + KeyCode::Char('o') if key.modifiers.contains(KeyModifiers::CONTROL) => { + // Ctrl+O: spawn $EDITOR on the composer contents (#91). // Only fires when no modal is active (the !view_stack // branch above already returns early in that case) and // the composer is the focused input target. We accept the @@ -7557,6 +7561,25 @@ fn is_copy_shortcut(key: &KeyEvent) -> bool { key.modifiers.contains(KeyModifiers::CONTROL) && key.modifiers.contains(KeyModifiers::SHIFT) } +fn is_file_tree_toggle_shortcut(key: &KeyEvent) -> bool { + let is_shifted_e = matches!(key.code, KeyCode::Char('E')) + || (matches!(key.code, KeyCode::Char('e')) && key.modifiers.contains(KeyModifiers::SHIFT)); + if !is_shifted_e { + return false; + } + + let has_forbidden_modifier = + key.modifiers.contains(KeyModifiers::ALT) || key.modifiers.contains(KeyModifiers::SUPER); + let ctrl_shift_e = key.modifiers.contains(KeyModifiers::CONTROL) && !has_forbidden_modifier; + + let cmd_shift_e = key.modifiers.contains(KeyModifiers::SUPER) + && key.modifiers.contains(KeyModifiers::SHIFT) + && !key.modifiers.contains(KeyModifiers::CONTROL) + && !key.modifiers.contains(KeyModifiers::ALT); + + ctrl_shift_e || cmd_shift_e +} + fn details_shortcut_modifiers(modifiers: KeyModifiers) -> bool { modifiers.is_empty() || modifiers == KeyModifiers::SHIFT diff --git a/crates/tui/src/tui/ui/tests.rs b/crates/tui/src/tui/ui/tests.rs index 7eedf80e..00954ce7 100644 --- a/crates/tui/src/tui/ui/tests.rs +++ b/crates/tui/src/tui/ui/tests.rs @@ -378,6 +378,26 @@ fn copy_shortcut_accepts_cmd_and_ctrl_shift_only() { ))); } +#[test] +fn file_tree_shortcut_does_not_steal_plain_ctrl_e() { + assert!(!is_file_tree_toggle_shortcut(&KeyEvent::new( + KeyCode::Char('e'), + KeyModifiers::CONTROL, + ))); + assert!(is_file_tree_toggle_shortcut(&KeyEvent::new( + KeyCode::Char('E'), + KeyModifiers::CONTROL, + ))); + assert!(is_file_tree_toggle_shortcut(&KeyEvent::new( + KeyCode::Char('e'), + KeyModifiers::CONTROL | KeyModifiers::SHIFT, + ))); + assert!(is_file_tree_toggle_shortcut(&KeyEvent::new( + KeyCode::Char('E'), + KeyModifiers::SUPER | KeyModifiers::SHIFT, + ))); +} + #[test] fn parse_plan_choice_accepts_numbers() { assert_eq!(parse_plan_choice("1"), Some(PlanChoice::AcceptAgent)); diff --git a/docs/KEYBINDINGS.md b/docs/KEYBINDINGS.md index 89010d27..15278a34 100644 --- a/docs/KEYBINDINGS.md +++ b/docs/KEYBINDINGS.md @@ -16,7 +16,7 @@ Bindings are not (yet) user-configurable — tracked for a future release (#436, | `Shift-Tab` | Cycle reasoning effort: off → high → max → off | | `Ctrl-R` | Open the resume-session picker | | `Ctrl-L` | Refresh / clear the screen | -| `Ctrl-T` | Toggle the file-tree sidebar | +| `Ctrl-Shift-E` / `Cmd-Shift-E` | Toggle the file-tree sidebar | | `Esc` | Close topmost modal · cancel slash menu · dismiss toast | ## Composer