Files
galaxy/crates/galaxy_terminal/src/model/escape_sequences.rs
T

598 lines
22 KiB
Rust

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<u8> {
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<u8> {
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<T> {
/// 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<Vec<u8>>;
}
/// 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<T: ModeProvider> ToEscapeSequence<T> for KeystrokeWithDetails<'_> {
fn to_escape_sequence(&self, mode_provider: &T) -> Option<Vec<u8>> {
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<T: ModeProvider> ToEscapeSequence<T> for MouseState {
fn to_escape_sequence(&self, _mode_provider: &T) -> Option<Vec<u8>> {
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<u8>;
}
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<u8> {
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<Vec<u8>> {
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<Vec<u8>> {
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<Vec<u8>> {
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<Vec<u8>> {
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<Vec<u8>> {
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<Vec<u8>> {
// 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;