Merge branch 'ghostty-org:main' into deb-package

This commit is contained in:
Ronit Gandhi
2024-12-31 11:07:42 +05:30
committed by GitHub
23 changed files with 384 additions and 179 deletions

View File

@ -45,6 +45,11 @@ class BaseTerminalController: NSWindowController,
didSet { surfaceTreeDidChange(from: oldValue, to: surfaceTree) } didSet { surfaceTreeDidChange(from: oldValue, to: surfaceTree) }
} }
/// Whether the terminal surface should focus when the mouse is over it.
var focusFollowsMouse: Bool {
self.derivedConfig.focusFollowsMouse
}
/// Non-nil when an alert is active so we don't overlap multiple. /// Non-nil when an alert is active so we don't overlap multiple.
private var alert: NSAlert? = nil private var alert: NSAlert? = nil
@ -106,8 +111,8 @@ class BaseTerminalController: NSWindowController,
// Listen for local events that we need to know of outside of // Listen for local events that we need to know of outside of
// single surface handlers. // single surface handlers.
self.eventMonitor = NSEvent.addLocalMonitorForEvents( self.eventMonitor = NSEvent.addLocalMonitorForEvents(
matching: [.flagsChanged], matching: [.flagsChanged]
handler: localEventHandler) ) { [weak self] event in self?.localEventHandler(event) }
} }
deinit { deinit {
@ -262,7 +267,6 @@ class BaseTerminalController: NSWindowController,
// Set the main window title // Set the main window title
window.title = to window.title = to
} }
func pwdDidChange(to: URL?) { func pwdDidChange(to: URL?) {
@ -604,15 +608,18 @@ class BaseTerminalController: NSWindowController,
private struct DerivedConfig { private struct DerivedConfig {
let macosTitlebarProxyIcon: Ghostty.MacOSTitlebarProxyIcon let macosTitlebarProxyIcon: Ghostty.MacOSTitlebarProxyIcon
let windowStepResize: Bool let windowStepResize: Bool
let focusFollowsMouse: Bool
init() { init() {
self.macosTitlebarProxyIcon = .visible self.macosTitlebarProxyIcon = .visible
self.windowStepResize = false self.windowStepResize = false
self.focusFollowsMouse = false
} }
init(_ config: Ghostty.Config) { init(_ config: Ghostty.Config) {
self.macosTitlebarProxyIcon = config.macosTitlebarProxyIcon self.macosTitlebarProxyIcon = config.macosTitlebarProxyIcon
self.windowStepResize = config.windowStepResize self.windowStepResize = config.windowStepResize
self.focusFollowsMouse = config.focusFollowsMouse
} }
} }
} }

View File

@ -117,9 +117,6 @@ class TerminalController: BaseTerminalController {
// Update our derived config // Update our derived config
self.derivedConfig = DerivedConfig(config) self.derivedConfig = DerivedConfig(config)
guard let window = window as? TerminalWindow else { return }
window.focusFollowsMouse = config.focusFollowsMouse
// If we have no surfaces in our window (is that possible?) then we update // If we have no surfaces in our window (is that possible?) then we update
// our window appearance based on the root config. If we have surfaces, we // our window appearance based on the root config. If we have surfaces, we
// don't call this because the TODO // don't call this because the TODO
@ -247,7 +244,7 @@ class TerminalController: BaseTerminalController {
let backgroundColor: OSColor let backgroundColor: OSColor
if let surfaceTree { if let surfaceTree {
if let focusedSurface, surfaceTree.doesBorderTop(view: focusedSurface) { if let focusedSurface, surfaceTree.doesBorderTop(view: focusedSurface) {
backgroundColor = OSColor(focusedSurface.backgroundColor ?? surfaceConfig.backgroundColor) backgroundColor = OSColor(focusedSurface.backgroundColor ?? surfaceConfig.backgroundColor).withAlphaComponent(0.0)
} else { } else {
// We don't have a focused surface or our surface doesn't border the // We don't have a focused surface or our surface doesn't border the
// top. We choose to match the color of the top-left most surface. // top. We choose to match the color of the top-left most surface.
@ -422,8 +419,6 @@ class TerminalController: BaseTerminalController {
} }
} }
window.focusFollowsMouse = config.focusFollowsMouse
// Apply any additional appearance-related properties to the new window. We // Apply any additional appearance-related properties to the new window. We
// apply this based on the root config but change it later based on surface // apply this based on the root config but change it later based on surface
// config (see focused surface change callback). // config (see focused surface change callback).

View File

@ -414,8 +414,6 @@ class TerminalWindow: NSWindow {
} }
} }
var focusFollowsMouse: Bool = false
// Find the NSTextField responsible for displaying the titlebar's title. // Find the NSTextField responsible for displaying the titlebar's title.
private var titlebarTextField: NSTextField? { private var titlebarTextField: NSTextField? {
guard let titlebarView = titlebarContainer?.subviews guard let titlebarView = titlebarContainer?.subviews

View File

@ -617,11 +617,12 @@ extension Ghostty {
let mods = Ghostty.ghosttyMods(event.modifierFlags) let mods = Ghostty.ghosttyMods(event.modifierFlags)
ghostty_surface_mouse_pos(surface, pos.x, frame.height - pos.y, mods) ghostty_surface_mouse_pos(surface, pos.x, frame.height - pos.y, mods)
// If focus follows mouse is enabled then move focus to this surface. // Handle focus-follows-mouse
if let window = self.window as? TerminalWindow, if let window,
window.isKeyWindow && let controller = window.windowController as? BaseTerminalController,
window.focusFollowsMouse && (window.isKeyWindow &&
!self.focused !self.focused &&
controller.focusFollowsMouse)
{ {
Ghostty.moveFocus(to: self) Ghostty.moveFocus(to: self)
} }

View File

@ -853,11 +853,8 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
}, },
.color_change => |change| { .color_change => |change| {
// On any color change, we have to report for mode 2031 // Notify our apprt, but don't send a mode 2031 DSR report
// if it is enabled. // because VT sequences were used to change the color.
self.reportColorScheme(false);
// Notify our apprt
try self.rt_app.performAction( try self.rt_app.performAction(
.{ .surface = self }, .{ .surface = self },
.color_change, .color_change,
@ -4293,11 +4290,16 @@ fn writeScreenFile(
tmp_dir.deinit(); tmp_dir.deinit();
return; return;
}; };
// Use topLeft and bottomRight to ensure correct coordinate ordering
const tl = sel.topLeft(&self.io.terminal.screen);
const br = sel.bottomRight(&self.io.terminal.screen);
try self.io.terminal.screen.dumpString( try self.io.terminal.screen.dumpString(
buf_writer.writer(), buf_writer.writer(),
.{ .{
.tl = sel.start(), .tl = tl,
.br = sel.end(), .br = br,
.unwrap = true, .unwrap = true,
}, },
); );

View File

@ -1400,7 +1400,15 @@ pub fn getColorScheme(self: *App) apprt.ColorScheme {
null, null,
&err, &err,
) orelse { ) orelse {
if (err) |e| log.err("unable to get current color scheme: {s}", .{e.message}); if (err) |e| {
// If ReadOne is not yet implemented, fall back to deprecated "Read" method
// Error code: GDBus.Error:org.freedesktop.DBus.Error.UnknownMethod: No such method ReadOne
if (e.code == 19) {
return self.getColorSchemeDeprecated();
}
// Otherwise, log the error and return .light
log.err("unable to get current color scheme: {s}", .{e.message});
}
return .light; return .light;
}; };
defer c.g_variant_unref(value); defer c.g_variant_unref(value);
@ -1417,6 +1425,49 @@ pub fn getColorScheme(self: *App) apprt.ColorScheme {
return .light; return .light;
} }
/// Call the deprecated D-Bus "Read" method to determine the current color scheme. If
/// there is any error at any point we'll log the error and return "light"
fn getColorSchemeDeprecated(self: *App) apprt.ColorScheme {
const dbus_connection = c.g_application_get_dbus_connection(@ptrCast(self.app));
var err: ?*c.GError = null;
defer if (err) |e| c.g_error_free(e);
const value = c.g_dbus_connection_call_sync(
dbus_connection,
"org.freedesktop.portal.Desktop",
"/org/freedesktop/portal/desktop",
"org.freedesktop.portal.Settings",
"Read",
c.g_variant_new("(ss)", "org.freedesktop.appearance", "color-scheme"),
c.G_VARIANT_TYPE("(v)"),
c.G_DBUS_CALL_FLAGS_NONE,
-1,
null,
&err,
) orelse {
if (err) |e| log.err("Read method failed: {s}", .{e.message});
return .light;
};
defer c.g_variant_unref(value);
if (c.g_variant_is_of_type(value, c.G_VARIANT_TYPE("(v)")) == 1) {
var inner: ?*c.GVariant = null;
c.g_variant_get(value, "(v)", &inner);
defer if (inner) |i| c.g_variant_unref(i);
if (inner) |i| {
const child = c.g_variant_get_child_value(i, 0) orelse {
return .light;
};
defer c.g_variant_unref(child);
const val = c.g_variant_get_uint32(child);
return if (val == 1) .dark else .light;
}
}
return .light;
}
/// This will be called by D-Bus when the style changes between light & dark. /// This will be called by D-Bus when the style changes between light & dark.
fn gtkNotifyColorScheme( fn gtkNotifyColorScheme(
_: ?*c.GDBusConnection, _: ?*c.GDBusConnection,

View File

@ -238,7 +238,7 @@ fn promptText(req: apprt.ClipboardRequest) [:0]const u8 {
\\Pasting this text into the terminal may be dangerous as it looks like some commands may be executed. \\Pasting this text into the terminal may be dangerous as it looks like some commands may be executed.
, ,
.osc_52_read => .osc_52_read =>
\\An appliclication is attempting to read from the clipboard. \\An application is attempting to read from the clipboard.
\\The current clipboard contents are shown below. \\The current clipboard contents are shown below.
, ,
.osc_52_write => .osc_52_write =>

View File

@ -156,6 +156,9 @@ pub fn init(self: *Window, app: *App) !void {
if (app.config.@"gtk-titlebar") { if (app.config.@"gtk-titlebar") {
const header = HeaderBar.init(self); const header = HeaderBar.init(self);
// If we are not decorated then we hide the titlebar.
header.setVisible(app.config.@"window-decoration");
{ {
const btn = c.gtk_menu_button_new(); const btn = c.gtk_menu_button_new();
c.gtk_widget_set_tooltip_text(btn, "Main Menu"); c.gtk_widget_set_tooltip_text(btn, "Main Menu");
@ -216,6 +219,14 @@ pub fn init(self: *Window, app: *App) !void {
} }
} }
// If Adwaita is enabled and is older than 1.4.0 we don't have the tab overview and so we
// need to stick the headerbar into the content box.
if (!adwaita.versionAtLeast(1, 4, 0) and adwaita.enabled(&self.app.config)) {
if (self.header) |h| {
c.gtk_box_append(@ptrCast(box), h.asWidget());
}
}
// In debug we show a warning and apply the 'devel' class to the window. // In debug we show a warning and apply the 'devel' class to the window.
// This is a really common issue where people build from source in debug and performance is really bad. // This is a really common issue where people build from source in debug and performance is really bad.
if (comptime std.debug.runtime_safety) { if (comptime std.debug.runtime_safety) {
@ -290,11 +301,6 @@ pub fn init(self: *Window, app: *App) !void {
if (self.header) |header| { if (self.header) |header| {
const header_widget = header.asWidget(); const header_widget = header.asWidget();
c.adw_toolbar_view_add_top_bar(toolbar_view, header_widget); c.adw_toolbar_view_add_top_bar(toolbar_view, header_widget);
// If we are not decorated then we hide the titlebar.
if (!app.config.@"window-decoration") {
c.gtk_widget_set_visible(header_widget, 0);
}
} }
if (self.app.config.@"gtk-tabs-location" != .hidden) { if (self.app.config.@"gtk-tabs-location" != .hidden) {
@ -363,8 +369,17 @@ pub fn init(self: *Window, app: *App) !void {
} }
// The box is our main child // The box is our main child
c.gtk_window_set_child(gtk_window, box); if (!adwaita.versionAtLeast(1, 4, 0) and adwaita.enabled(&self.app.config)) {
if (self.header) |h| c.gtk_window_set_titlebar(gtk_window, h.asWidget()); c.adw_application_window_set_content(
@ptrCast(gtk_window),
box,
);
} else {
c.gtk_window_set_child(gtk_window, box);
if (self.header) |h| {
c.gtk_window_set_titlebar(gtk_window, h.asWidget());
}
}
} }
// Show the window // Show the window
@ -499,13 +514,19 @@ pub fn toggleWindowDecorations(self: *Window) void {
const new_decorated = !old_decorated; const new_decorated = !old_decorated;
c.gtk_window_set_decorated(self.window, @intFromBool(new_decorated)); c.gtk_window_set_decorated(self.window, @intFromBool(new_decorated));
// Fix any artifacting that may occur in window corners.
if (new_decorated) {
c.gtk_widget_add_css_class(@ptrCast(self.window), "without-window-decoration-and-with-titlebar");
} else {
c.gtk_widget_remove_css_class(@ptrCast(self.window), "without-window-decoration-and-with-titlebar");
}
// If we have a titlebar, then we also show/hide it depending on the // If we have a titlebar, then we also show/hide it depending on the
// decorated state. GTK tends to consider the titlebar part of the frame // decorated state. GTK tends to consider the titlebar part of the frame
// and hides it with decorations, but libadwaita doesn't. This makes it // and hides it with decorations, but libadwaita doesn't. This makes it
// explicit. // explicit.
if (self.header) |v| { if (self.header) |headerbar| {
const widget = v.asWidget(); headerbar.setVisible(new_decorated);
c.gtk_widget_set_visible(widget, @intFromBool(new_decorated));
} }
} }

View File

@ -30,6 +30,10 @@ pub const HeaderBar = union(enum) {
return .{ .gtk = @ptrCast(headerbar) }; return .{ .gtk = @ptrCast(headerbar) };
} }
pub fn setVisible(self: HeaderBar, visible: bool) void {
c.gtk_widget_set_visible(self.asWidget(), @intFromBool(visible));
}
pub fn asWidget(self: HeaderBar) *c.GtkWidget { pub fn asWidget(self: HeaderBar) *c.GtkWidget {
return switch (self) { return switch (self) {
.adw => |headerbar| @ptrCast(@alignCast(headerbar)), .adw => |headerbar| @ptrCast(@alignCast(headerbar)),

View File

@ -2668,18 +2668,40 @@ pub fn loadFile(self: *Config, alloc: Allocator, path: []const u8) !void {
try self.expandPaths(std.fs.path.dirname(path).?); try self.expandPaths(std.fs.path.dirname(path).?);
} }
pub const OptionalFileAction = enum { loaded, not_found, @"error" };
/// Load optional configuration file from `path`. All errors are ignored. /// Load optional configuration file from `path`. All errors are ignored.
pub fn loadOptionalFile(self: *Config, alloc: Allocator, path: []const u8) void { ///
self.loadFile(alloc, path) catch |err| switch (err) { /// Returns the action that was taken.
error.FileNotFound => std.log.info( pub fn loadOptionalFile(
"optional config file not found, not loading path={s}", self: *Config,
.{path}, alloc: Allocator,
), path: []const u8,
else => std.log.warn( ) OptionalFileAction {
"error reading optional config file, not loading err={} path={s}", if (self.loadFile(alloc, path)) {
.{ err, path }, return .loaded;
), } else |err| switch (err) {
}; error.FileNotFound => return .not_found,
else => {
std.log.warn(
"error reading optional config file, not loading err={} path={s}",
.{ err, path },
);
return .@"error";
},
}
}
fn writeConfigTemplate(path: []const u8) !void {
log.info("creating template config file: path={s}", .{path});
const file = try std.fs.createFileAbsolute(path, .{});
defer file.close();
try std.fmt.format(
file.writer(),
@embedFile("./config-template"),
.{ .path = path },
);
} }
/// Load configurations from the default configuration files. The default /// Load configurations from the default configuration files. The default
@ -2688,14 +2710,30 @@ pub fn loadOptionalFile(self: *Config, alloc: Allocator, path: []const u8) void
/// On macOS, `$HOME/Library/Application Support/$CFBundleIdentifier/config` /// On macOS, `$HOME/Library/Application Support/$CFBundleIdentifier/config`
/// is also loaded. /// is also loaded.
pub fn loadDefaultFiles(self: *Config, alloc: Allocator) !void { pub fn loadDefaultFiles(self: *Config, alloc: Allocator) !void {
// Load XDG first
const xdg_path = try internal_os.xdg.config(alloc, .{ .subdir = "ghostty/config" }); const xdg_path = try internal_os.xdg.config(alloc, .{ .subdir = "ghostty/config" });
defer alloc.free(xdg_path); defer alloc.free(xdg_path);
self.loadOptionalFile(alloc, xdg_path); const xdg_action = self.loadOptionalFile(alloc, xdg_path);
// On macOS load the app support directory as well
if (comptime builtin.os.tag == .macos) { if (comptime builtin.os.tag == .macos) {
const app_support_path = try internal_os.macos.appSupportDir(alloc, "config"); const app_support_path = try internal_os.macos.appSupportDir(alloc, "config");
defer alloc.free(app_support_path); defer alloc.free(app_support_path);
self.loadOptionalFile(alloc, app_support_path); const app_support_action = self.loadOptionalFile(alloc, app_support_path);
// If both files are not found, then we create a template file.
// For macOS, we only create the template file in the app support
if (app_support_action == .not_found and xdg_action == .not_found) {
writeConfigTemplate(app_support_path) catch |err| {
log.warn("error creating template config file err={}", .{err});
};
}
} else {
if (xdg_action == .not_found) {
writeConfigTemplate(xdg_path) catch |err| {
log.warn("error creating template config file err={}", .{err});
};
}
} }
} }
@ -2805,17 +2843,21 @@ pub fn loadCliArgs(self: *Config, alloc_gpa: Allocator) !void {
// replace the entire list with the new list. // replace the entire list with the new list.
inline for (fields, 0..) |field, i| { inline for (fields, 0..) |field, i| {
const v = &@field(self, field); const v = &@field(self, field);
const len = v.list.items.len - counter[i];
if (len > 0) { // The list can be empty if it was reset, i.e. --font-family=""
// Note: we don't have to worry about freeing the memory if (v.list.items.len > 0) {
// that we overwrite or cut off here because its all in const len = v.list.items.len - counter[i];
// an arena. if (len > 0) {
v.list.replaceRangeAssumeCapacity( // Note: we don't have to worry about freeing the memory
0, // that we overwrite or cut off here because its all in
len, // an arena.
v.list.items[counter[i]..], v.list.replaceRangeAssumeCapacity(
); 0,
v.list.items.len = len; len,
v.list.items[counter[i]..],
);
v.list.items.len = len;
}
} }
} }
} }
@ -3797,17 +3839,22 @@ pub const Color = struct {
pub fn fromHex(input: []const u8) !Color { pub fn fromHex(input: []const u8) !Color {
// Trim the beginning '#' if it exists // Trim the beginning '#' if it exists
const trimmed = if (input.len != 0 and input[0] == '#') input[1..] else input; const trimmed = if (input.len != 0 and input[0] == '#') input[1..] else input;
if (trimmed.len != 6 and trimmed.len != 3) return error.InvalidValue;
// We expect exactly 6 for RRGGBB // Expand short hex values to full hex values
if (trimmed.len != 6) return error.InvalidValue; const rgb: []const u8 = if (trimmed.len == 3) &.{
trimmed[0], trimmed[0],
trimmed[1], trimmed[1],
trimmed[2], trimmed[2],
} else trimmed;
// Parse the colors two at a time. // Parse the colors two at a time.
var result: Color = undefined; var result: Color = undefined;
comptime var i: usize = 0; comptime var i: usize = 0;
inline while (i < 6) : (i += 2) { inline while (i < 6) : (i += 2) {
const v: u8 = const v: u8 =
((try std.fmt.charToDigit(trimmed[i], 16)) * 16) + ((try std.fmt.charToDigit(rgb[i], 16)) * 16) +
try std.fmt.charToDigit(trimmed[i + 1], 16); try std.fmt.charToDigit(rgb[i + 1], 16);
@field(result, switch (i) { @field(result, switch (i) {
0 => "r", 0 => "r",
@ -3827,6 +3874,8 @@ pub const Color = struct {
try testing.expectEqual(Color{ .r = 10, .g = 11, .b = 12 }, try Color.fromHex("#0A0B0C")); try testing.expectEqual(Color{ .r = 10, .g = 11, .b = 12 }, try Color.fromHex("#0A0B0C"));
try testing.expectEqual(Color{ .r = 10, .g = 11, .b = 12 }, try Color.fromHex("0A0B0C")); try testing.expectEqual(Color{ .r = 10, .g = 11, .b = 12 }, try Color.fromHex("0A0B0C"));
try testing.expectEqual(Color{ .r = 255, .g = 255, .b = 255 }, try Color.fromHex("FFFFFF")); try testing.expectEqual(Color{ .r = 255, .g = 255, .b = 255 }, try Color.fromHex("FFFFFF"));
try testing.expectEqual(Color{ .r = 255, .g = 255, .b = 255 }, try Color.fromHex("FFF"));
try testing.expectEqual(Color{ .r = 51, .g = 68, .b = 85 }, try Color.fromHex("#345"));
} }
test "parseCLI from name" { test "parseCLI from name" {
@ -4701,9 +4750,11 @@ pub const Keybinds = struct {
try list.parseCLI(alloc, "ctrl+z>2=goto_tab:2"); try list.parseCLI(alloc, "ctrl+z>2=goto_tab:2");
try list.formatEntry(formatterpkg.entryFormatter("keybind", buf.writer())); try list.formatEntry(formatterpkg.entryFormatter("keybind", buf.writer()));
// Note they turn into translated keys because they match
// their ASCII mapping.
const want = const want =
\\keybind = ctrl+z>1=goto_tab:1 \\keybind = ctrl+z>two=goto_tab:2
\\keybind = ctrl+z>2=goto_tab:2 \\keybind = ctrl+z>one=goto_tab:1
\\ \\
; ;
try std.testing.expectEqualStrings(want, buf.items); try std.testing.expectEqualStrings(want, buf.items);

View File

@ -0,0 +1,43 @@
# This is the configuration file for Ghostty.
#
# This template file has been automatically created at the following
# path since Ghostty couldn't find any existing config files on your system:
#
# {[path]s}
#
# The template does not set any default options, since Ghostty ships
# with sensible defaults for all options. Users should only need to set
# options that they want to change from the default.
#
# Run `ghostty +show-config --default --docs` to view a list of
# all available config options and their default values.
#
# Additionally, each config option is also explained in detail
# on Ghostty's website, at https://ghostty.org/docs/config.
# Config syntax crash course
# ==========================
# # The config file consists of simple key-value pairs,
# # separated by equals signs.
# font-family = Iosevka
# window-padding-x = 2
#
# # Spacing around the equals sign does not matter.
# # All of these are identical:
# key=value
# key= value
# key =value
# key = value
#
# # Any line beginning with a # is a comment. It's not possible to put
# # a comment after a config option, since it would be interpreted as a
# # part of the value. For example, this will have a value of "#123abc":
# background = #123abc
#
# # Empty values are used to reset config keys to default.
# key =
#
# # Some config options have unique syntaxes for their value,
# # which is explained in the docs for that config option.
# # Just for example:
# resize-overlay-duration = 4s 200ms

View File

@ -48,13 +48,7 @@ pub fn open(alloc_gpa: Allocator) !void {
/// ///
/// The allocator must be an arena allocator. No memory is freed by this /// The allocator must be an arena allocator. No memory is freed by this
/// function and the resulting path is not all the memory that is allocated. /// function and the resulting path is not all the memory that is allocated.
/// fn configPath(alloc_arena: Allocator) ![]const u8 {
/// NOTE: WHY IS THIS INLINE? This is inline because when this is not
/// inline then Zig 0.13 crashes [most of the time] when trying to compile
/// this file. This is a workaround for that issue. This function is only
/// called from one place that is not performance critical so it is fine
/// to be inline.
inline fn configPath(alloc_arena: Allocator) ![]const u8 {
const paths: []const []const u8 = try configPathCandidates(alloc_arena); const paths: []const []const u8 = try configPathCandidates(alloc_arena);
assert(paths.len > 0); assert(paths.len > 0);

View File

@ -362,16 +362,9 @@ pub const CoreText = struct {
const list = set.createMatchingFontDescriptors(); const list = set.createMatchingFontDescriptors();
defer list.release(); defer list.release();
// Bring the list of descriptors in to zig land
var zig_list = try copyMatchingDescriptors(alloc, list);
errdefer alloc.free(zig_list);
// Filter them. We don't use `CTFontCollectionSetExclusionDescriptors`
// to do this because that requires a mutable collection. This way is
// much more straight forward.
zig_list = try alloc.realloc(zig_list, filterDescriptors(zig_list));
// Sort our descriptors // Sort our descriptors
const zig_list = try copyMatchingDescriptors(alloc, list);
errdefer alloc.free(zig_list);
sortMatchingDescriptors(&desc, zig_list); sortMatchingDescriptors(&desc, zig_list);
return DiscoverIterator{ return DiscoverIterator{
@ -558,47 +551,13 @@ pub const CoreText = struct {
for (0..result.len) |i| { for (0..result.len) |i| {
result[i] = list.getValueAtIndex(macos.text.FontDescriptor, i); result[i] = list.getValueAtIndex(macos.text.FontDescriptor, i);
// We need to retain because once the list // We need to retain becauseonce the list is freed it will
// is freed it will release all its members. // release all its members.
result[i].retain(); result[i].retain();
} }
return result; return result;
} }
/// Filter any descriptors out of the list that aren't acceptable for
/// some reason or another (e.g. the font isn't in a format we can handle).
///
/// Invalid descriptors are filled in from the end of
/// the list and the new length for the list is returned.
fn filterDescriptors(list: []*macos.text.FontDescriptor) usize {
var end = list.len;
var i: usize = 0;
while (i < end) {
if (validDescriptor(list[i])) {
i += 1;
} else {
list[i].release();
end -= 1;
list[i] = list[end];
}
}
return end;
}
/// Used by `filterDescriptors` to decide whether a descriptor is valid.
fn validDescriptor(desc: *macos.text.FontDescriptor) bool {
if (desc.copyAttribute(macos.text.FontAttribute.format)) |format| {
defer format.release();
var value: c_int = undefined;
assert(format.getValue(.int, &value));
// Bitmap fonts are not currently supported.
if (value == macos.text.c.kCTFontFormatBitmap) return false;
}
return true;
}
fn sortMatchingDescriptors( fn sortMatchingDescriptors(
desc: *const Descriptor, desc: *const Descriptor,
list: []*macos.text.FontDescriptor, list: []*macos.text.FontDescriptor,

View File

@ -34,3 +34,6 @@ pub const cozette = @embedFile("res/CozetteVector.ttf");
/// Monaspace has weird ligature behaviors we want to test in our shapers /// Monaspace has weird ligature behaviors we want to test in our shapers
/// so we embed it here. /// so we embed it here.
pub const monaspace_neon = @embedFile("res/MonaspaceNeon-Regular.otf"); pub const monaspace_neon = @embedFile("res/MonaspaceNeon-Regular.otf");
/// Terminus TTF is a scalable font with bitmap glyphs at various sizes.
pub const terminus_ttf = @embedFile("res/TerminusTTF-Regular.ttf");

View File

@ -515,8 +515,17 @@ pub const Face = struct {
fn calcMetrics(ct_font: *macos.text.Font) CalcMetricsError!font.face.Metrics { fn calcMetrics(ct_font: *macos.text.Font) CalcMetricsError!font.face.Metrics {
// Read the 'head' table out of the font data. // Read the 'head' table out of the font data.
const head: opentype.Head = head: { const head: opentype.Head = head: {
const tag = macos.text.FontTableTag.init("head"); // macOS bitmap-only fonts use a 'bhed' tag rather than 'head', but
const data = ct_font.copyTable(tag) orelse return error.CopyTableError; // the table format is byte-identical to the 'head' table, so if we
// can't find 'head' we try 'bhed' instead before failing.
//
// ref: https://fontforge.org/docs/techref/bitmaponlysfnt.html
const head_tag = macos.text.FontTableTag.init("head");
const bhed_tag = macos.text.FontTableTag.init("bhed");
const data =
ct_font.copyTable(head_tag) orelse
ct_font.copyTable(bhed_tag) orelse
return error.CopyTableError;
defer data.release(); defer data.release();
const ptr = data.getPointer(); const ptr = data.getPointer();
const len = data.getLength(); const len = data.getLength();

View File

@ -288,7 +288,6 @@ pub const Face = struct {
self.face.loadGlyph(glyph_id, .{ self.face.loadGlyph(glyph_id, .{
.render = true, .render = true,
.color = self.face.hasColor(), .color = self.face.hasColor(),
.no_bitmap = !self.face.hasColor(),
}) catch return false; }) catch return false;
// If the glyph is SVG we assume colorized // If the glyph is SVG we assume colorized
@ -323,14 +322,6 @@ pub const Face = struct {
// glyph properties before render so we don't render here. // glyph properties before render so we don't render here.
.render = !self.synthetic.bold, .render = !self.synthetic.bold,
// Disable bitmap strikes for now since it causes issues with
// our cell metrics and rasterization. In the future, this is
// all fixable so we can enable it.
//
// This must be enabled for color faces though because those are
// often colored bitmaps, which we support.
.no_bitmap = !self.face.hasColor(),
// use options from config // use options from config
.no_hinting = !self.load_flags.hinting, .no_hinting = !self.load_flags.hinting,
.force_autohint = !self.load_flags.@"force-autohint", .force_autohint = !self.load_flags.@"force-autohint",
@ -385,7 +376,7 @@ pub const Face = struct {
return error.UnsupportedPixelMode; return error.UnsupportedPixelMode;
}; };
log.warn("converting from pixel_mode={} to atlas_format={}", .{ log.debug("converting from pixel_mode={} to atlas_format={}", .{
bitmap_ft.pixel_mode, bitmap_ft.pixel_mode,
atlas.format, atlas.format,
}); });
@ -1005,3 +996,59 @@ test "svg font table" {
try testing.expectEqual(430, table.len); try testing.expectEqual(430, table.len);
} }
const terminus_i =
\\........
\\........
\\...#....
\\...#....
\\........
\\..##....
\\...#....
\\...#....
\\...#....
\\...#....
\\...#....
\\..###...
\\........
\\........
\\........
\\........
;
// Including the newline
const terminus_i_pitch = 9;
test "bitmap glyph" {
const alloc = testing.allocator;
const testFont = font.embedded.terminus_ttf;
var lib = try Library.init();
defer lib.deinit();
var atlas = try font.Atlas.init(alloc, 512, .grayscale);
defer atlas.deinit(alloc);
// Any glyph at 12pt @ 96 DPI is a bitmap
var ft_font = try Face.init(lib, testFont, .{ .size = .{
.points = 12,
.xdpi = 96,
.ydpi = 96,
} });
defer ft_font.deinit();
// glyph 77 = 'i'
const glyph = try ft_font.renderGlyph(alloc, &atlas, 77, .{});
// should render crisp
try testing.expectEqual(8, glyph.width);
try testing.expectEqual(16, glyph.height);
for (0..glyph.height) |y| {
for (0..glyph.width) |x| {
const pixel = terminus_i[y * terminus_i_pitch + x];
try testing.expectEqual(
@as(u8, if (pixel == '#') 255 else 0),
atlas.data[(glyph.atlas_y + y) * atlas.size + (glyph.atlas_x + x)],
);
}
}
}

View File

@ -43,26 +43,14 @@ pub fn monoToGrayscale(alloc: Allocator, bm: Bitmap) Allocator.Error!Bitmap {
var buf = try alloc.alloc(u8, bm.width * bm.rows); var buf = try alloc.alloc(u8, bm.width * bm.rows);
errdefer alloc.free(buf); errdefer alloc.free(buf);
// width divided by 8 because each byte has 8 pixels. This is therefore for (0..bm.rows) |y| {
// the number of bytes in each row. const row_offset = y * @as(usize, @intCast(bm.pitch));
const bytes_per_row = bm.width >> 3; for (0..bm.width) |x| {
const byte_offset = row_offset + @divTrunc(x, 8);
var source_i: usize = 0; const mask = @as(u8, 1) << @intCast(7 - (x % 8));
var target_i: usize = 0; const bit: u8 = @intFromBool((bm.buffer[byte_offset] & mask) != 0);
var i: usize = bm.rows; buf[y * bm.width + x] = bit * 255;
while (i > 0) : (i -= 1) {
var j: usize = bytes_per_row;
while (j > 0) : (j -= 1) {
var bit: u4 = 8;
while (bit > 0) : (bit -= 1) {
const mask = @as(u8, 1) << @as(u3, @intCast(bit - 1));
const bitval: u8 = if (bm.buffer[source_i + (j - 1)] & mask > 0) 0xFF else 0;
buf[target_i] = bitval;
target_i += 1;
}
} }
source_i += @intCast(bm.pitch);
} }
var copy = bm; var copy = bm;

View File

@ -25,6 +25,9 @@ This project uses several fonts which fall under the SIL Open Font License (OFL-
- [Copyright 2013 Google LLC](https://github.com/googlefonts/noto-emoji/blob/main/LICENSE) - [Copyright 2013 Google LLC](https://github.com/googlefonts/noto-emoji/blob/main/LICENSE)
- Cozette (MIT) - Cozette (MIT)
- [Copyright (c) 2020, Slavfox](https://github.com/slavfox/Cozette/blob/main/LICENSE) - [Copyright (c) 2020, Slavfox](https://github.com/slavfox/Cozette/blob/main/LICENSE)
- Terminus TTF (OFL-1.1)
- [Copyright (c) 2010-2020 Dimitar Toshkov Zhekov with Reserved Font Name "Terminus Font"](https://sourceforge.net/projects/terminus-font/)
- [Copyright (c) 2011-2023 Tilman Blumenbach with Reserved Font Name "Terminus (TTF)"](https://files.ax86.net/terminus-ttf/)
A full copy of the OFL license can be found at [OFL.txt](./OFL.txt). A full copy of the OFL license can be found at [OFL.txt](./OFL.txt).
An accompanying FAQ is also available at <https://openfontlicense.org/>. An accompanying FAQ is also available at <https://openfontlicense.org/>.

Binary file not shown.

View File

@ -1019,6 +1019,14 @@ pub const Trigger = struct {
const cp = it.nextCodepoint() orelse break :unicode; const cp = it.nextCodepoint() orelse break :unicode;
if (it.nextCodepoint() != null) break :unicode; if (it.nextCodepoint() != null) break :unicode;
// If this is ASCII and we have a translated key, set that.
if (std.math.cast(u8, cp)) |ascii| {
if (key.Key.fromASCII(ascii)) |k| {
result.key = .{ .translated = k };
continue :loop;
}
}
result.key = .{ .unicode = cp }; result.key = .{ .unicode = cp };
continue :loop; continue :loop;
} }
@ -1554,6 +1562,19 @@ test "parse: triggers" {
try parseSingle("a=ignore"), try parseSingle("a=ignore"),
); );
// unicode keys that map to translated
try testing.expectEqual(Binding{
.trigger = .{ .key = .{ .translated = .one } },
.action = .{ .ignore = {} },
}, try parseSingle("1=ignore"));
try testing.expectEqual(Binding{
.trigger = .{
.mods = .{ .super = true },
.key = .{ .translated = .period },
},
.action = .{ .ignore = {} },
}, try parseSingle("cmd+.=ignore"));
// single modifier // single modifier
try testing.expectEqual(Binding{ try testing.expectEqual(Binding{
.trigger = .{ .trigger = .{

View File

@ -729,7 +729,9 @@ pub const Key = enum(c_int) {
.{ '\t', .tab }, .{ '\t', .tab },
// Keypad entries. We just assume keypad with the kp_ prefix // Keypad entries. We just assume keypad with the kp_ prefix
// so that has some special meaning. These must also always be last. // so that has some special meaning. These must also always be last,
// so that our `fromASCII` function doesn't accidentally map them
// over normal numerics and other keys.
.{ '0', .kp_0 }, .{ '0', .kp_0 },
.{ '1', .kp_1 }, .{ '1', .kp_1 },
.{ '2', .kp_2 }, .{ '2', .kp_2 },

View File

@ -18,7 +18,31 @@ pub fn open(
typ: Type, typ: Type,
url: []const u8, url: []const u8,
) !void { ) !void {
const cmd = try openCommand(alloc, typ, url); const cmd: OpenCommand = switch (builtin.os.tag) {
.linux => .{ .child = std.process.Child.init(
&.{ "xdg-open", url },
alloc,
) },
.windows => .{ .child = std.process.Child.init(
&.{ "rundll32", "url.dll,FileProtocolHandler", url },
alloc,
) },
.macos => .{
.child = std.process.Child.init(
switch (typ) {
.text => &.{ "open", "-t", url },
.unknown => &.{ "open", url },
},
alloc,
),
.wait = true,
},
.ios => return error.Unimplemented,
else => @compileError("unsupported OS"),
};
var exe = cmd.child; var exe = cmd.child;
if (cmd.wait) { if (cmd.wait) {
@ -53,31 +77,3 @@ const OpenCommand = struct {
child: std.process.Child, child: std.process.Child,
wait: bool = false, wait: bool = false,
}; };
fn openCommand(alloc: Allocator, typ: Type, url: []const u8) !OpenCommand {
return switch (builtin.os.tag) {
.linux => .{ .child = std.process.Child.init(
&.{ "xdg-open", url },
alloc,
) },
.windows => .{ .child = std.process.Child.init(
&.{ "rundll32", "url.dll,FileProtocolHandler", url },
alloc,
) },
.macos => .{
.child = std.process.Child.init(
switch (typ) {
.text => &.{ "open", "-t", url },
.unknown => &.{ "open", url },
},
alloc,
),
.wait = true,
},
.ios => return error.Unimplemented,
else => @compileError("unsupported OS"),
};
}

View File

@ -3413,6 +3413,16 @@ pub const Pin = struct {
direction: Direction, direction: Direction,
limit: ?Pin, limit: ?Pin,
) PageIterator { ) PageIterator {
if (build_config.slow_runtime_safety) {
if (limit) |l| {
// Check the order according to the iteration direction.
switch (direction) {
.right_down => assert(self.eql(l) or self.before(l)),
.left_up => assert(self.eql(l) or l.before(self)),
}
}
}
return .{ return .{
.row = self, .row = self,
.limit = if (limit) |p| .{ .row = p } else .{ .none = {} }, .limit = if (limit) |p| .{ .row = p } else .{ .none = {} },