feat(vt): port Ghostty's VT parsing layer
Ports the parsing half of ZIG/src/terminal into the ghostty-vt crate: - Parser state machine with a compile-time generated parse table and the UTF-8 decoder - OSC parser incl. all sub-parsers (titles, colors, clipboard, hyperlinks, semantic prompts/OSC 133, OSC 9 / ConEmu, iTerm2, kitty clipboard, color and text-sizing protocols, context signals) - CSI, SGR, modes table with save/restore, charsets, ansi, device status, mouse shapes, APC, DCS (incl. XTGETTCAP), tmux control-mode parser - color palette / X11 color names, kitty color and graphics command parser Deviations from Zig (documented in docs/rewrite/notes/ghostty-vt-W*.md): parser actions own their payloads instead of borrowing parser buffers; OSC strings are Vec<u8>; non-exhaustive Zig enums gain an Other(raw) variant; tmux regexes replaced by equivalent hand-written matchers. Known follow-up: semantic_prompt.rs carries local copies of the printf-q and percent decoders that belong in ghostty-os. Test Plan: - cargo test -p ghostty-vt: 395 passed, 1 ignored (skipped in Zig too) - cargo clippy -p ghostty-vt --all-targets -- -D warnings: clean
This commit is contained in:
1 parent
864ef82acb
commit
b2b248e003
44 files changed
+15040
-6
No files matched your search
@@ -0,0 +1,387 @@
|
||||
//! Terminal modes (ZIG/src/terminal/modes.zig): an enum of supported modes,
|
||||
//! a packed bit storage for mode values, and a state struct that handles
|
||||
//! save/restore.
|
||||
//!
|
||||
//! In Zig the enum, packed struct and lookup are generated at comptime from
|
||||
//! one table. Here the single [`ENTRIES`] table drives everything and the
|
||||
//! `Mode` discriminant doubles as the bit index in [`ModePacked`].
|
||||
|
||||
/// A struct that maintains the state of all the settable modes.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub struct ModeState {
|
||||
/// The values of the current modes.
|
||||
pub values: ModePacked,
|
||||
/// The saved values. We only allow saving each mode once.
|
||||
/// This is in line with other terminals that implement XTSAVE
|
||||
/// and XTRESTORE. We can improve this in the future if it becomes
|
||||
/// a real-world issue but we need to be aware of a DoS vector.
|
||||
pub saved: ModePacked,
|
||||
/// The default values for the modes. This is used to reset
|
||||
/// the modes to their default values during reset.
|
||||
pub default: ModePacked,
|
||||
}
|
||||
|
||||
impl ModeState {
|
||||
/// Reset the modes to their default values. This also clears the
|
||||
/// saved state.
|
||||
pub fn reset(&mut self) {
|
||||
self.values = self.default;
|
||||
self.saved = ModePacked::default();
|
||||
}
|
||||
|
||||
/// Set a mode to a value.
|
||||
pub fn set(&mut self, mode: Mode, value: bool) {
|
||||
self.values.set(mode, value);
|
||||
}
|
||||
|
||||
/// Get the value of a mode.
|
||||
pub fn get(&self, mode: Mode) -> bool {
|
||||
self.values.get(mode)
|
||||
}
|
||||
|
||||
/// Save the state of the given mode. This can then be restored
|
||||
/// with restore. This will only be accurate if the previous
|
||||
/// mode was saved exactly once and not restored. Otherwise this
|
||||
/// will just keep restoring the last stored value in memory.
|
||||
pub fn save(&mut self, mode: Mode) {
|
||||
let v = self.values.get(mode);
|
||||
self.saved.set(mode, v);
|
||||
}
|
||||
|
||||
/// See save. This will return the restored value.
|
||||
pub fn restore(&mut self, mode: Mode) -> bool {
|
||||
let v = self.saved.get(mode);
|
||||
self.values.set(mode, v);
|
||||
v
|
||||
}
|
||||
}
|
||||
|
||||
/// Packed storage of all the settable modes (one bit per mode). This
|
||||
/// shouldn't be used directly but rather through the [`ModeState`] struct.
|
||||
/// Defaults match each entry's default value.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct ModePacked(u64);
|
||||
|
||||
impl Default for ModePacked {
|
||||
fn default() -> Self {
|
||||
let mut bits = 0u64;
|
||||
for e in ENTRIES {
|
||||
if e.default {
|
||||
bits |= 1 << (e.mode as u64);
|
||||
}
|
||||
}
|
||||
Self(bits)
|
||||
}
|
||||
}
|
||||
|
||||
impl ModePacked {
|
||||
pub fn get(&self, mode: Mode) -> bool {
|
||||
self.0 & (1 << (mode as u64)) != 0
|
||||
}
|
||||
|
||||
pub fn set(&mut self, mode: Mode, value: bool) {
|
||||
if value {
|
||||
self.0 |= 1 << (mode as u64);
|
||||
} else {
|
||||
self.0 &= !(1 << (mode as u64));
|
||||
}
|
||||
}
|
||||
|
||||
/// Raw bits.
|
||||
pub fn bits(&self) -> u64 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// The available modes. See [`ENTRIES`] for numeric values.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[repr(u8)]
|
||||
pub enum Mode {
|
||||
DisableKeyboard,
|
||||
Insert,
|
||||
SendReceiveMode,
|
||||
Linefeed,
|
||||
CursorKeys,
|
||||
M132Column,
|
||||
SlowScroll,
|
||||
ReverseColors,
|
||||
Origin,
|
||||
Wraparound,
|
||||
Autorepeat,
|
||||
MouseEventX10,
|
||||
CursorBlinking,
|
||||
CursorVisible,
|
||||
EnableMode3,
|
||||
ReverseWrap,
|
||||
AltScreenLegacy,
|
||||
KeypadKeys,
|
||||
EnableLeftAndRightMargin,
|
||||
MouseEventNormal,
|
||||
MouseEventButton,
|
||||
MouseEventAny,
|
||||
FocusEvent,
|
||||
MouseFormatUtf8,
|
||||
MouseFormatSgr,
|
||||
MouseAlternateScroll,
|
||||
MouseFormatUrxvt,
|
||||
MouseFormatSgrPixels,
|
||||
IgnoreKeypadWithNumlock,
|
||||
AltEscPrefix,
|
||||
AltSendsEscape,
|
||||
ReverseWrapExtended,
|
||||
AltScreen,
|
||||
SaveCursor,
|
||||
AltScreenSaveCursorClearEnter,
|
||||
BracketedPaste,
|
||||
SynchronizedOutput,
|
||||
GraphemeCluster,
|
||||
ReportColorScheme,
|
||||
InBandSizeReports,
|
||||
}
|
||||
|
||||
impl Mode {
|
||||
/// The entry for this mode.
|
||||
pub fn entry(self) -> &'static Entry {
|
||||
&ENTRIES[self as usize]
|
||||
}
|
||||
|
||||
/// The numeric value of the mode.
|
||||
pub fn value(self) -> u16 {
|
||||
self.entry().value
|
||||
}
|
||||
|
||||
/// True if this is an ANSI mode, false if its a DEC mode (?-prefixed).
|
||||
pub fn ansi(self) -> bool {
|
||||
self.entry().ansi
|
||||
}
|
||||
|
||||
/// The snake_case name of the mode (as in the Zig tag name).
|
||||
pub fn name(self) -> &'static str {
|
||||
self.entry().name
|
||||
}
|
||||
}
|
||||
|
||||
/// Look up a mode from its numeric value and ANSI-ness.
|
||||
pub fn mode_from_int(v: u16, ansi: bool) -> Option<Mode> {
|
||||
ENTRIES
|
||||
.iter()
|
||||
.find(|e| !e.disabled && e.value == v && e.ansi == ansi)
|
||||
.map(|e| e.mode)
|
||||
}
|
||||
|
||||
/// A single entry of a possible mode we support.
|
||||
#[derive(Debug)]
|
||||
pub struct Entry {
|
||||
pub mode: Mode,
|
||||
pub name: &'static str,
|
||||
pub value: u16,
|
||||
pub default: bool,
|
||||
/// True if this is an ANSI mode, false if its a DEC mode (?-prefixed).
|
||||
pub ansi: bool,
|
||||
/// If true, this mode is disabled and Ghostty will not allow it to be
|
||||
/// set or queried. The mode enum still has it, allowing Ghostty developers
|
||||
/// to develop a mode without exposing it to real users.
|
||||
pub disabled: bool,
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
const fn Entry(mode: Mode, name: &'static str, value: u16, default: bool, ansi: bool) -> Entry {
|
||||
Entry {
|
||||
mode,
|
||||
name,
|
||||
value,
|
||||
default,
|
||||
ansi,
|
||||
disabled: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// The full list of available entries, in `Mode` declaration order.
|
||||
pub const ENTRIES: &[Entry; 40] = &[
|
||||
Entry(Mode::DisableKeyboard, "disable_keyboard", 2, false, true),
|
||||
Entry(Mode::Insert, "insert", 4, false, true),
|
||||
Entry(Mode::SendReceiveMode, "send_receive_mode", 12, true, true),
|
||||
Entry(Mode::Linefeed, "linefeed", 20, false, true),
|
||||
Entry(Mode::CursorKeys, "cursor_keys", 1, false, false),
|
||||
Entry(Mode::M132Column, "132_column", 3, false, false),
|
||||
Entry(Mode::SlowScroll, "slow_scroll", 4, false, false),
|
||||
Entry(Mode::ReverseColors, "reverse_colors", 5, false, false),
|
||||
Entry(Mode::Origin, "origin", 6, false, false),
|
||||
Entry(Mode::Wraparound, "wraparound", 7, true, false),
|
||||
Entry(Mode::Autorepeat, "autorepeat", 8, false, false),
|
||||
Entry(Mode::MouseEventX10, "mouse_event_x10", 9, false, false),
|
||||
Entry(Mode::CursorBlinking, "cursor_blinking", 12, false, false),
|
||||
Entry(Mode::CursorVisible, "cursor_visible", 25, true, false),
|
||||
Entry(Mode::EnableMode3, "enable_mode_3", 40, false, false),
|
||||
Entry(Mode::ReverseWrap, "reverse_wrap", 45, false, false),
|
||||
Entry(Mode::AltScreenLegacy, "alt_screen_legacy", 47, false, false),
|
||||
Entry(Mode::KeypadKeys, "keypad_keys", 66, false, false),
|
||||
Entry(
|
||||
Mode::EnableLeftAndRightMargin,
|
||||
"enable_left_and_right_margin",
|
||||
69,
|
||||
false,
|
||||
false,
|
||||
),
|
||||
Entry(
|
||||
Mode::MouseEventNormal,
|
||||
"mouse_event_normal",
|
||||
1000,
|
||||
false,
|
||||
false,
|
||||
),
|
||||
Entry(
|
||||
Mode::MouseEventButton,
|
||||
"mouse_event_button",
|
||||
1002,
|
||||
false,
|
||||
false,
|
||||
),
|
||||
Entry(Mode::MouseEventAny, "mouse_event_any", 1003, false, false),
|
||||
Entry(Mode::FocusEvent, "focus_event", 1004, false, false),
|
||||
Entry(
|
||||
Mode::MouseFormatUtf8,
|
||||
"mouse_format_utf8",
|
||||
1005,
|
||||
false,
|
||||
false,
|
||||
),
|
||||
Entry(Mode::MouseFormatSgr, "mouse_format_sgr", 1006, false, false),
|
||||
Entry(
|
||||
Mode::MouseAlternateScroll,
|
||||
"mouse_alternate_scroll",
|
||||
1007,
|
||||
true,
|
||||
false,
|
||||
),
|
||||
Entry(
|
||||
Mode::MouseFormatUrxvt,
|
||||
"mouse_format_urxvt",
|
||||
1015,
|
||||
false,
|
||||
false,
|
||||
),
|
||||
Entry(
|
||||
Mode::MouseFormatSgrPixels,
|
||||
"mouse_format_sgr_pixels",
|
||||
1016,
|
||||
false,
|
||||
false,
|
||||
),
|
||||
Entry(
|
||||
Mode::IgnoreKeypadWithNumlock,
|
||||
"ignore_keypad_with_numlock",
|
||||
1035,
|
||||
true,
|
||||
false,
|
||||
),
|
||||
Entry(Mode::AltEscPrefix, "alt_esc_prefix", 1036, true, false),
|
||||
Entry(Mode::AltSendsEscape, "alt_sends_escape", 1039, false, false),
|
||||
Entry(
|
||||
Mode::ReverseWrapExtended,
|
||||
"reverse_wrap_extended",
|
||||
1045,
|
||||
false,
|
||||
false,
|
||||
),
|
||||
Entry(Mode::AltScreen, "alt_screen", 1047, false, false),
|
||||
Entry(Mode::SaveCursor, "save_cursor", 1048, false, false),
|
||||
Entry(
|
||||
Mode::AltScreenSaveCursorClearEnter,
|
||||
"alt_screen_save_cursor_clear_enter",
|
||||
1049,
|
||||
false,
|
||||
false,
|
||||
),
|
||||
Entry(Mode::BracketedPaste, "bracketed_paste", 2004, false, false),
|
||||
Entry(
|
||||
Mode::SynchronizedOutput,
|
||||
"synchronized_output",
|
||||
2026,
|
||||
false,
|
||||
false,
|
||||
),
|
||||
Entry(
|
||||
Mode::GraphemeCluster,
|
||||
"grapheme_cluster",
|
||||
2027,
|
||||
false,
|
||||
false,
|
||||
),
|
||||
Entry(
|
||||
Mode::ReportColorScheme,
|
||||
"report_color_scheme",
|
||||
2031,
|
||||
false,
|
||||
false,
|
||||
),
|
||||
Entry(
|
||||
Mode::InBandSizeReports,
|
||||
"in_band_size_reports",
|
||||
2048,
|
||||
false,
|
||||
false,
|
||||
),
|
||||
];
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn entries_in_order() {
|
||||
for (i, e) in ENTRIES.iter().enumerate() {
|
||||
assert_eq!(e.mode as usize, i);
|
||||
}
|
||||
assert!(ENTRIES.len() <= 64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mode_from_int_lookup() {
|
||||
assert_eq!(mode_from_int(4, true), Some(Mode::Insert));
|
||||
assert_eq!(mode_from_int(9, true), None);
|
||||
assert_eq!(mode_from_int(9, false), Some(Mode::MouseEventX10));
|
||||
assert_eq!(mode_from_int(14, true), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mode_state() {
|
||||
let mut state = ModeState::default();
|
||||
// Normal set/get
|
||||
assert!(!state.get(Mode::CursorKeys));
|
||||
state.set(Mode::CursorKeys, true);
|
||||
assert!(state.get(Mode::CursorKeys));
|
||||
|
||||
// Save/restore
|
||||
state.save(Mode::CursorKeys);
|
||||
state.set(Mode::CursorKeys, false);
|
||||
assert!(!state.get(Mode::CursorKeys));
|
||||
assert!(state.restore(Mode::CursorKeys));
|
||||
assert!(state.get(Mode::CursorKeys));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn defaults_and_reset() {
|
||||
let mut state = ModeState::default();
|
||||
assert!(state.get(Mode::Wraparound));
|
||||
assert!(state.get(Mode::CursorVisible));
|
||||
assert!(state.get(Mode::SendReceiveMode));
|
||||
assert!(!state.get(Mode::Origin));
|
||||
state.set(Mode::Wraparound, false);
|
||||
state.reset();
|
||||
assert!(state.get(Mode::Wraparound));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mode_tag_order() {
|
||||
// Mirrors Zig ModeTag "order": value in low bits, ansi in the top bit.
|
||||
assert_eq!(Mode::CursorKeys.value(), 1);
|
||||
assert!(!Mode::CursorKeys.ansi());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn packed_size() {
|
||||
// Zig asserts @sizeOf(ModePacked) == 8.
|
||||
assert_eq!(std::mem::size_of::<ModePacked>(), 8);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user