T08: keys: xterm and kitty encoding, Option as Alt by side, IME composition at the cursor; shot types real AppKit keys
CI / platforms (windows-latest) (push) Failing after 9s
CI / macos (push) Failing after 8m31s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s

This commit is contained in:
ZacharyZhang-NY committed 2026-10-14 14:22:52 -04:00
1 parent 9cecb85d67
commit 25365a98c8
15 files changed
+1949 -28

No files matched your search

+260
View File
@@ -0,0 +1,260 @@
use super::*;
/// A key pressed with modifiers written as `ctrl+alt+shift+name`.
fn key(spec: &'static str, text: &'static str) -> Key<'static> {
let mut parts: Vec<&str> = spec.split('+').collect();
let name = parts.pop().expect("a name");
Key {
name,
text,
shift: parts.contains(&"shift"),
ctrl: parts.contains(&"ctrl"),
alt: parts.contains(&"alt"),
action: Action::Press,
}
}
fn legacy(key: Key, app_cursor: bool) -> Option<String> {
encode(&key, app_cursor, Kitty::default()).map(|bytes| String::from_utf8(bytes).expect("utf-8"))
}
fn kitty(key: Key, flags: Kitty) -> Option<String> {
encode(&key, false, flags).map(|bytes| String::from_utf8(bytes).expect("utf-8"))
}
const DISAMBIGUATE: Kitty = Kitty {
disambiguate: true,
events: false,
alternates: false,
all: false,
text: false,
};
#[test]
fn plain_text_is_left_to_the_input_method() {
assert_eq!(legacy(key("a", "a"), false), None);
assert_eq!(legacy(key("shift+a", "A"), false), None);
assert_eq!(legacy(key("space", " "), false), None);
assert_eq!(legacy(key("ctrl+a", ""), false).as_deref(), Some("\x01"));
let released = Key {
action: Action::Release,
..key("enter", "\n")
};
assert_eq!(legacy(released, false), None, "legacy sends no releases");
}
#[test]
fn cursor_keys_follow_decckm_and_modifiers() {
assert_eq!(legacy(key("up", ""), false).as_deref(), Some("\x1b[A"));
assert_eq!(legacy(key("up", ""), true).as_deref(), Some("\x1bOA"));
assert_eq!(
legacy(key("ctrl+up", ""), true).as_deref(),
Some("\x1b[1;5A")
);
assert_eq!(
legacy(key("alt+shift+left", ""), false).as_deref(),
Some("\x1b[1;4D")
);
assert_eq!(legacy(key("home", ""), true).as_deref(), Some("\x1bOH"));
assert_eq!(legacy(key("end", ""), false).as_deref(), Some("\x1b[F"));
}
#[test]
fn function_and_editing_keys_send_xterm_sequences() {
let cases = [
("f1", "\x1bOP"),
("ctrl+f1", "\x1b[1;5P"),
("f3", "\x1bOR"),
("shift+f3", "\x1b[13;2~"),
("f4", "\x1bOS"),
("f5", "\x1b[15~"),
("shift+f5", "\x1b[15;2~"),
("f12", "\x1b[24~"),
("f25", "\x1b[46~"),
("insert", "\x1b[2~"),
("delete", "\x1b[3~"),
("ctrl+pageup", "\x1b[5;5~"),
("pagedown", "\x1b[6~"),
];
for (spec, want) in cases {
assert_eq!(
legacy(key(spec, ""), false).as_deref(),
Some(want),
"{spec}"
);
}
assert_eq!(legacy(key("f26", ""), false), None);
}
#[test]
fn enter_tab_escape_and_backspace_carry_their_modifiers() {
let cases: [(&str, &str, &[u8]); 15] = [
("enter", "\n", b"\r"),
("shift+enter", "\n", b"\x1b[27;2;13~"),
("alt+enter", "\n", b"\x1b\r"),
("ctrl+enter", "", b"\x1b[27;5;13~"),
("tab", "\t", b"\t"),
("shift+tab", "", b"\x1b[Z"),
("alt+tab", "\t", b"\x1b\t"),
("ctrl+tab", "", b"\x1b[27;5;9~"),
("escape", "", b"\x1b"),
("alt+escape", "", b"\x1b\x1b"),
("shift+escape", "", b"\x1b[27;2;27~"),
("backspace", "", b"\x7f"),
("ctrl+backspace", "", b"\x08"),
("alt+backspace", "", b"\x1b\x7f"),
("ctrl+alt+backspace", "", b"\x1b\x08"),
];
for (spec, text, want) in cases {
assert_eq!(
encode(&key(spec, text), false, Kitty::default()).as_deref(),
Some(want),
"{spec}"
);
}
}
#[test]
fn ctrl_makes_c0_bytes_or_fixterms_csi_u() {
let bytes = |spec| encode(&key(spec, ""), false, Kitty::default());
assert_eq!(bytes("ctrl+c"), Some(vec![0x03]));
assert_eq!(bytes("ctrl+alt+c"), Some(vec![0x1B, 0x03]));
assert_eq!(bytes("ctrl+space"), Some(vec![0x00]));
assert_eq!(bytes("ctrl+_"), Some(vec![31]));
assert_eq!(bytes("ctrl+?"), Some(vec![127]));
assert_eq!(bytes("ctrl+2"), Some(vec![0x00]));
assert_eq!(bytes("ctrl+["), Some(b"\x1b[91;5u".to_vec()));
assert_eq!(bytes("ctrl+i"), Some(b"\x1b[105;5u".to_vec()));
assert_eq!(bytes("ctrl+m"), Some(b"\x1b[109;5u".to_vec()));
assert_eq!(bytes("ctrl+shift+a"), Some(b"\x1b[97;6u".to_vec()));
}
#[test]
fn alt_prefixes_escape_to_what_the_key_types() {
assert_eq!(legacy(key("alt+a", "a"), false).as_deref(), Some("\x1ba"));
assert_eq!(
legacy(key("alt+shift+a", ""), false).as_deref(),
Some("\x1bA")
);
assert_eq!(legacy(key("alt+e", "é"), false).as_deref(), Some("\x1bé"));
assert_eq!(
legacy(key("alt+space", " "), false).as_deref(),
Some("\x1b ")
);
}
#[test]
fn kitty_disambiguate_keeps_text_and_the_legacy_three() {
assert_eq!(kitty(key("a", "a"), DISAMBIGUATE), None);
assert_eq!(kitty(key("shift+a", "A"), DISAMBIGUATE), None);
assert_eq!(
kitty(key("enter", "\n"), DISAMBIGUATE).as_deref(),
Some("\r")
);
assert_eq!(kitty(key("tab", "\t"), DISAMBIGUATE).as_deref(), Some("\t"));
assert_eq!(
kitty(key("backspace", ""), DISAMBIGUATE).as_deref(),
Some("\x7f")
);
assert_eq!(
kitty(key("escape", ""), DISAMBIGUATE).as_deref(),
Some("\x1b[27u")
);
}
#[test]
fn kitty_reports_modified_keys_as_csi_u() {
let cases = [
("ctrl+a", "", "\x1b[97;5u"),
("alt+a", "a", "\x1b[97;3u"),
("ctrl+shift+a", "", "\x1b[97;6u"),
("shift+enter", "\n", "\x1b[13;2u"),
("shift+tab", "", "\x1b[9;2u"),
("up", "", "\x1b[A"),
("ctrl+up", "", "\x1b[1;5A"),
("f1", "", "\x1b[P"),
("f5", "", "\x1b[15~"),
("ctrl+f5", "", "\x1b[15;5~"),
("f13", "", "\x1b[57376u"),
("f25", "", "\x1b[57388u"),
];
for (spec, text, want) in cases {
assert_eq!(
kitty(key(spec, text), DISAMBIGUATE).as_deref(),
Some(want),
"{spec}"
);
}
}
#[test]
fn kitty_events_report_repeats_and_releases() {
let flags = Kitty {
events: true,
..DISAMBIGUATE
};
let with = |spec, text, action| Key {
action,
..key(spec, text)
};
assert_eq!(
kitty(with("a", "a", Action::Repeat), flags),
None,
"a repeat types again"
);
assert_eq!(
kitty(with("a", "a", Action::Release), flags).as_deref(),
Some("\x1b[97;1:3u")
);
assert_eq!(
kitty(with("ctrl+a", "", Action::Repeat), flags).as_deref(),
Some("\x1b[97;5:2u")
);
assert_eq!(kitty(with("enter", "\n", Action::Release), flags), None);
assert_eq!(
kitty(with("up", "", Action::Release), flags).as_deref(),
Some("\x1b[1;1:3A")
);
assert_eq!(kitty(with("a", "a", Action::Release), DISAMBIGUATE), None);
}
#[test]
fn kitty_report_all_alternates_and_text() {
let all = Kitty {
all: true,
..DISAMBIGUATE
};
assert_eq!(kitty(key("a", "a"), all).as_deref(), Some("\x1b[97u"));
assert_eq!(
kitty(key("shift+a", "A"), all).as_deref(),
Some("\x1b[97;2u")
);
assert_eq!(kitty(key("enter", "\n"), all).as_deref(), Some("\x1b[13u"));
let alternates = Kitty {
alternates: true,
..all
};
assert_eq!(
kitty(key("shift+a", "A"), alternates).as_deref(),
Some("\x1b[97:65;2u")
);
let text = Kitty { text: true, ..all };
assert_eq!(kitty(key("a", "a"), text).as_deref(), Some("\x1b[97;;97u"));
assert_eq!(
kitty(key("shift+a", "A"), text).as_deref(),
Some("\x1b[97;2;65u")
);
assert_eq!(
kitty(key("ctrl+a", ""), text).as_deref(),
Some("\x1b[97;5u")
);
let released = Key {
action: Action::Release,
..key("enter", "\n")
};
let everything = Kitty {
events: true,
..all
};
assert_eq!(kitty(released, everything).as_deref(), Some("\x1b[13;1:3u"));
}