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 querypull/8591/head
commit
2b24ac53f1
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
1351
src/terminal/osc.zig
1351
src/terminal/osc.zig
File diff suppressed because it is too large
Load Diff
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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});
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
Loading…
Reference in New Issue