aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/app/mod.rs23
-rw-r--r--src/input/parse.rs52
-rw-r--r--src/terminal_modes.rs7
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]