diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/app/mod.rs | 23 | ||||
| -rw-r--r-- | src/input/parse.rs | 52 | ||||
| -rw-r--r-- | src/terminal_modes.rs | 7 |
3 files changed, 66 insertions, 16 deletions
diff --git a/src/app/mod.rs b/src/app/mod.rs index 501f39f6..75568990 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -5537,6 +5537,29 @@ last_pane = "prefix+tab" } #[tokio::test] + async fn kitty_associated_ime_text_bypasses_report_all_key_encoding() { + let mut app = test_app(); + let mut workspace = Workspace::test_new("test"); + let focused = workspace.focused_pane_id().unwrap(); + let (runtime, mut rx) = + TerminalRuntime::test_with_channel_and_scrollback_bytes(80, 24, 0, b"\x1b[>15u", 2); + workspace.tabs[0].runtimes.insert(focused, runtime); + app.state.workspaces = vec![workspace]; + app.state.active = Some(0); + app.state.selected = 0; + app.state.mode = Mode::Terminal; + + app.route_client_input(b"\x1b[32;;20320:22909u".to_vec()); + + assert_eq!( + rx.recv().await.unwrap(), + bytes::Bytes::from_static("你好".as_bytes()) + ); + assert!(rx.try_recv().is_err()); + assert!(app.input_leases.is_empty()); + } + + #[tokio::test] async fn committed_ascii_uppercase_bypasses_report_all_key_encoding() { let mut app = test_app(); let mut workspace = Workspace::test_new("test"); diff --git a/src/input/parse.rs b/src/input/parse.rs index b2318339..ce87fd2f 100644 --- a/src/input/parse.rs +++ b/src/input/parse.rs @@ -15,8 +15,14 @@ fn parse_kitty_key_sequence(data: &str) -> Option<TerminalKey> { let mut fields = body.split(';'); let key_part = fields.next()?; - let modifier_part = fields.next().unwrap_or("1"); - let associated_text = fields.next(); + let modifier_part = fields + .next() + .filter(|field| !field.is_empty()) + .unwrap_or("1"); + let associated_text = match fields.next() { + Some(value) => Some(parse_kitty_associated_text(value)?), + None => None, + }; if fields.next().is_some() { return None; } @@ -31,12 +37,6 @@ fn parse_kitty_key_sequence(data: &str) -> Option<TerminalKey> { .filter(|field| !field.is_empty()) .and_then(|field| field.parse::<u32>().ok()); - if let Some(text) = associated_text { - if text.parse::<u32>().ok()? != codepoint { - return None; - } - } - let code = kitty_codepoint_to_keycode(codepoint)?; let kind = parse_kitty_event_type(event_type)?; let mut modifiers = key_modifiers_from_u8(modifier); @@ -53,7 +53,19 @@ fn parse_kitty_key_sequence(data: &str) -> Option<TerminalKey> { if let Some(shifted_codepoint) = shifted_codepoint { key = key.with_shifted_codepoint(shifted_codepoint); } - Some(key) + Some(key.with_generated_text(associated_text)) +} + +fn parse_kitty_associated_text(value: &str) -> Option<String> { + let mut text = String::new(); + for codepoint in value.split(':') { + let ch = char::from_u32(codepoint.parse::<u32>().ok()?)?; + if ch.is_control() { + return None; + } + text.push(ch); + } + (!text.is_empty()).then_some(text) } #[allow(dead_code)] // Reserved for the upcoming raw stdin parser. @@ -671,15 +683,25 @@ mod tests { crossterm::event::KeyEventKind::Press, None, ); + assert_eq!(key.generated_text.as_deref(), Some("😀")); } #[test] - fn reject_unmodeled_kitty_associated_text() { - assert_eq!(parse_terminal_key_sequence("\x1b[128512;1;128513u"), None); - assert_eq!( - parse_terminal_key_sequence("\x1b[128512;1;128512:65039u"), - None - ); + fn parse_kitty_sequence_with_multicodepoint_ime_text() { + let key = parse_terminal_key_sequence("\x1b[32;;20320:22909u").unwrap(); + + assert_eq!(key.code, KeyCode::Char(' ')); + assert_eq!(key.modifiers, KeyModifiers::empty()); + assert_eq!(key.kind, crossterm::event::KeyEventKind::Press); + assert_eq!(key.generated_text.as_deref(), Some("你好")); + } + + #[test] + fn reject_malformed_kitty_associated_text() { + assert_eq!(parse_terminal_key_sequence("\x1b[32;;1114112u"), None); + assert_eq!(parse_terminal_key_sequence("\x1b[32;;20320:bad:u"), None); + assert_eq!(parse_terminal_key_sequence("\x1b[32;;27u"), None); + assert_eq!(parse_terminal_key_sequence("\x1b[32;;133u"), None); } #[test] diff --git a/src/terminal_modes.rs b/src/terminal_modes.rs index 90ec512b..5b6a5d3b 100644 --- a/src/terminal_modes.rs +++ b/src/terminal_modes.rs @@ -25,6 +25,11 @@ pub(crate) fn set_host_kitty_keyboard_report_all<W: Write>( let mut flags = crate::input::ime_compatible_keyboard_enhancement_flags(); if report_all_keys { flags |= crossterm::event::KeyboardEnhancementFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES; + // Report-all turns IME commits into CSI-u key events in terminals such + // as Ghostty. Ask the terminal to carry the committed text with them. + flags = crossterm::event::KeyboardEnhancementFlags::from_bits_retain( + flags.bits() | 0b0001_0000, + ); } // Older iTerm2 releases clear the keyboard stack on SET, so a later pop // cannot restore the host state. Replace only Herdr's top entry instead. @@ -54,7 +59,7 @@ mod tests { set_host_kitty_keyboard_report_all(&mut output, true).unwrap(); set_host_kitty_keyboard_report_all(&mut output, false).unwrap(); - assert_eq!(output, b"\x1b[<1u\x1b[>15u\x1b[<1u\x1b[>7u"); + assert_eq!(output, b"\x1b[<1u\x1b[>31u\x1b[<1u\x1b[>7u"); } #[test] |
