diff --git a/src/cli/action.zig b/src/cli/action.zig index 2f4b63638..a84a40024 100644 --- a/src/cli/action.zig +++ b/src/cli/action.zig @@ -12,6 +12,7 @@ const list_actions = @import("list_actions.zig"); const show_config = @import("show_config.zig"); const validate_config = @import("validate_config.zig"); const crash_report = @import("crash_report.zig"); +const show_face = @import("show_face.zig"); /// Special commands that can be invoked via CLI flags. These are all /// invoked by using `+` as a CLI flag. The only exception is @@ -47,6 +48,9 @@ pub const Action = enum { // List, (eventually) view, and (eventually) send crash reports. @"crash-report", + // Show which font face Ghostty loads a codepoint from. + @"show-face", + pub const Error = error{ /// Multiple actions were detected. You can specify at most one /// action on the CLI otherwise the behavior desired is ambiguous. @@ -146,6 +150,7 @@ pub const Action = enum { .@"show-config" => try show_config.run(alloc), .@"validate-config" => try validate_config.run(alloc), .@"crash-report" => try crash_report.run(alloc), + .@"show-face" => try show_face.run(alloc), }; } @@ -180,6 +185,7 @@ pub const Action = enum { .@"show-config" => show_config.Options, .@"validate-config" => validate_config.Options, .@"crash-report" => crash_report.Options, + .@"show-face" => show_face.Options, }; } } diff --git a/src/cli/args.zig b/src/cli/args.zig index 454ca360e..be71b9096 100644 --- a/src/cli/args.zig +++ b/src/cli/args.zig @@ -126,7 +126,7 @@ pub fn parse( // The error set is dependent on comptime T, so we always add // an extra error so we can have the "else" below. - const ErrSet = @TypeOf(err) || error{Unknown}; + const ErrSet = @TypeOf(err) || error{ Unknown, OutOfMemory }; const message: [:0]const u8 = switch (@as(ErrSet, @errorCast(err))) { // OOM is not recoverable since we need to allocate to // track more error messages. @@ -319,6 +319,7 @@ pub fn parseIntoField( inline u8, u16, + u21, u32, u64, usize, diff --git a/src/cli/show_face.zig b/src/cli/show_face.zig new file mode 100644 index 000000000..8b460a623 --- /dev/null +++ b/src/cli/show_face.zig @@ -0,0 +1,224 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const ArenaAllocator = std.heap.ArenaAllocator; +const Action = @import("action.zig").Action; +const args = @import("args.zig"); +const diagnostics = @import("diagnostics.zig"); +const font = @import("../font/main.zig"); +const configpkg = @import("../config.zig"); +const Config = configpkg.Config; + +pub const Options = struct { + /// This is set by the CLI parser for deinit. + _arena: ?ArenaAllocator = null, + + /// The codepoint to search for. + cp: ?u21 = null, + + /// Search for all of the codepoints in the string. + string: ?[]const u8 = null, + + /// Font style to search for. + style: font.Style = .regular, + + /// If specified, force text or emoji presentation. + presentation: ?font.Presentation = null, + + // Enable arg parsing diagnostics so that we don't get an error if + // there is a "normal" config setting on the cli. + _diagnostics: diagnostics.DiagnosticList = .{}, + + pub fn deinit(self: *Options) void { + if (self._arena) |arena| arena.deinit(); + self.* = undefined; + } + + /// Enables "-h" and "--help" to work. + pub fn help(self: Options) !void { + _ = self; + return Action.help_error; + } +}; + +/// The `show-face` command shows what font face Ghostty will use to render a +/// specific codepoint. Note that this command does not take into consideration +/// grapheme clustering or any other Unicode features that might modify the +/// presentation of a codepoint, so this may show a different font face than +/// Ghostty uses to render a codepoint in a terminal session. +/// +/// Flags: +/// +/// * `--cp`: Find the face for a single codepoint. The codepoint may be specified +/// in decimal (`--cp=65`), hexadecimal (`--cp=0x41`), octal (`--cp=0o101`), or +/// binary (`--cp=0b1000001`). +/// +/// * `--string`: Find the face for all of the codepoints in a string. The +/// string must be a valid UTF-8 sequence. +/// +/// * `--style`: Search for a specific style. Valid options are `regular`, `bold`, +/// `italic`, and `bold_italic`. +/// +/// * `--presentation`: If set, force searching for a specific presentation +/// style. Valid options are `text` and `emoji`. If unset, the presentation +/// style of a codepoint will be inferred from the Unicode standard. +pub fn run(alloc: Allocator) !u8 { + var iter = try args.argsIterator(alloc); + defer iter.deinit(); + return try runArgs(alloc, &iter); +} + +fn runArgs(alloc_gpa: Allocator, argsIter: anytype) !u8 { + const stdout = std.io.getStdOut().writer(); + const stderr = std.io.getStdErr().writer(); + + // Its possible to build Ghostty without font discovery! + if (comptime font.Discover == void) { + 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; + } + + var opts: Options = .{}; + defer opts.deinit(); + + args.parse(Options, alloc_gpa, &opts, argsIter) catch |err| switch (err) { + error.ActionHelpRequested => return err, + else => { + try stderr.print("Error parsing args: {}\n", .{err}); + return 1; + }, + }; + + // Print out any diagnostics, unless it's likely that the diagnostic was + // generated trying to parse a "normal" configuration setting. Exit with an + // error code if any diagnostics were printed. + if (!opts._diagnostics.empty()) { + var exit: bool = false; + outer: for (opts._diagnostics.items()) |diagnostic| { + if (diagnostic.location != .cli) continue :outer; + inner: inline for (@typeInfo(Options).Struct.fields) |field| { + if (field.name[0] == '_') continue :inner; + if (std.mem.eql(u8, field.name, diagnostic.key)) { + try stderr.writeAll("config error: "); + try diagnostic.write(stderr); + try stderr.writeAll("\n"); + exit = true; + } + } + } + if (exit) return 1; + } + + var arena = ArenaAllocator.init(alloc_gpa); + defer arena.deinit(); + const alloc = arena.allocator(); + + if (opts.cp == null and opts.string == null) { + try stderr.print("You must specify a codepoint with --cp or a string with --string\n", .{}); + return 1; + } + + var config = Config.load(alloc) catch |err| { + try stderr.print("Unable to load config: {}", .{err}); + return 1; + }; + defer config.deinit(); + + // Print out any diagnostics generated from parsing the config, unless + // the diagnostic might have been generated because it's actually an + // action-specific argument. + if (!config._diagnostics.empty()) { + outer: for (config._diagnostics.items()) |diagnostic| { + inner: inline for (@typeInfo(Options).Struct.fields) |field| { + if (field.name[0] == '_') continue :inner; + if (std.mem.eql(u8, field.name, diagnostic.key) and (diagnostic.location == .none or diagnostic.location == .cli)) continue :outer; + } + try stderr.writeAll("config error: "); + try diagnostic.write(stderr); + try stderr.writeAll("\n"); + } + } + + var font_grid_set = font.SharedGridSet.init(alloc) catch |err| { + try stderr.print("Unable to initialize font grid set: {}", .{err}); + return 1; + }; + errdefer font_grid_set.deinit(); + + const font_size: font.face.DesiredSize = .{ + .points = config.@"font-size", + .xdpi = 96, + .ydpi = 96, + }; + + var font_config = font.SharedGridSet.DerivedConfig.init(alloc, config) catch |err| { + try stderr.print("Unable to initialize font config: {}", .{err}); + return 1; + }; + + const font_grid_key, const font_grid = font_grid_set.ref( + &font_config, + font_size, + ) catch |err| { + try stderr.print("Unable to get font grid: {}", .{err}); + return 1; + }; + defer font_grid_set.deref(font_grid_key); + + if (opts.cp) |cp| { + if (try lookup(alloc, stdout, stderr, font_grid, opts.style, opts.presentation, cp)) |rc| return rc; + } + if (opts.string) |string| { + const view = std.unicode.Utf8View.init(string) catch |err| { + try stderr.print("Unable to parse string as unicode: {}", .{err}); + return 1; + }; + var it = view.iterator(); + while (it.nextCodepoint()) |cp| { + if (try lookup(alloc, stdout, stderr, font_grid, opts.style, opts.presentation, cp)) |rc| return rc; + } + } + + return 0; +} + +fn lookup( + alloc: std.mem.Allocator, + stdout: anytype, + stderr: anytype, + font_grid: *font.SharedGrid, + style: font.Style, + presentation: ?font.Presentation, + cp: u21, +) !?u8 { + const idx = font_grid.resolver.getIndex(alloc, cp, style, presentation) orelse { + try stdout.print("U+{0X:0>2} « {0u} » not found.\n", .{cp}); + return null; + }; + + const face = font_grid.resolver.collection.getFace(idx) catch |err| switch (err) { + error.SpecialHasNoFace => { + try stdout.print("U+{0X:0>2} « {0u} » is handled by Ghostty's internal sprites.\n", .{cp}); + return null; + }, + else => { + try stderr.print("Unable to get face: {}", .{err}); + return 1; + }, + }; + + var buf: [1024]u8 = undefined; + const name = face.name(&buf) catch |err| { + try stderr.print("Unable to get name of face: {}", .{err}); + return 1; + }; + + try stdout.print("U+{0X:0>2} « {0u} » found in face “{1s}”.\n", .{ cp, name }); + + return null; +}