* 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 * fix: strip SUPER from modifiers after normalization per review * fix: gate macOS-specific modifier tests with #[cfg(target_os = "macos")]
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -3011,7 +3011,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;
|
||||
};
|
||||
|
||||
@@ -3019,6 +3019,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.
|
||||
|
||||
@@ -298,6 +298,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();
|
||||
|
||||
Reference in New Issue
Block a user