mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-15 08:16:13 +03:00
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. 
This commit is contained in:
@ -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 `+<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.
|
||||
@"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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
224
src/cli/show_face.zig
Normal file
224
src/cli/show_face.zig
Normal 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;
|
||||
}
|
Reference in New Issue
Block a user