terminal: move bg, fg, cursor color state into Terminal (#9412)
This moves the bg, fg, cursor color state into the `Terminal` struct, including all default and override handling. I've rewritten our color palette handling so that overrides, defaults, resets, etc are all handled by the terminal package. I've added much more unit test coverage. This has various benefits: * libghostty now handles OSC and Kitty color operations * formatters can be aware of all of these colors (not implemented in this PR) * renderer has way less inter-thread messages * renderer uses less memory * termio stream handler uses less memory and becomes simpler * override logic, changing defaults can all be unit tested * we have unit tests for kitty color operations end-to-end now (cc @jcollie heyo!) There's a ton of risk on this PR too. There's a lot of really tiny conditionals EVERYWHERE and I'm sure I got at least one wrong. We'll let this bake in tip to find those, I'm sure they're minor and will show up as just the wrong color somewhere. **AI disclosure:** Amp wrote many of the tests. I did all the implementation.pull/9414/head
commit
4c504560d4
|
|
@ -31,7 +31,7 @@ pub fn main() !void {
|
|||
// Use TerminalFormatter to emit HTML
|
||||
const formatter: ghostty_vt.formatter.TerminalFormatter = .init(&t, .{
|
||||
.emit = .html,
|
||||
.palette = &t.color_palette.colors,
|
||||
.palette = &t.colors.palette.current,
|
||||
});
|
||||
|
||||
// Write to stdout
|
||||
|
|
|
|||
|
|
@ -4960,6 +4960,13 @@ pub const TerminalColor = union(enum) {
|
|||
return .{ .color = try Color.parseCLI(input) };
|
||||
}
|
||||
|
||||
pub fn toTerminalRGB(self: TerminalColor) ?terminal.color.RGB {
|
||||
return switch (self) {
|
||||
.color => |v| v.toTerminalRGB(),
|
||||
.@"cell-foreground", .@"cell-background" => null,
|
||||
};
|
||||
}
|
||||
|
||||
/// Used by Formatter
|
||||
pub fn formatEntry(self: TerminalColor, formatter: formatterpkg.EntryFormatter) !void {
|
||||
switch (self) {
|
||||
|
|
|
|||
|
|
@ -130,7 +130,7 @@ pub const Cell = struct {
|
|||
switch (self.style.fg_color) {
|
||||
.none => cimgui.c.igText("default"),
|
||||
.palette => |idx| {
|
||||
const rgb = t.color_palette.colors[idx];
|
||||
const rgb = t.colors.palette.current[idx];
|
||||
cimgui.c.igValue_Int("Palette", idx);
|
||||
var color: [3]f32 = .{
|
||||
@as(f32, @floatFromInt(rgb.r)) / 255,
|
||||
|
|
@ -169,7 +169,7 @@ pub const Cell = struct {
|
|||
switch (self.style.bg_color) {
|
||||
.none => cimgui.c.igText("default"),
|
||||
.palette => |idx| {
|
||||
const rgb = t.color_palette.colors[idx];
|
||||
const rgb = t.colors.palette.current[idx];
|
||||
cimgui.c.igValue_Int("Palette", idx);
|
||||
var color: [3]f32 = .{
|
||||
@as(f32, @floatFromInt(rgb.r)) / 255,
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ pub fn renderInTable(
|
|||
switch (cursor.style.fg_color) {
|
||||
.none => cimgui.c.igText("default"),
|
||||
.palette => |idx| {
|
||||
const rgb = t.color_palette.colors[idx];
|
||||
const rgb = t.colors.palette.current[idx];
|
||||
cimgui.c.igValue_Int("Palette", idx);
|
||||
var color: [3]f32 = .{
|
||||
@as(f32, @floatFromInt(rgb.r)) / 255,
|
||||
|
|
@ -90,7 +90,7 @@ pub fn renderInTable(
|
|||
switch (cursor.style.bg_color) {
|
||||
.none => cimgui.c.igText("default"),
|
||||
.palette => |idx| {
|
||||
const rgb = t.color_palette.colors[idx];
|
||||
const rgb = t.colors.palette.current[idx];
|
||||
cimgui.c.igValue_Int("Palette", idx);
|
||||
var color: [3]f32 = .{
|
||||
@as(f32, @floatFromInt(rgb.r)) / 255,
|
||||
|
|
|
|||
|
|
@ -437,21 +437,6 @@ fn drainMailbox(self: *Thread) !void {
|
|||
grid.set.deref(grid.old_key);
|
||||
},
|
||||
|
||||
.foreground_color => |color| {
|
||||
self.renderer.foreground_color = color;
|
||||
self.renderer.markDirty();
|
||||
},
|
||||
|
||||
.background_color => |color| {
|
||||
self.renderer.background_color = color;
|
||||
self.renderer.markDirty();
|
||||
},
|
||||
|
||||
.cursor_color => |color| {
|
||||
self.renderer.cursor_color = color;
|
||||
self.renderer.markDirty();
|
||||
},
|
||||
|
||||
.resize => |v| self.renderer.setScreenSize(v),
|
||||
|
||||
.change_config => |config| {
|
||||
|
|
|
|||
|
|
@ -120,30 +120,6 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
|||
scrollbar: terminal.Scrollbar,
|
||||
scrollbar_dirty: bool,
|
||||
|
||||
/// The foreground color set by an OSC 10 sequence. If unset then
|
||||
/// default_foreground_color is used.
|
||||
foreground_color: ?terminal.color.RGB,
|
||||
|
||||
/// Foreground color set in the user's config file.
|
||||
default_foreground_color: terminal.color.RGB,
|
||||
|
||||
/// The background color set by an OSC 11 sequence. If unset then
|
||||
/// default_background_color is used.
|
||||
background_color: ?terminal.color.RGB,
|
||||
|
||||
/// Background color set in the user's config file.
|
||||
default_background_color: terminal.color.RGB,
|
||||
|
||||
/// The cursor color set by an OSC 12 sequence. If unset then
|
||||
/// default_cursor_color is used.
|
||||
cursor_color: ?terminal.color.RGB,
|
||||
|
||||
/// Default cursor color when no color is set explicitly by an OSC 12 command.
|
||||
/// This is cursor color as set in the user's config, if any. If no cursor color
|
||||
/// is set in the user's config, then the cursor color is determined by the
|
||||
/// current foreground color.
|
||||
default_cursor_color: ?configpkg.Config.TerminalColor,
|
||||
|
||||
/// 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.
|
||||
|
|
@ -691,12 +667,6 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
|||
.focused = true,
|
||||
.scrollbar = .zero,
|
||||
.scrollbar_dirty = false,
|
||||
.foreground_color = null,
|
||||
.default_foreground_color = options.config.foreground,
|
||||
.background_color = null,
|
||||
.default_background_color = options.config.background,
|
||||
.cursor_color = null,
|
||||
.default_cursor_color = options.config.cursor_color,
|
||||
|
||||
// Render state
|
||||
.cells = .{},
|
||||
|
|
@ -1094,10 +1064,12 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
|||
// Data we extract out of the critical area.
|
||||
const Critical = struct {
|
||||
bg: terminal.color.RGB,
|
||||
fg: terminal.color.RGB,
|
||||
screen: terminal.Screen,
|
||||
screen_type: terminal.ScreenType,
|
||||
mouse: renderer.State.Mouse,
|
||||
preedit: ?renderer.State.Preedit,
|
||||
cursor_color: ?terminal.color.RGB,
|
||||
cursor_style: ?renderer.CursorStyle,
|
||||
color_palette: terminal.color.Palette,
|
||||
scrollbar: terminal.Scrollbar,
|
||||
|
|
@ -1132,36 +1104,16 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
|||
// cross-thread mailbox message within the IO path.
|
||||
const scrollbar = state.terminal.screen.pages.scrollbar();
|
||||
|
||||
// Swap bg/fg if the terminal is reversed
|
||||
const bg = self.background_color orelse self.default_background_color;
|
||||
const fg = self.foreground_color orelse self.default_foreground_color;
|
||||
defer {
|
||||
if (self.background_color) |*c| {
|
||||
c.* = bg;
|
||||
} else {
|
||||
self.default_background_color = bg;
|
||||
}
|
||||
|
||||
if (self.foreground_color) |*c| {
|
||||
c.* = fg;
|
||||
} else {
|
||||
self.default_foreground_color = fg;
|
||||
}
|
||||
}
|
||||
|
||||
if (state.terminal.modes.get(.reverse_colors)) {
|
||||
if (self.background_color) |*c| {
|
||||
c.* = fg;
|
||||
} else {
|
||||
self.default_background_color = fg;
|
||||
}
|
||||
|
||||
if (self.foreground_color) |*c| {
|
||||
c.* = bg;
|
||||
} else {
|
||||
self.default_foreground_color = bg;
|
||||
}
|
||||
}
|
||||
// Get our bg/fg, swap them if reversed.
|
||||
const RGB = terminal.color.RGB;
|
||||
const bg: RGB, const fg: RGB = colors: {
|
||||
const bg = state.terminal.colors.background.get().?;
|
||||
const fg = state.terminal.colors.foreground.get().?;
|
||||
break :colors if (state.terminal.modes.get(.reverse_colors))
|
||||
.{ fg, bg }
|
||||
else
|
||||
.{ bg, fg };
|
||||
};
|
||||
|
||||
// Get the viewport pin so that we can compare it to the current.
|
||||
const viewport_pin = state.terminal.screen.pages.pin(.{ .viewport = .{} }).?;
|
||||
|
|
@ -1252,13 +1204,15 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
|||
self.cells_viewport = viewport_pin;
|
||||
|
||||
break :critical .{
|
||||
.bg = self.background_color orelse self.default_background_color,
|
||||
.bg = bg,
|
||||
.fg = fg,
|
||||
.screen = screen_copy,
|
||||
.screen_type = state.terminal.active_screen,
|
||||
.mouse = state.mouse,
|
||||
.preedit = preedit,
|
||||
.cursor_color = state.terminal.colors.cursor.get(),
|
||||
.cursor_style = cursor_style,
|
||||
.color_palette = state.terminal.color_palette.colors,
|
||||
.color_palette = state.terminal.colors.palette.current,
|
||||
.scrollbar = scrollbar,
|
||||
.full_rebuild = full_rebuild,
|
||||
};
|
||||
|
|
@ -1277,6 +1231,9 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
|||
critical.preedit,
|
||||
critical.cursor_style,
|
||||
&critical.color_palette,
|
||||
critical.bg,
|
||||
critical.fg,
|
||||
critical.cursor_color,
|
||||
);
|
||||
|
||||
// Notify our shaper we're done for the frame. For some shapers,
|
||||
|
|
@ -2104,11 +2061,6 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
|||
self.uniforms.bools.use_linear_blending = config.blending.isLinear();
|
||||
self.uniforms.bools.use_linear_correction = config.blending == .@"linear-corrected";
|
||||
|
||||
// Set our new colors
|
||||
self.default_background_color = config.background;
|
||||
self.default_foreground_color = config.foreground;
|
||||
self.default_cursor_color = config.cursor_color;
|
||||
|
||||
const bg_image_config_changed =
|
||||
self.config.bg_image_fit != config.bg_image_fit or
|
||||
self.config.bg_image_position != config.bg_image_position or
|
||||
|
|
@ -2370,6 +2322,9 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
|||
preedit: ?renderer.State.Preedit,
|
||||
cursor_style_: ?renderer.CursorStyle,
|
||||
color_palette: *const terminal.color.Palette,
|
||||
background: terminal.color.RGB,
|
||||
foreground: terminal.color.RGB,
|
||||
terminal_cursor_color: ?terminal.color.RGB,
|
||||
) !void {
|
||||
self.draw_mutex.lock();
|
||||
defer self.draw_mutex.unlock();
|
||||
|
|
@ -2503,12 +2458,12 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
|||
.extend => if (y == 0) {
|
||||
self.uniforms.padding_extend.up = !row.neverExtendBg(
|
||||
color_palette,
|
||||
self.background_color orelse self.default_background_color,
|
||||
background,
|
||||
);
|
||||
} else if (y == self.cells.size.rows - 1) {
|
||||
self.uniforms.padding_extend.down = !row.neverExtendBg(
|
||||
color_palette,
|
||||
self.background_color orelse self.default_background_color,
|
||||
background,
|
||||
);
|
||||
},
|
||||
}
|
||||
|
|
@ -2629,7 +2584,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
|||
// configuration, inversions, selections, etc.
|
||||
const bg_style = style.bg(cell, color_palette);
|
||||
const fg_style = style.fg(.{
|
||||
.default = self.foreground_color orelse self.default_foreground_color,
|
||||
.default = foreground,
|
||||
.palette = color_palette,
|
||||
.bold = self.config.bold_color,
|
||||
});
|
||||
|
|
@ -2649,7 +2604,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
|||
|
||||
// If no configuration, then our selection background
|
||||
// is our foreground color.
|
||||
break :bg self.foreground_color orelse self.default_foreground_color;
|
||||
break :bg foreground;
|
||||
}
|
||||
|
||||
// Not selected
|
||||
|
|
@ -2671,9 +2626,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
|||
const fg = fg: {
|
||||
// Our happy-path non-selection background color
|
||||
// is our style or our configured defaults.
|
||||
const final_bg = bg_style orelse
|
||||
self.background_color orelse
|
||||
self.default_background_color;
|
||||
const final_bg = bg_style orelse background;
|
||||
|
||||
// Whether we need to use the bg color as our fg color:
|
||||
// - Cell is selected, inverted, and set to cell-foreground
|
||||
|
|
@ -2689,7 +2642,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
|||
};
|
||||
}
|
||||
|
||||
break :fg self.background_color orelse self.default_background_color;
|
||||
break :fg background;
|
||||
}
|
||||
|
||||
break :fg if (style.flags.inverse)
|
||||
|
|
@ -2703,7 +2656,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
|||
|
||||
// Set the cell's background color.
|
||||
{
|
||||
const rgb = bg orelse self.background_color orelse self.default_background_color;
|
||||
const rgb = bg orelse background;
|
||||
|
||||
// Determine our background alpha. If we have transparency configured
|
||||
// then this is dynamic depending on some situations. This is all
|
||||
|
|
@ -2888,24 +2841,24 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
|||
const style = cursor_style_ orelse break :cursor;
|
||||
const cursor_color = cursor_color: {
|
||||
// If an explicit cursor color was set by OSC 12, use that.
|
||||
if (self.cursor_color) |v| break :cursor_color v;
|
||||
if (terminal_cursor_color) |v| break :cursor_color v;
|
||||
|
||||
// Use our configured color if specified
|
||||
if (self.default_cursor_color) |v| switch (v) {
|
||||
if (self.config.cursor_color) |v| switch (v) {
|
||||
.color => |color| break :cursor_color color.toTerminalRGB(),
|
||||
inline .@"cell-foreground",
|
||||
.@"cell-background",
|
||||
=> |_, tag| {
|
||||
const sty = screen.cursor.page_pin.style(screen.cursor.page_cell);
|
||||
const fg_style = sty.fg(.{
|
||||
.default = self.foreground_color orelse self.default_foreground_color,
|
||||
.default = foreground,
|
||||
.palette = color_palette,
|
||||
.bold = self.config.bold_color,
|
||||
});
|
||||
const bg_style = sty.bg(
|
||||
screen.cursor.page_cell,
|
||||
color_palette,
|
||||
) orelse self.background_color orelse self.default_background_color;
|
||||
) orelse background;
|
||||
|
||||
break :cursor_color switch (tag) {
|
||||
.color => unreachable,
|
||||
|
|
@ -2915,7 +2868,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
|||
},
|
||||
};
|
||||
|
||||
break :cursor_color self.foreground_color orelse self.default_foreground_color;
|
||||
break :cursor_color foreground;
|
||||
};
|
||||
|
||||
self.addCursor(screen, style, cursor_color);
|
||||
|
|
@ -2950,11 +2903,14 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
|||
|
||||
const sty = screen.cursor.page_pin.style(screen.cursor.page_cell);
|
||||
const fg_style = sty.fg(.{
|
||||
.default = self.foreground_color orelse self.default_foreground_color,
|
||||
.default = foreground,
|
||||
.palette = color_palette,
|
||||
.bold = self.config.bold_color,
|
||||
});
|
||||
const bg_style = sty.bg(screen.cursor.page_cell, color_palette) orelse self.background_color orelse self.default_background_color;
|
||||
const bg_style = sty.bg(
|
||||
screen.cursor.page_cell,
|
||||
color_palette,
|
||||
) orelse background;
|
||||
|
||||
break :blk switch (txt) {
|
||||
// If the cell is reversed, use the opposite cell color instead.
|
||||
|
|
@ -2962,7 +2918,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
|||
.@"cell-background" => if (sty.flags.inverse) fg_style else bg_style,
|
||||
else => unreachable,
|
||||
};
|
||||
} else self.background_color orelse self.default_background_color;
|
||||
} else background;
|
||||
|
||||
self.uniforms.cursor_color = .{
|
||||
uniform_color.r,
|
||||
|
|
@ -2978,7 +2934,12 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
|||
const range = preedit_range.?;
|
||||
var x = range.x[0];
|
||||
for (preedit_v.codepoints[range.cp_offset..]) |cp| {
|
||||
self.addPreeditCell(cp, .{ .x = x, .y = range.y }) catch |err| {
|
||||
self.addPreeditCell(
|
||||
cp,
|
||||
.{ .x = x, .y = range.y },
|
||||
background,
|
||||
foreground,
|
||||
) catch |err| {
|
||||
log.warn("error building preedit cell, will be invalid x={} y={}, err={}", .{
|
||||
x,
|
||||
range.y,
|
||||
|
|
@ -3253,10 +3214,12 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
|||
self: *Self,
|
||||
cp: renderer.State.Preedit.Codepoint,
|
||||
coord: terminal.Coordinate,
|
||||
screen_bg: terminal.color.RGB,
|
||||
screen_fg: terminal.color.RGB,
|
||||
) !void {
|
||||
// Preedit is rendered inverted
|
||||
const bg = self.foreground_color orelse self.default_foreground_color;
|
||||
const fg = self.background_color orelse self.default_background_color;
|
||||
const bg = screen_fg;
|
||||
const fg = screen_bg;
|
||||
|
||||
// Render the glyph for our preedit text
|
||||
const render_ = self.font_grid.renderCodepoint(
|
||||
|
|
|
|||
|
|
@ -42,16 +42,6 @@ pub const Message = union(enum) {
|
|||
old_key: font.SharedGridSet.Key,
|
||||
},
|
||||
|
||||
/// Change the foreground color as set by an OSC 10 command, if any.
|
||||
foreground_color: ?terminal.color.RGB,
|
||||
|
||||
/// Change the background color as set by an OSC 11 command, if any.
|
||||
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 size. The screen size might change, padding, grid, etc.
|
||||
resize: renderer.Size,
|
||||
|
||||
|
|
|
|||
|
|
@ -73,17 +73,8 @@ scrolling_region: ScrollingRegion,
|
|||
/// The last reported pwd, if any.
|
||||
pwd: std.ArrayList(u8),
|
||||
|
||||
/// 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 = .initEmpty(),
|
||||
} = .{},
|
||||
/// The color state for this terminal.
|
||||
colors: Colors,
|
||||
|
||||
/// The previous printed character. This is used for the repeat previous
|
||||
/// char CSI (ESC [ <n> b).
|
||||
|
|
@ -134,6 +125,23 @@ flags: packed struct {
|
|||
dirty: Dirty = .{},
|
||||
} = .{},
|
||||
|
||||
/// The various color configurations a terminal maintains and that can
|
||||
/// be set dynamically via OSC, with defaults usually coming from a
|
||||
/// configuration.
|
||||
pub const Colors = struct {
|
||||
background: color.DynamicRGB,
|
||||
foreground: color.DynamicRGB,
|
||||
cursor: color.DynamicRGB,
|
||||
palette: color.DynamicPalette,
|
||||
|
||||
pub const default: Colors = .{
|
||||
.background = .unset,
|
||||
.foreground = .unset,
|
||||
.cursor = .unset,
|
||||
.palette = .default,
|
||||
};
|
||||
};
|
||||
|
||||
/// This is a set of dirty flags the renderer can use to determine
|
||||
/// what parts of the screen need to be redrawn. It is up to the renderer
|
||||
/// to clear these flags.
|
||||
|
|
@ -199,6 +207,7 @@ pub const Options = struct {
|
|||
cols: size.CellCountInt,
|
||||
rows: size.CellCountInt,
|
||||
max_scrollback: usize = 10_000,
|
||||
colors: Colors = .default,
|
||||
|
||||
/// The default mode state. When the terminal gets a reset, it
|
||||
/// will revert back to this state.
|
||||
|
|
@ -212,7 +221,7 @@ pub fn init(
|
|||
) !Terminal {
|
||||
const cols = opts.cols;
|
||||
const rows = opts.rows;
|
||||
return Terminal{
|
||||
return .{
|
||||
.cols = cols,
|
||||
.rows = rows,
|
||||
.active_screen = .primary,
|
||||
|
|
@ -226,6 +235,7 @@ pub fn init(
|
|||
.right = cols - 1,
|
||||
},
|
||||
.pwd = .empty,
|
||||
.colors = opts.colors,
|
||||
.modes = .{
|
||||
.values = opts.default_modes,
|
||||
.default = opts.default_modes,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
const colorpkg = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const x11_color = @import("x11_color.zig");
|
||||
|
|
@ -45,6 +47,97 @@ pub const default: Palette = default: {
|
|||
/// Palette is the 256 color palette.
|
||||
pub const Palette = [256]RGB;
|
||||
|
||||
/// A palette that can have its colors changed and reset. Purposely built
|
||||
/// for terminal color operations.
|
||||
pub const DynamicPalette = struct {
|
||||
/// The current palette including any user modifications.
|
||||
current: Palette,
|
||||
|
||||
/// The original/default palette values.
|
||||
original: Palette,
|
||||
|
||||
/// A bitset where each bit represents whether the corresponding
|
||||
/// palette index has been modified from its default value.
|
||||
mask: Mask,
|
||||
|
||||
const Mask = std.StaticBitSet(@typeInfo(Palette).array.len);
|
||||
|
||||
pub const default: DynamicPalette = .init(colorpkg.default);
|
||||
|
||||
/// Initialize a dynamic palette with a default palette.
|
||||
pub fn init(def: Palette) DynamicPalette {
|
||||
return .{
|
||||
.current = def,
|
||||
.original = def,
|
||||
.mask = .initEmpty(),
|
||||
};
|
||||
}
|
||||
|
||||
/// Set a custom color at the given palette index.
|
||||
pub fn set(self: *DynamicPalette, idx: u8, color: RGB) void {
|
||||
self.current[idx] = color;
|
||||
self.mask.set(idx);
|
||||
}
|
||||
|
||||
/// Reset the color at the given palette index to its original value.
|
||||
pub fn reset(self: *DynamicPalette, idx: u8) void {
|
||||
self.current[idx] = self.original[idx];
|
||||
self.mask.unset(idx);
|
||||
}
|
||||
|
||||
/// Reset all colors to their original values.
|
||||
pub fn resetAll(self: *DynamicPalette) void {
|
||||
self.* = .init(self.original);
|
||||
}
|
||||
|
||||
/// Change the default palette, but preserve the changed values.
|
||||
pub fn changeDefault(self: *DynamicPalette, def: Palette) void {
|
||||
self.original = def;
|
||||
|
||||
// Fast path, the palette is usually not changed.
|
||||
if (self.mask.count() == 0) {
|
||||
self.current = self.original;
|
||||
return;
|
||||
}
|
||||
|
||||
// There are usually less set than unset, so iterate over the changed
|
||||
// values and override them.
|
||||
var current = def;
|
||||
var it = self.mask.iterator(.{});
|
||||
while (it.next()) |idx| current[idx] = self.current[idx];
|
||||
self.current = current;
|
||||
}
|
||||
};
|
||||
|
||||
/// RGB value that can be changed and reset. This can also be totally unset
|
||||
/// in every way, in which case the caller can determine their own ultimate
|
||||
/// default.
|
||||
pub const DynamicRGB = struct {
|
||||
override: ?RGB,
|
||||
default: ?RGB,
|
||||
|
||||
pub const unset: DynamicRGB = .{ .override = null, .default = null };
|
||||
|
||||
pub fn init(def: RGB) DynamicRGB {
|
||||
return .{
|
||||
.override = null,
|
||||
.default = def,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn get(self: *const DynamicRGB) ?RGB {
|
||||
return self.override orelse self.default;
|
||||
}
|
||||
|
||||
pub fn set(self: *DynamicRGB, color: RGB) void {
|
||||
self.override = color;
|
||||
}
|
||||
|
||||
pub fn reset(self: *DynamicRGB) void {
|
||||
self.override = self.default;
|
||||
}
|
||||
};
|
||||
|
||||
/// Color names in the standard 8 or 16 color palette.
|
||||
pub const Name = enum(u8) {
|
||||
black = 0,
|
||||
|
|
@ -456,3 +549,118 @@ test "RGB.parse" {
|
|||
try testing.expectError(error.InvalidFormat, RGB.parse("#fffff"));
|
||||
try testing.expectError(error.InvalidFormat, RGB.parse("#gggggg"));
|
||||
}
|
||||
|
||||
test "DynamicPalette: init" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: DynamicPalette = .init(default);
|
||||
try testing.expectEqual(default, p.current);
|
||||
try testing.expectEqual(default, p.original);
|
||||
try testing.expectEqual(@as(usize, 0), p.mask.count());
|
||||
}
|
||||
|
||||
test "DynamicPalette: set" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: DynamicPalette = .init(default);
|
||||
const new_color = RGB{ .r = 255, .g = 0, .b = 0 };
|
||||
|
||||
p.set(0, new_color);
|
||||
try testing.expectEqual(new_color, p.current[0]);
|
||||
try testing.expect(p.mask.isSet(0));
|
||||
try testing.expectEqual(@as(usize, 1), p.mask.count());
|
||||
|
||||
try testing.expectEqual(default[0], p.original[0]);
|
||||
}
|
||||
|
||||
test "DynamicPalette: reset" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: DynamicPalette = .init(default);
|
||||
const new_color = RGB{ .r = 255, .g = 0, .b = 0 };
|
||||
|
||||
p.set(0, new_color);
|
||||
try testing.expect(p.mask.isSet(0));
|
||||
|
||||
p.reset(0);
|
||||
try testing.expectEqual(default[0], p.current[0]);
|
||||
try testing.expect(!p.mask.isSet(0));
|
||||
try testing.expectEqual(@as(usize, 0), p.mask.count());
|
||||
}
|
||||
|
||||
test "DynamicPalette: resetAll" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: DynamicPalette = .init(default);
|
||||
const new_color = RGB{ .r = 255, .g = 0, .b = 0 };
|
||||
|
||||
p.set(0, new_color);
|
||||
p.set(5, new_color);
|
||||
p.set(10, new_color);
|
||||
try testing.expectEqual(@as(usize, 3), p.mask.count());
|
||||
|
||||
p.resetAll();
|
||||
try testing.expectEqual(default, p.current);
|
||||
try testing.expectEqual(default, p.original);
|
||||
try testing.expectEqual(@as(usize, 0), p.mask.count());
|
||||
}
|
||||
|
||||
test "DynamicPalette: changeDefault with no changes" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: DynamicPalette = .init(default);
|
||||
var new_palette = default;
|
||||
new_palette[0] = RGB{ .r = 100, .g = 100, .b = 100 };
|
||||
|
||||
p.changeDefault(new_palette);
|
||||
try testing.expectEqual(new_palette, p.original);
|
||||
try testing.expectEqual(new_palette, p.current);
|
||||
try testing.expectEqual(@as(usize, 0), p.mask.count());
|
||||
}
|
||||
|
||||
test "DynamicPalette: changeDefault preserves changes" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: DynamicPalette = .init(default);
|
||||
const custom_color = RGB{ .r = 255, .g = 0, .b = 0 };
|
||||
|
||||
p.set(5, custom_color);
|
||||
try testing.expect(p.mask.isSet(5));
|
||||
|
||||
var new_palette = default;
|
||||
new_palette[0] = RGB{ .r = 100, .g = 100, .b = 100 };
|
||||
new_palette[5] = RGB{ .r = 50, .g = 50, .b = 50 };
|
||||
|
||||
p.changeDefault(new_palette);
|
||||
|
||||
try testing.expectEqual(new_palette, p.original);
|
||||
try testing.expectEqual(new_palette[0], p.current[0]);
|
||||
try testing.expectEqual(custom_color, p.current[5]);
|
||||
try testing.expect(p.mask.isSet(5));
|
||||
try testing.expectEqual(@as(usize, 1), p.mask.count());
|
||||
}
|
||||
|
||||
test "DynamicPalette: changeDefault with multiple changes" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: DynamicPalette = .init(default);
|
||||
const red = RGB{ .r = 255, .g = 0, .b = 0 };
|
||||
const green = RGB{ .r = 0, .g = 255, .b = 0 };
|
||||
const blue = RGB{ .r = 0, .g = 0, .b = 255 };
|
||||
|
||||
p.set(1, red);
|
||||
p.set(2, green);
|
||||
p.set(3, blue);
|
||||
|
||||
var new_palette = default;
|
||||
new_palette[0] = RGB{ .r = 50, .g = 50, .b = 50 };
|
||||
new_palette[1] = RGB{ .r = 60, .g = 60, .b = 60 };
|
||||
|
||||
p.changeDefault(new_palette);
|
||||
|
||||
try testing.expectEqual(new_palette[0], p.current[0]);
|
||||
try testing.expectEqual(red, p.current[1]);
|
||||
try testing.expectEqual(green, p.current[2]);
|
||||
try testing.expectEqual(blue, p.current[3]);
|
||||
try testing.expectEqual(@as(usize, 3), p.mask.count());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -225,7 +225,7 @@ pub const TerminalFormatter = struct {
|
|||
.plain => break :palette,
|
||||
|
||||
.vt => {
|
||||
for (self.terminal.color_palette.colors, 0..) |rgb, i| {
|
||||
for (self.terminal.colors.palette.current, 0..) |rgb, i| {
|
||||
try writer.print(
|
||||
"\x1b]4;{d};rgb:{x:0>2}/{x:0>2}/{x:0>2}\x1b\\",
|
||||
.{ i, rgb.r, rgb.g, rgb.b },
|
||||
|
|
@ -236,7 +236,7 @@ pub const TerminalFormatter = struct {
|
|||
// For HTML, we emit CSS to setup our palette variables.
|
||||
.html => {
|
||||
try writer.writeAll("<style>:root{");
|
||||
for (self.terminal.color_palette.colors, 0..) |rgb, i| {
|
||||
for (self.terminal.colors.palette.current, 0..) |rgb, i| {
|
||||
try writer.print(
|
||||
"--vt-palette-{d}: #{x:0>2}{x:0>2}{x:0>2};",
|
||||
.{ i, rgb.r, rgb.g, rgb.b },
|
||||
|
|
@ -3839,9 +3839,9 @@ test "TerminalFormatter vt with palette" {
|
|||
try s2.nextSlice(output);
|
||||
|
||||
// Verify the palettes match
|
||||
try testing.expectEqual(t.color_palette.colors[0], t2.color_palette.colors[0]);
|
||||
try testing.expectEqual(t.color_palette.colors[1], t2.color_palette.colors[1]);
|
||||
try testing.expectEqual(t.color_palette.colors[255], t2.color_palette.colors[255]);
|
||||
try testing.expectEqual(t.colors.palette.current[0], t2.colors.palette.current[0]);
|
||||
try testing.expectEqual(t.colors.palette.current[1], t2.colors.palette.current[1]);
|
||||
try testing.expectEqual(t.colors.palette.current[255], t2.colors.palette.current[255]);
|
||||
}
|
||||
|
||||
test "TerminalFormatter with selection" {
|
||||
|
|
@ -4972,7 +4972,7 @@ test "Page VT with palette option emits RGB" {
|
|||
{
|
||||
builder.clearRetainingCapacity();
|
||||
var opts: Options = .vt;
|
||||
opts.palette = &t.color_palette.colors;
|
||||
opts.palette = &t.colors.palette.current;
|
||||
var formatter: PageFormatter = .init(page, opts);
|
||||
try formatter.format(&builder.writer);
|
||||
const output = builder.writer.buffered();
|
||||
|
|
@ -5021,7 +5021,7 @@ test "Page html with palette option emits RGB" {
|
|||
{
|
||||
builder.clearRetainingCapacity();
|
||||
var opts: Options = .{ .emit = .html };
|
||||
opts.palette = &t.color_palette.colors;
|
||||
opts.palette = &t.colors.palette.current;
|
||||
var formatter: PageFormatter = .init(page, opts);
|
||||
try formatter.format(&builder.writer);
|
||||
const output = builder.writer.buffered();
|
||||
|
|
|
|||
|
|
@ -161,6 +161,7 @@ pub const Handler = struct {
|
|||
.end_of_command => self.terminal.screen.cursor.page_row.semantic_prompt = .input,
|
||||
.mouse_shape => self.terminal.mouse_shape = value,
|
||||
.color_operation => try self.colorOperation(value.op, &value.requests),
|
||||
.kitty_color_report => try self.kittyColorOperation(value),
|
||||
|
||||
// No supported DCS commands have any terminal-modifying effects,
|
||||
// but they may in the future. For now we just ignore it.
|
||||
|
|
@ -186,7 +187,6 @@ pub const Handler = struct {
|
|||
.device_attributes,
|
||||
.device_status,
|
||||
.kitty_keyboard_query,
|
||||
.kitty_color_report,
|
||||
.window_title,
|
||||
.report_pwd,
|
||||
.show_desktop_notification,
|
||||
|
|
@ -306,31 +306,53 @@ pub const Handler = struct {
|
|||
.set => |set| {
|
||||
switch (set.target) {
|
||||
.palette => |i| {
|
||||
self.terminal.color_palette.colors[i] = set.color;
|
||||
self.terminal.color_palette.mask.set(i);
|
||||
self.terminal.flags.dirty.palette = true;
|
||||
self.terminal.colors.palette.set(i, set.color);
|
||||
},
|
||||
.dynamic,
|
||||
.special,
|
||||
=> {},
|
||||
.dynamic => |dynamic| switch (dynamic) {
|
||||
.foreground => self.terminal.colors.foreground.set(set.color),
|
||||
.background => self.terminal.colors.background.set(set.color),
|
||||
.cursor => self.terminal.colors.cursor.set(set.color),
|
||||
.pointer_foreground,
|
||||
.pointer_background,
|
||||
.tektronix_foreground,
|
||||
.tektronix_background,
|
||||
.highlight_background,
|
||||
.tektronix_cursor,
|
||||
.highlight_foreground,
|
||||
=> {},
|
||||
},
|
||||
.special => {},
|
||||
}
|
||||
},
|
||||
|
||||
.reset => |target| switch (target) {
|
||||
.palette => |i| {
|
||||
const mask = &self.terminal.color_palette.mask;
|
||||
self.terminal.color_palette.colors[i] = self.terminal.default_palette[i];
|
||||
mask.unset(i);
|
||||
self.terminal.flags.dirty.palette = true;
|
||||
self.terminal.colors.palette.reset(i);
|
||||
},
|
||||
.dynamic,
|
||||
.special,
|
||||
=> {},
|
||||
.dynamic => |dynamic| switch (dynamic) {
|
||||
.foreground => self.terminal.colors.foreground.reset(),
|
||||
.background => self.terminal.colors.background.reset(),
|
||||
.cursor => self.terminal.colors.cursor.reset(),
|
||||
.pointer_foreground,
|
||||
.pointer_background,
|
||||
.tektronix_foreground,
|
||||
.tektronix_background,
|
||||
.highlight_background,
|
||||
.tektronix_cursor,
|
||||
.highlight_foreground,
|
||||
=> {},
|
||||
},
|
||||
.special => {},
|
||||
},
|
||||
|
||||
.reset_palette => {
|
||||
const mask = &self.terminal.color_palette.mask;
|
||||
var mask_iterator = mask.iterator(.{});
|
||||
while (mask_iterator.next()) |i| {
|
||||
self.terminal.color_palette.colors[i] = self.terminal.default_palette[i];
|
||||
const mask = &self.terminal.colors.palette.mask;
|
||||
var mask_it = mask.iterator(.{});
|
||||
while (mask_it.next()) |i| {
|
||||
self.terminal.flags.dirty.palette = true;
|
||||
self.terminal.colors.palette.reset(@intCast(i));
|
||||
}
|
||||
mask.* = .initEmpty();
|
||||
},
|
||||
|
|
@ -341,6 +363,41 @@ pub const Handler = struct {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn kittyColorOperation(
|
||||
self: *Handler,
|
||||
request: @import("kitty/color.zig").OSC,
|
||||
) !void {
|
||||
for (request.list.items) |item| {
|
||||
switch (item) {
|
||||
.set => |v| switch (v.key) {
|
||||
.palette => |palette| {
|
||||
self.terminal.flags.dirty.palette = true;
|
||||
self.terminal.colors.palette.set(palette, v.color);
|
||||
},
|
||||
.special => |special| switch (special) {
|
||||
.foreground => self.terminal.colors.foreground.set(v.color),
|
||||
.background => self.terminal.colors.background.set(v.color),
|
||||
.cursor => self.terminal.colors.cursor.set(v.color),
|
||||
else => {},
|
||||
},
|
||||
},
|
||||
.reset => |key| switch (key) {
|
||||
.palette => |palette| {
|
||||
self.terminal.flags.dirty.palette = true;
|
||||
self.terminal.colors.palette.reset(palette);
|
||||
},
|
||||
.special => |special| switch (special) {
|
||||
.foreground => self.terminal.colors.foreground.reset(),
|
||||
.background => self.terminal.colors.background.reset(),
|
||||
.cursor => self.terminal.colors.cursor.reset(),
|
||||
else => {},
|
||||
},
|
||||
},
|
||||
.query => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
test "basic print" {
|
||||
|
|
@ -599,19 +656,19 @@ test "OSC 4 set and reset palette" {
|
|||
defer s.deinit();
|
||||
|
||||
// Save default color
|
||||
const default_color_0 = t.default_palette[0];
|
||||
const default_color_0 = t.colors.palette.original[0];
|
||||
|
||||
// Set color 0 to red
|
||||
try s.nextSlice("\x1b]4;0;rgb:ff/00/00\x1b\\");
|
||||
try testing.expectEqual(@as(u8, 0xff), t.color_palette.colors[0].r);
|
||||
try testing.expectEqual(@as(u8, 0x00), t.color_palette.colors[0].g);
|
||||
try testing.expectEqual(@as(u8, 0x00), t.color_palette.colors[0].b);
|
||||
try testing.expect(t.color_palette.mask.isSet(0));
|
||||
try testing.expectEqual(@as(u8, 0xff), t.colors.palette.current[0].r);
|
||||
try testing.expectEqual(@as(u8, 0x00), t.colors.palette.current[0].g);
|
||||
try testing.expectEqual(@as(u8, 0x00), t.colors.palette.current[0].b);
|
||||
try testing.expect(t.colors.palette.mask.isSet(0));
|
||||
|
||||
// Reset color 0
|
||||
try s.nextSlice("\x1b]104;0\x1b\\");
|
||||
try testing.expectEqual(default_color_0, t.color_palette.colors[0]);
|
||||
try testing.expect(!t.color_palette.mask.isSet(0));
|
||||
try testing.expectEqual(default_color_0, t.colors.palette.current[0]);
|
||||
try testing.expect(!t.colors.palette.mask.isSet(0));
|
||||
}
|
||||
|
||||
test "OSC 104 reset all palette colors" {
|
||||
|
|
@ -625,16 +682,195 @@ test "OSC 104 reset all palette colors" {
|
|||
try s.nextSlice("\x1b]4;0;rgb:ff/00/00\x1b\\");
|
||||
try s.nextSlice("\x1b]4;1;rgb:00/ff/00\x1b\\");
|
||||
try s.nextSlice("\x1b]4;2;rgb:00/00/ff\x1b\\");
|
||||
try testing.expect(t.color_palette.mask.isSet(0));
|
||||
try testing.expect(t.color_palette.mask.isSet(1));
|
||||
try testing.expect(t.color_palette.mask.isSet(2));
|
||||
try testing.expect(t.colors.palette.mask.isSet(0));
|
||||
try testing.expect(t.colors.palette.mask.isSet(1));
|
||||
try testing.expect(t.colors.palette.mask.isSet(2));
|
||||
|
||||
// Reset all palette colors
|
||||
try s.nextSlice("\x1b]104\x1b\\");
|
||||
try testing.expectEqual(t.default_palette[0], t.color_palette.colors[0]);
|
||||
try testing.expectEqual(t.default_palette[1], t.color_palette.colors[1]);
|
||||
try testing.expectEqual(t.default_palette[2], t.color_palette.colors[2]);
|
||||
try testing.expect(!t.color_palette.mask.isSet(0));
|
||||
try testing.expect(!t.color_palette.mask.isSet(1));
|
||||
try testing.expect(!t.color_palette.mask.isSet(2));
|
||||
try testing.expectEqual(t.colors.palette.original[0], t.colors.palette.current[0]);
|
||||
try testing.expectEqual(t.colors.palette.original[1], t.colors.palette.current[1]);
|
||||
try testing.expectEqual(t.colors.palette.original[2], t.colors.palette.current[2]);
|
||||
try testing.expect(!t.colors.palette.mask.isSet(0));
|
||||
try testing.expect(!t.colors.palette.mask.isSet(1));
|
||||
try testing.expect(!t.colors.palette.mask.isSet(2));
|
||||
}
|
||||
|
||||
test "OSC 10 set and reset foreground color" {
|
||||
var t: Terminal = try .init(testing.allocator, .{ .cols = 10, .rows = 10 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
defer s.deinit();
|
||||
|
||||
// Initially unset
|
||||
try testing.expect(t.colors.foreground.get() == null);
|
||||
|
||||
// Set foreground to red
|
||||
try s.nextSlice("\x1b]10;rgb:ff/00/00\x1b\\");
|
||||
const fg = t.colors.foreground.get().?;
|
||||
try testing.expectEqual(@as(u8, 0xff), fg.r);
|
||||
try testing.expectEqual(@as(u8, 0x00), fg.g);
|
||||
try testing.expectEqual(@as(u8, 0x00), fg.b);
|
||||
|
||||
// Reset foreground
|
||||
try s.nextSlice("\x1b]110\x1b\\");
|
||||
try testing.expect(t.colors.foreground.get() == null);
|
||||
}
|
||||
|
||||
test "OSC 11 set and reset background color" {
|
||||
var t: Terminal = try .init(testing.allocator, .{ .cols = 10, .rows = 10 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
defer s.deinit();
|
||||
|
||||
// Set background to green
|
||||
try s.nextSlice("\x1b]11;rgb:00/ff/00\x1b\\");
|
||||
const bg = t.colors.background.get().?;
|
||||
try testing.expectEqual(@as(u8, 0x00), bg.r);
|
||||
try testing.expectEqual(@as(u8, 0xff), bg.g);
|
||||
try testing.expectEqual(@as(u8, 0x00), bg.b);
|
||||
|
||||
// Reset background
|
||||
try s.nextSlice("\x1b]111\x1b\\");
|
||||
try testing.expect(t.colors.background.get() == null);
|
||||
}
|
||||
|
||||
test "OSC 12 set and reset cursor color" {
|
||||
var t: Terminal = try .init(testing.allocator, .{ .cols = 10, .rows = 10 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
defer s.deinit();
|
||||
|
||||
// Set cursor to blue
|
||||
try s.nextSlice("\x1b]12;rgb:00/00/ff\x1b\\");
|
||||
const cursor = t.colors.cursor.get().?;
|
||||
try testing.expectEqual(@as(u8, 0x00), cursor.r);
|
||||
try testing.expectEqual(@as(u8, 0x00), cursor.g);
|
||||
try testing.expectEqual(@as(u8, 0xff), cursor.b);
|
||||
|
||||
// Reset cursor
|
||||
try s.nextSlice("\x1b]112\x1b\\");
|
||||
// After reset, cursor might be null (using default)
|
||||
}
|
||||
|
||||
test "kitty color protocol set palette" {
|
||||
var t: Terminal = try .init(testing.allocator, .{ .cols = 10, .rows = 10 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
defer s.deinit();
|
||||
|
||||
// Set palette color 5 to magenta using kitty protocol
|
||||
try s.nextSlice("\x1b]21;5=rgb:ff/00/ff\x1b\\");
|
||||
try testing.expectEqual(@as(u8, 0xff), t.colors.palette.current[5].r);
|
||||
try testing.expectEqual(@as(u8, 0x00), t.colors.palette.current[5].g);
|
||||
try testing.expectEqual(@as(u8, 0xff), t.colors.palette.current[5].b);
|
||||
try testing.expect(t.colors.palette.mask.isSet(5));
|
||||
try testing.expect(t.flags.dirty.palette);
|
||||
}
|
||||
|
||||
test "kitty color protocol reset palette" {
|
||||
var t: Terminal = try .init(testing.allocator, .{ .cols = 10, .rows = 10 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
defer s.deinit();
|
||||
|
||||
// Set and then reset palette color
|
||||
const original = t.colors.palette.original[7];
|
||||
try s.nextSlice("\x1b]21;7=rgb:aa/bb/cc\x1b\\");
|
||||
try testing.expect(t.colors.palette.mask.isSet(7));
|
||||
|
||||
try s.nextSlice("\x1b]21;7=\x1b\\");
|
||||
try testing.expectEqual(original, t.colors.palette.current[7]);
|
||||
try testing.expect(!t.colors.palette.mask.isSet(7));
|
||||
}
|
||||
|
||||
test "kitty color protocol set foreground" {
|
||||
var t: Terminal = try .init(testing.allocator, .{ .cols = 10, .rows = 10 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
defer s.deinit();
|
||||
|
||||
// Set foreground using kitty protocol
|
||||
try s.nextSlice("\x1b]21;foreground=rgb:12/34/56\x1b\\");
|
||||
const fg = t.colors.foreground.get().?;
|
||||
try testing.expectEqual(@as(u8, 0x12), fg.r);
|
||||
try testing.expectEqual(@as(u8, 0x34), fg.g);
|
||||
try testing.expectEqual(@as(u8, 0x56), fg.b);
|
||||
}
|
||||
|
||||
test "kitty color protocol set background" {
|
||||
var t: Terminal = try .init(testing.allocator, .{ .cols = 10, .rows = 10 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
defer s.deinit();
|
||||
|
||||
// Set background using kitty protocol
|
||||
try s.nextSlice("\x1b]21;background=rgb:78/9a/bc\x1b\\");
|
||||
const bg = t.colors.background.get().?;
|
||||
try testing.expectEqual(@as(u8, 0x78), bg.r);
|
||||
try testing.expectEqual(@as(u8, 0x9a), bg.g);
|
||||
try testing.expectEqual(@as(u8, 0xbc), bg.b);
|
||||
}
|
||||
|
||||
test "kitty color protocol set cursor" {
|
||||
var t: Terminal = try .init(testing.allocator, .{ .cols = 10, .rows = 10 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
defer s.deinit();
|
||||
|
||||
// Set cursor using kitty protocol
|
||||
try s.nextSlice("\x1b]21;cursor=rgb:de/f0/12\x1b\\");
|
||||
const cursor = t.colors.cursor.get().?;
|
||||
try testing.expectEqual(@as(u8, 0xde), cursor.r);
|
||||
try testing.expectEqual(@as(u8, 0xf0), cursor.g);
|
||||
try testing.expectEqual(@as(u8, 0x12), cursor.b);
|
||||
}
|
||||
|
||||
test "kitty color protocol reset foreground" {
|
||||
var t: Terminal = try .init(testing.allocator, .{ .cols = 10, .rows = 10 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
defer s.deinit();
|
||||
|
||||
// Set and reset foreground
|
||||
try s.nextSlice("\x1b]21;foreground=rgb:11/22/33\x1b\\");
|
||||
try testing.expect(t.colors.foreground.get() != null);
|
||||
|
||||
try s.nextSlice("\x1b]21;foreground=\x1b\\");
|
||||
// After reset, should be unset
|
||||
try testing.expect(t.colors.foreground.get() == null);
|
||||
}
|
||||
|
||||
test "palette dirty flag set on color change" {
|
||||
var t: Terminal = try .init(testing.allocator, .{ .cols = 10, .rows = 10 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
defer s.deinit();
|
||||
|
||||
// Clear dirty flag
|
||||
t.flags.dirty.palette = false;
|
||||
|
||||
// Setting palette color should set dirty flag
|
||||
try s.nextSlice("\x1b]4;0;rgb:ff/00/00\x1b\\");
|
||||
try testing.expect(t.flags.dirty.palette);
|
||||
|
||||
// Clear and test reset
|
||||
t.flags.dirty.palette = false;
|
||||
try s.nextSlice("\x1b]104;0\x1b\\");
|
||||
try testing.expect(t.flags.dirty.palette);
|
||||
|
||||
// Clear and test kitty protocol
|
||||
t.flags.dirty.palette = false;
|
||||
try s.nextSlice("\x1b]21;1=rgb:00/ff/00\x1b\\");
|
||||
try testing.expect(t.flags.dirty.palette);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -231,11 +231,19 @@ pub fn init(self: *Termio, alloc: Allocator, opts: termio.Options) !void {
|
|||
.rows = grid_size.rows,
|
||||
.max_scrollback = opts.full_config.@"scrollback-limit",
|
||||
.default_modes = default_modes,
|
||||
.colors = .{
|
||||
.background = .init(opts.config.background.toTerminalRGB()),
|
||||
.foreground = .init(opts.config.foreground.toTerminalRGB()),
|
||||
.cursor = cursor: {
|
||||
const color = opts.config.cursor_color orelse break :cursor .unset;
|
||||
const rgb = color.toTerminalRGB() orelse break :cursor .unset;
|
||||
break :cursor .init(rgb);
|
||||
},
|
||||
.palette = .init(opts.config.palette),
|
||||
},
|
||||
};
|
||||
});
|
||||
errdefer term.deinit(alloc);
|
||||
term.default_palette = opts.config.palette;
|
||||
term.color_palette.colors = opts.config.palette;
|
||||
|
||||
// Set the image size limits
|
||||
try term.screen.kitty_images.setLimit(
|
||||
|
|
@ -262,39 +270,20 @@ pub fn init(self: *Termio, alloc: Allocator, opts: termio.Options) !void {
|
|||
|
||||
// Create our stream handler. This points to memory in self so it
|
||||
// isn't safe to use until self.* is set.
|
||||
const handler: StreamHandler = handler: {
|
||||
const default_cursor_color: ?terminalpkg.color.RGB = color: {
|
||||
if (opts.config.cursor_color) |color| switch (color) {
|
||||
.color => break :color color.color.toTerminalRGB(),
|
||||
.@"cell-foreground",
|
||||
.@"cell-background",
|
||||
=> {},
|
||||
};
|
||||
|
||||
break :color null;
|
||||
};
|
||||
|
||||
break :handler .{
|
||||
.alloc = alloc,
|
||||
.termio_mailbox = &self.mailbox,
|
||||
.surface_mailbox = opts.surface_mailbox,
|
||||
.renderer_state = opts.renderer_state,
|
||||
.renderer_wakeup = opts.renderer_wakeup,
|
||||
.renderer_mailbox = opts.renderer_mailbox,
|
||||
.size = &self.size,
|
||||
.terminal = &self.terminal,
|
||||
.osc_color_report_format = opts.config.osc_color_report_format,
|
||||
.clipboard_write = opts.config.clipboard_write,
|
||||
.enquiry_response = opts.config.enquiry_response,
|
||||
.default_foreground_color = opts.config.foreground.toTerminalRGB(),
|
||||
.default_background_color = opts.config.background.toTerminalRGB(),
|
||||
.default_cursor_style = opts.config.cursor_style,
|
||||
.default_cursor_blink = opts.config.cursor_blink,
|
||||
.default_cursor_color = default_cursor_color,
|
||||
.cursor_color = null,
|
||||
.foreground_color = null,
|
||||
.background_color = null,
|
||||
};
|
||||
const handler: StreamHandler = .{
|
||||
.alloc = alloc,
|
||||
.termio_mailbox = &self.mailbox,
|
||||
.surface_mailbox = opts.surface_mailbox,
|
||||
.renderer_state = opts.renderer_state,
|
||||
.renderer_wakeup = opts.renderer_wakeup,
|
||||
.renderer_mailbox = opts.renderer_mailbox,
|
||||
.size = &self.size,
|
||||
.terminal = &self.terminal,
|
||||
.osc_color_report_format = opts.config.osc_color_report_format,
|
||||
.clipboard_write = opts.config.clipboard_write,
|
||||
.enquiry_response = opts.config.enquiry_response,
|
||||
.default_cursor_style = opts.config.cursor_style,
|
||||
.default_cursor_blink = opts.config.cursor_blink,
|
||||
};
|
||||
|
||||
const thread_enter_state = try ThreadEnterState.create(
|
||||
|
|
@ -449,18 +438,17 @@ pub fn changeConfig(self: *Termio, td: *ThreadData, config: *DerivedConfig) !voi
|
|||
// - command, working-directory: we never restart the underlying
|
||||
// process so we don't care or need to know about these.
|
||||
|
||||
// Update the default palette. Note this will only apply to new colors drawn
|
||||
// since we decode all palette colors to RGB on usage.
|
||||
self.terminal.default_palette = config.palette;
|
||||
// Update the default palette.
|
||||
self.terminal.colors.palette.changeDefault(config.palette);
|
||||
self.terminal.flags.dirty.palette = true;
|
||||
|
||||
// Update the active palette, except for any colors that were modified with
|
||||
// OSC 4
|
||||
for (0..config.palette.len) |i| {
|
||||
if (!self.terminal.color_palette.mask.isSet(i)) {
|
||||
self.terminal.color_palette.colors[i] = config.palette[i];
|
||||
self.terminal.flags.dirty.palette = true;
|
||||
}
|
||||
}
|
||||
// Update all our other colors
|
||||
self.terminal.colors.background.default = config.background.toTerminalRGB();
|
||||
self.terminal.colors.foreground.default = config.foreground.toTerminalRGB();
|
||||
self.terminal.colors.cursor.default = cursor: {
|
||||
const color = config.cursor_color orelse break :cursor null;
|
||||
break :cursor color.toTerminalRGB() orelse break :cursor null;
|
||||
};
|
||||
|
||||
// Set the image size limits
|
||||
try self.terminal.screen.kitty_images.setLimit(
|
||||
|
|
|
|||
|
|
@ -45,22 +45,6 @@ pub const StreamHandler = struct {
|
|||
default_cursor: bool = true,
|
||||
default_cursor_style: terminal.CursorStyle,
|
||||
default_cursor_blink: ?bool,
|
||||
default_cursor_color: ?terminal.color.RGB,
|
||||
|
||||
/// Actual cursor color. This can be changed with OSC 12. If unset, falls
|
||||
/// back to the default cursor color.
|
||||
cursor_color: ?terminal.color.RGB,
|
||||
|
||||
/// The default foreground and background color are those set by the user's
|
||||
/// config file.
|
||||
default_foreground_color: terminal.color.RGB,
|
||||
default_background_color: terminal.color.RGB,
|
||||
|
||||
/// The foreground and background color as set by an OSC 10 or OSC 11
|
||||
/// sequence. If unset then the respective color falls back to the default
|
||||
/// value.
|
||||
foreground_color: ?terminal.color.RGB,
|
||||
background_color: ?terminal.color.RGB,
|
||||
|
||||
/// The response to use for ENQ requests. The memory is owned by
|
||||
/// whoever owns StreamHandler.
|
||||
|
|
@ -114,20 +98,8 @@ pub const StreamHandler = struct {
|
|||
self.osc_color_report_format = config.osc_color_report_format;
|
||||
self.clipboard_write = config.clipboard_write;
|
||||
self.enquiry_response = config.enquiry_response;
|
||||
self.default_foreground_color = config.foreground.toTerminalRGB();
|
||||
self.default_background_color = config.background.toTerminalRGB();
|
||||
self.default_cursor_style = config.cursor_style;
|
||||
self.default_cursor_blink = config.cursor_blink;
|
||||
self.default_cursor_color = color: {
|
||||
if (config.cursor_color) |color| switch (color) {
|
||||
.color => break :color color.color.toTerminalRGB(),
|
||||
.@"cell-foreground",
|
||||
.@"cell-background",
|
||||
=> {},
|
||||
};
|
||||
|
||||
break :color null;
|
||||
};
|
||||
|
||||
// If our cursor is the default, then we update it immediately.
|
||||
if (self.default_cursor) self.setCursorStyle(.default) catch |err| {
|
||||
|
|
@ -224,7 +196,6 @@ pub const StreamHandler = struct {
|
|||
.erase_display_above => self.terminal.eraseDisplay(.above, value),
|
||||
.erase_display_complete => {
|
||||
try self.terminal.scrollViewport(.{ .bottom = {} });
|
||||
try self.queueRender();
|
||||
self.terminal.eraseDisplay(.complete, value);
|
||||
},
|
||||
.erase_display_scrollback => self.terminal.eraseDisplay(.scrollback, value),
|
||||
|
|
@ -597,10 +568,7 @@ pub const StreamHandler = struct {
|
|||
.autorepeat => {},
|
||||
|
||||
// Schedule a render since we changed colors
|
||||
.reverse_colors => {
|
||||
self.terminal.flags.dirty.reverse_colors = true;
|
||||
try self.queueRender();
|
||||
},
|
||||
.reverse_colors => self.terminal.flags.dirty.reverse_colors = true,
|
||||
|
||||
// Origin resets cursor pos. This is called whether or not
|
||||
// we're enabling or disabling origin mode and whether or
|
||||
|
|
@ -616,17 +584,14 @@ pub const StreamHandler = struct {
|
|||
|
||||
.alt_screen_legacy => {
|
||||
self.terminal.switchScreenMode(.@"47", enabled);
|
||||
try self.queueRender();
|
||||
},
|
||||
|
||||
.alt_screen => {
|
||||
self.terminal.switchScreenMode(.@"1047", enabled);
|
||||
try self.queueRender();
|
||||
},
|
||||
|
||||
.alt_screen_save_cursor_clear_enter => {
|
||||
self.terminal.switchScreenMode(.@"1049", enabled);
|
||||
try self.queueRender();
|
||||
},
|
||||
|
||||
// Mode 1048 is xterm's conditional save cursor depending
|
||||
|
|
@ -662,7 +627,6 @@ pub const StreamHandler = struct {
|
|||
// forever.
|
||||
.synchronized_output => {
|
||||
if (enabled) self.messageWriter(.{ .start_synchronized_output = {} });
|
||||
try self.queueRender();
|
||||
},
|
||||
|
||||
.linefeed => {
|
||||
|
|
@ -1133,28 +1097,12 @@ pub const StreamHandler = struct {
|
|||
switch (set.target) {
|
||||
.palette => |i| {
|
||||
self.terminal.flags.dirty.palette = true;
|
||||
self.terminal.color_palette.colors[i] = set.color;
|
||||
self.terminal.color_palette.mask.set(i);
|
||||
self.terminal.colors.palette.set(i, set.color);
|
||||
},
|
||||
.dynamic => |dynamic| switch (dynamic) {
|
||||
.foreground => {
|
||||
self.foreground_color = set.color;
|
||||
self.rendererMessageWriter(.{
|
||||
.foreground_color = set.color,
|
||||
});
|
||||
},
|
||||
.background => {
|
||||
self.background_color = set.color;
|
||||
self.rendererMessageWriter(.{
|
||||
.background_color = set.color,
|
||||
});
|
||||
},
|
||||
.cursor => {
|
||||
self.cursor_color = set.color;
|
||||
self.rendererMessageWriter(.{
|
||||
.cursor_color = set.color,
|
||||
});
|
||||
},
|
||||
.foreground => self.terminal.colors.foreground.set(set.color),
|
||||
.background => self.terminal.colors.background.set(set.color),
|
||||
.cursor => self.terminal.colors.cursor.set(set.color),
|
||||
.pointer_foreground,
|
||||
.pointer_background,
|
||||
.tektronix_foreground,
|
||||
|
|
@ -1178,52 +1126,44 @@ pub const StreamHandler = struct {
|
|||
|
||||
.reset => |target| switch (target) {
|
||||
.palette => |i| {
|
||||
const mask = &self.terminal.color_palette.mask;
|
||||
self.terminal.flags.dirty.palette = true;
|
||||
self.terminal.color_palette.colors[i] = self.terminal.default_palette[i];
|
||||
mask.unset(i);
|
||||
self.terminal.colors.palette.reset(i);
|
||||
|
||||
self.surfaceMessageWriter(.{
|
||||
.color_change = .{
|
||||
.target = target,
|
||||
.color = self.terminal.color_palette.colors[i],
|
||||
.color = self.terminal.colors.palette.current[i],
|
||||
},
|
||||
});
|
||||
},
|
||||
.dynamic => |dynamic| switch (dynamic) {
|
||||
.foreground => {
|
||||
self.foreground_color = null;
|
||||
self.rendererMessageWriter(.{
|
||||
.foreground_color = self.foreground_color,
|
||||
});
|
||||
self.terminal.colors.foreground.reset();
|
||||
|
||||
self.surfaceMessageWriter(.{ .color_change = .{
|
||||
.target = target,
|
||||
.color = self.default_foreground_color,
|
||||
} });
|
||||
},
|
||||
.background => {
|
||||
self.background_color = null;
|
||||
self.rendererMessageWriter(.{
|
||||
.background_color = self.background_color,
|
||||
});
|
||||
|
||||
self.surfaceMessageWriter(.{ .color_change = .{
|
||||
.target = target,
|
||||
.color = self.default_background_color,
|
||||
} });
|
||||
},
|
||||
.cursor => {
|
||||
self.cursor_color = null;
|
||||
|
||||
self.rendererMessageWriter(.{
|
||||
.cursor_color = self.cursor_color,
|
||||
});
|
||||
|
||||
if (self.default_cursor_color) |color| {
|
||||
if (self.terminal.colors.foreground.default) |c| {
|
||||
self.surfaceMessageWriter(.{ .color_change = .{
|
||||
.target = target,
|
||||
.color = color,
|
||||
.color = c,
|
||||
} });
|
||||
}
|
||||
},
|
||||
.background => {
|
||||
self.terminal.colors.background.reset();
|
||||
|
||||
if (self.terminal.colors.background.default) |c| {
|
||||
self.surfaceMessageWriter(.{ .color_change = .{
|
||||
.target = target,
|
||||
.color = c,
|
||||
} });
|
||||
}
|
||||
},
|
||||
.cursor => {
|
||||
self.terminal.colors.cursor.reset();
|
||||
|
||||
if (self.terminal.colors.cursor.default) |c| {
|
||||
self.surfaceMessageWriter(.{ .color_change = .{
|
||||
.target = target,
|
||||
.color = c,
|
||||
} });
|
||||
}
|
||||
},
|
||||
|
|
@ -1242,15 +1182,15 @@ pub const StreamHandler = struct {
|
|||
},
|
||||
|
||||
.reset_palette => {
|
||||
const mask = &self.terminal.color_palette.mask;
|
||||
var mask_iterator = mask.iterator(.{});
|
||||
while (mask_iterator.next()) |i| {
|
||||
const mask = &self.terminal.colors.palette.mask;
|
||||
var mask_it = mask.iterator(.{});
|
||||
while (mask_it.next()) |i| {
|
||||
self.terminal.flags.dirty.palette = true;
|
||||
self.terminal.color_palette.colors[i] = self.terminal.default_palette[i];
|
||||
self.terminal.colors.palette.reset(@intCast(i));
|
||||
self.surfaceMessageWriter(.{
|
||||
.color_change = .{
|
||||
.target = .{ .palette = @intCast(i) },
|
||||
.color = self.terminal.color_palette.colors[i],
|
||||
.color = self.terminal.colors.palette.current[i],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -1266,14 +1206,12 @@ pub const StreamHandler = struct {
|
|||
if (self.osc_color_report_format == .none) break :report;
|
||||
|
||||
const color = switch (kind) {
|
||||
.palette => |i| self.terminal.color_palette.colors[i],
|
||||
.palette => |i| self.terminal.colors.palette.current[i],
|
||||
.dynamic => |dynamic| switch (dynamic) {
|
||||
.foreground => self.foreground_color orelse self.default_foreground_color,
|
||||
.background => self.background_color orelse self.default_background_color,
|
||||
.cursor => self.cursor_color orelse
|
||||
self.default_cursor_color orelse
|
||||
self.foreground_color orelse
|
||||
self.default_foreground_color,
|
||||
.foreground => self.terminal.colors.foreground.get().?,
|
||||
.background => self.terminal.colors.background.get().?,
|
||||
.cursor => self.terminal.colors.cursor.get() orelse
|
||||
self.terminal.colors.foreground.get().?,
|
||||
.pointer_foreground,
|
||||
.pointer_background,
|
||||
.tektronix_foreground,
|
||||
|
|
@ -1399,11 +1337,11 @@ pub const StreamHandler = struct {
|
|||
if (stream.written().len == 0) try writer.writeAll("\x1b]21");
|
||||
|
||||
const color: terminal.color.RGB = switch (key) {
|
||||
.palette => |palette| self.terminal.color_palette.colors[palette],
|
||||
.palette => |palette| self.terminal.colors.palette.current[palette],
|
||||
.special => |special| switch (special) {
|
||||
.foreground => self.foreground_color orelse self.default_foreground_color,
|
||||
.background => self.background_color orelse self.default_background_color,
|
||||
.cursor => self.cursor_color orelse self.default_cursor_color,
|
||||
.foreground => self.terminal.colors.foreground.get(),
|
||||
.background => self.terminal.colors.background.get(),
|
||||
.cursor => self.terminal.colors.cursor.get(),
|
||||
else => {
|
||||
log.warn("ignoring unsupported kitty color protocol key: {f}", .{key});
|
||||
continue;
|
||||
|
|
@ -1422,71 +1360,39 @@ pub const StreamHandler = struct {
|
|||
.set => |v| switch (v.key) {
|
||||
.palette => |palette| {
|
||||
self.terminal.flags.dirty.palette = true;
|
||||
self.terminal.color_palette.colors[palette] = v.color;
|
||||
self.terminal.color_palette.mask.unset(palette);
|
||||
self.terminal.colors.palette.set(palette, v.color);
|
||||
},
|
||||
|
||||
.special => |special| {
|
||||
const msg: renderer.Message = switch (special) {
|
||||
.foreground => msg: {
|
||||
self.foreground_color = v.color;
|
||||
break :msg .{ .foreground_color = v.color };
|
||||
},
|
||||
.background => msg: {
|
||||
self.background_color = v.color;
|
||||
break :msg .{ .background_color = v.color };
|
||||
},
|
||||
.cursor => msg: {
|
||||
self.cursor_color = v.color;
|
||||
break :msg .{ .cursor_color = v.color };
|
||||
},
|
||||
else => {
|
||||
log.warn(
|
||||
"ignoring unsupported kitty color protocol key: {f}",
|
||||
.{v.key},
|
||||
);
|
||||
continue;
|
||||
},
|
||||
};
|
||||
|
||||
// See messageWriter which has similar logic and
|
||||
// explains why we may have to do this.
|
||||
self.rendererMessageWriter(msg);
|
||||
.special => |special| switch (special) {
|
||||
.foreground => self.terminal.colors.foreground.set(v.color),
|
||||
.background => self.terminal.colors.background.set(v.color),
|
||||
.cursor => self.terminal.colors.cursor.set(v.color),
|
||||
else => {
|
||||
log.warn(
|
||||
"ignoring unsupported kitty color protocol key: {f}",
|
||||
.{v.key},
|
||||
);
|
||||
continue;
|
||||
},
|
||||
},
|
||||
},
|
||||
.reset => |key| switch (key) {
|
||||
.palette => |palette| {
|
||||
self.terminal.flags.dirty.palette = true;
|
||||
self.terminal.color_palette.colors[palette] = self.terminal.default_palette[palette];
|
||||
self.terminal.color_palette.mask.unset(palette);
|
||||
self.terminal.colors.palette.reset(palette);
|
||||
},
|
||||
|
||||
.special => |special| {
|
||||
const msg: renderer.Message = switch (special) {
|
||||
.foreground => msg: {
|
||||
self.foreground_color = null;
|
||||
break :msg .{ .foreground_color = self.foreground_color };
|
||||
},
|
||||
.background => msg: {
|
||||
self.background_color = null;
|
||||
break :msg .{ .background_color = self.background_color };
|
||||
},
|
||||
.cursor => msg: {
|
||||
self.cursor_color = null;
|
||||
break :msg .{ .cursor_color = self.cursor_color };
|
||||
},
|
||||
else => {
|
||||
log.warn(
|
||||
"ignoring unsupported kitty color protocol key: {f}",
|
||||
.{key},
|
||||
);
|
||||
continue;
|
||||
},
|
||||
};
|
||||
|
||||
// See messageWriter which has similar logic and
|
||||
// explains why we may have to do this.
|
||||
self.rendererMessageWriter(msg);
|
||||
.special => |special| switch (special) {
|
||||
.foreground => self.terminal.colors.foreground.reset(),
|
||||
.background => self.terminal.colors.background.reset(),
|
||||
.cursor => self.terminal.colors.cursor.reset(),
|
||||
else => {
|
||||
log.warn(
|
||||
"ignoring unsupported kitty color protocol key: {f}",
|
||||
.{key},
|
||||
);
|
||||
continue;
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue