fix(input): carry key and preedit text as bytes like Zig

Zig's KeyEvent.utf8 and the embedded API's key/preedit text are byte
slices passed through to key encoding and the pty; the port converted
them lossily to UTF-8.
- KeyEvent.utf8 is &[u8] end to end (ghostty-input encoder, core,
  ghostty-lib C API, libghostty-vt key events, GTK callers). Where Zig
  validates with Utf8View (alternates, modify-other-keys, ctrl CSI-u,
  associated text, binding unicode lookup) invalid text takes the same
  "no UTF-8" fallback.
- Invalid preedit UTF-8 behaves like Zig's `try Utf8View.init`: the
  prior preedit is cleared and marked dirty, the callback returns an
  error (logged by the C API) and no preedit is set.
- The inspector key widget shows invalid key text lossily, display only.

Test Plan:
- cargo test --workspace --no-fail-fast (3407 passed)
- cargo test -p ghostty-lib --features test-platform (C-API key text
  0xff 0xfe reaches the pty unchanged)
- just check-macos (both), just check-windows, just check-wasm
This commit is contained in:
ddidderr committed 2026-10-12 11:01:17 +02:00
1 parent 5f16c24760
commit db954441fa
19 files changed
+277 -144

No files matched your search

+3 -2
View File
@@ -574,8 +574,9 @@ impl Set {
// If our UTF-8 text is exactly one codepoint, we try to match that.
if !event.utf8.is_empty() {
let mut it = event.utf8.chars();
// No codepoints or multiple codepoints drops to invalid format
// Invalid UTF-8, no codepoints or multiple codepoints drops to
// invalid format.
let mut it = crate::key_encode::utf8_chars(event.utf8).unwrap_or_else(|| "".chars());
if let (Some(cp), None) = (it.next(), it.next()) {
trigger.key = TriggerKey::Unicode(cp as u32);
if let Some(v) = self.get_entry(trigger) {
+1 -1
View File
@@ -1097,7 +1097,7 @@ fn ev(key: Key, mods: Mods, utf8: &str, unshifted: u32) -> KeyEvent<'_> {
KeyEvent {
key,
mods,
utf8,
utf8: utf8.as_bytes(),
unshifted_codepoint: unshifted,
..KeyEvent::default()
}
+2 -2
View File
@@ -43,7 +43,7 @@ pub struct KeyEvent<'a> {
/// This will be an empty string if there is no text generated.
/// If composing is true and this is non-empty, this is preedit
/// text.
pub utf8: &'a str,
pub utf8: &'a [u8],
/// The codepoint for this key when it is unshifted. For example,
/// shift+a is "A" in UTF-8 but unshifted would provide 'a'.
@@ -58,7 +58,7 @@ impl Default for KeyEvent<'_> {
mods: Mods::default(),
consumed_mods: Mods::default(),
composing: false,
utf8: "",
utf8: b"",
unshifted_codepoint: 0,
}
}
+179 -105
View File
@@ -147,7 +147,7 @@ fn kitty<W: Write>(writer: &mut W, event: &KeyEvent, opts: &Options) -> io::Resu
if event.key == Key::Backspace {
return Ok(());
}
return writer.write_all(event.utf8.as_bytes());
return writer.write_all(event.utf8);
}
}
@@ -180,8 +180,12 @@ fn kitty<W: Write>(writer: &mut W, event: &KeyEvent, opts: &Options) -> io::Resu
// We only do this for printable characters. We should
// inspect the real unicode codepoint properties here but
// the real world issue is usually control characters.
if !event.utf8.chars().any(|c| is_control(u32::from(c))) {
return writer.write_all(event.utf8.as_bytes());
// Invalid UTF-8 falls back to encoding the key press as if it
// didn't produce text (the bytes are never sent as plain text).
if let Some(mut chars) = utf8_chars(event.utf8)
&& !chars.any(|c| is_control(u32::from(c)))
{
return writer.write_all(event.utf8);
}
}
}
@@ -192,7 +196,7 @@ fn kitty<W: Write>(writer: &mut W, event: &KeyEvent, opts: &Options) -> io::Resu
// (e.g. composed/IME text), so send it as-is so programs can
// still receive it.
if !event.utf8.is_empty() {
return writer.write_all(event.utf8.as_bytes());
return writer.write_all(event.utf8);
}
return Ok(());
};
@@ -209,7 +213,7 @@ fn kitty<W: Write>(writer: &mut W, event: &KeyEvent, opts: &Options) -> io::Resu
mods: KittyMods::from_input(event.action, event.key, all_mods),
event: KittyEvent::None,
alternates: [None, None],
text: "",
text: b"",
};
if opts.kitty_flags.report_events {
@@ -230,7 +234,10 @@ fn kitty<W: Write>(writer: &mut W, event: &KeyEvent, opts: &Options) -> io::Resu
break 'alternates;
}
let mut it = event.utf8.chars();
// Assume invalid UTF-8 means no UTF-8.
let Some(mut it) = utf8_chars(event.utf8) else {
break 'alternates;
};
// If we have a codepoint in our UTF-8 sequence, then we can
// report the shifted version.
@@ -376,7 +383,8 @@ fn legacy<W: Write>(writer: &mut W, event: &KeyEvent, opts: &Options) -> io::Res
// for any char with a modifier. Ctrl sequences like Ctrl+a
// are already handled above.
if opts.modify_other_keys_state_2 {
let mut it = utf8.chars();
// Assume invalid UTF-8 means we have no UTF-8.
let mut it = utf8_chars(utf8).unwrap_or_else(|| "".chars());
// We only do this if we have a single codepoint. There shouldn't
// ever be a multi-codepoint sequence that triggers this.
if let (Some(codepoint), None) = (it.next().map(u32::from), it.clone().next()) {
@@ -449,7 +457,9 @@ fn legacy<W: Write>(writer: &mut W, event: &KeyEvent, opts: &Options) -> io::Res
// Get our codepoint. If we have more than one codepoint this
// can't be valid CSIu.
let mut it = event.utf8.chars();
let Some(mut it) = utf8_chars(event.utf8) else {
break 'csiu;
};
let Some(first) = it.next() else {
break 'csiu;
};
@@ -501,7 +511,7 @@ fn legacy<W: Write>(writer: &mut W, event: &KeyEvent, opts: &Options) -> io::Res
return Ok(());
}
writer.write_all(utf8.as_bytes())
writer.write_all(utf8)
}
fn legacy_alt_prefix(
@@ -536,7 +546,7 @@ fn legacy_alt_prefix(
}
// Otherwise, we require utf8 to already have the byte represented.
let utf8 = event.utf8.as_bytes();
let utf8 = event.utf8;
if utf8.len() == 1 {
return Some(utf8[0]);
}
@@ -654,7 +664,12 @@ fn pc_style_function_key(
/// This will return None if the key event should not be converted
/// into a C0 byte. There are many cases for this and you should read
/// the source code to understand them.
fn ctrl_seq(logical_key: Key, utf8: &str, unshifted_codepoint: u32, mods: key::Mods) -> Option<u8> {
fn ctrl_seq(
logical_key: Key,
utf8: &[u8],
unshifted_codepoint: u32,
mods: key::Mods,
) -> Option<u8> {
let ctrl_only = key::Mods {
ctrl: true,
..key::Mods::default()
@@ -679,7 +694,7 @@ fn ctrl_seq(logical_key: Key, utf8: &str, unshifted_codepoint: u32, mods: key::M
// If we have exactly one UTF8 byte, we assume that is the
// character we want to convert to a C0 byte.
if utf8.len() == 1 {
break 'ch utf8.as_bytes()[0];
break 'ch utf8[0];
}
// If we have a logical key that maps to a single byte
@@ -813,8 +828,14 @@ fn is_control(cp: u32) -> bool {
/// Returns true if this string is comprised of a single
/// control character. This returns false for multi-byte strings.
fn is_control_utf8(s: &str) -> bool {
s.len() == 1 && is_control(u32::from(s.as_bytes()[0]))
fn is_control_utf8(s: &[u8]) -> bool {
s.len() == 1 && is_control(u32::from(s[0]))
}
/// Strictly decode `bytes` as UTF-8 (Zig: `std.unicode.Utf8View.init`),
/// returning `None` for invalid UTF-8.
pub(crate) fn utf8_chars(bytes: &[u8]) -> Option<std::str::Chars<'_>> {
std::str::from_utf8(bytes).ok().map(str::chars)
}
/// This is the bitmask for fixterm CSI u modifiers.
@@ -926,7 +947,7 @@ struct KittySequence<'a> {
mods: KittyMods,
event: KittyEvent,
alternates: [Option<u32>; 2],
text: &'a str,
text: &'a [u8],
}
impl KittySequence<'_> {
@@ -964,9 +985,10 @@ impl KittySequence<'_> {
}
// Text section
if !self.text.is_empty() {
// Assume invalid UTF-8 means we have no text.
if let Some(chars) = utf8_chars(self.text).filter(|_| !self.text.is_empty()) {
let mut count = 0;
for c in self.text.chars() {
for c in chars {
let cp = u32::from(c);
// If the codepoint is non-printable ASCII character, skip.
if is_control(cp) {
@@ -1022,7 +1044,7 @@ mod tests {
&KeyEvent {
key: Key::KeyA,
mods: Mods { ..Mods::default() },
utf8: "abcd",
utf8: "abcd".as_bytes(),
..KeyEvent::default()
},
&Options {
@@ -1048,7 +1070,7 @@ mod tests {
key: Key::KeyA,
action: key::Action::Repeat,
mods: Mods { ..Mods::default() },
utf8: "a",
utf8: "a".as_bytes(),
..KeyEvent::default()
},
&Options {
@@ -1073,7 +1095,7 @@ mod tests {
&KeyEvent {
key: Key::Enter,
mods: Mods { ..Mods::default() },
utf8: "",
utf8: "".as_bytes(),
..KeyEvent::default()
},
&Options {
@@ -1094,7 +1116,7 @@ mod tests {
&KeyEvent {
key: Key::Backspace,
mods: Mods { ..Mods::default() },
utf8: "",
utf8: "".as_bytes(),
..KeyEvent::default()
},
&Options {
@@ -1115,7 +1137,7 @@ mod tests {
&KeyEvent {
key: Key::Tab,
mods: Mods { ..Mods::default() },
utf8: "",
utf8: "".as_bytes(),
..KeyEvent::default()
},
&Options {
@@ -1137,7 +1159,7 @@ mod tests {
action: key::Action::Release,
key: Key::Enter,
mods: Mods { ..Mods::default() },
utf8: "",
utf8: "".as_bytes(),
..KeyEvent::default()
},
&Options {
@@ -1160,7 +1182,7 @@ mod tests {
action: key::Action::Release,
key: Key::Backspace,
mods: Mods { ..Mods::default() },
utf8: "",
utf8: "".as_bytes(),
..KeyEvent::default()
},
&Options {
@@ -1183,7 +1205,7 @@ mod tests {
action: key::Action::Release,
key: Key::Tab,
mods: Mods { ..Mods::default() },
utf8: "",
utf8: "".as_bytes(),
..KeyEvent::default()
},
&Options {
@@ -1206,7 +1228,7 @@ mod tests {
action: key::Action::Release,
key: Key::Enter,
mods: Mods { ..Mods::default() },
utf8: "",
utf8: "".as_bytes(),
..KeyEvent::default()
},
&Options {
@@ -1230,7 +1252,7 @@ mod tests {
action: key::Action::Release,
key: Key::Backspace,
mods: Mods { ..Mods::default() },
utf8: "",
utf8: "".as_bytes(),
..KeyEvent::default()
},
&Options {
@@ -1254,7 +1276,7 @@ mod tests {
action: key::Action::Release,
key: Key::Tab,
mods: Mods { ..Mods::default() },
utf8: "",
utf8: "".as_bytes(),
..KeyEvent::default()
},
&Options {
@@ -1284,7 +1306,7 @@ mod tests {
shift: true,
..Mods::default()
},
utf8: "",
utf8: "".as_bytes(),
..KeyEvent::default()
},
&Options {
@@ -1312,7 +1334,7 @@ mod tests {
shift: true,
..Mods::default()
},
utf8: "",
utf8: "".as_bytes(),
..KeyEvent::default()
},
&Options {
@@ -1340,7 +1362,7 @@ mod tests {
shift: true,
..Mods::default()
},
utf8: "",
utf8: "".as_bytes(),
..KeyEvent::default()
},
&Options {
@@ -1365,7 +1387,7 @@ mod tests {
&KeyEvent {
key: Key::Enter,
mods: Mods { ..Mods::default() },
utf8: "",
utf8: "".as_bytes(),
..KeyEvent::default()
},
&Options {
@@ -1396,7 +1418,7 @@ mod tests {
ctrl: true,
..Mods::default()
},
utf8: "",
utf8: "".as_bytes(),
..KeyEvent::default()
},
&Options {
@@ -1428,7 +1450,7 @@ mod tests {
ctrl: true,
..Mods::default()
},
utf8: "",
utf8: "".as_bytes(),
..KeyEvent::default()
},
&Options {
@@ -1456,7 +1478,7 @@ mod tests {
&KeyEvent {
key: Key::Delete,
mods: Mods { ..Mods::default() },
utf8: "\u{7f}",
utf8: "\u{7f}".as_bytes(),
..KeyEvent::default()
},
&Options {
@@ -1538,7 +1560,7 @@ mod tests {
&KeyEvent {
key: Key::Unidentified,
mods: Mods { ..Mods::default() },
utf8: "\u{fb}",
utf8: "\u{fb}".as_bytes(),
..KeyEvent::default()
},
&Options {
@@ -1569,7 +1591,7 @@ mod tests {
shift: true,
..Mods::default()
},
utf8: "A",
utf8: "A".as_bytes(),
unshifted_codepoint: 97,
..KeyEvent::default()
},
@@ -1599,7 +1621,7 @@ mod tests {
shift: true,
..Mods::default()
},
utf8: "A",
utf8: "A".as_bytes(),
unshifted_codepoint: 65,
..KeyEvent::default()
},
@@ -1629,7 +1651,7 @@ mod tests {
caps_lock: true,
..Mods::default()
},
utf8: "J",
utf8: "J".as_bytes(),
unshifted_codepoint: 106,
..KeyEvent::default()
},
@@ -1661,7 +1683,7 @@ mod tests {
shift: true,
..Mods::default()
},
utf8: ":",
utf8: ":".as_bytes(),
unshifted_codepoint: u32::from(';'),
..KeyEvent::default()
},
@@ -1690,7 +1712,7 @@ mod tests {
&KeyEvent {
key: Key::Semicolon,
mods: Mods { ..Mods::default() },
utf8: "\u{447}",
utf8: "\u{447}".as_bytes(),
unshifted_codepoint: 1095,
..KeyEvent::default()
},
@@ -1722,7 +1744,7 @@ mod tests {
shift: true,
..Mods::default()
},
utf8: "\u{427}",
utf8: "\u{427}".as_bytes(),
unshifted_codepoint: 1095,
..KeyEvent::default()
},
@@ -1754,7 +1776,7 @@ mod tests {
caps_lock: true,
..Mods::default()
},
utf8: "\u{427}",
utf8: "\u{427}".as_bytes(),
unshifted_codepoint: 1095,
..KeyEvent::default()
},
@@ -1787,7 +1809,7 @@ mod tests {
ctrl: true,
..Mods::default()
},
utf8: "",
utf8: "".as_bytes(),
unshifted_codepoint: 337,
..KeyEvent::default()
},
@@ -1816,7 +1838,7 @@ mod tests {
&KeyEvent {
key: Key::ArrowUp,
mods: Mods { ..Mods::default() },
utf8: "\u{1e}",
utf8: "\u{1e}".as_bytes(),
..KeyEvent::default()
},
&Options {
@@ -1844,7 +1866,7 @@ mod tests {
shift: true,
..Mods::default()
},
utf8: "",
utf8: "".as_bytes(),
..KeyEvent::default()
},
&Options {
@@ -1870,7 +1892,7 @@ mod tests {
&KeyEvent {
key: Key::ShiftLeft,
mods: Mods { ..Mods::default() },
utf8: "",
utf8: "".as_bytes(),
..KeyEvent::default()
},
&Options {
@@ -1896,7 +1918,7 @@ mod tests {
&KeyEvent {
key: Key::ShiftLeft,
mods: Mods { ..Mods::default() },
utf8: "",
utf8: "".as_bytes(),
..KeyEvent::default()
},
&Options {
@@ -1925,7 +1947,7 @@ mod tests {
ctrl: true,
..Mods::default()
},
utf8: "j",
utf8: "j".as_bytes(),
unshifted_codepoint: 106,
..KeyEvent::default()
},
@@ -1957,7 +1979,7 @@ mod tests {
shift: true,
..Mods::default()
},
utf8: "J",
utf8: "J".as_bytes(),
unshifted_codepoint: 106,
..KeyEvent::default()
},
@@ -1990,7 +2012,7 @@ mod tests {
shift: true,
..Mods::default()
},
utf8: "J",
utf8: "J".as_bytes(),
unshifted_codepoint: 106,
..KeyEvent::default()
},
@@ -2019,7 +2041,7 @@ mod tests {
&KeyEvent {
key: Key::Delete,
mods: Mods { ..Mods::default() },
utf8: "\u{7f}",
utf8: "\u{7f}".as_bytes(),
..KeyEvent::default()
},
&Options {
@@ -2045,7 +2067,7 @@ mod tests {
&mut w,
&KeyEvent {
key: Key::Enter,
utf8: "A",
utf8: "A".as_bytes(),
unshifted_codepoint: 0x0D,
..KeyEvent::default()
},
@@ -2073,7 +2095,7 @@ mod tests {
&KeyEvent {
key: Key::Numpad1,
mods: Mods { ..Mods::default() },
utf8: "1",
utf8: "1".as_bytes(),
..KeyEvent::default()
},
&Options {
@@ -2100,7 +2122,7 @@ mod tests {
&mut w,
&KeyEvent {
key: Key::Backspace,
utf8: "A",
utf8: "A".as_bytes(),
unshifted_codepoint: 0x0D,
..KeyEvent::default()
},
@@ -2128,7 +2150,7 @@ mod tests {
&mut w,
&KeyEvent {
key: Key::Backspace,
utf8: "A",
utf8: "A".as_bytes(),
unshifted_codepoint: 0x0D,
..KeyEvent::default()
},
@@ -2149,7 +2171,7 @@ mod tests {
&mut w,
&KeyEvent {
key: Key::Enter,
utf8: "A",
utf8: "A".as_bytes(),
unshifted_codepoint: 0x0D,
..KeyEvent::default()
},
@@ -2170,7 +2192,7 @@ mod tests {
&mut w,
&KeyEvent {
key: Key::Escape,
utf8: "A",
utf8: "A".as_bytes(),
unshifted_codepoint: 0x0D,
..KeyEvent::default()
},
@@ -2196,7 +2218,7 @@ mod tests {
shift: true,
..Mods::default()
},
utf8: "_",
utf8: "_".as_bytes(),
..KeyEvent::default()
},
&Options {
@@ -2221,7 +2243,7 @@ mod tests {
alt: true,
..Mods::default()
},
utf8: "c",
utf8: "c".as_bytes(),
..KeyEvent::default()
},
&Options {
@@ -2241,7 +2263,7 @@ mod tests {
&mut w,
&KeyEvent {
key: Key::KeyC,
utf8: "c",
utf8: "c".as_bytes(),
mods: Mods {
alt: true,
..Mods::default()
@@ -2293,7 +2315,7 @@ mod tests {
&mut w,
&KeyEvent {
key: Key::KeyF,
utf8: "\u{444}",
utf8: "\u{444}".as_bytes(),
mods: Mods {
alt: true,
..Mods::default()
@@ -2322,7 +2344,7 @@ mod tests {
ctrl: true,
..Mods::default()
},
utf8: "c",
utf8: "c".as_bytes(),
..KeyEvent::default()
},
&Options {
@@ -2346,7 +2368,7 @@ mod tests {
ctrl: true,
..Mods::default()
},
utf8: " ",
utf8: " ".as_bytes(),
..KeyEvent::default()
},
&Options {
@@ -2395,7 +2417,7 @@ mod tests {
shift: true,
..Mods::default()
},
utf8: "H",
utf8: "H".as_bytes(),
..KeyEvent::default()
},
&Options {
@@ -2425,7 +2447,7 @@ mod tests {
shift: true,
..Mods::default()
},
utf8: "H",
utf8: "H".as_bytes(),
..KeyEvent::default()
},
&Options {
@@ -2451,7 +2473,7 @@ mod tests {
..Mods::default()
},
consumed_mods: Mods { ..Mods::default() },
utf8: "8",
utf8: "8".as_bytes(),
..KeyEvent::default()
},
&Options {
@@ -2477,7 +2499,7 @@ mod tests {
ctrl: true,
..Mods::default()
},
utf8: "i",
utf8: "i".as_bytes(),
..KeyEvent::default()
},
&Options {
@@ -2497,7 +2519,7 @@ mod tests {
ctrl: true,
..Mods::default()
},
utf8: "m",
utf8: "m".as_bytes(),
..KeyEvent::default()
},
&Options {
@@ -2517,7 +2539,7 @@ mod tests {
ctrl: true,
..Mods::default()
},
utf8: "[",
utf8: "[".as_bytes(),
..KeyEvent::default()
},
&Options {
@@ -2538,7 +2560,7 @@ mod tests {
shift: true,
..Mods::default()
},
utf8: "@",
utf8: "@".as_bytes(),
unshifted_codepoint: u32::from('2'),
..KeyEvent::default()
},
@@ -2564,7 +2586,7 @@ mod tests {
shift: true,
..Mods::default()
},
utf8: "M",
utf8: "M".as_bytes(),
unshifted_codepoint: u32::from('m'),
..KeyEvent::default()
},
@@ -2635,7 +2657,7 @@ mod tests {
key: Key::Numpad1,
mods: Mods { ..Mods::default() },
consumed_mods: Mods { ..Mods::default() },
utf8: "1",
utf8: "1".as_bytes(),
..KeyEvent::default()
},
&Options {
@@ -2657,7 +2679,7 @@ mod tests {
key: Key::Numpad1,
mods: Mods { ..Mods::default() },
consumed_mods: Mods { ..Mods::default() },
utf8: "1",
utf8: "1".as_bytes(),
..KeyEvent::default()
},
&Options {
@@ -2683,7 +2705,7 @@ mod tests {
..Mods::default()
},
consumed_mods: Mods { ..Mods::default() },
utf8: "1",
utf8: "1".as_bytes(),
..KeyEvent::default()
},
&Options {
@@ -2709,7 +2731,7 @@ mod tests {
..Mods::default()
},
consumed_mods: Mods { ..Mods::default() },
utf8: "1",
utf8: "1".as_bytes(),
..KeyEvent::default()
},
&Options {
@@ -2893,7 +2915,7 @@ mod tests {
ctrl: true,
..Mods::default()
},
utf8: "\u{151}",
utf8: "\u{151}".as_bytes(),
unshifted_codepoint: 337,
..KeyEvent::default()
},
@@ -2914,7 +2936,7 @@ mod tests {
&mut w,
&KeyEvent {
key: Key::Backspace,
utf8: "\u{7f}",
utf8: "\u{7f}".as_bytes(),
unshifted_codepoint: 0x08,
..KeyEvent::default()
},
@@ -2939,7 +2961,7 @@ mod tests {
mods: KittyMods::default(),
event: KittyEvent::None,
alternates: [None, None],
text: "",
text: b"",
}
}
@@ -3010,7 +3032,7 @@ mod tests {
fn kitty_sequence_text() {
assert_eq!(
enc(KittySequence {
text: "A",
text: "A".as_bytes(),
..seq(127, b'u')
}),
b"\x1B[127;;65u"
@@ -3018,7 +3040,7 @@ mod tests {
assert_eq!(
enc(KittySequence {
event: KittyEvent::Release,
text: "A",
text: "A".as_bytes(),
..seq(127, b'u')
}),
b"\x1B[127;1:3;65u"
@@ -3029,7 +3051,7 @@ mod tests {
shift: true,
..KittyMods::default()
},
text: "A",
text: "A".as_bytes(),
..seq(127, b'u')
}),
b"\x1B[127;2;65u"
@@ -3040,14 +3062,14 @@ mod tests {
fn kitty_sequence_text_with_control_characters() {
assert_eq!(
enc(KittySequence {
text: "\n",
text: "\n".as_bytes(),
..seq(127, b'u')
}),
b"\x1b[127u"
);
assert_eq!(
enc(KittySequence {
text: "A\n",
text: "A\n".as_bytes(),
..seq(127, b'u')
}),
b"\x1b[127;;65u"
@@ -3102,7 +3124,7 @@ mod tests {
#[test]
fn ctrlseq_normal_ctrl_c() {
assert_eq!(
ctrl_seq(Key::Unidentified, "c", 'c' as u32, ctrl()),
ctrl_seq(Key::Unidentified, "c".as_bytes(), 'c' as u32, ctrl()),
Some(0x03)
);
}
@@ -3110,7 +3132,7 @@ mod tests {
#[test]
fn ctrlseq_normal_ctrl_c_right_control() {
assert_eq!(
ctrl_seq(Key::Unidentified, "c", 'c' as u32, right_ctrl()),
ctrl_seq(Key::Unidentified, "c".as_bytes(), 'c' as u32, right_ctrl()),
Some(0x03)
);
}
@@ -3123,7 +3145,7 @@ mod tests {
..Mods::default()
};
assert_eq!(
ctrl_seq(Key::Unidentified, "c", 'c' as u32, mods),
ctrl_seq(Key::Unidentified, "c".as_bytes(), 'c' as u32, mods),
Some(0x03)
);
}
@@ -3131,7 +3153,12 @@ mod tests {
#[test]
fn ctrlseq_no_ctrl_does_nothing() {
assert_eq!(
ctrl_seq(Key::Unidentified, "c", 'c' as u32, Mods::default()),
ctrl_seq(
Key::Unidentified,
"c".as_bytes(),
'c' as u32,
Mods::default()
),
None
);
}
@@ -3144,7 +3171,7 @@ mod tests {
..Mods::default()
};
assert_eq!(
ctrl_seq(Key::Unidentified, "_", '-' as u32, mods),
ctrl_seq(Key::Unidentified, "_".as_bytes(), '-' as u32, mods),
Some(0x1F)
);
}
@@ -3157,7 +3184,7 @@ mod tests {
..Mods::default()
};
assert_eq!(
ctrl_seq(Key::Unidentified, "C", 'c' as u32, mods),
ctrl_seq(Key::Unidentified, "C".as_bytes(), 'c' as u32, mods),
Some(0x03)
);
}
@@ -3168,18 +3195,27 @@ mod tests {
shift: true,
..Mods::default()
};
assert_eq!(ctrl_seq(Key::Unidentified, "C", 'c' as u32, shift), None);
assert_eq!(
ctrl_seq(Key::Unidentified, "C".as_bytes(), 'c' as u32, shift),
None
);
let both = Mods {
shift: true,
ctrl: true,
..Mods::default()
};
assert_eq!(ctrl_seq(Key::Unidentified, "C", 'c' as u32, both), None);
assert_eq!(
ctrl_seq(Key::Unidentified, "C".as_bytes(), 'c' as u32, both),
None
);
}
#[test]
fn ctrlseq_russian_ctrl_c() {
assert_eq!(ctrl_seq(Key::KeyC, "с", 0x0441, ctrl()), Some(0x03));
assert_eq!(
ctrl_seq(Key::KeyC, "с".as_bytes(), 0x0441, ctrl()),
Some(0x03)
);
}
#[test]
@@ -3189,7 +3225,7 @@ mod tests {
shift: true,
..Mods::default()
};
assert_eq!(ctrl_seq(Key::KeyC, "с", 0x0441, mods), None);
assert_eq!(ctrl_seq(Key::KeyC, "с".as_bytes(), 0x0441, mods), None);
}
#[test]
@@ -3199,16 +3235,54 @@ mod tests {
alt: true,
..Mods::default()
};
assert_eq!(ctrl_seq(Key::KeyC, "с", 0x0441, mods), Some(0x03));
assert_eq!(
ctrl_seq(Key::KeyC, "с".as_bytes(), 0x0441, mods),
Some(0x03)
);
}
#[test]
fn ctrlseq_right_ctrl_c() {
assert_eq!(
ctrl_seq(Key::KeyC, "с", 'c' as u32, right_ctrl()),
ctrl_seq(Key::KeyC, "с".as_bytes(), 'c' as u32, right_ctrl()),
Some(0x03)
);
}
#[test]
fn invalid_utf8_text_is_passed_through_or_ignored() {
let ev = KeyEvent {
key: Key::KeyA,
unshifted_codepoint: 'a' as u32,
utf8: b"\xff\xfe",
..KeyEvent::default()
};
let mut out = Vec::new();
legacy_like(&mut out, &ev);
assert_eq!(out, b"\xff\xfe");
// Kitty associated text with invalid UTF-8 is dropped, not lossy.
let mut out = Vec::new();
encode(
&mut out,
&ev,
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
report_all: true,
report_associated: true,
..KittyFlags::default()
},
..Options::default()
},
)
.unwrap();
assert_eq!(out, b"\x1b[97u");
}
fn legacy_like(out: &mut Vec<u8>, ev: &KeyEvent<'_>) {
encode(out, ev, &Options::default()).unwrap();
}
}
/// The Zig tests that only run on Darwin (`isDarwin()` / `os.tag == .macos`
@@ -3255,7 +3329,7 @@ mod macos_tests {
alt: true,
..Mods::default()
},
utf8: "∑",
utf8: "∑".as_bytes(),
unshifted_codepoint: 119,
..KeyEvent::default()
};
@@ -3276,7 +3350,7 @@ mod macos_tests {
alt: true,
..Mods::default()
},
utf8: "∑",
utf8: "∑".as_bytes(),
unshifted_codepoint: 119,
..KeyEvent::default()
};
@@ -3288,7 +3362,7 @@ mod macos_tests {
// Without Alt modifier
let ev = KeyEvent {
key: Key::KeyW,
utf8: "∑",
utf8: "∑".as_bytes(),
unshifted_codepoint: 119,
..KeyEvent::default()
};
@@ -3304,7 +3378,7 @@ mod macos_tests {
with_darwin(true, || {
let ev = KeyEvent {
key: Key::KeyC,
utf8: "≈",
utf8: "≈".as_bytes(),
unshifted_codepoint: u32::from('c'),
mods: Mods {
alt: true,
@@ -3333,7 +3407,7 @@ mod macos_tests {
with_darwin(true, || {
let ev = KeyEvent {
key: Key::Period,
utf8: ">",
utf8: ">".as_bytes(),
unshifted_codepoint: u32::from('.'),
mods: Mods {
alt: true,
@@ -3365,7 +3439,7 @@ mod macos_tests {
..Mods::default()
},
// common translation of option+8 with European keyboard layouts
utf8: "[",
utf8: "[".as_bytes(),
..KeyEvent::default()
};
let opts = Options {
@@ -3382,7 +3456,7 @@ mod macos_tests {
with_darwin(true, || {
let ev = KeyEvent {
key: Key::KeyB,
utf8: "b",
utf8: "b".as_bytes(),
mods: Mods {
super_: true,
..Mods::default()
@@ -3395,7 +3469,7 @@ mod macos_tests {
with_darwin(false, || {
let ev = KeyEvent {
key: Key::KeyB,
utf8: "b",
utf8: "b".as_bytes(),
mods: Mods {
super_: true,
..Mods::default()
@@ -3411,7 +3485,7 @@ mod macos_tests {
with_darwin(true, || {
let ev = KeyEvent {
key: Key::KeyB,
utf8: "B",
utf8: "B".as_bytes(),
mods: Mods {
super_: true,
shift: true,