diff --git a/src/config/Config.zig b/src/config/Config.zig index 26425fb12..deef6777e 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -551,15 +551,16 @@ keybind: Keybinds = .{}, @"shell-integration-features": ShellIntegrationFeatures = .{}, /// Sets the reporting format for OSC sequences that request color information. -/// Ghostty currently supports OSC 10 (foreground) and OSC 11 (background) queries, -/// and by default the reported values are scaled-up RGB values, where each component -/// are 16 bits. This is how most terminals report these values. However, some legacy -/// applications may require 8-bit, unscaled, components. We also support turning off -/// reporting alltogether. The components are lowercase hex values. +/// Ghostty currently supports OSC 10 (foreground), OSC 11 (background), and OSC +/// 4 (256 color palette) queries, and by default the reported values are +/// scaled-up RGB values, where each component are 16 bits. This is how most +/// terminals report these values. However, some legacy applications may require +/// 8-bit, unscaled, components. We also support turning off reporting +/// alltogether. The components are lowercase hex values. /// /// Allowable values are: /// -/// * "none" - OSC 10/11 queries receive no reply +/// * "none" - OSC 4/10/11 queries receive no reply /// * "8-bit" - Color components are return unscaled, i.e. rr/gg/bb /// * "16-bit" - Color components are returned scaled, e.g. rrrr/gggg/bbbb /// diff --git a/src/inspector/termio.zig b/src/inspector/termio.zig index 78a161069..1b9a45581 100644 --- a/src/inspector/termio.zig +++ b/src/inspector/termio.zig @@ -241,6 +241,24 @@ pub const VTEvent = struct { try alloc.dupeZ(u8, @tagName(value)), ), + .Union => |u| { + const Tag = u.tag_type orelse @compileError("Unions must have a tag"); + const tag_name = @tagName(@as(Tag, value)); + inline for (u.fields) |field| { + if (std.mem.eql(u8, field.name, tag_name)) { + const s = if (field.type == void) + try alloc.dupeZ(u8, tag_name) + else + try std.fmt.allocPrintZ(alloc, "{s}={}", .{ + tag_name, + @field(value, field.name), + }); + + try md.put(key, s); + } + } + }, + else => switch (Value) { u8 => try md.put( key, diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index d97ef6f21..6d75400ba 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -64,6 +64,18 @@ padding: renderer.Options.Padding, /// True if the window is focused focused: bool, +/// The actual foreground color. May differ from the config foreground color if +/// changed by a terminal application +foreground_color: terminal.color.RGB, + +/// The actual background color. May differ from the config background color if +/// changed by a terminal application +background_color: terminal.color.RGB, + +/// The actual cursor color. May differ from the config cursor color if changed +/// by a terminal application +cursor_color: ?terminal.color.RGB, + /// The current set of cells to render. This is rebuilt on every frame /// but we keep this around so that we don't reallocate. Each set of /// cells goes into a separate shader. @@ -254,6 +266,9 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { .screen_size = null, .padding = options.padding, .focused = true, + .foreground_color = options.config.foreground, + .background_color = options.config.background, + .cursor_color = options.config.cursor_color, // Render state .cells_bg = .{}, @@ -461,15 +476,15 @@ pub fn render( } // Swap bg/fg if the terminal is reversed - const bg = self.config.background; - const fg = self.config.foreground; + const bg = self.background_color; + const fg = self.foreground_color; defer { - self.config.background = bg; - self.config.foreground = fg; + self.background_color = bg; + self.foreground_color = fg; } if (state.terminal.modes.get(.reverse_colors)) { - self.config.background = fg; - self.config.foreground = bg; + self.background_color = fg; + self.foreground_color = bg; } // We used to share terminal state, but we've since learned through @@ -509,7 +524,7 @@ pub fn render( } break :critical .{ - .bg = self.config.background, + .bg = self.background_color, .selection = selection, .screen = screen_copy, .preedit = if (cursor_style != null) state.preedit else null, @@ -1267,21 +1282,21 @@ pub fn updateCell( const colors: BgFg = colors: { // If we are selected, we our colors are just inverted fg/bg var selection_res: ?BgFg = if (selected) .{ - .bg = self.config.selection_background orelse self.config.foreground, - .fg = self.config.selection_foreground orelse self.config.background, + .bg = self.config.selection_background orelse self.foreground_color, + .fg = self.config.selection_foreground orelse self.background_color, } else null; const res: BgFg = selection_res orelse if (!cell.attrs.inverse) .{ // In normal mode, background and fg match the cell. We // un-optionalize the fg by defaulting to our fg color. .bg = if (cell.attrs.has_bg) cell.bg else null, - .fg = if (cell.attrs.has_fg) cell.fg else self.config.foreground, + .fg = if (cell.attrs.has_fg) cell.fg else self.foreground_color, } else .{ // In inverted mode, the background MUST be set to something // (is never null) so it is either the fg or default fg. The // fg is either the bg or default background. - .bg = if (cell.attrs.has_fg) cell.fg else self.config.foreground, - .fg = if (cell.attrs.has_bg) cell.bg else self.config.background, + .bg = if (cell.attrs.has_fg) cell.fg else self.foreground_color, + .fg = if (cell.attrs.has_bg) cell.bg else self.background_color, }; // If the cell is "invisible" then we just make fg = bg so that @@ -1289,7 +1304,7 @@ pub fn updateCell( if (cell.attrs.invisible) { break :colors BgFg{ .bg = res.bg, - .fg = res.bg orelse self.config.background, + .fg = res.bg orelse self.background_color, }; } @@ -1316,7 +1331,7 @@ pub fn updateCell( // If we have a background and its not the default background // then we apply background opacity - if (cell.attrs.has_bg and !std.meta.eql(rgb, self.config.background)) { + if (cell.attrs.has_bg and !std.meta.eql(rgb, self.background_color)) { break :bg_alpha alpha; } @@ -1434,7 +1449,7 @@ fn addCursor( ), screen.cursor.x - 1 }; }; - const color = self.config.cursor_color orelse self.config.foreground; + const color = self.cursor_color orelse self.foreground_color; const alpha: u8 = if (!self.focused) 255 else alpha: { const alpha = 255 * self.config.cursor_opacity; break :alpha @intFromFloat(@ceil(alpha)); diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 6ee23fc92..bc8982b37 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -67,6 +67,18 @@ font_shaper: font.Shaper, /// True if the window is focused focused: bool, +/// The actual foreground color. May differ from the config foreground color if +/// changed by a terminal application +foreground_color: terminal.color.RGB, + +/// The actual background color. May differ from the config background color if +/// changed by a terminal application +background_color: terminal.color.RGB, + +/// The actual cursor color. May differ from the config cursor color if changed +/// by a terminal application +cursor_color: ?terminal.color.RGB, + /// Padding options padding: renderer.Options.Padding, @@ -310,6 +322,9 @@ pub fn init(alloc: Allocator, options: renderer.Options) !OpenGL { .font_shaper = shaper, .draw_background = options.config.background, .focused = true, + .foreground_color = options.config.foreground, + .background_color = options.config.background, + .cursor_color = options.config.cursor_color, .padding = options.padding, .surface_mailbox = options.surface_mailbox, .deferred_font_size = .{ .metrics = metrics }, @@ -586,15 +601,15 @@ pub fn render( } // Swap bg/fg if the terminal is reversed - const bg = self.config.background; - const fg = self.config.foreground; + const bg = self.background_color; + const fg = self.foreground_color; defer { - self.config.background = bg; - self.config.foreground = fg; + self.background_color = bg; + self.foreground_color = fg; } if (state.terminal.modes.get(.reverse_colors)) { - self.config.background = fg; - self.config.foreground = bg; + self.background_color = fg; + self.foreground_color = bg; } // We used to share terminal state, but we've since learned through @@ -627,7 +642,7 @@ pub fn render( ); break :critical .{ - .gl_bg = self.config.background, + .gl_bg = self.background_color, .selection = selection, .screen = screen_copy, .preedit = if (cursor_style != null) state.preedit else null, @@ -878,7 +893,7 @@ fn addCursor( ), screen.cursor.x - 1 }; }; - const color = self.config.cursor_color orelse self.config.foreground; + const color = self.cursor_color orelse self.foreground_color; const alpha: u8 = if (!self.focused) 255 else alpha: { const alpha = 255 * self.config.cursor_opacity; break :alpha @intFromFloat(@ceil(alpha)); @@ -1010,21 +1025,21 @@ pub fn updateCell( const colors: BgFg = colors: { // If we are selected, we our colors are just inverted fg/bg var selection_res: ?BgFg = if (selected) .{ - .bg = self.config.selection_background orelse self.config.foreground, - .fg = self.config.selection_foreground orelse self.config.background, + .bg = self.config.selection_background orelse self.foreground_color, + .fg = self.config.selection_foreground orelse self.background_color, } else null; const res: BgFg = selection_res orelse if (!cell.attrs.inverse) .{ // In normal mode, background and fg match the cell. We // un-optionalize the fg by defaulting to our fg color. .bg = if (cell.attrs.has_bg) cell.bg else null, - .fg = if (cell.attrs.has_fg) cell.fg else self.config.foreground, + .fg = if (cell.attrs.has_fg) cell.fg else self.foreground_color, } else .{ // In inverted mode, the background MUST be set to something // (is never null) so it is either the fg or default fg. The // fg is either the bg or default background. - .bg = if (cell.attrs.has_fg) cell.fg else self.config.foreground, - .fg = if (cell.attrs.has_bg) cell.bg else self.config.background, + .bg = if (cell.attrs.has_fg) cell.fg else self.foreground_color, + .fg = if (cell.attrs.has_bg) cell.bg else self.background_color, }; // If the cell is "invisible" then we just make fg = bg so that @@ -1032,7 +1047,7 @@ pub fn updateCell( if (cell.attrs.invisible) { break :colors BgFg{ .bg = res.bg, - .fg = res.bg orelse self.config.background, + .fg = res.bg orelse self.background_color, }; } @@ -1070,7 +1085,7 @@ pub fn updateCell( // If we have a background and its not the default background // then we apply background opacity - if (cell.attrs.has_bg and !std.meta.eql(rgb, self.config.background)) { + if (cell.attrs.has_bg and !std.meta.eql(rgb, self.background_color)) { break :bg_alpha alpha; } diff --git a/src/renderer/Thread.zig b/src/renderer/Thread.zig index f1c2280ca..17abd6325 100644 --- a/src/renderer/Thread.zig +++ b/src/renderer/Thread.zig @@ -262,6 +262,18 @@ fn drainMailbox(self: *Thread) !void { try self.renderer.setFontSize(size); }, + .foreground_color => |color| { + self.renderer.foreground_color = color; + }, + + .background_color => |color| { + self.renderer.background_color = color; + }, + + .cursor_color => |color| { + self.renderer.cursor_color = 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..3278a2c1c 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,18 @@ 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, + + /// Change the cursor color. This can be done separately from changing the + /// config file in response to an OSC 12 command. + cursor_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/Parser.zig b/src/terminal/Parser.zig index 032ac1bc6..200803647 100644 --- a/src/terminal/Parser.zig +++ b/src/terminal/Parser.zig @@ -827,7 +827,8 @@ test "osc: 112 incomplete sequence" { try testing.expect(a[2] == null); const cmd = a[0].?.osc_dispatch; - try testing.expect(cmd == .reset_cursor_color); + try testing.expect(cmd == .reset_color); + try testing.expectEqual(cmd.reset_color.kind, .cursor); } } diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 70e0e2a3a..c5f777006 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -75,8 +75,17 @@ scrolling_region: ScrollingRegion, /// The last reported pwd, if any. pwd: std.ArrayList(u8), -/// The color palette to use -color_palette: color.Palette = color.default, +/// The default color palette. This is only modified by changing the config file +/// and is used to reset the palette when receiving an OSC 104 command. +default_palette: color.Palette = color.default, + +/// The color palette to use. The mask indicates which palette indices have been +/// modified with OSC 4 +color_palette: struct { + const Mask = std.StaticBitSet(@typeInfo(color.Palette).Array.len); + colors: color.Palette = color.default, + mask: Mask = Mask.initEmpty(), +} = .{}, /// The previous printed character. This is used for the repeat previous /// char CSI (ESC [ b). @@ -560,12 +569,12 @@ pub fn setAttribute(self: *Terminal, attr: sgr.Attribute) !void { .@"8_fg" => |n| { self.screen.cursor.pen.attrs.has_fg = true; - self.screen.cursor.pen.fg = self.color_palette[@intFromEnum(n)]; + self.screen.cursor.pen.fg = self.color_palette.colors[@intFromEnum(n)]; }, .@"8_bg" => |n| { self.screen.cursor.pen.attrs.has_bg = true; - self.screen.cursor.pen.bg = self.color_palette[@intFromEnum(n)]; + self.screen.cursor.pen.bg = self.color_palette.colors[@intFromEnum(n)]; }, .reset_fg => self.screen.cursor.pen.attrs.has_fg = false, @@ -574,22 +583,22 @@ pub fn setAttribute(self: *Terminal, attr: sgr.Attribute) !void { .@"8_bright_fg" => |n| { self.screen.cursor.pen.attrs.has_fg = true; - self.screen.cursor.pen.fg = self.color_palette[@intFromEnum(n)]; + self.screen.cursor.pen.fg = self.color_palette.colors[@intFromEnum(n)]; }, .@"8_bright_bg" => |n| { self.screen.cursor.pen.attrs.has_bg = true; - self.screen.cursor.pen.bg = self.color_palette[@intFromEnum(n)]; + self.screen.cursor.pen.bg = self.color_palette.colors[@intFromEnum(n)]; }, .@"256_fg" => |idx| { self.screen.cursor.pen.attrs.has_fg = true; - self.screen.cursor.pen.fg = self.color_palette[idx]; + self.screen.cursor.pen.fg = self.color_palette.colors[idx]; }, .@"256_bg" => |idx| { self.screen.cursor.pen.attrs.has_bg = true; - self.screen.cursor.pen.bg = self.color_palette[idx]; + self.screen.cursor.pen.bg = self.color_palette.colors[idx]; }, .unknown => return error.InvalidAttribute, diff --git a/src/terminal/color.zig b/src/terminal/color.zig index 29e3f39de..eb2fecf5a 100644 --- a/src/terminal/color.zig +++ b/src/terminal/color.zig @@ -147,6 +147,112 @@ pub const RGB = struct { try std.testing.expectEqual(@as(usize, 24), @bitSizeOf(RGB)); try std.testing.expectEqual(@as(usize, 3), @sizeOf(RGB)); } + + /// Parse a color from a floating point intensity value. + /// + /// The value should be between 0.0 and 1.0, inclusive. + fn fromIntensity(value: []const u8) !u8 { + const i = std.fmt.parseFloat(f64, value) catch return error.InvalidFormat; + if (i < 0.0 or i > 1.0) { + return error.InvalidFormat; + } + + return @intFromFloat(i * std.math.maxInt(u8)); + } + + /// Parse a color from a string of hexadecimal digits + /// + /// The string can contain 1, 2, 3, or 4 characters and represents the color + /// value scaled in 4, 8, 12, or 16 bits, respectively. + fn fromHex(value: []const u8) !u8 { + if (value.len == 0 or value.len > 4) { + return error.InvalidFormat; + } + + const color = std.fmt.parseUnsigned(u16, value, 16) catch return error.InvalidFormat; + 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(@as(usize, 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. + pub fn parse(value: []const u8) !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 slice = if (std.mem.indexOfScalarPos(u8, value, i, '/')) |end| + value[i..end] + else + return error.InvalidFormat; + + i += slice.len + 1; + + break :r if (use_intensity) + try RGB.fromIntensity(slice) + else + try RGB.fromHex(slice); + }; + + const g = g: { + const slice = if (std.mem.indexOfScalarPos(u8, value, i, '/')) |end| + value[i..end] + else + return error.InvalidFormat; + + i += slice.len + 1; + + break :g if (use_intensity) + try RGB.fromIntensity(slice) + else + try RGB.fromHex(slice); + }; + + const b = if (use_intensity) + try RGB.fromIntensity(value[i..]) + else + try RGB.fromHex(value[i..]); + + return RGB{ + .r = r, + .g = g, + .b = b, + }; + } }; test "palette: default" { @@ -158,3 +264,23 @@ test "palette: default" { try testing.expectEqual(Name.default(@as(Name, @enumFromInt(i))), default[i]); } } + +test "RGB.parse" { + const testing = std.testing; + + try testing.expectEqual(RGB{ .r = 255, .g = 0, .b = 0 }, try RGB.parse("rgbi:1.0/0/0")); + try testing.expectEqual(RGB{ .r = 127, .g = 160, .b = 0 }, try RGB.parse("rgb:7f/a0a0/0")); + try testing.expectEqual(RGB{ .r = 255, .g = 255, .b = 255 }, try RGB.parse("rgb:f/ff/fff")); + + // Invalid format + try testing.expectError(error.InvalidFormat, RGB.parse("rgb;")); + try testing.expectError(error.InvalidFormat, RGB.parse("rgb:")); + try testing.expectError(error.InvalidFormat, RGB.parse(":a/a/a")); + try testing.expectError(error.InvalidFormat, RGB.parse("a/a/a")); + try testing.expectError(error.InvalidFormat, RGB.parse("rgb:a/a/a/")); + try testing.expectError(error.InvalidFormat, RGB.parse("rgb:00000///")); + try testing.expectError(error.InvalidFormat, RGB.parse("rgb:000/")); + try testing.expectError(error.InvalidFormat, RGB.parse("rgbi:a/a/a")); + try testing.expectError(error.InvalidFormat, RGB.parse("rgb:0.5/0.0/1.0")); + try testing.expectError(error.InvalidFormat, RGB.parse("rgb:not/hex/zz")); +} diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 823f9ff67..2e59eb279 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -64,10 +64,6 @@ pub const Command = union(enum) { // TODO: err option }, - /// Reset the color for the cursor. This reverts changes made with - /// change/read cursor color. - reset_cursor_color: void, - /// Set or get clipboard contents. If data is null, then the current /// clipboard contents are sent to the pty. If data is set, this /// contents is set on the clipboard. @@ -94,24 +90,49 @@ pub const Command = union(enum) { value: []const u8, }, - /// OSC 10 and OSC 11 default color report. - report_default_color: struct { - /// OSC 10 requests the foreground color, OSC 11 the background color. - kind: DefaultColorKind, + /// OSC 4, OSC 10, and OSC 11 color report. + report_color: struct { + /// OSC 4 requests a palette color, OSC 10 requests the foreground + /// color, OSC 11 the background color. + kind: ColorKind, /// We must reply with the same string terminator (ST) as used in the /// request. terminator: Terminator = .st, }, - pub const DefaultColorKind = enum { + /// Modify the foreground (OSC 10) or background color (OSC 11), or a palette color (OSC 4) + set_color: struct { + /// OSC 4 sets a palette color, OSC 10 sets the foreground color, OSC 11 + /// the background color. + kind: ColorKind, + + /// The color spec as a string + value: []const u8, + }, + + /// Reset a palette color (OSC 104) or the foreground (OSC 110), background + /// (OSC 111), or cursor (OSC 112) color. + reset_color: struct { + kind: ColorKind, + + /// OSC 104 can have parameters indicating which palette colors to + /// reset. + value: []const u8, + }, + + pub const ColorKind = union(enum) { + palette: u8, foreground, background, + cursor, - pub fn code(self: DefaultColorKind) []const u8 { + pub fn code(self: ColorKind) []const u8 { return switch (self) { + .palette => "4", .foreground => "10", .background => "11", + .cursor => "12", }; } }; @@ -195,21 +216,24 @@ pub const Parser = struct { @"1", @"10", @"11", + @"12", @"13", @"133", @"2", @"22", + @"4", @"5", @"52", @"7", - // OSC 10 is used to query the default foreground color, and to set the default foreground color. - // Only querying is currently supported. - query_default_fg, + // OSC 10 is used to query or set the current foreground color. + query_fg_color, - // OSC 11 is used to query the default background color, and to set the default background color. - // Only querying is currently supported. - query_default_bg, + // OSC 11 is used to query or set the current background color. + query_bg_color, + + // OSC 12 is used to query or set the current cursor color. + query_cursor_color, // We're in a semantic prompt OSC command but we aren't sure // what the command is yet, i.e. `133;` @@ -224,6 +248,13 @@ pub const Parser = struct { clipboard_kind, clipboard_kind_end, + // Get/set color palette index + color_palette_index, + color_palette_index_end, + + // Reset color palette index + reset_color_palette_index, + // Expect a string parameter. param_str must be set as well as // buf_start. string, @@ -277,6 +308,7 @@ pub const Parser = struct { '0' => self.state = .@"0", '1' => self.state = .@"1", '2' => self.state = .@"2", + '4' => self.state = .@"4", '5' => self.state = .@"5", '7' => self.state = .@"7", else => self.state = .invalid, @@ -296,22 +328,47 @@ pub const Parser = struct { .@"1" => switch (c) { '0' => self.state = .@"10", '1' => self.state = .@"11", + '2' => self.state = .@"12", '3' => self.state = .@"13", else => self.state = .invalid, }, .@"10" => switch (c) { - ';' => self.state = .query_default_fg, + ';' => self.state = .query_fg_color, + '4' => { + self.command = .{ .reset_color = .{ + .kind = .{ .palette = 0 }, + .value = "", + } }; + + self.state = .reset_color_palette_index; + self.complete = true; + }, else => self.state = .invalid, }, .@"11" => switch (c) { - ';' => self.state = .query_default_bg, - '2' => { + ';' => self.state = .query_bg_color, + '0' => { + self.command = .{ .reset_color = .{ .kind = .foreground, .value = undefined } }; self.complete = true; - self.command = .{ .reset_cursor_color = {} }; self.state = .invalid; }, + '1' => { + self.command = .{ .reset_color = .{ .kind = .background, .value = undefined } }; + self.complete = true; + self.state = .invalid; + }, + '2' => { + self.command = .{ .reset_color = .{ .kind = .cursor, .value = undefined } }; + self.complete = true; + self.state = .invalid; + }, + else => self.state = .invalid, + }, + + .@"12" => switch (c) { + ';' => self.state = .query_cursor_color, else => self.state = .invalid, }, @@ -348,6 +405,61 @@ pub const Parser = struct { else => self.state = .invalid, }, + .@"4" => switch (c) { + ';' => { + self.state = .color_palette_index; + self.buf_start = self.buf_idx; + }, + else => self.state = .invalid, + }, + + .color_palette_index => switch (c) { + '0'...'9' => {}, + ';' => { + if (std.fmt.parseUnsigned(u8, self.buf[self.buf_start .. self.buf_idx - 1], 10)) |num| { + self.state = .color_palette_index_end; + self.temp_state = .{ .num = num }; + } else |err| switch (err) { + error.Overflow => self.state = .invalid, + error.InvalidCharacter => unreachable, + } + }, + else => self.state = .invalid, + }, + + .color_palette_index_end => switch (c) { + '?' => { + self.command = .{ .report_color = .{ + .kind = .{ .palette = @intCast(self.temp_state.num) }, + } }; + + self.complete = true; + }, + else => { + self.command = .{ .set_color = .{ + .kind = .{ .palette = @intCast(self.temp_state.num) }, + .value = "", + } }; + + self.state = .string; + self.temp_state = .{ .str = &self.command.set_color.value }; + self.buf_start = self.buf_idx - 1; + }, + }, + + .reset_color_palette_index => switch (c) { + ';' => { + self.state = .string; + self.temp_state = .{ .str = &self.command.reset_color.value }; + self.buf_start = self.buf_idx; + self.complete = false; + }, + else => { + self.state = .invalid; + self.complete = false; + }, + }, + .@"5" => switch (c) { '2' => self.state = .@"52", else => self.state = .invalid, @@ -394,20 +506,58 @@ pub const Parser = struct { else => self.state = .invalid, }, - .query_default_fg => switch (c) { + .query_fg_color => switch (c) { '?' => { - self.command = .{ .report_default_color = .{ .kind = .foreground } }; + self.command = .{ .report_color = .{ .kind = .foreground } }; self.complete = true; + self.state = .invalid; + }, + else => { + self.command = .{ .set_color = .{ + .kind = .foreground, + .value = "", + } }; + + self.state = .string; + self.temp_state = .{ .str = &self.command.set_color.value }; + self.buf_start = self.buf_idx - 1; }, - else => self.state = .invalid, }, - .query_default_bg => switch (c) { + .query_bg_color => switch (c) { '?' => { - self.command = .{ .report_default_color = .{ .kind = .background } }; + self.command = .{ .report_color = .{ .kind = .background } }; self.complete = true; + self.state = .invalid; + }, + else => { + self.command = .{ .set_color = .{ + .kind = .background, + .value = "", + } }; + + self.state = .string; + self.temp_state = .{ .str = &self.command.set_color.value }; + self.buf_start = self.buf_idx - 1; + }, + }, + + .query_cursor_color => switch (c) { + '?' => { + self.command = .{ .report_color = .{ .kind = .cursor } }; + self.complete = true; + self.state = .invalid; + }, + else => { + self.command = .{ .set_color = .{ + .kind = .cursor, + .value = "", + } }; + + self.state = .string; + self.temp_state = .{ .str = &self.command.set_color.value }; + self.buf_start = self.buf_idx - 1; }, - else => self.state = .invalid, }, .semantic_prompt => switch (c) { @@ -616,7 +766,7 @@ pub const Parser = struct { } switch (self.command) { - .report_default_color => |*c| c.terminator = Terminator.init(terminator_ch), + .report_color => |*c| c.terminator = Terminator.init(terminator_ch), else => {}, } @@ -788,7 +938,7 @@ test "OSC: end_of_input" { try testing.expect(cmd == .end_of_input); } -test "OSC: reset_cursor_color" { +test "OSC: reset cursor color" { const testing = std.testing; var p: Parser = .{}; @@ -797,7 +947,8 @@ test "OSC: reset_cursor_color" { for (input) |ch| p.next(ch); const cmd = p.end(null).?; - try testing.expect(cmd == .reset_cursor_color); + try testing.expect(cmd == .reset_color); + try testing.expectEqual(cmd.reset_color.kind, .cursor); } test "OSC: get/set clipboard" { @@ -902,9 +1053,23 @@ test "OSC: report default foreground color" { // This corresponds to ST = ESC followed by \ const cmd = p.end('\x1b').?; - try testing.expect(cmd == .report_default_color); - try testing.expectEqual(cmd.report_default_color.kind, .foreground); - try testing.expectEqual(cmd.report_default_color.terminator, .st); + try testing.expect(cmd == .report_color); + try testing.expectEqual(cmd.report_color.kind, .foreground); + try testing.expectEqual(cmd.report_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_color); + try testing.expectEqual(cmd.set_color.kind, .foreground); + try testing.expectEqualStrings(cmd.set_color.value, "rgbi:0.0/0.5/1.0"); } test "OSC: report default background color" { @@ -917,7 +1082,49 @@ test "OSC: report default background color" { // This corresponds to ST = BEL character const cmd = p.end('\x07').?; - try testing.expect(cmd == .report_default_color); - try testing.expectEqual(cmd.report_default_color.kind, .background); - try testing.expectEqual(cmd.report_default_color.terminator, .bel); + try testing.expect(cmd == .report_color); + try testing.expectEqual(cmd.report_color.kind, .background); + try testing.expectEqual(cmd.report_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_color); + try testing.expectEqual(cmd.set_color.kind, .background); + try testing.expectEqualStrings(cmd.set_color.value, "rgb:f/ff/ffff"); +} + +test "OSC: get palette color" { + const testing = std.testing; + + var p: Parser = .{}; + + const input = "4;1;?"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?; + try testing.expect(cmd == .report_color); + try testing.expectEqual(cmd.report_color.kind, .{ .palette = 1 }); + try testing.expectEqual(cmd.report_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_color); + try testing.expectEqual(cmd.set_color.kind, .{ .palette = 17 }); + try testing.expectEqualStrings(cmd.set_color.value, "rgb:aa/bb/cc"); } diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index 6f27a9b06..e41b27849 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -1045,17 +1045,26 @@ pub fn Stream(comptime Handler: type) type { } else log.warn("unimplemented OSC callback: {}", .{cmd}); }, - .report_default_color => |v| { - if (@hasDecl(T, "reportDefaultColor")) { - try self.handler.reportDefaultColor(v.kind, v.terminator); + .report_color => |v| { + if (@hasDecl(T, "reportColor")) { + try self.handler.reportColor(v.kind, v.terminator); return; } else log.warn("unimplemented OSC callback: {}", .{cmd}); }, - else => if (@hasDecl(T, "oscUnimplemented")) - try self.handler.oscUnimplemented(cmd) - else - log.warn("unimplemented OSC command: {}", .{cmd}), + .set_color => |v| { + if (@hasDecl(T, "setColor")) { + try self.handler.setColor(v.kind, v.value); + return; + } else log.warn("unimplemented OSC callback: {}", .{cmd}); + }, + + .reset_color => |v| { + if (@hasDecl(T, "resetColor")) { + try self.handler.resetColor(v.kind, v.value); + return; + } else log.warn("unimplemented OSC callback: {}", .{cmd}); + }, } // Fall through for when we don't have a handler. diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index f48dbca0d..03891364f 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -69,13 +69,23 @@ grid_size: renderer.GridSize, /// it when a CSI q with default is called. default_cursor_style: terminal.Cursor.Style, default_cursor_blink: ?bool, +default_cursor_color: ?terminal.color.RGB, -/// Default foreground color for OSC 10 reporting. +/// Actual cursor color +cursor_color: ?terminal.color.RGB, + +/// Default foreground color as set by the config file default_foreground_color: terminal.color.RGB, -/// Default background color for OSC 11 reporting. +/// Default background color as set by the config file default_background_color: terminal.color.RGB, +/// Actual foreground color +foreground_color: terminal.color.RGB, + +/// Actual background color +background_color: terminal.color.RGB, + /// The OSC 10/11 reply style. osc_color_report_format: configpkg.Config.OSCColorReportFormat, @@ -90,6 +100,7 @@ pub const DerivedConfig = struct { image_storage_limit: usize, cursor_style: terminal.Cursor.Style, cursor_blink: ?bool, + cursor_color: ?configpkg.Config.Color, foreground: configpkg.Config.Color, background: configpkg.Config.Color, osc_color_report_format: configpkg.Config.OSCColorReportFormat, @@ -106,6 +117,7 @@ pub const DerivedConfig = struct { .image_storage_limit = config.@"image-storage-limit", .cursor_style = config.@"cursor-style", .cursor_blink = config.@"cursor-style-blink", + .cursor_color = config.@"cursor-color", .foreground = config.foreground, .background = config.background, .osc_color_report_format = config.@"osc-color-report-format", @@ -132,7 +144,7 @@ pub fn init(alloc: Allocator, opts: termio.Options) !Exec { opts.grid_size.rows, ); errdefer term.deinit(alloc); - term.color_palette = opts.config.palette; + term.default_palette = opts.config.palette; // Set the image size limits try term.screen.kitty_images.setLimit(alloc, opts.config.image_storage_limit); @@ -169,8 +181,18 @@ pub fn init(alloc: Allocator, opts: termio.Options) !Exec { .grid_size = opts.grid_size, .default_cursor_style = opts.config.cursor_style, .default_cursor_blink = opts.config.cursor_blink, + .default_cursor_color = if (opts.config.cursor_color) |col| + col.toTerminalRGB() + else + null, + .cursor_color = if (opts.config.cursor_color) |col| + col.toTerminalRGB() + else + null, .default_foreground_color = config.foreground.toTerminalRGB(), .default_background_color = config.background.toTerminalRGB(), + .foreground_color = config.foreground.toTerminalRGB(), + .background_color = config.background.toTerminalRGB(), .osc_color_report_format = config.osc_color_report_format, .data = null, }; @@ -236,8 +258,12 @@ pub fn threadEnter(self: *Exec, thread: *termio.Thread) !ThreadData { .grid_size = &self.grid_size, .default_cursor_style = self.default_cursor_style, .default_cursor_blink = self.default_cursor_blink, + .default_cursor_color = self.default_cursor_color, + .cursor_color = self.cursor_color, .default_foreground_color = self.default_foreground_color, .default_background_color = self.default_background_color, + .foreground_color = self.foreground_color, + .background_color = self.background_color, .osc_color_report_format = self.osc_color_report_format, }, @@ -322,11 +348,19 @@ pub fn changeConfig(self: *Exec, config: *DerivedConfig) !void { // Update the palette. Note this will only apply to new colors drawn // since we decode all palette colors to RGB on usage. - self.terminal.color_palette = config.palette; + self.terminal.default_palette = config.palette; // Update our default cursor style self.default_cursor_style = config.cursor_style; self.default_cursor_blink = config.cursor_blink; + self.default_cursor_color = if (config.cursor_color) |col| + col.toTerminalRGB() + else + null; + + // Update default foreground and background colors + self.default_foreground_color = config.foreground.toTerminalRGB(); + self.default_background_color = config.background.toTerminalRGB(); // If we have event data, then update our active stream too if (self.data) |data| { @@ -1326,8 +1360,23 @@ const StreamHandler = struct { default_cursor: bool = true, default_cursor_style: terminal.Cursor.Style, default_cursor_blink: ?bool, + default_cursor_color: ?terminal.color.RGB, + + /// Actual cursor color. This can be changed with OSC 12. + cursor_color: ?terminal.color.RGB, + + /// The default foreground and background color are those set by the user's + /// config file. These can be overridden by terminal applications using OSC + /// 10 and OSC 11, respectively. default_foreground_color: terminal.color.RGB, default_background_color: terminal.color.RGB, + + /// The actual foreground and background color. Normally this will be the + /// same as the default foreground and background color, unless changed by a + /// terminal application. + foreground_color: terminal.color.RGB, + background_color: terminal.color.RGB, + osc_color_report_format: configpkg.Config.OSCColorReportFormat, pub fn deinit(self: *StreamHandler) void { @@ -2150,18 +2199,20 @@ const StreamHandler = struct { } } - /// Implements OSC 10 and OSC 11, which reports default foreground and - /// background color respectively. - pub fn reportDefaultColor( + /// Implements OSC 4, OSC 10, and OSC 11, which reports palette color, + /// default foreground color, and background color respectively. + pub fn reportColor( self: *StreamHandler, - kind: terminal.osc.Command.DefaultColorKind, + kind: terminal.osc.Command.ColorKind, terminator: terminal.osc.Terminator, ) !void { if (self.osc_color_report_format == .none) return; const color = switch (kind) { - .foreground => self.default_foreground_color, - .background => self.default_background_color, + .palette => |i| self.terminal.color_palette.colors[i], + .foreground => self.foreground_color, + .background => self.background_color, + .cursor => self.cursor_color orelse self.foreground_color, }; var msg: termio.Message = .{ .write_small = .{} }; @@ -2195,4 +2246,88 @@ const StreamHandler = struct { msg.write_small.len = @intCast(resp.len); self.messageWriter(msg); } + + pub fn setColor( + self: *StreamHandler, + kind: terminal.osc.Command.ColorKind, + value: []const u8, + ) !void { + const color = try terminal.color.RGB.parse(value); + + switch (kind) { + .palette => |i| { + self.terminal.color_palette.colors[i] = color; + self.terminal.color_palette.mask.set(i); + }, + .foreground => { + self.foreground_color = color; + _ = self.ev.renderer_mailbox.push(.{ + .foreground_color = color, + }, .{ .forever = {} }); + }, + .background => { + self.background_color = color; + _ = self.ev.renderer_mailbox.push(.{ + .background_color = color, + }, .{ .forever = {} }); + }, + .cursor => { + self.cursor_color = color; + _ = self.ev.renderer_mailbox.push(.{ + .cursor_color = color, + }, .{ .forever = {} }); + }, + } + } + + pub fn resetColor( + self: *StreamHandler, + kind: terminal.osc.Command.ColorKind, + value: []const u8, + ) !void { + switch (kind) { + .palette => { + const mask = &self.terminal.color_palette.mask; + if (value.len == 0) { + // Find all bit positions in the mask which are set and + // reset those indices to the default palette + var it = mask.iterator(.{}); + while (it.next()) |i| { + log.warn("Resetting palette color {}", .{i}); + self.terminal.color_palette.colors[i] = self.terminal.default_palette[i]; + mask.unset(i); + } + } else { + var it = std.mem.tokenizeScalar(u8, value, ';'); + while (it.next()) |param| { + // Skip invalid parameters + const i = std.fmt.parseUnsigned(u8, param, 10) catch continue; + log.warn("Resetting palette color {}", .{i}); + if (mask.isSet(i)) { + self.terminal.color_palette.colors[i] = self.terminal.default_palette[i]; + mask.unset(i); + } + } + } + }, + .foreground => { + self.foreground_color = self.default_foreground_color; + _ = self.ev.renderer_mailbox.push(.{ + .foreground_color = self.foreground_color, + }, .{ .forever = {} }); + }, + .background => { + self.background_color = self.default_background_color; + _ = self.ev.renderer_mailbox.push(.{ + .background_color = self.background_color, + }, .{ .forever = {} }); + }, + .cursor => { + self.cursor_color = self.default_cursor_color; + _ = self.ev.renderer_mailbox.push(.{ + .cursor_color = self.cursor_color, + }, .{ .forever = {} }); + }, + } + } };