diff --git a/src/renderer/Thread.zig b/src/renderer/Thread.zig index f1c2280ca..35d0ee089 100644 --- a/src/renderer/Thread.zig +++ b/src/renderer/Thread.zig @@ -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); }, diff --git a/src/renderer/message.zig b/src/renderer/message.zig index d3fdc21de..873cbe7e5 100644 --- a/src/renderer/message.zig +++ b/src/renderer/message.zig @@ -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. diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 9c5ac9f9f..f73bcb6e8 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -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"); +} diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index 6f27a9b06..17cea8471 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -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 diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 5b0ef2a0c..e3c1a395c 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -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:// + /// + /// , , := h | hh | hhh | hhhh + /// + /// where `h` is a single hexadecimal digit. + /// + /// Alternatively, the form + /// + /// rgbi:// + /// + /// where , , and 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, + } + } };