libghostty: add resolved bg_color and fg_color to cells API

Fixes #11705

Add bg_color and fg_color options to GhosttyRenderStateRowCellsData
that resolve the final RGB color for a cell, flattening the multiple
possible sources. For background, this handles content-tag bg_color_rgb,
content-tag bg_color_palette (looked up in the palette), and the
style bg_color. For foreground, this resolves palette indices through
the palette; bold color handling is not applied and is left to the
caller.

Both return GHOSTTY_INVALID_VALUE when no explicit color is set, in
which case the caller should fall back to whatever default color it
wants (e.g. the terminal background/foreground).
This commit is contained in:
Mitchell Hashimoto
2026-03-21 20:16:35 -07:00
parent 1775c312ae
commit 8bd3a493be
5 changed files with 320 additions and 6 deletions

View File

@@ -514,6 +514,22 @@ typedef enum {
* The buffer must be at least graphemes_len elements. The base codepoint
* is written first, followed by any extra codepoints. */
GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_BUF = 4,
/** The resolved background color of the cell (GhosttyColorRgb).
* Flattens the three possible sources: content-tag bg_color_rgb,
* content-tag bg_color_palette (looked up in the palette), or the
* style's bg_color. Returns GHOSTTY_INVALID_VALUE if the cell has
* no background color, in which case the caller should use whatever
* default background color it wants (e.g. the terminal background). */
GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_BG_COLOR = 5,
/** The resolved foreground color of the cell (GhosttyColorRgb).
* Resolves palette indices through the palette. Bold color handling
* is not applied; the caller should handle bold styling separately.
* Returns GHOSTTY_INVALID_VALUE if the cell has no explicit foreground
* color, in which case the caller should use whatever default foreground
* color it wants (e.g. the terminal foreground). */
GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_FG_COLOR = 6,
} GhosttyRenderStateRowCellsData;
/**

View File

@@ -48,6 +48,7 @@ const string = @import("string.zig");
const terminal = struct {
const CursorStyle = @import("../terminal/cursor.zig").Style;
const color = @import("../terminal/color.zig");
const style = @import("../terminal/style.zig");
const x11_color = @import("../terminal/x11_color.zig");
};
@@ -5597,6 +5598,14 @@ pub const BoldColor = union(enum) {
color: Color,
bright,
/// Convert to the terminal-native BoldColor type.
pub fn toTerminal(self: BoldColor) terminal.style.Style.BoldColor {
return switch (self) {
.color => |col| .{ .color = col.toTerminalRGB() },
.bright => .bright,
};
}
pub fn parseCLI(input_: ?[]const u8) !BoldColor {
const input = input_ orelse return error.ValueRequired;
if (std.mem.eql(u8, input, "bright")) return .bright;

View File

@@ -554,7 +554,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
search_foreground: configpkg.Config.TerminalColor,
search_selected_background: configpkg.Config.TerminalColor,
search_selected_foreground: configpkg.Config.TerminalColor,
bold_color: ?configpkg.BoldColor,
bold_color: ?terminal.Style.BoldColor,
faint_opacity: u8,
min_contrast: f32,
padding_color: configpkg.WindowPaddingColor,
@@ -619,7 +619,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
.background = config.background.toTerminalRGB(),
.foreground = config.foreground.toTerminalRGB(),
.bold_color = config.@"bold-color",
.bold_color = if (config.@"bold-color") |b| b.toTerminal() else null,
.faint_opacity = @intFromFloat(@ceil(config.@"faint-opacity" * 255)),
.min_contrast = @floatCast(config.@"minimum-contrast"),

View File

@@ -33,6 +33,10 @@ const RowIteratorWrapper = struct {
raws: []const page.Row,
cells: []const std.MultiArrayList(renderpkg.RenderState.Cell),
dirty: []bool,
/// The color palette from the render state, needed to resolve
/// palette-indexed background colors on cells.
palette: *const colorpkg.Palette,
};
const RowCellsWrapper = struct {
@@ -41,6 +45,9 @@ const RowCellsWrapper = struct {
raws: []const page.Cell,
graphemes: []const []const u21,
styles: []const Style,
/// The color palette, needed to resolve palette-indexed background colors.
palette: *const colorpkg.Palette,
};
/// C: GhosttyRenderState
@@ -214,6 +221,7 @@ fn getTyped(
.raws = row_data.items(.raw),
.cells = row_data.items(.cells),
.dirty = row_data.items(.dirty),
.palette = &state.state.colors.palette,
};
},
.color_background => out.* = state.state.colors.background.cval(),
@@ -361,6 +369,7 @@ pub fn row_iterator_new(
.raws = undefined,
.cells = undefined,
.dirty = undefined,
.palette = undefined,
};
result.* = ptr;
return .success;
@@ -395,6 +404,7 @@ pub fn row_cells_new(
.raws = undefined,
.graphemes = undefined,
.styles = undefined,
.palette = undefined,
};
result.* = ptr;
return .success;
@@ -428,6 +438,8 @@ pub const RowCellsData = enum(c_int) {
style = 2,
graphemes_len = 3,
graphemes_buf = 4,
bg_color = 5,
fg_color = 6,
/// Output type expected for querying the data of the given kind.
pub fn OutType(comptime self: RowCellsData) type {
@@ -437,6 +449,7 @@ pub const RowCellsData = enum(c_int) {
.style => style_c.Style,
.graphemes_len => u32,
.graphemes_buf => u32,
.bg_color, .fg_color => colorpkg.RGB.C,
};
}
};
@@ -494,6 +507,17 @@ fn rowCellsGetTyped(
buf[i] = cp;
}
},
.bg_color => {
const s: Style = if (cell.hasStyling()) cells.styles[x] else .{};
const bg = s.bg(&cell, cells.palette) orelse return .invalid_value;
out.* = bg.cval();
},
.fg_color => {
const s: Style = if (cell.hasStyling()) cells.styles[x] else .{};
if (s.fg_color == .none) return .invalid_value;
const fg = s.fg(.{ .default = .{}, .palette = cells.palette });
out.* = fg.cval();
},
}
return .success;
@@ -570,6 +594,7 @@ fn rowGetTyped(
.raws = cell_data.items(.raw),
.graphemes = cell_data.items(.grapheme),
.styles = cell_data.items(.style),
.palette = it.palette,
};
},
}
@@ -1069,6 +1094,264 @@ test "render: colors get" {
}
}
test "render: row cells bg_color no background" {
var terminal: terminal_c.Terminal = null;
try testing.expectEqual(Result.success, terminal_c.new(
&lib_alloc.test_allocator,
&terminal,
.{
.cols = 80,
.rows = 24,
.max_scrollback = 10_000,
},
));
defer terminal_c.free(terminal);
// Write plain text (no background color set).
terminal_c.vt_write(terminal, "hello", 5);
var state: RenderState = null;
try testing.expectEqual(Result.success, new(
&lib_alloc.test_allocator,
&state,
));
defer free(state);
try testing.expectEqual(Result.success, update(state, terminal));
var it: RowIterator = null;
try testing.expectEqual(Result.success, row_iterator_new(
&lib_alloc.test_allocator,
&it,
));
defer row_iterator_free(it);
try testing.expectEqual(Result.success, get(state, .row_iterator, @ptrCast(&it)));
try testing.expect(row_iterator_next(it));
var cells: RowCells = null;
try testing.expectEqual(Result.success, row_cells_new(
&lib_alloc.test_allocator,
&cells,
));
defer row_cells_free(cells);
try testing.expectEqual(Result.success, row_get(it, .cells, @ptrCast(&cells)));
try testing.expect(row_cells_next(cells));
// No background set, should return invalid_value.
var bg: colorpkg.RGB.C = undefined;
try testing.expectEqual(Result.invalid_value, row_cells_get(cells, .bg_color, @ptrCast(&bg)));
}
test "render: row cells bg_color from style" {
var terminal: terminal_c.Terminal = null;
try testing.expectEqual(Result.success, terminal_c.new(
&lib_alloc.test_allocator,
&terminal,
.{
.cols = 80,
.rows = 24,
.max_scrollback = 10_000,
},
));
defer terminal_c.free(terminal);
// Set an RGB background via SGR 48;2;R;G;B and write text.
terminal_c.vt_write(terminal, "\x1b[48;2;10;20;30mA", 18);
var state: RenderState = null;
try testing.expectEqual(Result.success, new(
&lib_alloc.test_allocator,
&state,
));
defer free(state);
try testing.expectEqual(Result.success, update(state, terminal));
var it: RowIterator = null;
try testing.expectEqual(Result.success, row_iterator_new(
&lib_alloc.test_allocator,
&it,
));
defer row_iterator_free(it);
try testing.expectEqual(Result.success, get(state, .row_iterator, @ptrCast(&it)));
try testing.expect(row_iterator_next(it));
var cells: RowCells = null;
try testing.expectEqual(Result.success, row_cells_new(
&lib_alloc.test_allocator,
&cells,
));
defer row_cells_free(cells);
try testing.expectEqual(Result.success, row_get(it, .cells, @ptrCast(&cells)));
try testing.expect(row_cells_next(cells));
var bg: colorpkg.RGB.C = undefined;
try testing.expectEqual(Result.success, row_cells_get(cells, .bg_color, @ptrCast(&bg)));
try testing.expectEqual(@as(u8, 10), bg.r);
try testing.expectEqual(@as(u8, 20), bg.g);
try testing.expectEqual(@as(u8, 30), bg.b);
}
test "render: row cells bg_color from content tag" {
var terminal: terminal_c.Terminal = null;
try testing.expectEqual(Result.success, terminal_c.new(
&lib_alloc.test_allocator,
&terminal,
.{
.cols = 80,
.rows = 24,
.max_scrollback = 10_000,
},
));
defer terminal_c.free(terminal);
// Set an RGB background and then erase the line. The erased cells
// should carry the background color via the content tag (bg_color_rgb)
// rather than through the style.
terminal_c.vt_write(terminal, "\x1b[48;2;10;20;30m\x1b[2K", 21);
var state: RenderState = null;
try testing.expectEqual(Result.success, new(
&lib_alloc.test_allocator,
&state,
));
defer free(state);
try testing.expectEqual(Result.success, update(state, terminal));
var it: RowIterator = null;
try testing.expectEqual(Result.success, row_iterator_new(
&lib_alloc.test_allocator,
&it,
));
defer row_iterator_free(it);
try testing.expectEqual(Result.success, get(state, .row_iterator, @ptrCast(&it)));
try testing.expect(row_iterator_next(it));
var cells: RowCells = null;
try testing.expectEqual(Result.success, row_cells_new(
&lib_alloc.test_allocator,
&cells,
));
defer row_cells_free(cells);
try testing.expectEqual(Result.success, row_get(it, .cells, @ptrCast(&cells)));
try testing.expect(row_cells_next(cells));
var bg: colorpkg.RGB.C = undefined;
try testing.expectEqual(Result.success, row_cells_get(cells, .bg_color, @ptrCast(&bg)));
try testing.expectEqual(@as(u8, 10), bg.r);
try testing.expectEqual(@as(u8, 20), bg.g);
try testing.expectEqual(@as(u8, 30), bg.b);
}
test "render: row cells fg_color no foreground" {
var terminal: terminal_c.Terminal = null;
try testing.expectEqual(Result.success, terminal_c.new(
&lib_alloc.test_allocator,
&terminal,
.{
.cols = 80,
.rows = 24,
.max_scrollback = 10_000,
},
));
defer terminal_c.free(terminal);
// Write plain text (no foreground color set).
terminal_c.vt_write(terminal, "hello", 5);
var state: RenderState = null;
try testing.expectEqual(Result.success, new(
&lib_alloc.test_allocator,
&state,
));
defer free(state);
try testing.expectEqual(Result.success, update(state, terminal));
var it: RowIterator = null;
try testing.expectEqual(Result.success, row_iterator_new(
&lib_alloc.test_allocator,
&it,
));
defer row_iterator_free(it);
try testing.expectEqual(Result.success, get(state, .row_iterator, @ptrCast(&it)));
try testing.expect(row_iterator_next(it));
var cells: RowCells = null;
try testing.expectEqual(Result.success, row_cells_new(
&lib_alloc.test_allocator,
&cells,
));
defer row_cells_free(cells);
try testing.expectEqual(Result.success, row_get(it, .cells, @ptrCast(&cells)));
try testing.expect(row_cells_next(cells));
// No foreground set, should return invalid_value.
var fg: colorpkg.RGB.C = undefined;
try testing.expectEqual(Result.invalid_value, row_cells_get(cells, .fg_color, @ptrCast(&fg)));
}
test "render: row cells fg_color from style" {
var terminal: terminal_c.Terminal = null;
try testing.expectEqual(Result.success, terminal_c.new(
&lib_alloc.test_allocator,
&terminal,
.{
.cols = 80,
.rows = 24,
.max_scrollback = 10_000,
},
));
defer terminal_c.free(terminal);
// Set an RGB foreground via SGR 38;2;R;G;B and write text.
terminal_c.vt_write(terminal, "\x1b[38;2;10;20;30mA", 18);
var state: RenderState = null;
try testing.expectEqual(Result.success, new(
&lib_alloc.test_allocator,
&state,
));
defer free(state);
try testing.expectEqual(Result.success, update(state, terminal));
var it: RowIterator = null;
try testing.expectEqual(Result.success, row_iterator_new(
&lib_alloc.test_allocator,
&it,
));
defer row_iterator_free(it);
try testing.expectEqual(Result.success, get(state, .row_iterator, @ptrCast(&it)));
try testing.expect(row_iterator_next(it));
var cells: RowCells = null;
try testing.expectEqual(Result.success, row_cells_new(
&lib_alloc.test_allocator,
&cells,
));
defer row_cells_free(cells);
try testing.expectEqual(Result.success, row_get(it, .cells, @ptrCast(&cells)));
try testing.expect(row_cells_next(cells));
var fg: colorpkg.RGB.C = undefined;
try testing.expectEqual(Result.success, row_cells_get(cells, .fg_color, @ptrCast(&fg)));
try testing.expectEqual(@as(u8, 10), fg.r);
try testing.expectEqual(@as(u8, 20), fg.g);
try testing.expectEqual(@as(u8, 30), fg.b);
}
test "render: colors get supports truncated sized struct" {
var terminal: terminal_c.Terminal = null;
try testing.expectEqual(Result.success, terminal_c.new(

View File

@@ -1,6 +1,5 @@
const std = @import("std");
const assert = @import("../quirks.zig").inlineAssert;
const configpkg = @import("../config.zig");
const color = @import("color.zig");
const sgr = @import("sgr.zig");
const page = @import("page.zig");
@@ -126,6 +125,13 @@ pub const Style = struct {
};
}
/// The color to use for bold text. This avoids a dependency on the
/// config module by using terminal-native color types.
pub const BoldColor = union(enum) {
color: color.RGB,
bright,
};
pub const Fg = struct {
/// The default color to use if the style doesn't specify a
/// foreground color and no configuration options override
@@ -137,7 +143,7 @@ pub const Style = struct {
palette: *const color.Palette,
/// If specified, the color to use for bold text.
bold: ?configpkg.BoldColor = null,
bold: ?BoldColor = null,
};
/// Returns the fg color for a cell with this style given the palette
@@ -155,7 +161,7 @@ pub const Style = struct {
if (self.flags.bold) {
if (opts.bold) |bold| switch (bold) {
.bright => {},
.color => |v| break :default v.toTerminalRGB(),
.color => |v| break :default v,
};
}
@@ -178,7 +184,7 @@ pub const Style = struct {
.rgb => |rgb| rgb: {
if (self.flags.bold and rgb.eql(opts.default)) {
if (opts.bold) |bold| switch (bold) {
.color => |v| break :rgb v.toTerminalRGB(),
.color => |v| break :rgb v,
.bright => {},
};
}