From 5feee8c0c3a9a6a4be7ba22132d028509216a098 Mon Sep 17 00:00:00 2001 From: Emil Lerch Date: Sat, 29 Jul 2023 20:10:04 -0700 Subject: [PATCH] initial source code --- Dockerfile | 14 ++ LICENSE | 21 +++ README.md | 0 build.zig | 86 ++++++++++ src/core | Bin 0 -> 380928 bytes src/fontconfig.c | 54 +++++++ src/fontconfig.zig | 284 ++++++++++++++++++++++++++++++++ src/main.zig | 391 +++++++++++++++++++++++++++++++++++++++++++++ src/ranges.txt | 209 ++++++++++++++++++++++++ src/unicode.zig | 112 +++++++++++++ zig-via-docker | 4 + 11 files changed, 1175 insertions(+) create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 README.md create mode 100644 build.zig create mode 100644 src/core create mode 100644 src/fontconfig.c create mode 100644 src/fontconfig.zig create mode 100644 src/main.zig create mode 100644 src/ranges.txt create mode 100644 src/unicode.zig create mode 100755 zig-via-docker diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7d9efed --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM debian:bullseye + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + libfontconfig-dev \ + ca-certificates \ + curl \ + xz-utils \ + && curl https://mirror.bazel.build/ziglang.org/builds/zig-linux-x86_64-0.11.0-dev.3886+0c1bfe271.tar.xz | tar -C /usr/local/ -xJ \ + && apt-get -y remove curl xz-utils \ + && ln -s /usr/local/zig*/zig /usr/local/bin \ + && rm -rf /var/lib/apt/lists/* + +ENTRYPOINT ["/usr/local/bin/zig"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a339f72 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Emil Lerch + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..3588a9a --- /dev/null +++ b/build.zig @@ -0,0 +1,86 @@ +const std = @import("std"); + +// Although this function looks imperative, note that its job is to +// declaratively construct a build graph that will be executed by an external +// runner. +pub fn build(b: *std.Build) void { + // Standard target options allows the person running `zig build` to choose + // what target to build for. Here we do not override the defaults, which + // means any target is allowed, and the default is native. Other options + // for restricting supported target set are available. + const target = b.standardTargetOptions(.{}); + + // Standard optimization options allow the person running `zig build` to select + // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not + // set a preferred release mode, allowing the user to decide how to optimize. + const optimize = b.standardOptimizeOption(.{}); + + const exe = b.addExecutable(.{ + .name = "fontfinder", + // In this case the main source file is merely a path, however, in more + // complicated build scripts, this could be a generated file. + .root_source_file = .{ .path = "src/main.zig" }, + .target = target, + .optimize = optimize, + }); + + configure(exe); + + // This declares intent for the executable to be installed into the + // standard location when the user invokes the "install" step (the default + // step when running `zig build`). + b.installArtifact(exe); + + // This *creates* a Run step in the build graph, to be executed when another + // step is evaluated that depends on it. The next line below will establish + // such a dependency. + const run_cmd = b.addRunArtifact(exe); + + // By making the run step depend on the install step, it will be run from the + // installation directory rather than directly from within the cache directory. + // This is not necessary, however, if the application depends on other installed + // files, this ensures they will be present and in the expected location. + run_cmd.step.dependOn(b.getInstallStep()); + + // This allows the user to pass arguments to the application in the build + // command itself, like this: `zig build run -- arg1 arg2 etc` + if (b.args) |args| { + run_cmd.addArgs(args); + } + + // This creates a build step. It will be visible in the `zig build --help` menu, + // and can be selected like this: `zig build run` + // This will evaluate the `run` step rather than the default, which is "install". + const run_step = b.step("run", "Run the app"); + run_step.dependOn(&run_cmd.step); + + // Creates a step for unit testing. This only builds the test executable + // but does not run it. + const unit_tests = b.addTest(.{ + .root_source_file = .{ .path = "src/main.zig" }, + .target = target, + .optimize = optimize, + }); + + configure(unit_tests); + + const run_unit_tests = b.addRunArtifact(unit_tests); + + // Similar to creating the run step earlier, this exposes a `test` step to + // the `zig build --help` menu, providing a way for the user to request + // running the unit tests. + const test_step = b.step("test", "Run unit tests"); + test_step.dependOn(&run_unit_tests.step); +} + +fn configure(object: anytype) void { + // object.linkage = .static; + object.linkLibC(); + + // Fontconfig must be installed. Docker can also be used (see Dockerfile) + object.addSystemIncludePath("/usr/include"); + object.linkSystemLibrary("fontconfig"); + // object.linkSystemLibrary("expat"); // fontconfig dependency - needed for static builds + object.addLibraryPath("/usr/lib"); + object.addCSourceFile("src/fontconfig.c", &[_][]const u8{"-std=c99"}); +} diff --git a/src/core b/src/core new file mode 100644 index 0000000000000000000000000000000000000000..b2ddecc43241e5a9df226a0a3d4dfe52c854e02e GIT binary patch literal 380928 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 literal 0 HcmV?d00001 diff --git a/src/fontconfig.c b/src/fontconfig.c new file mode 100644 index 0000000..c123852 --- /dev/null +++ b/src/fontconfig.c @@ -0,0 +1,54 @@ +#include +#include + +/* #<{(| FcChar32 FcCharSetCount (const FcCharSet *a); |)}># */ +/* void printCharacters(FcPattern* fontPattern) { */ +/* FcCharSet* charset; */ +/* if (FcPatternGetCharSet(fontPattern, FC_CHARSET, 0, &charset) == FcResultMatch) { */ +/* FcChar32 ucs4; */ +/* FcCharSetIter iter; */ +/* FcCharSetIterInit(charset, &iter); */ +/* printf("Supported characters:\n"); */ +/* while (FcCharSetIterNext(&iter, &ucs4)) { */ +/* printf("%lc ", (wint_t)ucs4); */ +/* } */ +/* printf("\n"); */ +/* FcCharSetDestroy(charset); */ +/* } */ +/* } */ + +const FcChar32 MAX_UNICODE = 0x10FFFD; + +void freeAllCharacters(unsigned int *chars) { + free(chars); +} + +int allCharacters(void* fontPattern, FcChar32 ** chars) { + FcPattern* pat = (FcPattern*) fontPattern; + FcCharSet* charset; + if (FcPatternGetCharSet(pat, FC_CHARSET, 0, &charset) != FcResultMatch) { + return -1; + } + FcChar32 count = FcCharSetCount(charset); + unsigned int* char_array = (unsigned int*)malloc(count * sizeof(unsigned int)); + *chars = char_array; + + FcChar32 ucs4 = 0; + size_t found = 0; + size_t inx = 0; + + while (found < count && inx < MAX_UNICODE) { + if (FcCharSetHasChar(charset, inx) == FcTrue) { + char_array[ucs4] = inx; + ucs4++; + found++; + } + inx++; + } + FcCharSetDestroy(charset); + if (found < count) { + freeAllCharacters(*chars); + return -2; + } + return ucs4; +} diff --git a/src/fontconfig.zig b/src/fontconfig.zig new file mode 100644 index 0000000..3cf5c62 --- /dev/null +++ b/src/fontconfig.zig @@ -0,0 +1,284 @@ +const std = @import("std"); +const unicode = @import("unicode.zig"); +const c = @cImport({ + @cInclude("fontconfig/fontconfig.h"); +}); +const log = std.log.scoped(.fontconfig); + +extern fn allCharacters(p: ?*const c.FcPattern, chars: *[*]u32) c_int; +extern fn freeAllCharacters(chars: *[*]usize) void; + +pub const RangeFont = struct { + starting_codepoint: u21, + ending_codepoint: u21, + font: Font, +}; + +pub const Font = struct { + full_name: []const u8, + family: []const u8, + style: []const u8, + supported_chars: []const u21, + + const Self = @This(); + + pub fn deinit(self: *Self) void { + freeAllCharacters(self.supported_chars.ptr); + } +}; + +pub const FontList = struct { + list: std.ArrayList(Font), + allocator: std.mem.Allocator, + pattern: *c.FcPattern, + fontset: *c.FcFontSet, + + const Self = @This(); + pub fn initCapacity(allocator: std.mem.Allocator, num: usize, pattern: *c.FcPattern, fontset: *c.FcFontSet) std.mem.Allocator.Error!Self { + var al = try std.ArrayList(Font).initCapacity(allocator, num); + return Self{ + .allocator = allocator, + .list = al, + .pattern = pattern, + .fontset = fontset, + }; + } + + pub fn deinit(self: *Self) void { + c.FcPatternDestroy(self.pattern); + c.FcFontSetDestroy(self.fontset); + self.list.deinit(); + } + + pub fn addFontAssumeCapacity( + self: *Self, + full_name: []const u8, + family: []const u8, + style: []const u8, + supported_chars: []const u21, + ) !void { + self.list.appendAssumeCapacity(.{ + .full_name = full_name, + .family = family, + .style = style, + .supported_chars = supported_chars, + }); + } +}; + +var fc_config: ?*c.FcConfig = null; +var deinited = false; +// pub var test_should_deinit = true; +/// De-initializes the underlying c library. Should only be called +/// after all processing has completed +pub fn deinit() void { + // https://refspecs.linuxfoundation.org/fontconfig-2.6.0/r2370.html + // Says that "Note that calling this function with the return from FcConfigGetCurrent will place the library in an indeterminate state." + // However, it seems as though you can't do this either: + // + // 1. c.FcInitLoadConfigAndFonts(); + // 2. c.FcConfigDestroy(); + // 3. c.FcInitLoadConfigAndFonts(); + // 4. c.FcConfigDestroy(); // Seg fault here + if (deinited) @panic("Cannot deinitialize this library more than once"); + deinited = true; + if (fc_config) |conf| { + log.debug("destroying config: do not use library or call me again", .{}); + c.FcConfigDestroy(conf); + } + fc_config = null; +} + +pub const FontQuery = struct { + allocator: std.mem.Allocator, + // fc_config: ?*c.FcConfig = null, + + const Self = @This(); + + pub fn init(allocator: std.mem.Allocator) Self { + return Self{ + .allocator = allocator, + }; + } + pub fn deinit(self: *Self) void { + _ = self; + // if (self.all_fonts) |a| a.deinit(); + } + + pub fn fontList(self: *Self, pattern: [:0]const u8) !FontList { + if (fc_config == null and deinited) @panic("fontconfig C library is in an inconsistent state - should not use"); + if (fc_config == null) fc_config = c.FcInitLoadConfigAndFonts(); + const config = if (fc_config) |conf| conf else return error.FontConfigInitLoadFailure; + + // Pretty sure we want this... + const pat = c.FcNameParse(pattern); + // We cannot destroy the pattern until we're completely done + // This will be managed by FontList object + // defer if (pat != null) c.FcPatternDestroy(pat); + + // const pat = c.FcPatternCreate(); // *FcPattern + // defer if (pat != null) c.FcPatternDestroy(pat); + // + // // FC_WEIGHT_NORMAL is 80 + // // This is equivalent to "regular" style + // if (c.FcPatternAddInteger(pat, c.FC_WEIGHT, c.FC_WEIGHT_NORMAL) != c.FcTrue) return error.FontConfigCouldNotSetPattern; + // + // // This is "normal" vs Bold or Italic + // if (c.FcPatternAddInteger(pat, c.FC_WIDTH, c.FC_WIDTH_NORMAL) != c.FcTrue) return error.FontConfigCouldNotSetPattern; + // + // // Monospaced fonts + // if (c.FcPatternAddInteger(pat, c.FC_SPACING, c.FC_MONO) != c.FcTrue) return error.FontConfigCouldNotSetPattern; + // + // // FC_SLANT_ROMAN is 0 (italic 100, oblique 110) + // if (c.FcPatternAddInteger(pat, c.FC_SLANT, c.FC_SLANT_ROMAN) != c.FcTrue) return error.FontConfigCouldNotSetPattern; + // + const os = c.FcObjectSetBuild(c.FC_FAMILY, c.FC_STYLE, c.FC_LANG, c.FC_FULLNAME, c.FC_CHARSET, @as(?*u8, null)); // *FcObjectSet + defer if (os != null) c.FcObjectSetDestroy(os); + const fs = c.FcFontList(config, pat, os); // FcFontSet + // TODO: Move this defer into deinit + // defer if (fs != null) c.FcFontSetDestroy(fs); + + // Use the following only when needed. NameUnparse allocates memory + // log.debug("Total matching fonts: {d}. Pattern: {s}\n", .{ fs.*.nfont, c.FcNameUnparse(pat) }); + log.debug("Total matching fonts: {d}", .{fs.*.nfont}); + var rc = try FontList.initCapacity(self.allocator, @as(usize, @intCast(fs.*.nfont)), pat.?, fs.?); + errdefer rc.deinit(); + for (0..@as(usize, @intCast(fs.*.nfont))) |i| { + const font = fs.*.fonts[i].?; // *FcPattern + var fullname: [*:0]c.FcChar8 = undefined; + var style: [*:0]c.FcChar8 = undefined; + var family: [*:0]c.FcChar8 = undefined; + + var charset: [*]u21 = undefined; + const len = allCharacters(font, @ptrCast(&charset)); + if (len < 0) return error.FontConfigCouldNotGetCharSet; + + // https://refspecs.linuxfoundation.org/fontconfig-2.6.0/r600.html + // Note that these (like FcPatternGet) do not make a copy of any data structure referenced by the return value + // https://refspecs.linuxfoundation.org/fontconfig-2.6.0/r570.html + // The value returned is not a copy, but rather refers to the data stored within the pattern directly. Applications must not free this value. + if (c.FcPatternGetString(font, c.FC_FULLNAME, 0, @as([*c][*c]c.FcChar8, @ptrCast(&fullname))) != c.FcResultMatch) + fullname = @constCast(@ptrCast("".ptr)); + // return error.FontConfigCouldNotGetFontFullName; + + if (c.FcPatternGetString(font, c.FC_FAMILY, 0, @as([*c][*c]c.FcChar8, @ptrCast(&family))) != c.FcResultMatch) + return error.FontConfigHasNoFamily; + if (c.FcPatternGetString(font, c.FC_STYLE, 0, @as([*c][*c]c.FcChar8, @ptrCast(&style))) != c.FcResultMatch) + return error.FontConfigHasNoStyle; + + log.debug( + "Chars: {d:5.0} Family '{s}' Style '{s}' Full Name: {s}", + .{ @as(usize, @intCast(len)), family, style, fullname }, + ); + + try rc.addFontAssumeCapacity( + fullname[0..std.mem.len(fullname)], + family[0..std.mem.len(family)], + style[0..std.mem.len(style)], + charset[0..@as(usize, @intCast(len))], + ); + } + return rc; + } + + pub fn fontsForRange( + self: *Self, + starting_codepoint: u21, + ending_codepoint: u21, + fonts: []const Font, + exclude_previous: bool, + ) ![]RangeFont { + // const group_len = group.ending_codepoint - group.starting_codepoint; + var rc = std.ArrayList(RangeFont).init(self.allocator); + defer rc.deinit(); + + var previously_supported = blk: { + if (!exclude_previous) break :blk null; + var al = try std.ArrayList(bool).initCapacity(self.allocator, ending_codepoint - starting_codepoint); + defer al.deinit(); + for (starting_codepoint..ending_codepoint) |_| + al.appendAssumeCapacity(false); + break :blk try al.toOwnedSlice(); + }; + defer if (previously_supported) |p| self.allocator.free(p); + + for (fonts) |font| { + var current_start = @as(u21, 0); + var current_end = @as(u21, 0); + var inx = @as(usize, 0); + + var range_count = @as(usize, 0); + // Advance to the start of the range + while (inx < font.supported_chars.len and + font.supported_chars[inx] < starting_codepoint) + inx += 1; + + while (inx < font.supported_chars.len and + font.supported_chars[inx] < ending_codepoint) + { + if (previously_supported) |p| { + if (p[font.supported_chars[inx]]) { + inx += 1; + continue; // This was already supported - continue + } + } + // We found the beginning of a range + current_start = font.supported_chars[inx]; + current_end = font.supported_chars[inx]; + if (previously_supported) |p| + p[font.supported_chars[inx]] = true; + + // Advance to the next supported character, then start checking for continuous ranges + inx += 1; + while (inx < font.supported_chars.len and + font.supported_chars[inx] == current_end + 1 and + font.supported_chars[inx] <= ending_codepoint and + (!exclude_previous or !previously_supported.?[font.supported_chars[inx]])) + { + if (previously_supported) |p| + p[font.supported_chars[inx]] = true; + inx += 1; + current_end += 1; + } + + // We've found the end of the range (which could be the end of a group) + // If we have not hit the stops, inx at this point is at the beginning of + // a new range + range_count += 1; + try rc.append(.{ + .font = font, + .starting_codepoint = current_start, + .ending_codepoint = current_end, + }); + } + } + return rc.toOwnedSlice(); + } +}; + +test { + std.testing.refAllDecls(@This()); // Only catches public decls +} +test "Get fonts" { + // std.testing.log_level = .debug; + log.debug("get fonts", .{}); + var fq = FontQuery.init(std.testing.allocator); + defer fq.deinit(); + var fl = try fq.fontList(":regular:normal:spacing=100:slant=0"); + defer fl.deinit(); + try std.testing.expect(fl.list.items.len > 0); + var matched = blk: { + for (fl.list.items) |item| { + log.debug("full_name: '{s}'", .{item.full_name}); + if (std.mem.eql(u8, "DejaVu Sans Mono", item.full_name)) + break :blk item; + } + break :blk null; + }; + try std.testing.expect(matched != null); + try std.testing.expectEqual(@as(usize, 3322), matched.?.supported_chars.len); +} +test { + // if (test_should_deinit) deinit(); + deinit(); +} diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..92e528b --- /dev/null +++ b/src/main.zig @@ -0,0 +1,391 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const unicode = @import("unicode.zig"); +const fontconfig = @import("fontconfig.zig"); + +const max_unicode: u21 = 0x10FFFD; +const all_chars = blk: { + var all: [max_unicode + 1]u21 = undefined; + @setEvalBranchQuota(max_unicode); + for (0..max_unicode) |i| + all[i] = i; + break :blk all; +}; +pub fn main() !u8 { + // TODO: Add back in + // defer fontconfig.deinit(); + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + // stdout is for the actual output of your application, for example if you + // are implementing gzip, then only the compressed bytes should be sent to + // stdout, not any debugging messages. + const stdout_file = std.io.getStdOut().writer(); + var bw = std.io.bufferedWriter(stdout_file); + defer bw.flush() catch @panic("could not flush stdout"); // don't forget to flush! + const stdout = bw.writer(); + + // std.os.argv is os specific + var arg_iterator = std.process.args(); + const arg0 = arg_iterator.next().?; + const options = parseCommandLine(&arg_iterator) catch |err| { + if (err == error.UserRequestedHelp) { + try usage(stdout, arg0); + return 0; + } + try usage(std.io.getStdErr().writer(), arg0); + return 2; + }; + + var unicode_ranges = unicode.all_ranges(); + if (options.list_groups) { + defer unicode_ranges.reset(); + while (unicode_ranges.next()) |range| { + try stdout.print("{s}", .{range.name}); + for (range.name.len..unicode_ranges.longest_name_len + 2) |_| + try stdout.writeByte(' '); + try stdout.print("U+{X} - U+{X}\n", .{ range.starting_codepoint, range.ending_codepoint }); + } + return 0; + } + if (options.list_fonts) { + var fq = fontconfig.FontQuery.init(allocator); + defer fq.deinit(); + var fl = try fq.fontList(options.pattern); + var longest_family_name = @as(usize, 0); + var longest_style_name = @as(usize, 0); + for (fl.list.items) |f| { + longest_family_name = @max(f.family.len, longest_family_name); + longest_style_name = @max(f.style.len, longest_style_name); + } + + std.sort.insertion(fontconfig.Font, fl.list.items, {}, cmpFont); + for (fl.list.items) |f| { + try stdout.print("Family: {s}", .{f.family}); + for (f.family.len..longest_family_name + 1) |_| + try stdout.writeByte(' '); + try stdout.print("Chars: {d:5}\tStyle: {s}", .{ f.supported_chars.len, f.style }); + for (f.style.len..longest_style_name + 1) |_| + try stdout.writeByte(' '); + try stdout.print("\tName: {s}\n", .{ + f.full_name, + }); + } + return 0; + } + const exclude_previous = options.fonts != null; + const fonts: []fontconfig.Font = blk: { + if (options.fonts == null) break :blk &[_]fontconfig.Font{}; + const fo = options.fonts.?; + var si = std.mem.splitScalar(u8, fo, ','); + var fq = fontconfig.FontQuery.init(allocator); + defer fq.deinit(); + var fl = try fq.fontList(options.pattern); + // This messes with data after, and we don't need to deinit anyway + // defer fl.deinit(); + var al = try std.ArrayList(fontconfig.Font).initCapacity(allocator, std.mem.count(u8, fo, ",") + 2); + defer al.deinit(); + while (si.next()) |font_str| { + const font = font_blk: { + for (fl.list.items) |f| + if (std.ascii.eqlIgnoreCase(f.family, font_str)) + break :font_blk f; + try std.io.getStdErr().writer().print("Error: Font '{s}' not installed", .{font_str}); + return 255; + }; + + al.appendAssumeCapacity(font); + } + al.appendAssumeCapacity(.{ + .full_name = "Unsupported", + .family = "Unsupported by any preferred font", + .style = "Regular", + .supported_chars = &all_chars, + }); + break :blk try al.toOwnedSlice(); + }; + + const order_by_range = if (std.ascii.eqlIgnoreCase("font", options.order)) + false + else if (std.ascii.eqlIgnoreCase("range", options.order)) + true + else + null; + if (order_by_range == null) { + try std.io.getStdErr().writer().print("Error: Order type '{s}' invalid", .{options.order}); + return 255; + } + std.log.debug("{0} prefered fonts:", .{fonts.len - 1}); + for (fonts[0 .. fonts.len - 1]) |f| + std.log.debug("\t{s}", .{f.family}); + if (options.groups) |group| { + while (unicode_ranges.next()) |range| { + var it = std.mem.splitScalar(u8, group, ','); + while (it.next()) |desired_group| { + if (std.mem.eql(u8, range.name, desired_group)) { + try outputRange( + allocator, + range.starting_codepoint, + range.ending_codepoint, + fonts, + exclude_previous, + order_by_range.?, + stdout, + ); + } + } + } + } else { + try outputRange( + allocator, + 0, + max_unicode, + fonts, + exclude_previous, + order_by_range.?, + stdout, + ); + } + + return 0; +} +fn cmpFont(context: void, a: fontconfig.Font, b: fontconfig.Font) bool { + _ = context; + return std.mem.order(u8, a.family, b.family) == .lt; // a.family < b.family; +} +fn cmpRangeList(context: void, a: fontconfig.RangeFont, b: fontconfig.RangeFont) bool { + _ = context; + return a.starting_codepoint < b.starting_codepoint; +} +fn formatRangeFontEndingCodepoint( + data: fontconfig.RangeFont, + comptime fmt: []const u8, + options: std.fmt.FormatOptions, + writer: anytype, +) !void { + _ = options; + if (data.starting_codepoint == data.ending_codepoint) return; + try std.fmt.format(writer, "-{" ++ fmt ++ "}", .{data.ending_codepoint}); +} +fn fmtRangeFontEndingCodepoint(range_font: fontconfig.RangeFont) std.fmt.Formatter(formatRangeFontEndingCodepoint) { + return .{ + .data = range_font, + }; +} +fn outputRange( + allocator: std.mem.Allocator, + starting_codepoint: u21, + ending_codepoint: u21, + fonts: []const fontconfig.Font, + exclude_previous: bool, + order_by_range: bool, + writer: anytype, +) !void { + var fq = fontconfig.FontQuery.init(allocator); + defer fq.deinit(); + var range_fonts = try fq.fontsForRange(starting_codepoint, ending_codepoint, fonts, exclude_previous); // do we want hard limits around this? + defer allocator.free(range_fonts); + + std.log.debug("Got {d} range fonts back from query", .{range_fonts.len}); + if (order_by_range) + std.sort.insertion(fontconfig.RangeFont, range_fonts, {}, cmpRangeList); + + for (range_fonts) |range_font| { + try writer.print("{s}U+{x}{x}={s}\n", .{ + if (std.mem.eql(u8, range_font.font.full_name, "Unsupported")) "#" else "", + range_font.starting_codepoint, + fmtRangeFontEndingCodepoint(range_font), //.ending_codepoint, + range_font.font.family, + }); + } +} + +const Options = struct { + end_of_options_signifier: ?usize = null, + groups: ?[]const u8 = null, + fonts: ?[]const u8 = &[_]u8{}, + list_groups: bool = false, + list_fonts: bool = false, + pattern: [:0]const u8 = ":regular:normal:spacing=100:slant=0", + order: [:0]const u8 = "font", +}; + +fn usage(writer: anytype, arg0: []const u8) !void { + try writer.print( + \\usage: {s} [OPTION]... + \\ + \\Options: + \\ -p, --pattern font pattern to use (Default: :regular:normal:spacing=100:slant=0) + \\ -g, --groups group names to process, comma delimited (e.g. Thai,Lao - default is all groups) + \\ -f, --fonts prefered fonts in order, comma delimited (e.g. "DejaVu Sans Mono,Hack Nerd Font" - default is all fonts) + \\ note this will change the behavior such that ranges supported by the first font found will not + \\ be considered for use by subsequent fonts + \\ -o, --order order by (Default: font, can also order by range) + \\ -G, --list-groups list all groups and exit + \\ -F, --list-fonts list all fonts matching pattern and exit + \\ -h, --help display this help text and exit + \\ + , .{arg0}); +} + +fn parseCommandLine(arg_iterator: anytype) !Options { + var current_arg: usize = 0; + var rc = Options{}; + while (arg_iterator.next()) |arg| { + if (std.mem.eql(u8, arg, "--")) { + rc.end_of_options_signifier = current_arg + 1; + return rc; + } + if (try getArgValue(arg_iterator, arg, "groups", "g", .{})) |val| { + rc.groups = val; + } else if (try getArgValue(arg_iterator, arg, "pattern", "p", .{})) |val| { + rc.pattern = val; + } else if (try getArgValue(arg_iterator, arg, "fonts", "f", .{})) |val| { + rc.fonts = val; + } else if (try getArgValue(arg_iterator, arg, "order", "o", .{})) |val| { + rc.order = val; + } else if (try getArgValue(arg_iterator, arg, "list-groups", "G", .{ .is_bool = true })) |_| { + rc.list_groups = true; + } else if (try getArgValue(arg_iterator, arg, "list-fonts", "F", .{ .is_bool = true })) |_| { + rc.list_fonts = true; + } else if (try getArgValue(arg_iterator, arg, "help", "h", .{ .is_bool = true })) |_| { + return error.UserRequestedHelp; + } else { + if (!builtin.is_test) + try std.io.getStdErr().writer().print("invalid option: {s}\n\n", .{arg}); + return error.InvalidOption; + } + current_arg += 1; + } + return rc; +} +const ArgOptions = struct { + is_bool: bool = false, + is_required: bool = false, +}; +fn getArgValue( + arg_iterator: anytype, + arg: [:0]const u8, + comptime name: ?[]const u8, + comptime short_name: ?[]const u8, + arg_options: ArgOptions, +) !?[:0]const u8 { + if (short_name) |s| { + if (std.mem.eql(u8, "-" ++ s, arg)) { + if (arg_options.is_bool) return arg; + if (arg_iterator.next()) |val| { + return val; + } else return error.NoValueOnFlag; + } + } + if (name) |n| { + if (std.mem.eql(u8, "--" ++ n, arg)) { + if (arg_options.is_bool) return ""; + if (arg_iterator.next()) |val| { + return val; + } else return error.NoValueOnName; + } + if (std.mem.startsWith(u8, arg, "--" ++ n ++ "=")) { + if (arg_options.is_bool) return error.EqualsInvalidForBooleanArgument; + return arg[("--" ++ n ++ "=").len.. :0]; + } + } + return null; +} + +// Tests run in this order: +// +// 1. Main file +// - In order, from top to bottom +// 2. Referenced file(s), if any +// - In order, from top to bottom +// +// libfontconfig gets inconsistent in a hurry with a lot of init/deinit, so +// we only want to deinit once. Because we have no way of saying "go do other +// tests, then come back", we have no way of controlling deinitialization other +// than something that's not super obvious. So, we're adding this comment. +// We will allow fontconfig tests to do our deinit() call, and we shall ignore +// deinitialization here +test "startup" { + // std.testing.log_level = .debug; +} +test "command line parses with short name" { + var it = try std.process.ArgIteratorGeneral(.{}).init(std.testing.allocator, "-g Latin-1"); + defer it.deinit(); + const options = try parseCommandLine(&it); + try std.testing.expectEqualStrings("Latin-1", options.groups.?); +} +test "command line parses with long name no equals" { + var it = try std.process.ArgIteratorGeneral(.{}).init(std.testing.allocator, "--groups Latin-1"); + defer it.deinit(); + const options = try parseCommandLine(&it); + try std.testing.expectEqualStrings("Latin-1", options.groups.?); +} +test "command line parses with long name equals" { + var log_level = std.testing.log_level; + defer std.testing.log_level = log_level; + std.testing.log_level = .debug; + var it = try std.process.ArgIteratorGeneral(.{}).init(std.testing.allocator, "--groups=Latin-1"); + defer it.deinit(); + const options = try parseCommandLine(&it); + try std.testing.expectEqualStrings("Latin-1", options.groups.?); +} +test "Get ranges" { + std.log.debug("get ranges", .{}); + // defer fontconfig.deinit(); + var fq = fontconfig.FontQuery.init(std.testing.allocator); + defer fq.deinit(); + var fl = try fq.fontList(":regular:normal:spacing=100:slant=0"); + defer fl.deinit(); + try std.testing.expect(fl.list.items.len > 0); + var matched = blk: { + for (fl.list.items) |item| { + std.log.debug("full_name: '{s}'", .{item.full_name}); + if (std.mem.eql(u8, "DejaVu Sans Mono", item.full_name)) + break :blk item; + } + break :blk null; + }; + try std.testing.expect(matched != null); + const arr: []const fontconfig.Font = &[_]fontconfig.Font{matched.?}; + var al = std.ArrayList(u8).init(std.testing.allocator); + defer al.deinit(); + const range_name = "Basic Latin"; + var matched_range = try blk: { + var unicode_ranges = unicode.all_ranges(); + while (unicode_ranges.next()) |range| { + var it = std.mem.splitScalar(u8, range_name, ','); + while (it.next()) |desired_range| { + if (std.mem.eql(u8, range.name, desired_range)) { + break :blk range; + } + } + } + break :blk error.RangeNotFound; + }; + var log_level = std.testing.log_level; + std.testing.log_level = .debug; + defer std.testing.log_level = log_level; + try outputRange(std.testing.allocator, matched_range.starting_codepoint, matched_range.ending_codepoint, arr, false, al.writer()); + try std.testing.expectEqualStrings(al.items, "U+20-7e=DejaVu Sans Mono\n"); + + std.log.debug("\nwhole unicode space:", .{}); + try outputRange(std.testing.allocator, 0, max_unicode, arr, false, al.writer()); + const expected = + \\U+20-7e=DejaVu Sans Mono + \\U+20-7e=DejaVu Sans Mono + \\U+a0-1c3=DejaVu Sans Mono + \\U+1cd-1e3=DejaVu Sans Mono + \\U+1e6-1f0=DejaVu Sans Mono + \\U+1f4-1f6=DejaVu Sans Mono + ; + try std.testing.expectStringStartsWith(al.items, expected); + + // try std.testing.expectEqual(@as(usize, 3322), matched.?.supported_chars.len); +} + +test "teardown, followed by libraries" { + std.testing.refAllDecls(@This()); // Only catches public decls + _ = @import("unicode.zig"); +} diff --git a/src/ranges.txt b/src/ranges.txt new file mode 100644 index 0000000..61e828e --- /dev/null +++ b/src/ranges.txt @@ -0,0 +1,209 @@ +Basic Latin U+0 - U+7F +Latin-1 Supplement U+80 - U+FF +Latin Extended-A U+100 - U+17F +Latin Extended-B U+180 - U+24F +IPA Extensions U+250 - U+2AF +Spacing Modifier Letters U+2B0 - U+2FF +Combining Diacritical Marks U+300 - U+36F +Greek and Coptic U+370 - U+3FF +Cyrillic U+400 - U+4FF +Cyrillic Supplement U+500 - U+527 +Armenian U+531 - U+58A +Hebrew U+591 - U+5F4 +Arabic U+600 - U+6FF +Syriac U+700 - U+74F +Arabic Supplement U+750 - U+77F +Thaana U+780 - U+7B1 +NKo U+7C0 - U+7FA +Samaritan U+800 - U+83E +Mandaic U+840 - U+85E +Devanagari U+900 - U+97F +Bengali U+981 - U+9FB +Gurmukhi U+A01 - U+A75 +Gujarati U+A81 - U+AF1 +Oriya U+B01 - U+B77 +Tamil U+B82 - U+BFA +Telugu U+C01 - U+C7F +Kannada U+C82 - U+CF2 +Malayalam U+D02 - U+D7F +Sinhala U+D82 - U+DF4 +Thai U+E01 - U+E5B +Lao U+E81 - U+EDD +Tibetan U+F00 - U+FDA +Myanmar U+1000 - U+109F +Georgian U+10A0 - U+10FC +Hangul Jamo U+1100 - U+11FF +Ethiopic U+1200 - U+137C +Ethiopic Supplement U+1380 - U+1399 +Cherokee U+13A0 - U+13F4 +Unified Canadian Aboriginal Syllabics U+1400 - U+167F +Ogham U+1680 - U+169C +Runic U+16A0 - U+16F0 +Tagalog U+1700 - U+1714 +Hanunoo U+1720 - U+1736 +Buhid U+1740 - U+1753 +Tagbanwa U+1760 - U+1773 +Khmer U+1780 - U+17F9 +Mongolian U+1800 - U+18AA +Unified Canadian Aboriginal Syllabics Extended U+18B0 - U+18F5 +Limbu U+1900 - U+194F +Tai Le U+1950 - U+1974 +New Tai Lue U+1980 - U+19DF +Khmer Symbols U+19E0 - U+19FF +Buginese U+1A00 - U+1A1F +Tai Tham U+1A20 - U+1AAD +Balinese U+1B00 - U+1B7C +Sundanese U+1B80 - U+1BB9 +Batak U+1BC0 - U+1BFF +Lepcha U+1C00 - U+1C4F +Ol Chiki U+1C50 - U+1C7F +Vedic Extensions U+1CD0 - U+1CF2 +Phonetic Extensions U+1D00 - U+1D7F +Phonetic Extensions Supplement U+1D80 - U+1DBF +Combining Diacritical Marks Supplement U+1DC0 - U+1DFF +Latin Extended Additional U+1E00 - U+1EFF +Greek Extended U+1F00 - U+1FFE +General Punctuation U+2000 - U+206F +Superscripts and Subscripts U+2070 - U+209C +Currency Symbols U+20A0 - U+20B9 +Combining Diacritical Marks for Symbols U+20D0 - U+20F0 +Letterlike Symbols U+2100 - U+214F +Number Forms U+2150 - U+2189 +Arrows U+2190 - U+21FF +Mathematical Operators U+2200 - U+22FF +Miscellaneous Technical U+2300 - U+23F3 +Control Pictures U+2400 - U+2426 +Optical Character Recognition U+2440 - U+244A +Enclosed Alphanumerics U+2460 - U+24FF +Box Drawing U+2500 - U+257F +Block Elements U+2580 - U+259F +Geometric Shapes U+25A0 - U+25FF +Miscellaneous Symbols U+2600 - U+26FF +Dingbats U+2701 - U+27BF +Miscellaneous Mathematical Symbols-A U+27C0 - U+27EF +Supplemental Arrows-A U+27F0 - U+27FF +Braille Patterns U+2800 - U+28FF +Supplemental Arrows-B U+2900 - U+297F +Miscellaneous Mathematical Symbols-B U+2980 - U+29FF +Supplemental Mathematical Operators U+2A00 - U+2AFF +Miscellaneous Symbols and Arrows U+2B00 - U+2B59 +Glagolitic U+2C00 - U+2C5E +Latin Extended-C U+2C60 - U+2C7F +Coptic U+2C80 - U+2CFF +Georgian Supplement U+2D00 - U+2D25 +Tifinagh U+2D30 - U+2D7F +Ethiopic Extended U+2D80 - U+2DDE +Cyrillic Extended-A U+2DE0 - U+2DFF +Supplemental Punctuation U+2E00 - U+2E31 +CJK Radicals Supplement U+2E80 - U+2EF3 +Kangxi Radicals U+2F00 - U+2FD5 +Ideographic Description Characters U+2FF0 - U+2FFB +CJK Symbols and Punctuation U+3000 - U+303F +Hiragana U+3041 - U+309F +Katakana U+30A0 - U+30FF +Bopomofo U+3105 - U+312D +Hangul Compatibility Jamo U+3131 - U+318E +Kanbun U+3190 - U+319F +Bopomofo Extended U+31A0 - U+31BA +CJK Strokes U+31C0 - U+31E3 +Katakana Phonetic Extensions U+31F0 - U+31FF +Enclosed CJK Letters and Months U+3200 - U+32FE +CJK Compatibility U+3300 - U+33FF +CJK Unified Ideographs Extension A U+3400 - U+4DB5 +Yijing Hexagram Symbols U+4DC0 - U+4DFF +CJK Unified Ideographs U+4E00 - U+9FCB +Yi Syllables U+A000 - U+A48C +Yi Radicals U+A490 - U+A4C6 +Lisu U+A4D0 - U+A4FF +Vai U+A500 - U+A62B +Cyrillic Extended-B U+A640 - U+A697 +Bamum U+A6A0 - U+A6F7 +Modifier Tone Letters U+A700 - U+A71F +Latin Extended-D U+A720 - U+A7FF +Syloti Nagri U+A800 - U+A82B +Common Indic Number Forms U+A830 - U+A839 +Phags-pa U+A840 - U+A877 +Saurashtra U+A880 - U+A8D9 +Devanagari Extended U+A8E0 - U+A8FB +Kayah Li U+A900 - U+A92F +Rejang U+A930 - U+A95F +Hangul Jamo Extended-A U+A960 - U+A97C +Javanese U+A980 - U+A9DF +Cham U+AA00 - U+AA5F +Myanmar Extended-A U+AA60 - U+AA7B +Tai Viet U+AA80 - U+AADF +Ethiopic Extended-A U+AB01 - U+AB2E +Meetei Mayek U+ABC0 - U+ABF9 +Hangul Syllables U+AC00 - U+D7A3 +Hangul Jamo Extended-B U+D7B0 - U+D7FB +High Surrogates U+D800 - U+DB7F +High Private Use Surrogates U+DB80 - U+DBFF +Low Surrogates U+DC00 - U+DFFF +Private Use Area U+E000 - U+F8FF +CJK Compatibility Ideographs U+F900 - U+FAD9 +Alphabetic Presentation Forms U+FB00 - U+FB4F +Arabic Presentation Forms-A U+FB50 - U+FDFD +Variation Selectors U+FE00 - U+FE0F +Vertical Forms U+FE10 - U+FE19 +Combining Half Marks U+FE20 - U+FE26 +CJK Compatibility Forms U+FE30 - U+FE4F +Small Form Variants U+FE50 - U+FE6B +Arabic Presentation Forms-B U+FE70 - U+FEFF +Halfwidth and Fullwidth Forms U+FF01 - U+FFEE +Specials U+FFF9 - U+FFFD +Linear B Syllabary U+10000 - U+1005D +Linear B Ideograms U+10080 - U+100FA +Aegean Numbers U+10100 - U+1013F +Ancient Greek Numbers U+10140 - U+1018A +Ancient Symbols U+10190 - U+1019B +Phaistos Disc U+101D0 - U+101FD +Lycian U+10280 - U+1029C +Carian U+102A0 - U+102D0 +Old Italic U+10300 - U+10323 +Gothic U+10330 - U+1034A +Ugaritic U+10380 - U+1039F +Old Persian U+103A0 - U+103D5 +Deseret U+10400 - U+1044F +Shavian U+10450 - U+1047F +Osmanya U+10480 - U+104A9 +Cypriot Syllabary U+10800 - U+1083F +Imperial Aramaic U+10840 - U+1085F +Phoenician U+10900 - U+1091F +Lydian U+10920 - U+1093F +Kharoshthi U+10A00 - U+10A58 +Old South Arabian U+10A60 - U+10A7F +Avestan U+10B00 - U+10B3F +Inscriptional Parthian U+10B40 - U+10B5F +Inscriptional Pahlavi U+10B60 - U+10B7F +Old Turkic U+10C00 - U+10C48 +Rumi Numeral Symbols U+10E60 - U+10E7E +Brahmi U+11000 - U+1106F +Kaithi U+11080 - U+110C1 +Cuneiform U+12000 - U+1236E +Cuneiform Numbers and Punctuation U+12400 - U+12473 +Egyptian Hieroglyphs U+13000 - U+1342E +Bamum Supplement U+16800 - U+16A38 +Kana Supplement U+1B000 - U+1B001 +Byzantine Musical Symbols U+1D000 - U+1D0F5 +Musical Symbols U+1D100 - U+1D1DD +Ancient Greek Musical Notation U+1D200 - U+1D245 +Tai Xuan Jing Symbols U+1D300 - U+1D356 +Counting Rod Numerals U+1D360 - U+1D371 +Mathematical Alphanumeric Symbols U+1D400 - U+1D7FF +Mahjong Tiles U+1F000 - U+1F02B +Domino Tiles U+1F030 - U+1F093 +Playing Cards U+1F0A0 - U+1F0DF +Enclosed Alphanumeric Supplement U+1F100 - U+1F1FF +Enclosed Ideographic Supplement U+1F200 - U+1F251 +Miscellaneous Symbols And Pictographs U+1F300 - U+1F5FF +Emoticons U+1F601 - U+1F64F +Transport And Map Symbols U+1F680 - U+1F6C5 +Alchemical Symbols U+1F700 - U+1F773 +CJK Unified Ideographs Extension B U+20000 - U+2A6D6 +CJK Unified Ideographs Extension C U+2A700 - U+2B734 +CJK Unified Ideographs Extension D U+2B740 - U+2B81D +CJK Compatibility Ideographs Supplement U+2F800 - U+2FA1D +Tags U+E0001 - U+E007F +Variation Selectors Supplement U+E0100 - U+E01EF +Supplementary Private Use Area-A U+F0000 - U+FFFFD +Supplementary Private Use Area-B U+100000 - U+10FFFD diff --git a/src/unicode.zig b/src/unicode.zig new file mode 100644 index 0000000..98efa51 --- /dev/null +++ b/src/unicode.zig @@ -0,0 +1,112 @@ +const std = @import("std"); + +// Pulled from: https://www.unicodepedia.com/groups/ +const ranges = @embedFile("ranges.txt"); +const eval_branch_quota_base = 18500; +const range_count = blk: { + // This should be related to the number of characters in our embedded file above + @setEvalBranchQuota(eval_branch_quota_base); + break :blk std.mem.count(u8, ranges, "\n"); +}; +const Ranges = struct { + names: [range_count][]const u8 = undefined, + starting_codepoints: [range_count]u21 = undefined, + ending_codepoints: [range_count]u21 = undefined, + current_inx: usize = 0, + longest_name_len: usize = 0, + + const Self = @This(); + + pub fn first(self: *Self) ?UnicodeGroup { + self.reset(); + return self.next(); + } + pub fn reset(self: *Self) void { + self.current_inx = 0; + } + pub fn next(self: *Self) ?UnicodeGroup { + if (self.current_inx == range_count) return null; + self.current_inx += 1; + return self.item(self.current_inx - 1); + } + pub fn item(self: Self, index: usize) UnicodeGroup { + return .{ + .name = self.names[index], + .starting_codepoint = self.starting_codepoints[index], + .ending_codepoint = self.ending_codepoints[index], + }; + } +}; + +const _all_ranges = blk: { + @setEvalBranchQuota(eval_branch_quota_base * 2); + break :blk parseRanges(ranges) catch @compileError("Could not parse ranges.txt"); +}; + +pub fn all_ranges() Ranges { + return .{ + .names = _all_ranges.names, + .starting_codepoints = _all_ranges.starting_codepoints, + .ending_codepoints = _all_ranges.ending_codepoints, + .longest_name_len = _all_ranges.longest_name_len, + }; +} + +pub const UnicodeGroup = struct { + name: []const u8, + starting_codepoint: u21, + ending_codepoint: u21, +}; + +fn parseRanges(text: []const u8) !Ranges { + var rc = Ranges{}; + var iterator = std.mem.splitSequence(u8, text, "\n"); + var inx: usize = 0; + while (iterator.next()) |group| + if (group.len > 0) { + const uc = try parseGroup(group); + rc.names[inx] = uc.name; + rc.starting_codepoints[inx] = uc.starting_codepoint; + rc.ending_codepoints[inx] = uc.ending_codepoint; + rc.longest_name_len = @max(rc.longest_name_len, uc.name.len); + inx += 1; + }; + return rc; +} + +fn parseGroup(group_text: []const u8) !UnicodeGroup { + // Basic Latin U+0 - U+7F + var iterator = std.mem.splitSequence(u8, group_text, "\t"); + const name = std.mem.trimRight(u8, iterator.first(), " "); + const range_text = iterator.next() orelse { + std.log.err("failed parsing on group '{s}'", .{group_text}); + return error.NoRangeSpecifiedInGroup; + }; + var range_iterator = std.mem.splitSequence(u8, range_text, " - "); + const start_text = range_iterator.first(); + const end_text = range_iterator.next() orelse return error.NoEndingCodepointInGroup; + return UnicodeGroup{ + .name = name, + .starting_codepoint = try std.fmt.parseUnsigned(u21, start_text[2..], 16), + .ending_codepoint = try std.fmt.parseUnsigned(u21, end_text[2..], 16), + }; +} + +test "check ranges" { + var parsed_ranges = all_ranges(); + // Entry 8 should be: + // Cyrillic U+400 - U+4FF + try std.testing.expectEqual(@as(u21, 0x400), parsed_ranges.starting_codepoints[8]); + try std.testing.expectEqual(@as(u21, 0x4ff), parsed_ranges.ending_codepoints[8]); + try std.testing.expectEqualStrings("Cyrillic", parsed_ranges.names[8]); + + var range = parsed_ranges.first().?; + try std.testing.expectEqualStrings("Basic Latin", range.name); + try std.testing.expectEqual(@as(u21, 0x0), range.starting_codepoint); + try std.testing.expectEqual(@as(u21, 0x7f), range.ending_codepoint); + + range = parsed_ranges.next().?; + try std.testing.expectEqualStrings("Latin-1 Supplement", range.name); + try std.testing.expectEqual(@as(u21, 0x80), range.starting_codepoint); + try std.testing.expectEqual(@as(u21, 0xff), range.ending_codepoint); +} diff --git a/zig-via-docker b/zig-via-docker new file mode 100755 index 0000000..4809fef --- /dev/null +++ b/zig-via-docker @@ -0,0 +1,4 @@ +#!/bin/sh +scriptpath="$( cd "$(dirname "$0")" ; pwd -P )" +# podman run -t --rm -v "$HOME/.cache:/root/.cache" -v "${scriptpath}:/app" -w /app fontfinder-alpine "$@" +podman run -t --rm -v "$HOME/.cache:/root/.cache" -v "${scriptpath}:/app" -w /app fontfinder "$@"