From 6b9f91986c4790f831e1cb0981f083368b9b39c3 Mon Sep 17 00:00:00 2001 From: Tim Culverhouse Date: Fri, 24 May 2024 10:59:20 -0500 Subject: [PATCH] vaxis: add support for color reports (OSC 4, 10, 11, 12) Add support for querying colors (index, foreground, background, and cursor) --- src/Cell.zig | 56 +++++++++++++++++++++++++++++++++++++++++++++++++ src/Parser.zig | 45 +++++++++++++++++++++++++++++++++++++++ src/Tty.zig | 5 +++++ src/Vaxis.zig | 14 +++++++++++++ src/ctlseqs.zig | 10 +++++++++ src/event.zig | 2 ++ 6 files changed, 132 insertions(+) diff --git a/src/Cell.zig b/src/Cell.zig index e9d5fc4..c4364a2 100644 --- a/src/Cell.zig +++ b/src/Cell.zig @@ -105,6 +105,19 @@ pub const Color = union(enum) { index: u8, rgb: [3]u8, + pub const Kind = union(enum) { + fg, + bg, + cursor, + index: u8, + }; + + /// Returned when querying a color from the terminal + pub const Report = struct { + kind: Kind, + value: [3]u8, + }; + pub fn eql(a: Color, b: Color) bool { switch (a) { .default => return b == .default, @@ -136,4 +149,47 @@ pub const Color = union(enum) { }; return .{ .rgb = rgb }; } + + /// parse an XParseColor-style rgb specification into an rgb Color. The spec + /// is of the form: rgb:rrrr/gggg/bbbb. Generally, the high two bits will always + /// be the same as the low two bits. + pub fn rgbFromSpec(spec: []const u8) !Color { + var iter = std.mem.splitScalar(u8, spec, ':'); + const prefix = iter.next() orelse return error.InvalidColorSpec; + if (!std.mem.eql(u8, "rgb", prefix)) return error.InvalidColorSpec; + + const spec_str = iter.next() orelse return error.InvalidColorSpec; + + var spec_iter = std.mem.splitScalar(u8, spec_str, '/'); + + const r_raw = spec_iter.next() orelse return error.InvalidColorSpec; + if (r_raw.len != 4) return error.InvalidColorSpec; + + const g_raw = spec_iter.next() orelse return error.InvalidColorSpec; + if (g_raw.len != 4) return error.InvalidColorSpec; + + const b_raw = spec_iter.next() orelse return error.InvalidColorSpec; + if (b_raw.len != 4) return error.InvalidColorSpec; + + const r = try std.fmt.parseUnsigned(u8, r_raw[2..], 16); + const g = try std.fmt.parseUnsigned(u8, g_raw[2..], 16); + const b = try std.fmt.parseUnsigned(u8, b_raw[2..], 16); + + return .{ + .rgb = [_]u8{ r, g, b }, + }; + } + + test "rgbFromSpec" { + const spec = "rgb:aaaa/bbbb/cccc"; + const actual = try rgbFromSpec(spec); + switch (actual) { + .rgb => |rgb| { + try std.testing.expectEqual(0xAA, rgb[0]); + try std.testing.expectEqual(0xBB, rgb[1]); + try std.testing.expectEqual(0xCC, rgb[2]); + }, + else => try std.testing.expect(false), + } + } }; diff --git a/src/Parser.zig b/src/Parser.zig index 60db999..d10ec9e 100644 --- a/src/Parser.zig +++ b/src/Parser.zig @@ -1,5 +1,6 @@ const std = @import("std"); const testing = std.testing; +const Color = @import("Cell.zig").Color; const Event = @import("event.zig").Event; const Key = @import("Key.zig"); const Mouse = @import("Mouse.zig"); @@ -588,6 +589,50 @@ pub fn parse(self: *Parser, input: []const u8, paste_allocator: ?std.mem.Allocat seq.param_buf_idx = 0; seq.param_idx += 1; switch (p) { + 4, + 10, + 11, + 12, + => { + i += 1; + const index: ?u8 = if (p == 4) blk: { + const index_start = i; + const end: usize = while (i < n) : (i += 1) { + if (input[i] == ';') { + i += 1; + break i - 1; + } + } else unreachable; // invalid input + break :blk try std.fmt.parseUnsigned(u8, input[index_start..end], 10); + } else null; + const spec_start = i; + const end: usize = while (i < n) : (i += 1) { + if (input[i] == 0x1B) { + // advance one more for the backslash + i += 1; + break i - 1; + } + } else return .{ + .event = null, + .n = i, + }; + const color = try Color.rgbFromSpec(input[spec_start..end]); + + const event: Color.Report = .{ + .kind = switch (p) { + 4 => .{ .index = index.? }, + 10 => .fg, + 11 => .bg, + 12 => .cursor, + else => unreachable, + }, + .value = color.rgb, + }; + return .{ + .event = .{ .color_report = event }, + .n = i, + }; + }, 52 => { var payload: ?std.ArrayList(u8) = if (paste_allocator) |allocator| std.ArrayList(u8).init(allocator) diff --git a/src/Tty.zig b/src/Tty.zig index 5523f5a..643e5b4 100644 --- a/src/Tty.zig +++ b/src/Tty.zig @@ -219,6 +219,11 @@ pub fn run( paste_allocator.?.free(text); } }, + .color_report => |report| { + if (@hasField(Event, "color_report")) { + loop.postEvent(.{ .color_report = report }); + } + }, .cap_kitty_keyboard => { log.info("kitty keyboard capability detected", .{}); loop.vaxis.caps.kitty_keyboard = true; diff --git a/src/Vaxis.zig b/src/Vaxis.zig index 7d36e29..3f7ed0b 100644 --- a/src/Vaxis.zig +++ b/src/Vaxis.zig @@ -858,3 +858,17 @@ pub fn requestSystemClipboard(self: Vaxis) !void { ); try tty.flush(); } + +/// Request a color report from the terminal. Note: not all terminals support +/// reporting colors. It is always safe to try, but you may not receive a +/// response. +pub fn queryColor(self: Vaxis, kind: Cell.Color.Kind) !void { + var tty = self.tty orelse return; + switch (kind) { + .fg => _ = try tty.write(ctlseqs.osc10_query), + .bg => _ = try tty.write(ctlseqs.osc11_query), + .cursor => _ = try tty.write(ctlseqs.osc12_query), + .index => |idx| try tty.buffered_writer.writer().print(ctlseqs.osc4_query, .{idx}), + } + try tty.flush(); +} diff --git a/src/ctlseqs.zig b/src/ctlseqs.zig index 1fb9622..b0af893 100644 --- a/src/ctlseqs.zig +++ b/src/ctlseqs.zig @@ -113,3 +113,13 @@ pub const osc52_clipboard_request = "\x1b]52;c;?\x1b\\"; pub const kitty_graphics_clear = "\x1b_Ga=d\x1b\\"; pub const kitty_graphics_preamble = "\x1b_Ga=p,i={d}"; pub const kitty_graphics_closing = ",C=1\x1b\\"; + +// Color control sequences +pub const osc4_query = "\x1b]4;{d};?\x1b\\"; // color index {d} +pub const osc4_reset = "\x1b]104\x1b\\"; // this resets _all_ color indexes +pub const osc10_query = "\x1b]10;?\x1b\\"; // fg +pub const osc10_reset = "\x1b]110\x1b\\"; // reset fg to terminal default +pub const osc11_query = "\x1b]11;?\x1b\\"; // bg +pub const osc11_reset = "\x1b]111\x1b\\"; // reset bg to terminal default +pub const osc12_query = "\x1b]12;?\x1b\\"; // cursor color +pub const osc12_reset = "\x1b]112\x1b\\"; // reset cursor to terminal default diff --git a/src/event.zig b/src/event.zig index ba5cd29..a10910d 100644 --- a/src/event.zig +++ b/src/event.zig @@ -1,5 +1,6 @@ pub const Key = @import("Key.zig"); pub const Mouse = @import("Mouse.zig"); +pub const Color = @import("Cell.zig").Color; /// The events that Vaxis emits internally pub const Event = union(enum) { @@ -11,6 +12,7 @@ pub const Event = union(enum) { paste_start, // bracketed paste start paste_end, // bracketed paste end paste: []const u8, // osc 52 paste, caller must free + color_report: Color.Report, // osc 4, 10, 11, 12 response // these are delivered as discovered terminal capabilities cap_kitty_keyboard,