vt: expose terminal default colors via C API

Add set/get support for foreground, background, cursor, and palette
default colors through ghostty_terminal_set and ghostty_terminal_get.

Four new set options (COLOR_FOREGROUND, COLOR_BACKGROUND, COLOR_CURSOR,
COLOR_PALETTE) write directly to the terminal color defaults. Passing
NULL clears the value for RGB colors or resets the palette to the
built-in default. All set operations mark the palette dirty flag for
the renderer.

Eight new get data types retrieve either the effective color (override
or default, via DynamicRGB.get) or the default color only (ignoring
any OSC overrides). Effective getters for RGB colors return the new
NO_VALUE result code when no color is configured. The palette getters
return the current or original palette respectively.

Adds the GHOSTTY_NO_VALUE result code for cases where a queried value
is simply not configured, distinct from GHOSTTY_INVALID_VALUE which
indicates a caller error.
This commit is contained in:
Mitchell Hashimoto
2026-03-26 09:37:55 -07:00
parent 0752320d3b
commit 945920a186
11 changed files with 674 additions and 7 deletions

View File

@@ -0,0 +1,18 @@
# Example: `ghostty-vt` Terminal Colors
This contains a simple example of how to set default terminal colors,
read effective and default color values, and observe how OSC overrides
layer on top of defaults using the `ghostty-vt` C library.
This uses a `build.zig` and `Zig` to build the C program so that we
can reuse a lot of our build logic and depend directly on our source
tree, but Ghostty emits a standard C library that can be used with any
C tooling.
## Usage
Run the program:
```shell-session
zig build run
```

View File

@@ -0,0 +1,42 @@
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const run_step = b.step("run", "Run the app");
const exe_mod = b.createModule(.{
.target = target,
.optimize = optimize,
});
exe_mod.addCSourceFiles(.{
.root = b.path("src"),
.files = &.{"main.c"},
});
// You'll want to use a lazy dependency here so that ghostty is only
// downloaded if you actually need it.
if (b.lazyDependency("ghostty", .{
// Setting simd to false will force a pure static build that
// doesn't even require libc, but it has a significant performance
// penalty. If your embedding app requires libc anyway, you should
// always keep simd enabled.
// .simd = false,
})) |dep| {
exe_mod.linkLibrary(dep.artifact("ghostty-vt"));
}
// Exe
const exe = b.addExecutable(.{
.name = "c_vt_colors",
.root_module = exe_mod,
});
b.installArtifact(exe);
// Run
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| run_cmd.addArgs(args);
run_step.dependOn(&run_cmd.step);
}

View File

@@ -0,0 +1,24 @@
.{
.name = .c_vt_colors,
.version = "0.0.0",
.fingerprint = 0xe7ec4247f16d4fce,
.minimum_zig_version = "0.15.1",
.dependencies = .{
// Ghostty dependency. In reality, you'd probably use a URL-based
// dependency like the one showed (and commented out) below this one.
// We use a path dependency here for simplicity and to ensure our
// examples always test against the source they're bundled with.
.ghostty = .{ .path = "../../" },
// Example of what a URL-based dependency looks like:
// .ghostty = .{
// .url = "https://github.com/ghostty-org/ghostty/archive/COMMIT.tar.gz",
// .hash = "N-V-__8AAMVLTABmYkLqhZPLXnMl-KyN38R8UVYqGrxqO36s",
// },
},
.paths = .{
"build.zig",
"build.zig.zon",
"src",
},
}

View File

@@ -0,0 +1,121 @@
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <ghostty/vt.h>
//! [colors-set-defaults]
/// Set up a dark color theme with custom palette entries.
void set_color_theme(GhosttyTerminal terminal) {
// Set default foreground (light gray) and background (dark)
GhosttyColorRgb fg = { .r = 0xDD, .g = 0xDD, .b = 0xDD };
GhosttyColorRgb bg = { .r = 0x1E, .g = 0x1E, .b = 0x2E };
GhosttyColorRgb cursor = { .r = 0xF5, .g = 0xE0, .b = 0xDC };
ghostty_terminal_set(terminal, GHOSTTY_TERMINAL_OPT_COLOR_FOREGROUND, &fg);
ghostty_terminal_set(terminal, GHOSTTY_TERMINAL_OPT_COLOR_BACKGROUND, &bg);
ghostty_terminal_set(terminal, GHOSTTY_TERMINAL_OPT_COLOR_CURSOR, &cursor);
// Set a custom palette — start from the built-in default and override
// the first 8 entries with a custom dark theme.
GhosttyColorRgb palette[256];
ghostty_terminal_get(terminal, GHOSTTY_TERMINAL_DATA_COLOR_PALETTE, palette);
palette[GHOSTTY_COLOR_NAMED_BLACK] = (GhosttyColorRgb){ 0x45, 0x47, 0x5A };
palette[GHOSTTY_COLOR_NAMED_RED] = (GhosttyColorRgb){ 0xF3, 0x8B, 0xA8 };
palette[GHOSTTY_COLOR_NAMED_GREEN] = (GhosttyColorRgb){ 0xA6, 0xE3, 0xA1 };
palette[GHOSTTY_COLOR_NAMED_YELLOW] = (GhosttyColorRgb){ 0xF9, 0xE2, 0xAF };
palette[GHOSTTY_COLOR_NAMED_BLUE] = (GhosttyColorRgb){ 0x89, 0xB4, 0xFA };
palette[GHOSTTY_COLOR_NAMED_MAGENTA] = (GhosttyColorRgb){ 0xF5, 0xC2, 0xE7 };
palette[GHOSTTY_COLOR_NAMED_CYAN] = (GhosttyColorRgb){ 0x94, 0xE2, 0xD5 };
palette[GHOSTTY_COLOR_NAMED_WHITE] = (GhosttyColorRgb){ 0xBA, 0xC2, 0xDE };
ghostty_terminal_set(terminal, GHOSTTY_TERMINAL_OPT_COLOR_PALETTE, palette);
}
//! [colors-set-defaults]
//! [colors-read]
/// Print the effective and default values for a color, showing how
/// OSC overrides layer on top of defaults.
void print_color(GhosttyTerminal terminal,
const char* name,
GhosttyTerminalData effective_data,
GhosttyTerminalData default_data) {
GhosttyColorRgb color;
GhosttyResult res = ghostty_terminal_get(terminal, effective_data, &color);
if (res == GHOSTTY_SUCCESS) {
printf(" %-12s effective: #%02X%02X%02X", name, color.r, color.g, color.b);
} else {
printf(" %-12s effective: (not set)", name);
}
res = ghostty_terminal_get(terminal, default_data, &color);
if (res == GHOSTTY_SUCCESS) {
printf(" default: #%02X%02X%02X\n", color.r, color.g, color.b);
} else {
printf(" default: (not set)\n");
}
}
void print_all_colors(GhosttyTerminal terminal, const char* label) {
printf("%s:\n", label);
print_color(terminal, "foreground",
GHOSTTY_TERMINAL_DATA_COLOR_FOREGROUND,
GHOSTTY_TERMINAL_DATA_COLOR_FOREGROUND_DEFAULT);
print_color(terminal, "background",
GHOSTTY_TERMINAL_DATA_COLOR_BACKGROUND,
GHOSTTY_TERMINAL_DATA_COLOR_BACKGROUND_DEFAULT);
print_color(terminal, "cursor",
GHOSTTY_TERMINAL_DATA_COLOR_CURSOR,
GHOSTTY_TERMINAL_DATA_COLOR_CURSOR_DEFAULT);
// Show palette index 0 (black) as an example
GhosttyColorRgb palette[256];
ghostty_terminal_get(terminal, GHOSTTY_TERMINAL_DATA_COLOR_PALETTE, palette);
printf(" %-12s effective: #%02X%02X%02X", "palette[0]",
palette[0].r, palette[0].g, palette[0].b);
ghostty_terminal_get(terminal, GHOSTTY_TERMINAL_DATA_COLOR_PALETTE_DEFAULT,
palette);
printf(" default: #%02X%02X%02X\n", palette[0].r, palette[0].g, palette[0].b);
}
//! [colors-read]
//! [colors-main]
int main() {
// Create a terminal
GhosttyTerminal terminal = NULL;
GhosttyTerminalOptions opts = {
.cols = 80,
.rows = 24,
.max_scrollback = 0,
};
if (ghostty_terminal_new(NULL, &terminal, opts) != GHOSTTY_SUCCESS) {
fprintf(stderr, "Failed to create terminal\n");
return 1;
}
// Before setting any colors, everything is unset
print_all_colors(terminal, "Before setting defaults");
// Set our color theme defaults
set_color_theme(terminal);
print_all_colors(terminal, "\nAfter setting defaults");
// Simulate an OSC override (e.g. a program running inside the
// terminal changes the foreground via OSC 10)
const char* osc_fg = "\x1B]10;rgb:FF/00/00\x1B\\";
ghostty_terminal_vt_write(terminal, (const uint8_t*)osc_fg,
strlen(osc_fg));
print_all_colors(terminal, "\nAfter OSC foreground override");
// Clear the foreground default — the OSC override is still active
ghostty_terminal_set(terminal, GHOSTTY_TERMINAL_OPT_COLOR_FOREGROUND, NULL);
print_all_colors(terminal, "\nAfter clearing foreground default");
ghostty_terminal_free(terminal);
return 0;
}
//! [colors-main]

View File

@@ -88,6 +88,65 @@ extern "C" {
* ### Registering effects and processing VT data
* @snippet c-vt-effects/src/main.c effects-register
*
* ## Color Theme
*
* The terminal maintains a set of colors used for rendering: a foreground
* color, a background color, a cursor color, and a 256-color palette. Each
* of these has two layers: a **default** value set by the embedder, and an
* **override** value that programs running in the terminal can set via OSC
* escape sequences (e.g. OSC 10/11/12 for foreground/background/cursor,
* OSC 4 for individual palette entries).
*
* ### Default Colors
*
* Use ghostty_terminal_set() with the color options to configure the
* default colors. These represent the theme or configuration chosen by
* the embedder. Passing `NULL` clears the default, leaving the color
* unset.
*
* | Option | Input Type | Description |
* |-----------------------------------------|-------------------------|--------------------------------------|
* | `GHOSTTY_TERMINAL_OPT_COLOR_FOREGROUND` | `GhosttyColorRgb*` | Default foreground color |
* | `GHOSTTY_TERMINAL_OPT_COLOR_BACKGROUND` | `GhosttyColorRgb*` | Default background color |
* | `GHOSTTY_TERMINAL_OPT_COLOR_CURSOR` | `GhosttyColorRgb*` | Default cursor color |
* | `GHOSTTY_TERMINAL_OPT_COLOR_PALETTE` | `GhosttyColorRgb[256]*` | Default 256-color palette |
*
* For the palette, passing `NULL` resets to the built-in default palette.
* The palette set operation preserves any per-index OSC overrides that
* programs have applied; only unmodified indices are updated.
*
* ### Reading colors
*
* Use ghostty_terminal_get() to read colors. There are two variants for
* each color: the **effective** value (which returns the OSC override if
* one is active, otherwise the default) and the **default** value (which
* ignores any OSC overrides).
*
* | Data | Output Type | Description |
* |---------------------------------------------------|-------------------------|------------------------------------------------|
* | `GHOSTTY_TERMINAL_DATA_COLOR_FOREGROUND` | `GhosttyColorRgb*` | Effective foreground (override or default) |
* | `GHOSTTY_TERMINAL_DATA_COLOR_BACKGROUND` | `GhosttyColorRgb*` | Effective background (override or default) |
* | `GHOSTTY_TERMINAL_DATA_COLOR_CURSOR` | `GhosttyColorRgb*` | Effective cursor (override or default) |
* | `GHOSTTY_TERMINAL_DATA_COLOR_PALETTE` | `GhosttyColorRgb[256]*` | Current palette (with any OSC overrides) |
* | `GHOSTTY_TERMINAL_DATA_COLOR_FOREGROUND_DEFAULT` | `GhosttyColorRgb*` | Default foreground only (ignores OSC override) |
* | `GHOSTTY_TERMINAL_DATA_COLOR_BACKGROUND_DEFAULT` | `GhosttyColorRgb*` | Default background only (ignores OSC override) |
* | `GHOSTTY_TERMINAL_DATA_COLOR_CURSOR_DEFAULT` | `GhosttyColorRgb*` | Default cursor only (ignores OSC override) |
* | `GHOSTTY_TERMINAL_DATA_COLOR_PALETTE_DEFAULT` | `GhosttyColorRgb[256]*` | Default palette only (ignores OSC overrides) |
*
* For foreground, background, and cursor colors, the getters return
* `GHOSTTY_NO_VALUE` if no color is configured (neither a default nor an
* OSC override). The palette getters always succeed since the palette
* always has a value (the built-in default if nothing else is set).
*
* ### Setting a color theme
* @snippet c-vt-colors/src/main.c colors-set-defaults
*
* ### Reading effective and default colors
* @snippet c-vt-colors/src/main.c colors-read
*
* ### Full example with OSC overrides
* @snippet c-vt-colors/src/main.c colors-main
*
* @{
*/
@@ -434,6 +493,43 @@ typedef enum {
* Input type: GhosttyString*
*/
GHOSTTY_TERMINAL_OPT_PWD = 10,
/**
* Set the default foreground color.
*
* A NULL value pointer clears the default (unset).
*
* Input type: GhosttyColorRgb*
*/
GHOSTTY_TERMINAL_OPT_COLOR_FOREGROUND = 11,
/**
* Set the default background color.
*
* A NULL value pointer clears the default (unset).
*
* Input type: GhosttyColorRgb*
*/
GHOSTTY_TERMINAL_OPT_COLOR_BACKGROUND = 12,
/**
* Set the default cursor color.
*
* A NULL value pointer clears the default (unset).
*
* Input type: GhosttyColorRgb*
*/
GHOSTTY_TERMINAL_OPT_COLOR_CURSOR = 13,
/**
* Set the default 256-color palette.
*
* The value must point to an array of exactly 256 GhosttyColorRgb values.
* A NULL value pointer resets to the built-in default palette.
*
* Input type: GhosttyColorRgb[256]*
*/
GHOSTTY_TERMINAL_OPT_COLOR_PALETTE = 14,
} GhosttyTerminalOption;
/**
@@ -588,6 +684,74 @@ typedef enum {
* Output type: uint32_t *
*/
GHOSTTY_TERMINAL_DATA_HEIGHT_PX = 17,
/**
* The effective foreground color (override or default).
*
* Returns GHOSTTY_NO_VALUE if no foreground color is set.
*
* Output type: GhosttyColorRgb *
*/
GHOSTTY_TERMINAL_DATA_COLOR_FOREGROUND = 18,
/**
* The effective background color (override or default).
*
* Returns GHOSTTY_NO_VALUE if no background color is set.
*
* Output type: GhosttyColorRgb *
*/
GHOSTTY_TERMINAL_DATA_COLOR_BACKGROUND = 19,
/**
* The effective cursor color (override or default).
*
* Returns GHOSTTY_NO_VALUE if no cursor color is set.
*
* Output type: GhosttyColorRgb *
*/
GHOSTTY_TERMINAL_DATA_COLOR_CURSOR = 20,
/**
* The current 256-color palette.
*
* Output type: GhosttyColorRgb[256] *
*/
GHOSTTY_TERMINAL_DATA_COLOR_PALETTE = 21,
/**
* The default foreground color (ignoring any OSC override).
*
* Returns GHOSTTY_NO_VALUE if no default foreground color is set.
*
* Output type: GhosttyColorRgb *
*/
GHOSTTY_TERMINAL_DATA_COLOR_FOREGROUND_DEFAULT = 22,
/**
* The default background color (ignoring any OSC override).
*
* Returns GHOSTTY_NO_VALUE if no default background color is set.
*
* Output type: GhosttyColorRgb *
*/
GHOSTTY_TERMINAL_DATA_COLOR_BACKGROUND_DEFAULT = 23,
/**
* The default cursor color (ignoring any OSC override).
*
* Returns GHOSTTY_NO_VALUE if no default cursor color is set.
*
* Output type: GhosttyColorRgb *
*/
GHOSTTY_TERMINAL_DATA_COLOR_CURSOR_DEFAULT = 24,
/**
* The default 256-color palette (ignoring any OSC overrides).
*
* Output type: GhosttyColorRgb[256] *
*/
GHOSTTY_TERMINAL_DATA_COLOR_PALETTE_DEFAULT = 25,
} GhosttyTerminalData;
/**

View File

@@ -22,6 +22,8 @@ typedef enum {
GHOSTTY_INVALID_VALUE = -2,
/** Operation failed because the provided buffer was too small */
GHOSTTY_OUT_OF_SPACE = -3,
/** The requested value has no value */
GHOSTTY_NO_VALUE = -4,
} GhosttyResult;
/**

View File

@@ -108,7 +108,7 @@ pub const Data = enum(c_int) {
.row_iterator => RowIterator,
.color_background, .color_foreground, .color_cursor => colorpkg.RGB.C,
.color_cursor_has_value => bool,
.color_palette => [256]colorpkg.RGB.C,
.color_palette => colorpkg.PaletteC,
.cursor_visual_style => CursorVisualStyle,
.cursor_visible, .cursor_blinking, .cursor_password_input => bool,
.cursor_viewport_has_value, .cursor_viewport_wide_tail => bool,
@@ -136,7 +136,7 @@ pub const Colors = extern struct {
foreground: colorpkg.RGB.C,
cursor: colorpkg.RGB.C,
cursor_has_value: bool,
palette: [256]colorpkg.RGB.C,
palette: colorpkg.PaletteC,
};
pub fn new(
@@ -231,11 +231,7 @@ fn getTyped(
out.* = cursor.cval();
},
.color_cursor_has_value => out.* = state.state.colors.cursor != null,
.color_palette => {
for (&out.*, state.state.colors.palette) |*dst, src| {
dst.* = src.cval();
}
},
.color_palette => out.* = colorpkg.paletteCval(&state.state.colors.palette),
.cursor_visual_style => out.* = CursorVisualStyle.fromCursorStyle(state.state.cursor.visual_style),
.cursor_visible => out.* = state.state.cursor.visible,
.cursor_blinking => out.* = state.state.cursor.blinking,

View File

@@ -4,4 +4,5 @@ pub const Result = enum(c_int) {
out_of_memory = -1,
invalid_value = -2,
out_of_space = -3,
no_value = -4,
};

View File

@@ -17,6 +17,7 @@ const cell_c = @import("cell.zig");
const row_c = @import("row.zig");
const grid_ref_c = @import("grid_ref.zig");
const style_c = @import("style.zig");
const color = @import("../color.zig");
const Result = @import("result.zig").Result;
const Handler = @import("../stream_terminal.zig").Handler;
@@ -299,6 +300,10 @@ pub const Option = enum(c_int) {
device_attributes = 8,
title = 9,
pwd = 10,
color_foreground = 11,
color_background = 12,
color_cursor = 13,
color_palette = 14,
/// Input type expected for setting the option.
pub fn InType(comptime self: Option) type {
@@ -313,6 +318,8 @@ pub const Option = enum(c_int) {
.title_changed => ?Effects.TitleChangedFn,
.size_cb => ?Effects.SizeFn,
.title, .pwd => ?*const lib.String,
.color_foreground, .color_background, .color_cursor => ?*const color.RGB.C,
.color_palette => ?*const color.PaletteC,
};
}
};
@@ -363,6 +370,24 @@ fn setTyped(
const str = if (value) |v| v.ptr[0..v.len] else "";
wrapper.terminal.setPwd(str) catch return .out_of_memory;
},
.color_foreground => {
wrapper.terminal.colors.foreground.default = if (value) |v| .fromC(v.*) else null;
wrapper.terminal.flags.dirty.palette = true;
},
.color_background => {
wrapper.terminal.colors.background.default = if (value) |v| .fromC(v.*) else null;
wrapper.terminal.flags.dirty.palette = true;
},
.color_cursor => {
wrapper.terminal.colors.cursor.default = if (value) |v| .fromC(v.*) else null;
wrapper.terminal.flags.dirty.palette = true;
},
.color_palette => {
wrapper.terminal.colors.palette.changeDefault(
if (value) |v| color.paletteZval(v) else color.default,
);
wrapper.terminal.flags.dirty.palette = true;
},
}
return .success;
}
@@ -477,6 +502,14 @@ pub const TerminalData = enum(c_int) {
scrollback_rows = 15,
width_px = 16,
height_px = 17,
color_foreground = 18,
color_background = 19,
color_cursor = 20,
color_palette = 21,
color_foreground_default = 22,
color_background_default = 23,
color_cursor_default = 24,
color_palette_default = 25,
/// Output type expected for querying the data of the given kind.
pub fn OutType(comptime self: TerminalData) type {
@@ -491,6 +524,14 @@ pub const TerminalData = enum(c_int) {
.title, .pwd => lib.String,
.total_rows, .scrollback_rows => usize,
.width_px, .height_px => u32,
.color_foreground,
.color_background,
.color_cursor,
.color_foreground_default,
.color_background_default,
.color_cursor_default,
=> color.RGB.C,
.color_palette, .color_palette_default => color.PaletteC,
};
}
};
@@ -551,6 +592,14 @@ fn getTyped(
.scrollback_rows => out.* = t.screens.active.pages.total_rows - t.rows,
.width_px => out.* = t.width_px,
.height_px => out.* = t.height_px,
.color_foreground => out.* = (t.colors.foreground.get() orelse return .no_value).cval(),
.color_background => out.* = (t.colors.background.get() orelse return .no_value).cval(),
.color_cursor => out.* = (t.colors.cursor.get() orelse return .no_value).cval(),
.color_foreground_default => out.* = (t.colors.foreground.default orelse return .no_value).cval(),
.color_background_default => out.* = (t.colors.background.default orelse return .no_value).cval(),
.color_cursor_default => out.* = (t.colors.cursor.default orelse return .no_value).cval(),
.color_palette => out.* = color.paletteCval(&t.colors.palette.current),
.color_palette_default => out.* = color.paletteCval(&t.colors.palette.original),
}
return .success;
@@ -2075,3 +2124,232 @@ test "grid_ref out of bounds" {
.value = .{ .active = .{ .x = 100, .y = 0 } },
}, &out_ref));
}
test "set and get color_foreground" {
var t: Terminal = null;
try testing.expectEqual(Result.success, new(
&lib.alloc.test_allocator,
&t,
.{
.cols = 80,
.rows = 24,
.max_scrollback = 0,
},
));
defer free(t);
// Initially unset
var rgb: color.RGB.C = undefined;
try testing.expectEqual(Result.no_value, get(t, .color_foreground, @ptrCast(&rgb)));
// Set a value
const fg: color.RGB.C = .{ .r = 0xAA, .g = 0xBB, .b = 0xCC };
try testing.expectEqual(Result.success, set(t, .color_foreground, @ptrCast(&fg)));
try testing.expectEqual(Result.success, get(t, .color_foreground, @ptrCast(&rgb)));
try testing.expectEqual(fg, rgb);
// Clear with null
try testing.expectEqual(Result.success, set(t, .color_foreground, null));
try testing.expectEqual(Result.no_value, get(t, .color_foreground, @ptrCast(&rgb)));
}
test "set and get color_background" {
var t: Terminal = null;
try testing.expectEqual(Result.success, new(
&lib.alloc.test_allocator,
&t,
.{
.cols = 80,
.rows = 24,
.max_scrollback = 0,
},
));
defer free(t);
var rgb: color.RGB.C = undefined;
try testing.expectEqual(Result.no_value, get(t, .color_background, @ptrCast(&rgb)));
const bg: color.RGB.C = .{ .r = 0x11, .g = 0x22, .b = 0x33 };
try testing.expectEqual(Result.success, set(t, .color_background, @ptrCast(&bg)));
try testing.expectEqual(Result.success, get(t, .color_background, @ptrCast(&rgb)));
try testing.expectEqual(bg, rgb);
try testing.expectEqual(Result.success, set(t, .color_background, null));
try testing.expectEqual(Result.no_value, get(t, .color_background, @ptrCast(&rgb)));
}
test "set and get color_cursor" {
var t: Terminal = null;
try testing.expectEqual(Result.success, new(
&lib.alloc.test_allocator,
&t,
.{
.cols = 80,
.rows = 24,
.max_scrollback = 0,
},
));
defer free(t);
var rgb: color.RGB.C = undefined;
try testing.expectEqual(Result.no_value, get(t, .color_cursor, @ptrCast(&rgb)));
const cur: color.RGB.C = .{ .r = 0xFF, .g = 0x00, .b = 0x88 };
try testing.expectEqual(Result.success, set(t, .color_cursor, @ptrCast(&cur)));
try testing.expectEqual(Result.success, get(t, .color_cursor, @ptrCast(&rgb)));
try testing.expectEqual(cur, rgb);
try testing.expectEqual(Result.success, set(t, .color_cursor, null));
try testing.expectEqual(Result.no_value, get(t, .color_cursor, @ptrCast(&rgb)));
}
test "set and get color_palette" {
var t: Terminal = null;
try testing.expectEqual(Result.success, new(
&lib.alloc.test_allocator,
&t,
.{
.cols = 80,
.rows = 24,
.max_scrollback = 0,
},
));
defer free(t);
// Get default palette
var palette: color.PaletteC = undefined;
try testing.expectEqual(Result.success, get(t, .color_palette, @ptrCast(&palette)));
try testing.expectEqual(color.default[0].cval(), palette[0]);
// Set custom palette
var custom: color.PaletteC = color.paletteCval(&color.default);
custom[0] = .{ .r = 0x12, .g = 0x34, .b = 0x56 };
try testing.expectEqual(Result.success, set(t, .color_palette, @ptrCast(&custom)));
try testing.expectEqual(Result.success, get(t, .color_palette, @ptrCast(&palette)));
try testing.expectEqual(custom[0], palette[0]);
// Reset with null restores default
try testing.expectEqual(Result.success, set(t, .color_palette, null));
try testing.expectEqual(Result.success, get(t, .color_palette, @ptrCast(&palette)));
try testing.expectEqual(color.default[0].cval(), palette[0]);
}
test "get color default vs effective with override" {
var t: Terminal = null;
try testing.expectEqual(Result.success, new(
&lib.alloc.test_allocator,
&t,
.{
.cols = 80,
.rows = 24,
.max_scrollback = 0,
},
));
defer free(t);
const zt = t.?.terminal;
var rgb: color.RGB.C = undefined;
// Set defaults
const fg: color.RGB.C = .{ .r = 0xAA, .g = 0xBB, .b = 0xCC };
const bg: color.RGB.C = .{ .r = 0x11, .g = 0x22, .b = 0x33 };
const cur: color.RGB.C = .{ .r = 0xFF, .g = 0x00, .b = 0x88 };
try testing.expectEqual(Result.success, set(t, .color_foreground, @ptrCast(&fg)));
try testing.expectEqual(Result.success, set(t, .color_background, @ptrCast(&bg)));
try testing.expectEqual(Result.success, set(t, .color_cursor, @ptrCast(&cur)));
// Simulate OSC overrides
const override: color.RGB = .{ .r = 0x00, .g = 0x00, .b = 0x00 };
zt.colors.foreground.override = override;
zt.colors.background.override = override;
zt.colors.cursor.override = override;
// Effective returns override
try testing.expectEqual(Result.success, get(t, .color_foreground, @ptrCast(&rgb)));
try testing.expectEqual(override.cval(), rgb);
try testing.expectEqual(Result.success, get(t, .color_background, @ptrCast(&rgb)));
try testing.expectEqual(override.cval(), rgb);
try testing.expectEqual(Result.success, get(t, .color_cursor, @ptrCast(&rgb)));
try testing.expectEqual(override.cval(), rgb);
// Default returns original
try testing.expectEqual(Result.success, get(t, .color_foreground_default, @ptrCast(&rgb)));
try testing.expectEqual(fg, rgb);
try testing.expectEqual(Result.success, get(t, .color_background_default, @ptrCast(&rgb)));
try testing.expectEqual(bg, rgb);
try testing.expectEqual(Result.success, get(t, .color_cursor_default, @ptrCast(&rgb)));
try testing.expectEqual(cur, rgb);
}
test "get color default returns no_value when unset" {
var t: Terminal = null;
try testing.expectEqual(Result.success, new(
&lib.alloc.test_allocator,
&t,
.{
.cols = 80,
.rows = 24,
.max_scrollback = 0,
},
));
defer free(t);
var rgb: color.RGB.C = undefined;
try testing.expectEqual(Result.no_value, get(t, .color_foreground_default, @ptrCast(&rgb)));
try testing.expectEqual(Result.no_value, get(t, .color_background_default, @ptrCast(&rgb)));
try testing.expectEqual(Result.no_value, get(t, .color_cursor_default, @ptrCast(&rgb)));
}
test "get color_palette_default vs current" {
var t: Terminal = null;
try testing.expectEqual(Result.success, new(
&lib.alloc.test_allocator,
&t,
.{
.cols = 80,
.rows = 24,
.max_scrollback = 0,
},
));
defer free(t);
const zt = t.?.terminal;
// Set a custom default palette
var custom: color.PaletteC = color.paletteCval(&color.default);
custom[0] = .{ .r = 0x12, .g = 0x34, .b = 0x56 };
try testing.expectEqual(Result.success, set(t, .color_palette, @ptrCast(&custom)));
// Simulate OSC override on index 0
zt.colors.palette.set(0, .{ .r = 0xFF, .g = 0xFF, .b = 0xFF });
// Current palette returns the override
var palette: color.PaletteC = undefined;
try testing.expectEqual(Result.success, get(t, .color_palette, @ptrCast(&palette)));
try testing.expectEqual(color.RGB.C{ .r = 0xFF, .g = 0xFF, .b = 0xFF }, palette[0]);
// Default palette returns the original
try testing.expectEqual(Result.success, get(t, .color_palette_default, @ptrCast(&palette)));
try testing.expectEqual(custom[0], palette[0]);
}
test "set color sets dirty flag" {
var t: Terminal = null;
try testing.expectEqual(Result.success, new(
&lib.alloc.test_allocator,
&t,
.{
.cols = 80,
.rows = 24,
.max_scrollback = 0,
},
));
defer free(t);
const zt = t.?.terminal;
zt.flags.dirty.palette = false;
const fg: color.RGB.C = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF };
try testing.expectEqual(Result.success, set(t, .color_foreground, @ptrCast(&fg)));
try testing.expect(zt.flags.dirty.palette);
}

View File

@@ -47,6 +47,23 @@ pub const default: Palette = default: {
/// Palette is the 256 color palette.
pub const Palette = [256]RGB;
/// C-compatible palette type using the extern RGB struct.
pub const PaletteC = [256]RGB.C;
/// Convert a Palette to a PaletteC.
pub fn paletteCval(palette: *const Palette) PaletteC {
var result: PaletteC = undefined;
for (&result, palette) |*dst, src| dst.* = src.cval();
return result;
}
/// Convert a PaletteC to a Palette.
pub fn paletteZval(palette: *const PaletteC) Palette {
var result: Palette = undefined;
for (&result, palette) |*dst, src| dst.* = .fromC(src);
return result;
}
/// Mask that can be used to set which palette indexes were set.
pub const PaletteMask = std.StaticBitSet(@typeInfo(Palette).array.len);
@@ -408,6 +425,10 @@ pub const RGB = packed struct(u24) {
b: u8,
};
pub fn fromC(c: C) RGB {
return .{ .r = c.r, .g = c.g, .b = c.b };
}
pub fn cval(self: RGB) C {
return .{
.r = self.r,

0
test_align Executable file
View File