vaxis: add support for color reports (OSC 4, 10, 11, 12)

Add support for querying colors (index, foreground, background, and
cursor)
This commit is contained in:
Tim Culverhouse 2024-05-24 10:59:20 -05:00
parent fba8984cf8
commit 6b9f91986c
6 changed files with 132 additions and 0 deletions

View file

@ -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),
}
}
};

View file

@ -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)

View file

@ -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;

View file

@ -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();
}

View file

@ -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

View file

@ -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,