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 .unset to reset all attributes
  • Simple attributes (lines 235-260): Maps values like 0 to reset, 1 to bold, 3 to italic, 4 to underline
  • Underline variants (lines 244-270): Detects colon-separated styles (e.g., 4:2 for double underline) and falls back to single underline for unknown style values
  • 8-color standard (lines 312-345): Handles 30-37 (foreground) and 40-47 (background)
  • 256-color palette (lines 357-381): Parses 38;5;<n> and 48;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:

  1. Starting with 0 (reset)
  2. Appending flags in numerical order (1 bold, 2 faint, 3 italic, 4 underline)
  3. 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.zig using a type-safe Zig union(Tag) where each variant represents a specific graphic rendition attribute.
  • The Parser struct processes CSI m sequences, 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.zig exposes 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →