diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 6ee23fc92..828ffa090 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -67,6 +67,14 @@ 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, + /// Padding options padding: renderer.Options.Padding, @@ -310,6 +318,8 @@ 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, .padding = options.padding, .surface_mailbox = options.surface_mailbox, .deferred_font_size = .{ .metrics = metrics }, @@ -586,15 +596,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 +637,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 +888,7 @@ fn addCursor( ), screen.cursor.x - 1 }; }; - const color = self.config.cursor_color orelse self.config.foreground; + const color = self.config.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 +1020,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 +1042,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 +1080,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/message.zig b/src/renderer/message.zig index 873cbe7e5..adb504109 100644 --- a/src/renderer/message.zig +++ b/src/renderer/message.zig @@ -22,11 +22,11 @@ pub const Message = union(enum) { 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 + /// 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 + /// the config file in response to an OSC 11 command. background_color: terminal.color.RGB, /// Changes the screen size. diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 70e0e2a3a..1ea3c94fc 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -75,8 +75,16 @@ 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 { + colors: color.Palette = color.default, + mask: u256 = 0, +} = .{}, /// The previous printed character. This is used for the repeat previous /// char CSI (ESC [ b). @@ -560,12 +568,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 +582,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/osc.zig b/src/terminal/osc.zig index e16b4e31a..cfe62b8b9 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -98,32 +98,43 @@ pub const Command = union(enum) { report_color: struct { /// OSC 4 requests a palette color, OSC 10 requests the foreground /// color, OSC 11 the background color. - kind: DefaultColorKind, + kind: ColorKind, /// We must reply with the same string terminator (ST) as used in the /// request. terminator: Terminator = .st, }, + /// 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: DefaultColorKind, + kind: ColorKind, /// The color spec as a string value: []const u8, }, - pub const DefaultColorKind = union(enum) { + /// 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, - palette: u8, - pub fn code(self: DefaultColorKind) []const u8 { + pub fn code(self: ColorKind) []const u8 { return switch (self) { + .palette => "4", .foreground => "10", .background => "11", - .palette => "4", }; } }; @@ -216,13 +227,11 @@ pub const Parser = struct { @"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, // We're in a semantic prompt OSC command but we aren't sure // what the command is yet, i.e. `133;` @@ -241,6 +250,9 @@ pub const Parser = struct { 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, @@ -319,12 +331,31 @@ pub const Parser = struct { }, .@"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, + ';' => self.state = .query_bg_color, + '0' => { + self.command = .{ .reset_color = .{ .kind = .foreground, .value = undefined } }; + self.complete = true; + self.state = .invalid; + }, + '1' => { + self.command = .{ .reset_color = .{ .kind = .background, .value = undefined } }; + self.complete = true; + self.state = .invalid; + }, '2' => { self.complete = true; self.command = .{ .reset_cursor_color = {} }; @@ -408,6 +439,19 @@ pub const Parser = struct { }, }, + .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, @@ -454,10 +498,11 @@ pub const Parser = struct { else => self.state = .invalid, }, - .query_default_fg => switch (c) { + .query_fg_color => switch (c) { '?' => { self.command = .{ .report_color = .{ .kind = .foreground } }; self.complete = true; + self.state = .invalid; }, else => { self.command = .{ .set_color = .{ @@ -471,10 +516,11 @@ pub const Parser = struct { }, }, - .query_default_bg => switch (c) { + .query_bg_color => switch (c) { '?' => { self.command = .{ .report_color = .{ .kind = .background } }; self.complete = true; + self.state = .invalid; }, else => { self.command = .{ .set_color = .{ diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index d7d306922..e41b27849 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -1059,10 +1059,12 @@ pub fn Stream(comptime Handler: type) type { } else log.warn("unimplemented OSC callback: {}", .{cmd}); }, - else => if (@hasDecl(T, "oscUnimplemented")) - try self.handler.oscUnimplemented(cmd) - else - log.warn("unimplemented OSC command: {}", .{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 a0da3e9c4..178de96d8 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -138,7 +138,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); @@ -332,7 +332,7 @@ 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; @@ -2179,15 +2179,15 @@ const StreamHandler = struct { /// 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) { + .palette => |i| self.terminal.color_palette.colors[i], .foreground => self.foreground_color, .background => self.background_color, - .palette => |i| self.terminal.color_palette[i], }; var msg: termio.Message = .{ .write_small = .{} }; @@ -2224,12 +2224,16 @@ const StreamHandler = struct { pub fn setColor( self: *StreamHandler, - kind: terminal.osc.Command.DefaultColorKind, + 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 |= @as(u256, 1) << i; + }, .foreground => { self.foreground_color = color; _ = self.ev.renderer_mailbox.push(.{ @@ -2242,7 +2246,55 @@ const StreamHandler = struct { .background_color = color, }, .{ .forever = {} }); }, - .palette => |i| self.terminal.color_palette[i] = color, + } + } + + pub fn resetColor( + self: *StreamHandler, + kind: terminal.osc.Command.ColorKind, + value: []const u8, + ) !void { + switch (kind) { + .palette => { + var mask = self.terminal.color_palette.mask; + defer self.terminal.color_palette.mask = mask; + + if (value.len == 0) { + // Find all bit positions in the mask which are set and + // reset those indices to the default palette + while (mask != 0) { + // Safe to truncate, mask is non-zero so @ctz can never + // return a u9 + const i: u8 = @truncate(@ctz(mask)); + log.warn("Resetting palette color {}", .{i}); + self.terminal.color_palette.colors[i] = self.terminal.default_palette[i]; + mask ^= @as(u256, 1) << 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 & (@as(u256, 1) << i) != 0) { + self.terminal.color_palette.colors[i] = self.terminal.default_palette[i]; + mask ^= @as(u256, 1) << 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 = {} }); + }, } } };