mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
core: implement setting colors with OSC 4, 10, and 11
This commit is contained in:
@ -262,6 +262,14 @@ fn drainMailbox(self: *Thread) !void {
|
|||||||
try self.renderer.setFontSize(size);
|
try self.renderer.setFontSize(size);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
.foreground_color => |color| {
|
||||||
|
self.renderer.config.foreground = color;
|
||||||
|
},
|
||||||
|
|
||||||
|
.background_color => |color| {
|
||||||
|
self.renderer.config.background = color;
|
||||||
|
},
|
||||||
|
|
||||||
.resize => |v| {
|
.resize => |v| {
|
||||||
try self.renderer.setScreenSize(v.screen_size, v.padding);
|
try self.renderer.setScreenSize(v.screen_size, v.padding);
|
||||||
},
|
},
|
||||||
|
@ -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,14 @@ 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,
|
||||||
|
|
||||||
/// 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.
|
||||||
|
@ -105,6 +105,15 @@ pub const Command = union(enum) {
|
|||||||
terminator: Terminator = .st,
|
terminator: Terminator = .st,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
set_default_color: struct {
|
||||||
|
/// OSC 4 sets a palette color, OSC 10 sets the foreground color, OSC 11
|
||||||
|
/// the background color.
|
||||||
|
kind: DefaultColorKind,
|
||||||
|
|
||||||
|
/// The color spec as a string
|
||||||
|
value: []const u8,
|
||||||
|
},
|
||||||
|
|
||||||
pub const DefaultColorKind = union(enum) {
|
pub const DefaultColorKind = union(enum) {
|
||||||
foreground,
|
foreground,
|
||||||
background,
|
background,
|
||||||
@ -387,7 +396,16 @@ pub const Parser = struct {
|
|||||||
|
|
||||||
self.complete = true;
|
self.complete = true;
|
||||||
},
|
},
|
||||||
else => self.state = .invalid,
|
else => {
|
||||||
|
self.command = .{ .set_default_color = .{
|
||||||
|
.kind = .{ .palette = @intCast(self.temp_state.num) },
|
||||||
|
.value = "",
|
||||||
|
} };
|
||||||
|
|
||||||
|
self.state = .string;
|
||||||
|
self.temp_state = .{ .str = &self.command.set_default_color.value };
|
||||||
|
self.buf_start = self.buf_idx - 1;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
.@"5" => switch (c) {
|
.@"5" => switch (c) {
|
||||||
@ -441,7 +459,16 @@ pub const Parser = struct {
|
|||||||
self.command = .{ .report_default_color = .{ .kind = .foreground } };
|
self.command = .{ .report_default_color = .{ .kind = .foreground } };
|
||||||
self.complete = true;
|
self.complete = true;
|
||||||
},
|
},
|
||||||
else => self.state = .invalid,
|
else => {
|
||||||
|
self.command = .{ .set_default_color = .{
|
||||||
|
.kind = .foreground,
|
||||||
|
.value = "",
|
||||||
|
} };
|
||||||
|
|
||||||
|
self.state = .string;
|
||||||
|
self.temp_state = .{ .str = &self.command.set_default_color.value };
|
||||||
|
self.buf_start = self.buf_idx - 1;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
.query_default_bg => switch (c) {
|
.query_default_bg => switch (c) {
|
||||||
@ -449,7 +476,16 @@ pub const Parser = struct {
|
|||||||
self.command = .{ .report_default_color = .{ .kind = .background } };
|
self.command = .{ .report_default_color = .{ .kind = .background } };
|
||||||
self.complete = true;
|
self.complete = true;
|
||||||
},
|
},
|
||||||
else => self.state = .invalid,
|
else => {
|
||||||
|
self.command = .{ .set_default_color = .{
|
||||||
|
.kind = .background,
|
||||||
|
.value = "",
|
||||||
|
} };
|
||||||
|
|
||||||
|
self.state = .string;
|
||||||
|
self.temp_state = .{ .str = &self.command.set_default_color.value };
|
||||||
|
self.buf_start = self.buf_idx - 1;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
.semantic_prompt => switch (c) {
|
.semantic_prompt => switch (c) {
|
||||||
@ -949,6 +985,20 @@ test "OSC: report default foreground color" {
|
|||||||
try testing.expectEqual(cmd.report_default_color.terminator, .st);
|
try testing.expectEqual(cmd.report_default_color.terminator, .st);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "OSC: set foreground color" {
|
||||||
|
const testing = std.testing;
|
||||||
|
|
||||||
|
var p: Parser = .{};
|
||||||
|
|
||||||
|
const input = "10;rgbi:0.0/0.5/1.0";
|
||||||
|
for (input) |ch| p.next(ch);
|
||||||
|
|
||||||
|
const cmd = p.end('\x07').?;
|
||||||
|
try testing.expect(cmd == .set_default_color);
|
||||||
|
try testing.expectEqual(cmd.set_default_color.kind, .foreground);
|
||||||
|
try testing.expectEqualStrings(cmd.set_default_color.value, "rgbi:0.0/0.5/1.0");
|
||||||
|
}
|
||||||
|
|
||||||
test "OSC: report default background color" {
|
test "OSC: report default background color" {
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
|
|
||||||
@ -964,6 +1014,20 @@ test "OSC: report default background color" {
|
|||||||
try testing.expectEqual(cmd.report_default_color.terminator, .bel);
|
try testing.expectEqual(cmd.report_default_color.terminator, .bel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "OSC: set background color" {
|
||||||
|
const testing = std.testing;
|
||||||
|
|
||||||
|
var p: Parser = .{};
|
||||||
|
|
||||||
|
const input = "11;rgb:f/ff/ffff";
|
||||||
|
for (input) |ch| p.next(ch);
|
||||||
|
|
||||||
|
const cmd = p.end('\x1b').?;
|
||||||
|
try testing.expect(cmd == .set_default_color);
|
||||||
|
try testing.expectEqual(cmd.set_default_color.kind, .background);
|
||||||
|
try testing.expectEqualStrings(cmd.set_default_color.value, "rgb:f/ff/ffff");
|
||||||
|
}
|
||||||
|
|
||||||
test "OSC: get palette color" {
|
test "OSC: get palette color" {
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
|
|
||||||
@ -977,3 +1041,17 @@ test "OSC: get palette color" {
|
|||||||
try testing.expectEqual(cmd.report_default_color.kind, .{ .palette = 1 });
|
try testing.expectEqual(cmd.report_default_color.kind, .{ .palette = 1 });
|
||||||
try testing.expectEqual(cmd.report_default_color.terminator, .st);
|
try testing.expectEqual(cmd.report_default_color.terminator, .st);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "OSC: set palette color" {
|
||||||
|
const testing = std.testing;
|
||||||
|
|
||||||
|
var p: Parser = .{};
|
||||||
|
|
||||||
|
const input = "4;17;rgb:aa/bb/cc";
|
||||||
|
for (input) |ch| p.next(ch);
|
||||||
|
|
||||||
|
const cmd = p.end('\x1b').?;
|
||||||
|
try testing.expect(cmd == .set_default_color);
|
||||||
|
try testing.expectEqual(cmd.set_default_color.kind, .{ .palette = 17 });
|
||||||
|
try testing.expectEqualStrings(cmd.set_default_color.value, "rgb:aa/bb/cc");
|
||||||
|
}
|
||||||
|
@ -1052,6 +1052,13 @@ pub fn Stream(comptime Handler: type) type {
|
|||||||
} else log.warn("unimplemented OSC callback: {}", .{cmd});
|
} else log.warn("unimplemented OSC callback: {}", .{cmd});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
.set_default_color => |v| {
|
||||||
|
if (@hasDecl(T, "setDefaultColor")) {
|
||||||
|
try self.handler.setDefaultColor(v.kind, v.value);
|
||||||
|
return;
|
||||||
|
} else log.warn("unimplemented OSC callback: {}", .{cmd});
|
||||||
|
},
|
||||||
|
|
||||||
else => if (@hasDecl(T, "oscUnimplemented"))
|
else => if (@hasDecl(T, "oscUnimplemented"))
|
||||||
try self.handler.oscUnimplemented(cmd)
|
try self.handler.oscUnimplemented(cmd)
|
||||||
else
|
else
|
||||||
|
@ -2200,4 +2200,123 @@ const StreamHandler = struct {
|
|||||||
msg.write_small.len = @intCast(resp.len);
|
msg.write_small.len = @intCast(resp.len);
|
||||||
self.messageWriter(msg);
|
self.messageWriter(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parse a color from a string of hexadecimal digits or a floating point
|
||||||
|
/// intensity value.
|
||||||
|
///
|
||||||
|
/// If `intensity` is false, the string can contain 1, 2, 3, or 4 characters
|
||||||
|
/// and represents the color value scaled in 4, 8, 12, or 16 bits,
|
||||||
|
/// respectively.
|
||||||
|
///
|
||||||
|
/// If `intensity` is true, the string should contain a floating point value
|
||||||
|
/// between 0.0 and 1.0, inclusive.
|
||||||
|
fn parseColor(value: []const u8, intensity: bool) !u8 {
|
||||||
|
if (intensity) {
|
||||||
|
const i = try std.fmt.parseFloat(f64, value);
|
||||||
|
if (i < 0.0 or i > 1.0) {
|
||||||
|
return error.InvalidValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return @intFromFloat(i * std.math.maxInt(u8));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.len == 0 or value.len > 4) {
|
||||||
|
return error.InvalidValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const color = try std.fmt.parseUnsigned(u16, value, 16);
|
||||||
|
const divisor: usize = switch (value.len) {
|
||||||
|
1 => std.math.maxInt(u4),
|
||||||
|
2 => std.math.maxInt(u8),
|
||||||
|
3 => std.math.maxInt(u12),
|
||||||
|
4 => std.math.maxInt(u16),
|
||||||
|
else => unreachable,
|
||||||
|
};
|
||||||
|
|
||||||
|
return @intCast(color * std.math.maxInt(u8) / divisor);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a color specification of the form
|
||||||
|
///
|
||||||
|
/// rgb:<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.
|
||||||
|
fn parseColorSpec(value: []const u8) !terminal.color.RGB {
|
||||||
|
const minimum_length = "rgb:a/a/a".len;
|
||||||
|
if (value.len < minimum_length or !std.mem.eql(u8, value[0..3], "rgb")) {
|
||||||
|
return error.InvalidFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
var i: usize = 3;
|
||||||
|
|
||||||
|
const use_intensity = if (value[i] == 'i') blk: {
|
||||||
|
i += 1;
|
||||||
|
break :blk true;
|
||||||
|
} else false;
|
||||||
|
|
||||||
|
if (value[i] != ':') {
|
||||||
|
return error.InvalidFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
i += 1;
|
||||||
|
|
||||||
|
const r = r: {
|
||||||
|
const slash_i = std.mem.indexOfScalarPos(u8, value, i, '/') orelse
|
||||||
|
return error.InvalidFormat;
|
||||||
|
|
||||||
|
const r = try parseColor(value[i..slash_i], use_intensity);
|
||||||
|
i = slash_i + 1;
|
||||||
|
break :r r;
|
||||||
|
};
|
||||||
|
|
||||||
|
const g = g: {
|
||||||
|
const slash_i = std.mem.indexOfScalarPos(u8, value, i, '/') orelse
|
||||||
|
return error.InvalidFormat;
|
||||||
|
|
||||||
|
const g = try parseColor(value[i..slash_i], use_intensity);
|
||||||
|
i = slash_i + 1;
|
||||||
|
break :g g;
|
||||||
|
};
|
||||||
|
|
||||||
|
const b = try parseColor(value[i..], use_intensity);
|
||||||
|
|
||||||
|
return terminal.color.RGB{
|
||||||
|
.r = r,
|
||||||
|
.g = g,
|
||||||
|
.b = b,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setDefaultColor(
|
||||||
|
self: *StreamHandler,
|
||||||
|
kind: terminal.osc.Command.DefaultColorKind,
|
||||||
|
value: []const u8,
|
||||||
|
) !void {
|
||||||
|
const color = try parseColorSpec(value);
|
||||||
|
|
||||||
|
switch (kind) {
|
||||||
|
.foreground => {
|
||||||
|
self.default_foreground_color = color;
|
||||||
|
_ = self.ev.renderer_mailbox.push(.{
|
||||||
|
.foreground_color = color,
|
||||||
|
}, .{ .forever = {} });
|
||||||
|
},
|
||||||
|
.background => {
|
||||||
|
self.default_background_color = color;
|
||||||
|
_ = self.ev.renderer_mailbox.push(.{
|
||||||
|
.background_color = color,
|
||||||
|
}, .{ .forever = {} });
|
||||||
|
},
|
||||||
|
.palette => |i| self.terminal.color_palette[i] = color,
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
Reference in New Issue
Block a user