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