summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--build.zig13
-rw-r--r--src/fonts.zig42
2 files changed, 49 insertions, 6 deletions
diff --git a/build.zig b/build.zig
index dea403ca..8fe2b1e3 100644
--- a/build.zig
+++ b/build.zig
@@ -1091,6 +1091,19 @@ pub fn build(b: *std.Build) void {
.link_libc = true,
}) });
unit_step.dependOn(&b.addRunArtifact(nested_test).step);
+ // fonts.zig is the same shape once more, and it needs its own module
+ // for the reason spelled out above rather than as a convention: the
+ // core imports it behind `platform == .gui or .macos`, so on this build
+ // nothing analyses it and its tests would silently not exist. That is
+ // how a picker capped at 512 faces on a machine with a thousand of them
+ // went unnoticed.
+ const fonts_test = b.addTest(.{ .root_module = b.createModule(.{
+ .target = target,
+ .optimize = optimize,
+ .root_source_file = b.path("src/fonts.zig"),
+ .link_libc = true,
+ }) });
+ unit_step.dependOn(&b.addRunArtifact(fonts_test).step);
// crt.zig is pure std — the mouse-mapping-vs-shader-formula test
const crt_test = b.addTest(.{ .root_module = b.createModule(.{
.target = target,
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