mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-21 03:06:15 +03:00
333 lines
11 KiB
Zig
333 lines
11 KiB
Zig
const std = @import("std");
|
|
const assert = std.debug.assert;
|
|
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 hash_map = @import("hash_map.zig");
|
|
const AutoOffsetHashMap = hash_map.AutoOffsetHashMap;
|
|
|
|
/// 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: packed struct {
|
|
bold: bool = false,
|
|
italic: bool = false,
|
|
faint: bool = false,
|
|
blink: bool = false,
|
|
inverse: bool = false,
|
|
invisible: bool = false,
|
|
strikethrough: bool = false,
|
|
underline: sgr.Attribute.Underline = .none,
|
|
} = .{},
|
|
|
|
/// 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(enum) {
|
|
none: void,
|
|
palette: u8,
|
|
rgb: color.RGB,
|
|
};
|
|
|
|
/// True if the style is the default style.
|
|
pub fn default(self: Style) bool {
|
|
return std.meta.eql(self, .{});
|
|
}
|
|
|
|
/// True if the style is equal to another style.
|
|
pub fn eql(self: Style, other: Style) bool {
|
|
return std.meta.eql(self, 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,
|
|
},
|
|
};
|
|
}
|
|
|
|
/// Returns the fg color for a cell with this style given the palette.
|
|
pub fn fg(
|
|
self: Style,
|
|
palette: *const color.Palette,
|
|
) ?color.RGB {
|
|
return switch (self.fg_color) {
|
|
.none => null,
|
|
.palette => |idx| palette[idx],
|
|
.rgb => |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,
|
|
} },
|
|
},
|
|
};
|
|
}
|
|
|
|
test {
|
|
// The size of the struct so we can be aware of changes.
|
|
const testing = std.testing;
|
|
try testing.expectEqual(@as(usize, 14), @sizeOf(Style));
|
|
}
|
|
};
|
|
|
|
/// A set of styles.
|
|
///
|
|
/// This set is created with some capacity in mind. You can determine
|
|
/// the exact memory requirement for a capacity by calling `layout`
|
|
/// and checking the total size.
|
|
///
|
|
/// When the set exceeds capacity, `error.OutOfMemory` is returned
|
|
/// from memory-using methods. The caller is responsible for determining
|
|
/// a path forward.
|
|
///
|
|
/// The general idea behind this structure is that it is optimized for
|
|
/// the scenario common in terminals where there aren't many unique
|
|
/// styles, and many cells are usually drawn with a single style before
|
|
/// changing styles.
|
|
///
|
|
/// Callers should call `upsert` when a new style is set. This will
|
|
/// return a stable pointer to metadata. You should use this metadata
|
|
/// to keep a ref count of the style usage. When it falls to zero you
|
|
/// can remove it.
|
|
pub const Set = struct {
|
|
pub const base_align = @max(MetadataMap.base_align, IdMap.base_align);
|
|
|
|
/// The mapping of a style to associated metadata. This is
|
|
/// the map that contains the actual style definitions
|
|
/// (in the form of the key).
|
|
styles: MetadataMap,
|
|
|
|
/// The mapping from ID to style.
|
|
id_map: IdMap,
|
|
|
|
/// The next ID to use for a style that isn't in the set.
|
|
/// When this overflows we'll begin returning an IdOverflow
|
|
/// error and the caller must manually compact the style
|
|
/// set.
|
|
///
|
|
/// Id zero is reserved and always is the default style. The
|
|
/// default style isn't present in the map, its dependent on
|
|
/// the terminal configuration.
|
|
next_id: Id = 1,
|
|
|
|
/// Maps a style definition to metadata about that style.
|
|
const MetadataMap = AutoOffsetHashMap(Style, Metadata);
|
|
|
|
/// Maps the unique style ID to the concrete style definition.
|
|
const IdMap = AutoOffsetHashMap(Id, Offset(Style));
|
|
|
|
/// Returns the memory layout for the given base offset and
|
|
/// desired capacity. The layout can be used by the caller to
|
|
/// determine how much memory to allocate, and the layout must
|
|
/// be used to initialize the set so that the set knows all
|
|
/// the offsets for the various buffers.
|
|
pub fn layout(cap: usize) Layout {
|
|
const md_layout = MetadataMap.layout(@intCast(cap));
|
|
const md_start = 0;
|
|
const md_end = md_start + md_layout.total_size;
|
|
|
|
const id_layout = IdMap.layout(@intCast(cap));
|
|
const id_start = std.mem.alignForward(usize, md_end, IdMap.base_align);
|
|
const id_end = id_start + id_layout.total_size;
|
|
|
|
const total_size = id_end;
|
|
|
|
return .{
|
|
.md_start = md_start,
|
|
.md_layout = md_layout,
|
|
.id_start = id_start,
|
|
.id_layout = id_layout,
|
|
.total_size = total_size,
|
|
};
|
|
}
|
|
|
|
pub const Layout = struct {
|
|
md_start: usize,
|
|
md_layout: MetadataMap.Layout,
|
|
id_start: usize,
|
|
id_layout: IdMap.Layout,
|
|
total_size: usize,
|
|
};
|
|
|
|
pub fn init(base: OffsetBuf, l: Layout) Set {
|
|
const styles_buf = base.add(l.md_start);
|
|
const id_buf = base.add(l.id_start);
|
|
return .{
|
|
.styles = MetadataMap.init(styles_buf, l.md_layout),
|
|
.id_map = IdMap.init(id_buf, l.id_layout),
|
|
};
|
|
}
|
|
|
|
/// Possible errors for upsert.
|
|
pub const UpsertError = error{
|
|
/// No more space in the backing buffer. Remove styles or
|
|
/// grow and reinitialize.
|
|
OutOfMemory,
|
|
|
|
/// No more available IDs. Perform a garbage collection
|
|
/// operation to compact ID space.
|
|
Overflow,
|
|
};
|
|
|
|
/// Upsert a style into the set and return a pointer to the metadata
|
|
/// for that style. The pointer is valid for the lifetime of the set
|
|
/// so long as the style is not removed.
|
|
///
|
|
/// The ref count for new styles is initialized to zero and
|
|
/// for existing styles remains unmodified.
|
|
pub fn upsert(self: *Set, base: anytype, style: Style) UpsertError!*Metadata {
|
|
// If we already have the style in the map, this is fast.
|
|
var map = self.styles.map(base);
|
|
const gop = try map.getOrPut(style);
|
|
if (gop.found_existing) return gop.value_ptr;
|
|
|
|
// New style, we need to setup all the metadata. First thing,
|
|
// let's get the ID we'll assign, because if we're out of space
|
|
// we need to fail early.
|
|
errdefer map.removeByPtr(gop.key_ptr);
|
|
const id = self.next_id;
|
|
self.next_id = try std.math.add(Id, self.next_id, 1);
|
|
errdefer self.next_id -= 1;
|
|
gop.value_ptr.* = .{ .id = id };
|
|
|
|
// Setup our ID mapping
|
|
var id_map = self.id_map.map(base);
|
|
const id_gop = try id_map.getOrPut(id);
|
|
errdefer id_map.removeByPtr(id_gop.key_ptr);
|
|
assert(!id_gop.found_existing);
|
|
id_gop.value_ptr.* = size.getOffset(Style, base, gop.key_ptr);
|
|
return gop.value_ptr;
|
|
}
|
|
|
|
/// Lookup a style by its unique identifier.
|
|
pub fn lookupId(self: *const Set, base: anytype, id: Id) ?*Style {
|
|
const id_map = self.id_map.map(base);
|
|
const offset = id_map.get(id) orelse return null;
|
|
return @ptrCast(offset.ptr(base));
|
|
}
|
|
|
|
/// Remove a style by its id.
|
|
pub fn remove(self: *Set, base: anytype, id: Id) void {
|
|
// Lookup by ID, if it doesn't exist then we return. We use
|
|
// getEntry so that we can make removal faster later by using
|
|
// the entry's key pointer.
|
|
var id_map = self.id_map.map(base);
|
|
const id_entry = id_map.getEntry(id) orelse return;
|
|
|
|
var style_map = self.styles.map(base);
|
|
const style_ptr: *Style = @ptrCast(id_entry.value_ptr.ptr(base));
|
|
|
|
id_map.removeByPtr(id_entry.key_ptr);
|
|
style_map.removeByPtr(style_ptr);
|
|
}
|
|
|
|
/// Return the number of styles currently in the set.
|
|
pub fn count(self: *const Set, base: anytype) usize {
|
|
return self.id_map.map(base).count();
|
|
}
|
|
};
|
|
|
|
/// Metadata about a style. This is used to track the reference count
|
|
/// and the unique identifier for a style. The unique identifier is used
|
|
/// to track the style in the full style map.
|
|
pub const Metadata = struct {
|
|
ref: size.CellCountInt = 0,
|
|
id: Id = 0,
|
|
};
|
|
|
|
test "Set basic usage" {
|
|
const testing = std.testing;
|
|
const alloc = testing.allocator;
|
|
const layout = Set.layout(16);
|
|
const buf = try alloc.alignedAlloc(u8, Set.base_align, layout.total_size);
|
|
defer alloc.free(buf);
|
|
|
|
const style: Style = .{ .flags = .{ .bold = true } };
|
|
|
|
var set = Set.init(OffsetBuf.init(buf), layout);
|
|
|
|
// Upsert
|
|
const meta = try set.upsert(buf, style);
|
|
try testing.expect(meta.id > 0);
|
|
|
|
// Second upsert should return the same metadata.
|
|
{
|
|
const meta2 = try set.upsert(buf, style);
|
|
try testing.expectEqual(meta.id, meta2.id);
|
|
}
|
|
|
|
// Look it up
|
|
{
|
|
const v = set.lookupId(buf, meta.id).?;
|
|
try testing.expect(v.flags.bold);
|
|
|
|
const v2 = set.lookupId(buf, meta.id).?;
|
|
try testing.expectEqual(v, v2);
|
|
}
|
|
|
|
// Removal
|
|
set.remove(buf, meta.id);
|
|
try testing.expect(set.lookupId(buf, meta.id) == null);
|
|
}
|