From cabca0aca8cfec5386ac52fac8a422da750fb3ca Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 30 Oct 2025 08:56:07 -0700 Subject: [PATCH 1/6] terminal: unify palette functionality into shared type DynamicPalette --- src/inspector/cell.zig | 4 +- src/inspector/cursor.zig | 4 +- src/renderer/generic.zig | 2 +- src/terminal/Terminal.zig | 34 +++-- src/terminal/color.zig | 208 +++++++++++++++++++++++++++++++ src/terminal/formatter.zig | 38 +++--- src/terminal/stream_readonly.zig | 52 +++----- src/termio/Termio.zig | 15 +-- src/termio/stream_handler.zig | 29 ++--- 9 files changed, 288 insertions(+), 98 deletions(-) diff --git a/src/inspector/cell.zig b/src/inspector/cell.zig index 9a3112bdd..b2dc59fef 100644 --- a/src/inspector/cell.zig +++ b/src/inspector/cell.zig @@ -130,7 +130,7 @@ pub const Cell = struct { switch (self.style.fg_color) { .none => cimgui.c.igText("default"), .palette => |idx| { - const rgb = t.color_palette.colors[idx]; + const rgb = t.colors.palette.current[idx]; cimgui.c.igValue_Int("Palette", idx); var color: [3]f32 = .{ @as(f32, @floatFromInt(rgb.r)) / 255, @@ -169,7 +169,7 @@ pub const Cell = struct { switch (self.style.bg_color) { .none => cimgui.c.igText("default"), .palette => |idx| { - const rgb = t.color_palette.colors[idx]; + const rgb = t.colors.palette.current[idx]; cimgui.c.igValue_Int("Palette", idx); var color: [3]f32 = .{ @as(f32, @floatFromInt(rgb.r)) / 255, diff --git a/src/inspector/cursor.zig b/src/inspector/cursor.zig index be1cd63fe..37ec412e9 100644 --- a/src/inspector/cursor.zig +++ b/src/inspector/cursor.zig @@ -51,7 +51,7 @@ pub fn renderInTable( switch (cursor.style.fg_color) { .none => cimgui.c.igText("default"), .palette => |idx| { - const rgb = t.color_palette.colors[idx]; + const rgb = t.colors.palette.current[idx]; cimgui.c.igValue_Int("Palette", idx); var color: [3]f32 = .{ @as(f32, @floatFromInt(rgb.r)) / 255, @@ -90,7 +90,7 @@ pub fn renderInTable( switch (cursor.style.bg_color) { .none => cimgui.c.igText("default"), .palette => |idx| { - const rgb = t.color_palette.colors[idx]; + const rgb = t.colors.palette.current[idx]; cimgui.c.igValue_Int("Palette", idx); var color: [3]f32 = .{ @as(f32, @floatFromInt(rgb.r)) / 255, diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index 9e13d0b41..9d4e14ea7 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -1258,7 +1258,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { .mouse = state.mouse, .preedit = preedit, .cursor_style = cursor_style, - .color_palette = state.terminal.color_palette.colors, + .color_palette = state.terminal.colors.palette.current, .scrollbar = scrollbar, .full_rebuild = full_rebuild, }; diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 2201a324c..64cda5ee3 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -73,17 +73,8 @@ scrolling_region: ScrollingRegion, /// The last reported pwd, if any. pwd: std.ArrayList(u8), -/// The default color palette. This is only modified by changing the config file -/// and is used to reset the palette when receiving an OSC 104 command. -default_palette: color.Palette = color.default, - -/// The color palette to use. The mask indicates which palette indices have been -/// modified with OSC 4 -color_palette: struct { - const Mask = std.StaticBitSet(@typeInfo(color.Palette).array.len); - colors: color.Palette = color.default, - mask: Mask = .initEmpty(), -} = .{}, +/// The color state for this terminal. +colors: Colors, /// The previous printed character. This is used for the repeat previous /// char CSI (ESC [ b). @@ -134,6 +125,23 @@ flags: packed struct { dirty: Dirty = .{}, } = .{}, +/// The various color configurations a terminal maintains and that can +/// be set dynamically via OSC, with defaults usually coming from a +/// configuration. +pub const Colors = struct { + background: color.DynamicRGB, + foreground: color.DynamicRGB, + cursor: color.DynamicRGB, + palette: color.DynamicPalette, + + pub const default: Colors = .{ + .background = .unset, + .foreground = .unset, + .cursor = .unset, + .palette = .default, + }; +}; + /// This is a set of dirty flags the renderer can use to determine /// what parts of the screen need to be redrawn. It is up to the renderer /// to clear these flags. @@ -199,6 +207,7 @@ pub const Options = struct { cols: size.CellCountInt, rows: size.CellCountInt, max_scrollback: usize = 10_000, + colors: Colors = .default, /// The default mode state. When the terminal gets a reset, it /// will revert back to this state. @@ -212,7 +221,7 @@ pub fn init( ) !Terminal { const cols = opts.cols; const rows = opts.rows; - return Terminal{ + return .{ .cols = cols, .rows = rows, .active_screen = .primary, @@ -226,6 +235,7 @@ pub fn init( .right = cols - 1, }, .pwd = .empty, + .colors = opts.colors, .modes = .{ .values = opts.default_modes, .default = opts.default_modes, diff --git a/src/terminal/color.zig b/src/terminal/color.zig index b71279dbb..e4b71fe63 100644 --- a/src/terminal/color.zig +++ b/src/terminal/color.zig @@ -1,3 +1,5 @@ +const colorpkg = @This(); + const std = @import("std"); const assert = std.debug.assert; const x11_color = @import("x11_color.zig"); @@ -45,6 +47,97 @@ pub const default: Palette = default: { /// Palette is the 256 color palette. pub const Palette = [256]RGB; +/// A palette that can have its colors changed and reset. Purposely built +/// for terminal color operations. +pub const DynamicPalette = struct { + /// The current palette including any user modifications. + current: Palette, + + /// The original/default palette values. + original: Palette, + + /// A bitset where each bit represents whether the corresponding + /// palette index has been modified from its default value. + mask: Mask, + + const Mask = std.StaticBitSet(@typeInfo(Palette).array.len); + + pub const default: DynamicPalette = .init(colorpkg.default); + + /// Initialize a dynamic palette with a default palette. + pub fn init(def: Palette) DynamicPalette { + return .{ + .current = def, + .original = def, + .mask = .initEmpty(), + }; + } + + /// Set a custom color at the given palette index. + pub fn set(self: *DynamicPalette, idx: u8, color: RGB) void { + self.current[idx] = color; + self.mask.set(idx); + } + + /// Reset the color at the given palette index to its original value. + pub fn reset(self: *DynamicPalette, idx: u8) void { + self.current[idx] = self.original[idx]; + self.mask.unset(idx); + } + + /// Reset all colors to their original values. + pub fn resetAll(self: *DynamicPalette) void { + self.* = .init(self.original); + } + + /// Change the default palette, but preserve the changed values. + pub fn changeDefault(self: *DynamicPalette, def: Palette) void { + self.original = def; + + // Fast path, the palette is usually not changed. + if (self.mask.count() == 0) { + self.current = self.original; + return; + } + + // There are usually less set than unset, so iterate over the changed + // values and override them. + var current = def; + var it = self.mask.iterator(.{}); + while (it.next()) |idx| current[idx] = self.current[idx]; + self.current = current; + } +}; + +/// RGB value that can be changed and reset. This can also be totally unset +/// in every way, in which case the caller can determine their own ultimate +/// default. +pub const DynamicRGB = struct { + override: ?RGB, + default: ?RGB, + + pub const unset: DynamicRGB = .{ .override = null, .default = null }; + + pub fn init(def: RGB) DynamicRGB { + return .{ + .override = null, + .default = def, + }; + } + + pub fn get(self: *const DynamicRGB) ?RGB { + return self.override orelse self.default; + } + + pub fn set(self: *DynamicRGB, color: RGB) void { + self.current = color; + } + + pub fn reset(self: *DynamicRGB) void { + self.current = self.default; + } +}; + /// Color names in the standard 8 or 16 color palette. pub const Name = enum(u8) { black = 0, @@ -456,3 +549,118 @@ test "RGB.parse" { try testing.expectError(error.InvalidFormat, RGB.parse("#fffff")); try testing.expectError(error.InvalidFormat, RGB.parse("#gggggg")); } + +test "DynamicPalette: init" { + const testing = std.testing; + + var p: DynamicPalette = .init(default); + try testing.expectEqual(default, p.current); + try testing.expectEqual(default, p.original); + try testing.expectEqual(@as(usize, 0), p.mask.count()); +} + +test "DynamicPalette: set" { + const testing = std.testing; + + var p: DynamicPalette = .init(default); + const new_color = RGB{ .r = 255, .g = 0, .b = 0 }; + + p.set(0, new_color); + try testing.expectEqual(new_color, p.current[0]); + try testing.expect(p.mask.isSet(0)); + try testing.expectEqual(@as(usize, 1), p.mask.count()); + + try testing.expectEqual(default[0], p.original[0]); +} + +test "DynamicPalette: reset" { + const testing = std.testing; + + var p: DynamicPalette = .init(default); + const new_color = RGB{ .r = 255, .g = 0, .b = 0 }; + + p.set(0, new_color); + try testing.expect(p.mask.isSet(0)); + + p.reset(0); + try testing.expectEqual(default[0], p.current[0]); + try testing.expect(!p.mask.isSet(0)); + try testing.expectEqual(@as(usize, 0), p.mask.count()); +} + +test "DynamicPalette: resetAll" { + const testing = std.testing; + + var p: DynamicPalette = .init(default); + const new_color = RGB{ .r = 255, .g = 0, .b = 0 }; + + p.set(0, new_color); + p.set(5, new_color); + p.set(10, new_color); + try testing.expectEqual(@as(usize, 3), p.mask.count()); + + p.resetAll(); + try testing.expectEqual(default, p.current); + try testing.expectEqual(default, p.original); + try testing.expectEqual(@as(usize, 0), p.mask.count()); +} + +test "DynamicPalette: changeDefault with no changes" { + const testing = std.testing; + + var p: DynamicPalette = .init(default); + var new_palette = default; + new_palette[0] = RGB{ .r = 100, .g = 100, .b = 100 }; + + p.changeDefault(new_palette); + try testing.expectEqual(new_palette, p.original); + try testing.expectEqual(new_palette, p.current); + try testing.expectEqual(@as(usize, 0), p.mask.count()); +} + +test "DynamicPalette: changeDefault preserves changes" { + const testing = std.testing; + + var p: DynamicPalette = .init(default); + const custom_color = RGB{ .r = 255, .g = 0, .b = 0 }; + + p.set(5, custom_color); + try testing.expect(p.mask.isSet(5)); + + var new_palette = default; + new_palette[0] = RGB{ .r = 100, .g = 100, .b = 100 }; + new_palette[5] = RGB{ .r = 50, .g = 50, .b = 50 }; + + p.changeDefault(new_palette); + + try testing.expectEqual(new_palette, p.original); + try testing.expectEqual(new_palette[0], p.current[0]); + try testing.expectEqual(custom_color, p.current[5]); + try testing.expect(p.mask.isSet(5)); + try testing.expectEqual(@as(usize, 1), p.mask.count()); +} + +test "DynamicPalette: changeDefault with multiple changes" { + const testing = std.testing; + + var p: DynamicPalette = .init(default); + const red = RGB{ .r = 255, .g = 0, .b = 0 }; + const green = RGB{ .r = 0, .g = 255, .b = 0 }; + const blue = RGB{ .r = 0, .g = 0, .b = 255 }; + + p.set(1, red); + p.set(2, green); + p.set(3, blue); + + var new_palette = default; + new_palette[0] = RGB{ .r = 50, .g = 50, .b = 50 }; + new_palette[1] = RGB{ .r = 60, .g = 60, .b = 60 }; + + p.changeDefault(new_palette); + + try testing.expectEqual(new_palette[0], p.current[0]); + try testing.expectEqual(red, p.current[1]); + try testing.expectEqual(green, p.current[2]); + try testing.expectEqual(blue, p.current[3]); + try testing.expectEqual(@as(usize, 3), p.mask.count()); +} diff --git a/src/terminal/formatter.zig b/src/terminal/formatter.zig index 246624d5b..20dcf9a89 100644 --- a/src/terminal/formatter.zig +++ b/src/terminal/formatter.zig @@ -225,24 +225,24 @@ pub const TerminalFormatter = struct { .plain => break :palette, .vt => { - for (self.terminal.color_palette.colors, 0..) |rgb, i| { - try writer.print( - "\x1b]4;{d};rgb:{x:0>2}/{x:0>2}/{x:0>2}\x1b\\", - .{ i, rgb.r, rgb.g, rgb.b }, - ); - } + for (self.terminal.colors.palette.current, 0..) |rgb, i| { + try writer.print( + "\x1b]4;{d};rgb:{x:0>2}/{x:0>2}/{x:0>2}\x1b\\", + .{ i, rgb.r, rgb.g, rgb.b }, + ); + } }, // For HTML, we emit CSS to setup our palette variables. .html => { - try writer.writeAll(""); + try writer.writeAll(""); }, } @@ -3839,9 +3839,9 @@ test "TerminalFormatter vt with palette" { try s2.nextSlice(output); // Verify the palettes match - try testing.expectEqual(t.color_palette.colors[0], t2.color_palette.colors[0]); - try testing.expectEqual(t.color_palette.colors[1], t2.color_palette.colors[1]); - try testing.expectEqual(t.color_palette.colors[255], t2.color_palette.colors[255]); + try testing.expectEqual(t.colors.palette.current[0], t2.colors.palette.current[0]); + try testing.expectEqual(t.colors.palette.current[1], t2.colors.palette.current[1]); + try testing.expectEqual(t.colors.palette.current[255], t2.colors.palette.current[255]); } test "TerminalFormatter with selection" { @@ -4972,7 +4972,7 @@ test "Page VT with palette option emits RGB" { { builder.clearRetainingCapacity(); var opts: Options = .vt; - opts.palette = &t.color_palette.colors; + opts.palette = &t.colors.palette.current; var formatter: PageFormatter = .init(page, opts); try formatter.format(&builder.writer); const output = builder.writer.buffered(); @@ -5021,7 +5021,7 @@ test "Page html with palette option emits RGB" { { builder.clearRetainingCapacity(); var opts: Options = .{ .emit = .html }; - opts.palette = &t.color_palette.colors; + opts.palette = &t.colors.palette.current; var formatter: PageFormatter = .init(page, opts); try formatter.format(&builder.writer); const output = builder.writer.buffered(); diff --git a/src/terminal/stream_readonly.zig b/src/terminal/stream_readonly.zig index 86a525284..f73d21dce 100644 --- a/src/terminal/stream_readonly.zig +++ b/src/terminal/stream_readonly.zig @@ -305,10 +305,7 @@ pub const Handler = struct { switch (req.*) { .set => |set| { switch (set.target) { - .palette => |i| { - self.terminal.color_palette.colors[i] = set.color; - self.terminal.color_palette.mask.set(i); - }, + .palette => |i| self.terminal.colors.palette.set(i, set.color), .dynamic, .special, => {}, @@ -316,24 +313,13 @@ pub const Handler = struct { }, .reset => |target| switch (target) { - .palette => |i| { - const mask = &self.terminal.color_palette.mask; - self.terminal.color_palette.colors[i] = self.terminal.default_palette[i]; - mask.unset(i); - }, + .palette => |i| self.terminal.colors.palette.reset(i), .dynamic, .special, => {}, }, - .reset_palette => { - const mask = &self.terminal.color_palette.mask; - var mask_iterator = mask.iterator(.{}); - while (mask_iterator.next()) |i| { - self.terminal.color_palette.colors[i] = self.terminal.default_palette[i]; - } - mask.* = .initEmpty(); - }, + .reset_palette => self.terminal.colors.palette.resetAll(), .query, .reset_special, @@ -599,19 +585,19 @@ test "OSC 4 set and reset palette" { defer s.deinit(); // Save default color - const default_color_0 = t.default_palette[0]; + const default_color_0 = t.colors.palette.original[0]; // Set color 0 to red try s.nextSlice("\x1b]4;0;rgb:ff/00/00\x1b\\"); - try testing.expectEqual(@as(u8, 0xff), t.color_palette.colors[0].r); - try testing.expectEqual(@as(u8, 0x00), t.color_palette.colors[0].g); - try testing.expectEqual(@as(u8, 0x00), t.color_palette.colors[0].b); - try testing.expect(t.color_palette.mask.isSet(0)); + try testing.expectEqual(@as(u8, 0xff), t.colors.palette.current[0].r); + try testing.expectEqual(@as(u8, 0x00), t.colors.palette.current[0].g); + try testing.expectEqual(@as(u8, 0x00), t.colors.palette.current[0].b); + try testing.expect(t.colors.palette.mask.isSet(0)); // Reset color 0 try s.nextSlice("\x1b]104;0\x1b\\"); - try testing.expectEqual(default_color_0, t.color_palette.colors[0]); - try testing.expect(!t.color_palette.mask.isSet(0)); + try testing.expectEqual(default_color_0, t.colors.palette.current[0]); + try testing.expect(!t.colors.palette.mask.isSet(0)); } test "OSC 104 reset all palette colors" { @@ -625,16 +611,16 @@ test "OSC 104 reset all palette colors" { try s.nextSlice("\x1b]4;0;rgb:ff/00/00\x1b\\"); try s.nextSlice("\x1b]4;1;rgb:00/ff/00\x1b\\"); try s.nextSlice("\x1b]4;2;rgb:00/00/ff\x1b\\"); - try testing.expect(t.color_palette.mask.isSet(0)); - try testing.expect(t.color_palette.mask.isSet(1)); - try testing.expect(t.color_palette.mask.isSet(2)); + try testing.expect(t.colors.palette.mask.isSet(0)); + try testing.expect(t.colors.palette.mask.isSet(1)); + try testing.expect(t.colors.palette.mask.isSet(2)); // Reset all palette colors try s.nextSlice("\x1b]104\x1b\\"); - try testing.expectEqual(t.default_palette[0], t.color_palette.colors[0]); - try testing.expectEqual(t.default_palette[1], t.color_palette.colors[1]); - try testing.expectEqual(t.default_palette[2], t.color_palette.colors[2]); - try testing.expect(!t.color_palette.mask.isSet(0)); - try testing.expect(!t.color_palette.mask.isSet(1)); - try testing.expect(!t.color_palette.mask.isSet(2)); + try testing.expectEqual(t.colors.palette.original[0], t.colors.palette.current[0]); + try testing.expectEqual(t.colors.palette.original[1], t.colors.palette.current[1]); + try testing.expectEqual(t.colors.palette.original[2], t.colors.palette.current[2]); + try testing.expect(!t.colors.palette.mask.isSet(0)); + try testing.expect(!t.colors.palette.mask.isSet(1)); + try testing.expect(!t.colors.palette.mask.isSet(2)); } diff --git a/src/termio/Termio.zig b/src/termio/Termio.zig index 01a8ef312..f5e0af221 100644 --- a/src/termio/Termio.zig +++ b/src/termio/Termio.zig @@ -234,8 +234,7 @@ pub fn init(self: *Termio, alloc: Allocator, opts: termio.Options) !void { }; }); errdefer term.deinit(alloc); - term.default_palette = opts.config.palette; - term.color_palette.colors = opts.config.palette; + term.colors.palette.changeDefault(opts.config.palette); // Set the image size limits try term.screen.kitty_images.setLimit( @@ -451,16 +450,8 @@ pub fn changeConfig(self: *Termio, td: *ThreadData, config: *DerivedConfig) !voi // Update the default palette. Note this will only apply to new colors drawn // since we decode all palette colors to RGB on usage. - self.terminal.default_palette = config.palette; - - // Update the active palette, except for any colors that were modified with - // OSC 4 - for (0..config.palette.len) |i| { - if (!self.terminal.color_palette.mask.isSet(i)) { - self.terminal.color_palette.colors[i] = config.palette[i]; - self.terminal.flags.dirty.palette = true; - } - } + self.terminal.colors.palette.changeDefault(config.palette); + self.terminal.flags.dirty.palette = true; // Set the image size limits try self.terminal.screen.kitty_images.setLimit( diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 7f241f42c..8f3f845d6 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -1133,8 +1133,7 @@ pub const StreamHandler = struct { switch (set.target) { .palette => |i| { self.terminal.flags.dirty.palette = true; - self.terminal.color_palette.colors[i] = set.color; - self.terminal.color_palette.mask.set(i); + self.terminal.colors.palette.set(i, set.color); }, .dynamic => |dynamic| switch (dynamic) { .foreground => { @@ -1178,15 +1177,13 @@ pub const StreamHandler = struct { .reset => |target| switch (target) { .palette => |i| { - const mask = &self.terminal.color_palette.mask; self.terminal.flags.dirty.palette = true; - self.terminal.color_palette.colors[i] = self.terminal.default_palette[i]; - mask.unset(i); + self.terminal.colors.palette.reset(i); self.surfaceMessageWriter(.{ .color_change = .{ .target = target, - .color = self.terminal.color_palette.colors[i], + .color = self.terminal.colors.palette.current[i], }, }); }, @@ -1242,15 +1239,15 @@ pub const StreamHandler = struct { }, .reset_palette => { - const mask = &self.terminal.color_palette.mask; - var mask_iterator = mask.iterator(.{}); - while (mask_iterator.next()) |i| { + const mask = &self.terminal.colors.palette.mask; + var mask_it = mask.iterator(.{}); + while (mask_it.next()) |i| { self.terminal.flags.dirty.palette = true; - self.terminal.color_palette.colors[i] = self.terminal.default_palette[i]; + self.terminal.colors.palette.reset(@intCast(i)); self.surfaceMessageWriter(.{ .color_change = .{ .target = .{ .palette = @intCast(i) }, - .color = self.terminal.color_palette.colors[i], + .color = self.terminal.colors.palette.current[i], }, }); } @@ -1266,7 +1263,7 @@ pub const StreamHandler = struct { if (self.osc_color_report_format == .none) break :report; const color = switch (kind) { - .palette => |i| self.terminal.color_palette.colors[i], + .palette => |i| self.terminal.colors.palette.current[i], .dynamic => |dynamic| switch (dynamic) { .foreground => self.foreground_color orelse self.default_foreground_color, .background => self.background_color orelse self.default_background_color, @@ -1399,7 +1396,7 @@ pub const StreamHandler = struct { if (stream.written().len == 0) try writer.writeAll("\x1b]21"); const color: terminal.color.RGB = switch (key) { - .palette => |palette| self.terminal.color_palette.colors[palette], + .palette => |palette| self.terminal.colors.palette.current[palette], .special => |special| switch (special) { .foreground => self.foreground_color orelse self.default_foreground_color, .background => self.background_color orelse self.default_background_color, @@ -1422,8 +1419,7 @@ pub const StreamHandler = struct { .set => |v| switch (v.key) { .palette => |palette| { self.terminal.flags.dirty.palette = true; - self.terminal.color_palette.colors[palette] = v.color; - self.terminal.color_palette.mask.unset(palette); + self.terminal.colors.palette.set(palette, v.color); }, .special => |special| { @@ -1457,8 +1453,7 @@ pub const StreamHandler = struct { .reset => |key| switch (key) { .palette => |palette| { self.terminal.flags.dirty.palette = true; - self.terminal.color_palette.colors[palette] = self.terminal.default_palette[palette]; - self.terminal.color_palette.mask.unset(palette); + self.terminal.colors.palette.reset(palette); }, .special => |special| { From 77343bb06e65c7d7015e367ef4461f2bece80c4b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 30 Oct 2025 09:26:58 -0700 Subject: [PATCH 2/6] terminal: move color state fully into the terminal for fg/bg/cursor --- src/config/Config.zig | 7 ++ src/terminal/color.zig | 4 +- src/termio/Termio.zig | 69 +++++++++---------- src/termio/stream_handler.zig | 126 ++++++++++++++-------------------- 4 files changed, 92 insertions(+), 114 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index a9aaf8f86..78ea19aef 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -4960,6 +4960,13 @@ pub const TerminalColor = union(enum) { return .{ .color = try Color.parseCLI(input) }; } + pub fn toTerminalRGB(self: TerminalColor) ?terminal.color.RGB { + return switch (self) { + .color => |v| v.toTerminalRGB(), + .@"cell-foreground", .@"cell-background" => null, + }; + } + /// Used by Formatter pub fn formatEntry(self: TerminalColor, formatter: formatterpkg.EntryFormatter) !void { switch (self) { diff --git a/src/terminal/color.zig b/src/terminal/color.zig index e4b71fe63..4492d65ae 100644 --- a/src/terminal/color.zig +++ b/src/terminal/color.zig @@ -130,11 +130,11 @@ pub const DynamicRGB = struct { } pub fn set(self: *DynamicRGB, color: RGB) void { - self.current = color; + self.override = color; } pub fn reset(self: *DynamicRGB) void { - self.current = self.default; + self.override = self.default; } }; diff --git a/src/termio/Termio.zig b/src/termio/Termio.zig index f5e0af221..1e181a137 100644 --- a/src/termio/Termio.zig +++ b/src/termio/Termio.zig @@ -231,10 +231,19 @@ pub fn init(self: *Termio, alloc: Allocator, opts: termio.Options) !void { .rows = grid_size.rows, .max_scrollback = opts.full_config.@"scrollback-limit", .default_modes = default_modes, + .colors = .{ + .background = .init(opts.config.background.toTerminalRGB()), + .foreground = .init(opts.config.foreground.toTerminalRGB()), + .cursor = cursor: { + const color = opts.config.cursor_color orelse break :cursor .unset; + const rgb = color.toTerminalRGB() orelse break :cursor .unset; + break :cursor .init(rgb); + }, + .palette = .init(opts.config.palette), + }, }; }); errdefer term.deinit(alloc); - term.colors.palette.changeDefault(opts.config.palette); // Set the image size limits try term.screen.kitty_images.setLimit( @@ -261,39 +270,20 @@ pub fn init(self: *Termio, alloc: Allocator, opts: termio.Options) !void { // Create our stream handler. This points to memory in self so it // isn't safe to use until self.* is set. - const handler: StreamHandler = handler: { - const default_cursor_color: ?terminalpkg.color.RGB = color: { - if (opts.config.cursor_color) |color| switch (color) { - .color => break :color color.color.toTerminalRGB(), - .@"cell-foreground", - .@"cell-background", - => {}, - }; - - break :color null; - }; - - break :handler .{ - .alloc = alloc, - .termio_mailbox = &self.mailbox, - .surface_mailbox = opts.surface_mailbox, - .renderer_state = opts.renderer_state, - .renderer_wakeup = opts.renderer_wakeup, - .renderer_mailbox = opts.renderer_mailbox, - .size = &self.size, - .terminal = &self.terminal, - .osc_color_report_format = opts.config.osc_color_report_format, - .clipboard_write = opts.config.clipboard_write, - .enquiry_response = opts.config.enquiry_response, - .default_foreground_color = opts.config.foreground.toTerminalRGB(), - .default_background_color = opts.config.background.toTerminalRGB(), - .default_cursor_style = opts.config.cursor_style, - .default_cursor_blink = opts.config.cursor_blink, - .default_cursor_color = default_cursor_color, - .cursor_color = null, - .foreground_color = null, - .background_color = null, - }; + const handler: StreamHandler = .{ + .alloc = alloc, + .termio_mailbox = &self.mailbox, + .surface_mailbox = opts.surface_mailbox, + .renderer_state = opts.renderer_state, + .renderer_wakeup = opts.renderer_wakeup, + .renderer_mailbox = opts.renderer_mailbox, + .size = &self.size, + .terminal = &self.terminal, + .osc_color_report_format = opts.config.osc_color_report_format, + .clipboard_write = opts.config.clipboard_write, + .enquiry_response = opts.config.enquiry_response, + .default_cursor_style = opts.config.cursor_style, + .default_cursor_blink = opts.config.cursor_blink, }; const thread_enter_state = try ThreadEnterState.create( @@ -448,11 +438,18 @@ pub fn changeConfig(self: *Termio, td: *ThreadData, config: *DerivedConfig) !voi // - command, working-directory: we never restart the underlying // process so we don't care or need to know about these. - // Update the default palette. Note this will only apply to new colors drawn - // since we decode all palette colors to RGB on usage. + // Update the default palette. self.terminal.colors.palette.changeDefault(config.palette); self.terminal.flags.dirty.palette = true; + // Update all our other colors + self.terminal.colors.background.default = config.background.toTerminalRGB(); + self.terminal.colors.foreground.default = config.foreground.toTerminalRGB(); + self.terminal.colors.cursor.default = cursor: { + const color = config.cursor_color orelse break :cursor null; + break :cursor color.toTerminalRGB() orelse break :cursor null; + }; + // Set the image size limits try self.terminal.screen.kitty_images.setLimit( self.alloc, diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 8f3f845d6..551145cfb 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -45,22 +45,6 @@ pub const StreamHandler = struct { default_cursor: bool = true, default_cursor_style: terminal.CursorStyle, default_cursor_blink: ?bool, - default_cursor_color: ?terminal.color.RGB, - - /// Actual cursor color. This can be changed with OSC 12. If unset, falls - /// back to the default cursor color. - cursor_color: ?terminal.color.RGB, - - /// The default foreground and background color are those set by the user's - /// config file. - default_foreground_color: terminal.color.RGB, - default_background_color: terminal.color.RGB, - - /// The foreground and background color as set by an OSC 10 or OSC 11 - /// sequence. If unset then the respective color falls back to the default - /// value. - foreground_color: ?terminal.color.RGB, - background_color: ?terminal.color.RGB, /// The response to use for ENQ requests. The memory is owned by /// whoever owns StreamHandler. @@ -114,20 +98,8 @@ pub const StreamHandler = struct { self.osc_color_report_format = config.osc_color_report_format; self.clipboard_write = config.clipboard_write; self.enquiry_response = config.enquiry_response; - self.default_foreground_color = config.foreground.toTerminalRGB(); - self.default_background_color = config.background.toTerminalRGB(); self.default_cursor_style = config.cursor_style; self.default_cursor_blink = config.cursor_blink; - self.default_cursor_color = color: { - if (config.cursor_color) |color| switch (color) { - .color => break :color color.color.toTerminalRGB(), - .@"cell-foreground", - .@"cell-background", - => {}, - }; - - break :color null; - }; // If our cursor is the default, then we update it immediately. if (self.default_cursor) self.setCursorStyle(.default) catch |err| { @@ -1137,19 +1109,19 @@ pub const StreamHandler = struct { }, .dynamic => |dynamic| switch (dynamic) { .foreground => { - self.foreground_color = set.color; + self.terminal.colors.foreground.set(set.color); self.rendererMessageWriter(.{ .foreground_color = set.color, }); }, .background => { - self.background_color = set.color; + self.terminal.colors.background.set(set.color); self.rendererMessageWriter(.{ .background_color = set.color, }); }, .cursor => { - self.cursor_color = set.color; + self.terminal.colors.cursor.set(set.color); self.rendererMessageWriter(.{ .cursor_color = set.color, }); @@ -1189,38 +1161,42 @@ pub const StreamHandler = struct { }, .dynamic => |dynamic| switch (dynamic) { .foreground => { - self.foreground_color = null; + self.terminal.colors.foreground.reset(); self.rendererMessageWriter(.{ - .foreground_color = self.foreground_color, + .foreground_color = null, }); - self.surfaceMessageWriter(.{ .color_change = .{ - .target = target, - .color = self.default_foreground_color, - } }); - }, - .background => { - self.background_color = null; - self.rendererMessageWriter(.{ - .background_color = self.background_color, - }); - - self.surfaceMessageWriter(.{ .color_change = .{ - .target = target, - .color = self.default_background_color, - } }); - }, - .cursor => { - self.cursor_color = null; - - self.rendererMessageWriter(.{ - .cursor_color = self.cursor_color, - }); - - if (self.default_cursor_color) |color| { + if (self.terminal.colors.foreground.default) |c| { self.surfaceMessageWriter(.{ .color_change = .{ .target = target, - .color = color, + .color = c, + } }); + } + }, + .background => { + self.terminal.colors.background.reset(); + self.rendererMessageWriter(.{ + .background_color = null, + }); + + if (self.terminal.colors.background.default) |c| { + self.surfaceMessageWriter(.{ .color_change = .{ + .target = target, + .color = c, + } }); + } + }, + .cursor => { + self.terminal.colors.cursor.reset(); + + self.rendererMessageWriter(.{ + .cursor_color = null, + }); + + if (self.terminal.colors.cursor.default) |c| { + self.surfaceMessageWriter(.{ .color_change = .{ + .target = target, + .color = c, } }); } }, @@ -1265,12 +1241,10 @@ pub const StreamHandler = struct { const color = switch (kind) { .palette => |i| self.terminal.colors.palette.current[i], .dynamic => |dynamic| switch (dynamic) { - .foreground => self.foreground_color orelse self.default_foreground_color, - .background => self.background_color orelse self.default_background_color, - .cursor => self.cursor_color orelse - self.default_cursor_color orelse - self.foreground_color orelse - self.default_foreground_color, + .foreground => self.terminal.colors.foreground.get().?, + .background => self.terminal.colors.background.get().?, + .cursor => self.terminal.colors.cursor.get() orelse + self.terminal.colors.foreground.get().?, .pointer_foreground, .pointer_background, .tektronix_foreground, @@ -1398,9 +1372,9 @@ pub const StreamHandler = struct { const color: terminal.color.RGB = switch (key) { .palette => |palette| self.terminal.colors.palette.current[palette], .special => |special| switch (special) { - .foreground => self.foreground_color orelse self.default_foreground_color, - .background => self.background_color orelse self.default_background_color, - .cursor => self.cursor_color orelse self.default_cursor_color, + .foreground => self.terminal.colors.foreground.get(), + .background => self.terminal.colors.background.get(), + .cursor => self.terminal.colors.cursor.get(), else => { log.warn("ignoring unsupported kitty color protocol key: {f}", .{key}); continue; @@ -1425,15 +1399,15 @@ pub const StreamHandler = struct { .special => |special| { const msg: renderer.Message = switch (special) { .foreground => msg: { - self.foreground_color = v.color; + self.terminal.colors.foreground.set(v.color); break :msg .{ .foreground_color = v.color }; }, .background => msg: { - self.background_color = v.color; + self.terminal.colors.background.set(v.color); break :msg .{ .background_color = v.color }; }, .cursor => msg: { - self.cursor_color = v.color; + self.terminal.colors.cursor.set(v.color); break :msg .{ .cursor_color = v.color }; }, else => { @@ -1459,16 +1433,16 @@ pub const StreamHandler = struct { .special => |special| { const msg: renderer.Message = switch (special) { .foreground => msg: { - self.foreground_color = null; - break :msg .{ .foreground_color = self.foreground_color }; + self.terminal.colors.foreground.reset(); + break :msg .{ .foreground_color = null }; }, .background => msg: { - self.background_color = null; - break :msg .{ .background_color = self.background_color }; + self.terminal.colors.background.reset(); + break :msg .{ .background_color = null }; }, .cursor => msg: { - self.cursor_color = null; - break :msg .{ .cursor_color = self.cursor_color }; + self.terminal.colors.cursor.reset(); + break :msg .{ .cursor_color = null }; }, else => { log.warn( From 2daecd94a5a9b3fd67a5267f91f3a17b0e3a3818 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 30 Oct 2025 09:52:36 -0700 Subject: [PATCH 3/6] renderer: use terminal color state, remove color messages --- src/renderer/Thread.zig | 15 ---- src/renderer/generic.zig | 135 ++++++++++++---------------------- src/renderer/message.zig | 10 --- src/termio/stream_handler.zig | 115 +++++++---------------------- 4 files changed, 75 insertions(+), 200 deletions(-) diff --git a/src/renderer/Thread.zig b/src/renderer/Thread.zig index 210c2e337..fd9d0f51a 100644 --- a/src/renderer/Thread.zig +++ b/src/renderer/Thread.zig @@ -437,21 +437,6 @@ fn drainMailbox(self: *Thread) !void { grid.set.deref(grid.old_key); }, - .foreground_color => |color| { - self.renderer.foreground_color = color; - self.renderer.markDirty(); - }, - - .background_color => |color| { - self.renderer.background_color = color; - self.renderer.markDirty(); - }, - - .cursor_color => |color| { - self.renderer.cursor_color = color; - self.renderer.markDirty(); - }, - .resize => |v| self.renderer.setScreenSize(v), .change_config => |config| { diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index 9d4e14ea7..0b4c55896 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -120,30 +120,6 @@ pub fn Renderer(comptime GraphicsAPI: type) type { scrollbar: terminal.Scrollbar, scrollbar_dirty: bool, - /// The foreground color set by an OSC 10 sequence. If unset then - /// default_foreground_color is used. - foreground_color: ?terminal.color.RGB, - - /// Foreground color set in the user's config file. - default_foreground_color: terminal.color.RGB, - - /// The background color set by an OSC 11 sequence. If unset then - /// default_background_color is used. - background_color: ?terminal.color.RGB, - - /// Background color set in the user's config file. - default_background_color: terminal.color.RGB, - - /// The cursor color set by an OSC 12 sequence. If unset then - /// default_cursor_color is used. - cursor_color: ?terminal.color.RGB, - - /// Default cursor color when no color is set explicitly by an OSC 12 command. - /// This is cursor color as set in the user's config, if any. If no cursor color - /// is set in the user's config, then the cursor color is determined by the - /// current foreground color. - default_cursor_color: ?configpkg.Config.TerminalColor, - /// The current set of cells to render. This is rebuilt on every frame /// but we keep this around so that we don't reallocate. Each set of /// cells goes into a separate shader. @@ -691,12 +667,6 @@ pub fn Renderer(comptime GraphicsAPI: type) type { .focused = true, .scrollbar = .zero, .scrollbar_dirty = false, - .foreground_color = null, - .default_foreground_color = options.config.foreground, - .background_color = null, - .default_background_color = options.config.background, - .cursor_color = null, - .default_cursor_color = options.config.cursor_color, // Render state .cells = .{}, @@ -1094,10 +1064,12 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // Data we extract out of the critical area. const Critical = struct { bg: terminal.color.RGB, + fg: terminal.color.RGB, screen: terminal.Screen, screen_type: terminal.ScreenType, mouse: renderer.State.Mouse, preedit: ?renderer.State.Preedit, + cursor_color: ?terminal.color.RGB, cursor_style: ?renderer.CursorStyle, color_palette: terminal.color.Palette, scrollbar: terminal.Scrollbar, @@ -1132,36 +1104,16 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // cross-thread mailbox message within the IO path. const scrollbar = state.terminal.screen.pages.scrollbar(); - // Swap bg/fg if the terminal is reversed - const bg = self.background_color orelse self.default_background_color; - const fg = self.foreground_color orelse self.default_foreground_color; - defer { - if (self.background_color) |*c| { - c.* = bg; - } else { - self.default_background_color = bg; - } - - if (self.foreground_color) |*c| { - c.* = fg; - } else { - self.default_foreground_color = fg; - } - } - - if (state.terminal.modes.get(.reverse_colors)) { - if (self.background_color) |*c| { - c.* = fg; - } else { - self.default_background_color = fg; - } - - if (self.foreground_color) |*c| { - c.* = bg; - } else { - self.default_foreground_color = bg; - } - } + // Get our bg/fg, swap them if reversed. + const RGB = terminal.color.RGB; + const bg: RGB, const fg: RGB = colors: { + const bg = state.terminal.colors.background.get().?; + const fg = state.terminal.colors.foreground.get().?; + break :colors if (state.terminal.modes.get(.reverse_colors)) + .{ fg, bg } + else + .{ bg, fg }; + }; // Get the viewport pin so that we can compare it to the current. const viewport_pin = state.terminal.screen.pages.pin(.{ .viewport = .{} }).?; @@ -1252,11 +1204,13 @@ pub fn Renderer(comptime GraphicsAPI: type) type { self.cells_viewport = viewport_pin; break :critical .{ - .bg = self.background_color orelse self.default_background_color, + .bg = bg, + .fg = fg, .screen = screen_copy, .screen_type = state.terminal.active_screen, .mouse = state.mouse, .preedit = preedit, + .cursor_color = state.terminal.colors.cursor.get(), .cursor_style = cursor_style, .color_palette = state.terminal.colors.palette.current, .scrollbar = scrollbar, @@ -1277,6 +1231,9 @@ pub fn Renderer(comptime GraphicsAPI: type) type { critical.preedit, critical.cursor_style, &critical.color_palette, + critical.bg, + critical.fg, + critical.cursor_color, ); // Notify our shaper we're done for the frame. For some shapers, @@ -2104,11 +2061,6 @@ pub fn Renderer(comptime GraphicsAPI: type) type { self.uniforms.bools.use_linear_blending = config.blending.isLinear(); self.uniforms.bools.use_linear_correction = config.blending == .@"linear-corrected"; - // Set our new colors - self.default_background_color = config.background; - self.default_foreground_color = config.foreground; - self.default_cursor_color = config.cursor_color; - const bg_image_config_changed = self.config.bg_image_fit != config.bg_image_fit or self.config.bg_image_position != config.bg_image_position or @@ -2370,6 +2322,9 @@ pub fn Renderer(comptime GraphicsAPI: type) type { preedit: ?renderer.State.Preedit, cursor_style_: ?renderer.CursorStyle, color_palette: *const terminal.color.Palette, + background: terminal.color.RGB, + foreground: terminal.color.RGB, + terminal_cursor_color: ?terminal.color.RGB, ) !void { self.draw_mutex.lock(); defer self.draw_mutex.unlock(); @@ -2503,12 +2458,12 @@ pub fn Renderer(comptime GraphicsAPI: type) type { .extend => if (y == 0) { self.uniforms.padding_extend.up = !row.neverExtendBg( color_palette, - self.background_color orelse self.default_background_color, + background, ); } else if (y == self.cells.size.rows - 1) { self.uniforms.padding_extend.down = !row.neverExtendBg( color_palette, - self.background_color orelse self.default_background_color, + background, ); }, } @@ -2629,7 +2584,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // configuration, inversions, selections, etc. const bg_style = style.bg(cell, color_palette); const fg_style = style.fg(.{ - .default = self.foreground_color orelse self.default_foreground_color, + .default = foreground, .palette = color_palette, .bold = self.config.bold_color, }); @@ -2649,7 +2604,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // If no configuration, then our selection background // is our foreground color. - break :bg self.foreground_color orelse self.default_foreground_color; + break :bg foreground; } // Not selected @@ -2671,9 +2626,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { const fg = fg: { // Our happy-path non-selection background color // is our style or our configured defaults. - const final_bg = bg_style orelse - self.background_color orelse - self.default_background_color; + const final_bg = bg_style orelse background; // Whether we need to use the bg color as our fg color: // - Cell is selected, inverted, and set to cell-foreground @@ -2689,7 +2642,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { }; } - break :fg self.background_color orelse self.default_background_color; + break :fg background; } break :fg if (style.flags.inverse) @@ -2703,7 +2656,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // Set the cell's background color. { - const rgb = bg orelse self.background_color orelse self.default_background_color; + const rgb = bg orelse background; // Determine our background alpha. If we have transparency configured // then this is dynamic depending on some situations. This is all @@ -2888,24 +2841,24 @@ pub fn Renderer(comptime GraphicsAPI: type) type { const style = cursor_style_ orelse break :cursor; const cursor_color = cursor_color: { // If an explicit cursor color was set by OSC 12, use that. - if (self.cursor_color) |v| break :cursor_color v; + if (terminal_cursor_color) |v| break :cursor_color v; // Use our configured color if specified - if (self.default_cursor_color) |v| switch (v) { + if (self.config.cursor_color) |v| switch (v) { .color => |color| break :cursor_color color.toTerminalRGB(), inline .@"cell-foreground", .@"cell-background", => |_, tag| { const sty = screen.cursor.page_pin.style(screen.cursor.page_cell); const fg_style = sty.fg(.{ - .default = self.foreground_color orelse self.default_foreground_color, + .default = foreground, .palette = color_palette, .bold = self.config.bold_color, }); const bg_style = sty.bg( screen.cursor.page_cell, color_palette, - ) orelse self.background_color orelse self.default_background_color; + ) orelse background; break :cursor_color switch (tag) { .color => unreachable, @@ -2915,7 +2868,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { }, }; - break :cursor_color self.foreground_color orelse self.default_foreground_color; + break :cursor_color foreground; }; self.addCursor(screen, style, cursor_color); @@ -2950,11 +2903,14 @@ pub fn Renderer(comptime GraphicsAPI: type) type { const sty = screen.cursor.page_pin.style(screen.cursor.page_cell); const fg_style = sty.fg(.{ - .default = self.foreground_color orelse self.default_foreground_color, + .default = foreground, .palette = color_palette, .bold = self.config.bold_color, }); - const bg_style = sty.bg(screen.cursor.page_cell, color_palette) orelse self.background_color orelse self.default_background_color; + const bg_style = sty.bg( + screen.cursor.page_cell, + color_palette, + ) orelse background; break :blk switch (txt) { // If the cell is reversed, use the opposite cell color instead. @@ -2962,7 +2918,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { .@"cell-background" => if (sty.flags.inverse) fg_style else bg_style, else => unreachable, }; - } else self.background_color orelse self.default_background_color; + } else background; self.uniforms.cursor_color = .{ uniform_color.r, @@ -2978,7 +2934,12 @@ pub fn Renderer(comptime GraphicsAPI: type) type { const range = preedit_range.?; var x = range.x[0]; for (preedit_v.codepoints[range.cp_offset..]) |cp| { - self.addPreeditCell(cp, .{ .x = x, .y = range.y }) catch |err| { + self.addPreeditCell( + cp, + .{ .x = x, .y = range.y }, + background, + foreground, + ) catch |err| { log.warn("error building preedit cell, will be invalid x={} y={}, err={}", .{ x, range.y, @@ -3253,10 +3214,12 @@ pub fn Renderer(comptime GraphicsAPI: type) type { self: *Self, cp: renderer.State.Preedit.Codepoint, coord: terminal.Coordinate, + screen_bg: terminal.color.RGB, + screen_fg: terminal.color.RGB, ) !void { // Preedit is rendered inverted - const bg = self.foreground_color orelse self.default_foreground_color; - const fg = self.background_color orelse self.default_background_color; + const bg = screen_fg; + const fg = screen_bg; // Render the glyph for our preedit text const render_ = self.font_grid.renderCodepoint( diff --git a/src/renderer/message.zig b/src/renderer/message.zig index d6255661f..e33922ae2 100644 --- a/src/renderer/message.zig +++ b/src/renderer/message.zig @@ -42,16 +42,6 @@ pub const Message = union(enum) { old_key: font.SharedGridSet.Key, }, - /// Change the foreground color as set by an OSC 10 command, if any. - foreground_color: ?terminal.color.RGB, - - /// Change the background color as set by an OSC 11 command, if any. - background_color: ?terminal.color.RGB, - - /// Change the cursor color. This can be done separately from changing the - /// config file in response to an OSC 12 command. - cursor_color: ?terminal.color.RGB, - /// Changes the size. The screen size might change, padding, grid, etc. resize: renderer.Size, diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 551145cfb..0131ff2e1 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -196,7 +196,6 @@ pub const StreamHandler = struct { .erase_display_above => self.terminal.eraseDisplay(.above, value), .erase_display_complete => { try self.terminal.scrollViewport(.{ .bottom = {} }); - try self.queueRender(); self.terminal.eraseDisplay(.complete, value); }, .erase_display_scrollback => self.terminal.eraseDisplay(.scrollback, value), @@ -569,10 +568,7 @@ pub const StreamHandler = struct { .autorepeat => {}, // Schedule a render since we changed colors - .reverse_colors => { - self.terminal.flags.dirty.reverse_colors = true; - try self.queueRender(); - }, + .reverse_colors => self.terminal.flags.dirty.reverse_colors = true, // Origin resets cursor pos. This is called whether or not // we're enabling or disabling origin mode and whether or @@ -588,17 +584,14 @@ pub const StreamHandler = struct { .alt_screen_legacy => { self.terminal.switchScreenMode(.@"47", enabled); - try self.queueRender(); }, .alt_screen => { self.terminal.switchScreenMode(.@"1047", enabled); - try self.queueRender(); }, .alt_screen_save_cursor_clear_enter => { self.terminal.switchScreenMode(.@"1049", enabled); - try self.queueRender(); }, // Mode 1048 is xterm's conditional save cursor depending @@ -634,7 +627,6 @@ pub const StreamHandler = struct { // forever. .synchronized_output => { if (enabled) self.messageWriter(.{ .start_synchronized_output = {} }); - try self.queueRender(); }, .linefeed => { @@ -1108,24 +1100,9 @@ pub const StreamHandler = struct { self.terminal.colors.palette.set(i, set.color); }, .dynamic => |dynamic| switch (dynamic) { - .foreground => { - self.terminal.colors.foreground.set(set.color); - self.rendererMessageWriter(.{ - .foreground_color = set.color, - }); - }, - .background => { - self.terminal.colors.background.set(set.color); - self.rendererMessageWriter(.{ - .background_color = set.color, - }); - }, - .cursor => { - self.terminal.colors.cursor.set(set.color); - self.rendererMessageWriter(.{ - .cursor_color = set.color, - }); - }, + .foreground => self.terminal.colors.foreground.set(set.color), + .background => self.terminal.colors.background.set(set.color), + .cursor => self.terminal.colors.cursor.set(set.color), .pointer_foreground, .pointer_background, .tektronix_foreground, @@ -1162,9 +1139,6 @@ pub const StreamHandler = struct { .dynamic => |dynamic| switch (dynamic) { .foreground => { self.terminal.colors.foreground.reset(); - self.rendererMessageWriter(.{ - .foreground_color = null, - }); if (self.terminal.colors.foreground.default) |c| { self.surfaceMessageWriter(.{ .color_change = .{ @@ -1175,9 +1149,6 @@ pub const StreamHandler = struct { }, .background => { self.terminal.colors.background.reset(); - self.rendererMessageWriter(.{ - .background_color = null, - }); if (self.terminal.colors.background.default) |c| { self.surfaceMessageWriter(.{ .color_change = .{ @@ -1189,10 +1160,6 @@ pub const StreamHandler = struct { .cursor => { self.terminal.colors.cursor.reset(); - self.rendererMessageWriter(.{ - .cursor_color = null, - }); - if (self.terminal.colors.cursor.default) |c| { self.surfaceMessageWriter(.{ .color_change = .{ .target = target, @@ -1396,32 +1363,17 @@ pub const StreamHandler = struct { self.terminal.colors.palette.set(palette, v.color); }, - .special => |special| { - const msg: renderer.Message = switch (special) { - .foreground => msg: { - self.terminal.colors.foreground.set(v.color); - break :msg .{ .foreground_color = v.color }; - }, - .background => msg: { - self.terminal.colors.background.set(v.color); - break :msg .{ .background_color = v.color }; - }, - .cursor => msg: { - self.terminal.colors.cursor.set(v.color); - break :msg .{ .cursor_color = v.color }; - }, - else => { - log.warn( - "ignoring unsupported kitty color protocol key: {f}", - .{v.key}, - ); - continue; - }, - }; - - // See messageWriter which has similar logic and - // explains why we may have to do this. - self.rendererMessageWriter(msg); + .special => |special| switch (special) { + .foreground => self.terminal.colors.foreground.set(v.color), + .background => self.terminal.colors.background.set(v.color), + .cursor => self.terminal.colors.cursor.set(v.color), + else => { + log.warn( + "ignoring unsupported kitty color protocol key: {f}", + .{v.key}, + ); + continue; + }, }, }, .reset => |key| switch (key) { @@ -1430,32 +1382,17 @@ pub const StreamHandler = struct { self.terminal.colors.palette.reset(palette); }, - .special => |special| { - const msg: renderer.Message = switch (special) { - .foreground => msg: { - self.terminal.colors.foreground.reset(); - break :msg .{ .foreground_color = null }; - }, - .background => msg: { - self.terminal.colors.background.reset(); - break :msg .{ .background_color = null }; - }, - .cursor => msg: { - self.terminal.colors.cursor.reset(); - break :msg .{ .cursor_color = null }; - }, - else => { - log.warn( - "ignoring unsupported kitty color protocol key: {f}", - .{key}, - ); - continue; - }, - }; - - // See messageWriter which has similar logic and - // explains why we may have to do this. - self.rendererMessageWriter(msg); + .special => |special| switch (special) { + .foreground => self.terminal.colors.foreground.reset(), + .background => self.terminal.colors.background.reset(), + .cursor => self.terminal.colors.cursor.reset(), + else => { + log.warn( + "ignoring unsupported kitty color protocol key: {f}", + .{key}, + ); + continue; + }, }, }, } From 27a98123a0ffaf589e8fd91940dca687c3b0f813 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 30 Oct 2025 09:58:27 -0700 Subject: [PATCH 4/6] terminal: readonly stream can update more colors now --- src/terminal/stream_readonly.zig | 270 +++++++++++++++++++++++++++++-- 1 file changed, 260 insertions(+), 10 deletions(-) diff --git a/src/terminal/stream_readonly.zig b/src/terminal/stream_readonly.zig index f73d21dce..e762fdf86 100644 --- a/src/terminal/stream_readonly.zig +++ b/src/terminal/stream_readonly.zig @@ -161,6 +161,7 @@ pub const Handler = struct { .end_of_command => self.terminal.screen.cursor.page_row.semantic_prompt = .input, .mouse_shape => self.terminal.mouse_shape = value, .color_operation => try self.colorOperation(value.op, &value.requests), + .kitty_color_report => try self.kittyColorOperation(value), // No supported DCS commands have any terminal-modifying effects, // but they may in the future. For now we just ignore it. @@ -186,7 +187,6 @@ pub const Handler = struct { .device_attributes, .device_status, .kitty_keyboard_query, - .kitty_color_report, .window_title, .report_pwd, .show_desktop_notification, @@ -305,21 +305,57 @@ pub const Handler = struct { switch (req.*) { .set => |set| { switch (set.target) { - .palette => |i| self.terminal.colors.palette.set(i, set.color), - .dynamic, - .special, - => {}, + .palette => |i| { + self.terminal.flags.dirty.palette = true; + self.terminal.colors.palette.set(i, set.color); + }, + .dynamic => |dynamic| switch (dynamic) { + .foreground => self.terminal.colors.foreground.set(set.color), + .background => self.terminal.colors.background.set(set.color), + .cursor => self.terminal.colors.cursor.set(set.color), + .pointer_foreground, + .pointer_background, + .tektronix_foreground, + .tektronix_background, + .highlight_background, + .tektronix_cursor, + .highlight_foreground, + => {}, + }, + .special => {}, } }, .reset => |target| switch (target) { - .palette => |i| self.terminal.colors.palette.reset(i), - .dynamic, - .special, - => {}, + .palette => |i| { + self.terminal.flags.dirty.palette = true; + self.terminal.colors.palette.reset(i); + }, + .dynamic => |dynamic| switch (dynamic) { + .foreground => self.terminal.colors.foreground.reset(), + .background => self.terminal.colors.background.reset(), + .cursor => self.terminal.colors.cursor.reset(), + .pointer_foreground, + .pointer_background, + .tektronix_foreground, + .tektronix_background, + .highlight_background, + .tektronix_cursor, + .highlight_foreground, + => {}, + }, + .special => {}, }, - .reset_palette => self.terminal.colors.palette.resetAll(), + .reset_palette => { + const mask = &self.terminal.colors.palette.mask; + var mask_it = mask.iterator(.{}); + while (mask_it.next()) |i| { + self.terminal.flags.dirty.palette = true; + self.terminal.colors.palette.reset(@intCast(i)); + } + mask.* = .initEmpty(); + }, .query, .reset_special, @@ -327,6 +363,41 @@ pub const Handler = struct { } } } + + fn kittyColorOperation( + self: *Handler, + request: @import("kitty/color.zig").OSC, + ) !void { + for (request.list.items) |item| { + switch (item) { + .set => |v| switch (v.key) { + .palette => |palette| { + self.terminal.flags.dirty.palette = true; + self.terminal.colors.palette.set(palette, v.color); + }, + .special => |special| switch (special) { + .foreground => self.terminal.colors.foreground.set(v.color), + .background => self.terminal.colors.background.set(v.color), + .cursor => self.terminal.colors.cursor.set(v.color), + else => {}, + }, + }, + .reset => |key| switch (key) { + .palette => |palette| { + self.terminal.flags.dirty.palette = true; + self.terminal.colors.palette.reset(palette); + }, + .special => |special| switch (special) { + .foreground => self.terminal.colors.foreground.reset(), + .background => self.terminal.colors.background.reset(), + .cursor => self.terminal.colors.cursor.reset(), + else => {}, + }, + }, + .query => {}, + } + } + } }; test "basic print" { @@ -624,3 +695,182 @@ test "OSC 104 reset all palette colors" { try testing.expect(!t.colors.palette.mask.isSet(1)); try testing.expect(!t.colors.palette.mask.isSet(2)); } + +test "OSC 10 set and reset foreground color" { + var t: Terminal = try .init(testing.allocator, .{ .cols = 10, .rows = 10 }); + defer t.deinit(testing.allocator); + + var s: Stream = .initAlloc(testing.allocator, .init(&t)); + defer s.deinit(); + + // Initially unset + try testing.expect(t.colors.foreground.get() == null); + + // Set foreground to red + try s.nextSlice("\x1b]10;rgb:ff/00/00\x1b\\"); + const fg = t.colors.foreground.get().?; + try testing.expectEqual(@as(u8, 0xff), fg.r); + try testing.expectEqual(@as(u8, 0x00), fg.g); + try testing.expectEqual(@as(u8, 0x00), fg.b); + + // Reset foreground + try s.nextSlice("\x1b]110\x1b\\"); + try testing.expect(t.colors.foreground.get() == null); +} + +test "OSC 11 set and reset background color" { + var t: Terminal = try .init(testing.allocator, .{ .cols = 10, .rows = 10 }); + defer t.deinit(testing.allocator); + + var s: Stream = .initAlloc(testing.allocator, .init(&t)); + defer s.deinit(); + + // Set background to green + try s.nextSlice("\x1b]11;rgb:00/ff/00\x1b\\"); + const bg = t.colors.background.get().?; + try testing.expectEqual(@as(u8, 0x00), bg.r); + try testing.expectEqual(@as(u8, 0xff), bg.g); + try testing.expectEqual(@as(u8, 0x00), bg.b); + + // Reset background + try s.nextSlice("\x1b]111\x1b\\"); + try testing.expect(t.colors.background.get() == null); +} + +test "OSC 12 set and reset cursor color" { + var t: Terminal = try .init(testing.allocator, .{ .cols = 10, .rows = 10 }); + defer t.deinit(testing.allocator); + + var s: Stream = .initAlloc(testing.allocator, .init(&t)); + defer s.deinit(); + + // Set cursor to blue + try s.nextSlice("\x1b]12;rgb:00/00/ff\x1b\\"); + const cursor = t.colors.cursor.get().?; + try testing.expectEqual(@as(u8, 0x00), cursor.r); + try testing.expectEqual(@as(u8, 0x00), cursor.g); + try testing.expectEqual(@as(u8, 0xff), cursor.b); + + // Reset cursor + try s.nextSlice("\x1b]112\x1b\\"); + // After reset, cursor might be null (using default) +} + +test "kitty color protocol set palette" { + var t: Terminal = try .init(testing.allocator, .{ .cols = 10, .rows = 10 }); + defer t.deinit(testing.allocator); + + var s: Stream = .initAlloc(testing.allocator, .init(&t)); + defer s.deinit(); + + // Set palette color 5 to magenta using kitty protocol + try s.nextSlice("\x1b]21;5=rgb:ff/00/ff\x1b\\"); + try testing.expectEqual(@as(u8, 0xff), t.colors.palette.current[5].r); + try testing.expectEqual(@as(u8, 0x00), t.colors.palette.current[5].g); + try testing.expectEqual(@as(u8, 0xff), t.colors.palette.current[5].b); + try testing.expect(t.colors.palette.mask.isSet(5)); + try testing.expect(t.flags.dirty.palette); +} + +test "kitty color protocol reset palette" { + var t: Terminal = try .init(testing.allocator, .{ .cols = 10, .rows = 10 }); + defer t.deinit(testing.allocator); + + var s: Stream = .initAlloc(testing.allocator, .init(&t)); + defer s.deinit(); + + // Set and then reset palette color + const original = t.colors.palette.original[7]; + try s.nextSlice("\x1b]21;7=rgb:aa/bb/cc\x1b\\"); + try testing.expect(t.colors.palette.mask.isSet(7)); + + try s.nextSlice("\x1b]21;7=\x1b\\"); + try testing.expectEqual(original, t.colors.palette.current[7]); + try testing.expect(!t.colors.palette.mask.isSet(7)); +} + +test "kitty color protocol set foreground" { + var t: Terminal = try .init(testing.allocator, .{ .cols = 10, .rows = 10 }); + defer t.deinit(testing.allocator); + + var s: Stream = .initAlloc(testing.allocator, .init(&t)); + defer s.deinit(); + + // Set foreground using kitty protocol + try s.nextSlice("\x1b]21;foreground=rgb:12/34/56\x1b\\"); + const fg = t.colors.foreground.get().?; + try testing.expectEqual(@as(u8, 0x12), fg.r); + try testing.expectEqual(@as(u8, 0x34), fg.g); + try testing.expectEqual(@as(u8, 0x56), fg.b); +} + +test "kitty color protocol set background" { + var t: Terminal = try .init(testing.allocator, .{ .cols = 10, .rows = 10 }); + defer t.deinit(testing.allocator); + + var s: Stream = .initAlloc(testing.allocator, .init(&t)); + defer s.deinit(); + + // Set background using kitty protocol + try s.nextSlice("\x1b]21;background=rgb:78/9a/bc\x1b\\"); + const bg = t.colors.background.get().?; + try testing.expectEqual(@as(u8, 0x78), bg.r); + try testing.expectEqual(@as(u8, 0x9a), bg.g); + try testing.expectEqual(@as(u8, 0xbc), bg.b); +} + +test "kitty color protocol set cursor" { + var t: Terminal = try .init(testing.allocator, .{ .cols = 10, .rows = 10 }); + defer t.deinit(testing.allocator); + + var s: Stream = .initAlloc(testing.allocator, .init(&t)); + defer s.deinit(); + + // Set cursor using kitty protocol + try s.nextSlice("\x1b]21;cursor=rgb:de/f0/12\x1b\\"); + const cursor = t.colors.cursor.get().?; + try testing.expectEqual(@as(u8, 0xde), cursor.r); + try testing.expectEqual(@as(u8, 0xf0), cursor.g); + try testing.expectEqual(@as(u8, 0x12), cursor.b); +} + +test "kitty color protocol reset foreground" { + var t: Terminal = try .init(testing.allocator, .{ .cols = 10, .rows = 10 }); + defer t.deinit(testing.allocator); + + var s: Stream = .initAlloc(testing.allocator, .init(&t)); + defer s.deinit(); + + // Set and reset foreground + try s.nextSlice("\x1b]21;foreground=rgb:11/22/33\x1b\\"); + try testing.expect(t.colors.foreground.get() != null); + + try s.nextSlice("\x1b]21;foreground=\x1b\\"); + // After reset, should be unset + try testing.expect(t.colors.foreground.get() == null); +} + +test "palette dirty flag set on color change" { + var t: Terminal = try .init(testing.allocator, .{ .cols = 10, .rows = 10 }); + defer t.deinit(testing.allocator); + + var s: Stream = .initAlloc(testing.allocator, .init(&t)); + defer s.deinit(); + + // Clear dirty flag + t.flags.dirty.palette = false; + + // Setting palette color should set dirty flag + try s.nextSlice("\x1b]4;0;rgb:ff/00/00\x1b\\"); + try testing.expect(t.flags.dirty.palette); + + // Clear and test reset + t.flags.dirty.palette = false; + try s.nextSlice("\x1b]104;0\x1b\\"); + try testing.expect(t.flags.dirty.palette); + + // Clear and test kitty protocol + t.flags.dirty.palette = false; + try s.nextSlice("\x1b]21;1=rgb:00/ff/00\x1b\\"); + try testing.expect(t.flags.dirty.palette); +} From 450155f15062df3b8b3e27e579ac7eb4f50cf2cb Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 30 Oct 2025 10:06:12 -0700 Subject: [PATCH 5/6] zig fmt --- src/terminal/formatter.zig | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/terminal/formatter.zig b/src/terminal/formatter.zig index 20dcf9a89..70cdd347b 100644 --- a/src/terminal/formatter.zig +++ b/src/terminal/formatter.zig @@ -225,24 +225,24 @@ pub const TerminalFormatter = struct { .plain => break :palette, .vt => { - for (self.terminal.colors.palette.current, 0..) |rgb, i| { - try writer.print( - "\x1b]4;{d};rgb:{x:0>2}/{x:0>2}/{x:0>2}\x1b\\", - .{ i, rgb.r, rgb.g, rgb.b }, - ); - } + for (self.terminal.colors.palette.current, 0..) |rgb, i| { + try writer.print( + "\x1b]4;{d};rgb:{x:0>2}/{x:0>2}/{x:0>2}\x1b\\", + .{ i, rgb.r, rgb.g, rgb.b }, + ); + } }, // For HTML, we emit CSS to setup our palette variables. .html => { - try writer.writeAll(""); + try writer.writeAll(""); }, } From 799e4bca505142d71864b61e40385e916d80e9bc Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 30 Oct 2025 10:07:42 -0700 Subject: [PATCH 6/6] example/zig-formatter: fix build for new palette API --- example/zig-formatter/src/main.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/zig-formatter/src/main.zig b/example/zig-formatter/src/main.zig index 87a8e4915..085b6d116 100644 --- a/example/zig-formatter/src/main.zig +++ b/example/zig-formatter/src/main.zig @@ -31,7 +31,7 @@ pub fn main() !void { // Use TerminalFormatter to emit HTML const formatter: ghostty_vt.formatter.TerminalFormatter = .init(&t, .{ .emit = .html, - .palette = &t.color_palette.colors, + .palette = &t.colors.palette.current, }); // Write to stdout