feat(input): port keyboard, keybinding and mouse input handling

Ports ZIG/src/input (except macOS keymaps) into ghostty-input:
- key, key_mods (incl. RemapSet), the physical keycode table
- key_encode: legacy, modifyOtherKeys and kitty keyboard protocol
  encoders, function key tables
- Binding: all keybinding actions with their user-visible docs (for
  +list-actions), triggers, sets with sequences, chains, leader keys and
  key tables, and formatting back to config strings
- command palette defaults, mouse, paste safety checks, kitty helpers,
  links, helpgen action docs

Deviations (docs/rewrite/notes/ghostty-input.md): Mods.super is named
super_; case folding uses char::to_lowercase plus Ghostty's special
cases instead of uucode tables; chain parents are trigger paths instead
of pointers; a Zig bug where chained leaves inside sequences corrupted the
shared prefix when formatting is fixed (covered by a test).

Integration left: building encode/paste Options from terminal modes,
and implementing the EntryFormatter trait in ghostty-config.

Test Plan:
- cargo test -p ghostty-input (220 tests; 8 macOS-only skipped)
- cargo clippy -p ghostty-input --all-targets -- -D warnings
This commit is contained in:
ddidderr committed 2026-10-11 21:56:07 +02:00
1 parent 5b8c92b98c
commit e03432241b
21 files changed
+13413 -1

No files matched your search

+3161
View File
@@ -0,0 +1,3161 @@
//! Key event encoding: legacy (xterm/fixterms) and Kitty keyboard protocol
//! (ZIG/src/input/key_encode.zig).
use std::io::{self, Write};
use ghostty_vt::kitty::key::Flags as KittyFlags;
use crate::{
config::OptionAsAlt,
function_keys::{self, CursorMode, KeypadMode, ModifyKeys},
key::{self, Key, KeyEvent},
kitty::{self as kitty_table, Entry as KittyEntry},
};
/// Options that affect key encoding behavior. This is a mix of behavior
/// from terminal state as well as application configuration.
///
/// TODO(integration): Zig has `Options.fromTerminal(*const Terminal)`; the
/// terminal crate should construct this from its modes (`alt_esc_prefix`,
/// `cursor_keys`, `keypad_keys`, `ignore_keypad_with_numlock`, flag
/// `modify_other_keys_2`, active screen `kitty_keyboard.current()`).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Options {
/// Terminal DEC mode 1
pub cursor_key_application: bool,
/// Terminal DEC mode 66
pub keypad_key_application: bool,
/// Terminal DEC mode 1035
pub ignore_keypad_with_numlock: bool,
/// Terminal DEC mode 1036
pub alt_esc_prefix: bool,
/// xterm "modifyOtherKeys mode 2". Details here:
/// https://invisible-island.net/xterm/modified-keys.html
pub modify_other_keys_state_2: bool,
/// Kitty keyboard protocol flags.
pub kitty_flags: KittyFlags,
/// Determines whether the "option" key on macOS is treated
/// as "alt" or not. See the Ghostty `macos_option-as-alt` config
/// docs for a more detailed description of why this is needed.
pub macos_option_as_alt: OptionAsAlt,
}
impl Options {
pub const DEFAULT: Options = Options {
cursor_key_application: false,
keypad_key_application: false,
ignore_keypad_with_numlock: false,
alt_esc_prefix: false,
modify_other_keys_state_2: false,
kitty_flags: KittyFlags::DISABLED,
macos_option_as_alt: OptionAsAlt::False,
};
}
/// Encode the key event to the writer in the proper format given
/// the options. For example, this will properly encode a key press
/// such as "ctrl+A" to Kitty format if Kitty encoding is enabled.
///
/// Not all key events will result in output. It is up to the caller
/// to use a writer that can track whether any output was written if
/// they care about that.
pub fn encode<W: Write>(writer: &mut W, event: &KeyEvent, opts: &Options) -> io::Result<()> {
if opts.kitty_flags.int() != 0 {
kitty(writer, event, opts)
} else {
legacy(writer, event, opts)
}
}
fn is_enter_backspace_tab(k: Key) -> bool {
matches!(k, Key::Enter | Key::Backspace | Key::Tab)
}
/// Perform Kitty keyboard protocol encoding of the key event.
fn kitty<W: Write>(writer: &mut W, event: &KeyEvent, opts: &Options) -> io::Result<()> {
// This should never happen but we'll check anyway.
if opts.kitty_flags.int() == 0 {
return legacy(writer, event, opts);
}
// We only processed "press" events unless report events is active
if event.action == key::Action::Release {
if !opts.kitty_flags.report_events {
return Ok(());
}
// Enter, backspace, and tab do not report release events unless "report
// all" is set
if !opts.kitty_flags.report_all && is_enter_backspace_tab(event.key) {
return Ok(());
}
}
let all_mods = event.mods;
let effective_mods = event.effective_mods();
let binding_mods = effective_mods.binding();
// Find the entry for this key in the kitty table.
let entry_: Option<KittyEntry> = 'entry: {
// Functional or predefined keys
for entry in kitty_table::entries() {
if entry.key == event.key {
break 'entry Some(*entry);
}
}
// Otherwise, we use our unicode codepoint from UTF8. We
// always use the unshifted value.
if event.unshifted_codepoint > 0 {
break 'entry Some(KittyEntry {
key: event.key,
code: event.unshifted_codepoint,
final_: b'u',
modifier: false,
});
}
None
};
'preprocessing: {
// When composing, the only keys sent are plain modifiers.
if event.composing {
if let Some(entry) = &entry_
&& entry.modifier
{
break 'preprocessing;
}
return Ok(());
}
// IME confirmation still sends an enter key so if we have enter
// and UTF8 text we just send it directly since we assume that is
// what's happening. See legacy()'s similar logic for more details
// on how to verify this.
if !event.utf8.is_empty() && matches!(event.key, Key::Enter | Key::Backspace) {
// See legacy for why we handle this this way.
if !is_control_utf8(event.utf8) {
if event.key == Key::Backspace {
return Ok(());
}
return writer.write_all(event.utf8.as_bytes());
}
}
// If we're reporting all then we always send CSI sequences.
if !opts.kitty_flags.report_all {
// Quote:
// The only exceptions are the Enter, Tab and Backspace keys which
// still generate the same bytes as in legacy mode this is to allow the
// user to type and execute commands in the shell such as reset after a
// program that sets this mode crashes without clearing it.
//
// Quote ("report all" mode):
// Note that all keys are reported as escape codes, including Enter,
// Tab, Backspace etc.
if binding_mods.empty() {
match event.key {
Key::Enter => return writer.write_all(b"\r"),
Key::Tab => return writer.write_all(b"\t"),
Key::Backspace => return writer.write_all(&[0x7F]),
_ => {}
}
}
// Send plain-text non-modified text directly to the terminal.
// We don't send release events because those are specially encoded.
if !event.utf8.is_empty()
&& binding_mods.empty()
&& event.action != key::Action::Release
{
// 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());
}
}
}
}
let Some(entry) = entry_ else {
// No entry found. If we have UTF-8 text this is a pure text event
// (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 Ok(());
};
// If this is just a modifier we require "report all" to send the sequence.
if entry.modifier && !opts.kitty_flags.report_all {
return Ok(());
}
let seq = {
let mut seq = KittySequence {
key: entry.code,
final_: entry.final_,
mods: KittyMods::from_input(event.action, event.key, all_mods),
event: KittyEvent::None,
alternates: [None, None],
text: "",
};
if opts.kitty_flags.report_events {
seq.event = match event.action {
key::Action::Press => KittyEvent::Press,
key::Action::Release => KittyEvent::Release,
key::Action::Repeat => KittyEvent::Repeat,
};
}
'alternates: {
if !opts.kitty_flags.report_alternates {
break 'alternates;
}
// Break early if this is a control key
if is_control(seq.key) {
break 'alternates;
}
let mut it = event.utf8.chars();
// If we have a codepoint in our UTF-8 sequence, then we can
// report the shifted version.
if let Some(cp1) = it.next().map(u32::from) {
// Set the first alternate (shifted version)
if cp1 != seq.key && seq.mods.shift {
seq.alternates[0] = Some(cp1);
}
// We want to know if there are additional codepoints because
// our logic below depends on the utf8 being a single codepoint.
let has_cp2 = it.next().is_some();
// Set the base layout key. We only report this if this codepoint
// differs from our pressed key.
if let Some(base) = event.key.codepoint()
&& base != seq.key
&& (cp1 != base && !has_cp2)
{
seq.alternates[1] = Some(base);
}
} else {
// No UTF-8 so we can't report a shifted key but we can still
// report a base layout key.
if let Some(base) = event.key.codepoint()
&& base != seq.key
{
seq.alternates[1] = Some(base);
}
}
}
if opts.kitty_flags.report_associated && seq.event != KittyEvent::Release {
// Determine if the Alt modifier should be treated as an actual
// modifier (in which case it prevents associated text) or as
// the macOS Option key, which does not prevent associated text.
// (Non-macOS: always true.)
let alt_prevents_text = true;
if !seq.mods.prevents_text(alt_prevents_text) {
seq.text = event.utf8;
}
}
seq
};
seq.encode(writer)
}
/// Perform legacy encoding of the key event. "Legacy" in this case
/// is referring to the behavior of traditional terminals, plus
/// xterm's `modifyOtherKeys`, plus Paul Evans's "fixterms" spec.
/// These together combine the legacy protocol because they're all
/// meant to be extensions that do not change any existing behavior
/// and therefore safe to combine.
fn legacy<W: Write>(writer: &mut W, event: &KeyEvent, opts: &Options) -> io::Result<()> {
let all_mods = event.mods;
let effective_mods = event.effective_mods();
let binding_mods = effective_mods.binding();
// Legacy encoding only does press/repeat
if event.action != key::Action::Press && event.action != key::Action::Repeat {
return Ok(());
}
// If we're in a dead key state then we never emit a sequence.
if event.composing {
return Ok(());
}
// If we match a PC style function key then that is our result.
if let Some(sequence) = pc_style_function_key(
event.key,
all_mods,
opts.cursor_key_application,
opts.keypad_key_application,
opts.ignore_keypad_with_numlock,
opts.modify_other_keys_state_2,
) {
// If we have UTF-8 text, then we never emit PC style function
// keys. Many function keys (escape, enter, backspace) have
// a specific meaning when dead keys are active and so we don't
// want to send that to the terminal. Examples:
//
// - Japanese: escape clears the dead key state
// - Korean: escape commits the dead key state
// - Korean: backspace should delete a single preedit char
//
let mut skip_pc_style = false;
if !event.utf8.is_empty()
&& matches!(event.key, Key::Backspace | Key::Enter | Key::Escape)
// We want to ignore control characters. This is because
// some apprts (macOS) will send control characters as
// UTF-8 encodings and we handle that manually.
&& !is_control_utf8(event.utf8)
{
// Backspace encodes nothing because we modified IME.
// Enter/escape don't encode the PC-style encoding
// because we want to encode committed text.
if event.key == Key::Backspace {
return Ok(());
}
skip_pc_style = true;
}
if !skip_pc_style {
return writer.write_all(sequence.as_bytes());
}
}
// If we match a control sequence, we output that directly. For
// ctrlSeq we have to use all mods because we want it to only
// match ctrl+<char>.
if let Some(ch) = ctrl_seq(event.key, event.utf8, event.unshifted_codepoint, all_mods) {
// C0 sequences support alt-as-esc prefixing.
if binding_mods.alt {
return writer.write_all(&[0x1B, ch]);
}
return writer.write_all(&[ch]);
}
// If we have no UTF8 text then the only possibility is the
// alt-prefix handling of unshifted codepoints... so we process that.
let utf8 = event.utf8;
if utf8.is_empty() {
if let Some(byte) = legacy_alt_prefix(event, binding_mods, all_mods, opts) {
writer.write_all(&[0x1B, byte])?;
}
return Ok(());
}
// In modify other keys state 2, we send the CSI 27 sequence
// 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();
// 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()) {
// The mods we encode for this are just the binding mods (shift, ctrl,
// super, alt unless it is actually option).
let mods = event.mods.binding();
// This copies xterm's `ModifyOtherKeys` function that returns
// whether modify other keys should be encoded for the given
// input.
let should_modify = 'should_modify: {
// xterm IsControlInput
if (0x40..=0x7F).contains(&codepoint) {
break 'should_modify true;
}
// If we have anything other than shift pressed, encode.
let mut mods_no_shift = mods;
mods_no_shift.shift = false;
if !mods_no_shift.empty() {
break 'should_modify true;
}
// We only have shift pressed. We only allow space.
if codepoint == u32::from(' ') {
break 'should_modify true;
}
// This logic isn't complete but I don't fully understand
// the rest so I'm going to wait until we can have a
// reasonable test scenario.
false
};
if should_modify {
for (modset, code) in function_keys::MODIFIERS.iter().zip(2..) {
if !mods.equal(*modset) {
continue;
}
return write!(writer, "\x1B[27;{code};{codepoint}~");
}
}
}
}
// Let's see if we should apply fixterms to this codepoint.
// At this stage of key processing, we only need to apply fixterms
// to unicode codepoints if we have ctrl set.
if event.mods.ctrl {
'csiu: {
// Important: we want to use the original mods here, not the
// effective mods. The fixterms spec states the shifted chars
// should be sent uppercase but Kitty changes that behavior
// so we'll send all the mods.
let mut csi_u_mods = CsiUMods::from_input(event.mods);
// Get our codepoint. If we have more than one codepoint this
// can't be valid CSIu.
let mut it = event.utf8.chars();
let Some(first) = it.next() else {
break 'csiu;
};
if it.next().is_some() {
break 'csiu;
}
let mut ch = u32::from(first);
// If our character is A to Z and we have shift set, then
// we lowercase it. This is a Kitty-specific behavior that
// we choose to follow and diverge from the fixterms spec.
// This makes it easier for programs to detect shifted letters
// for keybindings and is not just theoretical but used by
// real programs.
if (u32::from('A')..=u32::from('Z')).contains(&ch) && csi_u_mods.shift {
// We want to rely on apprt to send us the correct
// unshifted codepoint...
ch += 32;
}
// If our unshifted codepoint is identical to the shifted
// then we consider shift. Otherwise, we do not because the
// shift key was used to obtain the character. This is specified
// by fixterms.
if event.unshifted_codepoint != ch {
csi_u_mods.shift = false;
}
return write!(writer, "\x1B[{};{}u", ch, csi_u_mods.seq_int());
}
}
// If we have alt-pressed and alt-esc-prefix is enabled, then
// we need to prefix the utf8 sequence with an esc.
if let Some(byte) = legacy_alt_prefix(event, binding_mods, all_mods, opts) {
return writer.write_all(&[0x1B, byte]);
}
// (On macOS, command+keys do not encode text. On Linux we continue to
// encode text because it is typical.)
writer.write_all(utf8.as_bytes())
}
fn legacy_alt_prefix(
event: &KeyEvent,
binding_mods: key::Mods,
_mods: key::Mods,
opts: &Options,
) -> Option<u8> {
// This only takes effect with alt pressed
if !binding_mods.alt || !opts.alt_esc_prefix {
return None;
}
// (macOS option-as-alt handling does not apply on Linux.)
// Otherwise, we require utf8 to already have the byte represented.
let utf8 = event.utf8.as_bytes();
if utf8.len() == 1 {
return Some(utf8[0]);
}
// If UTF8 isn't set, we will allow unshifted codepoints through.
if event.unshifted_codepoint > 0
&& let Ok(byte) = u8::try_from(event.unshifted_codepoint)
{
return Some(byte);
}
// Else, we can't figure out the byte to alt-prefix so we
// exit this handling.
None
}
/// Determines whether the key should be encoded in the xterm
/// "PC-style Function Key" syntax (roughly). This is a hardcoded
/// table of keys and modifiers that result in a specific sequence.
fn pc_style_function_key(
keyval: Key,
mods: key::Mods,
cursor_key_application: bool,
keypad_key_application_req: bool,
ignore_keypad_with_numlock: bool,
modify_other_keys: bool, // True if state 2
) -> Option<&'static str> {
// We only want binding-sensitive mods because lock keys
// and directional modifiers (left/right) don't matter for
// pc-style function keys.
let mods_int = mods.binding().int();
// Keypad application keymode isn't super straightforward.
// On xterm, in VT220 mode, numlock alone is enough to trigger
// application mode. But in more modern modes, numlock is
// ignored by default via mode 1035 (default true). If mode
// 1035 is on, we always are in numerical keypad mode. If
// mode 1035 is off, we are in application mode if the
// proper numlock state is pressed. The numlock state is implicitly
// determined based on the keycode sent (i.e. 1 with numlock
// on will be kp_end).
//
// If we're ignoring keypad then this is always false.
// In other words, we're always in numerical keypad mode.
let keypad_key_application = if ignore_keypad_with_numlock {
false
} else {
keypad_key_application_req
};
for entry in function_keys::keys(keyval) {
match entry.cursor {
CursorMode::Any => {}
CursorMode::Normal => {
if cursor_key_application {
continue;
}
}
CursorMode::Application => {
if !cursor_key_application {
continue;
}
}
}
match entry.keypad {
KeypadMode::Any => {}
KeypadMode::Normal => {
if keypad_key_application {
continue;
}
}
KeypadMode::Application => {
if !keypad_key_application {
continue;
}
}
}
match entry.modify_other_keys {
ModifyKeys::Any => {}
ModifyKeys::Set => {
if modify_other_keys {
continue;
}
}
ModifyKeys::SetOther => {
if !modify_other_keys {
continue;
}
}
}
let entry_mods_int = entry.mods.int();
if entry_mods_int == 0 {
if mods_int != 0 && !entry.mods_empty_is_any {
continue;
}
// mods are either empty, or empty means any so we allow it.
} else if entry_mods_int != mods_int {
// any set mods require an exact match
continue;
}
return Some(entry.sequence.as_str());
}
None
}
/// Returns the C0 byte for the key event if it should be used.
/// This converts a key event into the expected terminal behavior
/// such as Ctrl+C turning into 0x03, amongst many other translations.
///
/// 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> {
let ctrl_only = key::Mods {
ctrl: true,
..key::Mods::default()
}
.int();
// If ctrl is not pressed then we never do anything.
if !mods.ctrl {
return None;
}
// We need to only get binding modifiers so we strip lock
// keys, sides, etc.
let mut unset_mods = mods.binding();
// Remove alt from our modifiers because it does not impact whether
// we are generating a ctrl sequence and we handle the ESC-prefix
// logic separately.
unset_mods.alt = false;
let mut ch: u8 = 'ch: {
// 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];
}
// If we have a logical key that maps to a single byte
// printable character, we use that. History to explain this:
// this was added to support cyrillic keyboard layouts such
// as Russian and Mongolian. These layouts have a `c` key that
// maps to U+0441 (cyrillic small letter "c") but every
// terminal I've tested encodes this as ctrl+c.
if let Some(cp) = logical_key.codepoint()
&& let Ok(byte) = u8::try_from(cp)
{
// For this specific case, we only map to the key if
// we have exactly ctrl pressed. This is because shift
// would modify the key and we don't know how to do that
// properly here (don't have the layout). And we want
// to encode shift as CSIu.
if unset_mods.int() != ctrl_only {
return None;
}
break 'ch byte;
}
// Otherwise we don't have a character to convert that
// we can reliably map to a C0 byte.
return None;
};
// Remove shift if we have something outside of the US letter
// range. This is so that characters such as `ctrl+shift+-`
// generate the correct ctrl-seq (used by emacs).
if unset_mods.shift && !ch.is_ascii_uppercase() {
// Special case for fixterms awkward case as specified.
if ch != b'@' {
unset_mods.shift = false;
}
}
// If the character is uppercase, we convert it to lowercase. We
// rely on the unshifted codepoint to do this. This handles
// the scenario where we have caps lock pressed. Note that
// shifted characters are handled above, if we are just pressing
// shift then the ctrl-only check will fail later and we won't
// ctrl-seq encode.
if ch.is_ascii_uppercase()
&& unshifted_codepoint > 0
&& let Ok(byte) = u8::try_from(unshifted_codepoint)
{
ch = byte;
}
// An additional note on caps lock and shift interaction.
// If we have caps lock set and an ASCII letter is pressed,
// we lowercase it (above). If we have only control pressed,
// we process it as a ctrl seq. For example ctrl+M with caps
// lock but no shift will encode as 0x0D.
//
// But, if you press ctrl+shift+m, this will not encode as a
// ctrl-seq and falls through to CSIu encoding. This lets programs
// detect the difference between ctrl+M and ctrl+shift+M. This
// diverges from the fixterms "spec" and most terminals. This
// only matches Kitty in behavior. But I believe this is a
// justified divergence because it's a useful distinction.
// After unsetting, we only continue if we have ONLY control set.
if unset_mods.int() != ctrl_only {
return None;
}
// From Kitty's key encoding logic. I tried to discern the exact
// behavior across different terminals but it's not clear, so I'm
// just going to repeat what Kitty does.
match ch {
b' ' => Some(0),
b'/' => Some(31),
b'0' => Some(48),
b'1' => Some(49),
b'2' => Some(0),
b'3' => Some(27),
b'4' => Some(28),
b'5' => Some(29),
b'6' => Some(30),
b'7' => Some(31),
b'8' => Some(127),
b'9' => Some(57),
b'?' => Some(127),
b'@' => Some(0),
b'\\' => Some(28),
b']' => Some(29),
b'^' => Some(30),
b'_' => Some(31),
b'a' => Some(1),
b'b' => Some(2),
b'c' => Some(3),
b'd' => Some(4),
b'e' => Some(5),
b'f' => Some(6),
b'g' => Some(7),
b'h' => Some(8),
b'j' => Some(10),
b'k' => Some(11),
b'l' => Some(12),
b'n' => Some(14),
b'o' => Some(15),
b'p' => Some(16),
b'q' => Some(17),
b'r' => Some(18),
b's' => Some(19),
b't' => Some(20),
b'u' => Some(21),
b'v' => Some(22),
b'w' => Some(23),
b'x' => Some(24),
b'y' => Some(25),
b'z' => Some(26),
b'~' => Some(30),
// These are purposely NOT handled here because of the fixterms
// specification: https://www.leonerd.org.uk/hacks/fixterms/
// These are processed as CSI u.
// 'i' => 0x09,
// 'm' => 0x0D,
// '[' => 0x1B,
_ => None,
}
}
/// Returns true if this is an ASCII control character, matches libc implementation.
fn is_control(cp: u32) -> bool {
cp < 0x20 || cp == 0x7F
}
/// 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]))
}
/// This is the bitmask for fixterm CSI u modifiers.
#[derive(Debug, Clone, Copy, Default)]
struct CsiUMods {
shift: bool,
alt: bool,
ctrl: bool,
}
impl CsiUMods {
/// Convert an input mods value into the CSI u mods value.
fn from_input(mods: key::Mods) -> CsiUMods {
CsiUMods {
shift: mods.shift,
alt: mods.alt,
ctrl: mods.ctrl,
}
}
/// Returns the raw int value of this packed struct.
fn int(self) -> u8 {
u8::from(self.shift) | u8::from(self.alt) << 1 | u8::from(self.ctrl) << 2
}
/// Returns the integer value sent as part of the CSI u sequence.
/// This adds 1 to the bitmask value as described in the spec.
fn seq_int(self) -> u8 {
self.int() + 1
}
}
/// This is the bitfields for Kitty modifiers.
#[derive(Debug, Clone, Copy, Default)]
#[allow(dead_code)]
struct KittyMods {
shift: bool,
alt: bool,
ctrl: bool,
super_: bool,
hyper: bool,
meta: bool,
caps_lock: bool,
num_lock: bool,
}
impl KittyMods {
/// Convert an input mods value into the CSI u mods value.
fn from_input(_action: key::Action, _k: Key, mods: key::Mods) -> KittyMods {
KittyMods {
shift: mods.shift,
alt: mods.alt,
ctrl: mods.ctrl,
super_: mods.super_,
hyper: false,
meta: false,
caps_lock: mods.caps_lock,
num_lock: mods.num_lock,
}
}
/// Returns true if the modifiers prevent printable text.
///
/// The alt_prevents_text parameter determines whether or not the Alt
/// modifier prevents printable text. On Linux, this is always true. On
/// macOS, this is only true if macos-option-as-alt is set.
fn prevents_text(self, alt_prevents_text: bool) -> bool {
(self.alt && alt_prevents_text) || self.ctrl || self.super_ || self.hyper || self.meta
}
/// Returns the raw int value of this packed struct.
fn int(self) -> u16 {
u16::from(self.shift)
| u16::from(self.alt) << 1
| u16::from(self.ctrl) << 2
| u16::from(self.super_) << 3
| u16::from(self.hyper) << 4
| u16::from(self.meta) << 5
| u16::from(self.caps_lock) << 6
| u16::from(self.num_lock) << 7
}
/// Returns the integer value sent as part of the Kitty sequence.
/// This adds 1 to the bitmask value as described in the spec.
fn seq_int(self) -> u16 {
self.int() + 1
}
}
/// Values for the event code (see "event-type" in the KittySequence comment).
/// Note that Kitty omits the ":1" for the press event but other
/// terminals include it. We'll include it.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum KittyEvent {
None = 0,
Press = 1,
Repeat = 2,
Release = 3,
}
/// Represents a kitty key sequence and has helpers for encoding it.
/// The sequence from the Kitty specification:
///
/// CSI unicode-key-code:alternate-key-codes ; modifiers:event-type ; text-as-codepoints u
#[derive(Debug, Clone, Copy)]
struct KittySequence<'a> {
key: u32,
final_: u8,
mods: KittyMods,
event: KittyEvent,
alternates: [Option<u32>; 2],
text: &'a str,
}
impl KittySequence<'_> {
fn encode<W: Write>(&self, writer: &mut W) -> io::Result<()> {
if self.final_ == b'u' || self.final_ == b'~' {
return self.encode_full(writer);
}
self.encode_special(writer)
}
fn encode_full<W: Write>(&self, writer: &mut W) -> io::Result<()> {
// Key section
write!(writer, "\x1B[{}", self.key)?;
// Write our alternates
if let Some(shifted) = self.alternates[0] {
write!(writer, ":{shifted}")?;
}
if let Some(base) = self.alternates[1] {
if self.alternates[0].is_none() {
write!(writer, "::{base}")?;
} else {
write!(writer, ":{base}")?;
}
}
// Mods and events section
let mods = self.mods.seq_int();
let mut emit_prior = false;
if self.event != KittyEvent::None && self.event != KittyEvent::Press {
write!(writer, ";{}:{}", mods, self.event as u8)?;
emit_prior = true;
} else if mods > 1 {
write!(writer, ";{mods}")?;
emit_prior = true;
}
// Text section
if !self.text.is_empty() {
let mut count = 0;
for c in self.text.chars() {
let cp = u32::from(c);
// If the codepoint is non-printable ASCII character, skip.
if is_control(cp) {
continue;
}
// We need to add our ";". We need to add two if we didn't emit
// the modifier section. We only do this initially.
if count == 0 {
if !emit_prior {
writer.write_all(b";")?;
}
writer.write_all(b";")?;
} else {
writer.write_all(b":")?;
}
write!(writer, "{cp}")?;
count += 1;
}
}
writer.write_all(&[self.final_])
}
fn encode_special<W: Write>(&self, writer: &mut W) -> io::Result<()> {
let mods = self.mods.seq_int();
let f = self.final_ as char;
if self.event != KittyEvent::None {
return write!(writer, "\x1B[1;{}:{}{}", mods, self.event as u8, f);
}
if mods > 1 {
return write!(writer, "\x1B[1;{mods}{f}");
}
write!(writer, "\x1B[{f}")
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use crate::key_mods::{ModSide, Mods, Side};
#[test]
fn kitty_plain_text() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
key: Key::KeyA,
mods: Mods { ..Mods::default() },
utf8: "abcd",
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"abcd");
}
}
#[test]
fn kitty_repeat_with_just_disambiguate() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
key: Key::KeyA,
action: key::Action::Repeat,
mods: Mods { ..Mods::default() },
utf8: "a",
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"a");
}
}
#[test]
fn kitty_enter_backspace_tab() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
key: Key::Enter,
mods: Mods { ..Mods::default() },
utf8: "",
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x0d");
}
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
key: Key::Backspace,
mods: Mods { ..Mods::default() },
utf8: "",
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x7f");
}
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
key: Key::Tab,
mods: Mods { ..Mods::default() },
utf8: "",
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x09");
}
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
action: key::Action::Release,
key: Key::Enter,
mods: Mods { ..Mods::default() },
utf8: "",
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
report_events: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"");
}
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
action: key::Action::Release,
key: Key::Backspace,
mods: Mods { ..Mods::default() },
utf8: "",
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
report_events: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"");
}
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
action: key::Action::Release,
key: Key::Tab,
mods: Mods { ..Mods::default() },
utf8: "",
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
report_events: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"");
}
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
action: key::Action::Release,
key: Key::Enter,
mods: Mods { ..Mods::default() },
utf8: "",
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
report_events: true,
report_all: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[13;1:3u");
}
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
action: key::Action::Release,
key: Key::Backspace,
mods: Mods { ..Mods::default() },
utf8: "",
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
report_events: true,
report_all: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[127;1:3u");
}
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
action: key::Action::Release,
key: Key::Tab,
mods: Mods { ..Mods::default() },
utf8: "",
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
report_events: true,
report_all: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[9;1:3u");
}
}
#[test]
fn kitty_shift_backspace_emits_csi_u() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
key: Key::Backspace,
mods: Mods {
shift: true,
..Mods::default()
},
utf8: "",
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[127;2u");
}
}
#[test]
fn kitty_shift_enter_emits_csi_u() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
key: Key::Enter,
mods: Mods {
shift: true,
..Mods::default()
},
utf8: "",
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[13;2u");
}
}
#[test]
fn kitty_shift_tab_emits_csi_u() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
key: Key::Tab,
mods: Mods {
shift: true,
..Mods::default()
},
utf8: "",
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[9;2u");
}
}
#[test]
fn kitty_enter_with_all_flags() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
key: Key::Enter,
mods: Mods { ..Mods::default() },
utf8: "",
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
report_events: true,
report_alternates: true,
report_all: true,
report_associated: true,
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w[1..], b"[13u");
}
}
#[test]
fn kitty_ctrl_with_all_flags() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
key: Key::ControlLeft,
mods: Mods {
ctrl: true,
..Mods::default()
},
utf8: "",
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
report_events: true,
report_alternates: true,
report_all: true,
report_associated: true,
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w[1..], b"[57442;5u");
}
}
#[test]
fn kitty_ctrl_release_with_ctrl_mod_set() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
action: key::Action::Release,
key: Key::ControlLeft,
mods: Mods {
ctrl: true,
..Mods::default()
},
utf8: "",
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
report_events: true,
report_alternates: true,
report_all: true,
report_associated: true,
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w[1..], b"[57442;5:3u");
}
}
#[test]
fn kitty_delete() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
key: Key::Delete,
mods: Mods { ..Mods::default() },
utf8: "\u{7f}",
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[3~");
}
}
#[test]
fn kitty_composing_with_no_modifier() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
key: Key::KeyA,
mods: Mods {
shift: true,
..Mods::default()
},
composing: true,
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"");
}
}
#[test]
fn kitty_composing_with_modifier() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
key: Key::ShiftLeft,
mods: Mods {
shift: true,
..Mods::default()
},
composing: true,
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
report_all: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[57441;2u");
}
}
#[test]
fn kitty_composed_text_with_report_all() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
key: Key::Unidentified,
mods: Mods { ..Mods::default() },
utf8: "\u{fb}",
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
report_events: true,
report_alternates: true,
report_all: true,
report_associated: true,
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\xc3\xbb");
}
}
#[test]
fn kitty_shift_a_on_us_keyboard() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
key: Key::KeyA,
mods: Mods {
shift: true,
..Mods::default()
},
utf8: "A",
unshifted_codepoint: 97,
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
report_alternates: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[97:65;2u");
}
}
#[test]
fn kitty_matching_unshifted_codepoint() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
key: Key::KeyA,
mods: Mods {
shift: true,
..Mods::default()
},
utf8: "A",
unshifted_codepoint: 65,
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
report_alternates: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[65::97;2u");
}
}
#[test]
fn kitty_report_alternates_with_caps() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
key: Key::KeyJ,
mods: Mods {
caps_lock: true,
..Mods::default()
},
utf8: "J",
unshifted_codepoint: 106,
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
report_all: true,
report_alternates: true,
report_associated: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[106;65;74u");
}
}
#[test]
fn kitty_report_alternates_colon_shift() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
key: Key::Semicolon,
mods: Mods {
shift: true,
..Mods::default()
},
utf8: ":",
unshifted_codepoint: u32::from(';'),
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
report_all: true,
report_alternates: true,
report_associated: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[59:58;2;58u");
}
}
#[test]
fn kitty_report_alternates_with_ru_layout() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
key: Key::Semicolon,
mods: Mods { ..Mods::default() },
utf8: "\u{447}",
unshifted_codepoint: 1095,
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
report_all: true,
report_alternates: true,
report_associated: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[1095::59;;1095u");
}
}
#[test]
fn kitty_report_alternates_with_ru_layout_shifted() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
key: Key::Semicolon,
mods: Mods {
shift: true,
..Mods::default()
},
utf8: "\u{427}",
unshifted_codepoint: 1095,
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
report_all: true,
report_alternates: true,
report_associated: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[1095:1063:59;2;1063u");
}
}
#[test]
fn kitty_report_alternates_with_ru_layout_caps_lock() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
key: Key::Semicolon,
mods: Mods {
caps_lock: true,
..Mods::default()
},
utf8: "\u{427}",
unshifted_codepoint: 1095,
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
report_all: true,
report_alternates: true,
report_associated: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[1095::59;65;1063u");
}
}
#[test]
fn kitty_report_alternates_with_hu_layout_release() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
action: key::Action::Release,
key: Key::BracketLeft,
mods: Mods {
ctrl: true,
..Mods::default()
},
utf8: "",
unshifted_codepoint: 337,
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
report_all: true,
report_alternates: true,
report_associated: true,
report_events: true,
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w[1..], b"[337::91;5:3u");
}
}
#[test]
fn kitty_up_arrow_with_utf8() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
key: Key::ArrowUp,
mods: Mods { ..Mods::default() },
utf8: "\u{1e}",
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[A");
}
}
#[test]
fn kitty_shift_tab() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
key: Key::Tab,
mods: Mods {
shift: true,
..Mods::default()
},
utf8: "",
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
report_alternates: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[9;2u");
}
}
#[test]
fn kitty_left_shift() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
key: Key::ShiftLeft,
mods: Mods { ..Mods::default() },
utf8: "",
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
report_alternates: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"");
}
}
#[test]
fn kitty_left_shift_with_report_all() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
key: Key::ShiftLeft,
mods: Mods { ..Mods::default() },
utf8: "",
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
report_all: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[57441u");
}
}
#[test]
fn kitty_report_associated_with_modifiers() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
key: Key::KeyJ,
mods: Mods {
ctrl: true,
..Mods::default()
},
utf8: "j",
unshifted_codepoint: 106,
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
report_all: true,
report_alternates: true,
report_associated: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[106;5u");
}
}
#[test]
fn kitty_report_associated() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
key: Key::KeyJ,
mods: Mods {
shift: true,
..Mods::default()
},
utf8: "J",
unshifted_codepoint: 106,
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
report_all: true,
report_alternates: true,
report_associated: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[106:74;2;74u");
}
}
#[test]
fn kitty_report_associated_on_release() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
action: key::Action::Release,
key: Key::KeyJ,
mods: Mods {
shift: true,
..Mods::default()
},
utf8: "J",
unshifted_codepoint: 106,
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
report_all: true,
report_alternates: true,
report_associated: true,
report_events: true,
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w[1..], b"[106:74;2:3u");
}
}
#[test]
fn kitty_alternates_omit_control_characters() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
key: Key::Delete,
mods: Mods { ..Mods::default() },
utf8: "\u{7f}",
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
report_alternates: true,
report_all: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[3~");
}
}
#[test]
fn kitty_enter_with_utf8_dead_key_state() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
key: Key::Enter,
utf8: "A",
unshifted_codepoint: 0x0D,
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
report_alternates: true,
report_all: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"A");
}
}
#[test]
fn kitty_keypad_number() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
key: Key::Numpad1,
mods: Mods { ..Mods::default() },
utf8: "1",
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
report_events: true,
report_alternates: true,
report_all: true,
report_associated: true,
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w[1..], b"[57400;;49u");
}
}
#[test]
fn kitty_backspace_with_utf8_dead_key_state() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
key: Key::Backspace,
utf8: "A",
unshifted_codepoint: 0x0D,
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
report_events: true,
report_alternates: true,
report_all: true,
report_associated: true,
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"");
}
}
#[test]
fn legacy_backspace_with_utf8_dead_key_state() {
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::Backspace,
utf8: "A",
unshifted_codepoint: 0x0D,
..KeyEvent::default()
},
&Options {
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"");
}
}
#[test]
fn legacy_enter_with_utf8_dead_key_state() {
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::Enter,
utf8: "A",
unshifted_codepoint: 0x0D,
..KeyEvent::default()
},
&Options {
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"A");
}
}
#[test]
fn legacy_esc_with_utf8_dead_key_state() {
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::Escape,
utf8: "A",
unshifted_codepoint: 0x0D,
..KeyEvent::default()
},
&Options {
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"A");
}
}
#[test]
fn legacy_ctrl_shift_minus_underscore_on_us() {
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::Minus,
mods: Mods {
ctrl: true,
shift: true,
..Mods::default()
},
utf8: "_",
..KeyEvent::default()
},
&Options {
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1f");
}
}
#[test]
fn legacy_ctrl_alt_c() {
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::KeyC,
mods: Mods {
ctrl: true,
alt: true,
..Mods::default()
},
utf8: "c",
..KeyEvent::default()
},
&Options {
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b\x03");
}
}
#[test]
fn legacy_alt_c() {
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::KeyC,
utf8: "c",
mods: Mods {
alt: true,
..Mods::default()
},
..KeyEvent::default()
},
&Options {
alt_esc_prefix: true,
macos_option_as_alt: OptionAsAlt::True,
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1bc");
}
}
#[test]
fn legacy_alt_e_only_unshifted() {
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::KeyE,
unshifted_codepoint: u32::from('e'),
mods: Mods {
alt: true,
..Mods::default()
},
..KeyEvent::default()
},
&Options {
alt_esc_prefix: true,
macos_option_as_alt: OptionAsAlt::True,
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1be");
}
}
#[test]
fn legacy_alt() {
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::KeyF,
utf8: "\u{444}",
mods: Mods {
alt: true,
..Mods::default()
},
..KeyEvent::default()
},
&Options {
alt_esc_prefix: true,
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\xd1\x84");
}
}
#[test]
fn legacy_ctrl_c() {
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::KeyC,
mods: Mods {
ctrl: true,
..Mods::default()
},
utf8: "c",
..KeyEvent::default()
},
&Options {
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x03");
}
}
#[test]
fn legacy_ctrl_space() {
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::Space,
mods: Mods {
ctrl: true,
..Mods::default()
},
utf8: " ",
..KeyEvent::default()
},
&Options {
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x00");
}
}
#[test]
fn legacy_ctrl_shift_backspace() {
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::Backspace,
mods: Mods {
ctrl: true,
shift: true,
..Mods::default()
},
..KeyEvent::default()
},
&Options {
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x08");
}
}
#[test]
fn legacy_ctrl_shift_char_with_modify_other_state_2() {
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::KeyH,
mods: Mods {
ctrl: true,
shift: true,
..Mods::default()
},
utf8: "H",
..KeyEvent::default()
},
&Options {
modify_other_keys_state_2: true,
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[27;6;72~");
}
}
#[test]
fn legacy_ctrl_shift_char_with_modify_other_state_2_and_consumed_mods() {
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::KeyH,
mods: Mods {
ctrl: true,
shift: true,
..Mods::default()
},
consumed_mods: Mods {
shift: true,
..Mods::default()
},
utf8: "H",
..KeyEvent::default()
},
&Options {
modify_other_keys_state_2: true,
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[27;6;72~");
}
}
#[test]
fn legacy_alt_digit_with_modify_other_state_2() {
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::Digit8,
mods: Mods {
alt: true,
..Mods::default()
},
consumed_mods: Mods { ..Mods::default() },
utf8: "8",
..KeyEvent::default()
},
&Options {
modify_other_keys_state_2: true,
macos_option_as_alt: OptionAsAlt::True,
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[27;3;56~");
}
}
#[test]
fn legacy_fixterm_awkward_letters() {
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::KeyI,
mods: Mods {
ctrl: true,
..Mods::default()
},
utf8: "i",
..KeyEvent::default()
},
&Options {
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[105;5u");
}
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::KeyM,
mods: Mods {
ctrl: true,
..Mods::default()
},
utf8: "m",
..KeyEvent::default()
},
&Options {
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[109;5u");
}
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::BracketLeft,
mods: Mods {
ctrl: true,
..Mods::default()
},
utf8: "[",
..KeyEvent::default()
},
&Options {
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[91;5u");
}
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::Digit2,
mods: Mods {
ctrl: true,
shift: true,
..Mods::default()
},
utf8: "@",
unshifted_codepoint: u32::from('2'),
..KeyEvent::default()
},
&Options {
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[64;5u");
}
}
#[test]
fn legacy_ctrl_shift_letter_ascii() {
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::KeyM,
mods: Mods {
ctrl: true,
shift: true,
..Mods::default()
},
utf8: "M",
unshifted_codepoint: u32::from('m'),
..KeyEvent::default()
},
&Options {
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[109;6u");
}
}
#[test]
fn legacy_shift_function_key_should_use_all_mods() {
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::ArrowUp,
mods: Mods {
shift: true,
..Mods::default()
},
consumed_mods: Mods {
shift: true,
..Mods::default()
},
..KeyEvent::default()
},
&Options {
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[1;2A");
}
}
#[test]
fn legacy_keypad_enter() {
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::NumpadEnter,
mods: Mods { ..Mods::default() },
consumed_mods: Mods { ..Mods::default() },
..KeyEvent::default()
},
&Options {
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x0d");
}
}
#[test]
fn legacy_keypad_1() {
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::Numpad1,
mods: Mods { ..Mods::default() },
consumed_mods: Mods { ..Mods::default() },
utf8: "1",
..KeyEvent::default()
},
&Options {
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"1");
}
}
#[test]
fn legacy_keypad_1_with_application_keypad() {
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::Numpad1,
mods: Mods { ..Mods::default() },
consumed_mods: Mods { ..Mods::default() },
utf8: "1",
..KeyEvent::default()
},
&Options {
keypad_key_application: true,
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1bOq");
}
}
#[test]
fn legacy_keypad_1_with_application_keypad_and_numlock() {
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::Numpad1,
mods: Mods {
num_lock: true,
..Mods::default()
},
consumed_mods: Mods { ..Mods::default() },
utf8: "1",
..KeyEvent::default()
},
&Options {
keypad_key_application: true,
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1bOq");
}
}
#[test]
fn legacy_keypad_1_with_application_keypad_and_numlock_ignore() {
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::Numpad1,
mods: Mods {
num_lock: false,
..Mods::default()
},
consumed_mods: Mods { ..Mods::default() },
utf8: "1",
..KeyEvent::default()
},
&Options {
keypad_key_application: true,
ignore_keypad_with_numlock: true,
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"1");
}
}
#[test]
fn legacy_f1() {
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::F1,
mods: Mods {
ctrl: true,
..Mods::default()
},
consumed_mods: Mods { ..Mods::default() },
..KeyEvent::default()
},
&Options {
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[1;5P");
}
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::F2,
mods: Mods {
ctrl: true,
..Mods::default()
},
consumed_mods: Mods { ..Mods::default() },
..KeyEvent::default()
},
&Options {
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[1;5Q");
}
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::F3,
mods: Mods {
ctrl: true,
..Mods::default()
},
consumed_mods: Mods { ..Mods::default() },
..KeyEvent::default()
},
&Options {
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[13;5~");
}
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::F4,
mods: Mods {
ctrl: true,
..Mods::default()
},
consumed_mods: Mods { ..Mods::default() },
..KeyEvent::default()
},
&Options {
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[1;5S");
}
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::F5,
mods: Mods {
ctrl: true,
..Mods::default()
},
consumed_mods: Mods { ..Mods::default() },
..KeyEvent::default()
},
&Options {
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[15;5~");
}
}
#[test]
fn legacy_left_shift_tab() {
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::Tab,
mods: Mods {
shift: true,
sides: Side {
shift: ModSide::Left,
..Side::default()
},
..Mods::default()
},
..KeyEvent::default()
},
&Options {
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[Z");
}
}
#[test]
fn legacy_right_shift_tab() {
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::Tab,
mods: Mods {
shift: true,
sides: Side {
shift: ModSide::Right,
..Side::default()
},
..Mods::default()
},
..KeyEvent::default()
},
&Options {
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[Z");
}
}
#[test]
fn legacy_hu_layout_ctrl_sends_proper_codepoint() {
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::BracketLeft,
mods: Mods {
ctrl: true,
..Mods::default()
},
utf8: "\u{151}",
unshifted_codepoint: 337,
..KeyEvent::default()
},
&Options {
..Options::default()
},
)
.unwrap();
assert_eq!(&w[1..], b"[337;5u");
}
}
#[test]
fn legacy_backspace_with_del_utf8() {
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::Backspace,
utf8: "\u{7f}",
unshifted_codepoint: 0x08,
..KeyEvent::default()
},
&Options {
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x7f");
}
}
fn enc(seq: KittySequence) -> Vec<u8> {
let mut w = Vec::new();
seq.encode(&mut w).unwrap();
w
}
fn seq(key: u32, final_: u8) -> KittySequence<'static> {
KittySequence {
key,
final_,
mods: KittyMods::default(),
event: KittyEvent::None,
alternates: [None, None],
text: "",
}
}
fn ctrl() -> Mods {
Mods {
ctrl: true,
..Mods::default()
}
}
#[test]
fn csi_u_mods_modifier_sequence_values() {
let c = |shift, alt, ctrl| CsiUMods { shift, alt, ctrl }.seq_int();
assert_eq!(1, c(false, false, false));
assert_eq!(2, c(true, false, false));
assert_eq!(3, c(false, true, false));
assert_eq!(5, c(false, false, true));
assert_eq!(4, c(true, true, false));
assert_eq!(6, c(true, false, true));
assert_eq!(7, c(false, true, true));
assert_eq!(8, c(true, true, true));
}
#[test]
fn kitty_mods_modifier_sequence_values() {
let c = |shift, alt, ctrl| {
KittyMods {
shift,
alt,
ctrl,
..KittyMods::default()
}
.seq_int()
};
assert_eq!(1, c(false, false, false));
assert_eq!(2, c(true, false, false));
assert_eq!(3, c(false, true, false));
assert_eq!(5, c(false, false, true));
assert_eq!(4, c(true, true, false));
assert_eq!(6, c(true, false, true));
assert_eq!(7, c(false, true, true));
assert_eq!(8, c(true, true, true));
}
#[test]
fn kitty_sequence_backspace() {
assert_eq!(enc(seq(127, b'u')), b"\x1B[127u");
assert_eq!(
enc(KittySequence {
event: KittyEvent::Release,
..seq(127, b'u')
}),
b"\x1B[127;1:3u"
);
assert_eq!(
enc(KittySequence {
mods: KittyMods {
shift: true,
..KittyMods::default()
},
..seq(127, b'u')
}),
b"\x1B[127;2u"
);
}
#[test]
fn kitty_sequence_text() {
assert_eq!(
enc(KittySequence {
text: "A",
..seq(127, b'u')
}),
b"\x1B[127;;65u"
);
assert_eq!(
enc(KittySequence {
event: KittyEvent::Release,
text: "A",
..seq(127, b'u')
}),
b"\x1B[127;1:3;65u"
);
assert_eq!(
enc(KittySequence {
mods: KittyMods {
shift: true,
..KittyMods::default()
},
text: "A",
..seq(127, b'u')
}),
b"\x1B[127;2;65u"
);
}
#[test]
fn kitty_sequence_text_with_control_characters() {
assert_eq!(
enc(KittySequence {
text: "\n",
..seq(127, b'u')
}),
b"\x1b[127u"
);
assert_eq!(
enc(KittySequence {
text: "A\n",
..seq(127, b'u')
}),
b"\x1b[127;;65u"
);
}
#[test]
fn kitty_sequence_special_no_mods() {
assert_eq!(enc(seq(1, b'A')), b"\x1B[A");
}
#[test]
fn kitty_sequence_special_mods_only() {
assert_eq!(
enc(KittySequence {
mods: KittyMods {
shift: true,
..KittyMods::default()
},
..seq(1, b'A')
}),
b"\x1B[1;2A"
);
}
#[test]
fn kitty_sequence_special_mods_and_event() {
assert_eq!(
enc(KittySequence {
event: KittyEvent::Release,
mods: KittyMods {
shift: true,
..KittyMods::default()
},
..seq(1, b'A')
}),
b"\x1B[1;2:3A"
);
}
fn right_ctrl() -> Mods {
Mods {
ctrl: true,
sides: Side {
ctrl: ModSide::Right,
..Side::default()
},
..Mods::default()
}
}
#[test]
fn ctrlseq_normal_ctrl_c() {
assert_eq!(
ctrl_seq(Key::Unidentified, "c", 'c' as u32, ctrl()),
Some(0x03)
);
}
#[test]
fn ctrlseq_normal_ctrl_c_right_control() {
assert_eq!(
ctrl_seq(Key::Unidentified, "c", 'c' as u32, right_ctrl()),
Some(0x03)
);
}
#[test]
fn ctrlseq_alt_should_be_allowed() {
let mods = Mods {
alt: true,
ctrl: true,
..Mods::default()
};
assert_eq!(
ctrl_seq(Key::Unidentified, "c", 'c' as u32, mods),
Some(0x03)
);
}
#[test]
fn ctrlseq_no_ctrl_does_nothing() {
assert_eq!(
ctrl_seq(Key::Unidentified, "c", 'c' as u32, Mods::default()),
None
);
}
#[test]
fn ctrlseq_shifted_non_character() {
let mods = Mods {
ctrl: true,
shift: true,
..Mods::default()
};
assert_eq!(
ctrl_seq(Key::Unidentified, "_", '-' as u32, mods),
Some(0x1F)
);
}
#[test]
fn ctrlseq_caps_ascii_letter() {
let mods = Mods {
ctrl: true,
caps_lock: true,
..Mods::default()
};
assert_eq!(
ctrl_seq(Key::Unidentified, "C", 'c' as u32, mods),
Some(0x03)
);
}
#[test]
fn ctrlseq_shift_does_not_generate_ctrl_seq() {
let shift = Mods {
shift: true,
..Mods::default()
};
assert_eq!(ctrl_seq(Key::Unidentified, "C", '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);
}
#[test]
fn ctrlseq_russian_ctrl_c() {
assert_eq!(ctrl_seq(Key::KeyC, "с", 0x0441, ctrl()), Some(0x03));
}
#[test]
fn ctrlseq_russian_shifted_ctrl_c() {
let mods = Mods {
ctrl: true,
shift: true,
..Mods::default()
};
assert_eq!(ctrl_seq(Key::KeyC, "с", 0x0441, mods), None);
}
#[test]
fn ctrlseq_russian_alt_ctrl_c() {
let mods = Mods {
ctrl: true,
alt: true,
..Mods::default()
};
assert_eq!(ctrl_seq(Key::KeyC, "с", 0x0441, mods), Some(0x03));
}
#[test]
fn ctrlseq_right_ctrl_c() {
assert_eq!(
ctrl_seq(Key::KeyC, "с", 'c' as u32, right_ctrl()),
Some(0x03)
);
}
}