mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-05-20 20:04:26 +00:00
Merge branch 'main' into ssh-integration
This commit is contained in:
@@ -270,6 +270,7 @@ const DerivedConfig = struct {
|
||||
title: ?[:0]const u8,
|
||||
title_report: bool,
|
||||
links: []Link,
|
||||
link_previews: configpkg.LinkPreviews,
|
||||
|
||||
const Link = struct {
|
||||
regex: oni.Regex,
|
||||
@@ -336,6 +337,7 @@ const DerivedConfig = struct {
|
||||
.title = config.title,
|
||||
.title_report = config.@"title-report",
|
||||
.links = links,
|
||||
.link_previews = config.@"link-previews",
|
||||
|
||||
// Assignments happen sequentially so we have to do this last
|
||||
// so that the memory is captured from allocs above.
|
||||
@@ -1242,7 +1244,7 @@ fn mouseRefreshLinks(
|
||||
// Get our link at the current position. This returns null if there
|
||||
// isn't a link OR if we shouldn't be showing links for some reason
|
||||
// (see further comments for cases).
|
||||
const link_: ?apprt.action.MouseOverLink = link: {
|
||||
const link_: ?apprt.action.MouseOverLink, const preview: bool = link: {
|
||||
// If we clicked and our mouse moved cells then we never
|
||||
// highlight links until the mouse is unclicked. This follows
|
||||
// standard macOS and Linux behavior where a click and drag cancels
|
||||
@@ -1257,18 +1259,21 @@ fn mouseRefreshLinks(
|
||||
|
||||
if (!click_pt.coord().eql(pos_vp)) {
|
||||
log.debug("mouse moved while left click held, ignoring link hover", .{});
|
||||
break :link null;
|
||||
break :link .{ null, false };
|
||||
}
|
||||
}
|
||||
|
||||
const link = (try self.linkAtPos(pos)) orelse break :link null;
|
||||
const link = (try self.linkAtPos(pos)) orelse break :link .{ null, false };
|
||||
switch (link[0]) {
|
||||
.open => {
|
||||
const str = try self.io.terminal.screen.selectionString(alloc, .{
|
||||
.sel = link[1],
|
||||
.trim = false,
|
||||
});
|
||||
break :link .{ .url = str };
|
||||
break :link .{
|
||||
.{ .url = str },
|
||||
self.config.link_previews == .true,
|
||||
};
|
||||
},
|
||||
|
||||
._open_osc8 => {
|
||||
@@ -1276,9 +1281,14 @@ fn mouseRefreshLinks(
|
||||
const pin = link[1].start();
|
||||
const uri = self.osc8URI(pin) orelse {
|
||||
log.warn("failed to get URI for OSC8 hyperlink", .{});
|
||||
break :link null;
|
||||
break :link .{ null, false };
|
||||
};
|
||||
break :link .{
|
||||
.{
|
||||
.url = uri,
|
||||
},
|
||||
self.config.link_previews != .false,
|
||||
};
|
||||
break :link .{ .url = uri };
|
||||
},
|
||||
}
|
||||
};
|
||||
@@ -1294,11 +1304,15 @@ fn mouseRefreshLinks(
|
||||
.mouse_shape,
|
||||
.pointer,
|
||||
);
|
||||
_ = try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.mouse_over_link,
|
||||
link,
|
||||
);
|
||||
|
||||
if (preview) {
|
||||
_ = try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.mouse_over_link,
|
||||
link,
|
||||
);
|
||||
}
|
||||
|
||||
try self.queueRender();
|
||||
return;
|
||||
}
|
||||
@@ -3710,7 +3724,7 @@ fn processLinks(self: *Surface, pos: apprt.CursorPos) !bool {
|
||||
.trim = false,
|
||||
});
|
||||
defer self.alloc.free(str);
|
||||
try internal_os.open(self.alloc, .unknown, str);
|
||||
try self.openUrl(.{ .kind = .unknown, .url = str });
|
||||
},
|
||||
|
||||
._open_osc8 => {
|
||||
@@ -3718,13 +3732,35 @@ fn processLinks(self: *Surface, pos: apprt.CursorPos) !bool {
|
||||
log.warn("failed to get URI for OSC8 hyperlink", .{});
|
||||
return false;
|
||||
};
|
||||
try internal_os.open(self.alloc, .unknown, uri);
|
||||
try self.openUrl(.{ .kind = .unknown, .url = uri });
|
||||
},
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
fn openUrl(
|
||||
self: *Surface,
|
||||
action: apprt.action.OpenUrl,
|
||||
) !void {
|
||||
// If the apprt handles it then we're done.
|
||||
if (try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.open_url,
|
||||
action,
|
||||
)) return;
|
||||
|
||||
// apprt didn't handle it, fallback to our simple cross-platform
|
||||
// URL opener. We log a warning because we want well-behaved
|
||||
// apprts to handle this themselves.
|
||||
log.warn("apprt did not handle open URL action, falling back to default opener", .{});
|
||||
try internal_os.open(
|
||||
self.alloc,
|
||||
action.kind,
|
||||
action.url,
|
||||
);
|
||||
}
|
||||
|
||||
/// Return the URI for an OSC8 hyperlink at the given position or null
|
||||
/// if there is no hyperlink.
|
||||
fn osc8URI(self: *Surface, pin: terminal.Pin) ?[]const u8 {
|
||||
@@ -4443,6 +4479,18 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
||||
return false;
|
||||
},
|
||||
|
||||
.copy_title_to_clipboard => {
|
||||
const title = self.rt_surface.getTitle() orelse return false;
|
||||
if (title.len == 0) return false;
|
||||
|
||||
self.rt_surface.setClipboardString(title, .standard, false) catch |err| {
|
||||
log.err("error copying title to clipboard err={}", .{err});
|
||||
return true;
|
||||
};
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
.paste_from_clipboard => try self.startClipboardRequest(
|
||||
.standard,
|
||||
.{ .paste = {} },
|
||||
@@ -4484,6 +4532,14 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
||||
try self.setFontSize(size);
|
||||
},
|
||||
|
||||
.set_font_size => |points| {
|
||||
log.debug("set font size={d}", .{points});
|
||||
|
||||
var size = self.font_size;
|
||||
size.points = std.math.clamp(points, 1.0, 255.0);
|
||||
try self.setFontSize(size);
|
||||
},
|
||||
|
||||
.prompt_surface_title => return try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.prompt_title,
|
||||
@@ -4923,7 +4979,7 @@ fn writeScreenFile(
|
||||
defer self.alloc.free(pathZ);
|
||||
try self.rt_surface.setClipboardString(pathZ, .standard, false);
|
||||
},
|
||||
.open => try internal_os.open(self.alloc, .text, path),
|
||||
.open => try self.openUrl(.{ .kind = .text, .url = path }),
|
||||
.paste => self.io.queueMessage(try termio.Message.writeReq(
|
||||
self.alloc,
|
||||
path,
|
||||
|
||||
@@ -267,6 +267,11 @@ pub const Action = union(Key) {
|
||||
|
||||
check_for_updates,
|
||||
|
||||
/// Open a URL using the native OS mechanisms. On macOS this might be `open`
|
||||
/// or on Linux this might be `xdg-open`. The exact mechanism is up to the
|
||||
/// apprt.
|
||||
open_url: OpenUrl,
|
||||
|
||||
/// Sync with: ghostty_action_tag_e
|
||||
pub const Key = enum(c_int) {
|
||||
quit,
|
||||
@@ -317,6 +322,7 @@ pub const Action = union(Key) {
|
||||
undo,
|
||||
redo,
|
||||
check_for_updates,
|
||||
open_url,
|
||||
};
|
||||
|
||||
/// Sync with: ghostty_action_u
|
||||
@@ -357,7 +363,11 @@ pub const Action = union(Key) {
|
||||
// For ABI compatibility, we expect that this is our union size.
|
||||
// At the time of writing, we don't promise ABI compatibility
|
||||
// so we can change this but I want to be aware of it.
|
||||
assert(@sizeOf(CValue) == 16);
|
||||
assert(@sizeOf(CValue) == switch (@sizeOf(usize)) {
|
||||
4 => 16,
|
||||
8 => 24,
|
||||
else => unreachable,
|
||||
});
|
||||
}
|
||||
|
||||
/// Returns the value type for the given key.
|
||||
@@ -614,3 +624,44 @@ pub const ConfigChange = struct {
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// Open a URL
|
||||
pub const OpenUrl = struct {
|
||||
/// The type of data that the URL refers to.
|
||||
kind: Kind,
|
||||
|
||||
/// The URL.
|
||||
url: []const u8,
|
||||
|
||||
/// The type of the data at the URL to open. This is used as a hint to
|
||||
/// potentially open the URL in a different way.
|
||||
///
|
||||
/// Sync with: ghostty_action_open_url_kind_e
|
||||
pub const Kind = enum(c_int) {
|
||||
/// The type is unknown. This is the default and apprts should
|
||||
/// open the URL in the most generic way possible. For example,
|
||||
/// on macOS this would be the equivalent of `open` or on Linux
|
||||
/// this would be `xdg-open`.
|
||||
unknown,
|
||||
|
||||
/// The URL is known to be a text file. In this case, the apprt
|
||||
/// should try to open the URL in a text editor or viewer or
|
||||
/// some equivalent, if possible.
|
||||
text,
|
||||
};
|
||||
|
||||
// Sync with: ghostty_action_open_url_s
|
||||
pub const C = extern struct {
|
||||
kind: Kind,
|
||||
url: [*]const u8,
|
||||
len: usize,
|
||||
};
|
||||
|
||||
pub fn cval(self: OpenUrl) C {
|
||||
return .{
|
||||
.kind = self.kind,
|
||||
.url = self.url.ptr,
|
||||
.len = self.url.len,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -884,7 +884,7 @@ pub const Surface = struct {
|
||||
}
|
||||
|
||||
// Remove this so that running `ghostty` within Ghostty works.
|
||||
env.remove("GHOSTTY_MAC_APP");
|
||||
env.remove("GHOSTTY_MAC_LAUNCH_SOURCE");
|
||||
|
||||
// If we were launched from the desktop then we want to
|
||||
// remove the LANGUAGE env var so that we don't inherit
|
||||
|
||||
@@ -496,7 +496,7 @@ pub fn performAction(
|
||||
.resize_split => self.resizeSplit(target, value),
|
||||
.equalize_splits => self.equalizeSplits(target),
|
||||
.goto_split => return self.gotoSplit(target, value),
|
||||
.open_config => try configpkg.edit.open(self.core_app.alloc),
|
||||
.open_config => return self.openConfig(),
|
||||
.config_change => self.configChange(target, value.config),
|
||||
.reload_config => try self.reloadConfig(target, value),
|
||||
.inspector => self.controlInspector(target, value),
|
||||
@@ -519,6 +519,7 @@ pub fn performAction(
|
||||
.secure_input => self.setSecureInput(target, value),
|
||||
.ring_bell => try self.ringBell(target),
|
||||
.toggle_command_palette => try self.toggleCommandPalette(target),
|
||||
.open_url => self.openUrl(value),
|
||||
|
||||
// Unimplemented
|
||||
.close_all_windows,
|
||||
@@ -1757,3 +1758,34 @@ fn initActions(self: *App) void {
|
||||
action_map.addAction(action.as(gio.Action));
|
||||
}
|
||||
}
|
||||
|
||||
fn openConfig(self: *App) !bool {
|
||||
// Get the config file path
|
||||
const alloc = self.core_app.alloc;
|
||||
const path = configpkg.edit.openPath(alloc) catch |err| {
|
||||
log.warn("error getting config file path: {}", .{err});
|
||||
return false;
|
||||
};
|
||||
defer alloc.free(path);
|
||||
|
||||
// Open it using openURL. "path" isn't actually a URL but
|
||||
// at the time of writing that works just fine for GTK.
|
||||
self.openUrl(.{ .kind = .text, .url = path });
|
||||
return true;
|
||||
}
|
||||
|
||||
fn openUrl(
|
||||
app: *App,
|
||||
value: apprt.action.OpenUrl,
|
||||
) void {
|
||||
// TODO: use https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.OpenURI.html
|
||||
|
||||
// Fallback to the minimal cross-platform way of opening a URL.
|
||||
// This is always a safe fallback and enables for example Windows
|
||||
// to open URLs (GTK on Windows via WSL is a thing).
|
||||
internal_os.open(
|
||||
app.core_app.alloc,
|
||||
value.kind,
|
||||
value.url,
|
||||
) catch |err| log.warn("unable to open url: {}", .{err});
|
||||
}
|
||||
|
||||
@@ -214,6 +214,7 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
{
|
||||
const btn = gtk.MenuButton.new();
|
||||
btn.as(gtk.Widget).setTooltipText(i18n._("Main Menu"));
|
||||
btn.as(gtk.Widget).setCanFocus(0);
|
||||
btn.setIconName("open-menu-symbolic");
|
||||
btn.setPopover(self.titlebar_menu.asWidget());
|
||||
_ = gobject.Object.signals.notify.connect(
|
||||
@@ -253,6 +254,7 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
},
|
||||
};
|
||||
|
||||
btn.setCanFocus(0);
|
||||
btn.setFocusOnClick(0);
|
||||
self.headerbar.packEnd(btn);
|
||||
}
|
||||
|
||||
@@ -88,6 +88,19 @@ pub fn init(
|
||||
|
||||
// Our step to open the resulting Ghostty app.
|
||||
const open = open: {
|
||||
const disable_save_state = RunStep.create(b, "disable save state");
|
||||
disable_save_state.has_side_effects = true;
|
||||
disable_save_state.addArgs(&.{
|
||||
"/usr/libexec/PlistBuddy",
|
||||
"-c",
|
||||
// We'll have to change this to `Set` if we ever put this
|
||||
// into our Info.plist.
|
||||
"Add :NSQuitAlwaysKeepsWindows bool false",
|
||||
b.fmt("{s}/Contents/Info.plist", .{app_path}),
|
||||
});
|
||||
disable_save_state.expectExitCode(0);
|
||||
disable_save_state.step.dependOn(&build.step);
|
||||
|
||||
const open = RunStep.create(b, "run Ghostty app");
|
||||
open.has_side_effects = true;
|
||||
open.cwd = b.path("");
|
||||
@@ -98,22 +111,17 @@ pub fn init(
|
||||
|
||||
// Open depends on the app
|
||||
open.step.dependOn(&build.step);
|
||||
open.step.dependOn(&disable_save_state.step);
|
||||
|
||||
// This overrides our default behavior and forces logs to show
|
||||
// up on stderr (in addition to the centralized macOS log).
|
||||
open.setEnvironmentVariable("GHOSTTY_LOG", "1");
|
||||
|
||||
// This is hack so that we can activate the app and bring it to
|
||||
// the front forcibly even though we're executing directly
|
||||
// via the binary and not launch services.
|
||||
open.setEnvironmentVariable("GHOSTTY_MAC_ACTIVATE", "1");
|
||||
// Configure how we're launching
|
||||
open.setEnvironmentVariable("GHOSTTY_MAC_LAUNCH_SOURCE", "zig_run");
|
||||
|
||||
if (b.args) |args| {
|
||||
open.addArgs(args);
|
||||
} else {
|
||||
// This tricks the app into thinking it's running from the
|
||||
// app bundle so we don't execute our CLI mode.
|
||||
open.setEnvironmentVariable("GHOSTTY_MAC_APP", "1");
|
||||
}
|
||||
|
||||
break :open open;
|
||||
|
||||
@@ -760,6 +760,9 @@ pub fn gtkDistResources(
|
||||
});
|
||||
const resources_c = generate_c.addOutputFileArg("ghostty_resources.c");
|
||||
generate_c.addFileArg(gresource_xml);
|
||||
for (gresource.dependencies) |file| {
|
||||
generate_c.addFileInput(b.path(file));
|
||||
}
|
||||
|
||||
const generate_h = b.addSystemCommand(&.{
|
||||
"glib-compile-resources",
|
||||
@@ -770,6 +773,9 @@ pub fn gtkDistResources(
|
||||
});
|
||||
const resources_h = generate_h.addOutputFileArg("ghostty_resources.h");
|
||||
generate_h.addFileArg(gresource_xml);
|
||||
for (gresource.dependencies) |file| {
|
||||
generate_h.addFileInput(b.path(file));
|
||||
}
|
||||
|
||||
return .{
|
||||
.resources_c = .{
|
||||
|
||||
@@ -15,8 +15,6 @@ pub const Options = struct {};
|
||||
/// The `version` command is used to display information about Ghostty. Recognized as
|
||||
/// either `+version` or `--version`.
|
||||
pub fn run(alloc: Allocator) !u8 {
|
||||
_ = alloc;
|
||||
|
||||
const stdout = std.io.getStdOut().writer();
|
||||
const tty = std.io.getStdOut().isTty();
|
||||
|
||||
@@ -34,32 +32,37 @@ pub fn run(alloc: Allocator) !u8 {
|
||||
try stdout.print(" - channel: {s}\n", .{@tagName(build_config.release_channel)});
|
||||
|
||||
try stdout.print("Build Config\n", .{});
|
||||
try stdout.print(" - Zig version: {s}\n", .{builtin.zig_version_string});
|
||||
try stdout.print(" - build mode : {}\n", .{builtin.mode});
|
||||
try stdout.print(" - app runtime: {}\n", .{build_config.app_runtime});
|
||||
try stdout.print(" - font engine: {}\n", .{build_config.font_backend});
|
||||
try stdout.print(" - renderer : {}\n", .{renderer.Renderer});
|
||||
try stdout.print(" - libxev : {s}\n", .{@tagName(xev.backend)});
|
||||
try stdout.print(" - Zig version : {s}\n", .{builtin.zig_version_string});
|
||||
try stdout.print(" - build mode : {}\n", .{builtin.mode});
|
||||
try stdout.print(" - app runtime : {}\n", .{build_config.app_runtime});
|
||||
try stdout.print(" - font engine : {}\n", .{build_config.font_backend});
|
||||
try stdout.print(" - renderer : {}\n", .{renderer.Renderer});
|
||||
try stdout.print(" - libxev : {s}\n", .{@tagName(xev.backend)});
|
||||
if (comptime build_config.app_runtime == .gtk) {
|
||||
try stdout.print(" - desktop env: {s}\n", .{@tagName(internal_os.desktopEnvironment())});
|
||||
try stdout.print(" - GTK version:\n", .{});
|
||||
try stdout.print(" build : {}\n", .{gtk_version.comptime_version});
|
||||
try stdout.print(" runtime : {}\n", .{gtk_version.getRuntimeVersion()});
|
||||
try stdout.print(" - libadwaita : enabled\n", .{});
|
||||
try stdout.print(" build : {}\n", .{adw_version.comptime_version});
|
||||
try stdout.print(" runtime : {}\n", .{adw_version.getRuntimeVersion()});
|
||||
if (comptime builtin.os.tag == .linux) {
|
||||
const kernel_info = internal_os.getKernelInfo(alloc);
|
||||
defer if (kernel_info) |k| alloc.free(k);
|
||||
try stdout.print(" - kernel version: {s}\n", .{kernel_info orelse "Kernel information unavailable"});
|
||||
}
|
||||
try stdout.print(" - desktop env : {s}\n", .{@tagName(internal_os.desktopEnvironment())});
|
||||
try stdout.print(" - GTK version :\n", .{});
|
||||
try stdout.print(" build : {}\n", .{gtk_version.comptime_version});
|
||||
try stdout.print(" runtime : {}\n", .{gtk_version.getRuntimeVersion()});
|
||||
try stdout.print(" - libadwaita : enabled\n", .{});
|
||||
try stdout.print(" build : {}\n", .{adw_version.comptime_version});
|
||||
try stdout.print(" runtime : {}\n", .{adw_version.getRuntimeVersion()});
|
||||
if (comptime build_options.x11) {
|
||||
try stdout.print(" - libX11 : enabled\n", .{});
|
||||
try stdout.print(" - libX11 : enabled\n", .{});
|
||||
} else {
|
||||
try stdout.print(" - libX11 : disabled\n", .{});
|
||||
try stdout.print(" - libX11 : disabled\n", .{});
|
||||
}
|
||||
|
||||
// We say `libwayland` since it is possible to build Ghostty without
|
||||
// Wayland integration but with Wayland-enabled GTK
|
||||
if (comptime build_options.wayland) {
|
||||
try stdout.print(" - libwayland : enabled\n", .{});
|
||||
try stdout.print(" - libwayland : enabled\n", .{});
|
||||
} else {
|
||||
try stdout.print(" - libwayland : disabled\n", .{});
|
||||
try stdout.print(" - libwayland : disabled\n", .{});
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
|
||||
@@ -14,6 +14,7 @@ pub const entryFormatter = formatter.entryFormatter;
|
||||
pub const formatEntry = formatter.formatEntry;
|
||||
|
||||
// Field types
|
||||
pub const BoldColor = Config.BoldColor;
|
||||
pub const ClipboardAccess = Config.ClipboardAccess;
|
||||
pub const Command = Config.Command;
|
||||
pub const ConfirmCloseSurface = Config.ConfirmCloseSurface;
|
||||
@@ -37,6 +38,7 @@ pub const ShellIntegrationFeatures = Config.ShellIntegrationFeatures;
|
||||
pub const WindowPaddingColor = Config.WindowPaddingColor;
|
||||
pub const BackgroundImagePosition = Config.BackgroundImagePosition;
|
||||
pub const BackgroundImageFit = Config.BackgroundImageFit;
|
||||
pub const LinkPreviews = Config.LinkPreviews;
|
||||
|
||||
// Alternate APIs
|
||||
pub const CAPI = @import("config/CAPI.zig");
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const cli = @import("../cli.zig");
|
||||
const inputpkg = @import("../input.zig");
|
||||
const global = &@import("../global.zig").state;
|
||||
const state = &@import("../global.zig").state;
|
||||
const c = @import("../main_c.zig");
|
||||
|
||||
const Config = @import("Config.zig");
|
||||
const c_get = @import("c_get.zig");
|
||||
@@ -12,14 +14,14 @@ const log = std.log.scoped(.config);
|
||||
|
||||
/// Create a new configuration filled with the initial default values.
|
||||
export fn ghostty_config_new() ?*Config {
|
||||
const result = global.alloc.create(Config) catch |err| {
|
||||
const result = state.alloc.create(Config) catch |err| {
|
||||
log.err("error allocating config err={}", .{err});
|
||||
return null;
|
||||
};
|
||||
|
||||
result.* = Config.default(global.alloc) catch |err| {
|
||||
result.* = Config.default(state.alloc) catch |err| {
|
||||
log.err("error creating config err={}", .{err});
|
||||
global.alloc.destroy(result);
|
||||
state.alloc.destroy(result);
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -29,20 +31,20 @@ export fn ghostty_config_new() ?*Config {
|
||||
export fn ghostty_config_free(ptr: ?*Config) void {
|
||||
if (ptr) |v| {
|
||||
v.deinit();
|
||||
global.alloc.destroy(v);
|
||||
state.alloc.destroy(v);
|
||||
}
|
||||
}
|
||||
|
||||
/// Deep clone the configuration.
|
||||
export fn ghostty_config_clone(self: *Config) ?*Config {
|
||||
const result = global.alloc.create(Config) catch |err| {
|
||||
const result = state.alloc.create(Config) catch |err| {
|
||||
log.err("error allocating config err={}", .{err});
|
||||
return null;
|
||||
};
|
||||
|
||||
result.* = self.clone(global.alloc) catch |err| {
|
||||
result.* = self.clone(state.alloc) catch |err| {
|
||||
log.err("error cloning config err={}", .{err});
|
||||
global.alloc.destroy(result);
|
||||
state.alloc.destroy(result);
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -51,7 +53,7 @@ export fn ghostty_config_clone(self: *Config) ?*Config {
|
||||
|
||||
/// Load the configuration from the CLI args.
|
||||
export fn ghostty_config_load_cli_args(self: *Config) void {
|
||||
self.loadCliArgs(global.alloc) catch |err| {
|
||||
self.loadCliArgs(state.alloc) catch |err| {
|
||||
log.err("error loading config err={}", .{err});
|
||||
};
|
||||
}
|
||||
@@ -60,7 +62,7 @@ export fn ghostty_config_load_cli_args(self: *Config) void {
|
||||
/// is usually done first. The default file locations are locations
|
||||
/// such as the home directory.
|
||||
export fn ghostty_config_load_default_files(self: *Config) void {
|
||||
self.loadDefaultFiles(global.alloc) catch |err| {
|
||||
self.loadDefaultFiles(state.alloc) catch |err| {
|
||||
log.err("error loading config err={}", .{err});
|
||||
};
|
||||
}
|
||||
@@ -69,7 +71,7 @@ export fn ghostty_config_load_default_files(self: *Config) void {
|
||||
/// file locations in the previously loaded configuration. This will
|
||||
/// recursively continue to load up to a built-in limit.
|
||||
export fn ghostty_config_load_recursive_files(self: *Config) void {
|
||||
self.loadRecursiveFiles(global.alloc) catch |err| {
|
||||
self.loadRecursiveFiles(state.alloc) catch |err| {
|
||||
log.err("error loading config err={}", .{err});
|
||||
};
|
||||
}
|
||||
@@ -122,10 +124,13 @@ export fn ghostty_config_get_diagnostic(self: *Config, idx: u32) Diagnostic {
|
||||
return .{ .message = message.ptr };
|
||||
}
|
||||
|
||||
export fn ghostty_config_open() void {
|
||||
edit.open(global.alloc) catch |err| {
|
||||
export fn ghostty_config_open_path() c.String {
|
||||
const path = edit.openPath(state.alloc) catch |err| {
|
||||
log.err("error opening config in editor err={}", .{err});
|
||||
return .empty;
|
||||
};
|
||||
|
||||
return .fromSlice(path);
|
||||
}
|
||||
|
||||
/// Sync with ghostty_diagnostic_s
|
||||
|
||||
@@ -69,6 +69,10 @@ pub const compatibility = std.StaticStringMap(
|
||||
// this behavior. This applies to selection too.
|
||||
.{ "cursor-invert-fg-bg", compatCursorInvertFgBg },
|
||||
.{ "selection-invert-fg-bg", compatSelectionInvertFgBg },
|
||||
|
||||
// Ghostty 1.2 merged `bold-is-bright` into the new `bold-color`
|
||||
// by setting the value to "bright".
|
||||
.{ "bold-is-bright", compatBoldIsBright },
|
||||
});
|
||||
|
||||
/// The font families to use.
|
||||
@@ -435,7 +439,7 @@ pub const compatibility = std.StaticStringMap(
|
||||
/// * `hinting` - Enable or disable hinting. Enabled by default.
|
||||
///
|
||||
/// * `force-autohint` - Always use the freetype auto-hinter instead of
|
||||
/// the font's native hinter. Enabled by default.
|
||||
/// the font's native hinter. Disabled by default.
|
||||
///
|
||||
/// * `monochrome` - Instructs renderer to use 1-bit monochrome rendering.
|
||||
/// This will disable anti-aliasing, and probably not look very good unless
|
||||
@@ -1046,6 +1050,14 @@ link: RepeatableLink = .{},
|
||||
/// `link`). If you want to customize URL matching, use `link` and disable this.
|
||||
@"link-url": bool = true,
|
||||
|
||||
/// Show link previews for a matched URL.
|
||||
///
|
||||
/// When true, link previews are shown for all matched URLs. When false, link
|
||||
/// previews are never shown. When set to "osc8", link previews are only shown
|
||||
/// for hyperlinks created with the OSC 8 sequence (in this case, the link text
|
||||
/// can differ from the link destination).
|
||||
@"link-previews": LinkPreviews = .true,
|
||||
|
||||
/// Whether to start the window in a maximized state. This setting applies
|
||||
/// to new windows and does not apply to tabs, splits, etc. However, this setting
|
||||
/// will apply to all new windows, not just the first one.
|
||||
@@ -2815,8 +2827,24 @@ else
|
||||
/// notifications using certain escape sequences such as OSC 9 or OSC 777.
|
||||
@"desktop-notifications": bool = true,
|
||||
|
||||
/// If `true`, the bold text will use the bright color palette.
|
||||
@"bold-is-bright": bool = false,
|
||||
/// Modifies the color used for bold text in the terminal.
|
||||
///
|
||||
/// This can be set to a specific color, using the same format as
|
||||
/// `background` or `foreground` (e.g. `#RRGGBB` but other formats
|
||||
/// are also supported; see the aforementioned documentation). If a
|
||||
/// specific color is set, this color will always be used for all
|
||||
/// bold text regardless of the terminal's color scheme.
|
||||
///
|
||||
/// This can also be set to `bright`, which uses the bright color palette
|
||||
/// for bold text. For example, if the text is red, then the bold will
|
||||
/// use the bright red color. The terminal palette is set with `palette`
|
||||
/// but can also be overridden by the terminal application itself using
|
||||
/// escape sequences such as OSC 4. (Since Ghostty 1.2.0, the previous
|
||||
/// configuration `bold-is-bright` is deprecated and replaced by this
|
||||
/// usage).
|
||||
///
|
||||
/// Available since Ghostty 1.2.0.
|
||||
@"bold-color": ?BoldColor = null,
|
||||
|
||||
/// This will be used to set the `TERM` environment variable.
|
||||
/// HACK: We set this with an `xterm` prefix because vim uses that to enable key
|
||||
@@ -3921,6 +3949,23 @@ fn compatSelectionInvertFgBg(
|
||||
return true;
|
||||
}
|
||||
|
||||
fn compatBoldIsBright(
|
||||
self: *Config,
|
||||
alloc: Allocator,
|
||||
key: []const u8,
|
||||
value_: ?[]const u8,
|
||||
) bool {
|
||||
_ = alloc;
|
||||
assert(std.mem.eql(u8, key, "bold-is-bright"));
|
||||
|
||||
const set = cli.args.parseBool(value_ orelse "t") catch return false;
|
||||
if (set) {
|
||||
self.@"bold-color" = .bright;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Create a shallow copy of this config. This will share all the memory
|
||||
/// allocated with the previous config but will have a new arena for
|
||||
/// any changes or new allocations. The config should have `deinit`
|
||||
@@ -4345,6 +4390,12 @@ pub const WindowSubtitle = enum {
|
||||
@"working-directory",
|
||||
};
|
||||
|
||||
pub const LinkPreviews = enum {
|
||||
false,
|
||||
true,
|
||||
osc8,
|
||||
};
|
||||
|
||||
/// Color represents a color using RGB.
|
||||
///
|
||||
/// This is a packed struct so that the C API to read color values just
|
||||
@@ -4542,6 +4593,58 @@ pub const TerminalColor = union(enum) {
|
||||
}
|
||||
};
|
||||
|
||||
/// Represents color values that can be used for bold. See `bold-color`.
|
||||
pub const BoldColor = union(enum) {
|
||||
color: Color,
|
||||
bright,
|
||||
|
||||
pub fn parseCLI(input_: ?[]const u8) !BoldColor {
|
||||
const input = input_ orelse return error.ValueRequired;
|
||||
if (std.mem.eql(u8, input, "bright")) return .bright;
|
||||
return .{ .color = try Color.parseCLI(input) };
|
||||
}
|
||||
|
||||
/// Used by Formatter
|
||||
pub fn formatEntry(self: BoldColor, formatter: anytype) !void {
|
||||
switch (self) {
|
||||
.color => try self.color.formatEntry(formatter),
|
||||
.bright => try formatter.formatEntry(
|
||||
[:0]const u8,
|
||||
@tagName(self),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
test "parseCLI" {
|
||||
const testing = std.testing;
|
||||
|
||||
try testing.expectEqual(
|
||||
BoldColor{ .color = Color{ .r = 78, .g = 42, .b = 132 } },
|
||||
try BoldColor.parseCLI("#4e2a84"),
|
||||
);
|
||||
try testing.expectEqual(
|
||||
BoldColor{ .color = Color{ .r = 0, .g = 0, .b = 0 } },
|
||||
try BoldColor.parseCLI("black"),
|
||||
);
|
||||
try testing.expectEqual(
|
||||
BoldColor.bright,
|
||||
try BoldColor.parseCLI("bright"),
|
||||
);
|
||||
|
||||
try testing.expectError(error.InvalidValue, BoldColor.parseCLI("a"));
|
||||
}
|
||||
|
||||
test "formatConfig" {
|
||||
const testing = std.testing;
|
||||
var buf = std.ArrayList(u8).init(testing.allocator);
|
||||
defer buf.deinit();
|
||||
|
||||
var sc: BoldColor = .bright;
|
||||
try sc.formatEntry(formatterpkg.entryFormatter("a", buf.writer()));
|
||||
try testing.expectEqualSlices(u8, "a = bright\n", buf.items);
|
||||
}
|
||||
};
|
||||
|
||||
pub const ColorList = struct {
|
||||
const Self = @This();
|
||||
|
||||
@@ -6552,8 +6655,9 @@ pub const RepeatableCommand = struct {
|
||||
try list.parseCLI(alloc, "title:Foo,action:ignore");
|
||||
try list.parseCLI(alloc, "title:Bar,description:bobr,action:text:ale bydle");
|
||||
try list.parseCLI(alloc, "title:Quux,description:boo,action:increase_font_size:2.5");
|
||||
try list.parseCLI(alloc, "title:Baz,description:Raspberry Pie,action:set_font_size:3.14");
|
||||
|
||||
try testing.expectEqual(@as(usize, 3), list.value.items.len);
|
||||
try testing.expectEqual(@as(usize, 4), list.value.items.len);
|
||||
|
||||
try testing.expectEqual(inputpkg.Binding.Action.ignore, list.value.items[0].action);
|
||||
try testing.expectEqualStrings("Foo", list.value.items[0].title);
|
||||
@@ -6570,6 +6674,13 @@ pub const RepeatableCommand = struct {
|
||||
try testing.expectEqualStrings("Quux", list.value.items[2].title);
|
||||
try testing.expectEqualStrings("boo", list.value.items[2].description);
|
||||
|
||||
try testing.expectEqual(
|
||||
inputpkg.Binding.Action{ .set_font_size = 3.14 },
|
||||
list.value.items[3].action,
|
||||
);
|
||||
try testing.expectEqualStrings("Baz", list.value.items[3].title);
|
||||
try testing.expectEqualStrings("Raspberry Pie", list.value.items[3].description);
|
||||
|
||||
try list.parseCLI(alloc, "");
|
||||
try testing.expectEqual(@as(usize, 0), list.value.items.len);
|
||||
}
|
||||
@@ -7105,7 +7216,7 @@ pub const FreetypeLoadFlags = packed struct {
|
||||
// for Freetype itself. Ghostty hasn't made any opinionated changes
|
||||
// to these defaults.
|
||||
hinting: bool = true,
|
||||
@"force-autohint": bool = true,
|
||||
@"force-autohint": bool = false,
|
||||
monochrome: bool = false,
|
||||
autohint: bool = true,
|
||||
};
|
||||
@@ -8235,3 +8346,23 @@ test "compatibility: removed selection-invert-fg-bg" {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
test "compatibility: removed bold-is-bright" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
{
|
||||
var cfg = try Config.default(alloc);
|
||||
defer cfg.deinit();
|
||||
var it: TestIterator = .{ .data = &.{
|
||||
"--bold-is-bright",
|
||||
} };
|
||||
try cfg.loadIter(alloc, &it);
|
||||
try cfg.finalize();
|
||||
|
||||
try testing.expectEqual(
|
||||
BoldColor.bright,
|
||||
cfg.@"bold-color",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,18 +5,19 @@ const Allocator = std.mem.Allocator;
|
||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
const internal_os = @import("../os/main.zig");
|
||||
|
||||
/// Open the configuration in the OS default editor according to the default
|
||||
/// paths the main config file could be in.
|
||||
/// The path to the configuration that should be opened for editing.
|
||||
///
|
||||
/// On Linux, this will open the file at the XDG config path. This is the
|
||||
/// On Linux, this will use the file at the XDG config path. This is the
|
||||
/// only valid path for Linux so we don't need to check for other paths.
|
||||
///
|
||||
/// On macOS, both XDG and AppSupport paths are valid. Because Ghostty
|
||||
/// prioritizes AppSupport over XDG, we will open AppSupport if it exists,
|
||||
/// prioritizes AppSupport over XDG, we will use AppSupport if it exists,
|
||||
/// followed by XDG if it exists, and finally AppSupport if neither exist.
|
||||
/// For the existence check, we also prefer non-empty files over empty
|
||||
/// files.
|
||||
pub fn open(alloc_gpa: Allocator) !void {
|
||||
///
|
||||
/// The returned value is allocated using the provided allocator.
|
||||
pub fn openPath(alloc_gpa: Allocator) ![:0]const u8 {
|
||||
// Use an arena to make memory management easier in here.
|
||||
var arena = ArenaAllocator.init(alloc_gpa);
|
||||
defer arena.deinit();
|
||||
@@ -41,7 +42,7 @@ pub fn open(alloc_gpa: Allocator) !void {
|
||||
}
|
||||
};
|
||||
|
||||
try internal_os.open(alloc_gpa, .text, config_path);
|
||||
return try alloc_gpa.dupeZ(u8, config_path);
|
||||
}
|
||||
|
||||
/// Returns the config path to use for open for the current OS.
|
||||
|
||||
@@ -69,10 +69,14 @@ pub fn deinit(self: *Collection, alloc: Allocator) void {
|
||||
if (self.load_options) |*v| v.deinit(alloc);
|
||||
}
|
||||
|
||||
pub const AddError = Allocator.Error || error{
|
||||
CollectionFull,
|
||||
DeferredLoadingUnavailable,
|
||||
};
|
||||
pub const AddError =
|
||||
Allocator.Error ||
|
||||
AdjustSizeError ||
|
||||
error{
|
||||
CollectionFull,
|
||||
DeferredLoadingUnavailable,
|
||||
SetSizeFailed,
|
||||
};
|
||||
|
||||
/// Add a face to the collection for the given style. This face will be added
|
||||
/// next in priority if others exist already, i.e. it'll be the _last_ to be
|
||||
@@ -81,10 +85,9 @@ pub const AddError = Allocator.Error || error{
|
||||
/// If no error is encountered then the collection takes ownership of the face,
|
||||
/// in which case face will be deallocated when the collection is deallocated.
|
||||
///
|
||||
/// If a loaded face is added to the collection, it should be the same
|
||||
/// size as all the other faces in the collection. This function will not
|
||||
/// verify or modify the size until the size of the entire collection is
|
||||
/// changed.
|
||||
/// If a loaded face is added to the collection, its size will be changed to
|
||||
/// match the size specified in load_options, adjusted for harmonization with
|
||||
/// the primary face.
|
||||
pub fn add(
|
||||
self: *Collection,
|
||||
alloc: Allocator,
|
||||
@@ -103,9 +106,107 @@ pub fn add(
|
||||
return error.DeferredLoadingUnavailable;
|
||||
|
||||
try list.append(alloc, face);
|
||||
|
||||
var owned: *Entry = list.at(idx);
|
||||
|
||||
// If the face is already loaded, apply font size adjustment
|
||||
// now, otherwise we'll apply it whenever we do load it.
|
||||
if (owned.getLoaded()) |loaded| {
|
||||
if (try self.adjustedSize(loaded)) |opts| {
|
||||
loaded.setSize(opts.faceOptions()) catch return error.SetSizeFailed;
|
||||
}
|
||||
}
|
||||
|
||||
return .{ .style = style, .idx = @intCast(idx) };
|
||||
}
|
||||
|
||||
pub const AdjustSizeError = font.Face.GetMetricsError;
|
||||
|
||||
// Calculate a size for the provided face that will match it with the primary
|
||||
// font, metrically, to improve consistency with fallback fonts. Right now we
|
||||
// match the font based on the ex height, or the ideograph width if the font
|
||||
// has ideographs in it.
|
||||
//
|
||||
// This returns null if load options is null or if self.load_options is null.
|
||||
//
|
||||
// This is very much like the `font-size-adjust` CSS property in how it works.
|
||||
// ref: https://developer.mozilla.org/en-US/docs/Web/CSS/font-size-adjust
|
||||
//
|
||||
// TODO: In the future, provide config options that allow the user to select
|
||||
// which metric should be matched for fallback fonts, instead of hard
|
||||
// coding it as ex height.
|
||||
pub fn adjustedSize(
|
||||
self: *Collection,
|
||||
face: *Face,
|
||||
) AdjustSizeError!?LoadOptions {
|
||||
const load_options = self.load_options orelse return null;
|
||||
|
||||
// We silently do nothing if we can't get the primary
|
||||
// face, because this might be the primary face itself.
|
||||
const primary_face = self.getFace(.{ .idx = 0 }) catch return null;
|
||||
|
||||
// We do nothing if the primary face and this face are the same.
|
||||
if (@intFromPtr(primary_face) == @intFromPtr(face)) return null;
|
||||
|
||||
const primary_metrics = try primary_face.getMetrics();
|
||||
const face_metrics = try face.getMetrics();
|
||||
|
||||
// We use the ex height to match our font sizes, so that the height of
|
||||
// lower-case letters matches between all fonts in the fallback chain.
|
||||
//
|
||||
// We estimate ex height as 0.75 * cap height if it's not specifically
|
||||
// provided, and we estimate cap height as 0.75 * ascent in the same case.
|
||||
//
|
||||
// If the fallback font has an ic_width we prefer that, for normalization
|
||||
// of CJK font sizes when mixed with latin fonts.
|
||||
//
|
||||
// We estimate the ic_width as twice the cell width if it isn't provided.
|
||||
var primary_cap = primary_metrics.cap_height orelse 0.0;
|
||||
if (primary_cap <= 0) primary_cap = primary_metrics.ascent * 0.75;
|
||||
|
||||
var primary_ex = primary_metrics.ex_height orelse 0.0;
|
||||
if (primary_ex <= 0) primary_ex = primary_cap * 0.75;
|
||||
|
||||
var primary_ic = primary_metrics.ic_width orelse 0.0;
|
||||
if (primary_ic <= 0) primary_ic = primary_metrics.cell_width * 2;
|
||||
|
||||
var face_cap = face_metrics.cap_height orelse 0.0;
|
||||
if (face_cap <= 0) face_cap = face_metrics.ascent * 0.75;
|
||||
|
||||
var face_ex = face_metrics.ex_height orelse 0.0;
|
||||
if (face_ex <= 0) face_ex = face_cap * 0.75;
|
||||
|
||||
var face_ic = face_metrics.ic_width orelse 0.0;
|
||||
if (face_ic <= 0) face_ic = face_metrics.cell_width * 2;
|
||||
|
||||
// If the line height of the scaled font would be larger than
|
||||
// the line height of the primary font, we don't want that, so
|
||||
// we take the minimum between matching the ic/ex and the line
|
||||
// height.
|
||||
//
|
||||
// NOTE: We actually allow the line height to be up to 1.2
|
||||
// times the primary line height because empirically
|
||||
// this is usually fine and is better for CJK.
|
||||
//
|
||||
// TODO: We should probably provide a config option that lets
|
||||
// the user pick what metric to use for size adjustment.
|
||||
const scale = @min(
|
||||
1.2 * primary_metrics.lineHeight() / face_metrics.lineHeight(),
|
||||
if (face_metrics.ic_width != null)
|
||||
primary_ic / face_ic
|
||||
else
|
||||
primary_ex / face_ex,
|
||||
);
|
||||
|
||||
// Make a copy of our load options, set the size to the size of
|
||||
// the provided face, and then multiply that by our scaling factor.
|
||||
var opts = load_options;
|
||||
opts.size = face.size;
|
||||
opts.size.points *= @as(f32, @floatCast(scale));
|
||||
|
||||
return opts;
|
||||
}
|
||||
|
||||
/// Return the Face represented by a given Index. The returned pointer
|
||||
/// is only valid as long as this collection is not modified.
|
||||
///
|
||||
@@ -129,21 +230,38 @@ pub fn getFace(self: *Collection, index: Index) !*Face {
|
||||
break :item item;
|
||||
};
|
||||
|
||||
return try self.getFaceFromEntry(item);
|
||||
const face = try self.getFaceFromEntry(
|
||||
item,
|
||||
// We only want to adjust the size if this isn't the primary face.
|
||||
index.style != .regular or index.idx > 0,
|
||||
);
|
||||
|
||||
return face;
|
||||
}
|
||||
|
||||
/// Get the face from an entry.
|
||||
///
|
||||
/// This entry must not be an alias.
|
||||
fn getFaceFromEntry(self: *Collection, entry: *Entry) !*Face {
|
||||
fn getFaceFromEntry(
|
||||
self: *Collection,
|
||||
entry: *Entry,
|
||||
/// Whether to adjust the font size to match the primary face after loading.
|
||||
adjust: bool,
|
||||
) !*Face {
|
||||
assert(entry.* != .alias);
|
||||
|
||||
return switch (entry.*) {
|
||||
inline .deferred, .fallback_deferred => |*d, tag| deferred: {
|
||||
const opts = self.load_options orelse
|
||||
return error.DeferredLoadingUnavailable;
|
||||
const face = try d.load(opts.library, opts.faceOptions());
|
||||
var face = try d.load(opts.library, opts.faceOptions());
|
||||
d.deinit();
|
||||
|
||||
// If we need to adjust the size, do so.
|
||||
if (adjust) if (try self.adjustedSize(&face)) |new_opts| {
|
||||
try face.setSize(new_opts.faceOptions());
|
||||
};
|
||||
|
||||
entry.* = switch (tag) {
|
||||
.deferred => .{ .loaded = face },
|
||||
.fallback_deferred => .{ .fallback_loaded = face },
|
||||
@@ -247,7 +365,7 @@ pub fn completeStyles(
|
||||
while (it.next()) |entry| {
|
||||
// Load our face. If we fail to load it, we just skip it and
|
||||
// continue on to try the next one.
|
||||
const face = self.getFaceFromEntry(entry) catch |err| {
|
||||
const face = self.getFaceFromEntry(entry, false) catch |err| {
|
||||
log.warn("error loading regular entry={d} err={}", .{
|
||||
it.index - 1,
|
||||
err,
|
||||
@@ -371,7 +489,7 @@ fn syntheticBold(self: *Collection, entry: *Entry) !Face {
|
||||
const opts = self.load_options orelse return error.DeferredLoadingUnavailable;
|
||||
|
||||
// Try to bold it.
|
||||
const regular = try self.getFaceFromEntry(entry);
|
||||
const regular = try self.getFaceFromEntry(entry, false);
|
||||
const face = try regular.syntheticBold(opts.faceOptions());
|
||||
|
||||
var buf: [256]u8 = undefined;
|
||||
@@ -391,7 +509,7 @@ fn syntheticItalic(self: *Collection, entry: *Entry) !Face {
|
||||
const opts = self.load_options orelse return error.DeferredLoadingUnavailable;
|
||||
|
||||
// Try to italicize it.
|
||||
const regular = try self.getFaceFromEntry(entry);
|
||||
const regular = try self.getFaceFromEntry(entry, false);
|
||||
const face = try regular.syntheticItalic(opts.faceOptions());
|
||||
|
||||
var buf: [256]u8 = undefined;
|
||||
@@ -420,9 +538,12 @@ pub fn setSize(self: *Collection, size: DesiredSize) !void {
|
||||
while (it.next()) |array| {
|
||||
var entry_it = array.value.iterator(0);
|
||||
while (entry_it.next()) |entry| switch (entry.*) {
|
||||
.loaded, .fallback_loaded => |*f| try f.setSize(
|
||||
opts.faceOptions(),
|
||||
),
|
||||
.loaded,
|
||||
.fallback_loaded,
|
||||
=> |*f| {
|
||||
const new_opts = try self.adjustedSize(f) orelse opts.*;
|
||||
try f.setSize(new_opts.faceOptions());
|
||||
},
|
||||
|
||||
// Deferred aren't loaded so we don't need to set their size.
|
||||
// The size for when they're loaded is set since `opts` changed.
|
||||
@@ -549,6 +670,16 @@ pub const Entry = union(enum) {
|
||||
}
|
||||
}
|
||||
|
||||
/// If this face is loaded, or is an alias to a loaded face,
|
||||
/// then this returns the `Face`, otherwise returns null.
|
||||
pub fn getLoaded(self: *Entry) ?*Face {
|
||||
return switch (self.*) {
|
||||
.deferred, .fallback_deferred => null,
|
||||
.loaded, .fallback_loaded => |*face| face,
|
||||
.alias => |v| v.getLoaded(),
|
||||
};
|
||||
}
|
||||
|
||||
/// True if the entry is deferred.
|
||||
fn isDeferred(self: Entry) bool {
|
||||
return switch (self) {
|
||||
@@ -906,12 +1037,13 @@ test "metrics" {
|
||||
|
||||
var c = init();
|
||||
defer c.deinit(alloc);
|
||||
c.load_options = .{ .library = lib };
|
||||
const size: DesiredSize = .{ .points = 12, .xdpi = 96, .ydpi = 96 };
|
||||
c.load_options = .{ .library = lib, .size = size };
|
||||
|
||||
_ = try c.add(alloc, .regular, .{ .loaded = try .init(
|
||||
lib,
|
||||
testFont,
|
||||
.{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } },
|
||||
.{ .size = size },
|
||||
) });
|
||||
|
||||
try c.updateMetrics();
|
||||
@@ -958,3 +1090,62 @@ test "metrics" {
|
||||
.cursor_height = 34,
|
||||
}, c.metrics);
|
||||
}
|
||||
|
||||
// TODO: Also test CJK fallback sizing, we don't currently have a CJK test font.
|
||||
test "adjusted sizes" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
const testFont = font.embedded.inconsolata;
|
||||
const fallback = font.embedded.monaspace_neon;
|
||||
|
||||
var lib = try Library.init(alloc);
|
||||
defer lib.deinit();
|
||||
|
||||
var c = init();
|
||||
defer c.deinit(alloc);
|
||||
const size: DesiredSize = .{ .points = 12, .xdpi = 96, .ydpi = 96 };
|
||||
c.load_options = .{ .library = lib, .size = size };
|
||||
|
||||
// Add our primary face.
|
||||
_ = try c.add(alloc, .regular, .{ .loaded = try .init(
|
||||
lib,
|
||||
testFont,
|
||||
.{ .size = size },
|
||||
) });
|
||||
|
||||
try c.updateMetrics();
|
||||
|
||||
// Add the fallback face.
|
||||
const fallback_idx = try c.add(alloc, .regular, .{ .loaded = try .init(
|
||||
lib,
|
||||
fallback,
|
||||
.{ .size = size },
|
||||
) });
|
||||
|
||||
// The ex heights should match.
|
||||
{
|
||||
const primary_metrics = try (try c.getFace(.{ .idx = 0 })).getMetrics();
|
||||
const fallback_metrics = try (try c.getFace(fallback_idx)).getMetrics();
|
||||
|
||||
try std.testing.expectApproxEqAbs(
|
||||
primary_metrics.ex_height.?,
|
||||
fallback_metrics.ex_height.?,
|
||||
// We accept anything within half a pixel.
|
||||
0.5,
|
||||
);
|
||||
}
|
||||
|
||||
// Resize should keep that relationship.
|
||||
try c.setSize(.{ .points = 37, .xdpi = 96, .ydpi = 96 });
|
||||
{
|
||||
const primary_metrics = try (try c.getFace(.{ .idx = 0 })).getMetrics();
|
||||
const fallback_metrics = try (try c.getFace(fallback_idx)).getMetrics();
|
||||
|
||||
try std.testing.expectApproxEqAbs(
|
||||
primary_metrics.ex_height.?,
|
||||
fallback_metrics.ex_height.?,
|
||||
// We accept anything within half a pixel.
|
||||
0.5,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,9 +17,3 @@ offset_y: i32,
|
||||
/// be normalized to be between 0 and 1 prior to use in shaders.
|
||||
atlas_x: u32,
|
||||
atlas_y: u32,
|
||||
|
||||
/// horizontal position to increase drawing position for strings
|
||||
advance_x: f32,
|
||||
|
||||
/// Whether we drew this glyph ourselves with the sprite font.
|
||||
sprite: bool = false,
|
||||
|
||||
@@ -107,6 +107,19 @@ pub const FaceMetrics = struct {
|
||||
/// a provided ex height metric or measured from the height of the
|
||||
/// lowercase x glyph.
|
||||
ex_height: ?f64 = null,
|
||||
|
||||
/// The width of the character "水" (CJK water ideograph, U+6C34),
|
||||
/// if present. This is used for font size adjustment, to normalize
|
||||
/// the width of CJK fonts mixed with latin fonts.
|
||||
///
|
||||
/// NOTE: IC = Ideograph Character
|
||||
ic_width: ?f64 = null,
|
||||
|
||||
/// Convenience function for getting the line height
|
||||
/// (ascent - descent + line_gap).
|
||||
pub inline fn lineHeight(self: FaceMetrics) f64 {
|
||||
return self.ascent - self.descent + self.line_gap;
|
||||
}
|
||||
};
|
||||
|
||||
/// Calculate our metrics based on values extracted from a font.
|
||||
|
||||
@@ -147,6 +147,9 @@ pub const RenderOptions = struct {
|
||||
/// Maximum ratio of width to height when resizing.
|
||||
max_xy_ratio: ?f64 = null,
|
||||
|
||||
/// Maximum number of cells horizontally to use.
|
||||
max_constraint_width: u2 = 2,
|
||||
|
||||
pub const Size = enum {
|
||||
/// Don't change the size of this glyph.
|
||||
none,
|
||||
@@ -186,16 +189,26 @@ pub const RenderOptions = struct {
|
||||
pub fn constrain(
|
||||
self: Constraint,
|
||||
glyph: GlyphSize,
|
||||
/// Available width
|
||||
/// Width of one cell.
|
||||
cell_width: f64,
|
||||
/// Available height
|
||||
/// Height of one cell.
|
||||
cell_height: f64,
|
||||
/// Number of cells horizontally available for this glyph.
|
||||
constraint_width: u2,
|
||||
) GlyphSize {
|
||||
var g = glyph;
|
||||
|
||||
const w = cell_width -
|
||||
self.pad_left * cell_width -
|
||||
self.pad_right * cell_width;
|
||||
const available_width =
|
||||
cell_width * @as(f64, @floatFromInt(
|
||||
@min(
|
||||
self.max_constraint_width,
|
||||
constraint_width,
|
||||
),
|
||||
));
|
||||
|
||||
const w = available_width -
|
||||
self.pad_left * available_width -
|
||||
self.pad_right * available_width;
|
||||
const h = cell_height -
|
||||
self.pad_top * cell_height -
|
||||
self.pad_bottom * cell_height;
|
||||
@@ -203,7 +216,7 @@ pub const RenderOptions = struct {
|
||||
// Subtract padding from the bearings so that our
|
||||
// alignment and sizing code works correctly. We
|
||||
// re-add before returning.
|
||||
g.x -= self.pad_left * cell_width;
|
||||
g.x -= self.pad_left * available_width;
|
||||
g.y -= self.pad_bottom * cell_height;
|
||||
|
||||
switch (self.size_horizontal) {
|
||||
@@ -305,7 +318,7 @@ pub const RenderOptions = struct {
|
||||
}
|
||||
|
||||
// Re-add our padding before returning.
|
||||
g.x += self.pad_left * cell_width;
|
||||
g.x += self.pad_left * available_width;
|
||||
g.y += self.pad_bottom * cell_height;
|
||||
|
||||
return g;
|
||||
|
||||
@@ -31,6 +31,9 @@ pub const Face = struct {
|
||||
/// tables).
|
||||
color: ?ColorState = null,
|
||||
|
||||
/// The current size this font is set to.
|
||||
size: font.face.DesiredSize,
|
||||
|
||||
/// True if our build is using Harfbuzz. If we're not, we can avoid
|
||||
/// some Harfbuzz-specific code paths.
|
||||
const harfbuzz_shaper = font.options.backend.hasHarfbuzz();
|
||||
@@ -106,6 +109,7 @@ pub const Face = struct {
|
||||
.font = ct_font,
|
||||
.hb_font = hb_font,
|
||||
.color = color,
|
||||
.size = opts.size,
|
||||
};
|
||||
result.quirks_disable_default_font_features = quirks.disableDefaultFontFeatures(&result);
|
||||
|
||||
@@ -333,11 +337,10 @@ pub const Face = struct {
|
||||
.offset_y = 0,
|
||||
.atlas_x = 0,
|
||||
.atlas_y = 0,
|
||||
.advance_x = 0,
|
||||
};
|
||||
|
||||
const metrics = opts.grid_metrics;
|
||||
const cell_width: f64 = @floatFromInt(metrics.cell_width * opts.constraint_width);
|
||||
const cell_width: f64 = @floatFromInt(metrics.cell_width);
|
||||
const cell_height: f64 = @floatFromInt(metrics.cell_height);
|
||||
|
||||
const glyph_size = opts.constraint.constrain(
|
||||
@@ -349,6 +352,7 @@ pub const Face = struct {
|
||||
},
|
||||
cell_width,
|
||||
cell_height,
|
||||
opts.constraint_width,
|
||||
);
|
||||
|
||||
const width = glyph_size.width;
|
||||
@@ -356,8 +360,16 @@ pub const Face = struct {
|
||||
const x = glyph_size.x;
|
||||
const y = glyph_size.y;
|
||||
|
||||
const px_width: u32 = @intFromFloat(@ceil(width));
|
||||
const px_height: u32 = @intFromFloat(@ceil(height));
|
||||
// We have to include the fractional pixels that we won't be offsetting
|
||||
// in our width and height calculations, that is, we offset by the floor
|
||||
// of the bearings when we render the glyph, meaning there's still a bit
|
||||
// of extra width to the area that's drawn in beyond just the width of
|
||||
// the glyph itself, so we include that extra fraction of a pixel when
|
||||
// calculating the width and height here.
|
||||
const frac_x = rect.origin.x - @floor(rect.origin.x);
|
||||
const frac_y = rect.origin.y - @floor(rect.origin.y);
|
||||
const px_width: u32 = @intFromFloat(@ceil(width + frac_x));
|
||||
const px_height: u32 = @intFromFloat(@ceil(height + frac_y));
|
||||
|
||||
// Settings that are specific to if we are rendering text or emoji.
|
||||
const color: struct {
|
||||
@@ -475,26 +487,44 @@ pub const Face = struct {
|
||||
// This should be the distance from the left of
|
||||
// the cell to the left of the glyph's bounding box.
|
||||
const offset_x: i32 = offset_x: {
|
||||
var result: i32 = @intFromFloat(@round(x));
|
||||
|
||||
// If our cell was resized then we adjust our glyph's
|
||||
// position relative to the new center. This keeps glyphs
|
||||
// centered in the cell whether it was made wider or narrower.
|
||||
if (metrics.original_cell_width) |original_width| {
|
||||
const before: i32 = @intCast(original_width);
|
||||
const after: i32 = @intCast(metrics.cell_width);
|
||||
// Increase the offset by half of the difference
|
||||
// between the widths to keep things centered.
|
||||
result += @divTrunc(after - before, 2);
|
||||
// If the glyph's advance is narrower than the cell width then we
|
||||
// center the advance of the glyph within the cell width. At first
|
||||
// I implemented this to proportionally scale the center position
|
||||
// of the glyph but that messes up glyphs that are meant to align
|
||||
// vertically with others, so this is a compromise.
|
||||
//
|
||||
// This makes it so that when the `adjust-cell-width` config is
|
||||
// used, or when a fallback font with a different advance width
|
||||
// is used, we don't get weirdly aligned glyphs.
|
||||
//
|
||||
// We don't do this if the constraint has a horizontal alignment,
|
||||
// since in that case the position was already calculated with the
|
||||
// new cell width in mind.
|
||||
if (opts.constraint.align_horizontal == .none) {
|
||||
var advances: [glyphs.len]macos.graphics.Size = undefined;
|
||||
_ = self.font.getAdvancesForGlyphs(.horizontal, &glyphs, &advances);
|
||||
const advance = advances[0].width;
|
||||
const new_advance =
|
||||
cell_width * @as(f64, @floatFromInt(opts.cell_width orelse 1));
|
||||
// If the original advance is greater than the cell width then
|
||||
// it's possible that this is a ligature or other glyph that is
|
||||
// intended to overflow the cell to one side or the other, and
|
||||
// adjusting the bearings could mess that up, so we just leave
|
||||
// it alone if that's the case.
|
||||
//
|
||||
// We also don't want to do anything if the advance is zero or
|
||||
// less, since this is used for stuff like combining characters.
|
||||
if (advance > new_advance or advance <= 0.0) {
|
||||
break :offset_x @intFromFloat(@ceil(x - frac_x));
|
||||
}
|
||||
break :offset_x @intFromFloat(
|
||||
@ceil(x - frac_x + (new_advance - advance) / 2),
|
||||
);
|
||||
} else {
|
||||
break :offset_x @intFromFloat(@ceil(x - frac_x));
|
||||
}
|
||||
|
||||
break :offset_x result;
|
||||
};
|
||||
|
||||
// Get our advance
|
||||
var advances: [glyphs.len]macos.graphics.Size = undefined;
|
||||
_ = self.font.getAdvancesForGlyphs(.horizontal, &glyphs, &advances);
|
||||
|
||||
return .{
|
||||
.width = px_width,
|
||||
.height = px_height,
|
||||
@@ -502,7 +532,6 @@ pub const Face = struct {
|
||||
.offset_y = offset_y,
|
||||
.atlas_x = region.x,
|
||||
.atlas_y = region.y,
|
||||
.advance_x = @floatCast(advances[0].width),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -734,6 +763,20 @@ pub const Face = struct {
|
||||
break :cell_width max;
|
||||
};
|
||||
|
||||
// Measure "水" (CJK water ideograph, U+6C34) for our ic width.
|
||||
const ic_width: ?f64 = ic_width: {
|
||||
const glyph = self.glyphIndex('水') orelse break :ic_width null;
|
||||
|
||||
var advances: [1]macos.graphics.Size = undefined;
|
||||
_ = ct_font.getAdvancesForGlyphs(
|
||||
.horizontal,
|
||||
&.{@intCast(glyph)},
|
||||
&advances,
|
||||
);
|
||||
|
||||
break :ic_width advances[0].width;
|
||||
};
|
||||
|
||||
return .{
|
||||
.cell_width = cell_width,
|
||||
.ascent = ascent,
|
||||
@@ -745,6 +788,7 @@ pub const Face = struct {
|
||||
.strikethrough_thickness = strikethrough_thickness,
|
||||
.cap_height = cap_height,
|
||||
.ex_height = ex_height,
|
||||
.ic_width = ic_width,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -59,6 +59,9 @@ pub const Face = struct {
|
||||
bold: bool = false,
|
||||
} = .{},
|
||||
|
||||
/// The current size this font is set to.
|
||||
size: font.face.DesiredSize,
|
||||
|
||||
/// Initialize a new font face with the given source in-memory.
|
||||
pub fn initFile(
|
||||
lib: Library,
|
||||
@@ -107,6 +110,7 @@ pub const Face = struct {
|
||||
.hb_font = hb_font,
|
||||
.ft_mutex = ft_mutex,
|
||||
.load_flags = opts.freetype_load_flags,
|
||||
.size = opts.size,
|
||||
};
|
||||
result.quirks_disable_default_font_features = quirks.disableDefaultFontFeatures(&result);
|
||||
|
||||
@@ -203,6 +207,7 @@ pub const Face = struct {
|
||||
/// for clearing any glyph caches, font atlas data, etc.
|
||||
pub fn setSize(self: *Face, opts: font.face.Options) !void {
|
||||
try setSize_(self.face, opts.size);
|
||||
self.size = opts.size;
|
||||
}
|
||||
|
||||
fn setSize_(face: freetype.Face, size: font.face.DesiredSize) !void {
|
||||
@@ -348,7 +353,7 @@ pub const Face = struct {
|
||||
|
||||
// use options from config
|
||||
.no_hinting = !do_hinting,
|
||||
.force_autohint = !self.load_flags.@"force-autohint",
|
||||
.force_autohint = self.load_flags.@"force-autohint",
|
||||
.no_autohint = !self.load_flags.autohint,
|
||||
|
||||
// NO_SVG set to true because we don't currently support rendering
|
||||
@@ -373,7 +378,6 @@ pub const Face = struct {
|
||||
.offset_y = 0,
|
||||
.atlas_x = 0,
|
||||
.atlas_y = 0,
|
||||
.advance_x = 0,
|
||||
};
|
||||
|
||||
// For synthetic bold, we embolden the glyph.
|
||||
@@ -390,7 +394,7 @@ pub const Face = struct {
|
||||
// Next we need to apply any constraints.
|
||||
const metrics = opts.grid_metrics;
|
||||
|
||||
const cell_width: f64 = @floatFromInt(metrics.cell_width * opts.constraint_width);
|
||||
const cell_width: f64 = @floatFromInt(metrics.cell_width);
|
||||
const cell_height: f64 = @floatFromInt(metrics.cell_height);
|
||||
|
||||
const glyph_x: f64 = f26dot6ToF64(glyph.*.metrics.horiBearingX);
|
||||
@@ -405,6 +409,7 @@ pub const Face = struct {
|
||||
},
|
||||
cell_width,
|
||||
cell_height,
|
||||
opts.constraint_width,
|
||||
);
|
||||
|
||||
const width = glyph_size.width;
|
||||
@@ -638,20 +643,40 @@ pub const Face = struct {
|
||||
// This should be the distance from the left of
|
||||
// the cell to the left of the glyph's bounding box.
|
||||
const offset_x: i32 = offset_x: {
|
||||
var result: i32 = @intFromFloat(@floor(x));
|
||||
|
||||
// If our cell was resized then we adjust our glyph's
|
||||
// position relative to the new center. This keeps glyphs
|
||||
// centered in the cell whether it was made wider or narrower.
|
||||
if (metrics.original_cell_width) |original_width| {
|
||||
const before: i32 = @intCast(original_width);
|
||||
const after: i32 = @intCast(metrics.cell_width);
|
||||
// Increase the offset by half of the difference
|
||||
// between the widths to keep things centered.
|
||||
result += @divTrunc(after - before, 2);
|
||||
// If the glyph's advance is narrower than the cell width then we
|
||||
// center the advance of the glyph within the cell width. At first
|
||||
// I implemented this to proportionally scale the center position
|
||||
// of the glyph but that messes up glyphs that are meant to align
|
||||
// vertically with others, so this is a compromise.
|
||||
//
|
||||
// This makes it so that when the `adjust-cell-width` config is
|
||||
// used, or when a fallback font with a different advance width
|
||||
// is used, we don't get weirdly aligned glyphs.
|
||||
//
|
||||
// We don't do this if the constraint has a horizontal alignment,
|
||||
// since in that case the position was already calculated with the
|
||||
// new cell width in mind.
|
||||
if (opts.constraint.align_horizontal == .none) {
|
||||
const advance = f26dot6ToFloat(glyph.*.advance.x);
|
||||
const new_advance =
|
||||
cell_width * @as(f64, @floatFromInt(opts.cell_width orelse 1));
|
||||
// If the original advance is greater than the cell width then
|
||||
// it's possible that this is a ligature or other glyph that is
|
||||
// intended to overflow the cell to one side or the other, and
|
||||
// adjusting the bearings could mess that up, so we just leave
|
||||
// it alone if that's the case.
|
||||
//
|
||||
// We also don't want to do anything if the advance is zero or
|
||||
// less, since this is used for stuff like combining characters.
|
||||
if (advance > new_advance or advance <= 0.0) {
|
||||
break :offset_x @intFromFloat(@floor(x));
|
||||
}
|
||||
break :offset_x @intFromFloat(
|
||||
@floor(x + (new_advance - advance) / 2),
|
||||
);
|
||||
} else {
|
||||
break :offset_x @intFromFloat(@floor(x));
|
||||
}
|
||||
|
||||
break :offset_x result;
|
||||
};
|
||||
|
||||
return Glyph{
|
||||
@@ -661,7 +686,6 @@ pub const Face = struct {
|
||||
.offset_y = offset_y,
|
||||
.atlas_x = region.x,
|
||||
.atlas_y = region.y,
|
||||
.advance_x = f26dot6ToFloat(glyph.*.advance.x),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -832,7 +856,7 @@ pub const Face = struct {
|
||||
while (c < 127) : (c += 1) {
|
||||
if (face.getCharIndex(c)) |glyph_index| {
|
||||
if (face.loadGlyph(glyph_index, .{
|
||||
.render = true,
|
||||
.render = false,
|
||||
.no_svg = true,
|
||||
})) {
|
||||
max = @max(
|
||||
@@ -870,7 +894,7 @@ pub const Face = struct {
|
||||
defer self.ft_mutex.unlock();
|
||||
if (face.getCharIndex('H')) |glyph_index| {
|
||||
if (face.loadGlyph(glyph_index, .{
|
||||
.render = true,
|
||||
.render = false,
|
||||
.no_svg = true,
|
||||
})) {
|
||||
break :cap f26dot6ToF64(face.handle.*.glyph.*.metrics.height);
|
||||
@@ -883,7 +907,7 @@ pub const Face = struct {
|
||||
defer self.ft_mutex.unlock();
|
||||
if (face.getCharIndex('x')) |glyph_index| {
|
||||
if (face.loadGlyph(glyph_index, .{
|
||||
.render = true,
|
||||
.render = false,
|
||||
.no_svg = true,
|
||||
})) {
|
||||
break :ex f26dot6ToF64(face.handle.*.glyph.*.metrics.height);
|
||||
@@ -894,6 +918,21 @@ pub const Face = struct {
|
||||
};
|
||||
};
|
||||
|
||||
// Measure "水" (CJK water ideograph, U+6C34) for our ic width.
|
||||
const ic_width: ?f64 = ic_width: {
|
||||
self.ft_mutex.lock();
|
||||
defer self.ft_mutex.unlock();
|
||||
|
||||
const glyph = face.getCharIndex('水') orelse break :ic_width null;
|
||||
|
||||
face.loadGlyph(glyph, .{
|
||||
.render = false,
|
||||
.no_svg = true,
|
||||
}) catch break :ic_width null;
|
||||
|
||||
break :ic_width f26dot6ToF64(face.handle.*.glyph.*.advance.x);
|
||||
};
|
||||
|
||||
return .{
|
||||
.cell_width = cell_width,
|
||||
|
||||
@@ -909,6 +948,7 @@ pub const Face = struct {
|
||||
|
||||
.cap_height = cap_height,
|
||||
.ex_height = ex_height,
|
||||
.ic_width = ic_width,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -235,7 +235,6 @@ pub const Face = struct {
|
||||
.offset_y = 0,
|
||||
.atlas_x = region.x,
|
||||
.atlas_y = region.y,
|
||||
.advance_x = 0,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! This is a generate file, produced by nerd_font_codegen.py
|
||||
//! This is a generated file, produced by nerd_font_codegen.py
|
||||
//! DO NOT EDIT BY HAND!
|
||||
//!
|
||||
//! This file provides info extracted from the nerd fonts patcher script,
|
||||
@@ -13,6 +13,7 @@ pub fn getConstraint(cp: u21) Constraint {
|
||||
=> .{
|
||||
.size_horizontal = .stretch,
|
||||
.size_vertical = .stretch,
|
||||
.max_constraint_width = 1,
|
||||
.align_horizontal = .center,
|
||||
.align_vertical = .center,
|
||||
.pad_left = -0.02,
|
||||
@@ -24,24 +25,29 @@ pub fn getConstraint(cp: u21) Constraint {
|
||||
=> .{
|
||||
.size_horizontal = .cover,
|
||||
.size_vertical = .fit,
|
||||
.max_constraint_width = 1,
|
||||
.align_horizontal = .center,
|
||||
.align_vertical = .center,
|
||||
.pad_left = 0.1,
|
||||
.pad_right = 0.1,
|
||||
.pad_top = 0.01,
|
||||
.pad_bottom = 0.01,
|
||||
.pad_top = 0.1,
|
||||
.pad_bottom = 0.1,
|
||||
},
|
||||
0x276c...0x2771,
|
||||
=> .{
|
||||
.size_horizontal = .cover,
|
||||
.size_vertical = .fit,
|
||||
.max_constraint_width = 1,
|
||||
.align_horizontal = .center,
|
||||
.align_vertical = .center,
|
||||
.pad_top = 0.15,
|
||||
.pad_bottom = 0.15,
|
||||
},
|
||||
0xe0b0,
|
||||
=> .{
|
||||
.size_horizontal = .stretch,
|
||||
.size_vertical = .stretch,
|
||||
.max_constraint_width = 1,
|
||||
.align_horizontal = .start,
|
||||
.align_vertical = .center,
|
||||
.pad_left = -0.06,
|
||||
@@ -54,6 +60,7 @@ pub fn getConstraint(cp: u21) Constraint {
|
||||
=> .{
|
||||
.size_horizontal = .stretch,
|
||||
.size_vertical = .stretch,
|
||||
.max_constraint_width = 1,
|
||||
.align_horizontal = .start,
|
||||
.align_vertical = .center,
|
||||
.max_xy_ratio = 0.7,
|
||||
@@ -62,6 +69,7 @@ pub fn getConstraint(cp: u21) Constraint {
|
||||
=> .{
|
||||
.size_horizontal = .stretch,
|
||||
.size_vertical = .stretch,
|
||||
.max_constraint_width = 1,
|
||||
.align_horizontal = .end,
|
||||
.align_vertical = .center,
|
||||
.pad_left = -0.06,
|
||||
@@ -74,6 +82,7 @@ pub fn getConstraint(cp: u21) Constraint {
|
||||
=> .{
|
||||
.size_horizontal = .stretch,
|
||||
.size_vertical = .stretch,
|
||||
.max_constraint_width = 1,
|
||||
.align_horizontal = .end,
|
||||
.align_vertical = .center,
|
||||
.max_xy_ratio = 0.7,
|
||||
@@ -82,6 +91,7 @@ pub fn getConstraint(cp: u21) Constraint {
|
||||
=> .{
|
||||
.size_horizontal = .stretch,
|
||||
.size_vertical = .stretch,
|
||||
.max_constraint_width = 1,
|
||||
.align_horizontal = .start,
|
||||
.align_vertical = .center,
|
||||
.pad_left = -0.06,
|
||||
@@ -94,6 +104,7 @@ pub fn getConstraint(cp: u21) Constraint {
|
||||
=> .{
|
||||
.size_horizontal = .stretch,
|
||||
.size_vertical = .stretch,
|
||||
.max_constraint_width = 1,
|
||||
.align_horizontal = .start,
|
||||
.align_vertical = .center,
|
||||
.max_xy_ratio = 0.5,
|
||||
@@ -102,6 +113,7 @@ pub fn getConstraint(cp: u21) Constraint {
|
||||
=> .{
|
||||
.size_horizontal = .stretch,
|
||||
.size_vertical = .stretch,
|
||||
.max_constraint_width = 1,
|
||||
.align_horizontal = .end,
|
||||
.align_vertical = .center,
|
||||
.pad_left = -0.06,
|
||||
@@ -114,6 +126,7 @@ pub fn getConstraint(cp: u21) Constraint {
|
||||
=> .{
|
||||
.size_horizontal = .stretch,
|
||||
.size_vertical = .stretch,
|
||||
.max_constraint_width = 1,
|
||||
.align_horizontal = .end,
|
||||
.align_vertical = .center,
|
||||
.max_xy_ratio = 0.5,
|
||||
@@ -123,6 +136,7 @@ pub fn getConstraint(cp: u21) Constraint {
|
||||
=> .{
|
||||
.size_horizontal = .stretch,
|
||||
.size_vertical = .stretch,
|
||||
.max_constraint_width = 1,
|
||||
.align_horizontal = .start,
|
||||
.align_vertical = .center,
|
||||
.pad_left = -0.05,
|
||||
@@ -135,6 +149,7 @@ pub fn getConstraint(cp: u21) Constraint {
|
||||
=> .{
|
||||
.size_horizontal = .stretch,
|
||||
.size_vertical = .stretch,
|
||||
.max_constraint_width = 1,
|
||||
.align_horizontal = .start,
|
||||
.align_vertical = .center,
|
||||
},
|
||||
@@ -143,6 +158,7 @@ pub fn getConstraint(cp: u21) Constraint {
|
||||
=> .{
|
||||
.size_horizontal = .stretch,
|
||||
.size_vertical = .stretch,
|
||||
.max_constraint_width = 1,
|
||||
.align_horizontal = .end,
|
||||
.align_vertical = .center,
|
||||
.pad_left = -0.05,
|
||||
@@ -155,6 +171,7 @@ pub fn getConstraint(cp: u21) Constraint {
|
||||
=> .{
|
||||
.size_horizontal = .stretch,
|
||||
.size_vertical = .stretch,
|
||||
.max_constraint_width = 1,
|
||||
.align_horizontal = .end,
|
||||
.align_vertical = .center,
|
||||
},
|
||||
@@ -204,8 +221,8 @@ pub fn getConstraint(cp: u21) Constraint {
|
||||
.align_vertical = .center,
|
||||
.pad_left = 0.03,
|
||||
.pad_right = 0.03,
|
||||
.pad_top = 0.01,
|
||||
.pad_bottom = 0.01,
|
||||
.pad_top = 0.03,
|
||||
.pad_bottom = 0.03,
|
||||
.max_xy_ratio = 0.86,
|
||||
},
|
||||
0xe0c5,
|
||||
@@ -216,8 +233,8 @@ pub fn getConstraint(cp: u21) Constraint {
|
||||
.align_vertical = .center,
|
||||
.pad_left = 0.03,
|
||||
.pad_right = 0.03,
|
||||
.pad_top = 0.01,
|
||||
.pad_bottom = 0.01,
|
||||
.pad_top = 0.03,
|
||||
.pad_bottom = 0.03,
|
||||
.max_xy_ratio = 0.86,
|
||||
},
|
||||
0xe0c6,
|
||||
@@ -228,8 +245,8 @@ pub fn getConstraint(cp: u21) Constraint {
|
||||
.align_vertical = .center,
|
||||
.pad_left = 0.03,
|
||||
.pad_right = 0.03,
|
||||
.pad_top = 0.01,
|
||||
.pad_bottom = 0.01,
|
||||
.pad_top = 0.03,
|
||||
.pad_bottom = 0.03,
|
||||
.max_xy_ratio = 0.78,
|
||||
},
|
||||
0xe0c7,
|
||||
@@ -240,8 +257,8 @@ pub fn getConstraint(cp: u21) Constraint {
|
||||
.align_vertical = .center,
|
||||
.pad_left = 0.03,
|
||||
.pad_right = 0.03,
|
||||
.pad_top = 0.01,
|
||||
.pad_bottom = 0.01,
|
||||
.pad_top = 0.03,
|
||||
.pad_bottom = 0.03,
|
||||
.max_xy_ratio = 0.78,
|
||||
},
|
||||
0xe0cc,
|
||||
@@ -285,6 +302,7 @@ pub fn getConstraint(cp: u21) Constraint {
|
||||
=> .{
|
||||
.size_horizontal = .stretch,
|
||||
.size_vertical = .stretch,
|
||||
.max_constraint_width = 1,
|
||||
.align_horizontal = .start,
|
||||
.align_vertical = .center,
|
||||
.pad_left = -0.02,
|
||||
@@ -297,6 +315,7 @@ pub fn getConstraint(cp: u21) Constraint {
|
||||
=> .{
|
||||
.size_horizontal = .stretch,
|
||||
.size_vertical = .stretch,
|
||||
.max_constraint_width = 1,
|
||||
.align_horizontal = .end,
|
||||
.align_vertical = .center,
|
||||
.pad_left = -0.02,
|
||||
@@ -309,6 +328,7 @@ pub fn getConstraint(cp: u21) Constraint {
|
||||
=> .{
|
||||
.size_horizontal = .stretch,
|
||||
.size_vertical = .stretch,
|
||||
.max_constraint_width = 1,
|
||||
.align_horizontal = .start,
|
||||
.align_vertical = .center,
|
||||
.pad_left = -0.05,
|
||||
@@ -321,6 +341,7 @@ pub fn getConstraint(cp: u21) Constraint {
|
||||
=> .{
|
||||
.size_horizontal = .stretch,
|
||||
.size_vertical = .stretch,
|
||||
.max_constraint_width = 1,
|
||||
.align_horizontal = .end,
|
||||
.align_vertical = .center,
|
||||
.pad_left = -0.05,
|
||||
|
||||
@@ -1,102 +1,124 @@
|
||||
"""
|
||||
This file is mostly vibe coded because I don't like Python. It extracts the
|
||||
patch sets from the nerd fonts font patcher file in order to extract scaling
|
||||
rules and attributes for different codepoint ranges which it then codegens
|
||||
in to a Zig file with a function that switches over codepoints and returns
|
||||
the attributes and scaling rules.
|
||||
This file extracts the patch sets from the nerd fonts font patcher file in order to
|
||||
extract scaling rules and attributes for different codepoint ranges which it then
|
||||
codegens in to a Zig file with a function that switches over codepoints and returns the
|
||||
attributes and scaling rules.
|
||||
|
||||
This does include an `eval` call! This is spooky, but we trust
|
||||
the nerd fonts code to be safe and not malicious or anything.
|
||||
This does include an `eval` call! This is spooky, but we trust the nerd fonts code to
|
||||
be safe and not malicious or anything.
|
||||
|
||||
This script requires Python 3.12 or greater.
|
||||
"""
|
||||
|
||||
import ast
|
||||
import math
|
||||
from pathlib import Path
|
||||
from collections import defaultdict
|
||||
from contextlib import suppress
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from typing import Literal, TypedDict, cast
|
||||
|
||||
type PatchSetAttributes = dict[Literal["default"] | int, PatchSetAttributeEntry]
|
||||
type AttributeHash = tuple[str | None, str | None, str, float, float, float]
|
||||
type ResolvedSymbol = PatchSetAttributes | PatchSetScaleRules | int | None
|
||||
|
||||
|
||||
class PatchSetScaleRules(TypedDict):
|
||||
ShiftMode: str
|
||||
ScaleGroups: list[list[int] | range]
|
||||
|
||||
|
||||
class PatchSetAttributeEntry(TypedDict):
|
||||
align: str
|
||||
valign: str
|
||||
stretch: str
|
||||
params: dict[str, float | bool]
|
||||
|
||||
|
||||
class PatchSet(TypedDict):
|
||||
SymStart: int
|
||||
SymEnd: int
|
||||
SrcStart: int | None
|
||||
ScaleRules: PatchSetScaleRules | None
|
||||
Attributes: PatchSetAttributes
|
||||
|
||||
|
||||
class PatchSetExtractor(ast.NodeVisitor):
|
||||
def __init__(self):
|
||||
self.symbol_table = {}
|
||||
self.patch_set_values = []
|
||||
def __init__(self) -> None:
|
||||
self.symbol_table: dict[str, ast.expr] = {}
|
||||
self.patch_set_values: list[PatchSet] = []
|
||||
|
||||
def visit_ClassDef(self, node):
|
||||
if node.name == "font_patcher":
|
||||
for item in node.body:
|
||||
if isinstance(item, ast.FunctionDef) and item.name == "setup_patch_set":
|
||||
self.visit_setup_patch_set(item)
|
||||
def visit_ClassDef(self, node: ast.ClassDef) -> None:
|
||||
if node.name != "font_patcher":
|
||||
return
|
||||
for item in node.body:
|
||||
if isinstance(item, ast.FunctionDef) and item.name == "setup_patch_set":
|
||||
self.visit_setup_patch_set(item)
|
||||
|
||||
def visit_setup_patch_set(self, node):
|
||||
def visit_setup_patch_set(self, node: ast.FunctionDef) -> None:
|
||||
# First pass: gather variable assignments
|
||||
for stmt in node.body:
|
||||
if isinstance(stmt, ast.Assign):
|
||||
# Store simple variable assignments in the symbol table
|
||||
if len(stmt.targets) == 1 and isinstance(stmt.targets[0], ast.Name):
|
||||
var_name = stmt.targets[0].id
|
||||
self.symbol_table[var_name] = stmt.value
|
||||
match stmt:
|
||||
case ast.Assign(targets=[ast.Name(id=symbol)]):
|
||||
# Store simple variable assignments in the symbol table
|
||||
self.symbol_table[symbol] = stmt.value
|
||||
|
||||
# Second pass: process self.patch_set
|
||||
for stmt in node.body:
|
||||
if isinstance(stmt, ast.Assign):
|
||||
for target in stmt.targets:
|
||||
if isinstance(target, ast.Attribute) and target.attr == "patch_set":
|
||||
if isinstance(stmt.value, ast.List):
|
||||
for elt in stmt.value.elts:
|
||||
if isinstance(elt, ast.Dict):
|
||||
self.process_patch_entry(elt)
|
||||
if not isinstance(stmt, ast.Assign):
|
||||
continue
|
||||
for target in stmt.targets:
|
||||
if (
|
||||
isinstance(target, ast.Attribute)
|
||||
and target.attr == "patch_set"
|
||||
and isinstance(stmt.value, ast.List)
|
||||
):
|
||||
for elt in stmt.value.elts:
|
||||
if isinstance(elt, ast.Dict):
|
||||
self.process_patch_entry(elt)
|
||||
|
||||
def resolve_symbol(self, node):
|
||||
def resolve_symbol(self, node: ast.expr) -> ResolvedSymbol:
|
||||
"""Resolve named variables to their actual values from the symbol table."""
|
||||
if isinstance(node, ast.Name) and node.id in self.symbol_table:
|
||||
return self.safe_literal_eval(self.symbol_table[node.id])
|
||||
return self.safe_literal_eval(node)
|
||||
|
||||
def safe_literal_eval(self, node):
|
||||
def safe_literal_eval(self, node: ast.expr) -> ResolvedSymbol:
|
||||
"""Try to evaluate or stringify an AST node."""
|
||||
try:
|
||||
return ast.literal_eval(node)
|
||||
except Exception:
|
||||
except ValueError:
|
||||
# Spooky eval! But we trust nerd fonts to be safe...
|
||||
if hasattr(ast, "unparse"):
|
||||
return eval(
|
||||
ast.unparse(node), {"box_keep": True}, {"self": SpoofSelf()}
|
||||
ast.unparse(node),
|
||||
{"box_keep": True},
|
||||
{"self": SimpleNamespace(args=SimpleNamespace(careful=True))},
|
||||
)
|
||||
else:
|
||||
return f"<cannot eval: {type(node).__name__}>"
|
||||
msg = f"<cannot eval: {type(node).__name__}>"
|
||||
raise ValueError(msg) from None
|
||||
|
||||
def process_patch_entry(self, dict_node):
|
||||
def process_patch_entry(self, dict_node: ast.Dict) -> None:
|
||||
entry = {}
|
||||
disallowed_key_nodes = frozenset({"Enabled", "Name", "Filename", "Exact"})
|
||||
for key_node, value_node in zip(dict_node.keys, dict_node.values):
|
||||
if isinstance(key_node, ast.Constant) and key_node.value in (
|
||||
"Enabled",
|
||||
"Name",
|
||||
"Filename",
|
||||
"Exact",
|
||||
if (
|
||||
isinstance(key_node, ast.Constant)
|
||||
and key_node.value not in disallowed_key_nodes
|
||||
):
|
||||
continue
|
||||
key = ast.literal_eval(key_node)
|
||||
value = self.resolve_symbol(value_node)
|
||||
entry[key] = value
|
||||
self.patch_set_values.append(entry)
|
||||
key = ast.literal_eval(cast("ast.Constant", key_node))
|
||||
entry[key] = self.resolve_symbol(value_node)
|
||||
self.patch_set_values.append(cast("PatchSet", entry))
|
||||
|
||||
|
||||
def extract_patch_set_values(source_code):
|
||||
def extract_patch_set_values(source_code: str) -> list[PatchSet]:
|
||||
tree = ast.parse(source_code)
|
||||
extractor = PatchSetExtractor()
|
||||
extractor.visit(tree)
|
||||
return extractor.patch_set_values
|
||||
|
||||
|
||||
# We have to spoof `self` and `self.args` for the eval.
|
||||
class SpoofArgs:
|
||||
careful = True
|
||||
|
||||
|
||||
class SpoofSelf:
|
||||
args = SpoofArgs()
|
||||
|
||||
|
||||
def parse_alignment(val):
|
||||
def parse_alignment(val: str) -> str | None:
|
||||
return {
|
||||
"l": ".start",
|
||||
"r": ".end",
|
||||
@@ -105,28 +127,24 @@ def parse_alignment(val):
|
||||
}.get(val, ".none")
|
||||
|
||||
|
||||
def get_param(d, key, default):
|
||||
return float(d.get(key, default))
|
||||
|
||||
|
||||
def attr_key(attr):
|
||||
def attr_key(attr: PatchSetAttributeEntry) -> AttributeHash:
|
||||
"""Convert attributes to a hashable key for grouping."""
|
||||
stretch = attr.get("stretch", "")
|
||||
params = attr.get("params", {})
|
||||
return (
|
||||
parse_alignment(attr.get("align", "")),
|
||||
parse_alignment(attr.get("valign", "")),
|
||||
stretch,
|
||||
float(attr.get("params", {}).get("overlap", 0.0)),
|
||||
float(attr.get("params", {}).get("xy-ratio", -1.0)),
|
||||
float(attr.get("params", {}).get("ypadding", 0.0)),
|
||||
attr.get("stretch", ""),
|
||||
float(params.get("overlap", 0.0)),
|
||||
float(params.get("xy-ratio", -1.0)),
|
||||
float(params.get("ypadding", 0.0)),
|
||||
)
|
||||
|
||||
|
||||
def coalesce_codepoints_to_ranges(codepoints):
|
||||
def coalesce_codepoints_to_ranges(codepoints: list[int]) -> list[tuple[int, int]]:
|
||||
"""Convert a sorted list of integers to a list of single values and ranges."""
|
||||
ranges = []
|
||||
ranges: list[tuple[int, int]] = []
|
||||
cp_iter = iter(sorted(codepoints))
|
||||
try:
|
||||
with suppress(StopIteration):
|
||||
start = prev = next(cp_iter)
|
||||
for cp in cp_iter:
|
||||
if cp == prev + 1:
|
||||
@@ -135,88 +153,96 @@ def coalesce_codepoints_to_ranges(codepoints):
|
||||
ranges.append((start, prev))
|
||||
start = prev = cp
|
||||
ranges.append((start, prev))
|
||||
except StopIteration:
|
||||
pass
|
||||
return ranges
|
||||
|
||||
|
||||
def emit_zig_entry_multikey(codepoints, attr):
|
||||
def emit_zig_entry_multikey(codepoints: list[int], attr: PatchSetAttributeEntry) -> str:
|
||||
align = parse_alignment(attr.get("align", ""))
|
||||
valign = parse_alignment(attr.get("valign", ""))
|
||||
stretch = attr.get("stretch", "")
|
||||
params = attr.get("params", {})
|
||||
|
||||
overlap = get_param(params, "overlap", 0.0)
|
||||
xy_ratio = get_param(params, "xy-ratio", -1.0)
|
||||
y_padding = get_param(params, "ypadding", 0.0)
|
||||
overlap = params.get("overlap", 0.0)
|
||||
xy_ratio = params.get("xy-ratio", -1.0)
|
||||
y_padding = params.get("ypadding", 0.0)
|
||||
|
||||
ranges = coalesce_codepoints_to_ranges(codepoints)
|
||||
keys = "\n".join(
|
||||
f" 0x{start:x}...0x{end:x}," if start != end else f" 0x{start:x},"
|
||||
f" {start:#x}...{end:#x}," if start != end else f" {start:#x},"
|
||||
for start, end in ranges
|
||||
)
|
||||
|
||||
s = f"""{keys}
|
||||
=> .{{\n"""
|
||||
s = f"{keys}\n => .{{\n"
|
||||
|
||||
# These translations don't quite capture the way
|
||||
# the actual patcher does scaling, but they're a
|
||||
# good enough compromise.
|
||||
if ("xy" in stretch):
|
||||
if "xy" in stretch:
|
||||
s += " .size_horizontal = .stretch,\n"
|
||||
s += " .size_vertical = .stretch,\n"
|
||||
elif ("!" in stretch):
|
||||
elif "!" in stretch:
|
||||
s += " .size_horizontal = .cover,\n"
|
||||
s += " .size_vertical = .fit,\n"
|
||||
elif ("^" in stretch):
|
||||
elif "^" in stretch:
|
||||
s += " .size_horizontal = .cover,\n"
|
||||
s += " .size_vertical = .cover,\n"
|
||||
else:
|
||||
s += " .size_horizontal = .fit,\n"
|
||||
s += " .size_vertical = .fit,\n"
|
||||
|
||||
if (align is not None):
|
||||
# There are two cases where we want to limit the constraint width to 1:
|
||||
# - If there's a `1` in the stretch mode string.
|
||||
# - If the stretch mode is `xy` and there's not an explicit `2`.
|
||||
if "1" in stretch or ("xy" in stretch and "2" not in stretch):
|
||||
s += " .max_constraint_width = 1,\n"
|
||||
|
||||
if align is not None:
|
||||
s += f" .align_horizontal = {align},\n"
|
||||
if (valign is not None):
|
||||
if valign is not None:
|
||||
s += f" .align_vertical = {valign},\n"
|
||||
|
||||
if (overlap != 0.0):
|
||||
# `overlap` and `ypadding` are mutually exclusive,
|
||||
# this is asserted in the nerd fonts patcher itself.
|
||||
if overlap:
|
||||
pad = -overlap
|
||||
s += f" .pad_left = {pad},\n"
|
||||
s += f" .pad_right = {pad},\n"
|
||||
v_pad = y_padding - math.copysign(min(0.01, abs(overlap)), overlap)
|
||||
# In the nerd fonts patcher, overlap values
|
||||
# are capped at 0.01 in the vertical direction.
|
||||
v_pad = -min(0.01, overlap)
|
||||
s += f" .pad_top = {v_pad},\n"
|
||||
s += f" .pad_bottom = {v_pad},\n"
|
||||
elif y_padding:
|
||||
s += f" .pad_top = {y_padding / 2},\n"
|
||||
s += f" .pad_bottom = {y_padding / 2},\n"
|
||||
|
||||
if (xy_ratio > 0):
|
||||
if xy_ratio > 0:
|
||||
s += f" .max_xy_ratio = {xy_ratio},\n"
|
||||
|
||||
s += " },"
|
||||
|
||||
return s
|
||||
|
||||
def generate_zig_switch_arms(patch_set):
|
||||
entries = {}
|
||||
for entry in patch_set:
|
||||
|
||||
def generate_zig_switch_arms(patch_sets: list[PatchSet]) -> str:
|
||||
entries: dict[int, PatchSetAttributeEntry] = {}
|
||||
for entry in patch_sets:
|
||||
attributes = entry["Attributes"]
|
||||
|
||||
for cp in range(entry["SymStart"], entry["SymEnd"] + 1):
|
||||
entries[cp] = attributes["default"]
|
||||
|
||||
for k, v in attributes.items():
|
||||
if isinstance(k, int):
|
||||
entries[k] = v
|
||||
entries |= {k: v for k, v in attributes.items() if isinstance(k, int)}
|
||||
|
||||
del entries[0]
|
||||
|
||||
# Group codepoints by attribute key
|
||||
grouped = defaultdict(list)
|
||||
grouped = defaultdict[AttributeHash, list[int]](list)
|
||||
for cp, attr in entries.items():
|
||||
grouped[attr_key(attr)].append(cp)
|
||||
|
||||
# Emit zig switch arms
|
||||
result = []
|
||||
for _, codepoints in sorted(grouped.items(), key=lambda x: x[1]):
|
||||
result: list[str] = []
|
||||
for codepoints in sorted(grouped.values()):
|
||||
# Use one of the attrs in the group to emit the value
|
||||
attr = entries[codepoints[0]]
|
||||
result.append(emit_zig_entry_multikey(codepoints, attr))
|
||||
@@ -225,23 +251,16 @@ def generate_zig_switch_arms(patch_set):
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
path = (
|
||||
Path(__file__).resolve().parent
|
||||
/ ".."
|
||||
/ ".."
|
||||
/ "vendor"
|
||||
/ "nerd-fonts"
|
||||
/ "font-patcher.py"
|
||||
)
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
source = f.read()
|
||||
project_root = Path(__file__).resolve().parents[2]
|
||||
|
||||
patcher_path = project_root / "vendor" / "nerd-fonts" / "font-patcher.py"
|
||||
source = patcher_path.read_text(encoding="utf-8")
|
||||
patch_set = extract_patch_set_values(source)
|
||||
|
||||
out_path = Path(__file__).resolve().parent / "nerd_font_attributes.zig"
|
||||
out_path = project_root / "src" / "font" / "nerd_font_attributes.zig"
|
||||
|
||||
with open(out_path, "w", encoding="utf-8") as f:
|
||||
f.write("""//! This is a generate file, produced by nerd_font_codegen.py
|
||||
with out_path.open("w", encoding="utf-8") as f:
|
||||
f.write("""//! This is a generated file, produced by nerd_font_codegen.py
|
||||
//! DO NOT EDIT BY HAND!
|
||||
//!
|
||||
//! This file provides info extracted from the nerd fonts patcher script,
|
||||
@@ -254,6 +273,4 @@ pub fn getConstraint(cp: u21) Constraint {
|
||||
return switch (cp) {
|
||||
""")
|
||||
f.write(generate_zig_switch_arms(patch_set))
|
||||
f.write("\n")
|
||||
|
||||
f.write(" else => .none,\n };\n}\n")
|
||||
f.write("\n else => .none,\n };\n}\n")
|
||||
|
||||
@@ -195,7 +195,6 @@ pub fn renderGlyph(
|
||||
.offset_y = 0,
|
||||
.atlas_x = 0,
|
||||
.atlas_y = 0,
|
||||
.advance_x = 0,
|
||||
};
|
||||
|
||||
const metrics = self.metrics;
|
||||
@@ -227,8 +226,6 @@ pub fn renderGlyph(
|
||||
.offset_y = @as(i32, @intCast(region.height +| canvas.clip_bottom)) - @as(i32, @intCast(padding_y)),
|
||||
.atlas_x = region.x,
|
||||
.atlas_y = region.y,
|
||||
.advance_x = @floatFromInt(width),
|
||||
.sprite = true,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -140,24 +140,7 @@ pub const Canvas = struct {
|
||||
const region_height = sfc_height -| self.clip_top -| self.clip_bottom;
|
||||
|
||||
// Allocate our texture atlas region
|
||||
const region = region: {
|
||||
// Reserve a region with a 1px margin on the bottom and right edges
|
||||
// so that we can avoid interpolation between adjacent glyphs during
|
||||
// texture sampling.
|
||||
var region = try atlas.reserve(
|
||||
alloc,
|
||||
region_width + 1,
|
||||
region_height + 1,
|
||||
);
|
||||
|
||||
// Modify the region to remove the margin so that we write to the
|
||||
// non-zero location. The data in an Altlas is always initialized
|
||||
// to zero (Atlas.clear) so we don't need to worry about zero-ing
|
||||
// that.
|
||||
region.width -= 1;
|
||||
region.height -= 1;
|
||||
break :region region;
|
||||
};
|
||||
const region = try atlas.reserve(alloc, region_width, region_height);
|
||||
|
||||
if (region.width > 0 and region.height > 0) {
|
||||
const buffer: []u8 = @ptrCast(self.sfc.image_surface_alpha8.buf);
|
||||
|
||||
@@ -281,6 +281,10 @@ pub const Action = union(enum) {
|
||||
/// If there is a URL under the cursor, copy it to the default clipboard.
|
||||
copy_url_to_clipboard,
|
||||
|
||||
/// Copy the terminal title to the clipboard. If the terminal title is not
|
||||
/// set or is empty this has no effect.
|
||||
copy_title_to_clipboard,
|
||||
|
||||
/// Increase the font size by the specified amount in points (pt).
|
||||
///
|
||||
/// For example, `increase_font_size:1.5` will increase the font size
|
||||
@@ -296,6 +300,12 @@ pub const Action = union(enum) {
|
||||
/// Reset the font size to the original configured size.
|
||||
reset_font_size,
|
||||
|
||||
/// Set the font size to the specified size in points (pt).
|
||||
///
|
||||
/// For example, `set_font_size:14.5` will set the font size
|
||||
/// to 14.5 points.
|
||||
set_font_size: f32,
|
||||
|
||||
/// Clear the screen and all scrollback.
|
||||
clear_screen,
|
||||
|
||||
@@ -999,11 +1009,13 @@ pub const Action = union(enum) {
|
||||
.reset,
|
||||
.copy_to_clipboard,
|
||||
.copy_url_to_clipboard,
|
||||
.copy_title_to_clipboard,
|
||||
.paste_from_clipboard,
|
||||
.paste_from_selection,
|
||||
.increase_font_size,
|
||||
.decrease_font_size,
|
||||
.reset_font_size,
|
||||
.set_font_size,
|
||||
.prompt_surface_title,
|
||||
.clear_screen,
|
||||
.select_all,
|
||||
@@ -3065,6 +3077,7 @@ test "set: getEvent codepoint case folding" {
|
||||
try testing.expect(action == null);
|
||||
}
|
||||
}
|
||||
|
||||
test "Action: clone" {
|
||||
const testing = std.testing;
|
||||
var arena = std.heap.ArenaAllocator.init(testing.allocator);
|
||||
@@ -3083,3 +3096,42 @@ test "Action: clone" {
|
||||
try testing.expect(b == .text);
|
||||
}
|
||||
}
|
||||
|
||||
test "parse: increase_font_size" {
|
||||
const testing = std.testing;
|
||||
|
||||
{
|
||||
const binding = try parseSingle("a=increase_font_size:1.5");
|
||||
try testing.expect(binding.action == .increase_font_size);
|
||||
try testing.expectEqual(1.5, binding.action.increase_font_size);
|
||||
}
|
||||
}
|
||||
|
||||
test "parse: decrease_font_size" {
|
||||
const testing = std.testing;
|
||||
|
||||
{
|
||||
const binding = try parseSingle("a=decrease_font_size:2.5");
|
||||
try testing.expect(binding.action == .decrease_font_size);
|
||||
try testing.expectEqual(2.5, binding.action.decrease_font_size);
|
||||
}
|
||||
}
|
||||
|
||||
test "parse: reset_font_size" {
|
||||
const testing = std.testing;
|
||||
|
||||
{
|
||||
const binding = try parseSingle("a=reset_font_size");
|
||||
try testing.expect(binding.action == .reset_font_size);
|
||||
}
|
||||
}
|
||||
|
||||
test "parse: set_font_size" {
|
||||
const testing = std.testing;
|
||||
|
||||
{
|
||||
const binding = try parseSingle("a=set_font_size:13.5");
|
||||
try testing.expect(binding.action == .set_font_size);
|
||||
try testing.expectEqual(13.5, binding.action.set_font_size);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const Action = @import("Binding.zig").Action;
|
||||
@@ -131,6 +132,12 @@ fn actionCommands(action: Action.Key) []const Command {
|
||||
.description = "Copy the URL under the cursor to the clipboard.",
|
||||
}},
|
||||
|
||||
.copy_title_to_clipboard => comptime &.{.{
|
||||
.action = .copy_title_to_clipboard,
|
||||
.title = "Copy Terminal Title to Clipboard",
|
||||
.description = "Copy the terminal title to the clipboard. If the terminal title is not set this has no effect.",
|
||||
}},
|
||||
|
||||
.paste_from_clipboard => comptime &.{.{
|
||||
.action = .paste_from_clipboard,
|
||||
.title = "Paste from Clipboard",
|
||||
@@ -460,6 +467,7 @@ fn actionCommands(action: Action.Key) []const Command {
|
||||
.esc,
|
||||
.text,
|
||||
.cursor_key,
|
||||
.set_font_size,
|
||||
.scroll_page_fractional,
|
||||
.scroll_page_lines,
|
||||
.adjust_selection,
|
||||
|
||||
@@ -19,7 +19,12 @@ const internal_os = @import("os/main.zig");
|
||||
|
||||
// Some comptime assertions that our C API depends on.
|
||||
comptime {
|
||||
assert(apprt.runtime == apprt.embedded);
|
||||
// We allow tests to reference this file because we unit test
|
||||
// some of the C API. At runtime though we should never get these
|
||||
// functions unless we are building libghostty.
|
||||
if (!builtin.is_test) {
|
||||
assert(apprt.runtime == apprt.embedded);
|
||||
}
|
||||
}
|
||||
|
||||
/// Global options so we can log. This is identical to main.
|
||||
@@ -29,7 +34,9 @@ comptime {
|
||||
// These structs need to be referenced so the `export` functions
|
||||
// are truly exported by the C API lib.
|
||||
_ = @import("config.zig").CAPI;
|
||||
_ = apprt.runtime.CAPI;
|
||||
if (@hasDecl(apprt.runtime, "CAPI")) {
|
||||
_ = apprt.runtime.CAPI;
|
||||
}
|
||||
}
|
||||
|
||||
/// ghostty_info_s
|
||||
@@ -46,17 +53,29 @@ const Info = extern struct {
|
||||
};
|
||||
};
|
||||
|
||||
/// Initialize ghostty global state. It is possible to have more than
|
||||
/// one global state but it has zero practical benefit.
|
||||
export fn ghostty_init() c_int {
|
||||
/// ghostty_string_s
|
||||
pub const String = extern struct {
|
||||
ptr: ?[*]const u8,
|
||||
len: usize,
|
||||
|
||||
pub const empty: String = .{
|
||||
.ptr = null,
|
||||
.len = 0,
|
||||
};
|
||||
|
||||
pub fn fromSlice(slice: []const u8) String {
|
||||
return .{
|
||||
.ptr = slice.ptr,
|
||||
.len = slice.len,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// Initialize ghostty global state.
|
||||
export fn ghostty_init(argc: usize, argv: [*][*:0]u8) c_int {
|
||||
assert(builtin.link_libc);
|
||||
|
||||
// Since in the lib we don't go through start.zig, we need
|
||||
// to populate argv so that inspecting std.os.argv doesn't
|
||||
// touch uninitialized memory.
|
||||
var argv: [0][*:0]u8 = .{};
|
||||
std.os.argv = &argv;
|
||||
|
||||
std.os.argv = argv[0..argc];
|
||||
state.init() catch |err| {
|
||||
std.log.err("failed to initialize ghostty error={}", .{err});
|
||||
return 1;
|
||||
@@ -65,15 +84,17 @@ export fn ghostty_init() c_int {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// This is the entrypoint for the CLI version of Ghostty. This
|
||||
/// is mutually exclusive to ghostty_init. Do NOT run ghostty_init
|
||||
/// if you are going to run this. This will not return.
|
||||
export fn ghostty_cli_main(argc: usize, argv: [*][*:0]u8) noreturn {
|
||||
std.os.argv = argv[0..argc];
|
||||
main.main() catch |err| {
|
||||
std.log.err("failed to run ghostty error={}", .{err});
|
||||
/// Runs an action if it is specified. If there is no action this returns
|
||||
/// false. If there is an action then this doesn't return.
|
||||
export fn ghostty_cli_try_action() void {
|
||||
const action = state.action orelse return;
|
||||
std.log.info("executing CLI action={}", .{action});
|
||||
posix.exit(action.run(state.alloc) catch |err| {
|
||||
std.log.err("CLI action failed error={}", .{err});
|
||||
posix.exit(1);
|
||||
};
|
||||
});
|
||||
|
||||
posix.exit(0);
|
||||
}
|
||||
|
||||
/// Return metadata about Ghostty, such as version, build mode, etc.
|
||||
@@ -99,3 +120,8 @@ export fn ghostty_info() Info {
|
||||
export fn ghostty_translate(msgid: [*:0]const u8) [*:0]const u8 {
|
||||
return internal_os.i18n._(msgid);
|
||||
}
|
||||
|
||||
/// Free a string allocated by Ghostty.
|
||||
export fn ghostty_string_free(str: String) void {
|
||||
state.alloc.free(str.ptr.?[0..str.len]);
|
||||
}
|
||||
|
||||
@@ -24,8 +24,15 @@ pub fn launchedFromDesktop() bool {
|
||||
// This special case is so that if we launch the app via the
|
||||
// app bundle (i.e. via open) then we still treat it as if it
|
||||
// was launched from the desktop.
|
||||
if (build_config.artifact == .lib and
|
||||
posix.getenv("GHOSTTY_MAC_APP") != null) break :macos true;
|
||||
if (build_config.artifact == .lib) lib: {
|
||||
const env = "GHOSTTY_MAC_LAUNCH_SOURCE";
|
||||
const source = posix.getenv(env) orelse break :lib;
|
||||
|
||||
// Source can be "app", "cli", or "zig_run". We assume
|
||||
// its the desktop only if its "app". We may want to do
|
||||
// "zig_run" but at the moment there's no reason.
|
||||
if (std.mem.eql(u8, source, "app")) break :macos true;
|
||||
}
|
||||
|
||||
break :macos c.getppid() == 1;
|
||||
},
|
||||
|
||||
@@ -49,6 +49,7 @@ pub const locales = [_][:0]const u8{
|
||||
"ca_ES.UTF-8",
|
||||
"bg_BG.UTF-8",
|
||||
"ga_IE.UTF-8",
|
||||
"he_IL.UTF-8",
|
||||
};
|
||||
|
||||
/// Set for faster membership lookup of locales.
|
||||
|
||||
27
src/os/kernel_info.zig
Normal file
27
src/os/kernel_info.zig
Normal file
@@ -0,0 +1,27 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
|
||||
pub fn getKernelInfo(alloc: std.mem.Allocator) ?[]const u8 {
|
||||
if (comptime builtin.os.tag != .linux) return null;
|
||||
const path = "/proc/sys/kernel/osrelease";
|
||||
var file = std.fs.openFileAbsolute(path, .{}) catch return null;
|
||||
defer file.close();
|
||||
|
||||
// 128 bytes should be enough to hold the kernel information
|
||||
const kernel_info = file.readToEndAlloc(alloc, 128) catch return null;
|
||||
defer alloc.free(kernel_info);
|
||||
return alloc.dupe(u8, std.mem.trim(u8, kernel_info, &std.ascii.whitespace)) catch return null;
|
||||
}
|
||||
|
||||
test "read /proc/sys/kernel/osrelease" {
|
||||
if (comptime builtin.os.tag != .linux) return null;
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
const kernel_info = try getKernelInfo(allocator);
|
||||
defer allocator.free(kernel_info);
|
||||
|
||||
// Since we can't hardcode the info in tests, just check
|
||||
// if something was read from the file
|
||||
try std.testing.expect(kernel_info.len > 0);
|
||||
try std.testing.expect(!std.mem.eql(u8, kernel_info, ""));
|
||||
}
|
||||
@@ -14,6 +14,7 @@ const openpkg = @import("open.zig");
|
||||
const pipepkg = @import("pipe.zig");
|
||||
const resourcesdir = @import("resourcesdir.zig");
|
||||
const systemd = @import("systemd.zig");
|
||||
const kernelInfo = @import("kernel_info.zig");
|
||||
|
||||
// Namespaces
|
||||
pub const args = @import("args.zig");
|
||||
@@ -58,6 +59,7 @@ pub const pipe = pipepkg.pipe;
|
||||
pub const resourcesDir = resourcesdir.resourcesDir;
|
||||
pub const ResourcesDir = resourcesdir.ResourcesDir;
|
||||
pub const ShellEscapeWriter = shell.ShellEscapeWriter;
|
||||
pub const getKernelInfo = kernelInfo.getKernelInfo;
|
||||
|
||||
test {
|
||||
_ = i18n;
|
||||
|
||||
@@ -1,24 +1,23 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const apprt = @import("../apprt.zig");
|
||||
|
||||
const log = std.log.scoped(.@"os-open");
|
||||
|
||||
/// The type of the data at the URL to open. This is used as a hint
|
||||
/// to potentially open the URL in a different way.
|
||||
pub const Type = enum {
|
||||
text,
|
||||
unknown,
|
||||
};
|
||||
|
||||
/// Open a URL in the default handling application.
|
||||
///
|
||||
/// Any output on stderr is logged as a warning in the application logs.
|
||||
/// Output on stdout is ignored. The allocator is used to buffer the
|
||||
/// log output and may allocate from another thread.
|
||||
///
|
||||
/// This function is purposely simple for the sake of providing
|
||||
/// some portable way to open URLs. If you are implementing an
|
||||
/// apprt for Ghostty, you should consider doing something special-cased
|
||||
/// for your platform.
|
||||
pub fn open(
|
||||
alloc: Allocator,
|
||||
typ: Type,
|
||||
kind: apprt.action.OpenUrl.Kind,
|
||||
url: []const u8,
|
||||
) !void {
|
||||
var exe: std.process.Child = switch (builtin.os.tag) {
|
||||
@@ -33,7 +32,7 @@ pub fn open(
|
||||
),
|
||||
|
||||
.macos => .init(
|
||||
switch (typ) {
|
||||
switch (kind) {
|
||||
.text => &.{ "open", "-t", url },
|
||||
.unknown => &.{ "open", url },
|
||||
},
|
||||
|
||||
@@ -356,6 +356,10 @@ pub inline fn textureOptions(self: OpenGL) Texture.Options {
|
||||
.format = .rgba,
|
||||
.internal_format = .srgba,
|
||||
.target = .@"2D",
|
||||
.min_filter = .linear,
|
||||
.mag_filter = .linear,
|
||||
.wrap_s = .clamp_to_edge,
|
||||
.wrap_t = .clamp_to_edge,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -388,6 +392,16 @@ pub inline fn imageTextureOptions(
|
||||
.format = format.toPixelFormat(),
|
||||
.internal_format = if (srgb) .srgba else .rgba,
|
||||
.target = .@"2D",
|
||||
// TODO: Generate mipmaps for image textures and use
|
||||
// linear_mipmap_linear filtering so that they
|
||||
// look good even when scaled way down.
|
||||
.min_filter = .linear,
|
||||
.mag_filter = .linear,
|
||||
// TODO: Separate out background image options, use
|
||||
// repeating coordinate modes so we don't have
|
||||
// to do the modulus in the shader.
|
||||
.wrap_s = .clamp_to_edge,
|
||||
.wrap_t = .clamp_to_edge,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -409,6 +423,10 @@ pub fn initAtlasTexture(
|
||||
.format = format,
|
||||
.internal_format = internal_format,
|
||||
.target = .Rectangle,
|
||||
.min_filter = .nearest,
|
||||
.mag_filter = .nearest,
|
||||
.wrap_s = .clamp_to_edge,
|
||||
.wrap_t = .clamp_to_edge,
|
||||
},
|
||||
atlas.size,
|
||||
atlas.size,
|
||||
|
||||
@@ -519,7 +519,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
foreground: terminal.color.RGB,
|
||||
selection_background: ?configpkg.Config.TerminalColor,
|
||||
selection_foreground: ?configpkg.Config.TerminalColor,
|
||||
bold_is_bright: bool,
|
||||
bold_color: ?configpkg.BoldColor,
|
||||
min_contrast: f32,
|
||||
padding_color: configpkg.WindowPaddingColor,
|
||||
custom_shaders: configpkg.RepeatablePath,
|
||||
@@ -580,7 +580,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
|
||||
.background = config.background.toTerminalRGB(),
|
||||
.foreground = config.foreground.toTerminalRGB(),
|
||||
.bold_is_bright = config.@"bold-is-bright",
|
||||
.bold_color = config.@"bold-color",
|
||||
|
||||
.min_contrast = @floatCast(config.@"minimum-contrast"),
|
||||
.padding_color = config.@"window-padding-color",
|
||||
|
||||
@@ -2540,10 +2541,11 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
// the cell style (SGR), before applying any additional
|
||||
// configuration, inversions, selections, etc.
|
||||
const bg_style = style.bg(cell, color_palette);
|
||||
const fg_style = style.fg(
|
||||
color_palette,
|
||||
self.config.bold_is_bright,
|
||||
) orelse self.foreground_color orelse self.default_foreground_color;
|
||||
const fg_style = style.fg(.{
|
||||
.default = self.foreground_color orelse self.default_foreground_color,
|
||||
.palette = color_palette,
|
||||
.bold = self.config.bold_color,
|
||||
});
|
||||
|
||||
// The final background color for the cell.
|
||||
const bg = bg: {
|
||||
@@ -2801,10 +2803,11 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
.@"cell-background",
|
||||
=> |_, tag| {
|
||||
const sty = screen.cursor.page_pin.style(screen.cursor.page_cell);
|
||||
const fg_style = sty.fg(
|
||||
color_palette,
|
||||
self.config.bold_is_bright,
|
||||
) orelse self.foreground_color orelse self.default_foreground_color;
|
||||
const fg_style = sty.fg(.{
|
||||
.default = self.foreground_color orelse self.default_foreground_color,
|
||||
.palette = color_palette,
|
||||
.bold = self.config.bold_color,
|
||||
});
|
||||
const bg_style = sty.bg(
|
||||
screen.cursor.page_cell,
|
||||
color_palette,
|
||||
@@ -2852,7 +2855,11 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
}
|
||||
|
||||
const sty = screen.cursor.page_pin.style(screen.cursor.page_cell);
|
||||
const fg_style = sty.fg(color_palette, self.config.bold_is_bright) orelse self.foreground_color orelse self.default_foreground_color;
|
||||
const fg_style = sty.fg(.{
|
||||
.default = self.foreground_color orelse self.default_foreground_color,
|
||||
.palette = color_palette,
|
||||
.bold = self.config.bold_color,
|
||||
});
|
||||
const bg_style = sty.bg(screen.cursor.page_cell, color_palette) orelse self.background_color orelse self.default_background_color;
|
||||
|
||||
break :blk switch (txt) {
|
||||
|
||||
@@ -16,6 +16,10 @@ pub const Options = struct {
|
||||
format: gl.Texture.Format,
|
||||
internal_format: gl.Texture.InternalFormat,
|
||||
target: gl.Texture.Target,
|
||||
min_filter: gl.Texture.MinFilter,
|
||||
mag_filter: gl.Texture.MagFilter,
|
||||
wrap_s: gl.Texture.Wrap,
|
||||
wrap_t: gl.Texture.Wrap,
|
||||
};
|
||||
|
||||
texture: gl.Texture,
|
||||
@@ -48,10 +52,10 @@ pub fn init(
|
||||
{
|
||||
const texbind = tex.bind(opts.target) catch return error.OpenGLFailed;
|
||||
defer texbind.unbind();
|
||||
texbind.parameter(.WrapS, gl.c.GL_CLAMP_TO_EDGE) catch return error.OpenGLFailed;
|
||||
texbind.parameter(.WrapT, gl.c.GL_CLAMP_TO_EDGE) catch return error.OpenGLFailed;
|
||||
texbind.parameter(.MinFilter, gl.c.GL_LINEAR) catch return error.OpenGLFailed;
|
||||
texbind.parameter(.MagFilter, gl.c.GL_LINEAR) catch return error.OpenGLFailed;
|
||||
texbind.parameter(.WrapS, @intFromEnum(opts.wrap_s)) catch return error.OpenGLFailed;
|
||||
texbind.parameter(.WrapT, @intFromEnum(opts.wrap_t)) catch return error.OpenGLFailed;
|
||||
texbind.parameter(.MinFilter, @intFromEnum(opts.min_filter)) catch return error.OpenGLFailed;
|
||||
texbind.parameter(.MagFilter, @intFromEnum(opts.mag_filter)) catch return error.OpenGLFailed;
|
||||
texbind.image2D(
|
||||
0,
|
||||
opts.internal_format,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const configpkg = @import("../config.zig");
|
||||
const color = @import("color.zig");
|
||||
const sgr = @import("sgr.zig");
|
||||
const page = @import("page.zig");
|
||||
@@ -115,24 +116,68 @@ pub const Style = struct {
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns the fg color for a cell with this style given the palette.
|
||||
pub const Fg = struct {
|
||||
/// The default color to use if the style doesn't specify a
|
||||
/// foreground color and no configuration options override
|
||||
/// it.
|
||||
default: color.RGB,
|
||||
|
||||
/// The current color palette. Required to map palette indices to
|
||||
/// real color values.
|
||||
palette: *const color.Palette,
|
||||
|
||||
/// If specified, the color to use for bold text.
|
||||
bold: ?configpkg.BoldColor = null,
|
||||
};
|
||||
|
||||
/// Returns the fg color for a cell with this style given the palette
|
||||
/// and various configuration options.
|
||||
pub fn fg(
|
||||
self: Style,
|
||||
palette: *const color.Palette,
|
||||
bold_is_bright: bool,
|
||||
) ?color.RGB {
|
||||
opts: Fg,
|
||||
) color.RGB {
|
||||
// Note we don't pull the bold check to the top-level here because
|
||||
// we don't want to duplicate the conditional multiple times since
|
||||
// certain colors require more checks (e.g. `bold_is_bright`).
|
||||
|
||||
return switch (self.fg_color) {
|
||||
.none => null,
|
||||
.palette => |idx| palette: {
|
||||
if (bold_is_bright and self.flags.bold) {
|
||||
const bright_offset = @intFromEnum(color.Name.bright_black);
|
||||
if (idx < bright_offset)
|
||||
break :palette palette[idx + bright_offset];
|
||||
.none => default: {
|
||||
if (self.flags.bold) {
|
||||
if (opts.bold) |bold| switch (bold) {
|
||||
.bright => {},
|
||||
.color => |v| break :default v.toTerminalRGB(),
|
||||
};
|
||||
}
|
||||
|
||||
break :palette palette[idx];
|
||||
break :default opts.default;
|
||||
},
|
||||
|
||||
.palette => |idx| palette: {
|
||||
if (self.flags.bold) {
|
||||
if (opts.bold) |bold| switch (bold) {
|
||||
.color => |v| break :palette v.toTerminalRGB(),
|
||||
.bright => {
|
||||
const bright_offset = @intFromEnum(color.Name.bright_black);
|
||||
if (idx < bright_offset) {
|
||||
break :palette opts.palette[idx + bright_offset];
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
break :palette opts.palette[idx];
|
||||
},
|
||||
|
||||
.rgb => |rgb| rgb: {
|
||||
if (self.flags.bold and rgb.eql(opts.default)) {
|
||||
if (opts.bold) |bold| switch (bold) {
|
||||
.color => |v| break :rgb v.toTerminalRGB(),
|
||||
.bright => {},
|
||||
};
|
||||
}
|
||||
|
||||
break :rgb rgb;
|
||||
},
|
||||
.rgb => |rgb| rgb,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user