From b11b8be12463f39c3f51d69936db8b911a694839 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sat, 17 Aug 2024 06:55:51 -0500 Subject: [PATCH 1/9] Implement Kitty Color Protocol (OSC 21) Kitty 0.36.0 added support for a new OSC escape sequence for quering, setting, and resetting the terminal colors. Details can be found [here](https://sw.kovidgoyal.net/kitty/color-stack/#setting-and-querying-colors). This fully parses the OSC 21 escape sequences, but only supports actually querying and changing the foreground color, the background color, and the cursor color because that's what Ghostty currently supports. Adding support for the other settings that Kitty supports changing ranges from easy (cursor text) to difficult (visual bell, second transparent background color). --- src/inspector/termio.zig | 5 + src/terminal/color.zig | 36 +++++-- src/terminal/osc.zig | 172 ++++++++++++++++++++++++++++++++++ src/terminal/stream.zig | 7 ++ src/termio/stream_handler.zig | 93 ++++++++++++++++++ 5 files changed, 304 insertions(+), 9 deletions(-) diff --git a/src/inspector/termio.zig b/src/inspector/termio.zig index 1b9a45581..bae07bcfe 100644 --- a/src/inspector/termio.zig +++ b/src/inspector/termio.zig @@ -259,6 +259,11 @@ pub const VTEvent = struct { } }, + .Struct => try md.put( + key, + try alloc.dupeZ(u8, @typeName(Value)), + ), + else => switch (Value) { u8 => try md.put( key, diff --git a/src/terminal/color.zig b/src/terminal/color.zig index ed6d0be3d..c8929b060 100644 --- a/src/terminal/color.zig +++ b/src/terminal/color.zig @@ -208,7 +208,7 @@ pub const RGB = struct { /// where , , and are floating point values between /// 0.0 and 1.0 (inclusive). /// - /// 3. #hhhhhh + /// 3. #hhh, #hhhhhh, #hhhhhhhhh #hhhhhhhhhhhh /// /// where `h` is a single hexadecimal digit. pub fn parse(value: []const u8) !RGB { @@ -217,15 +217,30 @@ pub const RGB = struct { } if (value[0] == '#') { - if (value.len != 7) { - return error.InvalidFormat; - } + switch (value.len) { + 4 => return RGB{ + .r = try RGB.fromHex(value[1..2]), + .g = try RGB.fromHex(value[2..3]), + .b = try RGB.fromHex(value[3..4]), + }, + 7 => return RGB{ + .r = try RGB.fromHex(value[1..3]), + .g = try RGB.fromHex(value[3..5]), + .b = try RGB.fromHex(value[5..7]), + }, + 10 => return RGB{ + .r = try RGB.fromHex(value[1..4]), + .g = try RGB.fromHex(value[4..7]), + .b = try RGB.fromHex(value[7..10]), + }, + 13 => return RGB{ + .r = try RGB.fromHex(value[1..5]), + .g = try RGB.fromHex(value[5..9]), + .b = try RGB.fromHex(value[9..13]), + }, - return RGB{ - .r = try RGB.fromHex(value[1..3]), - .g = try RGB.fromHex(value[3..5]), - .b = try RGB.fromHex(value[5..7]), - }; + else => return error.InvalidFormat, + } } // Check for X11 named colors. We allow whitespace around the edges @@ -308,6 +323,9 @@ test "RGB.parse" { 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")); try testing.expectEqual(RGB{ .r = 255, .g = 255, .b = 255 }, try RGB.parse("#ffffff")); + try testing.expectEqual(RGB{ .r = 255, .g = 255, .b = 255 }, try RGB.parse("#fff")); + try testing.expectEqual(RGB{ .r = 255, .g = 255, .b = 255 }, try RGB.parse("#fffffffff")); + try testing.expectEqual(RGB{ .r = 255, .g = 255, .b = 255 }, try RGB.parse("#ffffffffffff")); try testing.expectEqual(RGB{ .r = 255, .g = 0, .b = 16 }, try RGB.parse("#ff0010")); try testing.expectEqual(RGB{ .r = 0, .g = 0, .b = 0 }, try RGB.parse("black")); diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 5aedaa6e0..326766f6a 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -9,6 +9,7 @@ const std = @import("std"); const mem = std.mem; const assert = std.debug.assert; const Allocator = mem.Allocator; +const RGB = @import("color.zig").RGB; const log = std.log.scoped(.osc); @@ -137,6 +138,10 @@ pub const Command = union(enum) { value: []const u8, }, + /// Kitty color protocl, OSC 21 + /// https://sw.kovidgoyal.net/kitty/color-stack/#id1 + kitty_color_protocol: KittyColorProtocol, + /// Show a desktop notification (OSC 9 or OSC 777) show_desktop_notification: struct { title: []const u8, @@ -167,6 +172,34 @@ pub const Command = union(enum) { }; } }; + + pub const KittyColorProtocol = struct { + const Kind = enum { + foreground, + background, + selection_foreground, + selection_background, + cursor, + cursor_text, + visual_bell, + second_transparent_background, + }; + const Request = union(enum) { + query: Kind, + set: struct { + key: Kind, + color: RGB, + }, + reset: Kind, + }; + + /// list of requests + list: std.ArrayList(Request), + + /// We must reply with the same string terminator (ST) as used in the + /// request. + terminator: Terminator = .st, + }; }; /// The terminator used to end an OSC command. For OSC commands that demand @@ -251,6 +284,7 @@ pub const Parser = struct { @"13", @"133", @"2", + @"21", @"22", @"4", @"5", @@ -310,6 +344,11 @@ pub const Parser = struct { // If the parser has no allocator then it is treated as if the // buffer is full. allocable_string, + + // Kitty color protocol + // https://sw.kovidgoyal.net/kitty/color-stack/#id1 + kitty_color_protocol_key, + kitty_color_protocol_value, }; /// This must be called to clean up any allocated memory. @@ -323,6 +362,9 @@ pub const Parser = struct { self.buf_start = 0; self.buf_idx = 0; self.complete = false; + if (self.command == .kitty_color_protocol) { + self.command.kitty_color_protocol.list.deinit(); + } if (self.buf_dynamic) |ptr| { const alloc = self.alloc.?; ptr.deinit(alloc); @@ -439,6 +481,7 @@ pub const Parser = struct { }, .@"2" => switch (c) { + '1' => self.state = .@"21", '2' => self.state = .@"22", ';' => { self.command = .{ .change_window_title = undefined }; @@ -450,6 +493,45 @@ pub const Parser = struct { else => self.state = .invalid, }, + .@"21" => switch (c) { + ';' => { + self.command = .{ + .kitty_color_protocol = .{ + .list = std.ArrayList(Command.KittyColorProtocol.Request).init(self.alloc.?), + }, + }; + + self.state = .kitty_color_protocol_key; + self.complete = true; + self.buf_start = self.buf_idx; + }, + else => self.state = .invalid, + }, + + .kitty_color_protocol_key => switch (c) { + ';' => { + self.temp_state = .{ .key = self.buf[self.buf_start .. self.buf_idx - 1] }; + self.endKittyColorProtocolOption(.key_only, false); + self.state = .kitty_color_protocol_key; + self.buf_start = self.buf_idx; + }, + '=' => { + self.temp_state = .{ .key = self.buf[self.buf_start .. self.buf_idx - 1] }; + self.state = .kitty_color_protocol_value; + self.buf_start = self.buf_idx; + }, + else => {}, + }, + + .kitty_color_protocol_value => switch (c) { + ';' => { + self.endKittyColorProtocolOption(.key_and_value, false); + self.state = .kitty_color_protocol_key; + self.buf_start = self.buf_idx; + }, + else => {}, + }, + .@"22" => switch (c) { ';' => { self.command = .{ .mouse_shape = undefined }; @@ -936,6 +1018,56 @@ pub const Parser = struct { self.temp_state.str.* = self.buf[self.buf_start..self.buf_idx]; } + fn endKittyColorProtocolOption(self: *Parser, kind: enum { key_only, key_and_value }, final: bool) void { + if (self.temp_state.key.len == 0) { + log.warn("zero length key in kitty color protocol", .{}); + return; + } + + const key = std.meta.stringToEnum(Command.KittyColorProtocol.Kind, self.temp_state.key) orelse { + log.warn("unknown key in kitty color protocol: {s}", .{self.temp_state.key}); + return; + }; + + const value = value: { + if (self.buf_start == self.buf_idx) break :value ""; + if (final) break :value std.mem.trim(u8, self.buf[self.buf_start..self.buf_idx], " "); + break :value std.mem.trim(u8, self.buf[self.buf_start .. self.buf_idx - 1], " "); + }; + + switch (self.command) { + .kitty_color_protocol => |*v| { + if (kind == .key_only) { + v.list.append(.{ .reset = key }) catch unreachable; + return; + } + if (value.len == 0) { + v.list.append(.{ .reset = key }) catch unreachable; + return; + } + if (mem.eql(u8, "?", value)) { + v.list.append(.{ .query = key }) catch unreachable; + return; + } + v.list.append( + .{ + .set = .{ + .key = key, + .color = RGB.parse(value) catch |err| switch (err) { + error.InvalidFormat => { + log.err("invalid color format in kitty color protocol: {s}", .{value}); + return; + }, + }, + }, + }, + ) catch unreachable; + return; + }, + else => {}, + } + } + fn endAllocableString(self: *Parser) void { const list = self.buf_dynamic.?; self.temp_state.str.* = list.items; @@ -958,11 +1090,14 @@ pub const Parser = struct { .hyperlink_uri => self.endHyperlink(), .string => self.endString(), .allocable_string => self.endAllocableString(), + .kitty_color_protocol_key => self.endKittyColorProtocolOption(.key_only, true), + .kitty_color_protocol_value => self.endKittyColorProtocolOption(.key_and_value, true), else => {}, } switch (self.command) { .report_color => |*c| c.terminator = Terminator.init(terminator_ch), + .kitty_color_protocol => |*c| c.terminator = Terminator.init(terminator_ch), else => {}, } @@ -1497,3 +1632,40 @@ test "OSC: hyperlink end" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .hyperlink_end); } + +test "OSC: kitty color protocol" { + const testing = std.testing; + + var p: Parser = .{ .alloc = std.testing.allocator }; + defer p.deinit(); + + const input = "21;foreground=?;background=rgb:f0/f8/ff;cursor=aliceblue;cursor_text;visual_bell=;selection_foreground=#xxxyyzz;selection_background=?;selection_background=#aabbcc"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?; + try testing.expect(cmd == .kitty_color_protocol); + try testing.expectEqual(@as(usize, 7), cmd.kitty_color_protocol.list.items.len); + try testing.expect(cmd.kitty_color_protocol.list.items[0] == .query); + try testing.expectEqual(@as(Command.KittyColorProtocol.Kind, .foreground), cmd.kitty_color_protocol.list.items[0].query); + try testing.expect(cmd.kitty_color_protocol.list.items[1] == .set); + try testing.expectEqual(@as(Command.KittyColorProtocol.Kind, .background), cmd.kitty_color_protocol.list.items[1].set.key); + try testing.expectEqual(@as(u8, 0xf0), cmd.kitty_color_protocol.list.items[1].set.color.r); + try testing.expectEqual(@as(u8, 0xf8), cmd.kitty_color_protocol.list.items[1].set.color.g); + try testing.expectEqual(@as(u8, 0xff), cmd.kitty_color_protocol.list.items[1].set.color.b); + try testing.expect(cmd.kitty_color_protocol.list.items[2] == .set); + try testing.expectEqual(@as(Command.KittyColorProtocol.Kind, .cursor), cmd.kitty_color_protocol.list.items[2].set.key); + try testing.expectEqual(@as(u8, 0xf0), cmd.kitty_color_protocol.list.items[2].set.color.r); + try testing.expectEqual(@as(u8, 0xf8), cmd.kitty_color_protocol.list.items[2].set.color.g); + try testing.expectEqual(@as(u8, 0xff), cmd.kitty_color_protocol.list.items[2].set.color.b); + try testing.expect(cmd.kitty_color_protocol.list.items[3] == .reset); + try testing.expectEqual(@as(Command.KittyColorProtocol.Kind, .cursor_text), cmd.kitty_color_protocol.list.items[3].reset); + try testing.expect(cmd.kitty_color_protocol.list.items[4] == .reset); + try testing.expectEqual(@as(Command.KittyColorProtocol.Kind, .visual_bell), cmd.kitty_color_protocol.list.items[4].reset); + try testing.expect(cmd.kitty_color_protocol.list.items[5] == .query); + try testing.expectEqual(@as(Command.KittyColorProtocol.Kind, .selection_background), cmd.kitty_color_protocol.list.items[5].query); + try testing.expect(cmd.kitty_color_protocol.list.items[6] == .set); + try testing.expectEqual(@as(Command.KittyColorProtocol.Kind, .selection_background), cmd.kitty_color_protocol.list.items[6].set.key); + try testing.expectEqual(@as(u8, 0xaa), cmd.kitty_color_protocol.list.items[6].set.color.r); + try testing.expectEqual(@as(u8, 0xbb), cmd.kitty_color_protocol.list.items[6].set.color.g); + try testing.expectEqual(@as(u8, 0xcc), cmd.kitty_color_protocol.list.items[6].set.color.b); +} diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index 6adfa3280..7ade11963 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -1393,6 +1393,13 @@ pub fn Stream(comptime Handler: type) type { } else log.warn("unimplemented OSC callback: {}", .{cmd}); }, + .kitty_color_protocol => |v| { + if (@hasDecl(T, "sendKittyColorReport")) { + try self.handler.sendKittyColorReport(v); + return; + } else log.warn("unimplemented OSC callback: {}", .{cmd}); + }, + .show_desktop_notification => |v| { if (@hasDecl(T, "showDesktopNotification")) { try self.handler.showDesktopNotification(v.title, v.body); diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 648efb6fb..b3f31e4b0 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -1269,4 +1269,97 @@ pub const StreamHandler = struct { .csi_21_t => self.surfaceMessageWriter(.{ .report_title = .csi_21_t }), } } + + pub fn sendKittyColorReport(self: *StreamHandler, request: terminal.osc.Command.KittyColorProtocol) !void { + var buf = std.ArrayList(u8).init(self.alloc); + errdefer buf.deinit(); + const writer = buf.writer(); + try writer.writeAll("\x1b[21"); + + for (request.list.items) |item| { + switch (item) { + .query => |key| { + const color = switch (key) { + .foreground => self.foreground_color, + .background => self.background_color, + .cursor => self.cursor_color, + else => { + log.warn("ignoring unsupported kitty color protocol key: {s}", .{@tagName(key)}); + continue; + }, + } orelse { + log.warn("no color configured for: {s}", .{@tagName(key)}); + continue; + }; + try writer.print( + ";rgb:{x:0>2}/{x:0>2}/{x:0>2}", + .{ + @as(u16, color.r), + @as(u16, color.g), + @as(u16, color.b), + }, + ); + }, + .set => |v| switch (v.key) { + .foreground => { + self.foreground_color = v.color; + _ = self.renderer_mailbox.push(.{ + .foreground_color = v.color, + }, .{ .forever = {} }); + }, + .background => { + self.background_color = v.color; + _ = self.renderer_mailbox.push(.{ + .background_color = v.color, + }, .{ .forever = {} }); + }, + .cursor => { + self.cursor_color = v.color; + _ = self.renderer_mailbox.push(.{ + .cursor_color = v.color, + }, .{ .forever = {} }); + }, + else => { + log.warn("ignoring unsupported kitty color protocol key: {s}", .{@tagName(v.key)}); + continue; + }, + }, + .reset => |key| switch (key) { + .foreground => { + self.foreground_color = self.default_foreground_color; + _ = self.renderer_mailbox.push(.{ + .foreground_color = self.foreground_color, + }, .{ .forever = {} }); + }, + .background => { + self.background_color = self.default_background_color; + _ = self.renderer_mailbox.push(.{ + .background_color = self.background_color, + }, .{ .forever = {} }); + }, + .cursor => { + self.cursor_color = self.default_cursor_color; + _ = self.renderer_mailbox.push(.{ + .cursor_color = self.cursor_color, + }, .{ .forever = {} }); + }, + else => { + log.warn("ignoring unsupported kitty color protocol key: {s}", .{@tagName(key)}); + continue; + }, + }, + } + } + + try writer.writeAll(request.terminator.string()); + + const msg = termio.Message{ + .write_alloc = .{ + .alloc = self.alloc, + .data = try buf.toOwnedSlice(), + }, + }; + + self.messageWriter(msg); + } }; From 254072e65625189ca23badccc5dcb3035fe2a46c Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sat, 17 Aug 2024 15:34:35 -0500 Subject: [PATCH 2/9] fix test error and improve error logging --- src/terminal/osc.zig | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 326766f6a..89bd8e976 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -1038,15 +1038,24 @@ pub const Parser = struct { switch (self.command) { .kitty_color_protocol => |*v| { if (kind == .key_only) { - v.list.append(.{ .reset = key }) catch unreachable; + v.list.append(.{ .reset = key }) catch |err| { + log.warn("unable to append kitty color protocol option: {}", .{err}); + return; + }; return; } if (value.len == 0) { - v.list.append(.{ .reset = key }) catch unreachable; + v.list.append(.{ .reset = key }) catch |err| { + log.warn("unable to append kitty color protocol option: {}", .{err}); + return; + }; return; } if (mem.eql(u8, "?", value)) { - v.list.append(.{ .query = key }) catch unreachable; + v.list.append(.{ .query = key }) catch |err| { + log.warn("unable to append kitty color protocol option: {}", .{err}); + return; + }; return; } v.list.append( @@ -1055,13 +1064,16 @@ pub const Parser = struct { .key = key, .color = RGB.parse(value) catch |err| switch (err) { error.InvalidFormat => { - log.err("invalid color format in kitty color protocol: {s}", .{value}); + log.warn("invalid color format in kitty color protocol: {s}", .{value}); return; }, }, }, }, - ) catch unreachable; + ) catch |err| { + log.warn("unable to append kitty color protocol option: {}", .{err}); + return; + }; return; }, else => {}, @@ -1636,7 +1648,7 @@ test "OSC: hyperlink end" { test "OSC: kitty color protocol" { const testing = std.testing; - var p: Parser = .{ .alloc = std.testing.allocator }; + var p: Parser = .{ .alloc = testing.allocator }; defer p.deinit(); const input = "21;foreground=?;background=rgb:f0/f8/ff;cursor=aliceblue;cursor_text;visual_bell=;selection_foreground=#xxxyyzz;selection_background=?;selection_background=#aabbcc"; From a2ef0ca75108c4be9ba495419823345cab5b442a Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Mon, 19 Aug 2024 00:15:36 -0500 Subject: [PATCH 3/9] Address review comments. - Cap the total number of requests at twice the maximum number of keys (currently 263, so 526 requests). Basically you can set and then query every key in one message. This is an absurdly high number but should prevent serious DOS attacks. - Clarify meaning of new hex color codes. - Better handle sending messages to the renderer in a way that should prevent deadlocks. - Handle 0-255 palette color requests by creatively using non-exhautive enums. - Fix an error in the query reply. --- src/terminal/color.zig | 6 +- src/terminal/osc.zig | 74 +++++++++++++++++---- src/termio/stream_handler.zig | 119 ++++++++++++++++++++++++++-------- 3 files changed, 155 insertions(+), 44 deletions(-) diff --git a/src/terminal/color.zig b/src/terminal/color.zig index c8929b060..46aa2aaae 100644 --- a/src/terminal/color.zig +++ b/src/terminal/color.zig @@ -208,9 +208,11 @@ pub const RGB = struct { /// where , , and are floating point values between /// 0.0 and 1.0 (inclusive). /// - /// 3. #hhh, #hhhhhh, #hhhhhhhhh #hhhhhhhhhhhh + /// 3. #rgb, #rrggbb, #rrrgggbbb #rrrrggggbbbb /// - /// where `h` is a single hexadecimal digit. + /// where `r`, `g`, and `b` are a single hexadecimal digit. + /// These specifiy a color with 4, 8, 12, and 16 bits of precision + /// per color channel. pub fn parse(value: []const u8) !RGB { if (value.len == 0) { return error.InvalidFormat; diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 89bd8e976..fd3917d60 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -174,16 +174,24 @@ pub const Command = union(enum) { }; pub const KittyColorProtocol = struct { - const Kind = enum { - foreground, - background, - selection_foreground, - selection_background, - cursor, - cursor_text, - visual_bell, - second_transparent_background, + const Kind = enum(u9) { + // These _must_ start at 256 since enum values 0-255 are reserved + // for the palette. + foreground = 256, + background = 257, + selection_foreground = 258, + selection_background = 259, + cursor = 260, + cursor_text = 261, + visual_bell = 262, + second_transparent_background = 263, + _, + + // Make sure that this stays in sync with the higest numbered enum + // value. + const max: u9 = 263; }; + const Request = union(enum) { query: Kind, set: struct { @@ -1024,9 +1032,18 @@ pub const Parser = struct { return; } - const key = std.meta.stringToEnum(Command.KittyColorProtocol.Kind, self.temp_state.key) orelse { - log.warn("unknown key in kitty color protocol: {s}", .{self.temp_state.key}); - return; + const key = key: { + break :key std.meta.stringToEnum(Command.KittyColorProtocol.Kind, self.temp_state.key) orelse { + const v = std.fmt.parseUnsigned(u9, self.temp_state.key, 10) catch { + log.warn("unknown key in kitty color protocol: {s}", .{self.temp_state.key}); + return; + }; + if (v > 255) { + log.warn("unknown key in kitty color protocol: {s}", .{self.temp_state.key}); + return; + } + break :key @as(Command.KittyColorProtocol.Kind, @enumFromInt(v)); + }; }; const value = value: { @@ -1037,6 +1054,11 @@ pub const Parser = struct { switch (self.command) { .kitty_color_protocol => |*v| { + if (v.list.items.len >= @as(usize, Command.KittyColorProtocol.Kind.max) * 2) { + self.state = .invalid; + log.warn("exceeded limit for number of keys in kitty color protocol, ignoring", .{}); + return; + } if (kind == .key_only) { v.list.append(.{ .reset = key }) catch |err| { log.warn("unable to append kitty color protocol option: {}", .{err}); @@ -1651,12 +1673,12 @@ test "OSC: kitty color protocol" { var p: Parser = .{ .alloc = testing.allocator }; defer p.deinit(); - const input = "21;foreground=?;background=rgb:f0/f8/ff;cursor=aliceblue;cursor_text;visual_bell=;selection_foreground=#xxxyyzz;selection_background=?;selection_background=#aabbcc"; + const input = "21;foreground=?;background=rgb:f0/f8/ff;cursor=aliceblue;cursor_text;visual_bell=;selection_foreground=#xxxyyzz;selection_background=?;selection_background=#aabbcc;2=?;3=rgbi:1.0/1.0/1.0"; for (input) |ch| p.next(ch); const cmd = p.end('\x1b').?; try testing.expect(cmd == .kitty_color_protocol); - try testing.expectEqual(@as(usize, 7), cmd.kitty_color_protocol.list.items.len); + try testing.expectEqual(@as(usize, 9), cmd.kitty_color_protocol.list.items.len); try testing.expect(cmd.kitty_color_protocol.list.items[0] == .query); try testing.expectEqual(@as(Command.KittyColorProtocol.Kind, .foreground), cmd.kitty_color_protocol.list.items[0].query); try testing.expect(cmd.kitty_color_protocol.list.items[1] == .set); @@ -1680,4 +1702,28 @@ test "OSC: kitty color protocol" { try testing.expectEqual(@as(u8, 0xaa), cmd.kitty_color_protocol.list.items[6].set.color.r); try testing.expectEqual(@as(u8, 0xbb), cmd.kitty_color_protocol.list.items[6].set.color.g); try testing.expectEqual(@as(u8, 0xcc), cmd.kitty_color_protocol.list.items[6].set.color.b); + try testing.expect(cmd.kitty_color_protocol.list.items[7] == .query); + try testing.expectEqual(@as(Command.KittyColorProtocol.Kind, @enumFromInt(2)), cmd.kitty_color_protocol.list.items[7].query); + try testing.expect(cmd.kitty_color_protocol.list.items[8] == .set); + try testing.expectEqual(@as(Command.KittyColorProtocol.Kind, @enumFromInt(3)), cmd.kitty_color_protocol.list.items[8].set.key); + try testing.expectEqual(@as(u8, 0xff), cmd.kitty_color_protocol.list.items[8].set.color.r); + try testing.expectEqual(@as(u8, 0xff), cmd.kitty_color_protocol.list.items[8].set.color.g); + try testing.expectEqual(@as(u8, 0xff), cmd.kitty_color_protocol.list.items[8].set.color.b); +} + +test "OSC: kitty color protocol kind" { + const info = @typeInfo(Command.KittyColorProtocol.Kind); + + try std.testing.expectEqual(false, info.Enum.is_exhaustive); + + var min: usize = std.math.maxInt(info.Enum.tag_type); + var max: usize = 0; + + inline for (info.Enum.fields) |field| { + if (field.value > max) max = field.value; + if (field.value < min) min = field.value; + } + + try std.testing.expect(min >= 256); + try std.testing.expect(max == Command.KittyColorProtocol.Kind.max); } diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index b3f31e4b0..2299c6e47 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -1279,73 +1279,136 @@ pub const StreamHandler = struct { for (request.list.items) |item| { switch (item) { .query => |key| { + const i = @intFromEnum(key); const color = switch (key) { .foreground => self.foreground_color, .background => self.background_color, .cursor => self.cursor_color, - else => { - log.warn("ignoring unsupported kitty color protocol key: {s}", .{@tagName(key)}); - continue; + else => color: { + if (i > 255) { + log.warn("ignoring unsupported kitty color protocol key: {s}", .{@tagName(key)}); + continue; + } + break :color self.terminal.color_palette.colors[i]; }, } orelse { log.warn("no color configured for: {s}", .{@tagName(key)}); continue; }; - try writer.print( - ";rgb:{x:0>2}/{x:0>2}/{x:0>2}", - .{ - @as(u16, color.r), - @as(u16, color.g), - @as(u16, color.b), - }, - ); + if (i <= 255) + try writer.print( + ";{d}=rgb:{x:0>2}/{x:0>2}/{x:0>2}", + .{ i, color.r, color.g, color.b }, + ) + else + try writer.print( + ";{s}=rgb:{x:0>2}/{x:0>2}/{x:0>2}", + .{ @tagName(key), color.r, color.g, color.b }, + ); }, .set => |v| switch (v.key) { .foreground => { self.foreground_color = v.color; - _ = self.renderer_mailbox.push(.{ + // See messageWriter which has similar logic and + // explains why we may have to do this. + const msg: renderer.Message = .{ .foreground_color = v.color, - }, .{ .forever = {} }); + }; + if (self.renderer_mailbox.push(msg, .{ .instant = {} }) == 0) { + self.renderer_state.mutex.unlock(); + defer self.renderer_state.mutex.lock(); + _ = self.renderer_mailbox.push(msg, .{ .forever = {} }); + } }, .background => { self.background_color = v.color; + // See messageWriter which has similar logic and + // explains why we may have to do this. + const msg: renderer.Message = .{ + .background_color = v.color, + }; + if (self.renderer_mailbox.push(msg, .{ .instant = {} }) == 0) { + self.renderer_state.mutex.unlock(); + defer self.renderer_state.mutex.lock(); + _ = self.renderer_mailbox.push(msg, .{ .forever = {} }); + } _ = self.renderer_mailbox.push(.{ .background_color = v.color, }, .{ .forever = {} }); }, .cursor => { self.cursor_color = v.color; - _ = self.renderer_mailbox.push(.{ + // See messageWriter which has similar logic and + // explains why we may have to do this. + const msg: renderer.Message = .{ .cursor_color = v.color, - }, .{ .forever = {} }); + }; + if (self.renderer_mailbox.push(msg, .{ .instant = {} }) == 0) { + self.renderer_state.mutex.unlock(); + defer self.renderer_state.mutex.lock(); + _ = self.renderer_mailbox.push(msg, .{ .forever = {} }); + } }, else => { - log.warn("ignoring unsupported kitty color protocol key: {s}", .{@tagName(v.key)}); - continue; + const i = @intFromEnum(v.key); + if (i > 255) { + log.warn("ignoring unsupported kitty color protocol key: {s}", .{@tagName(v.key)}); + continue; + } + self.terminal.flags.dirty.palette = true; + self.terminal.color_palette.colors[i] = v.color; + self.terminal.color_palette.mask.unset(i); }, }, .reset => |key| switch (key) { .foreground => { self.foreground_color = self.default_foreground_color; - _ = self.renderer_mailbox.push(.{ - .foreground_color = self.foreground_color, - }, .{ .forever = {} }); + // See messageWriter which has similar logic and + // explains why we may have to do this. + const msg: renderer.Message = .{ + .foreground_color = self.default_foreground_color, + }; + if (self.renderer_mailbox.push(msg, .{ .instant = {} }) == 0) { + self.renderer_state.mutex.unlock(); + defer self.renderer_state.mutex.lock(); + _ = self.renderer_mailbox.push(msg, .{ .forever = {} }); + } }, .background => { self.background_color = self.default_background_color; - _ = self.renderer_mailbox.push(.{ - .background_color = self.background_color, - }, .{ .forever = {} }); + // See messageWriter which has similar logic and + // explains why we may have to do this. + const msg: renderer.Message = .{ + .background_color = self.default_background_color, + }; + if (self.renderer_mailbox.push(msg, .{ .instant = {} }) == 0) { + self.renderer_state.mutex.unlock(); + defer self.renderer_state.mutex.lock(); + _ = self.renderer_mailbox.push(msg, .{ .forever = {} }); + } }, .cursor => { self.cursor_color = self.default_cursor_color; - _ = self.renderer_mailbox.push(.{ - .cursor_color = self.cursor_color, - }, .{ .forever = {} }); + // See messageWriter which has similar logic and + // explains why we may have to do this. + const msg: renderer.Message = .{ + .cursor_color = self.default_cursor_color, + }; + if (self.renderer_mailbox.push(msg, .{ .instant = {} }) == 0) { + self.renderer_state.mutex.unlock(); + defer self.renderer_state.mutex.lock(); + _ = self.renderer_mailbox.push(msg, .{ .forever = {} }); + } }, else => { - log.warn("ignoring unsupported kitty color protocol key: {s}", .{@tagName(key)}); - continue; + const i = @intFromEnum(key); + if (i > 255) { + log.warn("ignoring unsupported kitty color protocol key: {s}", .{@tagName(key)}); + continue; + } + self.terminal.flags.dirty.palette = true; + self.terminal.color_palette.colors[i] = self.terminal.default_palette[i]; + self.terminal.color_palette.mask.unset(i); }, }, } From f4b292543465e428f215057b6a86e62101275e03 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 20 Aug 2024 11:10:51 -0700 Subject: [PATCH 4/9] terminal: make kitty color tests a bit more readable --- src/terminal/osc.zig | 87 +++++++++++++++++++++++++++++--------------- 1 file changed, 57 insertions(+), 30 deletions(-) diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index fd3917d60..05026b1eb 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -1679,36 +1679,63 @@ test "OSC: kitty color protocol" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .kitty_color_protocol); try testing.expectEqual(@as(usize, 9), cmd.kitty_color_protocol.list.items.len); - try testing.expect(cmd.kitty_color_protocol.list.items[0] == .query); - try testing.expectEqual(@as(Command.KittyColorProtocol.Kind, .foreground), cmd.kitty_color_protocol.list.items[0].query); - try testing.expect(cmd.kitty_color_protocol.list.items[1] == .set); - try testing.expectEqual(@as(Command.KittyColorProtocol.Kind, .background), cmd.kitty_color_protocol.list.items[1].set.key); - try testing.expectEqual(@as(u8, 0xf0), cmd.kitty_color_protocol.list.items[1].set.color.r); - try testing.expectEqual(@as(u8, 0xf8), cmd.kitty_color_protocol.list.items[1].set.color.g); - try testing.expectEqual(@as(u8, 0xff), cmd.kitty_color_protocol.list.items[1].set.color.b); - try testing.expect(cmd.kitty_color_protocol.list.items[2] == .set); - try testing.expectEqual(@as(Command.KittyColorProtocol.Kind, .cursor), cmd.kitty_color_protocol.list.items[2].set.key); - try testing.expectEqual(@as(u8, 0xf0), cmd.kitty_color_protocol.list.items[2].set.color.r); - try testing.expectEqual(@as(u8, 0xf8), cmd.kitty_color_protocol.list.items[2].set.color.g); - try testing.expectEqual(@as(u8, 0xff), cmd.kitty_color_protocol.list.items[2].set.color.b); - try testing.expect(cmd.kitty_color_protocol.list.items[3] == .reset); - try testing.expectEqual(@as(Command.KittyColorProtocol.Kind, .cursor_text), cmd.kitty_color_protocol.list.items[3].reset); - try testing.expect(cmd.kitty_color_protocol.list.items[4] == .reset); - try testing.expectEqual(@as(Command.KittyColorProtocol.Kind, .visual_bell), cmd.kitty_color_protocol.list.items[4].reset); - try testing.expect(cmd.kitty_color_protocol.list.items[5] == .query); - try testing.expectEqual(@as(Command.KittyColorProtocol.Kind, .selection_background), cmd.kitty_color_protocol.list.items[5].query); - try testing.expect(cmd.kitty_color_protocol.list.items[6] == .set); - try testing.expectEqual(@as(Command.KittyColorProtocol.Kind, .selection_background), cmd.kitty_color_protocol.list.items[6].set.key); - try testing.expectEqual(@as(u8, 0xaa), cmd.kitty_color_protocol.list.items[6].set.color.r); - try testing.expectEqual(@as(u8, 0xbb), cmd.kitty_color_protocol.list.items[6].set.color.g); - try testing.expectEqual(@as(u8, 0xcc), cmd.kitty_color_protocol.list.items[6].set.color.b); - try testing.expect(cmd.kitty_color_protocol.list.items[7] == .query); - try testing.expectEqual(@as(Command.KittyColorProtocol.Kind, @enumFromInt(2)), cmd.kitty_color_protocol.list.items[7].query); - try testing.expect(cmd.kitty_color_protocol.list.items[8] == .set); - try testing.expectEqual(@as(Command.KittyColorProtocol.Kind, @enumFromInt(3)), cmd.kitty_color_protocol.list.items[8].set.key); - try testing.expectEqual(@as(u8, 0xff), cmd.kitty_color_protocol.list.items[8].set.color.r); - try testing.expectEqual(@as(u8, 0xff), cmd.kitty_color_protocol.list.items[8].set.color.g); - try testing.expectEqual(@as(u8, 0xff), cmd.kitty_color_protocol.list.items[8].set.color.b); + { + const item = cmd.kitty_color_protocol.list.items[0]; + try testing.expect(item == .query); + try testing.expectEqual(Command.KittyColorProtocol.Kind.foreground, item.query); + } + { + const item = cmd.kitty_color_protocol.list.items[1]; + try testing.expect(item == .set); + try testing.expectEqual(Command.KittyColorProtocol.Kind.background, item.set.key); + try testing.expectEqual(@as(u8, 0xf0), item.set.color.r); + try testing.expectEqual(@as(u8, 0xf8), item.set.color.g); + try testing.expectEqual(@as(u8, 0xff), item.set.color.b); + } + { + const item = cmd.kitty_color_protocol.list.items[2]; + try testing.expect(item == .set); + try testing.expectEqual(Command.KittyColorProtocol.Kind.cursor, item.set.key); + try testing.expectEqual(@as(u8, 0xf0), item.set.color.r); + try testing.expectEqual(@as(u8, 0xf8), item.set.color.g); + try testing.expectEqual(@as(u8, 0xff), item.set.color.b); + } + { + const item = cmd.kitty_color_protocol.list.items[3]; + try testing.expect(item == .reset); + try testing.expectEqual(Command.KittyColorProtocol.Kind.cursor_text, item.reset); + } + { + const item = cmd.kitty_color_protocol.list.items[4]; + try testing.expect(item == .reset); + try testing.expectEqual(Command.KittyColorProtocol.Kind.visual_bell, item.reset); + } + { + const item = cmd.kitty_color_protocol.list.items[5]; + try testing.expect(item == .query); + try testing.expectEqual(Command.KittyColorProtocol.Kind.selection_background, item.query); + } + { + const item = cmd.kitty_color_protocol.list.items[6]; + try testing.expect(item == .set); + try testing.expectEqual(Command.KittyColorProtocol.Kind.selection_background, item.set.key); + try testing.expectEqual(@as(u8, 0xaa), item.set.color.r); + try testing.expectEqual(@as(u8, 0xbb), item.set.color.g); + try testing.expectEqual(@as(u8, 0xcc), item.set.color.b); + } + { + const item = cmd.kitty_color_protocol.list.items[7]; + try testing.expect(item == .query); + try testing.expectEqual(@as(Command.KittyColorProtocol.Kind, @enumFromInt(2)), item.query); + } + { + const item = cmd.kitty_color_protocol.list.items[8]; + try testing.expect(item == .set); + try testing.expectEqual(@as(Command.KittyColorProtocol.Kind, @enumFromInt(3)), item.set.key); + try testing.expectEqual(@as(u8, 0xff), item.set.color.r); + try testing.expectEqual(@as(u8, 0xff), item.set.color.g); + try testing.expectEqual(@as(u8, 0xff), item.set.color.b); + } } test "OSC: kitty color protocol kind" { From 3b2ed40854c40c4a3a03a2dc798312dc41d38bd8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 20 Aug 2024 23:21:03 -0400 Subject: [PATCH 5/9] terminal: stylistic tweaks to kitty color protocol parsing --- src/terminal/osc.zig | 88 ++++++++++++++++++++++++-------------------- 1 file changed, 49 insertions(+), 39 deletions(-) diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 05026b1eb..4ecde7736 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -370,15 +370,18 @@ pub const Parser = struct { self.buf_start = 0; self.buf_idx = 0; self.complete = false; - if (self.command == .kitty_color_protocol) { - self.command.kitty_color_protocol.list.deinit(); - } if (self.buf_dynamic) |ptr| { const alloc = self.alloc.?; ptr.deinit(alloc); alloc.destroy(ptr); self.buf_dynamic = null; } + + // Some commands have their own memory management we need to clear. + switch (self.command) { + .kitty_color_protocol => |*v| v.list.deinit(), + else => {}, + } } /// Consume the next character c and advance the parser state. @@ -502,10 +505,16 @@ pub const Parser = struct { }, .@"21" => switch (c) { - ';' => { + ';' => kitty: { + const alloc = self.alloc orelse { + log.info("OSC 21 requires an allocator, but none was provided", .{}); + self.state = .invalid; + break :kitty; + }; + self.command = .{ .kitty_color_protocol = .{ - .list = std.ArrayList(Command.KittyColorProtocol.Request).init(self.alloc.?), + .list = std.ArrayList(Command.KittyColorProtocol.Request).init(alloc), }, }; @@ -1032,19 +1041,19 @@ pub const Parser = struct { return; } - const key = key: { - break :key std.meta.stringToEnum(Command.KittyColorProtocol.Kind, self.temp_state.key) orelse { - const v = std.fmt.parseUnsigned(u9, self.temp_state.key, 10) catch { - log.warn("unknown key in kitty color protocol: {s}", .{self.temp_state.key}); - return; - }; - if (v > 255) { - log.warn("unknown key in kitty color protocol: {s}", .{self.temp_state.key}); - return; - } - break :key @as(Command.KittyColorProtocol.Kind, @enumFromInt(v)); - }; - }; + // For our key, we first try to parse it as a special key. If that + // doesn't work then we try to parse it as a number for a palette. + const key: Command.KittyColorProtocol.Kind = std.meta.stringToEnum( + Command.KittyColorProtocol.Kind, + self.temp_state.key, + ) orelse @enumFromInt(std.fmt.parseUnsigned( + u8, + self.temp_state.key, + 10, + ) catch { + log.warn("unknown key in kitty color protocol: {s}", .{self.temp_state.key}); + return; + }); const value = value: { if (self.buf_start == self.buf_idx) break :value ""; @@ -1054,34 +1063,25 @@ pub const Parser = struct { switch (self.command) { .kitty_color_protocol => |*v| { + // Cap our allocation amount for our list. if (v.list.items.len >= @as(usize, Command.KittyColorProtocol.Kind.max) * 2) { self.state = .invalid; log.warn("exceeded limit for number of keys in kitty color protocol, ignoring", .{}); return; } - if (kind == .key_only) { + + if (kind == .key_only or value.len == 0) { v.list.append(.{ .reset = key }) catch |err| { log.warn("unable to append kitty color protocol option: {}", .{err}); return; }; - return; - } - if (value.len == 0) { - v.list.append(.{ .reset = key }) catch |err| { - log.warn("unable to append kitty color protocol option: {}", .{err}); - return; - }; - return; - } - if (mem.eql(u8, "?", value)) { + } else if (mem.eql(u8, "?", value)) { v.list.append(.{ .query = key }) catch |err| { log.warn("unable to append kitty color protocol option: {}", .{err}); return; }; - return; - } - v.list.append( - .{ + } else { + v.list.append(.{ .set = .{ .key = key, .color = RGB.parse(value) catch |err| switch (err) { @@ -1091,12 +1091,11 @@ pub const Parser = struct { }, }, }, - }, - ) catch |err| { - log.warn("unable to append kitty color protocol option: {}", .{err}); - return; - }; - return; + }) catch |err| { + log.warn("unable to append kitty color protocol option: {}", .{err}); + return; + }; + } }, else => {}, } @@ -1738,6 +1737,17 @@ test "OSC: kitty color protocol" { } } +test "OSC: kitty color protocol without allocator" { + const testing = std.testing; + + var p: Parser = .{}; + defer p.deinit(); + + const input = "21;foreground=?"; + for (input) |ch| p.next(ch); + try testing.expect(p.end('\x1b') == null); +} + test "OSC: kitty color protocol kind" { const info = @typeInfo(Command.KittyColorProtocol.Kind); From e12cfe80b0a59556917a90daab13992e7098dbde Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 20 Aug 2024 23:31:21 -0400 Subject: [PATCH 6/9] terminal: formatter for kitty color protocol kinds --- src/terminal/osc.zig | 45 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 4ecde7736..20986b667 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -175,6 +175,10 @@ pub const Command = union(enum) { pub const KittyColorProtocol = struct { const Kind = enum(u9) { + // Make sure that this stays in sync with the higest numbered enum + // value. + const max: u9 = 263; + // These _must_ start at 256 since enum values 0-255 are reserved // for the palette. foreground = 256, @@ -187,9 +191,29 @@ pub const Command = union(enum) { second_transparent_background = 263, _, - // Make sure that this stays in sync with the higest numbered enum - // value. - const max: u9 = 263; + /// Return the palette index that this kind is representing + /// or null if its a special color. + pub fn palette(self: Kind) ?u8 { + return std.math.cast(u8, @intFromEnum(self)) orelse null; + } + + pub fn format( + self: Kind, + comptime layout: []const u8, + opts: std.fmt.FormatOptions, + writer: anytype, + ) !void { + _ = layout; + _ = opts; + + // Format as a number if its a palette color otherwise + // format as a string. + if (self.palette()) |idx| { + try writer.print("{}", .{idx}); + } else { + try writer.print("{s}", .{@tagName(self)}); + } + } }; const Request = union(enum) { @@ -1764,3 +1788,18 @@ test "OSC: kitty color protocol kind" { try std.testing.expect(min >= 256); try std.testing.expect(max == Command.KittyColorProtocol.Kind.max); } + +test "OSC: kitty color protocol kind string" { + const testing = std.testing; + const Kind = Command.KittyColorProtocol.Kind; + + var buf: [256]u8 = undefined; + { + const actual = try std.fmt.bufPrint(&buf, "{}", .{Kind.foreground}); + try testing.expectEqualStrings("foreground", actual); + } + { + const actual = try std.fmt.bufPrint(&buf, "{}", .{@as(Kind, @enumFromInt(42))}); + try testing.expectEqualStrings("42", actual); + } +} From e15db8865664c3d58b0c758fd51ebf4038f72e70 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 20 Aug 2024 23:36:34 -0400 Subject: [PATCH 7/9] termio: use new formatter, palette helper for kitty color protocol --- src/termio/stream_handler.zig | 72 +++++++++++++++++------------------ 1 file changed, 35 insertions(+), 37 deletions(-) diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 2299c6e47..1c6f450ee 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -1272,43 +1272,37 @@ pub const StreamHandler = struct { pub fn sendKittyColorReport(self: *StreamHandler, request: terminal.osc.Command.KittyColorProtocol) !void { var buf = std.ArrayList(u8).init(self.alloc); - errdefer buf.deinit(); + defer buf.deinit(); const writer = buf.writer(); try writer.writeAll("\x1b[21"); for (request.list.items) |item| { switch (item) { .query => |key| { - const i = @intFromEnum(key); const color = switch (key) { .foreground => self.foreground_color, .background => self.background_color, .cursor => self.cursor_color, - else => color: { - if (i > 255) { - log.warn("ignoring unsupported kitty color protocol key: {s}", .{@tagName(key)}); - continue; - } - break :color self.terminal.color_palette.colors[i]; + else => if (key.palette()) |idx| + self.terminal.color_palette.colors[idx] + else { + log.warn("ignoring unsupported kitty color protocol key: {}", .{key}); + continue; }, } orelse { log.warn("no color configured for: {s}", .{@tagName(key)}); continue; }; - if (i <= 255) - try writer.print( - ";{d}=rgb:{x:0>2}/{x:0>2}/{x:0>2}", - .{ i, color.r, color.g, color.b }, - ) - else - try writer.print( - ";{s}=rgb:{x:0>2}/{x:0>2}/{x:0>2}", - .{ @tagName(key), color.r, color.g, color.b }, - ); + + try writer.print( + ";{}=rgb:{x:0>2}/{x:0>2}/{x:0>2}", + .{ key, color.r, color.g, color.b }, + ); }, .set => |v| switch (v.key) { .foreground => { self.foreground_color = v.color; + // See messageWriter which has similar logic and // explains why we may have to do this. const msg: renderer.Message = .{ @@ -1322,6 +1316,7 @@ pub const StreamHandler = struct { }, .background => { self.background_color = v.color; + // See messageWriter which has similar logic and // explains why we may have to do this. const msg: renderer.Message = .{ @@ -1332,12 +1327,10 @@ pub const StreamHandler = struct { defer self.renderer_state.mutex.lock(); _ = self.renderer_mailbox.push(msg, .{ .forever = {} }); } - _ = self.renderer_mailbox.push(.{ - .background_color = v.color, - }, .{ .forever = {} }); }, .cursor => { self.cursor_color = v.color; + // See messageWriter which has similar logic and // explains why we may have to do this. const msg: renderer.Message = .{ @@ -1349,20 +1342,23 @@ pub const StreamHandler = struct { _ = self.renderer_mailbox.push(msg, .{ .forever = {} }); } }, - else => { - const i = @intFromEnum(v.key); - if (i > 255) { - log.warn("ignoring unsupported kitty color protocol key: {s}", .{@tagName(v.key)}); - continue; - } + + else => if (v.key.palette()) |i| { self.terminal.flags.dirty.palette = true; self.terminal.color_palette.colors[i] = v.color; self.terminal.color_palette.mask.unset(i); + } else { + log.warn( + "ignoring unsupported kitty color protocol key: {}", + .{v.key}, + ); + continue; }, }, .reset => |key| switch (key) { .foreground => { self.foreground_color = self.default_foreground_color; + // See messageWriter which has similar logic and // explains why we may have to do this. const msg: renderer.Message = .{ @@ -1376,6 +1372,7 @@ pub const StreamHandler = struct { }, .background => { self.background_color = self.default_background_color; + // See messageWriter which has similar logic and // explains why we may have to do this. const msg: renderer.Message = .{ @@ -1389,6 +1386,7 @@ pub const StreamHandler = struct { }, .cursor => { self.cursor_color = self.default_cursor_color; + // See messageWriter which has similar logic and // explains why we may have to do this. const msg: renderer.Message = .{ @@ -1400,15 +1398,17 @@ pub const StreamHandler = struct { _ = self.renderer_mailbox.push(msg, .{ .forever = {} }); } }, - else => { - const i = @intFromEnum(key); - if (i > 255) { - log.warn("ignoring unsupported kitty color protocol key: {s}", .{@tagName(key)}); - continue; - } + + else => if (key.palette()) |i| { self.terminal.flags.dirty.palette = true; self.terminal.color_palette.colors[i] = self.terminal.default_palette[i]; self.terminal.color_palette.mask.unset(i); + } else { + log.warn( + "ignoring unsupported kitty color protocol key: {}", + .{key}, + ); + continue; }, }, } @@ -1416,13 +1416,11 @@ pub const StreamHandler = struct { try writer.writeAll(request.terminator.string()); - const msg = termio.Message{ + self.messageWriter(.{ .write_alloc = .{ .alloc = self.alloc, .data = try buf.toOwnedSlice(), }, - }; - - self.messageWriter(msg); + }); } }; From 9faca303dc12ad8d2876897a9bea5f45161ae7c9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 21 Aug 2024 09:11:11 -0400 Subject: [PATCH 8/9] termio: move renderer message sending out to separate func --- src/termio/stream_handler.zig | 92 +++++++++++++++++++---------------- 1 file changed, 49 insertions(+), 43 deletions(-) diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 1c6f450ee..c4d808f49 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -144,6 +144,38 @@ pub const StreamHandler = struct { self.termio_messaged = true; } + /// Send a renderer message and unlock the renderer state mutex + /// if necessary to ensure we don't deadlock. + /// + /// This assumes the renderer state mutex is locked. + inline fn rendererMessageWriter( + self: *StreamHandler, + msg: renderer.Message, + ) void { + // See termio.Mailbox.send for more details on how this works. + + // Try instant first. If it works then we can return. + if (self.renderer_mailbox.push(msg, .{ .instant = {} }) > 0) { + return; + } + + // Instant would have blocked. Release the renderer mutex, + // wake up the renderer to allow it to process the message, + // and then try again. + self.renderer_state.mutex.unlock(); + defer self.renderer_state.mutex.lock(); + self.renderer_wakeup.notify() catch |err| { + // This is an EXTREMELY unlikely case. We still don't return + // and attempt to send the message because its most likely + // that everything is fine, but log in case a freeze happens. + log.warn( + "failed to notify renderer, may deadlock err={}", + .{err}, + ); + }; + _ = self.renderer_mailbox.push(msg, .{ .forever = {} }); + } + pub fn dcsHook(self: *StreamHandler, dcs: terminal.DCS) !void { var cmd = self.dcs.hook(self.alloc, dcs) orelse return; defer cmd.deinit(); @@ -1279,7 +1311,7 @@ pub const StreamHandler = struct { for (request.list.items) |item| { switch (item) { .query => |key| { - const color = switch (key) { + const color: terminal.color.RGB = switch (key) { .foreground => self.foreground_color, .background => self.background_color, .cursor => self.cursor_color, @@ -1305,42 +1337,27 @@ pub const StreamHandler = struct { // See messageWriter which has similar logic and // explains why we may have to do this. - const msg: renderer.Message = .{ + self.rendererMessageWriter(.{ .foreground_color = v.color, - }; - if (self.renderer_mailbox.push(msg, .{ .instant = {} }) == 0) { - self.renderer_state.mutex.unlock(); - defer self.renderer_state.mutex.lock(); - _ = self.renderer_mailbox.push(msg, .{ .forever = {} }); - } + }); }, .background => { self.background_color = v.color; // See messageWriter which has similar logic and // explains why we may have to do this. - const msg: renderer.Message = .{ + self.rendererMessageWriter(.{ .background_color = v.color, - }; - if (self.renderer_mailbox.push(msg, .{ .instant = {} }) == 0) { - self.renderer_state.mutex.unlock(); - defer self.renderer_state.mutex.lock(); - _ = self.renderer_mailbox.push(msg, .{ .forever = {} }); - } + }); }, .cursor => { self.cursor_color = v.color; // See messageWriter which has similar logic and // explains why we may have to do this. - const msg: renderer.Message = .{ + self.rendererMessageWriter(.{ .cursor_color = v.color, - }; - if (self.renderer_mailbox.push(msg, .{ .instant = {} }) == 0) { - self.renderer_state.mutex.unlock(); - defer self.renderer_state.mutex.lock(); - _ = self.renderer_mailbox.push(msg, .{ .forever = {} }); - } + }); }, else => if (v.key.palette()) |i| { @@ -1361,42 +1378,27 @@ pub const StreamHandler = struct { // See messageWriter which has similar logic and // explains why we may have to do this. - const msg: renderer.Message = .{ + self.rendererMessageWriter(.{ .foreground_color = self.default_foreground_color, - }; - if (self.renderer_mailbox.push(msg, .{ .instant = {} }) == 0) { - self.renderer_state.mutex.unlock(); - defer self.renderer_state.mutex.lock(); - _ = self.renderer_mailbox.push(msg, .{ .forever = {} }); - } + }); }, .background => { self.background_color = self.default_background_color; // See messageWriter which has similar logic and // explains why we may have to do this. - const msg: renderer.Message = .{ + self.rendererMessageWriter(.{ .background_color = self.default_background_color, - }; - if (self.renderer_mailbox.push(msg, .{ .instant = {} }) == 0) { - self.renderer_state.mutex.unlock(); - defer self.renderer_state.mutex.lock(); - _ = self.renderer_mailbox.push(msg, .{ .forever = {} }); - } + }); }, .cursor => { self.cursor_color = self.default_cursor_color; // See messageWriter which has similar logic and // explains why we may have to do this. - const msg: renderer.Message = .{ + self.rendererMessageWriter(.{ .cursor_color = self.default_cursor_color, - }; - if (self.renderer_mailbox.push(msg, .{ .instant = {} }) == 0) { - self.renderer_state.mutex.unlock(); - defer self.renderer_state.mutex.lock(); - _ = self.renderer_mailbox.push(msg, .{ .forever = {} }); - } + }); }, else => if (key.palette()) |i| { @@ -1422,5 +1424,9 @@ pub const StreamHandler = struct { .data = try buf.toOwnedSlice(), }, }); + + // Note: we don't have to do a queueRender here because every + // processed stream will queue a render once it is done processing + // the read() syscall. } }; From 6bf1acc5a2f7e0e1ba06d8adaad95bf31b94e61a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 21 Aug 2024 09:18:30 -0400 Subject: [PATCH 9/9] terminal: move kitty color structs out to kitty package --- src/terminal/kitty.zig | 1 + src/terminal/kitty/color.zig | 92 ++++++++++++++++++++++++++ src/terminal/osc.zig | 121 +++++----------------------------- src/termio/stream_handler.zig | 5 +- 4 files changed, 112 insertions(+), 107 deletions(-) create mode 100644 src/terminal/kitty/color.zig diff --git a/src/terminal/kitty.zig b/src/terminal/kitty.zig index 6492aca6f..482919f9f 100644 --- a/src/terminal/kitty.zig +++ b/src/terminal/kitty.zig @@ -1,6 +1,7 @@ //! Types and functions related to Kitty protocols. const key = @import("kitty/key.zig"); +pub const color = @import("kitty/color.zig"); pub const graphics = @import("kitty/graphics.zig"); pub const KeyFlags = key.Flags; diff --git a/src/terminal/kitty/color.zig b/src/terminal/kitty/color.zig new file mode 100644 index 000000000..97acbcaff --- /dev/null +++ b/src/terminal/kitty/color.zig @@ -0,0 +1,92 @@ +const std = @import("std"); +const terminal = @import("../main.zig"); +const RGB = terminal.color.RGB; +const Terminator = terminal.osc.Terminator; + +pub const OSC = struct { + pub const Request = union(enum) { + query: Kind, + set: struct { key: Kind, color: RGB }, + reset: Kind, + }; + + /// list of requests + list: std.ArrayList(Request), + + /// We must reply with the same string terminator (ST) as used in the + /// request. + terminator: Terminator = .st, +}; + +pub const Kind = enum(u9) { + // Make sure that this stays in sync with the higest numbered enum + // value. + pub const max: u9 = 263; + + // These _must_ start at 256 since enum values 0-255 are reserved + // for the palette. + foreground = 256, + background = 257, + selection_foreground = 258, + selection_background = 259, + cursor = 260, + cursor_text = 261, + visual_bell = 262, + second_transparent_background = 263, + _, + + /// Return the palette index that this kind is representing + /// or null if its a special color. + pub fn palette(self: Kind) ?u8 { + return std.math.cast(u8, @intFromEnum(self)) orelse null; + } + + pub fn format( + self: Kind, + comptime layout: []const u8, + opts: std.fmt.FormatOptions, + writer: anytype, + ) !void { + _ = layout; + _ = opts; + + // Format as a number if its a palette color otherwise + // format as a string. + if (self.palette()) |idx| { + try writer.print("{}", .{idx}); + } else { + try writer.print("{s}", .{@tagName(self)}); + } + } +}; + +test "OSC: kitty color protocol kind" { + const info = @typeInfo(Kind); + + try std.testing.expectEqual(false, info.Enum.is_exhaustive); + + var min: usize = std.math.maxInt(info.Enum.tag_type); + var max: usize = 0; + + inline for (info.Enum.fields) |field| { + if (field.value > max) max = field.value; + if (field.value < min) min = field.value; + } + + try std.testing.expect(min >= 256); + try std.testing.expect(max == Kind.max); +} + +test "OSC: kitty color protocol kind string" { + const testing = std.testing; + + var buf: [256]u8 = undefined; + { + const actual = try std.fmt.bufPrint(&buf, "{}", .{Kind.foreground}); + try testing.expectEqualStrings("foreground", actual); + } + { + const actual = try std.fmt.bufPrint(&buf, "{}", .{@as(Kind, @enumFromInt(42))}); + try testing.expectEqualStrings("42", actual); + } +} diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 20986b667..17b52ab13 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -10,6 +10,7 @@ const mem = std.mem; const assert = std.debug.assert; const Allocator = mem.Allocator; const RGB = @import("color.zig").RGB; +const kitty = @import("kitty.zig"); const log = std.log.scoped(.osc); @@ -140,7 +141,7 @@ pub const Command = union(enum) { /// Kitty color protocl, OSC 21 /// https://sw.kovidgoyal.net/kitty/color-stack/#id1 - kitty_color_protocol: KittyColorProtocol, + kitty_color_protocol: kitty.color.OSC, /// Show a desktop notification (OSC 9 or OSC 777) show_desktop_notification: struct { @@ -172,66 +173,6 @@ pub const Command = union(enum) { }; } }; - - pub const KittyColorProtocol = struct { - const Kind = enum(u9) { - // Make sure that this stays in sync with the higest numbered enum - // value. - const max: u9 = 263; - - // These _must_ start at 256 since enum values 0-255 are reserved - // for the palette. - foreground = 256, - background = 257, - selection_foreground = 258, - selection_background = 259, - cursor = 260, - cursor_text = 261, - visual_bell = 262, - second_transparent_background = 263, - _, - - /// Return the palette index that this kind is representing - /// or null if its a special color. - pub fn palette(self: Kind) ?u8 { - return std.math.cast(u8, @intFromEnum(self)) orelse null; - } - - pub fn format( - self: Kind, - comptime layout: []const u8, - opts: std.fmt.FormatOptions, - writer: anytype, - ) !void { - _ = layout; - _ = opts; - - // Format as a number if its a palette color otherwise - // format as a string. - if (self.palette()) |idx| { - try writer.print("{}", .{idx}); - } else { - try writer.print("{s}", .{@tagName(self)}); - } - } - }; - - const Request = union(enum) { - query: Kind, - set: struct { - key: Kind, - color: RGB, - }, - reset: Kind, - }; - - /// list of requests - list: std.ArrayList(Request), - - /// We must reply with the same string terminator (ST) as used in the - /// request. - terminator: Terminator = .st, - }; }; /// The terminator used to end an OSC command. For OSC commands that demand @@ -538,7 +479,7 @@ pub const Parser = struct { self.command = .{ .kitty_color_protocol = .{ - .list = std.ArrayList(Command.KittyColorProtocol.Request).init(alloc), + .list = std.ArrayList(kitty.color.OSC.Request).init(alloc), }, }; @@ -1067,8 +1008,8 @@ pub const Parser = struct { // For our key, we first try to parse it as a special key. If that // doesn't work then we try to parse it as a number for a palette. - const key: Command.KittyColorProtocol.Kind = std.meta.stringToEnum( - Command.KittyColorProtocol.Kind, + const key: kitty.color.Kind = std.meta.stringToEnum( + kitty.color.Kind, self.temp_state.key, ) orelse @enumFromInt(std.fmt.parseUnsigned( u8, @@ -1088,7 +1029,7 @@ pub const Parser = struct { switch (self.command) { .kitty_color_protocol => |*v| { // Cap our allocation amount for our list. - if (v.list.items.len >= @as(usize, Command.KittyColorProtocol.Kind.max) * 2) { + if (v.list.items.len >= @as(usize, kitty.color.Kind.max) * 2) { self.state = .invalid; log.warn("exceeded limit for number of keys in kitty color protocol, ignoring", .{}); return; @@ -1705,12 +1646,12 @@ test "OSC: kitty color protocol" { { const item = cmd.kitty_color_protocol.list.items[0]; try testing.expect(item == .query); - try testing.expectEqual(Command.KittyColorProtocol.Kind.foreground, item.query); + try testing.expectEqual(kitty.color.Kind.foreground, item.query); } { const item = cmd.kitty_color_protocol.list.items[1]; try testing.expect(item == .set); - try testing.expectEqual(Command.KittyColorProtocol.Kind.background, item.set.key); + try testing.expectEqual(kitty.color.Kind.background, item.set.key); try testing.expectEqual(@as(u8, 0xf0), item.set.color.r); try testing.expectEqual(@as(u8, 0xf8), item.set.color.g); try testing.expectEqual(@as(u8, 0xff), item.set.color.b); @@ -1718,7 +1659,7 @@ test "OSC: kitty color protocol" { { const item = cmd.kitty_color_protocol.list.items[2]; try testing.expect(item == .set); - try testing.expectEqual(Command.KittyColorProtocol.Kind.cursor, item.set.key); + try testing.expectEqual(kitty.color.Kind.cursor, item.set.key); try testing.expectEqual(@as(u8, 0xf0), item.set.color.r); try testing.expectEqual(@as(u8, 0xf8), item.set.color.g); try testing.expectEqual(@as(u8, 0xff), item.set.color.b); @@ -1726,22 +1667,22 @@ test "OSC: kitty color protocol" { { const item = cmd.kitty_color_protocol.list.items[3]; try testing.expect(item == .reset); - try testing.expectEqual(Command.KittyColorProtocol.Kind.cursor_text, item.reset); + try testing.expectEqual(kitty.color.Kind.cursor_text, item.reset); } { const item = cmd.kitty_color_protocol.list.items[4]; try testing.expect(item == .reset); - try testing.expectEqual(Command.KittyColorProtocol.Kind.visual_bell, item.reset); + try testing.expectEqual(kitty.color.Kind.visual_bell, item.reset); } { const item = cmd.kitty_color_protocol.list.items[5]; try testing.expect(item == .query); - try testing.expectEqual(Command.KittyColorProtocol.Kind.selection_background, item.query); + try testing.expectEqual(kitty.color.Kind.selection_background, item.query); } { const item = cmd.kitty_color_protocol.list.items[6]; try testing.expect(item == .set); - try testing.expectEqual(Command.KittyColorProtocol.Kind.selection_background, item.set.key); + try testing.expectEqual(kitty.color.Kind.selection_background, item.set.key); try testing.expectEqual(@as(u8, 0xaa), item.set.color.r); try testing.expectEqual(@as(u8, 0xbb), item.set.color.g); try testing.expectEqual(@as(u8, 0xcc), item.set.color.b); @@ -1749,12 +1690,12 @@ test "OSC: kitty color protocol" { { const item = cmd.kitty_color_protocol.list.items[7]; try testing.expect(item == .query); - try testing.expectEqual(@as(Command.KittyColorProtocol.Kind, @enumFromInt(2)), item.query); + try testing.expectEqual(@as(kitty.color.Kind, @enumFromInt(2)), item.query); } { const item = cmd.kitty_color_protocol.list.items[8]; try testing.expect(item == .set); - try testing.expectEqual(@as(Command.KittyColorProtocol.Kind, @enumFromInt(3)), item.set.key); + try testing.expectEqual(@as(kitty.color.Kind, @enumFromInt(3)), item.set.key); try testing.expectEqual(@as(u8, 0xff), item.set.color.r); try testing.expectEqual(@as(u8, 0xff), item.set.color.g); try testing.expectEqual(@as(u8, 0xff), item.set.color.b); @@ -1771,35 +1712,3 @@ test "OSC: kitty color protocol without allocator" { for (input) |ch| p.next(ch); try testing.expect(p.end('\x1b') == null); } - -test "OSC: kitty color protocol kind" { - const info = @typeInfo(Command.KittyColorProtocol.Kind); - - try std.testing.expectEqual(false, info.Enum.is_exhaustive); - - var min: usize = std.math.maxInt(info.Enum.tag_type); - var max: usize = 0; - - inline for (info.Enum.fields) |field| { - if (field.value > max) max = field.value; - if (field.value < min) min = field.value; - } - - try std.testing.expect(min >= 256); - try std.testing.expect(max == Command.KittyColorProtocol.Kind.max); -} - -test "OSC: kitty color protocol kind string" { - const testing = std.testing; - const Kind = Command.KittyColorProtocol.Kind; - - var buf: [256]u8 = undefined; - { - const actual = try std.fmt.bufPrint(&buf, "{}", .{Kind.foreground}); - try testing.expectEqualStrings("foreground", actual); - } - { - const actual = try std.fmt.bufPrint(&buf, "{}", .{@as(Kind, @enumFromInt(42))}); - try testing.expectEqualStrings("42", actual); - } -} diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index c4d808f49..02cb132ba 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -1302,7 +1302,10 @@ pub const StreamHandler = struct { } } - pub fn sendKittyColorReport(self: *StreamHandler, request: terminal.osc.Command.KittyColorProtocol) !void { + pub fn sendKittyColorReport( + self: *StreamHandler, + request: terminal.kitty.color.OSC, + ) !void { var buf = std.ArrayList(u8).init(self.alloc); defer buf.deinit(); const writer = buf.writer();