From 6ac14a246d1eec8bdf6aad4b79348dd74648fcdf Mon Sep 17 00:00:00 2001 From: Hanmiao Li <894876246@qq.com> Date: Tue, 9 Jun 2026 15:26:43 +0800 Subject: [PATCH 1/3] fix(tui): normalize macOS SUPER (Cmd) to CONTROL for keyboard shortcuts (#2938) On macOS, terminal emulators may report Cmd (SUPER) instead of Ctrl (CONTROL) for keyboard shortcuts, depending on the terminal app and its configuration. This caused Ctrl+B, Ctrl+Alt+2, and other shortcuts to be inconsistent. Fix: - Add normalize_macos_modifiers() in composer_ui.rs - On macOS: map SUPER to CONTROL when CONTROL is not already set - On other platforms: no-op - Apply normalization at the key event entry point in ui.rs Tests: - normalize_macos_modifiers_maps_super_to_control - normalize_macos_modifiers_preserves_existing_control - normalize_macos_modifiers_leaves_alt_unchanged --- crates/tui/src/tui/composer_ui.rs | 17 +++++++++++++++++ crates/tui/src/tui/ui.rs | 9 ++++++++- crates/tui/src/tui/ui/tests.rs | 27 +++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/crates/tui/src/tui/composer_ui.rs b/crates/tui/src/tui/composer_ui.rs index fff29272..1dffc685 100644 --- a/crates/tui/src/tui/composer_ui.rs +++ b/crates/tui/src/tui/composer_ui.rs @@ -121,6 +121,23 @@ pub(crate) fn is_word_cursor_modifier(modifiers: KeyModifiers) -> bool { modifiers.contains(KeyModifiers::CONTROL) || modifiers.contains(KeyModifiers::ALT) } +/// On macOS, map `SUPER` (Cmd ⌘) to `CONTROL` when `CONTROL` is not already +/// set, so that terminal emulators that don't pass Ctrl faithfully still work. +/// On all other platforms this is a no-op. +#[cfg(target_os = "macos")] +pub(crate) fn normalize_macos_modifiers(modifiers: KeyModifiers) -> KeyModifiers { + if modifiers.contains(KeyModifiers::SUPER) && !modifiers.contains(KeyModifiers::CONTROL) { + modifiers | KeyModifiers::CONTROL + } else { + modifiers + } +} + +#[cfg(not(target_os = "macos"))] +pub(crate) fn normalize_macos_modifiers(modifiers: KeyModifiers) -> KeyModifiers { + modifiers +} + pub(crate) fn handle_composer_alt_word_motion_key(app: &mut App, key: KeyEvent) -> bool { if !key.modifiers.contains(KeyModifiers::ALT) || key.modifiers.contains(KeyModifiers::CONTROL) { return false; diff --git a/crates/tui/src/tui/ui.rs b/crates/tui/src/tui/ui.rs index 9320f7d9..048d504d 100644 --- a/crates/tui/src/tui/ui.rs +++ b/crates/tui/src/tui/ui.rs @@ -2972,7 +2972,7 @@ async fn run_event_loop( // User interaction — clear the ✅ completion marker from the title. crate::tui::notifications::reset_title_on_interaction(); - let Event::Key(key) = evt else { + let Event::Key(mut key) = evt else { continue; }; @@ -2980,6 +2980,13 @@ async fn run_event_loop( continue; } + // Normalize macOS modifiers: map SUPER (Cmd) to CONTROL so that + // keyboard shortcuts work consistently across terminal emulators + // (Terminal.app, iTerm2, Kitty, etc.) that may report different + // modifier flags (#2938). + let mapped = crate::tui::composer_ui::normalize_macos_modifiers(key.modifiers); + key.modifiers = mapped; + // Decision card keyboard routing (v0.8.43 truth-surface). // When a card is active, number keys 1-9 select options, // j/k or Up/Down navigate, and Enter confirms. diff --git a/crates/tui/src/tui/ui/tests.rs b/crates/tui/src/tui/ui/tests.rs index 908c62dd..bc2ec692 100644 --- a/crates/tui/src/tui/ui/tests.rs +++ b/crates/tui/src/tui/ui/tests.rs @@ -298,6 +298,33 @@ fn word_cursor_modifier_accepts_control_and_alt() { assert!(!is_word_cursor_modifier(KeyModifiers::SHIFT)); } +#[test] +fn normalize_macos_modifiers_maps_super_to_control() { + use crate::tui::composer_ui::normalize_macos_modifiers; + // SUPER (Cmd) without CONTROL should gain CONTROL. + let normalized = normalize_macos_modifiers(KeyModifiers::SUPER); + assert!(normalized.contains(KeyModifiers::CONTROL)); + assert!(normalized.contains(KeyModifiers::SUPER)); +} + +#[test] +fn normalize_macos_modifiers_preserves_existing_control() { + use crate::tui::composer_ui::normalize_macos_modifiers; + // CONTROL already set — shouldn't be removed. + let normalized = normalize_macos_modifiers(KeyModifiers::CONTROL | KeyModifiers::SUPER); + assert!(normalized.contains(KeyModifiers::CONTROL)); + assert!(normalized.contains(KeyModifiers::SUPER)); +} + +#[test] +fn normalize_macos_modifiers_leaves_alt_unchanged() { + use crate::tui::composer_ui::normalize_macos_modifiers; + let normalized = normalize_macos_modifiers(KeyModifiers::ALT); + // On non-macOS this is a no-op; on macOS ALT stays unchanged. + assert!(normalized.contains(KeyModifiers::ALT)); + assert!(!normalized.contains(KeyModifiers::SUPER)); +} + #[test] fn alt_f_and_alt_b_move_by_word_without_inserting_text() { let mut app = create_test_app(); From 1b5442f2b7c2a3e7ac54fdb4a4b7d12fd755cf16 Mon Sep 17 00:00:00 2001 From: Hanmiao Li <894876246@qq.com> Date: Tue, 9 Jun 2026 15:32:23 +0800 Subject: [PATCH 2/3] fix: strip SUPER from modifiers after normalization per review --- crates/tui/src/tui/composer_ui.rs | 7 +++++-- crates/tui/src/tui/ui/tests.rs | 8 ++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/crates/tui/src/tui/composer_ui.rs b/crates/tui/src/tui/composer_ui.rs index 1dffc685..a73cbd80 100644 --- a/crates/tui/src/tui/composer_ui.rs +++ b/crates/tui/src/tui/composer_ui.rs @@ -126,8 +126,11 @@ pub(crate) fn is_word_cursor_modifier(modifiers: KeyModifiers) -> bool { /// On all other platforms this is a no-op. #[cfg(target_os = "macos")] pub(crate) fn normalize_macos_modifiers(modifiers: KeyModifiers) -> KeyModifiers { - if modifiers.contains(KeyModifiers::SUPER) && !modifiers.contains(KeyModifiers::CONTROL) { - modifiers | KeyModifiers::CONTROL + // Strip SUPER and add CONTROL so that exact modifier equality checks + // (e.g. `modifiers == KeyModifiers::CONTROL` in Ctrl+S stashing) work + // correctly after normalization. + if modifiers.contains(KeyModifiers::SUPER) { + (modifiers - KeyModifiers::SUPER) | KeyModifiers::CONTROL } else { modifiers } diff --git a/crates/tui/src/tui/ui/tests.rs b/crates/tui/src/tui/ui/tests.rs index bc2ec692..f76ae97a 100644 --- a/crates/tui/src/tui/ui/tests.rs +++ b/crates/tui/src/tui/ui/tests.rs @@ -301,19 +301,19 @@ fn word_cursor_modifier_accepts_control_and_alt() { #[test] fn normalize_macos_modifiers_maps_super_to_control() { use crate::tui::composer_ui::normalize_macos_modifiers; - // SUPER (Cmd) without CONTROL should gain CONTROL. + // SUPER (Cmd) without CONTROL should gain CONTROL and lose SUPER. let normalized = normalize_macos_modifiers(KeyModifiers::SUPER); assert!(normalized.contains(KeyModifiers::CONTROL)); - assert!(normalized.contains(KeyModifiers::SUPER)); + assert!(!normalized.contains(KeyModifiers::SUPER)); } #[test] fn normalize_macos_modifiers_preserves_existing_control() { use crate::tui::composer_ui::normalize_macos_modifiers; - // CONTROL already set — shouldn't be removed. + // CONTROL already set — SUPER should be removed. let normalized = normalize_macos_modifiers(KeyModifiers::CONTROL | KeyModifiers::SUPER); assert!(normalized.contains(KeyModifiers::CONTROL)); - assert!(normalized.contains(KeyModifiers::SUPER)); + assert!(!normalized.contains(KeyModifiers::SUPER)); } #[test] From 7315ef3dde209b1121d37bcd893111fc19e0aef4 Mon Sep 17 00:00:00 2001 From: Hanmiao Li <894876246@qq.com> Date: Wed, 10 Jun 2026 11:40:35 +0800 Subject: [PATCH 3/3] fix: gate macOS-specific modifier tests with #[cfg(target_os = "macos")] --- crates/tui/src/tui/ui/tests.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/tui/src/tui/ui/tests.rs b/crates/tui/src/tui/ui/tests.rs index f76ae97a..765d13bf 100644 --- a/crates/tui/src/tui/ui/tests.rs +++ b/crates/tui/src/tui/ui/tests.rs @@ -298,6 +298,7 @@ fn word_cursor_modifier_accepts_control_and_alt() { assert!(!is_word_cursor_modifier(KeyModifiers::SHIFT)); } +#[cfg(target_os = "macos")] #[test] fn normalize_macos_modifiers_maps_super_to_control() { use crate::tui::composer_ui::normalize_macos_modifiers; @@ -307,6 +308,7 @@ fn normalize_macos_modifiers_maps_super_to_control() { assert!(!normalized.contains(KeyModifiers::SUPER)); } +#[cfg(target_os = "macos")] #[test] fn normalize_macos_modifiers_preserves_existing_control() { use crate::tui::composer_ui::normalize_macos_modifiers;