a73x

568caf21

feat(font): fontconfig monospace lookup

a73x   2026-04-08 06:19


diff --git a/build.zig b/build.zig
index 0f315c7..591d705 100644
--- a/build.zig
+++ b/build.zig
@@ -90,4 +90,29 @@ pub fn build(b: *std.Build) void {
        .root_module = vt_test_mod,
    });
    test_step.dependOn(&b.addRunArtifact(vt_tests).step);

    // font module — fontconfig lookup + freetype rasterization + glyph atlas
    const font_mod = b.createModule(.{
        .root_source_file = b.path("src/font.zig"),
        .target = target,
        .optimize = optimize,
        .link_libc = true,
    });
    font_mod.linkSystemLibrary("fontconfig", .{});
    font_mod.linkSystemLibrary("freetype2", .{});
    exe_mod.addImport("font", font_mod);

    // Test font.zig
    const font_test_mod = b.createModule(.{
        .root_source_file = b.path("src/font.zig"),
        .target = target,
        .optimize = optimize,
        .link_libc = true,
    });
    font_test_mod.linkSystemLibrary("fontconfig", .{});
    font_test_mod.linkSystemLibrary("freetype2", .{});
    const font_tests = b.addTest(.{
        .root_module = font_test_mod,
    });
    test_step.dependOn(&b.addRunArtifact(font_tests).step);
}
diff --git a/src/font.zig b/src/font.zig
new file mode 100644
index 0000000..7dcb9ca
--- /dev/null
+++ b/src/font.zig
@@ -0,0 +1,54 @@
const std = @import("std");
const c = @cImport({
    @cInclude("fontconfig/fontconfig.h");
    @cInclude("ft2build.h");
    @cInclude("freetype/freetype.h");
});

pub const FontLookup = struct {
    path: [:0]u8,
    index: c_int,

    pub fn deinit(self: *FontLookup, alloc: std.mem.Allocator) void {
        alloc.free(self.path);
    }
};

pub fn lookupMonospace(alloc: std.mem.Allocator) !FontLookup {
    if (c.FcInit() == c.FcFalse) return error.FcInitFailed;

    const pattern = c.FcPatternCreate() orelse return error.FcPatternCreate;
    defer c.FcPatternDestroy(pattern);

    _ = c.FcPatternAddString(pattern, c.FC_FAMILY, @ptrCast("monospace"));
    _ = c.FcPatternAddInteger(pattern, c.FC_WEIGHT, c.FC_WEIGHT_REGULAR);
    _ = c.FcPatternAddInteger(pattern, c.FC_SLANT, c.FC_SLANT_ROMAN);

    _ = c.FcConfigSubstitute(null, pattern, c.FcMatchPattern);
    c.FcDefaultSubstitute(pattern);

    var result: c.FcResult = undefined;
    const matched = c.FcFontMatch(null, pattern, &result) orelse return error.FcFontMatchFailed;
    defer c.FcPatternDestroy(matched);

    var file_cstr: [*c]c.FcChar8 = null;
    if (c.FcPatternGetString(matched, c.FC_FILE, 0, &file_cstr) != c.FcResultMatch) {
        return error.FcGetFileFailed;
    }

    var index: c_int = 0;
    _ = c.FcPatternGetInteger(matched, c.FC_INDEX, 0, &index);

    const slice = std.mem.span(@as([*:0]const u8, @ptrCast(file_cstr)));
    const dup = try alloc.dupeZ(u8, slice);
    return .{ .path = dup, .index = index };
}

test "lookupMonospace returns a valid font path" {
    var lookup = try lookupMonospace(std.testing.allocator);
    defer lookup.deinit(std.testing.allocator);

    // Just check the file exists
    const file = try std.fs.openFileAbsolute(lookup.path, .{});
    file.close();
}