All checks were successful
Generic zig build / build (push) Successful in 1m6s
409 lines
15 KiB
Zig
409 lines
15 KiB
Zig
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 {
|
|
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();
|
|
const 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;
|
|
var fl: ?fontconfig.FontList = null;
|
|
defer if (fl) |*f| f.deinit();
|
|
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();
|
|
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();
|
|
const 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" {
|
|
const 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);
|
|
const 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";
|
|
const 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;
|
|
};
|
|
const 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,
|
|
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,
|
|
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");
|
|
}
|