aboutsummaryrefslogtreecommitdiff
path: root/src/input/tests.rs
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-03-30 13:01:32 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-03-30 13:01:52 +0300
commit741fbbe8b30774247f8a68831d8df469672e1947 (patch)
tree608afe0a8415ea1a3ee2035f809795dc09809dfc /src/input/tests.rs
parentca14500717864b1c9b1885cbbc6a451a19b9caf8 (diff)
refactor: split terminal keyboard protocol module
Diffstat (limited to 'src/input/tests.rs')
-rw-r--r--src/input/tests.rs634
1 files changed, 634 insertions, 0 deletions
diff --git a/src/input/tests.rs b/src/input/tests.rs
new file mode 100644
index 00000000..ec8fdf63
--- /dev/null
+++ b/src/input/tests.rs
@@ -0,0 +1,634 @@
+use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
+
+use super::{
+ encode_key, encode_terminal_key, parse_terminal_key_sequence, KeyboardProtocol, TerminalKey,
+};
+
+fn assert_terminal_key_eq(
+ actual: TerminalKey,
+ code: KeyCode,
+ modifiers: KeyModifiers,
+ kind: crossterm::event::KeyEventKind,
+ shifted_codepoint: Option<u32>,
+) {
+ assert_eq!(actual.code, code);
+ assert_eq!(actual.modifiers, modifiers);
+ assert_eq!(actual.kind, kind);
+ assert_eq!(actual.shifted_codepoint, shifted_codepoint);
+}
+
+fn decode_hex(hex: &str) -> Vec<u8> {
+ let hex = hex.trim();
+ assert_eq!(hex.len() % 2, 0, "hex string must have even length");
+ (0..hex.len())
+ .step_by(2)
+ .map(|idx| u8::from_str_radix(&hex[idx..idx + 2], 16).unwrap())
+ .collect()
+}
+
+fn parse_fixture_key_code(value: &str) -> KeyCode {
+ match value {
+ "enter" => KeyCode::Enter,
+ "tab" => KeyCode::Tab,
+ "backspace" => KeyCode::Backspace,
+ "esc" => KeyCode::Esc,
+ "up" => KeyCode::Up,
+ "down" => KeyCode::Down,
+ "left" => KeyCode::Left,
+ "right" => KeyCode::Right,
+ "home" => KeyCode::Home,
+ "end" => KeyCode::End,
+ "pageup" => KeyCode::PageUp,
+ "pagedown" => KeyCode::PageDown,
+ "insert" => KeyCode::Insert,
+ "delete" => KeyCode::Delete,
+ value if value.starts_with("char:") => {
+ KeyCode::Char(value.trim_start_matches("char:").chars().next().unwrap())
+ }
+ other => panic!("unsupported fixture key code: {other}"),
+ }
+}
+
+fn parse_fixture_modifiers(value: &str) -> KeyModifiers {
+ if value == "-" || value.is_empty() {
+ return KeyModifiers::empty();
+ }
+
+ let mut modifiers = KeyModifiers::empty();
+ for part in value.split('+') {
+ match part {
+ "shift" => modifiers |= KeyModifiers::SHIFT,
+ "alt" => modifiers |= KeyModifiers::ALT,
+ "control" => modifiers |= KeyModifiers::CONTROL,
+ "super" => modifiers |= KeyModifiers::SUPER,
+ "hyper" => modifiers |= KeyModifiers::HYPER,
+ "meta" => modifiers |= KeyModifiers::META,
+ other => panic!("unsupported fixture modifier: {other}"),
+ }
+ }
+ modifiers
+}
+
+fn parse_fixture_kind(value: &str) -> crossterm::event::KeyEventKind {
+ match value {
+ "press" => crossterm::event::KeyEventKind::Press,
+ "repeat" => crossterm::event::KeyEventKind::Repeat,
+ "release" => crossterm::event::KeyEventKind::Release,
+ other => panic!("unsupported fixture kind: {other}"),
+ }
+}
+
+#[test]
+fn legacy_enter() {
+ let key = KeyEvent::new(KeyCode::Enter, KeyModifiers::empty());
+ assert_eq!(encode_key(key, KeyboardProtocol::Legacy), vec![b'\r']);
+}
+
+#[test]
+fn legacy_ctrl_c() {
+ let key = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL);
+ assert_eq!(encode_key(key, KeyboardProtocol::Legacy), vec![3]);
+}
+
+#[test]
+fn legacy_shift_enter_is_just_cr() {
+ let key = KeyEvent::new(KeyCode::Enter, KeyModifiers::SHIFT);
+ // Enter/Tab/Backspace/Esc aren't special keys with xterm modifier encoding,
+ // so Shift+Enter falls through to legacy which just sends CR
+ assert_eq!(encode_key(key, KeyboardProtocol::Legacy), vec![b'\r']);
+}
+
+#[test]
+fn legacy_alt_up() {
+ let key = KeyEvent::new(KeyCode::Up, KeyModifiers::ALT);
+ // xterm modified key format: CSI 1;3A (3 = 1 + Alt)
+ assert_eq!(encode_key(key, KeyboardProtocol::Legacy), b"\x1b[1;3A");
+}
+
+#[test]
+fn legacy_shift_right() {
+ let key = KeyEvent::new(KeyCode::Right, KeyModifiers::SHIFT);
+ assert_eq!(encode_key(key, KeyboardProtocol::Legacy), b"\x1b[1;2C");
+}
+
+#[test]
+fn legacy_ctrl_left() {
+ let key = KeyEvent::new(KeyCode::Left, KeyModifiers::CONTROL);
+ assert_eq!(encode_key(key, KeyboardProtocol::Legacy), b"\x1b[1;5D");
+}
+
+#[test]
+fn legacy_ctrl_shift_end() {
+ let key = KeyEvent::new(KeyCode::End, KeyModifiers::CONTROL | KeyModifiers::SHIFT);
+ assert_eq!(encode_key(key, KeyboardProtocol::Legacy), b"\x1b[1;6F");
+}
+
+#[test]
+fn legacy_alt_delete() {
+ let key = KeyEvent::new(KeyCode::Delete, KeyModifiers::ALT);
+ assert_eq!(encode_key(key, KeyboardProtocol::Legacy), b"\x1b[3;3~");
+}
+
+#[test]
+fn legacy_shift_f5() {
+ let key = KeyEvent::new(KeyCode::F(5), KeyModifiers::SHIFT);
+ assert_eq!(encode_key(key, KeyboardProtocol::Legacy), b"\x1b[15;2~");
+}
+
+#[test]
+fn legacy_alt_char_still_esc_prefix() {
+ // Alt+a on character keys still uses ESC prefix (not xterm modified)
+ let key = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::ALT);
+ assert_eq!(encode_key(key, KeyboardProtocol::Legacy), b"\x1ba");
+}
+
+#[test]
+fn kitty_shift_enter() {
+ let key = KeyEvent::new(KeyCode::Enter, KeyModifiers::SHIFT);
+ assert_eq!(
+ encode_key(key, KeyboardProtocol::Kitty { flags: 1 }),
+ b"\x1b[13;2u"
+ );
+}
+
+#[test]
+fn kitty_ctrl_shift_a() {
+ let key = KeyEvent::new(
+ KeyCode::Char('a'),
+ KeyModifiers::CONTROL | KeyModifiers::SHIFT,
+ );
+ assert_eq!(
+ encode_key(key, KeyboardProtocol::Kitty { flags: 1 }),
+ b"\x1b[97;6u"
+ );
+}
+
+#[test]
+fn kitty_shift_uppercase_letter_uses_base_codepoint() {
+ let key = KeyEvent::new(KeyCode::Char('L'), KeyModifiers::SHIFT);
+ assert_eq!(
+ encode_key(key, KeyboardProtocol::Kitty { flags: 1 }),
+ b"\x1b[108;2u"
+ );
+}
+
+#[test]
+fn kitty_shift_uppercase_letter_with_alternate_keys_reports_shifted_codepoint() {
+ let key = KeyEvent::new(KeyCode::Char('L'), KeyModifiers::SHIFT);
+ assert_eq!(
+ encode_key(key, KeyboardProtocol::Kitty { flags: 7 }),
+ b"\x1b[108:76;2:1u"
+ );
+}
+
+#[test]
+fn kitty_shift_lowercase_letter_normalizes_to_base_codepoint() {
+ let key = KeyEvent::new(KeyCode::Char('l'), KeyModifiers::SHIFT);
+ assert_eq!(
+ encode_key(key, KeyboardProtocol::Kitty { flags: 1 }),
+ b"\x1b[108;2u"
+ );
+}
+
+#[test]
+fn kitty_alt_shift_uppercase_letter_uses_base_codepoint() {
+ let key = KeyEvent::new(KeyCode::Char('L'), KeyModifiers::ALT | KeyModifiers::SHIFT);
+ assert_eq!(
+ encode_key(key, KeyboardProtocol::Kitty { flags: 1 }),
+ b"\x1b[108;4u"
+ );
+}
+
+#[test]
+fn kitty_ctrl_shift_uppercase_letter_uses_base_codepoint() {
+ let key = KeyEvent::new(
+ KeyCode::Char('L'),
+ KeyModifiers::CONTROL | KeyModifiers::SHIFT,
+ );
+ assert_eq!(
+ encode_key(key, KeyboardProtocol::Kitty { flags: 1 }),
+ b"\x1b[108;6u"
+ );
+}
+
+#[test]
+fn legacy_shift_uppercase_letter_stays_uppercase() {
+ let key = KeyEvent::new(KeyCode::Char('L'), KeyModifiers::SHIFT);
+ assert_eq!(encode_key(key, KeyboardProtocol::Legacy), b"L");
+}
+
+#[test]
+fn kitty_alt_enter() {
+ let key = KeyEvent::new(KeyCode::Enter, KeyModifiers::ALT);
+ assert_eq!(
+ encode_key(key, KeyboardProtocol::Kitty { flags: 1 }),
+ b"\x1b[13;3u"
+ );
+}
+
+#[test]
+fn kitty_plain_ctrl_c_uses_legacy() {
+ // Plain Ctrl+letter is well-represented in legacy
+ let key = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL);
+ assert_eq!(
+ encode_key(key, KeyboardProtocol::Kitty { flags: 1 }),
+ vec![3]
+ );
+}
+
+#[test]
+fn kitty_unmodified_uses_legacy() {
+ let key = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty());
+ assert_eq!(encode_key(key, KeyboardProtocol::Kitty { flags: 1 }), b"a");
+}
+
+#[test]
+fn kitty_shift_tab() {
+ let key = KeyEvent::new(KeyCode::Tab, KeyModifiers::SHIFT);
+ assert_eq!(
+ encode_key(key, KeyboardProtocol::Kitty { flags: 1 }),
+ b"\x1b[9;2u"
+ );
+}
+
+#[test]
+fn kitty_ctrl_shift_enter() {
+ let key = KeyEvent::new(KeyCode::Enter, KeyModifiers::CONTROL | KeyModifiers::SHIFT);
+ assert_eq!(
+ encode_key(key, KeyboardProtocol::Kitty { flags: 1 }),
+ b"\x1b[13;6u"
+ );
+}
+
+#[test]
+fn kitty_repeat_event_type_is_encoded_when_requested() {
+ let key = KeyEvent::new_with_kind(
+ KeyCode::Enter,
+ KeyModifiers::SHIFT,
+ crossterm::event::KeyEventKind::Repeat,
+ );
+ assert_eq!(
+ encode_key(key, KeyboardProtocol::Kitty { flags: 3 }),
+ b"\x1b[13;2:2u"
+ );
+}
+
+#[test]
+fn kitty_release_event_type_and_alternate_keys_are_encoded_together() {
+ let key = KeyEvent::new_with_kind(
+ KeyCode::Char('L'),
+ KeyModifiers::SHIFT,
+ crossterm::event::KeyEventKind::Release,
+ );
+ assert_eq!(
+ encode_key(key, KeyboardProtocol::Kitty { flags: 7 }),
+ b"\x1b[108:76;2:3u"
+ );
+}
+
+#[test]
+fn kitty_rich_key_data_can_preserve_shifted_symbol_pairs() {
+ let key = TerminalKey::new(KeyCode::Char('1'), KeyModifiers::SHIFT)
+ .with_shifted_codepoint('!' as u32);
+ assert_eq!(
+ encode_terminal_key(key, KeyboardProtocol::Kitty { flags: 7 }),
+ b"\x1b[49:33;2:1u"
+ );
+}
+
+#[test]
+fn parse_kitty_sequence_preserves_shifted_symbol_pair() {
+ let key = parse_terminal_key_sequence("\x1b[49:33;2:1u").unwrap();
+ assert_eq!(key.code, KeyCode::Char('1'));
+ assert_eq!(key.modifiers, KeyModifiers::SHIFT);
+ assert_eq!(key.kind, crossterm::event::KeyEventKind::Press);
+ assert_eq!(key.shifted_codepoint, Some('!' as u32));
+}
+
+#[test]
+fn parse_kitty_sequence_preserves_shifted_letter_pair_and_release() {
+ let key = parse_terminal_key_sequence("\x1b[108:76;2:3u").unwrap();
+ assert_eq!(key.code, KeyCode::Char('l'));
+ assert_eq!(key.modifiers, KeyModifiers::SHIFT);
+ assert_eq!(key.kind, crossterm::event::KeyEventKind::Release);
+ assert_eq!(key.shifted_codepoint, Some('L' as u32));
+}
+
+#[test]
+fn parse_modify_other_keys_sequence() {
+ let key = parse_terminal_key_sequence("\x1b[27;6;108~").unwrap();
+ assert_eq!(key.code, KeyCode::Char('l'));
+ assert_eq!(key.modifiers, KeyModifiers::CONTROL | KeyModifiers::SHIFT);
+ assert_eq!(key.kind, crossterm::event::KeyEventKind::Press);
+ assert_eq!(key.shifted_codepoint, None);
+}
+
+#[test]
+fn parse_legacy_uppercase_letter_as_shifted_char() {
+ let key = parse_terminal_key_sequence("L").unwrap();
+ assert_eq!(key.code, KeyCode::Char('L'));
+ assert_eq!(key.modifiers, KeyModifiers::SHIFT);
+}
+
+#[test]
+fn parse_legacy_up_arrow_sequence() {
+ let key = parse_terminal_key_sequence("\x1b[A").unwrap();
+ assert_eq!(key.code, KeyCode::Up);
+ assert_eq!(key.modifiers, KeyModifiers::empty());
+}
+
+#[test]
+fn parse_xterm_alt_up_arrow_sequence() {
+ let key = parse_terminal_key_sequence("\x1b[1;3A").unwrap();
+ assert_eq!(key.code, KeyCode::Up);
+ assert_eq!(key.modifiers, KeyModifiers::ALT);
+}
+
+#[test]
+fn parse_xterm_alt_down_arrow_sequence() {
+ let key = parse_terminal_key_sequence("\x1b[1;3B").unwrap();
+ assert_eq!(key.code, KeyCode::Down);
+ assert_eq!(key.modifiers, KeyModifiers::ALT);
+}
+
+#[test]
+fn parse_kitty_functional_up_arrow_sequence() {
+ let key = parse_terminal_key_sequence("\x1b[57419;1u").unwrap();
+ assert_eq!(key.code, KeyCode::Up);
+ assert_eq!(key.modifiers, KeyModifiers::empty());
+}
+
+#[test]
+fn parse_legacy_ctrl_b_sequence() {
+ let key = parse_terminal_key_sequence("\x02").unwrap();
+ assert_eq!(key.code, KeyCode::Char('b'));
+ assert_eq!(key.modifiers, KeyModifiers::CONTROL);
+}
+
+#[test]
+fn parse_legacy_ctrl_c_sequence() {
+ let key = parse_terminal_key_sequence("\x03").unwrap();
+ assert_eq!(key.code, KeyCode::Char('c'));
+ assert_eq!(key.modifiers, KeyModifiers::CONTROL);
+}
+
+#[test]
+fn legacy_ctrl_byte_matrix_is_covered() {
+ for (byte, expected) in [
+ (b'\x01', 'a'),
+ (b'\x02', 'b'),
+ (b'\x03', 'c'),
+ (b'\x1a', 'z'),
+ ] {
+ let key = parse_terminal_key_sequence(std::str::from_utf8(&[byte]).unwrap()).unwrap();
+ assert_terminal_key_eq(
+ key,
+ KeyCode::Char(expected),
+ KeyModifiers::CONTROL,
+ crossterm::event::KeyEventKind::Press,
+ None,
+ );
+ }
+
+ // Ctrl+[ is byte-identical to Escape in legacy terminals, so the parser
+ // intentionally treats 0x1b as Escape and only disambiguates the other
+ // legacy control-symbol bytes here.
+ for (byte, expected) in [
+ (b'\x1c', '\\'),
+ (b'\x1d', ']'),
+ (b'\x1e', '^'),
+ (b'\x1f', '-'),
+ ] {
+ let key = parse_terminal_key_sequence(std::str::from_utf8(&[byte]).unwrap()).unwrap();
+ assert_terminal_key_eq(
+ key,
+ KeyCode::Char(expected),
+ KeyModifiers::CONTROL,
+ crossterm::event::KeyEventKind::Press,
+ None,
+ );
+ }
+}
+
+#[test]
+fn legacy_modified_special_roundtrip_matrix() {
+ let cases = [
+ KeyEvent::new(KeyCode::Up, KeyModifiers::ALT),
+ KeyEvent::new(KeyCode::Down, KeyModifiers::ALT),
+ KeyEvent::new(KeyCode::Right, KeyModifiers::SHIFT),
+ KeyEvent::new(KeyCode::Left, KeyModifiers::CONTROL),
+ KeyEvent::new(KeyCode::Home, KeyModifiers::CONTROL),
+ KeyEvent::new(KeyCode::End, KeyModifiers::CONTROL | KeyModifiers::SHIFT),
+ KeyEvent::new(KeyCode::PageUp, KeyModifiers::ALT),
+ KeyEvent::new(KeyCode::PageDown, KeyModifiers::CONTROL),
+ KeyEvent::new(KeyCode::Insert, KeyModifiers::SHIFT),
+ KeyEvent::new(KeyCode::Delete, KeyModifiers::ALT),
+ ];
+
+ for key in cases {
+ let encoded = encode_key(key, KeyboardProtocol::Legacy);
+ let parsed = parse_terminal_key_sequence(std::str::from_utf8(&encoded).unwrap()).unwrap();
+ assert_terminal_key_eq(parsed, key.code, key.modifiers, key.kind, None);
+ }
+}
+
+#[test]
+fn kitty_shifted_symbol_roundtrip_preserves_alternate_key() {
+ let key = TerminalKey::new(KeyCode::Char('1'), KeyModifiers::SHIFT)
+ .with_shifted_codepoint('!' as u32);
+ let encoded = encode_terminal_key(key, KeyboardProtocol::Kitty { flags: 7 });
+ let parsed = parse_terminal_key_sequence(std::str::from_utf8(&encoded).unwrap()).unwrap();
+ assert_terminal_key_eq(
+ parsed,
+ KeyCode::Char('1'),
+ KeyModifiers::SHIFT,
+ crossterm::event::KeyEventKind::Press,
+ Some('!' as u32),
+ );
+}
+
+#[test]
+fn legacy_basic_special_roundtrip_matrix() {
+ let cases = [
+ KeyEvent::new(KeyCode::Enter, KeyModifiers::empty()),
+ KeyEvent::new(KeyCode::Tab, KeyModifiers::empty()),
+ KeyEvent::new(KeyCode::Backspace, KeyModifiers::empty()),
+ KeyEvent::new(KeyCode::Esc, KeyModifiers::empty()),
+ KeyEvent::new(KeyCode::Up, KeyModifiers::empty()),
+ KeyEvent::new(KeyCode::Down, KeyModifiers::empty()),
+ KeyEvent::new(KeyCode::Left, KeyModifiers::empty()),
+ KeyEvent::new(KeyCode::Right, KeyModifiers::empty()),
+ KeyEvent::new(KeyCode::Home, KeyModifiers::empty()),
+ KeyEvent::new(KeyCode::End, KeyModifiers::empty()),
+ KeyEvent::new(KeyCode::PageUp, KeyModifiers::empty()),
+ KeyEvent::new(KeyCode::PageDown, KeyModifiers::empty()),
+ KeyEvent::new(KeyCode::Insert, KeyModifiers::empty()),
+ KeyEvent::new(KeyCode::Delete, KeyModifiers::empty()),
+ ];
+
+ for key in cases {
+ let encoded = encode_key(key, KeyboardProtocol::Legacy);
+ let parsed = parse_terminal_key_sequence(std::str::from_utf8(&encoded).unwrap()).unwrap();
+ assert_terminal_key_eq(parsed, key.code, key.modifiers, key.kind, None);
+ }
+}
+
+#[test]
+fn kitty_functional_key_matrix_is_covered() {
+ let cases = [
+ ("\x1b[57417;1u", KeyCode::Left),
+ ("\x1b[57418;1u", KeyCode::Right),
+ ("\x1b[57419;1u", KeyCode::Up),
+ ("\x1b[57420;1u", KeyCode::Down),
+ ("\x1b[57421;1u", KeyCode::PageUp),
+ ("\x1b[57422;1u", KeyCode::PageDown),
+ ("\x1b[57423;1u", KeyCode::Home),
+ ("\x1b[57424;1u", KeyCode::End),
+ ("\x1b[57425;1u", KeyCode::Insert),
+ ("\x1b[57426;1u", KeyCode::Delete),
+ ];
+
+ for (sequence, code) in cases {
+ let parsed = parse_terminal_key_sequence(sequence).unwrap();
+ assert_terminal_key_eq(
+ parsed,
+ code,
+ KeyModifiers::empty(),
+ crossterm::event::KeyEventKind::Press,
+ None,
+ );
+ }
+}
+
+#[test]
+fn kitty_shifted_symbol_pair_matrix_is_covered() {
+ let cases = [('1', '!'), ('/', '?'), ('[', '{')];
+
+ for (base, shifted) in cases {
+ let key = TerminalKey::new(KeyCode::Char(base), KeyModifiers::SHIFT)
+ .with_shifted_codepoint(shifted as u32);
+ let encoded = encode_terminal_key(key, KeyboardProtocol::Kitty { flags: 7 });
+ let parsed = parse_terminal_key_sequence(std::str::from_utf8(&encoded).unwrap()).unwrap();
+ assert_terminal_key_eq(
+ parsed,
+ KeyCode::Char(base),
+ KeyModifiers::SHIFT,
+ crossterm::event::KeyEventKind::Press,
+ Some(shifted as u32),
+ );
+ }
+}
+
+fn assert_fixture_corpus_parses(corpus: &str) {
+ for line in corpus.lines() {
+ let line = line.trim();
+ if line.is_empty() || line.starts_with('#') {
+ continue;
+ }
+
+ let mut columns: Vec<_> = line.split('\t').collect();
+ if columns.len() == 5 {
+ columns.push("");
+ }
+
+ let (family, bytes_hex, code, modifiers, kind, shifted) = match columns.len() {
+ 6 => {
+ if columns[1].chars().all(|ch| ch.is_ascii_hexdigit()) {
+ (
+ columns[0], columns[1], columns[2], columns[3], columns[4], columns[5],
+ )
+ } else {
+ (
+ columns[0], columns[2], columns[3], columns[4], columns[5], "",
+ )
+ }
+ }
+ 7 => (
+ columns[0], columns[2], columns[3], columns[4], columns[5], columns[6],
+ ),
+ _ => panic!("fixture row must have 6 or 7 columns: {line}"),
+ };
+
+ assert!(
+ bytes_hex.chars().all(|ch| ch.is_ascii_hexdigit()),
+ "non-hex fixture bytes for {family}: {bytes_hex}"
+ );
+ let bytes = decode_hex(bytes_hex);
+ let text = std::str::from_utf8(&bytes).unwrap();
+ let parsed = parse_terminal_key_sequence(text)
+ .unwrap_or_else(|| panic!("fixture failed to parse: {family}"));
+
+ assert_terminal_key_eq(
+ parsed,
+ parse_fixture_key_code(code),
+ parse_fixture_modifiers(modifiers),
+ parse_fixture_kind(kind),
+ if shifted.is_empty() {
+ None
+ } else {
+ Some(shifted.parse::<u32>().unwrap())
+ },
+ );
+ }
+}
+
+#[test]
+fn keyboard_protocol_corpus_fixture_parses() {
+ let corpus = include_str!("../../tests/fixtures/keyboard_protocol_corpus.tsv");
+ assert_fixture_corpus_parses(corpus);
+}
+
+#[test]
+fn macos_terminal_variants_fixture_parses() {
+ let corpus = include_str!("../../tests/fixtures/macos_terminal_variants.tsv");
+ for line in corpus.lines() {
+ let line = line.trim();
+ if line.is_empty() || line.starts_with('#') {
+ continue;
+ }
+
+ let mut columns: Vec<_> = line.split('\t').collect();
+ if columns.len() == 6 {
+ columns.push("");
+ }
+ assert_eq!(
+ columns.len(),
+ 7,
+ "macOS fixture row must have 7 columns: {line}"
+ );
+
+ let source = format!("{}:{}", columns[0], columns[1]);
+ let transformed = [
+ source.as_str(),
+ columns[2],
+ columns[3],
+ columns[4],
+ columns[5],
+ columns[6],
+ ]
+ .join("\t");
+ assert_fixture_corpus_parses(&transformed);
+ }
+}
+
+#[test]
+fn linux_terminal_variants_fixture_parses() {
+ let corpus = include_str!("../../tests/fixtures/linux_terminal_variants.tsv");
+ assert_fixture_corpus_parses(corpus);
+}
+
+#[test]
+fn protocol_from_zero_flags_is_legacy() {
+ assert_eq!(
+ KeyboardProtocol::from_kitty_flags(0),
+ KeyboardProtocol::Legacy
+ );
+}
+
+#[test]
+fn protocol_from_nonzero_flags_is_kitty() {
+ assert_eq!(
+ KeyboardProtocol::from_kitty_flags(7),
+ KeyboardProtocol::Kitty { flags: 7 }
+ );
+}