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; }