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
388 lines
11 KiB
Rust
388 lines
11 KiB
Rust
//! 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);
|
|
}
|
|
}
|