Merge PR #2943: normalize macOS SUPER (Cmd) to CONTROL for keyboard shortcuts

Closes #2938. Verified on scratch/v0.8.59-clean-train-20260612: build and focused TUI modifier tests green.
This commit is contained in:
CodeWhale Agent
2026-06-12 09:59:53 -07:00
3 changed files with 57 additions and 1 deletions
+20
View File
@@ -121,6 +121,26 @@ 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 {
// 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
}
}
#[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
@@ -3022,7 +3022,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;
};
@@ -3030,6 +3030,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.
+29
View File
@@ -299,6 +299,35 @@ 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;
// 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));
}
#[cfg(target_os = "macos")]
#[test]
fn normalize_macos_modifiers_preserves_existing_control() {
use crate::tui::composer_ui::normalize_macos_modifiers;
// 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));
}
#[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();