diff options
Diffstat (limited to 'src/gui/fonts.zig')
| -rw-r--r-- | src/gui/fonts.zig | 291 |
1 files changed, 0 insertions, 291 deletions
diff --git a/src/gui/fonts.zig b/src/gui/fonts.zig deleted file mode 100644 index 54210136..00000000 --- a/src/gui/fonts.zig +++ /dev/null @@ -1,291 +0,0 @@ -//! The fonts installed on the machine: the list the picker shows, the path a -//! `Font <name>` resolves to, and the one word the two sides of that say to -//! each other. builtins.zig reads this file to build the rows and to resolve a -//! name; gui.zig reads it to learn which file to load. It is the whole seam, -//! because the core has no font and the shell has no builtin dispatch. -//! -//! It lives under gui/ rather than at src/ — where the core lies flat — -//! because it only exists in a GUI build: builtins.zig imports it behind -//! `platform == .gui`, so the tty binary compiles not one line of this and -//! never opens a font directory, and the browser (which has no font -//! directories to open) is out for a better reason than taste. -//! -//! No fontconfig. Enumerating fonts on a Unix box is a walk over four -//! well-known directories, and the one thing the picker must know about each -//! face — whether every glyph has the same advance — is four small sfnt reads. -//! That avoids initializing a FreeType face for every file in the directory. -const std = @import("std"); -const libc = std.c; - -/// Where a unix box keeps fonts. The last two are relative to $HOME (a machine -/// with no $HOME simply has neither). ponytail: this is the freedesktop list -/// minus /usr/share/X11/fonts, whose legacy bitmap formats are outside this -/// TTF/OTF picker; XDG_DATA_DIRS is the general answer if another font root -/// becomes common. -const system_dirs = [_][]const u8{ "/usr/share/fonts", "/usr/local/share/fonts" }; -const home_dirs = [_][]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 -/// 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; -const max_steps = 20_000; -const max_depth = 8; - -pub const Font = struct { name: []const u8, path: []const u8 }; - -/// The font the shell should be wearing, as a PATH — written by the Font -/// builtin, taken by the shell on its next pass through the loop. Exactly the -/// shape Pardes.restore_req has, including the buffer behind it: the request -/// outlives the scratch arena the walk found the path in. -/// -/// A module var rather than a field on Pardes because the core does not have a -/// font, has no opinion about one, and on every other platform does not have -/// this file either — a field would be state the tty build carries around to -/// never touch. -pub var want_buf: [4096]u8 = undefined; -pub var want: ?[]const u8 = null; - -/// Optional fallback faces, in preference order after the always-available -/// embedded Adwaita Mono face. Keep text faces before symbol faces so ordinary -/// letters retain terminal-like metrics; DejaVu Sans is the final broad, -/// proportional safety net. -pub const fallback_names = [_][]const u8{ - "NotoSansMono-Regular", - "DejaVuSansMono", - "SymbolsNerdFont-Regular", - "NotoSansSymbols2-Regular", - "NotoSansSymbols-Regular", - "DejaVuSans", -}; - -/// Every monospace font installed, `{name, path}`, arena-owned and sorted by -/// name — or, when `want_name` is given, just the one that answers to it. -pub fn list(arena: std.mem.Allocator, want_name: ?[]const u8) []const Font { - if (want_name) |name| { - const wanted = [1][]const u8{name}; - return scan(arena, &wanted); - } - return scan(arena, null); -} - -/// Installed members of `fallback_names`, returned in preference order. This -/// is runtime discovery only: no local font path or exploration result enters -/// the build, and a machine with none simply uses embedded Adwaita Mono. -pub fn fallbacks(arena: std.mem.Allocator) []const Font { - return scan(arena, &fallback_names); -} - -/// One bounded directory walk for the picker, one named font, or the fallback -/// 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; - 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 - // wants, the same one look.find uses. - const io = std.Io.Threaded.global_single_threaded.io(); - const home: []const u8 = if (libc.getenv("HOME")) |h| std.mem.span(h) else ""; - var root_buf: [512]u8 = undefined; - var path_buf: [4096]u8 = undefined; - roots: for (0..system_dirs.len + home_dirs.len) |i| { - const root: []const u8 = if (i < system_dirs.len) - system_dirs[i] - else if (home.len == 0) - continue - else - std.fmt.bufPrint(&root_buf, "{s}/{s}", .{ std.mem.trimEnd(u8, home, "/"), home_dirs[i - system_dirs.len] }) catch continue; - var dir = std.Io.Dir.cwd().openDir(io, root, .{ .iterate = true }) catch continue; - defer dir.close(io); - // walkSelectively, not walk: descending is opt-in, which is the only - // way to express the depth cap at all (look.find, same reason) - var w = dir.walkSelectively(arena) catch continue; - defer w.deinit(); - var steps: usize = 0; - while (steps < max_steps and found_len < found.len) { - steps += 1; // an unreadable dir burns a step too, so it cannot spin - const e = (w.next(io) catch continue) orelse break; - if (e.kind == .directory) { - if (e.depth() < max_depth) w.enter(io, e) catch {}; - continue; - } - const ext = std.fs.path.extension(e.basename); - if (!std.ascii.eqlIgnoreCase(ext, ".ttf") and !std.ascii.eqlIgnoreCase(ext, ".otf")) continue; - const name = e.basename[0 .. e.basename.len - ext.len]; - if (wanted) |names| { - var matches = false; - for (names) |candidate| { - if (std.mem.eql(u8, candidate, name)) { - matches = true; - break; - } - } - if (!matches) continue; - for (found[0..found_len]) |prior| { - if (std.mem.eql(u8, prior.name, name)) { - matches = false; - break; - } - } - if (!matches) continue; - } - // e.path points into the walker's own buffer and dies at the next - // next(), so the path is spelled out here and copied below - const path = std.fmt.bufPrintSentinel(&path_buf, "{s}/{s}", .{ root, e.path }, 0) catch continue; - if (wanted == null and !monospaced(path)) continue; - found[found_len] = .{ - .name = arena.dupe(u8, name) catch break, - .path = arena.dupe(u8, path) catch break, - }; - found_len += 1; - if (wanted) |names| { - if (names.len == 1) return arena.dupe(Font, found[0..found_len]) catch &.{}; - if (found_len == names.len) break :roots; - } - } - } - if (wanted) |names| { - std.mem.sort(Font, found[0..found_len], names, struct { - fn rank(order: []const []const u8, name: []const u8) usize { - for (order, 0..) |candidate, i| - if (std.mem.eql(u8, candidate, name)) return i; - return order.len; - } - fn lt(order: []const []const u8, a: Font, b: Font) bool { - return rank(order, a.name) < rank(order, b.name); - } - }.lt); - } else { - // readdir order is undefined; sort so the picker is the same list twice - // running and n/N walks a font's own variants in a row - std.mem.sort(Font, found[0..found_len], {}, struct { - fn lt(_: void, a: Font, b: Font) bool { - return std.mem.lessThan(u8, a.name, b.name); - } - }.lt); - } - return arena.dupe(Font, found[0..found_len]) catch &.{}; -} - -test "fallback discovery is unique and preserves candidate priority" { - for (fallback_names, 0..) |name, i| { - try std.testing.expect(name.len != 0); - for (fallback_names[0..i]) |prior| - try std.testing.expect(!std.mem.eql(u8, prior, name)); - } - - var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena_state.deinit(); - const installed = fallbacks(arena_state.allocator()); - var previous: ?usize = null; - for (installed) |font| { - const rank = for (fallback_names, 0..) |name, i| { - if (std.mem.eql(u8, name, font.name)) break i; - } else return error.UnknownFallback; - if (previous) |p| try std.testing.expect(rank > p); - previous = rank; - } -} - -/// 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 -/// row a different length, every column misaligned, and the mouse pointing at -/// the wrong character. That is why the picker filters rather than listing all -/// nine hundred faces and letting you find out one step into walking them; the -/// list you get is the list you can actually wear. -/// -/// Four reads and no allocation, which is why the walk can afford it per file: -/// the sfnt header and table directory, then `hhea`'s numberOfHMetrics, then -/// the start of `hmtx` — one {advance, lsb} pair per glyph. A font whose first -/// advances all agree is monospace. Zeros are skipped: .notdef and the -/// combining marks legitimately advance nothing, in any font. -/// -/// ponytail: the first 64 metrics, not all of them, so this is one 256-byte -/// read whatever the font's size. Those cover .notdef and the whole of basic -/// latin — the range a terminal is actually worn in — which also (deliberately) -/// keeps the CJK mono faces whose *later* glyphs are double-width, exactly the -/// fonts fontconfig calls "dual-width" and refuses. -fn monospaced(path_z: [*:0]const u8) bool { - const fd = libc.open(path_z, .{ .ACCMODE = .RDONLY, .CLOEXEC = true }); - if (fd < 0) return false; - defer _ = libc.close(fd); - // 12-byte header + one 16-byte record per table; 256 records is far more - // than any real font carries - var head: [12 + 16 * 256]u8 = undefined; - const n = libc.pread(fd, &head, head.len, 0); - if (n < 12) return false; - const hhea = tableOffset(head[0..@intCast(n)], "hhea") orelse return false; - const hmtx = tableOffset(head[0..@intCast(n)], "hmtx") orelse return false; - var hh: [36]u8 = undefined; - if (libc.pread(fd, &hh, hh.len, hhea) != @as(isize, hh.len)) return false; - const metrics = std.mem.readInt(u16, hh[34..36], .big); - const k: usize = @min(@as(usize, metrics), 64); - if (k == 0) return false; - var mx: [64 * 4]u8 = undefined; - if (libc.pread(fd, &mx, k * 4, hmtx) != @as(isize, @intCast(k * 4))) return false; - var ref: u16 = 0; - for (0..k) |i| { - const adv = std.mem.readInt(u16, mx[i * 4 ..][0..2], .big); - if (adv == 0) continue; - if (ref == 0) ref = adv else if (adv != ref) return false; - } - return ref != 0; -} - -/// Where `tag`'s table starts, read out of an sfnt table directory. Called -/// twice per font, which is the only reason it is not inline up there. -fn tableOffset(head: []const u8, tag: *const [4]u8) ?u32 { - if (head.len < 12) return null; - // 0x00010000 TrueType outlines, "OTTO" CFF ones, "true" the old Apple - // spelling. Anything else — a .ttc collection, a WOFF, a lie about its - // extension — is not an sfnt face this picker can inspect. - const ver = std.mem.readInt(u32, head[0..4], .big); - if (ver != 0x00010000 and ver != 0x4F54544F and ver != 0x74727565) return null; - const num = std.mem.readInt(u16, head[4..6], .big); - var i: usize = 0; - while (i < num and 12 + (i + 1) * 16 <= head.len) : (i += 1) { - const rec = head[12 + i * 16 ..][0..16]; - if (std.mem.eql(u8, rec[0..4], tag)) return std.mem.readInt(u32, rec[8..12], .big); - } - return null; -} - -test "monospaced reads the advances out of a real sfnt layout" { - // A whole font in 92 bytes: the header, a two-record table directory, and - // an hhea + hmtx that between them say "three glyphs, all 600 units wide". - // Everything a real .ttf has that this does not (glyf, cmap, name) is - // exactly what the probe never reads, which is the property under test. - var f: [92]u8 = @splat(0); - std.mem.writeInt(u32, f[0..4], 0x00010000, .big); // sfnt version - std.mem.writeInt(u16, f[4..6], 2, .big); // numTables - @memcpy(f[12..16], "hhea"); - std.mem.writeInt(u32, f[20..24], 44, .big); // hhea at 44, 36 bytes long - @memcpy(f[28..32], "hmtx"); - std.mem.writeInt(u32, f[36..40], 80, .big); // hmtx right after it - std.mem.writeInt(u16, f[44 + 34 ..][0..2], 3, .big); // numberOfHMetrics - for (0..3) |i| std.mem.writeInt(u16, f[80 + i * 4 ..][0..2], 600, .big); - - const path = "/tmp/pardes-fonts-test.ttf"; - { - const fd = libc.open(path, .{ .ACCMODE = .WRONLY, .CREAT = true, .TRUNC = true }, @as(c_uint, 0o644)); - try std.testing.expect(fd >= 0); - defer _ = libc.close(fd); - try std.testing.expectEqual(@as(isize, f.len), libc.write(fd, &f, f.len)); - } - try std.testing.expect(monospaced(path)); - - // ...and one glyph a different width is the whole difference between a - // font this can wear and one it cannot - std.mem.writeInt(u16, f[80 + 4 ..][0..2], 1200, .big); - { - const fd = libc.open(path, .{ .ACCMODE = .WRONLY, .CREAT = true, .TRUNC = true }, @as(c_uint, 0o644)); - try std.testing.expect(fd >= 0); - defer _ = libc.close(fd); - try std.testing.expectEqual(@as(isize, f.len), libc.write(fd, &f, f.len)); - } - try std.testing.expect(!monospaced(path)); -} |
