diff options
Diffstat (limited to 'src/fonts.zig')
| -rw-r--r-- | src/fonts.zig | 42 |
1 files changed, 36 insertions, 6 deletions
diff --git a/src/fonts.zig b/src/fonts.zig index 28789e3d..e4744daf 100644 --- a/src/fonts.zig +++ b/src/fonts.zig @@ -39,12 +39,20 @@ const home_dirs = switch (builtin.os.tag) { else => [_][]const u8{ ".local/share/fonts", ".fonts" }, }; -/// The same three safety rails look.find has, for the same reason: this walk -/// runs INSIDE the keystroke that asked for it, so it must end whatever it is +/// The same safety rails look.find has, for the same reason: this walk runs +/// INSIDE the keystroke that asked for it, so it must end whatever it is /// pointed at. A font tree is shallow and wide (one directory per family), so -/// the depth cap is lower than find's and the file cap is what a picker can -/// still be read as a list. -const max_fonts = 512; +/// the depth cap is lower than find's. +/// +/// `max_fonts` is a MEMORY bound and nothing else — the work is already bounded +/// by `max_steps`, since a face has to be walked past before it can be found. +/// It used to be 512, which is not a machine-sized number: a desktop with the +/// Noto and Nerd Font sets installed has nearly a thousand monospace faces, and +/// the picker silently served the first 512 the directories happened to hand +/// back. Sorting happens AFTER the cut, so the loss did not even look like +/// truncation — the list ran A to z with half the fonts missing out of the +/// middle of it, which is a far worse way to be wrong than a short list. +const max_fonts = 4096; const max_steps = 20_000; const max_depth = 8; @@ -96,7 +104,12 @@ pub fn fallbacks(arena: std.mem.Allocator) []const Font { /// chain. `wanted == null` means every monospace face; named scans accept /// proportional symbol/general faces and preserve the requested priority. fn scan(arena: std.mem.Allocator, wanted: ?[]const []const u8) []const Font { - var found: [max_fonts]Font = undefined; + // ...from the arena, not the stack: 4096 faces of {name, path} is 128 KiB, + // which is a fine thing to hand back to an arena that resets at the end of + // the keystroke and not a thing to put on a call stack. An allocator that + // cannot spare it answers with the embedded face, like every other failure + // in here. + const found = arena.alloc(Font, max_fonts) catch return &.{}; var found_len: usize = 0; // Zig 0.16 moved the filesystem behind std.Io; the blocking // single-threaded implementation is the synchronous walk a sans-IO core @@ -210,6 +223,23 @@ test "fallback discovery is unique and preserves candidate priority" { } } +test "the picker lists every monospace face rather than the first max_fonts" { + var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena_state.deinit(); + const installed = list(arena_state.allocator(), null); + // A canary, and only meaningful on a machine that HAS fonts: reaching the + // cap means the list handed to the picker is a silent lie, sorted after + // the cut so it reads as complete while missing faces out of the middle. + // That shipped once at max_fonts = 512 on a desktop with ~1000 of them. + try std.testing.expect(installed.len < max_fonts); + // ...and what it does return is sorted, which is what makes the picker the + // same list twice running. + for (installed, 0..) |font, i| { + try std.testing.expect(font.name.len != 0); + if (i > 0) try std.testing.expect(!std.mem.lessThan(u8, font.name, installed[i - 1].name)); + } +} + /// Is every glyph in this font the same width? The terminal grid IS a /// monospace cell — one advance for every column, chosen once from 'M' — so a /// proportional font does not render badly in it, it renders as rubble: every |
