mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-06 07:38:21 +00:00
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:
@@ -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;
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 => {},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user