mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-15 00:06:09 +03:00
Merge pull request #2113 from jcollie/osc-21
Implement Kitty Color Protocol (OSC 21)
This commit is contained in:
@ -259,6 +259,11 @@ pub const VTEvent = struct {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
.Struct => try md.put(
|
||||||
|
key,
|
||||||
|
try alloc.dupeZ(u8, @typeName(Value)),
|
||||||
|
),
|
||||||
|
|
||||||
else => switch (Value) {
|
else => switch (Value) {
|
||||||
u8 => try md.put(
|
u8 => try md.put(
|
||||||
key,
|
key,
|
||||||
|
@ -208,24 +208,41 @@ pub const RGB = struct {
|
|||||||
/// where <red>, <green>, and <blue> are floating point values between
|
/// where <red>, <green>, and <blue> are floating point values between
|
||||||
/// 0.0 and 1.0 (inclusive).
|
/// 0.0 and 1.0 (inclusive).
|
||||||
///
|
///
|
||||||
/// 3. #hhhhhh
|
/// 3. #rgb, #rrggbb, #rrrgggbbb #rrrrggggbbbb
|
||||||
///
|
///
|
||||||
/// where `h` is a single hexadecimal digit.
|
/// where `r`, `g`, and `b` are a single hexadecimal digit.
|
||||||
|
/// These specifiy a color with 4, 8, 12, and 16 bits of precision
|
||||||
|
/// per color channel.
|
||||||
pub fn parse(value: []const u8) !RGB {
|
pub fn parse(value: []const u8) !RGB {
|
||||||
if (value.len == 0) {
|
if (value.len == 0) {
|
||||||
return error.InvalidFormat;
|
return error.InvalidFormat;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value[0] == '#') {
|
if (value[0] == '#') {
|
||||||
if (value.len != 7) {
|
switch (value.len) {
|
||||||
return error.InvalidFormat;
|
4 => return RGB{
|
||||||
}
|
.r = try RGB.fromHex(value[1..2]),
|
||||||
|
.g = try RGB.fromHex(value[2..3]),
|
||||||
return RGB{
|
.b = try RGB.fromHex(value[3..4]),
|
||||||
|
},
|
||||||
|
7 => return RGB{
|
||||||
.r = try RGB.fromHex(value[1..3]),
|
.r = try RGB.fromHex(value[1..3]),
|
||||||
.g = try RGB.fromHex(value[3..5]),
|
.g = try RGB.fromHex(value[3..5]),
|
||||||
.b = try RGB.fromHex(value[5..7]),
|
.b = try RGB.fromHex(value[5..7]),
|
||||||
};
|
},
|
||||||
|
10 => return RGB{
|
||||||
|
.r = try RGB.fromHex(value[1..4]),
|
||||||
|
.g = try RGB.fromHex(value[4..7]),
|
||||||
|
.b = try RGB.fromHex(value[7..10]),
|
||||||
|
},
|
||||||
|
13 => return RGB{
|
||||||
|
.r = try RGB.fromHex(value[1..5]),
|
||||||
|
.g = try RGB.fromHex(value[5..9]),
|
||||||
|
.b = try RGB.fromHex(value[9..13]),
|
||||||
|
},
|
||||||
|
|
||||||
|
else => return error.InvalidFormat,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for X11 named colors. We allow whitespace around the edges
|
// Check for X11 named colors. We allow whitespace around the edges
|
||||||
@ -308,6 +325,9 @@ test "RGB.parse" {
|
|||||||
try testing.expectEqual(RGB{ .r = 127, .g = 160, .b = 0 }, try RGB.parse("rgb:7f/a0a0/0"));
|
try testing.expectEqual(RGB{ .r = 127, .g = 160, .b = 0 }, try RGB.parse("rgb:7f/a0a0/0"));
|
||||||
try testing.expectEqual(RGB{ .r = 255, .g = 255, .b = 255 }, try RGB.parse("rgb:f/ff/fff"));
|
try testing.expectEqual(RGB{ .r = 255, .g = 255, .b = 255 }, try RGB.parse("rgb:f/ff/fff"));
|
||||||
try testing.expectEqual(RGB{ .r = 255, .g = 255, .b = 255 }, try RGB.parse("#ffffff"));
|
try testing.expectEqual(RGB{ .r = 255, .g = 255, .b = 255 }, try RGB.parse("#ffffff"));
|
||||||
|
try testing.expectEqual(RGB{ .r = 255, .g = 255, .b = 255 }, try RGB.parse("#fff"));
|
||||||
|
try testing.expectEqual(RGB{ .r = 255, .g = 255, .b = 255 }, try RGB.parse("#fffffffff"));
|
||||||
|
try testing.expectEqual(RGB{ .r = 255, .g = 255, .b = 255 }, try RGB.parse("#ffffffffffff"));
|
||||||
try testing.expectEqual(RGB{ .r = 255, .g = 0, .b = 16 }, try RGB.parse("#ff0010"));
|
try testing.expectEqual(RGB{ .r = 255, .g = 0, .b = 16 }, try RGB.parse("#ff0010"));
|
||||||
|
|
||||||
try testing.expectEqual(RGB{ .r = 0, .g = 0, .b = 0 }, try RGB.parse("black"));
|
try testing.expectEqual(RGB{ .r = 0, .g = 0, .b = 0 }, try RGB.parse("black"));
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
//! Types and functions related to Kitty protocols.
|
//! Types and functions related to Kitty protocols.
|
||||||
|
|
||||||
const key = @import("kitty/key.zig");
|
const key = @import("kitty/key.zig");
|
||||||
|
pub const color = @import("kitty/color.zig");
|
||||||
pub const graphics = @import("kitty/graphics.zig");
|
pub const graphics = @import("kitty/graphics.zig");
|
||||||
|
|
||||||
pub const KeyFlags = key.Flags;
|
pub const KeyFlags = key.Flags;
|
||||||
|
92
src/terminal/kitty/color.zig
Normal file
92
src/terminal/kitty/color.zig
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const terminal = @import("../main.zig");
|
||||||
|
const RGB = terminal.color.RGB;
|
||||||
|
const Terminator = terminal.osc.Terminator;
|
||||||
|
|
||||||
|
pub const OSC = struct {
|
||||||
|
pub const Request = union(enum) {
|
||||||
|
query: Kind,
|
||||||
|
set: struct { key: Kind, color: RGB },
|
||||||
|
reset: Kind,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// list of requests
|
||||||
|
list: std.ArrayList(Request),
|
||||||
|
|
||||||
|
/// We must reply with the same string terminator (ST) as used in the
|
||||||
|
/// request.
|
||||||
|
terminator: Terminator = .st,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Kind = enum(u9) {
|
||||||
|
// Make sure that this stays in sync with the higest numbered enum
|
||||||
|
// value.
|
||||||
|
pub const max: u9 = 263;
|
||||||
|
|
||||||
|
// These _must_ start at 256 since enum values 0-255 are reserved
|
||||||
|
// for the palette.
|
||||||
|
foreground = 256,
|
||||||
|
background = 257,
|
||||||
|
selection_foreground = 258,
|
||||||
|
selection_background = 259,
|
||||||
|
cursor = 260,
|
||||||
|
cursor_text = 261,
|
||||||
|
visual_bell = 262,
|
||||||
|
second_transparent_background = 263,
|
||||||
|
_,
|
||||||
|
|
||||||
|
/// Return the palette index that this kind is representing
|
||||||
|
/// or null if its a special color.
|
||||||
|
pub fn palette(self: Kind) ?u8 {
|
||||||
|
return std.math.cast(u8, @intFromEnum(self)) orelse null;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn format(
|
||||||
|
self: Kind,
|
||||||
|
comptime layout: []const u8,
|
||||||
|
opts: std.fmt.FormatOptions,
|
||||||
|
writer: anytype,
|
||||||
|
) !void {
|
||||||
|
_ = layout;
|
||||||
|
_ = opts;
|
||||||
|
|
||||||
|
// Format as a number if its a palette color otherwise
|
||||||
|
// format as a string.
|
||||||
|
if (self.palette()) |idx| {
|
||||||
|
try writer.print("{}", .{idx});
|
||||||
|
} else {
|
||||||
|
try writer.print("{s}", .{@tagName(self)});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
test "OSC: kitty color protocol kind" {
|
||||||
|
const info = @typeInfo(Kind);
|
||||||
|
|
||||||
|
try std.testing.expectEqual(false, info.Enum.is_exhaustive);
|
||||||
|
|
||||||
|
var min: usize = std.math.maxInt(info.Enum.tag_type);
|
||||||
|
var max: usize = 0;
|
||||||
|
|
||||||
|
inline for (info.Enum.fields) |field| {
|
||||||
|
if (field.value > max) max = field.value;
|
||||||
|
if (field.value < min) min = field.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
try std.testing.expect(min >= 256);
|
||||||
|
try std.testing.expect(max == Kind.max);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "OSC: kitty color protocol kind string" {
|
||||||
|
const testing = std.testing;
|
||||||
|
|
||||||
|
var buf: [256]u8 = undefined;
|
||||||
|
{
|
||||||
|
const actual = try std.fmt.bufPrint(&buf, "{}", .{Kind.foreground});
|
||||||
|
try testing.expectEqualStrings("foreground", actual);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const actual = try std.fmt.bufPrint(&buf, "{}", .{@as(Kind, @enumFromInt(42))});
|
||||||
|
try testing.expectEqualStrings("42", actual);
|
||||||
|
}
|
||||||
|
}
|
@ -9,6 +9,8 @@ const std = @import("std");
|
|||||||
const mem = std.mem;
|
const mem = std.mem;
|
||||||
const assert = std.debug.assert;
|
const assert = std.debug.assert;
|
||||||
const Allocator = mem.Allocator;
|
const Allocator = mem.Allocator;
|
||||||
|
const RGB = @import("color.zig").RGB;
|
||||||
|
const kitty = @import("kitty.zig");
|
||||||
|
|
||||||
const log = std.log.scoped(.osc);
|
const log = std.log.scoped(.osc);
|
||||||
|
|
||||||
@ -137,6 +139,10 @@ pub const Command = union(enum) {
|
|||||||
value: []const u8,
|
value: []const u8,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Kitty color protocl, OSC 21
|
||||||
|
/// https://sw.kovidgoyal.net/kitty/color-stack/#id1
|
||||||
|
kitty_color_protocol: kitty.color.OSC,
|
||||||
|
|
||||||
/// Show a desktop notification (OSC 9 or OSC 777)
|
/// Show a desktop notification (OSC 9 or OSC 777)
|
||||||
show_desktop_notification: struct {
|
show_desktop_notification: struct {
|
||||||
title: []const u8,
|
title: []const u8,
|
||||||
@ -251,6 +257,7 @@ pub const Parser = struct {
|
|||||||
@"13",
|
@"13",
|
||||||
@"133",
|
@"133",
|
||||||
@"2",
|
@"2",
|
||||||
|
@"21",
|
||||||
@"22",
|
@"22",
|
||||||
@"4",
|
@"4",
|
||||||
@"5",
|
@"5",
|
||||||
@ -310,6 +317,11 @@ pub const Parser = struct {
|
|||||||
// If the parser has no allocator then it is treated as if the
|
// If the parser has no allocator then it is treated as if the
|
||||||
// buffer is full.
|
// buffer is full.
|
||||||
allocable_string,
|
allocable_string,
|
||||||
|
|
||||||
|
// Kitty color protocol
|
||||||
|
// https://sw.kovidgoyal.net/kitty/color-stack/#id1
|
||||||
|
kitty_color_protocol_key,
|
||||||
|
kitty_color_protocol_value,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// This must be called to clean up any allocated memory.
|
/// This must be called to clean up any allocated memory.
|
||||||
@ -329,6 +341,12 @@ pub const Parser = struct {
|
|||||||
alloc.destroy(ptr);
|
alloc.destroy(ptr);
|
||||||
self.buf_dynamic = null;
|
self.buf_dynamic = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Some commands have their own memory management we need to clear.
|
||||||
|
switch (self.command) {
|
||||||
|
.kitty_color_protocol => |*v| v.list.deinit(),
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Consume the next character c and advance the parser state.
|
/// Consume the next character c and advance the parser state.
|
||||||
@ -439,6 +457,7 @@ pub const Parser = struct {
|
|||||||
},
|
},
|
||||||
|
|
||||||
.@"2" => switch (c) {
|
.@"2" => switch (c) {
|
||||||
|
'1' => self.state = .@"21",
|
||||||
'2' => self.state = .@"22",
|
'2' => self.state = .@"22",
|
||||||
';' => {
|
';' => {
|
||||||
self.command = .{ .change_window_title = undefined };
|
self.command = .{ .change_window_title = undefined };
|
||||||
@ -450,6 +469,51 @@ pub const Parser = struct {
|
|||||||
else => self.state = .invalid,
|
else => self.state = .invalid,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
.@"21" => switch (c) {
|
||||||
|
';' => kitty: {
|
||||||
|
const alloc = self.alloc orelse {
|
||||||
|
log.info("OSC 21 requires an allocator, but none was provided", .{});
|
||||||
|
self.state = .invalid;
|
||||||
|
break :kitty;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.command = .{
|
||||||
|
.kitty_color_protocol = .{
|
||||||
|
.list = std.ArrayList(kitty.color.OSC.Request).init(alloc),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
self.state = .kitty_color_protocol_key;
|
||||||
|
self.complete = true;
|
||||||
|
self.buf_start = self.buf_idx;
|
||||||
|
},
|
||||||
|
else => self.state = .invalid,
|
||||||
|
},
|
||||||
|
|
||||||
|
.kitty_color_protocol_key => switch (c) {
|
||||||
|
';' => {
|
||||||
|
self.temp_state = .{ .key = self.buf[self.buf_start .. self.buf_idx - 1] };
|
||||||
|
self.endKittyColorProtocolOption(.key_only, false);
|
||||||
|
self.state = .kitty_color_protocol_key;
|
||||||
|
self.buf_start = self.buf_idx;
|
||||||
|
},
|
||||||
|
'=' => {
|
||||||
|
self.temp_state = .{ .key = self.buf[self.buf_start .. self.buf_idx - 1] };
|
||||||
|
self.state = .kitty_color_protocol_value;
|
||||||
|
self.buf_start = self.buf_idx;
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
},
|
||||||
|
|
||||||
|
.kitty_color_protocol_value => switch (c) {
|
||||||
|
';' => {
|
||||||
|
self.endKittyColorProtocolOption(.key_and_value, false);
|
||||||
|
self.state = .kitty_color_protocol_key;
|
||||||
|
self.buf_start = self.buf_idx;
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
},
|
||||||
|
|
||||||
.@"22" => switch (c) {
|
.@"22" => switch (c) {
|
||||||
';' => {
|
';' => {
|
||||||
self.command = .{ .mouse_shape = undefined };
|
self.command = .{ .mouse_shape = undefined };
|
||||||
@ -936,6 +1000,72 @@ pub const Parser = struct {
|
|||||||
self.temp_state.str.* = self.buf[self.buf_start..self.buf_idx];
|
self.temp_state.str.* = self.buf[self.buf_start..self.buf_idx];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn endKittyColorProtocolOption(self: *Parser, kind: enum { key_only, key_and_value }, final: bool) void {
|
||||||
|
if (self.temp_state.key.len == 0) {
|
||||||
|
log.warn("zero length key in kitty color protocol", .{});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For our key, we first try to parse it as a special key. If that
|
||||||
|
// doesn't work then we try to parse it as a number for a palette.
|
||||||
|
const key: kitty.color.Kind = std.meta.stringToEnum(
|
||||||
|
kitty.color.Kind,
|
||||||
|
self.temp_state.key,
|
||||||
|
) orelse @enumFromInt(std.fmt.parseUnsigned(
|
||||||
|
u8,
|
||||||
|
self.temp_state.key,
|
||||||
|
10,
|
||||||
|
) catch {
|
||||||
|
log.warn("unknown key in kitty color protocol: {s}", .{self.temp_state.key});
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
|
||||||
|
const value = value: {
|
||||||
|
if (self.buf_start == self.buf_idx) break :value "";
|
||||||
|
if (final) break :value std.mem.trim(u8, self.buf[self.buf_start..self.buf_idx], " ");
|
||||||
|
break :value std.mem.trim(u8, self.buf[self.buf_start .. self.buf_idx - 1], " ");
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (self.command) {
|
||||||
|
.kitty_color_protocol => |*v| {
|
||||||
|
// Cap our allocation amount for our list.
|
||||||
|
if (v.list.items.len >= @as(usize, kitty.color.Kind.max) * 2) {
|
||||||
|
self.state = .invalid;
|
||||||
|
log.warn("exceeded limit for number of keys in kitty color protocol, ignoring", .{});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kind == .key_only or value.len == 0) {
|
||||||
|
v.list.append(.{ .reset = key }) catch |err| {
|
||||||
|
log.warn("unable to append kitty color protocol option: {}", .{err});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
} else if (mem.eql(u8, "?", value)) {
|
||||||
|
v.list.append(.{ .query = key }) catch |err| {
|
||||||
|
log.warn("unable to append kitty color protocol option: {}", .{err});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
v.list.append(.{
|
||||||
|
.set = .{
|
||||||
|
.key = key,
|
||||||
|
.color = RGB.parse(value) catch |err| switch (err) {
|
||||||
|
error.InvalidFormat => {
|
||||||
|
log.warn("invalid color format in kitty color protocol: {s}", .{value});
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}) catch |err| {
|
||||||
|
log.warn("unable to append kitty color protocol option: {}", .{err});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn endAllocableString(self: *Parser) void {
|
fn endAllocableString(self: *Parser) void {
|
||||||
const list = self.buf_dynamic.?;
|
const list = self.buf_dynamic.?;
|
||||||
self.temp_state.str.* = list.items;
|
self.temp_state.str.* = list.items;
|
||||||
@ -958,11 +1088,14 @@ pub const Parser = struct {
|
|||||||
.hyperlink_uri => self.endHyperlink(),
|
.hyperlink_uri => self.endHyperlink(),
|
||||||
.string => self.endString(),
|
.string => self.endString(),
|
||||||
.allocable_string => self.endAllocableString(),
|
.allocable_string => self.endAllocableString(),
|
||||||
|
.kitty_color_protocol_key => self.endKittyColorProtocolOption(.key_only, true),
|
||||||
|
.kitty_color_protocol_value => self.endKittyColorProtocolOption(.key_and_value, true),
|
||||||
else => {},
|
else => {},
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (self.command) {
|
switch (self.command) {
|
||||||
.report_color => |*c| c.terminator = Terminator.init(terminator_ch),
|
.report_color => |*c| c.terminator = Terminator.init(terminator_ch),
|
||||||
|
.kitty_color_protocol => |*c| c.terminator = Terminator.init(terminator_ch),
|
||||||
else => {},
|
else => {},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1497,3 +1630,85 @@ test "OSC: hyperlink end" {
|
|||||||
const cmd = p.end('\x1b').?;
|
const cmd = p.end('\x1b').?;
|
||||||
try testing.expect(cmd == .hyperlink_end);
|
try testing.expect(cmd == .hyperlink_end);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "OSC: kitty color protocol" {
|
||||||
|
const testing = std.testing;
|
||||||
|
|
||||||
|
var p: Parser = .{ .alloc = testing.allocator };
|
||||||
|
defer p.deinit();
|
||||||
|
|
||||||
|
const input = "21;foreground=?;background=rgb:f0/f8/ff;cursor=aliceblue;cursor_text;visual_bell=;selection_foreground=#xxxyyzz;selection_background=?;selection_background=#aabbcc;2=?;3=rgbi:1.0/1.0/1.0";
|
||||||
|
for (input) |ch| p.next(ch);
|
||||||
|
|
||||||
|
const cmd = p.end('\x1b').?;
|
||||||
|
try testing.expect(cmd == .kitty_color_protocol);
|
||||||
|
try testing.expectEqual(@as(usize, 9), cmd.kitty_color_protocol.list.items.len);
|
||||||
|
{
|
||||||
|
const item = cmd.kitty_color_protocol.list.items[0];
|
||||||
|
try testing.expect(item == .query);
|
||||||
|
try testing.expectEqual(kitty.color.Kind.foreground, item.query);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const item = cmd.kitty_color_protocol.list.items[1];
|
||||||
|
try testing.expect(item == .set);
|
||||||
|
try testing.expectEqual(kitty.color.Kind.background, item.set.key);
|
||||||
|
try testing.expectEqual(@as(u8, 0xf0), item.set.color.r);
|
||||||
|
try testing.expectEqual(@as(u8, 0xf8), item.set.color.g);
|
||||||
|
try testing.expectEqual(@as(u8, 0xff), item.set.color.b);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const item = cmd.kitty_color_protocol.list.items[2];
|
||||||
|
try testing.expect(item == .set);
|
||||||
|
try testing.expectEqual(kitty.color.Kind.cursor, item.set.key);
|
||||||
|
try testing.expectEqual(@as(u8, 0xf0), item.set.color.r);
|
||||||
|
try testing.expectEqual(@as(u8, 0xf8), item.set.color.g);
|
||||||
|
try testing.expectEqual(@as(u8, 0xff), item.set.color.b);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const item = cmd.kitty_color_protocol.list.items[3];
|
||||||
|
try testing.expect(item == .reset);
|
||||||
|
try testing.expectEqual(kitty.color.Kind.cursor_text, item.reset);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const item = cmd.kitty_color_protocol.list.items[4];
|
||||||
|
try testing.expect(item == .reset);
|
||||||
|
try testing.expectEqual(kitty.color.Kind.visual_bell, item.reset);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const item = cmd.kitty_color_protocol.list.items[5];
|
||||||
|
try testing.expect(item == .query);
|
||||||
|
try testing.expectEqual(kitty.color.Kind.selection_background, item.query);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const item = cmd.kitty_color_protocol.list.items[6];
|
||||||
|
try testing.expect(item == .set);
|
||||||
|
try testing.expectEqual(kitty.color.Kind.selection_background, item.set.key);
|
||||||
|
try testing.expectEqual(@as(u8, 0xaa), item.set.color.r);
|
||||||
|
try testing.expectEqual(@as(u8, 0xbb), item.set.color.g);
|
||||||
|
try testing.expectEqual(@as(u8, 0xcc), item.set.color.b);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const item = cmd.kitty_color_protocol.list.items[7];
|
||||||
|
try testing.expect(item == .query);
|
||||||
|
try testing.expectEqual(@as(kitty.color.Kind, @enumFromInt(2)), item.query);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const item = cmd.kitty_color_protocol.list.items[8];
|
||||||
|
try testing.expect(item == .set);
|
||||||
|
try testing.expectEqual(@as(kitty.color.Kind, @enumFromInt(3)), item.set.key);
|
||||||
|
try testing.expectEqual(@as(u8, 0xff), item.set.color.r);
|
||||||
|
try testing.expectEqual(@as(u8, 0xff), item.set.color.g);
|
||||||
|
try testing.expectEqual(@as(u8, 0xff), item.set.color.b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "OSC: kitty color protocol without allocator" {
|
||||||
|
const testing = std.testing;
|
||||||
|
|
||||||
|
var p: Parser = .{};
|
||||||
|
defer p.deinit();
|
||||||
|
|
||||||
|
const input = "21;foreground=?";
|
||||||
|
for (input) |ch| p.next(ch);
|
||||||
|
try testing.expect(p.end('\x1b') == null);
|
||||||
|
}
|
||||||
|
@ -1393,6 +1393,13 @@ pub fn Stream(comptime Handler: type) type {
|
|||||||
} else log.warn("unimplemented OSC callback: {}", .{cmd});
|
} else log.warn("unimplemented OSC callback: {}", .{cmd});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
.kitty_color_protocol => |v| {
|
||||||
|
if (@hasDecl(T, "sendKittyColorReport")) {
|
||||||
|
try self.handler.sendKittyColorReport(v);
|
||||||
|
return;
|
||||||
|
} else log.warn("unimplemented OSC callback: {}", .{cmd});
|
||||||
|
},
|
||||||
|
|
||||||
.show_desktop_notification => |v| {
|
.show_desktop_notification => |v| {
|
||||||
if (@hasDecl(T, "showDesktopNotification")) {
|
if (@hasDecl(T, "showDesktopNotification")) {
|
||||||
try self.handler.showDesktopNotification(v.title, v.body);
|
try self.handler.showDesktopNotification(v.title, v.body);
|
||||||
|
@ -144,6 +144,38 @@ pub const StreamHandler = struct {
|
|||||||
self.termio_messaged = true;
|
self.termio_messaged = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Send a renderer message and unlock the renderer state mutex
|
||||||
|
/// if necessary to ensure we don't deadlock.
|
||||||
|
///
|
||||||
|
/// This assumes the renderer state mutex is locked.
|
||||||
|
inline fn rendererMessageWriter(
|
||||||
|
self: *StreamHandler,
|
||||||
|
msg: renderer.Message,
|
||||||
|
) void {
|
||||||
|
// See termio.Mailbox.send for more details on how this works.
|
||||||
|
|
||||||
|
// Try instant first. If it works then we can return.
|
||||||
|
if (self.renderer_mailbox.push(msg, .{ .instant = {} }) > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Instant would have blocked. Release the renderer mutex,
|
||||||
|
// wake up the renderer to allow it to process the message,
|
||||||
|
// and then try again.
|
||||||
|
self.renderer_state.mutex.unlock();
|
||||||
|
defer self.renderer_state.mutex.lock();
|
||||||
|
self.renderer_wakeup.notify() catch |err| {
|
||||||
|
// This is an EXTREMELY unlikely case. We still don't return
|
||||||
|
// and attempt to send the message because its most likely
|
||||||
|
// that everything is fine, but log in case a freeze happens.
|
||||||
|
log.warn(
|
||||||
|
"failed to notify renderer, may deadlock err={}",
|
||||||
|
.{err},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
_ = self.renderer_mailbox.push(msg, .{ .forever = {} });
|
||||||
|
}
|
||||||
|
|
||||||
pub fn dcsHook(self: *StreamHandler, dcs: terminal.DCS) !void {
|
pub fn dcsHook(self: *StreamHandler, dcs: terminal.DCS) !void {
|
||||||
var cmd = self.dcs.hook(self.alloc, dcs) orelse return;
|
var cmd = self.dcs.hook(self.alloc, dcs) orelse return;
|
||||||
defer cmd.deinit();
|
defer cmd.deinit();
|
||||||
@ -1273,4 +1305,135 @@ pub const StreamHandler = struct {
|
|||||||
.csi_21_t => self.surfaceMessageWriter(.{ .report_title = .csi_21_t }),
|
.csi_21_t => self.surfaceMessageWriter(.{ .report_title = .csi_21_t }),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn sendKittyColorReport(
|
||||||
|
self: *StreamHandler,
|
||||||
|
request: terminal.kitty.color.OSC,
|
||||||
|
) !void {
|
||||||
|
var buf = std.ArrayList(u8).init(self.alloc);
|
||||||
|
defer buf.deinit();
|
||||||
|
const writer = buf.writer();
|
||||||
|
try writer.writeAll("\x1b[21");
|
||||||
|
|
||||||
|
for (request.list.items) |item| {
|
||||||
|
switch (item) {
|
||||||
|
.query => |key| {
|
||||||
|
const color: terminal.color.RGB = switch (key) {
|
||||||
|
.foreground => self.foreground_color,
|
||||||
|
.background => self.background_color,
|
||||||
|
.cursor => self.cursor_color,
|
||||||
|
else => if (key.palette()) |idx|
|
||||||
|
self.terminal.color_palette.colors[idx]
|
||||||
|
else {
|
||||||
|
log.warn("ignoring unsupported kitty color protocol key: {}", .{key});
|
||||||
|
continue;
|
||||||
|
},
|
||||||
|
} orelse {
|
||||||
|
log.warn("no color configured for: {s}", .{@tagName(key)});
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
try writer.print(
|
||||||
|
";{}=rgb:{x:0>2}/{x:0>2}/{x:0>2}",
|
||||||
|
.{ key, color.r, color.g, color.b },
|
||||||
|
);
|
||||||
|
},
|
||||||
|
.set => |v| switch (v.key) {
|
||||||
|
.foreground => {
|
||||||
|
self.foreground_color = v.color;
|
||||||
|
|
||||||
|
// See messageWriter which has similar logic and
|
||||||
|
// explains why we may have to do this.
|
||||||
|
self.rendererMessageWriter(.{
|
||||||
|
.foreground_color = v.color,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
.background => {
|
||||||
|
self.background_color = v.color;
|
||||||
|
|
||||||
|
// See messageWriter which has similar logic and
|
||||||
|
// explains why we may have to do this.
|
||||||
|
self.rendererMessageWriter(.{
|
||||||
|
.background_color = v.color,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
.cursor => {
|
||||||
|
self.cursor_color = v.color;
|
||||||
|
|
||||||
|
// See messageWriter which has similar logic and
|
||||||
|
// explains why we may have to do this.
|
||||||
|
self.rendererMessageWriter(.{
|
||||||
|
.cursor_color = v.color,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
else => if (v.key.palette()) |i| {
|
||||||
|
self.terminal.flags.dirty.palette = true;
|
||||||
|
self.terminal.color_palette.colors[i] = v.color;
|
||||||
|
self.terminal.color_palette.mask.unset(i);
|
||||||
|
} else {
|
||||||
|
log.warn(
|
||||||
|
"ignoring unsupported kitty color protocol key: {}",
|
||||||
|
.{v.key},
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
.reset => |key| switch (key) {
|
||||||
|
.foreground => {
|
||||||
|
self.foreground_color = self.default_foreground_color;
|
||||||
|
|
||||||
|
// See messageWriter which has similar logic and
|
||||||
|
// explains why we may have to do this.
|
||||||
|
self.rendererMessageWriter(.{
|
||||||
|
.foreground_color = self.default_foreground_color,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
.background => {
|
||||||
|
self.background_color = self.default_background_color;
|
||||||
|
|
||||||
|
// See messageWriter which has similar logic and
|
||||||
|
// explains why we may have to do this.
|
||||||
|
self.rendererMessageWriter(.{
|
||||||
|
.background_color = self.default_background_color,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
.cursor => {
|
||||||
|
self.cursor_color = self.default_cursor_color;
|
||||||
|
|
||||||
|
// See messageWriter which has similar logic and
|
||||||
|
// explains why we may have to do this.
|
||||||
|
self.rendererMessageWriter(.{
|
||||||
|
.cursor_color = self.default_cursor_color,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
else => if (key.palette()) |i| {
|
||||||
|
self.terminal.flags.dirty.palette = true;
|
||||||
|
self.terminal.color_palette.colors[i] = self.terminal.default_palette[i];
|
||||||
|
self.terminal.color_palette.mask.unset(i);
|
||||||
|
} else {
|
||||||
|
log.warn(
|
||||||
|
"ignoring unsupported kitty color protocol key: {}",
|
||||||
|
.{key},
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try writer.writeAll(request.terminator.string());
|
||||||
|
|
||||||
|
self.messageWriter(.{
|
||||||
|
.write_alloc = .{
|
||||||
|
.alloc = self.alloc,
|
||||||
|
.data = try buf.toOwnedSlice(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Note: we don't have to do a queueRender here because every
|
||||||
|
// processed stream will queue a render once it is done processing
|
||||||
|
// the read() syscall.
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
Reference in New Issue
Block a user