From a3696a918590d16f3191bda8774d1ebe4f85aaab Mon Sep 17 00:00:00 2001 From: cryptocode Date: Thu, 14 Sep 2023 14:53:31 +0200 Subject: [PATCH 1/4] Implement OSC 10 and OSC 11 default color queries These OSC commands report the default foreground and background colors. Most terminals return the RGB components scaled up to 16-bit components, because some legacy software are unable to read 8-bit components. The PR follows this conventions. iTerm2 allow 8-bit reporting through a config option, and a similar option is added here. In addition to picking between scaled and unscaled reporting, the user can also turn off OSC 10/11 replies altogether. Scaling is essentially c / 1 * 65535, where c is the 8-bit component, and reporting is left-padded with zeros if necessary. This format appears to stem from the XParseColor format. --- src/config/Config.zig | 23 ++++++++++ src/terminal/Parser.zig | 2 +- src/terminal/osc.zig | 95 +++++++++++++++++++++++++++++++++++++++++ src/terminal/stream.zig | 6 +++ src/termio/Exec.zig | 52 ++++++++++++++++++++++ 5 files changed, 177 insertions(+), 1 deletion(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index ff599e1ee..69827c4c3 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -305,6 +305,22 @@ keybind: Keybinds = .{}, /// The default value is "detect". @"shell-integration": ShellIntegration = .detect, +/// Sets the reporting format for OSC sequences that request color information. +/// Ghostty currently supports OSC 10 (foreground) and OSC 11 (background) queries, +/// and by default the reported values are scaled-up RGB values, where each component +/// are 16 bits. This is how most terminals report these values. However, some legacy +/// applications may require 8-bit, unscaled, components. We also support turning off +/// reporting alltogether. The components are lowercase hex values. +/// +/// Allowable values are: +/// +/// * "none" - OSC 10/11 queries receive no reply +/// * "bits8" - Color components are return unscaled, i.e. rr/gg/bb +/// * "bits16" - Color components are returned scaled, e.g. rrrr/gggg/bbbb +/// +/// The default value is "bits16". +@"osc-color-report-format": OSCColorReportFormat = .bits16, + /// If anything other than false, fullscreen mode on macOS will not use the /// native fullscreen, but make the window fullscreen without animations and /// using a new space. It's faster than the native fullscreen mode since it @@ -1480,3 +1496,10 @@ pub const ShellIntegration = enum { fish, zsh, }; + +/// OSC 10 and 11 default color reporting format. +pub const OSCColorReportFormat = enum { + none, + bits8, + bits16, +}; diff --git a/src/terminal/Parser.zig b/src/terminal/Parser.zig index d376d5d10..1ddf2f55f 100644 --- a/src/terminal/Parser.zig +++ b/src/terminal/Parser.zig @@ -248,7 +248,7 @@ pub fn next(self: *Parser, c: u8) [3]?Action { return [3]?Action{ // Exit depends on current state if (self.state == next_state) null else switch (self.state) { - .osc_string => if (self.osc_parser.end()) |cmd| + .osc_string => if (self.osc_parser.endWithStringTerminator(c)) |cmd| Action{ .osc_dispatch = cmd } else null, diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index f61504daf..dd05a0940 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -84,6 +84,7 @@ pub const Command = union(enum) { value: []const u8, }, +<<<<<<< HEAD /// OSC 22. Set the mouse shape. There doesn't seem to be a standard /// naming scheme for cursors but it looks like terminals such as Foot /// are moving towards using the W3C CSS cursor names. For OSC parsing, @@ -91,6 +92,16 @@ pub const Command = union(enum) { mouse_shape: struct { value: []const u8, }, + + /// OSC 10 and OSC 11 default color report. + report_default_color: struct { + /// OSC 10 requests the foreground color, OSC 11 the background color. + kind: enum { foreground, background }, + + /// We must reply with the same string terminator (ST) as used in the + /// request. This is either ESC\ or BEL (0x07) + string_terminator: ?[]const u8 = null, + }, }; pub const Parser = struct { @@ -134,6 +145,7 @@ pub const Parser = struct { // but the state space is small enough that we just build it up this way. @"0", @"1", + @"10", @"11", @"13", @"133", @@ -143,6 +155,14 @@ pub const Parser = struct { @"52", @"7", + // OSC 10 is used to query the default foreground color, and to set the default foreground color. + // Only querying is currently supported. + osc_10, + + // OSC 11 is used to query the default background color, and to set the default background color. + // Only querying is currently supported. + osc_11, + // We're in a semantic prompt OSC command but we aren't sure // what the command is yet, i.e. `133;` semantic_prompt, @@ -210,12 +230,23 @@ pub const Parser = struct { }, .@"1" => switch (c) { + '0' => self.state = .@"10", '1' => self.state = .@"11", '3' => self.state = .@"13", else => self.state = .invalid, }, + .@"10" => switch (c) { + ';' => { + self.state = .osc_10; + }, + else => self.state = .invalid, + }, + .@"11" => switch (c) { + ';' => { + self.state = .osc_11; + }, '2' => { self.complete = true; self.command = .{ .reset_cursor_color = {} }; @@ -303,6 +334,22 @@ pub const Parser = struct { else => self.state = .invalid, }, + .osc_10 => switch (c) { + '?' => { + self.command = .{ .report_default_color = .{ .kind = .foreground } }; + self.complete = true; + }, + else => self.state = .invalid, + }, + + .osc_11 => switch (c) { + '?' => { + self.command = .{ .report_default_color = .{ .kind = .background } }; + self.complete = true; + }, + else => self.state = .invalid, + }, + .semantic_prompt => switch (c) { 'A' => { self.state = .semantic_option_start; @@ -466,6 +513,24 @@ pub const Parser = struct { return self.command; } + + /// End the sequence and return the command, if any. If the return value + /// is null, then no valid command was found. The provided `string_terminator` + /// character originates from the request. This way we can use the same + /// terminator in the reply, which would otherwise break applications that + /// don't consider both options. + pub fn endWithStringTerminator(self: *Parser, string_separator: u8) ?Command { + var maybe_cmd = self.end(); + if (maybe_cmd) |*cmd| { + switch (cmd.*) { + .report_default_color => |*c| { + c.string_terminator = if (string_separator == 0x07) "\x07" else "\x1b\\"; + }, + else => {}, + } + } + return maybe_cmd; + } }; test "OSC: change_window_title" { @@ -697,3 +762,33 @@ test "OSC: longer than buffer" { try testing.expect(p.end() == null); } + +test "OSC: report default foreground color" { + const testing = std.testing; + + var p: Parser = .{}; + + const input = "10;?"; + for (input) |ch| p.next(ch); + + // This corresponds to ST = ESC \ + const cmd = p.endWithStringTerminator('\x1b').?; + try testing.expect(cmd == .report_default_color); + try testing.expect(cmd.report_default_color.kind == .foreground); + try testing.expectEqualSlices(u8, cmd.report_default_color.string_terminator.?, "\x1b\\"); +} + +test "OSC: report default background color" { + const testing = std.testing; + + var p: Parser = .{}; + + const input = "11;?"; + for (input) |ch| p.next(ch); + + // This corresponds to ST = BELL + const cmd = p.endWithStringTerminator('\x07').?; + try testing.expect(cmd == .report_default_color); + try testing.expect(cmd.report_default_color.kind == .background); + try testing.expectEqualSlices(u8, cmd.report_default_color.string_terminator.?, "\x07"); +} diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index c12bc4063..5240b33b6 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -861,6 +861,12 @@ pub fn Stream(comptime Handler: type) type { } else log.warn("unimplemented OSC callback: {}", .{cmd}); }, + .report_default_color => |v| { + if (@hasDecl(T, "reportDefaultColor")) { + try self.handler.reportDefaultColor(if (v.kind == .foreground) "10" else "11", v.string_terminator); + } else log.warn("unimplemented OSC callback: {}", .{cmd}); + }, + else => if (@hasDecl(T, "oscUnimplemented")) try self.handler.oscUnimplemented(cmd) else diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 36deb1235..94deb6df5 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -68,6 +68,15 @@ grid_size: renderer.GridSize, default_cursor_style: terminal.Cursor.Style, default_cursor_blink: bool, +/// Default foreground color for OSC 10 reporting. +default_foreground_color: terminal.color.RGB, + +/// Default background color for OSC 11 reporting. +default_background_color: terminal.color.RGB, + +/// The OSC 10/11 reply style. +osc_color_report_format: configpkg.Config.OSCColorReportFormat, + /// The data associated with the currently running thread. data: ?*EventData, @@ -79,6 +88,9 @@ pub const DerivedConfig = struct { image_storage_limit: usize, cursor_style: terminal.Cursor.Style, cursor_blink: bool, + foreground: configpkg.Config.Color, + background: configpkg.Config.Color, + osc_color_report_format: configpkg.Config.OSCColorReportFormat, pub fn init( alloc_gpa: Allocator, @@ -91,6 +103,9 @@ pub const DerivedConfig = struct { .image_storage_limit = config.@"image-storage-limit", .cursor_style = config.@"cursor-style", .cursor_blink = config.@"cursor-style-blink", + .foreground = config.foreground, + .background = config.background, + .osc_color_report_format = config.@"osc-color-report-format", }; } @@ -140,6 +155,9 @@ pub fn init(alloc: Allocator, opts: termio.Options) !Exec { .grid_size = opts.grid_size, .default_cursor_style = opts.config.cursor_style, .default_cursor_blink = opts.config.cursor_blink, + .default_foreground_color = config.foreground.toTerminalRGB(), + .default_background_color = config.background.toTerminalRGB(), + .osc_color_report_format = config.osc_color_report_format, .data = null, }; } @@ -204,6 +222,9 @@ pub fn threadEnter(self: *Exec, thread: *termio.Thread) !ThreadData { .grid_size = &self.grid_size, .default_cursor_style = self.default_cursor_style, .default_cursor_blink = self.default_cursor_blink, + .default_foreground_color = self.default_foreground_color, + .default_background_color = self.default_background_color, + .osc_color_report_format = self.osc_color_report_format, }, }, }; @@ -1141,6 +1162,9 @@ const StreamHandler = struct { default_cursor: bool = true, default_cursor_style: terminal.Cursor.Style, default_cursor_blink: bool, + default_foreground_color: terminal.color.RGB, + default_background_color: terminal.color.RGB, + osc_color_report_format: configpkg.Config.OSCColorReportFormat, pub fn deinit(self: *StreamHandler) void { self.apc.deinit(); @@ -1779,4 +1803,32 @@ const StreamHandler = struct { self.ev.seen_title = false; } } + + /// Implements OSC 10 and OSC 11, which reports default foreground and background color respectively. + pub fn reportDefaultColor(self: *StreamHandler, osc_code: []const u8, string_terminator: ?[]const u8) !void { + if (self.osc_color_report_format == .none) return; + var msg: termio.Message = .{ .write_small = .{} }; + + const resp = resp: { + if (self.osc_color_report_format == .bits16) { + break :resp try std.fmt.bufPrint(&msg.write_small.data, "\x1B]{s};rgb:{x:0>4}/{x:0>4}/{x:0>4}{s}", .{ + osc_code, + @as(u16, self.default_foreground_color.r) * 257, + @as(u16, self.default_foreground_color.g) * 257, + @as(u16, self.default_foreground_color.b) * 257, + if (string_terminator) |st| st else "\x1b\\", + }); + } else { + break :resp try std.fmt.bufPrint(&msg.write_small.data, "\x1B]{s};rgb:{x:0>2}/{x:0>2}/{x:0>2}{s}", .{ + osc_code, + @as(u16, self.default_foreground_color.r), + @as(u16, self.default_foreground_color.g), + @as(u16, self.default_foreground_color.b), + if (string_terminator) |st| st else "\x1b\\", + }); + } + }; + msg.write_small.len = @intCast(resp.len); + self.messageWriter(msg); + } }; From dc14ca86ca5ba9f3ff698b8dbc5dc8de42ce67a2 Mon Sep 17 00:00:00 2001 From: cryptocode Date: Thu, 14 Sep 2023 21:14:23 +0200 Subject: [PATCH 2/4] Review updates: * Change state names to more human readable query_default_fg/bg * Single-line state prongs * String terminator is not an enum * Removed `endWithStringTerminator` and added nullabe arg to `end` * Fixed a color reporting bug, fg/bg wasn't correctly picked --- src/terminal/Parser.zig | 2 +- src/terminal/osc.zig | 104 ++++++++++++++++++---------------------- src/terminal/stream.zig | 5 +- src/termio/Exec.zig | 14 +++--- 4 files changed, 58 insertions(+), 67 deletions(-) diff --git a/src/terminal/Parser.zig b/src/terminal/Parser.zig index 1ddf2f55f..06b10e140 100644 --- a/src/terminal/Parser.zig +++ b/src/terminal/Parser.zig @@ -248,7 +248,7 @@ pub fn next(self: *Parser, c: u8) [3]?Action { return [3]?Action{ // Exit depends on current state if (self.state == next_state) null else switch (self.state) { - .osc_string => if (self.osc_parser.endWithStringTerminator(c)) |cmd| + .osc_string => if (self.osc_parser.end(c)) |cmd| Action{ .osc_dispatch = cmd } else null, diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index dd05a0940..c49ef38a0 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -84,7 +84,6 @@ pub const Command = union(enum) { value: []const u8, }, -<<<<<<< HEAD /// OSC 22. Set the mouse shape. There doesn't seem to be a standard /// naming scheme for cursors but it looks like terminals such as Foot /// are moving towards using the W3C CSS cursor names. For OSC parsing, @@ -99,8 +98,13 @@ pub const Command = union(enum) { kind: enum { foreground, background }, /// We must reply with the same string terminator (ST) as used in the - /// request. This is either ESC\ or BEL (0x07) - string_terminator: ?[]const u8 = null, + /// request. + string_terminator: ?enum { + /// The preferred string terminator is ESC followed by \ + st, + /// Some applications and terminals use BELL (0x07) as the string terminator. + bel, + } = null, }, }; @@ -157,11 +161,11 @@ pub const Parser = struct { // OSC 10 is used to query the default foreground color, and to set the default foreground color. // Only querying is currently supported. - osc_10, + query_default_fg, // OSC 11 is used to query the default background color, and to set the default background color. // Only querying is currently supported. - osc_11, + query_default_bg, // We're in a semantic prompt OSC command but we aren't sure // what the command is yet, i.e. `133;` @@ -237,16 +241,12 @@ pub const Parser = struct { }, .@"10" => switch (c) { - ';' => { - self.state = .osc_10; - }, + ';' => self.state = .query_default_fg, else => self.state = .invalid, }, .@"11" => switch (c) { - ';' => { - self.state = .osc_11; - }, + ';' => self.state = .query_default_bg, '2' => { self.complete = true; self.command = .{ .reset_cursor_color = {} }; @@ -334,7 +334,7 @@ pub const Parser = struct { else => self.state = .invalid, }, - .osc_10 => switch (c) { + .query_default_fg => switch (c) { '?' => { self.command = .{ .report_default_color = .{ .kind = .foreground } }; self.complete = true; @@ -342,7 +342,7 @@ pub const Parser = struct { else => self.state = .invalid, }, - .osc_11 => switch (c) { + .query_default_bg => switch (c) { '?' => { self.command = .{ .report_default_color = .{ .kind = .background } }; self.complete = true; @@ -497,7 +497,7 @@ pub const Parser = struct { /// End the sequence and return the command, if any. If the return value /// is null, then no valid command was found. - pub fn end(self: *Parser) ?Command { + pub fn end(self: *Parser, string_separator: ?u8) ?Command { if (!self.complete) { log.warn("invalid OSC command: {s}", .{self.buf[0..self.buf_idx]}); return null; @@ -511,25 +511,13 @@ pub const Parser = struct { else => {}, } - return self.command; - } - - /// End the sequence and return the command, if any. If the return value - /// is null, then no valid command was found. The provided `string_terminator` - /// character originates from the request. This way we can use the same - /// terminator in the reply, which would otherwise break applications that - /// don't consider both options. - pub fn endWithStringTerminator(self: *Parser, string_separator: u8) ?Command { - var maybe_cmd = self.end(); - if (maybe_cmd) |*cmd| { - switch (cmd.*) { - .report_default_color => |*c| { - c.string_terminator = if (string_separator == 0x07) "\x07" else "\x1b\\"; - }, - else => {}, - } + switch (self.command) { + .report_default_color => |*c| { + c.string_terminator = if (string_separator == 0x07) .bel else .st; + }, + else => {}, } - return maybe_cmd; + return self.command; } }; @@ -541,7 +529,7 @@ test "OSC: change_window_title" { p.next(';'); p.next('a'); p.next('b'); - const cmd = p.end().?; + const cmd = p.end(null).?; try testing.expect(cmd == .change_window_title); try testing.expectEqualStrings("ab", cmd.change_window_title); } @@ -554,7 +542,7 @@ test "OSC: change_window_title with 2" { p.next(';'); p.next('a'); p.next('b'); - const cmd = p.end().?; + const cmd = p.end(null).?; try testing.expect(cmd == .change_window_title); try testing.expectEqualStrings("ab", cmd.change_window_title); } @@ -567,7 +555,7 @@ test "OSC: prompt_start" { const input = "133;A"; for (input) |ch| p.next(ch); - const cmd = p.end().?; + const cmd = p.end(null).?; try testing.expect(cmd == .prompt_start); try testing.expect(cmd.prompt_start.aid == null); try testing.expect(cmd.prompt_start.redraw); @@ -581,7 +569,7 @@ test "OSC: prompt_start with single option" { const input = "133;A;aid=14"; for (input) |ch| p.next(ch); - const cmd = p.end().?; + const cmd = p.end(null).?; try testing.expect(cmd == .prompt_start); try testing.expectEqualStrings("14", cmd.prompt_start.aid.?); } @@ -594,7 +582,7 @@ test "OSC: prompt_start with redraw disabled" { const input = "133;A;redraw=0"; for (input) |ch| p.next(ch); - const cmd = p.end().?; + const cmd = p.end(null).?; try testing.expect(cmd == .prompt_start); try testing.expect(!cmd.prompt_start.redraw); } @@ -607,7 +595,7 @@ test "OSC: prompt_start with redraw invalid value" { const input = "133;A;redraw=42"; for (input) |ch| p.next(ch); - const cmd = p.end().?; + const cmd = p.end(null).?; try testing.expect(cmd == .prompt_start); try testing.expect(cmd.prompt_start.redraw); try testing.expect(cmd.prompt_start.kind == .primary); @@ -621,7 +609,7 @@ test "OSC: prompt_start with continuation" { const input = "133;A;k=c"; for (input) |ch| p.next(ch); - const cmd = p.end().?; + const cmd = p.end(null).?; try testing.expect(cmd == .prompt_start); try testing.expect(cmd.prompt_start.kind == .continuation); } @@ -634,7 +622,7 @@ test "OSC: end_of_command no exit code" { const input = "133;D"; for (input) |ch| p.next(ch); - const cmd = p.end().?; + const cmd = p.end(null).?; try testing.expect(cmd == .end_of_command); } @@ -646,7 +634,7 @@ test "OSC: end_of_command with exit code" { const input = "133;D;25"; for (input) |ch| p.next(ch); - const cmd = p.end().?; + const cmd = p.end(null).?; try testing.expect(cmd == .end_of_command); try testing.expectEqual(@as(u8, 25), cmd.end_of_command.exit_code.?); } @@ -659,7 +647,7 @@ test "OSC: prompt_end" { const input = "133;B"; for (input) |ch| p.next(ch); - const cmd = p.end().?; + const cmd = p.end(null).?; try testing.expect(cmd == .prompt_end); } @@ -671,7 +659,7 @@ test "OSC: end_of_input" { const input = "133;C"; for (input) |ch| p.next(ch); - const cmd = p.end().?; + const cmd = p.end(null).?; try testing.expect(cmd == .end_of_input); } @@ -683,7 +671,7 @@ test "OSC: reset_cursor_color" { const input = "112"; for (input) |ch| p.next(ch); - const cmd = p.end().?; + const cmd = p.end(null).?; try testing.expect(cmd == .reset_cursor_color); } @@ -695,7 +683,7 @@ test "OSC: get/set clipboard" { const input = "52;s;?"; for (input) |ch| p.next(ch); - const cmd = p.end().?; + const cmd = p.end(null).?; try testing.expect(cmd == .clipboard_contents); try testing.expect(cmd.clipboard_contents.kind == 's'); try testing.expect(std.mem.eql(u8, "?", cmd.clipboard_contents.data)); @@ -709,7 +697,7 @@ test "OSC: get/set clipboard (optional parameter)" { const input = "52;;?"; for (input) |ch| p.next(ch); - const cmd = p.end().?; + const cmd = p.end(null).?; try testing.expect(cmd == .clipboard_contents); try testing.expect(cmd.clipboard_contents.kind == 'c'); try testing.expect(std.mem.eql(u8, "?", cmd.clipboard_contents.data)); @@ -723,7 +711,7 @@ test "OSC: report pwd" { const input = "7;file:///tmp/example"; for (input) |ch| p.next(ch); - const cmd = p.end().?; + const cmd = p.end(null).?; try testing.expect(cmd == .report_pwd); try testing.expect(std.mem.eql(u8, "file:///tmp/example", cmd.report_pwd.value)); } @@ -736,7 +724,7 @@ test "OSC: pointer cursor" { const input = "22;pointer"; for (input) |ch| p.next(ch); - const cmd = p.end().?; + const cmd = p.end(null).?; try testing.expect(cmd == .mouse_shape); try testing.expect(std.mem.eql(u8, "pointer", cmd.mouse_shape.value)); } @@ -749,7 +737,7 @@ test "OSC: report pwd empty" { const input = "7;"; for (input) |ch| p.next(ch); - try testing.expect(p.end() == null); + try testing.expect(p.end(null) == null); } test "OSC: longer than buffer" { @@ -760,7 +748,7 @@ test "OSC: longer than buffer" { const input = "a" ** (Parser.MAX_BUF + 2); for (input) |ch| p.next(ch); - try testing.expect(p.end() == null); + try testing.expect(p.end(null) == null); } test "OSC: report default foreground color" { @@ -771,11 +759,11 @@ test "OSC: report default foreground color" { const input = "10;?"; for (input) |ch| p.next(ch); - // This corresponds to ST = ESC \ - const cmd = p.endWithStringTerminator('\x1b').?; + // This corresponds to ST = ESC followed by \ + const cmd = p.end('\x1b').?; try testing.expect(cmd == .report_default_color); - try testing.expect(cmd.report_default_color.kind == .foreground); - try testing.expectEqualSlices(u8, cmd.report_default_color.string_terminator.?, "\x1b\\"); + try testing.expectEqual(cmd.report_default_color.kind, .foreground); + try testing.expectEqual(cmd.report_default_color.string_terminator, .st); } test "OSC: report default background color" { @@ -786,9 +774,9 @@ test "OSC: report default background color" { const input = "11;?"; for (input) |ch| p.next(ch); - // This corresponds to ST = BELL - const cmd = p.endWithStringTerminator('\x07').?; + // This corresponds to ST = BEL character + const cmd = p.end('\x07').?; try testing.expect(cmd == .report_default_color); - try testing.expect(cmd.report_default_color.kind == .background); - try testing.expectEqualSlices(u8, cmd.report_default_color.string_terminator.?, "\x07"); + try testing.expectEqual(cmd.report_default_color.kind, .background); + try testing.expectEqual(cmd.report_default_color.string_terminator.?, .bel); } diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index 5240b33b6..79d02a8e8 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -863,7 +863,10 @@ pub fn Stream(comptime Handler: type) type { .report_default_color => |v| { if (@hasDecl(T, "reportDefaultColor")) { - try self.handler.reportDefaultColor(if (v.kind == .foreground) "10" else "11", v.string_terminator); + try self.handler.reportDefaultColor( + if (v.kind == .foreground) "10" else "11", + if (v.string_terminator == .st) "\x1b\\" else "\x07", + ); } else log.warn("unimplemented OSC callback: {}", .{cmd}); }, diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 94deb6df5..453673761 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -1808,22 +1808,22 @@ const StreamHandler = struct { pub fn reportDefaultColor(self: *StreamHandler, osc_code: []const u8, string_terminator: ?[]const u8) !void { if (self.osc_color_report_format == .none) return; var msg: termio.Message = .{ .write_small = .{} }; - + const color = if (std.mem.eql(u8, osc_code, "10")) self.default_foreground_color else self.default_background_color; const resp = resp: { if (self.osc_color_report_format == .bits16) { break :resp try std.fmt.bufPrint(&msg.write_small.data, "\x1B]{s};rgb:{x:0>4}/{x:0>4}/{x:0>4}{s}", .{ osc_code, - @as(u16, self.default_foreground_color.r) * 257, - @as(u16, self.default_foreground_color.g) * 257, - @as(u16, self.default_foreground_color.b) * 257, + @as(u16, color.r) * 257, + @as(u16, color.g) * 257, + @as(u16, color.b) * 257, if (string_terminator) |st| st else "\x1b\\", }); } else { break :resp try std.fmt.bufPrint(&msg.write_small.data, "\x1B]{s};rgb:{x:0>2}/{x:0>2}/{x:0>2}{s}", .{ osc_code, - @as(u16, self.default_foreground_color.r), - @as(u16, self.default_foreground_color.g), - @as(u16, self.default_foreground_color.b), + @as(u16, color.r), + @as(u16, color.g), + @as(u16, color.b), if (string_terminator) |st| st else "\x1b\\", }); } From 19ef4a22a950474a25b5dbbc14e26126c2707e8e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 14 Sep 2023 13:12:41 -0700 Subject: [PATCH 3/4] terminal: stylistic changes for OSC terminators, 10/11 params --- src/terminal/main.zig | 1 + src/terminal/osc.zig | 65 ++++++++++++++++++++++++++++++++--------- src/terminal/stream.zig | 5 +--- src/termio/Exec.zig | 46 +++++++++++++++++++---------- 4 files changed, 84 insertions(+), 33 deletions(-) diff --git a/src/terminal/main.zig b/src/terminal/main.zig index 3de1835d9..23b032daf 100644 --- a/src/terminal/main.zig +++ b/src/terminal/main.zig @@ -6,6 +6,7 @@ const ansi = @import("ansi.zig"); const csi = @import("csi.zig"); const sgr = @import("sgr.zig"); pub const apc = @import("apc.zig"); +pub const osc = @import("osc.zig"); pub const point = @import("point.zig"); pub const color = @import("color.zig"); pub const kitty = @import("kitty.zig"); diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index c49ef38a0..03d3baf99 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -95,17 +95,53 @@ pub const Command = union(enum) { /// OSC 10 and OSC 11 default color report. report_default_color: struct { /// OSC 10 requests the foreground color, OSC 11 the background color. - kind: enum { foreground, background }, + kind: DefaultColorKind, /// We must reply with the same string terminator (ST) as used in the /// request. - string_terminator: ?enum { - /// The preferred string terminator is ESC followed by \ - st, - /// Some applications and terminals use BELL (0x07) as the string terminator. - bel, - } = null, + terminator: Terminator = .st, }, + + pub const DefaultColorKind = enum { + foreground, + background, + + pub fn code(self: DefaultColorKind) []const u8 { + return switch (self) { + .foreground => "10", + .background => "11", + }; + } + }; +}; + +/// The terminator used to end an OSC command. For OSC commands that demand +/// a response, we try to match the terminator used in the request since that +/// is most likely to be accepted by the calling program. +pub const Terminator = enum { + /// The preferred string terminator is ESC followed by \ + st, + + /// Some applications and terminals use BELL (0x07) as the string terminator. + bel, + + /// Initialize the terminator based on the last byte seen. If the + /// last byte is a BEL then we use BEL, otherwise we just assume ST. + pub fn init(ch: ?u8) Terminator { + return switch (ch orelse return .st) { + 0x07 => .bel, + else => .st, + }; + } + + /// The terminator as a string. This is static memory so it doesn't + /// need to be freed. + pub fn string(self: Terminator) []const u8 { + return switch (self) { + .st => "\x1b\\", + .bel => "\x07", + }; + } }; pub const Parser = struct { @@ -496,8 +532,10 @@ pub const Parser = struct { } /// End the sequence and return the command, if any. If the return value - /// is null, then no valid command was found. - pub fn end(self: *Parser, string_separator: ?u8) ?Command { + /// 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 + /// the response terminator. + pub fn end(self: *Parser, terminator_ch: ?u8) ?Command { if (!self.complete) { log.warn("invalid OSC command: {s}", .{self.buf[0..self.buf_idx]}); return null; @@ -512,11 +550,10 @@ pub const Parser = struct { } switch (self.command) { - .report_default_color => |*c| { - c.string_terminator = if (string_separator == 0x07) .bel else .st; - }, + .report_default_color => |*c| c.terminator = Terminator.init(terminator_ch), else => {}, } + return self.command; } }; @@ -763,7 +800,7 @@ test "OSC: report default foreground color" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .report_default_color); try testing.expectEqual(cmd.report_default_color.kind, .foreground); - try testing.expectEqual(cmd.report_default_color.string_terminator, .st); + try testing.expectEqual(cmd.report_default_color.terminator, .st); } test "OSC: report default background color" { @@ -778,5 +815,5 @@ test "OSC: report default background color" { const cmd = p.end('\x07').?; try testing.expect(cmd == .report_default_color); try testing.expectEqual(cmd.report_default_color.kind, .background); - try testing.expectEqual(cmd.report_default_color.string_terminator.?, .bel); + try testing.expectEqual(cmd.report_default_color.terminator, .bel); } diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index 79d02a8e8..824d08ffa 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -863,10 +863,7 @@ pub fn Stream(comptime Handler: type) type { .report_default_color => |v| { if (@hasDecl(T, "reportDefaultColor")) { - try self.handler.reportDefaultColor( - if (v.kind == .foreground) "10" else "11", - if (v.string_terminator == .st) "\x1b\\" else "\x07", - ); + try self.handler.reportDefaultColor(v.kind, v.terminator); } else log.warn("unimplemented OSC callback: {}", .{cmd}); }, diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 453673761..8dec213d2 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -1804,29 +1804,45 @@ const StreamHandler = struct { } } - /// Implements OSC 10 and OSC 11, which reports default foreground and background color respectively. - pub fn reportDefaultColor(self: *StreamHandler, osc_code: []const u8, string_terminator: ?[]const u8) !void { + /// Implements OSC 10 and OSC 11, which reports default foreground and + /// background color respectively. + pub fn reportDefaultColor( + self: *StreamHandler, + kind: terminal.osc.Command.DefaultColorKind, + terminator: terminal.osc.Terminator, + ) !void { if (self.osc_color_report_format == .none) return; + + const color = switch (kind) { + .foreground => self.default_foreground_color, + .background => self.default_background_color, + }; + var msg: termio.Message = .{ .write_small = .{} }; - const color = if (std.mem.eql(u8, osc_code, "10")) self.default_foreground_color else self.default_background_color; - const resp = resp: { - if (self.osc_color_report_format == .bits16) { - break :resp try std.fmt.bufPrint(&msg.write_small.data, "\x1B]{s};rgb:{x:0>4}/{x:0>4}/{x:0>4}{s}", .{ - osc_code, + const resp = switch (self.osc_color_report_format) { + .bits16 => 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, - if (string_terminator) |st| st else "\x1b\\", - }); - } else { - break :resp try std.fmt.bufPrint(&msg.write_small.data, "\x1B]{s};rgb:{x:0>2}/{x:0>2}/{x:0>2}{s}", .{ - osc_code, + 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), - if (string_terminator) |st| st else "\x1b\\", - }); - } + terminator.string(), + }, + ), }; msg.write_small.len = @intCast(resp.len); self.messageWriter(msg); From 14724290d8fed2dd32dbb657662bb932649b694b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 14 Sep 2023 13:15:09 -0700 Subject: [PATCH 4/4] config: change osc-color-report-format enum --- src/config/Config.zig | 12 ++++++------ src/termio/Exec.zig | 6 ++++-- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 69827c4c3..b5e4340dd 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -315,11 +315,11 @@ keybind: Keybinds = .{}, /// Allowable values are: /// /// * "none" - OSC 10/11 queries receive no reply -/// * "bits8" - Color components are return unscaled, i.e. rr/gg/bb -/// * "bits16" - Color components are returned scaled, e.g. rrrr/gggg/bbbb +/// * "8-bit" - Color components are return unscaled, i.e. rr/gg/bb +/// * "16-bit" - Color components are returned scaled, e.g. rrrr/gggg/bbbb /// -/// The default value is "bits16". -@"osc-color-report-format": OSCColorReportFormat = .bits16, +/// The default value is "16-bit". +@"osc-color-report-format": OSCColorReportFormat = .@"16-bit", /// If anything other than false, fullscreen mode on macOS will not use the /// native fullscreen, but make the window fullscreen without animations and @@ -1500,6 +1500,6 @@ pub const ShellIntegration = enum { /// OSC 10 and 11 default color reporting format. pub const OSCColorReportFormat = enum { none, - bits8, - bits16, + @"8-bit", + @"16-bit", }; diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 8dec213d2..06911050a 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -1820,7 +1820,7 @@ const StreamHandler = struct { var msg: termio.Message = .{ .write_small = .{} }; const resp = switch (self.osc_color_report_format) { - .bits16 => try std.fmt.bufPrint( + .@"16-bit" => try std.fmt.bufPrint( &msg.write_small.data, "\x1B]{s};rgb:{x:0>4}/{x:0>4}/{x:0>4}{s}", .{ @@ -1832,7 +1832,7 @@ const StreamHandler = struct { }, ), - else => try std.fmt.bufPrint( + .@"8-bit" => try std.fmt.bufPrint( &msg.write_small.data, "\x1B]{s};rgb:{x:0>2}/{x:0>2}/{x:0>2}{s}", .{ @@ -1843,6 +1843,8 @@ const StreamHandler = struct { terminator.string(), }, ), + + .none => unreachable, // early return above }; msg.write_small.len = @intCast(resp.len); self.messageWriter(msg);