mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-22 11:46:11 +03:00
Merge pull request #150 from mitchellh/underline-color
Colored Underlines
This commit is contained in:
1
TODO.md
1
TODO.md
@ -30,4 +30,3 @@ Major Features:
|
|||||||
* Sixels: https://saitoha.github.io/libsixel/
|
* Sixels: https://saitoha.github.io/libsixel/
|
||||||
* Kitty keyboard protocol: https://sw.kovidgoyal.net/kitty/keyboard-protocol/
|
* Kitty keyboard protocol: https://sw.kovidgoyal.net/kitty/keyboard-protocol/
|
||||||
* Kitty graphics protocol: https://sw.kovidgoyal.net/kitty/graphics-protocol/
|
* Kitty graphics protocol: https://sw.kovidgoyal.net/kitty/graphics-protocol/
|
||||||
* Colored underlines: https://sw.kovidgoyal.net/kitty/underlines/
|
|
||||||
|
@ -1023,11 +1023,13 @@ pub fn updateCell(
|
|||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const color = if (cell.attrs.underline_color) cell.underline_fg else colors.fg;
|
||||||
|
|
||||||
self.cells.appendAssumeCapacity(.{
|
self.cells.appendAssumeCapacity(.{
|
||||||
.mode = .fg,
|
.mode = .fg,
|
||||||
.grid_pos = .{ @intToFloat(f32, x), @intToFloat(f32, y) },
|
.grid_pos = .{ @intToFloat(f32, x), @intToFloat(f32, y) },
|
||||||
.cell_width = cell.widthLegacy(),
|
.cell_width = cell.widthLegacy(),
|
||||||
.color = .{ colors.fg.r, colors.fg.g, colors.fg.b, alpha },
|
.color = .{ color.r, color.g, color.b, alpha },
|
||||||
.glyph_pos = .{ glyph.atlas_x, glyph.atlas_y },
|
.glyph_pos = .{ glyph.atlas_x, glyph.atlas_y },
|
||||||
.glyph_size = .{ glyph.width, glyph.height },
|
.glyph_size = .{ glyph.width, glyph.height },
|
||||||
.glyph_offset = .{ glyph.offset_x, glyph.offset_y },
|
.glyph_offset = .{ glyph.offset_x, glyph.offset_y },
|
||||||
|
@ -1181,6 +1181,9 @@ pub fn updateCell(
|
|||||||
@enumToInt(sprite),
|
@enumToInt(sprite),
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const color = if (cell.attrs.underline_color) cell.underline_fg else colors.fg;
|
||||||
|
|
||||||
self.cells.appendAssumeCapacity(.{
|
self.cells.appendAssumeCapacity(.{
|
||||||
.mode = .fg,
|
.mode = .fg,
|
||||||
.grid_col = @intCast(u16, x),
|
.grid_col = @intCast(u16, x),
|
||||||
@ -1192,9 +1195,9 @@ pub fn updateCell(
|
|||||||
.glyph_height = underline_glyph.height,
|
.glyph_height = underline_glyph.height,
|
||||||
.glyph_offset_x = underline_glyph.offset_x,
|
.glyph_offset_x = underline_glyph.offset_x,
|
||||||
.glyph_offset_y = underline_glyph.offset_y,
|
.glyph_offset_y = underline_glyph.offset_y,
|
||||||
.fg_r = colors.fg.r,
|
.fg_r = color.r,
|
||||||
.fg_g = colors.fg.g,
|
.fg_g = color.g,
|
||||||
.fg_b = colors.fg.b,
|
.fg_b = color.b,
|
||||||
.fg_a = alpha,
|
.fg_a = alpha,
|
||||||
.bg_r = 0,
|
.bg_r = 0,
|
||||||
.bg_g = 0,
|
.bg_g = 0,
|
||||||
|
@ -524,12 +524,67 @@ test "csi: SGR ESC [ 38 : 2 m" {
|
|||||||
|
|
||||||
const d = a[1].?.csi_dispatch;
|
const d = a[1].?.csi_dispatch;
|
||||||
try testing.expect(d.final == 'm');
|
try testing.expect(d.final == 'm');
|
||||||
|
try testing.expect(d.sep == .colon);
|
||||||
try testing.expect(d.params.len == 2);
|
try testing.expect(d.params.len == 2);
|
||||||
try testing.expectEqual(@as(u16, 38), d.params[0]);
|
try testing.expectEqual(@as(u16, 38), d.params[0]);
|
||||||
try testing.expectEqual(@as(u16, 2), d.params[1]);
|
try testing.expectEqual(@as(u16, 2), d.params[1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "csi: SGR ESC [4:3m colon" {
|
||||||
|
var p = init();
|
||||||
|
_ = p.next(0x1B);
|
||||||
|
_ = p.next('[');
|
||||||
|
_ = p.next('4');
|
||||||
|
_ = p.next(':');
|
||||||
|
_ = p.next('3');
|
||||||
|
|
||||||
|
{
|
||||||
|
const a = p.next('m');
|
||||||
|
try testing.expect(p.state == .ground);
|
||||||
|
try testing.expect(a[0] == null);
|
||||||
|
try testing.expect(a[1].? == .csi_dispatch);
|
||||||
|
try testing.expect(a[2] == null);
|
||||||
|
|
||||||
|
const d = a[1].?.csi_dispatch;
|
||||||
|
try testing.expect(d.final == 'm');
|
||||||
|
try testing.expect(d.sep == .colon);
|
||||||
|
try testing.expect(d.params.len == 2);
|
||||||
|
try testing.expectEqual(@as(u16, 4), d.params[0]);
|
||||||
|
try testing.expectEqual(@as(u16, 3), d.params[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "csi: SGR with many blank and colon" {
|
||||||
|
var p = init();
|
||||||
|
_ = p.next(0x1B);
|
||||||
|
for ("[58:2::240:143:104") |c| {
|
||||||
|
const a = p.next(c);
|
||||||
|
try testing.expect(a[0] == null);
|
||||||
|
try testing.expect(a[1] == null);
|
||||||
|
try testing.expect(a[2] == null);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const a = p.next('m');
|
||||||
|
try testing.expect(p.state == .ground);
|
||||||
|
try testing.expect(a[0] == null);
|
||||||
|
try testing.expect(a[1].? == .csi_dispatch);
|
||||||
|
try testing.expect(a[2] == null);
|
||||||
|
|
||||||
|
const d = a[1].?.csi_dispatch;
|
||||||
|
try testing.expect(d.final == 'm');
|
||||||
|
try testing.expect(d.sep == .colon);
|
||||||
|
try testing.expect(d.params.len == 6);
|
||||||
|
try testing.expectEqual(@as(u16, 58), d.params[0]);
|
||||||
|
try testing.expectEqual(@as(u16, 2), d.params[1]);
|
||||||
|
try testing.expectEqual(@as(u16, 0), d.params[2]);
|
||||||
|
try testing.expectEqual(@as(u16, 240), d.params[3]);
|
||||||
|
try testing.expectEqual(@as(u16, 143), d.params[4]);
|
||||||
|
try testing.expectEqual(@as(u16, 104), d.params[5]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
test "csi: mixing semicolon/colon" {
|
test "csi: mixing semicolon/colon" {
|
||||||
var p = init();
|
var p = init();
|
||||||
_ = p.next(0x1B);
|
_ = p.next(0x1B);
|
||||||
|
@ -182,6 +182,12 @@ pub const Cell = struct {
|
|||||||
fg: color.RGB = .{},
|
fg: color.RGB = .{},
|
||||||
bg: color.RGB = .{},
|
bg: color.RGB = .{},
|
||||||
|
|
||||||
|
/// Underline color.
|
||||||
|
/// NOTE(mitchellh): This is very rarely set so ideally we wouldn't waste
|
||||||
|
/// cell space for this. For now its on this struct because it is convenient
|
||||||
|
/// but we should consider a lookaside table for this.
|
||||||
|
underline_fg: color.RGB = .{},
|
||||||
|
|
||||||
/// On/off attributes that can be set
|
/// On/off attributes that can be set
|
||||||
attrs: packed struct {
|
attrs: packed struct {
|
||||||
has_bg: bool = false,
|
has_bg: bool = false,
|
||||||
@ -194,6 +200,7 @@ pub const Cell = struct {
|
|||||||
inverse: bool = false,
|
inverse: bool = false,
|
||||||
strikethrough: bool = false,
|
strikethrough: bool = false,
|
||||||
underline: sgr.Attribute.Underline = .none,
|
underline: sgr.Attribute.Underline = .none,
|
||||||
|
underline_color: bool = false,
|
||||||
|
|
||||||
/// True if this is a wide character. This char takes up
|
/// True if this is a wide character. This char takes up
|
||||||
/// two cells. The following cell ALWAYS is a space.
|
/// two cells. The following cell ALWAYS is a space.
|
||||||
@ -265,7 +272,7 @@ pub const Cell = struct {
|
|||||||
|
|
||||||
test {
|
test {
|
||||||
//log.warn("CELL={} bits={} {}", .{ @sizeOf(Cell), @bitSizeOf(Cell), @alignOf(Cell) });
|
//log.warn("CELL={} bits={} {}", .{ @sizeOf(Cell), @bitSizeOf(Cell), @alignOf(Cell) });
|
||||||
try std.testing.expectEqual(12, @sizeOf(Cell));
|
try std.testing.expectEqual(16, @sizeOf(Cell));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -461,6 +461,19 @@ pub fn setAttribute(self: *Terminal, attr: sgr.Attribute) !void {
|
|||||||
self.screen.cursor.pen.attrs.underline = .none;
|
self.screen.cursor.pen.attrs.underline = .none;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
.underline_color => |rgb| {
|
||||||
|
self.screen.cursor.pen.attrs.underline_color = true;
|
||||||
|
self.screen.cursor.pen.underline_fg = .{
|
||||||
|
.r = rgb.r,
|
||||||
|
.g = rgb.g,
|
||||||
|
.b = rgb.b,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
.reset_underline_color => {
|
||||||
|
self.screen.cursor.pen.attrs.underline_color = false;
|
||||||
|
},
|
||||||
|
|
||||||
.blink => {
|
.blink => {
|
||||||
log.warn("blink requested, but not implemented", .{});
|
log.warn("blink requested, but not implemented", .{});
|
||||||
self.screen.cursor.pen.attrs.blink = true;
|
self.screen.cursor.pen.attrs.blink = true;
|
||||||
|
@ -33,6 +33,8 @@ pub const Attribute = union(enum) {
|
|||||||
/// Underline the text
|
/// Underline the text
|
||||||
underline: Underline,
|
underline: Underline,
|
||||||
reset_underline: void,
|
reset_underline: void,
|
||||||
|
underline_color: RGB,
|
||||||
|
reset_underline_color: void,
|
||||||
|
|
||||||
/// Blink the text
|
/// Blink the text
|
||||||
blink: void,
|
blink: void,
|
||||||
@ -210,8 +212,12 @@ pub const Parser = struct {
|
|||||||
48 => if (slice.len >= 5 and slice[1] == 2) {
|
48 => if (slice.len >= 5 and slice[1] == 2) {
|
||||||
self.idx += 4;
|
self.idx += 4;
|
||||||
|
|
||||||
// In the 6-len form, ignore the 3rd param.
|
// In the 6-len form, ignore the 3rd param. Otherwise, use it.
|
||||||
const rgb = slice[2..5];
|
const rgb = if (slice.len == 5) slice[2..5] else rgb: {
|
||||||
|
// Consume one more element
|
||||||
|
self.idx += 1;
|
||||||
|
break :rgb slice[3..6];
|
||||||
|
};
|
||||||
|
|
||||||
// We use @truncate because the value should be 0 to 255. If
|
// We use @truncate because the value should be 0 to 255. If
|
||||||
// it isn't, the behavior is undefined so we just... truncate it.
|
// it isn't, the behavior is undefined so we just... truncate it.
|
||||||
@ -231,6 +237,29 @@ pub const Parser = struct {
|
|||||||
|
|
||||||
49 => return Attribute{ .reset_bg = {} },
|
49 => return Attribute{ .reset_bg = {} },
|
||||||
|
|
||||||
|
58 => if (slice.len >= 5 and slice[1] == 2) {
|
||||||
|
self.idx += 4;
|
||||||
|
|
||||||
|
// In the 6-len form, ignore the 3rd param. Otherwise, use it.
|
||||||
|
const rgb = if (slice.len == 5) slice[2..5] else rgb: {
|
||||||
|
// Consume one more element
|
||||||
|
self.idx += 1;
|
||||||
|
break :rgb slice[3..6];
|
||||||
|
};
|
||||||
|
|
||||||
|
// We use @truncate because the value should be 0 to 255. If
|
||||||
|
// it isn't, the behavior is undefined so we just... truncate it.
|
||||||
|
return Attribute{
|
||||||
|
.underline_color = .{
|
||||||
|
.r = @truncate(u8, rgb[0]),
|
||||||
|
.g = @truncate(u8, rgb[1]),
|
||||||
|
.b = @truncate(u8, rgb[2]),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
59 => return Attribute{ .reset_underline_color = {} },
|
||||||
|
|
||||||
90...97 => return Attribute{
|
90...97 => return Attribute{
|
||||||
// 82 instead of 90 to offset to "bright" colors
|
// 82 instead of 90 to offset to "bright" colors
|
||||||
.@"8_bright_fg" = @intToEnum(color.Name, slice[0] - 82),
|
.@"8_bright_fg" = @intToEnum(color.Name, slice[0] - 82),
|
||||||
@ -344,6 +373,12 @@ test "sgr: underline styles" {
|
|||||||
try testing.expect(v.underline == .single);
|
try testing.expect(v.underline == .single);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const v = testParseColon(&[_]u16{ 4, 3 });
|
||||||
|
try testing.expect(v == .underline);
|
||||||
|
try testing.expect(v.underline == .curly);
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
const v = testParseColon(&[_]u16{ 4, 4 });
|
const v = testParseColon(&[_]u16{ 4, 4 });
|
||||||
try testing.expect(v == .underline);
|
try testing.expect(v == .underline);
|
||||||
@ -431,3 +466,26 @@ test "sgr: 256 color" {
|
|||||||
try testing.expect(p.next().? == .@"256_fg");
|
try testing.expect(p.next().? == .@"256_fg");
|
||||||
try testing.expect(p.next().? == .@"256_bg");
|
try testing.expect(p.next().? == .@"256_bg");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "sgr: underline color" {
|
||||||
|
{
|
||||||
|
const v = testParseColon(&[_]u16{ 58, 2, 1, 2, 3 });
|
||||||
|
try testing.expect(v == .underline_color);
|
||||||
|
try testing.expectEqual(@as(u8, 1), v.underline_color.r);
|
||||||
|
try testing.expectEqual(@as(u8, 2), v.underline_color.g);
|
||||||
|
try testing.expectEqual(@as(u8, 3), v.underline_color.b);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const v = testParseColon(&[_]u16{ 58, 2, 0, 1, 2, 3 });
|
||||||
|
try testing.expect(v == .underline_color);
|
||||||
|
try testing.expectEqual(@as(u8, 1), v.underline_color.r);
|
||||||
|
try testing.expectEqual(@as(u8, 2), v.underline_color.g);
|
||||||
|
try testing.expectEqual(@as(u8, 3), v.underline_color.b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "sgr: reset underline color" {
|
||||||
|
var p: Parser = .{ .params = &[_]u16{59} };
|
||||||
|
try testing.expect(p.next().? == .reset_underline_color);
|
||||||
|
}
|
||||||
|
@ -385,7 +385,10 @@ pub fn Stream(comptime Handler: type) type {
|
|||||||
// SGR - Select Graphic Rendition
|
// SGR - Select Graphic Rendition
|
||||||
'm' => if (@hasDecl(T, "setAttribute")) {
|
'm' => if (@hasDecl(T, "setAttribute")) {
|
||||||
var p: sgr.Parser = .{ .params = action.params, .colon = action.sep == .colon };
|
var p: sgr.Parser = .{ .params = action.params, .colon = action.sep == .colon };
|
||||||
while (p.next()) |attr| try self.handler.setAttribute(attr);
|
while (p.next()) |attr| {
|
||||||
|
// log.info("SGR attribute: {}", .{attr});
|
||||||
|
try self.handler.setAttribute(attr);
|
||||||
|
}
|
||||||
} else log.warn("unimplemented CSI callback: {}", .{action}),
|
} else log.warn("unimplemented CSI callback: {}", .{action}),
|
||||||
|
|
||||||
// CPR - Request Cursor Postion Report
|
// CPR - Request Cursor Postion Report
|
||||||
|
@ -1081,7 +1081,7 @@ const StreamHandler = struct {
|
|||||||
|
|
||||||
pub fn setAttribute(self: *StreamHandler, attr: terminal.Attribute) !void {
|
pub fn setAttribute(self: *StreamHandler, attr: terminal.Attribute) !void {
|
||||||
switch (attr) {
|
switch (attr) {
|
||||||
.unknown => |unk| log.warn("unimplemented or unknown attribute: {any}", .{unk}),
|
.unknown => |unk| log.warn("unimplemented or unknown SGR attribute: {any}", .{unk}),
|
||||||
|
|
||||||
else => self.terminal.setAttribute(attr) catch |err|
|
else => self.terminal.setAttribute(attr) catch |err|
|
||||||
log.warn("error setting attribute {}: {}", .{ attr, err }),
|
log.warn("error setting attribute {}: {}", .{ attr, err }),
|
||||||
|
Reference in New Issue
Block a user