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