diff --git a/src/cli.zig b/src/cli.zig new file mode 100644 index 000000000..871060b02 --- /dev/null +++ b/src/cli.zig @@ -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()); +} diff --git a/src/cli_action.zig b/src/cli/action.zig similarity index 83% rename from src/cli_action.zig rename to src/cli/action.zig index e62b3bfd5..4151d7aa2 100644 --- a/src/cli_action.zig +++ b/src/cli/action.zig @@ -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 `+` 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; diff --git a/src/cli_args.zig b/src/cli/args.zig similarity index 99% rename from src/cli_args.zig rename to src/cli/args.zig index 7729548d5..833914c88 100644 --- a/src/cli_args.zig +++ b/src/cli/args.zig @@ -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) { diff --git a/src/cli/list_fonts.zig b/src/cli/list_fonts.zig new file mode 100644 index 000000000..f34df3cf1 --- /dev/null +++ b/src/cli/list_fonts.zig @@ -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; +} diff --git a/src/cli/version.zig b/src/cli/version.zig new file mode 100644 index 000000000..b3395f4cd --- /dev/null +++ b/src/cli/version.zig @@ -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; +} diff --git a/src/config/CAPI.zig b/src/config/CAPI.zig index be2a491e7..1912403bc 100644 --- a/src/config/CAPI.zig +++ b/src/config/CAPI.zig @@ -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 diff --git a/src/config/Config.zig b/src/config/Config.zig index 79b9ea39d..b0fac6b43 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -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 diff --git a/src/config/Wasm.zig b/src/config/Wasm.zig index e50b4853b..90c06b63a 100644 --- a/src/config/Wasm.zig +++ b/src/config/Wasm.zig @@ -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 { diff --git a/src/font/DeferredFace.zig b/src/font/DeferredFace.zig index 63d303d2d..c7146128d 100644 --- a/src/font/DeferredFace.zig +++ b/src/font/DeferredFace.zig @@ -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 { diff --git a/src/font/discovery.zig b/src/font/discovery.zig index d1dc6a601..19b7141d6 100644 --- a/src/font/discovery.zig +++ b/src/font/discovery.zig @@ -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(); diff --git a/src/main.zig b/src/main.zig index bd0a25bd6..bcd3711ba 100644 --- a/src/main.zig +++ b/src/main.zig @@ -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"); }