Merge pull request #527 from mitchellh/cmd-list-fonts

"ghostty +list-fonts" to inspect font discovery
This commit is contained in:
Mitchell Hashimoto
2023-09-24 08:57:03 -07:00
committed by GitHub
11 changed files with 216 additions and 39 deletions

6
src/cli.zig Normal file
View File

@ -0,0 +1,6 @@
pub const args = @import("cli/args.zig");
pub const Action = @import("cli/action.zig").Action;
test {
@import("std").testing.refAllDecls(@This());
}

View File

@ -1,9 +1,8 @@
const std = @import("std");
const builtin = @import("builtin");
const xev = @import("xev");
const Allocator = std.mem.Allocator;
const build_config = @import("build_config.zig");
const renderer = @import("renderer.zig");
const list_fonts = @import("list_fonts.zig");
const version = @import("version.zig");
/// Special commands that can be invoked via CLI flags. These are all
/// invoked by using `+<action>` as a CLI flag. The only exception is
@ -12,6 +11,9 @@ pub const Action = enum {
/// Output the version and exit
version,
/// List available fonts
@"list-fonts",
pub const Error = error{
/// Multiple actions were detected. You can specify at most one
/// action on the CLI otherwise the behavior desired is ambiguous.
@ -47,25 +49,13 @@ pub const Action = enum {
/// Run the action. This returns the exit code to exit with.
pub fn run(self: Action, alloc: Allocator) !u8 {
_ = alloc;
return switch (self) {
.version => try runVersion(),
.version => try version.run(),
.@"list-fonts" => try list_fonts.run(alloc),
};
}
};
fn runVersion() !u8 {
const stdout = std.io.getStdOut().writer();
try stdout.print("Ghostty {s}\n\n", .{build_config.version_string});
try stdout.print("Build Config\n", .{});
try stdout.print(" - build mode : {}\n", .{builtin.mode});
try stdout.print(" - app runtime: {}\n", .{build_config.app_runtime});
try stdout.print(" - font engine: {}\n", .{build_config.font_backend});
try stdout.print(" - renderer : {}\n", .{renderer.Renderer});
try stdout.print(" - libxev : {}\n", .{xev.backend});
return 0;
}
test "parse action none" {
const testing = std.testing;
const alloc = testing.allocator;

View File

@ -4,7 +4,7 @@ const assert = std.debug.assert;
const Allocator = mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const ErrorList = @import("config/ErrorList.zig");
const ErrorList = @import("../config/ErrorList.zig");
// TODO:
// - Only `--long=value` format is accepted. Do we want to allow
@ -55,7 +55,7 @@ pub fn parse(comptime T: type, alloc: Allocator, dst: *T, iter: anytype) !void {
break :arena dst._arena.?.allocator();
} else fail: {
// Note: this is... not safe...
var fail = std.testing.FailingAllocator.init(alloc, 0);
var fail = std.testing.FailingAllocator.init(alloc, .{});
break :fail fail.allocator();
};
errdefer if (arena_available and arena_owned) {

141
src/cli/list_fonts.zig Normal file
View File

@ -0,0 +1,141 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const args = @import("args.zig");
const font = @import("../font/main.zig");
const log = std.log.scoped(.list_fonts);
pub const Config = struct {
/// This is set by the CLI parser for deinit.
_arena: ?ArenaAllocator = null,
/// The font family to search for. If this is set, then only fonts
/// matching this family will be listed.
family: ?[:0]const u8 = null,
/// Font styles to search for. If this is set, then only fonts that
/// match the given styles will be listed.
bold: bool = false,
italic: bool = false,
pub fn deinit(self: *Config) void {
if (self._arena) |arena| arena.deinit();
self.* = undefined;
}
};
/// The list-fonts command is used to list all the available fonts for Ghostty.
/// This uses the exact same font discovery mechanism Ghostty uses to find
/// fonts to use.
///
/// When executed with no arguments, this will list all available fonts,
/// sorted by family name, then font name. If a family name is given
/// with "--family", the sorting will be disabled and the results instead
/// will be shown in the same priority order Ghostty would use to pick a
/// font.
///
/// The "--family" argument can be used to filter results to a specific family.
/// The family handling is identical to the "font-familiy" set of Ghostty
/// configuration values, so this can be used to debug why your desired font
/// may not be loading.
///
/// The "--bold" and "--italic" arguments can be used to filter results to
/// specific styles. It is not guaranteed that only those styles are returned,
/// it will just prioriiize fonts that match those styles.
pub fn run(alloc: Allocator) !u8 {
var iter = try std.process.argsWithAllocator(alloc);
defer iter.deinit();
return try runArgs(alloc, &iter);
}
fn runArgs(alloc_gpa: Allocator, argsIter: anytype) !u8 {
var config: Config = .{};
defer config.deinit();
try args.parse(Config, alloc_gpa, &config, argsIter);
// Use an arena for all our memory allocs
var arena = ArenaAllocator.init(alloc_gpa);
defer arena.deinit();
const alloc = arena.allocator();
// Its possible to build Ghostty without font discovery!
if (comptime font.Discover == void) {
const stderr = std.io.getStdErr().writer();
try stderr.print(
\\Ghostty was built without a font discovery mechanism. This is a compile-time
\\option. Please review how Ghostty was built from source, contact the
\\maintainer to enable a font discovery mechanism, and try again.
,
.{},
);
return 1;
}
const stdout = std.io.getStdOut().writer();
// We'll be putting our fonts into a list categorized by family
// so it is easier to read the output.
var families = std.ArrayList([]const u8).init(alloc);
var map = std.StringHashMap(std.ArrayListUnmanaged([]const u8)).init(alloc);
// Look up all available fonts
var disco = font.Discover.init();
defer disco.deinit();
var disco_it = try disco.discover(.{
.family = config.family,
.bold = config.bold,
.italic = config.italic,
});
defer disco_it.deinit();
while (try disco_it.next()) |face| {
var buf: [1024]u8 = undefined;
const family_buf = face.familyName(&buf) catch |err| {
log.err("failed to get font family name: {}", .{err});
continue;
};
const family = try alloc.dupe(u8, family_buf);
const full_name_buf = face.name(&buf) catch |err| {
log.err("failed to get font name: {}", .{err});
continue;
};
const full_name = try alloc.dupe(u8, full_name_buf);
const gop = try map.getOrPut(family);
if (!gop.found_existing) {
try families.append(family);
gop.value_ptr.* = .{};
}
try gop.value_ptr.append(alloc, full_name);
}
// Sort our keys.
if (config.family == null) {
std.mem.sortUnstable([]const u8, families.items, {}, struct {
fn lessThan(_: void, lhs: []const u8, rhs: []const u8) bool {
return std.mem.order(u8, lhs, rhs) == .lt;
}
}.lessThan);
}
// Output each
for (families.items) |family| {
const list = map.get(family) orelse continue;
if (list.items.len == 0) continue;
if (config.family == null) {
std.mem.sortUnstable([]const u8, list.items, {}, struct {
fn lessThan(_: void, lhs: []const u8, rhs: []const u8) bool {
return std.mem.order(u8, lhs, rhs) == .lt;
}
}.lessThan);
}
try stdout.print("{s}\n", .{family});
for (list.items) |item| try stdout.print(" {s}\n", .{item});
try stdout.print("\n", .{});
}
return 0;
}

17
src/cli/version.zig Normal file
View File

@ -0,0 +1,17 @@
const std = @import("std");
const builtin = @import("builtin");
const build_config = @import("../build_config.zig");
const xev = @import("xev");
const renderer = @import("../renderer.zig");
pub fn run() !u8 {
const stdout = std.io.getStdOut().writer();
try stdout.print("Ghostty {s}\n\n", .{build_config.version_string});
try stdout.print("Build Config\n", .{});
try stdout.print(" - build mode : {}\n", .{builtin.mode});
try stdout.print(" - app runtime: {}\n", .{build_config.app_runtime});
try stdout.print(" - font engine: {}\n", .{build_config.font_backend});
try stdout.print(" - renderer : {}\n", .{renderer.Renderer});
try stdout.print(" - libxev : {}\n", .{xev.backend});
return 0;
}

View File

@ -1,5 +1,5 @@
const std = @import("std");
const cli_args = @import("../cli_args.zig");
const cli = @import("../cli.zig");
const inputpkg = @import("../input.zig");
const global = &@import("../main.zig").state;
@ -52,8 +52,8 @@ export fn ghostty_config_load_string(
fn config_load_string_(self: *Config, str: []const u8) !void {
var fbs = std.io.fixedBufferStream(str);
var iter = cli_args.lineIterator(fbs.reader());
try cli_args.parse(Config, global.alloc, self, &iter);
var iter = cli.args.lineIterator(fbs.reader());
try cli.args.parse(Config, global.alloc, self, &iter);
}
/// Load the configuration from the default file locations. This

View File

@ -10,7 +10,7 @@ const fontpkg = @import("../font/main.zig");
const inputpkg = @import("../input.zig");
const terminal = @import("../terminal/main.zig");
const internal_os = @import("../os/main.zig");
const cli_args = @import("../cli_args.zig");
const cli = @import("../cli.zig");
const Key = @import("key.zig").Key;
const KeyValue = @import("key.zig").Value;
@ -765,8 +765,8 @@ pub fn loadDefaultFiles(self: *Config, alloc: Allocator) !void {
std.log.info("reading configuration file path={s}", .{home_config_path});
var buf_reader = std.io.bufferedReader(file.reader());
var iter = cli_args.lineIterator(buf_reader.reader());
try cli_args.parse(Config, alloc, self, &iter);
var iter = cli.args.lineIterator(buf_reader.reader());
try cli.args.parse(Config, alloc, self, &iter);
} else |err| switch (err) {
error.FileNotFound => std.log.info(
"homedir config not found, not loading path={s}",
@ -792,7 +792,7 @@ pub fn loadCliArgs(self: *Config, alloc_gpa: Allocator) !void {
// Parse the config from the CLI args
var iter = try std.process.argsWithAllocator(alloc_gpa);
defer iter.deinit();
try cli_args.parse(Config, alloc_gpa, self, &iter);
try cli.args.parse(Config, alloc_gpa, self, &iter);
}
/// Load and parse the config files that were added in the "config-file" key.
@ -819,8 +819,8 @@ pub fn loadRecursiveFiles(self: *Config, alloc: Allocator) !void {
defer file.close();
var buf_reader = std.io.bufferedReader(file.reader());
var iter = cli_args.lineIterator(buf_reader.reader());
try cli_args.parse(Config, alloc, self, &iter);
var iter = cli.args.lineIterator(buf_reader.reader());
try cli.args.parse(Config, alloc, self, &iter);
// We don't currently support adding more config files to load
// from within a loaded config file. This can be supported

View File

@ -1,6 +1,6 @@
const std = @import("std");
const wasm = @import("../os/wasm.zig");
const cli_args = @import("../cli_args.zig");
const cli = @import("../cli.zig");
const alloc = wasm.alloc;
const Config = @import("Config.zig");
@ -43,8 +43,8 @@ export fn config_load_string(
fn config_load_string_(self: *Config, str: []const u8) !void {
var fbs = std.io.fixedBufferStream(str);
var iter = cli_args.lineIterator(fbs.reader());
try cli_args.parse(Config, alloc, self, &iter);
var iter = cli.args.lineIterator(fbs.reader());
try cli.args.parse(Config, alloc, self, &iter);
}
export fn config_finalize(self: *Config) void {

View File

@ -101,6 +101,28 @@ pub fn deinit(self: *DeferredFace) void {
self.* = undefined;
}
/// Returns the family name of the font.
pub fn familyName(self: DeferredFace, buf: []u8) ![]const u8 {
switch (options.backend) {
.freetype => {},
.fontconfig_freetype => if (self.fc) |fc|
return (try fc.pattern.get(.family, 0)).string,
.coretext, .coretext_freetype => if (self.ct) |ct| {
const family_name = ct.font.copyAttribute(.family_name);
return family_name.cstringPtr(.utf8) orelse unsupported: {
break :unsupported family_name.cstring(buf, .utf8) orelse
return error.OutOfMemory;
};
},
.web_canvas => if (self.wc) |wc| return wc.font_str,
}
return "";
}
/// Returns the name of this face. The memory is always owned by the
/// face so it doesn't have to be freed.
pub fn name(self: DeferredFace, buf: []u8) ![]const u8 {

View File

@ -40,9 +40,10 @@ pub const Descriptor = struct {
size: u16 = 0,
/// True if we want to search specifically for a font that supports
/// bold, italic, or both.
/// specific styles.
bold: bool = false,
italic: bool = false,
monospace: bool = true,
/// Variation axes to apply to the font. This also impacts searching
/// for fonts since fonts with the ability to set these variations
@ -131,6 +132,7 @@ pub const Descriptor = struct {
const traits: macos.text.FontSymbolicTraits = .{
.bold = self.bold,
.italic = self.italic,
.monospace = self.monospace,
};
const traits_cval: u32 = @bitCast(traits);
if (traits_cval > 0) {
@ -196,7 +198,7 @@ pub const Fontconfig = struct {
pat.defaultSubstitute();
// Search
const res = self.fc_config.fontSort(pat, true, null);
const res = self.fc_config.fontSort(pat, false, null);
if (res.result != .match) return error.FontConfigFailed;
errdefer res.fs.destroy();

View File

@ -6,7 +6,7 @@ const options = @import("build_options");
const glfw = @import("glfw");
const macos = @import("macos");
const tracy = @import("tracy");
const cli_action = @import("cli_action.zig");
const cli = @import("cli.zig");
const internal_os = @import("os/main.zig");
const xev = @import("xev");
const fontconfig = @import("fontconfig");
@ -171,7 +171,7 @@ pub const GlobalState = struct {
gpa: ?GPA,
alloc: std.mem.Allocator,
tracy: if (tracy.enabled) ?tracy.Allocator(null) else void,
action: ?cli_action.Action,
action: ?cli.Action,
logging: Logging,
/// Where logging should go
@ -226,7 +226,7 @@ pub const GlobalState = struct {
};
// We first try to parse any action that we may be executing.
self.action = try cli_action.Action.detectCLI(self.alloc);
self.action = try cli.Action.detectCLI(self.alloc);
// If we have an action executing, we disable logging by default
// since we write to stderr we don't want logs messing up our
@ -290,7 +290,7 @@ test {
_ = @import("renderer.zig");
_ = @import("termio.zig");
_ = @import("input.zig");
_ = @import("cli_action.zig");
_ = @import("cli.zig");
// Libraries
_ = @import("segmented_pool.zig");
@ -300,6 +300,5 @@ test {
// TODO
_ = @import("blocking_queue.zig");
_ = @import("config.zig");
_ = @import("cli_args.zig");
_ = @import("lru.zig");
}