Merge pull request #850 from gpanders/osc4

Fully implement OSC 4, 10, 11, 12, 104, 110, 111, and 112
This commit is contained in:
Mitchell Hashimoto
2023-11-09 15:16:08 -08:00
committed by GitHub
12 changed files with 658 additions and 97 deletions

View File

@ -551,15 +551,16 @@ keybind: Keybinds = .{},
@"shell-integration-features": ShellIntegrationFeatures = .{}, @"shell-integration-features": ShellIntegrationFeatures = .{},
/// Sets the reporting format for OSC sequences that request color information. /// Sets the reporting format for OSC sequences that request color information.
/// Ghostty currently supports OSC 10 (foreground) and OSC 11 (background) queries, /// Ghostty currently supports OSC 10 (foreground), OSC 11 (background), and OSC
/// and by default the reported values are scaled-up RGB values, where each component /// 4 (256 color palette) queries, and by default the reported values are
/// are 16 bits. This is how most terminals report these values. However, some legacy /// scaled-up RGB values, where each component are 16 bits. This is how most
/// applications may require 8-bit, unscaled, components. We also support turning off /// terminals report these values. However, some legacy applications may require
/// reporting alltogether. The components are lowercase hex values. /// 8-bit, unscaled, components. We also support turning off reporting
/// alltogether. The components are lowercase hex values.
/// ///
/// Allowable values are: /// 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 /// * "8-bit" - Color components are return unscaled, i.e. rr/gg/bb
/// * "16-bit" - Color components are returned scaled, e.g. rrrr/gggg/bbbb /// * "16-bit" - Color components are returned scaled, e.g. rrrr/gggg/bbbb
/// ///

View File

@ -241,6 +241,24 @@ pub const VTEvent = struct {
try alloc.dupeZ(u8, @tagName(value)), 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) { else => switch (Value) {
u8 => try md.put( u8 => try md.put(
key, key,

View File

@ -64,6 +64,18 @@ padding: renderer.Options.Padding,
/// True if the window is focused /// True if the window is focused
focused: bool, 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 /// 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 /// but we keep this around so that we don't reallocate. Each set of
/// cells goes into a separate shader. /// cells goes into a separate shader.
@ -254,6 +266,9 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
.screen_size = null, .screen_size = null,
.padding = options.padding, .padding = options.padding,
.focused = true, .focused = true,
.foreground_color = options.config.foreground,
.background_color = options.config.background,
.cursor_color = options.config.cursor_color,
// Render state // Render state
.cells_bg = .{}, .cells_bg = .{},
@ -461,15 +476,15 @@ pub fn render(
} }
// Swap bg/fg if the terminal is reversed // Swap bg/fg if the terminal is reversed
const bg = self.config.background; const bg = self.background_color;
const fg = self.config.foreground; const fg = self.foreground_color;
defer { defer {
self.config.background = bg; self.background_color = bg;
self.config.foreground = fg; self.foreground_color = fg;
} }
if (state.terminal.modes.get(.reverse_colors)) { if (state.terminal.modes.get(.reverse_colors)) {
self.config.background = fg; self.background_color = fg;
self.config.foreground = bg; self.foreground_color = bg;
} }
// We used to share terminal state, but we've since learned through // We used to share terminal state, but we've since learned through
@ -509,7 +524,7 @@ pub fn render(
} }
break :critical .{ break :critical .{
.bg = self.config.background, .bg = self.background_color,
.selection = selection, .selection = selection,
.screen = screen_copy, .screen = screen_copy,
.preedit = if (cursor_style != null) state.preedit else null, .preedit = if (cursor_style != null) state.preedit else null,
@ -1267,21 +1282,21 @@ pub fn updateCell(
const colors: BgFg = colors: { const colors: BgFg = colors: {
// If we are selected, we our colors are just inverted fg/bg // If we are selected, we our colors are just inverted fg/bg
var selection_res: ?BgFg = if (selected) .{ var selection_res: ?BgFg = if (selected) .{
.bg = self.config.selection_background orelse self.config.foreground, .bg = self.config.selection_background orelse self.foreground_color,
.fg = self.config.selection_foreground orelse self.config.background, .fg = self.config.selection_foreground orelse self.background_color,
} else null; } else null;
const res: BgFg = selection_res orelse if (!cell.attrs.inverse) .{ const res: BgFg = selection_res orelse if (!cell.attrs.inverse) .{
// In normal mode, background and fg match the cell. We // In normal mode, background and fg match the cell. We
// un-optionalize the fg by defaulting to our fg color. // un-optionalize the fg by defaulting to our fg color.
.bg = if (cell.attrs.has_bg) cell.bg else null, .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 .{ } else .{
// In inverted mode, the background MUST be set to something // In inverted mode, the background MUST be set to something
// (is never null) so it is either the fg or default fg. The // (is never null) so it is either the fg or default fg. The
// fg is either the bg or default background. // fg is either the bg or default background.
.bg = if (cell.attrs.has_fg) cell.fg else self.config.foreground, .bg = if (cell.attrs.has_fg) cell.fg else self.foreground_color,
.fg = if (cell.attrs.has_bg) cell.bg else self.config.background, .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 // If the cell is "invisible" then we just make fg = bg so that
@ -1289,7 +1304,7 @@ pub fn updateCell(
if (cell.attrs.invisible) { if (cell.attrs.invisible) {
break :colors BgFg{ break :colors BgFg{
.bg = res.bg, .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 // If we have a background and its not the default background
// then we apply background opacity // 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; break :bg_alpha alpha;
} }
@ -1434,7 +1449,7 @@ fn addCursor(
), screen.cursor.x - 1 }; ), 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: u8 = if (!self.focused) 255 else alpha: {
const alpha = 255 * self.config.cursor_opacity; const alpha = 255 * self.config.cursor_opacity;
break :alpha @intFromFloat(@ceil(alpha)); break :alpha @intFromFloat(@ceil(alpha));

View File

@ -67,6 +67,18 @@ font_shaper: font.Shaper,
/// True if the window is focused /// True if the window is focused
focused: bool, 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 options
padding: renderer.Options.Padding, padding: renderer.Options.Padding,
@ -310,6 +322,9 @@ pub fn init(alloc: Allocator, options: renderer.Options) !OpenGL {
.font_shaper = shaper, .font_shaper = shaper,
.draw_background = options.config.background, .draw_background = options.config.background,
.focused = true, .focused = true,
.foreground_color = options.config.foreground,
.background_color = options.config.background,
.cursor_color = options.config.cursor_color,
.padding = options.padding, .padding = options.padding,
.surface_mailbox = options.surface_mailbox, .surface_mailbox = options.surface_mailbox,
.deferred_font_size = .{ .metrics = metrics }, .deferred_font_size = .{ .metrics = metrics },
@ -586,15 +601,15 @@ pub fn render(
} }
// Swap bg/fg if the terminal is reversed // Swap bg/fg if the terminal is reversed
const bg = self.config.background; const bg = self.background_color;
const fg = self.config.foreground; const fg = self.foreground_color;
defer { defer {
self.config.background = bg; self.background_color = bg;
self.config.foreground = fg; self.foreground_color = fg;
} }
if (state.terminal.modes.get(.reverse_colors)) { if (state.terminal.modes.get(.reverse_colors)) {
self.config.background = fg; self.background_color = fg;
self.config.foreground = bg; self.foreground_color = bg;
} }
// We used to share terminal state, but we've since learned through // We used to share terminal state, but we've since learned through
@ -627,7 +642,7 @@ pub fn render(
); );
break :critical .{ break :critical .{
.gl_bg = self.config.background, .gl_bg = self.background_color,
.selection = selection, .selection = selection,
.screen = screen_copy, .screen = screen_copy,
.preedit = if (cursor_style != null) state.preedit else null, .preedit = if (cursor_style != null) state.preedit else null,
@ -878,7 +893,7 @@ fn addCursor(
), screen.cursor.x - 1 }; ), 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: u8 = if (!self.focused) 255 else alpha: {
const alpha = 255 * self.config.cursor_opacity; const alpha = 255 * self.config.cursor_opacity;
break :alpha @intFromFloat(@ceil(alpha)); break :alpha @intFromFloat(@ceil(alpha));
@ -1010,21 +1025,21 @@ pub fn updateCell(
const colors: BgFg = colors: { const colors: BgFg = colors: {
// If we are selected, we our colors are just inverted fg/bg // If we are selected, we our colors are just inverted fg/bg
var selection_res: ?BgFg = if (selected) .{ var selection_res: ?BgFg = if (selected) .{
.bg = self.config.selection_background orelse self.config.foreground, .bg = self.config.selection_background orelse self.foreground_color,
.fg = self.config.selection_foreground orelse self.config.background, .fg = self.config.selection_foreground orelse self.background_color,
} else null; } else null;
const res: BgFg = selection_res orelse if (!cell.attrs.inverse) .{ const res: BgFg = selection_res orelse if (!cell.attrs.inverse) .{
// In normal mode, background and fg match the cell. We // In normal mode, background and fg match the cell. We
// un-optionalize the fg by defaulting to our fg color. // un-optionalize the fg by defaulting to our fg color.
.bg = if (cell.attrs.has_bg) cell.bg else null, .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 .{ } else .{
// In inverted mode, the background MUST be set to something // In inverted mode, the background MUST be set to something
// (is never null) so it is either the fg or default fg. The // (is never null) so it is either the fg or default fg. The
// fg is either the bg or default background. // fg is either the bg or default background.
.bg = if (cell.attrs.has_fg) cell.fg else self.config.foreground, .bg = if (cell.attrs.has_fg) cell.fg else self.foreground_color,
.fg = if (cell.attrs.has_bg) cell.bg else self.config.background, .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 // If the cell is "invisible" then we just make fg = bg so that
@ -1032,7 +1047,7 @@ pub fn updateCell(
if (cell.attrs.invisible) { if (cell.attrs.invisible) {
break :colors BgFg{ break :colors BgFg{
.bg = res.bg, .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 // If we have a background and its not the default background
// then we apply background opacity // 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; break :bg_alpha alpha;
} }

View File

@ -262,6 +262,18 @@ fn drainMailbox(self: *Thread) !void {
try self.renderer.setFontSize(size); 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| { .resize => |v| {
try self.renderer.setScreenSize(v.screen_size, v.padding); try self.renderer.setScreenSize(v.screen_size, v.padding);
}, },

View File

@ -3,6 +3,7 @@ const assert = std.debug.assert;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const font = @import("../font/main.zig"); const font = @import("../font/main.zig");
const renderer = @import("../renderer.zig"); const renderer = @import("../renderer.zig");
const terminal = @import("../terminal/main.zig");
/// The messages that can be sent to a renderer thread. /// The messages that can be sent to a renderer thread.
pub const Message = union(enum) { pub const Message = union(enum) {
@ -20,6 +21,18 @@ pub const Message = union(enum) {
/// the size changes. /// the size changes.
font_size: font.face.DesiredSize, 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. /// Changes the screen size.
resize: struct { resize: struct {
/// The full screen (drawable) size. This does NOT include padding. /// The full screen (drawable) size. This does NOT include padding.

View File

@ -827,7 +827,8 @@ test "osc: 112 incomplete sequence" {
try testing.expect(a[2] == null); try testing.expect(a[2] == null);
const cmd = a[0].?.osc_dispatch; 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);
} }
} }

View File

@ -75,8 +75,17 @@ scrolling_region: ScrollingRegion,
/// The last reported pwd, if any. /// The last reported pwd, if any.
pwd: std.ArrayList(u8), pwd: std.ArrayList(u8),
/// The color palette to use /// The default color palette. This is only modified by changing the config file
color_palette: color.Palette = color.default, /// 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 /// The previous printed character. This is used for the repeat previous
/// char CSI (ESC [ <n> b). /// char CSI (ESC [ <n> b).
@ -560,12 +569,12 @@ pub fn setAttribute(self: *Terminal, attr: sgr.Attribute) !void {
.@"8_fg" => |n| { .@"8_fg" => |n| {
self.screen.cursor.pen.attrs.has_fg = true; 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| { .@"8_bg" => |n| {
self.screen.cursor.pen.attrs.has_bg = true; 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, .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| { .@"8_bright_fg" => |n| {
self.screen.cursor.pen.attrs.has_fg = true; 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| { .@"8_bright_bg" => |n| {
self.screen.cursor.pen.attrs.has_bg = true; 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| { .@"256_fg" => |idx| {
self.screen.cursor.pen.attrs.has_fg = true; 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| { .@"256_bg" => |idx| {
self.screen.cursor.pen.attrs.has_bg = true; 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, .unknown => return error.InvalidAttribute,

View File

@ -147,6 +147,112 @@ pub const RGB = struct {
try std.testing.expectEqual(@as(usize, 24), @bitSizeOf(RGB)); try std.testing.expectEqual(@as(usize, 24), @bitSizeOf(RGB));
try std.testing.expectEqual(@as(usize, 3), @sizeOf(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:<red>/<green>/<blue>
///
/// <red>, <green>, <blue> := h | hh | hhh | hhhh
///
/// where `h` is a single hexadecimal digit.
///
/// Alternatively, the form
///
/// rgbi:<red>/<green>/<blue>
///
/// where <red>, <green>, and <blue> are floating point values between 0.0
/// and 1.0 (inclusive) is also accepted.
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" { test "palette: default" {
@ -158,3 +264,23 @@ test "palette: default" {
try testing.expectEqual(Name.default(@as(Name, @enumFromInt(i))), default[i]); 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"));
}

View File

@ -64,10 +64,6 @@ pub const Command = union(enum) {
// TODO: err option // 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 /// Set or get clipboard contents. If data is null, then the current
/// clipboard contents are sent to the pty. If data is set, this /// clipboard contents are sent to the pty. If data is set, this
/// contents is set on the clipboard. /// contents is set on the clipboard.
@ -94,24 +90,49 @@ pub const Command = union(enum) {
value: []const u8, value: []const u8,
}, },
/// OSC 10 and OSC 11 default color report. /// OSC 4, OSC 10, and OSC 11 color report.
report_default_color: struct { report_color: struct {
/// OSC 10 requests the foreground color, OSC 11 the background color. /// OSC 4 requests a palette color, OSC 10 requests the foreground
kind: DefaultColorKind, /// color, OSC 11 the background color.
kind: ColorKind,
/// We must reply with the same string terminator (ST) as used in the /// We must reply with the same string terminator (ST) as used in the
/// request. /// request.
terminator: Terminator = .st, 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, foreground,
background, background,
cursor,
pub fn code(self: DefaultColorKind) []const u8 { pub fn code(self: ColorKind) []const u8 {
return switch (self) { return switch (self) {
.palette => "4",
.foreground => "10", .foreground => "10",
.background => "11", .background => "11",
.cursor => "12",
}; };
} }
}; };
@ -195,21 +216,24 @@ pub const Parser = struct {
@"1", @"1",
@"10", @"10",
@"11", @"11",
@"12",
@"13", @"13",
@"133", @"133",
@"2", @"2",
@"22", @"22",
@"4",
@"5", @"5",
@"52", @"52",
@"7", @"7",
// OSC 10 is used to query the default foreground color, and to set the default foreground color. // OSC 10 is used to query or set the current foreground color.
// Only querying is currently supported. query_fg_color,
query_default_fg,
// OSC 11 is used to query the default background color, and to set the default background color. // OSC 11 is used to query or set the current background color.
// Only querying is currently supported. query_bg_color,
query_default_bg,
// 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 // We're in a semantic prompt OSC command but we aren't sure
// what the command is yet, i.e. `133;` // what the command is yet, i.e. `133;`
@ -224,6 +248,13 @@ pub const Parser = struct {
clipboard_kind, clipboard_kind,
clipboard_kind_end, 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 // Expect a string parameter. param_str must be set as well as
// buf_start. // buf_start.
string, string,
@ -277,6 +308,7 @@ pub const Parser = struct {
'0' => self.state = .@"0", '0' => self.state = .@"0",
'1' => self.state = .@"1", '1' => self.state = .@"1",
'2' => self.state = .@"2", '2' => self.state = .@"2",
'4' => self.state = .@"4",
'5' => self.state = .@"5", '5' => self.state = .@"5",
'7' => self.state = .@"7", '7' => self.state = .@"7",
else => self.state = .invalid, else => self.state = .invalid,
@ -296,22 +328,47 @@ pub const Parser = struct {
.@"1" => switch (c) { .@"1" => switch (c) {
'0' => self.state = .@"10", '0' => self.state = .@"10",
'1' => self.state = .@"11", '1' => self.state = .@"11",
'2' => self.state = .@"12",
'3' => self.state = .@"13", '3' => self.state = .@"13",
else => self.state = .invalid, else => self.state = .invalid,
}, },
.@"10" => switch (c) { .@"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, else => self.state = .invalid,
}, },
.@"11" => switch (c) { .@"11" => switch (c) {
';' => self.state = .query_default_bg, ';' => self.state = .query_bg_color,
'2' => { '0' => {
self.command = .{ .reset_color = .{ .kind = .foreground, .value = undefined } };
self.complete = true; self.complete = true;
self.command = .{ .reset_cursor_color = {} };
self.state = .invalid; 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, else => self.state = .invalid,
}, },
@ -348,6 +405,61 @@ pub const Parser = struct {
else => self.state = .invalid, 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) { .@"5" => switch (c) {
'2' => self.state = .@"52", '2' => self.state = .@"52",
else => self.state = .invalid, else => self.state = .invalid,
@ -394,20 +506,58 @@ pub const Parser = struct {
else => self.state = .invalid, 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.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.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) { .semantic_prompt => switch (c) {
@ -616,7 +766,7 @@ pub const Parser = struct {
} }
switch (self.command) { switch (self.command) {
.report_default_color => |*c| c.terminator = Terminator.init(terminator_ch), .report_color => |*c| c.terminator = Terminator.init(terminator_ch),
else => {}, else => {},
} }
@ -788,7 +938,7 @@ test "OSC: end_of_input" {
try testing.expect(cmd == .end_of_input); try testing.expect(cmd == .end_of_input);
} }
test "OSC: reset_cursor_color" { test "OSC: reset cursor color" {
const testing = std.testing; const testing = std.testing;
var p: Parser = .{}; var p: Parser = .{};
@ -797,7 +947,8 @@ test "OSC: reset_cursor_color" {
for (input) |ch| p.next(ch); for (input) |ch| p.next(ch);
const cmd = p.end(null).?; 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" { test "OSC: get/set clipboard" {
@ -902,9 +1053,23 @@ test "OSC: report default foreground color" {
// This corresponds to ST = ESC followed by \ // This corresponds to ST = ESC followed by \
const cmd = p.end('\x1b').?; const cmd = p.end('\x1b').?;
try testing.expect(cmd == .report_default_color); try testing.expect(cmd == .report_color);
try testing.expectEqual(cmd.report_default_color.kind, .foreground); try testing.expectEqual(cmd.report_color.kind, .foreground);
try testing.expectEqual(cmd.report_default_color.terminator, .st); 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" { test "OSC: report default background color" {
@ -917,7 +1082,49 @@ test "OSC: report default background color" {
// This corresponds to ST = BEL character // This corresponds to ST = BEL character
const cmd = p.end('\x07').?; const cmd = p.end('\x07').?;
try testing.expect(cmd == .report_default_color); try testing.expect(cmd == .report_color);
try testing.expectEqual(cmd.report_default_color.kind, .background); try testing.expectEqual(cmd.report_color.kind, .background);
try testing.expectEqual(cmd.report_default_color.terminator, .bel); 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");
} }

View File

@ -1045,17 +1045,26 @@ pub fn Stream(comptime Handler: type) type {
} else log.warn("unimplemented OSC callback: {}", .{cmd}); } else log.warn("unimplemented OSC callback: {}", .{cmd});
}, },
.report_default_color => |v| { .report_color => |v| {
if (@hasDecl(T, "reportDefaultColor")) { if (@hasDecl(T, "reportColor")) {
try self.handler.reportDefaultColor(v.kind, v.terminator); try self.handler.reportColor(v.kind, v.terminator);
return; return;
} else log.warn("unimplemented OSC callback: {}", .{cmd}); } else log.warn("unimplemented OSC callback: {}", .{cmd});
}, },
else => if (@hasDecl(T, "oscUnimplemented")) .set_color => |v| {
try self.handler.oscUnimplemented(cmd) if (@hasDecl(T, "setColor")) {
else try self.handler.setColor(v.kind, v.value);
log.warn("unimplemented OSC command: {}", .{cmd}), 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. // Fall through for when we don't have a handler.

View File

@ -69,13 +69,23 @@ grid_size: renderer.GridSize,
/// it when a CSI q with default is called. /// it when a CSI q with default is called.
default_cursor_style: terminal.Cursor.Style, default_cursor_style: terminal.Cursor.Style,
default_cursor_blink: ?bool, 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_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, 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. /// The OSC 10/11 reply style.
osc_color_report_format: configpkg.Config.OSCColorReportFormat, osc_color_report_format: configpkg.Config.OSCColorReportFormat,
@ -90,6 +100,7 @@ pub const DerivedConfig = struct {
image_storage_limit: usize, image_storage_limit: usize,
cursor_style: terminal.Cursor.Style, cursor_style: terminal.Cursor.Style,
cursor_blink: ?bool, cursor_blink: ?bool,
cursor_color: ?configpkg.Config.Color,
foreground: configpkg.Config.Color, foreground: configpkg.Config.Color,
background: configpkg.Config.Color, background: configpkg.Config.Color,
osc_color_report_format: configpkg.Config.OSCColorReportFormat, osc_color_report_format: configpkg.Config.OSCColorReportFormat,
@ -106,6 +117,7 @@ pub const DerivedConfig = struct {
.image_storage_limit = config.@"image-storage-limit", .image_storage_limit = config.@"image-storage-limit",
.cursor_style = config.@"cursor-style", .cursor_style = config.@"cursor-style",
.cursor_blink = config.@"cursor-style-blink", .cursor_blink = config.@"cursor-style-blink",
.cursor_color = config.@"cursor-color",
.foreground = config.foreground, .foreground = config.foreground,
.background = config.background, .background = config.background,
.osc_color_report_format = config.@"osc-color-report-format", .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, opts.grid_size.rows,
); );
errdefer term.deinit(alloc); errdefer term.deinit(alloc);
term.color_palette = opts.config.palette; term.default_palette = opts.config.palette;
// Set the image size limits // Set the image size limits
try term.screen.kitty_images.setLimit(alloc, opts.config.image_storage_limit); 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, .grid_size = opts.grid_size,
.default_cursor_style = opts.config.cursor_style, .default_cursor_style = opts.config.cursor_style,
.default_cursor_blink = opts.config.cursor_blink, .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_foreground_color = config.foreground.toTerminalRGB(),
.default_background_color = config.background.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, .osc_color_report_format = config.osc_color_report_format,
.data = null, .data = null,
}; };
@ -236,8 +258,12 @@ pub fn threadEnter(self: *Exec, thread: *termio.Thread) !ThreadData {
.grid_size = &self.grid_size, .grid_size = &self.grid_size,
.default_cursor_style = self.default_cursor_style, .default_cursor_style = self.default_cursor_style,
.default_cursor_blink = self.default_cursor_blink, .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_foreground_color = self.default_foreground_color,
.default_background_color = self.default_background_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, .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 // Update the palette. Note this will only apply to new colors drawn
// since we decode all palette colors to RGB on usage. // 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 // Update our default cursor style
self.default_cursor_style = config.cursor_style; self.default_cursor_style = config.cursor_style;
self.default_cursor_blink = config.cursor_blink; 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 we have event data, then update our active stream too
if (self.data) |data| { if (self.data) |data| {
@ -1326,8 +1360,23 @@ const StreamHandler = struct {
default_cursor: bool = true, default_cursor: bool = true,
default_cursor_style: terminal.Cursor.Style, default_cursor_style: terminal.Cursor.Style,
default_cursor_blink: ?bool, 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_foreground_color: terminal.color.RGB,
default_background_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, osc_color_report_format: configpkg.Config.OSCColorReportFormat,
pub fn deinit(self: *StreamHandler) void { pub fn deinit(self: *StreamHandler) void {
@ -2150,18 +2199,20 @@ const StreamHandler = struct {
} }
} }
/// Implements OSC 10 and OSC 11, which reports default foreground and /// Implements OSC 4, OSC 10, and OSC 11, which reports palette color,
/// background color respectively. /// default foreground color, and background color respectively.
pub fn reportDefaultColor( pub fn reportColor(
self: *StreamHandler, self: *StreamHandler,
kind: terminal.osc.Command.DefaultColorKind, kind: terminal.osc.Command.ColorKind,
terminator: terminal.osc.Terminator, terminator: terminal.osc.Terminator,
) !void { ) !void {
if (self.osc_color_report_format == .none) return; if (self.osc_color_report_format == .none) return;
const color = switch (kind) { const color = switch (kind) {
.foreground => self.default_foreground_color, .palette => |i| self.terminal.color_palette.colors[i],
.background => self.default_background_color, .foreground => self.foreground_color,
.background => self.background_color,
.cursor => self.cursor_color orelse self.foreground_color,
}; };
var msg: termio.Message = .{ .write_small = .{} }; var msg: termio.Message = .{ .write_small = .{} };
@ -2195,4 +2246,88 @@ const StreamHandler = struct {
msg.write_small.len = @intCast(resp.len); msg.write_small.len = @intCast(resp.len);
self.messageWriter(msg); 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 = {} });
},
}
}
}; };