mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
initial VT emulation table
This commit is contained in:
107
src/terminal/Parser.zig
Normal file
107
src/terminal/Parser.zig
Normal file
@ -0,0 +1,107 @@
|
||||
//! VT-series parser for escape and control sequences.
|
||||
//!
|
||||
//! This is implemented directly as the state machine described on
|
||||
//! vt100.net: https://vt100.net/emu/dec_ansi_parser
|
||||
const Parser = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const testing = std.testing;
|
||||
const table = @import("parse_table.zig").table;
|
||||
|
||||
/// States for the state machine
|
||||
pub const State = enum {
|
||||
anywhere,
|
||||
ground,
|
||||
escape,
|
||||
escape_intermediate,
|
||||
csi_entry,
|
||||
csi_intermediate,
|
||||
csi_param,
|
||||
csi_ignore,
|
||||
dcs_entry,
|
||||
dcs_param,
|
||||
dcs_intermediate,
|
||||
dcs_passthrough,
|
||||
dcs_ignore,
|
||||
osc_string,
|
||||
sos_pm_apc_string,
|
||||
};
|
||||
|
||||
pub const Action = enum {
|
||||
none,
|
||||
ignore,
|
||||
print,
|
||||
execute,
|
||||
clear,
|
||||
collect,
|
||||
param,
|
||||
esc_dispatch,
|
||||
csi_dispatch,
|
||||
hook,
|
||||
put,
|
||||
unhook,
|
||||
osc_start,
|
||||
osc_put,
|
||||
osc_end,
|
||||
};
|
||||
|
||||
/// Current state of the state machine
|
||||
state: State = .ground,
|
||||
|
||||
pub fn init() Parser {
|
||||
return .{};
|
||||
}
|
||||
|
||||
pub fn next(self: *Parser, c: u8) void {
|
||||
const effect = effect: {
|
||||
// First look up the transition in the anywhere table.
|
||||
const anywhere = table[c][@enumToInt(State.anywhere)];
|
||||
if (anywhere.state != .anywhere) break :effect anywhere;
|
||||
|
||||
// If we don't have any transition from anywhere, use our state.
|
||||
break :effect table[c][@enumToInt(self.state)];
|
||||
};
|
||||
|
||||
const next_state = effect.state;
|
||||
const action = effect.action;
|
||||
|
||||
// When going from one state to another, the actions take place in this order:
|
||||
//
|
||||
// 1. exit action from old state
|
||||
// 2. transition action
|
||||
// 3. entry action to new state
|
||||
|
||||
// Perform exit actions. "The action associated with the exit event happens
|
||||
// when an incoming symbol causes a transition from this state to another
|
||||
// state (or even back to the same state)."
|
||||
switch (self.state) {
|
||||
.osc_string => {}, // TODO: osc_end
|
||||
.dcs_passthrough => {}, // TODO: unhook
|
||||
else => {},
|
||||
}
|
||||
|
||||
// Perform the transition action
|
||||
self.doAction(action);
|
||||
|
||||
// Perform the entry action
|
||||
// TODO: when _first_ entered only?
|
||||
switch (self.state) {
|
||||
.escape, .dcs_entry, .csi_entry => {}, // TODO: clear
|
||||
.osc_string => {}, // TODO: osc_start
|
||||
.dcs_passthrough => {}, // TODO: hook
|
||||
else => {},
|
||||
}
|
||||
|
||||
self.state = next_state;
|
||||
}
|
||||
|
||||
fn doAction(self: *Parser, action: Action) void {
|
||||
_ = self;
|
||||
_ = action;
|
||||
}
|
||||
|
||||
test {
|
||||
var p = init();
|
||||
p.next(0x9E);
|
||||
try testing.expect(p.state == .sos_pm_apc_string);
|
||||
}
|
@ -42,3 +42,7 @@ pub fn init(cols: usize, rows: usize) Terminal {
|
||||
.cursor = .{ .x = 0, .y = 0 },
|
||||
};
|
||||
}
|
||||
|
||||
test {
|
||||
_ = @import("Parser.zig");
|
||||
}
|
||||
|
363
src/terminal/parse_table.zig
Normal file
363
src/terminal/parse_table.zig
Normal file
@ -0,0 +1,363 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const parser = @import("Parser.zig");
|
||||
const State = parser.State;
|
||||
const Action = parser.Action;
|
||||
|
||||
/// The state transition table. The type is [u8][State]Transition but
|
||||
/// comptime-generated to be exactly-sized.
|
||||
pub const table = genTable();
|
||||
|
||||
/// Table is the type of the state table. This is dynamically (comptime)
|
||||
/// generated to be exactly sized.
|
||||
pub const Table = genTableType();
|
||||
|
||||
// Transition is the transition to take within the table
|
||||
pub const Transition = struct {
|
||||
state: State,
|
||||
action: Action,
|
||||
};
|
||||
|
||||
/// Table is the type of the state transition table.
|
||||
fn genTableType() type {
|
||||
const max_u8 = std.math.maxInt(u8);
|
||||
const stateInfo = @typeInfo(State);
|
||||
const max_state = stateInfo.Enum.fields.len;
|
||||
return [max_u8][max_state]Transition;
|
||||
}
|
||||
|
||||
/// Function to generate the full state transition table for VT emulation.
|
||||
fn genTable() Table {
|
||||
@setEvalBranchQuota(15000);
|
||||
var result: Table = undefined;
|
||||
|
||||
// In debug mode, we initialize everything so that we can detect if
|
||||
// anything is overwritten. No value should be set more than once
|
||||
// since the state machine diagram is exact.
|
||||
if (builtin.mode == .Debug) {
|
||||
var i: u8 = 0;
|
||||
while (i < result.len) : (i += 1) {
|
||||
var j: u8 = 0;
|
||||
while (j < result[0].len) : (j += 1) {
|
||||
result[i][j] = transition(.anywhere, .none);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ground
|
||||
{
|
||||
// anywhere =>
|
||||
single(&result, 0x18, .anywhere, .ground, .execute);
|
||||
single(&result, 0x1A, .anywhere, .ground, .execute);
|
||||
single(&result, 0x9C, .anywhere, .ground, .none);
|
||||
|
||||
// events
|
||||
single(&result, 0x19, .ground, .ground, .execute);
|
||||
range(&result, 0, 0x17, .ground, .ground, .execute);
|
||||
range(&result, 0x1C, 0x1F, .ground, .ground, .execute);
|
||||
range(&result, 0x20, 0x7F, .ground, .ground, .print);
|
||||
}
|
||||
|
||||
// escape_intermediate
|
||||
{
|
||||
const source = State.escape_intermediate;
|
||||
|
||||
single(&result, 0x19, source, source, .execute);
|
||||
range(&result, 0, 0x17, source, source, .execute);
|
||||
range(&result, 0x1C, 0x1F, source, source, .execute);
|
||||
range(&result, 0x20, 0x2F, source, source, .collect);
|
||||
single(&result, 0x7F, source, source, .ignore);
|
||||
|
||||
// => ground
|
||||
range(&result, 0x30, 0x7E, source, .ground, .esc_dispatch);
|
||||
}
|
||||
|
||||
// sos_pm_apc_string
|
||||
{
|
||||
const source = State.sos_pm_apc_string;
|
||||
|
||||
// anywhere =>
|
||||
single(&result, 0x98, .anywhere, source, .none);
|
||||
single(&result, 0x9E, .anywhere, source, .none);
|
||||
single(&result, 0x9F, .anywhere, source, .none);
|
||||
|
||||
// events
|
||||
single(&result, 0x19, source, source, .ignore);
|
||||
range(&result, 0, 0x17, source, source, .ignore);
|
||||
range(&result, 0x1C, 0x1F, source, source, .ignore);
|
||||
range(&result, 0x20, 0x7F, source, source, .ignore);
|
||||
|
||||
// => ground
|
||||
single(&result, 0x9C, source, .ground, .none);
|
||||
}
|
||||
|
||||
// escape
|
||||
{
|
||||
const source = State.escape;
|
||||
|
||||
// anywhere =>
|
||||
single(&result, 0x1B, .anywhere, source, .none);
|
||||
|
||||
// events
|
||||
single(&result, 0x19, source, source, .execute);
|
||||
range(&result, 0, 0x17, source, source, .execute);
|
||||
range(&result, 0x1C, 0x1F, source, source, .execute);
|
||||
single(&result, 0x7F, source, source, .ignore);
|
||||
|
||||
// => ground
|
||||
range(&result, 0x30, 0x4F, source, .ground, .esc_dispatch);
|
||||
range(&result, 0x51, 0x57, source, .ground, .esc_dispatch);
|
||||
range(&result, 0x60, 0x7E, source, .ground, .esc_dispatch);
|
||||
single(&result, 0x59, source, .ground, .esc_dispatch);
|
||||
single(&result, 0x5A, source, .ground, .esc_dispatch);
|
||||
single(&result, 0x5C, source, .ground, .esc_dispatch);
|
||||
|
||||
// => escape_intermediate
|
||||
range(&result, 0x20, 0x2F, source, .escape_intermediate, .collect);
|
||||
|
||||
// => sos_pm_apc_string
|
||||
single(&result, 0x58, source, .sos_pm_apc_string, .none);
|
||||
single(&result, 0x5E, source, .sos_pm_apc_string, .none);
|
||||
single(&result, 0x5F, source, .sos_pm_apc_string, .none);
|
||||
|
||||
// => dcs_entry
|
||||
single(&result, 0x50, source, .dcs_entry, .none);
|
||||
|
||||
// => csi_entry
|
||||
single(&result, 0x5B, source, .csi_entry, .none);
|
||||
|
||||
// => osc_string
|
||||
single(&result, 0x5D, source, .osc_string, .none);
|
||||
}
|
||||
|
||||
// dcs_entry
|
||||
{
|
||||
const source = State.dcs_entry;
|
||||
|
||||
// anywhere =>
|
||||
single(&result, 0x90, .anywhere, source, .ignore);
|
||||
|
||||
// events
|
||||
single(&result, 0x19, source, source, .ignore);
|
||||
range(&result, 0, 0x17, source, source, .ignore);
|
||||
range(&result, 0x1C, 0x1F, source, source, .ignore);
|
||||
single(&result, 0x7F, source, source, .ignore);
|
||||
|
||||
// => dcs_intermediate
|
||||
range(&result, 0x20, 0x2F, source, .dcs_intermediate, .collect);
|
||||
|
||||
// => dcs_ignore
|
||||
single(&result, 0x3A, source, .dcs_ignore, .none);
|
||||
|
||||
// => dcs_param
|
||||
range(&result, 0x30, 0x39, source, .dcs_param, .param);
|
||||
single(&result, 0x3B, source, .dcs_param, .param);
|
||||
range(&result, 0x3C, 0x3F, source, .dcs_param, .collect);
|
||||
|
||||
// => dcs_passthrough
|
||||
range(&result, 0x40, 0x7E, source, .dcs_passthrough, .none);
|
||||
}
|
||||
|
||||
// dcs_intermediate
|
||||
{
|
||||
const source = State.dcs_intermediate;
|
||||
|
||||
// events
|
||||
single(&result, 0x19, source, source, .ignore);
|
||||
range(&result, 0, 0x17, source, source, .ignore);
|
||||
range(&result, 0x1C, 0x1F, source, source, .ignore);
|
||||
range(&result, 0x20, 0x2F, source, source, .collect);
|
||||
single(&result, 0x7F, source, source, .ignore);
|
||||
|
||||
// => dcs_ignore
|
||||
range(&result, 0x30, 0x3F, source, .dcs_ignore, .none);
|
||||
|
||||
// => dcs_passthrough
|
||||
range(&result, 0x40, 0x7E, source, .dcs_passthrough, .none);
|
||||
}
|
||||
|
||||
// dcs_ignore
|
||||
{
|
||||
const source = State.dcs_ignore;
|
||||
|
||||
// events
|
||||
single(&result, 0x19, source, source, .ignore);
|
||||
range(&result, 0, 0x17, source, source, .ignore);
|
||||
range(&result, 0x1C, 0x1F, source, source, .ignore);
|
||||
|
||||
// => ground
|
||||
single(&result, 0x9C, source, .ground, .none);
|
||||
}
|
||||
|
||||
// dcs_param
|
||||
{
|
||||
const source = State.dcs_param;
|
||||
|
||||
// events
|
||||
single(&result, 0x19, source, source, .ignore);
|
||||
range(&result, 0, 0x17, source, source, .ignore);
|
||||
range(&result, 0x1C, 0x1F, source, source, .ignore);
|
||||
range(&result, 0x30, 0x39, source, source, .param);
|
||||
single(&result, 0x3B, source, source, .param);
|
||||
single(&result, 0x7F, source, source, .ignore);
|
||||
|
||||
// => dcs_ignore
|
||||
single(&result, 0x3A, source, .dcs_ignore, .none);
|
||||
range(&result, 0x3C, 0x3F, source, .dcs_ignore, .none);
|
||||
|
||||
// => dcs_intermediate
|
||||
range(&result, 0x20, 0x2F, source, .dcs_intermediate, .collect);
|
||||
|
||||
// => dcs_passthrough
|
||||
range(&result, 0x40, 0x7E, source, .dcs_passthrough, .none);
|
||||
}
|
||||
|
||||
// dcs_passthrough
|
||||
{
|
||||
const source = State.dcs_passthrough;
|
||||
|
||||
// events
|
||||
single(&result, 0x19, source, source, .put);
|
||||
range(&result, 0, 0x17, source, source, .put);
|
||||
range(&result, 0x1C, 0x1F, source, source, .put);
|
||||
range(&result, 0x20, 0x7E, source, source, .put);
|
||||
single(&result, 0x7F, source, source, .ignore);
|
||||
|
||||
// => ground
|
||||
single(&result, 0x9C, source, .ground, .none);
|
||||
}
|
||||
|
||||
// csi_param
|
||||
{
|
||||
const source = State.csi_param;
|
||||
|
||||
// events
|
||||
single(&result, 0x19, source, source, .execute);
|
||||
range(&result, 0, 0x17, source, source, .execute);
|
||||
range(&result, 0x1C, 0x1F, source, source, .execute);
|
||||
range(&result, 0x30, 0x39, source, source, .param);
|
||||
single(&result, 0x3B, source, source, .param);
|
||||
single(&result, 0x7F, source, source, .ignore);
|
||||
|
||||
// => ground
|
||||
range(&result, 0x40, 0x7E, source, .ground, .csi_dispatch);
|
||||
|
||||
// => csi_ignore
|
||||
single(&result, 0x3A, source, .csi_ignore, .none);
|
||||
range(&result, 0x3C, 0x3F, source, .csi_ignore, .none);
|
||||
|
||||
// => csi_intermediate
|
||||
range(&result, 0x20, 0x2F, source, .csi_intermediate, .collect);
|
||||
}
|
||||
|
||||
// csi_ignore
|
||||
{
|
||||
const source = State.csi_ignore;
|
||||
|
||||
// events
|
||||
single(&result, 0x19, source, source, .execute);
|
||||
range(&result, 0, 0x17, source, source, .execute);
|
||||
range(&result, 0x1C, 0x1F, source, source, .execute);
|
||||
range(&result, 0x20, 0x3F, source, source, .ignore);
|
||||
single(&result, 0x7F, source, source, .ignore);
|
||||
|
||||
// => ground
|
||||
range(&result, 0x40, 0x7E, source, .ground, .none);
|
||||
}
|
||||
|
||||
// csi_intermediate
|
||||
{
|
||||
const source = State.csi_intermediate;
|
||||
|
||||
// events
|
||||
single(&result, 0x19, source, source, .execute);
|
||||
range(&result, 0, 0x17, source, source, .execute);
|
||||
range(&result, 0x1C, 0x1F, source, source, .execute);
|
||||
range(&result, 0x20, 0x2F, source, source, .collect);
|
||||
single(&result, 0x7F, source, source, .ignore);
|
||||
|
||||
// => ground
|
||||
range(&result, 0x40, 0x7E, source, .ground, .csi_dispatch);
|
||||
|
||||
// => csi_ignore
|
||||
range(&result, 0x30, 0x3F, source, .csi_ignore, .none);
|
||||
}
|
||||
|
||||
// csi_entry
|
||||
{
|
||||
const source = State.csi_entry;
|
||||
|
||||
// anywhere =>
|
||||
single(&result, 0x9B, .anywhere, source, .none);
|
||||
|
||||
// events
|
||||
single(&result, 0x19, source, source, .execute);
|
||||
range(&result, 0, 0x17, source, source, .execute);
|
||||
range(&result, 0x1C, 0x1F, source, source, .execute);
|
||||
single(&result, 0x7F, source, source, .ignore);
|
||||
|
||||
// => ground
|
||||
range(&result, 0x40, 0x7E, source, .ground, .csi_dispatch);
|
||||
|
||||
// => csi_ignore
|
||||
single(&result, 0x3A, source, .csi_ignore, .none);
|
||||
|
||||
// => csi_intermediate
|
||||
range(&result, 0x20, 0x2F, source, .csi_intermediate, .collect);
|
||||
|
||||
// => csi_param
|
||||
range(&result, 0x30, 0x39, source, .csi_param, .param);
|
||||
single(&result, 0x3B, source, .csi_param, .param);
|
||||
range(&result, 0x3C, 0x3F, source, .csi_param, .collect);
|
||||
}
|
||||
|
||||
// osc_string
|
||||
{
|
||||
const source = State.osc_string;
|
||||
|
||||
// anywhere =>
|
||||
single(&result, 0x9D, .anywhere, source, .none);
|
||||
|
||||
// events
|
||||
single(&result, 0x19, source, source, .ignore);
|
||||
range(&result, 0, 0x17, source, source, .ignore);
|
||||
range(&result, 0x1C, 0x1F, source, source, .ignore);
|
||||
range(&result, 0x20, 0x7F, source, source, .osc_put);
|
||||
|
||||
// => ground
|
||||
single(&result, 0x9C, source, .ground, .none);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
fn single(t: *Table, c: u8, s0: State, s1: State, a: Action) void {
|
||||
// In debug mode, we want to verify that every state is marked
|
||||
// exactly once.
|
||||
if (builtin.mode == .Debug) {
|
||||
const existing = t[c][@enumToInt(s0)];
|
||||
if (existing.state != .anywhere) {
|
||||
std.debug.print("transition set multiple times c={} s0={} existing={}", .{
|
||||
c, s0, existing,
|
||||
});
|
||||
unreachable;
|
||||
}
|
||||
}
|
||||
|
||||
t[c][@enumToInt(s0)] = transition(s1, a);
|
||||
}
|
||||
|
||||
fn range(t: *Table, from: u8, to: u8, s0: State, s1: State, a: Action) void {
|
||||
var i = from;
|
||||
while (i <= to) : (i += 1) single(t, i, s0, s1, a);
|
||||
}
|
||||
|
||||
fn transition(state: State, action: Action) Transition {
|
||||
return .{ .state = state, .action = action };
|
||||
}
|
||||
|
||||
test {
|
||||
// This forces comptime-evaluation of table, so we're just testing
|
||||
// that it succeeds in creation.
|
||||
_ = table;
|
||||
}
|
Reference in New Issue
Block a user