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| {