From f397353282f6cf7f1d69d29110d7038892863c19 Mon Sep 17 00:00:00 2001 From: Gregory Anders Date: Mon, 6 Nov 2023 21:40:20 -0600 Subject: [PATCH 1/8] core: implement querying with OSC 4 --- src/config/Config.zig | 13 +++++---- src/inspector/termio.zig | 19 ++++++++++++ src/terminal/osc.zig | 62 ++++++++++++++++++++++++++++++++++++++-- src/termio/Exec.zig | 5 ++-- 4 files changed, 88 insertions(+), 11 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 26425fb12..deef6777e 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -551,15 +551,16 @@ keybind: Keybinds = .{}, @"shell-integration-features": ShellIntegrationFeatures = .{}, /// Sets the reporting format for OSC sequences that request color information. -/// Ghostty currently supports OSC 10 (foreground) and OSC 11 (background) queries, -/// and by default the reported values are scaled-up RGB values, where each component -/// are 16 bits. This is how most terminals report these values. However, some legacy -/// applications may require 8-bit, unscaled, components. We also support turning off -/// reporting alltogether. The components are lowercase hex values. +/// Ghostty currently supports OSC 10 (foreground), OSC 11 (background), and OSC +/// 4 (256 color palette) queries, and by default the reported values are +/// scaled-up RGB values, where each component are 16 bits. This is how most +/// terminals report these values. However, some legacy applications may require +/// 8-bit, unscaled, components. We also support turning off reporting +/// alltogether. The components are lowercase hex values. /// /// Allowable values are: /// -/// * "none" - OSC 10/11 queries receive no reply +/// * "none" - OSC 4/10/11 queries receive no reply /// * "8-bit" - Color components are return unscaled, i.e. rr/gg/bb /// * "16-bit" - Color components are returned scaled, e.g. rrrr/gggg/bbbb /// diff --git a/src/inspector/termio.zig b/src/inspector/termio.zig index 78a161069..a9a2f5cde 100644 --- a/src/inspector/termio.zig +++ b/src/inspector/termio.zig @@ -241,6 +241,25 @@ pub const VTEvent = struct { try alloc.dupeZ(u8, @tagName(value)), ), + .Union => |u| if (u.tag_type) |Tag| { + const tag_name = @tagName(@as(Tag, value)); + inline for (u.fields) |field| { + if (std.mem.eql(u8, field.name, tag_name)) { + const s = if (field.type == void) + try alloc.dupeZ(u8, tag_name) + else + try std.fmt.allocPrintZ(alloc, "{s}={}", .{ + tag_name, + @field(value, field.name), + }); + + try md.put(key, s); + } + } + } else { + @compileError("Unions must have a tag"); + }, + else => switch (Value) { u8 => try md.put( key, diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 823f9ff67..9c5ac9f9f 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -94,9 +94,10 @@ pub const Command = union(enum) { value: []const u8, }, - /// OSC 10 and OSC 11 default color report. + /// OSC 4, OSC 10, and OSC 11 default color report. report_default_color: struct { - /// OSC 10 requests the foreground color, OSC 11 the background color. + /// OSC 4 requests a palette color, OSC 10 requests the foreground + /// color, OSC 11 the background color. kind: DefaultColorKind, /// We must reply with the same string terminator (ST) as used in the @@ -104,14 +105,16 @@ pub const Command = union(enum) { terminator: Terminator = .st, }, - pub const DefaultColorKind = enum { + pub const DefaultColorKind = union(enum) { foreground, background, + palette: u8, pub fn code(self: DefaultColorKind) []const u8 { return switch (self) { .foreground => "10", .background => "11", + .palette => "4", }; } }; @@ -199,6 +202,7 @@ pub const Parser = struct { @"133", @"2", @"22", + @"4", @"5", @"52", @"7", @@ -224,6 +228,10 @@ pub const Parser = struct { clipboard_kind, clipboard_kind_end, + // Get/set color palette index + color_palette_index, + color_palette_index_end, + // Expect a string parameter. param_str must be set as well as // buf_start. string, @@ -277,6 +285,7 @@ pub const Parser = struct { '0' => self.state = .@"0", '1' => self.state = .@"1", '2' => self.state = .@"2", + '4' => self.state = .@"4", '5' => self.state = .@"5", '7' => self.state = .@"7", else => self.state = .invalid, @@ -348,6 +357,39 @@ pub const Parser = struct { else => self.state = .invalid, }, + .@"4" => switch (c) { + ';' => { + self.state = .color_palette_index; + self.buf_start = self.buf_idx; + }, + else => self.state = .invalid, + }, + + .color_palette_index => switch (c) { + '0'...'9' => {}, + ';' => { + if (std.fmt.parseUnsigned(u8, self.buf[self.buf_start .. self.buf_idx - 1], 10)) |num| { + self.state = .color_palette_index_end; + self.temp_state = .{ .num = num }; + } else |err| switch (err) { + error.Overflow => self.state = .invalid, + error.InvalidCharacter => unreachable, + } + }, + else => self.state = .invalid, + }, + + .color_palette_index_end => switch (c) { + '?' => { + self.command = .{ .report_default_color = .{ + .kind = .{ .palette = @intCast(self.temp_state.num) }, + } }; + + self.complete = true; + }, + else => self.state = .invalid, + }, + .@"5" => switch (c) { '2' => self.state = .@"52", else => self.state = .invalid, @@ -921,3 +963,17 @@ test "OSC: report default background color" { try testing.expectEqual(cmd.report_default_color.kind, .background); try testing.expectEqual(cmd.report_default_color.terminator, .bel); } + +test "OSC: get palette color" { + const testing = std.testing; + + var p: Parser = .{}; + + const input = "4;1;?"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?; + try testing.expect(cmd == .report_default_color); + try testing.expectEqual(cmd.report_default_color.kind, .{ .palette = 1 }); + try testing.expectEqual(cmd.report_default_color.terminator, .st); +} diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index f48dbca0d..d342e02eb 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -2150,8 +2150,8 @@ const StreamHandler = struct { } } - /// Implements OSC 10 and OSC 11, which reports default foreground and - /// background color respectively. + /// Implements OSC 4, OSC 10, and OSC 11, which reports palette color, + /// default foreground color, and background color respectively. pub fn reportDefaultColor( self: *StreamHandler, kind: terminal.osc.Command.DefaultColorKind, @@ -2162,6 +2162,7 @@ const StreamHandler = struct { const color = switch (kind) { .foreground => self.default_foreground_color, .background => self.default_background_color, + .palette => |i| self.terminal.color_palette[i], }; var msg: termio.Message = .{ .write_small = .{} }; From 3b7e21df2675d40562b4ee90452acaec2031eb24 Mon Sep 17 00:00:00 2001 From: Gregory Anders Date: Thu, 9 Nov 2023 11:49:16 -0600 Subject: [PATCH 2/8] termio: update foreground and background color on config change --- src/termio/Exec.zig | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index d342e02eb..5b0ef2a0c 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -328,6 +328,10 @@ pub fn changeConfig(self: *Exec, config: *DerivedConfig) !void { self.default_cursor_style = config.cursor_style; self.default_cursor_blink = config.cursor_blink; + // Update foreground and background colors + self.default_foreground_color = config.foreground.toTerminalRGB(); + self.default_background_color = config.background.toTerminalRGB(); + // If we have event data, then update our active stream too if (self.data) |data| { data.terminal_stream.handler.changeDefaultCursor( From 006e93bd08ca998c108457587088c0179a7a8327 Mon Sep 17 00:00:00 2001 From: Gregory Anders Date: Thu, 9 Nov 2023 11:50:00 -0600 Subject: [PATCH 3/8] core: implement setting colors with OSC 4, 10, and 11 --- src/renderer/Thread.zig | 8 +++ src/renderer/message.zig | 9 +++ src/terminal/osc.zig | 84 ++++++++++++++++++++++++++- src/terminal/stream.zig | 7 +++ src/termio/Exec.zig | 119 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 224 insertions(+), 3 deletions(-) diff --git a/src/renderer/Thread.zig b/src/renderer/Thread.zig index f1c2280ca..35d0ee089 100644 --- a/src/renderer/Thread.zig +++ b/src/renderer/Thread.zig @@ -262,6 +262,14 @@ fn drainMailbox(self: *Thread) !void { try self.renderer.setFontSize(size); }, + .foreground_color => |color| { + self.renderer.config.foreground = color; + }, + + .background_color => |color| { + self.renderer.config.background = color; + }, + .resize => |v| { try self.renderer.setScreenSize(v.screen_size, v.padding); }, diff --git a/src/renderer/message.zig b/src/renderer/message.zig index d3fdc21de..873cbe7e5 100644 --- a/src/renderer/message.zig +++ b/src/renderer/message.zig @@ -3,6 +3,7 @@ const assert = std.debug.assert; const Allocator = std.mem.Allocator; const font = @import("../font/main.zig"); const renderer = @import("../renderer.zig"); +const terminal = @import("../terminal/main.zig"); /// The messages that can be sent to a renderer thread. pub const Message = union(enum) { @@ -20,6 +21,14 @@ pub const Message = union(enum) { /// the size changes. font_size: font.face.DesiredSize, + /// Change the foreground color. This can be done separately from changing + /// the config file in response to an OSC 10 command + foreground_color: terminal.color.RGB, + + /// Change the background color. This can be done separately from changing + /// the config file in response to an OSC 11 command + background_color: terminal.color.RGB, + /// Changes the screen size. resize: struct { /// The full screen (drawable) size. This does NOT include padding. diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 9c5ac9f9f..f73bcb6e8 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -105,6 +105,15 @@ pub const Command = union(enum) { terminator: Terminator = .st, }, + set_default_color: struct { + /// OSC 4 sets a palette color, OSC 10 sets the foreground color, OSC 11 + /// the background color. + kind: DefaultColorKind, + + /// The color spec as a string + value: []const u8, + }, + pub const DefaultColorKind = union(enum) { foreground, background, @@ -387,7 +396,16 @@ pub const Parser = struct { self.complete = true; }, - else => self.state = .invalid, + else => { + self.command = .{ .set_default_color = .{ + .kind = .{ .palette = @intCast(self.temp_state.num) }, + .value = "", + } }; + + self.state = .string; + self.temp_state = .{ .str = &self.command.set_default_color.value }; + self.buf_start = self.buf_idx - 1; + }, }, .@"5" => switch (c) { @@ -441,7 +459,16 @@ pub const Parser = struct { self.command = .{ .report_default_color = .{ .kind = .foreground } }; self.complete = true; }, - else => self.state = .invalid, + else => { + self.command = .{ .set_default_color = .{ + .kind = .foreground, + .value = "", + } }; + + self.state = .string; + self.temp_state = .{ .str = &self.command.set_default_color.value }; + self.buf_start = self.buf_idx - 1; + }, }, .query_default_bg => switch (c) { @@ -449,7 +476,16 @@ pub const Parser = struct { self.command = .{ .report_default_color = .{ .kind = .background } }; self.complete = true; }, - else => self.state = .invalid, + else => { + self.command = .{ .set_default_color = .{ + .kind = .background, + .value = "", + } }; + + self.state = .string; + self.temp_state = .{ .str = &self.command.set_default_color.value }; + self.buf_start = self.buf_idx - 1; + }, }, .semantic_prompt => switch (c) { @@ -949,6 +985,20 @@ test "OSC: report default foreground color" { try testing.expectEqual(cmd.report_default_color.terminator, .st); } +test "OSC: set foreground color" { + const testing = std.testing; + + var p: Parser = .{}; + + const input = "10;rgbi:0.0/0.5/1.0"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x07').?; + try testing.expect(cmd == .set_default_color); + try testing.expectEqual(cmd.set_default_color.kind, .foreground); + try testing.expectEqualStrings(cmd.set_default_color.value, "rgbi:0.0/0.5/1.0"); +} + test "OSC: report default background color" { const testing = std.testing; @@ -964,6 +1014,20 @@ test "OSC: report default background color" { try testing.expectEqual(cmd.report_default_color.terminator, .bel); } +test "OSC: set background color" { + const testing = std.testing; + + var p: Parser = .{}; + + const input = "11;rgb:f/ff/ffff"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?; + try testing.expect(cmd == .set_default_color); + try testing.expectEqual(cmd.set_default_color.kind, .background); + try testing.expectEqualStrings(cmd.set_default_color.value, "rgb:f/ff/ffff"); +} + test "OSC: get palette color" { const testing = std.testing; @@ -977,3 +1041,17 @@ test "OSC: get palette color" { try testing.expectEqual(cmd.report_default_color.kind, .{ .palette = 1 }); try testing.expectEqual(cmd.report_default_color.terminator, .st); } + +test "OSC: set palette color" { + const testing = std.testing; + + var p: Parser = .{}; + + const input = "4;17;rgb:aa/bb/cc"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?; + try testing.expect(cmd == .set_default_color); + try testing.expectEqual(cmd.set_default_color.kind, .{ .palette = 17 }); + try testing.expectEqualStrings(cmd.set_default_color.value, "rgb:aa/bb/cc"); +} diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index 6f27a9b06..17cea8471 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -1052,6 +1052,13 @@ pub fn Stream(comptime Handler: type) type { } else log.warn("unimplemented OSC callback: {}", .{cmd}); }, + .set_default_color => |v| { + if (@hasDecl(T, "setDefaultColor")) { + try self.handler.setDefaultColor(v.kind, v.value); + return; + } else log.warn("unimplemented OSC callback: {}", .{cmd}); + }, + else => if (@hasDecl(T, "oscUnimplemented")) try self.handler.oscUnimplemented(cmd) else diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 5b0ef2a0c..e3c1a395c 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -2200,4 +2200,123 @@ const StreamHandler = struct { msg.write_small.len = @intCast(resp.len); self.messageWriter(msg); } + + /// Parse a color from a string of hexadecimal digits or a floating point + /// intensity value. + /// + /// If `intensity` is false, the string can contain 1, 2, 3, or 4 characters + /// and represents the color value scaled in 4, 8, 12, or 16 bits, + /// respectively. + /// + /// If `intensity` is true, the string should contain a floating point value + /// between 0.0 and 1.0, inclusive. + fn parseColor(value: []const u8, intensity: bool) !u8 { + if (intensity) { + const i = try std.fmt.parseFloat(f64, value); + if (i < 0.0 or i > 1.0) { + return error.InvalidValue; + } + + return @intFromFloat(i * std.math.maxInt(u8)); + } + + if (value.len == 0 or value.len > 4) { + return error.InvalidValue; + } + + const color = try std.fmt.parseUnsigned(u16, value, 16); + const divisor: usize = switch (value.len) { + 1 => std.math.maxInt(u4), + 2 => std.math.maxInt(u8), + 3 => std.math.maxInt(u12), + 4 => std.math.maxInt(u16), + else => unreachable, + }; + + return @intCast(color * std.math.maxInt(u8) / divisor); + } + + /// Parse a color specification of the form + /// + /// rgb:// + /// + /// , , := h | hh | hhh | hhhh + /// + /// where `h` is a single hexadecimal digit. + /// + /// Alternatively, the form + /// + /// rgbi:// + /// + /// where , , and are floating point values between 0.0 + /// and 1.0 (inclusive) is also accepted. + fn parseColorSpec(value: []const u8) !terminal.color.RGB { + const minimum_length = "rgb:a/a/a".len; + if (value.len < minimum_length or !std.mem.eql(u8, value[0..3], "rgb")) { + return error.InvalidFormat; + } + + var i: usize = 3; + + const use_intensity = if (value[i] == 'i') blk: { + i += 1; + break :blk true; + } else false; + + if (value[i] != ':') { + return error.InvalidFormat; + } + + i += 1; + + const r = r: { + const slash_i = std.mem.indexOfScalarPos(u8, value, i, '/') orelse + return error.InvalidFormat; + + const r = try parseColor(value[i..slash_i], use_intensity); + i = slash_i + 1; + break :r r; + }; + + const g = g: { + const slash_i = std.mem.indexOfScalarPos(u8, value, i, '/') orelse + return error.InvalidFormat; + + const g = try parseColor(value[i..slash_i], use_intensity); + i = slash_i + 1; + break :g g; + }; + + const b = try parseColor(value[i..], use_intensity); + + return terminal.color.RGB{ + .r = r, + .g = g, + .b = b, + }; + } + + pub fn setDefaultColor( + self: *StreamHandler, + kind: terminal.osc.Command.DefaultColorKind, + value: []const u8, + ) !void { + const color = try parseColorSpec(value); + + switch (kind) { + .foreground => { + self.default_foreground_color = color; + _ = self.ev.renderer_mailbox.push(.{ + .foreground_color = color, + }, .{ .forever = {} }); + }, + .background => { + self.default_background_color = color; + _ = self.ev.renderer_mailbox.push(.{ + .background_color = color, + }, .{ .forever = {} }); + }, + .palette => |i| self.terminal.color_palette[i] = color, + } + } }; From 49feaedef6e32b0d9cb5220e93d37ef7eb5079a3 Mon Sep 17 00:00:00 2001 From: Gregory Anders Date: Thu, 9 Nov 2023 14:06:06 -0600 Subject: [PATCH 4/8] core: move color parsing functions into RGB namespace --- src/terminal/color.zig | 126 +++++++++++++++++++++++++++++++++++++++++ src/termio/Exec.zig | 97 +------------------------------ 2 files changed, 127 insertions(+), 96 deletions(-) diff --git a/src/terminal/color.zig b/src/terminal/color.zig index 29e3f39de..eb2fecf5a 100644 --- a/src/terminal/color.zig +++ b/src/terminal/color.zig @@ -147,6 +147,112 @@ pub const RGB = struct { try std.testing.expectEqual(@as(usize, 24), @bitSizeOf(RGB)); try std.testing.expectEqual(@as(usize, 3), @sizeOf(RGB)); } + + /// Parse a color from a floating point intensity value. + /// + /// The value should be between 0.0 and 1.0, inclusive. + fn fromIntensity(value: []const u8) !u8 { + const i = std.fmt.parseFloat(f64, value) catch return error.InvalidFormat; + if (i < 0.0 or i > 1.0) { + return error.InvalidFormat; + } + + return @intFromFloat(i * std.math.maxInt(u8)); + } + + /// Parse a color from a string of hexadecimal digits + /// + /// The string can contain 1, 2, 3, or 4 characters and represents the color + /// value scaled in 4, 8, 12, or 16 bits, respectively. + fn fromHex(value: []const u8) !u8 { + if (value.len == 0 or value.len > 4) { + return error.InvalidFormat; + } + + const color = std.fmt.parseUnsigned(u16, value, 16) catch return error.InvalidFormat; + const divisor: usize = switch (value.len) { + 1 => std.math.maxInt(u4), + 2 => std.math.maxInt(u8), + 3 => std.math.maxInt(u12), + 4 => std.math.maxInt(u16), + else => unreachable, + }; + + return @intCast(@as(usize, color) * std.math.maxInt(u8) / divisor); + } + + /// Parse a color specification of the form + /// + /// rgb:// + /// + /// , , := h | hh | hhh | hhhh + /// + /// where `h` is a single hexadecimal digit. + /// + /// Alternatively, the form + /// + /// rgbi:// + /// + /// where , , and are floating point values between 0.0 + /// and 1.0 (inclusive) is also accepted. + pub fn parse(value: []const u8) !RGB { + const minimum_length = "rgb:a/a/a".len; + if (value.len < minimum_length or !std.mem.eql(u8, value[0..3], "rgb")) { + return error.InvalidFormat; + } + + var i: usize = 3; + + const use_intensity = if (value[i] == 'i') blk: { + i += 1; + break :blk true; + } else false; + + if (value[i] != ':') { + return error.InvalidFormat; + } + + i += 1; + + const r = r: { + const slice = if (std.mem.indexOfScalarPos(u8, value, i, '/')) |end| + value[i..end] + else + return error.InvalidFormat; + + i += slice.len + 1; + + break :r if (use_intensity) + try RGB.fromIntensity(slice) + else + try RGB.fromHex(slice); + }; + + const g = g: { + const slice = if (std.mem.indexOfScalarPos(u8, value, i, '/')) |end| + value[i..end] + else + return error.InvalidFormat; + + i += slice.len + 1; + + break :g if (use_intensity) + try RGB.fromIntensity(slice) + else + try RGB.fromHex(slice); + }; + + const b = if (use_intensity) + try RGB.fromIntensity(value[i..]) + else + try RGB.fromHex(value[i..]); + + return RGB{ + .r = r, + .g = g, + .b = b, + }; + } }; test "palette: default" { @@ -158,3 +264,23 @@ test "palette: default" { try testing.expectEqual(Name.default(@as(Name, @enumFromInt(i))), default[i]); } } + +test "RGB.parse" { + const testing = std.testing; + + try testing.expectEqual(RGB{ .r = 255, .g = 0, .b = 0 }, try RGB.parse("rgbi:1.0/0/0")); + try testing.expectEqual(RGB{ .r = 127, .g = 160, .b = 0 }, try RGB.parse("rgb:7f/a0a0/0")); + try testing.expectEqual(RGB{ .r = 255, .g = 255, .b = 255 }, try RGB.parse("rgb:f/ff/fff")); + + // Invalid format + try testing.expectError(error.InvalidFormat, RGB.parse("rgb;")); + try testing.expectError(error.InvalidFormat, RGB.parse("rgb:")); + try testing.expectError(error.InvalidFormat, RGB.parse(":a/a/a")); + try testing.expectError(error.InvalidFormat, RGB.parse("a/a/a")); + try testing.expectError(error.InvalidFormat, RGB.parse("rgb:a/a/a/")); + try testing.expectError(error.InvalidFormat, RGB.parse("rgb:00000///")); + try testing.expectError(error.InvalidFormat, RGB.parse("rgb:000/")); + try testing.expectError(error.InvalidFormat, RGB.parse("rgbi:a/a/a")); + try testing.expectError(error.InvalidFormat, RGB.parse("rgb:0.5/0.0/1.0")); + try testing.expectError(error.InvalidFormat, RGB.parse("rgb:not/hex/zz")); +} diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index e3c1a395c..d875c6bfe 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -2201,107 +2201,12 @@ const StreamHandler = struct { self.messageWriter(msg); } - /// Parse a color from a string of hexadecimal digits or a floating point - /// intensity value. - /// - /// If `intensity` is false, the string can contain 1, 2, 3, or 4 characters - /// and represents the color value scaled in 4, 8, 12, or 16 bits, - /// respectively. - /// - /// If `intensity` is true, the string should contain a floating point value - /// between 0.0 and 1.0, inclusive. - fn parseColor(value: []const u8, intensity: bool) !u8 { - if (intensity) { - const i = try std.fmt.parseFloat(f64, value); - if (i < 0.0 or i > 1.0) { - return error.InvalidValue; - } - - return @intFromFloat(i * std.math.maxInt(u8)); - } - - if (value.len == 0 or value.len > 4) { - return error.InvalidValue; - } - - const color = try std.fmt.parseUnsigned(u16, value, 16); - const divisor: usize = switch (value.len) { - 1 => std.math.maxInt(u4), - 2 => std.math.maxInt(u8), - 3 => std.math.maxInt(u12), - 4 => std.math.maxInt(u16), - else => unreachable, - }; - - return @intCast(color * std.math.maxInt(u8) / divisor); - } - - /// Parse a color specification of the form - /// - /// rgb:// - /// - /// , , := h | hh | hhh | hhhh - /// - /// where `h` is a single hexadecimal digit. - /// - /// Alternatively, the form - /// - /// rgbi:// - /// - /// where , , and are floating point values between 0.0 - /// and 1.0 (inclusive) is also accepted. - fn parseColorSpec(value: []const u8) !terminal.color.RGB { - const minimum_length = "rgb:a/a/a".len; - if (value.len < minimum_length or !std.mem.eql(u8, value[0..3], "rgb")) { - return error.InvalidFormat; - } - - var i: usize = 3; - - const use_intensity = if (value[i] == 'i') blk: { - i += 1; - break :blk true; - } else false; - - if (value[i] != ':') { - return error.InvalidFormat; - } - - i += 1; - - const r = r: { - const slash_i = std.mem.indexOfScalarPos(u8, value, i, '/') orelse - return error.InvalidFormat; - - const r = try parseColor(value[i..slash_i], use_intensity); - i = slash_i + 1; - break :r r; - }; - - const g = g: { - const slash_i = std.mem.indexOfScalarPos(u8, value, i, '/') orelse - return error.InvalidFormat; - - const g = try parseColor(value[i..slash_i], use_intensity); - i = slash_i + 1; - break :g g; - }; - - const b = try parseColor(value[i..], use_intensity); - - return terminal.color.RGB{ - .r = r, - .g = g, - .b = b, - }; - } - pub fn setDefaultColor( self: *StreamHandler, kind: terminal.osc.Command.DefaultColorKind, value: []const u8, ) !void { - const color = try parseColorSpec(value); + const color = try terminal.color.RGB.parse(value); switch (kind) { .foreground => { From 1c0b79c40fc683aed06a7f49ec86701986256d38 Mon Sep 17 00:00:00 2001 From: Gregory Anders Date: Thu, 9 Nov 2023 14:08:14 -0600 Subject: [PATCH 5/8] core: separate default colors from modifiable colors Default colors are those set by the user in the config file, or an actual default value if unset. The actual colors are modifiable and can be changed using the OSC 4, 10, and 11 sequences. --- src/renderer/Metal.zig | 40 ++++++++++++++++---------- src/renderer/Thread.zig | 4 +-- src/terminal/osc.zig | 62 ++++++++++++++++++++--------------------- src/terminal/stream.zig | 12 ++++---- src/termio/Exec.zig | 39 ++++++++++++++++++++------ 5 files changed, 94 insertions(+), 63 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index d97ef6f21..73157ea59 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -64,6 +64,14 @@ padding: renderer.Options.Padding, /// True if the window is focused focused: bool, +/// The actual foreground color. May differ from the config foreground color if +/// changed by a terminal application +foreground_color: terminal.color.RGB, + +/// The actual background color. May differ from the config background color if +/// changed by a terminal application +background_color: terminal.color.RGB, + /// 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. @@ -254,6 +262,8 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { .screen_size = null, .padding = options.padding, .focused = true, + .foreground_color = options.config.foreground, + .background_color = options.config.background, // Render state .cells_bg = .{}, @@ -461,15 +471,15 @@ pub fn render( } // Swap bg/fg if the terminal is reversed - const bg = self.config.background; - const fg = self.config.foreground; + const bg = self.background_color; + const fg = self.foreground_color; defer { - self.config.background = bg; - self.config.foreground = fg; + self.background_color = bg; + self.foreground_color = fg; } if (state.terminal.modes.get(.reverse_colors)) { - self.config.background = fg; - self.config.foreground = bg; + self.background_color = fg; + self.foreground_color = bg; } // We used to share terminal state, but we've since learned through @@ -509,7 +519,7 @@ pub fn render( } break :critical .{ - .bg = self.config.background, + .bg = self.background_color, .selection = selection, .screen = screen_copy, .preedit = if (cursor_style != null) state.preedit else null, @@ -1267,21 +1277,21 @@ pub fn updateCell( const colors: BgFg = colors: { // If we are selected, we our colors are just inverted fg/bg var selection_res: ?BgFg = if (selected) .{ - .bg = self.config.selection_background orelse self.config.foreground, - .fg = self.config.selection_foreground orelse self.config.background, + .bg = self.config.selection_background orelse self.foreground_color, + .fg = self.config.selection_foreground orelse self.background_color, } else null; const res: BgFg = selection_res orelse if (!cell.attrs.inverse) .{ // In normal mode, background and fg match the cell. We // un-optionalize the fg by defaulting to our fg color. .bg = if (cell.attrs.has_bg) cell.bg else null, - .fg = if (cell.attrs.has_fg) cell.fg else self.config.foreground, + .fg = if (cell.attrs.has_fg) cell.fg else self.foreground_color, } else .{ // In inverted mode, the background MUST be set to something // (is never null) so it is either the fg or default fg. The // fg is either the bg or default background. - .bg = if (cell.attrs.has_fg) cell.fg else self.config.foreground, - .fg = if (cell.attrs.has_bg) cell.bg else self.config.background, + .bg = if (cell.attrs.has_fg) cell.fg else self.foreground_color, + .fg = if (cell.attrs.has_bg) cell.bg else self.background_color, }; // If the cell is "invisible" then we just make fg = bg so that @@ -1289,7 +1299,7 @@ pub fn updateCell( if (cell.attrs.invisible) { break :colors BgFg{ .bg = res.bg, - .fg = res.bg orelse self.config.background, + .fg = res.bg orelse self.background_color, }; } @@ -1316,7 +1326,7 @@ pub fn updateCell( // If we have a background and its not the default background // then we apply background opacity - if (cell.attrs.has_bg and !std.meta.eql(rgb, self.config.background)) { + if (cell.attrs.has_bg and !std.meta.eql(rgb, self.background_color)) { break :bg_alpha alpha; } @@ -1434,7 +1444,7 @@ fn addCursor( ), screen.cursor.x - 1 }; }; - const color = self.config.cursor_color orelse self.config.foreground; + const color = self.config.cursor_color orelse self.foreground_color; const alpha: u8 = if (!self.focused) 255 else alpha: { const alpha = 255 * self.config.cursor_opacity; break :alpha @intFromFloat(@ceil(alpha)); diff --git a/src/renderer/Thread.zig b/src/renderer/Thread.zig index 35d0ee089..654f370ad 100644 --- a/src/renderer/Thread.zig +++ b/src/renderer/Thread.zig @@ -263,11 +263,11 @@ fn drainMailbox(self: *Thread) !void { }, .foreground_color => |color| { - self.renderer.config.foreground = color; + self.renderer.foreground_color = color; }, .background_color => |color| { - self.renderer.config.background = color; + self.renderer.background_color = color; }, .resize => |v| { diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index f73bcb6e8..e16b4e31a 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -94,8 +94,8 @@ pub const Command = union(enum) { value: []const u8, }, - /// OSC 4, OSC 10, and OSC 11 default color report. - report_default_color: struct { + /// OSC 4, OSC 10, and OSC 11 color report. + report_color: struct { /// OSC 4 requests a palette color, OSC 10 requests the foreground /// color, OSC 11 the background color. kind: DefaultColorKind, @@ -105,7 +105,7 @@ pub const Command = union(enum) { terminator: Terminator = .st, }, - set_default_color: struct { + set_color: struct { /// OSC 4 sets a palette color, OSC 10 sets the foreground color, OSC 11 /// the background color. kind: DefaultColorKind, @@ -390,20 +390,20 @@ pub const Parser = struct { .color_palette_index_end => switch (c) { '?' => { - self.command = .{ .report_default_color = .{ + self.command = .{ .report_color = .{ .kind = .{ .palette = @intCast(self.temp_state.num) }, } }; self.complete = true; }, else => { - self.command = .{ .set_default_color = .{ + self.command = .{ .set_color = .{ .kind = .{ .palette = @intCast(self.temp_state.num) }, .value = "", } }; self.state = .string; - self.temp_state = .{ .str = &self.command.set_default_color.value }; + self.temp_state = .{ .str = &self.command.set_color.value }; self.buf_start = self.buf_idx - 1; }, }, @@ -456,34 +456,34 @@ pub const Parser = struct { .query_default_fg => switch (c) { '?' => { - self.command = .{ .report_default_color = .{ .kind = .foreground } }; + self.command = .{ .report_color = .{ .kind = .foreground } }; self.complete = true; }, else => { - self.command = .{ .set_default_color = .{ + self.command = .{ .set_color = .{ .kind = .foreground, .value = "", } }; self.state = .string; - self.temp_state = .{ .str = &self.command.set_default_color.value }; + self.temp_state = .{ .str = &self.command.set_color.value }; self.buf_start = self.buf_idx - 1; }, }, .query_default_bg => switch (c) { '?' => { - self.command = .{ .report_default_color = .{ .kind = .background } }; + self.command = .{ .report_color = .{ .kind = .background } }; self.complete = true; }, else => { - self.command = .{ .set_default_color = .{ + self.command = .{ .set_color = .{ .kind = .background, .value = "", } }; self.state = .string; - self.temp_state = .{ .str = &self.command.set_default_color.value }; + self.temp_state = .{ .str = &self.command.set_color.value }; self.buf_start = self.buf_idx - 1; }, }, @@ -694,7 +694,7 @@ pub const Parser = struct { } switch (self.command) { - .report_default_color => |*c| c.terminator = Terminator.init(terminator_ch), + .report_color => |*c| c.terminator = Terminator.init(terminator_ch), else => {}, } @@ -980,9 +980,9 @@ test "OSC: report default foreground color" { // This corresponds to ST = ESC followed by \ const cmd = p.end('\x1b').?; - try testing.expect(cmd == .report_default_color); - try testing.expectEqual(cmd.report_default_color.kind, .foreground); - try testing.expectEqual(cmd.report_default_color.terminator, .st); + try testing.expect(cmd == .report_color); + try testing.expectEqual(cmd.report_color.kind, .foreground); + try testing.expectEqual(cmd.report_color.terminator, .st); } test "OSC: set foreground color" { @@ -994,9 +994,9 @@ test "OSC: set foreground color" { for (input) |ch| p.next(ch); const cmd = p.end('\x07').?; - try testing.expect(cmd == .set_default_color); - try testing.expectEqual(cmd.set_default_color.kind, .foreground); - try testing.expectEqualStrings(cmd.set_default_color.value, "rgbi:0.0/0.5/1.0"); + try testing.expect(cmd == .set_color); + try testing.expectEqual(cmd.set_color.kind, .foreground); + try testing.expectEqualStrings(cmd.set_color.value, "rgbi:0.0/0.5/1.0"); } test "OSC: report default background color" { @@ -1009,9 +1009,9 @@ test "OSC: report default background color" { // This corresponds to ST = BEL character const cmd = p.end('\x07').?; - try testing.expect(cmd == .report_default_color); - try testing.expectEqual(cmd.report_default_color.kind, .background); - try testing.expectEqual(cmd.report_default_color.terminator, .bel); + try testing.expect(cmd == .report_color); + try testing.expectEqual(cmd.report_color.kind, .background); + try testing.expectEqual(cmd.report_color.terminator, .bel); } test "OSC: set background color" { @@ -1023,9 +1023,9 @@ test "OSC: set background color" { for (input) |ch| p.next(ch); const cmd = p.end('\x1b').?; - try testing.expect(cmd == .set_default_color); - try testing.expectEqual(cmd.set_default_color.kind, .background); - try testing.expectEqualStrings(cmd.set_default_color.value, "rgb:f/ff/ffff"); + try testing.expect(cmd == .set_color); + try testing.expectEqual(cmd.set_color.kind, .background); + try testing.expectEqualStrings(cmd.set_color.value, "rgb:f/ff/ffff"); } test "OSC: get palette color" { @@ -1037,9 +1037,9 @@ test "OSC: get palette color" { for (input) |ch| p.next(ch); const cmd = p.end('\x1b').?; - try testing.expect(cmd == .report_default_color); - try testing.expectEqual(cmd.report_default_color.kind, .{ .palette = 1 }); - try testing.expectEqual(cmd.report_default_color.terminator, .st); + try testing.expect(cmd == .report_color); + try testing.expectEqual(cmd.report_color.kind, .{ .palette = 1 }); + try testing.expectEqual(cmd.report_color.terminator, .st); } test "OSC: set palette color" { @@ -1051,7 +1051,7 @@ test "OSC: set palette color" { for (input) |ch| p.next(ch); const cmd = p.end('\x1b').?; - try testing.expect(cmd == .set_default_color); - try testing.expectEqual(cmd.set_default_color.kind, .{ .palette = 17 }); - try testing.expectEqualStrings(cmd.set_default_color.value, "rgb:aa/bb/cc"); + try testing.expect(cmd == .set_color); + try testing.expectEqual(cmd.set_color.kind, .{ .palette = 17 }); + try testing.expectEqualStrings(cmd.set_color.value, "rgb:aa/bb/cc"); } diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index 17cea8471..d7d306922 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -1045,16 +1045,16 @@ pub fn Stream(comptime Handler: type) type { } else log.warn("unimplemented OSC callback: {}", .{cmd}); }, - .report_default_color => |v| { - if (@hasDecl(T, "reportDefaultColor")) { - try self.handler.reportDefaultColor(v.kind, v.terminator); + .report_color => |v| { + if (@hasDecl(T, "reportColor")) { + try self.handler.reportColor(v.kind, v.terminator); return; } else log.warn("unimplemented OSC callback: {}", .{cmd}); }, - .set_default_color => |v| { - if (@hasDecl(T, "setDefaultColor")) { - try self.handler.setDefaultColor(v.kind, v.value); + .set_color => |v| { + if (@hasDecl(T, "setColor")) { + try self.handler.setColor(v.kind, v.value); return; } else log.warn("unimplemented OSC callback: {}", .{cmd}); }, diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index d875c6bfe..a0da3e9c4 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -70,12 +70,18 @@ grid_size: renderer.GridSize, default_cursor_style: terminal.Cursor.Style, default_cursor_blink: ?bool, -/// Default foreground color for OSC 10 reporting. +/// Default foreground color as set by the config file default_foreground_color: terminal.color.RGB, -/// Default background color for OSC 11 reporting. +/// Default background color as set by the config file default_background_color: terminal.color.RGB, +/// Actual foreground color +foreground_color: terminal.color.RGB, + +/// Actual background color +background_color: terminal.color.RGB, + /// The OSC 10/11 reply style. osc_color_report_format: configpkg.Config.OSCColorReportFormat, @@ -171,6 +177,8 @@ pub fn init(alloc: Allocator, opts: termio.Options) !Exec { .default_cursor_blink = opts.config.cursor_blink, .default_foreground_color = config.foreground.toTerminalRGB(), .default_background_color = config.background.toTerminalRGB(), + .foreground_color = config.foreground.toTerminalRGB(), + .background_color = config.background.toTerminalRGB(), .osc_color_report_format = config.osc_color_report_format, .data = null, }; @@ -238,6 +246,8 @@ pub fn threadEnter(self: *Exec, thread: *termio.Thread) !ThreadData { .default_cursor_blink = self.default_cursor_blink, .default_foreground_color = self.default_foreground_color, .default_background_color = self.default_background_color, + .foreground_color = self.foreground_color, + .background_color = self.background_color, .osc_color_report_format = self.osc_color_report_format, }, @@ -328,7 +338,7 @@ pub fn changeConfig(self: *Exec, config: *DerivedConfig) !void { self.default_cursor_style = config.cursor_style; self.default_cursor_blink = config.cursor_blink; - // Update foreground and background colors + // Update default foreground and background colors self.default_foreground_color = config.foreground.toTerminalRGB(); self.default_background_color = config.background.toTerminalRGB(); @@ -1330,8 +1340,19 @@ const StreamHandler = struct { default_cursor: bool = true, default_cursor_style: terminal.Cursor.Style, default_cursor_blink: ?bool, + + /// The default foreground and background color are those set by the user's + /// config file. These can be overridden by terminal applications using OSC + /// 10 and OSC 11, respectively. default_foreground_color: terminal.color.RGB, default_background_color: terminal.color.RGB, + + /// The actual foreground and background color. Normally this will be the + /// same as the default foreground and background color, unless changed by a + /// terminal application. + foreground_color: terminal.color.RGB, + background_color: terminal.color.RGB, + osc_color_report_format: configpkg.Config.OSCColorReportFormat, pub fn deinit(self: *StreamHandler) void { @@ -2156,7 +2177,7 @@ const StreamHandler = struct { /// Implements OSC 4, OSC 10, and OSC 11, which reports palette color, /// default foreground color, and background color respectively. - pub fn reportDefaultColor( + pub fn reportColor( self: *StreamHandler, kind: terminal.osc.Command.DefaultColorKind, terminator: terminal.osc.Terminator, @@ -2164,8 +2185,8 @@ const StreamHandler = struct { if (self.osc_color_report_format == .none) return; const color = switch (kind) { - .foreground => self.default_foreground_color, - .background => self.default_background_color, + .foreground => self.foreground_color, + .background => self.background_color, .palette => |i| self.terminal.color_palette[i], }; @@ -2201,7 +2222,7 @@ const StreamHandler = struct { self.messageWriter(msg); } - pub fn setDefaultColor( + pub fn setColor( self: *StreamHandler, kind: terminal.osc.Command.DefaultColorKind, value: []const u8, @@ -2210,13 +2231,13 @@ const StreamHandler = struct { switch (kind) { .foreground => { - self.default_foreground_color = color; + self.foreground_color = color; _ = self.ev.renderer_mailbox.push(.{ .foreground_color = color, }, .{ .forever = {} }); }, .background => { - self.default_background_color = color; + self.background_color = color; _ = self.ev.renderer_mailbox.push(.{ .background_color = color, }, .{ .forever = {} }); From 33753f59c861958fe3f4fdab07840bffe3288a41 Mon Sep 17 00:00:00 2001 From: Gregory Anders Date: Thu, 9 Nov 2023 16:10:05 -0600 Subject: [PATCH 6/8] core: implement OSC 104, 110, and 111 to reset colors --- src/renderer/OpenGL.zig | 40 ++++++++++++-------- src/renderer/message.zig | 4 +- src/terminal/Terminal.zig | 24 ++++++++---- src/terminal/osc.zig | 78 +++++++++++++++++++++++++++++++-------- src/terminal/stream.zig | 10 +++-- src/termio/Exec.zig | 64 +++++++++++++++++++++++++++++--- 6 files changed, 169 insertions(+), 51 deletions(-) diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 6ee23fc92..828ffa090 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -67,6 +67,14 @@ font_shaper: font.Shaper, /// True if the window is focused focused: bool, +/// The actual foreground color. May differ from the config foreground color if +/// changed by a terminal application +foreground_color: terminal.color.RGB, + +/// The actual background color. May differ from the config background color if +/// changed by a terminal application +background_color: terminal.color.RGB, + /// Padding options padding: renderer.Options.Padding, @@ -310,6 +318,8 @@ pub fn init(alloc: Allocator, options: renderer.Options) !OpenGL { .font_shaper = shaper, .draw_background = options.config.background, .focused = true, + .foreground_color = options.config.foreground, + .background_color = options.config.background, .padding = options.padding, .surface_mailbox = options.surface_mailbox, .deferred_font_size = .{ .metrics = metrics }, @@ -586,15 +596,15 @@ pub fn render( } // Swap bg/fg if the terminal is reversed - const bg = self.config.background; - const fg = self.config.foreground; + const bg = self.background_color; + const fg = self.foreground_color; defer { - self.config.background = bg; - self.config.foreground = fg; + self.background_color = bg; + self.foreground_color = fg; } if (state.terminal.modes.get(.reverse_colors)) { - self.config.background = fg; - self.config.foreground = bg; + self.background_color = fg; + self.foreground_color = bg; } // We used to share terminal state, but we've since learned through @@ -627,7 +637,7 @@ pub fn render( ); break :critical .{ - .gl_bg = self.config.background, + .gl_bg = self.background_color, .selection = selection, .screen = screen_copy, .preedit = if (cursor_style != null) state.preedit else null, @@ -878,7 +888,7 @@ fn addCursor( ), screen.cursor.x - 1 }; }; - const color = self.config.cursor_color orelse self.config.foreground; + const color = self.config.cursor_color orelse self.foreground_color; const alpha: u8 = if (!self.focused) 255 else alpha: { const alpha = 255 * self.config.cursor_opacity; break :alpha @intFromFloat(@ceil(alpha)); @@ -1010,21 +1020,21 @@ pub fn updateCell( const colors: BgFg = colors: { // If we are selected, we our colors are just inverted fg/bg var selection_res: ?BgFg = if (selected) .{ - .bg = self.config.selection_background orelse self.config.foreground, - .fg = self.config.selection_foreground orelse self.config.background, + .bg = self.config.selection_background orelse self.foreground_color, + .fg = self.config.selection_foreground orelse self.background_color, } else null; const res: BgFg = selection_res orelse if (!cell.attrs.inverse) .{ // In normal mode, background and fg match the cell. We // un-optionalize the fg by defaulting to our fg color. .bg = if (cell.attrs.has_bg) cell.bg else null, - .fg = if (cell.attrs.has_fg) cell.fg else self.config.foreground, + .fg = if (cell.attrs.has_fg) cell.fg else self.foreground_color, } else .{ // In inverted mode, the background MUST be set to something // (is never null) so it is either the fg or default fg. The // fg is either the bg or default background. - .bg = if (cell.attrs.has_fg) cell.fg else self.config.foreground, - .fg = if (cell.attrs.has_bg) cell.bg else self.config.background, + .bg = if (cell.attrs.has_fg) cell.fg else self.foreground_color, + .fg = if (cell.attrs.has_bg) cell.bg else self.background_color, }; // If the cell is "invisible" then we just make fg = bg so that @@ -1032,7 +1042,7 @@ pub fn updateCell( if (cell.attrs.invisible) { break :colors BgFg{ .bg = res.bg, - .fg = res.bg orelse self.config.background, + .fg = res.bg orelse self.background_color, }; } @@ -1070,7 +1080,7 @@ pub fn updateCell( // If we have a background and its not the default background // then we apply background opacity - if (cell.attrs.has_bg and !std.meta.eql(rgb, self.config.background)) { + if (cell.attrs.has_bg and !std.meta.eql(rgb, self.background_color)) { break :bg_alpha alpha; } diff --git a/src/renderer/message.zig b/src/renderer/message.zig index 873cbe7e5..adb504109 100644 --- a/src/renderer/message.zig +++ b/src/renderer/message.zig @@ -22,11 +22,11 @@ pub const Message = union(enum) { font_size: font.face.DesiredSize, /// Change the foreground color. This can be done separately from changing - /// the config file in response to an OSC 10 command + /// the config file in response to an OSC 10 command. foreground_color: terminal.color.RGB, /// Change the background color. This can be done separately from changing - /// the config file in response to an OSC 11 command + /// the config file in response to an OSC 11 command. background_color: terminal.color.RGB, /// Changes the screen size. diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 70e0e2a3a..1ea3c94fc 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -75,8 +75,16 @@ scrolling_region: ScrollingRegion, /// The last reported pwd, if any. pwd: std.ArrayList(u8), -/// The color palette to use -color_palette: color.Palette = color.default, +/// 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 { + colors: color.Palette = color.default, + mask: u256 = 0, +} = .{}, /// The previous printed character. This is used for the repeat previous /// char CSI (ESC [ b). @@ -560,12 +568,12 @@ pub fn setAttribute(self: *Terminal, attr: sgr.Attribute) !void { .@"8_fg" => |n| { self.screen.cursor.pen.attrs.has_fg = true; - self.screen.cursor.pen.fg = self.color_palette[@intFromEnum(n)]; + self.screen.cursor.pen.fg = self.color_palette.colors[@intFromEnum(n)]; }, .@"8_bg" => |n| { self.screen.cursor.pen.attrs.has_bg = true; - self.screen.cursor.pen.bg = self.color_palette[@intFromEnum(n)]; + self.screen.cursor.pen.bg = self.color_palette.colors[@intFromEnum(n)]; }, .reset_fg => self.screen.cursor.pen.attrs.has_fg = false, @@ -574,22 +582,22 @@ pub fn setAttribute(self: *Terminal, attr: sgr.Attribute) !void { .@"8_bright_fg" => |n| { self.screen.cursor.pen.attrs.has_fg = true; - self.screen.cursor.pen.fg = self.color_palette[@intFromEnum(n)]; + self.screen.cursor.pen.fg = self.color_palette.colors[@intFromEnum(n)]; }, .@"8_bright_bg" => |n| { self.screen.cursor.pen.attrs.has_bg = true; - self.screen.cursor.pen.bg = self.color_palette[@intFromEnum(n)]; + self.screen.cursor.pen.bg = self.color_palette.colors[@intFromEnum(n)]; }, .@"256_fg" => |idx| { self.screen.cursor.pen.attrs.has_fg = true; - self.screen.cursor.pen.fg = self.color_palette[idx]; + self.screen.cursor.pen.fg = self.color_palette.colors[idx]; }, .@"256_bg" => |idx| { self.screen.cursor.pen.attrs.has_bg = true; - self.screen.cursor.pen.bg = self.color_palette[idx]; + self.screen.cursor.pen.bg = self.color_palette.colors[idx]; }, .unknown => return error.InvalidAttribute, diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index e16b4e31a..cfe62b8b9 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -98,32 +98,43 @@ pub const Command = union(enum) { report_color: struct { /// OSC 4 requests a palette color, OSC 10 requests the foreground /// color, OSC 11 the background color. - kind: DefaultColorKind, + kind: ColorKind, /// We must reply with the same string terminator (ST) as used in the /// request. terminator: Terminator = .st, }, + /// Modify the foreground (OSC 10) or background color (OSC 11), or a palette color (OSC 4) set_color: struct { /// OSC 4 sets a palette color, OSC 10 sets the foreground color, OSC 11 /// the background color. - kind: DefaultColorKind, + kind: ColorKind, /// The color spec as a string value: []const u8, }, - pub const DefaultColorKind = union(enum) { + /// Reset a palette color (OSC 104) or the foreground (OSC 110), background + /// (OSC 111), or cursor (OSC 112) color. + reset_color: struct { + kind: ColorKind, + + /// OSC 104 can have parameters indicating which palette colors to + /// reset. + value: []const u8, + }, + + pub const ColorKind = union(enum) { + palette: u8, foreground, background, - palette: u8, - pub fn code(self: DefaultColorKind) []const u8 { + pub fn code(self: ColorKind) []const u8 { return switch (self) { + .palette => "4", .foreground => "10", .background => "11", - .palette => "4", }; } }; @@ -216,13 +227,11 @@ pub const Parser = struct { @"52", @"7", - // OSC 10 is used to query the default foreground color, and to set the default foreground color. - // Only querying is currently supported. - query_default_fg, + // OSC 10 is used to query or set the current foreground color. + query_fg_color, - // OSC 11 is used to query the default background color, and to set the default background color. - // Only querying is currently supported. - query_default_bg, + // OSC 11 is used to query or set the current background color. + query_bg_color, // We're in a semantic prompt OSC command but we aren't sure // what the command is yet, i.e. `133;` @@ -241,6 +250,9 @@ pub const Parser = struct { color_palette_index, color_palette_index_end, + // Reset color palette index + reset_color_palette_index, + // Expect a string parameter. param_str must be set as well as // buf_start. string, @@ -319,12 +331,31 @@ pub const Parser = struct { }, .@"10" => switch (c) { - ';' => self.state = .query_default_fg, + ';' => self.state = .query_fg_color, + '4' => { + self.command = .{ .reset_color = .{ + .kind = .{ .palette = 0 }, + .value = "", + } }; + + self.state = .reset_color_palette_index; + self.complete = true; + }, else => self.state = .invalid, }, .@"11" => switch (c) { - ';' => self.state = .query_default_bg, + ';' => self.state = .query_bg_color, + '0' => { + self.command = .{ .reset_color = .{ .kind = .foreground, .value = undefined } }; + self.complete = true; + self.state = .invalid; + }, + '1' => { + self.command = .{ .reset_color = .{ .kind = .background, .value = undefined } }; + self.complete = true; + self.state = .invalid; + }, '2' => { self.complete = true; self.command = .{ .reset_cursor_color = {} }; @@ -408,6 +439,19 @@ pub const Parser = struct { }, }, + .reset_color_palette_index => switch (c) { + ';' => { + self.state = .string; + self.temp_state = .{ .str = &self.command.reset_color.value }; + self.buf_start = self.buf_idx; + self.complete = false; + }, + else => { + self.state = .invalid; + self.complete = false; + }, + }, + .@"5" => switch (c) { '2' => self.state = .@"52", else => self.state = .invalid, @@ -454,10 +498,11 @@ pub const Parser = struct { else => self.state = .invalid, }, - .query_default_fg => switch (c) { + .query_fg_color => switch (c) { '?' => { self.command = .{ .report_color = .{ .kind = .foreground } }; self.complete = true; + self.state = .invalid; }, else => { self.command = .{ .set_color = .{ @@ -471,10 +516,11 @@ pub const Parser = struct { }, }, - .query_default_bg => switch (c) { + .query_bg_color => switch (c) { '?' => { self.command = .{ .report_color = .{ .kind = .background } }; self.complete = true; + self.state = .invalid; }, else => { self.command = .{ .set_color = .{ diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index d7d306922..e41b27849 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -1059,10 +1059,12 @@ pub fn Stream(comptime Handler: type) type { } else log.warn("unimplemented OSC callback: {}", .{cmd}); }, - else => if (@hasDecl(T, "oscUnimplemented")) - try self.handler.oscUnimplemented(cmd) - else - log.warn("unimplemented OSC command: {}", .{cmd}), + .reset_color => |v| { + if (@hasDecl(T, "resetColor")) { + try self.handler.resetColor(v.kind, v.value); + return; + } else log.warn("unimplemented OSC callback: {}", .{cmd}); + }, } // Fall through for when we don't have a handler. diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index a0da3e9c4..178de96d8 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -138,7 +138,7 @@ pub fn init(alloc: Allocator, opts: termio.Options) !Exec { opts.grid_size.rows, ); errdefer term.deinit(alloc); - term.color_palette = opts.config.palette; + term.default_palette = opts.config.palette; // Set the image size limits try term.screen.kitty_images.setLimit(alloc, opts.config.image_storage_limit); @@ -332,7 +332,7 @@ pub fn changeConfig(self: *Exec, config: *DerivedConfig) !void { // Update the palette. Note this will only apply to new colors drawn // since we decode all palette colors to RGB on usage. - self.terminal.color_palette = config.palette; + self.terminal.default_palette = config.palette; // Update our default cursor style self.default_cursor_style = config.cursor_style; @@ -2179,15 +2179,15 @@ const StreamHandler = struct { /// default foreground color, and background color respectively. pub fn reportColor( self: *StreamHandler, - kind: terminal.osc.Command.DefaultColorKind, + kind: terminal.osc.Command.ColorKind, terminator: terminal.osc.Terminator, ) !void { if (self.osc_color_report_format == .none) return; const color = switch (kind) { + .palette => |i| self.terminal.color_palette.colors[i], .foreground => self.foreground_color, .background => self.background_color, - .palette => |i| self.terminal.color_palette[i], }; var msg: termio.Message = .{ .write_small = .{} }; @@ -2224,12 +2224,16 @@ const StreamHandler = struct { pub fn setColor( self: *StreamHandler, - kind: terminal.osc.Command.DefaultColorKind, + kind: terminal.osc.Command.ColorKind, value: []const u8, ) !void { const color = try terminal.color.RGB.parse(value); switch (kind) { + .palette => |i| { + self.terminal.color_palette.colors[i] = color; + self.terminal.color_palette.mask |= @as(u256, 1) << i; + }, .foreground => { self.foreground_color = color; _ = self.ev.renderer_mailbox.push(.{ @@ -2242,7 +2246,55 @@ const StreamHandler = struct { .background_color = color, }, .{ .forever = {} }); }, - .palette => |i| self.terminal.color_palette[i] = color, + } + } + + pub fn resetColor( + self: *StreamHandler, + kind: terminal.osc.Command.ColorKind, + value: []const u8, + ) !void { + switch (kind) { + .palette => { + var mask = self.terminal.color_palette.mask; + defer self.terminal.color_palette.mask = mask; + + if (value.len == 0) { + // Find all bit positions in the mask which are set and + // reset those indices to the default palette + while (mask != 0) { + // Safe to truncate, mask is non-zero so @ctz can never + // return a u9 + const i: u8 = @truncate(@ctz(mask)); + log.warn("Resetting palette color {}", .{i}); + self.terminal.color_palette.colors[i] = self.terminal.default_palette[i]; + mask ^= @as(u256, 1) << i; + } + } else { + var it = std.mem.tokenizeScalar(u8, value, ';'); + while (it.next()) |param| { + // Skip invalid parameters + const i = std.fmt.parseUnsigned(u8, param, 10) catch continue; + log.warn("Resetting palette color {}", .{i}); + if (mask & (@as(u256, 1) << i) != 0) { + self.terminal.color_palette.colors[i] = self.terminal.default_palette[i]; + mask ^= @as(u256, 1) << i; + } + } + } + }, + .foreground => { + self.foreground_color = self.default_foreground_color; + _ = self.ev.renderer_mailbox.push(.{ + .foreground_color = self.foreground_color, + }, .{ .forever = {} }); + }, + .background => { + self.background_color = self.default_background_color; + _ = self.ev.renderer_mailbox.push(.{ + .background_color = self.background_color, + }, .{ .forever = {} }); + }, } } }; From 171292a0630162930a015f0cba3efd693f90023b Mon Sep 17 00:00:00 2001 From: Gregory Anders Date: Thu, 9 Nov 2023 16:10:43 -0600 Subject: [PATCH 7/8] core: implement OSC 12 and OSC 112 to query/set/reset cursor color --- src/renderer/Metal.zig | 7 ++++++- src/renderer/OpenGL.zig | 7 ++++++- src/renderer/Thread.zig | 4 ++++ src/renderer/message.zig | 4 ++++ src/terminal/Parser.zig | 3 ++- src/terminal/osc.zig | 41 +++++++++++++++++++++++++++++++++------- src/termio/Exec.zig | 37 ++++++++++++++++++++++++++++++++++++ 7 files changed, 93 insertions(+), 10 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 73157ea59..6d75400ba 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -72,6 +72,10 @@ foreground_color: terminal.color.RGB, /// changed by a terminal application background_color: terminal.color.RGB, +/// The actual cursor color. May differ from the config cursor color if changed +/// by a terminal application +cursor_color: ?terminal.color.RGB, + /// 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. @@ -264,6 +268,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { .focused = true, .foreground_color = options.config.foreground, .background_color = options.config.background, + .cursor_color = options.config.cursor_color, // Render state .cells_bg = .{}, @@ -1444,7 +1449,7 @@ fn addCursor( ), screen.cursor.x - 1 }; }; - const color = self.config.cursor_color orelse self.foreground_color; + const color = self.cursor_color orelse self.foreground_color; const alpha: u8 = if (!self.focused) 255 else alpha: { const alpha = 255 * self.config.cursor_opacity; break :alpha @intFromFloat(@ceil(alpha)); diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 828ffa090..bc8982b37 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -75,6 +75,10 @@ foreground_color: terminal.color.RGB, /// changed by a terminal application background_color: terminal.color.RGB, +/// The actual cursor color. May differ from the config cursor color if changed +/// by a terminal application +cursor_color: ?terminal.color.RGB, + /// Padding options padding: renderer.Options.Padding, @@ -320,6 +324,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !OpenGL { .focused = true, .foreground_color = options.config.foreground, .background_color = options.config.background, + .cursor_color = options.config.cursor_color, .padding = options.padding, .surface_mailbox = options.surface_mailbox, .deferred_font_size = .{ .metrics = metrics }, @@ -888,7 +893,7 @@ fn addCursor( ), screen.cursor.x - 1 }; }; - const color = self.config.cursor_color orelse self.foreground_color; + const color = self.cursor_color orelse self.foreground_color; const alpha: u8 = if (!self.focused) 255 else alpha: { const alpha = 255 * self.config.cursor_opacity; break :alpha @intFromFloat(@ceil(alpha)); diff --git a/src/renderer/Thread.zig b/src/renderer/Thread.zig index 654f370ad..17abd6325 100644 --- a/src/renderer/Thread.zig +++ b/src/renderer/Thread.zig @@ -270,6 +270,10 @@ fn drainMailbox(self: *Thread) !void { self.renderer.background_color = color; }, + .cursor_color => |color| { + self.renderer.cursor_color = color; + }, + .resize => |v| { try self.renderer.setScreenSize(v.screen_size, v.padding); }, diff --git a/src/renderer/message.zig b/src/renderer/message.zig index adb504109..3278a2c1c 100644 --- a/src/renderer/message.zig +++ b/src/renderer/message.zig @@ -29,6 +29,10 @@ pub const Message = union(enum) { /// the config file in response to an OSC 11 command. 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 screen size. resize: struct { /// The full screen (drawable) size. This does NOT include padding. diff --git a/src/terminal/Parser.zig b/src/terminal/Parser.zig index 032ac1bc6..200803647 100644 --- a/src/terminal/Parser.zig +++ b/src/terminal/Parser.zig @@ -827,7 +827,8 @@ test "osc: 112 incomplete sequence" { try testing.expect(a[2] == null); const cmd = a[0].?.osc_dispatch; - try testing.expect(cmd == .reset_cursor_color); + try testing.expect(cmd == .reset_color); + try testing.expectEqual(cmd.reset_color.kind, .cursor); } } diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index cfe62b8b9..2e59eb279 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -64,10 +64,6 @@ pub const Command = union(enum) { // TODO: err option }, - /// Reset the color for the cursor. This reverts changes made with - /// change/read cursor color. - reset_cursor_color: void, - /// Set or get clipboard contents. If data is null, then the current /// clipboard contents are sent to the pty. If data is set, this /// contents is set on the clipboard. @@ -129,12 +125,14 @@ pub const Command = union(enum) { palette: u8, foreground, background, + cursor, pub fn code(self: ColorKind) []const u8 { return switch (self) { .palette => "4", .foreground => "10", .background => "11", + .cursor => "12", }; } }; @@ -218,6 +216,7 @@ pub const Parser = struct { @"1", @"10", @"11", + @"12", @"13", @"133", @"2", @@ -233,6 +232,9 @@ pub const Parser = struct { // OSC 11 is used to query or set the current background color. query_bg_color, + // OSC 12 is used to query or set the current cursor color. + query_cursor_color, + // We're in a semantic prompt OSC command but we aren't sure // what the command is yet, i.e. `133;` semantic_prompt, @@ -326,6 +328,7 @@ pub const Parser = struct { .@"1" => switch (c) { '0' => self.state = .@"10", '1' => self.state = .@"11", + '2' => self.state = .@"12", '3' => self.state = .@"13", else => self.state = .invalid, }, @@ -357,13 +360,18 @@ pub const Parser = struct { self.state = .invalid; }, '2' => { + self.command = .{ .reset_color = .{ .kind = .cursor, .value = undefined } }; self.complete = true; - self.command = .{ .reset_cursor_color = {} }; self.state = .invalid; }, else => self.state = .invalid, }, + .@"12" => switch (c) { + ';' => self.state = .query_cursor_color, + else => self.state = .invalid, + }, + .@"13" => switch (c) { '3' => self.state = .@"133", else => self.state = .invalid, @@ -534,6 +542,24 @@ pub const Parser = struct { }, }, + .query_cursor_color => switch (c) { + '?' => { + self.command = .{ .report_color = .{ .kind = .cursor } }; + self.complete = true; + self.state = .invalid; + }, + else => { + self.command = .{ .set_color = .{ + .kind = .cursor, + .value = "", + } }; + + self.state = .string; + self.temp_state = .{ .str = &self.command.set_color.value }; + self.buf_start = self.buf_idx - 1; + }, + }, + .semantic_prompt => switch (c) { 'A' => { self.state = .semantic_option_start; @@ -912,7 +938,7 @@ test "OSC: end_of_input" { try testing.expect(cmd == .end_of_input); } -test "OSC: reset_cursor_color" { +test "OSC: reset cursor color" { const testing = std.testing; var p: Parser = .{}; @@ -921,7 +947,8 @@ test "OSC: reset_cursor_color" { for (input) |ch| p.next(ch); const cmd = p.end(null).?; - try testing.expect(cmd == .reset_cursor_color); + try testing.expect(cmd == .reset_color); + try testing.expectEqual(cmd.reset_color.kind, .cursor); } test "OSC: get/set clipboard" { diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 178de96d8..5e2d3c021 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -69,6 +69,10 @@ grid_size: renderer.GridSize, /// it when a CSI q with default is called. default_cursor_style: terminal.Cursor.Style, default_cursor_blink: ?bool, +default_cursor_color: ?terminal.color.RGB, + +/// Actual cursor color +cursor_color: ?terminal.color.RGB, /// Default foreground color as set by the config file default_foreground_color: terminal.color.RGB, @@ -96,6 +100,7 @@ pub const DerivedConfig = struct { image_storage_limit: usize, cursor_style: terminal.Cursor.Style, cursor_blink: ?bool, + cursor_color: ?configpkg.Config.Color, foreground: configpkg.Config.Color, background: configpkg.Config.Color, osc_color_report_format: configpkg.Config.OSCColorReportFormat, @@ -112,6 +117,7 @@ pub const DerivedConfig = struct { .image_storage_limit = config.@"image-storage-limit", .cursor_style = config.@"cursor-style", .cursor_blink = config.@"cursor-style-blink", + .cursor_color = config.@"cursor-color", .foreground = config.foreground, .background = config.background, .osc_color_report_format = config.@"osc-color-report-format", @@ -175,6 +181,14 @@ pub fn init(alloc: Allocator, opts: termio.Options) !Exec { .grid_size = opts.grid_size, .default_cursor_style = opts.config.cursor_style, .default_cursor_blink = opts.config.cursor_blink, + .default_cursor_color = if (opts.config.cursor_color) |col| + col.toTerminalRGB() + else + null, + .cursor_color = if (opts.config.cursor_color) |col| + col.toTerminalRGB() + else + null, .default_foreground_color = config.foreground.toTerminalRGB(), .default_background_color = config.background.toTerminalRGB(), .foreground_color = config.foreground.toTerminalRGB(), @@ -244,6 +258,8 @@ pub fn threadEnter(self: *Exec, thread: *termio.Thread) !ThreadData { .grid_size = &self.grid_size, .default_cursor_style = self.default_cursor_style, .default_cursor_blink = self.default_cursor_blink, + .default_cursor_color = self.default_cursor_color, + .cursor_color = self.cursor_color, .default_foreground_color = self.default_foreground_color, .default_background_color = self.default_background_color, .foreground_color = self.foreground_color, @@ -337,6 +353,10 @@ pub fn changeConfig(self: *Exec, config: *DerivedConfig) !void { // Update our default cursor style self.default_cursor_style = config.cursor_style; self.default_cursor_blink = config.cursor_blink; + self.default_cursor_color = if (config.cursor_color) |col| + col.toTerminalRGB() + else + null; // Update default foreground and background colors self.default_foreground_color = config.foreground.toTerminalRGB(); @@ -1340,6 +1360,10 @@ const StreamHandler = struct { default_cursor: bool = true, default_cursor_style: terminal.Cursor.Style, default_cursor_blink: ?bool, + default_cursor_color: ?terminal.color.RGB, + + /// Actual cursor color. This can be changed with OSC 12. + cursor_color: ?terminal.color.RGB, /// The default foreground and background color are those set by the user's /// config file. These can be overridden by terminal applications using OSC @@ -2188,6 +2212,7 @@ const StreamHandler = struct { .palette => |i| self.terminal.color_palette.colors[i], .foreground => self.foreground_color, .background => self.background_color, + .cursor => self.cursor_color orelse self.foreground_color, }; var msg: termio.Message = .{ .write_small = .{} }; @@ -2246,6 +2271,12 @@ const StreamHandler = struct { .background_color = color, }, .{ .forever = {} }); }, + .cursor => { + self.cursor_color = color; + _ = self.ev.renderer_mailbox.push(.{ + .cursor_color = color, + }, .{ .forever = {} }); + }, } } @@ -2295,6 +2326,12 @@ const StreamHandler = struct { .background_color = self.background_color, }, .{ .forever = {} }); }, + .cursor => { + self.cursor_color = self.default_cursor_color; + _ = self.ev.renderer_mailbox.push(.{ + .cursor_color = self.cursor_color, + }, .{ .forever = {} }); + }, } } }; From 53a5734d09f0c588a445e4fcdaa1fd13ca1daf6d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 9 Nov 2023 15:14:33 -0800 Subject: [PATCH 8/8] terminal: change mask from u256 to StaticBitSet --- src/inspector/termio.zig | 5 ++--- src/terminal/Terminal.zig | 3 ++- src/termio/Exec.zig | 18 +++++++----------- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/inspector/termio.zig b/src/inspector/termio.zig index a9a2f5cde..1b9a45581 100644 --- a/src/inspector/termio.zig +++ b/src/inspector/termio.zig @@ -241,7 +241,8 @@ pub const VTEvent = struct { try alloc.dupeZ(u8, @tagName(value)), ), - .Union => |u| if (u.tag_type) |Tag| { + .Union => |u| { + const Tag = u.tag_type orelse @compileError("Unions must have a tag"); const tag_name = @tagName(@as(Tag, value)); inline for (u.fields) |field| { if (std.mem.eql(u8, field.name, tag_name)) { @@ -256,8 +257,6 @@ pub const VTEvent = struct { try md.put(key, s); } } - } else { - @compileError("Unions must have a tag"); }, else => switch (Value) { diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 1ea3c94fc..c5f777006 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -82,8 +82,9 @@ 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: u256 = 0, + mask: Mask = Mask.initEmpty(), } = .{}, /// The previous printed character. This is used for the repeat previous diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 5e2d3c021..03891364f 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -2257,7 +2257,7 @@ const StreamHandler = struct { switch (kind) { .palette => |i| { self.terminal.color_palette.colors[i] = color; - self.terminal.color_palette.mask |= @as(u256, 1) << i; + self.terminal.color_palette.mask.set(i); }, .foreground => { self.foreground_color = color; @@ -2287,19 +2287,15 @@ const StreamHandler = struct { ) !void { switch (kind) { .palette => { - var mask = self.terminal.color_palette.mask; - defer self.terminal.color_palette.mask = mask; - + const mask = &self.terminal.color_palette.mask; if (value.len == 0) { // Find all bit positions in the mask which are set and // reset those indices to the default palette - while (mask != 0) { - // Safe to truncate, mask is non-zero so @ctz can never - // return a u9 - const i: u8 = @truncate(@ctz(mask)); + var it = mask.iterator(.{}); + while (it.next()) |i| { log.warn("Resetting palette color {}", .{i}); self.terminal.color_palette.colors[i] = self.terminal.default_palette[i]; - mask ^= @as(u256, 1) << i; + mask.unset(i); } } else { var it = std.mem.tokenizeScalar(u8, value, ';'); @@ -2307,9 +2303,9 @@ const StreamHandler = struct { // Skip invalid parameters const i = std.fmt.parseUnsigned(u8, param, 10) catch continue; log.warn("Resetting palette color {}", .{i}); - if (mask & (@as(u256, 1) << i) != 0) { + if (mask.isSet(i)) { self.terminal.color_palette.colors[i] = self.terminal.default_palette[i]; - mask ^= @as(u256, 1) << i; + mask.unset(i); } } }