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
987f2957ad
commit
bf84326c2f
44 files changed
+15040
-6
No files matched your search
@@ -0,0 +1,527 @@
|
||||
//! The primary export of this file is `TABLE`, which contains a
|
||||
//! compile-time-generated state transition table for VT emulation
|
||||
//! (ZIG/src/terminal/parse_table.zig).
|
||||
//!
|
||||
//! This is based on the vt100.net state machine:
|
||||
//! https://vt100.net/emu/dec_ansi_parser
|
||||
//! But has some modifications:
|
||||
//!
|
||||
//! * csi_param accepts the colon character (':') since the SGR command
|
||||
//! accepts colon as a valid parameter value.
|
||||
|
||||
use crate::parser::{State, TransitionAction as A};
|
||||
|
||||
const NUM_STATES: usize = 14;
|
||||
|
||||
const STATES: [State; NUM_STATES] = [
|
||||
State::Ground,
|
||||
State::Escape,
|
||||
State::EscapeIntermediate,
|
||||
State::CsiEntry,
|
||||
State::CsiIntermediate,
|
||||
State::CsiParam,
|
||||
State::CsiIgnore,
|
||||
State::DcsEntry,
|
||||
State::DcsParam,
|
||||
State::DcsIntermediate,
|
||||
State::DcsPassthrough,
|
||||
State::DcsIgnore,
|
||||
State::OscString,
|
||||
State::SosPmApcString,
|
||||
];
|
||||
|
||||
/// Transition is the transition to take within the table
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Transition {
|
||||
pub state: State,
|
||||
pub action: A,
|
||||
}
|
||||
|
||||
/// The type of the state transition table: `[byte][state]`.
|
||||
pub type Table = [[Transition; NUM_STATES]; 256];
|
||||
|
||||
type OptionalTable = [[Option<Transition>; NUM_STATES]; 256];
|
||||
|
||||
/// The state transition table, indexed `TABLE[byte][state as usize]`.
|
||||
pub const TABLE: Table = gen_table();
|
||||
|
||||
/// Function to generate the full state transition table for VT emulation.
|
||||
const fn gen_table() -> Table {
|
||||
// We accumulate using an "optional" table so we can detect unset entries.
|
||||
let mut result: OptionalTable = [[None; NUM_STATES]; 256];
|
||||
|
||||
// anywhere transitions
|
||||
let mut s = 0;
|
||||
while s < NUM_STATES {
|
||||
let source = STATES[s];
|
||||
|
||||
// anywhere => ground
|
||||
single(&mut result, 0x18, source, State::Ground, A::Execute);
|
||||
single(&mut result, 0x1A, source, State::Ground, A::Execute);
|
||||
range(&mut result, 0x80, 0x8F, source, State::Ground, A::Execute);
|
||||
range(&mut result, 0x91, 0x97, source, State::Ground, A::Execute);
|
||||
single(&mut result, 0x99, source, State::Ground, A::Execute);
|
||||
single(&mut result, 0x9A, source, State::Ground, A::Execute);
|
||||
single(&mut result, 0x9C, source, State::Ground, A::None);
|
||||
|
||||
// anywhere => escape
|
||||
single(&mut result, 0x1B, source, State::Escape, A::None);
|
||||
|
||||
// anywhere => sos_pm_apc_string
|
||||
single(&mut result, 0x98, source, State::SosPmApcString, A::None);
|
||||
single(&mut result, 0x9E, source, State::SosPmApcString, A::None);
|
||||
single(&mut result, 0x9F, source, State::SosPmApcString, A::None);
|
||||
|
||||
// anywhere => csi_entry
|
||||
single(&mut result, 0x9B, source, State::CsiEntry, A::None);
|
||||
|
||||
// anywhere => dcs_entry
|
||||
single(&mut result, 0x90, source, State::DcsEntry, A::None);
|
||||
|
||||
// anywhere => osc_string
|
||||
single(&mut result, 0x9D, source, State::OscString, A::None);
|
||||
s += 1;
|
||||
}
|
||||
// ground
|
||||
{
|
||||
// events
|
||||
single(&mut result, 0x19, State::Ground, State::Ground, A::Execute);
|
||||
range(
|
||||
&mut result,
|
||||
0,
|
||||
0x17,
|
||||
State::Ground,
|
||||
State::Ground,
|
||||
A::Execute,
|
||||
);
|
||||
range(
|
||||
&mut result,
|
||||
0x1C,
|
||||
0x1F,
|
||||
State::Ground,
|
||||
State::Ground,
|
||||
A::Execute,
|
||||
);
|
||||
range(
|
||||
&mut result,
|
||||
0x20,
|
||||
0x7F,
|
||||
State::Ground,
|
||||
State::Ground,
|
||||
A::Print,
|
||||
);
|
||||
}
|
||||
|
||||
// escape_intermediate
|
||||
{
|
||||
let source = State::EscapeIntermediate;
|
||||
|
||||
single(&mut result, 0x19, source, source, A::Execute);
|
||||
range(&mut result, 0, 0x17, source, source, A::Execute);
|
||||
range(&mut result, 0x1C, 0x1F, source, source, A::Execute);
|
||||
range(&mut result, 0x20, 0x2F, source, source, A::Collect);
|
||||
single(&mut result, 0x7F, source, source, A::Ignore);
|
||||
|
||||
// => ground
|
||||
range(
|
||||
&mut result,
|
||||
0x30,
|
||||
0x7E,
|
||||
source,
|
||||
State::Ground,
|
||||
A::EscDispatch,
|
||||
);
|
||||
}
|
||||
|
||||
// sos_pm_apc_string
|
||||
{
|
||||
let source = State::SosPmApcString;
|
||||
|
||||
// events
|
||||
single(&mut result, 0x19, source, source, A::ApcPut);
|
||||
range(&mut result, 0, 0x17, source, source, A::ApcPut);
|
||||
range(&mut result, 0x1C, 0x1F, source, source, A::ApcPut);
|
||||
range(&mut result, 0x20, 0x7F, source, source, A::ApcPut);
|
||||
}
|
||||
|
||||
// escape
|
||||
{
|
||||
let source = State::Escape;
|
||||
|
||||
// events
|
||||
single(&mut result, 0x19, source, source, A::Execute);
|
||||
range(&mut result, 0, 0x17, source, source, A::Execute);
|
||||
range(&mut result, 0x1C, 0x1F, source, source, A::Execute);
|
||||
single(&mut result, 0x7F, source, source, A::Ignore);
|
||||
|
||||
// => ground
|
||||
range(
|
||||
&mut result,
|
||||
0x30,
|
||||
0x4F,
|
||||
source,
|
||||
State::Ground,
|
||||
A::EscDispatch,
|
||||
);
|
||||
range(
|
||||
&mut result,
|
||||
0x51,
|
||||
0x57,
|
||||
source,
|
||||
State::Ground,
|
||||
A::EscDispatch,
|
||||
);
|
||||
range(
|
||||
&mut result,
|
||||
0x60,
|
||||
0x7E,
|
||||
source,
|
||||
State::Ground,
|
||||
A::EscDispatch,
|
||||
);
|
||||
single(&mut result, 0x59, source, State::Ground, A::EscDispatch);
|
||||
single(&mut result, 0x5A, source, State::Ground, A::EscDispatch);
|
||||
single(&mut result, 0x5C, source, State::Ground, A::EscDispatch);
|
||||
|
||||
// => escape_intermediate
|
||||
range(
|
||||
&mut result,
|
||||
0x20,
|
||||
0x2F,
|
||||
source,
|
||||
State::EscapeIntermediate,
|
||||
A::Collect,
|
||||
);
|
||||
|
||||
// => sos_pm_apc_string
|
||||
single(&mut result, 0x58, source, State::SosPmApcString, A::None);
|
||||
single(&mut result, 0x5E, source, State::SosPmApcString, A::None);
|
||||
single(&mut result, 0x5F, source, State::SosPmApcString, A::None);
|
||||
|
||||
// => dcs_entry
|
||||
single(&mut result, 0x50, source, State::DcsEntry, A::None);
|
||||
|
||||
// => csi_entry
|
||||
single(&mut result, 0x5B, source, State::CsiEntry, A::None);
|
||||
|
||||
// => osc_string
|
||||
single(&mut result, 0x5D, source, State::OscString, A::None);
|
||||
}
|
||||
|
||||
// dcs_entry
|
||||
{
|
||||
let source = State::DcsEntry;
|
||||
|
||||
// events
|
||||
single(&mut result, 0x19, source, source, A::Ignore);
|
||||
range(&mut result, 0, 0x17, source, source, A::Ignore);
|
||||
range(&mut result, 0x1C, 0x1F, source, source, A::Ignore);
|
||||
single(&mut result, 0x7F, source, source, A::Ignore);
|
||||
|
||||
// => dcs_intermediate
|
||||
range(
|
||||
&mut result,
|
||||
0x20,
|
||||
0x2F,
|
||||
source,
|
||||
State::DcsIntermediate,
|
||||
A::Collect,
|
||||
);
|
||||
|
||||
// => dcs_ignore
|
||||
single(&mut result, 0x3A, source, State::DcsIgnore, A::None);
|
||||
|
||||
// => dcs_param
|
||||
range(&mut result, 0x30, 0x39, source, State::DcsParam, A::Param);
|
||||
single(&mut result, 0x3B, source, State::DcsParam, A::Param);
|
||||
range(&mut result, 0x3C, 0x3F, source, State::DcsParam, A::Collect);
|
||||
|
||||
// => dcs_passthrough
|
||||
range(
|
||||
&mut result,
|
||||
0x40,
|
||||
0x7E,
|
||||
source,
|
||||
State::DcsPassthrough,
|
||||
A::None,
|
||||
);
|
||||
}
|
||||
|
||||
// dcs_intermediate
|
||||
{
|
||||
let source = State::DcsIntermediate;
|
||||
|
||||
// events
|
||||
single(&mut result, 0x19, source, source, A::Ignore);
|
||||
range(&mut result, 0, 0x17, source, source, A::Ignore);
|
||||
range(&mut result, 0x1C, 0x1F, source, source, A::Ignore);
|
||||
range(&mut result, 0x20, 0x2F, source, source, A::Collect);
|
||||
single(&mut result, 0x7F, source, source, A::Ignore);
|
||||
|
||||
// => dcs_ignore
|
||||
range(&mut result, 0x30, 0x3F, source, State::DcsIgnore, A::None);
|
||||
|
||||
// => dcs_passthrough
|
||||
range(
|
||||
&mut result,
|
||||
0x40,
|
||||
0x7E,
|
||||
source,
|
||||
State::DcsPassthrough,
|
||||
A::None,
|
||||
);
|
||||
}
|
||||
|
||||
// dcs_ignore
|
||||
{
|
||||
let source = State::DcsIgnore;
|
||||
|
||||
// events
|
||||
single(&mut result, 0x19, source, source, A::Ignore);
|
||||
range(&mut result, 0, 0x17, source, source, A::Ignore);
|
||||
range(&mut result, 0x1C, 0x1F, source, source, A::Ignore);
|
||||
}
|
||||
|
||||
// dcs_param
|
||||
{
|
||||
let source = State::DcsParam;
|
||||
|
||||
// events
|
||||
single(&mut result, 0x19, source, source, A::Ignore);
|
||||
range(&mut result, 0, 0x17, source, source, A::Ignore);
|
||||
range(&mut result, 0x1C, 0x1F, source, source, A::Ignore);
|
||||
range(&mut result, 0x30, 0x39, source, source, A::Param);
|
||||
single(&mut result, 0x3B, source, source, A::Param);
|
||||
single(&mut result, 0x7F, source, source, A::Ignore);
|
||||
|
||||
// => dcs_ignore
|
||||
single(&mut result, 0x3A, source, State::DcsIgnore, A::None);
|
||||
range(&mut result, 0x3C, 0x3F, source, State::DcsIgnore, A::None);
|
||||
|
||||
// => dcs_intermediate
|
||||
range(
|
||||
&mut result,
|
||||
0x20,
|
||||
0x2F,
|
||||
source,
|
||||
State::DcsIntermediate,
|
||||
A::Collect,
|
||||
);
|
||||
|
||||
// => dcs_passthrough
|
||||
range(
|
||||
&mut result,
|
||||
0x40,
|
||||
0x7E,
|
||||
source,
|
||||
State::DcsPassthrough,
|
||||
A::None,
|
||||
);
|
||||
}
|
||||
|
||||
// dcs_passthrough
|
||||
{
|
||||
let source = State::DcsPassthrough;
|
||||
|
||||
// events
|
||||
single(&mut result, 0x19, source, source, A::Put);
|
||||
range(&mut result, 0, 0x17, source, source, A::Put);
|
||||
range(&mut result, 0x1C, 0x1F, source, source, A::Put);
|
||||
range(&mut result, 0x20, 0x7E, source, source, A::Put);
|
||||
single(&mut result, 0x7F, source, source, A::Ignore);
|
||||
}
|
||||
|
||||
// csi_param
|
||||
{
|
||||
let source = State::CsiParam;
|
||||
|
||||
// events
|
||||
single(&mut result, 0x19, source, source, A::Execute);
|
||||
range(&mut result, 0, 0x17, source, source, A::Execute);
|
||||
range(&mut result, 0x1C, 0x1F, source, source, A::Execute);
|
||||
range(&mut result, 0x30, 0x39, source, source, A::Param);
|
||||
single(&mut result, 0x3A, source, source, A::Param);
|
||||
single(&mut result, 0x3B, source, source, A::Param);
|
||||
single(&mut result, 0x7F, source, source, A::Ignore);
|
||||
|
||||
// => ground
|
||||
range(
|
||||
&mut result,
|
||||
0x40,
|
||||
0x7E,
|
||||
source,
|
||||
State::Ground,
|
||||
A::CsiDispatch,
|
||||
);
|
||||
|
||||
// => csi_ignore
|
||||
range(&mut result, 0x3C, 0x3F, source, State::CsiIgnore, A::None);
|
||||
|
||||
// => csi_intermediate
|
||||
range(
|
||||
&mut result,
|
||||
0x20,
|
||||
0x2F,
|
||||
source,
|
||||
State::CsiIntermediate,
|
||||
A::Collect,
|
||||
);
|
||||
}
|
||||
|
||||
// csi_ignore
|
||||
{
|
||||
let source = State::CsiIgnore;
|
||||
|
||||
// events
|
||||
single(&mut result, 0x19, source, source, A::Execute);
|
||||
range(&mut result, 0, 0x17, source, source, A::Execute);
|
||||
range(&mut result, 0x1C, 0x1F, source, source, A::Execute);
|
||||
range(&mut result, 0x20, 0x3F, source, source, A::Ignore);
|
||||
single(&mut result, 0x7F, source, source, A::Ignore);
|
||||
|
||||
// => ground
|
||||
range(&mut result, 0x40, 0x7E, source, State::Ground, A::None);
|
||||
}
|
||||
|
||||
// csi_intermediate
|
||||
{
|
||||
let source = State::CsiIntermediate;
|
||||
|
||||
// events
|
||||
single(&mut result, 0x19, source, source, A::Execute);
|
||||
range(&mut result, 0, 0x17, source, source, A::Execute);
|
||||
range(&mut result, 0x1C, 0x1F, source, source, A::Execute);
|
||||
range(&mut result, 0x20, 0x2F, source, source, A::Collect);
|
||||
single(&mut result, 0x7F, source, source, A::Ignore);
|
||||
|
||||
// => ground
|
||||
range(
|
||||
&mut result,
|
||||
0x40,
|
||||
0x7E,
|
||||
source,
|
||||
State::Ground,
|
||||
A::CsiDispatch,
|
||||
);
|
||||
|
||||
// => csi_ignore
|
||||
range(&mut result, 0x30, 0x3F, source, State::CsiIgnore, A::None);
|
||||
}
|
||||
|
||||
// csi_entry
|
||||
{
|
||||
let source = State::CsiEntry;
|
||||
|
||||
// events
|
||||
single(&mut result, 0x19, source, source, A::Execute);
|
||||
range(&mut result, 0, 0x17, source, source, A::Execute);
|
||||
range(&mut result, 0x1C, 0x1F, source, source, A::Execute);
|
||||
single(&mut result, 0x7F, source, source, A::Ignore);
|
||||
|
||||
// => ground
|
||||
range(
|
||||
&mut result,
|
||||
0x40,
|
||||
0x7E,
|
||||
source,
|
||||
State::Ground,
|
||||
A::CsiDispatch,
|
||||
);
|
||||
|
||||
// => csi_ignore
|
||||
single(&mut result, 0x3A, source, State::CsiIgnore, A::None);
|
||||
|
||||
// => csi_intermediate
|
||||
range(
|
||||
&mut result,
|
||||
0x20,
|
||||
0x2F,
|
||||
source,
|
||||
State::CsiIntermediate,
|
||||
A::Collect,
|
||||
);
|
||||
|
||||
// => csi_param
|
||||
range(&mut result, 0x30, 0x39, source, State::CsiParam, A::Param);
|
||||
single(&mut result, 0x3B, source, State::CsiParam, A::Param);
|
||||
range(&mut result, 0x3C, 0x3F, source, State::CsiParam, A::Collect);
|
||||
}
|
||||
|
||||
// osc_string
|
||||
{
|
||||
let source = State::OscString;
|
||||
|
||||
// events
|
||||
single(&mut result, 0x19, source, source, A::Ignore);
|
||||
range(&mut result, 0, 0x06, source, source, A::Ignore);
|
||||
range(&mut result, 0x08, 0x17, source, source, A::Ignore);
|
||||
range(&mut result, 0x1C, 0x1F, source, source, A::Ignore);
|
||||
range(&mut result, 0x20, 0xFF, source, source, A::OscPut);
|
||||
|
||||
// XTerm accepts either BEL or ST for terminating OSC
|
||||
// sequences, and when returning information, uses the same
|
||||
// terminator used in a query.
|
||||
single(&mut result, 0x07, source, State::Ground, A::None);
|
||||
}
|
||||
|
||||
// Create our immutable version
|
||||
let mut fin: Table = [[Transition {
|
||||
state: State::Ground,
|
||||
action: A::None,
|
||||
}; NUM_STATES]; 256];
|
||||
let mut i = 0;
|
||||
while i < 256 {
|
||||
let mut j = 0;
|
||||
while j < NUM_STATES {
|
||||
fin[i][j] = match result[i][j] {
|
||||
Some(t) => t,
|
||||
None => transition(STATES[j], A::None),
|
||||
};
|
||||
j += 1;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
fin
|
||||
}
|
||||
|
||||
const fn single(t: &mut OptionalTable, c: u8, s0: State, s1: State, a: A) {
|
||||
t[c as usize][s0 as usize] = Some(transition(s1, a));
|
||||
}
|
||||
|
||||
const fn range(t: &mut OptionalTable, from: u8, to: u8, s0: State, s1: State, a: A) {
|
||||
let mut i = from;
|
||||
loop {
|
||||
single(t, i, s0, s1, a);
|
||||
// If 'to' is 0xFF, our next pass would overflow.
|
||||
if i == to {
|
||||
break;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
const fn transition(state: State, action: A) -> Transition {
|
||||
Transition { state, action }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::unwrap_used)]
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn table_builds() {
|
||||
// This forces evaluation of the table, so we're just testing
|
||||
// that it succeeds in creation.
|
||||
let _ = &TABLE;
|
||||
assert_eq!(
|
||||
TABLE[b'a' as usize][State::Ground as usize],
|
||||
transition(State::Ground, A::Print)
|
||||
);
|
||||
assert_eq!(
|
||||
TABLE[0x1B][State::CsiParam as usize],
|
||||
transition(State::Escape, A::None)
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user