From 3afc8019d596e0954074963887f535d8a516cb49 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 11 Sep 2025 12:09:52 -0700 Subject: [PATCH] terminal: update parser to use new color parser and stream handler --- src/Surface.zig | 16 +- src/apprt/surface.zig | 5 +- src/terminal/Parser.zig | 10 +- src/terminal/osc.zig | 604 ++++++++++++---------------------- src/terminal/osc/color.zig | 22 +- src/terminal/stream.zig | 6 +- src/termio/stream_handler.zig | 196 +++++++---- 7 files changed, 367 insertions(+), 492 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index bfadb3be8..dd2babf9b 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -863,18 +863,24 @@ pub fn handleMessage(self: *Surface, msg: Message) !void { }, .unlocked); }, - .color_change => |change| { + .color_change => |change| color_change: { // Notify our apprt, but don't send a mode 2031 DSR report // because VT sequences were used to change the color. _ = try self.rt_app.performAction( .{ .surface = self }, .color_change, .{ - .kind = switch (change.kind) { - .background => .background, - .foreground => .foreground, - .cursor => .cursor, + .kind = switch (change.target) { .palette => |v| @enumFromInt(v), + .dynamic => |dyn| switch (dyn) { + .foreground => .foreground, + .background => .background, + .cursor => .cursor, + // Unsupported dynamic color change notification type + else => break :color_change, + }, + // Special colors aren't supported for change notification + .special => break :color_change, }, .r = change.color.r, .g = change.color.g, diff --git a/src/apprt/surface.zig b/src/apprt/surface.zig index a4070c668..e4effe128 100644 --- a/src/apprt/surface.zig +++ b/src/apprt/surface.zig @@ -78,10 +78,7 @@ pub const Message = union(enum) { password_input: bool, /// A terminal color was changed using OSC sequences. - color_change: struct { - kind: terminal.osc.Command.ColorOperation.Kind, - color: terminal.color.RGB, - }, + color_change: terminal.osc.color.ColoredTarget, /// Notifies the surface that a tick of the timer that is timing /// out selection scrolling has occurred. "selection scrolling" diff --git a/src/terminal/Parser.zig b/src/terminal/Parser.zig index 428274878..0c814ff68 100644 --- a/src/terminal/Parser.zig +++ b/src/terminal/Parser.zig @@ -915,15 +915,15 @@ test "osc: 112 incomplete sequence" { const cmd = a[0].?.osc_dispatch; try testing.expect(cmd == .color_operation); try testing.expectEqual(cmd.color_operation.terminator, .bel); - try testing.expect(cmd.color_operation.source == .reset_cursor); - try testing.expect(cmd.color_operation.operations.count() == 1); - var it = cmd.color_operation.operations.constIterator(0); + try testing.expect(cmd.color_operation.op == .osc_112); + try testing.expect(cmd.color_operation.requests.count() == 1); + var it = cmd.color_operation.requests.constIterator(0); { const op = it.next().?; try testing.expect(op.* == .reset); try testing.expectEqual( - osc.Command.ColorOperation.Kind.cursor, - op.reset, + osc.color.Request{ .reset = .{ .dynamic = .cursor } }, + op.*, ); } try std.testing.expect(it.next() == null); diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 997db0350..72efc949e 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -13,6 +13,7 @@ const Allocator = mem.Allocator; const RGB = @import("color.zig").RGB; const kitty = @import("kitty.zig"); const osc_color = @import("osc/color.zig"); +pub const color = osc_color; const log = std.log.scoped(.osc); @@ -122,10 +123,10 @@ pub const Command = union(enum) { /// /// Currently, these OSCs are handled by `color_operation`: /// - /// 4, 10, 11, 12, 104, 110, 111, 112 + /// 4, 5, 10-19, 104, 105, 110-119 color_operation: struct { - source: ColorOperation.Source, - operations: ColorOperation.List = .{}, + op: osc_color.Operation, + requests: osc_color.List = .{}, terminator: Terminator = .st, }, @@ -171,46 +172,6 @@ pub const Command = union(enum) { /// ConEmu GUI macro (OSC 9;6) conemu_guimacro: []const u8, - pub const ColorOperation = union(enum) { - pub const Source = enum(u16) { - // these numbers are based on the OSC operation code - // see https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands - get_set_palette = 4, - get_set_foreground = 10, - get_set_background = 11, - get_set_cursor = 12, - reset_palette = 104, - reset_foreground = 110, - reset_background = 111, - reset_cursor = 112, - - pub fn format( - self: Source, - comptime _: []const u8, - options: std.fmt.FormatOptions, - writer: anytype, - ) !void { - try std.fmt.formatInt(@intFromEnum(self), 10, .lower, options, writer); - } - }; - - pub const List = std.SegmentedList(ColorOperation, 2); - - pub const Kind = union(enum) { - palette: u8, - foreground, - background, - cursor, - }; - - set: struct { - kind: Kind, - color: RGB, - }, - reset: Kind, - report: Kind, - }; - pub const ProgressReport = struct { pub const State = enum(c_int) { remove, @@ -346,6 +307,12 @@ pub const Parser = struct { @"12", @"13", @"133", + @"14", + @"15", + @"16", + @"17", + @"18", + @"19", @"2", @"21", @"22", @@ -371,21 +338,8 @@ pub const Parser = struct { clipboard_kind, clipboard_kind_end, - // Get/set color palette index - osc_4_index, - osc_4_color, - - // Get/set foreground color - osc_10, - - // Get/set background color - osc_11, - - // Get/set cursor color - osc_12, - - // Reset color palette index - osc_104, + // OSC color operation. + osc_color, // Hyperlinks hyperlink_param_key, @@ -492,7 +446,7 @@ pub const Parser = struct { // Some commands have their own memory management we need to clear. switch (self.command) { .kitty_color_protocol => |*v| v.list.deinit(), - .color_operation => |*v| v.operations.deinit(self.alloc.?), + .color_operation => |*v| v.requests.deinit(self.alloc.?), else => {}, } @@ -580,6 +534,12 @@ pub const Parser = struct { '1' => self.state = .@"11", '2' => self.state = .@"12", '3' => self.state = .@"13", + '4' => self.state = .@"14", + '5' => self.state = .@"15", + '6' => self.state = .@"16", + '7' => self.state = .@"17", + '8' => self.state = .@"18", + '9' => self.state = .@"19", else => self.state = .invalid, }, @@ -590,12 +550,10 @@ pub const Parser = struct { self.state = .invalid; break :osc_10; } - self.command = .{ - .color_operation = .{ - .source = .get_set_foreground, - }, - }; - self.state = .osc_10; + self.command = .{ .color_operation = .{ + .op = .osc_10, + } }; + self.state = .osc_color; self.buf_start = self.buf_idx; self.complete = true; }, @@ -603,11 +561,6 @@ pub const Parser = struct { else => self.state = .invalid, }, - .osc_10, .osc_11, .osc_12 => switch (c) { - ';' => self.parseOSC101112(false), - else => {}, - }, - .@"104" => switch (c) { ';' => osc_104: { if (self.alloc == null) { @@ -617,21 +570,16 @@ pub const Parser = struct { } self.command = .{ .color_operation = .{ - .source = .reset_palette, + .op = .osc_104, }, }; - self.state = .osc_104; + self.state = .osc_color; self.buf_start = self.buf_idx; self.complete = true; }, else => self.state = .invalid, }, - .osc_104 => switch (c) { - ';' => self.parseOSC104(false), - else => {}, - }, - .@"11" => switch (c) { ';' => osc_11: { if (self.alloc == null) { @@ -639,47 +587,39 @@ pub const Parser = struct { self.state = .invalid; break :osc_11; } - self.command = .{ - .color_operation = .{ - .source = .get_set_background, - }, - }; - self.state = .osc_11; + self.command = .{ .color_operation = .{ + .op = .osc_11, + } }; + self.state = .osc_color; self.buf_start = self.buf_idx; self.complete = true; }, - '0'...'2' => blk: { + '0'...'9' => blk: { if (self.alloc == null) { log.warn("OSC 11{c} requires an allocator, but none was provided", .{c}); self.state = .invalid; break :blk; } - const alloc = self.alloc orelse return; - self.command = .{ .color_operation = .{ - .source = switch (c) { - '0' => .reset_foreground, - '1' => .reset_background, - '2' => .reset_cursor, + .op = switch (c) { + '0' => .osc_110, + '1' => .osc_111, + '2' => .osc_112, + '3' => .osc_113, + '4' => .osc_114, + '5' => .osc_115, + '6' => .osc_116, + '7' => .osc_117, + '8' => .osc_118, + '9' => .osc_119, else => unreachable, }, }, }; - const op = self.command.color_operation.operations.addOne(alloc) catch |err| { - log.warn("unable to append color operation: {}", .{err}); - return; - }; - op.* = .{ - .reset = switch (c) { - '0' => .foreground, - '1' => .background, - '2' => .cursor, - else => unreachable, - }, - }; - self.state = .swallow; + self.state = .osc_color; + self.buf_start = self.buf_idx; self.complete = true; }, else => self.state = .invalid, @@ -692,12 +632,10 @@ pub const Parser = struct { self.state = .invalid; break :osc_12; } - self.command = .{ - .color_operation = .{ - .source = .get_set_cursor, - }, - }; - self.state = .osc_12; + self.command = .{ .color_operation = .{ + .op = .osc_12, + } }; + self.state = .osc_color; self.buf_start = self.buf_idx; self.complete = true; }, @@ -705,6 +643,19 @@ pub const Parser = struct { }, .@"13" => switch (c) { + ';' => osc_13: { + if (self.alloc == null) { + log.warn("OSC 13 requires an allocator, but none was provided", .{}); + self.state = .invalid; + break :osc_13; + } + self.command = .{ .color_operation = .{ + .op = .osc_13, + } }; + self.state = .osc_color; + self.buf_start = self.buf_idx; + self.complete = true; + }, '3' => self.state = .@"133", else => self.state = .invalid, }, @@ -714,6 +665,110 @@ pub const Parser = struct { else => self.state = .invalid, }, + .@"14" => switch (c) { + ';' => osc_14: { + if (self.alloc == null) { + log.warn("OSC 14 requires an allocator, but none was provided", .{}); + self.state = .invalid; + break :osc_14; + } + self.command = .{ .color_operation = .{ + .op = .osc_14, + } }; + self.state = .osc_color; + self.buf_start = self.buf_idx; + self.complete = true; + }, + else => self.state = .invalid, + }, + + .@"15" => switch (c) { + ';' => osc_15: { + if (self.alloc == null) { + log.warn("OSC 15 requires an allocator, but none was provided", .{}); + self.state = .invalid; + break :osc_15; + } + self.command = .{ .color_operation = .{ + .op = .osc_15, + } }; + self.state = .osc_color; + self.buf_start = self.buf_idx; + self.complete = true; + }, + else => self.state = .invalid, + }, + + .@"16" => switch (c) { + ';' => osc_16: { + if (self.alloc == null) { + log.warn("OSC 16 requires an allocator, but none was provided", .{}); + self.state = .invalid; + break :osc_16; + } + self.command = .{ .color_operation = .{ + .op = .osc_16, + } }; + self.state = .osc_color; + self.buf_start = self.buf_idx; + self.complete = true; + }, + else => self.state = .invalid, + }, + + .@"17" => switch (c) { + ';' => osc_17: { + if (self.alloc == null) { + log.warn("OSC 17 requires an allocator, but none was provided", .{}); + self.state = .invalid; + break :osc_17; + } + self.command = .{ .color_operation = .{ + .op = .osc_17, + } }; + self.state = .osc_color; + self.buf_start = self.buf_idx; + self.complete = true; + }, + else => self.state = .invalid, + }, + + .@"18" => switch (c) { + ';' => osc_18: { + if (self.alloc == null) { + log.warn("OSC 18 requires an allocator, but none was provided", .{}); + self.state = .invalid; + break :osc_18; + } + self.command = .{ .color_operation = .{ + .op = .osc_18, + } }; + self.state = .osc_color; + self.buf_start = self.buf_idx; + self.complete = true; + }, + else => self.state = .invalid, + }, + + .@"19" => switch (c) { + ';' => osc_19: { + if (self.alloc == null) { + log.warn("OSC 19 requires an allocator, but none was provided", .{}); + self.state = .invalid; + break :osc_19; + } + self.command = .{ .color_operation = .{ + .op = .osc_19, + } }; + self.state = .osc_color; + self.buf_start = self.buf_idx; + self.complete = true; + }, + else => self.state = .invalid, + }, + + .osc_color => {}, + .@"2" => switch (c) { '1' => self.state = .@"21", '2' => self.state = .@"22", @@ -793,30 +848,32 @@ pub const Parser = struct { } self.command = .{ .color_operation = .{ - .source = .get_set_palette, + .op = .osc_4, }, }; - self.state = .osc_4_index; + self.state = .osc_color; self.buf_start = self.buf_idx; self.complete = true; }, else => self.state = .invalid, }, - .osc_4_index => switch (c) { - ';' => self.state = .osc_4_color, - else => {}, - }, - - .osc_4_color => switch (c) { - ';' => { - self.parseOSC4(false); - self.state = .osc_4_index; - }, - else => {}, - }, - .@"5" => switch (c) { + ';' => osc_5: { + if (self.alloc == null) { + log.info("OSC 5 requires an allocator, but none was provided", .{}); + self.state = .invalid; + break :osc_5; + } + self.command = .{ + .color_operation = .{ + .op = .osc_5, + }, + }; + self.state = .osc_color; + self.buf_start = self.buf_idx; + self.complete = true; + }, '2' => self.state = .@"52", else => self.state = .invalid, }, @@ -1480,178 +1537,28 @@ pub const Parser = struct { } } + fn endOscColor(self: *Parser) void { + const alloc = self.alloc.?; + assert(self.command == .color_operation); + const data = self.buf[self.buf_start..self.buf_idx]; + self.command.color_operation.requests = osc_color.parse( + alloc, + self.command.color_operation.op, + data, + ) catch |err| list: { + log.info( + "failed to parse OSC color request err={} data={s}", + .{ err, data }, + ); + break :list .{}; + }; + } + fn endAllocableString(self: *Parser) void { const list = self.buf_dynamic.?; self.temp_state.str.* = list.items; } - fn parseOSC4(self: *Parser, final: bool) void { - assert(self.state == .osc_4_color); - assert(self.command == .color_operation); - assert(self.command.color_operation.source == .get_set_palette); - - const alloc = self.alloc orelse return; - const operations = &self.command.color_operation.operations; - - const str = self.buf[self.buf_start .. self.buf_idx - (1 - @intFromBool(final))]; - self.buf_start = 0; - self.buf_idx = 0; - - var it = std.mem.splitScalar(u8, str, ';'); - const index_str = it.next() orelse { - log.warn("OSC 4 is missing palette index", .{}); - return; - }; - const spec_str = it.next() orelse { - log.warn("OSC 4 is missing color spec", .{}); - return; - }; - const index = std.fmt.parseUnsigned(u8, index_str, 10) catch |err| switch (err) { - error.Overflow, error.InvalidCharacter => { - log.warn("invalid color palette index in OSC 4: {s} {}", .{ index_str, err }); - return; - }, - }; - if (std.mem.eql(u8, spec_str, "?")) { - const op = operations.addOne(alloc) catch |err| { - log.warn("unable to append color operation: {}", .{err}); - return; - }; - op.* = .{ - .report = .{ .palette = index }, - }; - } else { - const color = RGB.parse(spec_str) catch |err| { - log.warn("invalid color specification in OSC 4: '{s}' {}", .{ spec_str, err }); - return; - }; - const op = operations.addOne(alloc) catch |err| { - log.warn("unable to append color operation: {}", .{err}); - return; - }; - op.* = .{ - .set = .{ - .kind = .{ - .palette = index, - }, - .color = color, - }, - }; - } - } - - fn parseOSC101112(self: *Parser, final: bool) void { - assert(switch (self.state) { - .osc_10, .osc_11, .osc_12 => true, - else => false, - }); - assert(self.command == .color_operation); - assert(self.command.color_operation.source == switch (self.state) { - .osc_10 => Command.ColorOperation.Source.get_set_foreground, - .osc_11 => Command.ColorOperation.Source.get_set_background, - .osc_12 => Command.ColorOperation.Source.get_set_cursor, - else => unreachable, - }); - - const spec_str = self.buf[self.buf_start .. self.buf_idx - (1 - @intFromBool(final))]; - - if (self.command.color_operation.operations.count() > 0) { - // don't emit the warning if the string is empty - if (spec_str.len == 0) return; - - log.warn("OSC 1{s} can only accept 1 color", .{switch (self.state) { - .osc_10 => "0", - .osc_11 => "1", - .osc_12 => "2", - else => unreachable, - }}); - return; - } - - if (spec_str.len == 0) { - log.warn("OSC 1{s} requires an argument", .{switch (self.state) { - .osc_10 => "0", - .osc_11 => "1", - .osc_12 => "2", - else => unreachable, - }}); - return; - } - - const alloc = self.alloc orelse return; - const operations = &self.command.color_operation.operations; - - if (std.mem.eql(u8, spec_str, "?")) { - const op = operations.addOne(alloc) catch |err| { - log.warn("unable to append color operation: {}", .{err}); - return; - }; - op.* = .{ - .report = switch (self.state) { - .osc_10 => .foreground, - .osc_11 => .background, - .osc_12 => .cursor, - else => unreachable, - }, - }; - } else { - const color = RGB.parse(spec_str) catch |err| { - log.warn("invalid color specification in OSC 1{s}: {s} {}", .{ - switch (self.state) { - .osc_10 => "0", - .osc_11 => "1", - .osc_12 => "2", - else => unreachable, - }, - spec_str, - err, - }); - return; - }; - const op = operations.addOne(alloc) catch |err| { - log.warn("unable to append color operation: {}", .{err}); - return; - }; - op.* = .{ - .set = .{ - .kind = switch (self.state) { - .osc_10 => .foreground, - .osc_11 => .background, - .osc_12 => .cursor, - else => unreachable, - }, - .color = color, - }, - }; - } - } - - fn parseOSC104(self: *Parser, final: bool) void { - assert(self.state == .osc_104); - assert(self.command == .color_operation); - assert(self.command.color_operation.source == .reset_palette); - - const alloc = self.alloc orelse return; - - const index_str = self.buf[self.buf_start .. self.buf_idx - (1 - @intFromBool(final))]; - self.buf_start = 0; - self.buf_idx = 0; - - const index = std.fmt.parseUnsigned(u8, index_str, 10) catch |err| switch (err) { - error.Overflow, error.InvalidCharacter => { - log.warn("invalid color palette index in OSC 104: {s} {}", .{ index_str, err }); - return; - }, - }; - const op = self.command.color_operation.operations.addOne(alloc) catch |err| { - log.warn("unable to append color operation: {}", .{err}); - return; - }; - op.* = .{ - .reset = .{ .palette = index }, - }; - } - /// End the sequence and return the command, if any. If the return value /// is null, then no valid command was found. The optional terminator_ch /// is the final character in the OSC sequence. This is used to determine @@ -1667,11 +1574,15 @@ pub const Parser = struct { // Other cleanup we may have to do depending on state. switch (self.state) { + .allocable_string => self.endAllocableString(), .semantic_exit_code => self.endSemanticExitCode(), .semantic_option_value => self.endSemanticOptionValue(), .hyperlink_uri => self.endHyperlink(), .string => self.endString(), .conemu_sleep_value => self.endConEmuSleepValue(), + .kitty_color_protocol_key => self.endKittyColorProtocolOption(.key_only, true), + .kitty_color_protocol_value => self.endKittyColorProtocolOption(.key_and_value, true), + .osc_color => self.endOscColor(), // We received OSC 9;X ST, but nothing else, finish off as a // desktop notification with "X" as the body. @@ -1692,12 +1603,6 @@ pub const Parser = struct { .conemu_progress_value, => {}, - .allocable_string => self.endAllocableString(), - .kitty_color_protocol_key => self.endKittyColorProtocolOption(.key_only, true), - .kitty_color_protocol_value => self.endKittyColorProtocolOption(.key_and_value, true), - .osc_4_color => self.parseOSC4(true), - .osc_10, .osc_11, .osc_12 => self.parseOSC101112(true), - .osc_104 => self.parseOSC104(true), else => {}, } @@ -1916,111 +1821,6 @@ test "OSC: end_of_input" { try testing.expect(cmd == .end_of_input); } -test "OSC: OSC110: reset foreground color" { - const testing = std.testing; - - var p: Parser = .initAlloc(testing.allocator); - defer p.deinit(); - - const input = "110"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?; - try testing.expect(cmd == .color_operation); - try testing.expectEqual(cmd.color_operation.terminator, .st); - try testing.expect(cmd.color_operation.source == .reset_foreground); - try testing.expect(cmd.color_operation.operations.count() == 1); - var it = cmd.color_operation.operations.constIterator(0); - { - const op = it.next().?; - try testing.expect(op.* == .reset); - try testing.expectEqual( - Command.ColorOperation.Kind.foreground, - op.reset, - ); - } - try testing.expect(it.next() == null); -} - -test "OSC: OSC111: reset background color" { - const testing = std.testing; - - var p: Parser = .initAlloc(testing.allocator); - defer p.deinit(); - - const input = "111"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?; - try testing.expect(cmd == .color_operation); - try testing.expectEqual(cmd.color_operation.terminator, .st); - try testing.expect(cmd.color_operation.source == .reset_background); - try testing.expect(cmd.color_operation.operations.count() == 1); - var it = cmd.color_operation.operations.constIterator(0); - { - const op = it.next().?; - try testing.expect(op.* == .reset); - try testing.expectEqual( - Command.ColorOperation.Kind.background, - op.reset, - ); - } - try testing.expect(it.next() == null); -} - -test "OSC: OSC112: reset cursor color" { - const testing = std.testing; - - var p: Parser = .initAlloc(testing.allocator); - defer p.deinit(); - - const input = "112"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?; - try testing.expect(cmd == .color_operation); - try testing.expectEqual(cmd.color_operation.terminator, .st); - try testing.expect(cmd.color_operation.source == .reset_cursor); - try testing.expect(cmd.color_operation.operations.count() == 1); - var it = cmd.color_operation.operations.constIterator(0); - { - const op = it.next().?; - try testing.expect(op.* == .reset); - try testing.expectEqual( - Command.ColorOperation.Kind.cursor, - op.reset, - ); - } - try testing.expect(it.next() == null); -} - -test "OSC: OSC112: reset cursor color with semicolon" { - const testing = std.testing; - - var p: Parser = .initAlloc(testing.allocator); - defer p.deinit(); - - const input = "112;"; - for (input) |ch| p.next(ch); - log.warn("finish: {s}", .{@tagName(p.state)}); - - const cmd = p.end(0x07).?; - try testing.expect(cmd == .color_operation); - try testing.expectEqual(cmd.color_operation.terminator, .bel); - try testing.expect(cmd.color_operation.source == .reset_cursor); - try testing.expect(cmd.color_operation.operations.count() == 1); - var it = cmd.color_operation.operations.constIterator(0); - { - const op = it.next().?; - try testing.expect(op.* == .reset); - try testing.expectEqual( - Command.ColorOperation.Kind.cursor, - op.reset, - ); - } - try testing.expect(it.next() == null); -} - test "OSC: get/set clipboard" { const testing = std.testing; diff --git a/src/terminal/osc/color.zig b/src/terminal/osc/color.zig index 4d6c9a0de..8a8e8b942 100644 --- a/src/terminal/osc/color.zig +++ b/src/terminal/osc/color.zig @@ -105,7 +105,7 @@ fn parseGetSetAnsiColor( ) catch return result; // Parse the color. - const target: Request.Target = switch (op) { + const target: Target = switch (op) { // OSC5 maps directly to the Special enum. .osc_5 => .{ .special = std.meta.intToEnum( SpecialColor, @@ -178,7 +178,7 @@ fn parseResetAnsiColor( ) catch continue; // Parse the color. - const target: Request.Target = switch (op) { + const target: Target = switch (op) { // OSC105 maps directly to the Special enum. .osc_105 => .{ .special = std.meta.intToEnum( SpecialColor, @@ -266,17 +266,17 @@ pub const Request = union(enum) { reset: Target, reset_palette, reset_special, +}; - pub const Target = union(enum) { - palette: u8, - special: SpecialColor, - dynamic: DynamicColor, - }; +pub const Target = union(enum) { + palette: u8, + special: SpecialColor, + dynamic: DynamicColor, +}; - pub const ColoredTarget = struct { - target: Target, - color: RGB, - }; +pub const ColoredTarget = struct { + target: Target, + color: RGB, }; test "osc4" { diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index 3009935ec..29d8c42d3 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -1565,7 +1565,11 @@ pub fn Stream(comptime Handler: type) type { .color_operation => |v| { if (@hasDecl(T, "handleColorOperation")) { - try self.handler.handleColorOperation(v.source, &v.operations, v.terminator); + try self.handler.handleColorOperation( + v.op, + &v.requests, + v.terminator, + ); return; } else log.warn("unimplemented OSC callback: {}", .{cmd}); }, diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 002ccdb39..2d1f3293c 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -1187,12 +1187,15 @@ pub const StreamHandler = struct { pub fn handleColorOperation( self: *StreamHandler, - source: terminal.osc.Command.ColorOperation.Source, - operations: *const terminal.osc.Command.ColorOperation.List, + op: terminal.osc.color.Operation, + requests: *const terminal.osc.color.List, terminator: terminal.osc.Terminator, ) !void { + // We'll need op one day if we ever implement reporting special colors. + _ = op; + // return early if there is nothing to do - if (operations.count() == 0) return; + if (requests.count() == 0) return; var buffer: [1024]u8 = undefined; var fba: std.heap.FixedBufferAllocator = .init(&buffer); @@ -1201,63 +1204,71 @@ pub const StreamHandler = struct { var response: std.ArrayListUnmanaged(u8) = .empty; const writer = response.writer(alloc); - var report: bool = false; - - try writer.print("\x1b]{}", .{source}); - - var it = operations.constIterator(0); - - while (it.next()) |op| { - switch (op.*) { + var it = requests.constIterator(0); + while (it.next()) |req| { + switch (req.*) { .set => |set| { - switch (set.kind) { + switch (set.target) { .palette => |i| { self.terminal.flags.dirty.palette = true; self.terminal.color_palette.colors[i] = set.color; self.terminal.color_palette.mask.set(i); }, - .foreground => { - self.foreground_color = set.color; - _ = self.renderer_mailbox.push(.{ - .foreground_color = set.color, - }, .{ .forever = {} }); - }, - .background => { - self.background_color = set.color; - _ = self.renderer_mailbox.push(.{ - .background_color = set.color, - }, .{ .forever = {} }); - }, - .cursor => { - self.cursor_color = set.color; - _ = self.renderer_mailbox.push(.{ - .cursor_color = set.color, - }, .{ .forever = {} }); + .dynamic => |dynamic| switch (dynamic) { + .foreground => { + self.foreground_color = set.color; + _ = self.renderer_mailbox.push(.{ + .foreground_color = set.color, + }, .{ .forever = {} }); + }, + .background => { + self.background_color = set.color; + _ = self.renderer_mailbox.push(.{ + .background_color = set.color, + }, .{ .forever = {} }); + }, + .cursor => { + self.cursor_color = set.color; + _ = self.renderer_mailbox.push(.{ + .cursor_color = set.color, + }, .{ .forever = {} }); + }, + .pointer_foreground, + .pointer_background, + .tektronix_foreground, + .tektronix_background, + .highlight_background, + .tektronix_cursor, + .highlight_foreground, + => log.info("setting dynamic color {s} not implemented", .{ + @tagName(dynamic), + }), }, + .special => log.info("setting special colors not implemented", .{}), } // Notify the surface of the color change self.surfaceMessageWriter(.{ .color_change = .{ - .kind = set.kind, + .target = set.target, .color = set.color, } }); }, - .reset => |kind| { - switch (kind) { - .palette => |i| { - const mask = &self.terminal.color_palette.mask; - self.terminal.flags.dirty.palette = true; - self.terminal.color_palette.colors[i] = self.terminal.default_palette[i]; - mask.unset(i); + .reset => |target| switch (target) { + .palette => |i| { + const mask = &self.terminal.color_palette.mask; + self.terminal.flags.dirty.palette = true; + self.terminal.color_palette.colors[i] = self.terminal.default_palette[i]; + mask.unset(i); - self.surfaceMessageWriter(.{ - .color_change = .{ - .kind = .{ .palette = @intCast(i) }, - .color = self.terminal.color_palette.colors[i], - }, - }); - }, + self.surfaceMessageWriter(.{ + .color_change = .{ + .target = target, + .color = self.terminal.color_palette.colors[i], + }, + }); + }, + .dynamic => |dynamic| switch (dynamic) { .foreground => { self.foreground_color = null; _ = self.renderer_mailbox.push(.{ @@ -1265,7 +1276,7 @@ pub const StreamHandler = struct { }, .{ .forever = {} }); self.surfaceMessageWriter(.{ .color_change = .{ - .kind = .foreground, + .target = target, .color = self.default_foreground_color, } }); }, @@ -1276,7 +1287,7 @@ pub const StreamHandler = struct { }, .{ .forever = {} }); self.surfaceMessageWriter(.{ .color_change = .{ - .kind = .background, + .target = target, .color = self.default_background_color, } }); }, @@ -1289,33 +1300,83 @@ pub const StreamHandler = struct { if (self.default_cursor_color) |color| { self.surfaceMessageWriter(.{ .color_change = .{ - .kind = .cursor, + .target = target, .color = color, } }); } }, - } + .pointer_foreground, + .pointer_background, + .tektronix_foreground, + .tektronix_background, + .highlight_background, + .tektronix_cursor, + .highlight_foreground, + => log.warn("resetting dynamic color {s} not implemented", .{ + @tagName(dynamic), + }), + }, + .special => log.info("resetting special colors not implemented", .{}), }, - .report => |kind| report: { - if (self.osc_color_report_format == .none) break :report; + .reset_palette => { + const mask = &self.terminal.color_palette.mask; + var mask_iterator = mask.iterator(.{}); + while (mask_iterator.next()) |i| { + self.terminal.flags.dirty.palette = true; + self.terminal.color_palette.colors[i] = self.terminal.default_palette[i]; + self.surfaceMessageWriter(.{ + .color_change = .{ + .target = .{ .palette = @intCast(i) }, + .color = self.terminal.color_palette.colors[i], + }, + }); + } + mask.* = .initEmpty(); + }, - report = true; + .reset_special => log.warn( + "resetting all special colors not implemented", + .{}, + ), + + .query => |kind| report: { + if (self.osc_color_report_format == .none) break :report; const color = switch (kind) { .palette => |i| self.terminal.color_palette.colors[i], - .foreground => self.foreground_color orelse self.default_foreground_color, - .background => self.background_color orelse self.default_background_color, - .cursor => self.cursor_color orelse - self.default_cursor_color orelse - self.foreground_color orelse - self.default_foreground_color, + .dynamic => |dynamic| switch (dynamic) { + .foreground => self.foreground_color orelse self.default_foreground_color, + .background => self.background_color orelse self.default_background_color, + .cursor => self.cursor_color orelse + self.default_cursor_color orelse + self.foreground_color orelse + self.default_foreground_color, + .pointer_foreground, + .pointer_background, + .tektronix_foreground, + .tektronix_background, + .highlight_background, + .tektronix_cursor, + .highlight_foreground, + => { + log.info( + "reporting dynamic color {s} not implemented", + .{@tagName(dynamic)}, + ); + break :report; + }, + }, + .special => { + log.info("reporting special colors not implemented", .{}); + break :report; + }, }; switch (self.osc_color_report_format) { .@"16-bit" => switch (kind) { .palette => |i| try writer.print( - ";{d};rgb:{x:0>4}/{x:0>4}/{x:0>4}", + "\x1b]4;{d};rgb:{x:0>4}/{x:0>4}/{x:0>4}", .{ i, @as(u16, color.r) * 257, @@ -1323,19 +1384,21 @@ pub const StreamHandler = struct { @as(u16, color.b) * 257, }, ), - else => try writer.print( - ";rgb:{x:0>4}/{x:0>4}/{x:0>4}", + .dynamic => |dynamic| try writer.print( + "\x1b]{d};rgb:{x:0>4}/{x:0>4}/{x:0>4}", .{ + @intFromEnum(dynamic), @as(u16, color.r) * 257, @as(u16, color.g) * 257, @as(u16, color.b) * 257, }, ), + .special => unreachable, }, .@"8-bit" => switch (kind) { .palette => |i| try writer.print( - ";{d};rgb:{x:0>2}/{x:0>2}/{x:0>2}", + "\x1b]4;{d};rgb:{x:0>2}/{x:0>2}/{x:0>2}", .{ i, @as(u16, color.r), @@ -1343,22 +1406,27 @@ pub const StreamHandler = struct { @as(u16, color.b), }, ), - else => try writer.print( - ";rgb:{x:0>2}/{x:0>2}/{x:0>2}", + .dynamic => |dynamic| try writer.print( + "\x1b]{d};rgb:{x:0>2}/{x:0>2}/{x:0>2}", .{ + @intFromEnum(dynamic), @as(u16, color.r), @as(u16, color.g), @as(u16, color.b), }, ), + .special => unreachable, }, .none => unreachable, } + + try writer.writeAll(terminator.string()); }, } } - if (report) { + + if (response.items.len > 0) { // If any of the operations were reports, finalize the report // string and send it to the terminal. try writer.writeAll(terminator.string());