xterm color operations compatibility and rewrite (#8590)

Replaces #7952
Fixes #7951 

This reimplements our color operation parsing completely to make it
fully compatible (as far as I can tell) with xterm. Our previous
implementation had numerous problems, I think because we kept addressing
singular compatibility issues as they were experienced in the field
rather than doing a proper thoughtful audit compared to the xterm
implementation. This PR does that audit.

**Specifically, this updates/adds: OSC 4, 5, 10-19, 104, 105, 110-119.**

To ease maintenance, understanding, and testing, I've pulled color
operation parsing out into a separate file and function that operates on
the full buffered OSC command. This is similar to Kitty protocols
previously. This hurts performance but that's acceptable to me for now
while we get compatibility down and test coverage added.

We can address more performance later if it becomes a bottleneck, but
these color operations are pretty rare.

I've associated each test with a `printf` command you can run in xterm
to compare.

## Xterm Divergence

We purposely diverge from xterm in some scenarios:

- Whitespace is allowed around x11 color names. Kitty allows this.
- Invalid index values for 104/105 are ignored. xterm typically halts
processing. Kitty allows this.

## TODO

- [x] Update our parser to use the new color parsing functions
- [x] Update the stream handler to use the new types
- [x] Fix our stream handler to emit on response per query
pull/8591/head
Mitchell Hashimoto 2025-09-11 12:38:52 -07:00 committed by GitHub
commit 2b24ac53f1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 1145 additions and 1223 deletions

View File

@ -863,18 +863,24 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
}, .unlocked);
},
.color_change => |change| {
.color_change => |change| color_change: {
// Notify our apprt, but don't send a mode 2031 DSR report
// because VT sequences were used to change the color.
_ = try self.rt_app.performAction(
.{ .surface = self },
.color_change,
.{
.kind = switch (change.kind) {
.background => .background,
.foreground => .foreground,
.cursor => .cursor,
.kind = switch (change.target) {
.palette => |v| @enumFromInt(v),
.dynamic => |dyn| switch (dyn) {
.foreground => .foreground,
.background => .background,
.cursor => .cursor,
// Unsupported dynamic color change notification type
else => break :color_change,
},
// Special colors aren't supported for change notification
.special => break :color_change,
},
.r = change.color.r,
.g = change.color.g,

View File

@ -78,10 +78,7 @@ pub const Message = union(enum) {
password_input: bool,
/// A terminal color was changed using OSC sequences.
color_change: struct {
kind: terminal.osc.Command.ColorOperation.Kind,
color: terminal.color.RGB,
},
color_change: terminal.osc.color.ColoredTarget,
/// Notifies the surface that a tick of the timer that is timing
/// out selection scrolling has occurred. "selection scrolling"

View File

@ -915,15 +915,15 @@ test "osc: 112 incomplete sequence" {
const cmd = a[0].?.osc_dispatch;
try testing.expect(cmd == .color_operation);
try testing.expectEqual(cmd.color_operation.terminator, .bel);
try testing.expect(cmd.color_operation.source == .reset_cursor);
try testing.expect(cmd.color_operation.operations.count() == 1);
var it = cmd.color_operation.operations.constIterator(0);
try testing.expect(cmd.color_operation.op == .osc_112);
try testing.expect(cmd.color_operation.requests.count() == 1);
var it = cmd.color_operation.requests.constIterator(0);
{
const op = it.next().?;
try testing.expect(op.* == .reset);
try testing.expectEqual(
osc.Command.ColorOperation.Kind.cursor,
op.reset,
osc.color.Request{ .reset = .{ .dynamic = .cursor } },
op.*,
);
}
try std.testing.expect(it.next() == null);

View File

@ -94,6 +94,85 @@ pub const Name = enum(u8) {
}
};
/// The "special colors" as denoted by xterm. These can be set via
/// OSC 5 or via OSC 4 by adding the palette length to it.
///
/// https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
pub const Special = enum(u3) {
bold = 0,
underline = 1,
blink = 2,
reverse = 3,
italic = 4,
pub fn osc4(self: Special) u16 {
// "The special colors can also be set by adding the maximum
// number of colors (e.g., 88 or 256) to these codes in an
// OSC 4 control" - xterm ctlseqs
const max = @typeInfo(Palette).array.len;
return @as(u16, @intCast(@intFromEnum(self))) + max;
}
test "osc4" {
const testing = std.testing;
try testing.expectEqual(256, Special.bold.osc4());
try testing.expectEqual(257, Special.underline.osc4());
try testing.expectEqual(258, Special.blink.osc4());
try testing.expectEqual(259, Special.reverse.osc4());
try testing.expectEqual(260, Special.italic.osc4());
}
};
test Special {
_ = Special;
}
/// The "dynamic colors" as denoted by xterm. These can be set via
/// OSC 10 through 19.
pub const Dynamic = enum(u5) {
foreground = 10,
background = 11,
cursor = 12,
pointer_foreground = 13,
pointer_background = 14,
tektronix_foreground = 15,
tektronix_background = 16,
highlight_background = 17,
tektronix_cursor = 18,
highlight_foreground = 19,
/// The next dynamic color sequentially. This is required because
/// specifying colors sequentially without their index will automatically
/// use the next dynamic color.
///
/// "Each successive parameter changes the next color in the list. The
/// value of Ps tells the starting point in the list."
pub fn next(self: Dynamic) ?Dynamic {
return std.meta.intToEnum(
Dynamic,
@intFromEnum(self) + 1,
) catch null;
}
test "next" {
const testing = std.testing;
try testing.expectEqual(.background, Dynamic.foreground.next());
try testing.expectEqual(.cursor, Dynamic.background.next());
try testing.expectEqual(.pointer_foreground, Dynamic.cursor.next());
try testing.expectEqual(.pointer_background, Dynamic.pointer_foreground.next());
try testing.expectEqual(.tektronix_foreground, Dynamic.pointer_background.next());
try testing.expectEqual(.tektronix_background, Dynamic.tektronix_foreground.next());
try testing.expectEqual(.highlight_background, Dynamic.tektronix_background.next());
try testing.expectEqual(.tektronix_cursor, Dynamic.highlight_background.next());
try testing.expectEqual(.highlight_foreground, Dynamic.tektronix_cursor.next());
try testing.expectEqual(null, Dynamic.highlight_foreground.next());
}
};
test Dynamic {
_ = Dynamic;
}
/// RGB
pub const RGB = packed struct(u24) {
r: u8 = 0,

File diff suppressed because it is too large Load Diff

705
src/terminal/osc/color.zig Normal file
View File

@ -0,0 +1,705 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const DynamicColor = @import("../color.zig").Dynamic;
const SpecialColor = @import("../color.zig").Special;
const RGB = @import("../color.zig").RGB;
pub const ParseError = Allocator.Error || error{
MissingOperation,
};
/// The possible operations we support for colors.
pub const Operation = enum {
osc_4,
osc_5,
osc_10,
osc_11,
osc_12,
osc_13,
osc_14,
osc_15,
osc_16,
osc_17,
osc_18,
osc_19,
osc_104,
osc_105,
osc_110,
osc_111,
osc_112,
osc_113,
osc_114,
osc_115,
osc_116,
osc_117,
osc_118,
osc_119,
};
/// Parse any color operation string. This should NOT include the operation
/// itself, but only the body of the operation. e.g. for "4;a;b;c" the body
/// should be "a;b;c" and the operation should be set accordingly.
///
/// Color parsing is fairly complicated so we pull this out to a specialized
/// function rather than go through our OSC parsing state machine. This is
/// much slower and requires more memory (since we need to buffer the full
/// request) but grants us an easier to understand and testable implementation.
///
/// If color changing ends up being a bottleneck we can optimize this later.
pub fn parse(
alloc: Allocator,
op: Operation,
buf: []const u8,
) ParseError!List {
var it = std.mem.tokenizeScalar(u8, buf, ';');
return switch (op) {
.osc_4 => try parseGetSetAnsiColor(alloc, .osc_4, &it),
.osc_5 => try parseGetSetAnsiColor(alloc, .osc_5, &it),
.osc_104 => try parseResetAnsiColor(alloc, .osc_104, &it),
.osc_105 => try parseResetAnsiColor(alloc, .osc_105, &it),
.osc_10 => try parseGetSetDynamicColor(alloc, .foreground, &it),
.osc_11 => try parseGetSetDynamicColor(alloc, .background, &it),
.osc_12 => try parseGetSetDynamicColor(alloc, .cursor, &it),
.osc_13 => try parseGetSetDynamicColor(alloc, .pointer_foreground, &it),
.osc_14 => try parseGetSetDynamicColor(alloc, .pointer_background, &it),
.osc_15 => try parseGetSetDynamicColor(alloc, .tektronix_foreground, &it),
.osc_16 => try parseGetSetDynamicColor(alloc, .tektronix_background, &it),
.osc_17 => try parseGetSetDynamicColor(alloc, .highlight_background, &it),
.osc_18 => try parseGetSetDynamicColor(alloc, .tektronix_cursor, &it),
.osc_19 => try parseGetSetDynamicColor(alloc, .highlight_foreground, &it),
.osc_110 => try parseResetDynamicColor(alloc, .foreground, &it),
.osc_111 => try parseResetDynamicColor(alloc, .background, &it),
.osc_112 => try parseResetDynamicColor(alloc, .cursor, &it),
.osc_113 => try parseResetDynamicColor(alloc, .pointer_foreground, &it),
.osc_114 => try parseResetDynamicColor(alloc, .pointer_background, &it),
.osc_115 => try parseResetDynamicColor(alloc, .tektronix_foreground, &it),
.osc_116 => try parseResetDynamicColor(alloc, .tektronix_background, &it),
.osc_117 => try parseResetDynamicColor(alloc, .highlight_background, &it),
.osc_118 => try parseResetDynamicColor(alloc, .tektronix_cursor, &it),
.osc_119 => try parseResetDynamicColor(alloc, .highlight_foreground, &it),
};
}
/// OSC 4/5
fn parseGetSetAnsiColor(
alloc: Allocator,
comptime op: Operation,
it: *std.mem.TokenIterator(u8, .scalar),
) Allocator.Error!List {
// Note: in ANY error scenario below we return the accumulated results.
// This matches the xterm behavior (see misc.c ChangeAnsiColorRequest)
var result: List = .{};
errdefer result.deinit(alloc);
while (true) {
// We expect a `c; spec` pair. If either doesn't exist then
// we return the results up to this point.
const color_str = it.next() orelse return result;
const spec_str = it.next() orelse return result;
// Color must be numeric. u9 because that'll fit our palette + special
const color: u9 = std.fmt.parseInt(
u9,
color_str,
10,
) catch return result;
// Parse the color.
const target: Target = switch (op) {
// OSC5 maps directly to the Special enum.
.osc_5 => .{ .special = std.meta.intToEnum(
SpecialColor,
std.math.cast(u3, color) orelse return result,
) catch return result },
// OSC4 maps 0-255 to palette, 256-259 to special offset
// by the palette count.
.osc_4 => if (std.math.cast(u8, color)) |idx| .{
.palette = idx,
} else .{ .special = std.meta.intToEnum(
SpecialColor,
std.math.cast(u3, color - 256) orelse return result,
) catch return result },
else => comptime unreachable,
};
// "?" always results in a query.
if (std.mem.eql(u8, spec_str, "?")) {
const req = try result.addOne(alloc);
req.* = .{ .query = target };
continue;
}
const rgb = RGB.parse(spec_str) catch return result;
const req = try result.addOne(alloc);
req.* = .{ .set = .{
.target = target,
.color = rgb,
} };
}
}
/// OSC 104/105: Reset ANSI Colors
fn parseResetAnsiColor(
alloc: Allocator,
comptime op: Operation,
it: *std.mem.TokenIterator(u8, .scalar),
) Allocator.Error!List {
// Note: xterm stops parsing the reset list on any error, but we're
// more flexible and try the next value. This matches the behavior of
// Kitty and I don't see a downside to being more flexible here. Hopefully
// no one depends on the exact behavior of xterm.
var result: List = .{};
errdefer result.deinit(alloc);
while (true) {
const color_str = it.next() orelse {
// If no parameters are given, we reset the full table.
if (result.count() == 0) {
const req = try result.addOne(alloc);
req.* = switch (op) {
.osc_104 => .reset_palette,
.osc_105 => .reset_special,
else => comptime unreachable,
};
}
return result;
};
// Empty color strings are ignored, not treated as an error.
if (color_str.len == 0) continue;
// Color must be numeric. u9 because that'll fit our palette + special
const color: u9 = std.fmt.parseInt(
u9,
color_str,
10,
) catch continue;
// Parse the color.
const target: Target = switch (op) {
// OSC105 maps directly to the Special enum.
.osc_105 => .{ .special = std.meta.intToEnum(
SpecialColor,
std.math.cast(u3, color) orelse continue,
) catch continue },
// OSC104 maps 0-255 to palette, 256-259 to special offset
// by the palette count.
.osc_104 => if (std.math.cast(u8, color)) |idx| .{
.palette = idx,
} else .{ .special = std.meta.intToEnum(
SpecialColor,
std.math.cast(u3, color - 256) orelse continue,
) catch continue },
else => comptime unreachable,
};
const req = try result.addOne(alloc);
req.* = .{ .reset = target };
}
}
/// OSC 10-19: Get/Set Dynamic Colors
fn parseGetSetDynamicColor(
alloc: Allocator,
start: DynamicColor,
it: *std.mem.TokenIterator(u8, .scalar),
) Allocator.Error!List {
// Note: in ANY error scenario below we return the accumulated results.
// This matches the xterm behavior (see misc.c ChangeColorsRequest)
var result: List = .{};
var color: DynamicColor = start;
while (true) {
const spec_str = it.next() orelse return result;
if (std.mem.eql(u8, spec_str, "?")) {
const req = try result.addOne(alloc);
req.* = .{ .query = .{ .dynamic = color } };
} else {
const rgb = RGB.parse(spec_str) catch return result;
const req = try result.addOne(alloc);
req.* = .{ .set = .{
.target = .{ .dynamic = color },
.color = rgb,
} };
}
// Each successive value uses the next color so long as it exists.
color = color.next() orelse return result;
}
}
/// OSC 110-119: Reset Dynamic Colors
fn parseResetDynamicColor(
alloc: Allocator,
color: DynamicColor,
it: *std.mem.TokenIterator(u8, .scalar),
) Allocator.Error!List {
var result: List = .{};
errdefer result.deinit(alloc);
if (it.next() != null) return result;
const req = try result.addOne(alloc);
req.* = .{ .reset = .{ .dynamic = color } };
return result;
}
/// A segmented list is used to avoid copying when many operations
/// are given in a single OSC. In most cases, OSC 4/104/etc. send
/// very few so the prealloc is optimized for that.
///
/// The exact prealloc value is chosen arbitrarily assuming most
/// color ops have very few. If we can get empirical data on more
/// typical values we can switch to that.
pub const List = std.SegmentedList(
Request,
2,
);
/// A single operation related to the terminal color palette.
pub const Request = union(enum) {
set: ColoredTarget,
query: Target,
reset: Target,
reset_palette,
reset_special,
};
pub const Target = union(enum) {
palette: u8,
special: SpecialColor,
dynamic: DynamicColor,
};
pub const ColoredTarget = struct {
target: Target,
color: RGB,
};
test "osc4" {
const testing = std.testing;
const alloc = testing.allocator;
// Test every palette index
for (0..std.math.maxInt(u8)) |idx| {
// Simple color set
// printf '\e]4;0;red\\'
{
const body = try std.fmt.allocPrint(
alloc,
"{d};red",
.{idx},
);
defer alloc.free(body);
var list = try parse(alloc, .osc_4, body);
defer list.deinit(alloc);
try testing.expectEqual(1, list.count());
try testing.expectEqual(
Request{ .set = .{
.target = .{ .palette = @intCast(idx) },
.color = RGB{ .r = 255, .g = 0, .b = 0 },
} },
list.at(0).*,
);
}
// Simple color query
// printf '\e]4;0;?\\'
{
const body = try std.fmt.allocPrint(
alloc,
"{d};?",
.{idx},
);
defer alloc.free(body);
var list = try parse(alloc, .osc_4, body);
defer list.deinit(alloc);
try testing.expectEqual(1, list.count());
try testing.expectEqual(
Request{ .query = .{ .palette = @intCast(idx) } },
list.at(0).*,
);
}
// Trailing invalid data produces results up to that point
// printf '\e]4;0;red;\e\\'
{
const body = try std.fmt.allocPrint(
alloc,
"{d};red;",
.{idx},
);
defer alloc.free(body);
var list = try parse(alloc, .osc_4, body);
defer list.deinit(alloc);
try testing.expectEqual(1, list.count());
try testing.expectEqual(
Request{ .set = .{
.target = .{ .palette = @intCast(idx) },
.color = RGB{ .r = 255, .g = 0, .b = 0 },
} },
list.at(0).*,
);
}
// Whitespace doesn't produce a working value in xterm but we
// allow it because Kitty does and it seems harmless.
//
// printf '\e]4;0;red \e\\'
{
const body = try std.fmt.allocPrint(
alloc,
"{d};red ",
.{idx},
);
defer alloc.free(body);
var list = try parse(alloc, .osc_4, body);
defer list.deinit(alloc);
try testing.expectEqual(1, list.count());
try testing.expectEqual(
Request{ .set = .{
.target = .{ .palette = @intCast(idx) },
.color = RGB{ .r = 255, .g = 0, .b = 0 },
} },
list.at(0).*,
);
}
}
// Test every special color
for (0..@typeInfo(SpecialColor).@"enum".fields.len) |i| {
const special = try std.meta.intToEnum(SpecialColor, i);
// Simple color set
// printf '\e]4;256;red\\'
{
const body = try std.fmt.allocPrint(
alloc,
"{d};red",
.{256 + i},
);
defer alloc.free(body);
var list = try parse(alloc, .osc_4, body);
defer list.deinit(alloc);
try testing.expectEqual(1, list.count());
try testing.expectEqual(
Request{ .set = .{
.target = .{ .special = special },
.color = RGB{ .r = 255, .g = 0, .b = 0 },
} },
list.at(0).*,
);
}
}
}
test "osc5" {
const testing = std.testing;
const alloc = testing.allocator;
// Test every special color
for (0..@typeInfo(SpecialColor).@"enum".fields.len) |i| {
const special = try std.meta.intToEnum(SpecialColor, i);
// Simple color set
// printf '\e]4;256;red\\'
{
const body = try std.fmt.allocPrint(
alloc,
"{d};red",
.{i},
);
defer alloc.free(body);
var list = try parse(alloc, .osc_5, body);
defer list.deinit(alloc);
try testing.expectEqual(1, list.count());
try testing.expectEqual(
Request{ .set = .{
.target = .{ .special = special },
.color = RGB{ .r = 255, .g = 0, .b = 0 },
} },
list.at(0).*,
);
}
}
}
test "osc4: multiple requests" {
const testing = std.testing;
const alloc = testing.allocator;
// printf '\e]4;0;red;1;blue\e\\'
{
var list = try parse(
alloc,
.osc_4,
"0;red;1;blue",
);
defer list.deinit(alloc);
try testing.expectEqual(2, list.count());
try testing.expectEqual(
Request{ .set = .{
.target = .{ .palette = 0 },
.color = RGB{ .r = 255, .g = 0, .b = 0 },
} },
list.at(0).*,
);
try testing.expectEqual(
Request{ .set = .{
.target = .{ .palette = 1 },
.color = RGB{ .r = 0, .g = 0, .b = 255 },
} },
list.at(1).*,
);
}
// Multiple requests with same index overwrite each other
// printf '\e]4;0;red;0;blue\e\\'
{
var list = try parse(
alloc,
.osc_4,
"0;red;0;blue",
);
defer list.deinit(alloc);
try testing.expectEqual(2, list.count());
try testing.expectEqual(
Request{ .set = .{
.target = .{ .palette = 0 },
.color = RGB{ .r = 255, .g = 0, .b = 0 },
} },
list.at(0).*,
);
try testing.expectEqual(
Request{ .set = .{
.target = .{ .palette = 0 },
.color = RGB{ .r = 0, .g = 0, .b = 255 },
} },
list.at(1).*,
);
}
}
test "osc104" {
const testing = std.testing;
const alloc = testing.allocator;
// Test every palette index
for (0..std.math.maxInt(u8)) |idx| {
// Simple color set
// printf '\e]104;0\\'
{
const body = try std.fmt.allocPrint(
alloc,
"{d}",
.{idx},
);
defer alloc.free(body);
var list = try parse(alloc, .osc_104, body);
defer list.deinit(alloc);
try testing.expectEqual(1, list.count());
try testing.expectEqual(
Request{ .reset = .{ .palette = @intCast(idx) } },
list.at(0).*,
);
}
}
// Test every special color
for (0..@typeInfo(SpecialColor).@"enum".fields.len) |i| {
const special = try std.meta.intToEnum(SpecialColor, i);
// Simple color set
// printf '\e]104;256\\'
{
const body = try std.fmt.allocPrint(
alloc,
"{d}",
.{256 + i},
);
defer alloc.free(body);
var list = try parse(alloc, .osc_104, body);
defer list.deinit(alloc);
try testing.expectEqual(1, list.count());
try testing.expectEqual(
Request{ .reset = .{ .special = special } },
list.at(0).*,
);
}
}
}
test "osc104 empty index" {
const testing = std.testing;
const alloc = testing.allocator;
var list = try parse(alloc, .osc_104, "0;;1");
defer list.deinit(alloc);
try testing.expectEqual(2, list.count());
try testing.expectEqual(
Request{ .reset = .{ .palette = 0 } },
list.at(0).*,
);
try testing.expectEqual(
Request{ .reset = .{ .palette = 1 } },
list.at(1).*,
);
}
test "osc104 invalid index" {
const testing = std.testing;
const alloc = testing.allocator;
var list = try parse(alloc, .osc_104, "ffff;1");
defer list.deinit(alloc);
try testing.expectEqual(1, list.count());
try testing.expectEqual(
Request{ .reset = .{ .palette = 1 } },
list.at(0).*,
);
}
test "osc104 reset all" {
const testing = std.testing;
const alloc = testing.allocator;
var list = try parse(alloc, .osc_104, "");
defer list.deinit(alloc);
try testing.expectEqual(1, list.count());
try testing.expectEqual(
Request{ .reset_palette = {} },
list.at(0).*,
);
}
test "osc105 reset all" {
const testing = std.testing;
const alloc = testing.allocator;
var list = try parse(alloc, .osc_105, "");
defer list.deinit(alloc);
try testing.expectEqual(1, list.count());
try testing.expectEqual(
Request{ .reset_special = {} },
list.at(0).*,
);
}
// OSC 10-19: Get/Set Dynamic Colors
test "dynamic" {
const testing = std.testing;
const alloc = testing.allocator;
inline for (@typeInfo(DynamicColor).@"enum".fields) |field| {
const color = @field(DynamicColor, field.name);
const op = @field(Operation, std.fmt.comptimePrint(
"osc_{d}",
.{field.value},
));
// Example script:
// printf '\e]10;red\e\\'
{
var list = try parse(alloc, op, "red");
defer list.deinit(alloc);
try testing.expectEqual(1, list.count());
try testing.expectEqual(
Request{ .set = .{
.target = .{ .dynamic = color },
.color = RGB{ .r = 255, .g = 0, .b = 0 },
} },
list.at(0).*,
);
}
}
}
test "dynamic multiple" {
const testing = std.testing;
const alloc = testing.allocator;
// Example script:
// printf '\e]11;red;blue\e\\'
{
var list = try parse(
alloc,
.osc_11,
"red;blue",
);
defer list.deinit(alloc);
try testing.expectEqual(2, list.count());
try testing.expectEqual(
Request{ .set = .{
.target = .{ .dynamic = .background },
.color = RGB{ .r = 255, .g = 0, .b = 0 },
} },
list.at(0).*,
);
try testing.expectEqual(
Request{ .set = .{
.target = .{ .dynamic = .cursor },
.color = RGB{ .r = 0, .g = 0, .b = 255 },
} },
list.at(1).*,
);
}
}
// OSC 110-119: Reset Dynamic Colors
test "reset dynamic" {
const testing = std.testing;
const alloc = testing.allocator;
inline for (@typeInfo(DynamicColor).@"enum".fields) |field| {
const color = @field(DynamicColor, field.name);
const op = @field(Operation, std.fmt.comptimePrint(
"osc_1{d}",
.{field.value},
));
// Example script:
// printf '\e]110\e\\'
{
var list = try parse(alloc, op, "");
defer list.deinit(alloc);
try testing.expectEqual(1, list.count());
try testing.expectEqual(
Request{ .reset = .{ .dynamic = color } },
list.at(0).*,
);
}
// xterm allows a trailing semicolon. script to verify:
//
// printf '\e]110;\e\\'
{
var list = try parse(alloc, op, ";");
defer list.deinit(alloc);
try testing.expectEqual(1, list.count());
try testing.expectEqual(
Request{ .reset = .{ .dynamic = color } },
list.at(0).*,
);
}
// xterm does NOT allow any whitespace
//
// printf '\e]110 \e\\'
{
var list = try parse(alloc, op, " ");
defer list.deinit(alloc);
try testing.expectEqual(0, list.count());
}
}
}

View File

@ -1565,7 +1565,11 @@ pub fn Stream(comptime Handler: type) type {
.color_operation => |v| {
if (@hasDecl(T, "handleColorOperation")) {
try self.handler.handleColorOperation(v.source, &v.operations, v.terminator);
try self.handler.handleColorOperation(
v.op,
&v.requests,
v.terminator,
);
return;
} else log.warn("unimplemented OSC callback: {}", .{cmd});
},

View File

@ -1187,12 +1187,15 @@ pub const StreamHandler = struct {
pub fn handleColorOperation(
self: *StreamHandler,
source: terminal.osc.Command.ColorOperation.Source,
operations: *const terminal.osc.Command.ColorOperation.List,
op: terminal.osc.color.Operation,
requests: *const terminal.osc.color.List,
terminator: terminal.osc.Terminator,
) !void {
// We'll need op one day if we ever implement reporting special colors.
_ = op;
// return early if there is nothing to do
if (operations.count() == 0) return;
if (requests.count() == 0) return;
var buffer: [1024]u8 = undefined;
var fba: std.heap.FixedBufferAllocator = .init(&buffer);
@ -1201,63 +1204,71 @@ pub const StreamHandler = struct {
var response: std.ArrayListUnmanaged(u8) = .empty;
const writer = response.writer(alloc);
var report: bool = false;
try writer.print("\x1b]{}", .{source});
var it = operations.constIterator(0);
while (it.next()) |op| {
switch (op.*) {
var it = requests.constIterator(0);
while (it.next()) |req| {
switch (req.*) {
.set => |set| {
switch (set.kind) {
switch (set.target) {
.palette => |i| {
self.terminal.flags.dirty.palette = true;
self.terminal.color_palette.colors[i] = set.color;
self.terminal.color_palette.mask.set(i);
},
.foreground => {
self.foreground_color = set.color;
_ = self.renderer_mailbox.push(.{
.foreground_color = set.color,
}, .{ .forever = {} });
},
.background => {
self.background_color = set.color;
_ = self.renderer_mailbox.push(.{
.background_color = set.color,
}, .{ .forever = {} });
},
.cursor => {
self.cursor_color = set.color;
_ = self.renderer_mailbox.push(.{
.cursor_color = set.color,
}, .{ .forever = {} });
.dynamic => |dynamic| switch (dynamic) {
.foreground => {
self.foreground_color = set.color;
_ = self.renderer_mailbox.push(.{
.foreground_color = set.color,
}, .{ .forever = {} });
},
.background => {
self.background_color = set.color;
_ = self.renderer_mailbox.push(.{
.background_color = set.color,
}, .{ .forever = {} });
},
.cursor => {
self.cursor_color = set.color;
_ = self.renderer_mailbox.push(.{
.cursor_color = set.color,
}, .{ .forever = {} });
},
.pointer_foreground,
.pointer_background,
.tektronix_foreground,
.tektronix_background,
.highlight_background,
.tektronix_cursor,
.highlight_foreground,
=> log.info("setting dynamic color {s} not implemented", .{
@tagName(dynamic),
}),
},
.special => log.info("setting special colors not implemented", .{}),
}
// Notify the surface of the color change
self.surfaceMessageWriter(.{ .color_change = .{
.kind = set.kind,
.target = set.target,
.color = set.color,
} });
},
.reset => |kind| {
switch (kind) {
.palette => |i| {
const mask = &self.terminal.color_palette.mask;
self.terminal.flags.dirty.palette = true;
self.terminal.color_palette.colors[i] = self.terminal.default_palette[i];
mask.unset(i);
.reset => |target| switch (target) {
.palette => |i| {
const mask = &self.terminal.color_palette.mask;
self.terminal.flags.dirty.palette = true;
self.terminal.color_palette.colors[i] = self.terminal.default_palette[i];
mask.unset(i);
self.surfaceMessageWriter(.{
.color_change = .{
.kind = .{ .palette = @intCast(i) },
.color = self.terminal.color_palette.colors[i],
},
});
},
self.surfaceMessageWriter(.{
.color_change = .{
.target = target,
.color = self.terminal.color_palette.colors[i],
},
});
},
.dynamic => |dynamic| switch (dynamic) {
.foreground => {
self.foreground_color = null;
_ = self.renderer_mailbox.push(.{
@ -1265,7 +1276,7 @@ pub const StreamHandler = struct {
}, .{ .forever = {} });
self.surfaceMessageWriter(.{ .color_change = .{
.kind = .foreground,
.target = target,
.color = self.default_foreground_color,
} });
},
@ -1276,7 +1287,7 @@ pub const StreamHandler = struct {
}, .{ .forever = {} });
self.surfaceMessageWriter(.{ .color_change = .{
.kind = .background,
.target = target,
.color = self.default_background_color,
} });
},
@ -1289,33 +1300,83 @@ pub const StreamHandler = struct {
if (self.default_cursor_color) |color| {
self.surfaceMessageWriter(.{ .color_change = .{
.kind = .cursor,
.target = target,
.color = color,
} });
}
},
}
.pointer_foreground,
.pointer_background,
.tektronix_foreground,
.tektronix_background,
.highlight_background,
.tektronix_cursor,
.highlight_foreground,
=> log.warn("resetting dynamic color {s} not implemented", .{
@tagName(dynamic),
}),
},
.special => log.info("resetting special colors not implemented", .{}),
},
.report => |kind| report: {
if (self.osc_color_report_format == .none) break :report;
.reset_palette => {
const mask = &self.terminal.color_palette.mask;
var mask_iterator = mask.iterator(.{});
while (mask_iterator.next()) |i| {
self.terminal.flags.dirty.palette = true;
self.terminal.color_palette.colors[i] = self.terminal.default_palette[i];
self.surfaceMessageWriter(.{
.color_change = .{
.target = .{ .palette = @intCast(i) },
.color = self.terminal.color_palette.colors[i],
},
});
}
mask.* = .initEmpty();
},
report = true;
.reset_special => log.warn(
"resetting all special colors not implemented",
.{},
),
.query => |kind| report: {
if (self.osc_color_report_format == .none) break :report;
const color = switch (kind) {
.palette => |i| self.terminal.color_palette.colors[i],
.foreground => self.foreground_color orelse self.default_foreground_color,
.background => self.background_color orelse self.default_background_color,
.cursor => self.cursor_color orelse
self.default_cursor_color orelse
self.foreground_color orelse
self.default_foreground_color,
.dynamic => |dynamic| switch (dynamic) {
.foreground => self.foreground_color orelse self.default_foreground_color,
.background => self.background_color orelse self.default_background_color,
.cursor => self.cursor_color orelse
self.default_cursor_color orelse
self.foreground_color orelse
self.default_foreground_color,
.pointer_foreground,
.pointer_background,
.tektronix_foreground,
.tektronix_background,
.highlight_background,
.tektronix_cursor,
.highlight_foreground,
=> {
log.info(
"reporting dynamic color {s} not implemented",
.{@tagName(dynamic)},
);
break :report;
},
},
.special => {
log.info("reporting special colors not implemented", .{});
break :report;
},
};
switch (self.osc_color_report_format) {
.@"16-bit" => switch (kind) {
.palette => |i| try writer.print(
";{d};rgb:{x:0>4}/{x:0>4}/{x:0>4}",
"\x1b]4;{d};rgb:{x:0>4}/{x:0>4}/{x:0>4}",
.{
i,
@as(u16, color.r) * 257,
@ -1323,19 +1384,21 @@ pub const StreamHandler = struct {
@as(u16, color.b) * 257,
},
),
else => try writer.print(
";rgb:{x:0>4}/{x:0>4}/{x:0>4}",
.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(
";{d};rgb:{x:0>2}/{x:0>2}/{x:0>2}",
"\x1b]4;{d};rgb:{x:0>2}/{x:0>2}/{x:0>2}",
.{
i,
@as(u16, color.r),
@ -1343,22 +1406,27 @@ pub const StreamHandler = struct {
@as(u16, color.b),
},
),
else => try writer.print(
";rgb:{x:0>2}/{x:0>2}/{x:0>2}",
.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,
},
.none => unreachable,
}
try writer.writeAll(terminator.string());
},
}
}
if (report) {
if (response.items.len > 0) {
// If any of the operations were reports, finalize the report
// string and send it to the terminal.
try writer.writeAll(terminator.string());