mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
Merge branch 'ghostty-org:main' into deb-package
This commit is contained in:
@ -45,6 +45,11 @@ class BaseTerminalController: NSWindowController,
|
||||
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.
|
||||
private var alert: NSAlert? = nil
|
||||
|
||||
@ -106,8 +111,8 @@ class BaseTerminalController: NSWindowController,
|
||||
// Listen for local events that we need to know of outside of
|
||||
// single surface handlers.
|
||||
self.eventMonitor = NSEvent.addLocalMonitorForEvents(
|
||||
matching: [.flagsChanged],
|
||||
handler: localEventHandler)
|
||||
matching: [.flagsChanged]
|
||||
) { [weak self] event in self?.localEventHandler(event) }
|
||||
}
|
||||
|
||||
deinit {
|
||||
@ -155,7 +160,7 @@ class BaseTerminalController: NSWindowController,
|
||||
}
|
||||
|
||||
// MARK: Notifications
|
||||
|
||||
|
||||
@objc private func didChangeScreenParametersNotification(_ notification: Notification) {
|
||||
// If we have a window that is visible and it is outside the bounds of the
|
||||
// screen then we clamp it back to within the screen.
|
||||
@ -262,7 +267,6 @@ class BaseTerminalController: NSWindowController,
|
||||
|
||||
// Set the main window title
|
||||
window.title = to
|
||||
|
||||
}
|
||||
|
||||
func pwdDidChange(to: URL?) {
|
||||
@ -604,15 +608,18 @@ class BaseTerminalController: NSWindowController,
|
||||
private struct DerivedConfig {
|
||||
let macosTitlebarProxyIcon: Ghostty.MacOSTitlebarProxyIcon
|
||||
let windowStepResize: Bool
|
||||
let focusFollowsMouse: Bool
|
||||
|
||||
init() {
|
||||
self.macosTitlebarProxyIcon = .visible
|
||||
self.windowStepResize = false
|
||||
self.focusFollowsMouse = false
|
||||
}
|
||||
|
||||
init(_ config: Ghostty.Config) {
|
||||
self.macosTitlebarProxyIcon = config.macosTitlebarProxyIcon
|
||||
self.windowStepResize = config.windowStepResize
|
||||
self.focusFollowsMouse = config.focusFollowsMouse
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -117,9 +117,6 @@ class TerminalController: BaseTerminalController {
|
||||
// Update our derived 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
|
||||
// our window appearance based on the root config. If we have surfaces, we
|
||||
// don't call this because the TODO
|
||||
@ -247,7 +244,7 @@ class TerminalController: BaseTerminalController {
|
||||
let backgroundColor: OSColor
|
||||
if let surfaceTree {
|
||||
if let focusedSurface, surfaceTree.doesBorderTop(view: focusedSurface) {
|
||||
backgroundColor = OSColor(focusedSurface.backgroundColor ?? surfaceConfig.backgroundColor)
|
||||
backgroundColor = OSColor(focusedSurface.backgroundColor ?? surfaceConfig.backgroundColor).withAlphaComponent(0.0)
|
||||
} else {
|
||||
// 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.
|
||||
@ -422,8 +419,6 @@ class TerminalController: BaseTerminalController {
|
||||
}
|
||||
}
|
||||
|
||||
window.focusFollowsMouse = config.focusFollowsMouse
|
||||
|
||||
// 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
|
||||
// config (see focused surface change callback).
|
||||
|
@ -414,8 +414,6 @@ class TerminalWindow: NSWindow {
|
||||
}
|
||||
}
|
||||
|
||||
var focusFollowsMouse: Bool = false
|
||||
|
||||
// Find the NSTextField responsible for displaying the titlebar's title.
|
||||
private var titlebarTextField: NSTextField? {
|
||||
guard let titlebarView = titlebarContainer?.subviews
|
||||
|
@ -617,11 +617,12 @@ extension Ghostty {
|
||||
let mods = Ghostty.ghosttyMods(event.modifierFlags)
|
||||
ghostty_surface_mouse_pos(surface, pos.x, frame.height - pos.y, mods)
|
||||
|
||||
// If focus follows mouse is enabled then move focus to this surface.
|
||||
if let window = self.window as? TerminalWindow,
|
||||
window.isKeyWindow &&
|
||||
window.focusFollowsMouse &&
|
||||
!self.focused
|
||||
// Handle focus-follows-mouse
|
||||
if let window,
|
||||
let controller = window.windowController as? BaseTerminalController,
|
||||
(window.isKeyWindow &&
|
||||
!self.focused &&
|
||||
controller.focusFollowsMouse)
|
||||
{
|
||||
Ghostty.moveFocus(to: self)
|
||||
}
|
||||
|
@ -853,11 +853,8 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
|
||||
},
|
||||
|
||||
.color_change => |change| {
|
||||
// On any color change, we have to report for mode 2031
|
||||
// if it is enabled.
|
||||
self.reportColorScheme(false);
|
||||
|
||||
// Notify our apprt
|
||||
// Notify our apprt, but don't send a mode 2031 DSR report
|
||||
// because VT sequences were used to change the color.
|
||||
try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.color_change,
|
||||
@ -4293,11 +4290,16 @@ fn writeScreenFile(
|
||||
tmp_dir.deinit();
|
||||
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(
|
||||
buf_writer.writer(),
|
||||
.{
|
||||
.tl = sel.start(),
|
||||
.br = sel.end(),
|
||||
.tl = tl,
|
||||
.br = br,
|
||||
.unwrap = true,
|
||||
},
|
||||
);
|
||||
|
@ -1400,7 +1400,15 @@ pub fn getColorScheme(self: *App) apprt.ColorScheme {
|
||||
null,
|
||||
&err,
|
||||
) 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;
|
||||
};
|
||||
defer c.g_variant_unref(value);
|
||||
@ -1417,6 +1425,49 @@ pub fn getColorScheme(self: *App) apprt.ColorScheme {
|
||||
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.
|
||||
fn gtkNotifyColorScheme(
|
||||
_: ?*c.GDBusConnection,
|
||||
|
@ -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.
|
||||
,
|
||||
.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.
|
||||
,
|
||||
.osc_52_write =>
|
||||
|
@ -156,6 +156,9 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
if (app.config.@"gtk-titlebar") {
|
||||
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();
|
||||
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.
|
||||
// This is a really common issue where people build from source in debug and performance is really bad.
|
||||
if (comptime std.debug.runtime_safety) {
|
||||
@ -290,11 +301,6 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
if (self.header) |header| {
|
||||
const header_widget = header.asWidget();
|
||||
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) {
|
||||
@ -363,8 +369,17 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
}
|
||||
|
||||
// The box is our main child
|
||||
c.gtk_window_set_child(gtk_window, box);
|
||||
if (self.header) |h| c.gtk_window_set_titlebar(gtk_window, h.asWidget());
|
||||
if (!adwaita.versionAtLeast(1, 4, 0) and adwaita.enabled(&self.app.config)) {
|
||||
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
|
||||
@ -499,13 +514,19 @@ pub fn toggleWindowDecorations(self: *Window) void {
|
||||
const new_decorated = !old_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
|
||||
// decorated state. GTK tends to consider the titlebar part of the frame
|
||||
// and hides it with decorations, but libadwaita doesn't. This makes it
|
||||
// explicit.
|
||||
if (self.header) |v| {
|
||||
const widget = v.asWidget();
|
||||
c.gtk_widget_set_visible(widget, @intFromBool(new_decorated));
|
||||
if (self.header) |headerbar| {
|
||||
headerbar.setVisible(new_decorated);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -30,6 +30,10 @@ pub const HeaderBar = union(enum) {
|
||||
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 {
|
||||
return switch (self) {
|
||||
.adw => |headerbar| @ptrCast(@alignCast(headerbar)),
|
||||
|
@ -2668,18 +2668,40 @@ pub fn loadFile(self: *Config, alloc: Allocator, path: []const u8) !void {
|
||||
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.
|
||||
pub fn loadOptionalFile(self: *Config, alloc: Allocator, path: []const u8) void {
|
||||
self.loadFile(alloc, path) catch |err| switch (err) {
|
||||
error.FileNotFound => std.log.info(
|
||||
"optional config file not found, not loading path={s}",
|
||||
.{path},
|
||||
),
|
||||
else => std.log.warn(
|
||||
"error reading optional config file, not loading err={} path={s}",
|
||||
.{ err, path },
|
||||
),
|
||||
};
|
||||
///
|
||||
/// Returns the action that was taken.
|
||||
pub fn loadOptionalFile(
|
||||
self: *Config,
|
||||
alloc: Allocator,
|
||||
path: []const u8,
|
||||
) OptionalFileAction {
|
||||
if (self.loadFile(alloc, 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
|
||||
@ -2688,14 +2710,30 @@ pub fn loadOptionalFile(self: *Config, alloc: Allocator, path: []const u8) void
|
||||
/// On macOS, `$HOME/Library/Application Support/$CFBundleIdentifier/config`
|
||||
/// is also loaded.
|
||||
pub fn loadDefaultFiles(self: *Config, alloc: Allocator) !void {
|
||||
// Load XDG first
|
||||
const xdg_path = try internal_os.xdg.config(alloc, .{ .subdir = "ghostty/config" });
|
||||
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) {
|
||||
const app_support_path = try internal_os.macos.appSupportDir(alloc, "config");
|
||||
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.
|
||||
inline for (fields, 0..) |field, i| {
|
||||
const v = &@field(self, field);
|
||||
const len = v.list.items.len - counter[i];
|
||||
if (len > 0) {
|
||||
// Note: we don't have to worry about freeing the memory
|
||||
// that we overwrite or cut off here because its all in
|
||||
// an arena.
|
||||
v.list.replaceRangeAssumeCapacity(
|
||||
0,
|
||||
len,
|
||||
v.list.items[counter[i]..],
|
||||
);
|
||||
v.list.items.len = len;
|
||||
|
||||
// The list can be empty if it was reset, i.e. --font-family=""
|
||||
if (v.list.items.len > 0) {
|
||||
const len = v.list.items.len - counter[i];
|
||||
if (len > 0) {
|
||||
// Note: we don't have to worry about freeing the memory
|
||||
// that we overwrite or cut off here because its all in
|
||||
// an arena.
|
||||
v.list.replaceRangeAssumeCapacity(
|
||||
0,
|
||||
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 {
|
||||
// Trim the beginning '#' if it exists
|
||||
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
|
||||
if (trimmed.len != 6) return error.InvalidValue;
|
||||
// Expand short hex values to full hex values
|
||||
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.
|
||||
var result: Color = undefined;
|
||||
comptime var i: usize = 0;
|
||||
inline while (i < 6) : (i += 2) {
|
||||
const v: u8 =
|
||||
((try std.fmt.charToDigit(trimmed[i], 16)) * 16) +
|
||||
try std.fmt.charToDigit(trimmed[i + 1], 16);
|
||||
((try std.fmt.charToDigit(rgb[i], 16)) * 16) +
|
||||
try std.fmt.charToDigit(rgb[i + 1], 16);
|
||||
|
||||
@field(result, switch (i) {
|
||||
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 = 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" {
|
||||
@ -4701,9 +4750,11 @@ pub const Keybinds = struct {
|
||||
try list.parseCLI(alloc, "ctrl+z>2=goto_tab:2");
|
||||
try list.formatEntry(formatterpkg.entryFormatter("keybind", buf.writer()));
|
||||
|
||||
// Note they turn into translated keys because they match
|
||||
// their ASCII mapping.
|
||||
const want =
|
||||
\\keybind = ctrl+z>1=goto_tab:1
|
||||
\\keybind = ctrl+z>2=goto_tab:2
|
||||
\\keybind = ctrl+z>two=goto_tab:2
|
||||
\\keybind = ctrl+z>one=goto_tab:1
|
||||
\\
|
||||
;
|
||||
try std.testing.expectEqualStrings(want, buf.items);
|
||||
|
43
src/config/config-template
Normal file
43
src/config/config-template
Normal 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
|
@ -48,13 +48,7 @@ pub fn open(alloc_gpa: Allocator) !void {
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
/// 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 {
|
||||
fn configPath(alloc_arena: Allocator) ![]const u8 {
|
||||
const paths: []const []const u8 = try configPathCandidates(alloc_arena);
|
||||
assert(paths.len > 0);
|
||||
|
||||
|
@ -362,16 +362,9 @@ pub const CoreText = struct {
|
||||
const list = set.createMatchingFontDescriptors();
|
||||
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
|
||||
const zig_list = try copyMatchingDescriptors(alloc, list);
|
||||
errdefer alloc.free(zig_list);
|
||||
sortMatchingDescriptors(&desc, zig_list);
|
||||
|
||||
return DiscoverIterator{
|
||||
@ -558,47 +551,13 @@ pub const CoreText = struct {
|
||||
for (0..result.len) |i| {
|
||||
result[i] = list.getValueAtIndex(macos.text.FontDescriptor, i);
|
||||
|
||||
// We need to retain because once the list
|
||||
// is freed it will release all its members.
|
||||
// We need to retain becauseonce the list is freed it will
|
||||
// release all its members.
|
||||
result[i].retain();
|
||||
}
|
||||
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(
|
||||
desc: *const Descriptor,
|
||||
list: []*macos.text.FontDescriptor,
|
||||
|
@ -34,3 +34,6 @@ pub const cozette = @embedFile("res/CozetteVector.ttf");
|
||||
/// Monaspace has weird ligature behaviors we want to test in our shapers
|
||||
/// so we embed it here.
|
||||
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");
|
||||
|
@ -515,8 +515,17 @@ pub const Face = struct {
|
||||
fn calcMetrics(ct_font: *macos.text.Font) CalcMetricsError!font.face.Metrics {
|
||||
// Read the 'head' table out of the font data.
|
||||
const head: opentype.Head = head: {
|
||||
const tag = macos.text.FontTableTag.init("head");
|
||||
const data = ct_font.copyTable(tag) orelse return error.CopyTableError;
|
||||
// macOS bitmap-only fonts use a 'bhed' tag rather than 'head', but
|
||||
// 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();
|
||||
const ptr = data.getPointer();
|
||||
const len = data.getLength();
|
||||
|
@ -288,7 +288,6 @@ pub const Face = struct {
|
||||
self.face.loadGlyph(glyph_id, .{
|
||||
.render = true,
|
||||
.color = self.face.hasColor(),
|
||||
.no_bitmap = !self.face.hasColor(),
|
||||
}) catch return false;
|
||||
|
||||
// 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.
|
||||
.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
|
||||
.no_hinting = !self.load_flags.hinting,
|
||||
.force_autohint = !self.load_flags.@"force-autohint",
|
||||
@ -385,7 +376,7 @@ pub const Face = struct {
|
||||
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,
|
||||
atlas.format,
|
||||
});
|
||||
@ -1005,3 +996,59 @@ test "svg font table" {
|
||||
|
||||
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)],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -43,26 +43,14 @@ pub fn monoToGrayscale(alloc: Allocator, bm: Bitmap) Allocator.Error!Bitmap {
|
||||
var buf = try alloc.alloc(u8, bm.width * bm.rows);
|
||||
errdefer alloc.free(buf);
|
||||
|
||||
// width divided by 8 because each byte has 8 pixels. This is therefore
|
||||
// the number of bytes in each row.
|
||||
const bytes_per_row = bm.width >> 3;
|
||||
|
||||
var source_i: usize = 0;
|
||||
var target_i: usize = 0;
|
||||
var i: usize = bm.rows;
|
||||
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;
|
||||
}
|
||||
for (0..bm.rows) |y| {
|
||||
const row_offset = y * @as(usize, @intCast(bm.pitch));
|
||||
for (0..bm.width) |x| {
|
||||
const byte_offset = row_offset + @divTrunc(x, 8);
|
||||
const mask = @as(u8, 1) << @intCast(7 - (x % 8));
|
||||
const bit: u8 = @intFromBool((bm.buffer[byte_offset] & mask) != 0);
|
||||
buf[y * bm.width + x] = bit * 255;
|
||||
}
|
||||
|
||||
source_i += @intCast(bm.pitch);
|
||||
}
|
||||
|
||||
var copy = bm;
|
||||
|
@ -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)
|
||||
- Cozette (MIT)
|
||||
- [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).
|
||||
An accompanying FAQ is also available at <https://openfontlicense.org/>.
|
||||
|
BIN
src/font/res/TerminusTTF-Regular.ttf
Normal file
BIN
src/font/res/TerminusTTF-Regular.ttf
Normal file
Binary file not shown.
@ -1019,6 +1019,14 @@ pub const Trigger = struct {
|
||||
const cp = it.nextCodepoint() orelse 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 };
|
||||
continue :loop;
|
||||
}
|
||||
@ -1554,6 +1562,19 @@ test "parse: triggers" {
|
||||
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
|
||||
try testing.expectEqual(Binding{
|
||||
.trigger = .{
|
||||
|
@ -729,7 +729,9 @@ pub const Key = enum(c_int) {
|
||||
.{ '\t', .tab },
|
||||
|
||||
// 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 },
|
||||
.{ '1', .kp_1 },
|
||||
.{ '2', .kp_2 },
|
||||
|
@ -18,7 +18,31 @@ pub fn open(
|
||||
typ: Type,
|
||||
url: []const u8,
|
||||
) !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;
|
||||
if (cmd.wait) {
|
||||
@ -53,31 +77,3 @@ const OpenCommand = struct {
|
||||
child: std.process.Child,
|
||||
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"),
|
||||
};
|
||||
}
|
||||
|
@ -3413,6 +3413,16 @@ pub const Pin = struct {
|
||||
direction: Direction,
|
||||
limit: ?Pin,
|
||||
) 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 .{
|
||||
.row = self,
|
||||
.limit = if (limit) |p| .{ .row = p } else .{ .none = {} },
|
||||
|
Reference in New Issue
Block a user