This commit is contained in:
Jacob Sandlund
2025-07-04 00:50:38 -04:00
parent f1f9d5eb4b
commit 60812d418f
22 changed files with 427 additions and 107 deletions

View File

@ -41,6 +41,11 @@
.hash = "ziglyph-0.11.2-AAAAAHPtHwB4Mbzn1KvOV7Wpjo82NYEc_v0WC8oCLrkf",
.lazy = true,
},
.zg = .{
.url = "https://codeberg.org/jacobsandlund/zg/archive/21be4c72f2aff537d7630a7aeb641befc935db89.tar.gz",
.hash = "zg-0.14.0-oGqU3IkvswJcetRSabEnsUe_jsWz48WkZebbVS-pJ5jB",
.lazy = true,
},
.zig_wayland = .{
// codeberg ifreund/zig-wayland
.url = "https://codeberg.org/ifreund/zig-wayland/archive/f3c5d503e540ada8cbcb056420de240af0c094f7.tar.gz",

View File

@ -27,6 +27,8 @@ hyperfine \
"./zig-out/bin/bench-grapheme-break --mode=noop${ARGS} </tmp/ghostty_bench_data" \
-n ziglyph \
"./zig-out/bin/bench-grapheme-break --mode=ziglyph${ARGS} </tmp/ghostty_bench_data" \
-n zg \
"./zig-out/bin/bench-grapheme-break --mode=zg${ARGS} </tmp/ghostty_bench_data" \
-n table \
"./zig-out/bin/bench-grapheme-break --mode=table${ARGS} </tmp/ghostty_bench_data"

View File

@ -13,6 +13,7 @@ const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const ziglyph = @import("ziglyph");
const Graphemes = @import("Graphemes");
const cli = @import("../cli.zig");
const simd = @import("../simd/main.zig");
const unicode = @import("../unicode/main.zig");
@ -44,6 +45,9 @@ const Mode = enum {
/// Use ziglyph library to calculate the display width of each codepoint.
ziglyph,
/// Use zg library to calculate the display width of each codepoint.
zg,
/// Ghostty's table-based approach.
table,
};
@ -56,6 +60,10 @@ pub fn main() !void {
// We want to use the c allocator because it is much faster than GPA.
const alloc = std.heap.c_allocator;
// Initialize Graphemes for zg
const graphemes = try Graphemes.init(alloc);
graphemes.deinit(alloc);
// Parse our args
var args: Args = .{};
defer args.deinit();
@ -72,6 +80,7 @@ pub fn main() !void {
switch (args.mode) {
.noop => try benchNoop(reader, buf),
.ziglyph => try benchZiglyph(reader, buf),
.zg => try benchZg(&graphemes, reader, buf),
.table => try benchTable(reader, buf),
}
}
@ -118,6 +127,32 @@ noinline fn benchTable(
}
}
noinline fn benchZg(
graphemes: *const Graphemes,
reader: anytype,
buf: []u8,
) !void {
var d: UTF8Decoder = .{};
var state: Graphemes.State = .{};
var cp1: u21 = 0;
while (true) {
const n = try reader.read(buf);
if (n == 0) break;
// Using stream.next directly with a for loop applies a naive
// scalar approach.
for (buf[0..n]) |c| {
const cp_, const consumed = d.next(c);
assert(consumed);
if (cp_) |cp2| {
const v = Graphemes.graphemeBreak(cp1, @intCast(cp2), graphemes, &state);
buf[0] = @intCast(@intFromBool(v));
cp1 = cp2;
}
}
}
}
noinline fn benchZiglyph(
reader: anytype,
buf: []u8,

View File

@ -417,6 +417,18 @@ pub fn add(
})) |dep| {
step.root_module.addImport("ziglyph", dep.module("ziglyph"));
}
if (b.lazyDependency("zg", .{
.target = target,
.optimize = optimize,
})) |dep| {
step.root_module.addImport("CaseFolding", dep.module("CaseFolding"));
// Only needed for table gen, but still include to have build succeeed
step.root_module.addImport("DisplayWidth", dep.module("DisplayWidth"));
step.root_module.addImport("Emoji", dep.module("Emoji"));
step.root_module.addImport("GeneralCategories", dep.module("GeneralCategories"));
step.root_module.addImport("Graphemes", dep.module("Graphemes"));
step.root_module.addImport("LetterCasing", dep.module("LetterCasing"));
}
if (b.lazyDependency("zf", .{
.target = target,
.optimize = optimize,

View File

@ -21,13 +21,11 @@ pub fn init(b: *std.Build) !UnicodeTables {
}),
});
if (b.lazyDependency("ziglyph", .{
if (b.lazyDependency("zg", .{
.target = b.graph.host,
})) |ziglyph_dep| {
exe.root_module.addImport(
"ziglyph",
ziglyph_dep.module("ziglyph"),
);
})) |dep| {
exe.root_module.addImport("Graphemes", dep.module("Graphemes"));
exe.root_module.addImport("DisplayWidth", dep.module("DisplayWidth"));
}
const run = b.addRunArtifact(exe);

View File

@ -16,7 +16,6 @@ const build_config = @import("../build_config.zig");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const global_state = &@import("../global.zig").state;
const fontpkg = @import("../font/main.zig");
const inputpkg = @import("../input.zig");
const terminal = @import("../terminal/main.zig");
@ -5765,6 +5764,8 @@ pub const Keybinds = struct {
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
const zg = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var set: Keybinds = .{};
try set.parseCLI(alloc, "shift+a=copy_to_clipboard");
@ -5779,6 +5780,8 @@ pub const Keybinds = struct {
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
const zg = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var list: Keybinds = .{};
try list.parseCLI(alloc, "shift+a=csi:hello");
@ -5795,6 +5798,8 @@ pub const Keybinds = struct {
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
const zg = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var list: Keybinds = .{};
try list.parseCLI(alloc, "ctrl+z>1=goto_tab:1");
@ -5819,6 +5824,8 @@ pub const Keybinds = struct {
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
const zg = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var list: Keybinds = .{};
try list.parseCLI(alloc, "ctrl+a>ctrl+b>n=new_window");
@ -6030,6 +6037,8 @@ pub const RepeatableCodepointMap = struct {
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
const zg = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var list: Self = .{};
try list.parseCLI(alloc, "U+ABCD=Comic Sans");
@ -6067,6 +6076,8 @@ pub const RepeatableCodepointMap = struct {
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
const zg = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var list: Self = .{};
try list.parseCLI(alloc, "U+ABCD=Comic Sans");
@ -6082,6 +6093,8 @@ pub const RepeatableCodepointMap = struct {
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
const zg = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var list: Self = .{};
try list.parseCLI(alloc, "U+0001 - U+0005=Verdana");
@ -6097,6 +6110,8 @@ pub const RepeatableCodepointMap = struct {
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
const zg = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var list: Self = .{};
try list.parseCLI(alloc, "U+0006-U+0009, U+ABCD=Courier");
@ -6175,6 +6190,8 @@ pub const FontStyle = union(enum) {
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
const zg = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var p: Self = .{ .default = {} };
try p.parseCLI(alloc, "default");
@ -6195,6 +6212,8 @@ pub const FontStyle = union(enum) {
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
const zg = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var p: Self = .{ .default = {} };
try p.parseCLI(alloc, "default");
@ -6210,6 +6229,8 @@ pub const FontStyle = union(enum) {
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
const zg = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var p: Self = .{ .default = {} };
try p.parseCLI(alloc, "false");
@ -6225,6 +6246,8 @@ pub const FontStyle = union(enum) {
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
const zg = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var p: Self = .{ .default = {} };
try p.parseCLI(alloc, "bold");
@ -6402,6 +6425,8 @@ pub const RepeatableCommand = struct {
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
const zg = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var list: RepeatableCommand = .{};
try list.parseCLI(alloc, "title:Foo,action:ignore");
@ -6447,6 +6472,8 @@ pub const RepeatableCommand = struct {
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
const zg = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var list: RepeatableCommand = .{};
try list.parseCLI(alloc, "title:Bobr, action:text:Bober");
@ -6462,6 +6489,8 @@ pub const RepeatableCommand = struct {
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
const zg = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var list: RepeatableCommand = .{};
try list.parseCLI(alloc, "title:Bobr, action:text:kurwa");
@ -7180,6 +7209,8 @@ pub const Theme = struct {
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
const zg = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
// Single
{
@ -7597,6 +7628,8 @@ const TestIterator = struct {
test "parse hook: invalid command" {
const testing = std.testing;
const zg = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var cfg = try Config.default(testing.allocator);
defer cfg.deinit();
const alloc = cfg._arena.?.allocator();
@ -7608,6 +7641,8 @@ test "parse hook: invalid command" {
test "parse e: command only" {
const testing = std.testing;
const zg = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var cfg = try Config.default(testing.allocator);
defer cfg.deinit();
const alloc = cfg._arena.?.allocator();
@ -7623,6 +7658,8 @@ test "parse e: command only" {
test "parse e: command and args" {
const testing = std.testing;
const zg = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var cfg = try Config.default(testing.allocator);
defer cfg.deinit();
const alloc = cfg._arena.?.allocator();
@ -7641,6 +7678,8 @@ test "parse e: command and args" {
test "clone default" {
const testing = std.testing;
const alloc = testing.allocator;
const zg = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var source = try Config.default(alloc);
defer source.deinit();
@ -7658,6 +7697,8 @@ test "clone default" {
test "clone preserves conditional state" {
const testing = std.testing;
const alloc = testing.allocator;
const zg = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var a = try Config.default(alloc);
defer a.deinit();
@ -7686,6 +7727,8 @@ test "clone can then change conditional state" {
var arena = ArenaAllocator.init(alloc);
defer arena.deinit();
const alloc_arena = arena.allocator();
const zg = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
// Setup our test theme
var td = try internal_os.TempDir.init();
@ -7746,6 +7789,8 @@ test "clone can then change conditional state" {
test "clone preserves conditional set" {
const testing = std.testing;
const alloc = testing.allocator;
const zg = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var cfg = try Config.default(alloc);
defer cfg.deinit();
@ -7765,6 +7810,8 @@ test "clone preserves conditional set" {
test "changed" {
const testing = std.testing;
const alloc = testing.allocator;
const zg = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var source = try Config.default(alloc);
defer source.deinit();
@ -7779,6 +7826,8 @@ test "changed" {
test "changeConditionalState ignores irrelevant changes" {
const testing = std.testing;
const alloc = testing.allocator;
const zg = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
{
var cfg = try Config.default(alloc);
@ -7798,6 +7847,8 @@ test "changeConditionalState ignores irrelevant changes" {
test "changeConditionalState applies relevant changes" {
const testing = std.testing;
const alloc = testing.allocator;
const zg = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
{
var cfg = try Config.default(alloc);
@ -7820,6 +7871,8 @@ test "theme loading" {
var arena = ArenaAllocator.init(alloc);
defer arena.deinit();
const alloc_arena = arena.allocator();
const zg = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
// Setup our test theme
var td = try internal_os.TempDir.init();
@ -7856,6 +7909,8 @@ test "theme loading preserves conditional state" {
var arena = ArenaAllocator.init(alloc);
defer arena.deinit();
const alloc_arena = arena.allocator();
const zg = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
// Setup our test theme
var td = try internal_os.TempDir.init();
@ -7886,6 +7941,8 @@ test "theme priority is lower than config" {
var arena = ArenaAllocator.init(alloc);
defer arena.deinit();
const alloc_arena = arena.allocator();
const zg = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
// Setup our test theme
var td = try internal_os.TempDir.init();
@ -7920,6 +7977,8 @@ test "theme loading correct light/dark" {
var arena = ArenaAllocator.init(alloc);
defer arena.deinit();
const alloc_arena = arena.allocator();
const zg = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
// Setup our test theme
var td = try internal_os.TempDir.init();
@ -8009,6 +8068,8 @@ test "theme loading correct light/dark" {
test "theme specifying light/dark changes window-theme from auto" {
const testing = std.testing;
const alloc = testing.allocator;
const zg = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
{
var cfg = try Config.default(alloc);
@ -8027,6 +8088,8 @@ test "theme specifying light/dark changes window-theme from auto" {
test "theme specifying light/dark sets theme usage in conditional state" {
const testing = std.testing;
const alloc = testing.allocator;
const zg = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
{
var cfg = try Config.default(alloc);

View File

@ -121,6 +121,8 @@ fn fieldByKey(self: *const Config, comptime k: Key) Value(k) {
test "c_get: u8" {
const testing = std.testing;
const alloc = testing.allocator;
const zg = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var c = try Config.default(alloc);
defer c.deinit();
@ -134,6 +136,8 @@ test "c_get: u8" {
test "c_get: enum" {
const testing = std.testing;
const alloc = testing.allocator;
const zg = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var c = try Config.default(alloc);
defer c.deinit();
@ -149,6 +153,8 @@ test "c_get: enum" {
test "c_get: color" {
const testing = std.testing;
const alloc = testing.allocator;
const zg = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var c = try Config.default(alloc);
defer c.deinit();
@ -164,6 +170,8 @@ test "c_get: color" {
test "c_get: optional" {
const testing = std.testing;
const alloc = testing.allocator;
const zg = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var c = try Config.default(alloc);
defer c.deinit();
@ -187,6 +195,8 @@ test "c_get: optional" {
test "c_get: background-blur" {
const testing = std.testing;
const alloc = testing.allocator;
const zg = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var c = try Config.default(alloc);
defer c.deinit();

View File

@ -193,6 +193,8 @@ pub const FileFormatter = struct {
test "format default config" {
const testing = std.testing;
const alloc = testing.allocator;
const zg = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var cfg = try Config.default(alloc);
defer cfg.deinit();
@ -212,6 +214,8 @@ test "format default config" {
test "format default config changed" {
const testing = std.testing;
const alloc = testing.allocator;
const zg = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var cfg = try Config.default(alloc);
defer cfg.deinit();
cfg.@"font-size" = 42;

View File

@ -13,8 +13,9 @@ const CodepointResolver = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
const ziglyph = @import("ziglyph");
const Emoji = @import("Emoji");
const font = @import("main.zig");
const zg = &@import("../global.zig").state.zg;
const Atlas = font.Atlas;
const CodepointMap = font.CodepointMap;
const Collection = font.Collection;
@ -150,7 +151,7 @@ pub fn getIndex(
// we'll do this multiple times if we recurse, but this is a cached function
// call higher up (GroupCache) so this should be rare.
const p_mode: Collection.PresentationMode = if (p) |v| .{ .explicit = v } else .{
.default = if (ziglyph.emoji.isEmojiPresentation(@intCast(cp)))
.default = if (Emoji.isEmojiPresentation(zg.emoji, @intCast(cp)))
.emoji
else
.text,
@ -379,6 +380,8 @@ test getIndex {
const testFont = font.embedded.regular;
const testEmoji = font.embedded.emoji;
const testEmojiText = font.embedded.emoji_text;
_ = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var lib = try Library.init(alloc);
defer lib.deinit();
@ -457,6 +460,8 @@ test "getIndex disabled font style" {
const testing = std.testing;
const alloc = testing.allocator;
const testFont = font.embedded.regular;
_ = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var atlas_grayscale = try font.Atlas.init(alloc, 512, .grayscale);
defer atlas_grayscale.deinit(alloc);
@ -512,6 +517,8 @@ test "getIndex disabled font style" {
test "getIndex box glyph" {
const testing = std.testing;
const alloc = testing.allocator;
_ = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var lib = try Library.init(alloc);
defer lib.deinit();

View File

@ -369,6 +369,8 @@ fn testGrid(mode: TestMode, alloc: Allocator, lib: Library) !SharedGrid {
test getIndex {
const testing = std.testing;
const alloc = testing.allocator;
const zg = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
// const testEmoji = @import("test.zig").fontEmoji;
var lib = try Library.init(alloc);

View File

@ -694,6 +694,8 @@ pub const Key = struct {
test "Key" {
const testing = std.testing;
const alloc = testing.allocator;
const zg = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var cfg = try Config.default(alloc);
defer cfg.deinit();
@ -713,6 +715,8 @@ test "Key" {
test "Key different font points" {
const testing = std.testing;
const alloc = testing.allocator;
const zg = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var cfg = try Config.default(alloc);
defer cfg.deinit();
@ -731,6 +735,8 @@ test "Key different font points" {
test "Key different font DPI" {
const testing = std.testing;
const alloc = testing.allocator;
const zg = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var cfg = try Config.default(alloc);
defer cfg.deinit();
@ -749,6 +755,8 @@ test "Key different font DPI" {
test SharedGridSet {
const testing = std.testing;
const alloc = testing.allocator;
const zg = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var set = try SharedGridSet.init(alloc);
defer set.deinit();

View File

@ -576,6 +576,8 @@ pub const Shaper = struct {
test "run iterator" {
const testing = std.testing;
const alloc = testing.allocator;
const zg = try @import("../../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var testdata = try testShaper(alloc);
defer testdata.deinit();
@ -656,6 +658,8 @@ test "run iterator" {
test "run iterator: empty cells with background set" {
const testing = std.testing;
const alloc = testing.allocator;
const zg = try @import("../../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var testdata = try testShaper(alloc);
defer testdata.deinit();
@ -704,6 +708,8 @@ test "run iterator: empty cells with background set" {
test "shape" {
const testing = std.testing;
const alloc = testing.allocator;
const zg = try @import("../../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var testdata = try testShaper(alloc);
defer testdata.deinit();
@ -737,6 +743,8 @@ test "shape" {
test "shape nerd fonts" {
const testing = std.testing;
const alloc = testing.allocator;
const zg = try @import("../../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var testdata = try testShaperWithFont(alloc, .nerd_font);
defer testdata.deinit();
@ -770,6 +778,8 @@ test "shape nerd fonts" {
test "shape inconsolata ligs" {
const testing = std.testing;
const alloc = testing.allocator;
const zg = try @import("../../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var testdata = try testShaper(alloc);
defer testdata.deinit();
@ -824,6 +834,8 @@ test "shape inconsolata ligs" {
test "shape monaspace ligs" {
const testing = std.testing;
const alloc = testing.allocator;
const zg = try @import("../../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var testdata = try testShaperWithFont(alloc, .monaspace_neon);
defer testdata.deinit();
@ -856,6 +868,8 @@ test "shape monaspace ligs" {
test "shape left-replaced lig in last run" {
const testing = std.testing;
const alloc = testing.allocator;
const zg = try @import("../../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var testdata = try testShaperWithFont(alloc, .geist_mono);
defer testdata.deinit();
@ -888,6 +902,8 @@ test "shape left-replaced lig in last run" {
test "shape left-replaced lig in early run" {
const testing = std.testing;
const alloc = testing.allocator;
const zg = try @import("../../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var testdata = try testShaperWithFont(alloc, .geist_mono);
defer testdata.deinit();
@ -917,6 +933,8 @@ test "shape left-replaced lig in early run" {
test "shape U+3C9 with JB Mono" {
const testing = std.testing;
const alloc = testing.allocator;
const zg = try @import("../../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var testdata = try testShaperWithFont(alloc, .jetbrains_mono);
defer testdata.deinit();
@ -948,6 +966,8 @@ test "shape U+3C9 with JB Mono" {
test "shape emoji width" {
const testing = std.testing;
const alloc = testing.allocator;
const zg = try @import("../../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var testdata = try testShaper(alloc);
defer testdata.deinit();
@ -977,6 +997,8 @@ test "shape emoji width" {
test "shape emoji width long" {
const testing = std.testing;
const alloc = testing.allocator;
const zg = try @import("../../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var testdata = try testShaper(alloc);
defer testdata.deinit();
@ -1024,6 +1046,8 @@ test "shape emoji width long" {
test "shape variation selector VS15" {
const testing = std.testing;
const alloc = testing.allocator;
const zg = try @import("../../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var testdata = try testShaper(alloc);
defer testdata.deinit();
@ -1057,6 +1081,8 @@ test "shape variation selector VS15" {
test "shape variation selector VS16" {
const testing = std.testing;
const alloc = testing.allocator;
const zg = try @import("../../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var testdata = try testShaper(alloc);
defer testdata.deinit();
@ -1090,6 +1116,8 @@ test "shape variation selector VS16" {
test "shape with empty cells in between" {
const testing = std.testing;
const alloc = testing.allocator;
const zg = try @import("../../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var testdata = try testShaper(alloc);
defer testdata.deinit();
@ -1121,6 +1149,8 @@ test "shape with empty cells in between" {
test "shape Chinese characters" {
const testing = std.testing;
const alloc = testing.allocator;
const zg = try @import("../../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var testdata = try testShaper(alloc);
defer testdata.deinit();
@ -1161,6 +1191,8 @@ test "shape Chinese characters" {
test "shape box glyphs" {
const testing = std.testing;
const alloc = testing.allocator;
const zg = try @import("../../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var testdata = try testShaper(alloc);
defer testdata.deinit();
@ -1198,6 +1230,8 @@ test "shape box glyphs" {
test "shape selection boundary" {
const testing = std.testing;
const alloc = testing.allocator;
const zg = try @import("../../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var testdata = try testShaper(alloc);
defer testdata.deinit();
@ -1321,6 +1355,8 @@ test "shape selection boundary" {
test "shape cursor boundary" {
const testing = std.testing;
const alloc = testing.allocator;
const zg = try @import("../../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var testdata = try testShaper(alloc);
defer testdata.deinit();
@ -1458,6 +1494,8 @@ test "shape cursor boundary" {
test "shape cursor boundary and colored emoji" {
const testing = std.testing;
const alloc = testing.allocator;
const zg = try @import("../../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var testdata = try testShaper(alloc);
defer testdata.deinit();
@ -1552,6 +1590,8 @@ test "shape cursor boundary and colored emoji" {
test "shape cell attribute change" {
const testing = std.testing;
const alloc = testing.allocator;
const zg = try @import("../../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var testdata = try testShaper(alloc);
defer testdata.deinit();
@ -1680,6 +1720,8 @@ test "shape high plane sprite font codepoint" {
const testing = std.testing;
const alloc = testing.allocator;
const zg = try @import("../../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var testdata = try testShaper(alloc);
defer testdata.deinit();

View File

@ -1,9 +1,10 @@
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const ziglyph = @import("ziglyph");
const Graphemes = @import("Graphemes");
const font = @import("../main.zig");
const terminal = @import("../../terminal/main.zig");
const zg = &@import("../../global.zig").state.zg;
const log = std.log.scoped(.font_shaper);
@ -111,7 +112,7 @@ pub const Shaper = struct {
// font ligatures. However, we do support grapheme clustering.
// This means we can render things like skin tone emoji but
// we can't render things like single glyph "=>".
var break_state: u3 = 0;
var break_state: Graphemes.State = .{};
var cp1: u21 = @intCast(codepoints[0]);
var start: usize = 0;
@ -126,9 +127,10 @@ pub const Shaper = struct {
const cp2: u21 = @intCast(codepoints[i]);
defer cp1 = cp2;
break :blk ziglyph.graphemeBreak(
break :blk Graphemes.graphemeBreak(
cp1,
cp2,
zg.graphemes,
&break_state,
);
};

View File

@ -10,6 +10,12 @@ const oni = @import("oniguruma");
const crash = @import("crash/main.zig");
const renderer = @import("renderer.zig");
const apprt = @import("apprt.zig");
const CaseFolding = @import("CaseFolding");
const Emoji = @import("Emoji");
const GeneralCategories = @import("GeneralCategories");
const Graphemes = @import("Graphemes");
const LetterCasing = @import("LetterCasing");
const unicode = @import("unicode/main.zig");
/// We export the xev backend we want to use so that the rest of
/// Ghostty can import this once and have access to the proper
@ -33,6 +39,7 @@ pub const GlobalState = struct {
action: ?cli.Action,
logging: Logging,
rlimits: ResourceLimits = .{},
zg: Zg,
/// The app resources directory, equivalent to zig-out/share when we build
/// from source. This is null if we can't detect it.
@ -64,6 +71,7 @@ pub const GlobalState = struct {
.logging = .{ .stderr = {} },
.rlimits = .{},
.resources_dir = .{},
.zg = undefined,
};
errdefer self.deinit();
@ -150,6 +158,8 @@ pub const GlobalState = struct {
);
};
self.zg = try Zg.init(self.alloc);
// const sentrylib = @import("sentry");
// if (sentrylib.captureEvent(sentrylib.Value.initMessageEvent(
// .info,
@ -188,6 +198,8 @@ pub const GlobalState = struct {
// Flush our crash logs
crash.deinit();
self.zg.deinit(self.alloc);
if (self.gpa) |*value| {
// We want to ensure that we deinit the GPA because this is
// the point at which it will output if there were safety violations.
@ -216,6 +228,42 @@ pub const GlobalState = struct {
}
};
pub const Zg = struct {
case_folding: CaseFolding,
emoji: Emoji,
general_categories: GeneralCategories,
graphemes: Graphemes,
letter_casing: LetterCasing,
pub fn init(alloc: std.mem.Allocator) !Zg {
return .{
.case_folding = try CaseFolding.init(alloc),
.emoji = try Emoji.init(alloc),
.general_categories = try GeneralCategories.init(alloc),
.graphemes = try Graphemes.init(alloc),
.letter_casing = try LetterCasing.init(alloc),
};
}
pub fn deinit(self: *Zg, alloc: std.mem.Allocator) void {
self.case_folding.deinit(alloc);
self.emoji.deinit(alloc);
self.general_categories.deinit(alloc);
self.graphemes.deinit(alloc);
self.letter_casing.deinit(alloc);
}
pub fn initForTesting() !*Zg {
const zg = &state.zg;
zg.* = try Zg.init(std.testing.allocator);
return zg;
}
pub fn deinitForTesting(self: *Zg) void {
self.deinit(std.testing.allocator);
}
};
/// Maintains the Unix resource limits that we set for our process. This
/// can be used to restore the limits to their original values.
pub const ResourceLimits = struct {

View File

@ -5,9 +5,11 @@ const Binding = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const ziglyph = @import("ziglyph");
const LetterCasing = @import("LetterCasing");
const CaseFolding = @import("CaseFolding");
const key = @import("key.zig");
const KeyEvent = key.KeyEvent;
const zg = &@import("../global.zig").state.zg;
/// The trigger that needs to be performed to execute the action.
trigger: Trigger,
@ -1551,15 +1553,17 @@ pub const Trigger = struct {
/// in more codepoints so we need to use a 3 element array.
fn foldedCodepoint(cp: u21) [3]u21 {
// ASCII fast path
if (ziglyph.letter.isAsciiLetter(cp)) {
return .{ ziglyph.letter.toLower(cp), 0, 0 };
if (('A' <= cp and cp <= 'Z') or ('a' <= cp and cp <= 'z')) {
return .{ LetterCasing.toLower(zg.letter_casing, cp), 0, 0 };
}
// Unicode slow path. Case folding can resultin more codepoints.
// Unicode slow path. Case folding can result in more codepoints.
// If more codepoints are produced then we return the codepoint
// as-is which isn't correct but until we have a failing test
// then I don't want to handle this.
return ziglyph.letter.toCaseFold(cp);
var buf: [3]u21 = .{ 0, 0, 0 };
_ = CaseFolding.caseFold(&zg.case_folding, cp, &buf);
return buf;
}
/// Convert the trigger to a C API compatible trigger.
@ -2612,6 +2616,8 @@ test "parse: sequences" {
test "set: parseAndPut typical binding" {
const testing = std.testing;
const alloc = testing.allocator;
_ = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var s: Set = .{};
defer s.deinit(alloc);
@ -2635,6 +2641,8 @@ test "set: parseAndPut typical binding" {
test "set: parseAndPut unconsumed binding" {
const testing = std.testing;
const alloc = testing.allocator;
_ = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var s: Set = .{};
defer s.deinit(alloc);
@ -2659,6 +2667,8 @@ test "set: parseAndPut unconsumed binding" {
test "set: parseAndPut removed binding" {
const testing = std.testing;
const alloc = testing.allocator;
_ = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var s: Set = .{};
defer s.deinit(alloc);
@ -2677,6 +2687,8 @@ test "set: parseAndPut removed binding" {
test "set: parseAndPut sequence" {
const testing = std.testing;
const alloc = testing.allocator;
_ = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var s: Set = .{};
defer s.deinit(alloc);
@ -2701,6 +2713,8 @@ test "set: parseAndPut sequence" {
test "set: parseAndPut sequence with two actions" {
const testing = std.testing;
const alloc = testing.allocator;
_ = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var s: Set = .{};
defer s.deinit(alloc);
@ -2733,6 +2747,8 @@ test "set: parseAndPut sequence with two actions" {
test "set: parseAndPut overwrite sequence" {
const testing = std.testing;
const alloc = testing.allocator;
_ = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var s: Set = .{};
defer s.deinit(alloc);
@ -2758,6 +2774,8 @@ test "set: parseAndPut overwrite sequence" {
test "set: parseAndPut overwrite leader" {
const testing = std.testing;
const alloc = testing.allocator;
_ = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var s: Set = .{};
defer s.deinit(alloc);
@ -2783,6 +2801,8 @@ test "set: parseAndPut overwrite leader" {
test "set: parseAndPut unbind sequence unbinds leader" {
const testing = std.testing;
const alloc = testing.allocator;
_ = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var s: Set = .{};
defer s.deinit(alloc);
@ -2799,6 +2819,8 @@ test "set: parseAndPut unbind sequence unbinds leader" {
test "set: parseAndPut unbind sequence unbinds leader if not set" {
const testing = std.testing;
const alloc = testing.allocator;
_ = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var s: Set = .{};
defer s.deinit(alloc);
@ -2814,6 +2836,8 @@ test "set: parseAndPut unbind sequence unbinds leader if not set" {
test "set: parseAndPut sequence preserves reverse mapping" {
const testing = std.testing;
const alloc = testing.allocator;
_ = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var s: Set = .{};
defer s.deinit(alloc);
@ -2831,6 +2855,8 @@ test "set: parseAndPut sequence preserves reverse mapping" {
test "set: put overwrites sequence" {
const testing = std.testing;
const alloc = testing.allocator;
_ = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var s: Set = .{};
defer s.deinit(alloc);
@ -2851,6 +2877,8 @@ test "set: put overwrites sequence" {
test "set: maintains reverse mapping" {
const testing = std.testing;
const alloc = testing.allocator;
_ = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var s: Set = .{};
defer s.deinit(alloc);
@ -2879,6 +2907,8 @@ test "set: maintains reverse mapping" {
test "set: performable is not part of reverse mappings" {
const testing = std.testing;
const alloc = testing.allocator;
_ = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var s: Set = .{};
defer s.deinit(alloc);
@ -2912,6 +2942,8 @@ test "set: performable is not part of reverse mappings" {
test "set: overriding a mapping updates reverse" {
const testing = std.testing;
const alloc = testing.allocator;
_ = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var s: Set = .{};
defer s.deinit(alloc);
@ -2933,6 +2965,8 @@ test "set: overriding a mapping updates reverse" {
test "set: consumed state" {
const testing = std.testing;
const alloc = testing.allocator;
_ = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var s: Set = .{};
defer s.deinit(alloc);
@ -2958,6 +2992,8 @@ test "set: consumed state" {
test "set: getEvent physical" {
const testing = std.testing;
const alloc = testing.allocator;
_ = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var s: Set = .{};
defer s.deinit(alloc);
@ -2988,6 +3024,8 @@ test "set: getEvent physical" {
test "set: getEvent codepoint" {
const testing = std.testing;
const alloc = testing.allocator;
_ = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var s: Set = .{};
defer s.deinit(alloc);
@ -3028,6 +3066,8 @@ test "set: getEvent codepoint" {
test "set: getEvent codepoint case folding" {
const testing = std.testing;
const alloc = testing.allocator;
_ = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
var s: Set = .{};
defer s.deinit(alloc);
@ -3070,6 +3110,8 @@ test "Action: clone" {
var arena = std.heap.ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
_ = try @import("../global.zig").Zg.initForTesting();
defer zg.deinitForTesting();
{
var a: Action = .ignore;

View File

@ -1,12 +1,13 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const ziglyph = @import("ziglyph");
const font = @import("../font/main.zig");
const terminal = @import("../terminal/main.zig");
const renderer = @import("../renderer.zig");
const shaderpkg = renderer.Renderer.API.shaders;
const ArrayListCollection = @import("../datastruct/array_list_collection.zig").ArrayListCollection;
const GeneralCategories = @import("GeneralCategories");
const zg = &@import("../global.zig").state.zg;
/// The possible cell content keys that exist.
pub const Key = enum {
@ -246,8 +247,10 @@ pub fn fgMode(
const cell = cell_pin.rowAndCell().cell;
const cp = cell.codepoint();
if (!ziglyph.general_category.isPrivateUse(cp) and
!ziglyph.blocks.isDingbats(cp))
// If it's not Private Use (Co) or Dingbats (0x2700-0x27bf), use
// normal mode.
if (GeneralCategories.gc(zg.general_categories, cp) != .Co and
!(cp >= 0x2700 and cp <= 0x27bf))
{
break :text .normal;
}
@ -278,7 +281,8 @@ pub fn fgMode(
// Powerline is whitespace
if (isPowerline(prev_cp)) break :prev;
if (ziglyph.general_category.isPrivateUse(prev_cp)) {
// If it's Private Use (Co), then we use constrained mode.
if (GeneralCategories.gc(zg.general_categories, cp) == .Co) {
break :text .constrained;
}
}

View File

@ -4,7 +4,9 @@ const std = @import("std");
extern "c" fn ghostty_simd_codepoint_width(u32) i8;
pub fn codepointWidth(cp: u32) i8 {
//return @import("ziglyph").display_width.codePointWidth(@intCast(cp), .half);
// const zg = try @import("../../global.zig").Zg.initForTesting();
// defer zg.deinitForTesting();
// try testing.expectEqual(@as(i8, 1), @import("DisplayWidth").codePointWidth(zg.display_width, @intCast(cp)));
return ghostty_simd_codepoint_width(cp);
}
@ -19,27 +21,41 @@ test "codepointWidth basic" {
try testing.expectEqual(@as(i8, 2), codepointWidth(0xF900)); //
try testing.expectEqual(@as(i8, 2), codepointWidth(0x20000)); // 𠀀
try testing.expectEqual(@as(i8, 2), codepointWidth(0x30000)); // 𠀀
// try testing.expectEqual(@as(i8, 1), @import("ziglyph").display_width.codePointWidth(0x100, .half));
// const zg = try @import("../../global.zig").Zg.initForTesting();
// defer zg.deinitForTesting();
// try testing.expectEqual(@as(i8, 1), @import("DisplayWidth").codePointWidth(zg.display_width, 0x100));
}
// This is not very fast in debug modes, so its commented by default.
// IMPORTANT: UNCOMMENT THIS WHENEVER MAKING CODEPOINTWIDTH CHANGES.
// test "codepointWidth matches ziglyph" {
// const testing = std.testing;
// const ziglyph = @import("ziglyph");
//test "codepointWidth matches zg" {
// const testing = std.testing;
// const DisplayWidth = @import("DisplayWidth");
// const display_width = try DisplayWidth.init(std.testing.allocator);
// defer display_width.deinit(std.testing.allocator);
// var success: bool = true;
//
// const min = 0xFF + 1; // start outside ascii
// for (min..std.math.maxInt(u21)) |cp| {
// const simd = codepointWidth(@intCast(cp));
// const zg = ziglyph.display_width.codePointWidth(@intCast(cp), .half);
// if (simd != zg) mismatch: {
// if (cp == 0x2E3B) {
// try testing.expectEqual(@as(i8, 2), simd);
// break :mismatch;
// }
// const min = 0xFF + 1; // start outside ascii
// for (min..0x110000) |cp| {
// const simd = codepointWidth(@intCast(cp));
// const zg_width = DisplayWidth.codePointWidth(display_width, @intCast(cp));
// if (simd != zg_width) mismatch: {
// if (cp == 0x2E3B) {
// try testing.expectEqual(@as(i8, 2), simd);
// std.log.warn("mismatch for 0x2e3b cp=U+{x} simd={} zg={}", .{ cp, simd, zg_width });
// break :mismatch;
// }
//
// std.log.warn("mismatch cp=U+{x} simd={} zg={}", .{ cp, simd, zg });
// try testing.expect(false);
// }
// }
// }
// if (cp == 0x890) {
// try testing.expectEqual(@as(i8, 0), simd);
// try testing.expectEqual(@as(i8, 1), zg_width);
// break :mismatch;
// }
//
// std.log.warn("mismatch cp=U+{x} simd={} zg={}", .{ cp, simd, zg_width });
// success = false;
// }
// }
//
// try testing.expect(success);
//}

View File

@ -344,7 +344,7 @@ pub fn print(self: *Terminal, c: u21) !void {
// VS15 makes it narrow.
if (c == 0xFE0F or c == 0xFE0E) {
// This only applies to emoji
const prev_props = unicode.getProperties(prev.cell.content.codepoint);
const prev_props = unicode.table.get(prev.cell.content.codepoint);
const emoji = prev_props.grapheme_boundary_class.isExtendedPictographic();
if (!emoji) return;
@ -416,7 +416,7 @@ pub fn print(self: *Terminal, c: u21) !void {
const width: usize = if (c <= 0xFF) 1 else @intCast(unicode.table.get(c).width);
// Note: it is possible to have a width of "3" and a width of "-1"
// from ziglyph. We should look into those cases and handle them
// from zg. We should look into those cases and handle them
// appropriately.
assert(width <= 2);
// log.debug("c={x} width={}", .{ c, width });
@ -452,7 +452,7 @@ pub fn print(self: *Terminal, c: u21) !void {
// If this is a emoji variation selector, prev must be an emoji
if (c == 0xFE0F or c == 0xFE0E) {
const prev_props = unicode.getProperties(prev.content.codepoint);
const prev_props = unicode.table.get(prev.content.codepoint);
const emoji = prev_props.grapheme_boundary_class == .extended_pictographic;
if (!emoji) return;
}

View File

@ -149,21 +149,19 @@ fn graphemeBreakClass(
return true;
}
/// If you build this file as a binary, we will verify the grapheme break
/// implementation. This iterates over billions of codepoints so it is
/// SLOW. It's not meant to be run in CI, but it's useful for debugging.
pub fn main() !void {
// This test will verify the grapheme break implementation. This iterates over billions of codepoints so it is SLOW.
// It's not meant to be run in CI, but it's useful for debugging.
test "grapheme break check against ziglyph" {
const ziglyph = @import("ziglyph");
// Set the min and max to control the test range.
const min = 0;
const max = std.math.maxInt(u21) + 1;
var success: bool = true;
var state: BreakState = .{};
var zg_state: u3 = 0;
for (min..max) |cp1| {
if (cp1 % 1000 == 0) std.log.warn("progress cp1={}", .{cp1});
if (cp1 == '\r' or cp1 == '\n' or
ziglyph.grapheme_break.isControl(@intCast(cp1))) continue;
@ -174,6 +172,7 @@ pub fn main() !void {
const gb = graphemeBreak(@intCast(cp1), @intCast(cp2), &state);
const zg_gb = ziglyph.graphemeBreak(@intCast(cp1), @intCast(cp2), &zg_state);
if (gb != zg_gb) {
success = false;
std.log.warn("cp1={x} cp2={x} gb={} state={} zg_gb={} zg_state={}", .{
cp1,
cp2,
@ -185,6 +184,8 @@ pub fn main() !void {
}
}
}
try std.testing.expect(success);
}
pub const std_options = struct {

View File

@ -11,7 +11,7 @@ const Allocator = std.mem.Allocator;
/// can in theory be generated at runtime.
///
/// Context must have two functions:
/// - `get(Context, u21) Elem`: returns the mapping for a given codepoint
/// - `get(Context, u21) !Elem`: returns the mapping for a given codepoint
/// - `eql(Context, Elem, Elem) bool`: returns true if two mappings are equal
///
pub fn Generator(

View File

@ -4,7 +4,6 @@ const grapheme = @import("grapheme.zig");
const props = @import("props.zig");
pub const table = props.table;
pub const Properties = props.Properties;
pub const getProperties = props.get;
pub const graphemeBreak = grapheme.graphemeBreak;
pub const GraphemeBreakState = grapheme.BreakState;

View File

@ -1,9 +1,26 @@
const props = @This();
const std = @import("std");
const assert = std.debug.assert;
const ziglyph = @import("ziglyph");
const Graphemes = @import("Graphemes");
const DisplayWidth = @import("DisplayWidth");
const lut = @import("lut.zig");
graphemes: Graphemes,
display_width: DisplayWidth,
fn init(alloc: std.mem.Allocator) !props {
const graphemes = try Graphemes.init(alloc);
return .{
.graphemes = graphemes,
.display_width = try DisplayWidth.initWithGraphemes(alloc, graphemes),
};
}
fn deinit(self: *props, alloc: std.mem.Allocator) void {
self.graphemes.deinit(alloc);
self.display_width.deinit(alloc);
}
/// The lookup tables for Ghostty.
pub const table = table: {
// This is only available after running main() below as part of the Ghostty
@ -79,31 +96,26 @@ pub const GraphemeBoundaryClass = enum(u4) {
/// Gets the grapheme boundary class for a codepoint. This is VERY
/// SLOW. The use case for this is only in generating lookup tables.
pub fn init(cp: u21) GraphemeBoundaryClass {
// We special-case modifier bases because we should not break
// if a modifier isn't next to a base.
if (ziglyph.emoji.isEmojiModifierBase(cp)) {
assert(ziglyph.emoji.isExtendedPictographic(cp));
return .extended_pictographic_base;
}
if (ziglyph.emoji.isEmojiModifier(cp)) return .emoji_modifier;
if (ziglyph.emoji.isExtendedPictographic(cp)) return .extended_pictographic;
if (ziglyph.grapheme_break.isL(cp)) return .L;
if (ziglyph.grapheme_break.isV(cp)) return .V;
if (ziglyph.grapheme_break.isT(cp)) return .T;
if (ziglyph.grapheme_break.isLv(cp)) return .LV;
if (ziglyph.grapheme_break.isLvt(cp)) return .LVT;
if (ziglyph.grapheme_break.isPrepend(cp)) return .prepend;
if (ziglyph.grapheme_break.isExtend(cp)) return .extend;
if (ziglyph.grapheme_break.isZwj(cp)) return .zwj;
if (ziglyph.grapheme_break.isSpacingmark(cp)) return .spacing_mark;
if (ziglyph.grapheme_break.isRegionalIndicator(cp)) return .regional_indicator;
// This is obviously not INVALID invalid, there is SOME grapheme
// boundary class for every codepoint. But we don't care about
// anything that doesn't fit into the above categories.
return .invalid;
pub fn init(ctx: props, cp: u21) GraphemeBoundaryClass {
return switch (Graphemes.gbp(ctx.graphemes, cp)) {
.Emoji_Modifier_Base => .extended_pictographic_base,
.Emoji_Modifier => .emoji_modifier,
.Extended_Pictographic => .extended_pictographic,
.L => .L,
.V => .V,
.T => .T,
.LV => .LV,
.LVT => .LVT,
.Prepend => .prepend,
.Extend => .extend,
.ZWJ => .zwj,
.SpacingMark => .spacing_mark,
.Regional_Indicator => .regional_indicator,
// This is obviously not INVALID invalid, there is SOME grapheme
// boundary class for every codepoint. But we don't care about
// anything that doesn't fit into the above categories.
.none, .Control, .CR, .LF => .invalid,
};
}
/// Returns true if this is an extended pictographic type. This
@ -120,13 +132,25 @@ pub const GraphemeBoundaryClass = enum(u4) {
}
};
pub fn get(cp: u21) Properties {
const zg_width = ziglyph.display_width.codePointWidth(cp, .half);
pub fn get(ctx: props, cp: u21) !Properties {
if (cp > 0x10FFFF) {
return .{
.width = 0,
.grapheme_boundary_class = .invalid,
};
} else {
const zg_width = DisplayWidth.codePointWidth(ctx.display_width, cp);
return .{
.width = @intCast(@min(2, @max(0, zg_width))),
.grapheme_boundary_class = .init(cp),
};
return .{
.width = @intCast(@min(2, @max(0, zg_width))),
.grapheme_boundary_class = .init(ctx, cp),
};
}
}
pub fn eql(ctx: props, a: Properties, b: Properties) bool {
_ = ctx;
return a.eql(b);
}
/// Runnable binary to generate the lookup tables and output to stdout.
@ -135,20 +159,13 @@ pub fn main() !void {
defer arena_state.deinit();
const alloc = arena_state.allocator();
var self = try init(alloc);
defer self.deinit(alloc);
const gen: lut.Generator(
Properties,
struct {
pub fn get(ctx: @This(), cp: u21) !Properties {
_ = ctx;
return props.get(cp);
}
pub fn eql(ctx: @This(), a: Properties, b: Properties) bool {
_ = ctx;
return a.eql(b);
}
},
) = .{};
props,
) = .{ .ctx = self };
const t = try gen.generate(alloc);
defer alloc.free(t.stage1);
@ -166,16 +183,19 @@ pub fn main() !void {
// This is not very fast in debug modes, so its commented by default.
// IMPORTANT: UNCOMMENT THIS WHENEVER MAKING CODEPOINTWIDTH CHANGES.
// test "tables match ziglyph" {
// const testing = std.testing;
//test "tables match zg" {
// const testing = std.testing;
//
// const min = 0xFF + 1; // start outside ascii
// for (min..std.math.maxInt(u21)) |cp| {
// const t = table.get(@intCast(cp));
// const zg = @min(2, @max(0, ziglyph.display_width.codePointWidth(@intCast(cp), .half)));
// if (t.width != zg) {
// std.log.warn("mismatch cp=U+{x} t={} zg={}", .{ cp, t, zg });
// try testing.expect(false);
// }
// }
// }
// const display_width = try DisplayWidth.init(std.testing.allocator);
// defer display_width.deinit(std.testing.allocator);
//
// const min = 0xFF + 1; // start outside ascii
// for (min..0x110000) |cp| {
// const t = table.get(@intCast(cp));
// const zg = @min(2, @max(0, DisplayWidth.codePointWidth(display_width, @intCast(cp))));
// if (t.width != zg) {
// std.log.warn("mismatch cp=U+{x} t={} zg={}", .{ cp, t, zg });
// try testing.expect(false);
// }
// }
//}