From 61b9f5ed1043061623ef39d5c46697a859451ca5 Mon Sep 17 00:00:00 2001 From: Riccardo Mazzarini Date: Sat, 9 May 2026 00:57:36 +0200 Subject: [PATCH] libghostty-vt: handle OSC color queries This PR enables libghostty-vt to handle the OSC 10/11/12 queries by moving the formatting of the corresponding resposes into `color.zig`. --- src/terminal/osc/parsers/color.zig | 60 +++++++++++++++++++++ src/terminal/stream_terminal.zig | 85 +++++++++++++++++++++++++++++- src/termio/stream_handler.zig | 51 ++---------------- 3 files changed, 148 insertions(+), 48 deletions(-) diff --git a/src/terminal/osc/parsers/color.zig b/src/terminal/osc/parsers/color.zig index 547c98eaf..512e06ba2 100644 --- a/src/terminal/osc/parsers/color.zig +++ b/src/terminal/osc/parsers/color.zig @@ -41,6 +41,66 @@ pub const Operation = enum { osc_119, }; +/// The color precision to use when formatting OSC color responses. +pub const ReportFormat = enum { + @"16-bit", + @"8-bit", +}; + +/// Encode an OSC color query response for a palette or dynamic color. +pub fn formatReport( + writer: anytype, + format: ReportFormat, + kind: Target, + color: RGB, +) !void { + switch (format) { + .@"16-bit" => switch (kind) { + .palette => |i| try writer.print( + "\x1b]4;{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, + }, + ), + .dynamic => |dynamic| try writer.print( + "\x1b]{d};rgb:{x:0>4}/{x:0>4}/{x:0>4}", + .{ + @intFromEnum(dynamic), + @as(u16, color.r) * 257, + @as(u16, color.g) * 257, + @as(u16, color.b) * 257, + }, + ), + .special => unreachable, + }, + + .@"8-bit" => switch (kind) { + .palette => |i| try writer.print( + "\x1b]4;{d};rgb:{x:0>2}/{x:0>2}/{x:0>2}", + .{ + i, + @as(u16, color.r), + @as(u16, color.g), + @as(u16, color.b), + }, + ), + .dynamic => |dynamic| try writer.print( + "\x1b]{d};rgb:{x:0>2}/{x:0>2}/{x:0>2}", + .{ + @intFromEnum(dynamic), + @as(u16, color.r), + @as(u16, color.g), + @as(u16, color.b), + }, + ), + .special => unreachable, + }, + } +} + /// Parse OSCs 4, 5, 10-19, 104, 110-119 pub fn parse(parser: *Parser, terminator_ch: ?u8) ?*Command { const alloc = parser.alloc orelse { diff --git a/src/terminal/stream_terminal.zig b/src/terminal/stream_terminal.zig index f68f088bf..6864fab65 100644 --- a/src/terminal/stream_terminal.zig +++ b/src/terminal/stream_terminal.zig @@ -10,6 +10,7 @@ const Action = stream.Action; const Screen = @import("Screen.zig"); const modes = @import("modes.zig"); const osc_color = @import("osc/parsers/color.zig"); +const osc = @import("osc.zig"); const kitty_color = @import("kitty/color.zig"); const size_report = @import("size_report.zig"); const Terminal = @import("Terminal.zig"); @@ -233,7 +234,7 @@ pub const Handler = struct { .end_hyperlink => self.terminal.screens.active.endHyperlink(), .semantic_prompt => try self.terminal.semanticPrompt(value), .mouse_shape => self.terminal.mouse_shape = value, - .color_operation => try self.colorOperation(value.op, &value.requests), + .color_operation => try self.colorOperation(value.op, &value.requests, value.terminator), .kitty_color_report => try self.kittyColorOperation(value), // APC @@ -552,10 +553,19 @@ pub const Handler = struct { self: *Handler, op: osc_color.Operation, requests: *const osc_color.List, + terminator: osc.Terminator, ) !void { _ = op; if (requests.count() == 0) return; + var stack = std.heap.stackFallback(256, self.terminal.gpa()); + const alloc = stack.get(); + + var aw: std.Io.Writer.Allocating = .init(alloc); + defer aw.deinit(); + + var wrote_response = false; + var it = requests.constIterator(0); while (it.next()) |req| { switch (req.*) { @@ -613,11 +623,51 @@ pub const Handler = struct { mask.* = .initEmpty(); }, - .query, + .query => |kind| report: { + if (self.effects.write_pty == null) break :report; + if (!try self.colorQuery(kind, &aw.writer)) break :report; + try aw.writer.writeAll(terminator.string()); + wrote_response = true; + }, + .reset_special, => {}, } } + + if (wrote_response) { + const resp = aw.toOwnedSliceSentinel(0) catch return; + defer alloc.free(resp); + self.writePty(resp); + } + } + + fn colorQuery( + self: *Handler, + kind: osc_color.Target, + writer: *std.Io.Writer, + ) !bool { + const color = switch (kind) { + .palette => |i| self.terminal.colors.palette.current[i], + .dynamic => |dynamic| switch (dynamic) { + .foreground => self.terminal.colors.foreground.get() orelse return false, + .background => self.terminal.colors.background.get() orelse return false, + .cursor => self.terminal.colors.cursor.get() orelse + self.terminal.colors.foreground.get() orelse return false, + .pointer_foreground, + .pointer_background, + .tektronix_foreground, + .tektronix_background, + .highlight_background, + .tektronix_cursor, + .highlight_foreground, + => return false, + }, + .special => return false, + }; + + try osc_color.formatReport(writer, .@"16-bit", kind, color); + return true; } fn kittyColorOperation( @@ -1020,6 +1070,37 @@ test "OSC 11 set and reset background color" { try testing.expect(t.colors.background.get() == null); } +test "OSC 10/11 queries write color reports" { + var t: Terminal = try .init(testing.allocator, .{ .cols = 10, .rows = 10 }); + defer t.deinit(testing.allocator); + + t.colors.foreground.default = .{ .r = 0x12, .g = 0x34, .b = 0x56 }; + t.colors.background.default = .{ .r = 0xaa, .g = 0xbb, .b = 0xcc }; + + const S = struct { + var buf: [128]u8 = undefined; + var written: []const u8 = ""; + + fn writePty(_: *Handler, data: [:0]const u8) void { + @memcpy(buf[0..data.len], data); + written = buf[0..data.len]; + } + }; + S.written = ""; + + var handler: Handler = .init(&t); + handler.effects.write_pty = &S.writePty; + + var s: Stream = .initAlloc(testing.allocator, handler); + defer s.deinit(); + + s.nextSlice("\x1b]10;?\x1b\\"); + try testing.expectEqualStrings("\x1b]10;rgb:1212/3434/5656\x1b\\", S.written); + + s.nextSlice("\x1b]11;?\x07"); + try testing.expectEqualStrings("\x1b]11;rgb:aaaa/bbbb/cccc\x07", S.written); +} + test "OSC 12 set and reset cursor color" { var t: Terminal = try .init(testing.allocator, .{ .cols = 10, .rows = 10 }); defer t.deinit(testing.allocator); diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index fb3a6b3ff..5eba02366 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -1368,53 +1368,12 @@ pub const StreamHandler = struct { }, }; - switch (self.osc_color_report_format) { - .@"16-bit" => switch (kind) { - .palette => |i| try writer.print( - "\x1b]4;{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, - }, - ), - .dynamic => |dynamic| try writer.print( - "\x1b]{d};rgb:{x:0>4}/{x:0>4}/{x:0>4}", - .{ - @intFromEnum(dynamic), - @as(u16, color.r) * 257, - @as(u16, color.g) * 257, - @as(u16, color.b) * 257, - }, - ), - .special => unreachable, - }, - - .@"8-bit" => switch (kind) { - .palette => |i| try writer.print( - "\x1b]4;{d};rgb:{x:0>2}/{x:0>2}/{x:0>2}", - .{ - i, - @as(u16, color.r), - @as(u16, color.g), - @as(u16, color.b), - }, - ), - .dynamic => |dynamic| try writer.print( - "\x1b]{d};rgb:{x:0>2}/{x:0>2}/{x:0>2}", - .{ - @intFromEnum(dynamic), - @as(u16, color.r), - @as(u16, color.g), - @as(u16, color.b), - }, - ), - .special => unreachable, - }, - + const report_format: terminal.osc.color.ReportFormat = switch (self.osc_color_report_format) { + .@"16-bit" => .@"16-bit", + .@"8-bit" => .@"8-bit", .none => unreachable, - } + }; + try terminal.osc.color.formatReport(writer, report_format, kind, color); try writer.writeAll(terminator.string()); },