use std::collections::HashMap; use galaxyui::keymap::Keystroke; use galaxyui::platform::OperatingSystem; use lazy_static::lazy_static; use super::{ mouse::{MouseAction, MouseButton, MouseState}, TermMode, }; mod kitty_keyboard_protocol; use kitty_keyboard_protocol::maybe_convert_keystroke_to_csi_u; pub use kitty_keyboard_protocol::{maybe_kitty_keyboard_escape_sequence, modifier_key_to_csi_u}; /// C0 set of 7-bit control characters (from ANSI X3.4-1977). #[allow(non_snake_case)] #[allow(dead_code)] pub mod C0 { /// Null filler, terminal should ignore this character. pub const NUL: u8 = 0x00; /// Start of Header. pub const SOH: u8 = 0x01; /// Start of Text, implied end of header. pub const STX: u8 = 0x02; /// End of Text, causes some terminal to respond with ACK or NAK. pub const ETX: u8 = 0x03; /// End of Transmission. pub const EOT: u8 = 0x04; /// Enquiry, causes terminal to send ANSWER-BACK ID. pub const ENQ: u8 = 0x05; /// Acknowledge, usually sent by terminal in response to ETX. pub const ACK: u8 = 0x06; /// Bell, triggers the bell, buzzer, or beeper on the terminal. pub const BEL: u8 = 0x07; /// Backspace, can be used to define overstruck characters. pub const BS: u8 = 0x08; /// Horizontal Tabulation, move to next predetermined position. pub const HT: u8 = 0x09; /// Linefeed, move to same position on next line (see also NL). pub const LF: u8 = 0x0A; /// Vertical Tabulation, move to next predetermined line. pub const VT: u8 = 0x0B; /// Form Feed, move to next form or page. pub const FF: u8 = 0x0C; /// Carriage Return, move to first character of current line. pub const CR: u8 = 0x0D; /// Shift Out, switch to G1 (other half of character set). pub const SO: u8 = 0x0E; /// Shift In, switch to G0 (normal half of character set). pub const SI: u8 = 0x0F; /// Data Link Escape, interpret next control character specially. pub const DLE: u8 = 0x10; /// (DC1) Terminal is allowed to resume transmitting. pub const XON: u8 = 0x11; /// Device Control 2, causes ASR-33 to activate paper-tape reader. pub const DC2: u8 = 0x12; /// (DC3) Terminal must pause and refrain from transmitting. pub const XOFF: u8 = 0x13; /// Device Control 4, causes ASR-33 to deactivate paper-tape reader. pub const DC4: u8 = 0x14; /// Negative Acknowledge, used sometimes with ETX and ACK. pub const NAK: u8 = 0x15; /// Synchronous Idle, used to maintain timing in Sync communication. pub const SYN: u8 = 0x16; /// End of Transmission block. pub const ETB: u8 = 0x17; /// Cancel (makes VT100 abort current escape sequence if any). pub const CAN: u8 = 0x18; /// End of Medium. pub const EM: u8 = 0x19; /// Substitute (VT100 uses this to display parity errors). pub const SUB: u8 = 0x1A; /// Prefix to an escape sequence. pub const ESC: u8 = 0x1B; /// File Separator. pub const FS: u8 = 0x1C; /// Group Separator. pub const GS: u8 = 0x1D; /// Record Separator (sent by VT132 in block-transfer mode). pub const RS: u8 = 0x1E; /// Unit Separator. pub const US: u8 = 0x1F; /// Delete, should be ignored by terminal. pub const DEL: u8 = 0x7f; } /// C1 set of control characters. These are set to their 2-byte equivalent representations (rather /// than the 8-bit single byte representation). /// /// See https://www.xfree86.org/current/ctlseqs.html#C1%20(8-Bit)%20Control%20Characters. #[allow(non_snake_case)] pub mod C1 { use super::C0::ESC; /// Index pub const IND: &[u8] = &[ESC, b'D']; /// Next Line pub const NEL: &[u8] = &[ESC, b'E']; /// Tab Set pub const HTS: &[u8] = &[ESC, b'H']; /// Reverse Index pub const RI: &[u8] = &[ESC, b'M']; /// Single Shift Select of G2 Character Set pub const SS2: &[u8] = &[ESC, b'N']; /// Single Shift Select of G3 Character Set pub const SS3: &[u8] = &[ESC, b'O']; /// Device Control String pub const DCS: &[u8] = &[ESC, b'P']; /// Start of Guarded Area pub const SPA: &[u8] = &[ESC, b'V']; /// End of Guarded Area pub const EPA: &[u8] = &[ESC, b'W']; /// Start of String pub const SOS: &[u8] = &[ESC, b'X']; /// Return Terminal ID pub const DECID: &[u8] = &[ESC, b'Z']; //obsolete form of CSI c /// Control Sequence Introducer pub const CSI: &[u8] = &[ESC, b'[']; /// String Terminator pub const ST: &[u8] = &[ESC, b'\\']; /// Operating System Command pub const OSC: &[u8] = &[ESC, b']']; /// Privacy Message pub const PM: &[u8] = &[ESC, b'^']; /// Application Program Command pub const APC: &[u8] = &[ESC, b'_']; /// Converts the given `c1_sequence`, which is expected to be one of the constants defined in /// this module, into a string. C1 sequences are ASCII-encoded (which is by definition a subset /// of UTF-8), so no need to return an `Option` or check the result of `std::from_utf8()`. pub fn to_utf8(c1_sequence: &[u8]) -> &str { // We are certain that CSI is valid UTF-8. std::str::from_utf8(c1_sequence).expect( "Called with an invalid C1 sequence.This method should only be called with C1 \ sequences defined by constants in the C1 module.", ) } } /// Escape sequences used to control 'bracketed paste' mode. /// /// If the shell supports bracketed paste mode, these control sequences should be inserted at the /// start and end of text written to the pty. See the[xterm spec](http://www.xfree86.org/current/ctlseqs.html#Bracketed%20Paste%20Mode) /// for more details. pub const BRACKETED_PASTE_START: &[u8] = &[C0::ESC, b'[', b'2', b'0', b'0', b'~']; pub const BRACKETED_PASTE_END: &[u8] = &[C0::ESC, b'[', b'2', b'0', b'1', b'~']; #[allow(non_snake_case)] pub mod EscCodes { use super::{ModeProvider, TermMode, C0, C1}; // Arrows-related escape codes pub const ARROW_UP: u8 = b'A'; pub const ARROW_DOWN: u8 = b'B'; pub const ARROW_RIGHT: u8 = b'C'; pub const ARROW_LEFT: u8 = b'D'; pub const WORD_LEFT: &[u8] = &[C0::ESC, b'b']; pub const WORD_RIGHT: &[u8] = &[C0::ESC, b'f']; // Navigation escape codes pub const PAGE_UP: &[u8] = b"5~"; pub const PAGE_DOWN: &[u8] = b"6~"; pub const BACKWARD_TABULATION: &[u8] = b"Z"; // Special keys pub const HOME: u8 = b'H'; pub const END: u8 = b'F'; // Mouse-related escape codes pub const MOUSE_LEFT: u8 = 0; pub const MOUSE_RIGHT: u8 = 2; pub const MOUSE_DRAG: u8 = 32; pub const MOUSE_MOVE: u8 = 35; pub const MOUSE_WHEEL_UP: u8 = 64; pub const MOUSE_WHEEL_DOWN: u8 = 65; pub const FOCUS_IN: &[u8] = &[C0::ESC, b'[', b'I']; pub const FOCUS_OUT: &[u8] = &[C0::ESC, b'[', b'O']; pub fn build_escape_sequence_with_c1(c1: &[u8], c: &[u8]) -> Vec { let mut sequence = Vec::new(); sequence.extend_from_slice(c1); sequence.extend_from_slice(c); sequence } pub fn build_escape_sequence(mode_provider: &impl ModeProvider, c: &[u8]) -> Vec { let c1 = get_c1_sequence(mode_provider); build_escape_sequence_with_c1(c1, c) } /// Returns the C1 code that should be used to start an escape sequence based on terminal's /// term_mode. pub fn get_c1_sequence(mode_provider: &impl ModeProvider) -> &'static [u8] { // Usually we use CSI for most escape sequences. // However, for programs that set CursorKeys mode we should use SS3 instead. // This difference is critical when we want the arrow keys to work in the interactive // programs as well as alt_screen during the long running commands. // Check https://en.wikipedia.org/wiki/ANSI_escape_code#Fe_Escape_sequences for more // information about CSI/SS3 and others. if mode_provider.is_term_mode_set(TermMode::APP_CURSOR) { return C1::SS3; } C1::CSI } } /// Trait for objects that can provide information about the terminal's mode. pub trait ModeProvider { fn is_term_mode_set(&self, mode: TermMode) -> bool; } /// To be implemented on event objects (e.g. Keystroke or MouseState) that may be converted to /// escape sequences to be sent to the pty. pub trait ToEscapeSequence { /// Returns the appropriate escape code to be passed to the pty corresponding to this event, if /// any. fn to_escape_sequence(&self, mode_provider: &T) -> Option>; } /// Pairs a keystroke with platform-provided key details for accurate escape sequence encoding. pub struct KeystrokeWithDetails<'a> { pub keystroke: &'a Keystroke, pub key_without_modifiers: Option<&'a str>, /// The text that this key event would insert, as provided by the OS input system. /// Used for the REPORT_ASSOCIATED_TEXT enhancement (Kitty flag 16). pub chars: Option<&'a str>, } impl ToEscapeSequence for KeystrokeWithDetails<'_> { fn to_escape_sequence(&self, mode_provider: &T) -> Option> { if let Some(csi_u) = maybe_convert_keystroke_to_csi_u( self.keystroke, self.key_without_modifiers, self.chars, mode_provider, ) { return Some(csi_u); } // Legacy encoding fallback. // NOTE: Order matters! We assume all fn keystrokes have been handled by the // time we reach meta_keystroke_to_escape_sequence. let keystroke = self.keystroke; fn_keystroke_to_escape_sequence(keystroke, mode_provider) .or_else(|| keystroke_to_c0_control_code(keystroke, mode_provider)) .or_else(|| cursor_movement_keystroke_to_escape_sequence(keystroke, mode_provider)) .or_else(|| meta_keystroke_to_escape_sequence(keystroke, mode_provider)) } } impl ToEscapeSequence for MouseState { fn to_escape_sequence(&self, _mode_provider: &T) -> Option> { let action = match self.action() { MouseAction::Released => 'm', _ => 'M', }; let (button, repeats) = match self.button() { MouseButton::Left => (EscCodes::MOUSE_LEFT, 1), MouseButton::Right => (EscCodes::MOUSE_RIGHT, 1), MouseButton::LeftDrag => (EscCodes::MOUSE_DRAG, 1), MouseButton::Move => (EscCodes::MOUSE_MOVE, 1), MouseButton::Wheel => { if let MouseAction::Scrolled { delta } = self.action() { let lines = delta.unsigned_abs() as usize; if *delta > 0 { (EscCodes::MOUSE_WHEEL_UP, lines) } else { (EscCodes::MOUSE_WHEEL_DOWN, lines) } } else { panic!("Currently only scroll is supported for the Wheel button") } } }; let point = self.maybe_point()?; let msg = format!( "{}<{};{};{}{}", C1::to_utf8(C1::CSI), button, point.col + 1, point.row + 1, action ) .repeat(repeats); Some(msg.into_bytes()) } } pub trait ToModifierEscapeByte { /// Returns the modifier escape byte represented by this T. /// /// The returned modifier byte is typically meant to be inserted into escape sequence /// corresponding to the keystroke. See the implementation of this trait for /// `Keystroke` for more details. fn to_modifier_escape_byte(&self) -> Option; } impl ToModifierEscapeByte for Keystroke { // Mirrors the [xterm implementation](https://www.xfree86.org/current/ctlseqs.html#PC-Style%20Function%20Keys). fn to_modifier_escape_byte(&self) -> Option { match self { Keystroke { shift: true, alt: false, ctrl: false, meta: _, cmd: _, key: _, } => Some(b'2'), Keystroke { shift: false, alt: true, ctrl: false, meta: _, cmd: _, key: _, } => Some(b'3'), Keystroke { shift: true, alt: true, ctrl: false, meta: _, cmd: _, key: _, } => Some(b'4'), Keystroke { shift: false, alt: false, ctrl: true, meta: _, cmd: _, key: _, } => Some(b'5'), Keystroke { shift: true, alt: false, ctrl: true, meta: _, cmd: _, key: _, } => Some(b'6'), Keystroke { shift: false, alt: true, ctrl: true, meta: _, cmd: _, key: _, } => Some(b'7'), Keystroke { shift: true, alt: true, ctrl: true, meta: _, cmd: _, key: _, } => Some(b'8'), // meta can be basically treated the same way as alt... Keystroke { meta: true, ctrl: _, alt: _, shift: _, cmd: _, key: _, } => Some(b'3'), _ => None, } } } /// Returns the appropriate escape sequence for the given fn key, which may or may not be modified /// via modifier key(s). /// /// If the given keystroke is not an fn key, returns None. fn fn_keystroke_to_escape_sequence( keystroke: &Keystroke, _mode_provider: &impl ModeProvider, ) -> Option> { match keystroke.key.as_str() { "f1" | "f2" | "f3" | "f4" | "f5" | "f6" | "f7" | "f8" | "f9" | "f10" | "f11" | "f12" | "f13" | "f14" | "f15" | "f16" | "f17" | "f18" | "f19" | "f20" => { let modifier_byte = keystroke.to_modifier_escape_byte(); match modifier_byte { Some(modifier_byte) => fn_keystroke_with_modifier_to_escape_sequence( keystroke.key.as_str(), modifier_byte, ), None => fn_keystroke_without_modifier_to_escape_sequence(keystroke.key.as_str()), } } _ => None, } } /// Returns the escape sequence for the given fn key with no additional modifier key. If `key` is /// not a fn key, returns None. /// /// Mapping from key to sequence is adapted from the xterm spec /// [here](https://www.xfree86.org/current/ctlseqs.html). fn fn_keystroke_without_modifier_to_escape_sequence(key: &str) -> Option> { match key { "f1" => Some([C1::SS3, b"P"].concat()), "f2" => Some([C1::SS3, b"Q"].concat()), "f3" => Some([C1::SS3, b"R"].concat()), "f4" => Some([C1::SS3, b"S"].concat()), "f5" => Some([C1::CSI, b"15~"].concat()), "f6" => Some([C1::CSI, b"17~"].concat()), "f7" => Some([C1::CSI, b"18~"].concat()), "f8" => Some([C1::CSI, b"19~"].concat()), "f9" => Some([C1::CSI, b"20~"].concat()), "f10" => Some([C1::CSI, b"21~"].concat()), "f11" => Some([C1::CSI, b"23~"].concat()), "f12" => Some([C1::CSI, b"24~"].concat()), "f13" => Some([C1::CSI, b"25~"].concat()), "f14" => Some([C1::CSI, b"26~"].concat()), "f15" => Some([C1::CSI, b"28~"].concat()), "f16" => Some([C1::CSI, b"29~"].concat()), "f17" => Some([C1::CSI, b"31~"].concat()), "f18" => Some([C1::CSI, b"32~"].concat()), "f19" => Some([C1::CSI, b"33~"].concat()), "f20" => Some([C1::CSI, b"34~"].concat()), _ => None, } } /// Returns the escape sequence for the given fn key with the given modifier_byte, which is mapped /// from the modifiers in the original keystroke. If `key` is not a function key, returns None. /// /// Mapping from key to sequence is adapted from the xterm spec /// [here](https://www.xfree86.org/current/ctlseqs.html). fn fn_keystroke_with_modifier_to_escape_sequence(key: &str, modifier_byte: u8) -> Option> { match key { "f1" => Some([C1::CSI, format!("1;{}P", modifier_byte as char).as_bytes()].concat()), "f2" => Some([C1::CSI, format!("1;{}Q", modifier_byte as char).as_bytes()].concat()), "f3" => Some([C1::CSI, format!("1;{}R", modifier_byte as char).as_bytes()].concat()), "f4" => Some([C1::CSI, format!("1;{}S", modifier_byte as char).as_bytes()].concat()), "f5" => Some([C1::CSI, format!("15;{}~", modifier_byte as char).as_bytes()].concat()), "f6" => Some([C1::CSI, format!("17;{}~", modifier_byte as char).as_bytes()].concat()), "f7" => Some([C1::CSI, format!("18;{}~", modifier_byte as char).as_bytes()].concat()), "f8" => Some([C1::CSI, format!("19;{}~", modifier_byte as char).as_bytes()].concat()), "f9" => Some([C1::CSI, format!("20;{}~", modifier_byte as char).as_bytes()].concat()), "f10" => Some([C1::CSI, format!("21;{}~", modifier_byte as char).as_bytes()].concat()), "f11" => Some([C1::CSI, format!("23;{}~", modifier_byte as char).as_bytes()].concat()), "f12" => Some([C1::CSI, format!("24;{}~", modifier_byte as char).as_bytes()].concat()), "f13" => Some([C1::CSI, format!("25;{}~", modifier_byte as char).as_bytes()].concat()), "f14" => Some([C1::CSI, format!("26;{}~", modifier_byte as char).as_bytes()].concat()), "f15" => Some([C1::CSI, format!("28;{}~", modifier_byte as char).as_bytes()].concat()), "f16" => Some([C1::CSI, format!("29;{}~", modifier_byte as char).as_bytes()].concat()), "f17" => Some([C1::CSI, format!("31;{}~", modifier_byte as char).as_bytes()].concat()), "f18" => Some([C1::CSI, format!("32;{}~", modifier_byte as char).as_bytes()].concat()), "f19" => Some([C1::CSI, format!("33;{}~", modifier_byte as char).as_bytes()].concat()), "f20" => Some([C1::CSI, format!("34;{}~", modifier_byte as char).as_bytes()].concat()), _ => None, } } /// Returns the C0 control code for the given keystroke. /// /// These control codes are emitted on ctrl-modified keystrokes. Note that the spec explicitly /// specifies ctrl-only modified keystrokes. The excat control code mapping is taken from the /// VT-220 spec [here](https://vt100.net/docs/vt220-rm/chapter3.html#S3.2.5). /// /// Note that C0 control codes are (definitionally) a single byte, so the returned vector, if any, /// is always length 1. fn keystroke_to_c0_control_code( keystroke: &Keystroke, _mode_provider: &impl ModeProvider, ) -> Option> { lazy_static! { static ref KEYSTROKE_TO_C0_CODE: HashMap<&'static str, u8> = HashMap::from([ (" ", C0::NUL), ("2", C0::NUL), ("3", C0::ESC), ("4", C0::FS), ("5", C0::GS), ("6", C0::RS), ("7", C0::US), ("8", C0::DEL), ]); } // Only emit C0 codes on ctrl-modified keystrokes, without other modifiers, per the VT-220 // spec. if !(keystroke.ctrl && !keystroke.alt && !keystroke.shift && !keystroke.meta) { // Return None if the keystroke is not ctrl-key. return None; } if KEYSTROKE_TO_C0_CODE.contains_key(keystroke.key.as_str()) { return Some(vec![KEYSTROKE_TO_C0_CODE[keystroke.key.as_str()]]); } None } /// Returns the appropriate escape sequence for the given "cursor movement" keystroke. /// /// "cursor movement" keystroke is defined as one of the arrow keys, "home" or "end". If the given /// keystroke is not a "cursor movement" keystroke, returns None. /// /// Mapping from button to sequence is adapted from the xterm spec /// [here](https://www.xfree86.org/current/ctlseqs.html). fn cursor_movement_keystroke_to_escape_sequence( keystroke: &Keystroke, mode_provider: &impl ModeProvider, ) -> Option> { lazy_static! { static ref CURSOR_KEYSTROKE_TO_CONTROL_CODE: HashMap<&'static str, u8> = HashMap::from([ ("up", b'A'), ("down", b'B'), ("right", b'C'), ("left", b'D'), ("home", b'H'), ("end", b'F') ]); } let key = keystroke.key.as_str(); if !CURSOR_KEYSTROKE_TO_CONTROL_CODE.contains_key(key) { return None; } let modifier_bytes = keystroke.to_modifier_escape_byte(); match modifier_bytes { Some(modifier_bytes) => Some( [ C1::CSI, b"1;", &[modifier_bytes, CURSOR_KEYSTROKE_TO_CONTROL_CODE[key]], ] .concat(), ), None => Some( [ EscCodes::get_c1_sequence(mode_provider), &[CURSOR_KEYSTROKE_TO_CONTROL_CODE[key]], ] .concat(), ), } } /// Returns the byte array corresponding to a special key, if a special key is provided. /// Otherwise, returns None. /// We prefer using match over a HashMap due to LLVM being able to optimize this /// further than a HashMap. fn map_special_key_to_bytes(key: &str) -> Option<&[u8]> { match key { "backspace" => Some("\x7f".as_bytes()), "insert" => Some("\x1b[2~".as_bytes()), "delete" => Some("\x1b[3~".as_bytes()), "pageup" => Some("\x1b[5~".as_bytes()), "pagedown" => Some("\x1b[6~".as_bytes()), _ => None, } } /// Returns the appropriate escape sequence for the given meta-modified keystroke. /// /// If the given keystroke is not meta-modified, returns None. fn meta_keystroke_to_escape_sequence( keystroke: &Keystroke, _mode_provider: &impl ModeProvider, ) -> Option> { // On mac, we have a setting that allows users to map the Option keys to // meta. if OperatingSystem::get().is_mac() { if !keystroke.meta { return None; } } else { // On other platforms, interpret the alt key as the meta modifier. if !keystroke.alt { return None; } } let key = &keystroke.key; // We check if the key pressed was a special key i.e. not a normal character first. // If it is, we look up the correct byte sequence for that special key and combine that with Meta. // Note that we purposely do not check for fn keys here since we expect fn_keystroke_to_escape_sequence // already captured fn + Meta combos! if let Some(bytes) = map_special_key_to_bytes(key) { Some([&[C0::ESC], bytes].concat()) } else { Some([&[C0::ESC], key.as_bytes()].concat()) } } #[cfg(test)] #[path = "escape_sequences_test.rs"] mod tests;