| name | vte-escape-sequences |
| description | ANSI escape sequence parsing with VTE for terminal emulation |
vte-escape-sequences
The vte crate (version 0.15) provides a state-machine parser for ANSI escape sequences used in terminal emulators. Developed by the Alacritty team, it implements Paul Williams' ANSI parser state machine and is the foundation for terminal emulation in script-kit-gpui.
Key Types
Parser
The core state machine that parses raw bytes into terminal actions.
use vte::Parser;
let mut parser = Parser::new();
let mut performer = MyPerformer;
// Advance parser with raw bytes
parser.advance(&mut performer, &bytes);
// Or advance until early termination
let bytes_read = parser.advance_until_terminated(&mut performer, &bytes);
Key methods:
new()- Create a new parseradvance(&mut performer, &[u8])- Process bytes, calling performer methodsadvance_until_terminated(&mut performer, &[u8])- Process untilPerform::terminated()returns true
Perform Trait
Implement this trait to handle parsed escape sequences:
pub trait Perform {
fn print(&mut self, c: char) { } // Regular printable character
fn execute(&mut self, byte: u8) { } // C0/C1 control (e.g., \n, \r, \t)
fn csi_dispatch(&mut self, params: &Params, intermediates: &[u8], ignore: bool, action: char) { }
fn esc_dispatch(&mut self, intermediates: &[u8], ignore: bool, byte: u8) { }
fn osc_dispatch(&mut self, params: &[&[u8]], bell_terminated: bool) { }
fn hook(&mut self, params: &Params, intermediates: &[u8], ignore: bool, action: char) { }
fn put(&mut self, byte: u8) { } // DCS data bytes
fn unhook(&mut self) { } // DCS string terminated
fn terminated(&self) -> bool { false } // Early termination check
}
Params
Iterator over CSI sequence parameters:
fn csi_dispatch(&mut self, params: &Params, ...) {
for param in params.iter() {
// param is a subparam slice for `:` separated values
// e.g., SGR 38:2:255:0:0 for RGB colors
}
}
vte::ansi Module Types (via alacritty_terminal)
script-kit-gpui uses these from the vte::ansi submodule:
use vte::ansi::{Color, NamedColor, Processor, Rgb};
Rgb- RGB color struct withr,g,bfieldsColor- Enum for terminal colors (Named, Indexed, Rgb)NamedColor- Standard 16 ANSI color namesProcessor- High-level processor wrapping Parser + Perform
Usage in script-kit-gpui
The terminal module uses VTE via alacritty_terminal for escape sequence parsing:
Architecture
PTY Output --> VTE Parser --> Term Grid --> GPUI Render
Key Integration Points
src/terminal/alacritty.rs:
use vte::ansi::{Color, NamedColor, Processor, Rgb};
struct TerminalState {
term: Term<EventProxy>,
processor: Processor,
}
impl TerminalState {
fn process_bytes(&mut self, bytes: &[u8]) {
// VTE 0.15 advance() takes a slice of bytes
self.processor.advance(&mut self.term, bytes);
}
}
src/terminal/theme_adapter.rs:
use vte::ansi::Rgb;
// Convert hex colors to VTE's Rgb type
pub fn hex_to_rgb(hex: u32) -> Rgb {
Rgb {
r: ((hex >> 16) & 0xFF) as u8,
g: ((hex >> 8) & 0xFF) as u8,
b: (hex & 0xFF) as u8,
}
}
ANSI Escape Sequences Reference
Sequence Structure
ESC [ <params> <intermediate> <final>
^ ^ ^ ^ ^
| | | | +-- Action character (A, B, m, J, etc.)
| | | +-- Optional intermediate bytes (!, ?, >)
| | +-- Semicolon-separated numbers
| +-- CSI introducer '['
+-- Escape character (0x1B)
Common CSI Sequences
| Sequence | Description | Perform Method |
|---|---|---|
ESC[<n>A |
Cursor up n lines | csi_dispatch action='A' |
ESC[<n>B |
Cursor down n lines | csi_dispatch action='B' |
ESC[<n>C |
Cursor forward n cols | csi_dispatch action='C' |
ESC[<n>D |
Cursor back n cols | csi_dispatch action='D' |
ESC[<r>;<c>H |
Move cursor to row;col | csi_dispatch action='H' |
ESC[J |
Clear screen (0=below, 1=above, 2=all) | csi_dispatch action='J' |
ESC[K |
Clear line (0=right, 1=left, 2=all) | csi_dispatch action='K' |
ESC[<n>m |
Set Graphics Rendition (SGR) | csi_dispatch action='m' |
SGR (Select Graphic Rendition) Codes
| Code | Effect |
|---|---|
| 0 | Reset all attributes |
| 1 | Bold/bright |
| 2 | Dim/faint |
| 3 | Italic |
| 4 | Underline |
| 7 | Inverse/reverse video |
| 8 | Hidden |
| 9 | Strikethrough |
| 22 | Normal intensity (not bold/dim) |
| 23 | Not italic |
| 24 | Not underlined |
| 27 | Not inverse |
| 29 | Not strikethrough |
Color Codes
Basic Colors (30-37 fg, 40-47 bg):
| Code | Color |
|---|---|
| 30/40 | Black |
| 31/41 | Red |
| 32/42 | Green |
| 33/43 | Yellow |
| 34/44 | Blue |
| 35/45 | Magenta |
| 36/46 | Cyan |
| 37/47 | White |
| 39/49 | Default |
Bright Colors (90-97 fg, 100-107 bg): Same as above but bright variants.
256 Color Mode:
ESC[38;5;<n>m- ForegroundESC[48;5;<n>m- Background- n = 0-15 standard, 16-231 color cube, 232-255 grayscale
24-bit RGB:
ESC[38;2;<r>;<g>;<b>m- ForegroundESC[48;2;<r>;<g>;<b>m- Background
C0 Control Characters
| Byte | Name | Action |
|---|---|---|
| 0x07 | BEL | Bell |
| 0x08 | BS | Backspace |
| 0x09 | HT | Horizontal Tab |
| 0x0A | LF | Line Feed |
| 0x0D | CR | Carriage Return |
These trigger execute() in the Perform trait.
OSC (Operating System Command)
Format: ESC ] <code> ; <data> BEL or ESC ] <code> ; <data> ST
| Code | Purpose |
|---|---|
| 0 | Set icon name and window title |
| 1 | Set icon name |
| 2 | Set window title |
| 4 | Change color (palette) |
| 52 | Clipboard operations |
Triggers osc_dispatch() in Perform.
The Perform Trait
Method Dispatch Flow
Byte Stream
|
v
+--------+
| Parser |
+--------+
|
+---> print(char) <- Regular text
|
+---> execute(byte) <- Control chars (0x00-0x1F)
|
+---> csi_dispatch(...) <- ESC [ ... sequences
|
+---> esc_dispatch(...) <- ESC ... sequences
|
+---> osc_dispatch(...) <- ESC ] ... sequences
|
+---> hook/put/unhook <- DCS sequences
Example Implementation
struct MyTerminal {
cursor_x: usize,
cursor_y: usize,
}
impl vte::Perform for MyTerminal {
fn print(&mut self, c: char) {
// Draw character at cursor, advance cursor
}
fn execute(&mut self, byte: u8) {
match byte {
0x07 => self.bell(),
0x08 => self.backspace(),
0x0A => self.line_feed(),
0x0D => self.carriage_return(),
_ => {}
}
}
fn csi_dispatch(&mut self, params: &Params, _intermediates: &[u8], _ignore: bool, action: char) {
let params: Vec<u16> = params.iter()
.flat_map(|p| p.iter().copied())
.collect();
match action {
'H' | 'f' => {
// Cursor position: ESC[<row>;<col>H
let row = params.get(0).copied().unwrap_or(1) as usize;
let col = params.get(1).copied().unwrap_or(1) as usize;
self.cursor_y = row.saturating_sub(1);
self.cursor_x = col.saturating_sub(1);
}
'm' => self.handle_sgr(¶ms),
'J' => self.clear_screen(params.get(0).copied().unwrap_or(0)),
_ => {}
}
}
}
Parser State Machine
VTE implements the state machine from vt100.net/emu/dec_ansi_parser:
States
- Ground - Normal text input, calls
print()for printable chars - Escape - After ESC, waiting for sequence type
- CSI Entry - After
ESC[, collecting parameters - CSI Param - Collecting numeric parameters
- CSI Intermediate - After intermediate bytes (?, !, >)
- OSC String - Collecting OSC data
- DCS Entry/Param/Intermediate/Passthrough - Device Control String states
UTF-8 Support
VTE handles UTF-8 decoding internally. Multi-byte sequences are collected and delivered as complete char values to print().
Early Termination
Use terminated() for synchronized updates:
impl Perform for MyTerminal {
fn terminated(&self) -> bool {
// Return true to stop parsing (e.g., after sync marker)
self.sync_complete
}
}
// Then use:
let bytes_consumed = parser.advance_until_terminated(&mut performer, &bytes);
// Process remaining bytes[bytes_consumed..] later
Anti-patterns
1. Blocking in Perform Methods
Bad:
fn print(&mut self, c: char) {
self.gpu_draw(c).await; // Don't block!
}
Good:
fn print(&mut self, c: char) {
self.buffer.push(c); // Buffer, render later
}
2. Ignoring the ignore Flag
Bad:
fn csi_dispatch(&mut self, params: &Params, _intermediates: &[u8], _ignore: bool, action: char) {
// Always process...
}
Good:
fn csi_dispatch(&mut self, params: &Params, _intermediates: &[u8], ignore: bool, action: char) {
if ignore {
return; // Malformed sequence, skip
}
// Process valid sequence...
}
3. Hardcoding Parameter Defaults
Bad:
fn csi_dispatch(&mut self, params: &Params, ..., action: char) {
match action {
'A' => self.cursor_up(params[0]), // Panics on empty params!
}
}
Good:
fn csi_dispatch(&mut self, params: &Params, ..., action: char) {
let params: Vec<u16> = params.iter().flat_map(|p| p.iter().copied()).collect();
match action {
'A' => self.cursor_up(params.get(0).copied().unwrap_or(1)),
}
}
4. Not Handling Subparameters
Bad:
// Ignores 38:2:R:G:B syntax for true color
let params: Vec<u16> = params.iter().next().map(|p| p[0]).collect();
Good:
// Handle both semicolon and colon separated params
for subparams in params.iter() {
// subparams is a slice for colon-separated values
match subparams {
&[38, 2, r, g, b] => self.set_fg_rgb(r, g, b),
&[38, 5, idx] => self.set_fg_indexed(idx),
// ...
}
}
5. Creating Parser Per Parse
Bad:
fn process(&mut self, bytes: &[u8]) {
let mut parser = Parser::new(); // New parser loses state!
parser.advance(&mut self.performer, bytes);
}
Good:
struct Terminal {
parser: Parser,
state: TermState,
}
fn process(&mut self, bytes: &[u8]) {
self.parser.advance(&mut self.state, bytes);
}
Integration with alacritty_terminal
In script-kit-gpui, VTE is used indirectly through alacritty_terminal:
// alacritty_terminal provides Processor which wraps VTE Parser
use vte::ansi::Processor;
struct TerminalState {
term: Term<EventProxy>,
processor: Processor,
}
impl TerminalState {
fn process_bytes(&mut self, bytes: &[u8]) {
// Processor handles parsing and calls Term's Handler impl
self.processor.advance(&mut self.term, bytes);
}
}
The Term type from alacritty_terminal implements the equivalent of VTE's Perform trait, handling all escape sequences and updating the terminal grid.