VT Escape Sequence Parser

VT Escape Sequence Parser

The VtParser is a general-purpose ANSI/VT escape sequence parser based on Paul Flo Williams’ DEC-compatible parser. It classifies all terminal input into structured events: printable characters, control characters, CSI sequences, OSC strings, DCS passthrough, and escape sequences.

Overview

Terminal input contains a mix of printable text and escape sequences (key codes, mouse events, color query responses, device attribute reports). The VtParser classifies each byte using a pre-computed lookup table, guaranteeing that every byte in every state has a defined transition.

The parser is used internally by ActionDecoder to prevent unknown escape sequences from leaking byte-by-byte into the input stream. It can also be used directly by applications that need to process raw terminal responses.

Quick Start

import org.aesh.terminal.parser.VtParser;
import org.aesh.terminal.parser.VtHandler;

VtParser parser = new VtParser(new VtHandler() {
    @Override
    public void print(int codePoint) {
        System.out.print((char) codePoint);
    }

    @Override
    public void csiDispatch(int finalChar, int[] params, int paramCount,
                            int[] intermediates, int intermediateCount,
                            boolean hasSubParams) {
        System.out.printf("CSI sequence: final=%c params=%d%n",
                (char) finalChar, paramCount);
    }

    @Override
    public void oscEnd(String data) {
        System.out.println("OSC: " + data);
    }
});

// Process input
byte[] input = "\033[2JHello\033]0;title\007".getBytes();
parser.advance(input, 0, input.length);

How It Works

The parser uses a short[14][256] transition table (~7KB) where each entry encodes (action << 4 | nextState). This is a single array lookup per byte with no branching – the table is the state machine.

States

The 14 states follow the Williams specification:

StatePurpose
groundInitial state. Printable characters are dispatched via print().
escapeAfter ESC (0x1B). Routes to CSI, OSC, DCS, or simple escape sequence.
escape_intermediateCollecting intermediate characters (0x20-0x2F) in an escape sequence.
csi_entryAfter CSI. Handles first character (private marker or parameter).
csi_paramCollecting CSI parameters (digits, semicolons, colons).
csi_intermediateCollecting CSI intermediate characters.
csi_ignoreConsuming a malformed CSI sequence (discards without dispatching).
dcs_entryAfter DCS. Mirrors CSI entry structure.
dcs_paramCollecting DCS parameters.
dcs_intermediateCollecting DCS intermediate characters.
dcs_passthroughPassing DCS data to handler via put().
dcs_ignoreConsuming a malformed DCS string.
osc_stringCollecting OSC string content.
sos_pm_apc_stringIgnoring SOS/PM/APC strings (no DEC-defined functions).

Actions

ActionCallbackWhen
printVtHandler.print(codePoint)Printable character in ground state
executeVtHandler.execute(controlChar)C0/C1 control character
csi_dispatchVtHandler.csiDispatch(...)Complete CSI sequence
esc_dispatchVtHandler.escDispatch(...)Complete escape sequence
osc_endVtHandler.oscEnd(data)Complete OSC string
hookVtHandler.hook(...)DCS passthrough started
putVtHandler.put(byte)DCS data byte
unhookVtHandler.unhook()DCS passthrough ended

Deviations from Williams

Two modern extensions are included:

  1. Colon (0x3A) as subparameter separator – Williams specifies that colon causes CSI sequences to be ignored. Modern terminals use colon for SGR extended color syntax (38:2:R:G:B), so the parser treats it as a valid parameter separator and sets the hasSubParams flag.

  2. BEL (0x07) terminates OSC strings – Williams specifies only ST (0x9C or ESC \) as the OSC terminator. xterm and all modern terminals also accept BEL, which is the de facto standard.

Input Formats

The parser accepts both raw bytes and Unicode code points:

// Byte array (from terminal I/O)
parser.advance(bytes, offset, length);

// Code point array (from Decoder output in the readline pipeline)
parser.advance(codePoints, offset, length);

// Single value
parser.advance(byteOrCodePoint);

Code points above 255 are treated as printable in the ground state and ignored in other states.

Integration with ActionDecoder

The ActionDecoder uses VtParser as a fallback when the KeyMappingTrie has no match for an escape sequence. Previously, unknown sequences (like mouse events or DA responses) would leak byte-by-byte as individual key actions. Now:

  1. KeyMappingTrie is checked first (handles known keys like arrow keys, function keys)
  2. If no match and no prefix, and the buffer starts with ESC, VtParser measures the complete sequence length
  3. The entire sequence is consumed as a single SequenceKeyAction
  4. If the sequence is incomplete, the parser waits for more input

This means unknown CSI sequences (mouse events, paste brackets, terminal responses) are cleanly consumed rather than corrupting the input stream.

Performance

The table-driven design provides consistent per-byte cost regardless of sequence type:

InputTime
Single printable character~5 ns
CSI arrow key (3 bytes)~22 ns
CSI SGR color (18 bytes)~67 ns
OSC title (22 bytes)~88 ns
Mixed realistic input (93 bytes)~231 ns (~2.5 ns/byte)

See Also