CLI: add +show-face action (#3000)

This adds a `+show-face` CLI action to show what font face Ghostty will
use to display a particular codepoint. The codepoint can either be
specified via a single integer or via UTF-8 encoded string.

![Screenshot From 2024-12-17
12-32-31](https://github.com/user-attachments/assets/5a47e672-5ea2-4463-a1dc-7cd6d897e0a8)
This commit is contained in:
Mitchell Hashimoto
2024-12-22 07:25:10 -08:00
committed by GitHub
3 changed files with 232 additions and 1 deletions

View File

@ -12,6 +12,7 @@ const list_actions = @import("list_actions.zig");
const show_config = @import("show_config.zig"); const show_config = @import("show_config.zig");
const validate_config = @import("validate_config.zig"); const validate_config = @import("validate_config.zig");
const crash_report = @import("crash_report.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 /// Special commands that can be invoked via CLI flags. These are all
/// invoked by using `+<action>` as a CLI flag. The only exception is /// invoked by using `+<action>` as a CLI flag. The only exception is
@ -47,6 +48,9 @@ pub const Action = enum {
// List, (eventually) view, and (eventually) send crash reports. // List, (eventually) view, and (eventually) send crash reports.
@"crash-report", @"crash-report",
// Show which font face Ghostty loads a codepoint from.
@"show-face",
pub const Error = error{ pub const Error = error{
/// Multiple actions were detected. You can specify at most one /// Multiple actions were detected. You can specify at most one
/// action on the CLI otherwise the behavior desired is ambiguous. /// 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), .@"show-config" => try show_config.run(alloc),
.@"validate-config" => try validate_config.run(alloc), .@"validate-config" => try validate_config.run(alloc),
.@"crash-report" => try crash_report.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, .@"show-config" => show_config.Options,
.@"validate-config" => validate_config.Options, .@"validate-config" => validate_config.Options,
.@"crash-report" => crash_report.Options, .@"crash-report" => crash_report.Options,
.@"show-face" => show_face.Options,
}; };
} }
} }

View File

@ -126,7 +126,7 @@ pub fn parse(
// The error set is dependent on comptime T, so we always add // The error set is dependent on comptime T, so we always add
// an extra error so we can have the "else" below. // 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))) { const message: [:0]const u8 = switch (@as(ErrSet, @errorCast(err))) {
// OOM is not recoverable since we need to allocate to // OOM is not recoverable since we need to allocate to
// track more error messages. // track more error messages.
@ -319,6 +319,7 @@ pub fn parseIntoField(
inline u8, inline u8,
u16, u16,
u21,
u32, u32,
u64, u64,
usize, usize,

224
src/cli/show_face.zig Normal file
View File

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