From 261f0ea39645eba8494fe50c51dba39331d991ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toma=CC=81s=CC=8C=20Weiss?= Date: Thu, 30 Apr 2026 22:38:18 +0200 Subject: [PATCH 1/3] feat: add support for macOS modifier keys --- src/app/events/mod.rs | 16 +++++++++++----- src/app/keys.rs | 30 ++++++++++++++++++++---------- 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/src/app/events/mod.rs b/src/app/events/mod.rs index 9b8249fd..b86218b3 100644 --- a/src/app/events/mod.rs +++ b/src/app/events/mod.rs @@ -3392,12 +3392,14 @@ mod tests { fn ctrl_backspace_and_delete_use_word_operations() { let mut app = make_test_app(); app.input.set_text("hello world"); + let mod_key = + if cfg!(target_os = "macos") { KeyModifiers::ALT } else { KeyModifiers::CONTROL }; - handle_normal_key(&mut app, KeyEvent::new(KeyCode::Backspace, KeyModifiers::CONTROL)); + handle_normal_key(&mut app, KeyEvent::new(KeyCode::Backspace, mod_key)); assert_eq!(app.input.text(), "hello "); app.input.move_home(); - handle_normal_key(&mut app, KeyEvent::new(KeyCode::Delete, KeyModifiers::CONTROL)); + handle_normal_key(&mut app, KeyEvent::new(KeyCode::Delete, mod_key)); assert_eq!(app.input.text(), " "); } @@ -3405,14 +3407,18 @@ mod tests { fn ctrl_z_and_y_undo_and_redo_textarea_history() { let mut app = make_test_app(); app.input.set_text("hello world"); + let delete_mod_key = + if cfg!(target_os = "macos") { KeyModifiers::ALT } else { KeyModifiers::CONTROL }; + let history_mod_key = + if cfg!(target_os = "macos") { KeyModifiers::SUPER } else { KeyModifiers::CONTROL }; - handle_normal_key(&mut app, KeyEvent::new(KeyCode::Backspace, KeyModifiers::CONTROL)); + handle_normal_key(&mut app, KeyEvent::new(KeyCode::Backspace, delete_mod_key)); assert_eq!(app.input.text(), "hello "); - handle_normal_key(&mut app, KeyEvent::new(KeyCode::Char('z'), KeyModifiers::CONTROL)); + handle_normal_key(&mut app, KeyEvent::new(KeyCode::Char('z'), history_mod_key)); assert_eq!(app.input.text(), "hello world"); - handle_normal_key(&mut app, KeyEvent::new(KeyCode::Char('y'), KeyModifiers::CONTROL)); + handle_normal_key(&mut app, KeyEvent::new(KeyCode::Char('y'), history_mod_key)); assert_eq!(app.input.text(), "hello "); } diff --git a/src/app/keys.rs b/src/app/keys.rs index 42e44249..601cde37 100644 --- a/src/app/keys.rs +++ b/src/app/keys.rs @@ -438,9 +438,11 @@ fn handle_history_key(app: &mut App, key: KeyEvent) -> bool { if app.focus_owner() == FocusOwner::TodoList { return false; } + let mod_key = + if cfg!(target_os = "macos") { KeyModifiers::SUPER } else { KeyModifiers::CONTROL }; match (key.code, key.modifiers) { - (KeyCode::Char('z'), m) if m == KeyModifiers::CONTROL => app.input.textarea_undo(), - (KeyCode::Char('y'), m) if m == KeyModifiers::CONTROL => app.input.textarea_redo(), + (KeyCode::Char('z'), m) if m == mod_key => app.input.textarea_undo(), + (KeyCode::Char('y'), m) if m == mod_key => app.input.textarea_redo(), _ => false, } } @@ -449,15 +451,19 @@ fn handle_navigation_key(app: &mut App, key: KeyEvent) -> bool { match (key.code, key.modifiers) { (KeyCode::Left, m) if app.focus_owner() != FocusOwner::TodoList - && m.contains(KeyModifiers::CONTROL) - && !m.contains(KeyModifiers::ALT) => + && (cfg!(target_os = "macos") && m.contains(KeyModifiers::ALT) + || !cfg!(target_os = "macos") + && m.contains(KeyModifiers::CONTROL) + && !m.contains(KeyModifiers::ALT)) => { app.input.textarea_move_word_left() } (KeyCode::Right, m) if app.focus_owner() != FocusOwner::TodoList - && m.contains(KeyModifiers::CONTROL) - && !m.contains(KeyModifiers::ALT) => + && (cfg!(target_os = "macos") && m.contains(KeyModifiers::ALT) + || !cfg!(target_os = "macos") + && m.contains(KeyModifiers::CONTROL) + && !m.contains(KeyModifiers::ALT)) => { app.input.textarea_move_word_right() } @@ -677,8 +683,10 @@ fn handle_editing_key(app: &mut App, key: KeyEvent) -> bool { match (key.code, key.modifiers) { (KeyCode::Backspace, m) if app.focus_owner() != FocusOwner::TodoList - && m.contains(KeyModifiers::CONTROL) - && !m.contains(KeyModifiers::ALT) => + && (cfg!(target_os = "macos") && m.contains(KeyModifiers::ALT) + || !cfg!(target_os = "macos") + && m.contains(KeyModifiers::CONTROL) + && !m.contains(KeyModifiers::ALT)) => { reclaim_input_from_inline_prompt_if_needed(app); if try_delete_image_badge(app, "before") { @@ -688,8 +696,10 @@ fn handle_editing_key(app: &mut App, key: KeyEvent) -> bool { } (KeyCode::Delete, m) if app.focus_owner() != FocusOwner::TodoList - && m.contains(KeyModifiers::CONTROL) - && !m.contains(KeyModifiers::ALT) => + && (cfg!(target_os = "macos") && m.contains(KeyModifiers::ALT) + || !cfg!(target_os = "macos") + && m.contains(KeyModifiers::CONTROL) + && !m.contains(KeyModifiers::ALT)) => { reclaim_input_from_inline_prompt_if_needed(app); if try_delete_image_badge(app, "after") { From 99f17011d3e9cfdd2d6b8bf4dd76805adb453fc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Weiss?= Date: Mon, 4 May 2026 00:39:54 +0200 Subject: [PATCH 2/3] feat: adapt history shortcuts for macOS --- src/app/events/mod.rs | 12 ++++++++---- src/app/keys.rs | 24 ++++++++++++++++++++---- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/app/events/mod.rs b/src/app/events/mod.rs index b86218b3..d76200c7 100644 --- a/src/app/events/mod.rs +++ b/src/app/events/mod.rs @@ -3409,16 +3409,20 @@ mod tests { app.input.set_text("hello world"); let delete_mod_key = if cfg!(target_os = "macos") { KeyModifiers::ALT } else { KeyModifiers::CONTROL }; - let history_mod_key = - if cfg!(target_os = "macos") { KeyModifiers::SUPER } else { KeyModifiers::CONTROL }; handle_normal_key(&mut app, KeyEvent::new(KeyCode::Backspace, delete_mod_key)); assert_eq!(app.input.text(), "hello "); - handle_normal_key(&mut app, KeyEvent::new(KeyCode::Char('z'), history_mod_key)); + #[cfg(target_os = "macos")] + handle_normal_key(&mut app, KeyEvent::new(KeyCode::Char('z'), KeyModifiers::SUPER)); + #[cfg(not(target_os = "macos"))] + handle_normal_key(&mut app, KeyEvent::new(KeyCode::Char('z'), KeyModifiers::CONTROL)); assert_eq!(app.input.text(), "hello world"); - handle_normal_key(&mut app, KeyEvent::new(KeyCode::Char('y'), history_mod_key)); + #[cfg(target_os = "macos")] + handle_normal_key(&mut app, KeyEvent::new(KeyCode::Char('Z'), KeyModifiers::SUPER)); + #[cfg(not(target_os = "macos"))] + handle_normal_key(&mut app, KeyEvent::new(KeyCode::Char('y'), KeyModifiers::CONTROL)); assert_eq!(app.input.text(), "hello "); } diff --git a/src/app/keys.rs b/src/app/keys.rs index 601cde37..756e3a1e 100644 --- a/src/app/keys.rs +++ b/src/app/keys.rs @@ -438,11 +438,27 @@ fn handle_history_key(app: &mut App, key: KeyEvent) -> bool { if app.focus_owner() == FocusOwner::TodoList { return false; } - let mod_key = - if cfg!(target_os = "macos") { KeyModifiers::SUPER } else { KeyModifiers::CONTROL }; match (key.code, key.modifiers) { - (KeyCode::Char('z'), m) if m == mod_key => app.input.textarea_undo(), - (KeyCode::Char('y'), m) if m == mod_key => app.input.textarea_redo(), + #[cfg(target_os = "macos")] + (KeyCode::Char('z'), m) if m == KeyModifiers::SUPER => { + app.input.textarea_undo(); + true + } + #[cfg(target_os = "macos")] + (KeyCode::Char('Z'), m) if m == KeyModifiers::SUPER => { + app.input.textarea_redo(); + true + } + #[cfg(not(target_os = "macos"))] + (KeyCode::Char('z'), m) if m == KeyModifiers::META => { + app.input.textarea_undo(); + true + } + #[cfg(not(target_os = "macos"))] + (KeyCode::Char('y'), m) if m == KeyModifiers::META => { + app.input.textarea_redo(); + true + } _ => false, } } From 4fc0def37cfce5240d78be33f0d0822eb8948ef5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Weiss?= Date: Mon, 4 May 2026 09:48:21 +0200 Subject: [PATCH 3/3] fix: rework modifier key handling to global constants --- src/app/events/mod.rs | 25 +++++++++------------ src/app/keys.rs | 52 ++++++++++++++++++++++--------------------- 2 files changed, 37 insertions(+), 40 deletions(-) diff --git a/src/app/events/mod.rs b/src/app/events/mod.rs index d76200c7..c3fd7e89 100644 --- a/src/app/events/mod.rs +++ b/src/app/events/mod.rs @@ -19,6 +19,8 @@ use super::{ }; use crate::agent::model; use crate::app::keys::reclaim_input_from_inline_prompt_if_needed; +#[cfg(test)] +use crate::app::keys::{CMD_MOD, WORD_NAV_MOD}; use crate::app::todos::apply_plan_todos; #[cfg(test)] use crossterm::event::KeyEvent; @@ -3392,14 +3394,12 @@ mod tests { fn ctrl_backspace_and_delete_use_word_operations() { let mut app = make_test_app(); app.input.set_text("hello world"); - let mod_key = - if cfg!(target_os = "macos") { KeyModifiers::ALT } else { KeyModifiers::CONTROL }; - handle_normal_key(&mut app, KeyEvent::new(KeyCode::Backspace, mod_key)); + handle_normal_key(&mut app, KeyEvent::new(KeyCode::Backspace, WORD_NAV_MOD)); assert_eq!(app.input.text(), "hello "); app.input.move_home(); - handle_normal_key(&mut app, KeyEvent::new(KeyCode::Delete, mod_key)); + handle_normal_key(&mut app, KeyEvent::new(KeyCode::Delete, WORD_NAV_MOD)); assert_eq!(app.input.text(), " "); } @@ -3407,22 +3407,17 @@ mod tests { fn ctrl_z_and_y_undo_and_redo_textarea_history() { let mut app = make_test_app(); app.input.set_text("hello world"); - let delete_mod_key = - if cfg!(target_os = "macos") { KeyModifiers::ALT } else { KeyModifiers::CONTROL }; - handle_normal_key(&mut app, KeyEvent::new(KeyCode::Backspace, delete_mod_key)); + handle_normal_key(&mut app, KeyEvent::new(KeyCode::Backspace, WORD_NAV_MOD)); assert_eq!(app.input.text(), "hello "); - #[cfg(target_os = "macos")] - handle_normal_key(&mut app, KeyEvent::new(KeyCode::Char('z'), KeyModifiers::SUPER)); - #[cfg(not(target_os = "macos"))] - handle_normal_key(&mut app, KeyEvent::new(KeyCode::Char('z'), KeyModifiers::CONTROL)); + handle_normal_key(&mut app, KeyEvent::new(KeyCode::Char('z'), CMD_MOD)); assert_eq!(app.input.text(), "hello world"); #[cfg(target_os = "macos")] - handle_normal_key(&mut app, KeyEvent::new(KeyCode::Char('Z'), KeyModifiers::SUPER)); + handle_normal_key(&mut app, KeyEvent::new(KeyCode::Char('Z'), CMD_MOD)); #[cfg(not(target_os = "macos"))] - handle_normal_key(&mut app, KeyEvent::new(KeyCode::Char('y'), KeyModifiers::CONTROL)); + handle_normal_key(&mut app, KeyEvent::new(KeyCode::Char('y'), CMD_MOD)); assert_eq!(app.input.text(), "hello "); } @@ -3432,10 +3427,10 @@ mod tests { app.input.set_text("hello world"); app.input.move_home(); - handle_normal_key(&mut app, KeyEvent::new(KeyCode::Right, KeyModifiers::CONTROL)); + handle_normal_key(&mut app, KeyEvent::new(KeyCode::Right, WORD_NAV_MOD)); assert!(app.input.cursor_col() > 0); - handle_normal_key(&mut app, KeyEvent::new(KeyCode::Left, KeyModifiers::CONTROL)); + handle_normal_key(&mut app, KeyEvent::new(KeyCode::Left, WORD_NAV_MOD)); assert_eq!(app.input.cursor_col(), 0); } diff --git a/src/app/keys.rs b/src/app/keys.rs index 756e3a1e..8e105fca 100644 --- a/src/app/keys.rs +++ b/src/app/keys.rs @@ -24,6 +24,21 @@ use std::time::Instant; const HELP_TAB_PREV_KEY: KeyCode = KeyCode::Left; const HELP_TAB_NEXT_KEY: KeyCode = KeyCode::Right; +#[cfg(target_os = "macos")] +pub(crate) const CMD_MOD: KeyModifiers = KeyModifiers::SUPER; +#[cfg(not(target_os = "macos"))] +pub(crate) const CMD_MOD: KeyModifiers = KeyModifiers::CONTROL; + +#[cfg(target_os = "macos")] +pub(crate) const WORD_NAV_MOD: KeyModifiers = KeyModifiers::ALT; +#[cfg(not(target_os = "macos"))] +pub(crate) const WORD_NAV_MOD: KeyModifiers = KeyModifiers::CONTROL; + +#[cfg(target_os = "macos")] +pub(crate) const WORD_NAV_MOD_EXCLUDED: KeyModifiers = KeyModifiers::empty(); +#[cfg(not(target_os = "macos"))] +pub(crate) const WORD_NAV_MOD_EXCLUDED: KeyModifiers = KeyModifiers::ALT; + fn is_ctrl_shortcut(modifiers: KeyModifiers) -> bool { modifiers.contains(KeyModifiers::CONTROL) && !modifiers.contains(KeyModifiers::ALT) } @@ -439,23 +454,18 @@ fn handle_history_key(app: &mut App, key: KeyEvent) -> bool { return false; } match (key.code, key.modifiers) { - #[cfg(target_os = "macos")] - (KeyCode::Char('z'), m) if m == KeyModifiers::SUPER => { + (KeyCode::Char('z'), m) if m == CMD_MOD => { app.input.textarea_undo(); true } + #[cfg(target_os = "macos")] - (KeyCode::Char('Z'), m) if m == KeyModifiers::SUPER => { + (KeyCode::Char('Z'), m) if m == CMD_MOD => { app.input.textarea_redo(); true } #[cfg(not(target_os = "macos"))] - (KeyCode::Char('z'), m) if m == KeyModifiers::META => { - app.input.textarea_undo(); - true - } - #[cfg(not(target_os = "macos"))] - (KeyCode::Char('y'), m) if m == KeyModifiers::META => { + (KeyCode::Char('y'), m) if m == CMD_MOD => { app.input.textarea_redo(); true } @@ -467,19 +477,15 @@ fn handle_navigation_key(app: &mut App, key: KeyEvent) -> bool { match (key.code, key.modifiers) { (KeyCode::Left, m) if app.focus_owner() != FocusOwner::TodoList - && (cfg!(target_os = "macos") && m.contains(KeyModifiers::ALT) - || !cfg!(target_os = "macos") - && m.contains(KeyModifiers::CONTROL) - && !m.contains(KeyModifiers::ALT)) => + && m.contains(WORD_NAV_MOD) + && !m.intersects(WORD_NAV_MOD_EXCLUDED) => { app.input.textarea_move_word_left() } (KeyCode::Right, m) if app.focus_owner() != FocusOwner::TodoList - && (cfg!(target_os = "macos") && m.contains(KeyModifiers::ALT) - || !cfg!(target_os = "macos") - && m.contains(KeyModifiers::CONTROL) - && !m.contains(KeyModifiers::ALT)) => + && m.contains(WORD_NAV_MOD) + && !m.intersects(WORD_NAV_MOD_EXCLUDED) => { app.input.textarea_move_word_right() } @@ -699,10 +705,8 @@ fn handle_editing_key(app: &mut App, key: KeyEvent) -> bool { match (key.code, key.modifiers) { (KeyCode::Backspace, m) if app.focus_owner() != FocusOwner::TodoList - && (cfg!(target_os = "macos") && m.contains(KeyModifiers::ALT) - || !cfg!(target_os = "macos") - && m.contains(KeyModifiers::CONTROL) - && !m.contains(KeyModifiers::ALT)) => + && m.contains(WORD_NAV_MOD) + && !m.intersects(WORD_NAV_MOD_EXCLUDED) => { reclaim_input_from_inline_prompt_if_needed(app); if try_delete_image_badge(app, "before") { @@ -712,10 +716,8 @@ fn handle_editing_key(app: &mut App, key: KeyEvent) -> bool { } (KeyCode::Delete, m) if app.focus_owner() != FocusOwner::TodoList - && (cfg!(target_os = "macos") && m.contains(KeyModifiers::ALT) - || !cfg!(target_os = "macos") - && m.contains(KeyModifiers::CONTROL) - && !m.contains(KeyModifiers::ALT)) => + && m.contains(WORD_NAV_MOD) + && !m.intersects(WORD_NAV_MOD_EXCLUDED) => { reclaim_input_from_inline_prompt_if_needed(app); if try_delete_image_badge(app, "after") {