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
This commit is contained in:
Hanmiao Li
2026-06-09 15:26:43 +08:00
parent 9463266cb1
commit 6ac14a246d
3 changed files with 52 additions and 1 deletions
+17
View File
@@ -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;
+8 -1
View File
@@ -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.
+27
View File
@@ -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();