core: implement setting colors with OSC 4, 10, and 11

This commit is contained in:
Gregory Anders
2023-11-09 11:50:00 -06:00
parent 3b7e21df26
commit 006e93bd08
5 changed files with 224 additions and 3 deletions

View File

@ -262,6 +262,14 @@ fn drainMailbox(self: *Thread) !void {
try self.renderer.setFontSize(size);
},
.foreground_color => |color| {
self.renderer.config.foreground = color;
},
.background_color => |color| {
self.renderer.config.background = color;
},
.resize => |v| {
try self.renderer.setScreenSize(v.screen_size, v.padding);
},

View File

@ -3,6 +3,7 @@ const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const font = @import("../font/main.zig");
const renderer = @import("../renderer.zig");
const terminal = @import("../terminal/main.zig");
/// The messages that can be sent to a renderer thread.
pub const Message = union(enum) {
@ -20,6 +21,14 @@ pub const Message = union(enum) {
/// the size changes.
font_size: font.face.DesiredSize,
/// Change the foreground color. This can be done separately from changing
/// the config file in response to an OSC 10 command
foreground_color: terminal.color.RGB,
/// Change the background color. This can be done separately from changing
/// the config file in response to an OSC 11 command
background_color: terminal.color.RGB,
/// Changes the screen size.
resize: struct {
/// The full screen (drawable) size. This does NOT include padding.

View File

@ -105,6 +105,15 @@ pub const Command = union(enum) {
terminator: Terminator = .st,
},
set_default_color: struct {
/// OSC 4 sets a palette color, OSC 10 sets the foreground color, OSC 11
/// the background color.
kind: DefaultColorKind,
/// The color spec as a string
value: []const u8,
},
pub const DefaultColorKind = union(enum) {
foreground,
background,
@ -387,7 +396,16 @@ pub const Parser = struct {
self.complete = true;
},
else => self.state = .invalid,
else => {
self.command = .{ .set_default_color = .{
.kind = .{ .palette = @intCast(self.temp_state.num) },
.value = "",
} };
self.state = .string;
self.temp_state = .{ .str = &self.command.set_default_color.value };
self.buf_start = self.buf_idx - 1;
},
},
.@"5" => switch (c) {
@ -441,7 +459,16 @@ pub const Parser = struct {
self.command = .{ .report_default_color = .{ .kind = .foreground } };
self.complete = true;
},
else => self.state = .invalid,
else => {
self.command = .{ .set_default_color = .{
.kind = .foreground,
.value = "",
} };
self.state = .string;
self.temp_state = .{ .str = &self.command.set_default_color.value };
self.buf_start = self.buf_idx - 1;
},
},
.query_default_bg => switch (c) {
@ -449,7 +476,16 @@ pub const Parser = struct {
self.command = .{ .report_default_color = .{ .kind = .background } };
self.complete = true;
},
else => self.state = .invalid,
else => {
self.command = .{ .set_default_color = .{
.kind = .background,
.value = "",
} };
self.state = .string;
self.temp_state = .{ .str = &self.command.set_default_color.value };
self.buf_start = self.buf_idx - 1;
},
},
.semantic_prompt => switch (c) {
@ -949,6 +985,20 @@ test "OSC: report default foreground color" {
try testing.expectEqual(cmd.report_default_color.terminator, .st);
}
test "OSC: set foreground color" {
const testing = std.testing;
var p: Parser = .{};
const input = "10;rgbi:0.0/0.5/1.0";
for (input) |ch| p.next(ch);
const cmd = p.end('\x07').?;
try testing.expect(cmd == .set_default_color);
try testing.expectEqual(cmd.set_default_color.kind, .foreground);
try testing.expectEqualStrings(cmd.set_default_color.value, "rgbi:0.0/0.5/1.0");
}
test "OSC: report default background color" {
const testing = std.testing;
@ -964,6 +1014,20 @@ test "OSC: report default background color" {
try testing.expectEqual(cmd.report_default_color.terminator, .bel);
}
test "OSC: set background color" {
const testing = std.testing;
var p: Parser = .{};
const input = "11;rgb:f/ff/ffff";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
try testing.expect(cmd == .set_default_color);
try testing.expectEqual(cmd.set_default_color.kind, .background);
try testing.expectEqualStrings(cmd.set_default_color.value, "rgb:f/ff/ffff");
}
test "OSC: get palette color" {
const testing = std.testing;
@ -977,3 +1041,17 @@ test "OSC: get palette color" {
try testing.expectEqual(cmd.report_default_color.kind, .{ .palette = 1 });
try testing.expectEqual(cmd.report_default_color.terminator, .st);
}
test "OSC: set palette color" {
const testing = std.testing;
var p: Parser = .{};
const input = "4;17;rgb:aa/bb/cc";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
try testing.expect(cmd == .set_default_color);
try testing.expectEqual(cmd.set_default_color.kind, .{ .palette = 17 });
try testing.expectEqualStrings(cmd.set_default_color.value, "rgb:aa/bb/cc");
}

View File

@ -1052,6 +1052,13 @@ pub fn Stream(comptime Handler: type) type {
} else log.warn("unimplemented OSC callback: {}", .{cmd});
},
.set_default_color => |v| {
if (@hasDecl(T, "setDefaultColor")) {
try self.handler.setDefaultColor(v.kind, v.value);
return;
} else log.warn("unimplemented OSC callback: {}", .{cmd});
},
else => if (@hasDecl(T, "oscUnimplemented"))
try self.handler.oscUnimplemented(cmd)
else

View File

@ -2200,4 +2200,123 @@ const StreamHandler = struct {
msg.write_small.len = @intCast(resp.len);
self.messageWriter(msg);
}
/// Parse a color from a string of hexadecimal digits or a floating point
/// intensity value.
///
/// If `intensity` is false, the string can contain 1, 2, 3, or 4 characters
/// and represents the color value scaled in 4, 8, 12, or 16 bits,
/// respectively.
///
/// If `intensity` is true, the string should contain a floating point value
/// between 0.0 and 1.0, inclusive.
fn parseColor(value: []const u8, intensity: bool) !u8 {
if (intensity) {
const i = try std.fmt.parseFloat(f64, value);
if (i < 0.0 or i > 1.0) {
return error.InvalidValue;
}
return @intFromFloat(i * std.math.maxInt(u8));
}
if (value.len == 0 or value.len > 4) {
return error.InvalidValue;
}
const color = try std.fmt.parseUnsigned(u16, value, 16);
const divisor: usize = switch (value.len) {
1 => std.math.maxInt(u4),
2 => std.math.maxInt(u8),
3 => std.math.maxInt(u12),
4 => std.math.maxInt(u16),
else => unreachable,
};
return @intCast(color * std.math.maxInt(u8) / divisor);
}
/// Parse a color specification of the form
///
/// rgb:<red>/<green>/<blue>
///
/// <red>, <green>, <blue> := h | hh | hhh | hhhh
///
/// where `h` is a single hexadecimal digit.
///
/// Alternatively, the form
///
/// rgbi:<red>/<green>/<blue>
///
/// where <red>, <green>, and <blue> are floating point values between 0.0
/// and 1.0 (inclusive) is also accepted.
fn parseColorSpec(value: []const u8) !terminal.color.RGB {
const minimum_length = "rgb:a/a/a".len;
if (value.len < minimum_length or !std.mem.eql(u8, value[0..3], "rgb")) {
return error.InvalidFormat;
}
var i: usize = 3;
const use_intensity = if (value[i] == 'i') blk: {
i += 1;
break :blk true;
} else false;
if (value[i] != ':') {
return error.InvalidFormat;
}
i += 1;
const r = r: {
const slash_i = std.mem.indexOfScalarPos(u8, value, i, '/') orelse
return error.InvalidFormat;
const r = try parseColor(value[i..slash_i], use_intensity);
i = slash_i + 1;
break :r r;
};
const g = g: {
const slash_i = std.mem.indexOfScalarPos(u8, value, i, '/') orelse
return error.InvalidFormat;
const g = try parseColor(value[i..slash_i], use_intensity);
i = slash_i + 1;
break :g g;
};
const b = try parseColor(value[i..], use_intensity);
return terminal.color.RGB{
.r = r,
.g = g,
.b = b,
};
}
pub fn setDefaultColor(
self: *StreamHandler,
kind: terminal.osc.Command.DefaultColorKind,
value: []const u8,
) !void {
const color = try parseColorSpec(value);
switch (kind) {
.foreground => {
self.default_foreground_color = color;
_ = self.ev.renderer_mailbox.push(.{
.foreground_color = color,
}, .{ .forever = {} });
},
.background => {
self.default_background_color = color;
_ = self.ev.renderer_mailbox.push(.{
.background_color = color,
}, .{ .forever = {} });
},
.palette => |i| self.terminal.color_palette[i] = color,
}
}
};