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

> Explore how Ghostty handles color management with its theme-aware 256-color palette and 24-bit true color support. Learn about its CIELAB color space implementation and Zig struct integration.

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

---

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

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

```

*Source:* [`src/terminal/color.zig`](https://github.com/ghostty-org/ghostty/blob/main/src/terminal/color.zig#L16-L20)

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:

```zig
pub const Palette = [256]RGB;

```

*Source:* [`src/terminal/color.zig`](https://github.com/ghostty-org/ghostty/blob/main/src/terminal/color.zig#L48)

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`](https://github.com/ghostty-org/ghostty/blob/main/src/terminal/color.zig#L70-L110)

### CIELAB Interpolation Implementation

The algorithm first converts theme colors to CIELAB space:

```zig
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`](https://github.com/ghostty-org/ghostty/blob/main/src/terminal/color.zig#L112-L136)

### Cube Generation Loop

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

```zig
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`](https://github.com/ghostty-org/ghostty/blob/main/src/terminal/color.zig#L148-L175)

### Grayscale Ramp Construction

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

```zig
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`](https://github.com/ghostty-org/ghostty/blob/main/src/terminal/color.zig#L177-L188)

## 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`](https://github.com/ghostty-org/ghostty/blob/main/src/terminal/sgr.zig#L56-L61)

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:

```zig
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`](https://github.com/ghostty-org/ghostty/blob/main/src/termio/stream_handler.zig#L1211-L1240)

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`](https://github.com/ghostty-org/ghostty/blob/main/src/termio/Termio.zig#L166-L169)

### Rendering Surface Updates

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

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

```

*Source:* [`src/termio/stream_handler.zig`](https://github.com/ghostty-org/ghostty/blob/main/src/termio/stream_handler.zig#L1255-L1258)

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:

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

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

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