Skip to content

Commit bef2852

Browse files
authored
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)
2 parents 1861a39 + 82e6743 commit bef2852

File tree

3 files changed

+232
-1
lines changed

3 files changed

+232
-1
lines changed

src/cli/action.zig

+6
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const list_actions = @import("list_actions.zig");
1212
const show_config = @import("show_config.zig");
1313
const validate_config = @import("validate_config.zig");
1414
const crash_report = @import("crash_report.zig");
15+
const show_face = @import("show_face.zig");
1516

1617
/// Special commands that can be invoked via CLI flags. These are all
1718
/// invoked by using `+<action>` as a CLI flag. The only exception is
@@ -47,6 +48,9 @@ pub const Action = enum {
4748
// List, (eventually) view, and (eventually) send crash reports.
4849
@"crash-report",
4950

51+
// Show which font face Ghostty loads a codepoint from.
52+
@"show-face",
53+
5054
pub const Error = error{
5155
/// Multiple actions were detected. You can specify at most one
5256
/// action on the CLI otherwise the behavior desired is ambiguous.
@@ -146,6 +150,7 @@ pub const Action = enum {
146150
.@"show-config" => try show_config.run(alloc),
147151
.@"validate-config" => try validate_config.run(alloc),
148152
.@"crash-report" => try crash_report.run(alloc),
153+
.@"show-face" => try show_face.run(alloc),
149154
};
150155
}
151156

@@ -180,6 +185,7 @@ pub const Action = enum {
180185
.@"show-config" => show_config.Options,
181186
.@"validate-config" => validate_config.Options,
182187
.@"crash-report" => crash_report.Options,
188+
.@"show-face" => show_face.Options,
183189
};
184190
}
185191
}

src/cli/args.zig

+2-1
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ pub fn parse(
126126

127127
// The error set is dependent on comptime T, so we always add
128128
// an extra error so we can have the "else" below.
129-
const ErrSet = @TypeOf(err) || error{Unknown};
129+
const ErrSet = @TypeOf(err) || error{ Unknown, OutOfMemory };
130130
const message: [:0]const u8 = switch (@as(ErrSet, @errorCast(err))) {
131131
// OOM is not recoverable since we need to allocate to
132132
// track more error messages.
@@ -319,6 +319,7 @@ pub fn parseIntoField(
319319

320320
inline u8,
321321
u16,
322+
u21,
322323
u32,
323324
u64,
324325
usize,

src/cli/show_face.zig

+224
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
const std = @import("std");
2+
const Allocator = std.mem.Allocator;
3+
const ArenaAllocator = std.heap.ArenaAllocator;
4+
const Action = @import("action.zig").Action;
5+
const args = @import("args.zig");
6+
const diagnostics = @import("diagnostics.zig");
7+
const font = @import("../font/main.zig");
8+
const configpkg = @import("../config.zig");
9+
const Config = configpkg.Config;
10+
11+
pub const Options = struct {
12+
/// This is set by the CLI parser for deinit.
13+
_arena: ?ArenaAllocator = null,
14+
15+
/// The codepoint to search for.
16+
cp: ?u21 = null,
17+
18+
/// Search for all of the codepoints in the string.
19+
string: ?[]const u8 = null,
20+
21+
/// Font style to search for.
22+
style: font.Style = .regular,
23+
24+
/// If specified, force text or emoji presentation.
25+
presentation: ?font.Presentation = null,
26+
27+
// Enable arg parsing diagnostics so that we don't get an error if
28+
// there is a "normal" config setting on the cli.
29+
_diagnostics: diagnostics.DiagnosticList = .{},
30+
31+
pub fn deinit(self: *Options) void {
32+
if (self._arena) |arena| arena.deinit();
33+
self.* = undefined;
34+
}
35+
36+
/// Enables "-h" and "--help" to work.
37+
pub fn help(self: Options) !void {
38+
_ = self;
39+
return Action.help_error;
40+
}
41+
};
42+
43+
/// The `show-face` command shows what font face Ghostty will use to render a
44+
/// specific codepoint. Note that this command does not take into consideration
45+
/// grapheme clustering or any other Unicode features that might modify the
46+
/// presentation of a codepoint, so this may show a different font face than
47+
/// Ghostty uses to render a codepoint in a terminal session.
48+
///
49+
/// Flags:
50+
///
51+
/// * `--cp`: Find the face for a single codepoint. The codepoint may be specified
52+
/// in decimal (`--cp=65`), hexadecimal (`--cp=0x41`), octal (`--cp=0o101`), or
53+
/// binary (`--cp=0b1000001`).
54+
///
55+
/// * `--string`: Find the face for all of the codepoints in a string. The
56+
/// string must be a valid UTF-8 sequence.
57+
///
58+
/// * `--style`: Search for a specific style. Valid options are `regular`, `bold`,
59+
/// `italic`, and `bold_italic`.
60+
///
61+
/// * `--presentation`: If set, force searching for a specific presentation
62+
/// style. Valid options are `text` and `emoji`. If unset, the presentation
63+
/// style of a codepoint will be inferred from the Unicode standard.
64+
pub fn run(alloc: Allocator) !u8 {
65+
var iter = try args.argsIterator(alloc);
66+
defer iter.deinit();
67+
return try runArgs(alloc, &iter);
68+
}
69+
70+
fn runArgs(alloc_gpa: Allocator, argsIter: anytype) !u8 {
71+
const stdout = std.io.getStdOut().writer();
72+
const stderr = std.io.getStdErr().writer();
73+
74+
// Its possible to build Ghostty without font discovery!
75+
if (comptime font.Discover == void) {
76+
try stderr.print(
77+
\\Ghostty was built without a font discovery mechanism. This is a compile-time
78+
\\option. Please review how Ghostty was built from source, contact the
79+
\\maintainer to enable a font discovery mechanism, and try again.
80+
,
81+
.{},
82+
);
83+
return 1;
84+
}
85+
86+
var opts: Options = .{};
87+
defer opts.deinit();
88+
89+
args.parse(Options, alloc_gpa, &opts, argsIter) catch |err| switch (err) {
90+
error.ActionHelpRequested => return err,
91+
else => {
92+
try stderr.print("Error parsing args: {}\n", .{err});
93+
return 1;
94+
},
95+
};
96+
97+
// Print out any diagnostics, unless it's likely that the diagnostic was
98+
// generated trying to parse a "normal" configuration setting. Exit with an
99+
// error code if any diagnostics were printed.
100+
if (!opts._diagnostics.empty()) {
101+
var exit: bool = false;
102+
outer: for (opts._diagnostics.items()) |diagnostic| {
103+
if (diagnostic.location != .cli) continue :outer;
104+
inner: inline for (@typeInfo(Options).Struct.fields) |field| {
105+
if (field.name[0] == '_') continue :inner;
106+
if (std.mem.eql(u8, field.name, diagnostic.key)) {
107+
try stderr.writeAll("config error: ");
108+
try diagnostic.write(stderr);
109+
try stderr.writeAll("\n");
110+
exit = true;
111+
}
112+
}
113+
}
114+
if (exit) return 1;
115+
}
116+
117+
var arena = ArenaAllocator.init(alloc_gpa);
118+
defer arena.deinit();
119+
const alloc = arena.allocator();
120+
121+
if (opts.cp == null and opts.string == null) {
122+
try stderr.print("You must specify a codepoint with --cp or a string with --string\n", .{});
123+
return 1;
124+
}
125+
126+
var config = Config.load(alloc) catch |err| {
127+
try stderr.print("Unable to load config: {}", .{err});
128+
return 1;
129+
};
130+
defer config.deinit();
131+
132+
// Print out any diagnostics generated from parsing the config, unless
133+
// the diagnostic might have been generated because it's actually an
134+
// action-specific argument.
135+
if (!config._diagnostics.empty()) {
136+
outer: for (config._diagnostics.items()) |diagnostic| {
137+
inner: inline for (@typeInfo(Options).Struct.fields) |field| {
138+
if (field.name[0] == '_') continue :inner;
139+
if (std.mem.eql(u8, field.name, diagnostic.key) and (diagnostic.location == .none or diagnostic.location == .cli)) continue :outer;
140+
}
141+
try stderr.writeAll("config error: ");
142+
try diagnostic.write(stderr);
143+
try stderr.writeAll("\n");
144+
}
145+
}
146+
147+
var font_grid_set = font.SharedGridSet.init(alloc) catch |err| {
148+
try stderr.print("Unable to initialize font grid set: {}", .{err});
149+
return 1;
150+
};
151+
errdefer font_grid_set.deinit();
152+
153+
const font_size: font.face.DesiredSize = .{
154+
.points = config.@"font-size",
155+
.xdpi = 96,
156+
.ydpi = 96,
157+
};
158+
159+
var font_config = font.SharedGridSet.DerivedConfig.init(alloc, config) catch |err| {
160+
try stderr.print("Unable to initialize font config: {}", .{err});
161+
return 1;
162+
};
163+
164+
const font_grid_key, const font_grid = font_grid_set.ref(
165+
&font_config,
166+
font_size,
167+
) catch |err| {
168+
try stderr.print("Unable to get font grid: {}", .{err});
169+
return 1;
170+
};
171+
defer font_grid_set.deref(font_grid_key);
172+
173+
if (opts.cp) |cp| {
174+
if (try lookup(alloc, stdout, stderr, font_grid, opts.style, opts.presentation, cp)) |rc| return rc;
175+
}
176+
if (opts.string) |string| {
177+
const view = std.unicode.Utf8View.init(string) catch |err| {
178+
try stderr.print("Unable to parse string as unicode: {}", .{err});
179+
return 1;
180+
};
181+
var it = view.iterator();
182+
while (it.nextCodepoint()) |cp| {
183+
if (try lookup(alloc, stdout, stderr, font_grid, opts.style, opts.presentation, cp)) |rc| return rc;
184+
}
185+
}
186+
187+
return 0;
188+
}
189+
190+
fn lookup(
191+
alloc: std.mem.Allocator,
192+
stdout: anytype,
193+
stderr: anytype,
194+
font_grid: *font.SharedGrid,
195+
style: font.Style,
196+
presentation: ?font.Presentation,
197+
cp: u21,
198+
) !?u8 {
199+
const idx = font_grid.resolver.getIndex(alloc, cp, style, presentation) orelse {
200+
try stdout.print("U+{0X:0>2} « {0u} » not found.\n", .{cp});
201+
return null;
202+
};
203+
204+
const face = font_grid.resolver.collection.getFace(idx) catch |err| switch (err) {
205+
error.SpecialHasNoFace => {
206+
try stdout.print("U+{0X:0>2} « {0u} » is handled by Ghostty's internal sprites.\n", .{cp});
207+
return null;
208+
},
209+
else => {
210+
try stderr.print("Unable to get face: {}", .{err});
211+
return 1;
212+
},
213+
};
214+
215+
var buf: [1024]u8 = undefined;
216+
const name = face.name(&buf) catch |err| {
217+
try stderr.print("Unable to get name of face: {}", .{err});
218+
return 1;
219+
};
220+
221+
try stdout.print("U+{0X:0>2} « {0u} » found in face “{1s}”.\n", .{ cp, name });
222+
223+
return null;
224+
}

0 commit comments

Comments
 (0)