summaryrefslogtreecommitdiff
path: root/src/fonts.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/fonts.zig')
-rw-r--r--src/fonts.zig42
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