How Ghostty Implements Color Management: 256-Color and True Color Support

Ghostty implements color management through a theme-aware 256-color palette generated in CIELAB color space and direct 24-bit RGB true color support, handling both via Zig structs in src/terminal/color.zig and escape sequence parsers in src/terminal/sgr.zig.

Ghostty is a GPU-accelerated terminal emulator written in Zig that supports ANSI 8-color, 256-color indexed, and 24-bit true color modes. Its color architecture separates static color definitions from dynamic palette generation, allowing the terminal to adapt its 256-color cube to user themes while maintaining compatibility with standard escape sequences.

Core Color Types

Ghostty defines three fundamental color primitives in src/terminal/color.zig.

The RGB Struct

The 24-bit RGB representation is a packed struct consuming exactly 24 bits:

pub const RGB = packed struct(u24) {
    r: u8 = 0,
    g: u8 = 0,
    b: u8 = 0,
    // …
};

Source: src/terminal/color.zig

This struct provides methods for luminance calculation, contrast ratios, and conversion to CIELAB for palette generation.

Named Colors and Palettes

The 8-color ANSI set and bright variants live in the Name enum. For 256-color support, Ghostty uses a fixed-size array:

pub const Palette = [256]RGB;

Source: src/terminal/color.zig

A C-compatible variant (PaletteC) exists for FFI boundaries.

Theme-Aware 256-Color Palette Generation

Unlike terminals that use hardcoded xterm-256color values, Ghostty computes its 256-color palette at runtime using the CIELAB color space for perceptually uniform interpolation.

The generate256Color Algorithm

The generate256Color function in src/terminal/color.zig builds the palette through trilinear interpolation:

  1. Convert base16 colors (background, foreground, and 8 theme colors) to CIELAB (LAB)
  2. Handle light themes by optionally inverting the cube when harmonious is false
  3. Build the 6×6×6 RGB cube (indices 16-231) by interpolating between the 8 corner colors
  4. Generate the grayscale ramp (indices 232-255) by interpolating between background and foreground
  5. Respect the skip mask to preserve user-overridden palette entries

Source: src/terminal/color.zig

CIELAB Interpolation Implementation

The algorithm first converts theme colors to CIELAB space:

const base8_lab: [8]LAB = base8: {
    var base8: [8]LAB = .{
        .fromRgb(bg),                // index 0 → background
        LAB.fromRgb(base[1]),        // red
        LAB.fromRgb(base[2]),        // green
        // … additional colors …
        .fromRgb(fg),                // index 7 → foreground
    };
    const is_light_theme = base8[7].l < base8[0].l;
    const invert = is_light_theme and !harmonious;
    if (invert) std.mem.swap(LAB, &base8[0], &base8[7]);
    break :base8 base8;
};

Source: src/terminal/color.zig

Cube Generation Loop

The nested loops generate 216 colors (6×6×6) using linear interpolation (lerp) along the red, green, and blue axes:

var idx: usize = 16;
for (0..6) |ri| {
    const tr = @as(f32, @floatFromInt(ri)) / 5.0;
    const c0: LAB = .lerp(tr, base8_lab[0], base8_lab[1]);
    const c1: LAB = .lerp(tr, base8_lab[2], base8_lab[3]);
    // … additional interpolation steps …
    for (0..6) |gi| {
        const tg = @as(f32, @floatFromInt(gi)) / 5.0;
        const c4: LAB = .lerp(tg, c0, c1);
        for (0..6) |bi| {
            if (!skip.isSet(idx)) {
                const c6: LAB = .lerp(@as(f32, @floatFromInt(bi)) / 5.0, c4, c5);
                result[idx] = c6.toRgb();
            }
            idx += 1;
        }
    }
}

Source: src/terminal/color.zig

Grayscale Ramp Construction

The final 24 palette entries are generated by interpolating between background and foreground:

for (0..24) |i| {
    const t = @as(f32, @floatFromInt(i + 1)) / 25.0;
    if (!skip.isSet(idx)) {
        const c: LAB = .lerp(t, base8_lab[0], base8_lab[7]);
        result[idx] = c.toRgb();
    }
    idx += 1;
}

Source: src/terminal/color.zig

Runtime Escape Sequence Handling

Ghostty processes color changes through SGR (Select Graphic Rendition) sequences for immediate color application and OSC (Operating System Command) sequences for palette manipulation.

SGR True Color Attributes

In src/terminal/sgr.zig, the Attribute union supports direct RGB specification:

  • direct_color_fg: color.RGB — Parsed from CSI 38;2;<r>;<g>;<b>m
  • direct_color_bg: color.RGB — Parsed from CSI 48;2;<r>;<g>;<b>m

Source: src/terminal/sgr.zig

The parser creates these variants and forwards them to termio/stream_handler.zig for application to the terminal state.

OSC Palette Queries and Updates

OSC sequences (OSC 4, 10, 11) allow applications to query or set colors dynamically. The handler in termio/stream_handler.zig processes these:

fn colorOperation(op: terminal.osc.color.Operation,
                  requests: *const terminal.osc.color.List,
                  terminator: u8) !void {
    // …
    .palette => |i| self.terminal.colors.palette.set(i, set.color);
    .foreground => self.terminal.colors.foreground.set(set.color);
    // …
}

Source: src/termio/stream_handler.zig

The osc_color_report_format configuration in Termio.zig controls whether Ghostty reports colors as indexes, rgb: strings, or suppresses reports entirely.

Source: src/termio/Termio.zig

Rendering Surface Updates

When palette entries change, Ghostty notifies the rendering surface via message passing:

self.surfaceMessageWriter(.{ .color_change = .{ .color = set.color } });

Source: src/termio/stream_handler.zig

This ensures the GPU renderer immediately reflects new colors without waiting for the next frame.

Practical Usage Examples

Setting True Color in Zig

To set an orange foreground using direct RGB:

const term = try Terminal.init(allocator);
try term.setAttribute(.{ .direct_color_fg = .{ .r = 0xFF, .g = 0x80, .b = 0x00 } });

Implementation path: src/terminal/Terminal.zig → setAttribute → Screen handling of direct_color_fg.

Using 256-Color Palette Entries

To select a color from the generated palette:

// Use palette index 42 as foreground
try term.setAttribute(.{ .@"256_fg" = 42 });

Source: src/terminal/sgr.zig defines the @"256_fg" variant.

Querying the Current Scheme

Applications can request color reports via OSC:

term.messageWriter(.{ .color_scheme_report = .{ .force = false } });

Source: src/termio/message.zig — color_scheme_report struct.

Summary

  • Ghostty color management relies on the RGB packed struct and Palette array defined in src/terminal/color.zig.
  • 256-color support is theme-aware, generating a perceptually uniform 6×6×6 cube and grayscale ramp via generate256Color using CIELAB interpolation.
  • True color support handles 24-bit RGB through direct_color_fg and direct_color_bg attributes in the SGR parser.
  • Runtime updates flow through OSC handlers in stream_handler.zig, which respect user configuration and notify the rendering surface immediately.

Frequently Asked Questions

How does Ghostty's 256-color palette differ from xterm?

Ghostty generates its 256-color palette dynamically from your theme colors using CIELAB color space interpolation, whereas xterm uses fixed RGB values. This ensures the 216-color cube and 24 grayscale steps harmonize with your terminal's base16 colors.

Can applications override specific palette entries?

Yes. The generate256Color function accepts a skip bit mask that preserves user-overridden entries. Applications can also set specific indices via OSC 4 sequences handled in termio/stream_handler.zig.

What CIELAB conversion is used for?

CIELAB provides perceptually uniform color space, meaning equal numerical changes correspond to equal perceptual changes. Ghostty converts theme colors to CIELAB before interpolation in generate256Color to ensure smooth gradients in the 6×6×6 color cube.

Does Ghostty support light themes automatically?

Yes. The palette generator detects light themes by comparing the luminance (l value) of foreground and background colors in CIELAB space. When is_light_theme is true and harmonious is false, it inverts the interpolation cube to maintain color relationships.

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 →