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 },
|
.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