Skip to content

Instantly share code, notes, and snippets.

@moderation
Created August 27, 2026 21:41
Show Gist options
  • Select an option

  • Save moderation/68b06f35c19aed738a4b30b80192ea93 to your computer and use it in GitHub Desktop.

Select an option

Save moderation/68b06f35c19aed738a4b30b80192ea93 to your computer and use it in GitHub Desktop.
Implement ghostty's cursor background color option for monstar. Written by Grok 4.6 (high)
diff --git a/README.md b/README.md
index ae1a3de..fe14fb0 100644
--- a/README.md
+++ b/README.md
@@ -135,6 +135,8 @@ Other settings:
`0` disables the flash (default `200`).
- **background / foreground / cursor-color / cursor-text** — Terminal colors
as `#RRGGBB` or `RRGGBB`; explicit colors override the theme.
+ `cursor-color` and `cursor-text` also accept `cell-foreground` and
+ `cell-background` to match the cursor cell at runtime.
- **selection-background / selection-foreground** — Selection colors.
- **copy-highlight / copy-highlight-foreground** — Post-copy flash colors.
- **palette** — Override one palette entry by index `0`–`255`; repeat the key
diff --git a/dist/monstar.5 b/dist/monstar.5
index 0eb4711..e270708 100644
--- a/dist/monstar.5
+++ b/dist/monstar.5
@@ -219,6 +219,14 @@ For example,
and
.B ffe629
are equivalent.
+.B cursor-color
+and
+.B cursor-text
+also accept
+.B cell-foreground
+and
+.B cell-background
+to match the cursor cell's colors at runtime.
.PP
An explicit color in the main configuration overrides the named theme.
Runtime OSC color changes may override terminal, cursor, and selection colors
@@ -246,6 +254,14 @@ in both color schemes.
.TP
.BI cursor-color " = color"
Set the cursor color.
+A direct color uses six hexadecimal RGB digits, with an optional leading
+.BR # .
+The special values
+.B cell-foreground
+and
+.B cell-background
+match the cursor cell's foreground or background, after reverse video.
+An OSC 12 cursor color from the application overrides this until it is reset.
Built-in defaults are
.B #242424
in light mode and
@@ -254,7 +270,13 @@ in dark mode.
.TP
.BI cursor-text " = color"
Set text beneath a focused block cursor.
+Accepts the same values as
+.BR cursor-color .
When unset, the terminal background is used.
+The usual pairing with
+.B cursor-color = cell-foreground
+is
+.BR "cursor-text = cell-background" .
.TP
.BI foreground " = color"
Set the default terminal foreground.
diff --git a/src/App.zig b/src/App.zig
index 1ab47d5..8264baf 100644
--- a/src/App.zig
+++ b/src/App.zig
@@ -117,8 +117,10 @@ selection_fg_override: ?vt.color.RGB,
copy_highlight: vt.color.RGB,
copy_highlight_fg: vt.color.RGB,
copy_highlight_active: bool,
+/// Configured cursor fill when OSC 12 has not set a VT cursor color.
+cursor_color: Config.TerminalColor,
/// Configured text color beneath a focused block cursor.
-cursor_text: ?vt.color.RGB,
+cursor_text: ?Config.TerminalColor,
window: *Window,
keyboard: Keyboard,
/// Terminal contents changed since the last committed frame.
@@ -646,6 +648,7 @@ pub fn init(
.copy_highlight = config.effectiveCopyHighlight(.dark),
.copy_highlight_fg = config.effectiveCopyHighlightForeground(.dark),
.copy_highlight_active = false,
+ .cursor_color = config.effectiveCursorColor(.dark),
.cursor_text = config.effectiveCursorText(.dark),
.window = window,
.keyboard = try .init(),
@@ -2086,6 +2089,7 @@ fn applyColorDefaultsForConfig(self: *App, config: Config) void {
);
self.copy_highlight = config.effectiveCopyHighlight(self.color_scheme);
self.copy_highlight_fg = config.effectiveCopyHighlightForeground(self.color_scheme);
+ self.cursor_color = config.effectiveCursorColor(self.color_scheme);
self.cursor_text = config.effectiveCursorText(self.color_scheme);
}
@@ -4994,6 +4998,7 @@ fn startAsyncRender(self: *App) !AsyncRenderStart {
self.font.discovery(),
selection_bg,
selection_fg,
+ self.cursor_color,
self.cursor_text,
self.config.background_opacity,
self.config.background_opacity_cells,
@@ -5002,6 +5007,7 @@ fn startAsyncRender(self: *App) !AsyncRenderStart {
self.font.discovery(),
selection_bg,
selection_fg,
+ self.cursor_color,
self.cursor_text,
self.config.background_opacity,
self.config.background_opacity_cells,
@@ -5279,6 +5285,7 @@ fn startAsyncRasterLoad(self: *App) void {
self.font.discovery(),
self.selectionBackgroundForRender(),
self.selectionForegroundForRender(),
+ self.cursor_color,
self.cursor_text,
self.config.background_opacity,
self.config.background_opacity_cells,
@@ -5310,6 +5317,7 @@ fn finishAsyncRasterLoad(self: *App) void {
self.font.discovery(),
self.selectionBackgroundForRender(),
self.selectionForegroundForRender(),
+ self.cursor_color,
self.cursor_text,
self.config.background_opacity,
self.config.background_opacity_cells,
diff --git a/src/AsyncRaster.zig b/src/AsyncRaster.zig
index 8bc71cb..153bd5e 100644
--- a/src/AsyncRaster.zig
+++ b/src/AsyncRaster.zig
@@ -13,6 +13,7 @@ const std = @import("std");
const linux = std.os.linux;
const posix = std.posix;
const vt = @import("ghostty-vt");
+const Config = @import("Config.zig");
const Font = @import("Font.zig");
const Renderer = @import("Renderer.zig");
@@ -112,7 +113,8 @@ pub const Loader = struct {
discovery: *Font.Discovery,
selection_background: vt.color.RGB,
selection_foreground: ?vt.color.RGB,
- cursor_text: ?vt.color.RGB,
+ cursor_color: ?Config.TerminalColor,
+ cursor_text: ?Config.TerminalColor,
background_alpha: u8,
background_alpha_cells: bool,
state: *vt.RenderState,
@@ -128,7 +130,8 @@ pub const Loader = struct {
discovery: *Font.Discovery,
selection_background: vt.color.RGB,
selection_foreground: ?vt.color.RGB,
- cursor_text: ?vt.color.RGB,
+ cursor_color: ?Config.TerminalColor,
+ cursor_text: ?Config.TerminalColor,
background_alpha: u8,
background_alpha_cells: bool,
state: *vt.RenderState,
@@ -140,6 +143,7 @@ pub const Loader = struct {
.discovery = discovery,
.selection_background = selection_background,
.selection_foreground = selection_foreground,
+ .cursor_color = cursor_color,
.cursor_text = cursor_text,
.background_alpha = background_alpha,
.background_alpha_cells = background_alpha_cells,
@@ -191,6 +195,7 @@ pub const Loader = struct {
self.discovery,
self.selection_background,
self.selection_foreground,
+ self.cursor_color,
self.cursor_text,
self.background_alpha,
self.background_alpha_cells,
@@ -234,7 +239,8 @@ pub fn init(
discovery: *Font.Discovery,
selection_background: vt.color.RGB,
selection_foreground: ?vt.color.RGB,
- cursor_text: ?vt.color.RGB,
+ cursor_color: ?Config.TerminalColor,
+ cursor_text: ?Config.TerminalColor,
background_alpha: u8,
background_alpha_cells: bool,
state: *vt.RenderState,
@@ -247,6 +253,7 @@ pub fn init(
var renderer: Renderer = try .init(alloc, font, .{
.selection_background = selection_background,
.selection_foreground = selection_foreground,
+ .cursor_color = cursor_color,
.cursor_text = cursor_text,
.background_alpha = background_alpha,
.background_alpha_cells = background_alpha_cells,
@@ -304,7 +311,8 @@ pub fn reconfigure(
discovery: *Font.Discovery,
selection_background: vt.color.RGB,
selection_foreground: ?vt.color.RGB,
- cursor_text: ?vt.color.RGB,
+ cursor_color: ?Config.TerminalColor,
+ cursor_text: ?Config.TerminalColor,
background_alpha: u8,
background_alpha_cells: bool,
) !void {
@@ -318,6 +326,7 @@ pub fn reconfigure(
var renderer: Renderer = try .init(self.alloc, font, .{
.selection_background = selection_background,
.selection_foreground = selection_foreground,
+ .cursor_color = cursor_color,
.cursor_text = cursor_text,
.background_alpha = background_alpha,
.background_alpha_cells = background_alpha_cells,
@@ -344,7 +353,8 @@ pub fn configuredFor(
discovery: *Font.Discovery,
selection_background: vt.color.RGB,
selection_foreground: ?vt.color.RGB,
- cursor_text: ?vt.color.RGB,
+ cursor_color: ?Config.TerminalColor,
+ cursor_text: ?Config.TerminalColor,
background_alpha: u8,
background_alpha_cells: bool,
) bool {
@@ -353,7 +363,8 @@ pub fn configuredFor(
return self.font.discovery() == discovery and
self.renderer.selection_bg.eql(selection_background) and
optionalRgbEql(self.renderer.selection_fg, selection_foreground) and
- optionalRgbEql(self.renderer.cursor_text, cursor_text) and
+ optionalTerminalColorEql(self.renderer.cursor_color, cursor_color) and
+ optionalTerminalColorEql(self.renderer.cursor_text, cursor_text) and
self.renderer.background_alpha == background_alpha and
self.renderer.background_alpha_cells == background_alpha_cells;
}
@@ -406,6 +417,11 @@ fn optionalRgbEql(a: ?vt.color.RGB, b: ?vt.color.RGB) bool {
return a.?.eql(b.?);
}
+fn optionalTerminalColorEql(a: ?Config.TerminalColor, b: ?Config.TerminalColor) bool {
+ if (a == null or b == null) return a == null and b == null;
+ return a.?.eql(b.?);
+}
+
fn drainEventfd(self: *AsyncRaster) void {
drainEventFd(self.complete_fd);
}
@@ -671,6 +687,7 @@ test "unchanged dirty rows report no damage" {
.{ .r = 1, .g = 2, .b = 3 },
null,
null,
+ null,
255,
false,
&state,
diff --git a/src/Config.zig b/src/Config.zig
index 3700231..83deba2 100644
--- a/src/Config.zig
+++ b/src/Config.zig
@@ -37,6 +37,7 @@ pub const MouseScrollMultiplier = struct {
pub const Theme = config_theme.Theme;
pub const ThemeColors = config_theme.ThemeColors;
+pub const TerminalColor = config_theme.TerminalColor;
pub const light_theme = config_theme.light_theme;
pub const dark_theme = config_theme.dark_theme;
const ThemeOverrides = config_theme.ThemeOverrides;
@@ -103,8 +104,8 @@ light_theme_overrides: ?ThemeOverrides = null,
dark_theme_overrides: ?ThemeOverrides = null,
background: ?vt.color.RGB = null,
foreground: ?vt.color.RGB = null,
-cursor_color: ?vt.color.RGB = null,
-cursor_text: ?vt.color.RGB = null,
+cursor_color: ?TerminalColor = null,
+cursor_text: ?TerminalColor = null,
selection_background: ?vt.color.RGB = null,
selection_foreground: ?vt.color.RGB = null,
copy_highlight: ?vt.color.RGB = null,
@@ -253,9 +254,9 @@ pub fn set(self: *Config, arena: std.mem.Allocator, key: []const u8, value: []co
} else if (std.mem.eql(u8, key, "foreground")) {
self.foreground = try config_theme.parseColor(value);
} else if (std.mem.eql(u8, key, "cursor-color")) {
- self.cursor_color = try config_theme.parseColor(value);
+ self.cursor_color = try config_theme.parseTerminalColor(value);
} else if (std.mem.eql(u8, key, "cursor-text")) {
- self.cursor_text = try config_theme.parseColor(value);
+ self.cursor_text = try config_theme.parseTerminalColor(value);
} else if (std.mem.eql(u8, key, "selection-background")) {
self.selection_background = try config_theme.parseColor(value);
} else if (std.mem.eql(u8, key, "selection-foreground")) {
@@ -424,7 +425,16 @@ pub fn effectiveCopyHighlightForeground(self: *const Config, color_scheme: vt.de
return self.effectiveColor(color_scheme, "copy_highlight_foreground");
}
-pub fn effectiveCursorText(self: *const Config, color_scheme: vt.device_status.ColorScheme) ?vt.color.RGB {
+pub fn effectiveCursorColor(self: *const Config, color_scheme: vt.device_status.ColorScheme) TerminalColor {
+ const named = if (self.namedThemeOverrides(color_scheme)) |theme| theme.cursor_color else null;
+ return config_theme.resolveTerminalColor(
+ self.cursor_color,
+ named,
+ colorsForScheme(color_scheme).cursor_color,
+ );
+}
+
+pub fn effectiveCursorText(self: *const Config, color_scheme: vt.device_status.ColorScheme) ?TerminalColor {
const named = if (self.namedThemeOverrides(color_scheme)) |theme| theme.cursor_text else null;
return self.cursor_text orelse named;
}
@@ -441,7 +451,10 @@ pub fn terminalColors(self: *const Config, color_scheme: vt.device_status.ColorS
return .{
.background = .init(self.effectiveColor(color_scheme, "background")),
.foreground = .init(self.effectiveColor(color_scheme, "foreground")),
- .cursor = .init(self.effectiveColor(color_scheme, "cursor_color")),
+ .cursor = if (self.effectiveCursorColor(color_scheme).toRgb()) |rgb|
+ .init(rgb)
+ else
+ .unset,
.palette = .init(palette),
};
}
@@ -489,8 +502,8 @@ test "defaults" {
try std.testing.expectEqual(@as(?ThemeOverrides, null), config.dark_theme_overrides);
try std.testing.expectEqual(@as(?vt.color.RGB, null), config.background);
try std.testing.expectEqual(@as(?vt.color.RGB, null), config.foreground);
- try std.testing.expectEqual(@as(?vt.color.RGB, null), config.cursor_color);
- try std.testing.expectEqual(@as(?vt.color.RGB, null), config.cursor_text);
+ try std.testing.expectEqual(@as(?TerminalColor, null), config.cursor_color);
+ try std.testing.expectEqual(@as(?TerminalColor, null), config.cursor_text);
try std.testing.expectEqual(@as(?vt.color.RGB, null), config.selection_background);
try std.testing.expectEqual(@as(?vt.color.RGB, null), config.selection_foreground);
try std.testing.expectEqual(@as(?vt.color.RGB, null), config.copy_highlight);
@@ -560,8 +573,8 @@ test "parse config" {
try std.testing.expect(config.background_opacity_cells);
try std.testing.expectEqual(vt.color.RGB{ .r = 0x1a, .g = 0x1b, .b = 0x26 }, config.background.?);
try std.testing.expectEqual(vt.color.RGB{ .r = 0xc0, .g = 0xca, .b = 0xf5 }, config.foreground.?);
- try std.testing.expectEqual(vt.color.RGB{ .r = 0xaa, .g = 0xbb, .b = 0xcc }, config.cursor_color.?);
- try std.testing.expectEqual(vt.color.RGB{ .r = 1, .g = 2, .b = 3 }, config.cursor_text.?);
+ try std.testing.expectEqual(TerminalColor{ .rgb = .{ .r = 0xaa, .g = 0xbb, .b = 0xcc } }, config.cursor_color.?);
+ try std.testing.expectEqual(TerminalColor{ .rgb = .{ .r = 1, .g = 2, .b = 3 } }, config.cursor_text.?);
try std.testing.expectEqual(vt.color.RGB{ .r = 4, .g = 5, .b = 6 }, config.selection_background.?);
try std.testing.expectEqual(vt.color.RGB{ .r = 7, .g = 8, .b = 9 }, config.selection_foreground.?);
try std.testing.expectEqual(vt.color.RGB{ .r = 10, .g = 11, .b = 12 }, config.copy_highlight.?);
@@ -811,10 +824,10 @@ test "named themes follow color scheme and remain below explicit colors" {
try std.testing.expectEqual(vt.color.RGB{ .r = 0xdd, .g = 0xdd, .b = 0xdd }, config.effectiveSelectionBackground(.light));
try std.testing.expectEqual(vt.color.RGB{ .r = 0xff, .g = 0xe6, .b = 0x29 }, config.effectiveCopyHighlight(.light));
try std.testing.expectEqual(vt.color.RGB{ .r = 0x1c, .g = 0x20, .b = 0x24 }, config.effectiveCopyHighlightForeground(.light));
- try std.testing.expectEqual(vt.color.RGB{ .r = 0xfe, .g = 0xdc, .b = 0xba }, config.effectiveCursorText(.light).?);
+ try std.testing.expectEqual(TerminalColor{ .rgb = .{ .r = 0xfe, .g = 0xdc, .b = 0xba } }, config.effectiveCursorText(.light).?);
- config.cursor_text = .{ .r = 7, .g = 8, .b = 9 };
- try std.testing.expectEqual(vt.color.RGB{ .r = 7, .g = 8, .b = 9 }, config.effectiveCursorText(.light).?);
+ config.cursor_text = .{ .rgb = .{ .r = 7, .g = 8, .b = 9 } };
+ try std.testing.expectEqual(TerminalColor{ .rgb = .{ .r = 7, .g = 8, .b = 9 } }, config.effectiveCursorText(.light).?);
const dark = config.terminalColors(.dark);
try std.testing.expectEqual(vt.color.RGB{ .r = 0xf0, .g = 0xf0, .b = 0xf0 }, dark.foreground.get().?);
@@ -822,6 +835,44 @@ test "named themes follow color scheme and remain below explicit colors" {
try std.testing.expectEqual(vt.color.RGB{ .r = 0xe0, .g = 0xe0, .b = 0xe0 }, config.effectiveSelectionForeground(.dark));
}
+test "cell cursor colors stay out of the VT default layer" {
+ var arena_state: std.heap.ArenaAllocator = .init(std.testing.allocator);
+ defer arena_state.deinit();
+ const config = parse(arena_state.allocator(),
+ \\cursor-color = cell-foreground
+ \\cursor-text = cell-background
+ \\cursor-color = not-a-color
+ );
+ try std.testing.expectEqual(@as(TerminalColor, .cell_foreground), config.cursor_color.?);
+ try std.testing.expectEqual(@as(TerminalColor, .cell_background), config.cursor_text.?);
+ try std.testing.expectEqual(@as(TerminalColor, .cell_foreground), config.effectiveCursorColor(.dark));
+ try std.testing.expectEqual(@as(TerminalColor, .cell_background), config.effectiveCursorText(.dark).?);
+ try std.testing.expectEqual(@as(?vt.color.RGB, null), config.terminalColors(.dark).cursor.get());
+ try std.testing.expectEqual(@as(?vt.color.RGB, null), config.terminalColors(.light).cursor.get());
+}
+
+test "named theme cell cursor colors stay below explicit colors" {
+ var config: Config = .{};
+ config.light_theme_overrides = config_theme.parseOverrides(
+ \\cursor-color = cell-foreground
+ \\cursor-text = cell-background
+ );
+ try std.testing.expectEqual(@as(TerminalColor, .cell_foreground), config.effectiveCursorColor(.light));
+ try std.testing.expectEqual(@as(TerminalColor, .cell_background), config.effectiveCursorText(.light).?);
+ try std.testing.expectEqual(@as(?vt.color.RGB, null), config.terminalColors(.light).cursor.get());
+ try std.testing.expectEqual(
+ TerminalColor{ .rgb = dark_theme.cursor_color },
+ config.effectiveCursorColor(.dark),
+ );
+
+ config.cursor_color = .{ .rgb = .{ .r = 1, .g = 2, .b = 3 } };
+ try std.testing.expectEqual(
+ TerminalColor{ .rgb = .{ .r = 1, .g = 2, .b = 3 } },
+ config.effectiveCursorColor(.light),
+ );
+ try std.testing.expectEqual(vt.color.RGB{ .r = 1, .g = 2, .b = 3 }, config.terminalColors(.light).cursor.get().?);
+}
+
test "absolute theme file resolves" {
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
diff --git a/src/Renderer.zig b/src/Renderer.zig
index efb9715..24112ec 100644
--- a/src/Renderer.zig
+++ b/src/Renderer.zig
@@ -52,9 +52,12 @@ selection_fg: ?vt.color.RGB,
/// Highlight colors for the selected scrollback-search match.
search_bg: vt.color.RGB,
search_fg: vt.color.RGB,
+/// Configured cursor fill when OSC 12 has not set a VT cursor color.
+/// Null keeps the previous fallback of window foreground.
+cursor_color: ?Config.TerminalColor,
/// Explicit text color under a focused block cursor. Null preserves the
/// terminal background fallback.
-cursor_text: ?vt.color.RGB,
+cursor_text: ?Config.TerminalColor,
/// Alpha applied to the default terminal background and window padding.
background_alpha: u8,
/// Whether background alpha also applies to explicit terminal cell
@@ -129,7 +132,8 @@ const KittyScaleKey = struct {
pub const InitOptions = struct {
selection_background: ?vt.color.RGB = null,
selection_foreground: ?vt.color.RGB = null,
- cursor_text: ?vt.color.RGB = null,
+ cursor_color: ?Config.TerminalColor = null,
+ cursor_text: ?Config.TerminalColor = null,
background_alpha: u8 = 255,
background_alpha_cells: bool = false,
/// Benchmark escape hatch for comparing the superseded row path.
@@ -175,6 +179,7 @@ pub fn init(alloc: std.mem.Allocator, font: *Font, opts: InitOptions) !Renderer
.selection_fg = opts.selection_foreground,
.search_bg = Config.dark_theme.copy_highlight,
.search_fg = Config.dark_theme.copy_highlight_foreground,
+ .cursor_color = opts.cursor_color,
.cursor_text = opts.cursor_text,
.background_alpha = opts.background_alpha,
.background_alpha_cells = opts.background_alpha_cells,
@@ -1188,6 +1193,41 @@ fn renderRowCells(
try self.renderRowForegroundCells(state, cells, cell_range, y, pixels, width, height);
}
+/// Cell SGR colors after reverse video, used to resolve `cell-foreground`
+/// / `cell-background` cursor colors. Faint, selection, and search are
+/// not applied, matching Ghostty.
+fn cursorCellRgb(
+ style: vt.Style,
+ cell: anytype,
+ colors: *const vt.RenderState.Colors,
+) struct { fg: vt.color.RGB, bg: vt.color.RGB } {
+ const fg = style.fg(.{ .default = colors.foreground, .palette = &colors.palette });
+ const bg = style.bg(cell, &colors.palette) orelse colors.background;
+ if (style.flags.inverse) return .{ .fg = bg, .bg = fg };
+ return .{ .fg = fg, .bg = bg };
+}
+
+fn cursorFill(
+ self: *const Renderer,
+ colors: *const vt.RenderState.Colors,
+ cell_fg: vt.color.RGB,
+ cell_bg: vt.color.RGB,
+) vt.color.RGB {
+ if (colors.cursor) |color| return color;
+ if (self.cursor_color) |configured| return configured.resolve(cell_fg, cell_bg);
+ return colors.foreground;
+}
+
+fn cursorGlyph(
+ self: *const Renderer,
+ colors: *const vt.RenderState.Colors,
+ cell_fg: vt.color.RGB,
+ cell_bg: vt.color.RGB,
+) vt.color.RGB {
+ if (self.cursor_text) |configured| return configured.resolve(cell_fg, cell_bg);
+ return colors.background;
+}
+
/// Which cell backgrounds prepareRow paints. `.all` covers the entire
/// row rect (unstyled cells and the right margin get the default
/// background), so callers need no separate clear pass. `.styled`
@@ -1311,8 +1351,9 @@ fn prepareRowCells(
if (cursor_x != null and cursor_x.? == x and
state.cursor.visual_style == .block and self.focused)
{
- bg = colors.cursor orelse colors.foreground;
- fg = self.cursor_text orelse colors.background;
+ const cell = cursorCellRgb(style, &raws[x], colors);
+ bg = self.cursorFill(colors, cell.fg, cell.bg);
+ fg = self.cursorGlyph(colors, cell.fg, cell.bg);
reverse_color_glyph = false;
dim_search_bg = false;
background_uses_alpha = false;
@@ -1514,7 +1555,8 @@ fn renderRowForegroundCells(
.block_hollow => .cursor_hollow_rect,
};
if (kind) |k| {
- const color = colors.cursor orelse colors.foreground;
+ const cell = cursorCellRgb(state.cursor.style, &state.cursor.cell, colors);
+ const color = self.cursorFill(colors, cell.fg, cell.bg);
try self.blitDecoration(k, cx, y, argb(color), pixels, width, height);
}
}
@@ -2290,6 +2332,90 @@ test "cursor splits shaping runs" {
try std.testing.expectEqual(@as(usize, 3), renderer.text_shaper.readStats().cache_misses);
}
+test "cursor colors resolve from cells unless OSC 12 is set" {
+ const alloc = std.testing.allocator;
+ const window_bg: vt.color.RGB = .{ .r = 10, .g = 20, .b = 30 };
+ const window_fg: vt.color.RGB = .{ .r = 240, .g = 230, .b = 220 };
+ const cell_fg: vt.color.RGB = .{ .r = 200, .g = 10, .b = 20 };
+ const cell_bg: vt.color.RGB = .{ .r = 30, .g = 40, .b = 50 };
+ const osc_cursor: vt.color.RGB = .{ .r = 9, .g = 8, .b = 7 };
+
+ var term: vt.Terminal = try .init(std.testing.io, alloc, .{ .cols = 2, .rows = 1 });
+ defer term.deinit(alloc);
+ term.colors.background = .init(window_bg);
+ term.colors.foreground = .init(window_fg);
+ term.colors.cursor = .unset;
+
+ var stream = term.vtStream();
+ defer stream.deinit();
+ stream.nextSlice("\x1b[38;2;200;10;20;48;2;30;40;50m \x1b[1;1H");
+
+ var state: vt.RenderState = .empty;
+ defer state.deinit(alloc);
+ try state.update(alloc, &term);
+
+ var font: Font = try .init(alloc, "monospace", 16);
+ defer font.deinit(alloc);
+ var renderer: Renderer = try .init(alloc, &font, .{
+ .cursor_color = .cell_foreground,
+ .cursor_text = .cell_background,
+ });
+ defer renderer.deinit();
+
+ const width: u31 = font.cell_width * 2;
+ const height: u31 = font.cell_height;
+ const pixels = try alloc.alloc(u32, @as(usize, width) * height);
+ defer alloc.free(pixels);
+
+ const center = @as(usize, font.cell_height / 2) * width + font.cell_width / 2;
+ try renderer.render(&state, pixels, width, height);
+ try std.testing.expectEqual(argb(cell_fg), pixels[center]);
+
+ stream.nextSlice("\x1b[H\x1b[7;38;2;200;10;20;48;2;30;40;50m \x1b[1;1H");
+ try state.update(alloc, &term);
+ try renderer.render(&state, pixels, width, height);
+ try std.testing.expectEqual(argb(cell_bg), pixels[center]);
+
+ term.colors.cursor.set(osc_cursor);
+ try state.update(alloc, &term);
+ try renderer.render(&state, pixels, width, height);
+ try std.testing.expectEqual(argb(osc_cursor), pixels[center]);
+}
+
+test "cursor fill and glyph helpers follow TerminalColor" {
+ const alloc = std.testing.allocator;
+ const colors: vt.RenderState.Colors = .{
+ .background = .{ .r = 1, .g = 2, .b = 3 },
+ .foreground = .{ .r = 4, .g = 5, .b = 6 },
+ .cursor = null,
+ .palette = vt.color.default,
+ };
+ const fg: vt.color.RGB = .{ .r = 10, .g = 20, .b = 30 };
+ const bg: vt.color.RGB = .{ .r = 40, .g = 50, .b = 60 };
+
+ var font: Font = try .init(alloc, "monospace", 16);
+ defer font.deinit(alloc);
+ var renderer: Renderer = try .init(alloc, &font, .{});
+ defer renderer.deinit();
+
+ try std.testing.expectEqual(colors.foreground, renderer.cursorFill(&colors, fg, bg));
+ try std.testing.expectEqual(colors.background, renderer.cursorGlyph(&colors, fg, bg));
+
+ renderer.cursor_color = .cell_foreground;
+ renderer.cursor_text = .cell_background;
+ try std.testing.expectEqual(fg, renderer.cursorFill(&colors, fg, bg));
+ try std.testing.expectEqual(bg, renderer.cursorGlyph(&colors, fg, bg));
+
+ renderer.cursor_color = .cell_background;
+ renderer.cursor_text = .cell_foreground;
+ try std.testing.expectEqual(bg, renderer.cursorFill(&colors, fg, bg));
+ try std.testing.expectEqual(fg, renderer.cursorGlyph(&colors, fg, bg));
+
+ var osc_colors = colors;
+ osc_colors.cursor = .{ .r = 9, .g = 8, .b = 7 };
+ try std.testing.expectEqual(osc_colors.cursor.?, renderer.cursorFill(&osc_colors, fg, bg));
+}
+
test "background opacity cells controls explicit cell backgrounds" {
const alloc = std.testing.allocator;
diff --git a/src/config_theme.zig b/src/config_theme.zig
index f3acb73..d46094e 100644
--- a/src/config_theme.zig
+++ b/src/config_theme.zig
@@ -15,11 +15,51 @@ pub const Theme = struct {
dark: [:0]const u8,
};
+/// A configured color that can be a concrete RGB value or a runtime
+/// reference to the cell's foreground or background.
+pub const TerminalColor = union(enum) {
+ rgb: vt.color.RGB,
+ cell_foreground,
+ cell_background,
+
+ pub fn toRgb(self: TerminalColor) ?vt.color.RGB {
+ return switch (self) {
+ .rgb => |color| color,
+ .cell_foreground, .cell_background => null,
+ };
+ }
+
+ pub fn resolve(self: TerminalColor, cell_fg: vt.color.RGB, cell_bg: vt.color.RGB) vt.color.RGB {
+ return switch (self) {
+ .rgb => |color| color,
+ .cell_foreground => cell_fg,
+ .cell_background => cell_bg,
+ };
+ }
+
+ pub fn eql(self: TerminalColor, other: TerminalColor) bool {
+ return switch (self) {
+ .rgb => |color| switch (other) {
+ .rgb => |other_color| color.eql(other_color),
+ else => false,
+ },
+ .cell_foreground => switch (other) {
+ .cell_foreground => true,
+ else => false,
+ },
+ .cell_background => switch (other) {
+ .cell_background => true,
+ else => false,
+ },
+ };
+ }
+};
+
pub const ThemeOverrides = struct {
background: ?vt.color.RGB = null,
foreground: ?vt.color.RGB = null,
- cursor_color: ?vt.color.RGB = null,
- cursor_text: ?vt.color.RGB = null,
+ cursor_color: ?TerminalColor = null,
+ cursor_text: ?TerminalColor = null,
selection_background: ?vt.color.RGB = null,
selection_foreground: ?vt.color.RGB = null,
copy_highlight: ?vt.color.RGB = null,
@@ -160,6 +200,13 @@ pub fn parseColor(value: []const u8) error{InvalidValue}!vt.color.RGB {
};
}
+/// Hex color, `cell-foreground`, or `cell-background`.
+pub fn parseTerminalColor(value: []const u8) error{InvalidValue}!TerminalColor {
+ if (std.mem.eql(u8, value, "cell-foreground")) return .cell_foreground;
+ if (std.mem.eql(u8, value, "cell-background")) return .cell_background;
+ return .{ .rgb = try parseColor(value) };
+}
+
pub fn loadOverrides(
io: std.Io,
arena: std.mem.Allocator,
@@ -225,12 +272,12 @@ pub fn parseOverrides(text: []const u8) ThemeOverrides {
continue;
};
} else if (std.mem.eql(u8, key, "cursor-color")) {
- result.cursor_color = parseColor(value) catch {
+ result.cursor_color = parseTerminalColor(value) catch {
warn("theme line {d}: invalid cursor-color", .{line_no});
continue;
};
} else if (std.mem.eql(u8, key, "cursor-text")) {
- result.cursor_text = parseColor(value) catch {
+ result.cursor_text = parseTerminalColor(value) catch {
warn("theme line {d}: invalid cursor-text", .{line_no});
continue;
};
@@ -278,6 +325,14 @@ pub fn resolveColor(explicit: ?vt.color.RGB, named: ?vt.color.RGB, built_in: vt.
return explicit orelse named orelse built_in;
}
+pub fn resolveTerminalColor(
+ explicit: ?TerminalColor,
+ named: ?TerminalColor,
+ built_in: vt.color.RGB,
+) TerminalColor {
+ return explicit orelse named orelse .{ .rgb = built_in };
+}
+
pub fn resolvePalette(
explicit: *const [256]?vt.color.RGB,
named: ?*const ThemeOverrides,
@@ -314,3 +369,36 @@ fn readFile(arena: std.mem.Allocator, path: [:0]const u8) ?[]const u8 {
}
return buf.items;
}
+
+test "parse terminal colors" {
+ try std.testing.expectEqual(@as(TerminalColor, .cell_foreground), try parseTerminalColor("cell-foreground"));
+ try std.testing.expectEqual(@as(TerminalColor, .cell_background), try parseTerminalColor("cell-background"));
+ try std.testing.expectEqual(
+ TerminalColor{ .rgb = .{ .r = 0xaa, .g = 0xbb, .b = 0xcc } },
+ try parseTerminalColor("#aabbcc"),
+ );
+ try std.testing.expectError(error.InvalidValue, parseTerminalColor("cell-foo"));
+ try std.testing.expectError(error.InvalidValue, parseColor("cell-foreground"));
+}
+
+test "terminal color resolve and equality" {
+ const fg: vt.color.RGB = .{ .r = 1, .g = 2, .b = 3 };
+ const bg: vt.color.RGB = .{ .r = 4, .g = 5, .b = 6 };
+ const rgb: TerminalColor = .{ .rgb = fg };
+ try std.testing.expectEqual(fg, @as(TerminalColor, .cell_foreground).resolve(fg, bg));
+ try std.testing.expectEqual(bg, @as(TerminalColor, .cell_background).resolve(fg, bg));
+ try std.testing.expectEqual(fg, rgb.resolve(bg, bg));
+ try std.testing.expect(rgb.eql(.{ .rgb = fg }));
+ try std.testing.expect(!rgb.eql(.cell_foreground));
+ try std.testing.expect(@as(TerminalColor, .cell_foreground).eql(.cell_foreground));
+ try std.testing.expect(!@as(TerminalColor, .cell_foreground).eql(.cell_background));
+}
+
+test "theme overrides parse cell cursor colors" {
+ const theme = parseOverrides(
+ \\cursor-color = cell-foreground
+ \\cursor-text = cell-background
+ );
+ try std.testing.expectEqual(@as(TerminalColor, .cell_foreground), theme.cursor_color.?);
+ try std.testing.expectEqual(@as(TerminalColor, .cell_background), theme.cursor_text.?);
+}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment