a73x

568caf21

feat(font): fontconfig monospace lookup

a73x   2026-04-08 06:19

Commit message
feat(font): fontconfig monospace lookup

build.zig
Old New
@@ -90,4 +90,29 @@ pub fn build(b: *std.Build) void {
90 .root_module = vt_test_mod, 90 .root_module = vt_test_mod,
91 }); 91 });
92 test_step.dependOn(&b.addRunArtifact(vt_tests).step); 92 test_step.dependOn(&b.addRunArtifact(vt_tests).step);
93
94 // font module — fontconfig lookup + freetype rasterization + glyph atlas
95 const font_mod = b.createModule(.{
96 .root_source_file = b.path("src/font.zig"),
97 .target = target,
98 .optimize = optimize,
99 .link_libc = true,
100 });
101 font_mod.linkSystemLibrary("fontconfig", .{});
102 font_mod.linkSystemLibrary("freetype2", .{});
103 exe_mod.addImport("font", font_mod);
104
105 // Test font.zig
106 const font_test_mod = b.createModule(.{
107 .root_source_file = b.path("src/font.zig"),
108 .target = target,
109 .optimize = optimize,
110 .link_libc = true,
111 });
112 font_test_mod.linkSystemLibrary("fontconfig", .{});
113 font_test_mod.linkSystemLibrary("freetype2", .{});
114 const font_tests = b.addTest(.{
115 .root_module = font_test_mod,
116 });
117 test_step.dependOn(&b.addRunArtifact(font_tests).step);
93 } 118 }
src/font.zig
Old New
@@ -0,0 +1,54 @@
1 const std = @import("std");
2 const c = @cImport({
3 @cInclude("fontconfig/fontconfig.h");
4 @cInclude("ft2build.h");
5 @cInclude("freetype/freetype.h");
6 });
7
8 pub const FontLookup = struct {
9 path: [:0]u8,
10 index: c_int,
11
12 pub fn deinit(self: *FontLookup, alloc: std.mem.Allocator) void {
13 alloc.free(self.path);
14 }
15 };
16
17 pub fn lookupMonospace(alloc: std.mem.Allocator) !FontLookup {
18 if (c.FcInit() == c.FcFalse) return error.FcInitFailed;
19
20 const pattern = c.FcPatternCreate() orelse return error.FcPatternCreate;
21 defer c.FcPatternDestroy(pattern);
22
23 _ = c.FcPatternAddString(pattern, c.FC_FAMILY, @ptrCast("monospace"));
24 _ = c.FcPatternAddInteger(pattern, c.FC_WEIGHT, c.FC_WEIGHT_REGULAR);
25 _ = c.FcPatternAddInteger(pattern, c.FC_SLANT, c.FC_SLANT_ROMAN);
26
27 _ = c.FcConfigSubstitute(null, pattern, c.FcMatchPattern);
28 c.FcDefaultSubstitute(pattern);
29
30 var result: c.FcResult = undefined;
31 const matched = c.FcFontMatch(null, pattern, &result) orelse return error.FcFontMatchFailed;
32 defer c.FcPatternDestroy(matched);
33
34 var file_cstr: [*c]c.FcChar8 = null;
35 if (c.FcPatternGetString(matched, c.FC_FILE, 0, &file_cstr) != c.FcResultMatch) {
36 return error.FcGetFileFailed;
37 }
38
39 var index: c_int = 0;
40 _ = c.FcPatternGetInteger(matched, c.FC_INDEX, 0, &index);
41
42 const slice = std.mem.span(@as([*:0]const u8, @ptrCast(file_cstr)));
43 const dup = try alloc.dupeZ(u8, slice);
44 return .{ .path = dup, .index = index };
45 }
46
47 test "lookupMonospace returns a valid font path" {
48 var lookup = try lookupMonospace(std.testing.allocator);
49 defer lookup.deinit(std.testing.allocator);
50
51 // Just check the file exists
52 const file = try std.fs.openFileAbsolute(lookup.path, .{});
53 file.close();
54 }