From 9c1abf487e0f05cebc4f3e932e1a5a8cdb5aa6a6 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Fri, 23 May 2025 17:15:40 -0500 Subject: [PATCH 01/17] OSC: start adding structure to allow multiple color operations per OSC --- src/terminal/osc.zig | 418 ++++++++++++++++++++++++++++------ src/terminal/stream.zig | 7 + src/termio/stream_handler.zig | 175 ++++++++++++++ 3 files changed, 526 insertions(+), 74 deletions(-) diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 932964137..7729eaa6b 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -109,6 +109,13 @@ pub const Command = union(enum) { value: []const u8, }, + /// OSC color operations + color_operation: struct { + source: ColorOperationSource, + operations: std.ArrayListUnmanaged(ColorOperation) = .empty, + terminator: Terminator = .st, + }, + /// OSC 4, OSC 10, and OSC 11 color report. report_color: struct { /// OSC 4 requests a palette color, OSC 10 requests the foreground @@ -182,6 +189,32 @@ pub const Command = union(enum) { /// Wait input (OSC 9;5) wait_input: void, + pub const ColorOperationSource = enum(u16) { + osc_4 = 4, + osc_10 = 10, + osc_11 = 11, + osc_12 = 12, + osc_104 = 104, + + pub fn format( + self: ColorOperationSource, + comptime _: []const u8, + _: std.fmt.FormatOptions, + writer: anytype, + ) !void { + try writer.print("{d}", .{@intFromEnum(self)}); + } + }; + + pub const ColorOperation = union(enum) { + set: struct { + kind: ColorKind, + color: RGB, + }, + reset: ColorKind, + report: ColorKind, + }; + pub const ColorKind = union(enum) { palette: u8, foreground, @@ -234,6 +267,15 @@ pub const Terminator = enum { .bel => "\x07", }; } + + pub fn format( + self: Terminator, + comptime _: []const u8, + _: std.fmt.FormatOptions, + writer: anytype, + ) !void { + try writer.writeAll(self.string()); + } }; pub const Parser = struct { @@ -288,6 +330,7 @@ pub const Parser = struct { @"0", @"1", @"10", + @"104", @"11", @"12", @"13", @@ -327,17 +370,16 @@ pub const Parser = struct { clipboard_kind_end, // Get/set color palette index - color_palette_index, - color_palette_index_end, + osc_4, + + // Reset color palette index + osc_104, // Hyperlinks hyperlink_param_key, hyperlink_param_value, hyperlink_uri, - // Reset color palette index - reset_color_palette_index, - // rxvt extension. Only used for OSC 777 and only the value "notify" is // supported rxvt_extension, @@ -423,6 +465,10 @@ pub const Parser = struct { v.list.deinit(); self.command = default; }, + .color_operation => |*v| { + v.operations.deinit(self.alloc.?); + self.command = default; + }, else => {}, } } @@ -503,18 +549,26 @@ pub const Parser = struct { .@"10" => switch (c) { ';' => self.state = .query_fg_color, - '4' => { - self.command = .{ .reset_color = .{ - .kind = .{ .palette = 0 }, - .value = "", - } }; + '4' => self.state = .@"104", + else => self.state = .invalid, + }, - self.state = .reset_color_palette_index; + .@"104" => switch (c) { + ';' => osc_104: { + if (self.alloc == null) { + log.info("OSC 104 requires an allocator, but none was provided", .{}); + self.state = .invalid; + break :osc_104; + } + self.state = .osc_104; + self.buf_start = self.buf_idx; self.complete = true; }, else => self.state = .invalid, }, + .osc_104 => {}, + .@"11" => switch (c) { ';' => self.state = .query_bg_color, '0' => { @@ -621,65 +675,20 @@ pub const Parser = struct { }, .@"4" => switch (c) { - ';' => { - self.state = .color_palette_index; - self.buf_start = self.buf_idx; - }, - else => self.state = .invalid, - }, - - .color_palette_index => switch (c) { - '0'...'9' => {}, - ';' => blk: { - const str = self.buf[self.buf_start .. self.buf_idx - 1]; - if (str.len == 0) { + ';' => osc_4: { + if (self.alloc == null) { + log.info("OSC 4 requires an allocator, but none was provided", .{}); self.state = .invalid; - break :blk; + break :osc_4; } - - if (std.fmt.parseUnsigned(u8, str, 10)) |num| { - self.state = .color_palette_index_end; - self.temp_state = .{ .num = num }; - } else |err| switch (err) { - error.Overflow => self.state = .invalid, - error.InvalidCharacter => unreachable, - } - }, - else => self.state = .invalid, - }, - - .color_palette_index_end => switch (c) { - '?' => { - self.command = .{ .report_color = .{ - .kind = .{ .palette = @intCast(self.temp_state.num) }, - } }; - + self.state = .osc_4; + self.buf_start = self.buf_idx; self.complete = true; }, - else => { - self.command = .{ .set_color = .{ - .kind = .{ .palette = @intCast(self.temp_state.num) }, - .value = "", - } }; - - self.state = .string; - self.temp_state = .{ .str = &self.command.set_color.value }; - self.buf_start = self.buf_idx - 1; - }, + else => self.state = .invalid, }, - .reset_color_palette_index => switch (c) { - ';' => { - self.state = .string; - self.temp_state = .{ .str = &self.command.reset_color.value }; - self.buf_start = self.buf_idx; - self.complete = false; - }, - else => { - self.state = .invalid; - self.complete = false; - }, - }, + .osc_4 => {}, .@"5" => switch (c) { '2' => self.state = .@"52", @@ -1327,6 +1336,104 @@ pub const Parser = struct { self.temp_state.str.* = list.items; } + fn parseOSC4(self: *Parser) void { + assert(self.state == .osc_4); + + const alloc = self.alloc orelse return; + + self.command = .{ + .color_operation = .{ + .source = .osc_4, + .operations = std.ArrayListUnmanaged(Command.ColorOperation).initCapacity(alloc, 8) catch |err| { + log.warn("unable to allocate memory for OSC 4 parsing: {}", .{err}); + self.state = .invalid; + return; + }, + }, + }; + + const str = self.buf[self.buf_start..self.buf_idx]; + var it = std.mem.splitScalar(u8, str, ';'); + while (it.next()) |index_str| { + const index = std.fmt.parseUnsigned(u8, index_str, 10) catch |err| switch (err) { + error.Overflow, error.InvalidCharacter => { + log.warn("invalid palette index spec in OSC 4: {s}", .{index_str}); + // skip any spec + _ = it.next(); + continue; + }, + }; + const spec_str = it.next() orelse continue; + if (std.mem.eql(u8, spec_str, "?")) { + self.command.color_operation.operations.append( + alloc, + .{ + .report = .{ .palette = index }, + }, + ) catch |err| { + log.warn("unable to append color operation: {}", .{err}); + return; + }; + } else { + const color = RGB.parse(spec_str) catch |err| { + log.warn("invalid color specification {s} in OSC 4: {}", .{ spec_str, err }); + continue; + }; + self.command.color_operation.operations.append( + alloc, + .{ + .set = .{ + .kind = .{ + .palette = index, + }, + .color = color, + }, + }, + ) catch |err| { + log.warn("unable to append color operation: {}", .{err}); + return; + }; + } + } + } + + fn parseOSC104(self: *Parser) void { + assert(self.state == .osc_104); + + const alloc = self.alloc orelse return; + + self.command = .{ + .color_operation = .{ + .source = .osc_104, + .operations = std.ArrayListUnmanaged(Command.ColorOperation).initCapacity(alloc, 8) catch |err| { + log.warn("unable to allocate memory for OSC 104 parsing: {}", .{err}); + self.state = .invalid; + return; + }, + }, + }; + + const str = self.buf[self.buf_start..self.buf_idx]; + var it = std.mem.splitScalar(u8, str, ';'); + while (it.next()) |index_str| { + const index = std.fmt.parseUnsigned(u8, index_str, 10) catch |err| switch (err) { + error.Overflow, error.InvalidCharacter => { + log.warn("invalid palette index spec in OSC 104: {s}", .{index_str}); + continue; + }, + }; + self.command.color_operation.operations.append( + alloc, + .{ + .reset = .{ .palette = index }, + }, + ) catch |err| { + log.warn("unable to append color operation: {}", .{err}); + return; + }; + } + } + /// 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 @@ -1350,12 +1457,15 @@ pub const Parser = struct { .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 => self.parseOSC4(), + .osc_104 => self.parseOSC104(), else => {}, } switch (self.command) { .report_color => |*c| c.terminator = .init(terminator_ch), .kitty_color_protocol => |*c| c.terminator = .init(terminator_ch), + .color_operation => |*c| c.terminator = .init(terminator_ch), else => {}, } @@ -1729,32 +1839,192 @@ test "OSC: set background color" { try testing.expectEqualStrings(cmd.set_color.value, "rgb:f/ff/ffff"); } -test "OSC: get palette color" { +test "OSC: OSC4: get palette color 1" { const testing = std.testing; - var p: Parser = .{}; + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + var p: Parser = .{ .alloc = arena.allocator() }; const input = "4;1;?"; for (input) |ch| p.next(ch); const cmd = p.end('\x1b').?; - try testing.expect(cmd == .report_color); - try testing.expectEqual(Command.ColorKind{ .palette = 1 }, cmd.report_color.kind); - try testing.expectEqual(cmd.report_color.terminator, .st); + try testing.expect(cmd == .color_operation); + try testing.expect(cmd.color_operation.source == .osc_4); + try testing.expect(cmd.color_operation.operations.items.len == 1); + const op = cmd.color_operation.operations.items[0]; + try testing.expect(op == .report); + try testing.expectEqual(Command.ColorKind{ .palette = 1 }, op.report); + try testing.expectEqual(cmd.color_operation.terminator, .st); } -test "OSC: set palette color" { +test "OSC: OSC4: get palette color 2" { const testing = std.testing; - var p: Parser = .{}; + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + var p: Parser = .{ .alloc = arena.allocator() }; + + const input = "4;1;?;2;?"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?; + try testing.expect(cmd == .color_operation); + try testing.expect(cmd.color_operation.source == .osc_4); + try testing.expect(cmd.color_operation.operations.items.len == 2); + { + const op = cmd.color_operation.operations.items[0]; + try testing.expect(op == .report); + try testing.expectEqual(Command.ColorKind{ .palette = 1 }, op.report); + } + { + const op = cmd.color_operation.operations.items[1]; + try testing.expect(op == .report); + try testing.expectEqual(Command.ColorKind{ .palette = 2 }, op.report); + } + try testing.expectEqual(cmd.color_operation.terminator, .st); +} + +test "OSC: OSC4: set palette color 1" { + const testing = std.testing; + + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + var p: Parser = .{ .alloc = arena.allocator() }; const input = "4;17;rgb:aa/bb/cc"; for (input) |ch| p.next(ch); const cmd = p.end('\x1b').?; - try testing.expect(cmd == .set_color); - try testing.expectEqual(Command.ColorKind{ .palette = 17 }, cmd.set_color.kind); - try testing.expectEqualStrings(cmd.set_color.value, "rgb:aa/bb/cc"); + try testing.expect(cmd == .color_operation); + try testing.expect(cmd.color_operation.source == .osc_4); + try testing.expect(cmd.color_operation.operations.items.len == 1); + const op = cmd.color_operation.operations.items[0]; + try testing.expect(op == .set); + try testing.expectEqual(Command.ColorKind{ .palette = 17 }, op.set.kind); + try testing.expectEqual( + RGB{ .r = 0xaa, .g = 0xbb, .b = 0xcc }, + op.set.color, + ); +} + +test "OSC: OSC4: set palette color 2" { + const testing = std.testing; + + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + var p: Parser = .{ .alloc = arena.allocator() }; + + const input = "4;17;rgb:aa/bb/cc;1;rgb:00/11/22"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?; + try testing.expect(cmd == .color_operation); + try testing.expect(cmd.color_operation.source == .osc_4); + try testing.expect(cmd.color_operation.operations.items.len == 2); + { + const op = cmd.color_operation.operations.items[0]; + try testing.expect(op == .set); + try testing.expectEqual(Command.ColorKind{ .palette = 17 }, op.set.kind); + try testing.expectEqual( + RGB{ .r = 0xaa, .g = 0xbb, .b = 0xcc }, + op.set.color, + ); + } + { + const op = cmd.color_operation.operations.items[1]; + try testing.expect(op == .set); + try testing.expectEqual(Command.ColorKind{ .palette = 1 }, op.set.kind); + try testing.expectEqual( + RGB{ .r = 0x00, .g = 0x11, .b = 0x22 }, + op.set.color, + ); + } +} + +test "OSC: OSC4: mix get/set palette color" { + const testing = std.testing; + + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + var p: Parser = .{ .alloc = arena.allocator() }; + + const input = "4;17;rgb:aa/bb/cc;254;?"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?; + try testing.expect(cmd == .color_operation); + try testing.expect(cmd.color_operation.source == .osc_4); + try testing.expect(cmd.color_operation.operations.items.len == 2); + { + const op = cmd.color_operation.operations.items[0]; + try testing.expect(op == .set); + try testing.expectEqual(Command.ColorKind{ .palette = 17 }, op.set.kind); + try testing.expectEqual( + RGB{ .r = 0xaa, .g = 0xbb, .b = 0xcc }, + op.set.color, + ); + } + { + const op = cmd.color_operation.operations.items[1]; + try testing.expect(op == .report); + try testing.expectEqual(Command.ColorKind{ .palette = 254 }, op.report); + } +} + +test "OSC: OSC104: reset palette color 1" { + const testing = std.testing; + + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + var p: Parser = .{ .alloc = arena.allocator() }; + + const input = "104;17"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?; + try testing.expect(cmd == .color_operation); + try testing.expect(cmd.color_operation.source == .osc_104); + try testing.expect(cmd.color_operation.operations.items.len == 1); + { + const op = cmd.color_operation.operations.items[0]; + try testing.expect(op == .reset); + try testing.expectEqual(Command.ColorKind{ .palette = 17 }, op.reset); + } +} + +test "OSC: OSC104: reset palette color 2" { + const testing = std.testing; + + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + var p: Parser = .{ .alloc = arena.allocator() }; + + const input = "104;17;111"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?; + try testing.expect(cmd == .color_operation); + try testing.expect(cmd.color_operation.source == .osc_104); + try testing.expect(cmd.color_operation.operations.items.len == 2); + { + const op = cmd.color_operation.operations.items[0]; + try testing.expect(op == .reset); + try testing.expectEqual(Command.ColorKind{ .palette = 17 }, op.reset); + } + { + const op = cmd.color_operation.operations.items[1]; + try testing.expect(op == .reset); + try testing.expectEqual(Command.ColorKind{ .palette = 111 }, op.reset); + } } test "OSC: conemu sleep" { diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index 76fa6c129..2a1ae80c9 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -1555,6 +1555,13 @@ pub fn Stream(comptime Handler: type) type { } else log.warn("unimplemented OSC callback: {}", .{cmd}); }, + .color_operation => |v| { + if (@hasDecl(T, "handleColorOperation")) { + try self.handler.handleColorOperation(v.source, v.operations.items, v.terminator); + return; + } else log.warn("unimplemented OSC callback: {}", .{cmd}); + }, + .report_color => |v| { if (@hasDecl(T, "reportColor")) { try self.handler.reportColor(v.kind, v.terminator); diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index ffd00e14d..57a1eeacf 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -1195,6 +1195,181 @@ pub const StreamHandler = struct { } } + pub fn handleColorOperation( + self: *StreamHandler, + source: terminal.osc.Command.ColorOperationSource, + operations: []terminal.osc.Command.ColorOperation, + terminator: terminal.osc.Terminator, + ) !void { + var buffer: [1024]u8 = undefined; + var fba: std.heap.FixedBufferAllocator = .init(&buffer); + const alloc = fba.allocator(); + + var response: std.ArrayListUnmanaged(u8) = .empty; + const writer = response.writer(alloc); + + var report: bool = false; + + try writer.print("\x1b]{}", .{source}); + + for (operations) |op| { + switch (op) { + .set => |set| { + switch (set.kind) { + .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 = {} }); + }, + } + + // Notify the surface of the color change + self.surfaceMessageWriter(.{ .color_change = .{ + .kind = set.kind, + .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); + + self.surfaceMessageWriter(.{ + .color_change = .{ + .kind = .{ .palette = @intCast(i) }, + .color = self.terminal.color_palette.colors[i], + }, + }); + }, + .foreground => { + self.foreground_color = null; + _ = self.renderer_mailbox.push(.{ + .foreground_color = self.foreground_color, + }, .{ .forever = {} }); + + self.surfaceMessageWriter(.{ .color_change = .{ + .kind = .foreground, + .color = self.default_foreground_color, + } }); + }, + .background => { + self.background_color = null; + _ = self.renderer_mailbox.push(.{ + .background_color = self.background_color, + }, .{ .forever = {} }); + + self.surfaceMessageWriter(.{ .color_change = .{ + .kind = .background, + .color = self.default_background_color, + } }); + }, + .cursor => { + self.cursor_color = null; + + _ = self.renderer_mailbox.push(.{ + .cursor_color = self.cursor_color, + }, .{ .forever = {} }); + + if (self.default_cursor_color) |color| { + self.surfaceMessageWriter(.{ .color_change = .{ + .kind = .cursor, + .color = color, + } }); + } + }, + } + }, + + .report => |kind| report: { + if (self.osc_color_report_format == .none) break :report; + + report = true; + + 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, + }; + + 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}", + .{ + i, + @as(u16, color.r) * 257, + @as(u16, color.g) * 257, + @as(u16, color.b) * 257, + }, + ), + else => try writer.print( + ";rgb:{x:0>4}/{x:0>4}/{x:0>4}", + .{ + @as(u16, color.r) * 257, + @as(u16, color.g) * 257, + @as(u16, color.b) * 257, + }, + ), + }, + + .@"8-bit" => switch (kind) { + .palette => |i| try writer.print( + ";{d};rgb:{x:0>2}/{x:0>2}/{x:0>2}", + .{ + i, + @as(u16, color.r), + @as(u16, color.g), + @as(u16, color.b), + }, + ), + else => 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), + }, + ), + }, + + .none => unreachable, + } + }, + } + } + if (report) { + try writer.writeAll(terminator.string()); + const msg: termio.Message = .{ .write_stable = response.items }; + self.messageWriter(msg); + } + } + /// Implements OSC 4, OSC 10, and OSC 11, which reports palette color, /// default foreground color, and background color respectively. pub fn reportColor( From 5ec1c15ecfe5e2a29f4e0c9cbe116aeed7ad62d5 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Fri, 23 May 2025 18:05:59 -0500 Subject: [PATCH 02/17] OSC: add more tests --- src/terminal/osc.zig | 170 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 153 insertions(+), 17 deletions(-) diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 7729eaa6b..778196160 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -1357,8 +1357,8 @@ pub const Parser = struct { while (it.next()) |index_str| { const index = std.fmt.parseUnsigned(u8, index_str, 10) catch |err| switch (err) { error.Overflow, error.InvalidCharacter => { - log.warn("invalid palette index spec in OSC 4: {s}", .{index_str}); - // skip any spec + log.warn("invalid color palette index in OSC 4: {s} {}", .{ index_str, err }); + // skip any color spec _ = it.next(); continue; }, @@ -1376,7 +1376,7 @@ pub const Parser = struct { }; } else { const color = RGB.parse(spec_str) catch |err| { - log.warn("invalid color specification {s} in OSC 4: {}", .{ spec_str, err }); + log.warn("invalid color specification in OSC 4: {s} {}", .{ spec_str, err }); continue; }; self.command.color_operation.operations.append( @@ -1418,7 +1418,7 @@ pub const Parser = struct { while (it.next()) |index_str| { const index = std.fmt.parseUnsigned(u8, index_str, 10) catch |err| switch (err) { error.Overflow, error.InvalidCharacter => { - log.warn("invalid palette index spec in OSC 104: {s}", .{index_str}); + log.warn("invalid color palette index in OSC 104: {s} {}", .{ index_str, err }); continue; }, }; @@ -1854,10 +1854,15 @@ test "OSC: OSC4: get palette color 1" { try testing.expect(cmd == .color_operation); try testing.expect(cmd.color_operation.source == .osc_4); try testing.expect(cmd.color_operation.operations.items.len == 1); - const op = cmd.color_operation.operations.items[0]; - try testing.expect(op == .report); - try testing.expectEqual(Command.ColorKind{ .palette = 1 }, op.report); - try testing.expectEqual(cmd.color_operation.terminator, .st); + { + const op = cmd.color_operation.operations.items[0]; + try testing.expect(op == .report); + try testing.expectEqual( + Command.ColorKind{ .palette = 1 }, + op.report, + ); + try testing.expectEqual(cmd.color_operation.terminator, .st); + } } test "OSC: OSC4: get palette color 2" { @@ -1878,12 +1883,18 @@ test "OSC: OSC4: get palette color 2" { { const op = cmd.color_operation.operations.items[0]; try testing.expect(op == .report); - try testing.expectEqual(Command.ColorKind{ .palette = 1 }, op.report); + try testing.expectEqual( + Command.ColorKind{ .palette = 1 }, + op.report, + ); } { const op = cmd.color_operation.operations.items[1]; try testing.expect(op == .report); - try testing.expectEqual(Command.ColorKind{ .palette = 2 }, op.report); + try testing.expectEqual( + Command.ColorKind{ .palette = 2 }, + op.report, + ); } try testing.expectEqual(cmd.color_operation.terminator, .st); } @@ -1905,7 +1916,10 @@ test "OSC: OSC4: set palette color 1" { try testing.expect(cmd.color_operation.operations.items.len == 1); const op = cmd.color_operation.operations.items[0]; try testing.expect(op == .set); - try testing.expectEqual(Command.ColorKind{ .palette = 17 }, op.set.kind); + try testing.expectEqual( + Command.ColorKind{ .palette = 17 }, + op.set.kind, + ); try testing.expectEqual( RGB{ .r = 0xaa, .g = 0xbb, .b = 0xcc }, op.set.color, @@ -1930,7 +1944,10 @@ test "OSC: OSC4: set palette color 2" { { const op = cmd.color_operation.operations.items[0]; try testing.expect(op == .set); - try testing.expectEqual(Command.ColorKind{ .palette = 17 }, op.set.kind); + try testing.expectEqual( + Command.ColorKind{ .palette = 17 }, + op.set.kind, + ); try testing.expectEqual( RGB{ .r = 0xaa, .g = 0xbb, .b = 0xcc }, op.set.color, @@ -1939,7 +1956,10 @@ test "OSC: OSC4: set palette color 2" { { const op = cmd.color_operation.operations.items[1]; try testing.expect(op == .set); - try testing.expectEqual(Command.ColorKind{ .palette = 1 }, op.set.kind); + try testing.expectEqual( + Command.ColorKind{ .palette = 1 }, + op.set.kind, + ); try testing.expectEqual( RGB{ .r = 0x00, .g = 0x11, .b = 0x22 }, op.set.color, @@ -1947,6 +1967,60 @@ test "OSC: OSC4: set palette color 2" { } } +test "OSC: OSC4: get with invalid index" { + const testing = std.testing; + + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + var p: Parser = .{ .alloc = arena.allocator() }; + + const input = "4;1111;?;1;?"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?; + try testing.expect(cmd == .color_operation); + try testing.expect(cmd.color_operation.source == .osc_4); + try testing.expect(cmd.color_operation.operations.items.len == 1); + { + const op = cmd.color_operation.operations.items[0]; + try testing.expect(op == .report); + try testing.expectEqual( + Command.ColorKind{ .palette = 1 }, + op.report, + ); + } +} + +test "OSC: OSC4: set with invalid index" { + const testing = std.testing; + + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + var p: Parser = .{ .alloc = arena.allocator() }; + + const input = "4;256;#ffffff;1;#aabbcc"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?; + try testing.expect(cmd == .color_operation); + try testing.expect(cmd.color_operation.source == .osc_4); + try testing.expect(cmd.color_operation.operations.items.len == 1); + { + const op = cmd.color_operation.operations.items[0]; + try testing.expect(op == .set); + try testing.expectEqual( + Command.ColorKind{ .palette = 1 }, + op.set.kind, + ); + try testing.expectEqual( + RGB{ .r = 0xaa, .g = 0xbb, .b = 0xcc }, + op.set.color, + ); + } +} + test "OSC: OSC4: mix get/set palette color" { const testing = std.testing; @@ -1965,7 +2039,10 @@ test "OSC: OSC4: mix get/set palette color" { { const op = cmd.color_operation.operations.items[0]; try testing.expect(op == .set); - try testing.expectEqual(Command.ColorKind{ .palette = 17 }, op.set.kind); + try testing.expectEqual( + Command.ColorKind{ .palette = 17 }, + op.set.kind, + ); try testing.expectEqual( RGB{ .r = 0xaa, .g = 0xbb, .b = 0xcc }, op.set.color, @@ -1996,7 +2073,10 @@ test "OSC: OSC104: reset palette color 1" { { const op = cmd.color_operation.operations.items[0]; try testing.expect(op == .reset); - try testing.expectEqual(Command.ColorKind{ .palette = 17 }, op.reset); + try testing.expectEqual( + Command.ColorKind{ .palette = 17 }, + op.reset, + ); } } @@ -2018,12 +2098,68 @@ test "OSC: OSC104: reset palette color 2" { { const op = cmd.color_operation.operations.items[0]; try testing.expect(op == .reset); - try testing.expectEqual(Command.ColorKind{ .palette = 17 }, op.reset); + try testing.expectEqual( + Command.ColorKind{ .palette = 17 }, + op.reset, + ); } { const op = cmd.color_operation.operations.items[1]; try testing.expect(op == .reset); - try testing.expectEqual(Command.ColorKind{ .palette = 111 }, op.reset); + try testing.expectEqual( + Command.ColorKind{ .palette = 111 }, + op.reset, + ); + } +} + +test "OSC: OSC104: invalid palette index" { + const testing = std.testing; + + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + var p: Parser = .{ .alloc = arena.allocator() }; + + const input = "104;ffff;111"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?; + try testing.expect(cmd == .color_operation); + try testing.expect(cmd.color_operation.source == .osc_104); + try testing.expect(cmd.color_operation.operations.items.len == 1); + { + const op = cmd.color_operation.operations.items[0]; + try testing.expect(op == .reset); + try testing.expectEqual( + Command.ColorKind{ .palette = 111 }, + op.reset, + ); + } +} + +test "OSC: OSC104: empty palette index" { + const testing = std.testing; + + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + var p: Parser = .{ .alloc = arena.allocator() }; + + const input = "104;;111"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?; + try testing.expect(cmd == .color_operation); + try testing.expect(cmd.color_operation.source == .osc_104); + try testing.expect(cmd.color_operation.operations.items.len == 1); + { + const op = cmd.color_operation.operations.items[0]; + try testing.expect(op == .reset); + try testing.expectEqual( + Command.ColorKind{ .palette = 111 }, + op.reset, + ); } } From 5bb74929554e298651298d32f73ef29de14e4294 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Fri, 23 May 2025 20:44:33 -0500 Subject: [PATCH 03/17] OSC: convert OSC 110, 111, and 112 and add more tests --- src/terminal/Parser.zig | 21 +- src/terminal/osc.zig | 554 +++++++++++++++++++++++++--------- src/terminal/stream.zig | 21 -- src/termio/stream_handler.zig | 197 ------------ 4 files changed, 437 insertions(+), 356 deletions(-) diff --git a/src/terminal/Parser.zig b/src/terminal/Parser.zig index 14ed6d6df..80772d71f 100644 --- a/src/terminal/Parser.zig +++ b/src/terminal/Parser.zig @@ -877,7 +877,12 @@ test "osc: change window title (end in esc)" { // https://github.com/darrenstarr/VtNetCore/pull/14 // Saw this on HN, decided to add a test case because why not. test "osc: 112 incomplete sequence" { - var p = init(); + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + var p: Parser = init(); + p.osc_parser.alloc = arena.allocator(); + _ = p.next(0x1B); _ = p.next(']'); _ = p.next('1'); @@ -892,8 +897,18 @@ test "osc: 112 incomplete sequence" { try testing.expect(a[2] == null); const cmd = a[0].?.osc_dispatch; - try testing.expect(cmd == .reset_color); - try testing.expectEqual(cmd.reset_color.kind, .cursor); + try testing.expect(cmd == .color_operation); + try testing.expectEqual(cmd.color_operation.terminator, .bel); + try testing.expect(cmd.color_operation.source == .osc_112); + try testing.expect(cmd.color_operation.operations.items.len == 1); + { + const op = cmd.color_operation.operations.items[0]; + try testing.expect(op == .reset); + try testing.expectEqual( + osc.Command.ColorKind.cursor, + op.reset, + ); + } } } diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 778196160..7b9239e43 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -116,37 +116,6 @@ pub const Command = union(enum) { terminator: Terminator = .st, }, - /// OSC 4, OSC 10, and OSC 11 color report. - report_color: struct { - /// OSC 4 requests a palette color, OSC 10 requests the foreground - /// color, OSC 11 the background color. - kind: ColorKind, - - /// We must reply with the same string terminator (ST) as used in the - /// request. - terminator: Terminator = .st, - }, - - /// Modify the foreground (OSC 10) or background color (OSC 11), or a palette color (OSC 4) - set_color: struct { - /// OSC 4 sets a palette color, OSC 10 sets the foreground color, OSC 11 - /// the background color. - kind: ColorKind, - - /// The color spec as a string - value: []const u8, - }, - - /// Reset a palette color (OSC 104) or the foreground (OSC 110), background - /// (OSC 111), or cursor (OSC 112) color. - reset_color: struct { - kind: ColorKind, - - /// OSC 104 can have parameters indicating which palette colors to - /// reset. - value: []const u8, - }, - /// Kitty color protocol, OSC 21 /// https://sw.kovidgoyal.net/kitty/color-stack/#id1 kitty_color_protocol: kitty.color.OSC, @@ -195,6 +164,9 @@ pub const Command = union(enum) { osc_11 = 11, osc_12 = 12, osc_104 = 104, + osc_110 = 110, + osc_111 = 111, + osc_112 = 112, pub fn format( self: ColorOperationSource, @@ -347,15 +319,6 @@ pub const Parser = struct { @"8", @"9", - // OSC 10 is used to query or set the current foreground color. - query_fg_color, - - // OSC 11 is used to query or set the current background color. - query_bg_color, - - // OSC 12 is used to query or set the current cursor color. - query_cursor_color, - // We're in a semantic prompt OSC command but we aren't sure // what the command is yet, i.e. `133;` semantic_prompt, @@ -372,9 +335,27 @@ pub const Parser = struct { // Get/set color palette index osc_4, + // Get/set foreground color + osc_10, + + // Get/set background color + osc_11, + + // Get/set cursor color + osc_12, + // Reset color palette index osc_104, + // Reset foreground color + osc_110, + + // Reset background color + osc_111, + + // Reset cursor color + osc_112, + // Hyperlinks hyperlink_param_key, hyperlink_param_value, @@ -548,15 +529,26 @@ pub const Parser = struct { }, .@"10" => switch (c) { - ';' => self.state = .query_fg_color, + ';' => osc_10: { + if (self.alloc == null) { + log.warn("OSC 10 requires an allocator, but none was provided", .{}); + self.state = .invalid; + break :osc_10; + } + self.state = .osc_10; + self.buf_start = self.buf_idx; + self.complete = true; + }, '4' => self.state = .@"104", else => self.state = .invalid, }, + .osc_10 => {}, + .@"104" => switch (c) { ';' => osc_104: { if (self.alloc == null) { - log.info("OSC 104 requires an allocator, but none was provided", .{}); + log.warn("OSC 104 requires an allocator, but none was provided", .{}); self.state = .invalid; break :osc_104; } @@ -570,30 +562,73 @@ pub const Parser = struct { .osc_104 => {}, .@"11" => switch (c) { - ';' => self.state = .query_bg_color, - '0' => { - self.command = .{ .reset_color = .{ .kind = .foreground, .value = undefined } }; + ';' => osc_11: { + if (self.alloc == null) { + log.warn("OSC 11 requires an allocator, but none was provided", .{}); + self.state = .invalid; + break :osc_11; + } + self.state = .osc_11; + self.buf_start = self.buf_idx; self.complete = true; - self.state = .invalid; }, - '1' => { - self.command = .{ .reset_color = .{ .kind = .background, .value = undefined } }; + '0' => osc_110: { + if (self.alloc == null) { + log.warn("OSC 110 requires an allocator, but none was provided", .{}); + self.state = .invalid; + break :osc_110; + } + self.state = .osc_110; + self.buf_start = self.buf_idx; self.complete = true; - self.state = .invalid; }, - '2' => { - self.command = .{ .reset_color = .{ .kind = .cursor, .value = undefined } }; + '1' => osc_111: { + if (self.alloc == null) { + log.warn("OSC 111 requires an allocator, but none was provided", .{}); + self.state = .invalid; + break :osc_111; + } + self.state = .osc_111; + self.buf_start = self.buf_idx; + self.complete = true; + }, + '2' => osc_112: { + if (self.alloc == null) { + log.warn("OSC 112 requires an allocator, but none was provided", .{}); + self.state = .invalid; + break :osc_112; + } + self.state = .osc_112; + self.buf_start = self.buf_idx; self.complete = true; - self.state = .invalid; }, else => self.state = .invalid, }, + .osc_11 => {}, + + .osc_110 => {}, + + .osc_111 => {}, + + .osc_112 => {}, + .@"12" => switch (c) { - ';' => self.state = .query_cursor_color, + ';' => osc_12: { + if (self.alloc == null) { + log.warn("OSC 12 requires an allocator, but none was provided", .{}); + self.state = .invalid; + break :osc_12; + } + self.state = .osc_12; + self.buf_start = self.buf_idx; + self.complete = true; + }, else => self.state = .invalid, }, + .osc_12 => {}, + .@"13" => switch (c) { '3' => self.state = .@"133", else => self.state = .invalid, @@ -978,60 +1013,6 @@ pub const Parser = struct { }, }, - .query_fg_color => switch (c) { - '?' => { - self.command = .{ .report_color = .{ .kind = .foreground } }; - self.complete = true; - self.state = .invalid; - }, - else => { - self.command = .{ .set_color = .{ - .kind = .foreground, - .value = "", - } }; - - self.state = .string; - self.temp_state = .{ .str = &self.command.set_color.value }; - self.buf_start = self.buf_idx - 1; - }, - }, - - .query_bg_color => switch (c) { - '?' => { - self.command = .{ .report_color = .{ .kind = .background } }; - self.complete = true; - self.state = .invalid; - }, - else => { - self.command = .{ .set_color = .{ - .kind = .background, - .value = "", - } }; - - self.state = .string; - self.temp_state = .{ .str = &self.command.set_color.value }; - self.buf_start = self.buf_idx - 1; - }, - }, - - .query_cursor_color => switch (c) { - '?' => { - self.command = .{ .report_color = .{ .kind = .cursor } }; - self.complete = true; - self.state = .invalid; - }, - else => { - self.command = .{ .set_color = .{ - .kind = .cursor, - .value = "", - } }; - - self.state = .string; - self.temp_state = .{ .str = &self.command.set_color.value }; - self.buf_start = self.buf_idx - 1; - }, - }, - .semantic_prompt => switch (c) { 'A' => { self.state = .semantic_option_start; @@ -1397,6 +1378,115 @@ pub const Parser = struct { } } + fn parseOSC101112(self: *Parser) void { + assert(switch (self.state) { + .osc_10, .osc_11, .osc_12 => true, + else => false, + }); + + const alloc = self.alloc orelse return; + + self.command = .{ + .color_operation = .{ + .source = switch (self.state) { + .osc_10 => .osc_10, + .osc_11 => .osc_11, + .osc_12 => .osc_12, + else => unreachable, + }, + .operations = std.ArrayListUnmanaged(Command.ColorOperation).initCapacity(alloc, 1) catch |err| { + log.warn("unable to allocate memory for OSC 10/11/12 parsing: {}", .{err}); + self.state = .invalid; + return; + }, + }, + }; + const str = self.buf[self.buf_start..self.buf_idx]; + var it = std.mem.splitScalar(u8, str, ';'); + const color_str = it.next() orelse { + log.warn("OSC 10/11/12 requires an argument", .{}); + self.state = .invalid; + return; + }; + if (std.mem.eql(u8, color_str, "?")) { + self.command.color_operation.operations.append( + alloc, + .{ + .report = switch (self.state) { + .osc_10 => .foreground, + .osc_11 => .background, + .osc_12 => .cursor, + else => unreachable, + }, + }, + ) catch |err| { + log.warn("unable to append color operation: {}", .{err}); + return; + }; + } else { + const color = RGB.parse(color_str) catch |err| { + log.warn("invalid color specification in OSC 10/11/12: {s} {}", .{ color_str, err }); + return; + }; + self.command.color_operation.operations.append( + alloc, + .{ + .set = .{ + .kind = switch (self.state) { + .osc_10 => .foreground, + .osc_11 => .background, + .osc_12 => .cursor, + else => unreachable, + }, + .color = color, + }, + }, + ) catch |err| { + log.warn("unable to append color operation: {}", .{err}); + return; + }; + } + } + + fn parseOSC110111112(self: *Parser) void { + assert(switch (self.state) { + .osc_110, .osc_111, .osc_112 => true, + else => false, + }); + + const alloc = self.alloc orelse return; + + self.command = .{ + .color_operation = .{ + .source = switch (self.state) { + .osc_110 => .osc_110, + .osc_111 => .osc_111, + .osc_112 => .osc_112, + else => unreachable, + }, + .operations = std.ArrayListUnmanaged(Command.ColorOperation).initCapacity(alloc, 1) catch |err| { + log.warn("unable to allocate memory for OSC 110/111/112 parsing: {}", .{err}); + self.state = .invalid; + return; + }, + }, + }; + self.command.color_operation.operations.append( + alloc, + .{ + .reset = switch (self.state) { + .osc_110 => .foreground, + .osc_111 => .background, + .osc_112 => .cursor, + else => unreachable, + }, + }, + ) catch |err| { + log.warn("unable to append color operation: {}", .{err}); + return; + }; + } + fn parseOSC104(self: *Parser) void { assert(self.state == .osc_104); @@ -1457,13 +1547,22 @@ pub const Parser = struct { .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 => self.parseOSC4(), - .osc_104 => self.parseOSC104(), + .osc_4, + => self.parseOSC4(), + .osc_10, + .osc_11, + .osc_12, + => self.parseOSC101112(), + .osc_104, + => self.parseOSC104(), + .osc_110, + .osc_111, + .osc_112, + => self.parseOSC110111112(), else => {}, } switch (self.command) { - .report_color => |*c| c.terminator = .init(terminator_ch), .kitty_color_protocol => |*c| c.terminator = .init(terminator_ch), .color_operation => |*c| c.terminator = .init(terminator_ch), else => {}, @@ -1674,17 +1773,86 @@ test "OSC: end_of_input" { try testing.expect(cmd == .end_of_input); } -test "OSC: reset cursor color" { +test "OSC: OSC110: reset cursor color" { const testing = std.testing; - var p: Parser = .{}; + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); - const input = "112"; + var p: Parser = .{ .alloc = arena.allocator() }; + + const input = "110"; for (input) |ch| p.next(ch); const cmd = p.end(null).?; - try testing.expect(cmd == .reset_color); - try testing.expectEqual(cmd.reset_color.kind, .cursor); + try testing.expect(cmd == .color_operation); + try testing.expectEqual(cmd.color_operation.terminator, .st); + try testing.expect(cmd.color_operation.source == .osc_110); + try testing.expect(cmd.color_operation.operations.items.len == 1); + { + const op = cmd.color_operation.operations.items[0]; + try testing.expect(op == .reset); + try testing.expectEqual( + Command.ColorKind.foreground, + op.reset, + ); + } +} + +test "OSC: OSC111: reset cursor color" { + const testing = std.testing; + + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + var p: Parser = .{ .alloc = arena.allocator() }; + + 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 == .osc_111); + try testing.expect(cmd.color_operation.operations.items.len == 1); + { + const op = cmd.color_operation.operations.items[0]; + try testing.expect(op == .reset); + try testing.expectEqual( + Command.ColorKind.background, + op.reset, + ); + } +} + +test "OSC: OSC112: reset cursor color" { + const testing = std.testing; + + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + var p: Parser = .{ .alloc = arena.allocator() }; + + const input = "112"; + for (input) |ch| { + log.warn("feeding {c} {s}", .{ ch, @tagName(p.state) }); + p.next(ch); + } + log.warn("finish: {s}", .{@tagName(p.state)}); + + 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 == .osc_112); + try testing.expect(cmd.color_operation.operations.items.len == 1); + { + const op = cmd.color_operation.operations.items[0]; + try testing.expect(op == .reset); + try testing.expectEqual( + Command.ColorKind.cursor, + op.reset, + ); + } } test "OSC: get/set clipboard" { @@ -1781,62 +1949,178 @@ test "OSC: longer than buffer" { try testing.expect(p.complete == false); } -test "OSC: report default foreground color" { +test "OSC: OSC10: report default foreground color" { const testing = std.testing; - var p: Parser = .{}; + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + var p: Parser = .{ .alloc = arena.allocator() }; const input = "10;?"; for (input) |ch| p.next(ch); // This corresponds to ST = ESC followed by \ const cmd = p.end('\x1b').?; - try testing.expect(cmd == .report_color); - try testing.expectEqual(cmd.report_color.kind, .foreground); - try testing.expectEqual(cmd.report_color.terminator, .st); + + try testing.expect(cmd == .color_operation); + try testing.expectEqual(cmd.color_operation.terminator, .st); + try testing.expect(cmd.color_operation.source == .osc_10); + try testing.expect(cmd.color_operation.operations.items.len == 1); + { + const op = cmd.color_operation.operations.items[0]; + try testing.expect(op == .report); + try testing.expectEqual( + Command.ColorKind.foreground, + op.report, + ); + } } -test "OSC: set foreground color" { +test "OSC: OSC10: set foreground color" { const testing = std.testing; - var p: Parser = .{}; + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + var p: Parser = .{ .alloc = arena.allocator() }; const input = "10;rgbi:0.0/0.5/1.0"; for (input) |ch| p.next(ch); const cmd = p.end('\x07').?; - try testing.expect(cmd == .set_color); - try testing.expectEqual(cmd.set_color.kind, .foreground); - try testing.expectEqualStrings(cmd.set_color.value, "rgbi:0.0/0.5/1.0"); + try testing.expect(cmd == .color_operation); + try testing.expectEqual(cmd.color_operation.terminator, .bel); + try testing.expect(cmd.color_operation.source == .osc_10); + try testing.expect(cmd.color_operation.operations.items.len == 1); + { + const op = cmd.color_operation.operations.items[0]; + try testing.expect(op == .set); + try testing.expectEqual( + Command.ColorKind.foreground, + op.set.kind, + ); + try testing.expectEqual( + RGB{ .r = 0x00, .g = 0x7f, .b = 0xff }, + op.set.color, + ); + } } -test "OSC: report default background color" { +test "OSC: OSC11: report default background color" { const testing = std.testing; - var p: Parser = .{}; + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + var p: Parser = .{ .alloc = arena.allocator() }; const input = "11;?"; for (input) |ch| p.next(ch); // This corresponds to ST = BEL character const cmd = p.end('\x07').?; - try testing.expect(cmd == .report_color); - try testing.expectEqual(cmd.report_color.kind, .background); - try testing.expectEqual(cmd.report_color.terminator, .bel); + try testing.expect(cmd == .color_operation); + try testing.expectEqual(cmd.color_operation.terminator, .bel); + try testing.expect(cmd.color_operation.source == .osc_11); + try testing.expect(cmd.color_operation.operations.items.len == 1); + { + const op = cmd.color_operation.operations.items[0]; + try testing.expect(op == .report); + try testing.expectEqual( + Command.ColorKind.background, + op.report, + ); + } + try testing.expectEqual(cmd.color_operation.terminator, .bel); } -test "OSC: set background color" { +test "OSC: OSC11: set background color" { const testing = std.testing; - var p: Parser = .{}; + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + var p: Parser = .{ .alloc = arena.allocator() }; const input = "11;rgb:f/ff/ffff"; for (input) |ch| p.next(ch); const cmd = p.end('\x1b').?; - try testing.expect(cmd == .set_color); - try testing.expectEqual(cmd.set_color.kind, .background); - try testing.expectEqualStrings(cmd.set_color.value, "rgb:f/ff/ffff"); + try testing.expect(cmd == .color_operation); + try testing.expectEqual(cmd.color_operation.terminator, .st); + try testing.expect(cmd.color_operation.source == .osc_11); + try testing.expect(cmd.color_operation.operations.items.len == 1); + { + const op = cmd.color_operation.operations.items[0]; + try testing.expect(op == .set); + try testing.expectEqual( + Command.ColorKind.background, + op.set.kind, + ); + try testing.expectEqual( + RGB{ .r = 0xff, .g = 0xff, .b = 0xff }, + op.set.color, + ); + } +} + +test "OSC: OSC12: report background color" { + const testing = std.testing; + + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + var p: Parser = .{ .alloc = arena.allocator() }; + + const input = "12;?"; + for (input) |ch| p.next(ch); + + // This corresponds to ST = BEL character + const cmd = p.end('\x07').?; + try testing.expect(cmd == .color_operation); + try testing.expectEqual(cmd.color_operation.terminator, .bel); + try testing.expect(cmd.color_operation.source == .osc_12); + try testing.expect(cmd.color_operation.operations.items.len == 1); + { + const op = cmd.color_operation.operations.items[0]; + try testing.expect(op == .report); + try testing.expectEqual( + Command.ColorKind.cursor, + op.report, + ); + } + try testing.expectEqual(cmd.color_operation.terminator, .bel); +} + +test "OSC: OSC12: set background color" { + const testing = std.testing; + + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + var p: Parser = .{ .alloc = arena.allocator() }; + + const input = "12;rgb:f/ff/ffff"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?; + try testing.expect(cmd == .color_operation); + try testing.expectEqual(cmd.color_operation.terminator, .st); + try testing.expect(cmd.color_operation.source == .osc_12); + try testing.expect(cmd.color_operation.operations.items.len == 1); + { + const op = cmd.color_operation.operations.items[0]; + try testing.expect(op == .set); + try testing.expectEqual( + Command.ColorKind.cursor, + op.set.kind, + ); + try testing.expectEqual( + RGB{ .r = 0xff, .g = 0xff, .b = 0xff }, + op.set.color, + ); + } } test "OSC: OSC4: get palette color 1" { diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index 2a1ae80c9..08ce23098 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -1562,27 +1562,6 @@ pub fn Stream(comptime Handler: type) type { } else log.warn("unimplemented OSC callback: {}", .{cmd}); }, - .report_color => |v| { - if (@hasDecl(T, "reportColor")) { - try self.handler.reportColor(v.kind, v.terminator); - return; - } else log.warn("unimplemented OSC callback: {}", .{cmd}); - }, - - .set_color => |v| { - if (@hasDecl(T, "setColor")) { - try self.handler.setColor(v.kind, v.value); - return; - } else log.warn("unimplemented OSC callback: {}", .{cmd}); - }, - - .reset_color => |v| { - if (@hasDecl(T, "resetColor")) { - try self.handler.resetColor(v.kind, v.value); - return; - } else log.warn("unimplemented OSC callback: {}", .{cmd}); - }, - .kitty_color_protocol => |v| { if (@hasDecl(T, "sendKittyColorReport")) { try self.handler.sendKittyColorReport(v); diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 57a1eeacf..396aae01f 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -1370,203 +1370,6 @@ pub const StreamHandler = struct { } } - /// Implements OSC 4, OSC 10, and OSC 11, which reports palette color, - /// default foreground color, and background color respectively. - pub fn reportColor( - self: *StreamHandler, - kind: terminal.osc.Command.ColorKind, - terminator: terminal.osc.Terminator, - ) !void { - if (self.osc_color_report_format == .none) return; - - const color = switch (kind) { - .palette => |i| self.terminal.color_palette.colors[i], - .foreground => self.foreground_color 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, - }; - - var msg: termio.Message = .{ .write_small = .{} }; - const resp = switch (self.osc_color_report_format) { - .@"16-bit" => switch (kind) { - .palette => |i| try std.fmt.bufPrint( - &msg.write_small.data, - "\x1B]{s};{d};rgb:{x:0>4}/{x:0>4}/{x:0>4}{s}", - .{ - kind.code(), - i, - @as(u16, color.r) * 257, - @as(u16, color.g) * 257, - @as(u16, color.b) * 257, - terminator.string(), - }, - ), - else => try std.fmt.bufPrint( - &msg.write_small.data, - "\x1B]{s};rgb:{x:0>4}/{x:0>4}/{x:0>4}{s}", - .{ - kind.code(), - @as(u16, color.r) * 257, - @as(u16, color.g) * 257, - @as(u16, color.b) * 257, - terminator.string(), - }, - ), - }, - - .@"8-bit" => switch (kind) { - .palette => |i| try std.fmt.bufPrint( - &msg.write_small.data, - "\x1B]{s};{d};rgb:{x:0>2}/{x:0>2}/{x:0>2}{s}", - .{ - kind.code(), - i, - @as(u16, color.r), - @as(u16, color.g), - @as(u16, color.b), - terminator.string(), - }, - ), - else => try std.fmt.bufPrint( - &msg.write_small.data, - "\x1B]{s};rgb:{x:0>2}/{x:0>2}/{x:0>2}{s}", - .{ - kind.code(), - @as(u16, color.r), - @as(u16, color.g), - @as(u16, color.b), - terminator.string(), - }, - ), - }, - .none => unreachable, // early return above - }; - msg.write_small.len = @intCast(resp.len); - self.messageWriter(msg); - } - - pub fn setColor( - self: *StreamHandler, - kind: terminal.osc.Command.ColorKind, - value: []const u8, - ) !void { - const color = try terminal.color.RGB.parse(value); - - switch (kind) { - .palette => |i| { - self.terminal.flags.dirty.palette = true; - self.terminal.color_palette.colors[i] = color; - self.terminal.color_palette.mask.set(i); - }, - .foreground => { - self.foreground_color = color; - _ = self.renderer_mailbox.push(.{ - .foreground_color = color, - }, .{ .forever = {} }); - }, - .background => { - self.background_color = color; - _ = self.renderer_mailbox.push(.{ - .background_color = color, - }, .{ .forever = {} }); - }, - .cursor => { - self.cursor_color = color; - _ = self.renderer_mailbox.push(.{ - .cursor_color = color, - }, .{ .forever = {} }); - }, - } - - // Notify the surface of the color change - self.surfaceMessageWriter(.{ .color_change = .{ - .kind = kind, - .color = color, - } }); - } - - pub fn resetColor( - self: *StreamHandler, - kind: terminal.osc.Command.ColorKind, - value: []const u8, - ) !void { - switch (kind) { - .palette => { - const mask = &self.terminal.color_palette.mask; - if (value.len == 0) { - // Find all bit positions in the mask which are set and - // reset those indices to the default palette - var it = mask.iterator(.{}); - while (it.next()) |i| { - 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], - } }); - } - } else { - var it = std.mem.tokenizeScalar(u8, value, ';'); - while (it.next()) |param| { - // Skip invalid parameters - const i = std.fmt.parseUnsigned(u8, param, 10) catch continue; - if (mask.isSet(i)) { - 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], - } }); - } - } - } - }, - .foreground => { - self.foreground_color = null; - _ = self.renderer_mailbox.push(.{ - .foreground_color = self.foreground_color, - }, .{ .forever = {} }); - - self.surfaceMessageWriter(.{ .color_change = .{ - .kind = .foreground, - .color = self.default_foreground_color, - } }); - }, - .background => { - self.background_color = null; - _ = self.renderer_mailbox.push(.{ - .background_color = self.background_color, - }, .{ .forever = {} }); - - self.surfaceMessageWriter(.{ .color_change = .{ - .kind = .background, - .color = self.default_background_color, - } }); - }, - .cursor => { - self.cursor_color = null; - - _ = self.renderer_mailbox.push(.{ - .cursor_color = self.cursor_color, - }, .{ .forever = {} }); - - if (self.default_cursor_color) |color| { - self.surfaceMessageWriter(.{ .color_change = .{ - .kind = .cursor, - .color = color, - } }); - } - }, - } - } - pub fn showDesktopNotification( self: *StreamHandler, title: []const u8, From 1288296fdc718308049b124f031a3899973e59ab Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Fri, 23 May 2025 22:04:26 -0500 Subject: [PATCH 04/17] OSC: add a datastructure to prevent some (most?) allocations --- src/datastruct/list.zig | 100 ++++++++++++++++ src/terminal/Parser.zig | 6 +- src/terminal/osc.zig | 216 +++++++++++++++++++++------------- src/terminal/stream.zig | 2 +- src/termio/stream_handler.zig | 6 +- 5 files changed, 246 insertions(+), 84 deletions(-) create mode 100644 src/datastruct/list.zig diff --git a/src/datastruct/list.zig b/src/datastruct/list.zig new file mode 100644 index 000000000..e5f8bb483 --- /dev/null +++ b/src/datastruct/list.zig @@ -0,0 +1,100 @@ +const std = @import("std"); + +const assert = std.debug.assert; + +/// Datastructure to manage a (usually) small list of items. To prevent allocations +/// on the heap, statically allocate a small array that gets used to store items. Once +/// that small array is full then memory will be dynamically allocated on the heap +/// to store items. +pub fn ArrayListStaticUnmanaged(comptime static_size: usize, comptime T: type) type { + return struct { + count: usize, + static: [static_size]T, + dynamic: std.ArrayListUnmanaged(T), + + const Self = @This(); + + pub const empty: Self = .{ + .count = 0, + .static = undefined, + .dynamic = .empty, + }; + + pub fn deinit(self: *Self, alloc: std.mem.Allocator) void { + self.dynamic.deinit(alloc); + } + + pub fn append(self: *Self, alloc: std.mem.Allocator, item: T) !void { + if (self.count < static_size) { + self.static[self.count] = item; + self.count += 1; + assert(self.count <= static_size); + return; + } + try self.dynamic.append(alloc, item); + self.count += 1; + assert(self.count == static_size + self.dynamic.items.len); + } + + pub const Iterator = struct { + context: *const Self, + index: usize, + + pub fn next(self: *Iterator) ?T { + if (self.index >= self.context.count) return null; + + if (self.index < static_size) { + defer self.index += 1; + return self.context.static[self.index]; + } + + assert(self.index - static_size < self.context.dynamic.items.len); + + defer self.index += 1; + return self.context.dynamic.items[self.index - static_size]; + } + }; + + pub fn iterator(self: *const Self) Iterator { + return .{ + .context = self, + .index = 0, + }; + } + }; +} + +test "ArrayListStaticUnmanged: 1" { + const alloc = std.testing.allocator; + + var l: ArrayListStaticUnmanaged(1, usize) = .empty; + defer l.deinit(alloc); + + try l.append(alloc, 1); + + try std.testing.expectEqual(1, l.count); + try std.testing.expectEqual(1, l.static[0]); + try std.testing.expectEqual(0, l.dynamic.items.len); + + var it = l.iterator(); + try std.testing.expectEqual(1, it.next().?); + try std.testing.expectEqual(null, it.next()); +} + +test "ArrayListStaticUnmanged: 2" { + const alloc = std.testing.allocator; + + var l: ArrayListStaticUnmanaged(1, usize) = .empty; + defer l.deinit(alloc); + + try l.append(alloc, 1); + try l.append(alloc, 2); + + try std.testing.expectEqual(2, l.count); + try std.testing.expectEqual(1, l.static[0]); + try std.testing.expectEqual(1, l.dynamic.items.len); + var it = l.iterator(); + try std.testing.expectEqual(1, it.next().?); + try std.testing.expectEqual(2, it.next().?); + try std.testing.expectEqual(null, it.next()); +} diff --git a/src/terminal/Parser.zig b/src/terminal/Parser.zig index 80772d71f..0f035f7fb 100644 --- a/src/terminal/Parser.zig +++ b/src/terminal/Parser.zig @@ -900,15 +900,17 @@ test "osc: 112 incomplete sequence" { try testing.expect(cmd == .color_operation); try testing.expectEqual(cmd.color_operation.terminator, .bel); try testing.expect(cmd.color_operation.source == .osc_112); - try testing.expect(cmd.color_operation.operations.items.len == 1); + try testing.expect(cmd.color_operation.operations.count == 1); + var it = cmd.color_operation.operations.iterator(); { - const op = cmd.color_operation.operations.items[0]; + const op = it.next().?; try testing.expect(op == .reset); try testing.expectEqual( osc.Command.ColorKind.cursor, op.reset, ); } + try std.testing.expect(it.next() == null); } } diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 7b9239e43..0f5ecf724 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -13,6 +13,8 @@ const Allocator = mem.Allocator; const RGB = @import("color.zig").RGB; const kitty = @import("kitty.zig"); +const ArrayListStaticUnmanaged = @import("../datastruct/list.zig").ArrayListStaticUnmanaged; + const log = std.log.scoped(.osc); pub const Command = union(enum) { @@ -112,7 +114,7 @@ pub const Command = union(enum) { /// OSC color operations color_operation: struct { source: ColorOperationSource, - operations: std.ArrayListUnmanaged(ColorOperation) = .empty, + operations: ColorOperationList = .empty, terminator: Terminator = .st, }, @@ -187,6 +189,8 @@ pub const Command = union(enum) { report: ColorKind, }; + pub const ColorOperationList = ArrayListStaticUnmanaged(4, ColorOperation); + pub const ColorKind = union(enum) { palette: u8, foreground, @@ -1325,11 +1329,7 @@ pub const Parser = struct { self.command = .{ .color_operation = .{ .source = .osc_4, - .operations = std.ArrayListUnmanaged(Command.ColorOperation).initCapacity(alloc, 8) catch |err| { - log.warn("unable to allocate memory for OSC 4 parsing: {}", .{err}); - self.state = .invalid; - return; - }, + .operations = .empty, }, }; @@ -1394,11 +1394,7 @@ pub const Parser = struct { .osc_12 => .osc_12, else => unreachable, }, - .operations = std.ArrayListUnmanaged(Command.ColorOperation).initCapacity(alloc, 1) catch |err| { - log.warn("unable to allocate memory for OSC 10/11/12 parsing: {}", .{err}); - self.state = .invalid; - return; - }, + .operations = .empty, }, }; const str = self.buf[self.buf_start..self.buf_idx]; @@ -1464,11 +1460,7 @@ pub const Parser = struct { .osc_112 => .osc_112, else => unreachable, }, - .operations = std.ArrayListUnmanaged(Command.ColorOperation).initCapacity(alloc, 1) catch |err| { - log.warn("unable to allocate memory for OSC 110/111/112 parsing: {}", .{err}); - self.state = .invalid; - return; - }, + .operations = .empty, }, }; self.command.color_operation.operations.append( @@ -1495,11 +1487,7 @@ pub const Parser = struct { self.command = .{ .color_operation = .{ .source = .osc_104, - .operations = std.ArrayListUnmanaged(Command.ColorOperation).initCapacity(alloc, 8) catch |err| { - log.warn("unable to allocate memory for OSC 104 parsing: {}", .{err}); - self.state = .invalid; - return; - }, + .operations = .empty, }, }; @@ -1788,15 +1776,17 @@ test "OSC: OSC110: reset cursor color" { try testing.expect(cmd == .color_operation); try testing.expectEqual(cmd.color_operation.terminator, .st); try testing.expect(cmd.color_operation.source == .osc_110); - try testing.expect(cmd.color_operation.operations.items.len == 1); + try testing.expect(cmd.color_operation.operations.count == 1); + var it = cmd.color_operation.operations.iterator(); { - const op = cmd.color_operation.operations.items[0]; + const op = it.next().?; try testing.expect(op == .reset); try testing.expectEqual( Command.ColorKind.foreground, op.reset, ); } + try testing.expect(it.next() == null); } test "OSC: OSC111: reset cursor color" { @@ -1814,15 +1804,17 @@ test "OSC: OSC111: reset cursor color" { try testing.expect(cmd == .color_operation); try testing.expectEqual(cmd.color_operation.terminator, .st); try testing.expect(cmd.color_operation.source == .osc_111); - try testing.expect(cmd.color_operation.operations.items.len == 1); + try testing.expect(cmd.color_operation.operations.count == 1); + var it = cmd.color_operation.operations.iterator(); { - const op = cmd.color_operation.operations.items[0]; + const op = it.next().?; try testing.expect(op == .reset); try testing.expectEqual( Command.ColorKind.background, op.reset, ); } + try testing.expect(it.next() == null); } test "OSC: OSC112: reset cursor color" { @@ -1834,25 +1826,55 @@ test "OSC: OSC112: reset cursor color" { var p: Parser = .{ .alloc = arena.allocator() }; const input = "112"; - for (input) |ch| { - log.warn("feeding {c} {s}", .{ ch, @tagName(p.state) }); - p.next(ch); - } - log.warn("finish: {s}", .{@tagName(p.state)}); + 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 == .osc_112); - try testing.expect(cmd.color_operation.operations.items.len == 1); + try testing.expect(cmd.color_operation.operations.count == 1); + var it = cmd.color_operation.operations.iterator(); { - const op = cmd.color_operation.operations.items[0]; + const op = it.next().?; try testing.expect(op == .reset); try testing.expectEqual( Command.ColorKind.cursor, op.reset, ); } + try testing.expect(it.next() == null); +} + +test "OSC: OSC112: reset cursor color with semicolon" { + const testing = std.testing; + + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + var p: Parser = .{ .alloc = arena.allocator() }; + + const input = "112;"; + for (input) |ch| { + log.warn("feeding {c} {s}", .{ ch, @tagName(p.state) }); + 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 == .osc_112); + try testing.expect(cmd.color_operation.operations.count == 1); + var it = cmd.color_operation.operations.iterator(); + { + const op = it.next().?; + try testing.expect(op == .reset); + try testing.expectEqual( + Command.ColorKind.cursor, + op.reset, + ); + } + try testing.expect(it.next() == null); } test "OSC: get/set clipboard" { @@ -1966,15 +1988,17 @@ test "OSC: OSC10: report default foreground color" { try testing.expect(cmd == .color_operation); try testing.expectEqual(cmd.color_operation.terminator, .st); try testing.expect(cmd.color_operation.source == .osc_10); - try testing.expect(cmd.color_operation.operations.items.len == 1); + try testing.expect(cmd.color_operation.operations.count == 1); + var it = cmd.color_operation.operations.iterator(); { - const op = cmd.color_operation.operations.items[0]; + const op = it.next().?; try testing.expect(op == .report); try testing.expectEqual( Command.ColorKind.foreground, op.report, ); } + try testing.expect(it.next() == null); } test "OSC: OSC10: set foreground color" { @@ -1992,9 +2016,10 @@ test "OSC: OSC10: set foreground color" { try testing.expect(cmd == .color_operation); try testing.expectEqual(cmd.color_operation.terminator, .bel); try testing.expect(cmd.color_operation.source == .osc_10); - try testing.expect(cmd.color_operation.operations.items.len == 1); + try testing.expect(cmd.color_operation.operations.count == 1); + var it = cmd.color_operation.operations.iterator(); { - const op = cmd.color_operation.operations.items[0]; + const op = it.next().?; try testing.expect(op == .set); try testing.expectEqual( Command.ColorKind.foreground, @@ -2005,6 +2030,7 @@ test "OSC: OSC10: set foreground color" { op.set.color, ); } + try testing.expect(it.next() == null); } test "OSC: OSC11: report default background color" { @@ -2023,9 +2049,10 @@ test "OSC: OSC11: report default background color" { try testing.expect(cmd == .color_operation); try testing.expectEqual(cmd.color_operation.terminator, .bel); try testing.expect(cmd.color_operation.source == .osc_11); - try testing.expect(cmd.color_operation.operations.items.len == 1); + try testing.expect(cmd.color_operation.operations.count == 1); + var it = cmd.color_operation.operations.iterator(); { - const op = cmd.color_operation.operations.items[0]; + const op = it.next().?; try testing.expect(op == .report); try testing.expectEqual( Command.ColorKind.background, @@ -2033,6 +2060,7 @@ test "OSC: OSC11: report default background color" { ); } try testing.expectEqual(cmd.color_operation.terminator, .bel); + try testing.expect(it.next() == null); } test "OSC: OSC11: set background color" { @@ -2050,9 +2078,10 @@ test "OSC: OSC11: set background color" { try testing.expect(cmd == .color_operation); try testing.expectEqual(cmd.color_operation.terminator, .st); try testing.expect(cmd.color_operation.source == .osc_11); - try testing.expect(cmd.color_operation.operations.items.len == 1); + try testing.expect(cmd.color_operation.operations.count == 1); + var it = cmd.color_operation.operations.iterator(); { - const op = cmd.color_operation.operations.items[0]; + const op = it.next().?; try testing.expect(op == .set); try testing.expectEqual( Command.ColorKind.background, @@ -2063,6 +2092,7 @@ test "OSC: OSC11: set background color" { op.set.color, ); } + try testing.expect(it.next() == null); } test "OSC: OSC12: report background color" { @@ -2081,9 +2111,10 @@ test "OSC: OSC12: report background color" { try testing.expect(cmd == .color_operation); try testing.expectEqual(cmd.color_operation.terminator, .bel); try testing.expect(cmd.color_operation.source == .osc_12); - try testing.expect(cmd.color_operation.operations.items.len == 1); + try testing.expect(cmd.color_operation.operations.count == 1); + var it = cmd.color_operation.operations.iterator(); { - const op = cmd.color_operation.operations.items[0]; + const op = it.next().?; try testing.expect(op == .report); try testing.expectEqual( Command.ColorKind.cursor, @@ -2091,6 +2122,7 @@ test "OSC: OSC12: report background color" { ); } try testing.expectEqual(cmd.color_operation.terminator, .bel); + try testing.expect(it.next() == null); } test "OSC: OSC12: set background color" { @@ -2108,9 +2140,10 @@ test "OSC: OSC12: set background color" { try testing.expect(cmd == .color_operation); try testing.expectEqual(cmd.color_operation.terminator, .st); try testing.expect(cmd.color_operation.source == .osc_12); - try testing.expect(cmd.color_operation.operations.items.len == 1); + try testing.expect(cmd.color_operation.operations.count == 1); + var it = cmd.color_operation.operations.iterator(); { - const op = cmd.color_operation.operations.items[0]; + const op = it.next().?; try testing.expect(op == .set); try testing.expectEqual( Command.ColorKind.cursor, @@ -2121,6 +2154,7 @@ test "OSC: OSC12: set background color" { op.set.color, ); } + try testing.expect(it.next() == null); } test "OSC: OSC4: get palette color 1" { @@ -2137,9 +2171,10 @@ test "OSC: OSC4: get palette color 1" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); try testing.expect(cmd.color_operation.source == .osc_4); - try testing.expect(cmd.color_operation.operations.items.len == 1); + try testing.expect(cmd.color_operation.operations.count == 1); + var it = cmd.color_operation.operations.iterator(); { - const op = cmd.color_operation.operations.items[0]; + const op = it.next().?; try testing.expect(op == .report); try testing.expectEqual( Command.ColorKind{ .palette = 1 }, @@ -2147,6 +2182,7 @@ test "OSC: OSC4: get palette color 1" { ); try testing.expectEqual(cmd.color_operation.terminator, .st); } + try testing.expect(it.next() == null); } test "OSC: OSC4: get palette color 2" { @@ -2163,9 +2199,10 @@ test "OSC: OSC4: get palette color 2" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); try testing.expect(cmd.color_operation.source == .osc_4); - try testing.expect(cmd.color_operation.operations.items.len == 2); + try testing.expect(cmd.color_operation.operations.count == 2); + var it = cmd.color_operation.operations.iterator(); { - const op = cmd.color_operation.operations.items[0]; + const op = it.next().?; try testing.expect(op == .report); try testing.expectEqual( Command.ColorKind{ .palette = 1 }, @@ -2173,7 +2210,7 @@ test "OSC: OSC4: get palette color 2" { ); } { - const op = cmd.color_operation.operations.items[1]; + const op = it.next().?; try testing.expect(op == .report); try testing.expectEqual( Command.ColorKind{ .palette = 2 }, @@ -2181,6 +2218,7 @@ test "OSC: OSC4: get palette color 2" { ); } try testing.expectEqual(cmd.color_operation.terminator, .st); + try testing.expect(it.next() == null); } test "OSC: OSC4: set palette color 1" { @@ -2197,17 +2235,21 @@ test "OSC: OSC4: set palette color 1" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); try testing.expect(cmd.color_operation.source == .osc_4); - try testing.expect(cmd.color_operation.operations.items.len == 1); - const op = cmd.color_operation.operations.items[0]; - try testing.expect(op == .set); - try testing.expectEqual( - Command.ColorKind{ .palette = 17 }, - op.set.kind, - ); - try testing.expectEqual( - RGB{ .r = 0xaa, .g = 0xbb, .b = 0xcc }, - op.set.color, - ); + try testing.expect(cmd.color_operation.operations.count == 1); + var it = cmd.color_operation.operations.iterator(); + { + const op = it.next().?; + try testing.expect(op == .set); + try testing.expectEqual( + Command.ColorKind{ .palette = 17 }, + op.set.kind, + ); + try testing.expectEqual( + RGB{ .r = 0xaa, .g = 0xbb, .b = 0xcc }, + op.set.color, + ); + } + try testing.expect(it.next() == null); } test "OSC: OSC4: set palette color 2" { @@ -2224,9 +2266,10 @@ test "OSC: OSC4: set palette color 2" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); try testing.expect(cmd.color_operation.source == .osc_4); - try testing.expect(cmd.color_operation.operations.items.len == 2); + try testing.expect(cmd.color_operation.operations.count == 2); + var it = cmd.color_operation.operations.iterator(); { - const op = cmd.color_operation.operations.items[0]; + const op = it.next().?; try testing.expect(op == .set); try testing.expectEqual( Command.ColorKind{ .palette = 17 }, @@ -2238,7 +2281,7 @@ test "OSC: OSC4: set palette color 2" { ); } { - const op = cmd.color_operation.operations.items[1]; + const op = it.next().?; try testing.expect(op == .set); try testing.expectEqual( Command.ColorKind{ .palette = 1 }, @@ -2249,6 +2292,7 @@ test "OSC: OSC4: set palette color 2" { op.set.color, ); } + try testing.expect(it.next() == null); } test "OSC: OSC4: get with invalid index" { @@ -2265,15 +2309,17 @@ test "OSC: OSC4: get with invalid index" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); try testing.expect(cmd.color_operation.source == .osc_4); - try testing.expect(cmd.color_operation.operations.items.len == 1); + try testing.expect(cmd.color_operation.operations.count == 1); + var it = cmd.color_operation.operations.iterator(); { - const op = cmd.color_operation.operations.items[0]; + const op = it.next().?; try testing.expect(op == .report); try testing.expectEqual( Command.ColorKind{ .palette = 1 }, op.report, ); } + try testing.expect(it.next() == null); } test "OSC: OSC4: set with invalid index" { @@ -2290,9 +2336,10 @@ test "OSC: OSC4: set with invalid index" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); try testing.expect(cmd.color_operation.source == .osc_4); - try testing.expect(cmd.color_operation.operations.items.len == 1); + try testing.expect(cmd.color_operation.operations.count == 1); + var it = cmd.color_operation.operations.iterator(); { - const op = cmd.color_operation.operations.items[0]; + const op = it.next().?; try testing.expect(op == .set); try testing.expectEqual( Command.ColorKind{ .palette = 1 }, @@ -2303,6 +2350,7 @@ test "OSC: OSC4: set with invalid index" { op.set.color, ); } + try testing.expect(it.next() == null); } test "OSC: OSC4: mix get/set palette color" { @@ -2319,9 +2367,10 @@ test "OSC: OSC4: mix get/set palette color" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); try testing.expect(cmd.color_operation.source == .osc_4); - try testing.expect(cmd.color_operation.operations.items.len == 2); + try testing.expect(cmd.color_operation.operations.count == 2); + var it = cmd.color_operation.operations.iterator(); { - const op = cmd.color_operation.operations.items[0]; + const op = it.next().?; try testing.expect(op == .set); try testing.expectEqual( Command.ColorKind{ .palette = 17 }, @@ -2333,10 +2382,11 @@ test "OSC: OSC4: mix get/set palette color" { ); } { - const op = cmd.color_operation.operations.items[1]; + const op = it.next().?; try testing.expect(op == .report); try testing.expectEqual(Command.ColorKind{ .palette = 254 }, op.report); } + try testing.expect(it.next() == null); } test "OSC: OSC104: reset palette color 1" { @@ -2353,15 +2403,17 @@ test "OSC: OSC104: reset palette color 1" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); try testing.expect(cmd.color_operation.source == .osc_104); - try testing.expect(cmd.color_operation.operations.items.len == 1); + try testing.expect(cmd.color_operation.operations.count == 1); + var it = cmd.color_operation.operations.iterator(); { - const op = cmd.color_operation.operations.items[0]; + const op = it.next().?; try testing.expect(op == .reset); try testing.expectEqual( Command.ColorKind{ .palette = 17 }, op.reset, ); } + try testing.expect(it.next() == null); } test "OSC: OSC104: reset palette color 2" { @@ -2378,9 +2430,10 @@ test "OSC: OSC104: reset palette color 2" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); try testing.expect(cmd.color_operation.source == .osc_104); - try testing.expect(cmd.color_operation.operations.items.len == 2); + try testing.expect(cmd.color_operation.operations.count == 2); + var it = cmd.color_operation.operations.iterator(); { - const op = cmd.color_operation.operations.items[0]; + const op = it.next().?; try testing.expect(op == .reset); try testing.expectEqual( Command.ColorKind{ .palette = 17 }, @@ -2388,13 +2441,14 @@ test "OSC: OSC104: reset palette color 2" { ); } { - const op = cmd.color_operation.operations.items[1]; + const op = it.next().?; try testing.expect(op == .reset); try testing.expectEqual( Command.ColorKind{ .palette = 111 }, op.reset, ); } + try testing.expect(it.next() == null); } test "OSC: OSC104: invalid palette index" { @@ -2411,15 +2465,17 @@ test "OSC: OSC104: invalid palette index" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); try testing.expect(cmd.color_operation.source == .osc_104); - try testing.expect(cmd.color_operation.operations.items.len == 1); + try testing.expect(cmd.color_operation.operations.count == 1); + var it = cmd.color_operation.operations.iterator(); { - const op = cmd.color_operation.operations.items[0]; + const op = it.next().?; try testing.expect(op == .reset); try testing.expectEqual( Command.ColorKind{ .palette = 111 }, op.reset, ); } + try testing.expect(it.next() == null); } test "OSC: OSC104: empty palette index" { @@ -2436,15 +2492,17 @@ test "OSC: OSC104: empty palette index" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); try testing.expect(cmd.color_operation.source == .osc_104); - try testing.expect(cmd.color_operation.operations.items.len == 1); + try testing.expect(cmd.color_operation.operations.count == 1); + var it = cmd.color_operation.operations.iterator(); { - const op = cmd.color_operation.operations.items[0]; + const op = it.next().?; try testing.expect(op == .reset); try testing.expectEqual( Command.ColorKind{ .palette = 111 }, op.reset, ); } + try std.testing.expect(it.next() == null); } test "OSC: conemu sleep" { diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index 08ce23098..fd30720b3 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -1557,7 +1557,7 @@ pub fn Stream(comptime Handler: type) type { .color_operation => |v| { if (@hasDecl(T, "handleColorOperation")) { - try self.handler.handleColorOperation(v.source, v.operations.items, v.terminator); + try self.handler.handleColorOperation(v.source, &v.operations, 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 396aae01f..fd450f229 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -1198,7 +1198,7 @@ pub const StreamHandler = struct { pub fn handleColorOperation( self: *StreamHandler, source: terminal.osc.Command.ColorOperationSource, - operations: []terminal.osc.Command.ColorOperation, + operations: *const terminal.osc.Command.ColorOperationList, terminator: terminal.osc.Terminator, ) !void { var buffer: [1024]u8 = undefined; @@ -1212,7 +1212,9 @@ pub const StreamHandler = struct { try writer.print("\x1b]{}", .{source}); - for (operations) |op| { + var it = operations.iterator(); + + while (it.next()) |op| { switch (op) { .set => |set| { switch (set.kind) { From 04e8e521719e040e36e0814fd6944220a7e1cbd5 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Fri, 23 May 2025 22:26:01 -0500 Subject: [PATCH 05/17] OSC: reflow comment --- src/datastruct/list.zig | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/datastruct/list.zig b/src/datastruct/list.zig index e5f8bb483..7e9b78761 100644 --- a/src/datastruct/list.zig +++ b/src/datastruct/list.zig @@ -2,10 +2,10 @@ const std = @import("std"); const assert = std.debug.assert; -/// Datastructure to manage a (usually) small list of items. To prevent allocations -/// on the heap, statically allocate a small array that gets used to store items. Once -/// that small array is full then memory will be dynamically allocated on the heap -/// to store items. +/// Datastructure to manage a (usually) small list of items. To prevent +/// allocations on the heap, statically allocate a small array that gets used to +/// store items. Once that small array is full then memory will be dynamically +/// allocated on the heap to store items. pub fn ArrayListStaticUnmanaged(comptime static_size: usize, comptime T: type) type { return struct { count: usize, From 1d9d253e4d2651f5c075082e94bf0eecbcd706e8 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Fri, 23 May 2025 22:26:21 -0500 Subject: [PATCH 06/17] OSC: fix bug with buffer disappearing --- src/termio/stream_handler.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index fd450f229..51dec5347 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -1367,7 +1367,7 @@ pub const StreamHandler = struct { } if (report) { try writer.writeAll(terminator.string()); - const msg: termio.Message = .{ .write_stable = response.items }; + const msg = try termio.Message.writeReq(self.alloc, response.items); self.messageWriter(msg); } } From 397a8b13e06b74ba5a9a73f4bcbecf769ba3f485 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Fri, 23 May 2025 22:57:18 -0500 Subject: [PATCH 07/17] OSC: more tests --- src/terminal/osc.zig | 205 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 204 insertions(+), 1 deletion(-) diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 0f5ecf724..a8906b74f 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -2295,7 +2295,7 @@ test "OSC: OSC4: set palette color 2" { try testing.expect(it.next() == null); } -test "OSC: OSC4: get with invalid index" { +test "OSC: OSC4: get with invalid index 1" { const testing = std.testing; var arena = std.heap.ArenaAllocator.init(std.testing.allocator); @@ -2322,6 +2322,209 @@ test "OSC: OSC4: get with invalid index" { try testing.expect(it.next() == null); } +test "OSC: OSC4: get with invalid index 2" { + const testing = std.testing; + + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + var p: Parser = .{ .alloc = arena.allocator() }; + + const input = "4;5;?;1111;?;1;?"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?; + try testing.expect(cmd == .color_operation); + try testing.expect(cmd.color_operation.source == .osc_4); + try testing.expect(cmd.color_operation.operations.count == 2); + var it = cmd.color_operation.operations.iterator(); + { + const op = it.next().?; + try testing.expect(op == .report); + try testing.expectEqual( + Command.ColorKind{ .palette = 5 }, + op.report, + ); + } + { + const op = it.next().?; + try testing.expect(op == .report); + try testing.expectEqual( + Command.ColorKind{ .palette = 1 }, + op.report, + ); + } + try testing.expect(it.next() == null); +} + +// Inspired by Microsoft Edit +test "OSC: OSC4: multiple get 8a" { + const testing = std.testing; + + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + var p: Parser = .{ .alloc = arena.allocator() }; + + const input = "4;0;?;1;?;2;?;3;?;4;?;5;?;6;?;7;?"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?; + try testing.expect(cmd == .color_operation); + try testing.expect(cmd.color_operation.source == .osc_4); + try testing.expect(cmd.color_operation.operations.count == 8); + var it = cmd.color_operation.operations.iterator(); + { + const op = it.next().?; + try testing.expect(op == .report); + try testing.expectEqual( + Command.ColorKind{ .palette = 0 }, + op.report, + ); + } + { + const op = it.next().?; + try testing.expect(op == .report); + try testing.expectEqual( + Command.ColorKind{ .palette = 1 }, + op.report, + ); + } + { + const op = it.next().?; + try testing.expect(op == .report); + try testing.expectEqual( + Command.ColorKind{ .palette = 2 }, + op.report, + ); + } + { + const op = it.next().?; + try testing.expect(op == .report); + try testing.expectEqual( + Command.ColorKind{ .palette = 3 }, + op.report, + ); + } + { + const op = it.next().?; + try testing.expect(op == .report); + try testing.expectEqual( + Command.ColorKind{ .palette = 4 }, + op.report, + ); + } + { + const op = it.next().?; + try testing.expect(op == .report); + try testing.expectEqual( + Command.ColorKind{ .palette = 5 }, + op.report, + ); + } + { + const op = it.next().?; + try testing.expect(op == .report); + try testing.expectEqual( + Command.ColorKind{ .palette = 6 }, + op.report, + ); + } + { + const op = it.next().?; + try testing.expect(op == .report); + try testing.expectEqual( + Command.ColorKind{ .palette = 7 }, + op.report, + ); + } + try testing.expect(it.next() == null); +} + +// Inspired by Microsoft Edit +test "OSC: OSC4: multiple get 8b" { + const testing = std.testing; + + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + var p: Parser = .{ .alloc = arena.allocator() }; + + const input = "4;8;?;9;?;10;?;11;?;12;?;13;?;14;?;15;?"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?; + try testing.expect(cmd == .color_operation); + try testing.expect(cmd.color_operation.source == .osc_4); + try testing.expect(cmd.color_operation.operations.count == 8); + var it = cmd.color_operation.operations.iterator(); + { + const op = it.next().?; + try testing.expect(op == .report); + try testing.expectEqual( + Command.ColorKind{ .palette = 8 }, + op.report, + ); + } + { + const op = it.next().?; + try testing.expect(op == .report); + try testing.expectEqual( + Command.ColorKind{ .palette = 9 }, + op.report, + ); + } + { + const op = it.next().?; + try testing.expect(op == .report); + try testing.expectEqual( + Command.ColorKind{ .palette = 10 }, + op.report, + ); + } + { + const op = it.next().?; + try testing.expect(op == .report); + try testing.expectEqual( + Command.ColorKind{ .palette = 11 }, + op.report, + ); + } + { + const op = it.next().?; + try testing.expect(op == .report); + try testing.expectEqual( + Command.ColorKind{ .palette = 12 }, + op.report, + ); + } + { + const op = it.next().?; + try testing.expect(op == .report); + try testing.expectEqual( + Command.ColorKind{ .palette = 13 }, + op.report, + ); + } + { + const op = it.next().?; + try testing.expect(op == .report); + try testing.expectEqual( + Command.ColorKind{ .palette = 14 }, + op.report, + ); + } + { + const op = it.next().?; + try testing.expect(op == .report); + try testing.expectEqual( + Command.ColorKind{ .palette = 15 }, + op.report, + ); + } + try testing.expect(it.next() == null); +} + test "OSC: OSC4: set with invalid index" { const testing = std.testing; From 479fa9f809b079e638941afe8394564e2bffbd8f Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sat, 24 May 2025 08:04:33 -0500 Subject: [PATCH 08/17] OSC: use std.SegmentedList instead of custom data structure --- src/datastruct/list.zig | 100 ----------- src/terminal/Parser.zig | 6 +- src/terminal/osc.zig | 308 ++++++++++++++++------------------ src/termio/stream_handler.zig | 4 +- 4 files changed, 154 insertions(+), 264 deletions(-) delete mode 100644 src/datastruct/list.zig diff --git a/src/datastruct/list.zig b/src/datastruct/list.zig deleted file mode 100644 index 7e9b78761..000000000 --- a/src/datastruct/list.zig +++ /dev/null @@ -1,100 +0,0 @@ -const std = @import("std"); - -const assert = std.debug.assert; - -/// Datastructure to manage a (usually) small list of items. To prevent -/// allocations on the heap, statically allocate a small array that gets used to -/// store items. Once that small array is full then memory will be dynamically -/// allocated on the heap to store items. -pub fn ArrayListStaticUnmanaged(comptime static_size: usize, comptime T: type) type { - return struct { - count: usize, - static: [static_size]T, - dynamic: std.ArrayListUnmanaged(T), - - const Self = @This(); - - pub const empty: Self = .{ - .count = 0, - .static = undefined, - .dynamic = .empty, - }; - - pub fn deinit(self: *Self, alloc: std.mem.Allocator) void { - self.dynamic.deinit(alloc); - } - - pub fn append(self: *Self, alloc: std.mem.Allocator, item: T) !void { - if (self.count < static_size) { - self.static[self.count] = item; - self.count += 1; - assert(self.count <= static_size); - return; - } - try self.dynamic.append(alloc, item); - self.count += 1; - assert(self.count == static_size + self.dynamic.items.len); - } - - pub const Iterator = struct { - context: *const Self, - index: usize, - - pub fn next(self: *Iterator) ?T { - if (self.index >= self.context.count) return null; - - if (self.index < static_size) { - defer self.index += 1; - return self.context.static[self.index]; - } - - assert(self.index - static_size < self.context.dynamic.items.len); - - defer self.index += 1; - return self.context.dynamic.items[self.index - static_size]; - } - }; - - pub fn iterator(self: *const Self) Iterator { - return .{ - .context = self, - .index = 0, - }; - } - }; -} - -test "ArrayListStaticUnmanged: 1" { - const alloc = std.testing.allocator; - - var l: ArrayListStaticUnmanaged(1, usize) = .empty; - defer l.deinit(alloc); - - try l.append(alloc, 1); - - try std.testing.expectEqual(1, l.count); - try std.testing.expectEqual(1, l.static[0]); - try std.testing.expectEqual(0, l.dynamic.items.len); - - var it = l.iterator(); - try std.testing.expectEqual(1, it.next().?); - try std.testing.expectEqual(null, it.next()); -} - -test "ArrayListStaticUnmanged: 2" { - const alloc = std.testing.allocator; - - var l: ArrayListStaticUnmanaged(1, usize) = .empty; - defer l.deinit(alloc); - - try l.append(alloc, 1); - try l.append(alloc, 2); - - try std.testing.expectEqual(2, l.count); - try std.testing.expectEqual(1, l.static[0]); - try std.testing.expectEqual(1, l.dynamic.items.len); - var it = l.iterator(); - try std.testing.expectEqual(1, it.next().?); - try std.testing.expectEqual(2, it.next().?); - try std.testing.expectEqual(null, it.next()); -} diff --git a/src/terminal/Parser.zig b/src/terminal/Parser.zig index 0f035f7fb..df18fbc7a 100644 --- a/src/terminal/Parser.zig +++ b/src/terminal/Parser.zig @@ -900,11 +900,11 @@ test "osc: 112 incomplete sequence" { try testing.expect(cmd == .color_operation); try testing.expectEqual(cmd.color_operation.terminator, .bel); try testing.expect(cmd.color_operation.source == .osc_112); - try testing.expect(cmd.color_operation.operations.count == 1); - var it = cmd.color_operation.operations.iterator(); + 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.expect(op.* == .reset); try testing.expectEqual( osc.Command.ColorKind.cursor, op.reset, diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index a8906b74f..449713ff2 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -13,8 +13,6 @@ const Allocator = mem.Allocator; const RGB = @import("color.zig").RGB; const kitty = @import("kitty.zig"); -const ArrayListStaticUnmanaged = @import("../datastruct/list.zig").ArrayListStaticUnmanaged; - const log = std.log.scoped(.osc); pub const Command = union(enum) { @@ -111,10 +109,18 @@ pub const Command = union(enum) { value: []const u8, }, - /// OSC color operations + /// OSC color operations to set, reset, or report color settings. Some OSCs + /// allow multiple operations to be specified in a single OSC so we need a + /// list-like datastructure to manage them. We use std.SegmentedList because + /// it minimizes the number of allocations and copies because a large + /// majority of the time there will be only one operation per OSC. + /// + /// Currently, these OSCs are handled by `color_operation`: + /// + /// 4, 10, 11, 12, 104, 110, 111, 112 color_operation: struct { source: ColorOperationSource, - operations: ColorOperationList = .empty, + operations: ColorOperationList = .{}, terminator: Terminator = .st, }, @@ -189,7 +195,7 @@ pub const Command = union(enum) { report: ColorKind, }; - pub const ColorOperationList = ArrayListStaticUnmanaged(4, ColorOperation); + pub const ColorOperationList = std.SegmentedList(ColorOperation, 4); pub const ColorKind = union(enum) { palette: u8, @@ -1329,7 +1335,6 @@ pub const Parser = struct { self.command = .{ .color_operation = .{ .source = .osc_4, - .operations = .empty, }, }; @@ -1346,34 +1351,30 @@ pub const Parser = struct { }; const spec_str = it.next() orelse continue; if (std.mem.eql(u8, spec_str, "?")) { - self.command.color_operation.operations.append( - alloc, - .{ - .report = .{ .palette = index }, - }, - ) catch |err| { + const op = self.command.color_operation.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 }); continue; }; - self.command.color_operation.operations.append( - alloc, - .{ - .set = .{ - .kind = .{ - .palette = index, - }, - .color = color, - }, - }, - ) catch |err| { + const op = self.command.color_operation.operations.addOne(alloc) catch |err| { log.warn("unable to append color operation: {}", .{err}); return; }; + op.* = .{ + .set = .{ + .kind = .{ + .palette = index, + }, + .color = color, + }, + }; } } } @@ -1394,7 +1395,6 @@ pub const Parser = struct { .osc_12 => .osc_12, else => unreachable, }, - .operations = .empty, }, }; const str = self.buf[self.buf_start..self.buf_idx]; @@ -1405,42 +1405,38 @@ pub const Parser = struct { return; }; if (std.mem.eql(u8, color_str, "?")) { - self.command.color_operation.operations.append( - alloc, - .{ - .report = switch (self.state) { - .osc_10 => .foreground, - .osc_11 => .background, - .osc_12 => .cursor, - else => unreachable, - }, - }, - ) catch |err| { + const op = self.command.color_operation.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(color_str) catch |err| { log.warn("invalid color specification in OSC 10/11/12: {s} {}", .{ color_str, err }); return; }; - self.command.color_operation.operations.append( - alloc, - .{ - .set = .{ - .kind = switch (self.state) { - .osc_10 => .foreground, - .osc_11 => .background, - .osc_12 => .cursor, - else => unreachable, - }, - .color = color, - }, - }, - ) catch |err| { + const op = self.command.color_operation.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, + }, + }; } } @@ -1460,23 +1456,20 @@ pub const Parser = struct { .osc_112 => .osc_112, else => unreachable, }, - .operations = .empty, }, }; - self.command.color_operation.operations.append( - alloc, - .{ - .reset = switch (self.state) { - .osc_110 => .foreground, - .osc_111 => .background, - .osc_112 => .cursor, - else => unreachable, - }, - }, - ) catch |err| { + const op = self.command.color_operation.operations.addOne(alloc) catch |err| { log.warn("unable to append color operation: {}", .{err}); return; }; + op.* = .{ + .reset = switch (self.state) { + .osc_110 => .foreground, + .osc_111 => .background, + .osc_112 => .cursor, + else => unreachable, + }, + }; } fn parseOSC104(self: *Parser) void { @@ -1487,7 +1480,6 @@ pub const Parser = struct { self.command = .{ .color_operation = .{ .source = .osc_104, - .operations = .empty, }, }; @@ -1500,15 +1492,13 @@ pub const Parser = struct { continue; }, }; - self.command.color_operation.operations.append( - alloc, - .{ - .reset = .{ .palette = index }, - }, - ) catch |err| { + const op = self.command.color_operation.operations.addOne(alloc) catch |err| { log.warn("unable to append color operation: {}", .{err}); return; }; + op.* = .{ + .reset = .{ .palette = index }, + }; } } @@ -1776,11 +1766,11 @@ test "OSC: OSC110: reset cursor color" { try testing.expect(cmd == .color_operation); try testing.expectEqual(cmd.color_operation.terminator, .st); try testing.expect(cmd.color_operation.source == .osc_110); - try testing.expect(cmd.color_operation.operations.count == 1); - var it = cmd.color_operation.operations.iterator(); + 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.expect(op.* == .reset); try testing.expectEqual( Command.ColorKind.foreground, op.reset, @@ -1804,11 +1794,11 @@ test "OSC: OSC111: reset cursor color" { try testing.expect(cmd == .color_operation); try testing.expectEqual(cmd.color_operation.terminator, .st); try testing.expect(cmd.color_operation.source == .osc_111); - try testing.expect(cmd.color_operation.operations.count == 1); - var it = cmd.color_operation.operations.iterator(); + 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.expect(op.* == .reset); try testing.expectEqual( Command.ColorKind.background, op.reset, @@ -1832,11 +1822,11 @@ test "OSC: OSC112: reset cursor color" { try testing.expect(cmd == .color_operation); try testing.expectEqual(cmd.color_operation.terminator, .st); try testing.expect(cmd.color_operation.source == .osc_112); - try testing.expect(cmd.color_operation.operations.count == 1); - var it = cmd.color_operation.operations.iterator(); + 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.expect(op.* == .reset); try testing.expectEqual( Command.ColorKind.cursor, op.reset, @@ -1864,11 +1854,11 @@ test "OSC: OSC112: reset cursor color with semicolon" { try testing.expect(cmd == .color_operation); try testing.expectEqual(cmd.color_operation.terminator, .bel); try testing.expect(cmd.color_operation.source == .osc_112); - try testing.expect(cmd.color_operation.operations.count == 1); - var it = cmd.color_operation.operations.iterator(); + 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.expect(op.* == .reset); try testing.expectEqual( Command.ColorKind.cursor, op.reset, @@ -1988,11 +1978,11 @@ test "OSC: OSC10: report default foreground color" { try testing.expect(cmd == .color_operation); try testing.expectEqual(cmd.color_operation.terminator, .st); try testing.expect(cmd.color_operation.source == .osc_10); - try testing.expect(cmd.color_operation.operations.count == 1); - var it = cmd.color_operation.operations.iterator(); + 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 == .report); + try testing.expect(op.* == .report); try testing.expectEqual( Command.ColorKind.foreground, op.report, @@ -2016,11 +2006,11 @@ test "OSC: OSC10: set foreground color" { try testing.expect(cmd == .color_operation); try testing.expectEqual(cmd.color_operation.terminator, .bel); try testing.expect(cmd.color_operation.source == .osc_10); - try testing.expect(cmd.color_operation.operations.count == 1); - var it = cmd.color_operation.operations.iterator(); + 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 == .set); + try testing.expect(op.* == .set); try testing.expectEqual( Command.ColorKind.foreground, op.set.kind, @@ -2049,11 +2039,11 @@ test "OSC: OSC11: report default background color" { try testing.expect(cmd == .color_operation); try testing.expectEqual(cmd.color_operation.terminator, .bel); try testing.expect(cmd.color_operation.source == .osc_11); - try testing.expect(cmd.color_operation.operations.count == 1); - var it = cmd.color_operation.operations.iterator(); + 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 == .report); + try testing.expect(op.* == .report); try testing.expectEqual( Command.ColorKind.background, op.report, @@ -2078,11 +2068,11 @@ test "OSC: OSC11: set background color" { try testing.expect(cmd == .color_operation); try testing.expectEqual(cmd.color_operation.terminator, .st); try testing.expect(cmd.color_operation.source == .osc_11); - try testing.expect(cmd.color_operation.operations.count == 1); - var it = cmd.color_operation.operations.iterator(); + 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 == .set); + try testing.expect(op.* == .set); try testing.expectEqual( Command.ColorKind.background, op.set.kind, @@ -2111,11 +2101,11 @@ test "OSC: OSC12: report background color" { try testing.expect(cmd == .color_operation); try testing.expectEqual(cmd.color_operation.terminator, .bel); try testing.expect(cmd.color_operation.source == .osc_12); - try testing.expect(cmd.color_operation.operations.count == 1); - var it = cmd.color_operation.operations.iterator(); + 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 == .report); + try testing.expect(op.* == .report); try testing.expectEqual( Command.ColorKind.cursor, op.report, @@ -2140,11 +2130,11 @@ test "OSC: OSC12: set background color" { try testing.expect(cmd == .color_operation); try testing.expectEqual(cmd.color_operation.terminator, .st); try testing.expect(cmd.color_operation.source == .osc_12); - try testing.expect(cmd.color_operation.operations.count == 1); - var it = cmd.color_operation.operations.iterator(); + 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 == .set); + try testing.expect(op.* == .set); try testing.expectEqual( Command.ColorKind.cursor, op.set.kind, @@ -2171,11 +2161,11 @@ test "OSC: OSC4: get palette color 1" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); try testing.expect(cmd.color_operation.source == .osc_4); - try testing.expect(cmd.color_operation.operations.count == 1); - var it = cmd.color_operation.operations.iterator(); + 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 == .report); + try testing.expect(op.* == .report); try testing.expectEqual( Command.ColorKind{ .palette = 1 }, op.report, @@ -2199,11 +2189,11 @@ test "OSC: OSC4: get palette color 2" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); try testing.expect(cmd.color_operation.source == .osc_4); - try testing.expect(cmd.color_operation.operations.count == 2); - var it = cmd.color_operation.operations.iterator(); + try testing.expect(cmd.color_operation.operations.count() == 2); + var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; - try testing.expect(op == .report); + try testing.expect(op.* == .report); try testing.expectEqual( Command.ColorKind{ .palette = 1 }, op.report, @@ -2211,7 +2201,7 @@ test "OSC: OSC4: get palette color 2" { } { const op = it.next().?; - try testing.expect(op == .report); + try testing.expect(op.* == .report); try testing.expectEqual( Command.ColorKind{ .palette = 2 }, op.report, @@ -2235,11 +2225,11 @@ test "OSC: OSC4: set palette color 1" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); try testing.expect(cmd.color_operation.source == .osc_4); - try testing.expect(cmd.color_operation.operations.count == 1); - var it = cmd.color_operation.operations.iterator(); + 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 == .set); + try testing.expect(op.* == .set); try testing.expectEqual( Command.ColorKind{ .palette = 17 }, op.set.kind, @@ -2266,11 +2256,11 @@ test "OSC: OSC4: set palette color 2" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); try testing.expect(cmd.color_operation.source == .osc_4); - try testing.expect(cmd.color_operation.operations.count == 2); - var it = cmd.color_operation.operations.iterator(); + try testing.expect(cmd.color_operation.operations.count() == 2); + var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; - try testing.expect(op == .set); + try testing.expect(op.* == .set); try testing.expectEqual( Command.ColorKind{ .palette = 17 }, op.set.kind, @@ -2282,7 +2272,7 @@ test "OSC: OSC4: set palette color 2" { } { const op = it.next().?; - try testing.expect(op == .set); + try testing.expect(op.* == .set); try testing.expectEqual( Command.ColorKind{ .palette = 1 }, op.set.kind, @@ -2309,11 +2299,11 @@ test "OSC: OSC4: get with invalid index 1" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); try testing.expect(cmd.color_operation.source == .osc_4); - try testing.expect(cmd.color_operation.operations.count == 1); - var it = cmd.color_operation.operations.iterator(); + 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 == .report); + try testing.expect(op.* == .report); try testing.expectEqual( Command.ColorKind{ .palette = 1 }, op.report, @@ -2336,11 +2326,11 @@ test "OSC: OSC4: get with invalid index 2" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); try testing.expect(cmd.color_operation.source == .osc_4); - try testing.expect(cmd.color_operation.operations.count == 2); - var it = cmd.color_operation.operations.iterator(); + try testing.expect(cmd.color_operation.operations.count() == 2); + var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; - try testing.expect(op == .report); + try testing.expect(op.* == .report); try testing.expectEqual( Command.ColorKind{ .palette = 5 }, op.report, @@ -2348,7 +2338,7 @@ test "OSC: OSC4: get with invalid index 2" { } { const op = it.next().?; - try testing.expect(op == .report); + try testing.expect(op.* == .report); try testing.expectEqual( Command.ColorKind{ .palette = 1 }, op.report, @@ -2372,11 +2362,11 @@ test "OSC: OSC4: multiple get 8a" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); try testing.expect(cmd.color_operation.source == .osc_4); - try testing.expect(cmd.color_operation.operations.count == 8); - var it = cmd.color_operation.operations.iterator(); + try testing.expect(cmd.color_operation.operations.count() == 8); + var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; - try testing.expect(op == .report); + try testing.expect(op.* == .report); try testing.expectEqual( Command.ColorKind{ .palette = 0 }, op.report, @@ -2384,7 +2374,7 @@ test "OSC: OSC4: multiple get 8a" { } { const op = it.next().?; - try testing.expect(op == .report); + try testing.expect(op.* == .report); try testing.expectEqual( Command.ColorKind{ .palette = 1 }, op.report, @@ -2392,7 +2382,7 @@ test "OSC: OSC4: multiple get 8a" { } { const op = it.next().?; - try testing.expect(op == .report); + try testing.expect(op.* == .report); try testing.expectEqual( Command.ColorKind{ .palette = 2 }, op.report, @@ -2400,7 +2390,7 @@ test "OSC: OSC4: multiple get 8a" { } { const op = it.next().?; - try testing.expect(op == .report); + try testing.expect(op.* == .report); try testing.expectEqual( Command.ColorKind{ .palette = 3 }, op.report, @@ -2408,7 +2398,7 @@ test "OSC: OSC4: multiple get 8a" { } { const op = it.next().?; - try testing.expect(op == .report); + try testing.expect(op.* == .report); try testing.expectEqual( Command.ColorKind{ .palette = 4 }, op.report, @@ -2416,7 +2406,7 @@ test "OSC: OSC4: multiple get 8a" { } { const op = it.next().?; - try testing.expect(op == .report); + try testing.expect(op.* == .report); try testing.expectEqual( Command.ColorKind{ .palette = 5 }, op.report, @@ -2424,7 +2414,7 @@ test "OSC: OSC4: multiple get 8a" { } { const op = it.next().?; - try testing.expect(op == .report); + try testing.expect(op.* == .report); try testing.expectEqual( Command.ColorKind{ .palette = 6 }, op.report, @@ -2432,7 +2422,7 @@ test "OSC: OSC4: multiple get 8a" { } { const op = it.next().?; - try testing.expect(op == .report); + try testing.expect(op.* == .report); try testing.expectEqual( Command.ColorKind{ .palette = 7 }, op.report, @@ -2456,11 +2446,11 @@ test "OSC: OSC4: multiple get 8b" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); try testing.expect(cmd.color_operation.source == .osc_4); - try testing.expect(cmd.color_operation.operations.count == 8); - var it = cmd.color_operation.operations.iterator(); + try testing.expect(cmd.color_operation.operations.count() == 8); + var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; - try testing.expect(op == .report); + try testing.expect(op.* == .report); try testing.expectEqual( Command.ColorKind{ .palette = 8 }, op.report, @@ -2468,7 +2458,7 @@ test "OSC: OSC4: multiple get 8b" { } { const op = it.next().?; - try testing.expect(op == .report); + try testing.expect(op.* == .report); try testing.expectEqual( Command.ColorKind{ .palette = 9 }, op.report, @@ -2476,7 +2466,7 @@ test "OSC: OSC4: multiple get 8b" { } { const op = it.next().?; - try testing.expect(op == .report); + try testing.expect(op.* == .report); try testing.expectEqual( Command.ColorKind{ .palette = 10 }, op.report, @@ -2484,7 +2474,7 @@ test "OSC: OSC4: multiple get 8b" { } { const op = it.next().?; - try testing.expect(op == .report); + try testing.expect(op.* == .report); try testing.expectEqual( Command.ColorKind{ .palette = 11 }, op.report, @@ -2492,7 +2482,7 @@ test "OSC: OSC4: multiple get 8b" { } { const op = it.next().?; - try testing.expect(op == .report); + try testing.expect(op.* == .report); try testing.expectEqual( Command.ColorKind{ .palette = 12 }, op.report, @@ -2500,7 +2490,7 @@ test "OSC: OSC4: multiple get 8b" { } { const op = it.next().?; - try testing.expect(op == .report); + try testing.expect(op.* == .report); try testing.expectEqual( Command.ColorKind{ .palette = 13 }, op.report, @@ -2508,7 +2498,7 @@ test "OSC: OSC4: multiple get 8b" { } { const op = it.next().?; - try testing.expect(op == .report); + try testing.expect(op.* == .report); try testing.expectEqual( Command.ColorKind{ .palette = 14 }, op.report, @@ -2516,7 +2506,7 @@ test "OSC: OSC4: multiple get 8b" { } { const op = it.next().?; - try testing.expect(op == .report); + try testing.expect(op.* == .report); try testing.expectEqual( Command.ColorKind{ .palette = 15 }, op.report, @@ -2539,11 +2529,11 @@ test "OSC: OSC4: set with invalid index" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); try testing.expect(cmd.color_operation.source == .osc_4); - try testing.expect(cmd.color_operation.operations.count == 1); - var it = cmd.color_operation.operations.iterator(); + 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 == .set); + try testing.expect(op.* == .set); try testing.expectEqual( Command.ColorKind{ .palette = 1 }, op.set.kind, @@ -2570,11 +2560,11 @@ test "OSC: OSC4: mix get/set palette color" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); try testing.expect(cmd.color_operation.source == .osc_4); - try testing.expect(cmd.color_operation.operations.count == 2); - var it = cmd.color_operation.operations.iterator(); + try testing.expect(cmd.color_operation.operations.count() == 2); + var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; - try testing.expect(op == .set); + try testing.expect(op.* == .set); try testing.expectEqual( Command.ColorKind{ .palette = 17 }, op.set.kind, @@ -2586,7 +2576,7 @@ test "OSC: OSC4: mix get/set palette color" { } { const op = it.next().?; - try testing.expect(op == .report); + try testing.expect(op.* == .report); try testing.expectEqual(Command.ColorKind{ .palette = 254 }, op.report); } try testing.expect(it.next() == null); @@ -2606,11 +2596,11 @@ test "OSC: OSC104: reset palette color 1" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); try testing.expect(cmd.color_operation.source == .osc_104); - try testing.expect(cmd.color_operation.operations.count == 1); - var it = cmd.color_operation.operations.iterator(); + 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.expect(op.* == .reset); try testing.expectEqual( Command.ColorKind{ .palette = 17 }, op.reset, @@ -2633,11 +2623,11 @@ test "OSC: OSC104: reset palette color 2" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); try testing.expect(cmd.color_operation.source == .osc_104); - try testing.expect(cmd.color_operation.operations.count == 2); - var it = cmd.color_operation.operations.iterator(); + try testing.expect(cmd.color_operation.operations.count() == 2); + var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; - try testing.expect(op == .reset); + try testing.expect(op.* == .reset); try testing.expectEqual( Command.ColorKind{ .palette = 17 }, op.reset, @@ -2645,7 +2635,7 @@ test "OSC: OSC104: reset palette color 2" { } { const op = it.next().?; - try testing.expect(op == .reset); + try testing.expect(op.* == .reset); try testing.expectEqual( Command.ColorKind{ .palette = 111 }, op.reset, @@ -2668,11 +2658,11 @@ test "OSC: OSC104: invalid palette index" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); try testing.expect(cmd.color_operation.source == .osc_104); - try testing.expect(cmd.color_operation.operations.count == 1); - var it = cmd.color_operation.operations.iterator(); + 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.expect(op.* == .reset); try testing.expectEqual( Command.ColorKind{ .palette = 111 }, op.reset, @@ -2695,11 +2685,11 @@ test "OSC: OSC104: empty palette index" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); try testing.expect(cmd.color_operation.source == .osc_104); - try testing.expect(cmd.color_operation.operations.count == 1); - var it = cmd.color_operation.operations.iterator(); + 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.expect(op.* == .reset); try testing.expectEqual( Command.ColorKind{ .palette = 111 }, op.reset, diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 51dec5347..5977f6564 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -1212,10 +1212,10 @@ pub const StreamHandler = struct { try writer.print("\x1b]{}", .{source}); - var it = operations.iterator(); + var it = operations.constIterator(0); while (it.next()) |op| { - switch (op) { + switch (op.*) { .set => |set| { switch (set.kind) { .palette => |i| { From bd4d1950ce12855479424a7d7713bc65383e6d32 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sat, 24 May 2025 08:17:31 -0500 Subject: [PATCH 09/17] OSC: remove unused code --- src/terminal/osc.zig | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 449713ff2..0c429c70e 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -202,15 +202,6 @@ pub const Command = union(enum) { foreground, background, cursor, - - pub fn code(self: ColorKind) []const u8 { - return switch (self) { - .palette => "4", - .foreground => "10", - .background => "11", - .cursor => "12", - }; - } }; pub const ProgressState = enum { From f2dfd9f6779f0cabb61f2c4ef3a70216ab9d42de Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sat, 24 May 2025 08:18:09 -0500 Subject: [PATCH 10/17] OSC: improve formatting of ColorOperationSource --- src/terminal/osc.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 0c429c70e..78a3560af 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -179,10 +179,10 @@ pub const Command = union(enum) { pub fn format( self: ColorOperationSource, comptime _: []const u8, - _: std.fmt.FormatOptions, + options: std.fmt.FormatOptions, writer: anytype, ) !void { - try writer.print("{d}", .{@intFromEnum(self)}); + try std.fmt.formatInt(@intFromEnum(self), 10, .lower, options, writer); } }; From e0ddc7a2fa66b5e990fc1bfa6ddc0e3ef930bb40 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sat, 24 May 2025 08:32:10 -0500 Subject: [PATCH 11/17] OSC: clean up color_operation handling --- src/termio/stream_handler.zig | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 5977f6564..4bb0f9c9d 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -1201,6 +1201,9 @@ pub const StreamHandler = struct { operations: *const terminal.osc.Command.ColorOperationList, terminator: terminal.osc.Terminator, ) !void { + // return early if there is nothing to do + if (operations.count() == 0) return; + var buffer: [1024]u8 = undefined; var fba: std.heap.FixedBufferAllocator = .init(&buffer); const alloc = fba.allocator(); @@ -1366,6 +1369,8 @@ pub const StreamHandler = struct { } } if (report) { + // If any of the operations were reports, finialize the report + // string and send it to the terminal. try writer.writeAll(terminator.string()); const msg = try termio.Message.writeReq(self.alloc, response.items); self.messageWriter(msg); From 35384670c4c80f532966955c2e4ad78dc41e2a48 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sat, 24 May 2025 08:59:37 -0500 Subject: [PATCH 12/17] OSC: fix typo --- src/termio/stream_handler.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 4bb0f9c9d..ca16b0bd2 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -1369,7 +1369,7 @@ pub const StreamHandler = struct { } } if (report) { - // If any of the operations were reports, finialize the report + // If any of the operations were reports, finalize the report // string and send it to the terminal. try writer.writeAll(terminator.string()); const msg = try termio.Message.writeReq(self.alloc, response.items); From fa03115f01abf63e45e37316479e291150fc6787 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sat, 24 May 2025 10:52:36 -0500 Subject: [PATCH 13/17] OSC: don't use arena during testing --- src/terminal/Parser.zig | 6 +- src/terminal/osc.zig | 147 ++++++++++++++-------------------------- 2 files changed, 51 insertions(+), 102 deletions(-) diff --git a/src/terminal/Parser.zig b/src/terminal/Parser.zig index df18fbc7a..8cf2996d6 100644 --- a/src/terminal/Parser.zig +++ b/src/terminal/Parser.zig @@ -877,11 +877,9 @@ test "osc: change window title (end in esc)" { // https://github.com/darrenstarr/VtNetCore/pull/14 // Saw this on HN, decided to add a test case because why not. test "osc: 112 incomplete sequence" { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - var p: Parser = init(); - p.osc_parser.alloc = arena.allocator(); + defer p.deinit(); + p.osc_parser.alloc = std.testing.allocator; _ = p.next(0x1B); _ = p.next(']'); diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 78a3560af..7e5a71536 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -1745,10 +1745,8 @@ test "OSC: end_of_input" { test "OSC: OSC110: reset cursor color" { const testing = std.testing; - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - var p: Parser = .{ .alloc = arena.allocator() }; + var p: Parser = .{ .alloc = testing.allocator }; + defer p.deinit(); const input = "110"; for (input) |ch| p.next(ch); @@ -1773,10 +1771,8 @@ test "OSC: OSC110: reset cursor color" { test "OSC: OSC111: reset cursor color" { const testing = std.testing; - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - var p: Parser = .{ .alloc = arena.allocator() }; + var p: Parser = .{ .alloc = testing.allocator }; + defer p.deinit(); const input = "111"; for (input) |ch| p.next(ch); @@ -1801,10 +1797,8 @@ test "OSC: OSC111: reset cursor color" { test "OSC: OSC112: reset cursor color" { const testing = std.testing; - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - var p: Parser = .{ .alloc = arena.allocator() }; + var p: Parser = .{ .alloc = testing.allocator }; + defer p.deinit(); const input = "112"; for (input) |ch| p.next(ch); @@ -1829,10 +1823,8 @@ test "OSC: OSC112: reset cursor color" { test "OSC: OSC112: reset cursor color with semicolon" { const testing = std.testing; - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - var p: Parser = .{ .alloc = arena.allocator() }; + var p: Parser = .{ .alloc = testing.allocator }; + defer p.deinit(); const input = "112;"; for (input) |ch| { @@ -1888,9 +1880,8 @@ test "OSC: get/set clipboard (optional parameter)" { test "OSC: get/set clipboard with allocator" { const testing = std.testing; - const alloc = testing.allocator; - var p: Parser = .{ .alloc = alloc }; + var p: Parser = .{ .alloc = testing.allocator }; defer p.deinit(); const input = "52;s;?"; @@ -1955,10 +1946,8 @@ test "OSC: longer than buffer" { test "OSC: OSC10: report default foreground color" { const testing = std.testing; - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - var p: Parser = .{ .alloc = arena.allocator() }; + var p: Parser = .{ .alloc = testing.allocator }; + defer p.deinit(); const input = "10;?"; for (input) |ch| p.next(ch); @@ -1985,10 +1974,8 @@ test "OSC: OSC10: report default foreground color" { test "OSC: OSC10: set foreground color" { const testing = std.testing; - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - var p: Parser = .{ .alloc = arena.allocator() }; + var p: Parser = .{ .alloc = testing.allocator }; + defer p.deinit(); const input = "10;rgbi:0.0/0.5/1.0"; for (input) |ch| p.next(ch); @@ -2017,10 +2004,8 @@ test "OSC: OSC10: set foreground color" { test "OSC: OSC11: report default background color" { const testing = std.testing; - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - var p: Parser = .{ .alloc = arena.allocator() }; + var p: Parser = .{ .alloc = testing.allocator }; + defer p.deinit(); const input = "11;?"; for (input) |ch| p.next(ch); @@ -2047,10 +2032,8 @@ test "OSC: OSC11: report default background color" { test "OSC: OSC11: set background color" { const testing = std.testing; - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - var p: Parser = .{ .alloc = arena.allocator() }; + var p: Parser = .{ .alloc = testing.allocator }; + defer p.deinit(); const input = "11;rgb:f/ff/ffff"; for (input) |ch| p.next(ch); @@ -2079,10 +2062,8 @@ test "OSC: OSC11: set background color" { test "OSC: OSC12: report background color" { const testing = std.testing; - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - var p: Parser = .{ .alloc = arena.allocator() }; + var p: Parser = .{ .alloc = testing.allocator }; + defer p.deinit(); const input = "12;?"; for (input) |ch| p.next(ch); @@ -2109,10 +2090,8 @@ test "OSC: OSC12: report background color" { test "OSC: OSC12: set background color" { const testing = std.testing; - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - var p: Parser = .{ .alloc = arena.allocator() }; + var p: Parser = .{ .alloc = testing.allocator }; + defer p.deinit(); const input = "12;rgb:f/ff/ffff"; for (input) |ch| p.next(ch); @@ -2141,10 +2120,8 @@ test "OSC: OSC12: set background color" { test "OSC: OSC4: get palette color 1" { const testing = std.testing; - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - var p: Parser = .{ .alloc = arena.allocator() }; + var p: Parser = .{ .alloc = testing.allocator }; + defer p.deinit(); const input = "4;1;?"; for (input) |ch| p.next(ch); @@ -2169,10 +2146,8 @@ test "OSC: OSC4: get palette color 1" { test "OSC: OSC4: get palette color 2" { const testing = std.testing; - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - var p: Parser = .{ .alloc = arena.allocator() }; + var p: Parser = .{ .alloc = testing.allocator }; + defer p.deinit(); const input = "4;1;?;2;?"; for (input) |ch| p.next(ch); @@ -2205,10 +2180,8 @@ test "OSC: OSC4: get palette color 2" { test "OSC: OSC4: set palette color 1" { const testing = std.testing; - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - var p: Parser = .{ .alloc = arena.allocator() }; + var p: Parser = .{ .alloc = testing.allocator }; + defer p.deinit(); const input = "4;17;rgb:aa/bb/cc"; for (input) |ch| p.next(ch); @@ -2236,10 +2209,8 @@ test "OSC: OSC4: set palette color 1" { test "OSC: OSC4: set palette color 2" { const testing = std.testing; - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - var p: Parser = .{ .alloc = arena.allocator() }; + var p: Parser = .{ .alloc = testing.allocator }; + defer p.deinit(); const input = "4;17;rgb:aa/bb/cc;1;rgb:00/11/22"; for (input) |ch| p.next(ch); @@ -2279,10 +2250,8 @@ test "OSC: OSC4: set palette color 2" { test "OSC: OSC4: get with invalid index 1" { const testing = std.testing; - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - var p: Parser = .{ .alloc = arena.allocator() }; + var p: Parser = .{ .alloc = testing.allocator }; + defer p.deinit(); const input = "4;1111;?;1;?"; for (input) |ch| p.next(ch); @@ -2306,10 +2275,8 @@ test "OSC: OSC4: get with invalid index 1" { test "OSC: OSC4: get with invalid index 2" { const testing = std.testing; - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - var p: Parser = .{ .alloc = arena.allocator() }; + var p: Parser = .{ .alloc = testing.allocator }; + defer p.deinit(); const input = "4;5;?;1111;?;1;?"; for (input) |ch| p.next(ch); @@ -2342,10 +2309,8 @@ test "OSC: OSC4: get with invalid index 2" { test "OSC: OSC4: multiple get 8a" { const testing = std.testing; - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - var p: Parser = .{ .alloc = arena.allocator() }; + var p: Parser = .{ .alloc = testing.allocator }; + defer p.deinit(); const input = "4;0;?;1;?;2;?;3;?;4;?;5;?;6;?;7;?"; for (input) |ch| p.next(ch); @@ -2426,10 +2391,8 @@ test "OSC: OSC4: multiple get 8a" { test "OSC: OSC4: multiple get 8b" { const testing = std.testing; - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - var p: Parser = .{ .alloc = arena.allocator() }; + var p: Parser = .{ .alloc = testing.allocator }; + defer p.deinit(); const input = "4;8;?;9;?;10;?;11;?;12;?;13;?;14;?;15;?"; for (input) |ch| p.next(ch); @@ -2509,10 +2472,8 @@ test "OSC: OSC4: multiple get 8b" { test "OSC: OSC4: set with invalid index" { const testing = std.testing; - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - var p: Parser = .{ .alloc = arena.allocator() }; + var p: Parser = .{ .alloc = testing.allocator }; + defer p.deinit(); const input = "4;256;#ffffff;1;#aabbcc"; for (input) |ch| p.next(ch); @@ -2540,10 +2501,8 @@ test "OSC: OSC4: set with invalid index" { test "OSC: OSC4: mix get/set palette color" { const testing = std.testing; - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - var p: Parser = .{ .alloc = arena.allocator() }; + var p: Parser = .{ .alloc = testing.allocator }; + defer p.deinit(); const input = "4;17;rgb:aa/bb/cc;254;?"; for (input) |ch| p.next(ch); @@ -2576,10 +2535,8 @@ test "OSC: OSC4: mix get/set palette color" { test "OSC: OSC104: reset palette color 1" { const testing = std.testing; - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - var p: Parser = .{ .alloc = arena.allocator() }; + var p: Parser = .{ .alloc = testing.allocator }; + defer p.deinit(); const input = "104;17"; for (input) |ch| p.next(ch); @@ -2603,10 +2560,8 @@ test "OSC: OSC104: reset palette color 1" { test "OSC: OSC104: reset palette color 2" { const testing = std.testing; - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - var p: Parser = .{ .alloc = arena.allocator() }; + var p: Parser = .{ .alloc = testing.allocator }; + defer p.deinit(); const input = "104;17;111"; for (input) |ch| p.next(ch); @@ -2638,10 +2593,8 @@ test "OSC: OSC104: reset palette color 2" { test "OSC: OSC104: invalid palette index" { const testing = std.testing; - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - var p: Parser = .{ .alloc = arena.allocator() }; + var p: Parser = .{ .alloc = testing.allocator }; + defer p.deinit(); const input = "104;ffff;111"; for (input) |ch| p.next(ch); @@ -2665,10 +2618,8 @@ test "OSC: OSC104: invalid palette index" { test "OSC: OSC104: empty palette index" { const testing = std.testing; - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - var p: Parser = .{ .alloc = arena.allocator() }; + var p: Parser = .{ .alloc = testing.allocator }; + defer p.deinit(); const input = "104;;111"; for (input) |ch| p.next(ch); From bcf4d55dad47472e317130f4372fb3ddfa35b512 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sat, 24 May 2025 11:30:17 -0500 Subject: [PATCH 14/17] OSC: nest ColorOperation-related structs --- src/apprt/surface.zig | 2 +- src/terminal/Parser.zig | 4 +- src/terminal/osc.zig | 232 +++++++++++++++++----------------- src/termio/stream_handler.zig | 4 +- 4 files changed, 122 insertions(+), 120 deletions(-) diff --git a/src/apprt/surface.zig b/src/apprt/surface.zig index 6de41c544..dce6a3a56 100644 --- a/src/apprt/surface.zig +++ b/src/apprt/surface.zig @@ -74,7 +74,7 @@ pub const Message = union(enum) { /// A terminal color was changed using OSC sequences. color_change: struct { - kind: terminal.osc.Command.ColorKind, + kind: terminal.osc.Command.ColorOperation.Kind, color: terminal.color.RGB, }, diff --git a/src/terminal/Parser.zig b/src/terminal/Parser.zig index 8cf2996d6..ec3f322f6 100644 --- a/src/terminal/Parser.zig +++ b/src/terminal/Parser.zig @@ -897,14 +897,14 @@ 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 == .osc_112); + 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( - osc.Command.ColorKind.cursor, + osc.Command.ColorOperation.Kind.cursor, op.reset, ); } diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 7e5a71536..67f665f1a 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -119,8 +119,8 @@ pub const Command = union(enum) { /// /// 4, 10, 11, 12, 104, 110, 111, 112 color_operation: struct { - source: ColorOperationSource, - operations: ColorOperationList = .{}, + source: ColorOperation.Source, + operations: ColorOperation.List = .{}, terminator: Terminator = .st, }, @@ -166,42 +166,44 @@ pub const Command = union(enum) { /// Wait input (OSC 9;5) wait_input: void, - pub const ColorOperationSource = enum(u16) { - osc_4 = 4, - osc_10 = 10, - osc_11 = 11, - osc_12 = 12, - osc_104 = 104, - osc_110 = 110, - osc_111 = 111, - osc_112 = 112, - - pub fn format( - self: ColorOperationSource, - comptime _: []const u8, - options: std.fmt.FormatOptions, - writer: anytype, - ) !void { - try std.fmt.formatInt(@intFromEnum(self), 10, .lower, options, writer); - } - }; - 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, 4); + + pub const Kind = union(enum) { + palette: u8, + foreground, + background, + cursor, + }; + set: struct { - kind: ColorKind, + kind: Kind, color: RGB, }, - reset: ColorKind, - report: ColorKind, - }; - - pub const ColorOperationList = std.SegmentedList(ColorOperation, 4); - - pub const ColorKind = union(enum) { - palette: u8, - foreground, - background, - cursor, + reset: Kind, + report: Kind, }; pub const ProgressState = enum { @@ -1325,7 +1327,7 @@ pub const Parser = struct { self.command = .{ .color_operation = .{ - .source = .osc_4, + .source = .get_set_palette, }, }; @@ -1381,9 +1383,9 @@ pub const Parser = struct { self.command = .{ .color_operation = .{ .source = switch (self.state) { - .osc_10 => .osc_10, - .osc_11 => .osc_11, - .osc_12 => .osc_12, + .osc_10 => .get_set_foreground, + .osc_11 => .get_set_background, + .osc_12 => .get_set_cursor, else => unreachable, }, }, @@ -1442,9 +1444,9 @@ pub const Parser = struct { self.command = .{ .color_operation = .{ .source = switch (self.state) { - .osc_110 => .osc_110, - .osc_111 => .osc_111, - .osc_112 => .osc_112, + .osc_110 => .reset_foreground, + .osc_111 => .reset_background, + .osc_112 => .reset_cursor, else => unreachable, }, }, @@ -1470,7 +1472,7 @@ pub const Parser = struct { self.command = .{ .color_operation = .{ - .source = .osc_104, + .source = .get_set_palette, }, }; @@ -1742,7 +1744,7 @@ test "OSC: end_of_input" { try testing.expect(cmd == .end_of_input); } -test "OSC: OSC110: reset cursor color" { +test "OSC: OSC110: reset foreground color" { const testing = std.testing; var p: Parser = .{ .alloc = testing.allocator }; @@ -1754,21 +1756,21 @@ test "OSC: OSC110: reset cursor color" { 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 == .osc_110); + 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.ColorKind.foreground, + Command.ColorOperation.Kind.foreground, op.reset, ); } try testing.expect(it.next() == null); } -test "OSC: OSC111: reset cursor color" { +test "OSC: OSC111: reset background color" { const testing = std.testing; var p: Parser = .{ .alloc = testing.allocator }; @@ -1780,14 +1782,14 @@ test "OSC: OSC111: reset cursor color" { 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 == .osc_111); + 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.ColorKind.background, + Command.ColorOperation.Kind.background, op.reset, ); } @@ -1806,14 +1808,14 @@ test "OSC: OSC112: reset cursor color" { 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 == .osc_112); + 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.ColorKind.cursor, + Command.ColorOperation.Kind.cursor, op.reset, ); } @@ -1836,14 +1838,14 @@ test "OSC: OSC112: reset cursor color with semicolon" { 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 == .osc_112); + 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.ColorKind.cursor, + Command.ColorOperation.Kind.cursor, op.reset, ); } @@ -1943,7 +1945,7 @@ test "OSC: longer than buffer" { try testing.expect(p.complete == false); } -test "OSC: OSC10: report default foreground color" { +test "OSC: OSC10: report foreground color" { const testing = std.testing; var p: Parser = .{ .alloc = testing.allocator }; @@ -1957,14 +1959,14 @@ test "OSC: OSC10: report default foreground color" { try testing.expect(cmd == .color_operation); try testing.expectEqual(cmd.color_operation.terminator, .st); - try testing.expect(cmd.color_operation.source == .osc_10); + try testing.expect(cmd.color_operation.source == .get_set_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.* == .report); try testing.expectEqual( - Command.ColorKind.foreground, + Command.ColorOperation.Kind.foreground, op.report, ); } @@ -1983,14 +1985,14 @@ test "OSC: OSC10: set foreground color" { const cmd = p.end('\x07').?; try testing.expect(cmd == .color_operation); try testing.expectEqual(cmd.color_operation.terminator, .bel); - try testing.expect(cmd.color_operation.source == .osc_10); + try testing.expect(cmd.color_operation.source == .get_set_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.* == .set); try testing.expectEqual( - Command.ColorKind.foreground, + Command.ColorOperation.Kind.foreground, op.set.kind, ); try testing.expectEqual( @@ -2001,7 +2003,7 @@ test "OSC: OSC10: set foreground color" { try testing.expect(it.next() == null); } -test "OSC: OSC11: report default background color" { +test "OSC: OSC11: report background color" { const testing = std.testing; var p: Parser = .{ .alloc = testing.allocator }; @@ -2014,14 +2016,14 @@ test "OSC: OSC11: report default background color" { const cmd = p.end('\x07').?; try testing.expect(cmd == .color_operation); try testing.expectEqual(cmd.color_operation.terminator, .bel); - try testing.expect(cmd.color_operation.source == .osc_11); + try testing.expect(cmd.color_operation.source == .get_set_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.* == .report); try testing.expectEqual( - Command.ColorKind.background, + Command.ColorOperation.Kind.background, op.report, ); } @@ -2041,14 +2043,14 @@ test "OSC: OSC11: set background color" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); try testing.expectEqual(cmd.color_operation.terminator, .st); - try testing.expect(cmd.color_operation.source == .osc_11); + try testing.expect(cmd.color_operation.source == .get_set_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.* == .set); try testing.expectEqual( - Command.ColorKind.background, + Command.ColorOperation.Kind.background, op.set.kind, ); try testing.expectEqual( @@ -2059,7 +2061,7 @@ test "OSC: OSC11: set background color" { try testing.expect(it.next() == null); } -test "OSC: OSC12: report background color" { +test "OSC: OSC12: report cursor color" { const testing = std.testing; var p: Parser = .{ .alloc = testing.allocator }; @@ -2072,14 +2074,14 @@ test "OSC: OSC12: report background color" { const cmd = p.end('\x07').?; try testing.expect(cmd == .color_operation); try testing.expectEqual(cmd.color_operation.terminator, .bel); - try testing.expect(cmd.color_operation.source == .osc_12); + try testing.expect(cmd.color_operation.source == .get_set_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.* == .report); try testing.expectEqual( - Command.ColorKind.cursor, + Command.ColorOperation.Kind.cursor, op.report, ); } @@ -2087,7 +2089,7 @@ test "OSC: OSC12: report background color" { try testing.expect(it.next() == null); } -test "OSC: OSC12: set background color" { +test "OSC: OSC12: set cursor color" { const testing = std.testing; var p: Parser = .{ .alloc = testing.allocator }; @@ -2099,14 +2101,14 @@ test "OSC: OSC12: set background color" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); try testing.expectEqual(cmd.color_operation.terminator, .st); - try testing.expect(cmd.color_operation.source == .osc_12); + try testing.expect(cmd.color_operation.source == .get_set_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.* == .set); try testing.expectEqual( - Command.ColorKind.cursor, + Command.ColorOperation.Kind.cursor, op.set.kind, ); try testing.expectEqual( @@ -2128,14 +2130,14 @@ test "OSC: OSC4: get palette color 1" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); - try testing.expect(cmd.color_operation.source == .osc_4); + try testing.expect(cmd.color_operation.source == .get_set_palette); 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.* == .report); try testing.expectEqual( - Command.ColorKind{ .palette = 1 }, + Command.ColorOperation.Kind{ .palette = 1 }, op.report, ); try testing.expectEqual(cmd.color_operation.terminator, .st); @@ -2154,14 +2156,14 @@ test "OSC: OSC4: get palette color 2" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); - try testing.expect(cmd.color_operation.source == .osc_4); + try testing.expect(cmd.color_operation.source == .get_set_palette); try testing.expect(cmd.color_operation.operations.count() == 2); var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; try testing.expect(op.* == .report); try testing.expectEqual( - Command.ColorKind{ .palette = 1 }, + Command.ColorOperation.Kind{ .palette = 1 }, op.report, ); } @@ -2169,7 +2171,7 @@ test "OSC: OSC4: get palette color 2" { const op = it.next().?; try testing.expect(op.* == .report); try testing.expectEqual( - Command.ColorKind{ .palette = 2 }, + Command.ColorOperation.Kind{ .palette = 2 }, op.report, ); } @@ -2188,14 +2190,14 @@ test "OSC: OSC4: set palette color 1" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); - try testing.expect(cmd.color_operation.source == .osc_4); + try testing.expect(cmd.color_operation.source == .get_set_palette); 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.* == .set); try testing.expectEqual( - Command.ColorKind{ .palette = 17 }, + Command.ColorOperation.Kind{ .palette = 17 }, op.set.kind, ); try testing.expectEqual( @@ -2217,14 +2219,14 @@ test "OSC: OSC4: set palette color 2" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); - try testing.expect(cmd.color_operation.source == .osc_4); + try testing.expect(cmd.color_operation.source == .get_set_palette); try testing.expect(cmd.color_operation.operations.count() == 2); var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; try testing.expect(op.* == .set); try testing.expectEqual( - Command.ColorKind{ .palette = 17 }, + Command.ColorOperation.Kind{ .palette = 17 }, op.set.kind, ); try testing.expectEqual( @@ -2236,7 +2238,7 @@ test "OSC: OSC4: set palette color 2" { const op = it.next().?; try testing.expect(op.* == .set); try testing.expectEqual( - Command.ColorKind{ .palette = 1 }, + Command.ColorOperation.Kind{ .palette = 1 }, op.set.kind, ); try testing.expectEqual( @@ -2258,14 +2260,14 @@ test "OSC: OSC4: get with invalid index 1" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); - try testing.expect(cmd.color_operation.source == .osc_4); + try testing.expect(cmd.color_operation.source == .get_set_palette); 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.* == .report); try testing.expectEqual( - Command.ColorKind{ .palette = 1 }, + Command.ColorOperation.Kind{ .palette = 1 }, op.report, ); } @@ -2283,14 +2285,14 @@ test "OSC: OSC4: get with invalid index 2" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); - try testing.expect(cmd.color_operation.source == .osc_4); + try testing.expect(cmd.color_operation.source == .get_set_palette); try testing.expect(cmd.color_operation.operations.count() == 2); var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; try testing.expect(op.* == .report); try testing.expectEqual( - Command.ColorKind{ .palette = 5 }, + Command.ColorOperation.Kind{ .palette = 5 }, op.report, ); } @@ -2298,7 +2300,7 @@ test "OSC: OSC4: get with invalid index 2" { const op = it.next().?; try testing.expect(op.* == .report); try testing.expectEqual( - Command.ColorKind{ .palette = 1 }, + Command.ColorOperation.Kind{ .palette = 1 }, op.report, ); } @@ -2317,14 +2319,14 @@ test "OSC: OSC4: multiple get 8a" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); - try testing.expect(cmd.color_operation.source == .osc_4); + try testing.expect(cmd.color_operation.source == .get_set_palette); try testing.expect(cmd.color_operation.operations.count() == 8); var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; try testing.expect(op.* == .report); try testing.expectEqual( - Command.ColorKind{ .palette = 0 }, + Command.ColorOperation.Kind{ .palette = 0 }, op.report, ); } @@ -2332,7 +2334,7 @@ test "OSC: OSC4: multiple get 8a" { const op = it.next().?; try testing.expect(op.* == .report); try testing.expectEqual( - Command.ColorKind{ .palette = 1 }, + Command.ColorOperation.Kind{ .palette = 1 }, op.report, ); } @@ -2340,7 +2342,7 @@ test "OSC: OSC4: multiple get 8a" { const op = it.next().?; try testing.expect(op.* == .report); try testing.expectEqual( - Command.ColorKind{ .palette = 2 }, + Command.ColorOperation.Kind{ .palette = 2 }, op.report, ); } @@ -2348,7 +2350,7 @@ test "OSC: OSC4: multiple get 8a" { const op = it.next().?; try testing.expect(op.* == .report); try testing.expectEqual( - Command.ColorKind{ .palette = 3 }, + Command.ColorOperation.Kind{ .palette = 3 }, op.report, ); } @@ -2356,7 +2358,7 @@ test "OSC: OSC4: multiple get 8a" { const op = it.next().?; try testing.expect(op.* == .report); try testing.expectEqual( - Command.ColorKind{ .palette = 4 }, + Command.ColorOperation.Kind{ .palette = 4 }, op.report, ); } @@ -2364,7 +2366,7 @@ test "OSC: OSC4: multiple get 8a" { const op = it.next().?; try testing.expect(op.* == .report); try testing.expectEqual( - Command.ColorKind{ .palette = 5 }, + Command.ColorOperation.Kind{ .palette = 5 }, op.report, ); } @@ -2372,7 +2374,7 @@ test "OSC: OSC4: multiple get 8a" { const op = it.next().?; try testing.expect(op.* == .report); try testing.expectEqual( - Command.ColorKind{ .palette = 6 }, + Command.ColorOperation.Kind{ .palette = 6 }, op.report, ); } @@ -2380,7 +2382,7 @@ test "OSC: OSC4: multiple get 8a" { const op = it.next().?; try testing.expect(op.* == .report); try testing.expectEqual( - Command.ColorKind{ .palette = 7 }, + Command.ColorOperation.Kind{ .palette = 7 }, op.report, ); } @@ -2399,14 +2401,14 @@ test "OSC: OSC4: multiple get 8b" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); - try testing.expect(cmd.color_operation.source == .osc_4); + try testing.expect(cmd.color_operation.source == .get_set_palette); try testing.expect(cmd.color_operation.operations.count() == 8); var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; try testing.expect(op.* == .report); try testing.expectEqual( - Command.ColorKind{ .palette = 8 }, + Command.ColorOperation.Kind{ .palette = 8 }, op.report, ); } @@ -2414,7 +2416,7 @@ test "OSC: OSC4: multiple get 8b" { const op = it.next().?; try testing.expect(op.* == .report); try testing.expectEqual( - Command.ColorKind{ .palette = 9 }, + Command.ColorOperation.Kind{ .palette = 9 }, op.report, ); } @@ -2422,7 +2424,7 @@ test "OSC: OSC4: multiple get 8b" { const op = it.next().?; try testing.expect(op.* == .report); try testing.expectEqual( - Command.ColorKind{ .palette = 10 }, + Command.ColorOperation.Kind{ .palette = 10 }, op.report, ); } @@ -2430,7 +2432,7 @@ test "OSC: OSC4: multiple get 8b" { const op = it.next().?; try testing.expect(op.* == .report); try testing.expectEqual( - Command.ColorKind{ .palette = 11 }, + Command.ColorOperation.Kind{ .palette = 11 }, op.report, ); } @@ -2438,7 +2440,7 @@ test "OSC: OSC4: multiple get 8b" { const op = it.next().?; try testing.expect(op.* == .report); try testing.expectEqual( - Command.ColorKind{ .palette = 12 }, + Command.ColorOperation.Kind{ .palette = 12 }, op.report, ); } @@ -2446,7 +2448,7 @@ test "OSC: OSC4: multiple get 8b" { const op = it.next().?; try testing.expect(op.* == .report); try testing.expectEqual( - Command.ColorKind{ .palette = 13 }, + Command.ColorOperation.Kind{ .palette = 13 }, op.report, ); } @@ -2454,7 +2456,7 @@ test "OSC: OSC4: multiple get 8b" { const op = it.next().?; try testing.expect(op.* == .report); try testing.expectEqual( - Command.ColorKind{ .palette = 14 }, + Command.ColorOperation.Kind{ .palette = 14 }, op.report, ); } @@ -2462,7 +2464,7 @@ test "OSC: OSC4: multiple get 8b" { const op = it.next().?; try testing.expect(op.* == .report); try testing.expectEqual( - Command.ColorKind{ .palette = 15 }, + Command.ColorOperation.Kind{ .palette = 15 }, op.report, ); } @@ -2480,14 +2482,14 @@ test "OSC: OSC4: set with invalid index" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); - try testing.expect(cmd.color_operation.source == .osc_4); + try testing.expect(cmd.color_operation.source == .get_set_palette); 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.* == .set); try testing.expectEqual( - Command.ColorKind{ .palette = 1 }, + Command.ColorOperation.Kind{ .palette = 1 }, op.set.kind, ); try testing.expectEqual( @@ -2509,14 +2511,14 @@ test "OSC: OSC4: mix get/set palette color" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); - try testing.expect(cmd.color_operation.source == .osc_4); + try testing.expect(cmd.color_operation.source == .get_set_palette); try testing.expect(cmd.color_operation.operations.count() == 2); var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; try testing.expect(op.* == .set); try testing.expectEqual( - Command.ColorKind{ .palette = 17 }, + Command.ColorOperation.Kind{ .palette = 17 }, op.set.kind, ); try testing.expectEqual( @@ -2527,7 +2529,7 @@ test "OSC: OSC4: mix get/set palette color" { { const op = it.next().?; try testing.expect(op.* == .report); - try testing.expectEqual(Command.ColorKind{ .palette = 254 }, op.report); + try testing.expectEqual(Command.ColorOperation.Kind{ .palette = 254 }, op.report); } try testing.expect(it.next() == null); } @@ -2543,14 +2545,14 @@ test "OSC: OSC104: reset palette color 1" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); - try testing.expect(cmd.color_operation.source == .osc_104); + try testing.expect(cmd.color_operation.source == .get_set_palette); 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.ColorKind{ .palette = 17 }, + Command.ColorOperation.Kind{ .palette = 17 }, op.reset, ); } @@ -2568,14 +2570,14 @@ test "OSC: OSC104: reset palette color 2" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); - try testing.expect(cmd.color_operation.source == .osc_104); + try testing.expect(cmd.color_operation.source == .get_set_palette); try testing.expect(cmd.color_operation.operations.count() == 2); var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; try testing.expect(op.* == .reset); try testing.expectEqual( - Command.ColorKind{ .palette = 17 }, + Command.ColorOperation.Kind{ .palette = 17 }, op.reset, ); } @@ -2583,7 +2585,7 @@ test "OSC: OSC104: reset palette color 2" { const op = it.next().?; try testing.expect(op.* == .reset); try testing.expectEqual( - Command.ColorKind{ .palette = 111 }, + Command.ColorOperation.Kind{ .palette = 111 }, op.reset, ); } @@ -2601,14 +2603,14 @@ test "OSC: OSC104: invalid palette index" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); - try testing.expect(cmd.color_operation.source == .osc_104); + try testing.expect(cmd.color_operation.source == .get_set_palette); 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.ColorKind{ .palette = 111 }, + Command.ColorOperation.Kind{ .palette = 111 }, op.reset, ); } @@ -2626,14 +2628,14 @@ test "OSC: OSC104: empty palette index" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); - try testing.expect(cmd.color_operation.source == .osc_104); + try testing.expect(cmd.color_operation.source == .get_set_palette); 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.ColorKind{ .palette = 111 }, + Command.ColorOperation.Kind{ .palette = 111 }, op.reset, ); } diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index ca16b0bd2..554a87805 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -1197,8 +1197,8 @@ pub const StreamHandler = struct { pub fn handleColorOperation( self: *StreamHandler, - source: terminal.osc.Command.ColorOperationSource, - operations: *const terminal.osc.Command.ColorOperationList, + source: terminal.osc.Command.ColorOperation.Source, + operations: *const terminal.osc.Command.ColorOperation.List, terminator: terminal.osc.Terminator, ) !void { // return early if there is nothing to do From 5fb32fd8a0d43412cf9375ad5f1fe850f23810ca Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sat, 24 May 2025 11:37:34 -0500 Subject: [PATCH 15/17] OSC: add comptime check for size of OSC Command --- src/terminal/osc.zig | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 67f665f1a..63d3e4c6b 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -189,7 +189,7 @@ pub const Command = union(enum) { } }; - pub const List = std.SegmentedList(ColorOperation, 4); + pub const List = std.SegmentedList(ColorOperation, 2); pub const Kind = union(enum) { palette: u8, @@ -213,6 +213,11 @@ pub const Command = union(enum) { indeterminate, pause, }; + + comptime { + assert(@sizeOf(Command) == 64); + // @compileLog(@sizeOf(Command)); + } }; /// The terminator used to end an OSC command. For OSC commands that demand From f0fc82c80f070937234198f6404e3626c514ad9f Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sat, 24 May 2025 12:12:28 -0500 Subject: [PATCH 16/17] OSC: account for 32-bit systems in comptime Command size check --- src/terminal/osc.zig | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 63d3e4c6b..8ca4326c5 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -215,7 +215,11 @@ pub const Command = union(enum) { }; comptime { - assert(@sizeOf(Command) == 64); + assert(@sizeOf(Command) == switch (@sizeOf(usize)) { + 4 => 44, + 8 => 64, + else => unreachable, + }); // @compileLog(@sizeOf(Command)); } }; From 1104993c940a26dcf3baad6917e988ea0c913cfb Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sat, 24 May 2025 16:42:55 -0500 Subject: [PATCH 17/17] OSC: move some processing back inside the OSC state machine --- src/terminal/osc.zig | 423 ++++++++++++++++++++++++------------------- 1 file changed, 238 insertions(+), 185 deletions(-) diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 8ca4326c5..d0b59e834 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -345,7 +345,8 @@ pub const Parser = struct { clipboard_kind_end, // Get/set color palette index - osc_4, + osc_4_index, + osc_4_color, // Get/set foreground color osc_10, @@ -359,15 +360,6 @@ pub const Parser = struct { // Reset color palette index osc_104, - // Reset foreground color - osc_110, - - // Reset background color - osc_111, - - // Reset cursor color - osc_112, - // Hyperlinks hyperlink_param_key, hyperlink_param_value, @@ -547,6 +539,11 @@ pub const Parser = struct { self.state = .invalid; break :osc_10; } + self.command = .{ + .color_operation = .{ + .source = .get_set_foreground, + }, + }; self.state = .osc_10; self.buf_start = self.buf_idx; self.complete = true; @@ -555,7 +552,10 @@ pub const Parser = struct { else => self.state = .invalid, }, - .osc_10 => {}, + .osc_10, .osc_11, .osc_12 => switch (c) { + ';' => self.parseOSC101112(false), + else => {}, + }, .@"104" => switch (c) { ';' => osc_104: { @@ -564,6 +564,11 @@ pub const Parser = struct { self.state = .invalid; break :osc_104; } + self.command = .{ + .color_operation = .{ + .source = .reset_palette, + }, + }; self.state = .osc_104; self.buf_start = self.buf_idx; self.complete = true; @@ -571,7 +576,10 @@ pub const Parser = struct { else => self.state = .invalid, }, - .osc_104 => {}, + .osc_104 => switch (c) { + ';' => self.parseOSC104(false), + else => {}, + }, .@"11" => switch (c) { ';' => osc_11: { @@ -580,51 +588,52 @@ pub const Parser = struct { self.state = .invalid; break :osc_11; } + self.command = .{ + .color_operation = .{ + .source = .get_set_background, + }, + }; self.state = .osc_11; self.buf_start = self.buf_idx; self.complete = true; }, - '0' => osc_110: { + '0'...'2' => blk: { if (self.alloc == null) { - log.warn("OSC 110 requires an allocator, but none was provided", .{}); + log.warn("OSC 11{c} requires an allocator, but none was provided", .{c}); self.state = .invalid; - break :osc_110; + break :blk; } - self.state = .osc_110; - self.buf_start = self.buf_idx; - self.complete = true; - }, - '1' => osc_111: { - if (self.alloc == null) { - log.warn("OSC 111 requires an allocator, but none was provided", .{}); - self.state = .invalid; - break :osc_111; - } - self.state = .osc_111; - self.buf_start = self.buf_idx; - self.complete = true; - }, - '2' => osc_112: { - if (self.alloc == null) { - log.warn("OSC 112 requires an allocator, but none was provided", .{}); - self.state = .invalid; - break :osc_112; - } - self.state = .osc_112; - self.buf_start = self.buf_idx; + + const alloc = self.alloc orelse return; + + self.command = .{ + .color_operation = .{ + .source = switch (c) { + '0' => .reset_foreground, + '1' => .reset_background, + '2' => .reset_cursor, + 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.complete = true; }, else => self.state = .invalid, }, - .osc_11 => {}, - - .osc_110 => {}, - - .osc_111 => {}, - - .osc_112 => {}, - .@"12" => switch (c) { ';' => osc_12: { if (self.alloc == null) { @@ -632,6 +641,11 @@ pub const Parser = struct { self.state = .invalid; break :osc_12; } + self.command = .{ + .color_operation = .{ + .source = .get_set_cursor, + }, + }; self.state = .osc_12; self.buf_start = self.buf_idx; self.complete = true; @@ -639,8 +653,6 @@ pub const Parser = struct { else => self.state = .invalid, }, - .osc_12 => {}, - .@"13" => switch (c) { '3' => self.state = .@"133", else => self.state = .invalid, @@ -728,14 +740,30 @@ pub const Parser = struct { self.state = .invalid; break :osc_4; } - self.state = .osc_4; + self.command = .{ + .color_operation = .{ + .source = .get_set_palette, + }, + }; + self.state = .osc_4_index; self.buf_start = self.buf_idx; self.complete = true; }, else => self.state = .invalid, }, - .osc_4 => {}, + .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) { '2' => self.state = .@"52", @@ -1329,85 +1357,104 @@ pub const Parser = struct { self.temp_state.str.* = list.items; } - fn parseOSC4(self: *Parser) void { - assert(self.state == .osc_4); + 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; - self.command = .{ - .color_operation = .{ - .source = .get_set_palette, + 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; }, }; - - const str = self.buf[self.buf_start..self.buf_idx]; - var it = std.mem.splitScalar(u8, str, ';'); - while (it.next()) |index_str| { - 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 }); - // skip any color spec - _ = it.next(); - continue; + 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, }, }; - const spec_str = it.next() orelse continue; - if (std.mem.eql(u8, spec_str, "?")) { - const op = self.command.color_operation.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 }); - continue; - }; - const op = self.command.color_operation.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) void { + 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; - self.command = .{ - .color_operation = .{ - .source = switch (self.state) { - .osc_10 => .get_set_foreground, - .osc_11 => .get_set_background, - .osc_12 => .get_set_cursor, - else => unreachable, - }, - }, - }; - const str = self.buf[self.buf_start..self.buf_idx]; - var it = std.mem.splitScalar(u8, str, ';'); - const color_str = it.next() orelse { - log.warn("OSC 10/11/12 requires an argument", .{}); - self.state = .invalid; - return; - }; - if (std.mem.eql(u8, color_str, "?")) { - const op = self.command.color_operation.operations.addOne(alloc) catch |err| { + if (std.mem.eql(u8, spec_str, "?")) { + const op = operations.addOne(alloc) catch |err| { log.warn("unable to append color operation: {}", .{err}); return; }; @@ -1420,11 +1467,20 @@ pub const Parser = struct { }, }; } else { - const color = RGB.parse(color_str) catch |err| { - log.warn("invalid color specification in OSC 10/11/12: {s} {}", .{ color_str, err }); + 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 = self.command.color_operation.operations.addOne(alloc) catch |err| { + const op = operations.addOne(alloc) catch |err| { log.warn("unable to append color operation: {}", .{err}); return; }; @@ -1442,22 +1498,21 @@ pub const Parser = struct { } } - fn parseOSC110111112(self: *Parser) void { - assert(switch (self.state) { - .osc_110, .osc_111, .osc_112 => true, - else => false, - }); + 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; - self.command = .{ - .color_operation = .{ - .source = switch (self.state) { - .osc_110 => .reset_foreground, - .osc_111 => .reset_background, - .osc_112 => .reset_cursor, - else => unreachable, - }, + 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| { @@ -1465,45 +1520,10 @@ pub const Parser = struct { return; }; op.* = .{ - .reset = switch (self.state) { - .osc_110 => .foreground, - .osc_111 => .background, - .osc_112 => .cursor, - else => unreachable, - }, + .reset = .{ .palette = index }, }; } - fn parseOSC104(self: *Parser) void { - assert(self.state == .osc_104); - - const alloc = self.alloc orelse return; - - self.command = .{ - .color_operation = .{ - .source = .get_set_palette, - }, - }; - - const str = self.buf[self.buf_start..self.buf_idx]; - var it = std.mem.splitScalar(u8, str, ';'); - while (it.next()) |index_str| { - 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 }); - continue; - }, - }; - 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 @@ -1527,18 +1547,9 @@ pub const Parser = struct { .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, - => self.parseOSC4(), - .osc_10, - .osc_11, - .osc_12, - => self.parseOSC101112(), - .osc_104, - => self.parseOSC104(), - .osc_110, - .osc_111, - .osc_112, - => self.parseOSC110111112(), + .osc_4_color => self.parseOSC4(true), + .osc_10, .osc_11, .osc_12 => self.parseOSC101112(true), + .osc_104 => self.parseOSC104(true), else => {}, } @@ -1838,10 +1849,7 @@ test "OSC: OSC112: reset cursor color with semicolon" { defer p.deinit(); const input = "112;"; - for (input) |ch| { - log.warn("feeding {c} {s}", .{ ch, @tagName(p.state) }); - p.next(ch); - } + for (input) |ch| p.next(ch); log.warn("finish: {s}", .{@tagName(p.state)}); const cmd = p.end(0x07).?; @@ -2538,7 +2546,52 @@ test "OSC: OSC4: mix get/set palette color" { { const op = it.next().?; try testing.expect(op.* == .report); - try testing.expectEqual(Command.ColorOperation.Kind{ .palette = 254 }, op.report); + try testing.expectEqual( + Command.ColorOperation.Kind{ .palette = 254 }, + op.report, + ); + } + try testing.expect(it.next() == null); +} + +test "OSC: OSC4: incomplete color/spec 1" { + const testing = std.testing; + + var p: Parser = .{ .alloc = testing.allocator }; + defer p.deinit(); + + const input = "4;17"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?; + try testing.expect(cmd == .color_operation); + try testing.expect(cmd.color_operation.source == .get_set_palette); + try testing.expect(cmd.color_operation.operations.count() == 0); + var it = cmd.color_operation.operations.constIterator(0); + try testing.expect(it.next() == null); +} + +test "OSC: OSC4: incomplete color/spec 2" { + const testing = std.testing; + + var p: Parser = .{ .alloc = testing.allocator }; + defer p.deinit(); + + const input = "4;17;?;42"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?; + try testing.expect(cmd == .color_operation); + try testing.expect(cmd.color_operation.source == .get_set_palette); + 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.* == .report); + try testing.expectEqual( + Command.ColorOperation.Kind{ .palette = 17 }, + op.report, + ); } try testing.expect(it.next() == null); } @@ -2554,7 +2607,7 @@ test "OSC: OSC104: reset palette color 1" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); - try testing.expect(cmd.color_operation.source == .get_set_palette); + try testing.expect(cmd.color_operation.source == .reset_palette); try testing.expect(cmd.color_operation.operations.count() == 1); var it = cmd.color_operation.operations.constIterator(0); { @@ -2579,8 +2632,8 @@ test "OSC: OSC104: reset palette color 2" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); - try testing.expect(cmd.color_operation.source == .get_set_palette); - try testing.expect(cmd.color_operation.operations.count() == 2); + try testing.expect(cmd.color_operation.source == .reset_palette); + try testing.expectEqual(2, cmd.color_operation.operations.count()); var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; @@ -2612,7 +2665,7 @@ test "OSC: OSC104: invalid palette index" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); - try testing.expect(cmd.color_operation.source == .get_set_palette); + try testing.expect(cmd.color_operation.source == .reset_palette); try testing.expect(cmd.color_operation.operations.count() == 1); var it = cmd.color_operation.operations.constIterator(0); { @@ -2637,7 +2690,7 @@ test "OSC: OSC104: empty palette index" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); - try testing.expect(cmd.color_operation.source == .get_set_palette); + try testing.expect(cmd.color_operation.source == .reset_palette); try testing.expect(cmd.color_operation.operations.count() == 1); var it = cmd.color_operation.operations.constIterator(0); {