Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/next/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
- Experimental pane graphics now support bounded named layers, acknowledged full-RGBA primary-layer direct file frames on audited local terminals, owned BGRA fallback, exact pixel mouse input, and placement-only resize replay.

### Fixed
- Chinese IME commits now reach panes on macOS when the focused application requests printable key-release events. (#2924)
- Foreground typing no longer waits behind render cadence consumed by output from panes in hidden tabs. (#2890)
- The Windows ARM64 installer now waits for x64 emulation to release the verified executable before activating the downloaded release. (#2916)
- On Unix, Ctrl-click URL openers are now reaped after they exit, preventing defunct child processes from accumulating on long-running servers. (#2903)
Expand Down
23 changes: 23 additions & 0 deletions src/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5536,6 +5536,29 @@ last_pane = "prefix+tab"
assert!(app.input_leases.is_empty());
}

#[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());
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

#[tokio::test]
async fn committed_ascii_uppercase_bypasses_report_all_key_encoding() {
let mut app = test_app();
Expand Down
52 changes: 37 additions & 15 deletions src/input/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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);
Expand All @@ -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)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

#[allow(dead_code)] // Reserved for the upcoming raw stdin parser.
Expand Down Expand Up @@ -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]
Expand Down
7 changes: 6 additions & 1 deletion src/terminal_modes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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]
Expand Down
Loading