How Ghostty Implements SGR (Select Graphic Rendition) for Terminal Text Styling
Ghostty implements SGR handling through a modular Zig architecture that separates parsing, attribute representation, and application to the terminal state, supporting 8-color, 256-color, true-color RGB, and colon-separated syntax.
The ghostty-org/ghostty repository implements ANSI SGR sequences using a type-safe union-based approach in Zig. This design separates the concerns of parsing escape sequences from applying stylistic attributes to terminal cells. Understanding this implementation reveals how the terminal handles text styling, colors, and formatting codes ranging from simple bold flags to complex 24-bit RGB values.
Attribute Representation with Zig Unions
Ghostty defines SGR attributes in src/terminal/sgr.zig using a discriminated union that maps each SGR code to a specific type variant.
pub const Attribute = union(Tag) {
unset,
unknown: Unknown,
bold, reset_bold,
italic, reset_italic,
faint,
underline: Underline,
underline_color: color.RGB,
@"256_underline_color": u8,
reset_underline_color,
overline, reset_overline,
blink, reset_blink,
inverse, reset_inverse,
invisible, reset_invisible,
strikethrough, reset_strikethrough,
direct_color_fg: color.RGB,
direct_color_bg: color.RGB,
@"8_bg": color.Name, @"8_fg": color.Name,
reset_fg, reset_bg,
@"8_bright_bg": color.Name, @"8_bright_fg": color.Name,
@"256_bg": u8, @"256_fg": u8,
// …
};
Each variant corresponds to a specific SGR functionality. Simple flags like bold use empty variants, while complex attributes like underline or direct_color_fg carry structured data (Underline structs or color.RGB values). The unknown variant preserves raw CSI parameters for sequences that cannot be interpreted, enabling graceful fallback without crashing the parser.
Parsing Logic in the SGR Parser
The Parser struct in src/terminal/sgr.zig processes CSI […m sequences by iterating through numeric parameters stored as []u16. It maintains state via three key fields: params (the parameter slice), params_sep (a bit-mask tracking colon separators), and idx (current position).
Handling Different SGR Codes
The next() method returns ?Attribute and implements a state machine that switches on parameter values:
- Empty parameter list (lines 184-190): Returns
.unsetto reset all attributes - Simple attributes (lines 235-260): Maps values like
0to reset,1to bold,3to italic,4to underline - Underline variants (lines 244-270): Detects colon-separated styles (e.g.,
4:2for double underline) and falls back to single underline for unknown style values - 8-color standard (lines 312-345): Handles
30-37(foreground) and40-47(background) - 256-color palette (lines 357-381): Parses
38;5;<n>and48;5;<n>sequences - 24-bit direct color (lines 388-424): Extracts RGB components via
parseDirectColor, supporting both semicolon (38;2;r;g;b) and colon (38:2::r:g:b) forms with optional colorspace identifiers
When encountering unrecognized codes, the parser returns .unknown (lines 588-600), preserving the raw parameter list for potential later inspection.
Colon Separator Support
The parser handles the rare colon-separated syntax used by some terminal emulators. Helper methods like isColon, countColon, and consumeUnknownColon manage the params_sep bit-mask, allowing the parser to distinguish between 38;2;255;0;0 and 38:2:255:0:0 forms when processing true-color values.
C-Compatible API Wrapper
Ghostty exposes SGR parsing functionality to C callers through src/terminal/c/sgr.zig. The ParserWrapper struct owns a Zig Parser instance and manages memory via CAllocator.
GhosttySgrParser *parser;
GhosttySgrResult result = ghostty_sgr_new(NULL, &parser);
ghostty_sgr_set_params(parser, params, seps, len);
while (ghostty_sgr_next(parser, &attr)) {
// attr.tag identifies which attribute was parsed
}
ghostty_sgr_free(parser);
This abstraction guarantees correct memory management across language boundaries while maintaining the type safety of the underlying Zig implementation.
Applying Attributes to Terminal State
Once parsed, attributes flow through src/terminal/Terminal.zig via the setAttribute method:
pub fn setAttribute(self: *Terminal, attr: sgr.Attribute) !void {
try self.screens.active.setAttribute(attr);
}
The active screen updates its Pen (cursor.style) structure—defined in src/terminal/style.zig—modifying flags for bold, italic, underline, and color fields (fg_color, bg_color).
For attribute reporting (such as DECRQSS queries), Terminal.printAttributes (lines 2670-2830) constructs SGR response strings by:
- Starting with
0(reset) - Appending flags in numerical order (
1bold,2faint,3italic,4underline) - Encoding colors using appropriate syntax (
38:5:for 256-color,38:2:for true-color)
This ensures round-trip fidelity between parsed attributes and generated escape sequences.
Testing and Fuzzing Coverage
The SGR module includes extensive inline tests (test "sgr: ..." blocks) verifying:
- Simple attribute parsing (bold, italic, underline styles)
- 8-color, 256-color, and 24-bit color variants
- Colon-separated forms and edge cases (missing parameters, extra colons)
- Real-world sequences from editors like Kakoune
Additional regression coverage comes from fuzz corpora in test/fuzz-libghostty/corpus/, which contains malformed inputs that previously triggered crashes, ensuring robustness against adversarial escape sequences.
Code Examples
Parsing SGR Parameters in Zig
const sgr = @import("src/terminal/sgr.zig");
// ESC[1;38:2:255:0:0;4m → bold, true-red foreground, underline
const params = &[_]u16{ 1, 38, 2, 255, 0, 0, 4 };
var parser = sgr.Parser{ .params = params };
while (parser.next()) |attr| {
std.debug.print("Parsed: {any}\n", .{attr});
}
Output:
Parsed: .bold
Parsed: .direct_color_fg{ .r = 255, .g = 0, .b = 0 }
Parsed: .underline{ .single }
Using the C API
#include "ghostty_sgr.h"
int main(void) {
GhosttySgrParser *p;
ghostty_sgr_new(NULL, &p);
// ESC[4:2;38:2:100:150:200m (double underline + RGB foreground)
uint16_t params[] = {4, 2, 38, 2, 100, 150, 200};
const char seps[] = {':', ':'};
ghostty_sgr_set_params(p, params, seps, 7);
GhosttySgrAttribute attr;
while (ghostty_sgr_next(p, &attr)) {
printf("Tag: %d\n", ghostty_sgr_attribute_tag(attr));
}
ghostty_sgr_free(p);
return 0;
}
Setting Attributes Programmatically
const Terminal = @import("src/terminal/Terminal.zig");
const sgr = @import("src/terminal/sgr.zig");
// Apply bold + overline + bright blue foreground
try term.setAttribute(.bold);
try term.setAttribute(.overline);
try term.setAttribute(.@"8_bright_fg" = .bright_blue);
Summary
- Ghostty implements SGR parsing in
src/terminal/sgr.zigusing a type-safe Zigunion(Tag)where each variant represents a specific graphic rendition attribute. - The Parser struct processes CSI
msequences, handling 8-color, 256-color, and 24-bit RGB formats with support for both semicolon and colon separators. - A C API wrapper in
src/terminal/c/sgr.zigexposes parsing functionality to external callers with safe memory management. - Attributes are applied to terminal state via
Terminal.setAttribute, which updates the Pen style structure in the active screen buffer. - Fuzz testing and comprehensive unit tests ensure correct handling of edge cases, unknown codes, and malformed sequences.
Frequently Asked Questions
What color formats does Ghostty's SGR implementation support?
Ghostty supports 8-color standard ANSI (codes 30-37, 40-47), 256-color palette (codes 38;5;n and 48;5;n), and 24-bit direct color (codes 38;2;r;g;b). The parser handles both semicolon-separated and colon-separated syntax for true-color sequences, accommodating legacy and modern terminal conventions.
How does Ghostty handle unknown or malformed SGR sequences?
Unknown SGR codes are captured in the unknown variant of the Attribute union, which preserves the raw CSI parameters. This allows the terminal to skip unrecognized attributes gracefully without crashing, while potentially logging the sequence for debugging purposes.
Can Ghostty's SGR parser be used from C or other languages?
Yes. The src/terminal/c/sgr.zig wrapper provides a ParserWrapper struct that exposes ghostty_sgr_new, ghostty_sgr_set_params, ghostty_sgr_next, and ghostty_sgr_free functions. These use the library's CAllocator abstraction to ensure safe memory management across the Zig/C boundary.
How are SGR attributes stored in memory?
Attributes are stored as discriminated unions where the Tag enum indicates the active variant. When applied to the terminal, these populate the Pen struct (src/terminal/style.zig), which contains boolean flags for styles (bold, italic, underline) and dedicated fields for foreground/background colors supporting various color depths.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →