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:
- Convert base16 colors (background, foreground, and 8 theme colors) to CIELAB (
LAB) - Handle light themes by optionally inverting the cube when
harmoniousis false - Build the 6×6×6 RGB cube (indices 16-231) by interpolating between the 8 corner colors
- Generate the grayscale ramp (indices 232-255) by interpolating between background and foreground
- 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 fromCSI 38;2;<r>;<g>;<b>mdirect_color_bg: color.RGB— Parsed fromCSI 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
RGBpacked struct andPalettearray defined insrc/terminal/color.zig. - 256-color support is theme-aware, generating a perceptually uniform 6×6×6 cube and grayscale ramp via
generate256Colorusing CIELAB interpolation. - True color support handles 24-bit RGB through
direct_color_fganddirect_color_bgattributes 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →