Files
ghostty-rs/crates/ghostty-input/src/key_encode.rs
T
ddidderr 3afe938b95 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
2026-10-11 21:56:07 +02:00

3162 lines
93 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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)
);
}
}