# How Ghostty Implements SGR (Select Graphic Rendition) for Terminal Text Styling

> Discover how Ghostty implements SGR terminal text styling with its Zig architecture. Explore 8-color, 256-color, and true-color RGB support for enhanced terminal visuals.

- Repository: [Ghostty/ghostty](https://github.com/ghostty-org/ghostty)
- Tags: internals
- Published: 2026-05-01

---

**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.

```zig
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`.

```c
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:

```zig
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

```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

```c
#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

```zig
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.