From a3696a918590d16f3191bda8774d1ebe4f85aaab Mon Sep 17 00:00:00 2001 From: cryptocode Date: Thu, 14 Sep 2023 14:53:31 +0200 Subject: [PATCH] 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); + } };