terminal: OSC 4/5 matches xterm

pull/8590/head
Mitchell Hashimoto 2025-09-11 10:55:38 -07:00
parent aba8981d28
commit b4fdefc2c0
No known key found for this signature in database
GPG Key ID: 523D5DC389D273BC
1 changed files with 279 additions and 3 deletions

View File

@ -10,6 +10,8 @@ pub const ParseError = Allocator.Error || error{
/// The possible operations we support for colors.
pub const Operation = enum {
osc_4,
osc_5,
osc_110,
osc_111,
osc_112,
@ -39,6 +41,8 @@ pub fn parse(
) 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_110 => try parseResetDynamicColor(alloc, .foreground, &it),
.osc_111 => try parseResetDynamicColor(alloc, .background, &it),
.osc_112 => try parseResetDynamicColor(alloc, .cursor, &it),
@ -52,12 +56,74 @@ pub fn parse(
};
}
/// 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: Request.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 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 } };
@ -95,6 +161,216 @@ pub const Request = union(enum) {
};
};
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).*,
);
}
}
// OSC 110-119: Reset Dynamic Colors
test "reset dynamic" {
const testing = std.testing;
@ -111,7 +387,7 @@ test "reset dynamic" {
// printf '\e]110\e\\'
{
var list = try parse(alloc, op, "");
errdefer list.deinit(alloc);
defer list.deinit(alloc);
try testing.expectEqual(1, list.count());
try testing.expectEqual(
Request{ .reset = .{ .dynamic = color } },
@ -124,7 +400,7 @@ test "reset dynamic" {
// printf '\e]110;\e\\'
{
var list = try parse(alloc, op, ";");
errdefer list.deinit(alloc);
defer list.deinit(alloc);
try testing.expectEqual(1, list.count());
try testing.expectEqual(
Request{ .reset = .{ .dynamic = color } },
@ -137,7 +413,7 @@ test "reset dynamic" {
// printf '\e]110 \e\\'
{
var list = try parse(alloc, op, " ");
errdefer list.deinit(alloc);
defer list.deinit(alloc);
try testing.expectEqual(0, list.count());
}
}