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.
This commit is contained in:
cryptocode
2023-09-14 14:53:31 +02:00
parent 2c070acdcd
commit a3696a9185
5 changed files with 177 additions and 1 deletions

View File

@ -305,6 +305,22 @@ keybind: Keybinds = .{},
/// The default value is "detect". /// The default value is "detect".
@"shell-integration": ShellIntegration = .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 /// If anything other than false, fullscreen mode on macOS will not use the
/// native fullscreen, but make the window fullscreen without animations and /// native fullscreen, but make the window fullscreen without animations and
/// using a new space. It's faster than the native fullscreen mode since it /// using a new space. It's faster than the native fullscreen mode since it
@ -1480,3 +1496,10 @@ pub const ShellIntegration = enum {
fish, fish,
zsh, zsh,
}; };
/// OSC 10 and 11 default color reporting format.
pub const OSCColorReportFormat = enum {
none,
bits8,
bits16,
};

View File

@ -248,7 +248,7 @@ pub fn next(self: *Parser, c: u8) [3]?Action {
return [3]?Action{ return [3]?Action{
// Exit depends on current state // Exit depends on current state
if (self.state == next_state) null else switch (self.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 } Action{ .osc_dispatch = cmd }
else else
null, null,

View File

@ -84,6 +84,7 @@ pub const Command = union(enum) {
value: []const u8, value: []const u8,
}, },
<<<<<<< HEAD
/// OSC 22. Set the mouse shape. There doesn't seem to be a standard /// 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 /// naming scheme for cursors but it looks like terminals such as Foot
/// are moving towards using the W3C CSS cursor names. For OSC parsing, /// are moving towards using the W3C CSS cursor names. For OSC parsing,
@ -91,6 +92,16 @@ pub const Command = union(enum) {
mouse_shape: struct { mouse_shape: struct {
value: []const u8, 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 { 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. // but the state space is small enough that we just build it up this way.
@"0", @"0",
@"1", @"1",
@"10",
@"11", @"11",
@"13", @"13",
@"133", @"133",
@ -143,6 +155,14 @@ pub const Parser = struct {
@"52", @"52",
@"7", @"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 // We're in a semantic prompt OSC command but we aren't sure
// what the command is yet, i.e. `133;` // what the command is yet, i.e. `133;`
semantic_prompt, semantic_prompt,
@ -210,12 +230,23 @@ pub const Parser = struct {
}, },
.@"1" => switch (c) { .@"1" => switch (c) {
'0' => self.state = .@"10",
'1' => self.state = .@"11", '1' => self.state = .@"11",
'3' => self.state = .@"13", '3' => self.state = .@"13",
else => self.state = .invalid, else => self.state = .invalid,
}, },
.@"10" => switch (c) {
';' => {
self.state = .osc_10;
},
else => self.state = .invalid,
},
.@"11" => switch (c) { .@"11" => switch (c) {
';' => {
self.state = .osc_11;
},
'2' => { '2' => {
self.complete = true; self.complete = true;
self.command = .{ .reset_cursor_color = {} }; self.command = .{ .reset_cursor_color = {} };
@ -303,6 +334,22 @@ pub const Parser = struct {
else => self.state = .invalid, 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) { .semantic_prompt => switch (c) {
'A' => { 'A' => {
self.state = .semantic_option_start; self.state = .semantic_option_start;
@ -466,6 +513,24 @@ pub const Parser = struct {
return self.command; 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" { test "OSC: change_window_title" {
@ -697,3 +762,33 @@ test "OSC: longer than buffer" {
try testing.expect(p.end() == null); 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");
}

View File

@ -861,6 +861,12 @@ pub fn Stream(comptime Handler: type) type {
} else log.warn("unimplemented OSC callback: {}", .{cmd}); } 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")) else => if (@hasDecl(T, "oscUnimplemented"))
try self.handler.oscUnimplemented(cmd) try self.handler.oscUnimplemented(cmd)
else else

View File

@ -68,6 +68,15 @@ grid_size: renderer.GridSize,
default_cursor_style: terminal.Cursor.Style, default_cursor_style: terminal.Cursor.Style,
default_cursor_blink: bool, 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. /// The data associated with the currently running thread.
data: ?*EventData, data: ?*EventData,
@ -79,6 +88,9 @@ pub const DerivedConfig = struct {
image_storage_limit: usize, image_storage_limit: usize,
cursor_style: terminal.Cursor.Style, cursor_style: terminal.Cursor.Style,
cursor_blink: bool, cursor_blink: bool,
foreground: configpkg.Config.Color,
background: configpkg.Config.Color,
osc_color_report_format: configpkg.Config.OSCColorReportFormat,
pub fn init( pub fn init(
alloc_gpa: Allocator, alloc_gpa: Allocator,
@ -91,6 +103,9 @@ pub const DerivedConfig = struct {
.image_storage_limit = config.@"image-storage-limit", .image_storage_limit = config.@"image-storage-limit",
.cursor_style = config.@"cursor-style", .cursor_style = config.@"cursor-style",
.cursor_blink = config.@"cursor-style-blink", .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, .grid_size = opts.grid_size,
.default_cursor_style = opts.config.cursor_style, .default_cursor_style = opts.config.cursor_style,
.default_cursor_blink = opts.config.cursor_blink, .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, .data = null,
}; };
} }
@ -204,6 +222,9 @@ pub fn threadEnter(self: *Exec, thread: *termio.Thread) !ThreadData {
.grid_size = &self.grid_size, .grid_size = &self.grid_size,
.default_cursor_style = self.default_cursor_style, .default_cursor_style = self.default_cursor_style,
.default_cursor_blink = self.default_cursor_blink, .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: bool = true,
default_cursor_style: terminal.Cursor.Style, default_cursor_style: terminal.Cursor.Style,
default_cursor_blink: bool, 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 { pub fn deinit(self: *StreamHandler) void {
self.apc.deinit(); self.apc.deinit();
@ -1779,4 +1803,32 @@ const StreamHandler = struct {
self.ev.seen_title = false; 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);
}
}; };