mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
446 lines
14 KiB
Zig
446 lines
14 KiB
Zig
const std = @import("std");
|
|
const assert = std.debug.assert;
|
|
const configpkg = @import("../config.zig");
|
|
const color = @import("color.zig");
|
|
const sgr = @import("sgr.zig");
|
|
const page = @import("page.zig");
|
|
const size = @import("size.zig");
|
|
const Offset = size.Offset;
|
|
const OffsetBuf = size.OffsetBuf;
|
|
const RefCountedSet = @import("ref_counted_set.zig").RefCountedSet;
|
|
|
|
/// The unique identifier for a style. This is at most the number of cells
|
|
/// that can fit into a terminal page.
|
|
pub const Id = size.CellCountInt;
|
|
|
|
/// The Id to use for default styling.
|
|
pub const default_id: Id = 0;
|
|
|
|
/// The style attributes for a cell.
|
|
pub const Style = struct {
|
|
/// Various colors, all self-explanatory.
|
|
fg_color: Color = .none,
|
|
bg_color: Color = .none,
|
|
underline_color: Color = .none,
|
|
|
|
/// On/off attributes that don't require much bit width so we use
|
|
/// a packed struct to make this take up significantly less space.
|
|
flags: Flags = .{},
|
|
|
|
const Flags = packed struct(u16) {
|
|
bold: bool = false,
|
|
italic: bool = false,
|
|
faint: bool = false,
|
|
blink: bool = false,
|
|
inverse: bool = false,
|
|
invisible: bool = false,
|
|
strikethrough: bool = false,
|
|
overline: bool = false,
|
|
underline: sgr.Attribute.Underline = .none,
|
|
_padding: u5 = 0,
|
|
};
|
|
|
|
/// The color for an SGR attribute. A color can come from multiple
|
|
/// sources so we use this to track the source plus color value so that
|
|
/// we can properly react to things like palette changes.
|
|
pub const Color = union(Tag) {
|
|
none: void,
|
|
palette: u8,
|
|
rgb: color.RGB,
|
|
|
|
const Tag = enum(u8) {
|
|
none,
|
|
palette,
|
|
rgb,
|
|
};
|
|
|
|
/// Formatting to make debug logs easier to read
|
|
/// by only including non-default attributes.
|
|
pub fn format(
|
|
self: Color,
|
|
comptime fmt: []const u8,
|
|
options: std.fmt.FormatOptions,
|
|
writer: anytype,
|
|
) !void {
|
|
_ = fmt;
|
|
_ = options;
|
|
switch (self) {
|
|
.none => {
|
|
_ = try writer.write("Color.none");
|
|
},
|
|
.palette => |p| {
|
|
_ = try writer.print("Color.palette{{ {} }}", .{p});
|
|
},
|
|
.rgb => |rgb| {
|
|
_ = try writer.print("Color.rgb{{ {}, {}, {} }}", .{ rgb.r, rgb.g, rgb.b });
|
|
},
|
|
}
|
|
}
|
|
};
|
|
|
|
/// True if the style is the default style.
|
|
pub fn default(self: Style) bool {
|
|
return self.eql(.{});
|
|
}
|
|
|
|
/// True if the style is equal to another style.
|
|
pub fn eql(self: Style, other: Style) bool {
|
|
// We convert the styles to packed structs and compare as integers
|
|
// because this is much faster than comparing each field separately.
|
|
return PackedStyle.fromStyle(self) == PackedStyle.fromStyle(other);
|
|
}
|
|
|
|
/// Returns the bg color for a cell with this style given the cell
|
|
/// that has this style and the palette to use.
|
|
///
|
|
/// Note that generally if a cell is a color-only cell, it SHOULD
|
|
/// only have the default style, but this is meant to work with the
|
|
/// default style as well.
|
|
pub fn bg(
|
|
self: Style,
|
|
cell: *const page.Cell,
|
|
palette: *const color.Palette,
|
|
) ?color.RGB {
|
|
return switch (cell.content_tag) {
|
|
.bg_color_palette => palette[cell.content.color_palette],
|
|
.bg_color_rgb => rgb: {
|
|
const rgb = cell.content.color_rgb;
|
|
break :rgb .{ .r = rgb.r, .g = rgb.g, .b = rgb.b };
|
|
},
|
|
|
|
else => switch (self.bg_color) {
|
|
.none => null,
|
|
.palette => |idx| palette[idx],
|
|
.rgb => |rgb| rgb,
|
|
},
|
|
};
|
|
}
|
|
|
|
pub const Fg = struct {
|
|
/// The default color to use if the style doesn't specify a
|
|
/// foreground color and no configuration options override
|
|
/// it.
|
|
default: color.RGB,
|
|
|
|
/// The current color palette. Required to map palette indices to
|
|
/// real color values.
|
|
palette: *const color.Palette,
|
|
|
|
/// If specified, the color to use for bold text.
|
|
bold: ?configpkg.BoldColor = null,
|
|
};
|
|
|
|
/// Returns the fg color for a cell with this style given the palette
|
|
/// and various configuration options.
|
|
pub fn fg(
|
|
self: Style,
|
|
opts: Fg,
|
|
) color.RGB {
|
|
// Note we don't pull the bold check to the top-level here because
|
|
// we don't want to duplicate the conditional multiple times since
|
|
// certain colors require more checks (e.g. `bold_is_bright`).
|
|
|
|
return switch (self.fg_color) {
|
|
.none => default: {
|
|
if (self.flags.bold) {
|
|
if (opts.bold) |bold| switch (bold) {
|
|
.bright => {},
|
|
.color => |v| break :default v.toTerminalRGB(),
|
|
};
|
|
}
|
|
|
|
break :default opts.default;
|
|
},
|
|
|
|
.palette => |idx| palette: {
|
|
if (self.flags.bold) {
|
|
if (opts.bold) |bold| switch (bold) {
|
|
.color => |v| break :palette v.toTerminalRGB(),
|
|
.bright => {
|
|
const bright_offset = @intFromEnum(color.Name.bright_black);
|
|
if (idx < bright_offset) {
|
|
break :palette opts.palette[idx + bright_offset];
|
|
}
|
|
},
|
|
};
|
|
}
|
|
|
|
break :palette opts.palette[idx];
|
|
},
|
|
|
|
.rgb => |rgb| rgb: {
|
|
if (self.flags.bold and rgb.eql(opts.default)) {
|
|
if (opts.bold) |bold| switch (bold) {
|
|
.color => |v| break :rgb v.toTerminalRGB(),
|
|
.bright => {},
|
|
};
|
|
}
|
|
|
|
break :rgb rgb;
|
|
},
|
|
};
|
|
}
|
|
|
|
/// Returns the underline color for this style.
|
|
pub fn underlineColor(
|
|
self: Style,
|
|
palette: *const color.Palette,
|
|
) ?color.RGB {
|
|
return switch (self.underline_color) {
|
|
.none => null,
|
|
.palette => |idx| palette[idx],
|
|
.rgb => |rgb| rgb,
|
|
};
|
|
}
|
|
|
|
/// Returns a bg-color only cell from this style, if it exists.
|
|
pub fn bgCell(self: Style) ?page.Cell {
|
|
return switch (self.bg_color) {
|
|
.none => null,
|
|
.palette => |idx| .{
|
|
.content_tag = .bg_color_palette,
|
|
.content = .{ .color_palette = idx },
|
|
},
|
|
.rgb => |rgb| .{
|
|
.content_tag = .bg_color_rgb,
|
|
.content = .{ .color_rgb = .{
|
|
.r = rgb.r,
|
|
.g = rgb.g,
|
|
.b = rgb.b,
|
|
} },
|
|
},
|
|
};
|
|
}
|
|
|
|
/// Formatting to make debug logs easier to read
|
|
/// by only including non-default attributes.
|
|
pub fn format(
|
|
self: Style,
|
|
comptime fmt: []const u8,
|
|
options: std.fmt.FormatOptions,
|
|
writer: anytype,
|
|
) !void {
|
|
_ = fmt;
|
|
_ = options;
|
|
|
|
const dflt: Style = .{};
|
|
|
|
_ = try writer.write("Style{ ");
|
|
|
|
var started = false;
|
|
|
|
inline for (std.meta.fields(Style)) |f| {
|
|
if (std.mem.eql(u8, f.name, "flags")) {
|
|
if (started) {
|
|
_ = try writer.write(", ");
|
|
}
|
|
|
|
_ = try writer.write("flags={ ");
|
|
|
|
started = false;
|
|
|
|
inline for (std.meta.fields(@TypeOf(self.flags))) |ff| {
|
|
const v = @as(ff.type, @field(self.flags, ff.name));
|
|
const d = @as(ff.type, @field(dflt.flags, ff.name));
|
|
if (ff.type == bool) {
|
|
if (v) {
|
|
if (started) {
|
|
_ = try writer.write(", ");
|
|
}
|
|
_ = try writer.print("{s}", .{ff.name});
|
|
started = true;
|
|
}
|
|
} else if (!std.meta.eql(v, d)) {
|
|
if (started) {
|
|
_ = try writer.write(", ");
|
|
}
|
|
_ = try writer.print(
|
|
"{s}={any}",
|
|
.{ ff.name, v },
|
|
);
|
|
started = true;
|
|
}
|
|
}
|
|
_ = try writer.write(" }");
|
|
|
|
started = true;
|
|
comptime continue;
|
|
}
|
|
const value = @as(f.type, @field(self, f.name));
|
|
const d_val = @as(f.type, @field(dflt, f.name));
|
|
if (!std.meta.eql(value, d_val)) {
|
|
if (started) {
|
|
_ = try writer.write(", ");
|
|
}
|
|
_ = try writer.print(
|
|
"{s}={any}",
|
|
.{ f.name, value },
|
|
);
|
|
started = true;
|
|
}
|
|
}
|
|
|
|
_ = try writer.write(" }");
|
|
}
|
|
|
|
/// `PackedStyle` represents the same data as `Style` but without padding,
|
|
/// which is necessary for hashing via re-interpretation of the underlying
|
|
/// bytes.
|
|
///
|
|
/// `Style` is still preferred for everything else as it has type-safety
|
|
/// when using the `Color` tagged union.
|
|
///
|
|
/// Empirical testing shows that storing all of the tags first and then the
|
|
/// data provides a better layout for serializing into and is faster on
|
|
/// benchmarks.
|
|
const PackedStyle = packed struct(u128) {
|
|
tags: packed struct {
|
|
fg: Color.Tag,
|
|
bg: Color.Tag,
|
|
underline: Color.Tag,
|
|
},
|
|
data: packed struct {
|
|
fg: Data,
|
|
bg: Data,
|
|
underline: Data,
|
|
},
|
|
flags: Flags,
|
|
_padding: u16 = 0,
|
|
|
|
/// After https://github.com/ziglang/zig/issues/19754 is implemented,
|
|
/// it will be an compiler-error to have packed union fields of
|
|
/// differing size.
|
|
///
|
|
/// For now we just need to be careful not to accidentally introduce
|
|
/// padding.
|
|
const Data = packed union {
|
|
none: u24,
|
|
palette: packed struct(u24) {
|
|
idx: u8,
|
|
_padding: u16 = 0,
|
|
},
|
|
rgb: color.RGB,
|
|
|
|
fn fromColor(c: Color) Data {
|
|
return switch (c) {
|
|
inline else => |v, t| @unionInit(
|
|
Data,
|
|
@tagName(t),
|
|
switch (t) {
|
|
.none => 0,
|
|
.palette => .{ .idx = v },
|
|
.rgb => v,
|
|
},
|
|
),
|
|
};
|
|
}
|
|
};
|
|
|
|
fn fromStyle(style: Style) PackedStyle {
|
|
return .{
|
|
.tags = .{
|
|
.fg = std.meta.activeTag(style.fg_color),
|
|
.bg = std.meta.activeTag(style.bg_color),
|
|
.underline = std.meta.activeTag(style.underline_color),
|
|
},
|
|
.data = .{
|
|
.fg = .fromColor(style.fg_color),
|
|
.bg = .fromColor(style.bg_color),
|
|
.underline = .fromColor(style.underline_color),
|
|
},
|
|
.flags = style.flags,
|
|
};
|
|
}
|
|
};
|
|
|
|
pub fn hash(self: *const Style) u64 {
|
|
const packed_style = PackedStyle.fromStyle(self.*);
|
|
return std.hash.XxHash3.hash(0, std.mem.asBytes(&packed_style));
|
|
}
|
|
|
|
comptime {
|
|
assert(@sizeOf(PackedStyle) == 16);
|
|
assert(std.meta.hasUniqueRepresentation(PackedStyle));
|
|
for (@typeInfo(PackedStyle.Data).@"union".fields) |field| {
|
|
assert(@bitSizeOf(field.type) == @bitSizeOf(PackedStyle.Data));
|
|
}
|
|
}
|
|
};
|
|
|
|
pub const Set = RefCountedSet(
|
|
Style,
|
|
Id,
|
|
size.CellCountInt,
|
|
struct {
|
|
pub fn hash(self: *const @This(), style: Style) u64 {
|
|
_ = self;
|
|
return style.hash();
|
|
}
|
|
|
|
pub fn eql(self: *const @This(), a: Style, b: Style) bool {
|
|
_ = self;
|
|
return a.eql(b);
|
|
}
|
|
},
|
|
);
|
|
|
|
test "Set basic usage" {
|
|
const testing = std.testing;
|
|
const alloc = testing.allocator;
|
|
const layout: Set.Layout = .init(16);
|
|
const buf = try alloc.alignedAlloc(u8, Set.base_align, layout.total_size);
|
|
defer alloc.free(buf);
|
|
|
|
const style: Style = .{ .flags = .{ .bold = true } };
|
|
const style2: Style = .{ .flags = .{ .italic = true } };
|
|
|
|
var set = Set.init(.init(buf), layout, .{});
|
|
|
|
// Add style
|
|
const id = try set.add(buf, style);
|
|
try testing.expect(id > 0);
|
|
|
|
// Second add should return the same metadata.
|
|
{
|
|
const id2 = try set.add(buf, style);
|
|
try testing.expectEqual(id, id2);
|
|
}
|
|
|
|
// Look it up
|
|
{
|
|
const v = set.get(buf, id);
|
|
try testing.expect(v.flags.bold);
|
|
|
|
const v2 = set.get(buf, id);
|
|
try testing.expectEqual(v, v2);
|
|
}
|
|
|
|
// Add a second style
|
|
const id2 = try set.add(buf, style2);
|
|
|
|
// Look it up
|
|
{
|
|
const v = set.get(buf, id2);
|
|
try testing.expect(v.flags.italic);
|
|
}
|
|
|
|
// Ref count
|
|
try testing.expect(set.refCount(buf, id) == 2);
|
|
try testing.expect(set.refCount(buf, id2) == 1);
|
|
|
|
// Release
|
|
set.release(buf, id);
|
|
try testing.expect(set.refCount(buf, id) == 1);
|
|
set.release(buf, id2);
|
|
try testing.expect(set.refCount(buf, id2) == 0);
|
|
|
|
// We added the first one twice, so
|
|
set.release(buf, id);
|
|
try testing.expect(set.refCount(buf, id) == 0);
|
|
}
|
|
|
|
test "Set capacities" {
|
|
// We want to support at least this many styles without overflowing.
|
|
_ = Set.Layout.init(16384);
|
|
}
|