diff --git a/cli/src/native/actions.rs b/cli/src/native/actions.rs index c3ad7e8..02c2174 100644 --- a/cli/src/native/actions.rs +++ b/cli/src/native/actions.rs @@ -2093,10 +2093,53 @@ async fn handle_press(cmd: &Value, state: &mut DaemonState) -> Result (String, Option) { + let parts: Vec<&str> = input.split('+').collect(); + if parts.len() < 2 { + return (input.to_string(), None); + } + + let mut modifiers = 0i32; + let mut key_parts: Vec<&str> = Vec::new(); + + for part in &parts { + match part.to_lowercase().as_str() { + "alt" => modifiers |= 1, + "control" | "ctrl" => modifiers |= 2, + "meta" | "cmd" | "command" => modifiers |= 4, + "shift" => modifiers |= 8, + _ => key_parts.push(part), + } + } + + // If no modifiers were found, the '+' was part of the key name (e.g. "+") + // or the input was something unexpected — treat the whole string as the key. + if modifiers == 0 { + return (input.to_string(), None); + } + + // The actual key is whatever remains after stripping modifiers. + // If nothing remains (e.g. "Control+"), treat the whole string as-is. + let actual_key = if key_parts.is_empty() { + input.to_string() + } else { + key_parts.join("+") + }; + + (actual_key, Some(modifiers)) +} + async fn handle_hover(cmd: &Value, state: &mut DaemonState) -> Result { let mgr = state.browser.as_ref().ok_or("Browser not launched")?; let session_id = mgr.active_session_id()?.to_string(); @@ -7279,4 +7322,68 @@ mod tests { selectors explicitly" ); } + + #[test] + fn test_parse_key_chord_plain_key() { + let (key, mods) = parse_key_chord("a"); + assert_eq!(key, "a"); + assert_eq!(mods, None); + } + + #[test] + fn test_parse_key_chord_enter() { + let (key, mods) = parse_key_chord("Enter"); + assert_eq!(key, "Enter"); + assert_eq!(mods, None); + } + + #[test] + fn test_parse_key_chord_control_a() { + let (key, mods) = parse_key_chord("Control+a"); + assert_eq!(key, "a"); + assert_eq!(mods, Some(2)); + } + + #[test] + fn test_parse_key_chord_ctrl_alias() { + let (key, mods) = parse_key_chord("Ctrl+c"); + assert_eq!(key, "c"); + assert_eq!(mods, Some(2)); + } + + #[test] + fn test_parse_key_chord_shift_enter() { + let (key, mods) = parse_key_chord("Shift+Enter"); + assert_eq!(key, "Enter"); + assert_eq!(mods, Some(8)); + } + + #[test] + fn test_parse_key_chord_control_shift_a() { + let (key, mods) = parse_key_chord("Control+Shift+a"); + assert_eq!(key, "a"); + assert_eq!(mods, Some(2 | 8)); + } + + #[test] + fn test_parse_key_chord_meta_a() { + let (key, mods) = parse_key_chord("Meta+a"); + assert_eq!(key, "a"); + assert_eq!(mods, Some(4)); + } + + #[test] + fn test_parse_key_chord_alt_tab() { + let (key, mods) = parse_key_chord("Alt+Tab"); + assert_eq!(key, "Tab"); + assert_eq!(mods, Some(1)); + } + + #[test] + fn test_parse_key_chord_plus_key() { + // A bare "+" should not be confused with a separator + let (key, mods) = parse_key_chord("+"); + assert_eq!(key, "+"); + assert_eq!(mods, None); + } } diff --git a/cli/src/native/interaction.rs b/cli/src/native/interaction.rs index 56c3a96..c18d226 100644 --- a/cli/src/native/interaction.rs +++ b/cli/src/native/interaction.rs @@ -282,7 +282,15 @@ pub async fn press_key_with_modifiers( modifiers: Option, ) -> Result<(), String> { let (key_name, code, key_code) = named_key_info(key); - let text = key_text(&key_name); + + // Suppress text insertion when Control (2) or Meta (4) modifiers are active, + // since these are command chords (e.g. Ctrl+A = select-all), not text input. + let has_command_modifier = modifiers.is_some_and(|m| m & (2 | 4) != 0); + let text = if has_command_modifier { + None + } else { + key_text(&key_name) + }; client .send_command_typed::<_, Value>(