From 945920a1863fc05079b331fdc2f914ad122cd81d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 26 Mar 2026 09:37:55 -0700 Subject: [PATCH] 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. --- example/c-vt-colors/README.md | 18 ++ example/c-vt-colors/build.zig | 42 +++++ example/c-vt-colors/build.zig.zon | 24 +++ example/c-vt-colors/src/main.c | 121 +++++++++++++ include/ghostty/vt/terminal.h | 164 ++++++++++++++++++ include/ghostty/vt/types.h | 2 + src/terminal/c/render.zig | 10 +- src/terminal/c/result.zig | 1 + src/terminal/c/terminal.zig | 278 ++++++++++++++++++++++++++++++ src/terminal/color.zig | 21 +++ test_align | 0 11 files changed, 674 insertions(+), 7 deletions(-) create mode 100644 example/c-vt-colors/README.md create mode 100644 example/c-vt-colors/build.zig create mode 100644 example/c-vt-colors/build.zig.zon create mode 100644 example/c-vt-colors/src/main.c create mode 100755 test_align diff --git a/example/c-vt-colors/README.md b/example/c-vt-colors/README.md new file mode 100644 index 000000000..881abfcc2 --- /dev/null +++ b/example/c-vt-colors/README.md @@ -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 +``` diff --git a/example/c-vt-colors/build.zig b/example/c-vt-colors/build.zig new file mode 100644 index 000000000..ddb62ece3 --- /dev/null +++ b/example/c-vt-colors/build.zig @@ -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); +} diff --git a/example/c-vt-colors/build.zig.zon b/example/c-vt-colors/build.zig.zon new file mode 100644 index 000000000..3d0023d3d --- /dev/null +++ b/example/c-vt-colors/build.zig.zon @@ -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", + }, +} diff --git a/example/c-vt-colors/src/main.c b/example/c-vt-colors/src/main.c new file mode 100644 index 000000000..6838527f2 --- /dev/null +++ b/example/c-vt-colors/src/main.c @@ -0,0 +1,121 @@ +#include +#include +#include +#include +#include +#include + +//! [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] diff --git a/include/ghostty/vt/terminal.h b/include/ghostty/vt/terminal.h index e23aea93b..ec0b46508 100644 --- a/include/ghostty/vt/terminal.h +++ b/include/ghostty/vt/terminal.h @@ -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; /** diff --git a/include/ghostty/vt/types.h b/include/ghostty/vt/types.h index b5b0fa651..e3d668e29 100644 --- a/include/ghostty/vt/types.h +++ b/include/ghostty/vt/types.h @@ -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; /** diff --git a/src/terminal/c/render.zig b/src/terminal/c/render.zig index 08a5c00f3..b95bedd5f 100644 --- a/src/terminal/c/render.zig +++ b/src/terminal/c/render.zig @@ -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, diff --git a/src/terminal/c/result.zig b/src/terminal/c/result.zig index b76326e46..328663285 100644 --- a/src/terminal/c/result.zig +++ b/src/terminal/c/result.zig @@ -4,4 +4,5 @@ pub const Result = enum(c_int) { out_of_memory = -1, invalid_value = -2, out_of_space = -3, + no_value = -4, }; diff --git a/src/terminal/c/terminal.zig b/src/terminal/c/terminal.zig index 62d3f61c4..c3ad20771 100644 --- a/src/terminal/c/terminal.zig +++ b/src/terminal/c/terminal.zig @@ -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); +} diff --git a/src/terminal/color.zig b/src/terminal/color.zig index 3b806f8b8..cb1511c35 100644 --- a/src/terminal/color.zig +++ b/src/terminal/color.zig @@ -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, diff --git a/test_align b/test_align new file mode 100755 index 000000000..e69de29bb