From 8da600a62e45776d6653da94bab132b28061f7a1 Mon Sep 17 00:00:00 2001
From: Bryan Lee <38807139+liby@users.noreply.github.com>
Date: Mon, 30 Dec 2024 00:14:48 +0800
Subject: [PATCH 001/365] Add keyboard navigation for Terminal IO window
- Add J/K and arrow keys navigation in Terminal IO window
---
src/inspector/Inspector.zig | 118 +++++++++++++++++++++++++++++++++++-
1 file changed, 116 insertions(+), 2 deletions(-)
diff --git a/src/inspector/Inspector.zig b/src/inspector/Inspector.zig
index 7dd61c8a1..e2d70660a 100644
--- a/src/inspector/Inspector.zig
+++ b/src/inspector/Inspector.zig
@@ -52,6 +52,22 @@ key_events: inspector.key.EventRing,
vt_events: inspector.termio.VTEventRing,
vt_stream: inspector.termio.Stream,
+/// The currently selected event sequence number for keyboard navigation
+selected_event_seq: ?u32 = null,
+
+/// Flag indicating whether we need to scroll to the selected item
+need_scroll_to_selected: bool = false,
+
+/// Flag indicating whether the selection was made by keyboard
+is_keyboard_selection: bool = false,
+
+/// Enum representing keyboard navigation actions
+const KeyAction = enum {
+ down,
+ none,
+ up,
+};
+
const CellInspect = union(enum) {
/// Idle, no cell inspection is requested
idle: void,
@@ -1013,6 +1029,24 @@ fn renderKeyboardWindow(self: *Inspector) void {
} // table
}
+/// Helper function to check keyboard state and determine navigation action.
+fn getKeyAction(self: *Inspector) KeyAction {
+ _ = self;
+ const keys = .{
+ .{ .key = cimgui.c.ImGuiKey_J, .action = KeyAction.down },
+ .{ .key = cimgui.c.ImGuiKey_DownArrow, .action = KeyAction.down },
+ .{ .key = cimgui.c.ImGuiKey_K, .action = KeyAction.up },
+ .{ .key = cimgui.c.ImGuiKey_UpArrow, .action = KeyAction.up },
+ };
+
+ inline for (keys) |k| {
+ if (cimgui.c.igIsKeyPressed_Bool(k.key, false)) {
+ return k.action;
+ }
+ }
+ return .none;
+}
+
fn renderTermioWindow(self: *Inspector) void {
// Start our window. If we're collapsed we do nothing.
defer cimgui.c.igEnd();
@@ -1089,6 +1123,60 @@ fn renderTermioWindow(self: *Inspector) void {
0,
);
+ // Handle keyboard navigation when window is focused
+ if (cimgui.c.igIsWindowFocused(cimgui.c.ImGuiFocusedFlags_RootAndChildWindows)) {
+ const key_pressed = self.getKeyAction();
+
+ switch (key_pressed) {
+ .none => {},
+ .up, .down => {
+ // If no event is selected, select the first/last event based on direction
+ if (self.selected_event_seq == null) {
+ if (!self.vt_events.empty()) {
+ var it = self.vt_events.iterator(if (key_pressed == .up) .forward else .reverse);
+ if (it.next()) |ev| {
+ self.selected_event_seq = @as(u32, @intCast(ev.seq));
+ }
+ }
+ } else {
+ // Find next/previous event based on current selection
+ var it = self.vt_events.iterator(.reverse);
+ switch (key_pressed) {
+ .down => {
+ var found = false;
+ while (it.next()) |ev| {
+ if (found) {
+ self.selected_event_seq = @as(u32, @intCast(ev.seq));
+ break;
+ }
+ if (ev.seq == self.selected_event_seq.?) {
+ found = true;
+ }
+ }
+ },
+ .up => {
+ var prev_ev: ?*const inspector.termio.VTEvent = null;
+ while (it.next()) |ev| {
+ if (ev.seq == self.selected_event_seq.?) {
+ if (prev_ev) |prev| {
+ self.selected_event_seq = @as(u32, @intCast(prev.seq));
+ break;
+ }
+ }
+ prev_ev = ev;
+ }
+ },
+ .none => unreachable,
+ }
+ }
+
+ // Mark that we need to scroll to the newly selected item
+ self.need_scroll_to_selected = true;
+ self.is_keyboard_selection = true;
+ },
+ }
+ }
+
var it = self.vt_events.iterator(.reverse);
while (it.next()) |ev| {
// Need to push an ID so that our selectable is unique.
@@ -1097,12 +1185,32 @@ fn renderTermioWindow(self: *Inspector) void {
cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0);
_ = cimgui.c.igTableNextColumn();
- _ = cimgui.c.igSelectable_BoolPtr(
+
+ // Store the previous selection state to detect changes
+ const was_selected = ev.imgui_selected;
+
+ // Update selection state based on keyboard navigation
+ if (self.selected_event_seq) |seq| {
+ ev.imgui_selected = (@as(u32, @intCast(ev.seq)) == seq);
+ }
+
+ // Handle selectable widget
+ if (cimgui.c.igSelectable_BoolPtr(
"##select",
&ev.imgui_selected,
cimgui.c.ImGuiSelectableFlags_SpanAllColumns,
.{ .x = 0, .y = 0 },
- );
+ )) {
+ // If selection state changed, update keyboard navigation state
+ if (ev.imgui_selected != was_selected) {
+ self.selected_event_seq = if (ev.imgui_selected)
+ @as(u32, @intCast(ev.seq))
+ else
+ null;
+ self.is_keyboard_selection = false;
+ }
+ }
+
cimgui.c.igSameLine(0, 0);
cimgui.c.igText("%d", ev.seq);
_ = cimgui.c.igTableNextColumn();
@@ -1158,6 +1266,12 @@ fn renderTermioWindow(self: *Inspector) void {
cimgui.c.igText("%s", entry.value_ptr.ptr);
}
}
+
+ // If this is the selected event and scrolling is needed, scroll to it
+ if (self.need_scroll_to_selected and self.is_keyboard_selection) {
+ cimgui.c.igSetScrollHereY(0.5);
+ self.need_scroll_to_selected = false;
+ }
}
}
} // table
From c16dbc01f0ffcc5ff61156dcf9a0d17eb5a5f3ef Mon Sep 17 00:00:00 2001
From: Ryan Rotter
Date: Tue, 31 Dec 2024 22:59:32 -0500
Subject: [PATCH 002/365] correct default keybinding cmd+backspace for macOS
cmd+backspace=text:\x15
---
src/config/Config.zig | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/config/Config.zig b/src/config/Config.zig
index 91c07cc78..f03a0d726 100644
--- a/src/config/Config.zig
+++ b/src/config/Config.zig
@@ -2603,7 +2603,7 @@ pub fn default(alloc_gpa: Allocator) Allocator.Error!Config {
try result.keybind.set.put(
alloc,
.{ .key = .{ .translated = .backspace }, .mods = .{ .super = true } },
- .{ .esc = "\x15" },
+ .{ .text = "\\x15" },
);
try result.keybind.set.put(
alloc,
From 3698b37588a99af6b6664cb23caeece743d528e4 Mon Sep 17 00:00:00 2001
From: Tristan Partin
Date: Sun, 17 Nov 2024 13:38:44 -0600
Subject: [PATCH 003/365] apprt/gtk: use a subtitle to mark the current working
directory
If the title is already the current working directory, hide the
subtitle. Otherwise show the current working directory, like if
a command is running for instance.
Signed-off-by: Tristan Partin
---
src/apprt/gtk/Surface.zig | 32 ++++++++++++++++++++++++++++++--
src/apprt/gtk/Window.zig | 16 ++++++++++++++++
src/apprt/gtk/headerbar.zig | 27 +++++++++++++++++++++++++--
src/apprt/gtk/notebook_adw.zig | 2 +-
src/apprt/gtk/notebook_gtk.zig | 2 +-
src/config/Config.zig | 14 ++++++++++++++
6 files changed, 87 insertions(+), 6 deletions(-)
diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig
index 056a3f40b..180f986ca 100644
--- a/src/apprt/gtk/Surface.zig
+++ b/src/apprt/gtk/Surface.zig
@@ -347,6 +347,11 @@ cursor: ?*c.GdkCursor = null,
/// pass it to GTK.
title_text: ?[:0]const u8 = null,
+/// Our current working directory. We use this value for setting tooltips in
+/// the headerbar subtitle if we have focus. When set, the text in this buf
+/// will be null-terminated because we need to pass it to GTK.
+pwd: ?[:0]const u8 = null,
+
/// The timer used to delay title updates in order to prevent flickering.
update_title_timer: ?c.guint = null,
@@ -628,6 +633,7 @@ fn realize(self: *Surface) !void {
pub fn deinit(self: *Surface) void {
self.init_config.deinit(self.app.core_app.alloc);
if (self.title_text) |title| self.app.core_app.alloc.free(title);
+ if (self.pwd) |pwd| self.app.core_app.alloc.free(pwd);
// We don't allocate anything if we aren't realized.
if (!self.realized) return;
@@ -876,7 +882,7 @@ fn updateTitleLabels(self: *Surface) void {
// I don't know a way around this yet. I've tried re-hiding the
// cursor after setting the title but it doesn't work, I think
// due to some gtk event loop things...
- c.gtk_window_set_title(window.window, title.ptr);
+ window.setTitle(title);
}
}
}
@@ -929,11 +935,27 @@ pub fn getTitle(self: *Surface) ?[:0]const u8 {
return null;
}
+/// Set the current working directory of the surface.
+///
+/// In addition, update the tab's tooltip text, and if we are the focused child,
+/// update the subtitle of the containing window.
pub fn setPwd(self: *Surface, pwd: [:0]const u8) !void {
- // If we have a tab and are the focused child, then we have to update the tab
if (self.container.tab()) |tab| {
tab.setTooltipText(pwd);
+
+ if (tab.focus_child == self) {
+ if (self.container.window()) |window| {
+ if (self.app.config.@"window-subtitle" == .@"working-directory") window.setSubtitle(pwd);
+ }
+ }
}
+
+ const alloc = self.app.core_app.alloc;
+
+ // Failing to set the surface's current working directory is not a big
+ // deal since we just used our slice parameter which is the same value.
+ if (self.pwd) |old| alloc.free(old);
+ self.pwd = alloc.dupeZ(u8, pwd) catch null;
}
pub fn setMouseShape(
@@ -1896,6 +1918,12 @@ fn gtkFocusEnter(_: *c.GtkEventControllerFocus, ud: ?*anyopaque) callconv(.C) vo
self.unfocused_widget = null;
}
+ if (self.pwd) |pwd| {
+ if (self.container.window()) |window| {
+ if (self.app.config.@"window-subtitle" == .@"working-directory") window.setSubtitle(pwd);
+ }
+ }
+
// Notify our surface
self.core_surface.focusCallback(true) catch |err| {
log.err("error in focus callback err={}", .{err});
diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig
index 63ee57d95..c2c69e281 100644
--- a/src/apprt/gtk/Window.zig
+++ b/src/apprt/gtk/Window.zig
@@ -453,6 +453,22 @@ pub fn deinit(self: *Window) void {
}
}
+/// Set the title of the window.
+pub fn setTitle(self: *Window, title: [:0]const u8) void {
+ if ((comptime adwaita.versionAtLeast(1, 4, 0)) and adwaita.versionAtLeast(1, 4, 0) and adwaita.enabled(&self.app.config) and self.app.config.@"gtk-titlebar") {
+ if (self.header) |header| header.setTitle(title);
+ } else {
+ c.gtk_window_set_title(self.window, title);
+ }
+}
+
+/// Set the subtitle of the window if it has one.
+pub fn setSubtitle(self: *Window, subtitle: [:0]const u8) void {
+ if ((comptime adwaita.versionAtLeast(1, 4, 0)) and adwaita.versionAtLeast(1, 4, 0) and adwaita.enabled(&self.app.config) and self.app.config.@"gtk-titlebar") {
+ if (self.header) |header| header.setSubtitle(subtitle);
+ }
+}
+
/// Add a new tab to this window.
pub fn newTab(self: *Window, parent: ?*CoreSurface) !void {
const alloc = self.app.core_app.alloc;
diff --git a/src/apprt/gtk/headerbar.zig b/src/apprt/gtk/headerbar.zig
index 5bb92aca2..97c48a4c2 100644
--- a/src/apprt/gtk/headerbar.zig
+++ b/src/apprt/gtk/headerbar.zig
@@ -14,14 +14,15 @@ pub const HeaderBar = union(enum) {
if ((comptime adwaita.versionAtLeast(1, 4, 0)) and
adwaita.enabled(&window.app.config))
{
- return initAdw();
+ return initAdw(window);
}
return initGtk();
}
- fn initAdw() HeaderBar {
+ fn initAdw(window: *Window) HeaderBar {
const headerbar = c.adw_header_bar_new();
+ c.adw_header_bar_set_title_widget(@ptrCast(headerbar), @ptrCast(c.adw_window_title_new(c.gtk_window_get_title(window.window) orelse "Ghostty", null)));
return .{ .adw = @ptrCast(headerbar) };
}
@@ -70,4 +71,26 @@ pub const HeaderBar = union(enum) {
),
}
}
+
+ pub fn setTitle(self: HeaderBar, title: [:0]const u8) void {
+ switch (self) {
+ .adw => |headerbar| if (comptime adwaita.versionAtLeast(0, 0, 0)) {
+ const window_title: *c.AdwWindowTitle = @ptrCast(c.adw_header_bar_get_title_widget(@ptrCast(headerbar)));
+ c.adw_window_title_set_title(window_title, title);
+ },
+ // The title is owned by the window when not using Adwaita
+ .gtk => unreachable,
+ }
+ }
+
+ pub fn setSubtitle(self: HeaderBar, subtitle: [:0]const u8) void {
+ switch (self) {
+ .adw => |headerbar| if (comptime adwaita.versionAtLeast(0, 0, 0)) {
+ const window_title: *c.AdwWindowTitle = @ptrCast(c.adw_header_bar_get_title_widget(@ptrCast(headerbar)));
+ c.adw_window_title_set_subtitle(window_title, subtitle);
+ },
+ // There is no subtitle unless Adwaita is used
+ .gtk => unreachable,
+ }
+ }
};
diff --git a/src/apprt/gtk/notebook_adw.zig b/src/apprt/gtk/notebook_adw.zig
index 85083a97e..48f005467 100644
--- a/src/apprt/gtk/notebook_adw.zig
+++ b/src/apprt/gtk/notebook_adw.zig
@@ -159,5 +159,5 @@ fn adwSelectPage(_: *c.GObject, _: *c.GParamSpec, ud: ?*anyopaque) void {
const window: *Window = @ptrCast(@alignCast(ud.?));
const page = c.adw_tab_view_get_selected_page(window.notebook.adw.tab_view) orelse return;
const title = c.adw_tab_page_get_title(page);
- c.gtk_window_set_title(window.window, title);
+ window.setTitle(std.mem.span(title));
}
diff --git a/src/apprt/gtk/notebook_gtk.zig b/src/apprt/gtk/notebook_gtk.zig
index 6e8b016ba..a2c482500 100644
--- a/src/apprt/gtk/notebook_gtk.zig
+++ b/src/apprt/gtk/notebook_gtk.zig
@@ -259,7 +259,7 @@ fn gtkSwitchPage(_: *c.GtkNotebook, page: *c.GtkWidget, _: usize, ud: ?*anyopaqu
const gtk_label_box = @as(*c.GtkWidget, @ptrCast(c.gtk_notebook_get_tab_label(self.notebook, page)));
const gtk_label = @as(*c.GtkLabel, @ptrCast(c.gtk_widget_get_first_child(gtk_label_box)));
const label_text = c.gtk_label_get_text(gtk_label);
- c.gtk_window_set_title(window.window, label_text);
+ window.setTitle(std.mem.span(label_text));
}
fn gtkNotebookCreateWindow(
diff --git a/src/config/Config.zig b/src/config/Config.zig
index 6cd6ad75e..6d2c026fe 100644
--- a/src/config/Config.zig
+++ b/src/config/Config.zig
@@ -1118,6 +1118,15 @@ keybind: Keybinds = .{},
/// required to be a fixed-width font.
@"window-title-font-family": ?[:0]const u8 = null,
+/// The text that will be displayed in the subtitle of the window. Valid values:
+///
+/// * `false` - Disable the subtitle.
+/// * `working-directory` - Set the subtitle to the working directory of the
+/// surface.
+///
+/// This feature is only supported on GTK with Adwaita enabled.
+@"window-subtitle": WindowSubtitle = .false,
+
/// The theme to use for the windows. Valid values:
///
/// * `auto` - Determine the theme based on the configured terminal
@@ -3968,6 +3977,11 @@ pub const WindowPaddingColor = enum {
@"extend-always",
};
+pub const WindowSubtitle = enum {
+ false,
+ @"working-directory",
+};
+
/// Color represents a color using RGB.
///
/// This is a packed struct so that the C API to read color values just
From c97205161155c227cd4102e050e16933ec7e806f Mon Sep 17 00:00:00 2001
From: Gabriele Musco
Date: Wed, 8 Jan 2025 13:43:53 +0100
Subject: [PATCH 004/365] linux: add "Open in Ghostty" shortcut for nautilus
---
dist/linux/ghostty_nautilus.py | 97 ++++++++++++++++++++++++++++++++++
src/build/GhosttyResources.zig | 6 +++
2 files changed, 103 insertions(+)
create mode 100644 dist/linux/ghostty_nautilus.py
diff --git a/dist/linux/ghostty_nautilus.py b/dist/linux/ghostty_nautilus.py
new file mode 100644
index 000000000..42c397642
--- /dev/null
+++ b/dist/linux/ghostty_nautilus.py
@@ -0,0 +1,97 @@
+# Adapted from wezterm: https://github.com/wez/wezterm/blob/main/assets/wezterm-nautilus.py
+# original copyright notice:
+#
+# Copyright (C) 2022 Sebastian Wiesner
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+from os.path import isdir
+from gi import require_version
+from gi.repository import Nautilus, GObject, Gio, GLib
+
+
+class OpenInGhosttyAction(GObject.GObject, Nautilus.MenuProvider):
+ def __init__(self):
+ super().__init__()
+ session = Gio.bus_get_sync(Gio.BusType.SESSION, None)
+ self._systemd = None
+ # Check if the this system runs under systemd, per sd_booted(3)
+ if isdir('/run/systemd/system/'):
+ self._systemd = Gio.DBusProxy.new_sync(session,
+ Gio.DBusProxyFlags.NONE,
+ None,
+ "org.freedesktop.systemd1",
+ "/org/freedesktop/systemd1",
+ "org.freedesktop.systemd1.Manager", None)
+
+ def _open_terminal(self, path):
+ cmd = ['ghostty', f'--working-directory={path}', '--gtk-single-instance=false']
+ child = Gio.Subprocess.new(cmd, Gio.SubprocessFlags.NONE)
+ if self._systemd:
+ # Move new terminal into a dedicated systemd scope to make systemd
+ # track the terminal separately; in particular this makes systemd
+ # keep a separate CPU and memory account for the terminal which in turn
+ # ensures that oomd doesn't take nautilus down if a process in
+ # ghostty consumes a lot of memory.
+ pid = int(child.get_identifier())
+ props = [("PIDs", GLib.Variant('au', [pid])),
+ ('CollectMode', GLib.Variant('s', 'inactive-or-failed'))]
+ name = 'app-nautilus-com.mitchellh.ghostty-{}.scope'.format(pid)
+ args = GLib.Variant('(ssa(sv)a(sa(sv)))', (name, 'fail', props, []))
+ self._systemd.call_sync('StartTransientUnit', args,
+ Gio.DBusCallFlags.NO_AUTO_START, 500, None)
+
+ def _menu_item_activated(self, _menu, paths):
+ for path in paths:
+ self._open_terminal(path)
+
+ def _make_item(self, name, paths):
+ item = Nautilus.MenuItem(name=name, label='Open in Ghostty',
+ icon='com.mitchellh.ghostty')
+ item.connect('activate', self._menu_item_activated, paths)
+ return item
+
+ def _paths_to_open(self, files):
+ paths = []
+ for file in files:
+ location = file.get_location() if file.is_directory() else file.get_parent_location()
+ path = location.get_path()
+ if path and path not in paths:
+ paths.append(path)
+ if 10 < len(paths):
+ # Let's not open anything if the user selected a lot of directories,
+ # to avoid accidentally spamming their desktop with dozends of
+ # new windows or tabs. Ten is a totally arbitrary limit :)
+ return []
+ else:
+ return paths
+
+ def get_file_items(self, *args):
+ # Nautilus 3.0 API passes args (window, files), 4.0 API just passes files
+ files = args[0] if len(args) == 1 else args[1]
+ paths = self._paths_to_open(files)
+ if paths:
+ return [self._make_item(name='GhosttyNautilus::open_in_ghostty', paths=paths)]
+ else:
+ return []
+
+ def get_background_items(self, *args):
+ # Nautilus 3.0 API passes args (window, file), 4.0 API just passes file
+ file = args[0] if len(args) == 1 else args[1]
+ paths = self._paths_to_open([file])
+ if paths:
+ return [self._make_item(name='GhosttyNautilus::open_folder_in_ghostty', paths=paths)]
+ else:
+ return []
diff --git a/src/build/GhosttyResources.zig b/src/build/GhosttyResources.zig
index 9c5f7f809..cae907ec2 100644
--- a/src/build/GhosttyResources.zig
+++ b/src/build/GhosttyResources.zig
@@ -200,6 +200,12 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources {
"share/kio/servicemenus/com.mitchellh.ghostty.desktop",
).step);
+ // Right click menu action for Nautilus
+ try steps.append(&b.addInstallFile(
+ b.path("dist/linux/ghostty_nautilus.py"),
+ "share/nautilus-python/extensions/com.mitchellh.ghostty.py",
+ ).step);
+
// Various icons that our application can use, including the icon
// that will be used for the desktop.
try steps.append(&b.addInstallFile(
From e7c71df0b7c056a426e6d61a2feff785741d1e6d Mon Sep 17 00:00:00 2001
From: "Jeffrey C. Ollie"
Date: Wed, 1 Jan 2025 13:38:46 -0600
Subject: [PATCH 005/365] gtk: implement dropping files and strings
---
src/apprt/gtk/Surface.zig | 104 ++++++++++++++++++++++++++++++++++++++
src/os/main.zig | 2 +
src/os/shell.zig | 90 +++++++++++++++++++++++++++++++++
3 files changed, 196 insertions(+)
create mode 100644 src/os/shell.zig
diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig
index 056a3f40b..f0ae073f9 100644
--- a/src/apprt/gtk/Surface.zig
+++ b/src/apprt/gtk/Surface.zig
@@ -492,6 +492,17 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void {
c.gtk_widget_set_focusable(gl_area, 1);
c.gtk_widget_set_focus_on_click(gl_area, 1);
+ // Set up to handle items being dropped on our surface. Files can be dropped
+ // from Nautilus and strings can be dropped from many programs.
+ const drop_target = c.gtk_drop_target_new(c.G_TYPE_INVALID, c.GDK_ACTION_COPY);
+ errdefer c.g_object_unref(drop_target);
+ var drop_target_types = [_]c.GType{
+ c.gdk_file_list_get_type(),
+ c.G_TYPE_STRING,
+ };
+ c.gtk_drop_target_set_gtypes(drop_target, @ptrCast(&drop_target_types), drop_target_types.len);
+ c.gtk_widget_add_controller(@ptrCast(overlay), @ptrCast(drop_target));
+
// Inherit the parent's font size if we have a parent.
const font_size: ?font.face.DesiredSize = font_size: {
if (!app.config.@"window-inherit-font-size") break :font_size null;
@@ -574,6 +585,7 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void {
_ = c.g_signal_connect_data(im_context, "preedit-changed", c.G_CALLBACK(>kInputPreeditChanged), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(im_context, "preedit-end", c.G_CALLBACK(>kInputPreeditEnd), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(im_context, "commit", c.G_CALLBACK(>kInputCommit), self, null, c.G_CONNECT_DEFAULT);
+ _ = c.g_signal_connect_data(drop_target, "drop", c.G_CALLBACK(>kDrop), self, null, c.G_CONNECT_DEFAULT);
}
fn realize(self: *Surface) !void {
@@ -2025,3 +2037,95 @@ pub fn setSplitZoom(self: *Surface, new_split_zoom: bool) void {
pub fn toggleSplitZoom(self: *Surface) void {
self.setSplitZoom(!self.zoomed_in);
}
+
+/// Handle items being dropped on our surface.
+fn gtkDrop(
+ _: *c.GtkDropTarget,
+ value: *c.GValue,
+ x: f64,
+ y: f64,
+ ud: ?*anyopaque,
+) callconv(.C) c.gboolean {
+ _ = x;
+ _ = y;
+ const self = userdataSelf(ud.?);
+ const alloc = self.app.core_app.alloc;
+
+ if (g_value_holds(value, c.G_TYPE_BOXED)) {
+ var data = std.ArrayList(u8).init(alloc);
+ defer data.deinit();
+
+ var shell_escape_writer: internal_os.ShellEscapeWriter(std.ArrayList(u8).Writer) = .{
+ .child_writer = data.writer(),
+ };
+ const writer = shell_escape_writer.writer();
+
+ const fl: *c.GdkFileList = @ptrCast(c.g_value_get_boxed(value));
+ var l = c.gdk_file_list_get_files(fl);
+
+ while (l != null) : (l = l.*.next) {
+ const file: *c.GFile = @ptrCast(l.*.data);
+ const path = c.g_file_get_path(file) orelse continue;
+
+ writer.writeAll(std.mem.span(path)) catch |err| {
+ log.err("unable to write path to buffer: {}", .{err});
+ continue;
+ };
+ writer.writeAll("\n") catch |err| {
+ log.err("unable to write to buffer: {}", .{err});
+ continue;
+ };
+ }
+
+ const string = data.toOwnedSliceSentinel(0) catch |err| {
+ log.err("unable to convert to a slice: {}", .{err});
+ return 1;
+ };
+ defer alloc.free(string);
+
+ self.doPaste(string);
+
+ return 1;
+ }
+
+ if (g_value_holds(value, c.G_TYPE_STRING)) {
+ if (c.g_value_get_string(value)) |string| {
+ self.doPaste(std.mem.span(string));
+ }
+ return 1;
+ }
+
+ return 1;
+}
+
+fn doPaste(self: *Surface, data: [:0]const u8) void {
+ if (data.len == 0) return;
+
+ self.core_surface.completeClipboardRequest(.paste, data, false) catch |err| switch (err) {
+ error.UnsafePaste,
+ error.UnauthorizedPaste,
+ => {
+ ClipboardConfirmationWindow.create(
+ self.app,
+ data,
+ &self.core_surface,
+ .paste,
+ ) catch |window_err| {
+ log.err("failed to create clipboard confirmation window err={}", .{window_err});
+ };
+ },
+ error.OutOfMemory,
+ error.NoSpaceLeft,
+ => log.err("failed to complete clipboard request err={}", .{err}),
+ };
+}
+
+/// Check a GValue to see what's type its wrapping. This is equivalent to GTK's
+/// `G_VALUE_HOLDS` macro but Zig's C translator does not like it.
+fn g_value_holds(value_: ?*c.GValue, g_type: c.GType) bool {
+ if (value_) |value| {
+ if (value.*.g_type == g_type) return true;
+ return c.g_type_check_value_holds(value, g_type) != 0;
+ }
+ return false;
+}
diff --git a/src/os/main.zig b/src/os/main.zig
index e652a7981..df6f894f5 100644
--- a/src/os/main.zig
+++ b/src/os/main.zig
@@ -21,6 +21,7 @@ pub const passwd = @import("passwd.zig");
pub const xdg = @import("xdg.zig");
pub const windows = @import("windows.zig");
pub const macos = @import("macos.zig");
+pub const shell = @import("shell.zig");
// Functions and types
pub const CFReleaseThread = @import("cf_release_thread.zig");
@@ -48,3 +49,4 @@ pub const open = openpkg.open;
pub const OpenType = openpkg.Type;
pub const pipe = pipepkg.pipe;
pub const resourcesDir = resourcesdir.resourcesDir;
+pub const ShellEscapeWriter = shell.ShellEscapeWriter;
diff --git a/src/os/shell.zig b/src/os/shell.zig
new file mode 100644
index 000000000..a9cb61847
--- /dev/null
+++ b/src/os/shell.zig
@@ -0,0 +1,90 @@
+const std = @import("std");
+const testing = std.testing;
+
+pub fn ShellEscapeWriter(comptime T: type) type {
+ return struct {
+ child_writer: T,
+
+ fn write(self: *ShellEscapeWriter(T), data: []const u8) error{Error}!usize {
+ var count: usize = 0;
+ for (data) |byte| {
+ const buf = switch (byte) {
+ '\\',
+ '"',
+ '\'',
+ '$',
+ '`',
+ '*',
+ '?',
+ ' ',
+ '|',
+ => &[_]u8{ '\\', byte },
+ else => &[_]u8{byte},
+ };
+ self.child_writer.writeAll(buf) catch return error.Error;
+ count += 1;
+ }
+ return count;
+ }
+
+ const Writer = std.io.Writer(*ShellEscapeWriter(T), error{Error}, write);
+
+ pub fn writer(self: *ShellEscapeWriter(T)) Writer {
+ return .{ .context = self };
+ }
+ };
+}
+
+test "shell escape 1" {
+ var buf: [128]u8 = undefined;
+ var fmt = std.io.fixedBufferStream(&buf);
+ var shell: ShellEscapeWriter(@TypeOf(fmt).Writer) = .{ .child_writer = fmt.writer() };
+ const writer = shell.writer();
+ try writer.writeAll("abc");
+ try testing.expectEqualStrings("abc", fmt.getWritten());
+}
+
+test "shell escape 2" {
+ var buf: [128]u8 = undefined;
+ var fmt = std.io.fixedBufferStream(&buf);
+ var shell: ShellEscapeWriter(@TypeOf(fmt).Writer) = .{ .child_writer = fmt.writer() };
+ const writer = shell.writer();
+ try writer.writeAll("a c");
+ try testing.expectEqualStrings("a\\ c", fmt.getWritten());
+}
+
+test "shell escape 3" {
+ var buf: [128]u8 = undefined;
+ var fmt = std.io.fixedBufferStream(&buf);
+ var shell: ShellEscapeWriter(@TypeOf(fmt).Writer) = .{ .child_writer = fmt.writer() };
+ const writer = shell.writer();
+ try writer.writeAll("a?c");
+ try testing.expectEqualStrings("a\\?c", fmt.getWritten());
+}
+
+test "shell escape 4" {
+ var buf: [128]u8 = undefined;
+ var fmt = std.io.fixedBufferStream(&buf);
+ var shell: ShellEscapeWriter(@TypeOf(fmt).Writer) = .{ .child_writer = fmt.writer() };
+ const writer = shell.writer();
+ try writer.writeAll("a\\c");
+ try testing.expectEqualStrings("a\\\\c", fmt.getWritten());
+}
+
+test "shell escape 5" {
+ var buf: [128]u8 = undefined;
+ var fmt = std.io.fixedBufferStream(&buf);
+ var shell: ShellEscapeWriter(@TypeOf(fmt).Writer) = .{ .child_writer = fmt.writer() };
+ const writer = shell.writer();
+ try writer.writeAll("a|c");
+ try testing.expectEqualStrings("a\\|c", fmt.getWritten());
+}
+
+test "shell escape 6" {
+ var buf: [128]u8 = undefined;
+ var fmt = std.io.fixedBufferStream(&buf);
+ var shell: ShellEscapeWriter(@TypeOf(fmt).Writer) = .{ .child_writer = fmt.writer() };
+ const writer = shell.writer();
+ try writer.writeAll("a\"c");
+ try testing.expectEqualStrings("a\\\"c", fmt.getWritten());
+}
From e86b9a112e49dd147fe63210ef9ce9c8d0d5e72f Mon Sep 17 00:00:00 2001
From: Wes Campaigne
Date: Mon, 6 Jan 2025 19:01:19 -0500
Subject: [PATCH 006/365] Implement "Paste Selection" on macOS like
Terminal.app
---
macos/Sources/App/macOS/AppDelegate.swift | 2 ++
macos/Sources/App/macOS/MainMenu.xib | 11 +++++++--
.../Terminal/BaseTerminalController.swift | 4 ++--
macos/Sources/Ghostty/Ghostty.App.swift | 18 +++++++-------
macos/Sources/Ghostty/Package.swift | 4 ++--
.../Sources/Ghostty/SurfaceView_AppKit.swift | 24 +++++++++++++++++++
macos/Sources/Helpers/CrossKit.swift | 2 ++
.../Helpers/NSPasteboard+Extension.swift | 20 ++++++++++++++++
src/config/Config.zig | 14 +++++++----
9 files changed, 78 insertions(+), 21 deletions(-)
diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift
index e3518cd2b..2fe835303 100644
--- a/macos/Sources/App/macOS/AppDelegate.swift
+++ b/macos/Sources/App/macOS/AppDelegate.swift
@@ -35,6 +35,7 @@ class AppDelegate: NSObject,
@IBOutlet private var menuCopy: NSMenuItem?
@IBOutlet private var menuPaste: NSMenuItem?
+ @IBOutlet private var menuPasteSelection: NSMenuItem?
@IBOutlet private var menuSelectAll: NSMenuItem?
@IBOutlet private var menuToggleVisibility: NSMenuItem?
@@ -353,6 +354,7 @@ class AppDelegate: NSObject,
syncMenuShortcut(config, action: "copy_to_clipboard", menuItem: self.menuCopy)
syncMenuShortcut(config, action: "paste_from_clipboard", menuItem: self.menuPaste)
+ syncMenuShortcut(config, action: "paste_from_selection", menuItem: self.menuPasteSelection)
syncMenuShortcut(config, action: "select_all", menuItem: self.menuSelectAll)
syncMenuShortcut(config, action: "toggle_split_zoom", menuItem: self.menuZoomSplit)
diff --git a/macos/Sources/App/macOS/MainMenu.xib b/macos/Sources/App/macOS/MainMenu.xib
index 7a8e0d894..0a197fe65 100644
--- a/macos/Sources/App/macOS/MainMenu.xib
+++ b/macos/Sources/App/macOS/MainMenu.xib
@@ -1,8 +1,8 @@
-
+
-
+
@@ -31,6 +31,7 @@
+
@@ -185,6 +186,12 @@
+
+
]]>
"""
elem = ET.SubElement(item, "enclosure")
elem.set("url", f"https://release.files.ghostty.org/{version}/Ghostty.dmg")
diff --git a/dist/macos/update_appcast_tip.py b/dist/macos/update_appcast_tip.py
index ff1fb4be5..c6a20f75f 100644
--- a/dist/macos/update_appcast_tip.py
+++ b/dist/macos/update_appcast_tip.py
@@ -83,7 +83,7 @@ elem = ET.SubElement(item, "sparkle:minimumSystemVersion")
elem.text = "13.0.0"
elem = ET.SubElement(item, "description")
elem.text = f"""
-
+
Automated build from commit {commit}
on {now.strftime('%Y-%m-%d')}.
@@ -91,7 +91,7 @@ on {now.strftime('%Y-%m-%d')}.
These are automatic per-commit builds generated from the main Git branch.
We do not generate any release notes for these builds. You can view the full
commit history on GitHub for all changes.
-
+]]>
"""
elem = ET.SubElement(item, "enclosure")
elem.set("url", f"https://tip.files.ghostty.org/{commit_long}/Ghostty.dmg")
From c2da843dfdcbe74fa1db8a676bb6e76d76621ee6 Mon Sep 17 00:00:00 2001
From: Qwerasd
Date: Fri, 17 Jan 2025 13:28:14 -0500
Subject: [PATCH 117/365] fix(wuffs): don't premul alpha when loading images
It seems like the raw data version of the kitty graphics transmit
operation is meant to be unassociated (aka straight) alpha, though I
can't find any definitive documentation either way- but in any case
unassociated alpha is more common in image formats and makes the
handling easier for the rest of it.
Also removed a redundant call to `decode_frame_config`, since it's
called implicitly when we call `decode_frame` right after.
---
pkg/wuffs/src/jpeg.zig | 12 +-----------
pkg/wuffs/src/png.zig | 12 +-----------
2 files changed, 2 insertions(+), 22 deletions(-)
diff --git a/pkg/wuffs/src/jpeg.zig b/pkg/wuffs/src/jpeg.zig
index 69628f582..c07278eed 100644
--- a/pkg/wuffs/src/jpeg.zig
+++ b/pkg/wuffs/src/jpeg.zig
@@ -55,7 +55,7 @@ pub fn decode(alloc: Allocator, data: []const u8) Error!ImageData {
c.wuffs_base__pixel_config__set(
&image_config.pixcfg,
- c.WUFFS_BASE__PIXEL_FORMAT__RGBA_PREMUL,
+ c.WUFFS_BASE__PIXEL_FORMAT__RGBA_NONPREMUL,
c.WUFFS_BASE__PIXEL_SUBSAMPLING__NONE,
width,
height,
@@ -95,16 +95,6 @@ pub fn decode(alloc: Allocator, data: []const u8) Error!ImageData {
try check(log, &status);
}
- var frame_config: c.wuffs_base__frame_config = undefined;
- {
- const status = c.wuffs_jpeg__decoder__decode_frame_config(
- decoder,
- &frame_config,
- &source_buffer,
- );
- try check(log, &status);
- }
-
{
const status = c.wuffs_jpeg__decoder__decode_frame(
decoder,
diff --git a/pkg/wuffs/src/png.zig b/pkg/wuffs/src/png.zig
index b85e4d747..1f37bb375 100644
--- a/pkg/wuffs/src/png.zig
+++ b/pkg/wuffs/src/png.zig
@@ -55,7 +55,7 @@ pub fn decode(alloc: Allocator, data: []const u8) Error!ImageData {
c.wuffs_base__pixel_config__set(
&image_config.pixcfg,
- c.WUFFS_BASE__PIXEL_FORMAT__RGBA_PREMUL,
+ c.WUFFS_BASE__PIXEL_FORMAT__RGBA_NONPREMUL,
c.WUFFS_BASE__PIXEL_SUBSAMPLING__NONE,
width,
height,
@@ -95,16 +95,6 @@ pub fn decode(alloc: Allocator, data: []const u8) Error!ImageData {
try check(log, &status);
}
- var frame_config: c.wuffs_base__frame_config = undefined;
- {
- const status = c.wuffs_png__decoder__decode_frame_config(
- decoder,
- &frame_config,
- &source_buffer,
- );
- try check(log, &status);
- }
-
{
const status = c.wuffs_png__decoder__decode_frame(
decoder,
From 8ee4deddb4edf0d4b39b673b14c3d7df2ec5244d Mon Sep 17 00:00:00 2001
From: Bryan Lee <38807139+liby@users.noreply.github.com>
Date: Tue, 14 Jan 2025 16:37:28 +0800
Subject: [PATCH 118/365] Fix `shell-integration-features` being ignored when
`shell-integration` is `none`
---
src/termio/Exec.zig | 6 +++++-
src/termio/shell_integration.zig | 15 ++++++++++++---
2 files changed, 17 insertions(+), 4 deletions(-)
diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig
index 1a3b8cad0..b1a19a6c7 100644
--- a/src/termio/Exec.zig
+++ b/src/termio/Exec.zig
@@ -875,7 +875,11 @@ const Subprocess = struct {
};
const force: ?shell_integration.Shell = switch (cfg.shell_integration) {
- .none => break :shell .{ null, default_shell_command },
+ .none => {
+ // Even if shell integration is none, we still want to set up the feature env vars
+ try shell_integration.setupFeatures(&env, cfg.shell_integration_features);
+ break :shell .{ null, default_shell_command };
+ },
.detect => null,
.bash => .bash,
.elvish => .elvish,
diff --git a/src/termio/shell_integration.zig b/src/termio/shell_integration.zig
index 8cd2a92ae..85d9a8376 100644
--- a/src/termio/shell_integration.zig
+++ b/src/termio/shell_integration.zig
@@ -114,9 +114,7 @@ pub fn setup(
};
// Setup our feature env vars
- if (!features.cursor) try env.put("GHOSTTY_SHELL_INTEGRATION_NO_CURSOR", "1");
- if (!features.sudo) try env.put("GHOSTTY_SHELL_INTEGRATION_NO_SUDO", "1");
- if (!features.title) try env.put("GHOSTTY_SHELL_INTEGRATION_NO_TITLE", "1");
+ try setupFeatures(env, features);
return result;
}
@@ -138,6 +136,17 @@ test "force shell" {
}
}
+/// Setup shell integration feature environment variables without
+/// performing full shell integration setup.
+pub fn setupFeatures(
+ env: *EnvMap,
+ features: config.ShellIntegrationFeatures,
+) !void {
+ if (!features.cursor) try env.put("GHOSTTY_SHELL_INTEGRATION_NO_CURSOR", "1");
+ if (!features.sudo) try env.put("GHOSTTY_SHELL_INTEGRATION_NO_SUDO", "1");
+ if (!features.title) try env.put("GHOSTTY_SHELL_INTEGRATION_NO_TITLE", "1");
+}
+
/// Setup the bash automatic shell integration. This works by
/// starting bash in POSIX mode and using the ENV environment
/// variable to load our bash integration script. This prevents
From 9c1edb544998bb64545b3a52561c6f9e43bf0005 Mon Sep 17 00:00:00 2001
From: Bryan Lee <38807139+liby@users.noreply.github.com>
Date: Tue, 14 Jan 2025 16:57:41 +0800
Subject: [PATCH 119/365] Add tests for setup shell integration features
---
src/termio/shell_integration.zig | 41 ++++++++++++++++++++++++++++++++
1 file changed, 41 insertions(+)
diff --git a/src/termio/shell_integration.zig b/src/termio/shell_integration.zig
index 85d9a8376..8b12cabbe 100644
--- a/src/termio/shell_integration.zig
+++ b/src/termio/shell_integration.zig
@@ -147,6 +147,47 @@ pub fn setupFeatures(
if (!features.title) try env.put("GHOSTTY_SHELL_INTEGRATION_NO_TITLE", "1");
}
+test "setup features" {
+ const testing = std.testing;
+
+ var arena = ArenaAllocator.init(testing.allocator);
+ defer arena.deinit();
+ const alloc = arena.allocator();
+
+ // Test: all features enabled (no environment variables should be set)
+ {
+ var env = EnvMap.init(alloc);
+ defer env.deinit();
+
+ try setupFeatures(&env, .{ .cursor = true, .sudo = true, .title = true });
+ try testing.expect(env.get("GHOSTTY_SHELL_INTEGRATION_NO_CURSOR") == null);
+ try testing.expect(env.get("GHOSTTY_SHELL_INTEGRATION_NO_SUDO") == null);
+ try testing.expect(env.get("GHOSTTY_SHELL_INTEGRATION_NO_TITLE") == null);
+ }
+
+ // Test: all features disabled
+ {
+ var env = EnvMap.init(alloc);
+ defer env.deinit();
+
+ try setupFeatures(&env, .{ .cursor = false, .sudo = false, .title = false });
+ try testing.expectEqualStrings("1", env.get("GHOSTTY_SHELL_INTEGRATION_NO_CURSOR").?);
+ try testing.expectEqualStrings("1", env.get("GHOSTTY_SHELL_INTEGRATION_NO_SUDO").?);
+ try testing.expectEqualStrings("1", env.get("GHOSTTY_SHELL_INTEGRATION_NO_TITLE").?);
+ }
+
+ // Test: mixed features
+ {
+ var env = EnvMap.init(alloc);
+ defer env.deinit();
+
+ try setupFeatures(&env, .{ .cursor = false, .sudo = true, .title = false });
+ try testing.expectEqualStrings("1", env.get("GHOSTTY_SHELL_INTEGRATION_NO_CURSOR").?);
+ try testing.expect(env.get("GHOSTTY_SHELL_INTEGRATION_NO_SUDO") == null);
+ try testing.expectEqualStrings("1", env.get("GHOSTTY_SHELL_INTEGRATION_NO_TITLE").?);
+ }
+}
+
/// Setup the bash automatic shell integration. This works by
/// starting bash in POSIX mode and using the ENV environment
/// variable to load our bash integration script. This prevents
From 6853a5423f9cd546e54a1e2b37a286d24ef54db3 Mon Sep 17 00:00:00 2001
From: Bryan Lee <38807139+liby@users.noreply.github.com>
Date: Tue, 14 Jan 2025 21:32:56 +0800
Subject: [PATCH 120/365] Update the documentation to better explain that
`shell-integration-features`
---
src/config/Config.zig | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/src/config/Config.zig b/src/config/Config.zig
index baac2cde7..386e6f923 100644
--- a/src/config/Config.zig
+++ b/src/config/Config.zig
@@ -1670,7 +1670,9 @@ keybind: Keybinds = .{},
/// The default value is `detect`.
@"shell-integration": ShellIntegration = .detect,
-/// Shell integration features to enable if shell integration itself is enabled.
+/// Shell integration features to enable. These require our shell integration
+/// to be loaded, either automatically via shell-integration or manually.
+///
/// The format of this is a list of features to enable separated by commas. If
/// you prefix a feature with `no-` then it is disabled. If you omit a feature,
/// its default value is used, so you must explicitly disable features you don't
From ccd6fd26ecfeb652ce726ded7648dce9181a6ccc Mon Sep 17 00:00:00 2001
From: Bryan Lee <38807139+liby@users.noreply.github.com>
Date: Wed, 15 Jan 2025 08:30:40 +0800
Subject: [PATCH 121/365] Ensure `setup_features` runs even when shell
detection fails
---
src/termio/shell_integration.zig | 116 +++++++++++++++++--------------
1 file changed, 62 insertions(+), 54 deletions(-)
diff --git a/src/termio/shell_integration.zig b/src/termio/shell_integration.zig
index 8b12cabbe..915d5be9e 100644
--- a/src/termio/shell_integration.zig
+++ b/src/termio/shell_integration.zig
@@ -58,60 +58,7 @@ pub fn setup(
break :exe std.fs.path.basename(command[0..idx]);
};
- const result: ShellIntegration = shell: {
- if (std.mem.eql(u8, "bash", exe)) {
- // Apple distributes their own patched version of Bash 3.2
- // on macOS that disables the ENV-based POSIX startup path.
- // This means we're unable to perform our automatic shell
- // integration sequence in this specific environment.
- //
- // If we're running "/bin/bash" on Darwin, we can assume
- // we're using Apple's Bash because /bin is non-writable
- // on modern macOS due to System Integrity Protection.
- if (comptime builtin.target.isDarwin()) {
- if (std.mem.eql(u8, "/bin/bash", command)) {
- return null;
- }
- }
-
- const new_command = try setupBash(
- alloc_arena,
- command,
- resource_dir,
- env,
- ) orelse return null;
- break :shell .{
- .shell = .bash,
- .command = new_command,
- };
- }
-
- if (std.mem.eql(u8, "elvish", exe)) {
- try setupXdgDataDirs(alloc_arena, resource_dir, env);
- break :shell .{
- .shell = .elvish,
- .command = try alloc_arena.dupe(u8, command),
- };
- }
-
- if (std.mem.eql(u8, "fish", exe)) {
- try setupXdgDataDirs(alloc_arena, resource_dir, env);
- break :shell .{
- .shell = .fish,
- .command = try alloc_arena.dupe(u8, command),
- };
- }
-
- if (std.mem.eql(u8, "zsh", exe)) {
- try setupZsh(resource_dir, env);
- break :shell .{
- .shell = .zsh,
- .command = try alloc_arena.dupe(u8, command),
- };
- }
-
- return null;
- };
+ const result = try setupShell(alloc_arena, resource_dir, command, env, exe);
// Setup our feature env vars
try setupFeatures(env, features);
@@ -119,6 +66,67 @@ pub fn setup(
return result;
}
+fn setupShell(
+ alloc_arena: Allocator,
+ resource_dir: []const u8,
+ command: []const u8,
+ env: *EnvMap,
+ exe: []const u8,
+) !?ShellIntegration {
+ if (std.mem.eql(u8, "bash", exe)) {
+ // Apple distributes their own patched version of Bash 3.2
+ // on macOS that disables the ENV-based POSIX startup path.
+ // This means we're unable to perform our automatic shell
+ // integration sequence in this specific environment.
+ //
+ // If we're running "/bin/bash" on Darwin, we can assume
+ // we're using Apple's Bash because /bin is non-writable
+ // on modern macOS due to System Integrity Protection.
+ if (comptime builtin.target.isDarwin()) {
+ if (std.mem.eql(u8, "/bin/bash", command)) {
+ return null;
+ }
+ }
+
+ const new_command = try setupBash(
+ alloc_arena,
+ command,
+ resource_dir,
+ env,
+ ) orelse return null;
+ return .{
+ .shell = .bash,
+ .command = new_command,
+ };
+ }
+
+ if (std.mem.eql(u8, "elvish", exe)) {
+ try setupXdgDataDirs(alloc_arena, resource_dir, env);
+ return .{
+ .shell = .elvish,
+ .command = try alloc_arena.dupe(u8, command),
+ };
+ }
+
+ if (std.mem.eql(u8, "fish", exe)) {
+ try setupXdgDataDirs(alloc_arena, resource_dir, env);
+ return .{
+ .shell = .fish,
+ .command = try alloc_arena.dupe(u8, command),
+ };
+ }
+
+ if (std.mem.eql(u8, "zsh", exe)) {
+ try setupZsh(resource_dir, env);
+ return .{
+ .shell = .zsh,
+ .command = try alloc_arena.dupe(u8, command),
+ };
+ }
+
+ return null;
+}
+
test "force shell" {
const testing = std.testing;
From 1b52365541a7ea10fa5b334981e3128bee48ccf3 Mon Sep 17 00:00:00 2001
From: Bryan Lee <38807139+liby@users.noreply.github.com>
Date: Sun, 12 Jan 2025 14:23:52 +0800
Subject: [PATCH 122/365] Add default documentation for undocumented keybind
actions
Previously, `ghostty +list-actions` would only show actions that had doc
comments, making it difficult for users to discover all available actions.
This change ensures all actions are listed with appropriate documentation.
For actions without doc comments, we now generate a default message
encouraging contribution.
---
src/helpgen.zig | 34 +++++++++++++++++++++++++++++++++-
1 file changed, 33 insertions(+), 1 deletion(-)
diff --git a/src/helpgen.zig b/src/helpgen.zig
index 2084fb9f7..646f7fd47 100644
--- a/src/helpgen.zig
+++ b/src/helpgen.zig
@@ -115,6 +115,38 @@ fn genActions(alloc: std.mem.Allocator, writer: anytype) !void {
try writer.writeAll("};\n");
}
+fn genKeybindField(
+ alloc: std.mem.Allocator,
+ writer: anytype,
+ ast: std.zig.Ast,
+ comptime field: []const u8,
+) !void {
+ const tokens = ast.tokens.items(.tag);
+
+ // Find the field and check if it has doc comments
+ for (tokens, 0..) |token, i| {
+ if (token != .identifier) continue;
+ const name = ast.tokenSlice(@intCast(i));
+ if (!std.mem.eql(u8, name, field)) continue;
+
+ try writer.writeAll("pub const ");
+ try writer.writeAll(name);
+ try writer.writeAll(": [:0]const u8 = \n");
+
+ // If it has doc comments, use them
+ if (i > 0 and tokens[i - 1] == .doc_comment) {
+ const comment = try extractDocComments(alloc, ast, @intCast(i - 1), tokens);
+ try writer.writeAll(comment);
+ } else {
+ // Otherwise use default documentation
+ try writer.writeAll(" \\\\This action is currently undocumented.\n");
+ try writer.writeAll(" \\\\Please refer to the source code or contribute documentation.\n");
+ try writer.writeAll(";\n");
+ }
+ break;
+ }
+}
+
fn genKeybindActions(alloc: std.mem.Allocator, writer: anytype) !void {
var ast = try std.zig.Ast.parse(alloc, @embedFile("input/Binding.zig"), .zig);
defer ast.deinit(alloc);
@@ -128,7 +160,7 @@ fn genKeybindActions(alloc: std.mem.Allocator, writer: anytype) !void {
inline for (@typeInfo(KeybindAction).Union.fields) |field| {
if (field.name[0] == '_') continue;
- try genConfigField(alloc, writer, ast, field.name);
+ try genKeybindField(alloc, writer, ast, field.name);
}
try writer.writeAll("};\n");
From 05fe3e7ec3d20c26f3f1cb500ab9ee0e9a0f01f5 Mon Sep 17 00:00:00 2001
From: Bryan Lee <38807139+liby@users.noreply.github.com>
Date: Sun, 12 Jan 2025 19:54:51 +0800
Subject: [PATCH 123/365] Ensure last action's documentation is properly
generated
The issue was caused by the documentation generation logic not writing the final buffered content.
---
src/build/webgen/main_actions.zig | 7 +++++++
src/input/Binding.zig | 6 +++---
2 files changed, 10 insertions(+), 3 deletions(-)
diff --git a/src/build/webgen/main_actions.zig b/src/build/webgen/main_actions.zig
index f4dffbc13..e802aac4a 100644
--- a/src/build/webgen/main_actions.zig
+++ b/src/build/webgen/main_actions.zig
@@ -50,9 +50,16 @@ pub fn genKeybindActions(writer: anytype) !void {
'\n',
);
while (iter.next()) |s| {
+ // If it is the last line and empty, then skip it.
+ if (iter.peek() == null and s.len == 0) continue;
try buffer.appendSlice(s);
try buffer.appendSlice("\n");
}
}
}
+
+ // Write any remaining buffered documentation
+ if (buffer.items.len > 0) {
+ try writer.writeAll(buffer.items);
+ }
}
diff --git a/src/input/Binding.zig b/src/input/Binding.zig
index 48725fb13..f29030bde 100644
--- a/src/input/Binding.zig
+++ b/src/input/Binding.zig
@@ -236,9 +236,9 @@ pub const Action = union(enum) {
/// Send an `ESC` sequence.
esc: []const u8,
- // Send the given text. Uses Zig string literal syntax. This is currently
- // not validated. If the text is invalid (i.e. contains an invalid escape
- // sequence), the error will currently only show up in logs.
+ /// Send the given text. Uses Zig string literal syntax. This is currently
+ /// not validated. If the text is invalid (i.e. contains an invalid escape
+ /// sequence), the error will currently only show up in logs.
text: []const u8,
/// Send data to the pty depending on whether cursor key mode is enabled
From 8e2c55a5dab4a4be18ca74f8f3895137fd73d8d0 Mon Sep 17 00:00:00 2001
From: Bryan Lee <38807139+liby@users.noreply.github.com>
Date: Mon, 13 Jan 2025 00:20:28 +0800
Subject: [PATCH 124/365] Improve `list-actions` command documentation
formatting
This commit fixes two issues with the `list-actions` command:
1. Ensures all actions are listed, including those without individual
documentation but sharing docs with related actions
2. Improves documentation formatting with proper indentation and grouping
---
src/cli/list_actions.zig | 50 +++++++++++++++++++++++++++++++---------
src/helpgen.zig | 34 +--------------------------
2 files changed, 40 insertions(+), 44 deletions(-)
diff --git a/src/cli/list_actions.zig b/src/cli/list_actions.zig
index 65b9dcdad..e4b22023d 100644
--- a/src/cli/list_actions.zig
+++ b/src/cli/list_actions.zig
@@ -3,6 +3,7 @@ const args = @import("args.zig");
const Action = @import("action.zig").Action;
const Allocator = std.mem.Allocator;
const help_strings = @import("help_strings");
+const KeybindAction = @import("../input/Binding.zig").Action;
pub const Options = struct {
/// If `true`, print out documentation about the action associated with the
@@ -36,18 +37,45 @@ pub fn run(alloc: Allocator) !u8 {
}
const stdout = std.io.getStdOut().writer();
- const info = @typeInfo(help_strings.KeybindAction);
- inline for (info.Struct.decls) |field| {
- try stdout.print("{s}", .{field.name});
- if (opts.docs) {
- try stdout.print(":\n", .{});
- var iter = std.mem.splitScalar(u8, std.mem.trimRight(u8, @field(help_strings.KeybindAction, field.name), &std.ascii.whitespace), '\n');
- while (iter.next()) |line| {
- try stdout.print(" {s}\n", .{line});
- }
- } else {
- try stdout.print("\n", .{});
+
+ var buffer = std.ArrayList(u8).init(std.heap.page_allocator);
+ defer buffer.deinit();
+
+ const fields = @typeInfo(KeybindAction).Union.fields;
+ inline for (fields) |field| {
+ if (field.name[0] == '_') continue;
+
+ // Write previously stored doc comment below all related actions
+ if (@hasDecl(help_strings.KeybindAction, field.name)) {
+ try stdout.writeAll(buffer.items);
+ try stdout.writeAll("\n");
+
+ buffer.clearRetainingCapacity();
}
+
+ // Write the field name.
+ try stdout.writeAll(field.name);
+ try stdout.writeAll(":\n");
+
+ if (@hasDecl(help_strings.KeybindAction, field.name)) {
+ var iter = std.mem.splitScalar(
+ u8,
+ @field(help_strings.KeybindAction, field.name),
+ '\n',
+ );
+ while (iter.next()) |s| {
+ // If it is the last line and empty, then skip it.
+ if (iter.peek() == null and s.len == 0) continue;
+ try buffer.appendSlice(" ");
+ try buffer.appendSlice(s);
+ try buffer.appendSlice("\n");
+ }
+ }
+ }
+
+ // Write any remaining buffered documentation
+ if (buffer.items.len > 0) {
+ try stdout.writeAll(buffer.items);
}
return 0;
diff --git a/src/helpgen.zig b/src/helpgen.zig
index 646f7fd47..2084fb9f7 100644
--- a/src/helpgen.zig
+++ b/src/helpgen.zig
@@ -115,38 +115,6 @@ fn genActions(alloc: std.mem.Allocator, writer: anytype) !void {
try writer.writeAll("};\n");
}
-fn genKeybindField(
- alloc: std.mem.Allocator,
- writer: anytype,
- ast: std.zig.Ast,
- comptime field: []const u8,
-) !void {
- const tokens = ast.tokens.items(.tag);
-
- // Find the field and check if it has doc comments
- for (tokens, 0..) |token, i| {
- if (token != .identifier) continue;
- const name = ast.tokenSlice(@intCast(i));
- if (!std.mem.eql(u8, name, field)) continue;
-
- try writer.writeAll("pub const ");
- try writer.writeAll(name);
- try writer.writeAll(": [:0]const u8 = \n");
-
- // If it has doc comments, use them
- if (i > 0 and tokens[i - 1] == .doc_comment) {
- const comment = try extractDocComments(alloc, ast, @intCast(i - 1), tokens);
- try writer.writeAll(comment);
- } else {
- // Otherwise use default documentation
- try writer.writeAll(" \\\\This action is currently undocumented.\n");
- try writer.writeAll(" \\\\Please refer to the source code or contribute documentation.\n");
- try writer.writeAll(";\n");
- }
- break;
- }
-}
-
fn genKeybindActions(alloc: std.mem.Allocator, writer: anytype) !void {
var ast = try std.zig.Ast.parse(alloc, @embedFile("input/Binding.zig"), .zig);
defer ast.deinit(alloc);
@@ -160,7 +128,7 @@ fn genKeybindActions(alloc: std.mem.Allocator, writer: anytype) !void {
inline for (@typeInfo(KeybindAction).Union.fields) |field| {
if (field.name[0] == '_') continue;
- try genKeybindField(alloc, writer, ast, field.name);
+ try genConfigField(alloc, writer, ast, field.name);
}
try writer.writeAll("};\n");
From 0016199ec3c6806b2db185060062138dddf3af14 Mon Sep 17 00:00:00 2001
From: Bryan Lee <38807139+liby@users.noreply.github.com>
Date: Mon, 13 Jan 2025 11:20:00 +0800
Subject: [PATCH 125/365] Extract keybind actions help generation into a
dedicated module
---
src/build/webgen/main_actions.zig | 61 +----------------
src/cli/list_actions.zig | 44 +-----------
src/input/helpgen_actions.zig | 107 ++++++++++++++++++++++++++++++
3 files changed, 111 insertions(+), 101 deletions(-)
create mode 100644 src/input/helpgen_actions.zig
diff --git a/src/build/webgen/main_actions.zig b/src/build/webgen/main_actions.zig
index e802aac4a..65420d7e2 100644
--- a/src/build/webgen/main_actions.zig
+++ b/src/build/webgen/main_actions.zig
@@ -1,65 +1,8 @@
const std = @import("std");
const help_strings = @import("help_strings");
-const KeybindAction = @import("../../input/Binding.zig").Action;
+const helpgen_actions = @import("../../helpgen_actions.zig");
pub fn main() !void {
const output = std.io.getStdOut().writer();
- try genKeybindActions(output);
-}
-
-pub fn genKeybindActions(writer: anytype) !void {
- // Write the header
- try writer.writeAll(
- \\---
- \\title: Keybinding Action Reference
- \\description: Reference of all Ghostty keybinding actions.
- \\editOnGithubLink: https://github.com/ghostty-org/ghostty/edit/main/src/input/Binding.zig
- \\---
- \\
- \\This is a reference of all Ghostty keybinding actions.
- \\
- \\
- );
-
- @setEvalBranchQuota(5_000);
-
- var buffer = std.ArrayList(u8).init(std.heap.page_allocator);
- defer buffer.deinit();
-
- const fields = @typeInfo(KeybindAction).Union.fields;
- inline for (fields) |field| {
- if (field.name[0] == '_') continue;
-
- // Write previously stored doc comment below all related actions
- if (@hasDecl(help_strings.KeybindAction, field.name)) {
- try writer.writeAll(buffer.items);
- try writer.writeAll("\n");
-
- buffer.clearRetainingCapacity();
- }
-
- // Write the field name.
- try writer.writeAll("## `");
- try writer.writeAll(field.name);
- try writer.writeAll("`\n");
-
- if (@hasDecl(help_strings.KeybindAction, field.name)) {
- var iter = std.mem.splitScalar(
- u8,
- @field(help_strings.KeybindAction, field.name),
- '\n',
- );
- while (iter.next()) |s| {
- // If it is the last line and empty, then skip it.
- if (iter.peek() == null and s.len == 0) continue;
- try buffer.appendSlice(s);
- try buffer.appendSlice("\n");
- }
- }
- }
-
- // Write any remaining buffered documentation
- if (buffer.items.len > 0) {
- try writer.writeAll(buffer.items);
- }
+ try helpgen_actions.generate(output, .markdown, std.heap.page_allocator);
}
diff --git a/src/cli/list_actions.zig b/src/cli/list_actions.zig
index e4b22023d..e6c34adb2 100644
--- a/src/cli/list_actions.zig
+++ b/src/cli/list_actions.zig
@@ -2,8 +2,7 @@ const std = @import("std");
const args = @import("args.zig");
const Action = @import("action.zig").Action;
const Allocator = std.mem.Allocator;
-const help_strings = @import("help_strings");
-const KeybindAction = @import("../input/Binding.zig").Action;
+const helpgen_actions = @import("../helpgen_actions.zig");
pub const Options = struct {
/// If `true`, print out documentation about the action associated with the
@@ -37,46 +36,7 @@ pub fn run(alloc: Allocator) !u8 {
}
const stdout = std.io.getStdOut().writer();
-
- var buffer = std.ArrayList(u8).init(std.heap.page_allocator);
- defer buffer.deinit();
-
- const fields = @typeInfo(KeybindAction).Union.fields;
- inline for (fields) |field| {
- if (field.name[0] == '_') continue;
-
- // Write previously stored doc comment below all related actions
- if (@hasDecl(help_strings.KeybindAction, field.name)) {
- try stdout.writeAll(buffer.items);
- try stdout.writeAll("\n");
-
- buffer.clearRetainingCapacity();
- }
-
- // Write the field name.
- try stdout.writeAll(field.name);
- try stdout.writeAll(":\n");
-
- if (@hasDecl(help_strings.KeybindAction, field.name)) {
- var iter = std.mem.splitScalar(
- u8,
- @field(help_strings.KeybindAction, field.name),
- '\n',
- );
- while (iter.next()) |s| {
- // If it is the last line and empty, then skip it.
- if (iter.peek() == null and s.len == 0) continue;
- try buffer.appendSlice(" ");
- try buffer.appendSlice(s);
- try buffer.appendSlice("\n");
- }
- }
- }
-
- // Write any remaining buffered documentation
- if (buffer.items.len > 0) {
- try stdout.writeAll(buffer.items);
- }
+ try helpgen_actions.generate(stdout, .plaintext, std.heap.page_allocator);
return 0;
}
diff --git a/src/input/helpgen_actions.zig b/src/input/helpgen_actions.zig
new file mode 100644
index 000000000..9a7612b57
--- /dev/null
+++ b/src/input/helpgen_actions.zig
@@ -0,0 +1,107 @@
+//! This module is a help generator for keybind actions documentation.
+//! It can generate documentation in different formats (plaintext for CLI,
+//! markdown for website) while maintaining consistent content.
+
+const std = @import("std");
+const KeybindAction = @import("Binding.zig").Action;
+const help_strings = @import("help_strings");
+
+/// Format options for generating keybind actions documentation
+pub const Format = enum {
+ /// Plain text output with indentation
+ plaintext,
+ /// Markdown formatted output
+ markdown,
+
+ fn formatFieldName(self: Format, writer: anytype, field_name: []const u8) !void {
+ switch (self) {
+ .plaintext => {
+ try writer.writeAll(field_name);
+ try writer.writeAll(":\n");
+ },
+ .markdown => {
+ try writer.writeAll("## `");
+ try writer.writeAll(field_name);
+ try writer.writeAll("`\n");
+ },
+ }
+ }
+
+ fn formatDocLine(self: Format, writer: anytype, line: []const u8) !void {
+ switch (self) {
+ .plaintext => {
+ try writer.appendSlice(" ");
+ try writer.appendSlice(line);
+ try writer.appendSlice("\n");
+ },
+ .markdown => {
+ try writer.appendSlice(line);
+ try writer.appendSlice("\n");
+ },
+ }
+ }
+
+ fn header(self: Format) ?[]const u8 {
+ return switch (self) {
+ .plaintext => null,
+ .markdown =>
+ \\---
+ \\title: Keybinding Action Reference
+ \\description: Reference of all Ghostty keybinding actions.
+ \\editOnGithubLink: https://github.com/ghostty-org/ghostty/edit/main/src/input/Binding.zig
+ \\---
+ \\
+ \\This is a reference of all Ghostty keybinding actions.
+ \\
+ \\
+ ,
+ };
+ }
+};
+
+/// Generate keybind actions documentation with the specified format
+pub fn generate(
+ writer: anytype,
+ format: Format,
+ page_allocator: std.mem.Allocator,
+) !void {
+ if (format.header()) |header| {
+ try writer.writeAll(header);
+ }
+
+ var buffer = std.ArrayList(u8).init(page_allocator);
+ defer buffer.deinit();
+
+ const fields = @typeInfo(KeybindAction).Union.fields;
+ inline for (fields) |field| {
+ if (field.name[0] == '_') continue;
+
+ // Write previously stored doc comment below all related actions
+ if (@hasDecl(help_strings.KeybindAction, field.name)) {
+ try writer.writeAll(buffer.items);
+ try writer.writeAll("\n");
+
+ buffer.clearRetainingCapacity();
+ }
+
+ try format.formatFieldName(writer, field.name);
+
+ if (@hasDecl(help_strings.KeybindAction, field.name)) {
+ var iter = std.mem.splitScalar(
+ u8,
+ @field(help_strings.KeybindAction, field.name),
+ '\n',
+ );
+ while (iter.next()) |s| {
+ // If it is the last line and empty, then skip it.
+ if (iter.peek() == null and s.len == 0) continue;
+ try format.formatDocLine(&buffer, s);
+ }
+ }
+ }
+
+ // Write any remaining buffered documentation
+ if (buffer.items.len > 0) {
+ try writer.writeAll(buffer.items);
+ }
+}
From 68124f60c75fde05d8d3bab6448114228ee16885 Mon Sep 17 00:00:00 2001
From: Leah Amelia Chen
Date: Fri, 17 Jan 2025 20:48:03 +0100
Subject: [PATCH 126/365] gtk: don't toggle headerbar on (un)maximize while
using SSDs
See #5137. We should never display the header bar when using SSDs anyway
---
src/apprt/gtk/Window.zig | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig
index 03fcd05db..59d6437d7 100644
--- a/src/apprt/gtk/Window.zig
+++ b/src/apprt/gtk/Window.zig
@@ -640,6 +640,11 @@ fn gtkWindowNotifyMaximized(
) callconv(.C) void {
const self = userdataSelf(ud orelse return);
const maximized = c.gtk_window_is_maximized(self.window) != 0;
+
+ // Only toggle visibility of the header bar when we're using CSDs,
+ // and actually intend on displaying the header bar
+ if (!self.winproto.clientSideDecorationEnabled()) return;
+
if (!maximized) {
self.headerbar.setVisible(true);
return;
From 0c2c847af37bafe3144d9c13035b921098a18567 Mon Sep 17 00:00:00 2001
From: Ikko Eltociear Ashimine
Date: Sat, 18 Jan 2025 22:47:18 +0900
Subject: [PATCH 127/365] chore: update stb_image.h
exitting -> exiting
---
src/stb/stb_image.h | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/stb/stb_image.h b/src/stb/stb_image.h
index 5e807a0a6..3ae1815c1 100644
--- a/src/stb/stb_image.h
+++ b/src/stb/stb_image.h
@@ -4962,7 +4962,7 @@ static int stbi__expand_png_palette(stbi__png *a, stbi_uc *palette, int len, int
p = (stbi_uc *) stbi__malloc_mad2(pixel_count, pal_img_n, 0);
if (p == NULL) return stbi__err("outofmem", "Out of memory");
- // between here and free(out) below, exitting would leak
+ // between here and free(out) below, exiting would leak
temp_out = p;
if (pal_img_n == 3) {
From ecad3e75ff8aa4a14811efaad8e6b9436eb6774b Mon Sep 17 00:00:00 2001
From: Leorize
Date: Sat, 18 Jan 2025 13:38:29 -0600
Subject: [PATCH 128/365] fix(flatpak): construct null-terminated array for
arguments
The variant format string `^aay` is said to be equivalent to
g_variant_new_bytestring_array. Given that no length parameter is
provided, g_variant_new assumed a null-terminated array, but the array
constructed by the code was not, causing a crash as glib exceed the read
boundaries to copy arbitrary memory.
This commit replaces the array construction code to use its arena
equivalents instead of trying to build one using glib, and make sure
that the resulting array is null-terminated.
---
src/os/flatpak.zig | 16 ++++++----------
1 file changed, 6 insertions(+), 10 deletions(-)
diff --git a/src/os/flatpak.zig b/src/os/flatpak.zig
index faac4bd27..09570554e 100644
--- a/src/os/flatpak.zig
+++ b/src/os/flatpak.zig
@@ -265,16 +265,12 @@ pub const FlatpakHostCommand = struct {
}
// Build our args
- const args_ptr = c.g_ptr_array_new();
- {
- errdefer _ = c.g_ptr_array_free(args_ptr, 1);
- for (self.argv) |arg| {
- const argZ = try arena.dupeZ(u8, arg);
- c.g_ptr_array_add(args_ptr, argZ.ptr);
- }
+ const args = try arena.alloc(?[*:0]u8, self.argv.len + 1);
+ for (0.., self.argv) |i, arg| {
+ const argZ = try arena.dupeZ(u8, arg);
+ args[i] = argZ.ptr;
}
- const args = c.g_ptr_array_free(args_ptr, 0);
- defer c.g_free(@as(?*anyopaque, @ptrCast(args)));
+ args[args.len - 1] = null;
// Get the cwd in case we don't have ours set. A small optimization
// would be to do this only if we need it but this isn't a
@@ -286,7 +282,7 @@ pub const FlatpakHostCommand = struct {
const params = c.g_variant_new(
"(^ay^aay@a{uh}@a{ss}u)",
@as(*const anyopaque, if (self.cwd) |*cwd| cwd.ptr else g_cwd),
- args,
+ args.ptr,
c.g_variant_builder_end(fd_builder),
c.g_variant_builder_end(env_builder),
@as(c_int, 0),
From 2ee6e005d02132d857685ad4e88c0a6e627f8fb3 Mon Sep 17 00:00:00 2001
From: Jon Parise
Date: Sat, 18 Jan 2025 14:29:30 -0500
Subject: [PATCH 129/365] termio: revise macOS-specific .hushlogin note
login(1)'s .hushlogin logic was "fixed" in macOS Sonoma 14.4, so this
comment (and our workaround) is only relevant for versions earlier than
that.
The relevant change to login/login.c is part of system_cmds-979.100.8.
> login.c: look for .hushlogin in home directory (112854361)
- https://github.com/apple-oss-distributions/system_cmds/commit/1bca46ecc5b76432f42eb23ec39fe63e8159f251
- https://github.com/apple-oss-distributions/distribution-macOS/tree/macos-144
---
src/termio/Exec.zig | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig
index 1a3b8cad0..dd1f65305 100644
--- a/src/termio/Exec.zig
+++ b/src/termio/Exec.zig
@@ -971,12 +971,12 @@ const Subprocess = struct {
// which we may not want. If we specify "-l" then we can avoid
// this behavior but now the shell isn't a login shell.
//
- // There is another issue: `login(1)` only checks for ".hushlogin"
- // in the working directory. This means that if we specify "-l"
- // then we won't get hushlogin honored if its in the home
- // directory (which is standard). To get around this, we
- // check for hushlogin ourselves and if present specify the
- // "-q" flag to login(1).
+ // There is another issue: `login(1)` on macOS 14.3 and earlier
+ // checked for ".hushlogin" in the working directory. This means
+ // that if we specify "-l" then we won't get hushlogin honored
+ // if its in the home directory (which is standard). To get
+ // around this, we check for hushlogin ourselves and if present
+ // specify the "-q" flag to login(1).
//
// So to get all the behaviors we want, we specify "-l" but
// execute "bash" (which is built-in to macOS). We then use
From 3f367857fc30edce9d9de32184df0789b30c8809 Mon Sep 17 00:00:00 2001
From: mitchellh <1299+mitchellh@users.noreply.github.com>
Date: Sun, 19 Jan 2025 00:58:22 +0000
Subject: [PATCH 130/365] deps: Update iTerm2 color schemes
---
build.zig.zon | 4 ++--
nix/zigCacheHash.nix | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/build.zig.zon b/build.zig.zon
index 4b9a3856b..09dc9847e 100644
--- a/build.zig.zon
+++ b/build.zig.zon
@@ -79,8 +79,8 @@
// Other
.apple_sdk = .{ .path = "./pkg/apple-sdk" },
.iterm2_themes = .{
- .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/25cb3c3f52c7011cd8a599f8d144fc63f4409eb6.tar.gz",
- .hash = "1220dc1096bda9721c1f5256177539bf37b41ac6fb70d58eadf0eec45359676382e5",
+ .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/0e23daf59234fc892cba949562d7bf69204594bb.tar.gz",
+ .hash = "12204fc99743d8232e691ac22e058519bfc6ea92de4a11c6dba59b117531c847cd6a",
},
},
}
diff --git a/nix/zigCacheHash.nix b/nix/zigCacheHash.nix
index def5a11e3..dfc2e5f7f 100644
--- a/nix/zigCacheHash.nix
+++ b/nix/zigCacheHash.nix
@@ -1,3 +1,3 @@
# This file is auto-generated! check build-support/check-zig-cache-hash.sh for
# more details.
-"sha256-2zXNHWSSWjnpW8oHu2sufT5+Ms4IKWaH6yRARQeMcxk="
+"sha256-H6o4Y09ATIylMUWuL9Y1fHwpuxSWyJ3Pl8fn4VeoDZo="
From 4956d36ee6f230c5f910dcb1db4fa5027e5114d8 Mon Sep 17 00:00:00 2001
From: Damien Mehala
Date: Sun, 19 Jan 2025 11:04:01 +0100
Subject: [PATCH 131/365] fix: quick terminal hidden by macos menu bar
ghostty#5000 changed the window level from `.popupMenu` to `.floating`
to improve IME support. However, this introduced a side effect which
render the Quick Terminal (QT) below the macOS menu bar, whereas
previously it would cover it.
When positioned on `right` and `left`, the top of the QT becomes
partially hidden. This PR adjust the size of the QT to ensure it remains
fully visible and stays below the menu bar.
---
.../Sources/Features/QuickTerminal/QuickTerminalPosition.swift | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalPosition.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalPosition.swift
index 0acbfec1b..6ba224a28 100644
--- a/macos/Sources/Features/QuickTerminal/QuickTerminalPosition.swift
+++ b/macos/Sources/Features/QuickTerminal/QuickTerminalPosition.swift
@@ -69,7 +69,7 @@ enum QuickTerminalPosition : String {
finalSize.width = screen.frame.width
case .left, .right:
- finalSize.height = screen.frame.height
+ finalSize.height = screen.visibleFrame.height
case .center:
finalSize.width = screen.frame.width / 2
From bb58710fa8f4257eede4592ea56b7f18e65f4dde Mon Sep 17 00:00:00 2001
From: Bruno Bachmann
Date: Sun, 19 Jan 2025 14:49:59 -0800
Subject: [PATCH 132/365] Fix typo in binding comments
---
src/input/Binding.zig | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/input/Binding.zig b/src/input/Binding.zig
index 48725fb13..757c19c06 100644
--- a/src/input/Binding.zig
+++ b/src/input/Binding.zig
@@ -419,7 +419,7 @@ pub const Action = union(enum) {
/// is preserved between appearances, so you can always press the keybinding
/// to bring it back up.
///
- /// To enable the quick terminally globally so that Ghostty doesn't
+ /// To enable the quick terminal globally so that Ghostty doesn't
/// have to be focused, prefix your keybind with `global`. Example:
///
/// ```ini
From afa23532b66870873671ff702798ed8b49cfd6bd Mon Sep 17 00:00:00 2001
From: Jon Parise
Date: Mon, 20 Jan 2025 10:36:35 -0500
Subject: [PATCH 133/365] bash: revert automatic shell integration changes
The intention of #5075 was to create a less intrusive, more hermetic
environment in which to source the bash startup files. This caused
problems for multiple people, and I believe that's because the general
expectation is that these files are sourced at global (not function)
scope.
For example, when a file is sourced from within a function scope, any
variables that weren't explicitly exported into the global environment
won't be available outside of the scope of the function. Most system and
personal startup files aren't written with that constraint because it's
not how bash itself loads these files.
As a small improvement over the original code, `rcfile` has been renamed
to `__ghostty_rcfile`. Avoiding leaking this variable while sourcing
these files was a goal of #5075, and prefixing it make it much less of a
potential issue.
This change also reverts the $HOME to ~/ change. While the ~/ notation
is more concise, using $HOME is more common and easier to implement
safely with regard to quoting.
---
src/shell-integration/bash/ghostty.bash | 52 +++++++++++--------------
1 file changed, 23 insertions(+), 29 deletions(-)
diff --git a/src/shell-integration/bash/ghostty.bash b/src/shell-integration/bash/ghostty.bash
index 7de55f982..7fae435a3 100644
--- a/src/shell-integration/bash/ghostty.bash
+++ b/src/shell-integration/bash/ghostty.bash
@@ -41,37 +41,31 @@ if [ -n "$GHOSTTY_BASH_INJECT" ]; then
# Manually source the startup files. See INVOCATION in bash(1) and
# run_startup_files() in shell.c in the Bash source code.
- function __ghostty_bash_startup() {
- builtin local rcfile
-
- if builtin shopt -q login_shell; then
- if [[ $__ghostty_bash_flags != *"--noprofile"* ]]; then
- [ -r /etc/profile ] && builtin source "/etc/profile"
- for rcfile in ~/.bash_profile ~/.bash_login ~/.profile; do
- [ -r "$rcfile" ] && { builtin source "$rcfile"; break; }
- done
- fi
- else
- if [[ $__ghostty_bash_flags != *"--norc"* ]]; then
- # The location of the system bashrc is determined at bash build
- # time via -DSYS_BASHRC and can therefore vary across distros:
- # Arch, Debian, Ubuntu use /etc/bash.bashrc
- # Fedora uses /etc/bashrc sourced from ~/.bashrc instead of SYS_BASHRC
- # Void Linux uses /etc/bash/bashrc
- # Nixos uses /etc/bashrc
- for rcfile in /etc/bash.bashrc /etc/bash/bashrc /etc/bashrc; do
- [ -r "$rcfile" ] && { builtin source "$rcfile"; break; }
- done
- if [[ -z "$GHOSTTY_BASH_RCFILE" ]]; then GHOSTTY_BASH_RCFILE=~/.bashrc; fi
- [ -r "$GHOSTTY_BASH_RCFILE" ] && builtin source "$GHOSTTY_BASH_RCFILE"
- fi
+ if builtin shopt -q login_shell; then
+ if [[ $__ghostty_bash_flags != *"--noprofile"* ]]; then
+ [ -r /etc/profile ] && builtin source "/etc/profile"
+ for __ghostty_rcfile in "$HOME/.bash_profile" "$HOME/.bash_login" "$HOME/.profile"; do
+ [ -r "$__ghostty_rcfile" ] && { builtin source "$__ghostty_rcfile"; break; }
+ done
fi
- }
+ else
+ if [[ $__ghostty_bash_flags != *"--norc"* ]]; then
+ # The location of the system bashrc is determined at bash build
+ # time via -DSYS_BASHRC and can therefore vary across distros:
+ # Arch, Debian, Ubuntu use /etc/bash.bashrc
+ # Fedora uses /etc/bashrc sourced from ~/.bashrc instead of SYS_BASHRC
+ # Void Linux uses /etc/bash/bashrc
+ # Nixos uses /etc/bashrc
+ for __ghostty_rcfile in /etc/bash.bashrc /etc/bash/bashrc /etc/bashrc; do
+ [ -r "$__ghostty_rcfile" ] && { builtin source "$__ghostty_rcfile"; break; }
+ done
+ if [[ -z "$GHOSTTY_BASH_RCFILE" ]]; then GHOSTTY_BASH_RCFILE="$HOME/.bashrc"; fi
+ [ -r "$GHOSTTY_BASH_RCFILE" ] && builtin source "$GHOSTTY_BASH_RCFILE"
+ fi
+ fi
- __ghostty_bash_startup
-
- builtin unset -f __ghostty_bash_startup
- builtin unset -v __ghostty_bash_flags
+ builtin unset __ghostty_rcfile
+ builtin unset __ghostty_bash_flags
builtin unset GHOSTTY_BASH_RCFILE
fi
From e5a3be3c46418bb150a991bd70a8a38565849b2b Mon Sep 17 00:00:00 2001
From: otomist
Date: Wed, 15 Jan 2025 12:04:34 -0500
Subject: [PATCH 134/365] use whitespace instead of new flag for selecting full
line
---
src/Surface.zig | 16 +++++++---------
src/terminal/Screen.zig | 6 ------
2 files changed, 7 insertions(+), 15 deletions(-)
diff --git a/src/Surface.zig b/src/Surface.zig
index a8fd4a817..138aa2ea2 100644
--- a/src/Surface.zig
+++ b/src/Surface.zig
@@ -3563,17 +3563,15 @@ fn dragLeftClickTriple(
const screen = &self.io.terminal.screen;
const click_pin = self.mouse.left_click_pin.?.*;
- // Get the line selection under our current drag point. If there isn't a line, do nothing.
+ // Get the line selection under our current drag point. If there isn't a
+ // line, do nothing.
const line = screen.selectLine(.{ .pin = drag_pin }) orelse return;
- // get the selection under our click point.
- var sel_ = screen.selectLine(.{ .pin = click_pin });
-
- // We may not have a selection if we started our triple-click in an area
- // that had no data, in this case recall selectLine with allow_empty_lines.
- if (sel_ == null) {
- sel_ = screen.selectLine(.{ .pin = click_pin, .allow_empty_lines = true });
- }
+ // Get the selection under our click point. We first try to trim
+ // whitespace if we've selected a word. But if no word exists then
+ // we select the blank line.
+ const sel_ = screen.selectLine(.{ .pin = click_pin }) orelse
+ screen.selectLine(.{ .pin = click_pin, .whitespace = null });
var sel = sel_ orelse return;
if (drag_pin.before(click_pin)) {
diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig
index db890ad3f..eb70d32d0 100644
--- a/src/terminal/Screen.zig
+++ b/src/terminal/Screen.zig
@@ -2215,7 +2215,6 @@ pub const SelectLine = struct {
/// state changing a boundary. State changing is ANY state
/// change.
semantic_prompt_boundary: bool = true,
- allow_empty_lines: bool = false,
};
/// Select the line under the given point. This will select across soft-wrapped
@@ -2293,11 +2292,6 @@ pub fn selectLine(self: *const Screen, opts: SelectLine) ?Selection {
return null;
};
- // If we allow empty lines, we don't need to do any further checks.
- if (opts.allow_empty_lines) {
- return Selection.init(start_pin, end_pin, false);
- }
-
// Go forward from the start to find the first non-whitespace character.
const start: Pin = start: {
const whitespace = opts.whitespace orelse break :start start_pin;
From 4cc1fa2111848a78536e54ea34d9720d4f995dac Mon Sep 17 00:00:00 2001
From: julia
Date: Tue, 14 Jan 2025 10:13:09 +1100
Subject: [PATCH 135/365] render consecutive shaders to the fbo
not that big. see comments
---
src/renderer/OpenGL.zig | 22 +++++++++++++++++-----
src/renderer/opengl/custom.zig | 1 -
2 files changed, 17 insertions(+), 6 deletions(-)
diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig
index e5dec6b2b..8b6552bb9 100644
--- a/src/renderer/OpenGL.zig
+++ b/src/renderer/OpenGL.zig
@@ -2350,11 +2350,9 @@ pub fn drawFrame(self: *OpenGL, surface: *apprt.Surface) !void {
}
/// Draw the custom shaders.
-fn drawCustomPrograms(
- self: *OpenGL,
- custom_state: *custom.State,
-) !void {
+fn drawCustomPrograms(self: *OpenGL, custom_state: *custom.State) !void {
_ = self;
+ assert(custom_state.programs.len > 0);
// Bind our state that is global to all custom shaders
const custom_bind = try custom_state.bind();
@@ -2363,8 +2361,22 @@ fn drawCustomPrograms(
// Setup the new frame
try custom_state.newFrame();
+ // To allow programs to retrieve each other via a texture
+ // then we must render the next shaders to the screen fbo.
+ // However, the first shader must be run while the default fbo
+ // is attached
+ {
+ const bind = try custom_state.programs[0].bind();
+ defer bind.unbind();
+ try bind.draw();
+ if (custom_state.programs.len == 1) return;
+ }
+
+ const fbobind = try custom_state.fbo.bind(.framebuffer);
+ defer fbobind.unbind();
+
// Go through each custom shader and draw it.
- for (custom_state.programs) |program| {
+ for (custom_state.programs[1..]) |program| {
// Bind our cell program state, buffers
const bind = try program.bind();
defer bind.unbind();
diff --git a/src/renderer/opengl/custom.zig b/src/renderer/opengl/custom.zig
index 2cab0940c..3806921df 100644
--- a/src/renderer/opengl/custom.zig
+++ b/src/renderer/opengl/custom.zig
@@ -251,7 +251,6 @@ pub const Program = struct {
const program = try gl.Program.createVF(
@embedFile("../shaders/custom.v.glsl"),
src,
- //@embedFile("../shaders/temp.f.glsl"),
);
errdefer program.destroy();
From 8c1db16c79caf322153bd41b07e377829d6ae87a Mon Sep 17 00:00:00 2001
From: Andrej Daskalov
Date: Mon, 20 Jan 2025 20:19:12 +0100
Subject: [PATCH 136/365] added exec permission back to dolphin action
---
dist/linux/ghostty_dolphin.desktop | 0
1 file changed, 0 insertions(+), 0 deletions(-)
mode change 100644 => 100755 dist/linux/ghostty_dolphin.desktop
diff --git a/dist/linux/ghostty_dolphin.desktop b/dist/linux/ghostty_dolphin.desktop
old mode 100644
new mode 100755
From 3b8ab10776664ed40f9cf571941785436546eb69 Mon Sep 17 00:00:00 2001
From: Qwerasd
Date: Mon, 20 Jan 2025 18:36:48 -0500
Subject: [PATCH 137/365] fix(renderer): clip terminal contents to expected
grid size (#4523)
This significantly improves the robustness of the renderers since it
prevents synchronization issues from causing memory corruption due to
out of bounds read/writes while building the cells.
TODO: when viewport is narrower than renderer grid size, fill blank
margin with bg color- currently appears as black, this only affects
DECCOLM right now, and possibly could create single-frame artefacts
during poorly managed resizes, but it's not ideal regardless.
---
src/renderer/Metal.zig | 37 +++++++++++++++++++------------------
src/renderer/OpenGL.zig | 38 ++++++++++++++++++++------------------
2 files changed, 39 insertions(+), 36 deletions(-)
diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig
index 45d8f84c2..bf28b58ac 100644
--- a/src/renderer/Metal.zig
+++ b/src/renderer/Metal.zig
@@ -1046,19 +1046,6 @@ pub fn updateFrame(
}
}
- // If our terminal screen size doesn't match our expected renderer
- // size then we skip a frame. This can happen if the terminal state
- // is resized between when the renderer mailbox is drained and when
- // the state mutex is acquired inside this function.
- //
- // For some reason this doesn't seem to cause any significant issues
- // with flickering while resizing. '\_('-')_/'
- if (self.cells.size.rows != state.terminal.rows or
- self.cells.size.columns != state.terminal.cols)
- {
- return;
- }
-
// Get the viewport pin so that we can compare it to the current.
const viewport_pin = state.terminal.screen.pages.pin(.{ .viewport = .{} }).?;
@@ -2437,12 +2424,22 @@ fn rebuildCells(
}
}
- // Go row-by-row to build the cells. We go row by row because we do
- // font shaping by row. In the future, we will also do dirty tracking
- // by row.
+ // We rebuild the cells row-by-row because we
+ // do font shaping and dirty tracking by row.
var row_it = screen.pages.rowIterator(.left_up, .{ .viewport = .{} }, null);
- var y: terminal.size.CellCountInt = screen.pages.rows;
+ // If our cell contents buffer is shorter than the screen viewport,
+ // we render the rows that fit, starting from the bottom. If instead
+ // the viewport is shorter than the cell contents buffer, we align
+ // the top of the viewport with the top of the contents buffer.
+ var y: terminal.size.CellCountInt = @min(
+ screen.pages.rows,
+ self.cells.size.rows,
+ );
while (row_it.next()) |row| {
+ // The viewport may have more rows than our cell contents,
+ // so we need to break from the loop early if we hit y = 0.
+ if (y == 0) break;
+
y -= 1;
if (!rebuild) {
@@ -2501,7 +2498,11 @@ fn rebuildCells(
var shaper_cells: ?[]const font.shape.Cell = null;
var shaper_cells_i: usize = 0;
- const row_cells = row.cells(.all);
+ const row_cells_all = row.cells(.all);
+
+ // If our viewport is wider than our cell contents buffer,
+ // we still only process cells up to the width of the buffer.
+ const row_cells = row_cells_all[0..@min(row_cells_all.len, self.cells.size.columns)];
for (row_cells, 0..) |*cell, x| {
// If this cell falls within our preedit range then we
diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig
index 8b6552bb9..80fc3cab9 100644
--- a/src/renderer/OpenGL.zig
+++ b/src/renderer/OpenGL.zig
@@ -706,8 +706,6 @@ pub fn updateFrame(
// Update all our data as tightly as possible within the mutex.
var critical: Critical = critical: {
- const grid_size = self.size.grid();
-
state.mutex.lock();
defer state.mutex.unlock();
@@ -748,19 +746,6 @@ pub fn updateFrame(
}
}
- // If our terminal screen size doesn't match our expected renderer
- // size then we skip a frame. This can happen if the terminal state
- // is resized between when the renderer mailbox is drained and when
- // the state mutex is acquired inside this function.
- //
- // For some reason this doesn't seem to cause any significant issues
- // with flickering while resizing. '\_('-')_/'
- if (grid_size.rows != state.terminal.rows or
- grid_size.columns != state.terminal.cols)
- {
- return;
- }
-
// Get the viewport pin so that we can compare it to the current.
const viewport_pin = state.terminal.screen.pages.pin(.{ .viewport = .{} }).?;
@@ -1276,10 +1261,23 @@ pub fn rebuildCells(
}
}
- // Build each cell
+ const grid_size = self.size.grid();
+
+ // We rebuild the cells row-by-row because we do font shaping by row.
var row_it = screen.pages.rowIterator(.left_up, .{ .viewport = .{} }, null);
- var y: terminal.size.CellCountInt = screen.pages.rows;
+ // If our cell contents buffer is shorter than the screen viewport,
+ // we render the rows that fit, starting from the bottom. If instead
+ // the viewport is shorter than the cell contents buffer, we align
+ // the top of the viewport with the top of the contents buffer.
+ var y: terminal.size.CellCountInt = @min(
+ screen.pages.rows,
+ grid_size.rows,
+ );
while (row_it.next()) |row| {
+ // The viewport may have more rows than our cell contents,
+ // so we need to break from the loop early if we hit y = 0.
+ if (y == 0) break;
+
y -= 1;
// True if we want to do font shaping around the cursor. We want to
@@ -1356,7 +1354,11 @@ pub fn rebuildCells(
var shaper_cells: ?[]const font.shape.Cell = null;
var shaper_cells_i: usize = 0;
- const row_cells = row.cells(.all);
+ const row_cells_all = row.cells(.all);
+
+ // If our viewport is wider than our cell contents buffer,
+ // we still only process cells up to the width of the buffer.
+ const row_cells = row_cells_all[0..@min(row_cells_all.len, grid_size.columns)];
for (row_cells, 0..) |*cell, x| {
// If this cell falls within our preedit range then we
From 2d3db866e62e7cd2f09f83308c5f16287f10c7e0 Mon Sep 17 00:00:00 2001
From: Ryan Liptak
Date: Mon, 20 Jan 2025 18:30:22 -0800
Subject: [PATCH 138/365] unigen: Remove libc dependency, use ArenaAllocator
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Not linking libc avoids potential problems when compiling from/for certain targets (see https://github.com/ghostty-org/ghostty/discussions/3218), and using an ArenaAllocator makes unigen run just as fast (in both release and debug modes) while also taking less memory.
Benchmark 1 (3 runs): ./zig-out/bin/unigen-release-c
measurement mean ± σ min … max outliers delta
wall_time 1.75s ± 15.8ms 1.73s … 1.76s 0 ( 0%) 0%
peak_rss 2.23MB ± 0 2.23MB … 2.23MB 0 ( 0%) 0%
cpu_cycles 7.22G ± 62.8M 7.16G … 7.29G 0 ( 0%) 0%
instructions 11.5G ± 16.0 11.5G … 11.5G 0 ( 0%) 0%
cache_references 436M ± 6.54M 430M … 443M 0 ( 0%) 0%
cache_misses 310K ± 203K 134K … 532K 0 ( 0%) 0%
branch_misses 1.03M ± 29.9K 997K … 1.06M 0 ( 0%) 0%
Benchmark 2 (3 runs): ./zig-out/bin/unigen-release-arena
measurement mean ± σ min … max outliers delta
wall_time 1.73s ± 6.40ms 1.72s … 1.73s 0 ( 0%) - 1.0% ± 1.6%
peak_rss 1.27MB ± 75.7KB 1.18MB … 1.31MB 0 ( 0%) ⚡- 43.1% ± 5.4%
cpu_cycles 7.16G ± 26.5M 7.13G … 7.18G 0 ( 0%) - 0.9% ± 1.5%
instructions 11.4G ± 28.2 11.4G … 11.4G 0 ( 0%) - 0.8% ± 0.0%
cache_references 441M ± 2.89M 439M … 444M 0 ( 0%) + 1.2% ± 2.6%
cache_misses 152K ± 102K 35.2K … 220K 0 ( 0%) - 50.8% ± 117.8%
branch_misses 1.05M ± 13.4K 1.04M … 1.06M 0 ( 0%) + 2.0% ± 5.1%
Benchmark 1 (3 runs): ./zig-out/bin/unigen-debug-c
measurement mean ± σ min … max outliers delta
wall_time 1.75s ± 32.4ms 1.71s … 1.77s 0 ( 0%) 0%
peak_rss 2.23MB ± 0 2.23MB … 2.23MB 0 ( 0%) 0%
cpu_cycles 7.23G ± 136M 7.08G … 7.34G 0 ( 0%) 0%
instructions 11.5G ± 37.9 11.5G … 11.5G 0 ( 0%) 0%
cache_references 448M ± 1.03M 447M … 449M 0 ( 0%) 0%
cache_misses 148K ± 42.6K 99.3K … 180K 0 ( 0%) 0%
branch_misses 987K ± 5.27K 983K … 993K 0 ( 0%) 0%
Benchmark 2 (3 runs): ./zig-out/bin/unigen-debug-arena
measurement mean ± σ min … max outliers delta
wall_time 1.76s ± 4.12ms 1.76s … 1.76s 0 ( 0%) + 0.4% ± 3.0%
peak_rss 1.22MB ± 75.7KB 1.18MB … 1.31MB 0 ( 0%) ⚡- 45.1% ± 5.4%
cpu_cycles 7.27G ± 17.1M 7.26G … 7.29G 0 ( 0%) + 0.6% ± 3.0%
instructions 11.4G ± 3.79 11.4G … 11.4G 0 ( 0%) - 0.8% ± 0.0%
cache_references 440M ± 4.52M 435M … 444M 0 ( 0%) - 1.7% ± 1.7%
cache_misses 43.6K ± 19.2K 26.5K … 64.3K 0 ( 0%) ⚡- 70.5% ± 50.8%
branch_misses 1.04M ± 2.25K 1.04M … 1.05M 0 ( 0%) 💩+ 5.8% ± 0.9%
---
src/build/UnicodeTables.zig | 1 -
src/unicode/props.zig | 4 +++-
2 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/src/build/UnicodeTables.zig b/src/build/UnicodeTables.zig
index 0159de442..7a4b0a5a2 100644
--- a/src/build/UnicodeTables.zig
+++ b/src/build/UnicodeTables.zig
@@ -15,7 +15,6 @@ pub fn init(b: *std.Build) !UnicodeTables {
.root_source_file = b.path("src/unicode/props.zig"),
.target = b.host,
});
- exe.linkLibC();
const ziglyph_dep = b.dependency("ziglyph", .{
.target = b.host,
diff --git a/src/unicode/props.zig b/src/unicode/props.zig
index d77bf4c8a..8c7621b79 100644
--- a/src/unicode/props.zig
+++ b/src/unicode/props.zig
@@ -131,7 +131,9 @@ pub fn get(cp: u21) Properties {
/// Runnable binary to generate the lookup tables and output to stdout.
pub fn main() !void {
- const alloc = std.heap.c_allocator;
+ var arena_state = std.heap.ArenaAllocator.init(std.heap.page_allocator);
+ defer arena_state.deinit();
+ const alloc = arena_state.allocator();
const gen: lut.Generator(
Properties,
From 25ccdfe495dde9755b20b248bebc65d918cefc10 Mon Sep 17 00:00:00 2001
From: m154k1 <139042094+m154k1@users.noreply.github.com>
Date: Tue, 21 Jan 2025 17:37:28 +0200
Subject: [PATCH 139/365] Fix sudo fish shell integration
Set sudo_has_sudoedit_flags scope to --function.
---
.../fish/vendor_conf.d/ghostty-shell-integration.fish | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish
index 420a49528..cd4f56105 100644
--- a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish
+++ b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish
@@ -71,11 +71,11 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration"
and test -n "$TERMINFO"; and test "file" = (type -t sudo 2> /dev/null; or echo "x")
# Wrap `sudo` command to ensure Ghostty terminfo is preserved
function sudo -d "Wrap sudo to preserve terminfo"
- set --local sudo_has_sudoedit_flags "no"
+ set --function sudo_has_sudoedit_flags "no"
for arg in $argv
# Check if argument is '-e' or '--edit' (sudoedit flags)
if string match -q -- "-e" "$arg"; or string match -q -- "--edit" "$arg"
- set --local sudo_has_sudoedit_flags "yes"
+ set --function sudo_has_sudoedit_flags "yes"
break
end
# Check if argument is neither an option nor a key-value pair
From bf6cce23da165c80622f7b12fefd3193d02c2d96 Mon Sep 17 00:00:00 2001
From: Bryan Lee <38807139+liby@users.noreply.github.com>
Date: Tue, 21 Jan 2025 09:50:15 +0800
Subject: [PATCH 140/365] Prevent hyperlink hover state when mouse is outside
viewport
---
src/Surface.zig | 3 +++
src/apprt/embedded.zig | 2 +-
src/apprt/gtk/Surface.zig | 2 +-
3 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/src/Surface.zig b/src/Surface.zig
index 138aa2ea2..d9a985aa7 100644
--- a/src/Surface.zig
+++ b/src/Surface.zig
@@ -1041,6 +1041,9 @@ fn mouseRefreshLinks(
pos_vp: terminal.point.Coordinate,
over_link: bool,
) !void {
+ // If the position is outside our viewport, do nothing
+ if (pos.x < 0 or pos.y < 0) return;
+
self.mouse.link_point = pos_vp;
if (try self.linkAtPos(pos)) |link| {
diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig
index 44c4c5f20..013117f15 100644
--- a/src/apprt/embedded.zig
+++ b/src/apprt/embedded.zig
@@ -638,7 +638,7 @@ pub const Surface = struct {
.y = @floatCast(opts.scale_factor),
},
.size = .{ .width = 800, .height = 600 },
- .cursor_pos = .{ .x = 0, .y = 0 },
+ .cursor_pos = .{ .x = -1, .y = -1 },
.keymap_state = .{},
};
diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig
index 61866dcec..21d811623 100644
--- a/src/apprt/gtk/Surface.zig
+++ b/src/apprt/gtk/Surface.zig
@@ -560,7 +560,7 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void {
.font_size = font_size,
.init_config = init_config,
.size = .{ .width = 800, .height = 600 },
- .cursor_pos = .{ .x = 0, .y = 0 },
+ .cursor_pos = .{ .x = -1, .y = -1 },
.im_context = im_context,
.cgroup_path = cgroup_path,
};
From 52936b9b681e92f7bab37c3ec341be0013f591da Mon Sep 17 00:00:00 2001
From: Mitchell Hashimoto
Date: Tue, 21 Jan 2025 14:29:43 -0800
Subject: [PATCH 141/365] apprt/gtk: fundamentally rework input method handling
Fixes #4332
This commit fundamentally reworks the input method handling in the GTK
apprt, making it work properly (as reported in the linked issue) on both
Wayland and X11. This was tested with both a Gnome desktop on Wayland
and i3 on X11 with fcitx and mozc.
The main changes are:
- Both key press and release events must be forwarded to the input
method.
- Input method callbacks such as preedit and commit must be expected
outside of keypress events to handle on-screen keyboards and
non-keyboard input devices.
- Input methods should always commit when told to. Previously, we would
only commit when a keypress event was given. This is incorrect. For
example, it didn't work with input method changes outside the app
which should result in committed text (as can be seen with "official"
Gnome apps like Notes or Console).
The key input handling also now generally does less so I think input
latency should be positively affected by this change. I didn't measure.
---
src/apprt/gtk/Surface.zig | 251 ++++++++++++++++++++------------------
1 file changed, 131 insertions(+), 120 deletions(-)
diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig
index 61866dcec..5a6ce1a38 100644
--- a/src/apprt/gtk/Surface.zig
+++ b/src/apprt/gtk/Surface.zig
@@ -368,10 +368,9 @@ cursor_pos: apprt.CursorPos,
inspector: ?*inspector.Inspector = null,
/// Key input states. See gtkKeyPressed for detailed descriptions.
-in_keypress: bool = false,
+in_keyevent: bool = false,
im_context: *c.GtkIMContext,
im_composing: bool = false,
-im_commit_buffered: bool = false,
im_buf: [128]u8 = undefined,
im_len: u7 = 0,
@@ -1604,30 +1603,36 @@ fn gtkKeyReleased(
)) 1 else 0;
}
-/// Key press event. This is where we do ALL of our key handling,
-/// translation to keyboard layouts, dead key handling, etc. Key handling
-/// is complicated so this comment will explain what's going on.
+/// Key press event (press or release).
///
/// At a high level, we want to construct an `input.KeyEvent` and
/// pass that to `keyCallback`. At a low level, this is more complicated
/// than it appears because we need to construct all of this information
/// and its not given to us.
///
-/// For press events, we run the keypress through the input method context
-/// in order to determine if we're in a dead key state, completed unicode
-/// char, etc. This all happens through various callbacks: preedit, commit,
-/// etc. These inspect "in_keypress" if they have to and set some instance
-/// state.
+/// For all events, we run the GdkEvent through the input method context.
+/// This allows the input method to capture the event and trigger
+/// callbacks such as preedit, commit, etc.
///
-/// We then take all of the information in order to determine if we have
+/// There are a couple important aspects to the prior paragraph: we must
+/// send ALL events through the input method context. This is because
+/// input methods use both key press and key release events to determine
+/// the state of the input method. For example, fcitx uses key release
+/// events on modifiers (i.e. ctrl+shift) to switch the input method.
+///
+/// We set some state to note we're in a key event (self.in_keyevent)
+/// because some of the input method callbacks change behavior based on
+/// this state. For example, we don't want to send character events
+/// like "a" via the input "commit" event if we're actively processing
+/// a keypress because we'd lose access to the keycode information.
+/// However, a "commit" event may still happen outside of a keypress
+/// event from e.g. a tablet or on-screen keyboard.
+///
+/// Finally, we take all of the information in order to determine if we have
/// a unicode character or if we have to map the keyval to a code to
/// get the underlying logical key, etc.
///
-/// Finally, we can emit the keyCallback.
-///
-/// Note we ALSO have an IMContext attached directly to the widget
-/// which can emit preedit and commit callbacks. But, if we're not
-/// in a keypress, we let those automatically work.
+/// Then we can emit the keyCallback.
pub fn keyEvent(
self: *Surface,
action: input.Action,
@@ -1636,26 +1641,15 @@ pub fn keyEvent(
keycode: c.guint,
gtk_mods: c.GdkModifierType,
) bool {
+ // log.warn("GTKIM: keyEvent action={}", .{action});
const event = c.gtk_event_controller_get_current_event(
@ptrCast(ec_key),
) orelse return false;
- const keyval_unicode = c.gdk_keyval_to_unicode(keyval);
-
- // Get the unshifted unicode value of the keyval. This is used
- // by the Kitty keyboard protocol.
- const keyval_unicode_unshifted: u21 = gtk_key.keyvalUnicodeUnshifted(
- @ptrCast(self.gl_area),
- event,
- keycode,
- );
-
- // We always reset our committed text when ending a keypress so that
- // future keypresses don't think we have a commit event.
- defer self.im_len = 0;
-
- // We only want to send the event through the IM context if we're a press
- if (action == .press or action == .repeat) {
+ // The block below is all related to input method handling. See the function
+ // comment for some high level details and then the comments within
+ // the block for more specifics.
+ {
// This can trigger an input method so we need to notify the im context
// where the cursor is so it can render the dropdowns in the correct
// place.
@@ -1667,41 +1661,77 @@ pub fn keyEvent(
.height = 1,
});
- // We mark that we're in a keypress event. We use this in our
- // IM commit callback to determine if we need to send a char callback
- // to the core surface or not.
- self.in_keypress = true;
- defer self.in_keypress = false;
+ // Pass the event through the IM controller. This will return true
+ // if the input method handled the event.
+ //
+ // Confusingly, not all events handled by the input method result
+ // in this returning true so we have to maintain some local state to
+ // find those and in one case we simply lose information.
+ //
+ // - If we change the input method via keypress while we have preedit
+ // text, the input method will commit the pending text but will not
+ // mark it as handled. We use the `was_composing` variable to detect
+ // this case.
+ //
+ // - If we switch input methods (i.e. via ctrl+shift with fcitx),
+ // the input method will handle the key release event but will not
+ // mark it as handled. I don't know any way to detect this case so
+ // it will result in a key event being sent to the key callback.
+ // For Kitty text encoding, this will result in modifiers being
+ // triggered despite being technically consumed. At the time of
+ // writing, both Kitty and Alacritty have the same behavior. I
+ // know of no way to fix this.
+ const was_composing = self.im_composing;
+ const im_handled = filter: {
+ // We note that we're in a keypress because we want some logic to
+ // depend on this. For example, we don't want to send character events
+ // like "a" via the input "commit" event if we're actively processing
+ // a keypress because we'd lose access to the keycode information.
+ self.in_keyevent = true;
+ defer self.in_keyevent = false;
+ break :filter c.gtk_im_context_filter_keypress(
+ self.im_context,
+ event,
+ ) != 0;
+ };
+ // log.warn("GTKIM: im_handled={} im_len={} im_composing={}", .{
+ // im_handled,
+ // self.im_len,
+ // self.im_composing,
+ // });
- // Pass the event through the IM controller to handle dead key states.
- // Filter is true if the event was handled by the IM controller.
- const im_handled = c.gtk_im_context_filter_keypress(self.im_context, event) != 0;
- // log.warn("im_handled={} im_len={} im_composing={}", .{ im_handled, self.im_len, self.im_composing });
-
- // If this is a dead key, then we're composing a character and
- // we need to set our proper preedit state.
- if (self.im_composing) preedit: {
- const text = self.im_buf[0..self.im_len];
- self.core_surface.preeditCallback(text) catch |err| {
- log.err("error in preedit callback err={}", .{err});
- break :preedit;
- };
-
- // If we're composing then we don't want to send the key
- // event to the core surface so we always return immediately.
+ if (self.im_composing) {
+ // If we're composing and the input method handled this event then
+ // we don't continue processing it. Any preedit changes or any of that
+ // would've been handled by the preedit events.
if (im_handled) return true;
- } else {
- // If we aren't composing, then we set our preedit to
- // empty no matter what.
- self.core_surface.preeditCallback(null) catch {};
-
- // If the IM handled this and we have no text, then we just
- // return because this probably just changed the input method
- // or something.
- if (im_handled and self.im_len == 0) return true;
+ } else if (was_composing) {
+ // If we were composing and now we're not it means that we committed
+ // the text. We also don't want to encode a key event for this.
+ return true;
}
+
+ // At this point, for the sake of explanation of internal state:
+ // it is possible that im_len > 0 and im_composing == false. This
+ // means that we received a commit event from the input method that
+ // we want associated with the key event. This is common: its how
+ // basic character translation for simple inputs like "a" work.
}
+ // We always reset the length of the im buffer. There's only one scenario
+ // we reach this point with im_len > 0 and that's if we received a commit
+ // event from the input method. We don't want to keep that state around
+ // since we've handled it here.
+ defer self.im_len = 0;
+
+ // Get the keyvals for this event.
+ const keyval_unicode = c.gdk_keyval_to_unicode(keyval);
+ const keyval_unicode_unshifted: u21 = gtk_key.keyvalUnicodeUnshifted(
+ @ptrCast(self.gl_area),
+ event,
+ keycode,
+ );
+
// We want to get the physical unmapped key to process physical keybinds.
// (These are keybinds explicitly marked as requesting physical mapping).
const physical_key = keycode: for (input.keycodes.entries) |entry| {
@@ -1834,12 +1864,11 @@ fn gtkInputPreeditStart(
_: *c.GtkIMContext,
ud: ?*anyopaque,
) callconv(.C) void {
- //log.debug("preedit start", .{});
+ // log.warn("GTKIM: preedit start", .{});
const self = userdataSelf(ud.?);
- if (!self.in_keypress) return;
- // Mark that we are now composing a string with a dead key state.
- // We'll record the string in the preedit-changed callback.
+ // Start our composing state for the input method and reset our
+ // input buffer to empty.
self.im_composing = true;
self.im_len = 0;
}
@@ -1848,54 +1877,35 @@ fn gtkInputPreeditChanged(
ctx: *c.GtkIMContext,
ud: ?*anyopaque,
) callconv(.C) void {
+ // log.warn("GTKIM: preedit change", .{});
const self = userdataSelf(ud.?);
- // If there's buffered character, send the characters directly to the surface.
- if (self.im_composing and self.im_commit_buffered) {
- defer self.im_commit_buffered = false;
- defer self.im_len = 0;
- _ = self.core_surface.keyCallback(.{
- .action = .press,
- .key = .invalid,
- .physical_key = .invalid,
- .mods = .{},
- .consumed_mods = .{},
- .composing = false,
- .utf8 = self.im_buf[0..self.im_len],
- }) catch |err| {
- log.err("error in key callback err={}", .{err});
- return;
- };
- }
-
- if (!self.in_keypress) return;
-
// Get our pre-edit string that we'll use to show the user.
var buf: [*c]u8 = undefined;
_ = c.gtk_im_context_get_preedit_string(ctx, &buf, null, null);
defer c.g_free(buf);
const str = std.mem.sliceTo(buf, 0);
- // If our string becomes empty we ignore this. This can happen after
- // a commit event when the preedit is being cleared and we don't want
- // to set im_len to zero. This is safe because preeditstart always sets
- // im_len to zero.
- if (str.len == 0) return;
-
- // Copy the preedit string into the im_buf. This is safe because
- // commit will always overwrite this.
- self.im_len = @intCast(@min(self.im_buf.len, str.len));
- @memcpy(self.im_buf[0..self.im_len], str);
+ // Update our preedit state in Ghostty core
+ self.core_surface.preeditCallback(str) catch |err| {
+ log.err("error in preedit callback err={}", .{err});
+ };
}
fn gtkInputPreeditEnd(
_: *c.GtkIMContext,
ud: ?*anyopaque,
) callconv(.C) void {
- //log.debug("preedit end", .{});
+ // log.warn("GTKIM: preedit end", .{});
const self = userdataSelf(ud.?);
- if (!self.in_keypress) return;
+
+ // End our composing state for GTK, allowing us to commit the text.
self.im_composing = false;
+
+ // End our preedit state in Ghostty core
+ self.core_surface.preeditCallback(null) catch |err| {
+ log.err("error in preedit callback err={}", .{err});
+ };
}
fn gtkInputCommit(
@@ -1903,38 +1913,39 @@ fn gtkInputCommit(
bytes: [*:0]u8,
ud: ?*anyopaque,
) callconv(.C) void {
+ // log.warn("GTKIM: input commit", .{});
const self = userdataSelf(ud.?);
const str = std.mem.sliceTo(bytes, 0);
- // If we're in a key event, then we want to buffer the commit so
- // that we can send the proper keycallback followed by the char
- // callback.
- if (self.in_keypress) {
- if (str.len <= self.im_buf.len) {
- @memcpy(self.im_buf[0..str.len], str);
- self.im_len = @intCast(str.len);
-
- // If composing is done and character should be committed,
- // It should be committed in preedit callback.
- if (self.im_composing) {
- self.im_commit_buffered = true;
- }
-
- // log.debug("input commit len={}", .{self.im_len});
- } else {
+ // If we're in a keyEvent (i.e. a keyboard event) and we're not composing,
+ // then this is just a normal key press resulting in UTF-8 text. We
+ // want the keyEvent to handle this so that the UTF-8 text can be associated
+ // with a keyboard event.
+ if (!self.im_composing and self.in_keyevent) {
+ if (str.len > self.im_buf.len) {
log.warn("not enough buffer space for input method commit", .{});
+ return;
}
+ // Copy our committed text to the buffer
+ @memcpy(self.im_buf[0..str.len], str);
+ self.im_len = @intCast(str.len);
+
+ // log.debug("input commit len={}", .{self.im_len});
return;
}
- // This prevents staying in composing state after commit even though
- // input method has changed.
+ // If we reach this point from above it means we're composing OR
+ // not in a keypress. In either case, we want to commit the text
+ // given to us because that's what GTK is asking us to do. If we're
+ // not in a keypress it means that this commit came via a non-keyboard
+ // event (i.e. on-screen keyboard, tablet of some kind, etc.).
+
+ // Committing ends composing state
self.im_composing = false;
- // We're not in a keypress, so this was sent from an on-screen emoji
- // keyboard or something like that. Send the characters directly to
- // the surface.
+ // Send the text to the core surface, associated with no key (an
+ // invalid key, which should produce no PTY encoding).
_ = self.core_surface.keyCallback(.{
.action = .press,
.key = .invalid,
@@ -1944,7 +1955,7 @@ fn gtkInputCommit(
.composing = false,
.utf8 = str,
}) catch |err| {
- log.err("error in key callback err={}", .{err});
+ log.warn("error in key callback err={}", .{err});
return;
};
}
From a8d218561121671bdce3aaab76688970f9893e9e Mon Sep 17 00:00:00 2001
From: Eric Eastwood
Date: Tue, 21 Jan 2025 20:13:14 -0600
Subject: [PATCH 142/365] Switch default key bindings to include on and
offscreen contents
Previous discussions:
- https://github.com/ghostty-org/ghostty/discussions/3652
- https://github.com/ghostty-org/ghostty/issues/3496
- https://github.com/ghostty-org/ghostty/discussions/4911
- https://github.com/ghostty-org/ghostty/discussions/4390
- https://github.com/ghostty-org/ghostty/discussions/2363#discussioncomment-11735957
- https://github.com/ghostty-org/ghostty/issues/189#issuecomment-2564719973
- https://github.com/ghostty-org/ghostty/pull/2040
---
src/config/Config.zig | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/config/Config.zig b/src/config/Config.zig
index 386e6f923..fd0f58669 100644
--- a/src/config/Config.zig
+++ b/src/config/Config.zig
@@ -2370,13 +2370,13 @@ pub fn default(alloc_gpa: Allocator) Allocator.Error!Config {
try result.keybind.set.put(
alloc,
.{ .key = .{ .translated = .j }, .mods = inputpkg.ctrlOrSuper(.{ .shift = true }) },
- .{ .write_scrollback_file = .paste },
+ .{ .write_screen_file = .paste },
);
try result.keybind.set.put(
alloc,
.{ .key = .{ .translated = .j }, .mods = inputpkg.ctrlOrSuper(.{ .shift = true, .alt = true }) },
- .{ .write_scrollback_file = .open },
+ .{ .write_screen_file = .open },
);
// Expand Selection
From 4408101b8d71d6819e0b82d3ccc1d714bec2c928 Mon Sep 17 00:00:00 2001
From: Mitchell Hashimoto
Date: Wed, 22 Jan 2025 20:07:26 -0800
Subject: [PATCH 143/365] apprt/gtk: ibus activation should not encode keys
This cleans up our handling of when GTK tells us the input method
handled the key press to address more scenarios we should not encode the
key event. The comments in this diff should explain clearly.
---
src/apprt/gtk/Surface.zig | 31 ++++++++++++++++++++++++-------
1 file changed, 24 insertions(+), 7 deletions(-)
diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig
index 5a6ce1a38..4539e61cb 100644
--- a/src/apprt/gtk/Surface.zig
+++ b/src/apprt/gtk/Surface.zig
@@ -1700,15 +1700,32 @@ pub fn keyEvent(
// self.im_composing,
// });
- if (self.im_composing) {
- // If we're composing and the input method handled this event then
- // we don't continue processing it. Any preedit changes or any of that
- // would've been handled by the preedit events.
- if (im_handled) return true;
- } else if (was_composing) {
+ // If the input method handled the event, you would think we would
+ // never proceed with key encoding for Ghostty but that is not the
+ // case. Input methods will handle basic character encoding like
+ // typing "a" and we want to associate that with the key event.
+ // So we have to check additional state to determine if we exit.
+ if (im_handled) {
+ // If we are composing then we're in a preedit state and do
+ // not want to encode any keys. For example: type a deadkey
+ // such as single quote on a US international keyboard layout.
+ if (self.im_composing) return true;
+
// If we were composing and now we're not it means that we committed
// the text. We also don't want to encode a key event for this.
- return true;
+ // Example: enable Japanese input method, press "konn" and then
+ // press enter. The final enter should not be encoded and "konn"
+ // (in hiragana) should be written as "こん".
+ if (was_composing) return true;
+
+ // Not composing and our input method buffer is empty. This could
+ // mean that the input method reacted to this event by activating
+ // an onscreen keyboard or something equivalent. We don't know.
+ // But the input method handled it and didn't give us text so
+ // we will just assume we should not encode this. This handles a
+ // real scenario when ibus starts the emoji input method
+ // (super+.).
+ if (self.im_len == 0) return true;
}
// At this point, for the sake of explanation of internal state:
From a2018d7b20b557eeb2e0e59e39e9ad3c0c16e44a Mon Sep 17 00:00:00 2001
From: Jon Parise
Date: Thu, 23 Jan 2025 10:34:27 -0500
Subject: [PATCH 144/365] bash: handle additional command arguments
A '-' or '--' argument signals the end of bash's own options. All
remaining arguments are treated as filenames and arguments. We shouldn't
perform any additional argument processing once we see this signal.
We could also assume a non-interactive shell session in this case unless
the '-i' (interactive) shell option has been explicitly specified, but
let's wait on that until we know that doing so would solve a real user
problem (and avoid any false negatives).
---
src/termio/shell_integration.zig | 40 +++++++++++++++++++++++++-------
1 file changed, 32 insertions(+), 8 deletions(-)
diff --git a/src/termio/shell_integration.zig b/src/termio/shell_integration.zig
index 915d5be9e..423e2f518 100644
--- a/src/termio/shell_integration.zig
+++ b/src/termio/shell_integration.zig
@@ -203,8 +203,6 @@ test "setup features" {
/// our script's responsibility (along with disabling POSIX
/// mode).
///
-/// This approach requires bash version 4 or later.
-///
/// This returns a new (allocated) shell command string that
/// enables the integration or null if integration failed.
fn setupBash(
@@ -246,12 +244,6 @@ fn setupBash(
// Unsupported options:
// -c -c is always non-interactive
// --posix POSIX mode (a la /bin/sh)
- //
- // Some additional cases we don't yet cover:
- //
- // - If additional file arguments are provided (after a `-` or `--` flag),
- // and the `i` shell option isn't being explicitly set, we can assume a
- // non-interactive shell session and skip loading our shell integration.
var rcfile: ?[]const u8 = null;
while (iter.next()) |arg| {
if (std.mem.eql(u8, arg, "--posix")) {
@@ -268,6 +260,14 @@ fn setupBash(
return null;
}
try args.append(arg);
+ } else if (std.mem.eql(u8, arg, "-") or std.mem.eql(u8, arg, "--")) {
+ // All remaining arguments should be passed directly to the shell
+ // command. We shouldn't perform any further option processing.
+ try args.append(arg);
+ while (iter.next()) |remaining_arg| {
+ try args.append(remaining_arg);
+ }
+ break;
} else {
try args.append(arg);
}
@@ -430,6 +430,30 @@ test "bash: HISTFILE" {
}
}
+test "bash: additional arguments" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var env = EnvMap.init(alloc);
+ defer env.deinit();
+
+ // "-" argument separator
+ {
+ const command = try setupBash(alloc, "bash - --arg file1 file2", ".", &env);
+ defer if (command) |c| alloc.free(c);
+
+ try testing.expectEqualStrings("bash --posix - --arg file1 file2", command.?);
+ }
+
+ // "--" argument separator
+ {
+ const command = try setupBash(alloc, "bash -- --arg file1 file2", ".", &env);
+ defer if (command) |c| alloc.free(c);
+
+ try testing.expectEqualStrings("bash --posix -- --arg file1 file2", command.?);
+ }
+}
+
/// Setup automatic shell integration for shells that include
/// their modules from paths in `XDG_DATA_DIRS` env variable.
///
From d1e45ef768d0d342fff686c387b1e414d1b48c4c Mon Sep 17 00:00:00 2001
From: Mitchell Hashimoto
Date: Thu, 23 Jan 2025 07:24:11 -0800
Subject: [PATCH 145/365] apprt/gtk: when text is committed, end the preedit
state
Fixes #3567
ibus 1.5.29 doesn't trigger a preedit end state when text is committed.
This is fixed in ibus 1.5.30, but we need to handle this case for older
versions which are shipped on LTS distributions such as Ubuntu.
Every other input method engine I've tried thus far also triggers a
preedit end state when text is committed, and none would expect preedit
to continue after text is committed. So I think it's safe to assume that
this is the expected behavior.
---
src/apprt/gtk/Surface.zig | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig
index 4539e61cb..b429c7233 100644
--- a/src/apprt/gtk/Surface.zig
+++ b/src/apprt/gtk/Surface.zig
@@ -1961,6 +1961,12 @@ fn gtkInputCommit(
// Committing ends composing state
self.im_composing = false;
+ // End our preedit state. Well-behaved input methods do this for us
+ // by triggering a preedit-end event but some do not (ibus 1.5.29).
+ self.core_surface.preeditCallback(null) catch |err| {
+ log.err("error in preedit callback err={}", .{err});
+ };
+
// Send the text to the core surface, associated with no key (an
// invalid key, which should produce no PTY encoding).
_ = self.core_surface.keyCallback(.{
From 8f49a227b7c352083b0815e7818db900402513e0 Mon Sep 17 00:00:00 2001
From: "Jeffrey C. Ollie"
Date: Wed, 22 Jan 2025 22:29:25 -0600
Subject: [PATCH 146/365] build: options to enable/disable terminfo & termcap
install
Fixes #5253
Add `-Demit-terminfo` and `-Demit-termcap` build options to
enable/disable installtion of source terminfo and termcap files.
---
src/build/Config.zig | 25 +++++++++++++++++++-
src/build/GhosttyResources.zig | 42 +++++++++++++++++-----------------
src/os/resourcesdir.zig | 5 +++-
3 files changed, 49 insertions(+), 23 deletions(-)
diff --git a/src/build/Config.zig b/src/build/Config.zig
index 71dffce4a..1d51525d0 100644
--- a/src/build/Config.zig
+++ b/src/build/Config.zig
@@ -55,6 +55,8 @@ emit_helpgen: bool = false,
emit_docs: bool = false,
emit_webdata: bool = false,
emit_xcframework: bool = false,
+emit_terminfo: bool = false,
+emit_termcap: bool = false,
/// Environmental properties
env: std.process.EnvMap,
@@ -306,11 +308,32 @@ pub fn init(b: *std.Build) !Config {
break :emit_docs path != null;
};
+ config.emit_terminfo = b.option(
+ bool,
+ "emit-terminfo",
+ "Install Ghostty terminfo source file",
+ ) orelse switch (target.result.os.tag) {
+ .windows => true,
+ else => switch (optimize) {
+ .Debug => true,
+ .ReleaseSafe, .ReleaseFast, .ReleaseSmall => false,
+ },
+ };
+
+ config.emit_termcap = b.option(
+ bool,
+ "emit-termcap",
+ "Install Ghostty termcap file",
+ ) orelse false;
+
config.emit_webdata = b.option(
bool,
"emit-webdata",
"Build the website data for the website.",
- ) orelse false;
+ ) orelse switch (optimize) {
+ .Debug => true,
+ .ReleaseSafe, .ReleaseFast, .ReleaseSmall => false,
+ };
config.emit_xcframework = b.option(
bool,
diff --git a/src/build/GhosttyResources.zig b/src/build/GhosttyResources.zig
index cae907ec2..c0830e5f6 100644
--- a/src/build/GhosttyResources.zig
+++ b/src/build/GhosttyResources.zig
@@ -23,9 +23,12 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources {
// Write it
var wf = b.addWriteFiles();
- const src_source = wf.add("share/terminfo/ghostty.terminfo", str.items);
- const src_install = b.addInstallFile(src_source, "share/terminfo/ghostty.terminfo");
- try steps.append(&src_install.step);
+ const source = wf.add("ghostty.terminfo", str.items);
+
+ if (cfg.emit_terminfo) {
+ const source_install = b.addInstallFile(source, "share/terminfo/ghostty.terminfo");
+ try steps.append(&source_install.step);
+ }
// Windows doesn't have the binaries below.
if (cfg.target.result.os.tag == .windows) break :terminfo;
@@ -33,10 +36,10 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources {
// Convert to termcap source format if thats helpful to people and
// install it. The resulting value here is the termcap source in case
// that is used for other commands.
- {
+ if (cfg.emit_termcap) {
const run_step = RunStep.create(b, "infotocap");
- run_step.addArg("infotocap");
- run_step.addFileArg(src_source);
+ run_step.addArgs(&.{ "infotocap", "-" });
+ run_step.setStdIn(.{ .lazy_path = source });
const out_source = run_step.captureStdOut();
_ = run_step.captureStdErr(); // so we don't see stderr
@@ -48,24 +51,21 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources {
{
const run_step = RunStep.create(b, "tic");
run_step.addArgs(&.{ "tic", "-x", "-o" });
- const path = run_step.addOutputFileArg("terminfo");
- run_step.addFileArg(src_source);
+ const path = run_step.addOutputDirectoryArg("share/terminfo");
+ run_step.addArg("-");
+ run_step.setStdIn(.{ .lazy_path = source });
_ = run_step.captureStdErr(); // so we don't see stderr
- // Depend on the terminfo source install step so that Zig build
- // creates the "share" directory for us.
- run_step.step.dependOn(&src_install.step);
+ try steps.append(&run_step.step);
- {
- // Use cp -R instead of Step.InstallDir because we need to preserve
- // symlinks in the terminfo database. Zig's InstallDir step doesn't
- // handle symlinks correctly yet.
- const copy_step = RunStep.create(b, "copy terminfo db");
- copy_step.addArgs(&.{ "cp", "-R" });
- copy_step.addFileArg(path);
- copy_step.addArg(b.fmt("{s}/share", .{b.install_path}));
- try steps.append(©_step.step);
- }
+ // Use cp -R instead of Step.InstallDir because we need to preserve
+ // symlinks in the terminfo database. Zig's InstallDir step doesn't
+ // handle symlinks correctly yet.
+ const copy_step = RunStep.create(b, "copy terminfo db");
+ copy_step.addArgs(&.{ "cp", "-R" });
+ copy_step.addFileArg(path);
+ copy_step.addArg(b.fmt("{s}/share", .{b.install_path}));
+ try steps.append(©_step.step);
}
}
diff --git a/src/os/resourcesdir.zig b/src/os/resourcesdir.zig
index c0f82dec5..d2b274e87 100644
--- a/src/os/resourcesdir.zig
+++ b/src/os/resourcesdir.zig
@@ -21,7 +21,10 @@ pub fn resourcesDir(alloc: std.mem.Allocator) !?[]const u8 {
// This is the sentinel value we look for in the path to know
// we've found the resources directory.
- const sentinel = "terminfo/ghostty.termcap";
+ const sentinel = switch (comptime builtin.target.os.tag) {
+ .windows => "terminfo/ghostty.terminfo",
+ else => "terminfo/x/xterm-ghostty",
+ };
// Get the path to our running binary
var exe_buf: [std.fs.max_path_bytes]u8 = undefined;
From 9c8c53bffb9d5a8f4cc712fcea083fbf79f833c4 Mon Sep 17 00:00:00 2001
From: Julia <58243358+juliapaci@users.noreply.github.com>
Date: Fri, 24 Jan 2025 07:57:14 +1100
Subject: [PATCH 147/365] use main buffer and copy data to fbo texture (opengl)
(#5294)
NEEDS REVIEW
continuation of #5037
resolves #4729
renders all shaders to the default buffer and then copies it to the
designated custom shader texture.
this is a draft pr because:
- it introduces a new shader "pipeline" which doesnt fit in with how the
system was designed to work (which is only rendering to the fbo)
- im not sure if this is the best way to achieve shaders being able to
sample their output while also drawing to the screen. the cusom fbo
(previous implementation) was useful in that it modularized the custom
shader stage in rendering
---------
Co-authored-by: Mitchell Hashimoto
---
pkg/opengl/Texture.zig | 22 ++++++++++++++++++++++
src/renderer/OpenGL.zig | 18 ++----------------
src/renderer/opengl/custom.zig | 15 +++++++++++++++
3 files changed, 39 insertions(+), 16 deletions(-)
diff --git a/pkg/opengl/Texture.zig b/pkg/opengl/Texture.zig
index 4cd1cf9f9..a9fa5d4fe 100644
--- a/pkg/opengl/Texture.zig
+++ b/pkg/opengl/Texture.zig
@@ -162,4 +162,26 @@ pub const Binding = struct {
data,
);
}
+
+ pub fn copySubImage2D(
+ b: Binding,
+ level: c.GLint,
+ xoffset: c.GLint,
+ yoffset: c.GLint,
+ x: c.GLint,
+ y: c.GLint,
+ width: c.GLsizei,
+ height: c.GLsizei,
+ ) !void {
+ glad.context.CopyTexSubImage2D.?(
+ @intFromEnum(b.target),
+ level,
+ xoffset,
+ yoffset,
+ x,
+ y,
+ width,
+ height
+ );
+ }
};
diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig
index 80fc3cab9..3e674c715 100644
--- a/src/renderer/OpenGL.zig
+++ b/src/renderer/OpenGL.zig
@@ -2363,26 +2363,12 @@ fn drawCustomPrograms(self: *OpenGL, custom_state: *custom.State) !void {
// Setup the new frame
try custom_state.newFrame();
- // To allow programs to retrieve each other via a texture
- // then we must render the next shaders to the screen fbo.
- // However, the first shader must be run while the default fbo
- // is attached
- {
- const bind = try custom_state.programs[0].bind();
- defer bind.unbind();
- try bind.draw();
- if (custom_state.programs.len == 1) return;
- }
-
- const fbobind = try custom_state.fbo.bind(.framebuffer);
- defer fbobind.unbind();
-
// Go through each custom shader and draw it.
- for (custom_state.programs[1..]) |program| {
- // Bind our cell program state, buffers
+ for (custom_state.programs) |program| {
const bind = try program.bind();
defer bind.unbind();
try bind.draw();
+ try custom_state.copyFramebuffer();
}
}
diff --git a/src/renderer/opengl/custom.zig b/src/renderer/opengl/custom.zig
index 3806921df..859277ce5 100644
--- a/src/renderer/opengl/custom.zig
+++ b/src/renderer/opengl/custom.zig
@@ -230,6 +230,21 @@ pub const State = struct {
};
}
+ /// Copy the fbo's attached texture to the backbuffer.
+ pub fn copyFramebuffer(self: *State) !void {
+ const texbind = try self.fb_texture.bind(.@"2D");
+ errdefer texbind.unbind();
+ try texbind.copySubImage2D(
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ @intFromFloat(self.uniforms.resolution[0]),
+ @intFromFloat(self.uniforms.resolution[1]),
+ );
+ }
+
pub const Binding = struct {
vao: gl.VertexArray.Binding,
ebo: gl.Buffer.Binding,
From 5477eb87c17a76d847ba34093d85fd4ef1490149 Mon Sep 17 00:00:00 2001
From: Qwerasd
Date: Mon, 28 Oct 2024 18:41:53 -0400
Subject: [PATCH 148/365] macOS: prevent native window drag by top region when
titlebar hidden
The native window drag region is driven ultimately by the window's
`contentLayoutRect`, so we can just override it in `TerminalWindow`
to return a rect the size of the full window, disabling the gesture
without causing any side effects by altering the responder chain.
---
.../Terminal/TerminalController.swift | 22 +++++++++----------
.../Features/Terminal/TerminalWindow.swift | 15 +++++++++++++
src/config/Config.zig | 9 +++++---
3 files changed, 32 insertions(+), 14 deletions(-)
diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift
index 89da6bfeb..f24261b9b 100644
--- a/macos/Sources/Features/Terminal/TerminalController.swift
+++ b/macos/Sources/Features/Terminal/TerminalController.swift
@@ -22,7 +22,7 @@ class TerminalController: BaseTerminalController {
private var restorable: Bool = true
/// The configuration derived from the Ghostty config so we don't need to rely on references.
- private var derivedConfig: DerivedConfig
+ private(set) var derivedConfig: DerivedConfig
/// The notification cancellable for focused surface property changes.
private var surfaceAppearanceCancellables: Set = []
@@ -315,28 +315,28 @@ class TerminalController: BaseTerminalController {
window.styleMask = [
// We need `titled` in the mask to get the normal window frame
.titled,
-
+
// Full size content view so we can extend
// content in to the hidden titlebar's area
- .fullSizeContentView,
-
- .resizable,
+ .fullSizeContentView,
+
+ .resizable,
.closable,
.miniaturizable,
]
-
+
// Hide the title
window.titleVisibility = .hidden
window.titlebarAppearsTransparent = true
-
+
// Hide the traffic lights (window control buttons)
window.standardWindowButton(.closeButton)?.isHidden = true
window.standardWindowButton(.miniaturizeButton)?.isHidden = true
window.standardWindowButton(.zoomButton)?.isHidden = true
-
+
// Disallow tabbing if the titlebar is hidden, since that will (should) also hide the tab bar.
window.tabbingMode = .disallowed
-
+
// Nuke it from orbit -- hide the titlebar container entirely, just in case. There are
// some operations that appear to bring back the titlebar visibility so this ensures
// it is gone forever.
@@ -345,7 +345,7 @@ class TerminalController: BaseTerminalController {
titleBarContainer.isHidden = true
}
}
-
+
override func windowDidLoad() {
super.windowDidLoad()
guard let window = window as? TerminalWindow else { return }
@@ -776,7 +776,7 @@ class TerminalController: BaseTerminalController {
toggleFullscreen(mode: fullscreenMode)
}
- private struct DerivedConfig {
+ struct DerivedConfig {
let backgroundColor: Color
let macosTitlebarStyle: String
diff --git a/macos/Sources/Features/Terminal/TerminalWindow.swift b/macos/Sources/Features/Terminal/TerminalWindow.swift
index 0eb8daeeb..9d29c193f 100644
--- a/macos/Sources/Features/Terminal/TerminalWindow.swift
+++ b/macos/Sources/Features/Terminal/TerminalWindow.swift
@@ -115,6 +115,21 @@ class TerminalWindow: NSWindow {
}
}
+ // We override this so that with the hidden titlebar style the titlebar
+ // area is not draggable.
+ override var contentLayoutRect: CGRect {
+ var rect = super.contentLayoutRect
+
+ // If we are using a hidden titlebar style, the content layout is the
+ // full frame making it so that it is not draggable.
+ if let controller = windowController as? TerminalController,
+ controller.derivedConfig.macosTitlebarStyle == "hidden" {
+ rect.origin.y = 0
+ rect.size.height = self.frame.height
+ }
+ return rect
+ }
+
// The window theme configuration from Ghostty. This is used to control some
// behaviors that don't look quite right in certain situations.
var windowTheme: TerminalWindowTheme?
diff --git a/src/config/Config.zig b/src/config/Config.zig
index fd0f58669..e32a3485f 100644
--- a/src/config/Config.zig
+++ b/src/config/Config.zig
@@ -1816,9 +1816,12 @@ keybind: Keybinds = .{},
/// The "hidden" style hides the titlebar. Unlike `window-decoration = false`,
/// however, it does not remove the frame from the window or cause it to have
/// squared corners. Changing to or from this option at run-time may affect
-/// existing windows in buggy ways. The top titlebar area of the window will
-/// continue to drag the window around and you will not be able to use
-/// the mouse for terminal events in this space.
+/// existing windows in buggy ways.
+///
+/// When "hidden", the top titlebar area can no longer be used for dragging
+/// the window. To drag the window, you can use option+click on the resizable
+/// areas of the frame to drag the window. This is a standard macOS behavior
+/// and not something Ghostty enables.
///
/// The default value is "transparent". This is an opinionated choice
/// but its one I think is the most aesthetically pleasing and works in
From e854b38872adc38050c39b6f2e8f580268d1e08c Mon Sep 17 00:00:00 2001
From: Mitchell Hashimoto
Date: Thu, 23 Jan 2025 14:11:10 -0800
Subject: [PATCH 149/365] cli: allow renaming config fields to maintain
backwards compatibility
Fixes #4631
This introduces a mechanism by which parsed config fields can be renamed
to maintain backwards compatibility. This already has a use case --
implemented in this commit -- for `background-blur-radius` to be renamed
to `background-blur`.
The remapping is comptime-known which lets us do some comptime
validation. The remap check isn't done unless no fields match which
means for well-formed config files, there's no overhead.
For future improvements:
- We should update our config help generator to note renamed fields.
- We could offer automatic migration of config files be rewriting them.
- We can enrich the value type with more metadata to help with
config gen or other tooling.
---
macos/Sources/Ghostty/Ghostty.Config.swift | 2 +-
src/apprt/embedded.zig | 2 +-
src/apprt/gtk/winproto/wayland.zig | 2 +-
src/apprt/gtk/winproto/x11.zig | 2 +-
src/cli/args.zig | 52 ++++++++++++++++++++++
src/config/Config.zig | 13 +++++-
src/config/c_get.zig | 12 ++---
7 files changed, 73 insertions(+), 12 deletions(-)
diff --git a/macos/Sources/Ghostty/Ghostty.Config.swift b/macos/Sources/Ghostty/Ghostty.Config.swift
index 2a24b0257..9c8042c63 100644
--- a/macos/Sources/Ghostty/Ghostty.Config.swift
+++ b/macos/Sources/Ghostty/Ghostty.Config.swift
@@ -339,7 +339,7 @@ extension Ghostty {
var backgroundBlurRadius: Int {
guard let config = self.config else { return 1 }
var v: Int = 0
- let key = "background-blur-radius"
+ let key = "background-blur"
_ = ghostty_config_get(config, &v, key, UInt(key.count))
return v;
}
diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig
index 44c4c5f20..890901c07 100644
--- a/src/apprt/embedded.zig
+++ b/src/apprt/embedded.zig
@@ -1958,7 +1958,7 @@ pub const CAPI = struct {
_ = CGSSetWindowBackgroundBlurRadius(
CGSDefaultConnectionForThread(),
nswindow.msgSend(usize, objc.sel("windowNumber"), .{}),
- @intCast(config.@"background-blur-radius".cval()),
+ @intCast(config.@"background-blur".cval()),
);
}
diff --git a/src/apprt/gtk/winproto/wayland.zig b/src/apprt/gtk/winproto/wayland.zig
index 7a28fc92c..8df3e57b3 100644
--- a/src/apprt/gtk/winproto/wayland.zig
+++ b/src/apprt/gtk/winproto/wayland.zig
@@ -176,7 +176,7 @@ pub const Window = struct {
pub fn init(config: *const Config) DerivedConfig {
return .{
- .blur = config.@"background-blur-radius".enabled(),
+ .blur = config.@"background-blur".enabled(),
.window_decoration = config.@"window-decoration",
};
}
diff --git a/src/apprt/gtk/winproto/x11.zig b/src/apprt/gtk/winproto/x11.zig
index 4f607d1ef..7a6b8b4c7 100644
--- a/src/apprt/gtk/winproto/x11.zig
+++ b/src/apprt/gtk/winproto/x11.zig
@@ -165,7 +165,7 @@ pub const Window = struct {
pub fn init(config: *const Config) DerivedConfig {
return .{
- .blur = config.@"background-blur-radius".enabled(),
+ .blur = config.@"background-blur".enabled(),
.has_decoration = switch (config.@"window-decoration") {
.none => false,
.auto, .client, .server => true,
diff --git a/src/cli/args.zig b/src/cli/args.zig
index 23dcf7733..166b2daf5 100644
--- a/src/cli/args.zig
+++ b/src/cli/args.zig
@@ -38,6 +38,12 @@ pub const Error = error{
/// "DiagnosticList" and any diagnostic messages will be added to that list.
/// When diagnostics are present, only allocation errors will be returned.
///
+/// If the destination type has a decl "renamed", it must be of type
+/// std.StaticStringMap([]const u8) and contains a mapping from the old
+/// field name to the new field name. This is used to allow renaming fields
+/// while still supporting the old name. If a renamed field is set, parsing
+/// will automatically set the new field name.
+///
/// Note: If the arena is already non-null, then it will be used. In this
/// case, in the case of an error some memory might be leaked into the arena.
pub fn parse(
@@ -49,6 +55,24 @@ pub fn parse(
const info = @typeInfo(T);
assert(info == .Struct);
+ comptime {
+ // Verify all renamed fields are valid (source does not exist,
+ // destination does exist).
+ if (@hasDecl(T, "renamed")) {
+ for (T.renamed.keys(), T.renamed.values()) |key, value| {
+ if (@hasField(T, key)) {
+ @compileLog(key);
+ @compileError("renamed field source exists");
+ }
+
+ if (!@hasField(T, value)) {
+ @compileLog(value);
+ @compileError("renamed field destination does not exist");
+ }
+ }
+ }
+ }
+
// Make an arena for all our allocations if we support it. Otherwise,
// use an allocator that always fails. If the arena is already set on
// the config, then we reuse that. See memory note in parse docs.
@@ -367,6 +391,16 @@ pub fn parseIntoField(
}
}
+ // Unknown field, is the field renamed?
+ if (@hasDecl(T, "renamed")) {
+ for (T.renamed.keys(), T.renamed.values()) |old, new| {
+ if (mem.eql(u8, old, key)) {
+ try parseIntoField(T, alloc, dst, new, value);
+ return;
+ }
+ }
+ }
+
return error.InvalidField;
}
@@ -1104,6 +1138,24 @@ test "parseIntoField: tagged union missing tag" {
);
}
+test "parseIntoField: renamed field" {
+ const testing = std.testing;
+ var arena = ArenaAllocator.init(testing.allocator);
+ defer arena.deinit();
+ const alloc = arena.allocator();
+
+ var data: struct {
+ a: []const u8,
+
+ const renamed = std.StaticStringMap([]const u8).initComptime(&.{
+ .{ "old", "a" },
+ });
+ } = undefined;
+
+ try parseIntoField(@TypeOf(data), alloc, &data, "old", "42");
+ try testing.expectEqualStrings("42", data.a);
+}
+
/// An iterator that considers its location to be CLI args. It
/// iterates through an underlying iterator and increments a counter
/// to track the current CLI arg index.
diff --git a/src/config/Config.zig b/src/config/Config.zig
index e32a3485f..16e08bf08 100644
--- a/src/config/Config.zig
+++ b/src/config/Config.zig
@@ -42,6 +42,15 @@ const c = @cImport({
@cInclude("unistd.h");
});
+/// Renamed fields, used by cli.parse
+pub const renamed = std.StaticStringMap([]const u8).initComptime(&.{
+ // Ghostty 1.1 introduced background-blur support for Linux which
+ // doesn't support a specific radius value. The renaming is to let
+ // one field be used for both platforms (macOS retained the ability
+ // to set a radius).
+ .{ "background-blur-radius", "background-blur" },
+});
+
/// The font families to use.
///
/// You can generate the list of valid values using the CLI:
@@ -649,7 +658,7 @@ palette: Palette = .{},
/// need to set environment-specific settings and/or install third-party plugins
/// in order to support background blur, as there isn't a unified interface for
/// doing so.
-@"background-blur-radius": BackgroundBlur = .false,
+@"background-blur": BackgroundBlur = .false,
/// The opacity level (opposite of transparency) of an unfocused split.
/// Unfocused splits by default are slightly faded out to make it easier to see
@@ -5854,7 +5863,7 @@ pub const AutoUpdate = enum {
download,
};
-/// See background-blur-radius
+/// See background-blur
pub const BackgroundBlur = union(enum) {
false,
true,
diff --git a/src/config/c_get.zig b/src/config/c_get.zig
index 6804b0ae0..251a95e77 100644
--- a/src/config/c_get.zig
+++ b/src/config/c_get.zig
@@ -192,21 +192,21 @@ test "c_get: background-blur" {
defer c.deinit();
{
- c.@"background-blur-radius" = .false;
+ c.@"background-blur" = .false;
var cval: u8 = undefined;
- try testing.expect(get(&c, .@"background-blur-radius", @ptrCast(&cval)));
+ try testing.expect(get(&c, .@"background-blur", @ptrCast(&cval)));
try testing.expectEqual(0, cval);
}
{
- c.@"background-blur-radius" = .true;
+ c.@"background-blur" = .true;
var cval: u8 = undefined;
- try testing.expect(get(&c, .@"background-blur-radius", @ptrCast(&cval)));
+ try testing.expect(get(&c, .@"background-blur", @ptrCast(&cval)));
try testing.expectEqual(20, cval);
}
{
- c.@"background-blur-radius" = .{ .radius = 42 };
+ c.@"background-blur" = .{ .radius = 42 };
var cval: u8 = undefined;
- try testing.expect(get(&c, .@"background-blur-radius", @ptrCast(&cval)));
+ try testing.expect(get(&c, .@"background-blur", @ptrCast(&cval)));
try testing.expectEqual(42, cval);
}
}
From 80eb406b8200b99c05e126e28015718ff2a35047 Mon Sep 17 00:00:00 2001
From: Adam Wolf
Date: Wed, 15 Jan 2025 00:44:56 -0600
Subject: [PATCH 150/365] fix: gtk titlebar being restored if it shouldn't be
---
src/apprt/gtk/Window.zig | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig
index 59d6437d7..3512e211d 100644
--- a/src/apprt/gtk/Window.zig
+++ b/src/apprt/gtk/Window.zig
@@ -646,7 +646,7 @@ fn gtkWindowNotifyMaximized(
if (!self.winproto.clientSideDecorationEnabled()) return;
if (!maximized) {
- self.headerbar.setVisible(true);
+ self.headerbar.setVisible(self.app.config.@"gtk-titlebar");
return;
}
if (self.app.config.@"gtk-titlebar-hide-when-maximized") {
From 1be89cb1461654962c67f9eede10c7c29a8304d7 Mon Sep 17 00:00:00 2001
From: Adam Wolf
Date: Wed, 15 Jan 2025 00:59:34 -0600
Subject: [PATCH 151/365] fix: also respect gtk-titlebar value in fullscreened
callback
---
src/apprt/gtk/Window.zig | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig
index 3512e211d..b850ece81 100644
--- a/src/apprt/gtk/Window.zig
+++ b/src/apprt/gtk/Window.zig
@@ -675,7 +675,13 @@ fn gtkWindowNotifyFullscreened(
ud: ?*anyopaque,
) callconv(.C) void {
const self = userdataSelf(ud orelse return);
- self.headerbar.setVisible(c.gtk_window_is_fullscreen(@ptrCast(object)) == 0);
+ const fullscreened = c.gtk_window_is_fullscreen(@ptrCast(object)) != 0;
+ if (!fullscreened) {
+ self.headerbar.setVisible(self.app.config.@"gtk-titlebar");
+ return;
+ }
+
+ self.headerbar.setVisible(false);
}
// Note: we MUST NOT use the GtkButton parameter because gtkActionNewTab
From 078ee42be32c3bde946d3ce18b05d79ae518649e Mon Sep 17 00:00:00 2001
From: Mitchell Hashimoto
Date: Thu, 23 Jan 2025 15:04:39 -0800
Subject: [PATCH 152/365] apprt/gtk: we should only show the headerbar again if
csd
---
src/apprt/gtk/Window.zig | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig
index b850ece81..58f5659f0 100644
--- a/src/apprt/gtk/Window.zig
+++ b/src/apprt/gtk/Window.zig
@@ -639,16 +639,20 @@ fn gtkWindowNotifyMaximized(
ud: ?*anyopaque,
) callconv(.C) void {
const self = userdataSelf(ud orelse return);
- const maximized = c.gtk_window_is_maximized(self.window) != 0;
// Only toggle visibility of the header bar when we're using CSDs,
// and actually intend on displaying the header bar
if (!self.winproto.clientSideDecorationEnabled()) return;
+ // If we aren't maximized, we should show the headerbar again
+ // if it was originally visible.
+ const maximized = c.gtk_window_is_maximized(self.window) != 0;
if (!maximized) {
self.headerbar.setVisible(self.app.config.@"gtk-titlebar");
return;
}
+
+ // If we are maximized, we should hide the headerbar if requested.
if (self.app.config.@"gtk-titlebar-hide-when-maximized") {
self.headerbar.setVisible(false);
}
@@ -677,7 +681,8 @@ fn gtkWindowNotifyFullscreened(
const self = userdataSelf(ud orelse return);
const fullscreened = c.gtk_window_is_fullscreen(@ptrCast(object)) != 0;
if (!fullscreened) {
- self.headerbar.setVisible(self.app.config.@"gtk-titlebar");
+ const csd_enabled = self.winproto.clientSideDecorationEnabled();
+ self.headerbar.setVisible(self.app.config.@"gtk-titlebar" and csd_enabled);
return;
}
From 956bb8f02b860be27de966eb31b15028d5544409 Mon Sep 17 00:00:00 2001
From: Leah Amelia Chen
Date: Tue, 14 Jan 2025 14:47:17 +0100
Subject: [PATCH 153/365] gtk: request initial color scheme asynchronously
Requesting the initial color scheme on systems where the D-Bus interface
is nonexistent would delay Ghostty startup by 1-2 minutes. That's not
acceptable. Our color scheme events are already async-friendly anyway.
Fixes #4632
---
src/apprt/gtk/App.zig | 148 +++++++++++++++++---------------------
src/apprt/gtk/Surface.zig | 3 -
2 files changed, 66 insertions(+), 85 deletions(-)
diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig
index 193710293..df74cefb2 100644
--- a/src/apprt/gtk/App.zig
+++ b/src/apprt/gtk/App.zig
@@ -73,6 +73,11 @@ clipboard_confirmation_window: ?*ClipboardConfirmationWindow = null,
/// This is set to false when the main loop should exit.
running: bool = true,
+/// If we should retry querying D-Bus for the color scheme with the deprecated
+/// Read method, instead of the recommended ReadOne method. This is kind of
+/// nasty to have as struct state but its just a byte...
+dbus_color_scheme_retry: bool = true,
+
/// The base path of the transient cgroup used to put all surfaces
/// into their own cgroup. This is only set if cgroups are enabled
/// and initialization was successful.
@@ -1271,7 +1276,8 @@ pub fn run(self: *App) !void {
self.transient_cgroup_base = path;
} else log.debug("cgroup isolation disabled config={}", .{self.config.@"linux-cgroup"});
- // Setup our D-Bus connection for listening to settings changes.
+ // Setup our D-Bus connection for listening to settings changes,
+ // and asynchronously request the initial color scheme
self.initDbus();
// Setup our menu items
@@ -1279,9 +1285,6 @@ pub fn run(self: *App) !void {
self.initMenu();
self.initContextMenu();
- // Setup our initial color scheme
- self.colorSchemeEvent(self.getColorScheme());
-
// On startup, we want to check for configuration errors right away
// so we can show our error window. We also need to setup other initial
// state.
@@ -1329,6 +1332,22 @@ fn initDbus(self: *App) void {
self,
null,
);
+
+ // Request the initial color scheme asynchronously.
+ c.g_dbus_connection_call(
+ dbus,
+ "org.freedesktop.portal.Desktop",
+ "/org/freedesktop/portal/desktop",
+ "org.freedesktop.portal.Settings",
+ "ReadOne",
+ c.g_variant_new("(ss)", "org.freedesktop.appearance", "color-scheme"),
+ c.G_VARIANT_TYPE("(v)"),
+ c.G_DBUS_CALL_FLAGS_NONE,
+ -1,
+ null,
+ dbusColorSchemeCallback,
+ self,
+ );
}
// This timeout function is started when no surfaces are open. It can be
@@ -1566,93 +1585,58 @@ fn gtkWindowIsActive(
core_app.focusEvent(false);
}
-/// Call a D-Bus method to determine the current color scheme. If there
-/// is any error at any point we'll log the error and return "light"
-pub fn getColorScheme(self: *App) apprt.ColorScheme {
- const dbus_connection = c.g_application_get_dbus_connection(@ptrCast(self.app));
+fn dbusColorSchemeCallback(
+ source_object: [*c]c.GObject,
+ res: ?*c.GAsyncResult,
+ ud: ?*anyopaque,
+) callconv(.C) void {
+ const self: *App = @ptrCast(@alignCast(ud.?));
+ const dbus: *c.GDBusConnection = @ptrCast(source_object);
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",
- "ReadOne",
- 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| {
- // 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();
+ if (c.g_dbus_connection_call_finish(dbus, res, &err)) |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 c.g_variant_unref(inner);
+ if (c.g_variant_is_of_type(inner, c.G_VARIANT_TYPE("u")) == 1) {
+ self.colorSchemeEvent(if (c.g_variant_get_uint32(inner) == 1)
+ .dark
+ else
+ .light);
+ return;
}
- // 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);
+ } else 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 (self.dbus_color_scheme_retry and e.code == 19) {
+ self.dbus_color_scheme_retry = false;
+ c.g_dbus_connection_call(
+ dbus,
+ "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,
+ dbusColorSchemeCallback,
+ self,
+ );
+ return;
+ }
- 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 c.g_variant_unref(inner);
- if (c.g_variant_is_of_type(inner, c.G_VARIANT_TYPE("u")) == 1) {
- return if (c.g_variant_get_uint32(inner) == 1) .dark else .light;
- }
+ // Otherwise, log the error and return .light
+ log.warn("unable to get current color scheme: {s}", .{e.message});
}
- 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;
+ // Fall back
+ self.colorSchemeEvent(.light);
}
/// This will be called by D-Bus when the style changes between light & dark.
diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig
index b429c7233..a72830786 100644
--- a/src/apprt/gtk/Surface.zig
+++ b/src/apprt/gtk/Surface.zig
@@ -633,9 +633,6 @@ fn realize(self: *Surface) !void {
try self.core_surface.setFontSize(size);
}
- // Set the initial color scheme
- try self.core_surface.colorSchemeCallback(self.app.getColorScheme());
-
// Note we're realized
self.realized = true;
}
From 5327646d583d46686a31dec902e349144d52c81c Mon Sep 17 00:00:00 2001
From: Mitchell Hashimoto
Date: Thu, 23 Jan 2025 15:41:00 -0800
Subject: [PATCH 154/365] config: rename adw-toasts to app-notifications
There is no `renamed` entry for this because this was never part of a
released version of Ghostty. This is not considered a break change.
Fixes #4460
---
src/apprt/gtk/Surface.zig | 2 +-
src/config/Config.zig | 52 ++++++++++++++++++++-------------------
2 files changed, 28 insertions(+), 26 deletions(-)
diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig
index a72830786..76be18591 100644
--- a/src/apprt/gtk/Surface.zig
+++ b/src/apprt/gtk/Surface.zig
@@ -1133,7 +1133,7 @@ pub fn setClipboardString(
c.gdk_clipboard_set_text(clipboard, val.ptr);
// We only toast if we are copying to the standard clipboard.
if (clipboard_type == .standard and
- self.app.config.@"adw-toast".@"clipboard-copy")
+ self.app.config.@"app-notifications".@"clipboard-copy")
{
if (self.container.window()) |window|
window.sendToast("Copied to clipboard");
diff --git a/src/config/Config.zig b/src/config/Config.zig
index 16e08bf08..839656169 100644
--- a/src/config/Config.zig
+++ b/src/config/Config.zig
@@ -1771,6 +1771,31 @@ keybind: Keybinds = .{},
/// open terminals.
@"custom-shader-animation": CustomShaderAnimation = .true,
+/// Control the in-app notifications that Ghostty shows.
+///
+/// On Linux (GTK) with Adwaita, in-app notifications show up as toasts. Toasts
+/// appear overlaid on top of the terminal window. They are used to show
+/// information that is not critical but may be important.
+///
+/// Possible notifications are:
+///
+/// - `clipboard-copy` (default: true) - Show a notification when text is copied
+/// to the clipboard.
+///
+/// To specify a notification to enable, specify the name of the notification.
+/// To specify a notification to disable, prefix the name with `no-`. For
+/// example, to disable `clipboard-copy`, set this configuration to
+/// `no-clipboard-copy`. To enable it, set this configuration to `clipboard-copy`.
+///
+/// Multiple notifications can be enabled or disabled by separating them
+/// with a comma.
+///
+/// A value of "false" will disable all notifications. A value of "true" will
+/// enable all notifications.
+///
+/// This configuration only applies to GTK with Adwaita enabled.
+@"app-notifications": AppNotifications = .{},
+
/// If anything other than false, fullscreen mode on macOS will not use the
/// native fullscreen, but make the window fullscreen without animations and
/// using a new space. It's faster than the native fullscreen mode since it
@@ -2121,29 +2146,6 @@ keybind: Keybinds = .{},
/// Changing this value at runtime will only affect new windows.
@"adw-toolbar-style": AdwToolbarStyle = .raised,
-/// Control the toasts that Ghostty shows. Toasts are small notifications
-/// that appear overlaid on top of the terminal window. They are used to
-/// show information that is not critical but may be important.
-///
-/// Possible toasts are:
-///
-/// - `clipboard-copy` (default: true) - Show a toast when text is copied
-/// to the clipboard.
-///
-/// To specify a toast to enable, specify the name of the toast. To specify
-/// a toast to disable, prefix the name with `no-`. For example, to disable
-/// the clipboard-copy toast, set this configuration to `no-clipboard-copy`.
-/// To enable the clipboard-copy toast, set this configuration to
-/// `clipboard-copy`.
-///
-/// Multiple toasts can be enabled or disabled by separating them with a comma.
-///
-/// A value of "false" will disable all toasts. A value of "true" will
-/// enable all toasts.
-///
-/// This configuration only applies to GTK with Adwaita enabled.
-@"adw-toast": AdwToast = .{},
-
/// If `true` (default), then the Ghostty GTK tabs will be "wide." Wide tabs
/// are the new typical Gnome style where tabs fill their available space.
/// If you set this to `false` then tabs will only take up space they need,
@@ -5745,8 +5747,8 @@ pub const AdwToolbarStyle = enum {
@"raised-border",
};
-/// See adw-toast
-pub const AdwToast = packed struct {
+/// See app-notifications
+pub const AppNotifications = packed struct {
@"clipboard-copy": bool = true,
};
From 168dd3136756836259b43a72bca9791827fb717b Mon Sep 17 00:00:00 2001
From: Anund
Date: Fri, 3 Jan 2025 23:53:22 +1100
Subject: [PATCH 155/365] documentation: consistent format for actions help
---
src/cli/action.zig | 6 +++---
src/cli/help.zig | 8 +++++---
src/cli/list_actions.zig | 4 +++-
src/cli/list_fonts.zig | 21 ++++++++++++++-------
src/cli/list_keybinds.zig | 12 ++++++++----
src/cli/list_themes.zig | 1 +
src/cli/validate_config.zig | 9 ++++++---
src/cli/version.zig | 3 ++-
src/shell-integration/README.md | 2 +-
9 files changed, 43 insertions(+), 23 deletions(-)
diff --git a/src/cli/action.zig b/src/cli/action.zig
index a84a40024..693d509fc 100644
--- a/src/cli/action.zig
+++ b/src/cli/action.zig
@@ -45,12 +45,12 @@ pub const Action = enum {
// Validate passed config file
@"validate-config",
- // List, (eventually) view, and (eventually) send crash reports.
- @"crash-report",
-
// Show which font face Ghostty loads a codepoint from.
@"show-face",
+ // List, (eventually) view, and (eventually) send crash reports.
+ @"crash-report",
+
pub const Error = error{
/// Multiple actions were detected. You can specify at most one
/// action on the CLI otherwise the behavior desired is ambiguous.
diff --git a/src/cli/help.zig b/src/cli/help.zig
index daadc37cc..22fe27d8d 100644
--- a/src/cli/help.zig
+++ b/src/cli/help.zig
@@ -15,9 +15,11 @@ pub const Options = struct {
}
};
-/// The `help` command shows general help about Ghostty. You can also specify
-/// `--help` or `-h` along with any action such as `+list-themes` to see help
-/// for a specific action.
+/// The `help` command shows general help about Ghostty. Recognized as either
+/// `-h, `--help`, or like other actions `+help`.
+///
+/// You can also specify `--help` or `-h` along with any action such as
+/// `+list-themes` to see help for a specific action.
pub fn run(alloc: Allocator) !u8 {
var opts: Options = .{};
defer opts.deinit();
diff --git a/src/cli/list_actions.zig b/src/cli/list_actions.zig
index 65b9dcdad..6f67a92d2 100644
--- a/src/cli/list_actions.zig
+++ b/src/cli/list_actions.zig
@@ -24,7 +24,9 @@ pub const Options = struct {
/// actions for Ghostty. These are distinct from the CLI Actions which can
/// be listed via `+help`
///
-/// The `--docs` argument will print out the documentation for each action.
+/// Flags:
+///
+/// * `--docs`: will print out the documentation for each action.
pub fn run(alloc: Allocator) !u8 {
var opts: Options = .{};
defer opts.deinit();
diff --git a/src/cli/list_fonts.zig b/src/cli/list_fonts.zig
index 9d1f34cd1..e8a010ecd 100644
--- a/src/cli/list_fonts.zig
+++ b/src/cli/list_fonts.zig
@@ -44,14 +44,21 @@ pub const Options = struct {
/// the sorting will be disabled and the results instead will be shown in the
/// same priority order Ghostty would use to pick a font.
///
-/// The `--family` argument can be used to filter results to a specific family.
-/// The family handling is identical to the `font-family` set of Ghostty
-/// configuration values, so this can be used to debug why your desired font may
-/// not be loading.
+/// Flags:
///
-/// The `--bold` and `--italic` arguments can be used to filter results to
-/// specific styles. It is not guaranteed that only those styles are returned,
-/// it will just prioritize fonts that match those styles.
+/// * `--bold`: Filter results to specific bold styles. It is not guaranteed
+/// that only those styles are returned. They are only prioritized.
+///
+/// * `--italic`: Filter results to specific italic styles. It is not guaranteed
+/// that only those styles are returned. They are only prioritized.
+///
+/// * `--style`: Filter results based on the style string advertised by a font.
+/// It is not guaranteed that only those styles are returned. They are only
+/// prioritized.
+///
+/// * `--family`: Filter results to a specific font family. The family handling
+/// is identical to the `font-family` set of Ghostty configuration values, so
+/// this can be used to debug why your desired font may not be loading.
pub fn run(alloc: Allocator) !u8 {
var iter = try args.argsIterator(alloc);
defer iter.deinit();
diff --git a/src/cli/list_keybinds.zig b/src/cli/list_keybinds.zig
index ddaf75177..13c69d970 100644
--- a/src/cli/list_keybinds.zig
+++ b/src/cli/list_keybinds.zig
@@ -42,11 +42,15 @@ pub const Options = struct {
/// changes to the keybinds it will print out the default ones configured for
/// Ghostty
///
-/// The `--default` argument will print out all the default keybinds configured
-/// for Ghostty
+/// Flags:
///
-/// The `--plain` flag will disable formatting and make the output more
-/// friendly for Unix tooling. This is default when not printing to a tty.
+/// * `--default`: will print out all the default keybinds
+///
+/// * `--docs`: currently does nothing, intended to print out documentation
+/// about the action associated with the keybinds
+///
+/// * `--plain`: will disable formatting and make the output more
+/// friendly for Unix tooling. This is default when not printing to a tty.
pub fn run(alloc: Allocator) !u8 {
var opts: Options = .{};
defer opts.deinit();
diff --git a/src/cli/list_themes.zig b/src/cli/list_themes.zig
index 22e22a972..f7ee10ce6 100644
--- a/src/cli/list_themes.zig
+++ b/src/cli/list_themes.zig
@@ -91,6 +91,7 @@ const ThemeListElement = struct {
/// Flags:
///
/// * `--path`: Show the full path to the theme.
+///
/// * `--plain`: Force a plain listing of themes.
pub fn run(gpa_alloc: std.mem.Allocator) !u8 {
var opts: Options = .{};
diff --git a/src/cli/validate_config.zig b/src/cli/validate_config.zig
index 1615ef66b..5bc6ff406 100644
--- a/src/cli/validate_config.zig
+++ b/src/cli/validate_config.zig
@@ -23,10 +23,13 @@ pub const Options = struct {
/// The `validate-config` command is used to validate a Ghostty config file.
///
-/// When executed without any arguments, this will load the config from the default location.
+/// When executed without any arguments, this will load the config from the default
+/// location.
///
-/// The `--config-file` argument can be passed to validate a specific target config
-/// file in a non-default location.
+/// Flags:
+///
+/// * `--config-file`: can be passed to validate a specific target config file in
+/// a non-default location
pub fn run(alloc: std.mem.Allocator) !u8 {
var opts: Options = .{};
defer opts.deinit();
diff --git a/src/cli/version.zig b/src/cli/version.zig
index b00152589..4a6af242c 100644
--- a/src/cli/version.zig
+++ b/src/cli/version.zig
@@ -10,7 +10,8 @@ const gtk = if (build_config.app_runtime == .gtk) @import("../apprt/gtk/c.zig").
pub const Options = struct {};
-/// The `version` command is used to display information about Ghostty.
+/// The `version` command is used to display information about Ghostty. Recognized as
+/// either `+version` or `--version`.
pub fn run(alloc: Allocator) !u8 {
_ = alloc;
diff --git a/src/shell-integration/README.md b/src/shell-integration/README.md
index 976cf4924..3d5159c71 100644
--- a/src/shell-integration/README.md
+++ b/src/shell-integration/README.md
@@ -6,7 +6,7 @@ supports.
This README is meant as developer documentation and not as
user documentation. For user documentation, see the main
-README.
+README or [ghostty.org](https://ghostty.org/docs)
## Implementation Details
From 098a46f0773c544b07e8d812247719893f1b6b6d Mon Sep 17 00:00:00 2001
From: Anund
Date: Wed, 8 Jan 2025 17:32:11 +1100
Subject: [PATCH 156/365] docs: generate mdx file for cli actions
---
src/build/Config.zig | 1 +
src/build/GhosttyWebdata.zig | 29 +++++++++++++++++
src/build/webgen/main_commands.zig | 51 ++++++++++++++++++++++++++++++
src/cli/README.md | 13 ++++++++
src/main.zig | 1 +
5 files changed, 95 insertions(+)
create mode 100644 src/build/webgen/main_commands.zig
create mode 100644 src/cli/README.md
diff --git a/src/build/Config.zig b/src/build/Config.zig
index 71dffce4a..b65a8d566 100644
--- a/src/build/Config.zig
+++ b/src/build/Config.zig
@@ -486,6 +486,7 @@ pub const ExeEntrypoint = enum {
mdgen_ghostty_5,
webgen_config,
webgen_actions,
+ webgen_commands,
bench_parser,
bench_stream,
bench_codepoint_width,
diff --git a/src/build/GhosttyWebdata.zig b/src/build/GhosttyWebdata.zig
index 6e0acaf17..860feb705 100644
--- a/src/build/GhosttyWebdata.zig
+++ b/src/build/GhosttyWebdata.zig
@@ -73,6 +73,35 @@ pub fn init(
).step);
}
+ {
+ const webgen_commands = b.addExecutable(.{
+ .name = "webgen_commands",
+ .root_source_file = b.path("src/main.zig"),
+ .target = b.host,
+ });
+ deps.help_strings.addImport(webgen_commands);
+
+ {
+ const buildconfig = config: {
+ var copy = deps.config.*;
+ copy.exe_entrypoint = .webgen_commands;
+ break :config copy;
+ };
+
+ const options = b.addOptions();
+ try buildconfig.addOptions(options);
+ webgen_commands.root_module.addOptions("build_options", options);
+ }
+
+ const webgen_commands_step = b.addRunArtifact(webgen_commands);
+ const webgen_commands_out = webgen_commands_step.captureStdOut();
+
+ try steps.append(&b.addInstallFile(
+ webgen_commands_out,
+ "share/ghostty/webdata/commands.mdx",
+ ).step);
+ }
+
return .{ .steps = steps.items };
}
diff --git a/src/build/webgen/main_commands.zig b/src/build/webgen/main_commands.zig
new file mode 100644
index 000000000..6e6b00c5e
--- /dev/null
+++ b/src/build/webgen/main_commands.zig
@@ -0,0 +1,51 @@
+const std = @import("std");
+const Action = @import("../../cli/action.zig").Action;
+const help_strings = @import("help_strings");
+
+pub fn main() !void {
+ const output = std.io.getStdOut().writer();
+ try genActions(output);
+}
+
+// Note: as a shortcut for defining inline editOnGithubLinks per cli action the user
+// is directed to the folder view on Github. This includes a README pointing them to
+// the files to edit.
+pub fn genActions(writer: anytype) !void {
+ // Write the header
+ try writer.writeAll(
+ \\---
+ \\title: Reference
+ \\description: Reference of all Ghostty action subcommands.
+ \\editOnGithubLink: https://github.com/ghostty-org/ghostty/tree/main/src/cli
+ \\---
+ \\Ghostty includes a number of utility actions that can be accessed as subcommands.
+ \\Actions provide utilities to work with config, list keybinds, list fonts, demo themes,
+ \\and debug.
+ \\
+ );
+
+ inline for (@typeInfo(Action).Enum.fields) |field| {
+ const action = std.meta.stringToEnum(Action, field.name).?;
+
+ switch (action) {
+ .help, .version => try writer.writeAll("## " ++ field.name ++ "\n"),
+ else => try writer.writeAll("## " ++ field.name ++ "\n"),
+ }
+
+ if (@hasDecl(help_strings.Action, field.name)) {
+ var iter = std.mem.splitScalar(u8, @field(help_strings.Action, field.name), '\n');
+ var first = true;
+ while (iter.next()) |s| {
+ try writer.writeAll(s);
+ try writer.writeAll("\n");
+ first = false;
+ }
+ try writer.writeAll("\n```\n");
+ switch (action) {
+ .help, .version => try writer.writeAll("ghostty --" ++ field.name ++ "\n"),
+ else => try writer.writeAll("ghostty +" ++ field.name ++ "\n"),
+ }
+ try writer.writeAll("```\n\n");
+ }
+ }
+}
diff --git a/src/cli/README.md b/src/cli/README.md
new file mode 100644
index 000000000..7a1d99409
--- /dev/null
+++ b/src/cli/README.md
@@ -0,0 +1,13 @@
+# Subcommand Actions
+
+This is the cli specific code. It contains cli actions and tui definitions and
+argument parsing.
+
+This README is meant as developer documentation and not as user documentation.
+For user documentation, see the main README or [ghostty.org](https://ghostty.org/docs).
+
+## Updating documentation
+
+Each cli action is defined in it's own file. Documentation for each action is defined
+in the doc comment associated with the `run` function. For example the `run` function
+in `list_keybinds.zig` contains the help text for `ghostty +list-keybinds`.
diff --git a/src/main.zig b/src/main.zig
index ecf38fbb3..121a3b7d2 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -9,6 +9,7 @@ const entrypoint = switch (build_config.exe_entrypoint) {
.mdgen_ghostty_5 => @import("build/mdgen/main_ghostty_5.zig"),
.webgen_config => @import("build/webgen/main_config.zig"),
.webgen_actions => @import("build/webgen/main_actions.zig"),
+ .webgen_commands => @import("build/webgen/main_commands.zig"),
.bench_parser => @import("bench/parser.zig"),
.bench_stream => @import("bench/stream.zig"),
.bench_codepoint_width => @import("bench/codepoint-width.zig"),
From 78790f6ef75c95929ad8dcb56b79efe91be55c64 Mon Sep 17 00:00:00 2001
From: Qwerasd
Date: Thu, 23 Jan 2025 20:06:53 -0500
Subject: [PATCH 157/365] fix(Metal): always render explicit background colors
fully opaque
This fixes a regression introduced by the rework of this area before
during the color space changes. It seems like the original intent of
this code was the behavior it regressed to, but it turns out to be
better like this.
---
src/renderer/Metal.zig | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig
index bf28b58ac..52a5437c6 100644
--- a/src/renderer/Metal.zig
+++ b/src/renderer/Metal.zig
@@ -2672,9 +2672,8 @@ fn rebuildCells(
// Cells that are reversed should be fully opaque.
if (style.flags.inverse) break :bg_alpha default;
- // Cells that have an explicit bg color, which does not
- // match the current surface bg, should be fully opaque.
- if (bg != null and !rgb.eql(self.background_color orelse self.default_background_color)) {
+ // Cells that have an explicit bg color should be fully opaque.
+ if (bg_style != null) {
break :bg_alpha default;
}
From c0eb6985ee6adaeb031751fddb0d0448533d68c6 Mon Sep 17 00:00:00 2001
From: Mitchell Hashimoto
Date: Thu, 23 Jan 2025 19:38:13 -0800
Subject: [PATCH 158/365] Revert "build: options to enable/disable terminfo &
termcap install"
This reverts commit 8f49a227b7c352083b0815e7818db900402513e0.
---
src/build/Config.zig | 25 +-------------------
src/build/GhosttyResources.zig | 42 +++++++++++++++++-----------------
src/os/resourcesdir.zig | 5 +---
3 files changed, 23 insertions(+), 49 deletions(-)
diff --git a/src/build/Config.zig b/src/build/Config.zig
index 8b28a6a04..b65a8d566 100644
--- a/src/build/Config.zig
+++ b/src/build/Config.zig
@@ -55,8 +55,6 @@ emit_helpgen: bool = false,
emit_docs: bool = false,
emit_webdata: bool = false,
emit_xcframework: bool = false,
-emit_terminfo: bool = false,
-emit_termcap: bool = false,
/// Environmental properties
env: std.process.EnvMap,
@@ -308,32 +306,11 @@ pub fn init(b: *std.Build) !Config {
break :emit_docs path != null;
};
- config.emit_terminfo = b.option(
- bool,
- "emit-terminfo",
- "Install Ghostty terminfo source file",
- ) orelse switch (target.result.os.tag) {
- .windows => true,
- else => switch (optimize) {
- .Debug => true,
- .ReleaseSafe, .ReleaseFast, .ReleaseSmall => false,
- },
- };
-
- config.emit_termcap = b.option(
- bool,
- "emit-termcap",
- "Install Ghostty termcap file",
- ) orelse false;
-
config.emit_webdata = b.option(
bool,
"emit-webdata",
"Build the website data for the website.",
- ) orelse switch (optimize) {
- .Debug => true,
- .ReleaseSafe, .ReleaseFast, .ReleaseSmall => false,
- };
+ ) orelse false;
config.emit_xcframework = b.option(
bool,
diff --git a/src/build/GhosttyResources.zig b/src/build/GhosttyResources.zig
index c0830e5f6..cae907ec2 100644
--- a/src/build/GhosttyResources.zig
+++ b/src/build/GhosttyResources.zig
@@ -23,12 +23,9 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources {
// Write it
var wf = b.addWriteFiles();
- const source = wf.add("ghostty.terminfo", str.items);
-
- if (cfg.emit_terminfo) {
- const source_install = b.addInstallFile(source, "share/terminfo/ghostty.terminfo");
- try steps.append(&source_install.step);
- }
+ const src_source = wf.add("share/terminfo/ghostty.terminfo", str.items);
+ const src_install = b.addInstallFile(src_source, "share/terminfo/ghostty.terminfo");
+ try steps.append(&src_install.step);
// Windows doesn't have the binaries below.
if (cfg.target.result.os.tag == .windows) break :terminfo;
@@ -36,10 +33,10 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources {
// Convert to termcap source format if thats helpful to people and
// install it. The resulting value here is the termcap source in case
// that is used for other commands.
- if (cfg.emit_termcap) {
+ {
const run_step = RunStep.create(b, "infotocap");
- run_step.addArgs(&.{ "infotocap", "-" });
- run_step.setStdIn(.{ .lazy_path = source });
+ run_step.addArg("infotocap");
+ run_step.addFileArg(src_source);
const out_source = run_step.captureStdOut();
_ = run_step.captureStdErr(); // so we don't see stderr
@@ -51,21 +48,24 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources {
{
const run_step = RunStep.create(b, "tic");
run_step.addArgs(&.{ "tic", "-x", "-o" });
- const path = run_step.addOutputDirectoryArg("share/terminfo");
- run_step.addArg("-");
- run_step.setStdIn(.{ .lazy_path = source });
+ const path = run_step.addOutputFileArg("terminfo");
+ run_step.addFileArg(src_source);
_ = run_step.captureStdErr(); // so we don't see stderr
- try steps.append(&run_step.step);
+ // Depend on the terminfo source install step so that Zig build
+ // creates the "share" directory for us.
+ run_step.step.dependOn(&src_install.step);
- // Use cp -R instead of Step.InstallDir because we need to preserve
- // symlinks in the terminfo database. Zig's InstallDir step doesn't
- // handle symlinks correctly yet.
- const copy_step = RunStep.create(b, "copy terminfo db");
- copy_step.addArgs(&.{ "cp", "-R" });
- copy_step.addFileArg(path);
- copy_step.addArg(b.fmt("{s}/share", .{b.install_path}));
- try steps.append(©_step.step);
+ {
+ // Use cp -R instead of Step.InstallDir because we need to preserve
+ // symlinks in the terminfo database. Zig's InstallDir step doesn't
+ // handle symlinks correctly yet.
+ const copy_step = RunStep.create(b, "copy terminfo db");
+ copy_step.addArgs(&.{ "cp", "-R" });
+ copy_step.addFileArg(path);
+ copy_step.addArg(b.fmt("{s}/share", .{b.install_path}));
+ try steps.append(©_step.step);
+ }
}
}
diff --git a/src/os/resourcesdir.zig b/src/os/resourcesdir.zig
index d2b274e87..c0f82dec5 100644
--- a/src/os/resourcesdir.zig
+++ b/src/os/resourcesdir.zig
@@ -21,10 +21,7 @@ pub fn resourcesDir(alloc: std.mem.Allocator) !?[]const u8 {
// This is the sentinel value we look for in the path to know
// we've found the resources directory.
- const sentinel = switch (comptime builtin.target.os.tag) {
- .windows => "terminfo/ghostty.terminfo",
- else => "terminfo/x/xterm-ghostty",
- };
+ const sentinel = "terminfo/ghostty.termcap";
// Get the path to our running binary
var exe_buf: [std.fs.max_path_bytes]u8 = undefined;
From 0d6a1d3fdb93ee5444a2f998e085266ad443442a Mon Sep 17 00:00:00 2001
From: Mitchell Hashimoto
Date: Thu, 23 Jan 2025 21:22:06 -0800
Subject: [PATCH 159/365] Prevent fd leaks to the running shell or command
Multiple fixes to prevent file descriptor leaks:
- libxev eventfd now uses CLOEXEC
- linux: cgroup clone now uses CLOEXEC for the cgroup fd
- termio pipe uses pipe2 with CLOEXEC
- pty master always sets CLOEXEC because the child doesn't need it
- termio exec now closes pty slave fd after fork
There still appear to be some fd leaks happening. They seem related to
GTK, they aren't things we're accessig directly. I still want to
investigate them but this at least cleans up the major sources of fd
leakage.
---
build.zig.zon | 4 ++--
nix/zigCacheHash.nix | 2 +-
src/os/cgroup.zig | 17 ++++++++++++++++-
src/os/pipe.zig | 5 +++--
src/pty.zig | 26 ++++++++++++++++++++++----
src/termio/Exec.zig | 11 +++++++++++
6 files changed, 55 insertions(+), 10 deletions(-)
diff --git a/build.zig.zon b/build.zig.zon
index 09dc9847e..9c00a4704 100644
--- a/build.zig.zon
+++ b/build.zig.zon
@@ -5,8 +5,8 @@
.dependencies = .{
// Zig libs
.libxev = .{
- .url = "https://github.com/mitchellh/libxev/archive/db6a52bafadf00360e675fefa7926e8e6c0e9931.tar.gz",
- .hash = "12206029de146b685739f69b10a6f08baee86b3d0a5f9a659fa2b2b66c9602078bbf",
+ .url = "https://github.com/mitchellh/libxev/archive/aceef3d11efacd9d237c91632f930ed13a2834bf.tar.gz",
+ .hash = "12205b2b47fe61a4cde3a45ee4b9cddee75897739dbc196d6396e117cb1ce28e1ad0",
},
.mach_glfw = .{
.url = "https://github.com/mitchellh/mach-glfw/archive/37c2995f31abcf7e8378fba68ddcf4a3faa02de0.tar.gz",
diff --git a/nix/zigCacheHash.nix b/nix/zigCacheHash.nix
index dfc2e5f7f..c687a5a79 100644
--- a/nix/zigCacheHash.nix
+++ b/nix/zigCacheHash.nix
@@ -1,3 +1,3 @@
# This file is auto-generated! check build-support/check-zig-cache-hash.sh for
# more details.
-"sha256-H6o4Y09ATIylMUWuL9Y1fHwpuxSWyJ3Pl8fn4VeoDZo="
+"sha256-AvfYl8vLxxsRnf/ERpw5jQIro5rVd98q63hwFsgQOvo="
diff --git a/src/os/cgroup.zig b/src/os/cgroup.zig
index 0a66c5987..bef101acc 100644
--- a/src/os/cgroup.zig
+++ b/src/os/cgroup.zig
@@ -77,7 +77,22 @@ pub fn cloneInto(cgroup: []const u8) !posix.pid_t {
// Get a file descriptor that refers to the cgroup directory in the cgroup
// sysfs to pass to the kernel in clone3.
const fd: linux.fd_t = fd: {
- const rc = linux.open(path, linux.O{ .PATH = true, .DIRECTORY = true }, 0);
+ const rc = linux.open(
+ path,
+ .{
+ // Self-explanatory: we expect to open a directory, and
+ // we only need the path-level permissions.
+ .PATH = true,
+ .DIRECTORY = true,
+
+ // We don't want to leak this fd to the child process
+ // when we clone below since we're using this fd for
+ // a cgroup clone.
+ .CLOEXEC = true,
+ },
+ 0,
+ );
+
switch (posix.errno(rc)) {
.SUCCESS => break :fd @as(linux.fd_t, @intCast(rc)),
else => |errno| {
diff --git a/src/os/pipe.zig b/src/os/pipe.zig
index 392f72083..2cb7bd4a3 100644
--- a/src/os/pipe.zig
+++ b/src/os/pipe.zig
@@ -3,10 +3,11 @@ const builtin = @import("builtin");
const windows = @import("windows.zig");
const posix = std.posix;
-/// pipe() that works on Windows and POSIX.
+/// pipe() that works on Windows and POSIX. For POSIX systems, this sets
+/// CLOEXEC on the file descriptors.
pub fn pipe() ![2]posix.fd_t {
switch (builtin.os.tag) {
- else => return try posix.pipe(),
+ else => return try posix.pipe2(.{ .CLOEXEC = true }),
.windows => {
var read: windows.HANDLE = undefined;
var write: windows.HANDLE = undefined;
diff --git a/src/pty.zig b/src/pty.zig
index c0d082411..1df09d79c 100644
--- a/src/pty.zig
+++ b/src/pty.zig
@@ -94,6 +94,9 @@ const PosixPty = struct {
};
/// The file descriptors for the master and slave side of the pty.
+ /// The slave side is never closed automatically by this struct
+ /// so the caller is responsible for closing it if things
+ /// go wrong.
master: Fd,
slave: Fd,
@@ -117,6 +120,24 @@ const PosixPty = struct {
_ = posix.system.close(slave_fd);
}
+ // Set CLOEXEC on the master fd, only the slave fd should be inherited
+ // by the child process (shell/command).
+ cloexec: {
+ const flags = std.posix.fcntl(master_fd, std.posix.F.GETFD, 0) catch |err| {
+ log.warn("error getting flags for master fd err={}", .{err});
+ break :cloexec;
+ };
+
+ _ = std.posix.fcntl(
+ master_fd,
+ std.posix.F.SETFD,
+ flags | std.posix.FD_CLOEXEC,
+ ) catch |err| {
+ log.warn("error setting CLOEXEC on master fd err={}", .{err});
+ break :cloexec;
+ };
+ }
+
// Enable UTF-8 mode. I think this is on by default on Linux but it
// is NOT on by default on macOS so we ensure that it is always set.
var attrs: c.termios = undefined;
@@ -126,7 +147,7 @@ const PosixPty = struct {
if (c.tcsetattr(master_fd, c.TCSANOW, &attrs) != 0)
return error.OpenptyFailed;
- return Pty{
+ return .{
.master = master_fd,
.slave = slave_fd,
};
@@ -134,7 +155,6 @@ const PosixPty = struct {
pub fn deinit(self: *Pty) void {
_ = posix.system.close(self.master);
- _ = posix.system.close(self.slave);
self.* = undefined;
}
@@ -201,8 +221,6 @@ const PosixPty = struct {
// Can close master/slave pair now
posix.close(self.slave);
posix.close(self.master);
-
- // TODO: reset signals
}
};
diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig
index e320152ec..c55e66729 100644
--- a/src/termio/Exec.zig
+++ b/src/termio/Exec.zig
@@ -1098,6 +1098,10 @@ const Subprocess = struct {
});
self.pty = pty;
errdefer {
+ if (comptime builtin.os.tag != .windows) {
+ _ = posix.close(pty.slave);
+ }
+
pty.deinit();
self.pty = null;
}
@@ -1182,6 +1186,13 @@ const Subprocess = struct {
log.info("subcommand cgroup={s}", .{self.linux_cgroup orelse "-"});
}
+ if (comptime builtin.os.tag != .windows) {
+ // Once our subcommand is started we can close the slave
+ // side. This prevents the slave fd from being leaked to
+ // future children.
+ _ = posix.close(pty.slave);
+ }
+
self.command = cmd;
return switch (builtin.os.tag) {
.windows => .{
From 076bcccde4a3ca55cf44b5e9850bd117cfcdccf5 Mon Sep 17 00:00:00 2001
From: Erlend Lind Madsen
Date: Fri, 24 Jan 2025 14:55:33 +0700
Subject: [PATCH 160/365] Docs: improve doc structure for action bindings with
args and examples
minor doc changes
---
src/input/Binding.zig | 48 ++++++++++++++++++++++++++++++++++---------
1 file changed, 38 insertions(+), 10 deletions(-)
diff --git a/src/input/Binding.zig b/src/input/Binding.zig
index 33ed80c8b..0b2ee05de 100644
--- a/src/input/Binding.zig
+++ b/src/input/Binding.zig
@@ -280,8 +280,15 @@ pub const Action = union(enum) {
scroll_page_fractional: f32,
scroll_page_lines: i16,
- /// Adjust an existing selection in a given direction. This action
- /// does nothing if there is no active selection.
+ /// Adjust the current selection in a given direction. Does nothing if no
+ /// selection exists.
+ ///
+ /// Arguments:
+ /// - AdjustSelection: left, right, up, down, page_up, page_down, home, end,
+ /// beginning_of_line, end_of_line
+ ///
+ /// Example: Extend selection to the right
+ /// keybind = shift+right=adjust_selection:right
adjust_selection: AdjustSelection,
/// Jump the viewport forward or back by prompt. Positive number is the
@@ -337,25 +344,47 @@ pub const Action = union(enum) {
/// This only works with libadwaita enabled currently.
toggle_tab_overview: void,
- /// Create a new split in the given direction. The new split will appear in
- /// the direction given. For example `new_split:up`. Valid values are left, right, up, down and auto.
+ /// Create a new split in the given direction.
+ ///
+ /// Arguments:
+ /// - SplitDirection: right, down, left, up, auto (along the larger dir)
+ ///
+ /// Example: Create split on the right
+ /// keybind = cmd+shift+d=new_split:right
new_split: SplitDirection,
- /// Focus on a split in a given direction. For example `goto_split:up`. Valid values are left, right, up, down, previous and next.
+ /// Focus a split in a given direction.
+ ///
+ /// Arguments:
+ /// - SplitFocusDirection: previous, next, up, left, down, right
+ ///
+ /// Example: Focus split on the right
+ /// keybind = cmd+right=goto_split:right
goto_split: SplitFocusDirection,
/// zoom/unzoom the current split.
toggle_split_zoom: void,
- /// Resize the current split by moving the split divider in the given
- /// direction. For example `resize_split:left,10`. The valid directions are up, down, left and right.
+ /// Resize the current split in a given direction.
+ ///
+ /// Arguments:
+ /// - SplitResizeDirection: up, down, left, right
+ /// - The number of pixels to resize the split by
+ ///
+ /// Example: Move divider up 10 pixels
+ /// keybind = cmd+shift+up=resize_split:up,10
resize_split: SplitResizeParameter,
/// Equalize all splits in the current window
equalize_splits: void,
- /// Show, hide, or toggle the terminal inspector for the currently focused
- /// terminal.
+ /// Control the terminal inspector visibility.
+ ///
+ /// Arguments:
+ /// - InspectorMode: toggle, show, hide
+ ///
+ /// Example: Toggle inspector visibility
+ /// keybind = cmd+i=inspector:toggle
inspector: InspectorMode,
/// Open the configuration file in the default OS editor. If your default OS
@@ -504,7 +533,6 @@ pub const Action = union(enum) {
pub const SplitFocusDirection = enum {
previous,
next,
-
up,
left,
down,
From c4c2d065711cdb4012002bec5d94109eb4942828 Mon Sep 17 00:00:00 2001
From: Erlend Lind Madsen
Date: Fri, 24 Jan 2025 14:59:46 +0700
Subject: [PATCH 161/365] fmt
---
src/apprt/gtk/App.zig | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig
index 12bac989a..b36c5b96a 100644
--- a/src/apprt/gtk/App.zig
+++ b/src/apprt/gtk/App.zig
@@ -1075,9 +1075,7 @@ fn loadCustomCss(self: *App) !void {
defer file.close();
log.info("loading gtk-custom-css path={s}", .{path});
- const contents = try file.reader().readAllAlloc(
- self.core_app.alloc,
- 5 * 1024 * 1024 // 5MB
+ const contents = try file.reader().readAllAlloc(self.core_app.alloc, 5 * 1024 * 1024 // 5MB
);
defer self.core_app.alloc.free(contents);
From 2f8b0dc899dd197f1e018ccedcfadbe6d82994ad Mon Sep 17 00:00:00 2001
From: "Jeffrey C. Ollie"
Date: Thu, 23 Jan 2025 22:29:47 -0600
Subject: [PATCH 162/365] build: options to enable/disable terminfo & termcap
install (take 2)
Fixes #5253
Add -Demit-terminfo and -Demit-termcap build options to enable/disable
installation of source terminfo and termcap files.
---
src/build/Config.zig | 23 +++++++++++++++++
src/build/GhosttyResources.zig | 47 ++++++++++++++++++++--------------
src/os/resourcesdir.zig | 6 ++++-
3 files changed, 56 insertions(+), 20 deletions(-)
diff --git a/src/build/Config.zig b/src/build/Config.zig
index b65a8d566..c6f0e6d09 100644
--- a/src/build/Config.zig
+++ b/src/build/Config.zig
@@ -55,6 +55,8 @@ emit_helpgen: bool = false,
emit_docs: bool = false,
emit_webdata: bool = false,
emit_xcframework: bool = false,
+emit_terminfo: bool = false,
+emit_termcap: bool = false,
/// Environmental properties
env: std.process.EnvMap,
@@ -306,6 +308,27 @@ pub fn init(b: *std.Build) !Config {
break :emit_docs path != null;
};
+ config.emit_terminfo = b.option(
+ bool,
+ "emit-terminfo",
+ "Install Ghostty terminfo source file",
+ ) orelse switch (target.result.os.tag) {
+ .windows => true,
+ else => switch (optimize) {
+ .Debug => true,
+ .ReleaseSafe, .ReleaseFast, .ReleaseSmall => false,
+ },
+ };
+
+ config.emit_termcap = b.option(
+ bool,
+ "emit-termcap",
+ "Install Ghostty termcap file",
+ ) orelse switch (optimize) {
+ .Debug => true,
+ .ReleaseSafe, .ReleaseFast, .ReleaseSmall => false,
+ };
+
config.emit_webdata = b.option(
bool,
"emit-webdata",
diff --git a/src/build/GhosttyResources.zig b/src/build/GhosttyResources.zig
index cae907ec2..2fdfbe81d 100644
--- a/src/build/GhosttyResources.zig
+++ b/src/build/GhosttyResources.zig
@@ -16,6 +16,15 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources {
// Terminfo
terminfo: {
+ const mkdir_step = RunStep.create(b, "make share/terminfo directory");
+ switch (cfg.target.result.os.tag) {
+ // windows mkdir shouldn't need "-p"
+ .windows => mkdir_step.addArgs(&.{"mkdir"}),
+ else => mkdir_step.addArgs(&.{ "mkdir", "-p" }),
+ }
+ mkdir_step.addArg(b.fmt("{s}/share/terminfo", .{b.install_path}));
+ try steps.append(&mkdir_step.step);
+
// Encode our terminfo
var str = std.ArrayList(u8).init(b.allocator);
defer str.deinit();
@@ -23,9 +32,13 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources {
// Write it
var wf = b.addWriteFiles();
- const src_source = wf.add("share/terminfo/ghostty.terminfo", str.items);
- const src_install = b.addInstallFile(src_source, "share/terminfo/ghostty.terminfo");
- try steps.append(&src_install.step);
+ const source = wf.add("ghostty.terminfo", str.items);
+
+ if (cfg.emit_terminfo) {
+ const source_install = b.addInstallFile(source, "share/terminfo/ghostty.terminfo");
+ source_install.step.dependOn(&mkdir_step.step);
+ try steps.append(&source_install.step);
+ }
// Windows doesn't have the binaries below.
if (cfg.target.result.os.tag == .windows) break :terminfo;
@@ -36,11 +49,12 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources {
{
const run_step = RunStep.create(b, "infotocap");
run_step.addArg("infotocap");
- run_step.addFileArg(src_source);
+ run_step.addFileArg(source);
const out_source = run_step.captureStdOut();
_ = run_step.captureStdErr(); // so we don't see stderr
const cap_install = b.addInstallFile(out_source, "share/terminfo/ghostty.termcap");
+ cap_install.step.dependOn(&mkdir_step.step);
try steps.append(&cap_install.step);
}
@@ -49,23 +63,18 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources {
const run_step = RunStep.create(b, "tic");
run_step.addArgs(&.{ "tic", "-x", "-o" });
const path = run_step.addOutputFileArg("terminfo");
- run_step.addFileArg(src_source);
+ run_step.addFileArg(source);
_ = run_step.captureStdErr(); // so we don't see stderr
- // Depend on the terminfo source install step so that Zig build
- // creates the "share" directory for us.
- run_step.step.dependOn(&src_install.step);
-
- {
- // Use cp -R instead of Step.InstallDir because we need to preserve
- // symlinks in the terminfo database. Zig's InstallDir step doesn't
- // handle symlinks correctly yet.
- const copy_step = RunStep.create(b, "copy terminfo db");
- copy_step.addArgs(&.{ "cp", "-R" });
- copy_step.addFileArg(path);
- copy_step.addArg(b.fmt("{s}/share", .{b.install_path}));
- try steps.append(©_step.step);
- }
+ // Use cp -R instead of Step.InstallDir because we need to preserve
+ // symlinks in the terminfo database. Zig's InstallDir step doesn't
+ // handle symlinks correctly yet.
+ const copy_step = RunStep.create(b, "copy terminfo db");
+ copy_step.addArgs(&.{ "cp", "-R" });
+ copy_step.addFileArg(path);
+ copy_step.addArg(b.fmt("{s}/share", .{b.install_path}));
+ copy_step.step.dependOn(&mkdir_step.step);
+ try steps.append(©_step.step);
}
}
diff --git a/src/os/resourcesdir.zig b/src/os/resourcesdir.zig
index c0f82dec5..4ef256c1a 100644
--- a/src/os/resourcesdir.zig
+++ b/src/os/resourcesdir.zig
@@ -21,7 +21,11 @@ pub fn resourcesDir(alloc: std.mem.Allocator) !?[]const u8 {
// This is the sentinel value we look for in the path to know
// we've found the resources directory.
- const sentinel = "terminfo/ghostty.termcap";
+ const sentinel = switch (comptime builtin.target.os.tag) {
+ .windows => "terminfo/ghostty.terminfo",
+ .macos => "terminfo/78/xterm-ghostty",
+ else => "terminfo/x/xterm-ghostty",
+ };
// Get the path to our running binary
var exe_buf: [std.fs.max_path_bytes]u8 = undefined;
From d1969f74acee0b8d34ebc0b4fba4de7dd4494618 Mon Sep 17 00:00:00 2001
From: "Jeffrey C. Ollie"
Date: Fri, 24 Jan 2025 10:05:56 -0600
Subject: [PATCH 163/365] only the cp step needs to depend on the mkdir step
---
src/build/GhosttyResources.zig | 22 +++++++++++-----------
1 file changed, 11 insertions(+), 11 deletions(-)
diff --git a/src/build/GhosttyResources.zig b/src/build/GhosttyResources.zig
index 2fdfbe81d..1ce3fd66c 100644
--- a/src/build/GhosttyResources.zig
+++ b/src/build/GhosttyResources.zig
@@ -16,15 +16,6 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources {
// Terminfo
terminfo: {
- const mkdir_step = RunStep.create(b, "make share/terminfo directory");
- switch (cfg.target.result.os.tag) {
- // windows mkdir shouldn't need "-p"
- .windows => mkdir_step.addArgs(&.{"mkdir"}),
- else => mkdir_step.addArgs(&.{ "mkdir", "-p" }),
- }
- mkdir_step.addArg(b.fmt("{s}/share/terminfo", .{b.install_path}));
- try steps.append(&mkdir_step.step);
-
// Encode our terminfo
var str = std.ArrayList(u8).init(b.allocator);
defer str.deinit();
@@ -36,7 +27,6 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources {
if (cfg.emit_terminfo) {
const source_install = b.addInstallFile(source, "share/terminfo/ghostty.terminfo");
- source_install.step.dependOn(&mkdir_step.step);
try steps.append(&source_install.step);
}
@@ -54,7 +44,6 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources {
_ = run_step.captureStdErr(); // so we don't see stderr
const cap_install = b.addInstallFile(out_source, "share/terminfo/ghostty.termcap");
- cap_install.step.dependOn(&mkdir_step.step);
try steps.append(&cap_install.step);
}
@@ -66,6 +55,17 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources {
run_step.addFileArg(source);
_ = run_step.captureStdErr(); // so we don't see stderr
+ // Ensure that `share/terminfo` is a directory, otherwise the `cp
+ // -R` will create a file named `share/terminfo`
+ const mkdir_step = RunStep.create(b, "make share/terminfo directory");
+ switch (cfg.target.result.os.tag) {
+ // windows mkdir shouldn't need "-p"
+ .windows => mkdir_step.addArgs(&.{"mkdir"}),
+ else => mkdir_step.addArgs(&.{ "mkdir", "-p" }),
+ }
+ mkdir_step.addArg(b.fmt("{s}/share/terminfo", .{b.install_path}));
+ try steps.append(&mkdir_step.step);
+
// Use cp -R instead of Step.InstallDir because we need to preserve
// symlinks in the terminfo database. Zig's InstallDir step doesn't
// handle symlinks correctly yet.
From 593d70a42f8a7d0c87136a7f222eb45ef2821c37 Mon Sep 17 00:00:00 2001
From: "Jeffrey C. Ollie"
Date: Fri, 24 Jan 2025 10:06:32 -0600
Subject: [PATCH 164/365] fix missing check of emit_termcap build option
---
src/build/GhosttyResources.zig | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/build/GhosttyResources.zig b/src/build/GhosttyResources.zig
index 1ce3fd66c..a7ff40cbd 100644
--- a/src/build/GhosttyResources.zig
+++ b/src/build/GhosttyResources.zig
@@ -36,7 +36,7 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources {
// Convert to termcap source format if thats helpful to people and
// install it. The resulting value here is the termcap source in case
// that is used for other commands.
- {
+ if (cfg.emit_termcap) {
const run_step = RunStep.create(b, "infotocap");
run_step.addArg("infotocap");
run_step.addFileArg(source);
From 0c5ef5578c1f0aa33ed8ecc7fdc11d207024021a Mon Sep 17 00:00:00 2001
From: Erlend Lind Madsen
Date: Fri, 24 Jan 2025 23:13:42 +0700
Subject: [PATCH 165/365] Docs: remove type from action arguments
---
src/input/Binding.zig | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/src/input/Binding.zig b/src/input/Binding.zig
index 0b2ee05de..a655effb5 100644
--- a/src/input/Binding.zig
+++ b/src/input/Binding.zig
@@ -284,7 +284,7 @@ pub const Action = union(enum) {
/// selection exists.
///
/// Arguments:
- /// - AdjustSelection: left, right, up, down, page_up, page_down, home, end,
+ /// - left, right, up, down, page_up, page_down, home, end,
/// beginning_of_line, end_of_line
///
/// Example: Extend selection to the right
@@ -347,7 +347,7 @@ pub const Action = union(enum) {
/// Create a new split in the given direction.
///
/// Arguments:
- /// - SplitDirection: right, down, left, up, auto (along the larger dir)
+ /// - right, down, left, up, auto (splits along the larger direction)
///
/// Example: Create split on the right
/// keybind = cmd+shift+d=new_split:right
@@ -356,7 +356,7 @@ pub const Action = union(enum) {
/// Focus a split in a given direction.
///
/// Arguments:
- /// - SplitFocusDirection: previous, next, up, left, down, right
+ /// - previous, next, up, left, down, right
///
/// Example: Focus split on the right
/// keybind = cmd+right=goto_split:right
@@ -368,8 +368,8 @@ pub const Action = union(enum) {
/// Resize the current split in a given direction.
///
/// Arguments:
- /// - SplitResizeDirection: up, down, left, right
- /// - The number of pixels to resize the split by
+ /// - up, down, left, right
+ /// - the number of pixels to resize the split by
///
/// Example: Move divider up 10 pixels
/// keybind = cmd+shift+up=resize_split:up,10
@@ -381,7 +381,7 @@ pub const Action = union(enum) {
/// Control the terminal inspector visibility.
///
/// Arguments:
- /// - InspectorMode: toggle, show, hide
+ /// - toggle, show, hide
///
/// Example: Toggle inspector visibility
/// keybind = cmd+i=inspector:toggle
From 8475768ad1e6ada5b8c96ed02a661580ad8166de Mon Sep 17 00:00:00 2001
From: Mitchell Hashimoto
Date: Fri, 24 Jan 2025 09:38:01 -0800
Subject: [PATCH 166/365] termio/exec: if pty fd HUP, end read thread
Fixes #4884
When our command exits, it will close the pty slave fd. This will
trigger a HUP on our poll. Previously, we only checked for IN. When a fd
is closed, IN triggers forever which would leave to an infinite loop and
100% CPU.
Now, detect the HUP and exit the read thread.
---
src/termio/Exec.zig | 20 ++++++++++++++++++--
1 file changed, 18 insertions(+), 2 deletions(-)
diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig
index c55e66729..4428b16e1 100644
--- a/src/termio/Exec.zig
+++ b/src/termio/Exec.zig
@@ -179,8 +179,17 @@ pub fn threadExit(self: *Exec, td: *termio.Termio.ThreadData) void {
// Quit our read thread after exiting the subprocess so that
// we don't get stuck waiting for data to stop flowing if it is
// a particularly noisy process.
- _ = posix.write(exec.read_thread_pipe, "x") catch |err|
- log.warn("error writing to read thread quit pipe err={}", .{err});
+ _ = posix.write(exec.read_thread_pipe, "x") catch |err| switch (err) {
+ // BrokenPipe means that our read thread is closed already,
+ // which is completely fine since that is what we were trying
+ // to achieve.
+ error.BrokenPipe => {},
+
+ else => log.warn(
+ "error writing to read thread quit pipe err={}",
+ .{err},
+ ),
+ };
if (comptime builtin.os.tag == .windows) {
// Interrupt the blocking read so the thread can see the quit message
@@ -1467,6 +1476,13 @@ pub const ReadThread = struct {
log.info("read thread got quit signal", .{});
return;
}
+
+ // If our pty fd is closed, then we're also done with our
+ // read thread.
+ if (pollfds[0].revents & posix.POLL.HUP != 0) {
+ log.info("pty fd closed, read thread exiting", .{});
+ return;
+ }
}
}
From 5ad2ec8f71e743c1bfc8ce3b244dc0bdeacee4f3 Mon Sep 17 00:00:00 2001
From: Daniel Patterson
Date: Fri, 24 Jan 2025 17:51:36 +0000
Subject: [PATCH 167/365] Add chorded/sequenced keybinds to +list-keybinds
output
---
src/cli/list_keybinds.zig | 253 ++++++++++++++++++++++++++++++--------
1 file changed, 201 insertions(+), 52 deletions(-)
diff --git a/src/cli/list_keybinds.zig b/src/cli/list_keybinds.zig
index 13c69d970..6cd989201 100644
--- a/src/cli/list_keybinds.zig
+++ b/src/cli/list_keybinds.zig
@@ -68,7 +68,9 @@ pub fn run(alloc: Allocator) !u8 {
// Despite being under the posix namespace, this also works on Windows as of zig 0.13.0
if (tui.can_pretty_print and !opts.plain and std.posix.isatty(stdout.handle)) {
- return prettyPrint(alloc, config.keybind);
+ var arena = std.heap.ArenaAllocator.init(alloc);
+ defer arena.deinit();
+ return prettyPrint(arena.allocator(), config.keybind);
} else {
try config.keybind.formatEntryDocs(
configpkg.entryFormatter("keybind", stdout.writer()),
@@ -79,6 +81,111 @@ pub fn run(alloc: Allocator) !u8 {
return 0;
}
+const TriggerList = std.SinglyLinkedList(Binding.Trigger);
+
+const ChordBinding = struct {
+ triggers: TriggerList,
+ action: Binding.Action,
+
+ // Order keybinds based on various properties
+ // 1. Longest chord sequence
+ // 2. Most active modifiers
+ // 3. Alphabetically by active modifiers
+ // 4. Trigger key order
+ // These properties propagate through chorded keypresses
+ //
+ // Adapted from Binding.lessThan
+ pub fn lessThan(_: void, lhs: ChordBinding, rhs: ChordBinding) bool {
+ const lhs_len = lhs.triggers.len();
+ const rhs_len = rhs.triggers.len();
+
+ std.debug.assert(lhs_len != 0);
+ std.debug.assert(rhs_len != 0);
+
+ if (lhs_len != rhs_len) {
+ return lhs_len > rhs_len;
+ }
+
+ const lhs_count: usize = blk: {
+ var count: usize = 0;
+ var maybe_trigger = lhs.triggers.first;
+ while (maybe_trigger) |trigger| : (maybe_trigger = trigger.next) {
+ if (trigger.data.mods.super) count += 1;
+ if (trigger.data.mods.ctrl) count += 1;
+ if (trigger.data.mods.shift) count += 1;
+ if (trigger.data.mods.alt) count += 1;
+ }
+ break :blk count;
+ };
+ const rhs_count: usize = blk: {
+ var count: usize = 0;
+ var maybe_trigger = rhs.triggers.first;
+ while (maybe_trigger) |trigger| : (maybe_trigger = trigger.next) {
+ if (trigger.data.mods.super) count += 1;
+ if (trigger.data.mods.ctrl) count += 1;
+ if (trigger.data.mods.shift) count += 1;
+ if (trigger.data.mods.alt) count += 1;
+ }
+
+ break :blk count;
+ };
+
+ if (lhs_count != rhs_count)
+ return lhs_count > rhs_count;
+
+ {
+ var l_trigger = lhs.triggers.first;
+ var r_trigger = rhs.triggers.first;
+ while (l_trigger != null and r_trigger != null) {
+ const l_int = l_trigger.?.data.mods.int();
+ const r_int = r_trigger.?.data.mods.int();
+
+ if (l_int != r_int) {
+ return l_int > r_int;
+ }
+
+ l_trigger = l_trigger.?.next;
+ r_trigger = r_trigger.?.next;
+ }
+ }
+
+ var l_trigger = lhs.triggers.first;
+ var r_trigger = rhs.triggers.first;
+
+ while (l_trigger != null and r_trigger != null) {
+ const lhs_key: c_int = blk: {
+ switch (l_trigger.?.data.key) {
+ .translated => |key| break :blk @intFromEnum(key),
+ .physical => |key| break :blk @intFromEnum(key),
+ .unicode => |key| break :blk @intCast(key),
+ }
+ };
+ const rhs_key: c_int = blk: {
+ switch (r_trigger.?.data.key) {
+ .translated => |key| break :blk @intFromEnum(key),
+ .physical => |key| break :blk @intFromEnum(key),
+ .unicode => |key| break :blk @intCast(key),
+ }
+ };
+
+ l_trigger = l_trigger.?.next;
+ r_trigger = r_trigger.?.next;
+
+ if (l_trigger == null or r_trigger == null) {
+ return lhs_key < rhs_key;
+ }
+
+ if (lhs_key != rhs_key) {
+ return lhs_key < rhs_key;
+ }
+ }
+
+ // The previous loop will always return something on its final iteration so we cannot
+ // reach this point
+ unreachable;
+ }
+};
+
fn prettyPrint(alloc: Allocator, keybinds: Config.Keybinds) !u8 {
// Set up vaxis
var tty = try vaxis.Tty.init();
@@ -111,26 +218,11 @@ fn prettyPrint(alloc: Allocator, keybinds: Config.Keybinds) !u8 {
const win = vx.window();
- // Get all of our keybinds into a list. We also search for the longest printed keyname so we can
- // align things nicely
+ // Generate a list of bindings, recursively traversing chorded keybindings
var iter = keybinds.set.bindings.iterator();
- var bindings = std.ArrayList(Binding).init(alloc);
- var widest_key: u16 = 0;
- var buf: [64]u8 = undefined;
- while (iter.next()) |bind| {
- const action = switch (bind.value_ptr.*) {
- .leader => continue, // TODO: support this
- .leaf => |leaf| leaf.action,
- };
- const key = switch (bind.key_ptr.key) {
- .translated => |k| try std.fmt.bufPrint(&buf, "{s}", .{@tagName(k)}),
- .physical => |k| try std.fmt.bufPrint(&buf, "physical:{s}", .{@tagName(k)}),
- .unicode => |c| try std.fmt.bufPrint(&buf, "{u}", .{c}),
- };
- widest_key = @max(widest_key, win.gwidth(key));
- try bindings.append(.{ .trigger = bind.key_ptr.*, .action = action });
- }
- std.mem.sort(Binding, bindings.items, {}, Binding.lessThan);
+ const bindings, const widest_chord = try iterateBindings(alloc, &iter, &win);
+
+ std.mem.sort(ChordBinding, bindings, {}, ChordBinding.lessThan);
// Set up styles for each modifier
const super_style: vaxis.Style = .{ .fg = .{ .index = 1 } };
@@ -138,41 +230,41 @@ fn prettyPrint(alloc: Allocator, keybinds: Config.Keybinds) !u8 {
const alt_style: vaxis.Style = .{ .fg = .{ .index = 3 } };
const shift_style: vaxis.Style = .{ .fg = .{ .index = 4 } };
- var longest_col: u16 = 0;
-
// Print the list
- for (bindings.items) |bind| {
+ for (bindings) |bind| {
win.clear();
var result: vaxis.Window.PrintResult = .{ .col = 0, .row = 0, .overflow = false };
- const trigger = bind.trigger;
- if (trigger.mods.super) {
- result = win.printSegment(.{ .text = "super", .style = super_style }, .{ .col_offset = result.col });
- result = win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col });
- }
- if (trigger.mods.ctrl) {
- result = win.printSegment(.{ .text = "ctrl ", .style = ctrl_style }, .{ .col_offset = result.col });
- result = win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col });
- }
- if (trigger.mods.alt) {
- result = win.printSegment(.{ .text = "alt ", .style = alt_style }, .{ .col_offset = result.col });
- result = win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col });
- }
- if (trigger.mods.shift) {
- result = win.printSegment(.{ .text = "shift", .style = shift_style }, .{ .col_offset = result.col });
- result = win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col });
- }
+ var maybe_trigger = bind.triggers.first;
+ while (maybe_trigger) |trigger| : (maybe_trigger = trigger.next) {
+ if (trigger.data.mods.super) {
+ result = win.printSegment(.{ .text = "super", .style = super_style }, .{ .col_offset = result.col });
+ result = win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col });
+ }
+ if (trigger.data.mods.ctrl) {
+ result = win.printSegment(.{ .text = "ctrl ", .style = ctrl_style }, .{ .col_offset = result.col });
+ result = win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col });
+ }
+ if (trigger.data.mods.alt) {
+ result = win.printSegment(.{ .text = "alt ", .style = alt_style }, .{ .col_offset = result.col });
+ result = win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col });
+ }
+ if (trigger.data.mods.shift) {
+ result = win.printSegment(.{ .text = "shift", .style = shift_style }, .{ .col_offset = result.col });
+ result = win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col });
+ }
+ const key = switch (trigger.data.key) {
+ .translated => |k| try std.fmt.allocPrint(alloc, "{s}", .{@tagName(k)}),
+ .physical => |k| try std.fmt.allocPrint(alloc, "physical:{s}", .{@tagName(k)}),
+ .unicode => |c| try std.fmt.allocPrint(alloc, "{u}", .{c}),
+ };
+ result = win.printSegment(.{ .text = key }, .{ .col_offset = result.col });
- const key = switch (trigger.key) {
- .translated => |k| try std.fmt.allocPrint(alloc, "{s}", .{@tagName(k)}),
- .physical => |k| try std.fmt.allocPrint(alloc, "physical:{s}", .{@tagName(k)}),
- .unicode => |c| try std.fmt.allocPrint(alloc, "{u}", .{c}),
- };
- // We don't track the key print because we index the action off the *widest* key so we get
- // nice alignment no matter what was printed for mods
- _ = win.printSegment(.{ .text = key }, .{ .col_offset = result.col });
-
- if (longest_col < result.col) longest_col = result.col;
+ // Print a separator between chorded keys
+ if (trigger.next != null) {
+ result = win.printSegment(.{ .text = " > ", .style = .{ .bold = true, .fg = .{ .index = 6 } } }, .{ .col_offset = result.col });
+ }
+ }
const action = try std.fmt.allocPrint(alloc, "{}", .{bind.action});
// If our action has an argument, we print the argument in a different color
@@ -181,12 +273,69 @@ fn prettyPrint(alloc: Allocator, keybinds: Config.Keybinds) !u8 {
.{ .text = action[0..idx] },
.{ .text = action[idx .. idx + 1], .style = .{ .dim = true } },
.{ .text = action[idx + 1 ..], .style = .{ .fg = .{ .index = 5 } } },
- }, .{ .col_offset = longest_col + widest_key + 2 });
+ }, .{ .col_offset = widest_chord + 3 });
} else {
- _ = win.printSegment(.{ .text = action }, .{ .col_offset = longest_col + widest_key + 2 });
+ _ = win.printSegment(.{ .text = action }, .{ .col_offset = widest_chord + 3 });
}
try vx.prettyPrint(writer);
}
try buf_writer.flush();
return 0;
}
+
+fn iterateBindings(alloc: Allocator, iter: anytype, win: *const vaxis.Window) !struct { []ChordBinding, u16 } {
+ var widest_chord: u16 = 0;
+ var bindings = std.ArrayList(ChordBinding).init(alloc);
+ while (iter.next()) |bind| {
+ const width = blk: {
+ var buf = std.ArrayList(u8).init(alloc);
+ const t = bind.key_ptr.*;
+
+ if (t.mods.super) try std.fmt.format(buf.writer(), "super + ", .{});
+ if (t.mods.ctrl) try std.fmt.format(buf.writer(), "ctrl + ", .{});
+ if (t.mods.alt) try std.fmt.format(buf.writer(), "alt + ", .{});
+ if (t.mods.shift) try std.fmt.format(buf.writer(), "shift + ", .{});
+
+ switch (t.key) {
+ .translated => |k| try std.fmt.format(buf.writer(), "{s}", .{@tagName(k)}),
+ .physical => |k| try std.fmt.format(buf.writer(), "physical:{s}", .{@tagName(k)}),
+ .unicode => |c| try std.fmt.format(buf.writer(), "{u}", .{c}),
+ }
+
+ break :blk win.gwidth(buf.items);
+ };
+
+ switch (bind.value_ptr.*) {
+ .leader => |leader| {
+
+ // Recursively iterate on the set of bindings for this leader key
+ var n_iter = leader.bindings.iterator();
+ const sub_bindings, const max_width = try iterateBindings(alloc, &n_iter, win);
+
+ // Prepend the current keybind onto the list of sub-binds
+ for (sub_bindings) |*nb| {
+ const prepend_node = try alloc.create(TriggerList.Node);
+ prepend_node.* = TriggerList.Node{ .data = bind.key_ptr.* };
+ nb.triggers.prepend(prepend_node);
+ }
+
+ // Add the longest sub-bind width to the current bind width along with a padding
+ // of 5 for the ' > ' spacer
+ widest_chord = @max(widest_chord, width + max_width + 5);
+ try bindings.appendSlice(sub_bindings);
+ },
+ .leaf => |leaf| {
+ const node = try alloc.create(TriggerList.Node);
+ node.* = TriggerList.Node{ .data = bind.key_ptr.* };
+ const triggers = TriggerList{
+ .first = node,
+ };
+
+ widest_chord = @max(widest_chord, width);
+ try bindings.append(.{ .triggers = triggers, .action = leaf.action });
+ },
+ }
+ }
+
+ return .{ try bindings.toOwnedSlice(), widest_chord };
+}
From 9ab2e563bbb626f0c76008d026b92a77f95b6321 Mon Sep 17 00:00:00 2001
From: Mitchell Hashimoto
Date: Fri, 24 Jan 2025 10:01:19 -0800
Subject: [PATCH 168/365] Update libxev to fix zombie processes on macOS
Fixes #4554
xev.Process.wait is documented as being equivalent to calling `waitpid`,
i.e. including reaping the process. On Linux, it does this automatically
by using pidfd and the `waitid` syscall. On macOS, it wasn't doing this.
This commit updates libxev to include a fix that explicitly calls
`waitpid` for kqueue.
---
build.zig.zon | 4 ++--
nix/zigCacheHash.nix | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/build.zig.zon b/build.zig.zon
index 9c00a4704..a8f45e6ea 100644
--- a/build.zig.zon
+++ b/build.zig.zon
@@ -5,8 +5,8 @@
.dependencies = .{
// Zig libs
.libxev = .{
- .url = "https://github.com/mitchellh/libxev/archive/aceef3d11efacd9d237c91632f930ed13a2834bf.tar.gz",
- .hash = "12205b2b47fe61a4cde3a45ee4b9cddee75897739dbc196d6396e117cb1ce28e1ad0",
+ .url = "https://github.com/mitchellh/libxev/archive/31eed4e337fed7b0149319e5cdbb62b848c24fbd.tar.gz",
+ .hash = "1220ebf88622c4d502dc59e71347e4d28c47e033f11b59aff774ae5787565c40999c",
},
.mach_glfw = .{
.url = "https://github.com/mitchellh/mach-glfw/archive/37c2995f31abcf7e8378fba68ddcf4a3faa02de0.tar.gz",
diff --git a/nix/zigCacheHash.nix b/nix/zigCacheHash.nix
index c687a5a79..66b8eb8b6 100644
--- a/nix/zigCacheHash.nix
+++ b/nix/zigCacheHash.nix
@@ -1,3 +1,3 @@
# This file is auto-generated! check build-support/check-zig-cache-hash.sh for
# more details.
-"sha256-AvfYl8vLxxsRnf/ERpw5jQIro5rVd98q63hwFsgQOvo="
+"sha256-Bjy31evaKgpRX1mGwAFkai44eiiorTV1gW3VdP9Ins8="
From f73cae07383ffd790a43d2f6b2e77e1236b61f68 Mon Sep 17 00:00:00 2001
From: Mitchell Hashimoto
Date: Fri, 24 Jan 2025 13:46:52 -0800
Subject: [PATCH 169/365] Ignore SIGPIPE
Fixes #5359
The comments explain what's going on. Longer term we should adjust our
termio/exec to avoid the SIGPIPE but its still possible (i.e. that
thread crashes) to happen so we should be robust to it.
---
src/global.zig | 25 +++++++++++++++++++++++++
src/pty.zig | 1 +
2 files changed, 26 insertions(+)
diff --git a/src/global.zig b/src/global.zig
index c00ce27a4..d5a7af630 100644
--- a/src/global.zig
+++ b/src/global.zig
@@ -111,6 +111,9 @@ pub const GlobalState = struct {
}
}
+ // Setup our signal handlers before logging
+ initSignals();
+
// Output some debug information right away
std.log.info("ghostty version={s}", .{build_config.version_string});
std.log.info("ghostty build optimize={s}", .{build_config.mode_string});
@@ -175,6 +178,28 @@ pub const GlobalState = struct {
_ = value.deinit();
}
}
+
+ fn initSignals() void {
+ // Only posix systems.
+ if (comptime builtin.os.tag == .windows) return;
+
+ const p = std.posix;
+
+ var sa: p.Sigaction = .{
+ .handler = .{ .handler = p.SIG.IGN },
+ .mask = p.empty_sigset,
+ .flags = 0,
+ };
+
+ // We ignore SIGPIPE because it is a common signal we may get
+ // due to how we implement termio. When a terminal is closed we
+ // often write to a broken pipe to exit the read thread. This should
+ // be fixed one day but for now this helps make this a bit more
+ // robust.
+ p.sigaction(p.SIG.PIPE, &sa, null) catch |err| {
+ std.log.warn("failed to ignore SIGPIPE err={}", .{err});
+ };
+ }
};
/// Maintains the Unix resource limits that we set for our process. This
diff --git a/src/pty.zig b/src/pty.zig
index 1df09d79c..b6dc2e145 100644
--- a/src/pty.zig
+++ b/src/pty.zig
@@ -201,6 +201,7 @@ const PosixPty = struct {
try posix.sigaction(posix.SIG.HUP, &sa, null);
try posix.sigaction(posix.SIG.ILL, &sa, null);
try posix.sigaction(posix.SIG.INT, &sa, null);
+ try posix.sigaction(posix.SIG.PIPE, &sa, null);
try posix.sigaction(posix.SIG.SEGV, &sa, null);
try posix.sigaction(posix.SIG.TRAP, &sa, null);
try posix.sigaction(posix.SIG.TERM, &sa, null);
From a5a73f83522836400a24624c565491f43feebd0d Mon Sep 17 00:00:00 2001
From: Mitchell Hashimoto
Date: Fri, 24 Jan 2025 14:36:40 -0800
Subject: [PATCH 170/365] macos: autohide dock if quick terminal would conflict
with it
Fixes #5328
The dock sits above the level of the quick terminal, and the quick
terminal frame typical includes the dock. Hence, if the dock is visible
and the quick terminal would conflict with it, then part of the terminal
is obscured.
This commit makes the dock autohide if the quick terminal would conflict
with it. The autohide is disabled when the quick terminal is closed.
We can't set our window level above the dock, as this would prevent
things such as input methods from rendering properly in the quick
terminal window.
iTerm2 (the only other macOS terminal I know of that supports a dropdown
mode) frames the terminal around the dock. I think this looks less
aesthetically pleasing and I prefer autohiding the dock instead.
We can introduce a setting to change this behavior if desired later.
Additionally, this commit introduces a mechanism to safely set
app-global presentation options from multiple sources without stepping
on each other.
---
macos/Ghostty.xcodeproj/project.pbxproj | 8 +++++
.../QuickTerminalController.swift | 32 ++++++++++++++++--
.../QuickTerminal/QuickTerminalPosition.swift | 18 ++++++++++
macos/Sources/Helpers/Dock.swift | 33 +++++++++++++++++++
macos/Sources/Helpers/Fullscreen.swift | 8 ++---
.../Helpers/NSApplication+Extension.swift | 31 +++++++++++++++++
6 files changed, 124 insertions(+), 6 deletions(-)
create mode 100644 macos/Sources/Helpers/Dock.swift
create mode 100644 macos/Sources/Helpers/NSApplication+Extension.swift
diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj
index efa4a07c9..02c8258cb 100644
--- a/macos/Ghostty.xcodeproj/project.pbxproj
+++ b/macos/Ghostty.xcodeproj/project.pbxproj
@@ -69,6 +69,8 @@
A59FB5CF2AE0DB50009128F3 /* InspectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59FB5CE2AE0DB50009128F3 /* InspectorView.swift */; };
A59FB5D12AE0DEA7009128F3 /* MetalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59FB5D02AE0DEA7009128F3 /* MetalView.swift */; };
A5A1F8852A489D6800D1E8BC /* terminfo in Resources */ = {isa = PBXBuildFile; fileRef = A5A1F8842A489D6800D1E8BC /* terminfo */; };
+ A5A2A3CA2D4445E30033CF96 /* Dock.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A2A3C92D4445E20033CF96 /* Dock.swift */; };
+ A5A2A3CC2D444ABB0033CF96 /* NSApplication+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A2A3CB2D444AB80033CF96 /* NSApplication+Extension.swift */; };
A5A6F72A2CC41B8900B232A5 /* Xcode.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A6F7292CC41B8700B232A5 /* Xcode.swift */; };
A5B30539299BEAAB0047F10C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; };
A5CA378C2D2A4DEB00931030 /* KeyboardLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CA378B2D2A4DE800931030 /* KeyboardLayout.swift */; };
@@ -163,6 +165,8 @@
A59FB5CE2AE0DB50009128F3 /* InspectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InspectorView.swift; sourceTree = ""; };
A59FB5D02AE0DEA7009128F3 /* MetalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetalView.swift; sourceTree = ""; };
A5A1F8842A489D6800D1E8BC /* terminfo */ = {isa = PBXFileReference; lastKnownFileType = folder; name = terminfo; path = "../zig-out/share/terminfo"; sourceTree = ""; };
+ A5A2A3C92D4445E20033CF96 /* Dock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dock.swift; sourceTree = ""; };
+ A5A2A3CB2D444AB80033CF96 /* NSApplication+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSApplication+Extension.swift"; sourceTree = ""; };
A5A6F7292CC41B8700B232A5 /* Xcode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Xcode.swift; sourceTree = ""; };
A5B30531299BEAAA0047F10C /* Ghostty.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Ghostty.app; sourceTree = BUILT_PRODUCTS_DIR; };
A5B30538299BEAAB0047F10C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
@@ -271,6 +275,7 @@
A5333E1B2B5A1CE3008AEFF7 /* CrossKit.swift */,
A5CBD0572C9F30860017A1AE /* Cursor.swift */,
A5D0AF3C2B37804400D21823 /* CodableBridge.swift */,
+ A5A2A3C92D4445E20033CF96 /* Dock.swift */,
A52FFF582CAA4FF1000C6A5B /* Fullscreen.swift */,
A59630962AEE163600D64628 /* HostingWindow.swift */,
A5CA378B2D2A4DE800931030 /* KeyboardLayout.swift */,
@@ -278,6 +283,7 @@
A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */,
C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */,
A599CDAF2CF103F20049FA26 /* NSAppearance+Extension.swift */,
+ A5A2A3CB2D444AB80033CF96 /* NSApplication+Extension.swift */,
A54B0CEA2D0CFB4A00CBEFF8 /* NSImage+Extension.swift */,
A52FFF5C2CAB4D05000C6A5B /* NSScreen+Extension.swift */,
C1F26EA62B738B9900404083 /* NSView+Extension.swift */,
@@ -635,6 +641,7 @@
A5CBD0602CA0C90A0017A1AE /* QuickTerminalWindow.swift in Sources */,
A5CBD05E2CA0C5EC0017A1AE /* QuickTerminalController.swift in Sources */,
A5CF66D72D29DDB500139794 /* Ghostty.Event.swift in Sources */,
+ A5A2A3CA2D4445E30033CF96 /* Dock.swift in Sources */,
A51BFC222B2FB6B400E92F16 /* AboutView.swift in Sources */,
A5278A9B2AA05B2600CD3039 /* Ghostty.Input.swift in Sources */,
A5CBD0562C9E65B80017A1AE /* DraggableWindowView.swift in Sources */,
@@ -657,6 +664,7 @@
A59444F729A2ED5200725BBA /* SettingsView.swift in Sources */,
A56D58862ACDDB4100508D2C /* Ghostty.Shell.swift in Sources */,
A5985CD72C320C4500C57AD3 /* String+Extension.swift in Sources */,
+ A5A2A3CC2D444ABB0033CF96 /* NSApplication+Extension.swift in Sources */,
A59630A22AF0415000D64628 /* Ghostty.TerminalSplit.swift in Sources */,
A5FEB3002ABB69450068369E /* main.swift in Sources */,
A55B7BB829B6F53A0055DE60 /* Package.swift in Sources */,
diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift
index bc89022f5..05c8677a7 100644
--- a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift
+++ b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift
@@ -27,6 +27,10 @@ class QuickTerminalController: BaseTerminalController {
// The active space when the quick terminal was last shown.
private var previousActiveSpace: size_t = 0
+ /// This is set to true of the dock was autohid when the terminal animated in. This lets us
+ /// know if we have to unhide when the terminal is animated out.
+ private var hidDock: Bool = false
+
/// The configuration derived from the Ghostty config so we don't need to rely on references.
private var derivedConfig: DerivedConfig
@@ -224,6 +228,18 @@ class QuickTerminalController: BaseTerminalController {
animateWindowOut(window: window, to: position)
}
+ private func hideDock() {
+ guard !hidDock else { return }
+ NSApp.acquirePresentationOption(.autoHideDock)
+ hidDock = true
+ }
+
+ private func unhideDock() {
+ guard hidDock else { return }
+ NSApp.releasePresentationOption(.autoHideDock)
+ hidDock = false
+ }
+
private func animateWindowIn(window: NSWindow, from position: QuickTerminalPosition) {
guard let screen = derivedConfig.quickTerminalScreen.screen else { return }
@@ -240,6 +256,12 @@ class QuickTerminalController: BaseTerminalController {
window.makeKeyAndOrderFront(nil)
}
+ // If our dock position would conflict with our target location then
+ // we autohide the dock.
+ if position.conflictsWithDock(on: screen) {
+ hideDock()
+ }
+
// Run the animation that moves our window into the proper place and makes
// it visible.
NSAnimationContext.runAnimationGroup({ context in
@@ -250,8 +272,11 @@ class QuickTerminalController: BaseTerminalController {
// There is a very minor delay here so waiting at least an event loop tick
// keeps us safe from the view not being on the window.
DispatchQueue.main.async {
- // If we canceled our animation in we do nothing
- guard self.visible else { return }
+ // If we canceled our animation clean up some state.
+ guard self.visible else {
+ self.unhideDock()
+ return
+ }
// After animating in, we reset the window level to a value that
// is above other windows but not as high as popUpMenu. This allows
@@ -320,6 +345,9 @@ class QuickTerminalController: BaseTerminalController {
}
private func animateWindowOut(window: NSWindow, to position: QuickTerminalPosition) {
+ // If we hid the dock then we unhide it.
+ unhideDock()
+
// If the window isn't on our active space then we don't animate, we just
// hide it.
if !window.isOnActiveSpace {
diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalPosition.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalPosition.swift
index 6ba224a28..7ba124a30 100644
--- a/macos/Sources/Features/QuickTerminal/QuickTerminalPosition.swift
+++ b/macos/Sources/Features/QuickTerminal/QuickTerminalPosition.swift
@@ -118,4 +118,22 @@ enum QuickTerminalPosition : String {
return .init(x: screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2, y: screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2)
}
}
+
+ func conflictsWithDock(on screen: NSScreen) -> Bool {
+ // Screen must have a dock for it to conflict
+ guard screen.hasDock else { return false }
+
+ // Get the dock orientation for this screen
+ guard let orientation = Dock.orientation else { return false }
+
+ // Depending on the orientation of the dock, we conflict if our quick terminal
+ // would potentially "hit" the dock. In the future we should probably consider
+ // the frame of the quick terminal.
+ return switch (orientation) {
+ case .top: self == .top || self == .left || self == .right
+ case .bottom: self == .bottom || self == .left || self == .right
+ case .left: self == .top || self == .bottom
+ case .right: self == .top || self == .bottom
+ }
+ }
}
diff --git a/macos/Sources/Helpers/Dock.swift b/macos/Sources/Helpers/Dock.swift
new file mode 100644
index 000000000..70fb904d9
--- /dev/null
+++ b/macos/Sources/Helpers/Dock.swift
@@ -0,0 +1,33 @@
+import Cocoa
+
+// Private API to get Dock location
+@_silgen_name("CoreDockGetOrientationAndPinning")
+func CoreDockGetOrientationAndPinning(
+ _ outOrientation: UnsafeMutablePointer,
+ _ outPinning: UnsafeMutablePointer)
+
+// Private API to get the current Dock auto-hide state
+@_silgen_name("CoreDockGetAutoHideEnabled")
+func CoreDockGetAutoHideEnabled() -> Bool
+
+enum DockOrientation: Int {
+ case top = 1
+ case bottom = 2
+ case left = 3
+ case right = 4
+}
+
+class Dock {
+ /// Returns the orientation of the dock or nil if it can't be determined.
+ static var orientation: DockOrientation? {
+ var orientation: Int32 = 0
+ var pinning: Int32 = 0
+ CoreDockGetOrientationAndPinning(&orientation, &pinning)
+ return .init(rawValue: Int(orientation)) ?? nil
+ }
+
+ /// Returns true if the dock has auto-hide enabled.
+ static var autoHideEnabled: Bool {
+ return CoreDockGetAutoHideEnabled()
+ }
+}
diff --git a/macos/Sources/Helpers/Fullscreen.swift b/macos/Sources/Helpers/Fullscreen.swift
index a16f329f8..320eca013 100644
--- a/macos/Sources/Helpers/Fullscreen.swift
+++ b/macos/Sources/Helpers/Fullscreen.swift
@@ -307,21 +307,21 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle {
// MARK: Dock
private func hideDock() {
- NSApp.presentationOptions.insert(.autoHideDock)
+ NSApp.acquirePresentationOption(.autoHideDock)
}
private func unhideDock() {
- NSApp.presentationOptions.remove(.autoHideDock)
+ NSApp.releasePresentationOption(.autoHideDock)
}
// MARK: Menu
func hideMenu() {
- NSApp.presentationOptions.insert(.autoHideMenuBar)
+ NSApp.acquirePresentationOption(.autoHideMenuBar)
}
func unhideMenu() {
- NSApp.presentationOptions.remove(.autoHideMenuBar)
+ NSApp.releasePresentationOption(.autoHideMenuBar)
}
/// The state that must be saved for non-native fullscreen to exit fullscreen.
diff --git a/macos/Sources/Helpers/NSApplication+Extension.swift b/macos/Sources/Helpers/NSApplication+Extension.swift
new file mode 100644
index 000000000..0580cd5fc
--- /dev/null
+++ b/macos/Sources/Helpers/NSApplication+Extension.swift
@@ -0,0 +1,31 @@
+import Cocoa
+
+extension NSApplication {
+ private static var presentationOptionCounts: [NSApplication.PresentationOptions.Element: UInt] = [:]
+
+ /// Add a presentation option to the application and main a reference count so that and equal
+ /// number of pops is required to disable it. This is useful so that multiple classes can affect global
+ /// app state without overriding others.
+ func acquirePresentationOption(_ option: NSApplication.PresentationOptions.Element) {
+ Self.presentationOptionCounts[option, default: 0] += 1
+ presentationOptions.insert(option)
+ }
+
+ /// See acquirePresentationOption
+ func releasePresentationOption(_ option: NSApplication.PresentationOptions.Element) {
+ guard let value = Self.presentationOptionCounts[option] else { return }
+ guard value > 0 else { return }
+ if (value == 1) {
+ presentationOptions.remove(option)
+ Self.presentationOptionCounts.removeValue(forKey: option)
+ } else {
+ Self.presentationOptionCounts[option] = value - 1
+ }
+ }
+}
+
+extension NSApplication.PresentationOptions.Element: @retroactive Hashable {
+ public func hash(into hasher: inout Hasher) {
+ hasher.combine(rawValue)
+ }
+}
From a58b1998a992dcef3bfd54246f0b7673bea5e517 Mon Sep 17 00:00:00 2001
From: Mitchell Hashimoto
Date: Fri, 24 Jan 2025 20:10:24 -0800
Subject: [PATCH 171/365] macos: hide dock globally if the dock conflicts
Related to #5361
The fix in 5361 wasn't sufficient since it only applied if our app was
in the foreground. Our quick terminal is a non-activating NSPanel to
allow it to work on any space (fullscreen included). This means that
Ghostty doesn't become the active app when the quick terminal is shown
and another app is in the foreground.
To work around this, we now hide the dock globally when the quick
terminal is shown AND the dock is in a conflicting position. We restore
this state when the quick terminal is hidden, loses focus, or Ghostty is
quit.
---
.../QuickTerminalController.swift | 90 +++++++++++++++----
macos/Sources/Helpers/Dock.swift | 9 +-
2 files changed, 79 insertions(+), 20 deletions(-)
diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift
index 05c8677a7..807935806 100644
--- a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift
+++ b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift
@@ -27,9 +27,8 @@ class QuickTerminalController: BaseTerminalController {
// The active space when the quick terminal was last shown.
private var previousActiveSpace: size_t = 0
- /// This is set to true of the dock was autohid when the terminal animated in. This lets us
- /// know if we have to unhide when the terminal is animated out.
- private var hidDock: Bool = false
+ /// Non-nil if we have hidden dock state.
+ private var hiddenDock: HiddenDock? = nil
/// The configuration derived from the Ghostty config so we don't need to rely on references.
private var derivedConfig: DerivedConfig
@@ -45,6 +44,11 @@ class QuickTerminalController: BaseTerminalController {
// Setup our notifications for behaviors
let center = NotificationCenter.default
+ center.addObserver(
+ self,
+ selector: #selector(applicationWillTerminate(_:)),
+ name: NSApplication.willTerminateNotification,
+ object: nil)
center.addObserver(
self,
selector: #selector(onToggleFullscreen),
@@ -65,6 +69,9 @@ class QuickTerminalController: BaseTerminalController {
// Remove all of our notificationcenter subscriptions
let center = NotificationCenter.default
center.removeObserver(self)
+
+ // Make sure we restore our hidden dock
+ hiddenDock = nil
}
// MARK: NSWindowController
@@ -100,6 +107,17 @@ class QuickTerminalController: BaseTerminalController {
// MARK: NSWindowDelegate
+ override func windowDidBecomeKey(_ notification: Notification) {
+ super.windowDidBecomeKey(notification)
+
+ // If we're not visible we don't care to run the logic below. It only
+ // applies if we can be seen.
+ guard visible else { return }
+
+ // Re-hide the dock if we were hiding it before.
+ hiddenDock?.hide()
+ }
+
override func windowDidResignKey(_ notification: Notification) {
super.windowDidResignKey(notification)
@@ -120,6 +138,10 @@ class QuickTerminalController: BaseTerminalController {
self.previousApp = nil
}
+ // Regardless of autohide, we always want to bring the dock back
+ // when we lose focus.
+ hiddenDock?.restore()
+
if derivedConfig.quickTerminalAutoHide {
switch derivedConfig.quickTerminalSpaceBehavior {
case .remain:
@@ -228,18 +250,6 @@ class QuickTerminalController: BaseTerminalController {
animateWindowOut(window: window, to: position)
}
- private func hideDock() {
- guard !hidDock else { return }
- NSApp.acquirePresentationOption(.autoHideDock)
- hidDock = true
- }
-
- private func unhideDock() {
- guard hidDock else { return }
- NSApp.releasePresentationOption(.autoHideDock)
- hidDock = false
- }
-
private func animateWindowIn(window: NSWindow, from position: QuickTerminalPosition) {
guard let screen = derivedConfig.quickTerminalScreen.screen else { return }
@@ -259,7 +269,15 @@ class QuickTerminalController: BaseTerminalController {
// If our dock position would conflict with our target location then
// we autohide the dock.
if position.conflictsWithDock(on: screen) {
- hideDock()
+ if (hiddenDock == nil) {
+ hiddenDock = .init()
+ }
+
+ hiddenDock?.hide()
+ } else {
+ // Ensure we don't have any hidden dock if we don't conflict.
+ // The deinit will restore.
+ hiddenDock = nil
}
// Run the animation that moves our window into the proper place and makes
@@ -274,7 +292,7 @@ class QuickTerminalController: BaseTerminalController {
DispatchQueue.main.async {
// If we canceled our animation clean up some state.
guard self.visible else {
- self.unhideDock()
+ self.hiddenDock = nil
return
}
@@ -346,7 +364,7 @@ class QuickTerminalController: BaseTerminalController {
private func animateWindowOut(window: NSWindow, to position: QuickTerminalPosition) {
// If we hid the dock then we unhide it.
- unhideDock()
+ hiddenDock = nil
// If the window isn't on our active space then we don't animate, we just
// hide it.
@@ -443,6 +461,13 @@ class QuickTerminalController: BaseTerminalController {
// MARK: Notifications
+ @objc private func applicationWillTerminate(_ notification: Notification) {
+ // If the application is going to terminate we want to make sure we
+ // restore any global dock state. I think deinit should be called which
+ // would call this anyways but I can't be sure so I will do this too.
+ hiddenDock = nil
+ }
+
@objc private func onToggleFullscreen(notification: SwiftUI.Notification) {
guard let target = notification.object as? Ghostty.SurfaceView else { return }
guard target == self.focusedSurface else { return }
@@ -490,6 +515,35 @@ class QuickTerminalController: BaseTerminalController {
self.backgroundOpacity = config.backgroundOpacity
}
}
+
+ /// Hides the dock globally (not just NSApp). This is only used if the quick terminal is
+ /// in a conflicting position with the dock.
+ private class HiddenDock {
+ let previousAutoHide: Bool
+ private var hidden: Bool = false
+
+ init() {
+ previousAutoHide = Dock.autoHideEnabled
+ }
+
+ deinit {
+ restore()
+ }
+
+ func hide() {
+ guard !hidden else { return }
+ NSApp.acquirePresentationOption(.autoHideDock)
+ Dock.autoHideEnabled = true
+ hidden = true
+ }
+
+ func restore() {
+ guard hidden else { return }
+ NSApp.releasePresentationOption(.autoHideDock)
+ Dock.autoHideEnabled = previousAutoHide
+ hidden = false
+ }
+ }
}
extension Notification.Name {
diff --git a/macos/Sources/Helpers/Dock.swift b/macos/Sources/Helpers/Dock.swift
index 70fb904d9..a71fcaa5b 100644
--- a/macos/Sources/Helpers/Dock.swift
+++ b/macos/Sources/Helpers/Dock.swift
@@ -10,6 +10,10 @@ func CoreDockGetOrientationAndPinning(
@_silgen_name("CoreDockGetAutoHideEnabled")
func CoreDockGetAutoHideEnabled() -> Bool
+// Toggles the Dock's auto-hide state
+@_silgen_name("CoreDockSetAutoHideEnabled")
+func CoreDockSetAutoHideEnabled(_ flag: Bool)
+
enum DockOrientation: Int {
case top = 1
case bottom = 2
@@ -26,8 +30,9 @@ class Dock {
return .init(rawValue: Int(orientation)) ?? nil
}
- /// Returns true if the dock has auto-hide enabled.
+ /// Set the dock autohide.
static var autoHideEnabled: Bool {
- return CoreDockGetAutoHideEnabled()
+ get { return CoreDockGetAutoHideEnabled() }
+ set { CoreDockSetAutoHideEnabled(newValue) }
}
}
From 645b4b0031a2e704628e1eed86a5fb20377a2404 Mon Sep 17 00:00:00 2001
From: Daniel Patterson
Date: Sat, 25 Jan 2025 23:22:40 +0000
Subject: [PATCH 172/365] Fix cmd+triple click not selecting full output
---
src/terminal/Screen.zig | 63 +++++++++++++++++++++++++++--------------
1 file changed, 42 insertions(+), 21 deletions(-)
diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig
index eb70d32d0..af3d13392 100644
--- a/src/terminal/Screen.zig
+++ b/src/terminal/Screen.zig
@@ -2591,13 +2591,24 @@ pub fn selectOutput(self: *Screen, pin: Pin) ?Selection {
const start: Pin = boundary: {
var it = pin.rowIterator(.left_up, null);
var it_prev = pin;
+
+ // First, iterate until we find the first line of command output
+ while (it.next()) |p| {
+ it_prev = p;
+ const row = p.rowAndCell().row;
+ if (row.semantic_prompt == .command) {
+ break;
+ }
+ }
+
+ // Because the first line of command output may span multiple visual rows we must now
+ // iterate until we find the first row of anything other than command output and then
+ // yield the previous row.
while (it.next()) |p| {
const row = p.rowAndCell().row;
- switch (row.semantic_prompt) {
- .command => break :boundary p,
- else => {},
+ if (row.semantic_prompt != .command) {
+ break :boundary it_prev;
}
-
it_prev = p;
}
@@ -7641,17 +7652,17 @@ test "Screen: selectOutput" {
// zig fmt: off
{
- // line number:
- try s.testWriteString("output1\n"); // 0
- try s.testWriteString("output1\n"); // 1
- try s.testWriteString("prompt2\n"); // 2
- try s.testWriteString("input2\n"); // 3
- try s.testWriteString("output2\n"); // 4
- try s.testWriteString("output2\n"); // 5
- try s.testWriteString("prompt3$ input3\n"); // 6
- try s.testWriteString("output3\n"); // 7
- try s.testWriteString("output3\n"); // 8
- try s.testWriteString("output3"); // 9
+ // line number:
+ try s.testWriteString("output1\n"); // 0
+ try s.testWriteString("output1\n"); // 1
+ try s.testWriteString("prompt2\n"); // 2
+ try s.testWriteString("input2\n"); // 3
+ try s.testWriteString("output2output2output2output2\n"); // 4, 5, 6 due to overflow
+ try s.testWriteString("output2\n"); // 7
+ try s.testWriteString("prompt3$ input3\n"); // 8
+ try s.testWriteString("output3\n"); // 9
+ try s.testWriteString("output3\n"); // 10
+ try s.testWriteString("output3"); // 11
}
// zig fmt: on
@@ -7670,13 +7681,23 @@ test "Screen: selectOutput" {
const row = pin.rowAndCell().row;
row.semantic_prompt = .command;
}
+ {
+ const pin = s.pages.pin(.{ .screen = .{ .y = 5 } }).?;
+ const row = pin.rowAndCell().row;
+ row.semantic_prompt = .command;
+ }
{
const pin = s.pages.pin(.{ .screen = .{ .y = 6 } }).?;
const row = pin.rowAndCell().row;
+ row.semantic_prompt = .command;
+ }
+ {
+ const pin = s.pages.pin(.{ .screen = .{ .y = 8 } }).?;
+ const row = pin.rowAndCell().row;
row.semantic_prompt = .input;
}
{
- const pin = s.pages.pin(.{ .screen = .{ .y = 7 } }).?;
+ const pin = s.pages.pin(.{ .screen = .{ .y = 9 } }).?;
const row = pin.rowAndCell().row;
row.semantic_prompt = .command;
}
@@ -7701,7 +7722,7 @@ test "Screen: selectOutput" {
{
var sel = s.selectOutput(s.pages.pin(.{ .active = .{
.x = 3,
- .y = 5,
+ .y = 7,
} }).?).?;
defer sel.deinit(&s);
try testing.expectEqual(point.Point{ .active = .{
@@ -7710,23 +7731,23 @@ test "Screen: selectOutput" {
} }, s.pages.pointFromPin(.active, sel.start()).?);
try testing.expectEqual(point.Point{ .active = .{
.x = 9,
- .y = 5,
+ .y = 7,
} }, s.pages.pointFromPin(.active, sel.end()).?);
}
// No end marker, should select till the end
{
var sel = s.selectOutput(s.pages.pin(.{ .active = .{
.x = 2,
- .y = 7,
+ .y = 10,
} }).?).?;
defer sel.deinit(&s);
try testing.expectEqual(point.Point{ .active = .{
.x = 0,
- .y = 7,
+ .y = 9,
} }, s.pages.pointFromPin(.active, sel.start()).?);
try testing.expectEqual(point.Point{ .active = .{
.x = 9,
- .y = 10,
+ .y = 12,
} }, s.pages.pointFromPin(.active, sel.end()).?);
}
// input / prompt at y = 0, pt.y = 0
From 4b8010a6f4a0a4b56cb1609b6ecc47ff688d70f1 Mon Sep 17 00:00:00 2001
From: Daniel Patterson
Date: Sun, 26 Jan 2025 15:22:01 +0000
Subject: [PATCH 173/365] Change ifs to exhaustive switches
---
src/terminal/Screen.zig | 19 +++++++++++++++++--
1 file changed, 17 insertions(+), 2 deletions(-)
diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig
index af3d13392..fe2eab5ac 100644
--- a/src/terminal/Screen.zig
+++ b/src/terminal/Screen.zig
@@ -2596,6 +2596,15 @@ pub fn selectOutput(self: *Screen, pin: Pin) ?Selection {
while (it.next()) |p| {
it_prev = p;
const row = p.rowAndCell().row;
+ switch (row.semantic_prompt) {
+ .command => break,
+
+ .unknown,
+ .prompt,
+ .prompt_continuation,
+ .input,
+ => {},
+ }
if (row.semantic_prompt == .command) {
break;
}
@@ -2606,8 +2615,14 @@ pub fn selectOutput(self: *Screen, pin: Pin) ?Selection {
// yield the previous row.
while (it.next()) |p| {
const row = p.rowAndCell().row;
- if (row.semantic_prompt != .command) {
- break :boundary it_prev;
+ switch (row.semantic_prompt) {
+ .command => {},
+
+ .unknown,
+ .prompt,
+ .prompt_continuation,
+ .input,
+ => break :boundary it_prev,
}
it_prev = p;
}
From ac568900a527be8740c3c999eb98accef3e8dc22 Mon Sep 17 00:00:00 2001
From: Qwerasd
Date: Sun, 26 Jan 2025 20:40:19 -0500
Subject: [PATCH 174/365] fix(renderer/Metal): properly load cursor color
---
src/renderer/shaders/cell.metal | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/src/renderer/shaders/cell.metal b/src/renderer/shaders/cell.metal
index 17f811a19..1a6d17ea3 100644
--- a/src/renderer/shaders/cell.metal
+++ b/src/renderer/shaders/cell.metal
@@ -473,7 +473,11 @@ vertex CellTextVertexOut cell_text_vertex(
) &&
in.grid_pos.y == uniforms.cursor_pos.y
) {
- out.color = float4(uniforms.cursor_color) / 255.0f;
+ out.color = load_color(
+ uniforms.cursor_color,
+ uniforms.use_display_p3,
+ false
+ );
}
return out;
From 5c8f984ea157bd40da631a17ddafcbf07a5b04db Mon Sep 17 00:00:00 2001
From: Qwerasd
Date: Mon, 27 Jan 2025 19:15:18 -0500
Subject: [PATCH 175/365] renderer/Metal: improve linear blending correction
More mathematically sound approach, does a much better job of matching
the appearance of non-linear blending. Removed `experimental` from name
because it's not really an experiment anymore.
---
src/config/Config.zig | 7 ++-
src/renderer/Metal.zig | 6 +--
src/renderer/metal/shaders.zig | 2 +-
src/renderer/shaders/cell.metal | 89 +++++++++++++++++++++------------
4 files changed, 63 insertions(+), 41 deletions(-)
diff --git a/src/config/Config.zig b/src/config/Config.zig
index 839656169..c7aeebb01 100644
--- a/src/config/Config.zig
+++ b/src/config/Config.zig
@@ -273,10 +273,9 @@ pub const renamed = std.StaticStringMap([]const u8).initComptime(&.{
/// This is also sometimes known as "gamma correction".
/// (Currently only supported on macOS. Has no effect on Linux.)
///
-/// * `linear-corrected` - Corrects the thinning/thickening effect of linear
-/// by applying a correction curve to the text alpha depending on its
-/// brightness. This compensates for the thinning and makes the weight of
-/// most text appear very similar to when it's blended non-linearly.
+/// * `linear-corrected` - Same as `linear`, but with a correction step applied
+/// for text that makes it look nearly or completely identical to `native`,
+/// but without any of the darkening artifacts.
///
/// Note: This setting affects more than just text, images will also be blended
/// in the selected color space, and custom shaders will receive colors in that
diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig
index 52a5437c6..dccccef17 100644
--- a/src/renderer/Metal.zig
+++ b/src/renderer/Metal.zig
@@ -667,7 +667,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
.cursor_wide = false,
.use_display_p3 = options.config.colorspace == .@"display-p3",
.use_linear_blending = options.config.blending.isLinear(),
- .use_experimental_linear_correction = options.config.blending == .@"linear-corrected",
+ .use_linear_correction = options.config.blending == .@"linear-corrected",
},
// Fonts
@@ -2099,7 +2099,7 @@ pub fn changeConfig(self: *Metal, config: *DerivedConfig) !void {
// Set our new color space and blending
self.uniforms.use_display_p3 = config.colorspace == .@"display-p3";
self.uniforms.use_linear_blending = config.blending.isLinear();
- self.uniforms.use_experimental_linear_correction = config.blending == .@"linear-corrected";
+ self.uniforms.use_linear_correction = config.blending == .@"linear-corrected";
// Set our new colors
self.default_background_color = config.background;
@@ -2242,7 +2242,7 @@ pub fn setScreenSize(
.cursor_wide = old.cursor_wide,
.use_display_p3 = old.use_display_p3,
.use_linear_blending = old.use_linear_blending,
- .use_experimental_linear_correction = old.use_experimental_linear_correction,
+ .use_linear_correction = old.use_linear_correction,
};
// Reset our cell contents if our grid size has changed.
diff --git a/src/renderer/metal/shaders.zig b/src/renderer/metal/shaders.zig
index 62d363173..b297de809 100644
--- a/src/renderer/metal/shaders.zig
+++ b/src/renderer/metal/shaders.zig
@@ -158,7 +158,7 @@ pub const Uniforms = extern struct {
/// Enables a weight correction step that makes text rendered
/// with linear alpha blending have a similar apparent weight
/// (thickness) to gamma-incorrect blending.
- use_experimental_linear_correction: bool align(1) = false,
+ use_linear_correction: bool align(1) = false,
const PaddingExtend = packed struct(u8) {
left: bool = false,
diff --git a/src/renderer/shaders/cell.metal b/src/renderer/shaders/cell.metal
index 1a6d17ea3..3ca0f9149 100644
--- a/src/renderer/shaders/cell.metal
+++ b/src/renderer/shaders/cell.metal
@@ -22,7 +22,7 @@ struct Uniforms {
bool cursor_wide;
bool use_display_p3;
bool use_linear_blending;
- bool use_experimental_linear_correction;
+ bool use_linear_correction;
};
//-------------------------------------------------------------------
@@ -59,22 +59,28 @@ float3 srgb_to_display_p3(float3 srgb) {
// Converts a color from sRGB gamma encoding to linear.
float4 linearize(float4 srgb) {
- bool3 cutoff = srgb.rgb <= 0.04045;
- float3 lower = srgb.rgb / 12.92;
- float3 higher = pow((srgb.rgb + 0.055) / 1.055, 2.4);
- srgb.rgb = mix(higher, lower, float3(cutoff));
+ bool3 cutoff = srgb.rgb <= 0.04045;
+ float3 lower = srgb.rgb / 12.92;
+ float3 higher = pow((srgb.rgb + 0.055) / 1.055, 2.4);
+ srgb.rgb = mix(higher, lower, float3(cutoff));
- return srgb;
+ return srgb;
+}
+float linearize(float v) {
+ return v <= 0.04045 ? v / 12.92 : pow((v + 0.055) / 1.055, 2.4);
}
// Converts a color from linear to sRGB gamma encoding.
float4 unlinearize(float4 linear) {
- bool3 cutoff = linear.rgb <= 0.0031308;
- float3 lower = linear.rgb * 12.92;
- float3 higher = pow(linear.rgb, 1.0 / 2.4) * 1.055 - 0.055;
- linear.rgb = mix(higher, lower, float3(cutoff));
+ bool3 cutoff = linear.rgb <= 0.0031308;
+ float3 lower = linear.rgb * 12.92;
+ float3 higher = pow(linear.rgb, 1.0 / 2.4) * 1.055 - 0.055;
+ linear.rgb = mix(higher, lower, float3(cutoff));
- return linear;
+ return linear;
+}
+float unlinearize(float v) {
+ return v <= 0.0031308 ? v * 12.92 : pow(v, 1.0 / 2.4) * 1.055 - 0.055;
}
// Compute the luminance of the provided color.
@@ -353,8 +359,9 @@ struct CellTextVertexIn {
struct CellTextVertexOut {
float4 position [[position]];
- uint8_t mode;
- float4 color;
+ uint8_t mode [[flat]];
+ float4 color [[flat]];
+ float4 bg_color [[flat]];
float2 tex_coord;
};
@@ -445,6 +452,13 @@ vertex CellTextVertexOut cell_text_vertex(
true
);
+ // Get the BG color
+ out.bg_color = load_color(
+ bg_colors[in.grid_pos.y * uniforms.grid_size.x + in.grid_pos.x],
+ uniforms.use_display_p3,
+ true
+ );
+
// If we have a minimum contrast, we need to check if we need to
// change the color of the text to ensure it has enough contrast
// with the background.
@@ -453,14 +467,8 @@ vertex CellTextVertexOut cell_text_vertex(
// and Powerline glyphs to be unaffected (else parts of the line would
// have different colors as some parts are displayed via background colors).
if (uniforms.min_contrast > 1.0f && in.mode == MODE_TEXT) {
- // Get the BG color
- float4 bg_color = load_color(
- bg_colors[in.grid_pos.y * uniforms.grid_size.x + in.grid_pos.x],
- uniforms.use_display_p3,
- true
- );
// Ensure our minimum contrast
- out.color = contrasted_color(uniforms.min_contrast, out.color, bg_color);
+ out.color = contrasted_color(uniforms.min_contrast, out.color, out.bg_color);
}
// If this cell is the cursor cell, then we need to change the color.
@@ -480,6 +488,12 @@ vertex CellTextVertexOut cell_text_vertex(
);
}
+ // Don't bother rendering if the bg and fg colors are identical, just return
+ // the same point which will be culled because it makes the quad zero sized.
+ if (all(out.color == out.bg_color)) {
+ out.position = float4(0.0);
+ }
+
return out;
}
@@ -518,19 +532,28 @@ fragment float4 cell_text_fragment(
// Fetch our alpha mask for this pixel.
float a = textureGrayscale.sample(textureSampler, in.tex_coord).r;
- // Experimental linear blending weight correction.
- if (uniforms.use_experimental_linear_correction) {
- float l = luminance(color.rgb);
-
- // TODO: This is a dynamic dilation term that biases
- // the alpha adjustment for small font sizes;
- // it should be computed by dividing the font
- // size in `pt`s by `13.0` and using that if
- // it's less than `1.0`, but for now it's
- // hard coded at 1.0, which has no effect.
- float d = 13.0 / 13.0;
-
- a += pow(a, d + d * l) - pow(a, d + 1.0 - d * l);
+ // Linear blending weight correction corrects the alpha value to
+ // produce blending results which match gamma-incorrect blending.
+ if (uniforms.use_linear_correction) {
+ // Short explanation of how this works:
+ //
+ // We get the luminances of the foreground and background colors,
+ // and then unlinearize them and perform blending on them. This
+ // gives us our desired luminance, which we derive our new alpha
+ // value from by mapping the range [bg_l, fg_l] to [0, 1], since
+ // our final blend will be a linear interpolation from bg to fg.
+ //
+ // This yields virtually identical results for grayscale blending,
+ // and very similar but non-identical results for color blending.
+ float4 bg = in.bg_color;
+ float fg_l = luminance(color.rgb);
+ float bg_l = luminance(bg.rgb);
+ // To avoid numbers going haywire, we don't apply correction
+ // when the bg and fg luminances are within 0.001 of each other.
+ if (abs(fg_l - bg_l) > 0.001) {
+ float blend_l = linearize(unlinearize(fg_l) * a + unlinearize(bg_l) * (1.0 - a));
+ a = clamp((blend_l - bg_l) / (fg_l - bg_l), 0.0, 1.0);
+ }
}
// Multiply our whole color by the alpha mask.
From 016a26cf984b16a987f45f27dd7dca0a5686c972 Mon Sep 17 00:00:00 2001
From: Qwerasd
Date: Mon, 27 Jan 2025 19:37:44 -0500
Subject: [PATCH 176/365] cleanup: rename `text-blending` to `alpha-blending`
+ correct docs
---
src/config/Config.zig | 27 ++++++++++++++-------------
src/renderer/Metal.zig | 4 ++--
2 files changed, 16 insertions(+), 15 deletions(-)
diff --git a/src/config/Config.zig b/src/config/Config.zig
index c7aeebb01..1e1bca74a 100644
--- a/src/config/Config.zig
+++ b/src/config/Config.zig
@@ -259,7 +259,8 @@ pub const renamed = std.StaticStringMap([]const u8).initComptime(&.{
/// What color space to use when performing alpha blending.
///
-/// This affects how text looks for different background/foreground color pairs.
+/// This affects the appearance of text and of any images with transparency.
+/// Additionally, custom shaders will receive colors in the configured space.
///
/// Valid values:
///
@@ -276,11 +277,7 @@ pub const renamed = std.StaticStringMap([]const u8).initComptime(&.{
/// * `linear-corrected` - Same as `linear`, but with a correction step applied
/// for text that makes it look nearly or completely identical to `native`,
/// but without any of the darkening artifacts.
-///
-/// Note: This setting affects more than just text, images will also be blended
-/// in the selected color space, and custom shaders will receive colors in that
-/// color space as well.
-@"text-blending": TextBlending = .native,
+@"alpha-blending": AlphaBlending = .native,
/// All of the configurations behavior adjust various metrics determined by the
/// font. The values can be integers (1, -1, etc.) or a percentage (20%, -15%,
@@ -1220,12 +1217,16 @@ keybind: Keybinds = .{},
/// This is currently only supported on macOS and Linux.
@"window-theme": WindowTheme = .auto,
-/// The colorspace to use for the terminal window. The default is `srgb` but
-/// this can also be set to `display-p3` to use the Display P3 colorspace.
+/// The color space to use when interpreting terminal colors. "Terminal colors"
+/// refers to colors specified in your configuration and colors produced by
+/// direct-color SGR sequences.
///
-/// Changing this value at runtime will only affect new windows.
+/// Valid values:
///
-/// This setting is only supported on macOS.
+/// * `srgb` - Interpret colors in the sRGB color space. This is the default.
+/// * `display-p3` - Interpret colors in the Display P3 color space.
+///
+/// This setting is currently only supported on macOS.
@"window-colorspace": WindowColorspace = .srgb,
/// The initial window size. This size is in terminal grid cells by default.
@@ -5825,13 +5826,13 @@ pub const GraphemeWidthMethod = enum {
unicode,
};
-/// See text-blending
-pub const TextBlending = enum {
+/// See alpha-blending
+pub const AlphaBlending = enum {
native,
linear,
@"linear-corrected",
- pub fn isLinear(self: TextBlending) bool {
+ pub fn isLinear(self: AlphaBlending) bool {
return switch (self) {
.native => false,
.linear, .@"linear-corrected" => true,
diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig
index dccccef17..866f9682d 100644
--- a/src/renderer/Metal.zig
+++ b/src/renderer/Metal.zig
@@ -391,7 +391,7 @@ pub const DerivedConfig = struct {
links: link.Set,
vsync: bool,
colorspace: configpkg.Config.WindowColorspace,
- blending: configpkg.Config.TextBlending,
+ blending: configpkg.Config.AlphaBlending,
pub fn init(
alloc_gpa: Allocator,
@@ -463,7 +463,7 @@ pub const DerivedConfig = struct {
.links = links,
.vsync = config.@"window-vsync",
.colorspace = config.@"window-colorspace",
- .blending = config.@"text-blending",
+ .blending = config.@"alpha-blending",
.arena = arena,
};
}
From a80cf3db9c51143f0055305703ca035bdbab6fdb Mon Sep 17 00:00:00 2001
From: Daniel Fox
Date: Tue, 28 Jan 2025 12:30:57 -0800
Subject: [PATCH 177/365] Fix older adwaita tab bars at top of window
---
src/apprt/gtk/Window.zig | 5 +----
1 file changed, 1 insertion(+), 4 deletions(-)
diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig
index 58f5659f0..1294ed416 100644
--- a/src/apprt/gtk/Window.zig
+++ b/src/apprt/gtk/Window.zig
@@ -335,10 +335,7 @@ pub fn init(self: *Window, app: *App) !void {
.top,
.left,
.right,
- => c.gtk_box_prepend(
- @ptrCast(box),
- @ptrCast(@alignCast(tab_bar)),
- ),
+ => c.gtk_box_insert_child_after(@ptrCast(box), @ptrCast(@alignCast(tab_bar)), @ptrCast(@alignCast(self.headerbar.asWidget()))),
.bottom => c.gtk_box_append(
@ptrCast(box),
From 71d0481da8da8cf366bfbabd4dd62e08d2d46de3 Mon Sep 17 00:00:00 2001
From: Lee Marlow
Date: Wed, 29 Jan 2025 11:38:35 -0700
Subject: [PATCH 178/365] Remove if check that was left in after change to
exhaustive switch
This came out of cmd+triple-click fix in PR #5373
---
src/terminal/Screen.zig | 3 ---
1 file changed, 3 deletions(-)
diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig
index fe2eab5ac..a838e0e10 100644
--- a/src/terminal/Screen.zig
+++ b/src/terminal/Screen.zig
@@ -2605,9 +2605,6 @@ pub fn selectOutput(self: *Screen, pin: Pin) ?Selection {
.input,
=> {},
}
- if (row.semantic_prompt == .command) {
- break;
- }
}
// Because the first line of command output may span multiple visual rows we must now
From 27b254db8a843b9ee4b090dc1a33c30fdb86f8f9 Mon Sep 17 00:00:00 2001
From: Mitchell Hashimoto
Date: Wed, 29 Jan 2025 13:12:11 -0800
Subject: [PATCH 179/365] apprt/gtk: set key modifier flag if physical modifier
key is pressed
Fixes #5191
---
src/apprt/gtk/Surface.zig | 1 +
src/apprt/gtk/key.zig | 57 +++++++++++++++++++++++++++++++++------
2 files changed, 50 insertions(+), 8 deletions(-)
diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig
index 3677c5e8d..1ca39425b 100644
--- a/src/apprt/gtk/Surface.zig
+++ b/src/apprt/gtk/Surface.zig
@@ -1757,6 +1757,7 @@ pub fn keyEvent(
event,
physical_key,
gtk_mods,
+ action,
&self.app.winproto,
);
diff --git a/src/apprt/gtk/key.zig b/src/apprt/gtk/key.zig
index 40c9ca9a4..60f12edca 100644
--- a/src/apprt/gtk/key.zig
+++ b/src/apprt/gtk/key.zig
@@ -108,6 +108,7 @@ pub fn eventMods(
event: *c.GdkEvent,
physical_key: input.Key,
gtk_mods: c.GdkModifierType,
+ action: input.Action,
app_winproto: *winproto.App,
) input.Mods {
const device = c.gdk_event_get_device(event);
@@ -115,15 +116,55 @@ pub fn eventMods(
var mods = app_winproto.eventMods(device, gtk_mods);
mods.num_lock = c.gdk_device_get_num_lock_state(device) == 1;
+ // We use the physical key to determine sided modifiers. As
+ // far as I can tell there's no other way to reliably determine
+ // this.
+ //
+ // We also set the main modifier to true if either side is true,
+ // since on both X11/Wayland, GTK doesn't set the main modifier
+ // if only the modifier key is pressed, but our core logic
+ // relies on it.
switch (physical_key) {
- .left_shift => mods.sides.shift = .left,
- .right_shift => mods.sides.shift = .right,
- .left_control => mods.sides.ctrl = .left,
- .right_control => mods.sides.ctrl = .right,
- .left_alt => mods.sides.alt = .left,
- .right_alt => mods.sides.alt = .right,
- .left_super => mods.sides.super = .left,
- .right_super => mods.sides.super = .right,
+ .left_shift => {
+ mods.shift = action != .release;
+ mods.sides.shift = .left;
+ },
+
+ .right_shift => {
+ mods.shift = action != .release;
+ mods.sides.shift = .right;
+ },
+
+ .left_control => {
+ mods.ctrl = action != .release;
+ mods.sides.ctrl = .left;
+ },
+
+ .right_control => {
+ mods.ctrl = action != .release;
+ mods.sides.ctrl = .right;
+ },
+
+ .left_alt => {
+ mods.alt = action != .release;
+ mods.sides.alt = .left;
+ },
+
+ .right_alt => {
+ mods.alt = action != .release;
+ mods.sides.alt = .right;
+ },
+
+ .left_super => {
+ mods.super = action != .release;
+ mods.sides.super = .left;
+ },
+
+ .right_super => {
+ mods.super = action != .release;
+ mods.sides.super = .right;
+ },
+
else => {},
}
From ce2a3773d2dc2e6461efd3f5fcca6d5888e69ba1 Mon Sep 17 00:00:00 2001
From: Mitchell Hashimoto
Date: Wed, 29 Jan 2025 14:08:04 -0800
Subject: [PATCH 180/365] input: performable bindings aren't part of the
reverse mapping
Fixes #4522
This is a bit of a hammer-meets-nail solution, but it's a simple
solution to the problem. The reverse mapping is used to find the
binding that an action is bound to, and it's used by apprt's to populate
the accelerator label in the UI.
The problem is that accelerators in GTK are handled early in the event
handling process and its difficult to get that event mapping to a
specific surface. Therefore, the "performable" prefix was not working.
On macOS, this issue didn't exist because there exists an OS mechanism
to install an event handler earlier than the menu system.
This commit changes the reverse mapping to only include bindings that
are not performable. This way, the keybind always reaches the surface
and can be handled by `Surface.keyCallback` which processes
`performable`.
The caveat is that performable bindings will not show up in the UI
for menu items. This is documented in this commit now. They still work,
its just a UI issue.
---
src/config/Config.zig | 6 +++++
src/input/Binding.zig | 56 +++++++++++++++++++++++++++++++++++++++----
2 files changed, 57 insertions(+), 5 deletions(-)
diff --git a/src/config/Config.zig b/src/config/Config.zig
index 1e1bca74a..0ed98bdea 100644
--- a/src/config/Config.zig
+++ b/src/config/Config.zig
@@ -1015,6 +1015,12 @@ class: ?[:0]const u8 = null,
/// performable (acting identically to not having a keybind set at
/// all).
///
+/// Performable keybinds will not appear as menu shortcuts in the
+/// application menu. This is because the menu shortcuts force the
+/// action to be performed regardless of the state of the terminal.
+/// Performable keybinds will still work, they just won't appear as
+/// a shortcut label in the menu.
+///
/// Keybind triggers are not unique per prefix combination. For example,
/// `ctrl+a` and `global:ctrl+a` are not two separate keybinds. The keybind
/// set later will overwrite the keybind set earlier. In this case, the
diff --git a/src/input/Binding.zig b/src/input/Binding.zig
index a1e759bf8..19c103195 100644
--- a/src/input/Binding.zig
+++ b/src/input/Binding.zig
@@ -284,11 +284,11 @@ pub const Action = union(enum) {
scroll_page_fractional: f32,
scroll_page_lines: i16,
- /// Adjust the current selection in a given direction. Does nothing if no
+ /// Adjust the current selection in a given direction. Does nothing if no
/// selection exists.
///
/// Arguments:
- /// - left, right, up, down, page_up, page_down, home, end,
+ /// - left, right, up, down, page_up, page_down, home, end,
/// beginning_of_line, end_of_line
///
/// Example: Extend selection to the right
@@ -1230,6 +1230,13 @@ pub const Set = struct {
/// This is a conscious decision since the primary use case of the reverse
/// map is to support GUI toolkit keyboard accelerators and no mainstream
/// GUI toolkit supports sequences.
+ ///
+ /// Performable triggers are also not present in the reverse map. This
+ /// is so that GUI toolkits don't register performable triggers as
+ /// menu shortcuts (the primary use case of the reverse map). GUI toolkits
+ /// such as GTK handle menu shortcuts too early in the event lifecycle
+ /// for performable to work so this is a conscious decision to ease the
+ /// integration with GUI toolkits.
reverse: ReverseMap = .{},
/// The entry type for the forward mapping of trigger to action.
@@ -1494,6 +1501,11 @@ pub const Set = struct {
// unbind should never go into the set, it should be handled prior
assert(action != .unbind);
+ // This is true if we're going to track this entry as
+ // a reverse mapping. There are certain scenarios we don't.
+ // See the reverse map docs for more information.
+ const track_reverse: bool = !flags.performable;
+
const gop = try self.bindings.getOrPut(alloc, t);
if (gop.found_existing) switch (gop.value_ptr.*) {
@@ -1505,7 +1517,7 @@ pub const Set = struct {
// If we have an existing binding for this trigger, we have to
// update the reverse mapping to remove the old action.
- .leaf => {
+ .leaf => if (track_reverse) {
const t_hash = t.hash();
var it = self.reverse.iterator();
while (it.next()) |reverse_entry| it: {
@@ -1522,8 +1534,9 @@ pub const Set = struct {
.flags = flags,
} };
errdefer _ = self.bindings.remove(t);
- try self.reverse.put(alloc, action, t);
- errdefer _ = self.reverse.remove(action);
+
+ if (track_reverse) try self.reverse.put(alloc, action, t);
+ errdefer if (track_reverse) self.reverse.remove(action);
}
/// Get a binding for a given trigger.
@@ -2373,6 +2386,39 @@ test "set: maintains reverse mapping" {
}
}
+test "set: performable is not part of reverse mappings" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var s: Set = .{};
+ defer s.deinit(alloc);
+
+ try s.put(alloc, .{ .key = .{ .translated = .a } }, .{ .new_window = {} });
+ {
+ const trigger = s.getTrigger(.{ .new_window = {} }).?;
+ try testing.expect(trigger.key.translated == .a);
+ }
+
+ // trigger should be non-performable
+ try s.putFlags(
+ alloc,
+ .{ .key = .{ .translated = .b } },
+ .{ .new_window = {} },
+ .{ .performable = true },
+ );
+ {
+ const trigger = s.getTrigger(.{ .new_window = {} }).?;
+ try testing.expect(trigger.key.translated == .a);
+ }
+
+ // removal of performable should do nothing
+ s.remove(alloc, .{ .key = .{ .translated = .b } });
+ {
+ const trigger = s.getTrigger(.{ .new_window = {} }).?;
+ try testing.expect(trigger.key.translated == .a);
+ }
+}
+
test "set: overriding a mapping updates reverse" {
const testing = std.testing;
const alloc = testing.allocator;
From 692168f8dda6c05561744d978e3a7ed45adc56a8 Mon Sep 17 00:00:00 2001
From: Mitchell Hashimoto
Date: Wed, 29 Jan 2025 15:08:20 -0800
Subject: [PATCH 181/365] dist: remove cdata tags from appcast, we escape it
all
---
dist/macos/update_appcast_tag.py | 4 ++--
dist/macos/update_appcast_tip.py | 4 ++--
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/dist/macos/update_appcast_tag.py b/dist/macos/update_appcast_tag.py
index 6abfb66db..2cb20dd5d 100644
--- a/dist/macos/update_appcast_tag.py
+++ b/dist/macos/update_appcast_tag.py
@@ -87,7 +87,7 @@ elem = ET.SubElement(item, "sparkle:fullReleaseNotesLink")
elem.text = f"https://ghostty.org/docs/install/release-notes/{version_dash}"
elem = ET.SubElement(item, "description")
elem.text = f"""
-Ghostty v{version}
+Ghostty v{version}
This release was built from commit {commit}
on {now.strftime('%Y-%m-%d')}.
@@ -96,7 +96,7 @@ on {now.strftime('%Y-%m-%d')}.
We don't currently generate release notes for auto-updates.
You can view the complete changelog and release notes
at ghostty.org/docs/install/release-notes/{version_dash}.
-
]]>
+
"""
elem = ET.SubElement(item, "enclosure")
elem.set("url", f"https://release.files.ghostty.org/{version}/Ghostty.dmg")
diff --git a/dist/macos/update_appcast_tip.py b/dist/macos/update_appcast_tip.py
index c6a20f75f..ff1fb4be5 100644
--- a/dist/macos/update_appcast_tip.py
+++ b/dist/macos/update_appcast_tip.py
@@ -83,7 +83,7 @@ elem = ET.SubElement(item, "sparkle:minimumSystemVersion")
elem.text = "13.0.0"
elem = ET.SubElement(item, "description")
elem.text = f"""
-
+
Automated build from commit {commit}
on {now.strftime('%Y-%m-%d')}.
@@ -91,7 +91,7 @@ on {now.strftime('%Y-%m-%d')}.
These are automatic per-commit builds generated from the main Git branch.
We do not generate any release notes for these builds. You can view the full
commit history on GitHub for all changes.
-]]>
+
"""
elem = ET.SubElement(item, "enclosure")
elem.set("url", f"https://tip.files.ghostty.org/{commit_long}/Ghostty.dmg")
From dbc4edc58347b68d1064246434e50847d160151a Mon Sep 17 00:00:00 2001
From: Mitchell Hashimoto
Date: Wed, 29 Jan 2025 16:37:59 -0800
Subject: [PATCH 182/365] ci: separate publish and release workflows
This now requires a separate manually triggered `publish` workflow to be
run after the release completes in order to transition the release to
the `published` state.
Practically today this only means that the release will be published to
the macOS auto-updater, but in the future we could add additional steps
such as creating a GH release or some other notifications.
Importantly, this lets us verify the release in the uploaded location
before general users are notified of the update.
---
.github/workflows/publish-tag.yml | 74 +++++++++++++++++++++++++++++++
.github/workflows/release-tag.yml | 17 +------
2 files changed, 76 insertions(+), 15 deletions(-)
create mode 100644 .github/workflows/publish-tag.yml
diff --git a/.github/workflows/publish-tag.yml b/.github/workflows/publish-tag.yml
new file mode 100644
index 000000000..160034a52
--- /dev/null
+++ b/.github/workflows/publish-tag.yml
@@ -0,0 +1,74 @@
+on:
+ workflow_dispatch:
+ inputs:
+ version:
+ description: "Version to deploy (format: vX.Y.Z)"
+ required: true
+
+name: Publish Tagged Release
+
+# We must only run one release workflow at a time to prevent corrupting
+# our release artifacts.
+concurrency:
+ group: ${{ github.workflow }}
+ cancel-in-progress: false
+
+jobs:
+ setup:
+ runs-on: namespace-profile-ghostty-sm
+ outputs:
+ version: ${{ steps.extract_version.outputs.version }}
+ steps:
+ - name: Validate Version Input
+ run: |
+ if [[ ! "${{ github.event.inputs.version }}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
+ echo "Error: Version must follow the format vX.Y.Z (e.g., v1.0.0)."
+ exit 1
+ fi
+
+ echo "Version is valid: ${{ github.event.inputs.version }}"
+
+ - name: Exract the Version
+ id: extract_version
+ run: |
+ VERSION=${{ github.event.inputs.version }}
+ VERSION=${VERSION#v}
+ echo "version=$VERSION" >> $GITHUB_OUTPUT
+
+ upload:
+ needs: [setup]
+ runs-on: namespace-profile-ghostty-sm
+ env:
+ GHOSTTY_VERSION: ${{ needs.setup.outputs.version }}
+ steps:
+ - name: Validate Release Files
+ run: |
+ BASE="https://release.files.ghostty.org/${GHOSTTY_VERSION}"
+ curl -I -s -o /dev/null -w "%{http_code}" "${BASE}/ghostty-${GHOSTTY_VERSION}.tar.gz" | grep -q "^200$" || exit 1
+ curl -I -s -o /dev/null -w "%{http_code}" "${BASE}/ghostty-${GHOSTTY_VERSION}.tar.gz.minisig" | grep -q "^200$" || exit 1
+ curl -I -s -o /dev/null -w "%{http_code}" "${BASE}/ghostty-source.tar.gz" | grep -q "^200$" || exit 1
+ curl -I -s -o /dev/null -w "%{http_code}" "${BASE}/ghostty-source.tar.gz.minisig" | grep -q "^200$" || exit 1
+ curl -I -s -o /dev/null -w "%{http_code}" "${BASE}/ghostty-macos-universal.zip" | grep -q "^200$" || exit 1
+ curl -I -s -o /dev/null -w "%{http_code}" "${BASE}/ghostty-macos-universal-dsym.zip" | grep -q "^200$" || exit 1
+ curl -I -s -o /dev/null -w "%{http_code}" "${BASE}/Ghostty.dmg" | grep -q "^200$" || exit 1
+ curl -I -s -o /dev/null -w "%{http_code}" "${BASE}/appcast-staged.xml" | grep -q "^200$" || exit 1
+
+ - name: Download Staged Appcast
+ run: |
+ curl -L https://release.files.ghostty.org/${GHOSTTY_VERSION}/appcast-staged.xml
+ mv appcast-staged.xml appcast.xml
+
+ - name: Upload Appcast
+ run: |
+ rm -rf blob
+ mkdir blob
+ mv appcast.xml blob/appcast.xml
+ - name: Upload Appcast to R2
+ uses: ryand56/r2-upload-action@latest
+ with:
+ r2-account-id: ${{ secrets.CF_R2_RELEASE_ACCOUNT_ID }}
+ r2-access-key-id: ${{ secrets.CF_R2_RELEASE_AWS_KEY }}
+ r2-secret-access-key: ${{ secrets.CF_R2_RELEASE_SECRET_KEY }}
+ r2-bucket: ghostty-release
+ source-dir: blob
+ destination-dir: ./
diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml
index cf94bf23e..0767152f5 100644
--- a/.github/workflows/release-tag.yml
+++ b/.github/workflows/release-tag.yml
@@ -7,6 +7,7 @@ on:
upload:
description: "Upload final artifacts to R2"
default: false
+
push:
tags:
- "v[0-9]+.[0-9]+.[0-9]+"
@@ -367,6 +368,7 @@ jobs:
mv ghostty-macos-universal.zip blob/${GHOSTTY_VERSION}/ghostty-macos-universal.zip
mv ghostty-macos-universal-dsym.zip blob/${GHOSTTY_VERSION}/ghostty-macos-universal-dsym.zip
mv Ghostty.dmg blob/${GHOSTTY_VERSION}/Ghostty.dmg
+ mv appcast.xml blob/${GHOSTTY_VERSION}/appcast-staged.xml
- name: Upload to R2
uses: ryand56/r2-upload-action@latest
with:
@@ -376,18 +378,3 @@ jobs:
r2-bucket: ghostty-release
source-dir: blob
destination-dir: ./
-
- - name: Prep Appcast
- run: |
- rm -rf blob
- mkdir blob
- mv appcast.xml blob/appcast.xml
- - name: Upload Appcast to R2
- uses: ryand56/r2-upload-action@latest
- with:
- r2-account-id: ${{ secrets.CF_R2_RELEASE_ACCOUNT_ID }}
- r2-access-key-id: ${{ secrets.CF_R2_RELEASE_AWS_KEY }}
- r2-secret-access-key: ${{ secrets.CF_R2_RELEASE_SECRET_KEY }}
- r2-bucket: ghostty-release
- source-dir: blob
- destination-dir: ./
From a62b26cd2fac4d3685aeec6aeebeaf2ad5300fc9 Mon Sep 17 00:00:00 2001
From: Mitchell Hashimoto
Date: Thu, 30 Jan 2025 07:18:07 -0800
Subject: [PATCH 183/365] next version will be 1.1.0
---
build.zig.zon | 2 +-
nix/package.nix | 2 +-
src/build/Config.zig | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/build.zig.zon b/build.zig.zon
index a8f45e6ea..5839b090f 100644
--- a/build.zig.zon
+++ b/build.zig.zon
@@ -1,6 +1,6 @@
.{
.name = "ghostty",
- .version = "1.0.2",
+ .version = "1.1.0",
.paths = .{""},
.dependencies = .{
// Zig libs
diff --git a/nix/package.nix b/nix/package.nix
index 2f7825a56..6f69be8d7 100644
--- a/nix/package.nix
+++ b/nix/package.nix
@@ -114,7 +114,7 @@
in
stdenv.mkDerivation (finalAttrs: {
pname = "ghostty";
- version = "1.0.2";
+ version = "1.1.0";
inherit src;
nativeBuildInputs =
diff --git a/src/build/Config.zig b/src/build/Config.zig
index c6f0e6d09..c832b77ad 100644
--- a/src/build/Config.zig
+++ b/src/build/Config.zig
@@ -19,7 +19,7 @@ const GitVersion = @import("GitVersion.zig");
/// TODO: When Zig 0.14 is released, derive this from build.zig.zon directly.
/// Until then this MUST match build.zig.zon and should always be the
/// _next_ version to release.
-const app_version: std.SemanticVersion = .{ .major = 1, .minor = 0, .patch = 2 };
+const app_version: std.SemanticVersion = .{ .major = 1, .minor = 1, .patch = 0 };
/// Standard build configuration options.
optimize: std.builtin.OptimizeMode,
From 40973417d040135be6740ab7cf4a4e2d0f85fe81 Mon Sep 17 00:00:00 2001
From: "Jeffrey C. Ollie"
Date: Thu, 30 Jan 2025 10:16:45 -0600
Subject: [PATCH 184/365] nix: use --system build flag instead of relying on
$ZIG_GLOBAL_CACHE_DIR
Fixes #5431
---
nix/package.nix | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/nix/package.nix b/nix/package.nix
index 2f7825a56..0974ff9c9 100644
--- a/nix/package.nix
+++ b/nix/package.nix
@@ -40,7 +40,7 @@
# ultimately acted on and has made its way to a nixpkgs implementation, this
# can probably be removed in favor of that.
zig_hook = zig_0_13.hook.overrideAttrs {
- zig_default_flags = "-Dcpu=baseline -Doptimize=${optimize}";
+ zig_default_flags = "-Dcpu=baseline -Doptimize=${optimize} --color off";
};
# We limit source like this to try and reduce the amount of rebuilds as possible
@@ -162,13 +162,13 @@ in
dontConfigure = true;
- zigBuildFlags = "-Dversion-string=${finalAttrs.version}-${revision}-nix -Dgtk-x11=${lib.boolToString enableX11} -Dgtk-wayland=${lib.boolToString enableWayland}";
-
- preBuild = ''
- rm -rf $ZIG_GLOBAL_CACHE_DIR
- cp -r --reflink=auto ${zigCache} $ZIG_GLOBAL_CACHE_DIR
- chmod u+rwX -R $ZIG_GLOBAL_CACHE_DIR
- '';
+ zigBuildFlags = [
+ "--system"
+ "${zigCache}/p"
+ "-Dversion-string=${finalAttrs.version}-${revision}-nix"
+ "-Dgtk-x11=${lib.boolToString enableX11}"
+ "-Dgtk-wayland=${lib.boolToString enableWayland}"
+ ];
outputs = [
"out"
From 6dd9bf0038238a6817e930f5664878f422156f2e Mon Sep 17 00:00:00 2001
From: "Jeffrey C. Ollie"
Date: Thu, 30 Jan 2025 11:04:44 -0600
Subject: [PATCH 185/365] nix: fix Ghostty homepage in package
---
nix/package.nix | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/nix/package.nix b/nix/package.nix
index 8ab9ad9dd..dae87ed81 100644
--- a/nix/package.nix
+++ b/nix/package.nix
@@ -202,7 +202,7 @@ in
'';
meta = {
- homepage = "https://github.com/ghostty-org/ghostty";
+ homepage = "https://ghostty.org";
license = lib.licenses.mit;
platforms = [
"x86_64-linux"
From 48a1a10330ef82f6662079565ffd6adf6dcf7fdf Mon Sep 17 00:00:00 2001
From: Leah Amelia Chen
Date: Thu, 30 Jan 2025 19:59:46 +0100
Subject: [PATCH 186/365] gtk(x11): update blur region upon syncAppearance
Fixes a bug where the blur region offset used to accomodate CSDs
are applied even when CSDs are disabled.
---
src/apprt/gtk/Window.zig | 8 ++++++-
src/apprt/gtk/winproto/x11.zig | 43 +++++++++++++++-------------------
2 files changed, 26 insertions(+), 25 deletions(-)
diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig
index 58f5659f0..28bbfe54f 100644
--- a/src/apprt/gtk/Window.zig
+++ b/src/apprt/gtk/Window.zig
@@ -661,8 +661,9 @@ fn gtkWindowNotifyMaximized(
fn gtkWindowNotifyDecorated(
object: *c.GObject,
_: *c.GParamSpec,
- _: ?*anyopaque,
+ ud: ?*anyopaque,
) callconv(.C) void {
+ const self = userdataSelf(ud orelse return);
const is_decorated = c.gtk_window_get_decorated(@ptrCast(object)) == 1;
// Fix any artifacting that may occur in window corners. The .ssd CSS
@@ -671,6 +672,11 @@ fn gtkWindowNotifyDecorated(
// for .ssd is provided by GTK and Adwaita.
toggleCssClass(@ptrCast(object), "ssd", !is_decorated);
toggleCssClass(@ptrCast(object), "no-border-radius", !is_decorated);
+
+ // FIXME: This is to update the blur region offset on X11.
+ // Remove this when we move everything related to window appearance
+ // to `syncAppearance` for Ghostty 1.2.
+ self.winproto.syncAppearance() catch {};
}
fn gtkWindowNotifyFullscreened(
diff --git a/src/apprt/gtk/winproto/x11.zig b/src/apprt/gtk/winproto/x11.zig
index 7a6b8b4c7..e7137a74d 100644
--- a/src/apprt/gtk/winproto/x11.zig
+++ b/src/apprt/gtk/winproto/x11.zig
@@ -157,7 +157,7 @@ pub const Window = struct {
config: DerivedConfig,
window: c.Window,
gtk_window: *c.GtkWindow,
- blur_region: Region,
+ blur_region: Region = .{},
const DerivedConfig = struct {
blur: bool,
@@ -190,34 +190,11 @@ pub const Window = struct {
c.gdk_x11_surface_get_type(),
) == 0) return error.NotX11Surface;
- const blur_region: Region = blur: {
- if ((comptime !adwaita.versionAtLeast(0, 0, 0)) or
- !adwaita.enabled(config)) break :blur .{};
-
- // NOTE(pluiedev): CSDs are a f--king mistake.
- // Please, GNOME, stop this nonsense of making a window ~30% bigger
- // internally than how they really are just for your shadows and
- // rounded corners and all that fluff. Please. I beg of you.
- var x: f64 = 0;
- var y: f64 = 0;
- c.gtk_native_get_surface_transform(
- @ptrCast(gtk_window),
- &x,
- &y,
- );
-
- break :blur .{
- .x = @intFromFloat(x),
- .y = @intFromFloat(y),
- };
- };
-
return .{
.app = app,
.config = DerivedConfig.init(config),
.window = c.gdk_x11_surface_get_xid(surface),
.gtk_window = gtk_window,
- .blur_region = blur_region,
};
}
@@ -241,6 +218,24 @@ pub const Window = struct {
}
pub fn syncAppearance(self: *Window) !void {
+ self.blur_region = blur: {
+ // NOTE(pluiedev): CSDs are a f--king mistake.
+ // Please, GNOME, stop this nonsense of making a window ~30% bigger
+ // internally than how they really are just for your shadows and
+ // rounded corners and all that fluff. Please. I beg of you.
+ var x: f64 = 0;
+ var y: f64 = 0;
+ c.gtk_native_get_surface_transform(
+ @ptrCast(self.gtk_window),
+ &x,
+ &y,
+ );
+
+ break :blur .{
+ .x = @intFromFloat(x),
+ .y = @intFromFloat(y),
+ };
+ };
try self.syncBlur();
}
From dda242c96e029a0f1e5c10491daa62d2ccc18503 Mon Sep 17 00:00:00 2001
From: Mitchell Hashimoto
Date: Thu, 30 Jan 2025 13:58:11 -0800
Subject: [PATCH 187/365] ci: update publish job
---
.github/workflows/publish-tag.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/publish-tag.yml b/.github/workflows/publish-tag.yml
index 160034a52..458982140 100644
--- a/.github/workflows/publish-tag.yml
+++ b/.github/workflows/publish-tag.yml
@@ -55,7 +55,7 @@ jobs:
- name: Download Staged Appcast
run: |
- curl -L https://release.files.ghostty.org/${GHOSTTY_VERSION}/appcast-staged.xml
+ curl -L https://release.files.ghostty.org/${GHOSTTY_VERSION}/appcast-staged.xml > appcast-staged.xml
mv appcast-staged.xml appcast.xml
- name: Upload Appcast
From c5508e7d1922842ecd3160ea73b97da1282168b3 Mon Sep 17 00:00:00 2001
From: Mitchell Hashimoto
Date: Thu, 30 Jan 2025 14:23:17 -0800
Subject: [PATCH 188/365] update version for development
---
build.zig.zon | 2 +-
nix/package.nix | 2 +-
src/build/Config.zig | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/build.zig.zon b/build.zig.zon
index 5839b090f..cf16b35ae 100644
--- a/build.zig.zon
+++ b/build.zig.zon
@@ -1,6 +1,6 @@
.{
.name = "ghostty",
- .version = "1.1.0",
+ .version = "1.1.1",
.paths = .{""},
.dependencies = .{
// Zig libs
diff --git a/nix/package.nix b/nix/package.nix
index dae87ed81..ceb6a7688 100644
--- a/nix/package.nix
+++ b/nix/package.nix
@@ -114,7 +114,7 @@
in
stdenv.mkDerivation (finalAttrs: {
pname = "ghostty";
- version = "1.1.0";
+ version = "1.1.1";
inherit src;
nativeBuildInputs =
diff --git a/src/build/Config.zig b/src/build/Config.zig
index c832b77ad..0ff0fc914 100644
--- a/src/build/Config.zig
+++ b/src/build/Config.zig
@@ -19,7 +19,7 @@ const GitVersion = @import("GitVersion.zig");
/// TODO: When Zig 0.14 is released, derive this from build.zig.zon directly.
/// Until then this MUST match build.zig.zon and should always be the
/// _next_ version to release.
-const app_version: std.SemanticVersion = .{ .major = 1, .minor = 1, .patch = 0 };
+const app_version: std.SemanticVersion = .{ .major = 1, .minor = 1, .patch = 1 };
/// Standard build configuration options.
optimize: std.builtin.OptimizeMode,
From ac582ccf7c8fa1c3185643795ea30b29aad52004 Mon Sep 17 00:00:00 2001
From: Leah Amelia Chen
Date: Fri, 31 Jan 2025 12:03:44 +0100
Subject: [PATCH 189/365] gtk(wayland): respect window-decoration=none on GNOME
This is, admittedly, a very silly bug. On GNOME the SSD protocol is not
available and past me just decided to always enable CSDs in that case,
*even when* `window-decoration = none`. I now question my own intelligence.
---
src/apprt/gtk/winproto/wayland.zig | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/src/apprt/gtk/winproto/wayland.zig b/src/apprt/gtk/winproto/wayland.zig
index 8df3e57b3..3e239eb29 100644
--- a/src/apprt/gtk/winproto/wayland.zig
+++ b/src/apprt/gtk/winproto/wayland.zig
@@ -251,12 +251,13 @@ pub const Window = struct {
}
pub fn clientSideDecorationEnabled(self: Window) bool {
- // Compositor doesn't support the SSD protocol
- if (self.decoration == null) return true;
-
return switch (self.getDecorationMode()) {
.Client => true,
- .Server, .None => false,
+ // If we support SSDs, then we should *not* enable CSDs if we prefer SSDs.
+ // However, if we do not support SSDs (e.g. GNOME) then we should enable
+ // CSDs even if the user prefers SSDs.
+ .Server => if (self.app_context.kde_decoration_manager) |_| false else true,
+ .None => false,
else => unreachable,
};
}
From 660d9dc6718d7a18ad157dcd64be7b7de8518701 Mon Sep 17 00:00:00 2001
From: "Jeffrey C. Ollie"
Date: Fri, 31 Jan 2025 09:14:50 -0600
Subject: [PATCH 190/365] linux: fix installation path of nautilus-python
extension
Fixes #5468
---
src/build/GhosttyResources.zig | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/src/build/GhosttyResources.zig b/src/build/GhosttyResources.zig
index a7ff40cbd..912308e46 100644
--- a/src/build/GhosttyResources.zig
+++ b/src/build/GhosttyResources.zig
@@ -209,10 +209,11 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources {
"share/kio/servicemenus/com.mitchellh.ghostty.desktop",
).step);
- // Right click menu action for Nautilus
+ // Right click menu action for Nautilus. Note that this _must_ be named
+ // `ghostty.py`. Using the full app id causes problems (see #5468).
try steps.append(&b.addInstallFile(
b.path("dist/linux/ghostty_nautilus.py"),
- "share/nautilus-python/extensions/com.mitchellh.ghostty.py",
+ "share/nautilus-python/extensions/ghostty.py",
).step);
// Various icons that our application can use, including the icon
From 5100f4ff7da6eaa94aa7b01ee9f25c96f561b9ca Mon Sep 17 00:00:00 2001
From: mitchellh <1299+mitchellh@users.noreply.github.com>
Date: Sun, 2 Feb 2025 00:10:42 +0000
Subject: [PATCH 191/365] deps: Update iTerm2 color schemes
---
build.zig.zon | 4 ++--
nix/zigCacheHash.nix | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/build.zig.zon b/build.zig.zon
index cf16b35ae..a5abdc586 100644
--- a/build.zig.zon
+++ b/build.zig.zon
@@ -79,8 +79,8 @@
// Other
.apple_sdk = .{ .path = "./pkg/apple-sdk" },
.iterm2_themes = .{
- .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/0e23daf59234fc892cba949562d7bf69204594bb.tar.gz",
- .hash = "12204fc99743d8232e691ac22e058519bfc6ea92de4a11c6dba59b117531c847cd6a",
+ .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/db227d159adc265818f2e898da0f70ef8d7b580e.tar.gz",
+ .hash = "12203d2647e5daf36a9c85b969e03f422540786ce9ea624eb4c26d204fe1f46218f3",
},
},
}
diff --git a/nix/zigCacheHash.nix b/nix/zigCacheHash.nix
index 66b8eb8b6..975605049 100644
--- a/nix/zigCacheHash.nix
+++ b/nix/zigCacheHash.nix
@@ -1,3 +1,3 @@
# This file is auto-generated! check build-support/check-zig-cache-hash.sh for
# more details.
-"sha256-Bjy31evaKgpRX1mGwAFkai44eiiorTV1gW3VdP9Ins8="
+"sha256-I7uuv0MkaW3gWAw6NHci+II42OfM7NdtKh2Npw2pTis="
From bc156266c6a653a6d6071a73a3f1cca6131b285d Mon Sep 17 00:00:00 2001
From: rhodes-b <59537185+rhodes-b@users.noreply.github.com>
Date: Sun, 2 Feb 2025 16:27:10 -0600
Subject: [PATCH 192/365] fix errno handle
---
src/os/cgroup.zig | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/os/cgroup.zig b/src/os/cgroup.zig
index bef101acc..095c90aa3 100644
--- a/src/os/cgroup.zig
+++ b/src/os/cgroup.zig
@@ -130,7 +130,7 @@ pub fn cloneInto(cgroup: []const u8) !posix.pid_t {
};
const rc = linux.syscall2(linux.SYS.clone3, @intFromPtr(&args), @sizeOf(@TypeOf(args)));
- return switch (posix.errno(rc)) {
+ return switch (std.os.linux.E.init(rc)) {
.SUCCESS => @as(posix.pid_t, @intCast(rc)),
else => |errno| err: {
log.err("unable to clone: {}", .{errno});
From fb2516fbf68a171ffd3223cc46ffb423518bed90 Mon Sep 17 00:00:00 2001
From: rhodes-b <59537185+rhodes-b@users.noreply.github.com>
Date: Sun, 2 Feb 2025 16:35:20 -0600
Subject: [PATCH 193/365] comment on why to not use posix.errno
---
src/os/cgroup.zig | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/os/cgroup.zig b/src/os/cgroup.zig
index 095c90aa3..d94a4a728 100644
--- a/src/os/cgroup.zig
+++ b/src/os/cgroup.zig
@@ -130,6 +130,7 @@ pub fn cloneInto(cgroup: []const u8) !posix.pid_t {
};
const rc = linux.syscall2(linux.SYS.clone3, @intFromPtr(&args), @sizeOf(@TypeOf(args)));
+ // do not use posix.errno, when linking libc it will use the libc errno which will not be set when making the syscall directly
return switch (std.os.linux.E.init(rc)) {
.SUCCESS => @as(posix.pid_t, @intCast(rc)),
else => |errno| err: {
From 1bd5ea6f7304ad004167254b9f7fb917e6c3e82a Mon Sep 17 00:00:00 2001
From: Kat <65649991+00-kat@users.noreply.github.com>
Date: Mon, 3 Feb 2025 05:58:48 +0000
Subject: [PATCH 194/365] Fix a typo in the docs for
`adjust-strikethrough-position` and `adjust-overline-position`
---
src/config/Config.zig | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/config/Config.zig b/src/config/Config.zig
index 0ed98bdea..f99c4594b 100644
--- a/src/config/Config.zig
+++ b/src/config/Config.zig
@@ -317,14 +317,14 @@ pub const renamed = std.StaticStringMap([]const u8).initComptime(&.{
/// See the notes about adjustments in `adjust-cell-width`.
@"adjust-underline-thickness": ?MetricModifier = null,
/// Distance in pixels or percentage adjustment from the top of the cell to the top of the strikethrough.
-/// Increase to move strikethrough DOWN, decrease to move underline UP.
+/// Increase to move strikethrough DOWN, decrease to move strikethrough UP.
/// See the notes about adjustments in `adjust-cell-width`.
@"adjust-strikethrough-position": ?MetricModifier = null,
/// Thickness in pixels or percentage adjustment of the strikethrough.
/// See the notes about adjustments in `adjust-cell-width`.
@"adjust-strikethrough-thickness": ?MetricModifier = null,
/// Distance in pixels or percentage adjustment from the top of the cell to the top of the overline.
-/// Increase to move overline DOWN, decrease to move underline UP.
+/// Increase to move overline DOWN, decrease to move overline UP.
/// See the notes about adjustments in `adjust-cell-width`.
@"adjust-overline-position": ?MetricModifier = null,
/// Thickness in pixels or percentage adjustment of the overline.
From f70ba14abd989938ba7183872a698ade67ceecf9 Mon Sep 17 00:00:00 2001
From: Mitchell Hashimoto
Date: Mon, 3 Feb 2025 08:19:08 -0800
Subject: [PATCH 195/365] apprt/gtk: handle input methods that end preedit
before commit
Fixes #5494
When ibus/fcitx is not running (the GTK "simple" input method is
active), the preedit end event triggers _before_ the commit event. For
both ibus/fcitx, the opposite is true. We were relying on this ordering.
This commit changes the GTK input handling to not rely on this ordering.
Instead, we encode our composing state into the boolean state of whether
a key event is pressed. This happens before ANY input method events are
triggered.
Tested dead key handling on: X11/Wayland, ibus/fcitx/none.
---
CONTRIBUTING.md | 83 +++++++++++++++++++++++++++
src/apprt/gtk/Surface.zig | 114 +++++++++++++++++++++++++-------------
2 files changed, 160 insertions(+), 37 deletions(-)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index a7233b2c2..e4d148df8 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -78,6 +78,89 @@ pull request will be accepted with a high degree of certainty.
> not open a WIP pull request to discuss a feature. Instead, use a discussion
> and link to your branch.
+# Developer Guide
+
+> [!NOTE]
+>
+> **The remainder of this file is dedicated to developers actively
+> working on Ghostty.** If you're a user reporting an issue, you can
+> ignore the rest of this document.
+
+## Input Stack Testing
+
+The input stack is the part of the codebase that starts with a
+key event and ends with text encoding being sent to the pty (it
+does not include _rendering_ the text, which is part of the
+font or rendering stack).
+
+If you modify any part of the input stack, you must manually verify
+all the following input cases work properly. We unfortunately do
+not automate this in any way, but if we can do that one day that'd
+save a LOT of grief and time.
+
+Note: this list may not be exhaustive, I'm still working on it.
+
+### Linux IME
+
+IME (Input Method Editors) are a common source of bugs in the input stack,
+especially on Linux since there are multiple different IME systems
+interacting with different windowing systems and application frameworks
+all written by different organizations.
+
+The following matrix should be tested to ensure that all IME input works
+properly:
+
+1. Wayland, X11
+2. ibus, fcitx, none
+3. Dead key input (e.g. Spanish), CJK (e.g. Japanese), Emoji, Unicode Hex
+4. ibus versions: 1.5.29, 1.5.30, 1.5.31 (each exhibit slightly different behaviors)
+
+> [!NOTE]
+>
+> This is a **work in progress**. I'm still working on this list and it
+> is not complete. As I find more test cases, I will add them here.
+
+#### Dead Key Input
+
+Set your keyboard layout to "Spanish" (or another layout that uses dead keys).
+
+1. Launch Ghostty
+2. Press `'`
+3. Press `a`
+4. Verify that `á` is displayed
+
+Note that the dead key may or may not show a preedit state visually.
+For ibus and fcitx it does but for the "none" case it does not. Importantly,
+the text should be correct when it is sent to the pty.
+
+We should also test canceling dead key input:
+
+1. Launch Ghostty
+2. Press `'`
+3. Press escape
+4. Press `a`
+5. Verify that `a` is displayed (no diacritic)
+
+#### CJK Input
+
+Configure fcitx or ibus with a keyboard layout like Japanese or Mozc. The
+exact layout doesn't matter.
+
+1. Launch Ghostty
+2. Press `Ctrl+Shift` to switch to "Hiragana"
+3. On a US physical layout, type: `konn`, you should see `こん` in preedit.
+4. Press `Enter`
+5. Verify that `こん` is displayed in the terminal.
+
+We should also test switching input methods while preedit is active, which
+should commit the text:
+
+1. Launch Ghostty
+2. Press `Ctrl+Shift` to switch to "Hiragana"
+3. On a US physical layout, type: `konn`, you should see `こん` in preedit.
+4. Press `Ctrl+Shift` to switch to another layout (any)
+5. Verify that `こん` is displayed in the terminal as committed text.
+
## Nix Virtual Machines
Several Nix virtual machine definitions are provided by the project for testing
diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig
index 1ca39425b..b34ca9aa3 100644
--- a/src/apprt/gtk/Surface.zig
+++ b/src/apprt/gtk/Surface.zig
@@ -368,7 +368,7 @@ cursor_pos: apprt.CursorPos,
inspector: ?*inspector.Inspector = null,
/// Key input states. See gtkKeyPressed for detailed descriptions.
-in_keyevent: bool = false,
+in_keyevent: IMKeyEvent = .false,
im_context: *c.GtkIMContext,
im_composing: bool = false,
im_buf: [128]u8 = undefined,
@@ -378,6 +378,20 @@ im_len: u7 = 0,
/// details on what this is.
cgroup_path: ?[]const u8 = null,
+/// The state of the key event while we're doing IM composition.
+/// See gtkKeyPressed for detailed descriptions.
+pub const IMKeyEvent = enum {
+ /// Not in a key event.
+ false,
+
+ /// In a key event but im_composing was either true or false
+ /// prior to the calling IME processing. This is important to
+ /// work around different input methods calling commit and
+ /// preedit end in a different order.
+ composing,
+ not_composing,
+};
+
/// Configuration used for initializing the surface. We have to copy some
/// data since initialization is delayed with GTK (on realize).
pub const InitConfig = struct {
@@ -1658,16 +1672,29 @@ pub fn keyEvent(
.height = 1,
});
- // Pass the event through the IM controller. This will return true
- // if the input method handled the event.
+ // We note that we're in a keypress because we want some logic to
+ // depend on this. For example, we don't want to send character events
+ // like "a" via the input "commit" event if we're actively processing
+ // a keypress because we'd lose access to the keycode information.
//
+ // We have to maintain some additional state here of whether we
+ // were composing because different input methods call the callbacks
+ // in different orders. For example, ibus calls commit THEN preedit
+ // end but simple calls preedit end THEN commit.
+ self.in_keyevent = if (self.im_composing) .composing else .not_composing;
+ defer self.in_keyevent = .false;
+
+ // Pass the event through the input method which returns true if handled.
// Confusingly, not all events handled by the input method result
- // in this returning true so we have to maintain some local state to
- // find those and in one case we simply lose information.
+ // in this returning true so we have to maintain some additional
+ // state about whether we were composing or not to determine if
+ // we should proceed with key encoding.
+ //
+ // Cases where the input method does not mark the event as handled:
//
// - If we change the input method via keypress while we have preedit
// text, the input method will commit the pending text but will not
- // mark it as handled. We use the `was_composing` variable to detect
+ // mark it as handled. We use the `.composing` state to detect
// this case.
//
// - If we switch input methods (i.e. via ctrl+shift with fcitx),
@@ -1678,19 +1705,10 @@ pub fn keyEvent(
// triggered despite being technically consumed. At the time of
// writing, both Kitty and Alacritty have the same behavior. I
// know of no way to fix this.
- const was_composing = self.im_composing;
- const im_handled = filter: {
- // We note that we're in a keypress because we want some logic to
- // depend on this. For example, we don't want to send character events
- // like "a" via the input "commit" event if we're actively processing
- // a keypress because we'd lose access to the keycode information.
- self.in_keyevent = true;
- defer self.in_keyevent = false;
- break :filter c.gtk_im_context_filter_keypress(
- self.im_context,
- event,
- ) != 0;
- };
+ const im_handled = c.gtk_im_context_filter_keypress(
+ self.im_context,
+ event,
+ ) != 0;
// log.warn("GTKIM: im_handled={} im_len={} im_composing={}", .{
// im_handled,
// self.im_len,
@@ -1713,7 +1731,7 @@ pub fn keyEvent(
// Example: enable Japanese input method, press "konn" and then
// press enter. The final enter should not be encoded and "konn"
// (in hiragana) should be written as "こん".
- if (was_composing) return true;
+ if (self.in_keyevent == .composing) return true;
// Not composing and our input method buffer is empty. This could
// mean that the input method reacted to this event by activating
@@ -1892,7 +1910,6 @@ fn gtkInputPreeditChanged(
ctx: *c.GtkIMContext,
ud: ?*anyopaque,
) callconv(.C) void {
- // log.warn("GTKIM: preedit change", .{});
const self = userdataSelf(ud.?);
// Get our pre-edit string that we'll use to show the user.
@@ -1902,6 +1919,7 @@ fn gtkInputPreeditChanged(
const str = std.mem.sliceTo(buf, 0);
// Update our preedit state in Ghostty core
+ // log.warn("GTKIM: preedit change str={s}", .{str});
self.core_surface.preeditCallback(str) catch |err| {
log.err("error in preedit callback err={}", .{err});
};
@@ -1928,26 +1946,48 @@ fn gtkInputCommit(
bytes: [*:0]u8,
ud: ?*anyopaque,
) callconv(.C) void {
- // log.warn("GTKIM: input commit", .{});
const self = userdataSelf(ud.?);
const str = std.mem.sliceTo(bytes, 0);
- // If we're in a keyEvent (i.e. a keyboard event) and we're not composing,
- // then this is just a normal key press resulting in UTF-8 text. We
- // want the keyEvent to handle this so that the UTF-8 text can be associated
- // with a keyboard event.
- if (!self.im_composing and self.in_keyevent) {
- if (str.len > self.im_buf.len) {
- log.warn("not enough buffer space for input method commit", .{});
+ // log.debug("GTKIM: input commit composing={} keyevent={} str={s}", .{
+ // self.im_composing,
+ // self.in_keyevent,
+ // str,
+ // });
+
+ // We need to handle commit specially if we're in a key event.
+ // Specifically, GTK will send us a commit event for basic key
+ // encodings like "a" (on a US layout keyboard). We don't want
+ // to treat this as IME committed text because we want to associate
+ // it with a key event (i.e. "a" key press).
+ switch (self.in_keyevent) {
+ // If we're not in a key event then this commit is from
+ // some other source (i.e. on-screen keyboard, tablet, etc.)
+ // and we want to commit the text to the core surface.
+ .false => {},
+
+ // If we're in a composing state and in a key event then this
+ // key event is resulting in a commit of multiple keypresses
+ // and we don't want to encode it alongside the keypress.
+ .composing => {},
+
+ // If we're not composing then this commit is just a normal
+ // key encoding and we want our key event to handle it so
+ // that Ghostty can be aware of the key event alongside
+ // the text.
+ .not_composing => {
+ if (str.len > self.im_buf.len) {
+ log.warn("not enough buffer space for input method commit", .{});
+ return;
+ }
+
+ // Copy our committed text to the buffer
+ @memcpy(self.im_buf[0..str.len], str);
+ self.im_len = @intCast(str.len);
+
+ // log.debug("input commit len={}", .{self.im_len});
return;
- }
-
- // Copy our committed text to the buffer
- @memcpy(self.im_buf[0..str.len], str);
- self.im_len = @intCast(str.len);
-
- // log.debug("input commit len={}", .{self.im_len});
- return;
+ },
}
// If we reach this point from above it means we're composing OR
From 4cd49632b23fd3505eb2d68a82aa9440957c45c1 Mon Sep 17 00:00:00 2001
From: Leah Amelia Chen
Date: Sun, 2 Feb 2025 22:24:48 +0100
Subject: [PATCH 196/365] gtk(x11): support server-side decorations
Remind me to never touch Xlib code ever again.
---
src/apprt/gtk/winproto/x11.zig | 254 ++++++++++++++++++++++++++++-----
1 file changed, 219 insertions(+), 35 deletions(-)
diff --git a/src/apprt/gtk/winproto/x11.zig b/src/apprt/gtk/winproto/x11.zig
index e7137a74d..c58df6dea 100644
--- a/src/apprt/gtk/winproto/x11.zig
+++ b/src/apprt/gtk/winproto/x11.zig
@@ -13,7 +13,7 @@ const log = std.log.scoped(.gtk_x11);
pub const App = struct {
display: *c.Display,
base_event_code: c_int,
- kde_blur_atom: c.Atom,
+ atoms: Atoms,
pub fn init(
alloc: Allocator,
@@ -95,10 +95,7 @@ pub const App = struct {
return .{
.display = display,
.base_event_code = base_event_code,
- .kde_blur_atom = c.gdk_x11_get_xatom_by_name_for_display(
- gdk_display,
- "_KDE_NET_WM_BLUR_BEHIND_REGION",
- ),
+ .atoms = Atoms.init(gdk_display),
};
}
@@ -154,28 +151,27 @@ pub const App = struct {
pub const Window = struct {
app: *App,
+ alloc: Allocator,
config: DerivedConfig,
window: c.Window,
gtk_window: *c.GtkWindow,
+
blur_region: Region = .{},
const DerivedConfig = struct {
blur: bool,
- has_decoration: bool,
+ window_decoration: Config.WindowDecoration,
pub fn init(config: *const Config) DerivedConfig {
return .{
.blur = config.@"background-blur".enabled(),
- .has_decoration = switch (config.@"window-decoration") {
- .none => false,
- .auto, .client, .server => true,
- },
+ .window_decoration = config.@"window-decoration",
};
}
};
pub fn init(
- _: Allocator,
+ alloc: Allocator,
app: *App,
gtk_window: *c.GtkWindow,
config: *const Config,
@@ -192,6 +188,7 @@ pub const Window = struct {
return .{
.app = app,
+ .alloc = alloc,
.config = DerivedConfig.init(config),
.window = c.gdk_x11_surface_get_xid(surface),
.gtk_window = gtk_window,
@@ -236,11 +233,19 @@ pub const Window = struct {
.y = @intFromFloat(y),
};
};
- try self.syncBlur();
+ self.syncBlur() catch |err| {
+ log.err("failed to synchronize blur={}", .{err});
+ };
+ self.syncDecorations() catch |err| {
+ log.err("failed to synchronize decorations={}", .{err});
+ };
}
pub fn clientSideDecorationEnabled(self: Window) bool {
- return self.config.has_decoration;
+ return switch (self.config.window_decoration) {
+ .auto, .client => true,
+ .server, .none => false,
+ };
}
fn syncBlur(self: *Window) !void {
@@ -260,33 +265,192 @@ pub const Window = struct {
});
if (blur) {
- _ = c.XChangeProperty(
- self.app.display,
- self.window,
- self.app.kde_blur_atom,
+ try self.changeProperty(
+ Region,
+ self.app.atoms.kde_blur,
c.XA_CARDINAL,
- // Despite what you might think, the "32" here does NOT mean
- // that the data should be in u32s. Instead, they should be
- // c_longs, which on any 64-bit architecture would be obviously
- // 64 bits. WTF?!
- 32,
- c.PropModeReplace,
- // SAFETY: Region is an extern struct that has the same
- // representation of 4 c_longs put next to each other.
- // Therefore, reinterpretation should be safe.
- // We don't have to care about endianness either since
- // Xlib converts it to network byte order for us.
- @ptrCast(std.mem.asBytes(&self.blur_region)),
- 4,
+ ._32,
+ .{ .mode = .replace },
+ &self.blur_region,
);
} else {
- _ = c.XDeleteProperty(
- self.app.display,
- self.window,
- self.app.kde_blur_atom,
- );
+ try self.deleteProperty(self.app.atoms.kde_blur);
}
}
+
+ fn syncDecorations(self: *Window) !void {
+ var hints: MotifWMHints = .{};
+
+ self.getWindowProperty(
+ MotifWMHints,
+ self.app.atoms.motif_wm_hints,
+ self.app.atoms.motif_wm_hints,
+ ._32,
+ .{},
+ &hints,
+ ) catch |err| switch (err) {
+ // motif_wm_hints is already initialized, so this is fine
+ error.PropertyNotFound => {},
+
+ error.RequestFailed,
+ error.PropertyTypeMismatch,
+ error.PropertyFormatMismatch,
+ => return err,
+ };
+
+ hints.flags.decorations = true;
+ hints.decorations.all = switch (self.config.window_decoration) {
+ .server => true,
+ .auto, .client, .none => false,
+ };
+
+ try self.changeProperty(
+ MotifWMHints,
+ self.app.atoms.motif_wm_hints,
+ self.app.atoms.motif_wm_hints,
+ ._32,
+ .{ .mode = .replace },
+ &hints,
+ );
+ }
+
+ fn getWindowProperty(
+ self: *Window,
+ comptime T: type,
+ name: c.Atom,
+ typ: c.Atom,
+ comptime format: PropertyFormat,
+ options: struct {
+ offset: c_long = 0,
+ length: c_long = std.math.maxInt(c_long),
+ delete: bool = false,
+ },
+ result: *T,
+ ) GetWindowPropertyError!void {
+ // FIXME: Maybe we should switch to libxcb one day.
+ // Sounds like a much better idea than whatever this is
+ var actual_type_return: c.Atom = undefined;
+ var actual_format_return: c_int = undefined;
+ var nitems_return: c_ulong = undefined;
+ var bytes_after_return: c_ulong = undefined;
+ var prop_return: ?format.bufferType() = null;
+
+ const code = c.XGetWindowProperty(
+ self.app.display,
+ self.window,
+ name,
+ options.offset,
+ options.length,
+ @intFromBool(options.delete),
+ typ,
+ &actual_type_return,
+ &actual_format_return,
+ &nitems_return,
+ &bytes_after_return,
+ &prop_return,
+ );
+ if (code != c.Success) return error.RequestFailed;
+
+ if (actual_type_return == c.None) return error.PropertyNotFound;
+ if (typ != actual_type_return) return error.PropertyTypeMismatch;
+ if (@intFromEnum(format) != actual_format_return) return error.PropertyFormatMismatch;
+
+ const data_ptr: *T = @ptrCast(prop_return);
+ result.* = data_ptr.*;
+ _ = c.XFree(prop_return);
+ }
+
+ fn changeProperty(
+ self: *Window,
+ comptime T: type,
+ name: c.Atom,
+ typ: c.Atom,
+ comptime format: PropertyFormat,
+ options: struct {
+ mode: PropertyChangeMode,
+ },
+ value: *T,
+ ) X11Error!void {
+ const data: format.bufferType() = @ptrCast(value);
+
+ const status = c.XChangeProperty(
+ self.app.display,
+ self.window,
+ name,
+ typ,
+ @intFromEnum(format),
+ @intFromEnum(options.mode),
+ data,
+ @divExact(@sizeOf(T), @sizeOf(format.elemType())),
+ );
+
+ // For some godforsaken reason Xlib alternates between
+ // error values (0 = success) and booleans (1 = success), and they look exactly
+ // the same in the signature (just `int`, since Xlib is written in C89)...
+ if (status == 0) return error.RequestFailed;
+ }
+
+ fn deleteProperty(self: *Window, name: c.Atom) X11Error!void {
+ const status = c.XDeleteProperty(self.app.display, self.window, name);
+ if (status == 0) return error.RequestFailed;
+ }
+};
+
+const X11Error = error{
+ RequestFailed,
+};
+
+const GetWindowPropertyError = X11Error || error{
+ PropertyNotFound,
+ PropertyTypeMismatch,
+ PropertyFormatMismatch,
+};
+
+const Atoms = struct {
+ kde_blur: c.Atom,
+ motif_wm_hints: c.Atom,
+
+ fn init(display: *c.GdkDisplay) Atoms {
+ return .{
+ .kde_blur = c.gdk_x11_get_xatom_by_name_for_display(
+ display,
+ "_KDE_NET_WM_BLUR_BEHIND_REGION",
+ ),
+ .motif_wm_hints = c.gdk_x11_get_xatom_by_name_for_display(
+ display,
+ "_MOTIF_WM_HINTS",
+ ),
+ };
+ }
+};
+
+const PropertyChangeMode = enum(c_int) {
+ replace = c.PropModeReplace,
+ prepend = c.PropModePrepend,
+ append = c.PropModeAppend,
+};
+
+const PropertyFormat = enum(c_int) {
+ _8 = 8,
+ _16 = 16,
+ _32 = 32,
+
+ fn elemType(comptime self: PropertyFormat) type {
+ return switch (self) {
+ ._8 => c_char,
+ ._16 => c_int,
+ ._32 => c_long,
+ };
+ }
+
+ fn bufferType(comptime self: PropertyFormat) type {
+ // The buffer type has to be a multi-pointer to bytes
+ // *aligned to the element type* (very important,
+ // otherwise you'll read garbage!)
+ //
+ // I know this is really ugly. X11 is ugly. I consider it apropos.
+ return [*]align(@alignOf(self.elemType())) u8;
+ }
};
const Region = extern struct {
@@ -295,3 +459,23 @@ const Region = extern struct {
width: c_long = 0,
height: c_long = 0,
};
+
+// See Xm/MwmUtil.h, packaged with the Motif Window Manager
+const MotifWMHints = extern struct {
+ flags: packed struct(c_ulong) {
+ _pad: u1 = 0,
+ decorations: bool = false,
+
+ // We don't really care about the other flags
+ _rest: std.meta.Int(.unsigned, @bitSizeOf(c_ulong) - 2) = 0,
+ } = .{},
+ functions: c_ulong = 0,
+ decorations: packed struct(c_ulong) {
+ all: bool = false,
+
+ // We don't really care about the other flags
+ _rest: std.meta.Int(.unsigned, @bitSizeOf(c_ulong) - 1) = 0,
+ } = .{},
+ input_mode: c_long = 0,
+ status: c_ulong = 0,
+};
From d61ee462107dc6bc1560e4fd4cd5cfc20dcbaca4 Mon Sep 17 00:00:00 2001
From: Leah Amelia Chen
Date: Mon, 3 Feb 2025 09:17:55 +0100
Subject: [PATCH 197/365] config: mention X11 support in window-decoration docs
---
src/config/Config.zig | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/config/Config.zig b/src/config/Config.zig
index 0ed98bdea..b39470615 100644
--- a/src/config/Config.zig
+++ b/src/config/Config.zig
@@ -1157,9 +1157,9 @@ keybind: Keybinds = .{},
/// * `client` - Prefer client-side decorations.
///
/// * `server` - Prefer server-side decorations. This is only relevant
-/// on Linux with GTK. This currently only works on Linux with Wayland
-/// and the `org_kde_kwin_server_decoration` protocol available (e.g.
-/// KDE Plasma, but almost any non-GNOME desktop supports this protocol).
+/// on Linux with GTK, either on X11, or Wayland on a compositor that
+/// supports the `org_kde_kwin_server_decoration` protocol (e.g. KDE Plasma,
+/// but almost any non-GNOME desktop supports this protocol).
///
/// If `server` is set but the environment doesn't support server-side
/// decorations, client-side decorations will be used instead.
From 8660cdaad5d2607136f2c2b6e4693cce15ef8e0e Mon Sep 17 00:00:00 2001
From: "Jeffrey C. Ollie"
Date: Sun, 2 Feb 2025 12:58:42 -0600
Subject: [PATCH 198/365] core: add explicit errors to src/os/env.zig
---
src/os/env.zig | 10 ++++++----
1 file changed, 6 insertions(+), 4 deletions(-)
diff --git a/src/os/env.zig b/src/os/env.zig
index cf6cc0fe7..d1cbbc01f 100644
--- a/src/os/env.zig
+++ b/src/os/env.zig
@@ -3,13 +3,15 @@ const builtin = @import("builtin");
const Allocator = std.mem.Allocator;
const posix = std.posix;
+pub const Errors = Allocator.Error;
+
/// Append a value to an environment variable such as PATH.
/// The returned value is always allocated so it must be freed.
pub fn appendEnv(
alloc: Allocator,
current: []const u8,
value: []const u8,
-) ![]u8 {
+) Errors![]u8 {
// If there is no prior value, we return it as-is
if (current.len == 0) return try alloc.dupe(u8, value);
@@ -26,7 +28,7 @@ pub fn appendEnvAlways(
alloc: Allocator,
current: []const u8,
value: []const u8,
-) ![]u8 {
+) Errors![]u8 {
return try std.fmt.allocPrint(alloc, "{s}{c}{s}", .{
current,
std.fs.path.delimiter,
@@ -40,7 +42,7 @@ pub fn prependEnv(
alloc: Allocator,
current: []const u8,
value: []const u8,
-) ![]u8 {
+) Errors![]u8 {
// If there is no prior value, we return it as-is
if (current.len == 0) return try alloc.dupe(u8, value);
@@ -68,7 +70,7 @@ pub const GetEnvResult = struct {
/// This will allocate on Windows but not on other platforms. The returned
/// value should have deinit called to do the proper cleanup no matter what
/// platform you are on.
-pub fn getenv(alloc: Allocator, key: []const u8) !?GetEnvResult {
+pub fn getenv(alloc: Allocator, key: []const u8) Errors!?GetEnvResult {
return switch (builtin.os.tag) {
// Non-Windows doesn't need to allocate
else => if (posix.getenv(key)) |v| .{ .value = v } else null,
From 002cce4e81c1d67fbc163e5ae4d97ac5e1237474 Mon Sep 17 00:00:00 2001
From: "Jeffrey C. Ollie"
Date: Sun, 2 Feb 2025 13:16:53 -0600
Subject: [PATCH 199/365] core: handle src/os/env.zig errors on windows
---
src/os/env.zig | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/os/env.zig b/src/os/env.zig
index d1cbbc01f..f3611663b 100644
--- a/src/os/env.zig
+++ b/src/os/env.zig
@@ -80,7 +80,8 @@ pub fn getenv(alloc: Allocator, key: []const u8) Errors!?GetEnvResult {
.value = v,
} else |err| switch (err) {
error.EnvironmentVariableNotFound => null,
- else => err,
+ error.InvalidWtf8 => null,
+ else => |e| e,
},
};
}
From 8607d463ff3bd5e48bb85498ea329bce44c511c8 Mon Sep 17 00:00:00 2001
From: "Jeffrey C. Ollie"
Date: Mon, 3 Feb 2025 14:38:00 -0600
Subject: [PATCH 200/365] core: fix puralization of src/os/env.zig
Errors->Error
---
src/os/env.zig | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/src/os/env.zig b/src/os/env.zig
index f3611663b..fe2be20de 100644
--- a/src/os/env.zig
+++ b/src/os/env.zig
@@ -3,7 +3,7 @@ const builtin = @import("builtin");
const Allocator = std.mem.Allocator;
const posix = std.posix;
-pub const Errors = Allocator.Error;
+pub const Error = Allocator.Error;
/// Append a value to an environment variable such as PATH.
/// The returned value is always allocated so it must be freed.
@@ -11,7 +11,7 @@ pub fn appendEnv(
alloc: Allocator,
current: []const u8,
value: []const u8,
-) Errors![]u8 {
+) Error![]u8 {
// If there is no prior value, we return it as-is
if (current.len == 0) return try alloc.dupe(u8, value);
@@ -28,7 +28,7 @@ pub fn appendEnvAlways(
alloc: Allocator,
current: []const u8,
value: []const u8,
-) Errors![]u8 {
+) Error![]u8 {
return try std.fmt.allocPrint(alloc, "{s}{c}{s}", .{
current,
std.fs.path.delimiter,
@@ -42,7 +42,7 @@ pub fn prependEnv(
alloc: Allocator,
current: []const u8,
value: []const u8,
-) Errors![]u8 {
+) Error![]u8 {
// If there is no prior value, we return it as-is
if (current.len == 0) return try alloc.dupe(u8, value);
@@ -70,7 +70,7 @@ pub const GetEnvResult = struct {
/// This will allocate on Windows but not on other platforms. The returned
/// value should have deinit called to do the proper cleanup no matter what
/// platform you are on.
-pub fn getenv(alloc: Allocator, key: []const u8) Errors!?GetEnvResult {
+pub fn getenv(alloc: Allocator, key: []const u8) Error!?GetEnvResult {
return switch (builtin.os.tag) {
// Non-Windows doesn't need to allocate
else => if (posix.getenv(key)) |v| .{ .value = v } else null,
From 7f0d22a31eec3d68ccae45ce6bcc39fd3277776b Mon Sep 17 00:00:00 2001
From: "Jeffrey C. Ollie"
Date: Sun, 2 Feb 2025 13:31:00 -0600
Subject: [PATCH 201/365] core: add explicit errors to src/pty.zig
---
src/pty.zig | 38 ++++++++++++++++++++++++++++----------
1 file changed, 28 insertions(+), 10 deletions(-)
diff --git a/src/pty.zig b/src/pty.zig
index b6dc2e145..52b231d59 100644
--- a/src/pty.zig
+++ b/src/pty.zig
@@ -41,12 +41,14 @@ pub const Mode = packed struct {
// a termio that doesn't use a pty. This isn't used in any user-facing
// artifacts, this is just a stopgap to get compilation to work on iOS.
const NullPty = struct {
+ pub const Errors = error{GetModeFailed};
+
pub const Fd = posix.fd_t;
master: Fd,
slave: Fd,
- pub fn open(size: winsize) !Pty {
+ pub fn open(size: winsize) error{}!Pty {
_ = size;
return .{ .master = 0, .slave = 0 };
}
@@ -60,12 +62,12 @@ const NullPty = struct {
return .{};
}
- pub fn setSize(self: *Pty, size: winsize) !void {
+ pub fn setSize(self: *Pty, size: winsize) error{}!void {
_ = self;
_ = size;
}
- pub fn childPreExec(self: Pty) !void {
+ pub fn childPreExec(self: Pty) error{}!void {
_ = self;
}
};
@@ -74,6 +76,15 @@ const NullPty = struct {
/// of Linux syscalls. The caller is responsible for detail-oriented handling
/// of the returned file handles.
const PosixPty = struct {
+ pub const Errors = error{
+ GetModeFailed,
+ IoctlFailed,
+ OpenPtyFailed,
+ OperationNotSupported,
+ ProcessGroupFailed,
+ SetControllingTerminalFailed,
+ };
+
pub const Fd = posix.fd_t;
// https://github.com/ziglang/zig/issues/13277
@@ -101,7 +112,9 @@ const PosixPty = struct {
slave: Fd,
/// Open a new PTY with the given initial size.
- pub fn open(size: winsize) !Pty {
+ pub fn open(size: winsize) error{
+ OpenptyFailed,
+ }!Pty {
// Need to copy so that it becomes non-const.
var sizeCopy = size;
@@ -170,7 +183,7 @@ const PosixPty = struct {
}
/// Return the size of the pty.
- pub fn getSize(self: Pty) !winsize {
+ pub fn getSize(self: Pty) error{IoctlFailed}!winsize {
var ws: winsize = undefined;
if (c.ioctl(self.master, TIOCGWINSZ, @intFromPtr(&ws)) < 0)
return error.IoctlFailed;
@@ -179,14 +192,14 @@ const PosixPty = struct {
}
/// Set the size of the pty.
- pub fn setSize(self: *Pty, size: winsize) !void {
+ pub fn setSize(self: *Pty, size: winsize) error{IoctlFailed}!void {
if (c.ioctl(self.master, TIOCSWINSZ, @intFromPtr(&size)) < 0)
return error.IoctlFailed;
}
/// This should be called prior to exec in the forked child process
/// in order to setup the tty properly.
- pub fn childPreExec(self: Pty) !void {
+ pub fn childPreExec(self: Pty) error{ OperationNotSupported, ProcessGroupFailed, SetControllingTerminalFailed }!void {
// Reset our signals
var sa: posix.Sigaction = .{
.handler = .{ .handler = posix.SIG.DFL },
@@ -227,6 +240,11 @@ const PosixPty = struct {
/// Windows PTY creation and management.
const WindowsPty = struct {
+ pub const Errors = error{
+ ResizeFailed,
+ Unexpected,
+ };
+
pub const Fd = windows.HANDLE;
// Process-wide counter for pipe names
@@ -240,7 +258,7 @@ const WindowsPty = struct {
size: winsize,
/// Open a new PTY with the given initial size.
- pub fn open(size: winsize) !Pty {
+ pub fn open(size: winsize) error{Unexpected}!Pty {
var pty: Pty = undefined;
var pipe_path_buf: [128]u8 = undefined;
@@ -350,12 +368,12 @@ const WindowsPty = struct {
}
/// Return the size of the pty.
- pub fn getSize(self: Pty) !winsize {
+ pub fn getSize(self: Pty) error{}!winsize {
return self.size;
}
/// Set the size of the pty.
- pub fn setSize(self: *Pty, size: winsize) !void {
+ pub fn setSize(self: *Pty, size: winsize) error{ResizeFailed}!void {
const result = windows.exp.kernel32.ResizePseudoConsole(
self.pseudo_console,
.{ .X = @intCast(size.ws_col), .Y = @intCast(size.ws_row) },
From 3fdbd5f7ba4a7d97b249004f7e81892fbca52546 Mon Sep 17 00:00:00 2001
From: "Jeffrey C. Ollie"
Date: Mon, 3 Feb 2025 15:06:35 -0600
Subject: [PATCH 202/365] core: fix up pty Zig error sets
---
src/pty.zig | 66 +++++++++++++++++++++++++++++++----------------------
1 file changed, 39 insertions(+), 27 deletions(-)
diff --git a/src/pty.zig b/src/pty.zig
index 52b231d59..6f97e190d 100644
--- a/src/pty.zig
+++ b/src/pty.zig
@@ -41,14 +41,16 @@ pub const Mode = packed struct {
// a termio that doesn't use a pty. This isn't used in any user-facing
// artifacts, this is just a stopgap to get compilation to work on iOS.
const NullPty = struct {
- pub const Errors = error{GetModeFailed};
+ pub const Error = OpenError || GetModeError || SetSizeError || ChildPreExecError;
pub const Fd = posix.fd_t;
master: Fd,
slave: Fd,
- pub fn open(size: winsize) error{}!Pty {
+ pub const OpenError = error{};
+
+ pub fn open(size: winsize) OpenError!Pty {
_ = size;
return .{ .master = 0, .slave = 0 };
}
@@ -57,17 +59,23 @@ const NullPty = struct {
_ = self;
}
- pub fn getMode(self: Pty) error{GetModeFailed}!Mode {
+ pub const GetModeError = error{GetModeFailed};
+
+ pub fn getMode(self: Pty) GetModeError!Mode {
_ = self;
return .{};
}
- pub fn setSize(self: *Pty, size: winsize) error{}!void {
+ pub const SetSizeError = error{};
+
+ pub fn setSize(self: *Pty, size: winsize) SetSizeError!void {
_ = self;
_ = size;
}
- pub fn childPreExec(self: Pty) error{}!void {
+ pub const ChildPreExecError = error{};
+
+ pub fn childPreExec(self: Pty) ChildPreExecError!void {
_ = self;
}
};
@@ -76,14 +84,7 @@ const NullPty = struct {
/// of Linux syscalls. The caller is responsible for detail-oriented handling
/// of the returned file handles.
const PosixPty = struct {
- pub const Errors = error{
- GetModeFailed,
- IoctlFailed,
- OpenPtyFailed,
- OperationNotSupported,
- ProcessGroupFailed,
- SetControllingTerminalFailed,
- };
+ pub const Error = OpenError || GetModeError || GetSizeError || SetSizeError || ChildPreExecError;
pub const Fd = posix.fd_t;
@@ -111,10 +112,10 @@ const PosixPty = struct {
master: Fd,
slave: Fd,
+ pub const OpenError = error{OpenptyFailed};
+
/// Open a new PTY with the given initial size.
- pub fn open(size: winsize) error{
- OpenptyFailed,
- }!Pty {
+ pub fn open(size: winsize) OpenError!Pty {
// Need to copy so that it becomes non-const.
var sizeCopy = size;
@@ -171,7 +172,9 @@ const PosixPty = struct {
self.* = undefined;
}
- pub fn getMode(self: Pty) error{GetModeFailed}!Mode {
+ pub const GetModeError = error{GetModeFailed};
+
+ pub fn getMode(self: Pty) GetModeError!Mode {
var attrs: c.termios = undefined;
if (c.tcgetattr(self.master, &attrs) != 0)
return error.GetModeFailed;
@@ -182,8 +185,10 @@ const PosixPty = struct {
};
}
+ pub const GetSizeError = error{IoctlFailed};
+
/// Return the size of the pty.
- pub fn getSize(self: Pty) error{IoctlFailed}!winsize {
+ pub fn getSize(self: Pty) GetSizeError!winsize {
var ws: winsize = undefined;
if (c.ioctl(self.master, TIOCGWINSZ, @intFromPtr(&ws)) < 0)
return error.IoctlFailed;
@@ -191,15 +196,19 @@ const PosixPty = struct {
return ws;
}
+ pub const SetSizeError = error{IoctlFailed};
+
/// Set the size of the pty.
- pub fn setSize(self: *Pty, size: winsize) error{IoctlFailed}!void {
+ pub fn setSize(self: *Pty, size: winsize) SetSizeError!void {
if (c.ioctl(self.master, TIOCSWINSZ, @intFromPtr(&size)) < 0)
return error.IoctlFailed;
}
+ pub const ChildPreExecError = error{ OperationNotSupported, ProcessGroupFailed, SetControllingTerminalFailed };
+
/// This should be called prior to exec in the forked child process
/// in order to setup the tty properly.
- pub fn childPreExec(self: Pty) error{ OperationNotSupported, ProcessGroupFailed, SetControllingTerminalFailed }!void {
+ pub fn childPreExec(self: Pty) ChildPreExecError!void {
// Reset our signals
var sa: posix.Sigaction = .{
.handler = .{ .handler = posix.SIG.DFL },
@@ -240,10 +249,7 @@ const PosixPty = struct {
/// Windows PTY creation and management.
const WindowsPty = struct {
- pub const Errors = error{
- ResizeFailed,
- Unexpected,
- };
+ pub const Error = OpenError || GetSizeError || SetSizeError;
pub const Fd = windows.HANDLE;
@@ -257,8 +263,10 @@ const WindowsPty = struct {
pseudo_console: windows.exp.HPCON,
size: winsize,
+ pub const OpenError = error{Unexpected};
+
/// Open a new PTY with the given initial size.
- pub fn open(size: winsize) error{Unexpected}!Pty {
+ pub fn open(size: winsize) OpenError!Pty {
var pty: Pty = undefined;
var pipe_path_buf: [128]u8 = undefined;
@@ -367,13 +375,17 @@ const WindowsPty = struct {
self.* = undefined;
}
+ pub const GetSizeError = error{};
+
/// Return the size of the pty.
- pub fn getSize(self: Pty) error{}!winsize {
+ pub fn getSize(self: Pty) GetSizeError!winsize {
return self.size;
}
+ pub const SetSizeError = error{ResizeFailed};
+
/// Set the size of the pty.
- pub fn setSize(self: *Pty, size: winsize) error{ResizeFailed}!void {
+ pub fn setSize(self: *Pty, size: winsize) SetSizeError!void {
const result = windows.exp.kernel32.ResizePseudoConsole(
self.pseudo_console,
.{ .X = @intCast(size.ws_col), .Y = @intCast(size.ws_row) },
From b7fa8e5947f0b6a0fda9f8c222b2ca13a3f313d2 Mon Sep 17 00:00:00 2001
From: "Jeffrey C. Ollie"
Date: Sun, 2 Feb 2025 00:46:01 -0600
Subject: [PATCH 203/365] linux: ensure that group dir fd is closed
The CLOEXEC flag on the fd will ensure that the directory is closed on
the child, but also need to close the fd in the parent.
---
src/os/cgroup.zig | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/os/cgroup.zig b/src/os/cgroup.zig
index d94a4a728..5645e337a 100644
--- a/src/os/cgroup.zig
+++ b/src/os/cgroup.zig
@@ -102,6 +102,7 @@ pub fn cloneInto(cgroup: []const u8) !posix.pid_t {
}
};
assert(fd >= 0);
+ defer _ = linux.close(fd);
const args: extern struct {
flags: u64,
From 730c6884f7d29a0833cb01b9ede44aaf71778a4d Mon Sep 17 00:00:00 2001
From: Mitchell Hashimoto
Date: Mon, 3 Feb 2025 13:28:56 -0800
Subject: [PATCH 204/365] macOS: binding checks should never trigger preedit
callbacks
Fixes #5558
Binding checks would sometimes trigger preedit which would cause some
characters to leak through.
This is a bit of a band-aid. The real long term solution is noted in the
TODO comment in this commit, but I wanted to avoid regressions in a
patch release so I'm going to defer that to 1.2. This commit fixes the
main issue for 1.1.1.
---
src/apprt/embedded.zig | 15 ++++++++++++++-
1 file changed, 14 insertions(+), 1 deletion(-)
diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig
index 3c3723d4f..358e9f291 100644
--- a/src/apprt/embedded.zig
+++ b/src/apprt/embedded.zig
@@ -238,6 +238,14 @@ pub const App = struct {
translate_mods,
);
+ // TODO(mitchellh): I think we can get rid of the above keymap
+ // translation code completely and defer to AppKit/Swift
+ // (for macOS) for handling all translations. The translation
+ // within libghostty is an artifact of an earlier design and
+ // it is buggy (see #5558). We should move closer to a GTK-style
+ // model of tracking composing states and preedit in the apprt
+ // and not in libghostty.
+
// If this is a dead key, then we're composing a character and
// we need to set our proper preedit state if we're targeting a
// surface.
@@ -1652,7 +1660,12 @@ pub const CAPI = struct {
event: KeyEvent,
) bool {
const core_event = surface.app.coreKeyEvent(
- .{ .surface = surface },
+ // Note: this "app" target here looks like a bug, but it is
+ // intentional. coreKeyEvent uses the target only as a way to
+ // trigger preedit callbacks for keymap translation and we don't
+ // want to trigger that here. See the todo item in coreKeyEvent
+ // for a long term solution to this and removing target altogether.
+ .app,
event.keyEvent(),
) catch |err| {
log.warn("error processing key event err={}", .{err});
From 8d31f6ce2e8f301be871c7a41206c5198bca2dc6 Mon Sep 17 00:00:00 2001
From: Shaps Benkau
Date: Fri, 31 Jan 2025 15:41:03 +0000
Subject: [PATCH 205/365] Toggling visibility is now ignored when in fullscreen
mode.
---
macos/Sources/App/macOS/AppDelegate.swift | 12 +++++++++++-
src/input/Binding.zig | 2 ++
2 files changed, 13 insertions(+), 1 deletion(-)
diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift
index 4b11b68aa..c719d3ce7 100644
--- a/macos/Sources/App/macOS/AppDelegate.swift
+++ b/macos/Sources/App/macOS/AppDelegate.swift
@@ -709,11 +709,21 @@ class AppDelegate: NSObject,
/// Toggles visibility of all Ghosty Terminal windows. When hidden, activates Ghostty as the frontmost application
@IBAction func toggleVisibility(_ sender: Any) {
+ // Toggle visibility doesn't do anything if the focused window is native
+ // fullscreen.
+ guard let keyWindow = NSApp.keyWindow,
+ !keyWindow.styleMask.contains(.fullScreen) else { return }
+
// If we have focus, then we hide all windows.
if NSApp.isActive {
// We need to keep track of the windows that were visible because we only
// want to bring back these windows if we remove the toggle.
- self.hiddenWindows = NSApp.windows.filter { $0.isVisible }.map { Weak($0) }
+ //
+ // We also ignore fullscreen windows because they don't hide anyways.
+ self.hiddenWindows = NSApp.windows.filter {
+ $0.isVisible &&
+ !$0.styleMask.contains(.fullScreen)
+ }.map { Weak($0) }
NSApp.hide(nil)
return
}
diff --git a/src/input/Binding.zig b/src/input/Binding.zig
index 19c103195..90ea436af 100644
--- a/src/input/Binding.zig
+++ b/src/input/Binding.zig
@@ -470,6 +470,8 @@ pub const Action = union(enum) {
/// Ghostty becomes focused. When hiding all windows, focus is yielded
/// to the next application as determined by the OS.
///
+ /// Note: When the focused surface is fullscreen, this method does nothing.
+ ///
/// This currently only works on macOS.
toggle_visibility: void,
From 4a95e8e48cdb3555ade20e7e3959a12856a1a6b4 Mon Sep 17 00:00:00 2001
From: Bryan Lee <38807139+liby@users.noreply.github.com>
Date: Tue, 4 Feb 2025 20:50:34 +0800
Subject: [PATCH 206/365] Fix cursor character not visible when cursor color
matches background
---
src/renderer/shaders/cell.metal | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/src/renderer/shaders/cell.metal b/src/renderer/shaders/cell.metal
index 3ca0f9149..5f161c6b7 100644
--- a/src/renderer/shaders/cell.metal
+++ b/src/renderer/shaders/cell.metal
@@ -471,16 +471,15 @@ vertex CellTextVertexOut cell_text_vertex(
out.color = contrasted_color(uniforms.min_contrast, out.color, out.bg_color);
}
- // If this cell is the cursor cell, then we need to change the color.
- if (
- in.mode != MODE_TEXT_CURSOR &&
- (
+ // Check if current position is under cursor (including wide cursor)
+ bool is_cursor_pos = (
in.grid_pos.x == uniforms.cursor_pos.x ||
uniforms.cursor_wide &&
in.grid_pos.x == uniforms.cursor_pos.x + 1
- ) &&
- in.grid_pos.y == uniforms.cursor_pos.y
- ) {
+ ) && in.grid_pos.y == uniforms.cursor_pos.y;
+
+ // If this cell is the cursor cell, then we need to change the color.
+ if (in.mode != MODE_TEXT_CURSOR && is_cursor_pos) {
out.color = load_color(
uniforms.cursor_color,
uniforms.use_display_p3,
@@ -490,7 +489,8 @@ vertex CellTextVertexOut cell_text_vertex(
// Don't bother rendering if the bg and fg colors are identical, just return
// the same point which will be culled because it makes the quad zero sized.
- if (all(out.color == out.bg_color)) {
+ // However, we should still render if this is the cursor position
+ if (all(out.color == out.bg_color) && !is_cursor_pos) {
out.position = float4(0.0);
}
From 0c1f3d724dcf087a86b41a153fc470ab4d432f6e Mon Sep 17 00:00:00 2001
From: Leah Amelia Chen
Date: Wed, 5 Feb 2025 09:01:15 +0100
Subject: [PATCH 207/365] gtk: remove CSD styling when CSDs aren't used
Fixes #2023
---
src/apprt/gtk/Window.zig | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig
index 3a72e1752..bb49165b9 100644
--- a/src/apprt/gtk/Window.zig
+++ b/src/apprt/gtk/Window.zig
@@ -667,6 +667,7 @@ fn gtkWindowNotifyDecorated(
// class is defined in the GtkWindow documentation:
// https://docs.gtk.org/gtk4/class.Window.html#css-nodes. A definition
// for .ssd is provided by GTK and Adwaita.
+ toggleCssClass(@ptrCast(object), "csd", is_decorated);
toggleCssClass(@ptrCast(object), "ssd", !is_decorated);
toggleCssClass(@ptrCast(object), "no-border-radius", !is_decorated);
From 47e50abe24de72205be30c0e0bdd2de27c1496a2 Mon Sep 17 00:00:00 2001
From: Kat <65649991+00-kat@users.noreply.github.com>
Date: Wed, 5 Feb 2025 08:22:58 +0000
Subject: [PATCH 208/365] Add a final newline to "No crash reports!"
---
src/cli/crash_report.zig | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/cli/crash_report.zig b/src/cli/crash_report.zig
index dd5fe99cc..ff8509797 100644
--- a/src/cli/crash_report.zig
+++ b/src/cli/crash_report.zig
@@ -53,7 +53,7 @@ pub fn run(alloc_gpa: Allocator) !u8 {
// print a message, otherwise we do nothing.
if (reports.items.len == 0) {
if (std.posix.isatty(stdout.handle)) {
- try stdout.writeAll("No crash reports! 👻");
+ try stdout.writeAll("No crash reports! 👻\n");
}
return 0;
}
From f660ec8bd2d08f3b0c1bbde0167241cd1cb5dd10 Mon Sep 17 00:00:00 2001
From: Ofir Levitan
Date: Wed, 5 Feb 2025 16:04:53 +0200
Subject: [PATCH 209/365] GTK: disable color management
---
src/apprt/gtk/App.zig | 3 +++
1 file changed, 3 insertions(+)
diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig
index df74cefb2..251cbdceb 100644
--- a/src/apprt/gtk/App.zig
+++ b/src/apprt/gtk/App.zig
@@ -146,6 +146,9 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
var gdk_disable: struct {
@"gles-api": bool = false,
+ /// current gtk implementation for color management is not good enough.
+ /// see: https://bugs.kde.org/show_bug.cgi?id=495647
+ @"color-mgmt": bool = true,
/// Disabling Vulkan can improve startup times by hundreds of
/// milliseconds on some systems. We don't use Vulkan so we can just
/// disable it.
From 1d7f041f55f012afc5af27d98a828e13f29751fe Mon Sep 17 00:00:00 2001
From: Mitchell Hashimoto
Date: Wed, 5 Feb 2025 10:44:38 -0800
Subject: [PATCH 210/365] macos: update Sparkle to 2.6.4 to workaround security
issue
https://github.com/ghostty-org/ghostty/security/dependabot/4
---
.../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/macos/Ghostty.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/macos/Ghostty.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
index d380910a8..5ace476e0 100644
--- a/macos/Ghostty.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/macos/Ghostty.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -6,8 +6,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/sparkle-project/Sparkle",
"state" : {
- "revision" : "b456fd404954a9e13f55aa0c88cd5a40b8399638",
- "version" : "2.6.3"
+ "revision" : "0ef1ee0220239b3776f433314515fd849025673f",
+ "version" : "2.6.4"
}
}
],
From d87bfdff1ab1e6b39e8cac8caa613d15456cb4c4 Mon Sep 17 00:00:00 2001
From: eifr
Date: Wed, 5 Feb 2025 22:48:28 +0200
Subject: [PATCH 211/365] move color-mgmt to config
---
src/apprt/gtk/App.zig | 8 ++++----
src/config/Config.zig | 4 ++++
2 files changed, 8 insertions(+), 4 deletions(-)
diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig
index 251cbdceb..c9096ee01 100644
--- a/src/apprt/gtk/App.zig
+++ b/src/apprt/gtk/App.zig
@@ -146,14 +146,14 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
var gdk_disable: struct {
@"gles-api": bool = false,
- /// current gtk implementation for color management is not good enough.
- /// see: https://bugs.kde.org/show_bug.cgi?id=495647
- @"color-mgmt": bool = true,
+ @"color-mgmt": bool,
/// Disabling Vulkan can improve startup times by hundreds of
/// milliseconds on some systems. We don't use Vulkan so we can just
/// disable it.
vulkan: bool = false,
- } = .{};
+ } = .{
+ .@"color-mgmt" = config.@"disable-gtk-color-mgmt",
+ };
environment: {
if (version.runtimeAtLeast(4, 16, 0)) {
diff --git a/src/config/Config.zig b/src/config/Config.zig
index 3010b87d1..52a1f2885 100644
--- a/src/config/Config.zig
+++ b/src/config/Config.zig
@@ -1350,6 +1350,10 @@ keybind: Keybinds = .{},
/// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color.
@"window-titlebar-foreground": ?Color = null,
+/// current gtk implementation for color management is not good enough.
+/// see: https://bugs.kde.org/show_bug.cgi?id=495647
+@"disable-gtk-color-mgmt": bool = false,
+
/// This controls when resize overlays are shown. Resize overlays are a
/// transient popup that shows the size of the terminal while the surfaces are
/// being resized. The possible options are:
From 1ce23c079ec5d78f6b98e5a8e5aa5b115c4fc18f Mon Sep 17 00:00:00 2001
From: eifr
Date: Wed, 5 Feb 2025 23:35:12 +0200
Subject: [PATCH 212/365] expand comment + rename
---
src/apprt/gtk/App.zig | 4 +++-
src/config/Config.zig | 15 ++++++++++++---
2 files changed, 15 insertions(+), 4 deletions(-)
diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig
index c9096ee01..4781b0ac3 100644
--- a/src/apprt/gtk/App.zig
+++ b/src/apprt/gtk/App.zig
@@ -146,13 +146,15 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
var gdk_disable: struct {
@"gles-api": bool = false,
+ /// current gtk implementation for color management is not good enough.
+ /// see: https://bugs.kde.org/show_bug.cgi?id=495647
@"color-mgmt": bool,
/// Disabling Vulkan can improve startup times by hundreds of
/// milliseconds on some systems. We don't use Vulkan so we can just
/// disable it.
vulkan: bool = false,
} = .{
- .@"color-mgmt" = config.@"disable-gtk-color-mgmt",
+ .@"color-mgmt" = config.@"gtk-gdk-disable-color-mgmt",
};
environment: {
diff --git a/src/config/Config.zig b/src/config/Config.zig
index 52a1f2885..6d09fc9ce 100644
--- a/src/config/Config.zig
+++ b/src/config/Config.zig
@@ -1350,9 +1350,18 @@ keybind: Keybinds = .{},
/// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color.
@"window-titlebar-foreground": ?Color = null,
-/// current gtk implementation for color management is not good enough.
-/// see: https://bugs.kde.org/show_bug.cgi?id=495647
-@"disable-gtk-color-mgmt": bool = false,
+/// Controls whether to disable GDK color management in GTK applications.
+///
+/// By default this is set to `false`, meaning color management is enabled.
+/// You may want to enable this setting (set to `true`) if you experience:
+/// - Incorrect or washed out colors in your terminal
+/// - Color inconsistencies between GTK applications
+/// - Performance issues related to color management
+///
+/// This is a workaround for known issues with GTK's color management implementation,
+/// particularly affecting applications running under Wayland.
+/// See: https://bugs.kde.org/show_bug.cgi?id=495647
+@"gtk-gdk-disable-color-mgmt": bool = false,
/// This controls when resize overlays are shown. Resize overlays are a
/// transient popup that shows the size of the terminal while the surfaces are
From ae25ad4047f80896458049208653d2b5ee025944 Mon Sep 17 00:00:00 2001
From: Mitchell Hashimoto
Date: Wed, 5 Feb 2025 11:23:22 -0800
Subject: [PATCH 213/365] Introduce subsystem maintainers via CODEOWNERS
This commit introduces the proposed subsystem maintainer system for
Ghostty. This commit doesn't assign anyone yet to the subsystems, but
defines the CODEOWNERS file, creates the GitHub teams, and documents the
system.
---
CODEOWNERS | 149 +++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 149 insertions(+)
create mode 100644 CODEOWNERS
diff --git a/CODEOWNERS b/CODEOWNERS
new file mode 100644
index 000000000..835244ebc
--- /dev/null
+++ b/CODEOWNERS
@@ -0,0 +1,149 @@
+# This file documents the subsystem maintainers of the Ghostty project
+# along with the responsibilities of a maintainer and how one can become
+# a maintainer.
+#
+# Ghostty follows a subsystem maintainer model where distinguished
+# contributors (with mutual agreement) are designated as maintainers of a
+# specific subset of the project. A subsystem maintainer has more privileges
+# and authority over a specific part of the project than a regular
+# contributor and deference is given to them when making decisions about
+# their subsystem.
+#
+# Ultimately Ghostty has a BDFL (Benevolent Dictator For Life) model
+# currently with @mitchellh as the BDFL. The BDFL has the final say in all
+# decisions and may override a maintainer's decision if necessary. I like to
+# say its a BDFLFN (Benevolent Dictator For Life "For Now") model because
+# long term I'd like to see the project be more community driven. But for
+# now, early in its life, we're going with this model.
+#
+# ## Privileges
+#
+# - Authority to approve or reject pull requests in their subsystem.
+# - Authority to moderate issues and discussions in their subsystem.
+# - Authority to make roadmap and design decisions about their subsystem
+# with input only from other subsystem maintainers.
+#
+# In all scenarios, the BDFL doesn't need to be consulted for decisions
+# but may revert or override decisions if necessary. The expectation is
+# that maintainers will be trusted to make the right decisions for their
+# subsystem and this will be rare.
+#
+# ## Responsibilities
+#
+# Subsystem maintainership is a voluntary role and maintainers are not
+# expected to dedicate any amount of time to the project. However, if a
+# maintainer is inactive for a long period of time, they may be removed from
+# the maintainers list to avoid bitrot or outdated information.
+#
+# Maintainers are expected to be exemplary members of the community and
+# should be respectful, helpful, and professional in all interactions.
+# This is both in regards to the community at large as well as other
+# subsystem maintainers as well as @mitchellh.
+#
+# As technical leaders, maintainers are expected to be mindful about
+# breaking changes, performance, user impact, and other technical
+# considerations in their subsystem. They should be considerate of large
+# changes and should be able to justify their decisions.
+#
+# Notably, maintainers have NO OBLIGATION to review pull requests or issues
+# in their subsystem. They have full discretion to review or not review
+# anything they want. This isn't a job! It is a role of trust and authority
+# and the expectation is that maintainers will use their best judgement.
+#
+# ## Becoming a Maintainer
+#
+# Maintainer candidates are noticed and proposed by the community. Anyone
+# may propose themselves or someone else as a maintainer. The BDFL along
+# with existing maintainers will discuss and decide.
+#
+# Generally, we want to see consistent high quality contributions to a
+# specific subsystem before considering someone as a maintainer. There isn't
+# an exact number of contributions or time period required but generally
+# we're looking for an order of a dozen or more contributions over a period of
+# months, at least.
+#
+# # Subsystem List
+#
+# The subsystems don't fully cover the entirety of the Ghostty project but
+# are created organically as experts in certain areas emerge. If you feel
+# you are an expert in a certain area and would like to be a maintainer,
+# please reach out to @mitchellh on Discord.
+#
+# (Alphabetical order)
+#
+# - @ghostty-org/font - All things font related including discovery,
+# rasterization, shaping, coloring, etc.
+#
+# - @ghostty-org/gtk - Anything GTK-related in the project, primarily
+# the GTK apprt. Also includes X11/Wayland integrations and general
+# Linux support.
+#
+# - @ghostty-org/macos - The Ghostty macOS app and any macOS-specific
+# features, configurations, etc.
+#
+# - @ghostty-org/renderer - Ghostty rendering subsystem, including the
+# rendering abstractions as well as specific renderers like OpenGL
+# and Metal.
+#
+# - @ghostty-org/shell - Ghostty shell integration, including shell
+# completions, shell detection, and any other shell interactions.
+#
+# - @ghostty-org/terminal - The terminal emulator subsystem, including
+# subprocess management and pty handling, escape sequence parsing,
+# key encoding, etc.
+#
+# ## Outside of Ghostty
+#
+# Other "subsystems" exist outside of Ghostty and will not be represented
+# in this CODEOWNERS file:
+#
+# - @ghostty-org/discord-bot - Maintainers of the Ghostty Discord bot.
+#
+# - @ghostty-org/website - Maintainers of the Ghostty website.
+
+# Font
+/src/font/ @ghostty-org/font
+/pkg/fontconfig/ @ghostty-org/font
+/pkg/freetype/ @ghostty-org/font
+/pkg/harfbuzz/ @ghostty-org/font
+
+# GTK
+/src/apprt/gtk/ @ghostty-org/gtk
+/src/os/cgroup.zig @ghostty-org/gtk
+/src/os/flatpak.zig @ghostty-org/gtk
+/dist/linux/ @ghostty-org/gtk
+
+# macOS
+#
+# This includes libghostty because the macOS apprt is built on top of
+# libghostty and often requires or is impacted by changes to libghostty.
+# macOS subsystem maintainers are expected to only work on libghostty
+# insofar as it impacts the macOS apprt.
+/include/ghostty.h @ghostty-org/macos
+/src/apprt/embedded.zig @ghostty-org/macos
+/src/os/cf_release_thread.zig @ghostty-org/macos
+/src/os/macos.zig @ghostty-org/macos
+/macos/ @ghostty-org/macos
+/dist/macos/ @ghostty-org/macos
+/pkg/apple-sdk/ @ghostty-org/macos
+/pkg/macos/ @ghostty-org/macos
+
+# Renderer
+/src/renderer.zig @ghostty-org/renderer
+/src/renderer/ @ghostty-org/renderer
+/pkg/glslang/ @ghostty-org/renderer
+/pkg/opengl/ @ghostty-org/renderer
+/pkg/spirv-cross/ @ghostty-org/renderer
+/pkg/wuffs/ @ghostty-org/renderer
+
+# Shell
+/src/shell-integration/ @ghostty-org/shell
+/src/termio/shell-integration.zig @ghostty-org/shell
+
+# Terminal
+/src/simd/ @ghostty-org/terminal
+/src/terminal/ @ghostty-org/terminal
+/src/terminfo/ @ghostty-org/terminal
+/src/unicode/ @ghostty-org/terminal
+/src/Surface.zig @ghostty-org/terminal
+/src/surface_mouse.zig @ghostty-org/terminal
From cbe04785724f14626ef9cfd2dadf94eb059385b9 Mon Sep 17 00:00:00 2001
From: eifr
Date: Thu, 6 Feb 2025 10:19:23 +0200
Subject: [PATCH 214/365] remove config
---
src/apprt/gtk/App.zig | 10 ++++++----
src/config/Config.zig | 13 -------------
2 files changed, 6 insertions(+), 17 deletions(-)
diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig
index 4781b0ac3..e8bb98914 100644
--- a/src/apprt/gtk/App.zig
+++ b/src/apprt/gtk/App.zig
@@ -148,16 +148,18 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
@"gles-api": bool = false,
/// current gtk implementation for color management is not good enough.
/// see: https://bugs.kde.org/show_bug.cgi?id=495647
- @"color-mgmt": bool,
+ @"color-mgmt": bool = true,
/// Disabling Vulkan can improve startup times by hundreds of
/// milliseconds on some systems. We don't use Vulkan so we can just
/// disable it.
vulkan: bool = false,
- } = .{
- .@"color-mgmt" = config.@"gtk-gdk-disable-color-mgmt",
- };
+ } = .{};
environment: {
+ if (version.runtimeAtLeast(4, 17, 0)) {
+ gdk_disable.@"color-mgmt" = false;
+ }
+
if (version.runtimeAtLeast(4, 16, 0)) {
// From gtk 4.16, GDK_DEBUG is split into GDK_DEBUG and GDK_DISABLE.
// For the remainder of "why" see the 4.14 comment below.
diff --git a/src/config/Config.zig b/src/config/Config.zig
index 6d09fc9ce..3010b87d1 100644
--- a/src/config/Config.zig
+++ b/src/config/Config.zig
@@ -1350,19 +1350,6 @@ keybind: Keybinds = .{},
/// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color.
@"window-titlebar-foreground": ?Color = null,
-/// Controls whether to disable GDK color management in GTK applications.
-///
-/// By default this is set to `false`, meaning color management is enabled.
-/// You may want to enable this setting (set to `true`) if you experience:
-/// - Incorrect or washed out colors in your terminal
-/// - Color inconsistencies between GTK applications
-/// - Performance issues related to color management
-///
-/// This is a workaround for known issues with GTK's color management implementation,
-/// particularly affecting applications running under Wayland.
-/// See: https://bugs.kde.org/show_bug.cgi?id=495647
-@"gtk-gdk-disable-color-mgmt": bool = false,
-
/// This controls when resize overlays are shown. Resize overlays are a
/// transient popup that shows the size of the terminal while the surfaces are
/// being resized. The possible options are:
From 76cf58915be20ba002fd49dc1db75bf254753843 Mon Sep 17 00:00:00 2001
From: eifr
Date: Thu, 6 Feb 2025 10:25:26 +0200
Subject: [PATCH 215/365] add comments
---
src/apprt/gtk/App.zig | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig
index e8bb98914..a3290f783 100644
--- a/src/apprt/gtk/App.zig
+++ b/src/apprt/gtk/App.zig
@@ -148,6 +148,7 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
@"gles-api": bool = false,
/// current gtk implementation for color management is not good enough.
/// see: https://bugs.kde.org/show_bug.cgi?id=495647
+ /// gtk issue: https://gitlab.gnome.org/GNOME/gtk/-/issues/6864
@"color-mgmt": bool = true,
/// Disabling Vulkan can improve startup times by hundreds of
/// milliseconds on some systems. We don't use Vulkan so we can just
From 5d6c021e267183190815b7871c72739d96af3aab Mon Sep 17 00:00:00 2001
From: eifr
Date: Fri, 7 Feb 2025 18:00:08 +0200
Subject: [PATCH 216/365] update gtk version
---
src/apprt/gtk/App.zig | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig
index a3290f783..ed27f8394 100644
--- a/src/apprt/gtk/App.zig
+++ b/src/apprt/gtk/App.zig
@@ -157,7 +157,7 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
} = .{};
environment: {
- if (version.runtimeAtLeast(4, 17, 0)) {
+ if (version.runtimeAtLeast(4, 18, 0)) {
gdk_disable.@"color-mgmt" = false;
}
From ea16890fd3388781c6cfe3b62d7222a9da620401 Mon Sep 17 00:00:00 2001
From: Qwerasd
Date: Fri, 7 Feb 2025 12:19:09 -0500
Subject: [PATCH 217/365] Metal: use "Managed" resource storage mode on
discrete GPUs
Discrete GPUs cannot use the "shared" storage mode. This causes
undefined behavior right now, and I believe it's what's causing a
problem on Intel systems with discrete GPUs with "inverted" cells.
This commit also sets the CPU cache mode to "write combined" for our
resources since we don't read them back so Metal can optimize them
further with this hint.
---
src/renderer/Metal.zig | 125 +++++++++++++++++++++++++++-------
src/renderer/metal/api.zig | 40 ++++++++---
src/renderer/metal/buffer.zig | 52 +++++++++++---
src/renderer/metal/image.zig | 20 +++++-
4 files changed, 193 insertions(+), 44 deletions(-)
diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig
index 866f9682d..ca13f87de 100644
--- a/src/renderer/Metal.zig
+++ b/src/renderer/Metal.zig
@@ -182,15 +182,34 @@ pub const GPUState = struct {
/// This buffer is written exactly once so we can use it globally.
instance: InstanceBuffer, // MTLBuffer
+ /// The default storage mode to use for resources created with our device.
+ ///
+ /// This is based on whether the device is a discrete GPU or not, since
+ /// discrete GPUs do not have unified memory and therefore do not support
+ /// the "shared" storage mode, instead we have to use the "managed" mode.
+ default_storage_mode: mtl.MTLResourceOptions.StorageMode,
+
pub fn init() !GPUState {
const device = try chooseDevice();
const queue = device.msgSend(objc.Object, objc.sel("newCommandQueue"), .{});
errdefer queue.release();
+ // We determine whether our device is a discrete GPU based on these:
+ // - We're on macOS (iOS, iPadOS, etc. are guaranteed to be integrated).
+ // - We're not on aarch64 (Apple Silicon, therefore integrated).
+ // - The device reports that it does not have unified memory.
+ const is_discrete =
+ builtin.target.os.tag == .macos and
+ builtin.target.cpu.arch != .aarch64 and
+ !device.getProperty(bool, "hasUnifiedMemory");
+
+ const default_storage_mode: mtl.MTLResourceOptions.StorageMode =
+ if (is_discrete) .managed else .shared;
+
var instance = try InstanceBuffer.initFill(device, &.{
0, 1, 3, // Top-left triangle
1, 2, 3, // Bottom-right triangle
- });
+ }, .{ .storage_mode = default_storage_mode });
errdefer instance.deinit();
var result: GPUState = .{
@@ -198,11 +217,12 @@ pub const GPUState = struct {
.queue = queue,
.instance = instance,
.frames = undefined,
+ .default_storage_mode = default_storage_mode,
};
// Initialize all of our frame state.
for (&result.frames) |*frame| {
- frame.* = try FrameState.init(result.device);
+ frame.* = try FrameState.init(result.device, default_storage_mode);
}
return result;
@@ -288,18 +308,47 @@ pub const FrameState = struct {
const CellBgBuffer = mtl_buffer.Buffer(mtl_shaders.CellBg);
const CellTextBuffer = mtl_buffer.Buffer(mtl_shaders.CellText);
- pub fn init(device: objc.Object) !FrameState {
+ pub fn init(
+ device: objc.Object,
+ /// Storage mode for buffers and textures.
+ storage_mode: mtl.MTLResourceOptions.StorageMode,
+ ) !FrameState {
// Uniform buffer contains exactly 1 uniform struct. The
// uniform data will be undefined so this must be set before
// a frame is drawn.
- var uniforms = try UniformBuffer.init(device, 1);
+ var uniforms = try UniformBuffer.init(
+ device,
+ 1,
+ .{
+ // Indicate that the CPU writes to this resource but never reads it.
+ .cpu_cache_mode = .write_combined,
+ .storage_mode = storage_mode,
+ },
+ );
errdefer uniforms.deinit();
// Create the buffers for our vertex data. The preallocation size
// is likely too small but our first frame update will resize it.
- var cells = try CellTextBuffer.init(device, 10 * 10);
+ var cells = try CellTextBuffer.init(
+ device,
+ 10 * 10,
+ .{
+ // Indicate that the CPU writes to this resource but never reads it.
+ .cpu_cache_mode = .write_combined,
+ .storage_mode = storage_mode,
+ },
+ );
errdefer cells.deinit();
- var cells_bg = try CellBgBuffer.init(device, 10 * 10);
+ var cells_bg = try CellBgBuffer.init(
+ device,
+ 10 * 10,
+ .{
+ // Indicate that the CPU writes to this resource but never reads it.
+ .cpu_cache_mode = .write_combined,
+ .storage_mode = storage_mode,
+ },
+ );
+
errdefer cells_bg.deinit();
// Initialize our textures for our font atlas.
@@ -307,13 +356,13 @@ pub const FrameState = struct {
.data = undefined,
.size = 8,
.format = .grayscale,
- });
+ }, storage_mode);
errdefer grayscale.release();
const color = try initAtlasTexture(device, &.{
.data = undefined,
.size = 8,
.format = .rgba,
- });
+ }, storage_mode);
errdefer color.release();
return .{
@@ -1215,7 +1264,11 @@ pub fn updateFrame(
.replace_gray_alpha,
.replace_rgb,
.replace_rgba,
- => try kv.value_ptr.image.upload(self.alloc, self.gpu_state.device),
+ => try kv.value_ptr.image.upload(
+ self.alloc,
+ self.gpu_state.device,
+ self.gpu_state.default_storage_mode,
+ ),
.unload_pending,
.unload_replace,
@@ -1283,7 +1336,12 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void {
self.font_grid.lock.lockShared();
defer self.font_grid.lock.unlockShared();
frame.grayscale_modified = self.font_grid.atlas_grayscale.modified.load(.monotonic);
- try syncAtlasTexture(self.gpu_state.device, &self.font_grid.atlas_grayscale, &frame.grayscale);
+ try syncAtlasTexture(
+ self.gpu_state.device,
+ &self.font_grid.atlas_grayscale,
+ &frame.grayscale,
+ self.gpu_state.default_storage_mode,
+ );
}
texture: {
const modified = self.font_grid.atlas_color.modified.load(.monotonic);
@@ -1291,7 +1349,12 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void {
self.font_grid.lock.lockShared();
defer self.font_grid.lock.unlockShared();
frame.color_modified = self.font_grid.atlas_color.modified.load(.monotonic);
- try syncAtlasTexture(self.gpu_state.device, &self.font_grid.atlas_color, &frame.color);
+ try syncAtlasTexture(
+ self.gpu_state.device,
+ &self.font_grid.atlas_color,
+ &frame.color,
+ self.gpu_state.default_storage_mode,
+ );
}
// Command buffer (MTLCommandBuffer)
@@ -1618,7 +1681,11 @@ fn drawImagePlacement(
@as(f32, @floatFromInt(p.width)),
@as(f32, @floatFromInt(p.height)),
},
- }});
+ }}, .{
+ // Indicate that the CPU writes to this resource but never reads it.
+ .cpu_cache_mode = .write_combined,
+ .storage_mode = self.gpu_state.default_storage_mode,
+ });
defer buf.deinit();
// Set our buffer
@@ -3217,14 +3284,20 @@ fn addPreeditCell(
/// Sync the atlas data to the given texture. This copies the bytes
/// associated with the atlas to the given texture. If the atlas no longer
/// fits into the texture, the texture will be resized.
-fn syncAtlasTexture(device: objc.Object, atlas: *const font.Atlas, texture: *objc.Object) !void {
+fn syncAtlasTexture(
+ device: objc.Object,
+ atlas: *const font.Atlas,
+ texture: *objc.Object,
+ /// Storage mode for the MTLTexture object
+ storage_mode: mtl.MTLResourceOptions.StorageMode,
+) !void {
const width = texture.getProperty(c_ulong, "width");
if (atlas.size > width) {
// Free our old texture
texture.*.release();
// Reallocate
- texture.* = try initAtlasTexture(device, atlas);
+ texture.* = try initAtlasTexture(device, atlas, storage_mode);
}
texture.msgSend(
@@ -3247,7 +3320,12 @@ fn syncAtlasTexture(device: objc.Object, atlas: *const font.Atlas, texture: *obj
}
/// Initialize a MTLTexture object for the given atlas.
-fn initAtlasTexture(device: objc.Object, atlas: *const font.Atlas) !objc.Object {
+fn initAtlasTexture(
+ device: objc.Object,
+ atlas: *const font.Atlas,
+ /// Storage mode for the MTLTexture object
+ storage_mode: mtl.MTLResourceOptions.StorageMode,
+) !objc.Object {
// Determine our pixel format
const pixel_format: mtl.MTLPixelFormat = switch (atlas.format) {
.grayscale => .r8unorm,
@@ -3268,15 +3346,14 @@ fn initAtlasTexture(device: objc.Object, atlas: *const font.Atlas) !objc.Object
desc.setProperty("width", @as(c_ulong, @intCast(atlas.size)));
desc.setProperty("height", @as(c_ulong, @intCast(atlas.size)));
- // Xcode tells us that this texture should be shared mode on
- // aarch64. This configuration is not supported on x86_64 so
- // we only set it on aarch64.
- if (comptime builtin.target.cpu.arch == .aarch64) {
- desc.setProperty(
- "storageMode",
- @as(c_ulong, mtl.MTLResourceStorageModeShared),
- );
- }
+ desc.setProperty(
+ "resourceOptions",
+ mtl.MTLResourceOptions{
+ // Indicate that the CPU writes to this resource but never reads it.
+ .cpu_cache_mode = .write_combined,
+ .storage_mode = storage_mode,
+ },
+ );
// Initialize
const id = device.msgSend(
diff --git a/src/renderer/metal/api.zig b/src/renderer/metal/api.zig
index 6ab42bbd6..535a0b42b 100644
--- a/src/renderer/metal/api.zig
+++ b/src/renderer/metal/api.zig
@@ -24,12 +24,36 @@ pub const MTLStoreAction = enum(c_ulong) {
store = 1,
};
-/// https://developer.apple.com/documentation/metal/mtlstoragemode?language=objc
-pub const MTLStorageMode = enum(c_ulong) {
- shared = 0,
- managed = 1,
- private = 2,
- memoryless = 3,
+/// https://developer.apple.com/documentation/metal/mtlresourceoptions?language=objc
+pub const MTLResourceOptions = packed struct(c_ulong) {
+ /// https://developer.apple.com/documentation/metal/mtlcpucachemode?language=objc
+ cpu_cache_mode: CPUCacheMode = .default,
+ /// https://developer.apple.com/documentation/metal/mtlstoragemode?language=objc
+ storage_mode: StorageMode,
+ /// https://developer.apple.com/documentation/metal/mtlhazardtrackingmode?language=objc
+ hazard_tracking_mode: HazardTrackingMode = .default,
+
+ _pad: @Type(.{
+ .Int = .{ .signedness = .unsigned, .bits = @bitSizeOf(c_ulong) - 10 },
+ }) = 0,
+
+ pub const CPUCacheMode = enum(u4) {
+ default = 0,
+ write_combined = 1,
+ };
+
+ pub const StorageMode = enum(u4) {
+ shared = 0,
+ managed = 1,
+ private = 2,
+ memoryless = 3,
+ };
+
+ pub const HazardTrackingMode = enum(u2) {
+ default = 0,
+ untracked = 1,
+ tracked = 2,
+ };
};
/// https://developer.apple.com/documentation/metal/mtlprimitivetype?language=objc
@@ -139,10 +163,6 @@ pub const MTLTextureUsage = enum(c_ulong) {
pixel_format_view = 8,
};
-/// https://developer.apple.com/documentation/metal/mtlresourceoptions?language=objc
-/// (incomplete, we only use this mode so we just hardcode it)
-pub const MTLResourceStorageModeShared: c_ulong = @intFromEnum(MTLStorageMode.shared) << 4;
-
pub const MTLClearColor = extern struct {
red: f64,
green: f64,
diff --git a/src/renderer/metal/buffer.zig b/src/renderer/metal/buffer.zig
index 55a207f03..4128e297b 100644
--- a/src/renderer/metal/buffer.zig
+++ b/src/renderer/metal/buffer.zig
@@ -2,6 +2,7 @@ const std = @import("std");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const objc = @import("objc");
+const macos = @import("macos");
const mtl = @import("api.zig");
@@ -14,35 +15,46 @@ pub fn Buffer(comptime T: type) type {
return struct {
const Self = @This();
+ /// The resource options for this buffer.
+ options: mtl.MTLResourceOptions,
+
buffer: objc.Object, // MTLBuffer
/// Initialize a buffer with the given length pre-allocated.
- pub fn init(device: objc.Object, len: usize) !Self {
+ pub fn init(
+ device: objc.Object,
+ len: usize,
+ options: mtl.MTLResourceOptions,
+ ) !Self {
const buffer = device.msgSend(
objc.Object,
objc.sel("newBufferWithLength:options:"),
.{
@as(c_ulong, @intCast(len * @sizeOf(T))),
- mtl.MTLResourceStorageModeShared,
+ options,
},
);
- return .{ .buffer = buffer };
+ return .{ .buffer = buffer, .options = options };
}
/// Init the buffer filled with the given data.
- pub fn initFill(device: objc.Object, data: []const T) !Self {
+ pub fn initFill(
+ device: objc.Object,
+ data: []const T,
+ options: mtl.MTLResourceOptions,
+ ) !Self {
const buffer = device.msgSend(
objc.Object,
objc.sel("newBufferWithBytes:length:options:"),
.{
@as(*const anyopaque, @ptrCast(data.ptr)),
@as(c_ulong, @intCast(data.len * @sizeOf(T))),
- mtl.MTLResourceStorageModeShared,
+ options,
},
);
- return .{ .buffer = buffer };
+ return .{ .buffer = buffer, .options = options };
}
pub fn deinit(self: *Self) void {
@@ -85,7 +97,7 @@ pub fn Buffer(comptime T: type) type {
objc.sel("newBufferWithLength:options:"),
.{
@as(c_ulong, @intCast(size * @sizeOf(T))),
- mtl.MTLResourceStorageModeShared,
+ self.options,
},
);
}
@@ -106,6 +118,18 @@ pub fn Buffer(comptime T: type) type {
};
@memcpy(dst, src);
+
+ // If we're using the managed resource storage mode, then
+ // we need to signal Metal to synchronize the buffer data.
+ //
+ // Ref: https://developer.apple.com/documentation/metal/synchronizing-a-managed-resource-in-macos?language=objc
+ if (self.options.storage_mode == .managed) {
+ self.buffer.msgSend(
+ void,
+ "didModifyRange:",
+ .{macos.foundation.Range.init(0, req_bytes)},
+ );
+ }
}
/// Like Buffer.sync but takes data from an array of ArrayLists,
@@ -130,7 +154,7 @@ pub fn Buffer(comptime T: type) type {
objc.sel("newBufferWithLength:options:"),
.{
@as(c_ulong, @intCast(size * @sizeOf(T))),
- mtl.MTLResourceStorageModeShared,
+ self.options,
},
);
}
@@ -153,6 +177,18 @@ pub fn Buffer(comptime T: type) type {
i += list.items.len * @sizeOf(T);
}
+ // If we're using the managed resource storage mode, then
+ // we need to signal Metal to synchronize the buffer data.
+ //
+ // Ref: https://developer.apple.com/documentation/metal/synchronizing-a-managed-resource-in-macos?language=objc
+ if (self.options.storage_mode == .managed) {
+ self.buffer.msgSend(
+ void,
+ "didModifyRange:",
+ .{macos.foundation.Range.init(0, req_bytes)},
+ );
+ }
+
return total_len;
}
};
diff --git a/src/renderer/metal/image.zig b/src/renderer/metal/image.zig
index 9d72cae96..835fbd672 100644
--- a/src/renderer/metal/image.zig
+++ b/src/renderer/metal/image.zig
@@ -358,6 +358,8 @@ pub const Image = union(enum) {
self: *Image,
alloc: Allocator,
device: objc.Object,
+ /// Storage mode for the MTLTexture object
+ storage_mode: mtl.MTLResourceOptions.StorageMode,
) !void {
// Convert our data if we have to
try self.convert(alloc);
@@ -366,7 +368,7 @@ pub const Image = union(enum) {
const p = self.pending().?;
// Create our texture
- const texture = try initTexture(p, device);
+ const texture = try initTexture(p, device, storage_mode);
errdefer texture.msgSend(void, objc.sel("release"), .{});
// Upload our data
@@ -424,7 +426,12 @@ pub const Image = union(enum) {
};
}
- fn initTexture(p: Pending, device: objc.Object) !objc.Object {
+ fn initTexture(
+ p: Pending,
+ device: objc.Object,
+ /// Storage mode for the MTLTexture object
+ storage_mode: mtl.MTLResourceOptions.StorageMode,
+ ) !objc.Object {
// Create our descriptor
const desc = init: {
const Class = objc.getClass("MTLTextureDescriptor").?;
@@ -438,6 +445,15 @@ pub const Image = union(enum) {
desc.setProperty("width", @as(c_ulong, @intCast(p.width)));
desc.setProperty("height", @as(c_ulong, @intCast(p.height)));
+ desc.setProperty(
+ "resourceOptions",
+ mtl.MTLResourceOptions{
+ // Indicate that the CPU writes to this resource but never reads it.
+ .cpu_cache_mode = .write_combined,
+ .storage_mode = storage_mode,
+ },
+ );
+
// Initialize
const id = device.msgSend(
?*anyopaque,
From 1947ba9c68446f3ec793906923d1d95e654ae649 Mon Sep 17 00:00:00 2001
From: "Jeffrey C. Ollie"
Date: Fri, 7 Feb 2025 22:20:37 -0600
Subject: [PATCH 218/365] core: protect against crashes and hangs when themes
are not files
If a theme was not a file or a directory you could get a crash or a hang
(depending on platform) if the theme references a directory. This patch
also prevents attempts to load from other non-file sources.
Fixes: #5596
---
src/cli/args.zig | 10 +++++---
src/config/theme.zig | 58 +++++++++++++++++++++++++++++++++++++++++---
2 files changed, 62 insertions(+), 6 deletions(-)
diff --git a/src/cli/args.zig b/src/cli/args.zig
index 166b2daf5..7385e6a3e 100644
--- a/src/cli/args.zig
+++ b/src/cli/args.zig
@@ -8,6 +8,8 @@ const internal_os = @import("../os/main.zig");
const Diagnostic = diags.Diagnostic;
const DiagnosticList = diags.DiagnosticList;
+const log = std.log.scoped(.cli);
+
// TODO:
// - Only `--long=value` format is accepted. Do we want to allow
// `--long value`? Not currently allowed.
@@ -1258,9 +1260,11 @@ pub fn LineIterator(comptime ReaderType: type) type {
const buf = buf: {
while (true) {
// Read the full line
- var entry = self.r.readUntilDelimiterOrEof(self.entry[2..], '\n') catch {
- // TODO: handle errors
- unreachable;
+ var entry = self.r.readUntilDelimiterOrEof(self.entry[2..], '\n') catch |err| switch (err) {
+ inline else => |e| {
+ log.warn("cannot read from \"{s}\": {}", .{ self.filepath, e });
+ return null;
+ },
} orelse return null;
// Increment our line counter
diff --git a/src/config/theme.zig b/src/config/theme.zig
index b851ec3d4..2d206e1f6 100644
--- a/src/config/theme.zig
+++ b/src/config/theme.zig
@@ -104,6 +104,10 @@ pub const LocationIterator = struct {
/// Due to the way allocations are handled, an Arena allocator (or another
/// similar allocator implementation) should be used. It may not be safe to
/// free the returned allocations.
+///
+/// This will never return anything other than a handle to a regular file. If
+/// the theme resolves to something other than a regular file a diagnostic entry
+/// will be added to the list and null will be returned.
pub fn open(
arena_alloc: Allocator,
theme: []const u8,
@@ -119,6 +123,29 @@ pub fn open(
theme,
diags,
) orelse return null;
+ const stat = file.stat() catch |err| {
+ try diags.append(arena_alloc, .{
+ .message = try std.fmt.allocPrintZ(
+ arena_alloc,
+ "not reading theme from \"{s}\": {}",
+ .{ theme, err },
+ ),
+ });
+ return null;
+ };
+ switch (stat.kind) {
+ .file => {},
+ else => {
+ try diags.append(arena_alloc, .{
+ .message = try std.fmt.allocPrintZ(
+ arena_alloc,
+ "not reading theme from \"{s}\": it is a {s}",
+ .{ theme, @tagName(stat.kind) },
+ ),
+ });
+ return null;
+ },
+ }
return .{ .path = theme, .file = file };
}
@@ -140,9 +167,34 @@ pub fn open(
const cwd = std.fs.cwd();
while (try it.next()) |loc| {
const path = try std.fs.path.join(arena_alloc, &.{ loc.dir, theme });
- if (cwd.openFile(path, .{})) |file| return .{
- .path = path,
- .file = file,
+ if (cwd.openFile(path, .{})) |file| {
+ const stat = file.stat() catch |err| {
+ try diags.append(arena_alloc, .{
+ .message = try std.fmt.allocPrintZ(
+ arena_alloc,
+ "not reading theme from \"{s}\": {}",
+ .{ theme, err },
+ ),
+ });
+ return null;
+ };
+ switch (stat.kind) {
+ .file => {},
+ else => {
+ try diags.append(arena_alloc, .{
+ .message = try std.fmt.allocPrintZ(
+ arena_alloc,
+ "not reading theme from \"{s}\": it is a {s}",
+ .{ theme, @tagName(stat.kind) },
+ ),
+ });
+ return null;
+ },
+ }
+ return .{
+ .path = path,
+ .file = file,
+ };
} else |err| switch (err) {
// Not an error, just continue to the next location.
error.FileNotFound => {},
From 28200fb9e5e0e54cbb075b9bd47357ea4f71a04a Mon Sep 17 00:00:00 2001
From: Qwerasd
Date: Sun, 9 Feb 2025 17:45:00 -0500
Subject: [PATCH 219/365] fix(Metal): don't throw away glyphs when the fg and
bg colors match
This optimization is extremely niche and seems to be related to a
strange bug experienced by Intel Mac users. Considering it costs some
amount to have this extra check here even though it's false in the vast
majority of cases, I feel it's pretty safe to just remove it entirely.
---
src/renderer/shaders/cell.metal | 7 -------
1 file changed, 7 deletions(-)
diff --git a/src/renderer/shaders/cell.metal b/src/renderer/shaders/cell.metal
index 5f161c6b7..e24ddcb1e 100644
--- a/src/renderer/shaders/cell.metal
+++ b/src/renderer/shaders/cell.metal
@@ -487,13 +487,6 @@ vertex CellTextVertexOut cell_text_vertex(
);
}
- // Don't bother rendering if the bg and fg colors are identical, just return
- // the same point which will be culled because it makes the quad zero sized.
- // However, we should still render if this is the cursor position
- if (all(out.color == out.bg_color) && !is_cursor_pos) {
- out.position = float4(0.0);
- }
-
return out;
}
From 09c76d95c7d235db0953623f980a7c47bfc94568 Mon Sep 17 00:00:00 2001
From: Qwerasd
Date: Sun, 9 Feb 2025 22:48:38 -0500
Subject: [PATCH 220/365] fix(terminal): handle errors in Screen.adjustCapacity
Previously assumed adjusted capacities would always fit the cursor style
and hyperlink, this is not necessarily the case and led to memory
corruption in scenarios with large hyperlinks.
---
src/terminal/Screen.zig | 68 ++++++++++++++++++++++++++++++++++-------
1 file changed, 57 insertions(+), 11 deletions(-)
diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig
index a838e0e10..046ecb1b4 100644
--- a/src/terminal/Screen.zig
+++ b/src/terminal/Screen.zig
@@ -474,26 +474,40 @@ pub fn adjustCapacity(
const new_node = try self.pages.adjustCapacity(node, adjustment);
const new_page: *Page = &new_node.data;
- // All additions below have unreachable catches because when
- // we adjust cap we should have enough memory to fit the
- // existing data.
-
- // Re-add the style
+ // Re-add the style, if the page somehow doesn't have enough
+ // memory to add it, we emit a warning and gracefully degrade
+ // to the default style for the cursor.
if (self.cursor.style_id != 0) {
self.cursor.style_id = new_page.styles.add(
new_page.memory,
self.cursor.style,
- ) catch unreachable;
+ ) catch |err| id: {
+ // TODO: Should we increase the capacity further in this case?
+ log.err(
+ "(Screen.adjustCapacity) Failed to add cursor style back to page, err={}",
+ .{err},
+ );
+
+ break :id style.default_id;
+ };
}
- // Re-add the hyperlink
+ // Re-add the hyperlink, if the page somehow doesn't have enough
+ // memory to add it, we emit a warning and gracefully degrade to
+ // no hyperlink.
if (self.cursor.hyperlink) |link| {
// So we don't attempt to free any memory in the replaced page.
self.cursor.hyperlink_id = 0;
self.cursor.hyperlink = null;
// Re-add
- self.startHyperlinkOnce(link.*) catch unreachable;
+ self.startHyperlinkOnce(link.*) catch |err| {
+ // TODO: Should we increase the capacity further in this case?
+ log.err(
+ "(Screen.adjustCapacity) Failed to add cursor hyperlink back to page, err={}",
+ .{err},
+ );
+ };
// Remove our old link
link.deinit(self.alloc);
@@ -1003,8 +1017,9 @@ pub fn cursorCopy(self: *Screen, other: Cursor, opts: struct {
/// Always use this to write to cursor.page_pin.*.
///
/// This specifically handles the case when the new pin is on a different
-/// page than the old AND we have a style set. In that case, we must release
-/// our old style and upsert our new style since styles are stored per-page.
+/// page than the old AND we have a style or hyperlink set. In that case,
+/// we must release our old one and insert the new one, since styles are
+/// stored per-page.
fn cursorChangePin(self: *Screen, new: Pin) void {
// Moving the cursor affects text run splitting (ligatures) so
// we must mark the old and new page dirty. We do this as long
@@ -1986,9 +2001,40 @@ pub fn cursorSetHyperlink(self: *Screen) !void {
} else |err| switch (err) {
// hyperlink_map is out of space, realloc the page to be larger
error.HyperlinkMapOutOfMemory => {
+ const uri_size = if (self.cursor.hyperlink) |link| link.uri.len else 0;
+
+ var string_bytes = page.capacity.string_bytes;
+
+ // Attempt to allocate the space that would be required to
+ // insert a new copy of the cursor hyperlink uri in to the
+ // string alloc, since right now adjustCapacity always just
+ // adds an extra copy even if one already exists in the page.
+ // If this alloc fails then we know we also need to grow our
+ // string bytes.
+ //
+ // FIXME: This SUCKS
+ if (page.string_alloc.alloc(
+ u8,
+ page.memory,
+ uri_size,
+ )) |slice| {
+ // We don't bother freeing because we're
+ // about to free the entire page anyway.
+ _ = &slice;
+ } else |_| {
+ // We didn't have enough room, let's just double our
+ // string bytes until there's definitely enough room
+ // for our uri.
+ const before = string_bytes;
+ while (string_bytes - before < uri_size) string_bytes *= 2;
+ }
+
_ = try self.adjustCapacity(
self.cursor.page_pin.node,
- .{ .hyperlink_bytes = page.capacity.hyperlink_bytes * 2 },
+ .{
+ .hyperlink_bytes = page.capacity.hyperlink_bytes * 2,
+ .string_bytes = string_bytes,
+ },
);
// Retry
From 03fd9a970bf1d1760aef500eda4f83c0f13f5962 Mon Sep 17 00:00:00 2001
From: Qwerasd
Date: Mon, 10 Feb 2025 11:49:05 -0500
Subject: [PATCH 221/365] fix(terminal): properly invalidate over-sized OSCs
The `self.complete = false` here is important so we don't accidentally
dispatch, for example, a hyperlink command with an empty URI.
---
src/terminal/osc.zig | 19 ++++++++++++++++++-
1 file changed, 18 insertions(+), 1 deletion(-)
diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig
index 10ba5b5e7..90dd079a0 100644
--- a/src/terminal/osc.zig
+++ b/src/terminal/osc.zig
@@ -272,6 +272,9 @@ pub const Parser = struct {
// Maximum length of a single OSC command. This is the full OSC command
// sequence length (excluding ESC ]). This is arbitrary, I couldn't find
// any definitive resource on how long this should be.
+ //
+ // NOTE: This does mean certain OSC sequences such as OSC 8 (hyperlinks)
+ // won't work if their parameters are larger than fit in the buffer.
const MAX_BUF = 2048;
pub const State = enum {
@@ -425,9 +428,23 @@ pub const Parser = struct {
/// Consume the next character c and advance the parser state.
pub fn next(self: *Parser, c: u8) void {
- // If our buffer is full then we're invalid.
+ // If our buffer is full then we're invalid, so we set our state
+ // accordingly and indicate the sequence is incomplete so that we
+ // don't accidentally issue a command when ending.
if (self.buf_idx >= self.buf.len) {
+ if (self.state != .invalid) {
+ log.warn(
+ "OSC sequence too long (> {d}), ignoring. state={}",
+ .{ self.buf.len, self.state },
+ );
+ }
+
self.state = .invalid;
+
+ // We have to do this here because it will never reach the
+ // switch statement below, since our buf_idx will always be
+ // too high after this.
+ self.complete = false;
return;
}
From 843c7140880f002c52d4c4a3715062b02e967990 Mon Sep 17 00:00:00 2001
From: Leah Amelia Chen
Date: Fri, 31 Jan 2025 11:54:31 +0100
Subject: [PATCH 222/365] gtk: introduce Zig bindings for GTK/GObject
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
`zig-gobject` is a set of GObject bindings that allow us to write
GTK-facing code in Zig instead of getting hands dirty with C.
It's been tested and refined in real-life applications and several
GTK contributors agree that it is a marked improvement over using
the C API directly, such as allowing method call syntax and avoiding
many manual `@ptrCast`s.
This commit doesn't actually contain any changes to our preexisting
GTK code — the migration process is intended to begin in chunks,
firstly in self-contained components (e.g. the header bar, overlays,
etc.), and then full-scale migration can begin when we remove non-Adwaita
GTK builds for 1.2. (After all, why port code that you'll remove later
either way?)
---
build.zig.zon | 4 ++++
nix/devShell.nix | 3 +++
nix/package.nix | 2 ++
nix/zigCacheHash.nix | 2 +-
src/build/SharedDeps.zig | 28 ++++++++++++++++++++++++++--
5 files changed, 36 insertions(+), 3 deletions(-)
diff --git a/build.zig.zon b/build.zig.zon
index a5abdc586..9a4772c53 100644
--- a/build.zig.zon
+++ b/build.zig.zon
@@ -41,6 +41,10 @@
.url = "git+https://github.com/natecraddock/zf/?ref=main#ed99ca18b02dda052e20ba467e90b623c04690dd",
.hash = "1220edc3b8d8bedbb50555947987e5e8e2f93871ca3c8e8d4cc8f1377c15b5dd35e8",
},
+ .gobject = .{
+ .url = "https://github.com/ianprime0509/zig-gobject/releases/download/v0.2.2/bindings-gnome47.tar.zst",
+ .hash = "12208d70ee791d7ef7e16e1c3c9c1127b57f1ed066a24f87d57fc9f730c5dc394b9d",
+ },
// C libs
.cimgui = .{ .path = "./pkg/cimgui" },
diff --git a/nix/devShell.nix b/nix/devShell.nix
index c52afb6c0..9779dc4c6 100644
--- a/nix/devShell.nix
+++ b/nix/devShell.nix
@@ -30,6 +30,7 @@
glib,
glslang,
gtk4,
+ gobject-introspection,
libadwaita,
adwaita-icon-theme,
hicolor-icon-theme,
@@ -83,6 +84,7 @@
libadwaita
gtk4
glib
+ gobject-introspection
wayland
];
in
@@ -157,6 +159,7 @@ in
libadwaita
gtk4
glib
+ gobject-introspection
wayland
wayland-scanner
wayland-protocols
diff --git a/nix/package.nix b/nix/package.nix
index ceb6a7688..55d00b76c 100644
--- a/nix/package.nix
+++ b/nix/package.nix
@@ -12,6 +12,7 @@
libGL,
glib,
gtk4,
+ gobject-introspection,
libadwaita,
wrapGAppsHook4,
gsettings-desktop-schemas,
@@ -124,6 +125,7 @@ in
pandoc
pkg-config
zig_hook
+ gobject-introspection
wrapGAppsHook4
]
++ lib.optionals enableWayland [
diff --git a/nix/zigCacheHash.nix b/nix/zigCacheHash.nix
index 975605049..78e2d509d 100644
--- a/nix/zigCacheHash.nix
+++ b/nix/zigCacheHash.nix
@@ -1,3 +1,3 @@
# This file is auto-generated! check build-support/check-zig-cache-hash.sh for
# more details.
-"sha256-I7uuv0MkaW3gWAw6NHci+II42OfM7NdtKh2Npw2pTis="
+"sha256-+Ag900R3lDV7iEeRFGe2HWJDtquW3I9GFvHGle+U3k0="
diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig
index 64068658d..34666cf8a 100644
--- a/src/build/SharedDeps.zig
+++ b/src/build/SharedDeps.zig
@@ -430,9 +430,32 @@ pub fn add(
},
.gtk => {
+ const gobject = b.dependency("gobject", .{
+ .target = target,
+ .optimize = optimize,
+ });
+ const gobject_imports = .{
+ .{ "gobject", "gobject2" },
+ .{ "glib", "glib2" },
+ .{ "gtk", "gtk4" },
+ .{ "gdk", "gdk4" },
+ };
+ inline for (gobject_imports) |import| {
+ const name, const module = import;
+ step.root_module.addImport(name, gobject.module(module));
+ }
+
step.linkSystemLibrary2("gtk4", dynamic_link_opts);
- if (self.config.adwaita) step.linkSystemLibrary2("libadwaita-1", dynamic_link_opts);
- if (self.config.x11) step.linkSystemLibrary2("X11", dynamic_link_opts);
+
+ if (self.config.adwaita) {
+ step.linkSystemLibrary2("libadwaita-1", dynamic_link_opts);
+ step.root_module.addImport("adw", gobject.module("adw1"));
+ }
+
+ if (self.config.x11) {
+ step.linkSystemLibrary2("X11", dynamic_link_opts);
+ step.root_module.addImport("gdk_x11", gobject.module("gdkx114"));
+ }
if (self.config.wayland) {
const scanner = Scanner.create(b.dependency("zig_wayland", .{}), .{
@@ -460,6 +483,7 @@ pub fn add(
scanner.generate("org_kde_kwin_server_decoration_manager", 1);
step.root_module.addImport("wayland", wayland);
+ step.root_module.addImport("gdk_wayland", gobject.module("gdkwayland4"));
step.linkSystemLibrary2("wayland-client", dynamic_link_opts);
}
From 1c524238c837cbd0d550a2e74af6df6e6235794a Mon Sep 17 00:00:00 2001
From: Qwerasd
Date: Mon, 10 Feb 2025 13:27:26 -0500
Subject: [PATCH 223/365] test(terminal/osc): fix command longer than buffer
test
Ensure that the state is invalidated properly, this previously wasn't
the case but was fixed in 03fd9a97
---
src/terminal/osc.zig | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig
index 90dd079a0..faf376d13 100644
--- a/src/terminal/osc.zig
+++ b/src/terminal/osc.zig
@@ -1660,10 +1660,11 @@ test "OSC: longer than buffer" {
var p: Parser = .{};
- const input = "a" ** (Parser.MAX_BUF + 2);
+ const input = "0;" ++ "a" ** (Parser.MAX_BUF + 2);
for (input) |ch| p.next(ch);
try testing.expect(p.end(null) == null);
+ try testing.expect(p.complete == false);
}
test "OSC: report default foreground color" {
From e540a79032c4c7c691e1299b3e0a6b4fced10013 Mon Sep 17 00:00:00 2001
From: Qwerasd
Date: Mon, 10 Feb 2025 14:19:26 -0500
Subject: [PATCH 224/365] test(terminal/screen): OOM handling in adjustCapacity
Adds tests for the adjustCapacity changes from 09c76d95
Fixes a small oversight in that change as well (resetting cursor style).
---
src/terminal/Screen.zig | 114 +++++++++++++++++++++++++++++++++++++++-
1 file changed, 112 insertions(+), 2 deletions(-)
diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig
index 046ecb1b4..b11c207a5 100644
--- a/src/terminal/Screen.zig
+++ b/src/terminal/Screen.zig
@@ -483,11 +483,13 @@ pub fn adjustCapacity(
self.cursor.style,
) catch |err| id: {
// TODO: Should we increase the capacity further in this case?
- log.err(
+ log.warn(
"(Screen.adjustCapacity) Failed to add cursor style back to page, err={}",
.{err},
);
+ // Reset the cursor style.
+ self.cursor.style = .{};
break :id style.default_id;
};
}
@@ -503,7 +505,7 @@ pub fn adjustCapacity(
// Re-add
self.startHyperlinkOnce(link.*) catch |err| {
// TODO: Should we increase the capacity further in this case?
- log.err(
+ log.warn(
"(Screen.adjustCapacity) Failed to add cursor hyperlink back to page, err={}",
.{err},
);
@@ -2928,6 +2930,9 @@ pub fn testWriteString(self: *Screen, text: []const u8) !void {
.protected = self.cursor.protected,
};
+ // If we have a hyperlink, add it to the cell.
+ if (self.cursor.hyperlink_id > 0) try self.cursorSetHyperlink();
+
// If we have a ref-counted style, increase.
if (self.cursor.style_id != style.default_id) {
const page = self.cursor.page_pin.node.data;
@@ -2946,6 +2951,9 @@ pub fn testWriteString(self: *Screen, text: []const u8) !void {
.protected = self.cursor.protected,
};
+ // If we have a hyperlink, add it to the cell.
+ if (self.cursor.hyperlink_id > 0) try self.cursorSetHyperlink();
+
self.cursor.page_row.wrap = true;
try self.cursorDownOrScroll();
self.cursorHorizontalAbsolute(0);
@@ -2961,6 +2969,9 @@ pub fn testWriteString(self: *Screen, text: []const u8) !void {
.protected = self.cursor.protected,
};
+ // If we have a hyperlink, add it to the cell.
+ if (self.cursor.hyperlink_id > 0) try self.cursorSetHyperlink();
+
// Write our tail
self.cursorRight(1);
self.cursor.page_cell.* = .{
@@ -2970,6 +2981,9 @@ pub fn testWriteString(self: *Screen, text: []const u8) !void {
.protected = self.cursor.protected,
};
+ // If we have a hyperlink, add it to the cell.
+ if (self.cursor.hyperlink_id > 0) try self.cursorSetHyperlink();
+
// If we have a ref-counted style, increase twice.
if (self.cursor.style_id != style.default_id) {
const page = self.cursor.page_pin.node.data;
@@ -8805,6 +8819,102 @@ test "Screen: adjustCapacity cursor style ref count" {
}
}
+test "Screen: adjustCapacity cursor hyperlink exceeds string alloc size" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var s = try init(alloc, 80, 24, 0);
+ defer s.deinit();
+
+ // Start a hyperlink with a URI that just barely fits in the string alloc.
+ // This will ensure that the redundant copy added in `adjustCapacity` won't
+ // fit in the available string alloc space.
+ const uri = "a" ** (pagepkg.std_capacity.string_bytes - 8);
+ try s.startHyperlink(uri, null);
+
+ // Write some characters with this so that the URI
+ // is copied to the new page when adjusting capacity.
+ try s.testWriteString("Hello");
+
+ // Adjust the capacity, right now this will cause a redundant copy of
+ // the URI to be added to the string alloc, but since there isn't room
+ // for this this will clear the cursor hyperlink.
+ _ = try s.adjustCapacity(s.cursor.page_pin.node, .{});
+
+ // The cursor hyperlink should have been cleared by the `adjustCapacity`
+ // call, because there isn't enough room to add the redundant URI string.
+ //
+ // This behavior will change, causing this test to fail, if any of these
+ // changes are made:
+ //
+ // - The string alloc is changed to intern strings.
+ //
+ // - The adjustCapacity function is changed to ensure the new
+ // capacity will fit the redundant copy of the hyperlink uri.
+ //
+ // - The cursor managed memory handling is reworked so that it
+ // doesn't reside in the pages anymore and doesn't need this
+ // accounting.
+ //
+ // In such a case, adjust this test accordingly.
+ try testing.expectEqual(null, s.cursor.hyperlink);
+ try testing.expectEqual(0, s.cursor.hyperlink_id);
+}
+
+test "Screen: adjustCapacity cursor style exceeds style set capacity" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var s = try init(alloc, 80, 24, 1000);
+ defer s.deinit();
+
+ const page = &s.cursor.page_pin.node.data;
+
+ // We add unique styles to the page until no more will fit.
+ fill: for (0..255) |bg| {
+ for (0..255) |fg| {
+ const st: style.Style = .{
+ .bg_color = .{ .palette = @intCast(bg) },
+ .fg_color = .{ .palette = @intCast(fg) },
+ };
+
+ s.cursor.style = st;
+
+ // Try to insert the new style, if it doesn't fit then
+ // we succeeded in filling the style set, so we break.
+ s.cursor.style_id = page.styles.add(
+ page.memory,
+ s.cursor.style,
+ ) catch break :fill;
+
+ try s.testWriteString("a");
+ }
+ }
+
+ // Adjust the capacity, this should cause the style set to reach the
+ // same state it was in to begin with, since it will clone the page
+ // in the same order as the styles were added to begin with, meaning
+ // the cursor style will not be able to be added to the set, which
+ // should, right now, result in the cursor style being cleared.
+ _ = try s.adjustCapacity(s.cursor.page_pin.node, .{});
+
+ // The cursor style should have been cleared by the `adjustCapacity`.
+ //
+ // This behavior will change, causing this test to fail, if either
+ // of these changes are made:
+ //
+ // - The adjustCapacity function is changed to ensure the
+ // new capacity will definitely fit the cursor style.
+ //
+ // - The cursor managed memory handling is reworked so that it
+ // doesn't reside in the pages anymore and doesn't need this
+ // accounting.
+ //
+ // In such a case, adjust this test accordingly.
+ try testing.expect(s.cursor.style.default());
+ try testing.expectEqual(style.default_id, s.cursor.style_id);
+}
+
test "Screen UTF8 cell map with newlines" {
const testing = std.testing;
const alloc = testing.allocator;
From a1b682d0da53fbdc1d9e40b758ab61f744b3897d Mon Sep 17 00:00:00 2001
From: Qwerasd
Date: Mon, 10 Feb 2025 15:45:53 -0500
Subject: [PATCH 225/365] fix(terminal): properly lookup hyperlinks when
cloning rows across pages
Before this we were doing bad things with the memory, looking at
`PageEntry`s for links not actually in the page we were reading the
strings from.
---
src/terminal/hyperlink.zig | 14 ++++++++++++--
src/terminal/page.zig | 6 +-----
src/terminal/ref_counted_set.zig | 6 ++++++
3 files changed, 19 insertions(+), 7 deletions(-)
diff --git a/src/terminal/hyperlink.zig b/src/terminal/hyperlink.zig
index 1ab3c5ea7..bb9e78ca6 100644
--- a/src/terminal/hyperlink.zig
+++ b/src/terminal/hyperlink.zig
@@ -194,14 +194,24 @@ pub const Set = RefCountedSet(
Id,
size.CellCountInt,
struct {
+ /// The page which holds the strings for items in this set.
page: ?*Page = null,
+ /// The page which holds the strings for items
+ /// looked up with, e.g., `add` or `lookup`,
+ /// if different from the destination page.
+ src_page: ?*const Page = null,
+
pub fn hash(self: *const @This(), link: PageEntry) u64 {
- return link.hash(self.page.?.memory);
+ return link.hash((self.src_page orelse self.page.?).memory);
}
pub fn eql(self: *const @This(), a: PageEntry, b: PageEntry) bool {
- return a.eql(self.page.?.memory, &b, self.page.?.memory);
+ return a.eql(
+ (self.src_page orelse self.page.?).memory,
+ &b,
+ self.page.?.memory,
+ );
}
pub fn deleted(self: *const @This(), link: PageEntry) void {
diff --git a/src/terminal/page.zig b/src/terminal/page.zig
index ae14b8c01..30f6658aa 100644
--- a/src/terminal/page.zig
+++ b/src/terminal/page.zig
@@ -821,11 +821,7 @@ pub const Page = struct {
if (self.hyperlink_set.lookupContext(
self.memory,
other_link.*,
-
- // `lookupContext` uses the context for hashing, and
- // that doesn't write to the page, so this constCast
- // is completely safe.
- .{ .page = @constCast(other) },
+ .{ .page = self, .src_page = @constCast(other) },
)) |i| {
self.hyperlink_set.use(self.memory, i);
break :dst_id i;
diff --git a/src/terminal/ref_counted_set.zig b/src/terminal/ref_counted_set.zig
index 1a58a4e5b..b674295dc 100644
--- a/src/terminal/ref_counted_set.zig
+++ b/src/terminal/ref_counted_set.zig
@@ -38,8 +38,14 @@ const fastmem = @import("../fastmem.zig");
///
/// `Context`
/// A type containing methods to define behaviors.
+///
/// - `fn hash(*Context, T) u64` - Return a hash for an item.
+///
/// - `fn eql(*Context, T, T) bool` - Check two items for equality.
+/// The first of the two items passed in is guaranteed to be from
+/// a value passed in to an `add` or `lookup` function, the second
+/// is guaranteed to be a value already resident in the set.
+///
/// - `fn deleted(*Context, T) void` - [OPTIONAL] Deletion callback.
/// If present, called whenever an item is finally deleted.
/// Useful if the item has memory that needs to be freed.
From dd8c795ec605f808aaf82d8fa551844cf09178fc Mon Sep 17 00:00:00 2001
From: Qwerasd
Date: Mon, 10 Feb 2025 15:48:06 -0500
Subject: [PATCH 226/365] test(terminal/screen): cursorSetHyperlink OOM
handling edge case
Tests handling introduced in 09c76d95 which ensures sufficient space for
the cursor hyperlink uri in the string alloc when adjusting capacity.
---
src/terminal/Screen.zig | 34 ++++++++++++++++++++++++++++++++++
1 file changed, 34 insertions(+)
diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig
index b11c207a5..98894a83d 100644
--- a/src/terminal/Screen.zig
+++ b/src/terminal/Screen.zig
@@ -8785,6 +8785,40 @@ test "Screen: hyperlink cursor state on resize" {
}
}
+test "Screen: cursorSetHyperlink OOM + URI too large for string alloc" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var s = try init(alloc, 80, 24, 0);
+ defer s.deinit();
+
+ // Start a hyperlink with a URI that just barely fits in the string alloc.
+ // This will ensure that additional string alloc space is needed for the
+ // redundant copy of the URI when the page is re-alloced.
+ const uri = "a" ** (pagepkg.std_capacity.string_bytes - 8);
+ try s.startHyperlink(uri, null);
+
+ // Figure out how many cells should can have hyperlinks in this page,
+ // and write twice that number, to guarantee the capacity needs to be
+ // increased at some point.
+ const base_capacity = s.cursor.page_pin.node.data.hyperlinkCapacity();
+ const base_string_bytes = s.cursor.page_pin.node.data.capacity.string_bytes;
+ for (0..base_capacity * 2) |_| {
+ try s.cursorSetHyperlink();
+ if (s.cursor.x >= s.pages.cols - 1) {
+ try s.cursorDownOrScroll();
+ s.cursorHorizontalAbsolute(0);
+ } else {
+ s.cursorRight(1);
+ }
+ }
+
+ // Make sure the capacity really did increase.
+ try testing.expect(base_capacity < s.cursor.page_pin.node.data.hyperlinkCapacity());
+ // And that our string_bytes increased as well.
+ try testing.expect(base_string_bytes < s.cursor.page_pin.node.data.capacity.string_bytes);
+}
+
test "Screen: adjustCapacity cursor style ref count" {
const testing = std.testing;
const alloc = testing.allocator;
From 3a8c934b1937762e00de1959d9f25a25794b4d85 Mon Sep 17 00:00:00 2001
From: "Jeffrey C. Ollie"
Date: Thu, 30 Jan 2025 11:55:29 -0600
Subject: [PATCH 227/365] nix: use zig2nix to manage nix cache/deps
This brings the internal package more in line with how the nixpkgs
package is built. It also handles recursive dependencies better than the
current system.
---
.gitattributes | 1 +
.github/workflows/nix.yml | 4 +-
.github/workflows/update-colorschemes.yml | 8 +-
.gitignore | 1 +
build.zig.zon.nix | 390 ++++++++++++++++++++++
flake.lock | 62 +++-
flake.nix | 12 +
nix/build-support/check-zig-cache-hash.sh | 63 ----
nix/build-support/check-zig-cache.sh | 64 ++++
nix/build-support/fetch-zig-cache.sh | 39 ---
nix/devShell.nix | 3 +
nix/package.nix | 97 ++----
nix/zigCacheHash.nix | 3 -
13 files changed, 548 insertions(+), 199 deletions(-)
create mode 100644 build.zig.zon.nix
delete mode 100755 nix/build-support/check-zig-cache-hash.sh
create mode 100755 nix/build-support/check-zig-cache.sh
delete mode 100755 nix/build-support/fetch-zig-cache.sh
delete mode 100644 nix/zigCacheHash.nix
diff --git a/.gitattributes b/.gitattributes
index eef19e09b..148515873 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,3 +1,4 @@
+build.zig.zon.nix linguist-generated=true
vendor/** linguist-vendored
website/** linguist-documentation
pkg/breakpad/vendor/** linguist-vendored
diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml
index 3339ee71c..ec55f2dff 100644
--- a/.github/workflows/nix.yml
+++ b/.github/workflows/nix.yml
@@ -50,5 +50,5 @@ jobs:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
useDaemon: false # sometimes fails on short jobs
- - name: Check Zig cache hash
- run: nix develop -c ./nix/build-support/check-zig-cache-hash.sh
+ - name: Check Zig cache
+ run: nix develop -c ./nix/build-support/check-zig-cache.sh
diff --git a/.github/workflows/update-colorschemes.yml b/.github/workflows/update-colorschemes.yml
index 569ef6765..8a6c77ea5 100644
--- a/.github/workflows/update-colorschemes.yml
+++ b/.github/workflows/update-colorschemes.yml
@@ -48,14 +48,14 @@ jobs:
run: |
# Only proceed if build.zig.zon has changed
if ! git diff --exit-code build.zig.zon; then
- nix develop -c ./nix/build-support/check-zig-cache-hash.sh --update
- nix develop -c ./nix/build-support/check-zig-cache-hash.sh
+ nix develop -c ./nix/build-support/check-zig-cache.sh --update
+ nix develop -c ./nix/build-support/check-zig-cache.sh
fi
# Verify the build still works. We choose an arbitrary build type
# as a canary instead of testing all build types.
- name: Test Build
- run: nix develop -c zig build -Dapp-runtime=gtk -Dgtk-adwaita=true
+ run: nix build .#ghostty
- name: Create pull request
uses: peter-evans/create-pull-request@v7
@@ -66,7 +66,7 @@ jobs:
commit-message: "deps: Update iTerm2 color schemes"
add-paths: |
build.zig.zon
- nix/zigCacheHash.nix
+ build.zig.zon.nix
body: |
Upstream revision: https://github.com/mbadolato/iTerm2-Color-Schemes/tree/${{ steps.zig_fetch.outputs.upstream_rev }}
labels: dependencies
diff --git a/.gitignore b/.gitignore
index db8457e1f..b37c80ebe 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,3 +18,4 @@ glad.zip
/Box_test.ppm
/Box_test_diff.ppm
/ghostty.qcow2
+/build.zig.zon2json-lock
diff --git a/build.zig.zon.nix b/build.zig.zon.nix
new file mode 100644
index 000000000..64cb8f369
--- /dev/null
+++ b/build.zig.zon.nix
@@ -0,0 +1,390 @@
+# generated by zon2nix (https://github.com/Cloudef/zig2nix)
+{
+ lib,
+ linkFarm,
+ fetchurl,
+ fetchgit,
+ runCommandLocal,
+ zig,
+ name ? "zig-packages",
+}:
+with builtins;
+with lib; let
+ unpackZigArtifact = {
+ name,
+ artifact,
+ }:
+ runCommandLocal name
+ {
+ nativeBuildInputs = [zig];
+ }
+ ''
+ hash="$(zig fetch --global-cache-dir "$TMPDIR" ${artifact})"
+ mv "$TMPDIR/p/$hash" "$out"
+ chmod 755 "$out"
+ '';
+
+ fetchZig = {
+ name,
+ url,
+ hash,
+ }: let
+ artifact = fetchurl {inherit url hash;};
+ in
+ unpackZigArtifact {inherit name artifact;};
+
+ fetchGitZig = {
+ name,
+ url,
+ hash,
+ }: let
+ parts = splitString "#" url;
+ url_base = elemAt parts 0;
+ url_without_query = elemAt (splitString "?" url_base) 0;
+ rev_base = elemAt parts 1;
+ rev =
+ if match "^[a-fA-F0-9]{40}$" rev_base != null
+ then rev_base
+ else "refs/heads/${rev_base}";
+ in
+ fetchgit {
+ inherit name rev hash;
+ url = url_without_query;
+ deepClone = false;
+ };
+
+ fetchZigArtifact = {
+ name,
+ url,
+ hash,
+ }: let
+ parts = splitString "://" url;
+ proto = elemAt parts 0;
+ path = elemAt parts 1;
+ fetcher = {
+ "git+http" = fetchGitZig {
+ inherit name hash;
+ url = "http://${path}";
+ };
+ "git+https" = fetchGitZig {
+ inherit name hash;
+ url = "https://${path}";
+ };
+ http = fetchZig {
+ inherit name hash;
+ url = "http://${path}";
+ };
+ https = fetchZig {
+ inherit name hash;
+ url = "https://${path}";
+ };
+ };
+ in
+ fetcher.${proto};
+in
+ linkFarm name [
+ {
+ name = "1220ebf88622c4d502dc59e71347e4d28c47e033f11b59aff774ae5787565c40999c";
+ path = fetchZigArtifact {
+ name = "libxev";
+ url = "https://github.com/mitchellh/libxev/archive/31eed4e337fed7b0149319e5cdbb62b848c24fbd.tar.gz";
+ hash = "sha256-VHP90NTytIZ8UZsYRKOOxN490/I6yv6ec40sP8y5MJ8=";
+ };
+ }
+ {
+ name = "12206ed982e709e565d536ce930701a8c07edfd2cfdce428683f3f2a601d37696a62";
+ path = fetchZigArtifact {
+ name = "mach_glfw";
+ url = "https://github.com/mitchellh/mach-glfw/archive/37c2995f31abcf7e8378fba68ddcf4a3faa02de0.tar.gz";
+ hash = "sha256-HhXIvWUS8/CHWY4VXPG2ZEo+we8XOn3o5rYJCQ1n8Nk=";
+ };
+ }
+ {
+ name = "1220736fa4ba211162c7a0e46cc8fe04d95921927688bff64ab5da7420d098a7272d";
+ path = fetchZigArtifact {
+ name = "glfw";
+ url = "https://github.com/mitchellh/glfw/archive/b552c6ec47326b94015feddb36058ea567b87159.tar.gz";
+ hash = "sha256-IeBVAOQmtyFqVxzuXPek1onuPwIamcOyYtxqKpPEQjU=";
+ };
+ }
+ {
+ name = "12202adbfecdad671d585c9a5bfcbd5cdf821726779430047742ce1bf94ad67d19cb";
+ path = fetchZigArtifact {
+ name = "xcode_frameworks";
+ url = "https://github.com/mitchellh/xcode-frameworks/archive/69801c154c39d7ae6129ea1ba8fe1afe00585fc8.tar.gz";
+ hash = "sha256-mP/I2coL57UJm/3+4Q8sPAgQwk8V4zM+S4VBBTrX2To=";
+ };
+ }
+ {
+ name = "122004bfd4c519dadfb8e6281a42fc34fd1aa15aea654ea8a492839046f9894fa2cf";
+ path = fetchZigArtifact {
+ name = "vulkan_headers";
+ url = "https://github.com/mitchellh/vulkan-headers/archive/04c8a0389d5a0236a96312988017cd4ce27d8041.tar.gz";
+ hash = "sha256-K+zrRudgHFukOM6En1StRYRMNYkeRk+qHTXvrXaG+FU=";
+ };
+ }
+ {
+ name = "1220b3164434d2ec9db146a40bf3a30f490590d68fa8529776a3138074f0da2c11ca";
+ path = fetchZigArtifact {
+ name = "wayland_headers";
+ url = "https://github.com/mitchellh/wayland-headers/archive/5f991515a29f994d87b908115a2ab0b899474bd1.tar.gz";
+ hash = "sha256-uFilLZinKkZt6RdVTV3lUmJpzpswDdFva22FvwU/XQI=";
+ };
+ }
+ {
+ name = "122089c326186c84aa2fd034b16abc38f3ebf4862d9ae106dc1847ac44f557b36465";
+ path = fetchZigArtifact {
+ name = "x11_headers";
+ url = "https://github.com/mitchellh/x11-headers/archive/2ffbd62d82ff73ec929dd8de802bc95effa0ef88.tar.gz";
+ hash = "sha256-EhV2bmTY/OMYN1wEul35gD0hQgS/Al262jO3pVr0O+c=";
+ };
+ }
+ {
+ name = "12200df4ebeaed45de26cb2c9f3b6f3746d8013b604e035dae658f86f586c8c91d2f";
+ path = fetchZigArtifact {
+ name = "vaxis";
+ url = "git+https://github.com/rockorager/libvaxis/?ref=main#6d729a2dc3b934818dffe06d2ba3ce02841ed74b";
+ hash = "sha256-fFf79fCy4QQFVNcN722tSMjB6FyVEzCB36oH1olk9JQ=";
+ };
+ }
+ {
+ name = "1220dd654ef941fc76fd96f9ec6adadf83f69b9887a0d3f4ee5ac0a1a3e11be35cf5";
+ path = fetchZigArtifact {
+ name = "zigimg";
+ url = "git+https://github.com/zigimg/zigimg#3a667bdb3d7f0955a5a51c8468eac83210c1439e";
+ hash = "sha256-oLf3YH3yeg4ikVO/GahMCDRMTU31AHkfSnF4rt7xTKo=";
+ };
+ }
+ {
+ name = "122055beff332830a391e9895c044d33b15ea21063779557024b46169fb1984c6e40";
+ path = fetchZigArtifact {
+ name = "zg";
+ url = "https://codeberg.org/atman/zg/archive/v0.13.2.tar.gz";
+ hash = "sha256-2x9hT7bYq9KJYWLVOf21a+QvTG/F7HWT+YK15IMRzNY=";
+ };
+ }
+ {
+ name = "12201f0d542e7541cf492a001d4d0d0155c92f58212fbcb0d224e95edeba06b5416a";
+ path = fetchZigArtifact {
+ name = "z2d";
+ url = "git+https://github.com/vancluever/z2d?ref=v0.4.0#4638bb02a9dc41cc2fb811f092811f6a951c752a";
+ hash = "sha256-YpWXn1J3JKQSCrWB25mAfzd1/T56QstEZnhPzBwxgoM=";
+ };
+ }
+ {
+ name = "1220e17e64ef0ef561b3e4b9f3a96a2494285f2ec31c097721bf8c8677ec4415c634";
+ path = fetchZigArtifact {
+ name = "zig_objc";
+ url = "https://github.com/mitchellh/zig-objc/archive/9b8ba849b0f58fe207ecd6ab7c147af55b17556e.tar.gz";
+ hash = "sha256-H+HIbh2T23uzrsg9/1/vl9Ir1HCAa2pzeTx6zktJH9Q=";
+ };
+ }
+ {
+ name = "12205a66d423259567764fa0fc60c82be35365c21aeb76c5a7dc99698401f4f6fefc";
+ path = fetchZigArtifact {
+ name = "zig_js";
+ url = "https://github.com/mitchellh/zig-js/archive/d0b8b0a57c52fbc89f9d9fecba75ca29da7dd7d1.tar.gz";
+ hash = "sha256-fyNeCVbC9UAaKJY6JhAZlT0A479M/AKYMPIWEZbDWD0=";
+ };
+ }
+ {
+ name = "12207831bce7d4abce57b5a98e8f3635811cfefd160bca022eb91fe905d36a02cf25";
+ path = fetchZigArtifact {
+ name = "ziglyph";
+ url = "https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz";
+ hash = "sha256-cse98+Ft8QUjX+P88yyYfaxJOJGQ9M7Ymw7jFxDz89k=";
+ };
+ }
+ {
+ name = "12209ca054cb1919fa276e328967f10b253f7537c4136eb48f3332b0f7cf661cad38";
+ path = fetchZigArtifact {
+ name = "zig_wayland";
+ url = "https://codeberg.org/ifreund/zig-wayland/archive/fbfe3b4ac0b472a27b1f1a67405436c58cbee12d.tar.gz";
+ hash = "sha256-RtAystqK/GRYIquTK1KfD7rRSCrfuzAvCD1Z9DE1ldc=";
+ };
+ }
+ {
+ name = "1220edc3b8d8bedbb50555947987e5e8e2f93871ca3c8e8d4cc8f1377c15b5dd35e8";
+ path = fetchZigArtifact {
+ name = "zf";
+ url = "git+https://github.com/natecraddock/zf/?ref=main#ed99ca18b02dda052e20ba467e90b623c04690dd";
+ hash = "sha256-t6QNrEJZ4GZZsYixjYvpdrYoCmNbG8TTUmGs2MFa4sU=";
+ };
+ }
+ {
+ name = "1220c72c1697dd9008461ead702997a15d8a1c5810247f02e7983b9f74c6c6e4c087";
+ path = fetchZigArtifact {
+ name = "vaxis";
+ url = "git+https://github.com/rockorager/libvaxis/?ref=main#dc0a228a5544988d4a920cfb40be9cd28db41423";
+ hash = "sha256-QWN4jOrA91KlbqmeEHHJ4HTnCC9nmfxt8DHUXJpAzLI=";
+ };
+ }
+ {
+ name = "12208d70ee791d7ef7e16e1c3c9c1127b57f1ed066a24f87d57fc9f730c5dc394b9d";
+ path = fetchZigArtifact {
+ name = "gobject";
+ url = "https://github.com/ianprime0509/zig-gobject/releases/download/v0.2.2/bindings-gnome47.tar.zst";
+ hash = "sha256-UU97kNv/bZzQPKz1djhEDLapLguvfBpFfWVb6FthtcI=";
+ };
+ }
+ {
+ name = "12202cdac858abc52413a6c6711d5026d2d3c8e13f95ca2c327eade0736298bb021f";
+ path = fetchZigArtifact {
+ name = "wayland";
+ url = "https://deps.files.ghostty.org/wayland-9cb3d7aa9dc995ffafdbdef7ab86a949d0fb0e7d.tar.gz";
+ hash = "sha256-m9G72jdG30KH2yQhNBcBJ46OnekzuX0i2uIOyczkpLk=";
+ };
+ }
+ {
+ name = "12201a57c6ce0001aa034fa80fba3e1cd2253c560a45748f4f4dd21ff23b491cddef";
+ path = fetchZigArtifact {
+ name = "wayland_protocols";
+ url = "https://deps.files.ghostty.org/wayland-protocols-258d8f88f2c8c25a830c6316f87d23ce1a0f12d9.tar.gz";
+ hash = "sha256-XO3K3egbdeYPI+XoO13SuOtO+5+Peb16NH0UiusFMPg=";
+ };
+ }
+ {
+ name = "12207e0851c12acdeee0991e893e0132fc87bb763969a585dc16ecca33e88334c566";
+ path = fetchZigArtifact {
+ name = "plasma_wayland_protocols";
+ url = "git+https://github.com/KDE/plasma-wayland-protocols?ref=main#db525e8f9da548cffa2ac77618dd0fbe7f511b86";
+ hash = "sha256-iWRv3+OfmHxmeCJ8m0ChjgZX6mwXlcFmK8P/Vt8gDj4=";
+ };
+ }
+ {
+ name = "12203d2647e5daf36a9c85b969e03f422540786ce9ea624eb4c26d204fe1f46218f3";
+ path = fetchZigArtifact {
+ name = "iterm2_themes";
+ url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/db227d159adc265818f2e898da0f70ef8d7b580e.tar.gz";
+ hash = "sha256-Iyf7U4rpvNkPX4AOEbYSYGte5+SjRwsWD2luOn1Hz8U=";
+ };
+ }
+ {
+ name = "1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402";
+ path = fetchZigArtifact {
+ name = "imgui";
+ url = "https://github.com/ocornut/imgui/archive/e391fe2e66eb1c96b1624ae8444dc64c23146ef4.tar.gz";
+ hash = "sha256-oF/QHgTPEat4Hig4fGIdLkIPHmBEyOJ6JeYD6pnveGA=";
+ };
+ }
+ {
+ name = "1220b81f6ecfb3fd222f76cf9106fecfa6554ab07ec7fdc4124b9bb063ae2adf969d";
+ path = fetchZigArtifact {
+ name = "freetype";
+ url = "https://github.com/freetype/freetype/archive/refs/tags/VER-2-13-2.tar.gz";
+ hash = "sha256-QnIB9dUVFnDQXB9bRb713aHy592XHvVPD+qqf/0quQw=";
+ };
+ }
+ {
+ name = "1220aa013f0c83da3fb64ea6d327f9173fa008d10e28bc9349eac3463457723b1c66";
+ path = fetchZigArtifact {
+ name = "libpng";
+ url = "https://github.com/glennrp/libpng/archive/refs/tags/v1.6.43.tar.gz";
+ hash = "sha256-/syVtGzwXo4/yKQUdQ4LparQDYnp/fF16U/wQcrxoDo=";
+ };
+ }
+ {
+ name = "1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb";
+ path = fetchZigArtifact {
+ name = "zlib";
+ url = "https://github.com/madler/zlib/archive/refs/tags/v1.3.1.tar.gz";
+ hash = "sha256-F+iIY/NgBnKrSRgvIXKBtvxNPHYr3jYZNeQ2qVIU0Fw=";
+ };
+ }
+ {
+ name = "12201149afb3326c56c05bb0a577f54f76ac20deece63aa2f5cd6ff31a4fa4fcb3b7";
+ path = fetchZigArtifact {
+ name = "fontconfig";
+ url = "https://deps.files.ghostty.org/fontconfig-2.14.2.tar.gz";
+ hash = "sha256-O6LdkhWHGKzsXKrxpxYEO1qgVcJ7CB2RSvPMtA3OilU=";
+ };
+ }
+ {
+ name = "122032442d95c3b428ae8e526017fad881e7dc78eab4d558e9a58a80bfbd65a64f7d";
+ path = fetchZigArtifact {
+ name = "libxml2";
+ url = "https://github.com/GNOME/libxml2/archive/refs/tags/v2.11.5.tar.gz";
+ hash = "sha256-bCgFni4+60K1tLFkieORamNGwQladP7jvGXNxdiaYhU=";
+ };
+ }
+ {
+ name = "1220b8588f106c996af10249bfa092c6fb2f35fbacb1505ef477a0b04a7dd1063122";
+ path = fetchZigArtifact {
+ name = "harfbuzz";
+ url = "https://github.com/harfbuzz/harfbuzz/archive/refs/tags/8.4.0.tar.gz";
+ hash = "sha256-nxygiYE7BZRK0c6MfgGCEwJtNdybq0gKIeuHaDg5ZVY=";
+ };
+ }
+ {
+ name = "12205c83b8311a24b1d5ae6d21640df04f4b0726e314337c043cde1432758cbe165b";
+ path = fetchZigArtifact {
+ name = "highway";
+ url = "https://github.com/google/highway/archive/refs/tags/1.1.0.tar.gz";
+ hash = "sha256-NUqLRTm1iOcLmOxwhEJz4/J0EwLEw3e8xOgbPRhm98k=";
+ };
+ }
+ {
+ name = "1220c15e72eadd0d9085a8af134904d9a0f5dfcbed5f606ad60edc60ebeccd9706bb";
+ path = fetchZigArtifact {
+ name = "oniguruma";
+ url = "https://github.com/kkos/oniguruma/archive/refs/tags/v6.9.9.tar.gz";
+ hash = "sha256-ABqhIC54RI9MC/GkjHblVodrNvFtks4yB+zP1h2Z8qA=";
+ };
+ }
+ {
+ name = "1220446be831adcca918167647c06c7b825849fa3fba5f22da394667974537a9c77e";
+ path = fetchZigArtifact {
+ name = "sentry";
+ url = "https://github.com/getsentry/sentry-native/archive/refs/tags/0.7.8.tar.gz";
+ hash = "sha256-KsZJfMjWGo0xCT5HrduMmyxFsWsHBbszSoNbZCPDGN8=";
+ };
+ }
+ {
+ name = "12207fd37bb8251919c112dcdd8f616a491857b34a451f7e4486490077206dc2a1ea";
+ path = fetchZigArtifact {
+ name = "breakpad";
+ url = "https://github.com/getsentry/breakpad/archive/b99f444ba5f6b98cac261cbb391d8766b34a5918.tar.gz";
+ hash = "sha256-bMqYlD0amQdmzvYQd8Ca/1k4Bj/heh7+EijlQSttatk=";
+ };
+ }
+ {
+ name = "1220d4d18426ca72fc2b7e56ce47273149815501d0d2395c2a98c726b31ba931e641";
+ path = fetchZigArtifact {
+ name = "utfcpp";
+ url = "https://github.com/nemtrif/utfcpp/archive/refs/tags/v4.0.5.tar.gz";
+ hash = "sha256-/8ZooxDndgfTk/PBizJxXyI9oerExNbgV5oR345rWc8=";
+ };
+ }
+ {
+ name = "122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd";
+ path = fetchZigArtifact {
+ name = "wuffs";
+ url = "https://github.com/google/wuffs/archive/refs/tags/v0.4.0-alpha.9.tar.gz";
+ hash = "sha256-nkzSCr6W5sTG7enDBXEIhgEm574uLD41UVR2wlC+HBM=";
+ };
+ }
+ {
+ name = "12207ff340169c7d40c570b4b6a97db614fe47e0d83b5801a932dcd44917424c8806";
+ path = fetchZigArtifact {
+ name = "pixels";
+ url = "git+https://github.com/make-github-pseudonymous-again/pixels?ref=main#d843c2714d32e15b48b8d7eeb480295af537f877";
+ hash = "sha256-kXYGO0qn2PfyOYCrRA49BHIgTzdmKhI8SNO1ZKfUYEE=";
+ };
+ }
+ {
+ name = "12201278a1a05c0ce0b6eb6026c65cd3e9247aa041b1c260324bf29cee559dd23ba1";
+ path = fetchZigArtifact {
+ name = "glslang";
+ url = "https://github.com/KhronosGroup/glslang/archive/refs/tags/14.2.0.tar.gz";
+ hash = "sha256-FKLtu1Ccs+UamlPj9eQ12/WXFgS0uDPmPmB26MCpl7U=";
+ };
+ }
+ {
+ name = "1220fb3b5586e8be67bc3feb34cbe749cf42a60d628d2953632c2f8141302748c8da";
+ path = fetchZigArtifact {
+ name = "spirv_cross";
+ url = "https://github.com/KhronosGroup/SPIRV-Cross/archive/476f384eb7d9e48613c45179e502a15ab95b6b49.tar.gz";
+ hash = "sha256-tStvz8Ref6abHwahNiwVVHNETizAmZVVaxVsU7pmV+M=";
+ };
+ }
+ ]
diff --git a/flake.lock b/flake.lock
index bf678156b..7905635b3 100644
--- a/flake.lock
+++ b/flake.lock
@@ -3,11 +3,11 @@
"flake-compat": {
"flake": false,
"locked": {
- "lastModified": 1696426674,
- "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
+ "lastModified": 1733328505,
+ "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=",
"owner": "edolstra",
"repo": "flake-compat",
- "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
+ "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec",
"type": "github"
},
"original": {
@@ -21,11 +21,11 @@
"systems": "systems"
},
"locked": {
- "lastModified": 1705309234,
- "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=",
+ "lastModified": 1731533236,
+ "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
- "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26",
+ "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
@@ -36,11 +36,11 @@
},
"nixpkgs-stable": {
"locked": {
- "lastModified": 1733423277,
- "narHash": "sha256-TxabjxEgkNbCGFRHgM/b9yZWlBj60gUOUnRT/wbVQR8=",
+ "lastModified": 1738255539,
+ "narHash": "sha256-hP2eOqhIO/OILW+3moNWO4GtdJFYCqAe9yJZgvlCoDQ=",
"owner": "nixos",
"repo": "nixpkgs",
- "rev": "e36963a147267afc055f7cf65225958633e536bf",
+ "rev": "c3511a3b53b482aa7547c9d1626fd7310c1de1c5",
"type": "github"
},
"original": {
@@ -52,11 +52,11 @@
},
"nixpkgs-unstable": {
"locked": {
- "lastModified": 1733229606,
- "narHash": "sha256-FLYY5M0rpa5C2QAE3CKLYAM6TwbKicdRK6qNrSHlNrE=",
+ "lastModified": 1738136902,
+ "narHash": "sha256-pUvLijVGARw4u793APze3j6mU1Zwdtz7hGkGGkD87qw=",
"owner": "nixos",
"repo": "nixpkgs",
- "rev": "566e53c2ad750c84f6d31f9ccb9d00f823165550",
+ "rev": "9a5db3142ce450045840cc8d832b13b8a2018e0c",
"type": "github"
},
"original": {
@@ -69,9 +69,11 @@
"root": {
"inputs": {
"flake-compat": "flake-compat",
+ "flake-utils": "flake-utils",
"nixpkgs-stable": "nixpkgs-stable",
"nixpkgs-unstable": "nixpkgs-unstable",
- "zig": "zig"
+ "zig": "zig",
+ "zig2nix": "zig2nix"
}
},
"systems": {
@@ -92,17 +94,19 @@
"zig": {
"inputs": {
"flake-compat": [],
- "flake-utils": "flake-utils",
+ "flake-utils": [
+ "flake-utils"
+ ],
"nixpkgs": [
"nixpkgs-stable"
]
},
"locked": {
- "lastModified": 1717848532,
- "narHash": "sha256-d+xIUvSTreHl8pAmU1fnmkfDTGQYCn2Rb/zOwByxS2M=",
+ "lastModified": 1738239110,
+ "narHash": "sha256-Y5i9mQ++dyIQr+zEPNy+KIbc5wjPmfllBrag3cHZgcE=",
"owner": "mitchellh",
"repo": "zig-overlay",
- "rev": "02fc5cc555fc14fda40c42d7c3250efa43812b43",
+ "rev": "1a8fb6f3a04724519436355564b95fce5e272504",
"type": "github"
},
"original": {
@@ -110,6 +114,30 @@
"repo": "zig-overlay",
"type": "github"
}
+ },
+ "zig2nix": {
+ "inputs": {
+ "flake-utils": [
+ "flake-utils"
+ ],
+ "nixpkgs": [
+ "nixpkgs-stable"
+ ]
+ },
+ "locked": {
+ "lastModified": 1738263917,
+ "narHash": "sha256-j/3fwe2pEOquHabP/puljOKwAZFjIE9gXZqA91sC48M=",
+ "owner": "jcollie",
+ "repo": "zig2nix",
+ "rev": "c311d8e77a6ee0d995f40a6e10a89a3a4ab04f9a",
+ "type": "github"
+ },
+ "original": {
+ "owner": "jcollie",
+ "ref": "c311d8e77a6ee0d995f40a6e10a89a3a4ab04f9a",
+ "repo": "zig2nix",
+ "type": "github"
+ }
}
},
"root": "root",
diff --git a/flake.nix b/flake.nix
index 3256c7c15..cf82d392a 100644
--- a/flake.nix
+++ b/flake.nix
@@ -8,6 +8,7 @@
# glibc versions used by our dependencies from Nix are compatible with the
# system glibc that the user is building for.
nixpkgs-stable.url = "github:nixos/nixpkgs/release-24.11";
+ flake-utils.url = "github:numtide/flake-utils";
# Used for shell.nix
flake-compat = {
@@ -19,9 +20,18 @@
url = "github:mitchellh/zig-overlay";
inputs = {
nixpkgs.follows = "nixpkgs-stable";
+ flake-utils.follows = "flake-utils";
flake-compat.follows = "";
};
};
+
+ zig2nix = {
+ url = "github:jcollie/zig2nix?ref=c311d8e77a6ee0d995f40a6e10a89a3a4ab04f9a";
+ inputs = {
+ nixpkgs.follows = "nixpkgs-stable";
+ flake-utils.follows = "flake-utils";
+ };
+ };
};
outputs = {
@@ -29,6 +39,7 @@
nixpkgs-unstable,
nixpkgs-stable,
zig,
+ zig2nix,
...
}:
builtins.foldl' nixpkgs-stable.lib.recursiveUpdate {} (
@@ -40,6 +51,7 @@
devShell.${system} = pkgs-stable.callPackage ./nix/devShell.nix {
zig = zig.packages.${system}."0.13.0";
wraptest = pkgs-stable.callPackage ./nix/wraptest.nix {};
+ zig2nix = zig2nix;
};
packages.${system} = let
diff --git a/nix/build-support/check-zig-cache-hash.sh b/nix/build-support/check-zig-cache-hash.sh
deleted file mode 100755
index 49ea29ffb..000000000
--- a/nix/build-support/check-zig-cache-hash.sh
+++ /dev/null
@@ -1,63 +0,0 @@
-#!/usr/bin/env bash
-
-# Nothing in this script should fail.
-set -e
-
-CACHE_HASH_FILE="$(realpath "$(dirname "$0")/../zigCacheHash.nix")"
-
-help() {
- echo ""
- echo "To fix, please (manually) re-run the script from the repository root,"
- echo "commit, and push the update:"
- echo ""
- echo " ./nix/build-support/check-zig-cache-hash.sh --update"
- echo " git add nix/zigCacheHash.nix"
- echo " git commit -m \"nix: update Zig cache hash\""
- echo " git push"
- echo ""
-}
-
-if [ -f "${CACHE_HASH_FILE}" ]; then
- OLD_CACHE_HASH="$(nix eval --raw --file "${CACHE_HASH_FILE}")"
-elif [ "$1" != "--update" ]; then
- echo -e "\nERROR: Zig cache hash file missing."
- help
- exit 1
-fi
-
-ZIG_GLOBAL_CACHE_DIR="$(mktemp --directory --suffix nix-zig-cache)"
-export ZIG_GLOBAL_CACHE_DIR
-
-# This is not 100% necessary in CI but is helpful when running locally to keep
-# a local workstation clean.
-trap 'rm -rf "${ZIG_GLOBAL_CACHE_DIR}"' EXIT
-
-# Run Zig and download the cache to the temporary directory.
-
-sh ./nix/build-support/fetch-zig-cache.sh
-
-# Now, calculate the hash.
-ZIG_CACHE_HASH="sha256-$(nix-hash --type sha256 --to-base64 "$(nix-hash --type sha256 "${ZIG_GLOBAL_CACHE_DIR}")")"
-
-if [ "${OLD_CACHE_HASH}" == "${ZIG_CACHE_HASH}" ]; then
- echo -e "\nOK: Zig cache store hash unchanged."
- exit 0
-elif [ "$1" != "--update" ]; then
- echo -e "\nERROR: The Zig cache store hash has updated."
- echo ""
- echo " * Old hash: ${OLD_CACHE_HASH}"
- echo " * New hash: ${ZIG_CACHE_HASH}"
- help
- exit 1
-else
- echo -e "\nNew Zig cache store hash: ${ZIG_CACHE_HASH}"
-fi
-
-# Write out the cache file
-cat > "${CACHE_HASH_FILE}" < "$WORK_DIR/build.zig.zon.nix"
+alejandra --quiet "$WORK_DIR/build.zig.zon.nix"
+rm -f "$BUILD_ZIG_ZON_LOCK"
+
+NEW_HASH=$(sha512sum "$WORK_DIR/build.zig.zon.nix" | awk '{print $1}')
+
+if [ "${OLD_HASH}" == "${NEW_HASH}" ]; then
+ echo -e "\nOK: build.zig.zon.nix unchanged."
+ exit 0
+elif [ "$1" != "--update" ]; then
+ echo -e "\nERROR: build.zig.zon.nix needs to be updated."
+ echo ""
+ echo " * Old hash: ${OLD_HASH}"
+ echo " * New hash: ${NEW_HASH}"
+ help
+ exit 1
+else
+ mv "$WORK_DIR/build.zig.zon.nix" "$BUILD_ZIG_ZON_NIX"
+ echo -e "\nOK: build.zig.zon.nix updated."
+ exit 0
+fi
+
diff --git a/nix/build-support/fetch-zig-cache.sh b/nix/build-support/fetch-zig-cache.sh
deleted file mode 100755
index 56b94e35d..000000000
--- a/nix/build-support/fetch-zig-cache.sh
+++ /dev/null
@@ -1,39 +0,0 @@
-#!/bin/sh
-
-set -e
-
-# Because Zig does not fetch recursive dependencies when you run `zig build
-# --fetch` (see https://github.com/ziglang/zig/issues/20976) we need to do some
-# extra work to fetch everything that we actually need to build without Internet
-# access (such as when building a Nix package).
-#
-# An example of this happening:
-#
-# error: builder for '/nix/store/cx8qcwrhjmjxik2547fw99v5j6np5san-ghostty-0.1.0.drv' failed with exit code 1;
-# la/build/tmp.xgHOheUF7V/p/12208cfdda4d5fdbc81b0c44b82e4d6dba2d4a86bff644a153e026fdfc80f8469133/build.zig.zon:7:20: error: unable to discover remote git server capabilities: TemporaryNameServerFailure
-# > .url = "git+https://github.com/zigimg/zigimg#3a667bdb3d7f0955a5a51c8468eac83210c1439e",
-# > ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-# > /build/tmp.xgHOheUF7V/p/12208cfdda4d5fdbc81b0c44b82e4d6dba2d4a86bff644a153e026fdfc80f8469133/build.zig.zon:16:20: error: unable to discover remote git server capabilities: TemporaryNameServerFailure
-# > .url = "git+https://github.com/mitchellh/libxev#f6a672a78436d8efee1aa847a43a900ad773618b",
-# > ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-# >
-# For full logs, run 'nix log /nix/store/cx8qcwrhjmjxik2547fw99v5j6np5san-ghostty-0.1.0.drv'.
-#
-# To update this script, add any failing URLs with a line like this:
-#
-# zig fetch
-#
-# Periodically old URLs may need to be cleaned out.
-#
-# Hopefully when the Zig issue is fixed this script can be eliminated in favor
-# of a plain `zig build --fetch`.
-
-if [ -z ${ZIG_GLOBAL_CACHE_DIR+x} ]
-then
- echo "must set ZIG_GLOBAL_CACHE_DIR!"
- exit 1
-fi
-
-zig build --fetch
-zig fetch git+https://github.com/zigimg/zigimg#3a667bdb3d7f0955a5a51c8468eac83210c1439e
-zig fetch git+https://github.com/mitchellh/libxev#f6a672a78436d8efee1aa847a43a900ad773618b
diff --git a/nix/devShell.nix b/nix/devShell.nix
index 9779dc4c6..3014b34b7 100644
--- a/nix/devShell.nix
+++ b/nix/devShell.nix
@@ -55,6 +55,8 @@
wayland,
wayland-scanner,
wayland-protocols,
+ zig2nix,
+ system,
}: let
# See package.nix. Keep in sync.
rpathLibs =
@@ -102,6 +104,7 @@ in
scdoc
zig
zip
+ zig2nix.packages.${system}.zon2nix
# For web and wasm stuff
nodejs
diff --git a/nix/package.nix b/nix/package.nix
index 55d00b76c..892d5e956 100644
--- a/nix/package.nix
+++ b/nix/package.nix
@@ -2,6 +2,7 @@
lib,
stdenv,
bzip2,
+ callPackage,
expat,
fontconfig,
freetype,
@@ -43,80 +44,34 @@
zig_hook = zig_0_13.hook.overrideAttrs {
zig_default_flags = "-Dcpu=baseline -Doptimize=${optimize} --color off";
};
-
- # We limit source like this to try and reduce the amount of rebuilds as possible
- # thus we only provide the source that is needed for the build
- #
- # NOTE: as of the current moment only linux files are provided,
- # since darwin support is not finished
- src = lib.fileset.toSource {
- root = ../.;
- fileset = lib.fileset.intersection (lib.fileset.fromSource (lib.sources.cleanSource ../.)) (
- lib.fileset.unions [
- ../dist/linux
- ../images
- ../include
- ../pkg
- ../src
- ../vendor
- ../build.zig
- ../build.zig.zon
- ./build-support/fetch-zig-cache.sh
- ]
- );
- };
-
- # This hash is the computation of the zigCache fixed-output derivation. This
- # allows us to use remote package dependencies without breaking the sandbox.
- #
- # This will need updating whenever dependencies get updated (e.g. changes are
- # made to zig.build.zon). If you see that the main build is trying to reach
- # out to the internet and failing, this is likely the cause. Change this
- # value back to lib.fakeHash, and re-run. The build failure should emit the
- # updated hash, which of course, should be validated before updating here.
- #
- # (It's also possible that you might see a hash mismatch - without the
- # network errors - if you don't have a previous instance of the cache
- # derivation in your store already. If so, just update the value as above.)
- zigCacheHash = import ./zigCacheHash.nix;
-
- zigCache = stdenv.mkDerivation {
- inherit src;
- name = "ghostty-cache";
- nativeBuildInputs = [
- git
- zig_hook
- ];
-
- dontConfigure = true;
- dontUseZigBuild = true;
- dontUseZigInstall = true;
- dontFixup = true;
-
- buildPhase = ''
- runHook preBuild
-
- sh ./nix/build-support/fetch-zig-cache.sh
-
- runHook postBuild
- '';
-
- installPhase = ''
- runHook preInstall
-
- cp -r --reflink=auto $ZIG_GLOBAL_CACHE_DIR $out
-
- runHook postInstall
- '';
-
- outputHashMode = "recursive";
- outputHash = zigCacheHash;
- };
in
stdenv.mkDerivation (finalAttrs: {
pname = "ghostty";
version = "1.1.1";
- inherit src;
+
+ # We limit source like this to try and reduce the amount of rebuilds as possible
+ # thus we only provide the source that is needed for the build
+ #
+ # NOTE: as of the current moment only linux files are provided,
+ # since darwin support is not finished
+ src = lib.fileset.toSource {
+ root = ../.;
+ fileset = lib.fileset.intersection (lib.fileset.fromSource (lib.sources.cleanSource ../.)) (
+ lib.fileset.unions [
+ ../dist/linux
+ ../images
+ ../include
+ ../pkg
+ ../src
+ ../vendor
+ ../build.zig
+ ../build.zig.zon
+ ../build.zig.zon.nix
+ ]
+ );
+ };
+
+ deps = callPackage ../build.zig.zon.nix {name = "ghostty-cache-${finalAttrs.version}";};
nativeBuildInputs =
[
@@ -166,7 +121,7 @@ in
zigBuildFlags = [
"--system"
- "${zigCache}/p"
+ "${finalAttrs.deps}"
"-Dversion-string=${finalAttrs.version}-${revision}-nix"
"-Dgtk-x11=${lib.boolToString enableX11}"
"-Dgtk-wayland=${lib.boolToString enableWayland}"
diff --git a/nix/zigCacheHash.nix b/nix/zigCacheHash.nix
deleted file mode 100644
index 78e2d509d..000000000
--- a/nix/zigCacheHash.nix
+++ /dev/null
@@ -1,3 +0,0 @@
-# This file is auto-generated! check build-support/check-zig-cache-hash.sh for
-# more details.
-"sha256-+Ag900R3lDV7iEeRFGe2HWJDtquW3I9GFvHGle+U3k0="
From f986a321853cbf1d7191b08fd88959e7b67ecafe Mon Sep 17 00:00:00 2001
From: Mitchell Hashimoto
Date: Tue, 11 Feb 2025 10:59:31 -0800
Subject: [PATCH 228/365] macos: toggle_visibility fullscreen check requires
active app
This fixes a regression from #5472. The fullscreen check must check if
the app is active otherwise the guard statement fails and we can't bring
the macOS app back from the background.
---
macos/Sources/App/macOS/AppDelegate.swift | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift
index c719d3ce7..c6d723a5f 100644
--- a/macos/Sources/App/macOS/AppDelegate.swift
+++ b/macos/Sources/App/macOS/AppDelegate.swift
@@ -709,13 +709,13 @@ class AppDelegate: NSObject,
/// Toggles visibility of all Ghosty Terminal windows. When hidden, activates Ghostty as the frontmost application
@IBAction func toggleVisibility(_ sender: Any) {
- // Toggle visibility doesn't do anything if the focused window is native
- // fullscreen.
- guard let keyWindow = NSApp.keyWindow,
- !keyWindow.styleMask.contains(.fullScreen) else { return }
-
// If we have focus, then we hide all windows.
if NSApp.isActive {
+ // Toggle visibility doesn't do anything if the focused window is native
+ // fullscreen. This is only relevant if Ghostty is active.
+ guard let keyWindow = NSApp.keyWindow,
+ !keyWindow.styleMask.contains(.fullScreen) else { return }
+
// We need to keep track of the windows that were visible because we only
// want to bring back these windows if we remove the toggle.
//
From c6da845f33e54d6bcae2eb5491337b06e8e10982 Mon Sep 17 00:00:00 2001
From: Mitchell Hashimoto
Date: Tue, 11 Feb 2025 11:11:41 -0800
Subject: [PATCH 229/365] macos: ensure previously key window regains key on
toggle_visibility
Fixes #5690
When we hide the app and then show it again, the previously key window
is lost. This is because we are not using unhide and are manually
doing it (and we're not using unhide for good reasons commented in the
source already).
Modify our hidden state to include what the key window was (as a weak
ref) and restore it when we show the app again.
---
macos/Sources/App/macOS/AppDelegate.swift | 49 +++++++++++++++++------
1 file changed, 36 insertions(+), 13 deletions(-)
diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift
index c6d723a5f..9d300e91f 100644
--- a/macos/Sources/App/macOS/AppDelegate.swift
+++ b/macos/Sources/App/macOS/AppDelegate.swift
@@ -93,7 +93,7 @@ class AppDelegate: NSObject,
}
/// Tracks the windows that we hid for toggleVisibility.
- private var hiddenWindows: [Weak] = []
+ private var hiddenState: ToggleVisibilityState? = nil
/// The observer for the app appearance.
private var appearanceObserver: NSKeyValueObservation? = nil
@@ -217,8 +217,8 @@ class AppDelegate: NSObject,
}
func applicationDidBecomeActive(_ notification: Notification) {
- // If we're back then clear the hidden windows
- self.hiddenWindows = []
+ // If we're back manually then clear the hidden state because macOS handles it.
+ self.hiddenState = nil
// First launch stuff
if (!applicationHasBecomeActive) {
@@ -716,14 +716,8 @@ class AppDelegate: NSObject,
guard let keyWindow = NSApp.keyWindow,
!keyWindow.styleMask.contains(.fullScreen) else { return }
- // We need to keep track of the windows that were visible because we only
- // want to bring back these windows if we remove the toggle.
- //
- // We also ignore fullscreen windows because they don't hide anyways.
- self.hiddenWindows = NSApp.windows.filter {
- $0.isVisible &&
- !$0.styleMask.contains(.fullScreen)
- }.map { Weak($0) }
+ // Keep track of our hidden state to restore properly
+ self.hiddenState = .init()
NSApp.hide(nil)
return
}
@@ -734,8 +728,8 @@ class AppDelegate: NSObject,
// Bring all windows to the front. Note: we don't use NSApp.unhide because
// that will unhide ALL hidden windows. We want to only bring forward the
// ones that we hid.
- self.hiddenWindows.forEach { $0.value?.orderFrontRegardless() }
- self.hiddenWindows = []
+ hiddenState?.restore()
+ hiddenState = nil
}
private struct DerivedConfig {
@@ -755,4 +749,33 @@ class AppDelegate: NSObject,
self.quickTerminalPosition = config.quickTerminalPosition
}
}
+
+ private struct ToggleVisibilityState {
+ let hiddenWindows: [Weak]
+ let keyWindow: Weak?
+
+ init() {
+ // We need to know the key window so that we can bring focus back to the
+ // right window if it was hidden.
+ self.keyWindow = if let keyWindow = NSApp.keyWindow {
+ .init(keyWindow)
+ } else {
+ nil
+ }
+
+ // We need to keep track of the windows that were visible because we only
+ // want to bring back these windows if we remove the toggle.
+ //
+ // We also ignore fullscreen windows because they don't hide anyways.
+ self.hiddenWindows = NSApp.windows.filter {
+ $0.isVisible &&
+ !$0.styleMask.contains(.fullScreen)
+ }.map { Weak($0) }
+ }
+
+ func restore() {
+ hiddenWindows.forEach { $0.value?.orderFrontRegardless() }
+ keyWindow?.value?.makeKey()
+ }
+ }
}
From b624cfe262c817591e7a7e054b66327b80fe4bd0 Mon Sep 17 00:00:00 2001
From: Qwerasd
Date: Tue, 11 Feb 2025 14:20:47 -0500
Subject: [PATCH 230/365] fix(terminal): avoid Screen.reset causing use of
undefined
Fully reset the kitty image storage instead of using the delete handler,
previously this caused a memory corruption / likely segfault due to use
of undefined, since the delete handler tries to clear the tracked pins
for placements, which it gets from the terminal, for which `undefined`
was passed in before.
---
src/terminal/Screen.zig | 9 +++------
1 file changed, 3 insertions(+), 6 deletions(-)
diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig
index 98894a83d..339599728 100644
--- a/src/terminal/Screen.zig
+++ b/src/terminal/Screen.zig
@@ -278,12 +278,9 @@ pub fn reset(self: *Screen) void {
.page_cell = cursor_rac.cell,
};
- // Clear kitty graphics
- self.kitty_images.delete(
- self.alloc,
- undefined, // All image deletion doesn't need the terminal
- .{ .all = true },
- );
+ // Reset kitty graphics storage
+ self.kitty_images.deinit(self.alloc, self);
+ self.kitty_images = .{ .dirty = true };
// Reset our basic state
self.saved_cursor = null;
From 31273aaabc34179d236b79f58db56e46e1aa1db9 Mon Sep 17 00:00:00 2001
From: Bryan Lee <38807139+liby@users.noreply.github.com>
Date: Mon, 3 Feb 2025 01:49:00 +0800
Subject: [PATCH 231/365] Remember last focused window position for next
startup
---
macos/Ghostty.xcodeproj/project.pbxproj | 4 +++
.../Terminal/TerminalController.swift | 21 ++++++++++--
.../Sources/Helpers/LastWindowPosition.swift | 34 +++++++++++++++++++
3 files changed, 57 insertions(+), 2 deletions(-)
create mode 100644 macos/Sources/Helpers/LastWindowPosition.swift
diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj
index 02c8258cb..0c68da534 100644
--- a/macos/Ghostty.xcodeproj/project.pbxproj
+++ b/macos/Ghostty.xcodeproj/project.pbxproj
@@ -72,6 +72,7 @@
A5A2A3CA2D4445E30033CF96 /* Dock.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A2A3C92D4445E20033CF96 /* Dock.swift */; };
A5A2A3CC2D444ABB0033CF96 /* NSApplication+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A2A3CB2D444AB80033CF96 /* NSApplication+Extension.swift */; };
A5A6F72A2CC41B8900B232A5 /* Xcode.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A6F7292CC41B8700B232A5 /* Xcode.swift */; };
+ A5AEB1652D5BE7D000513529 /* LastWindowPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5AEB1642D5BE7BF00513529 /* LastWindowPosition.swift */; };
A5B30539299BEAAB0047F10C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; };
A5CA378C2D2A4DEB00931030 /* KeyboardLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CA378B2D2A4DE800931030 /* KeyboardLayout.swift */; };
A5CA378E2D31D6C300931030 /* Weak.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CA378D2D31D6C100931030 /* Weak.swift */; };
@@ -168,6 +169,7 @@
A5A2A3C92D4445E20033CF96 /* Dock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dock.swift; sourceTree = ""; };
A5A2A3CB2D444AB80033CF96 /* NSApplication+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSApplication+Extension.swift"; sourceTree = ""; };
A5A6F7292CC41B8700B232A5 /* Xcode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Xcode.swift; sourceTree = ""; };
+ A5AEB1642D5BE7BF00513529 /* LastWindowPosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LastWindowPosition.swift; sourceTree = ""; };
A5B30531299BEAAA0047F10C /* Ghostty.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Ghostty.app; sourceTree = BUILT_PRODUCTS_DIR; };
A5B30538299BEAAB0047F10C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
A5B3053D299BEAAB0047F10C /* Ghostty.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Ghostty.entitlements; sourceTree = ""; };
@@ -270,6 +272,7 @@
A534263D2A7DCBB000EBB7A2 /* Helpers */ = {
isa = PBXGroup;
children = (
+ A5AEB1642D5BE7BF00513529 /* LastWindowPosition.swift */,
A5A6F7292CC41B8700B232A5 /* Xcode.swift */,
A5CEAFFE29C2410700646FDA /* Backport.swift */,
A5333E1B2B5A1CE3008AEFF7 /* CrossKit.swift */,
@@ -623,6 +626,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ A5AEB1652D5BE7D000513529 /* LastWindowPosition.swift in Sources */,
A59630A42AF059BB00D64628 /* Ghostty.SplitNode.swift in Sources */,
A514C8D62B54A16400493A16 /* Ghostty.Config.swift in Sources */,
A54B0CEB2D0CFB4C00CBEFF8 /* NSImage+Extension.swift in Sources */,
diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift
index f24261b9b..8507cf620 100644
--- a/macos/Sources/Features/Terminal/TerminalController.swift
+++ b/macos/Sources/Features/Terminal/TerminalController.swift
@@ -283,9 +283,12 @@ class TerminalController: BaseTerminalController {
private func setInitialWindowPosition(x: Int16?, y: Int16?, windowDecorations: Bool) {
guard let window else { return }
- // If we don't have both an X and Y we center.
+ // If we don't have an X/Y then we try to use the previously saved window pos.
guard let x, let y else {
- window.center()
+ if (!LastWindowPosition.shared.restore(window)) {
+ window.center()
+ }
+
return
}
@@ -490,6 +493,20 @@ class TerminalController: BaseTerminalController {
override func windowDidMove(_ notification: Notification) {
super.windowDidMove(notification)
self.fixTabBar()
+
+ // Whenever we move save our last position for the next start.
+ if let window {
+ LastWindowPosition.shared.save(window)
+ }
+ }
+
+ func windowDidBecomeMain(_ notification: Notification) {
+ // Whenever we get focused, use that as our last window position for
+ // restart. This differs from Terminal.app but matches iTerm2 behavior
+ // and I think its sensible.
+ if let window {
+ LastWindowPosition.shared.save(window)
+ }
}
// Called when the window will be encoded. We handle the data encoding here in the
diff --git a/macos/Sources/Helpers/LastWindowPosition.swift b/macos/Sources/Helpers/LastWindowPosition.swift
new file mode 100644
index 000000000..a0dfa90dd
--- /dev/null
+++ b/macos/Sources/Helpers/LastWindowPosition.swift
@@ -0,0 +1,34 @@
+import Cocoa
+
+/// Manages the persistence and restoration of window positions across app launches.
+class LastWindowPosition {
+ static let shared = LastWindowPosition()
+
+ private let positionKey = "NSWindowLastPosition"
+
+ func save(_ window: NSWindow) {
+ let origin = window.frame.origin
+ let point = [origin.x, origin.y]
+ UserDefaults.standard.set(point, forKey: positionKey)
+ }
+
+ func restore(_ window: NSWindow) -> Bool {
+ guard let points = UserDefaults.standard.array(forKey: positionKey) as? [Double],
+ points.count == 2 else { return false }
+
+ let lastPosition = CGPoint(x: points[0], y: points[1])
+
+ guard let screen = window.screen ?? NSScreen.main else { return false }
+ let visibleFrame = screen.visibleFrame
+
+ var newFrame = window.frame
+ newFrame.origin = lastPosition
+ if !visibleFrame.contains(newFrame.origin) {
+ newFrame.origin.x = max(visibleFrame.minX, min(visibleFrame.maxX - newFrame.width, newFrame.origin.x))
+ newFrame.origin.y = max(visibleFrame.minY, min(visibleFrame.maxY - newFrame.height, newFrame.origin.y))
+ }
+
+ window.setFrame(newFrame, display: true)
+ return true
+ }
+}
From c627231b0f3911b0d47297027980d9187dd639de Mon Sep 17 00:00:00 2001
From: Bryan Lee <38807139+liby@users.noreply.github.com>
Date: Sun, 9 Feb 2025 20:27:49 +0800
Subject: [PATCH 232/365] Fix confirm-close-surface not working for hidden
quick terminal
---
macos/Sources/App/macOS/AppDelegate.swift | 20 +++++++++++++-------
1 file changed, 13 insertions(+), 7 deletions(-)
diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift
index 9d300e91f..ccd890951 100644
--- a/macos/Sources/App/macOS/AppDelegate.swift
+++ b/macos/Sources/App/macOS/AppDelegate.swift
@@ -245,7 +245,13 @@ class AppDelegate: NSObject,
// This probably isn't fully safe. The isEmpty check above is aspirational, it doesn't
// quite work with SwiftUI because windows are retained on close. So instead we check
// if there are any that are visible. I'm guessing this breaks under certain scenarios.
- if (windows.allSatisfy { !$0.isVisible }) { return .terminateNow }
+ //
+ // NOTE(mitchellh): I don't think we need this check at all anymore. I'm keeping it
+ // here because I don't want to remove it in a patch release cycle but we should
+ // target removing it soon.
+ if (self.quickController == nil && windows.allSatisfy { !$0.isVisible }) {
+ return .terminateNow
+ }
// If the user is shutting down, restarting, or logging out, we don't confirm quit.
why: if let event = NSAppleEventManager.shared().currentAppleEvent {
@@ -431,7 +437,7 @@ class AppDelegate: NSObject,
// If we have a main window then we don't process any of the keys
// because we let it capture and propagate.
guard NSApp.mainWindow == nil else { return event }
-
+
// If this event as-is would result in a key binding then we send it.
if let app = ghostty.app,
ghostty_app_key_is_binding(
@@ -447,26 +453,26 @@ class AppDelegate: NSObject,
return nil
}
}
-
+
// If this event would be handled by our menu then we do nothing.
if let mainMenu = NSApp.mainMenu,
mainMenu.performKeyEquivalent(with: event) {
return nil
}
-
+
// If we reach this point then we try to process the key event
// through the Ghostty key mechanism.
-
+
// Ghostty must be loaded
guard let ghostty = self.ghostty.app else { return event }
-
+
// Build our event input and call ghostty
if (ghostty_app_key(ghostty, event.ghosttyKeyEvent(GHOSTTY_ACTION_PRESS))) {
// The key was used so we want to stop it from going to our Mac app
Ghostty.logger.debug("local key event handled event=\(event)")
return nil
}
-
+
return event
}
From f8b547f92e4d4da665b2c2f5757b39e53c60e042 Mon Sep 17 00:00:00 2001
From: Mitchell Hashimoto
Date: Tue, 11 Feb 2025 12:55:30 -0800
Subject: [PATCH 233/365] Revert "Fixed documentation generation in
`list-actions --docs` command (#4974)"
This reverts commit f3d0c7c2ad01968de5191ccfae09a25e5c760760, reversing
changes made to 4b77a1c71e7994f6c040c4bee8a9e0b3d77b6286.
---
src/build/webgen/main_actions.zig | 54 ++++++++++++++-
src/cli/list_actions.zig | 16 ++++-
src/input/Binding.zig | 6 +-
src/input/helpgen_actions.zig | 107 ------------------------------
4 files changed, 69 insertions(+), 114 deletions(-)
delete mode 100644 src/input/helpgen_actions.zig
diff --git a/src/build/webgen/main_actions.zig b/src/build/webgen/main_actions.zig
index 65420d7e2..f4dffbc13 100644
--- a/src/build/webgen/main_actions.zig
+++ b/src/build/webgen/main_actions.zig
@@ -1,8 +1,58 @@
const std = @import("std");
const help_strings = @import("help_strings");
-const helpgen_actions = @import("../../helpgen_actions.zig");
+const KeybindAction = @import("../../input/Binding.zig").Action;
pub fn main() !void {
const output = std.io.getStdOut().writer();
- try helpgen_actions.generate(output, .markdown, std.heap.page_allocator);
+ try genKeybindActions(output);
+}
+
+pub fn genKeybindActions(writer: anytype) !void {
+ // Write the header
+ try writer.writeAll(
+ \\---
+ \\title: Keybinding Action Reference
+ \\description: Reference of all Ghostty keybinding actions.
+ \\editOnGithubLink: https://github.com/ghostty-org/ghostty/edit/main/src/input/Binding.zig
+ \\---
+ \\
+ \\This is a reference of all Ghostty keybinding actions.
+ \\
+ \\
+ );
+
+ @setEvalBranchQuota(5_000);
+
+ var buffer = std.ArrayList(u8).init(std.heap.page_allocator);
+ defer buffer.deinit();
+
+ const fields = @typeInfo(KeybindAction).Union.fields;
+ inline for (fields) |field| {
+ if (field.name[0] == '_') continue;
+
+ // Write previously stored doc comment below all related actions
+ if (@hasDecl(help_strings.KeybindAction, field.name)) {
+ try writer.writeAll(buffer.items);
+ try writer.writeAll("\n");
+
+ buffer.clearRetainingCapacity();
+ }
+
+ // Write the field name.
+ try writer.writeAll("## `");
+ try writer.writeAll(field.name);
+ try writer.writeAll("`\n");
+
+ if (@hasDecl(help_strings.KeybindAction, field.name)) {
+ var iter = std.mem.splitScalar(
+ u8,
+ @field(help_strings.KeybindAction, field.name),
+ '\n',
+ );
+ while (iter.next()) |s| {
+ try buffer.appendSlice(s);
+ try buffer.appendSlice("\n");
+ }
+ }
+ }
}
diff --git a/src/cli/list_actions.zig b/src/cli/list_actions.zig
index 429480277..6f67a92d2 100644
--- a/src/cli/list_actions.zig
+++ b/src/cli/list_actions.zig
@@ -2,7 +2,7 @@ const std = @import("std");
const args = @import("args.zig");
const Action = @import("action.zig").Action;
const Allocator = std.mem.Allocator;
-const helpgen_actions = @import("../helpgen_actions.zig");
+const help_strings = @import("help_strings");
pub const Options = struct {
/// If `true`, print out documentation about the action associated with the
@@ -38,7 +38,19 @@ pub fn run(alloc: Allocator) !u8 {
}
const stdout = std.io.getStdOut().writer();
- try helpgen_actions.generate(stdout, .plaintext, std.heap.page_allocator);
+ const info = @typeInfo(help_strings.KeybindAction);
+ inline for (info.Struct.decls) |field| {
+ try stdout.print("{s}", .{field.name});
+ if (opts.docs) {
+ try stdout.print(":\n", .{});
+ var iter = std.mem.splitScalar(u8, std.mem.trimRight(u8, @field(help_strings.KeybindAction, field.name), &std.ascii.whitespace), '\n');
+ while (iter.next()) |line| {
+ try stdout.print(" {s}\n", .{line});
+ }
+ } else {
+ try stdout.print("\n", .{});
+ }
+ }
return 0;
}
diff --git a/src/input/Binding.zig b/src/input/Binding.zig
index bef2ef613..90ea436af 100644
--- a/src/input/Binding.zig
+++ b/src/input/Binding.zig
@@ -236,9 +236,9 @@ pub const Action = union(enum) {
/// Send an `ESC` sequence.
esc: []const u8,
- /// Send the given text. Uses Zig string literal syntax. This is currently
- /// not validated. If the text is invalid (i.e. contains an invalid escape
- /// sequence), the error will currently only show up in logs.
+ // Send the given text. Uses Zig string literal syntax. This is currently
+ // not validated. If the text is invalid (i.e. contains an invalid escape
+ // sequence), the error will currently only show up in logs.
text: []const u8,
/// Send data to the pty depending on whether cursor key mode is enabled
diff --git a/src/input/helpgen_actions.zig b/src/input/helpgen_actions.zig
deleted file mode 100644
index 9a7612b57..000000000
--- a/src/input/helpgen_actions.zig
+++ /dev/null
@@ -1,107 +0,0 @@
-//! This module is a help generator for keybind actions documentation.
-//! It can generate documentation in different formats (plaintext for CLI,
-//! markdown for website) while maintaining consistent content.
-
-const std = @import("std");
-const KeybindAction = @import("Binding.zig").Action;
-const help_strings = @import("help_strings");
-
-/// Format options for generating keybind actions documentation
-pub const Format = enum {
- /// Plain text output with indentation
- plaintext,
- /// Markdown formatted output
- markdown,
-
- fn formatFieldName(self: Format, writer: anytype, field_name: []const u8) !void {
- switch (self) {
- .plaintext => {
- try writer.writeAll(field_name);
- try writer.writeAll(":\n");
- },
- .markdown => {
- try writer.writeAll("## `");
- try writer.writeAll(field_name);
- try writer.writeAll("`\n");
- },
- }
- }
-
- fn formatDocLine(self: Format, writer: anytype, line: []const u8) !void {
- switch (self) {
- .plaintext => {
- try writer.appendSlice(" ");
- try writer.appendSlice(line);
- try writer.appendSlice("\n");
- },
- .markdown => {
- try writer.appendSlice(line);
- try writer.appendSlice("\n");
- },
- }
- }
-
- fn header(self: Format) ?[]const u8 {
- return switch (self) {
- .plaintext => null,
- .markdown =>
- \\---
- \\title: Keybinding Action Reference
- \\description: Reference of all Ghostty keybinding actions.
- \\editOnGithubLink: https://github.com/ghostty-org/ghostty/edit/main/src/input/Binding.zig
- \\---
- \\
- \\This is a reference of all Ghostty keybinding actions.
- \\
- \\
- ,
- };
- }
-};
-
-/// Generate keybind actions documentation with the specified format
-pub fn generate(
- writer: anytype,
- format: Format,
- page_allocator: std.mem.Allocator,
-) !void {
- if (format.header()) |header| {
- try writer.writeAll(header);
- }
-
- var buffer = std.ArrayList(u8).init(page_allocator);
- defer buffer.deinit();
-
- const fields = @typeInfo(KeybindAction).Union.fields;
- inline for (fields) |field| {
- if (field.name[0] == '_') continue;
-
- // Write previously stored doc comment below all related actions
- if (@hasDecl(help_strings.KeybindAction, field.name)) {
- try writer.writeAll(buffer.items);
- try writer.writeAll("\n");
-
- buffer.clearRetainingCapacity();
- }
-
- try format.formatFieldName(writer, field.name);
-
- if (@hasDecl(help_strings.KeybindAction, field.name)) {
- var iter = std.mem.splitScalar(
- u8,
- @field(help_strings.KeybindAction, field.name),
- '\n',
- );
- while (iter.next()) |s| {
- // If it is the last line and empty, then skip it.
- if (iter.peek() == null and s.len == 0) continue;
- try format.formatDocLine(&buffer, s);
- }
- }
- }
-
- // Write any remaining buffered documentation
- if (buffer.items.len > 0) {
- try writer.writeAll(buffer.items);
- }
-}
From 56ea6c406c155a48947274cd871ae274e654c957 Mon Sep 17 00:00:00 2001
From: Leah Amelia Chen
Date: Sun, 9 Feb 2025 19:52:09 +0100
Subject: [PATCH 234/365] gtk(x11): set `WINDOWID` env var for subprocesses
`WINDOWID` is the conventional environment variable for scripts that
want to know the X11 window ID of the terminal, so that it may call
tools like `xprop` or `xdotool`. We already know the window ID for
window protocol handling, so we might as well throw this in for
convenience.
---
src/Surface.zig | 8 ++++++
src/apprt/embedded.zig | 25 ++++++++++++++++++
src/apprt/glfw.zig | 5 ++++
src/apprt/gtk/Surface.zig | 19 ++++++++++++++
src/apprt/gtk/winproto.zig | 6 +++++
src/apprt/gtk/winproto/noop.zig | 2 ++
src/apprt/gtk/winproto/wayland.zig | 5 ++++
src/apprt/gtk/winproto/x11.zig | 7 +++++
src/os/env.zig | 9 +++++++
src/os/main.zig | 1 +
src/termio/Exec.zig | 42 ++++--------------------------
11 files changed, 92 insertions(+), 37 deletions(-)
diff --git a/src/Surface.zig b/src/Surface.zig
index d9a985aa7..e7e8e20af 100644
--- a/src/Surface.zig
+++ b/src/Surface.zig
@@ -519,9 +519,17 @@ pub fn init(
// This separate block ({}) is important because our errdefers must
// be scoped here to be valid.
{
+ var env_ = rt_surface.defaultTermioEnv() catch |err| env: {
+ // If an error occurs, we don't want to block surface startup.
+ log.warn("error getting env map for surface err={}", .{err});
+ break :env null;
+ };
+ errdefer if (env_) |*env| env.deinit();
+
// Initialize our IO backend
var io_exec = try termio.Exec.init(alloc, .{
.command = command,
+ .env = env_,
.shell_integration = config.@"shell-integration",
.shell_integration_features = config.@"shell-integration-features",
.working_directory = config.@"working-directory",
diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig
index 358e9f291..864e00205 100644
--- a/src/apprt/embedded.zig
+++ b/src/apprt/embedded.zig
@@ -12,6 +12,7 @@ const objc = @import("objc");
const apprt = @import("../apprt.zig");
const font = @import("../font/main.zig");
const input = @import("../input.zig");
+const internal_os = @import("../os/main.zig");
const renderer = @import("../renderer.zig");
const terminal = @import("../terminal/main.zig");
const CoreApp = @import("../App.zig");
@@ -1026,6 +1027,30 @@ pub const Surface = struct {
};
}
+ pub fn defaultTermioEnv(self: *const Surface) !?std.process.EnvMap {
+ const alloc = self.app.core_app.alloc;
+ var env = try internal_os.getEnvMap(alloc);
+ errdefer env.deinit();
+
+ if (comptime builtin.target.isDarwin()) {
+ if (env.get("__XCODE_BUILT_PRODUCTS_DIR_PATHS") != null) {
+ env.remove("__XCODE_BUILT_PRODUCTS_DIR_PATHS");
+ env.remove("__XPC_DYLD_LIBRARY_PATH");
+ env.remove("DYLD_FRAMEWORK_PATH");
+ env.remove("DYLD_INSERT_LIBRARIES");
+ env.remove("DYLD_LIBRARY_PATH");
+ env.remove("LD_LIBRARY_PATH");
+ env.remove("SECURITYSESSIONID");
+ env.remove("XPC_SERVICE_NAME");
+ }
+
+ // Remove this so that running `ghostty` within Ghostty works.
+ env.remove("GHOSTTY_MAC_APP");
+ }
+
+ return env;
+ }
+
/// The cursor position from the host directly is in screen coordinates but
/// all our interface works in pixels.
fn cursorPosToPixels(self: *const Surface, pos: apprt.CursorPos) !apprt.CursorPos {
diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig
index 686a70ddb..729decc0f 100644
--- a/src/apprt/glfw.zig
+++ b/src/apprt/glfw.zig
@@ -874,6 +874,11 @@ pub const Surface = struct {
};
}
+ pub fn defaultTermioEnv(self: *Surface) !?std.process.EnvMap {
+ _ = self;
+ return null;
+ }
+
fn sizeCallback(window: glfw.Window, width: i32, height: i32) void {
_ = width;
_ = height;
diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig
index b34ca9aa3..b9f8949fb 100644
--- a/src/apprt/gtk/Surface.zig
+++ b/src/apprt/gtk/Surface.zig
@@ -2252,6 +2252,25 @@ fn doPaste(self: *Surface, data: [:0]const u8) void {
};
}
+pub fn defaultTermioEnv(self: *Surface) !?std.process.EnvMap {
+ const alloc = self.app.core_app.alloc;
+ var env = try internal_os.getEnvMap(alloc);
+ errdefer env.deinit();
+
+ // Don't leak these GTK environment variables to child processes.
+ env.remove("GDK_DEBUG");
+ env.remove("GDK_DISABLE");
+ env.remove("GSK_RENDERER");
+
+ if (self.container.window()) |window| {
+ // On some window protocols we might want to add specific
+ // environment variables to subprocesses, such as WINDOWID on X11.
+ try window.winproto.addSubprocessEnv(&env);
+ }
+
+ return env;
+}
+
/// Check a GValue to see what's type its wrapping. This is equivalent to GTK's
/// `G_VALUE_HOLDS` macro but Zig's C translator does not like it.
fn g_value_holds(value_: ?*c.GValue, g_type: c.GType) bool {
diff --git a/src/apprt/gtk/winproto.zig b/src/apprt/gtk/winproto.zig
index e6020f49e..c752ee692 100644
--- a/src/apprt/gtk/winproto.zig
+++ b/src/apprt/gtk/winproto.zig
@@ -131,4 +131,10 @@ pub const Window = union(Protocol) {
inline else => |v| v.clientSideDecorationEnabled(),
};
}
+
+ pub fn addSubprocessEnv(self: *Window, env: *std.process.EnvMap) !void {
+ switch (self.*) {
+ inline else => |*v| try v.addSubprocessEnv(env),
+ }
+ }
};
diff --git a/src/apprt/gtk/winproto/noop.zig b/src/apprt/gtk/winproto/noop.zig
index 38703aecb..cb1c0e9eb 100644
--- a/src/apprt/gtk/winproto/noop.zig
+++ b/src/apprt/gtk/winproto/noop.zig
@@ -61,4 +61,6 @@ pub const Window = struct {
_ = self;
return true;
}
+
+ pub fn addSubprocessEnv(_: *Window, _: *std.process.EnvMap) !void {}
};
diff --git a/src/apprt/gtk/winproto/wayland.zig b/src/apprt/gtk/winproto/wayland.zig
index 3e239eb29..f2ef17d73 100644
--- a/src/apprt/gtk/winproto/wayland.zig
+++ b/src/apprt/gtk/winproto/wayland.zig
@@ -262,6 +262,11 @@ pub const Window = struct {
};
}
+ pub fn addSubprocessEnv(self: *Window, env: *std.process.EnvMap) !void {
+ _ = self;
+ _ = env;
+ }
+
/// Update the blur state of the window.
fn syncBlur(self: *Window) !void {
const manager = self.app_context.kde_blur_manager orelse return;
diff --git a/src/apprt/gtk/winproto/x11.zig b/src/apprt/gtk/winproto/x11.zig
index c58df6dea..6b60b0edf 100644
--- a/src/apprt/gtk/winproto/x11.zig
+++ b/src/apprt/gtk/winproto/x11.zig
@@ -314,6 +314,13 @@ pub const Window = struct {
);
}
+ pub fn addSubprocessEnv(self: *Window, env: *std.process.EnvMap) !void {
+ var buf: [64]u8 = undefined;
+ const window_id = try std.fmt.bufPrint(&buf, "{}", .{self.window});
+
+ try env.put("WINDOWID", window_id);
+ }
+
fn getWindowProperty(
self: *Window,
comptime T: type,
diff --git a/src/os/env.zig b/src/os/env.zig
index fe2be20de..1916053b3 100644
--- a/src/os/env.zig
+++ b/src/os/env.zig
@@ -2,9 +2,18 @@ const std = @import("std");
const builtin = @import("builtin");
const Allocator = std.mem.Allocator;
const posix = std.posix;
+const isFlatpak = @import("flatpak.zig").isFlatpak;
pub const Error = Allocator.Error;
+/// Get the environment map.
+pub fn getEnvMap(alloc: Allocator) !std.process.EnvMap {
+ return if (isFlatpak())
+ std.process.EnvMap.init(alloc)
+ else
+ try std.process.getEnvMap(alloc);
+}
+
/// Append a value to an environment variable such as PATH.
/// The returned value is always allocated so it must be freed.
pub fn appendEnv(
diff --git a/src/os/main.zig b/src/os/main.zig
index df6f894f5..cb9355931 100644
--- a/src/os/main.zig
+++ b/src/os/main.zig
@@ -26,6 +26,7 @@ pub const shell = @import("shell.zig");
// Functions and types
pub const CFReleaseThread = @import("cf_release_thread.zig");
pub const TempDir = @import("TempDir.zig");
+pub const getEnvMap = env.getEnvMap;
pub const appendEnv = env.appendEnv;
pub const appendEnvAlways = env.appendEnvAlways;
pub const prependEnv = env.prependEnv;
diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig
index 4428b16e1..d9730b970 100644
--- a/src/termio/Exec.zig
+++ b/src/termio/Exec.zig
@@ -682,6 +682,7 @@ pub const ThreadData = struct {
pub const Config = struct {
command: ?[]const u8 = null,
+ env: ?EnvMap = null,
shell_integration: configpkg.Config.ShellIntegration = .detect,
shell_integration_features: configpkg.Config.ShellIntegrationFeatures = .{},
working_directory: ?[]const u8 = null,
@@ -721,18 +722,9 @@ const Subprocess = struct {
errdefer arena.deinit();
const alloc = arena.allocator();
- // Set our env vars. For Flatpak builds running in Flatpak we don't
- // inherit our environment because the login shell on the host side
- // will get it.
- var env = env: {
- if (comptime build_config.flatpak) {
- if (internal_os.isFlatpak()) {
- break :env std.process.EnvMap.init(alloc);
- }
- }
-
- break :env try std.process.getEnvMap(alloc);
- };
+ // Get our env. If a default env isn't provided by the caller
+ // then we get it ourselves.
+ var env = cfg.env orelse try internal_os.getEnvMap(alloc);
errdefer env.deinit();
// If we have a resources dir then set our env var
@@ -847,35 +839,11 @@ const Subprocess = struct {
try env.put("TERM_PROGRAM", "ghostty");
try env.put("TERM_PROGRAM_VERSION", build_config.version_string);
- // When embedding in macOS and running via XCode, XCode injects
- // a bunch of things that break our shell process. We remove those.
- if (comptime builtin.target.isDarwin() and build_config.artifact == .lib) {
- if (env.get("__XCODE_BUILT_PRODUCTS_DIR_PATHS") != null) {
- env.remove("__XCODE_BUILT_PRODUCTS_DIR_PATHS");
- env.remove("__XPC_DYLD_LIBRARY_PATH");
- env.remove("DYLD_FRAMEWORK_PATH");
- env.remove("DYLD_INSERT_LIBRARIES");
- env.remove("DYLD_LIBRARY_PATH");
- env.remove("LD_LIBRARY_PATH");
- env.remove("SECURITYSESSIONID");
- env.remove("XPC_SERVICE_NAME");
- }
-
- // Remove this so that running `ghostty` within Ghostty works.
- env.remove("GHOSTTY_MAC_APP");
- }
-
// VTE_VERSION is set by gnome-terminal and other VTE-based terminals.
// We don't want our child processes to think we're running under VTE.
+ // This is not apprt-specific, so we do it here.
env.remove("VTE_VERSION");
- // Don't leak these GTK environment variables to child processes.
- if (comptime build_config.app_runtime == .gtk) {
- env.remove("GDK_DEBUG");
- env.remove("GDK_DISABLE");
- env.remove("GSK_RENDERER");
- }
-
// Setup our shell integration, if we can.
const integrated_shell: ?shell_integration.Shell, const shell_command: []const u8 = shell: {
const default_shell_command = cfg.command orelse switch (builtin.os.tag) {
From 1674224c1a195066e455b812a0b621b8b95d36fb Mon Sep 17 00:00:00 2001
From: Bryan Lee <38807139+liby@users.noreply.github.com>
Date: Wed, 12 Feb 2025 06:29:40 +0800
Subject: [PATCH 235/365] Refactor keybinding actions reference generation
---
src/build/webgen/main_actions.zig | 54 +-------------
src/cli/list_actions.zig | 16 +----
src/input/Binding.zig | 6 +-
src/input/helpgen_actions.zig | 113 ++++++++++++++++++++++++++++++
4 files changed, 120 insertions(+), 69 deletions(-)
create mode 100644 src/input/helpgen_actions.zig
diff --git a/src/build/webgen/main_actions.zig b/src/build/webgen/main_actions.zig
index f4dffbc13..5002a5bac 100644
--- a/src/build/webgen/main_actions.zig
+++ b/src/build/webgen/main_actions.zig
@@ -1,58 +1,8 @@
const std = @import("std");
const help_strings = @import("help_strings");
-const KeybindAction = @import("../../input/Binding.zig").Action;
+const helpgen_actions = @import("../../input/helpgen_actions.zig");
pub fn main() !void {
const output = std.io.getStdOut().writer();
- try genKeybindActions(output);
-}
-
-pub fn genKeybindActions(writer: anytype) !void {
- // Write the header
- try writer.writeAll(
- \\---
- \\title: Keybinding Action Reference
- \\description: Reference of all Ghostty keybinding actions.
- \\editOnGithubLink: https://github.com/ghostty-org/ghostty/edit/main/src/input/Binding.zig
- \\---
- \\
- \\This is a reference of all Ghostty keybinding actions.
- \\
- \\
- );
-
- @setEvalBranchQuota(5_000);
-
- var buffer = std.ArrayList(u8).init(std.heap.page_allocator);
- defer buffer.deinit();
-
- const fields = @typeInfo(KeybindAction).Union.fields;
- inline for (fields) |field| {
- if (field.name[0] == '_') continue;
-
- // Write previously stored doc comment below all related actions
- if (@hasDecl(help_strings.KeybindAction, field.name)) {
- try writer.writeAll(buffer.items);
- try writer.writeAll("\n");
-
- buffer.clearRetainingCapacity();
- }
-
- // Write the field name.
- try writer.writeAll("## `");
- try writer.writeAll(field.name);
- try writer.writeAll("`\n");
-
- if (@hasDecl(help_strings.KeybindAction, field.name)) {
- var iter = std.mem.splitScalar(
- u8,
- @field(help_strings.KeybindAction, field.name),
- '\n',
- );
- while (iter.next()) |s| {
- try buffer.appendSlice(s);
- try buffer.appendSlice("\n");
- }
- }
- }
+ try helpgen_actions.generate(output, .markdown, true, std.heap.page_allocator);
}
diff --git a/src/cli/list_actions.zig b/src/cli/list_actions.zig
index 6f67a92d2..1d17873cc 100644
--- a/src/cli/list_actions.zig
+++ b/src/cli/list_actions.zig
@@ -2,7 +2,7 @@ const std = @import("std");
const args = @import("args.zig");
const Action = @import("action.zig").Action;
const Allocator = std.mem.Allocator;
-const help_strings = @import("help_strings");
+const helpgen_actions = @import("../input/helpgen_actions.zig");
pub const Options = struct {
/// If `true`, print out documentation about the action associated with the
@@ -38,19 +38,7 @@ pub fn run(alloc: Allocator) !u8 {
}
const stdout = std.io.getStdOut().writer();
- const info = @typeInfo(help_strings.KeybindAction);
- inline for (info.Struct.decls) |field| {
- try stdout.print("{s}", .{field.name});
- if (opts.docs) {
- try stdout.print(":\n", .{});
- var iter = std.mem.splitScalar(u8, std.mem.trimRight(u8, @field(help_strings.KeybindAction, field.name), &std.ascii.whitespace), '\n');
- while (iter.next()) |line| {
- try stdout.print(" {s}\n", .{line});
- }
- } else {
- try stdout.print("\n", .{});
- }
- }
+ try helpgen_actions.generate(stdout, .plaintext, opts.docs, std.heap.page_allocator);
return 0;
}
diff --git a/src/input/Binding.zig b/src/input/Binding.zig
index 90ea436af..bef2ef613 100644
--- a/src/input/Binding.zig
+++ b/src/input/Binding.zig
@@ -236,9 +236,9 @@ pub const Action = union(enum) {
/// Send an `ESC` sequence.
esc: []const u8,
- // Send the given text. Uses Zig string literal syntax. This is currently
- // not validated. If the text is invalid (i.e. contains an invalid escape
- // sequence), the error will currently only show up in logs.
+ /// Send the given text. Uses Zig string literal syntax. This is currently
+ /// not validated. If the text is invalid (i.e. contains an invalid escape
+ /// sequence), the error will currently only show up in logs.
text: []const u8,
/// Send data to the pty depending on whether cursor key mode is enabled
diff --git a/src/input/helpgen_actions.zig b/src/input/helpgen_actions.zig
new file mode 100644
index 000000000..58305455b
--- /dev/null
+++ b/src/input/helpgen_actions.zig
@@ -0,0 +1,113 @@
+//! This module is a help generator for keybind actions documentation.
+//! It can generate documentation in different formats (plaintext for CLI,
+//! markdown for website) while maintaining consistent content.
+
+const std = @import("std");
+const KeybindAction = @import("Binding.zig").Action;
+const help_strings = @import("help_strings");
+
+/// Format options for generating keybind actions documentation
+pub const Format = enum {
+ /// Plain text output with indentation
+ plaintext,
+ /// Markdown formatted output
+ markdown,
+
+ fn formatFieldName(self: Format, writer: anytype, field_name: []const u8) !void {
+ switch (self) {
+ .plaintext => {
+ try writer.writeAll(field_name);
+ try writer.writeAll(":\n");
+ },
+ .markdown => {
+ try writer.writeAll("## `");
+ try writer.writeAll(field_name);
+ try writer.writeAll("`\n");
+ },
+ }
+ }
+
+ fn formatDocLine(self: Format, writer: anytype, line: []const u8) !void {
+ switch (self) {
+ .plaintext => {
+ try writer.appendSlice(" ");
+ try writer.appendSlice(line);
+ try writer.appendSlice("\n");
+ },
+ .markdown => {
+ try writer.appendSlice(line);
+ try writer.appendSlice("\n");
+ },
+ }
+ }
+
+ fn header(self: Format) ?[]const u8 {
+ return switch (self) {
+ .plaintext => null,
+ .markdown =>
+ \\---
+ \\title: Keybinding Action Reference
+ \\description: Reference of all Ghostty keybinding actions.
+ \\editOnGithubLink: https://github.com/ghostty-org/ghostty/edit/main/src/input/Binding.zig
+ \\---
+ \\
+ \\This is a reference of all Ghostty keybinding actions.
+ \\
+ \\
+ ,
+ };
+ }
+};
+
+/// Generate keybind actions documentation with the specified format
+pub fn generate(
+ writer: anytype,
+ format: Format,
+ show_docs: bool,
+ page_allocator: std.mem.Allocator,
+) !void {
+ if (format.header()) |header| {
+ try writer.writeAll(header);
+ }
+
+ var buffer = std.ArrayList(u8).init(page_allocator);
+ defer buffer.deinit();
+
+ const fields = @typeInfo(KeybindAction).Union.fields;
+ inline for (fields) |field| {
+ if (field.name[0] == '_') continue;
+
+ // Write previously stored doc comment below all related actions
+ if (show_docs and @hasDecl(help_strings.KeybindAction, field.name)) {
+ try writer.writeAll(buffer.items);
+ try writer.writeAll("\n");
+
+ buffer.clearRetainingCapacity();
+ }
+
+ if (show_docs) {
+ try format.formatFieldName(writer, field.name);
+ } else {
+ try writer.writeAll(field.name);
+ try writer.writeAll("\n");
+ }
+
+ if (show_docs and @hasDecl(help_strings.KeybindAction, field.name)) {
+ var iter = std.mem.splitScalar(
+ u8,
+ @field(help_strings.KeybindAction, field.name),
+ '\n',
+ );
+ while (iter.next()) |s| {
+ // If it is the last line and empty, then skip it.
+ if (iter.peek() == null and s.len == 0) continue;
+ try format.formatDocLine(&buffer, s);
+ }
+ }
+ }
+
+ // Write any remaining buffered documentation
+ if (buffer.items.len > 0) {
+ try writer.writeAll(buffer.items);
+ }
+}
From 58ab66f094b8ab2bd04d7a74cd90f7beb600764a Mon Sep 17 00:00:00 2001
From: Mitchell Hashimoto
Date: Tue, 11 Feb 2025 14:06:03 -0800
Subject: [PATCH 236/365] macos: add a variety of artist-drawn alternate icons
This is just a fun change to add a bunch of alternate icons. We don't
want to add too many since this increases the final bundle size but we
also want to have some fun. :)
---
.../BlueprintImage.imageset/Contents.json | 12 +++++++++
.../macOS-AppIcon-1024px.png | Bin 0 -> 443891 bytes
.../ChalkboardImage.imageset/Contents.json | 12 +++++++++
.../macOS-AppIcon-1024px.png | Bin 0 -> 590208 bytes
.../Alternate Icons/Contents.json | 6 +++++
.../GlassImage.imageset/Contents.json | 12 +++++++++
.../macOS-AppIcon-1024px.png | Bin 0 -> 527199 bytes
.../HolographicImage.imageset/Contents.json | 12 +++++++++
.../macOS-AppIcon-1024px.png | Bin 0 -> 602467 bytes
.../MicrochipImage.imageset/Contents.json | 12 +++++++++
.../macOS-AppIcon-1024px.png | Bin 0 -> 645261 bytes
.../PaperImage.imageset/Contents.json | 12 +++++++++
.../macOS-AppIcon-1024px.png | Bin 0 -> 342677 bytes
.../RetroImage.imageset/Contents.json | 12 +++++++++
.../macOS-AppIcon-1024px.png | Bin 0 -> 1083552 bytes
.../XrayImage.imageset/Contents.json | 12 +++++++++
.../macOS-AppIcon-1024px.png | Bin 0 -> 453961 bytes
macos/Sources/App/macOS/AppDelegate.swift | 24 ++++++++++++++++++
macos/Sources/Ghostty/Package.swift | 8 ++++++
src/config/Config.zig | 11 ++++++++
20 files changed, 145 insertions(+)
create mode 100644 macos/Assets.xcassets/Alternate Icons/BlueprintImage.imageset/Contents.json
create mode 100644 macos/Assets.xcassets/Alternate Icons/BlueprintImage.imageset/macOS-AppIcon-1024px.png
create mode 100644 macos/Assets.xcassets/Alternate Icons/ChalkboardImage.imageset/Contents.json
create mode 100644 macos/Assets.xcassets/Alternate Icons/ChalkboardImage.imageset/macOS-AppIcon-1024px.png
create mode 100644 macos/Assets.xcassets/Alternate Icons/Contents.json
create mode 100644 macos/Assets.xcassets/Alternate Icons/GlassImage.imageset/Contents.json
create mode 100644 macos/Assets.xcassets/Alternate Icons/GlassImage.imageset/macOS-AppIcon-1024px.png
create mode 100644 macos/Assets.xcassets/Alternate Icons/HolographicImage.imageset/Contents.json
create mode 100644 macos/Assets.xcassets/Alternate Icons/HolographicImage.imageset/macOS-AppIcon-1024px.png
create mode 100644 macos/Assets.xcassets/Alternate Icons/MicrochipImage.imageset/Contents.json
create mode 100644 macos/Assets.xcassets/Alternate Icons/MicrochipImage.imageset/macOS-AppIcon-1024px.png
create mode 100644 macos/Assets.xcassets/Alternate Icons/PaperImage.imageset/Contents.json
create mode 100644 macos/Assets.xcassets/Alternate Icons/PaperImage.imageset/macOS-AppIcon-1024px.png
create mode 100644 macos/Assets.xcassets/Alternate Icons/RetroImage.imageset/Contents.json
create mode 100644 macos/Assets.xcassets/Alternate Icons/RetroImage.imageset/macOS-AppIcon-1024px.png
create mode 100644 macos/Assets.xcassets/Alternate Icons/XrayImage.imageset/Contents.json
create mode 100644 macos/Assets.xcassets/Alternate Icons/XrayImage.imageset/macOS-AppIcon-1024px.png
diff --git a/macos/Assets.xcassets/Alternate Icons/BlueprintImage.imageset/Contents.json b/macos/Assets.xcassets/Alternate Icons/BlueprintImage.imageset/Contents.json
new file mode 100644
index 000000000..1c1b9b47e
--- /dev/null
+++ b/macos/Assets.xcassets/Alternate Icons/BlueprintImage.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "macOS-AppIcon-1024px.png",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/macos/Assets.xcassets/Alternate Icons/BlueprintImage.imageset/macOS-AppIcon-1024px.png b/macos/Assets.xcassets/Alternate Icons/BlueprintImage.imageset/macOS-AppIcon-1024px.png
new file mode 100644
index 0000000000000000000000000000000000000000..ffba7d94dc4024201b5f3ec72f37fecc80813e5e
GIT binary patch
literal 443891
zcmeE~WmHtr`|r;TL+8+4A|V|Tf(!@b|_sT4$~O<~;j3`}_Gmd!HStrJ+nrKu-Vw0I{lyq7DE+fPW7j4gdgf
zCf;BGr>{j-QT~x#blweRnV;hiRcGcDA7Nf8ecBIr3%Erb+)mitQZ+NOOE
zgOU1bT3W#$%ArR-ZKEMrE5*L$<*h
z0idQXktRI)9jEAyrtVqiPJ>7hwJTPPZC1Jcn^nBDY9TCA9#`+!Xepxd@sp5H>HXEd
zqvw5(hf-o97y-pzfn8SjK!Ll~s|lUX=)WM;vnm9Z7QgyDHPN
z?~JuEP!TK1h?dkM!?aH6?sWE-x~9h`*eu^Q!S_BQg!D048hEbXd<5?oF3#Do|9-*K
zPQlx|p@9F+qigekDNrM@r@$ZbM3IrO9lIUWqI7(Oc%$L^wY4NxJ4Ya;bZ5=v;lnhT
zIFCJ3E0xc|x0}~h?e;QNi%t47Ms7wG3tJ~pE8vg;)9r_gfdrV+IGVgS0a6#vpb1!r
zi1}BzJ5T=-HH;y23n}s`CZr3^CssbD(UknedG(Wby;G#owwlKBS@Kj6v&1Hxyd6p;NnQlk1zF50e<=oHNPtCL+OAk@E6eAMH`o10{hNiha`=Xaz$26uHpID$$
zK*T%~e}`kG5Ye?=DRjmt~?**~$rVSl1F9g1FbIYZ`f)@j%4e$UkH)
z0||414!NvCzcO>%Q?dvAxRm)mz2a!*u;W0~;uDCESKu|wNJbS3&I|NcYQ^6;tyQyIzSGh@wD$Q#QqQpm|dt2e##R`*GhvSuceMuKj;R_<47EPXGV4arm=Y3b#aS%h)F
zc^$MR6B(Z0{(fi@ES$!QXX^Y)E{88-We<8I93r9TG-gYIm3=w@l^#Mk0n%G@pu2aS
zwCovRR(8_dg2S06IOwJ-1TAV4haU2x5LIi(H@TplPdnASm%P?
zTZXJ+uQ4xMu)hEz2Ahems~`tMg=lFTP4EF;1~Mlb$>JIaTSCH2s^i=jaISmKZmhpM
zRF8j4IeD&y!trC}S(EeH{*EbA
z#q$HLd$QiOw(Kdswj)5QHUtfZUR`spbrQHwz@c&o{C(`(S)9|iRhSQf0kWQOneW>h
z6FJ*99kNvUQ4xdmT;v;umAn(LHym;V;<<27u_1LLFA2`c0pX>q79x!0&q&1hfdf*|
zEiS?af`~Y0!kN~~Gg4o~1bNWe0K|osAlMSRuPcE0@AgyFO@PFA7Q+2A^H)d#cJ|YW
zKJsw$yS;*^)DJW%OO#S*X*CGEP(E-mIoEGstyJheRtOwyyc~O+2pG=2QH%zn7!^~4
zcI>7qaSl4=e0%xU!a?wtQ4xk^Lnx)?g63=NP*;JQ^PiebxH
zFx}zv87UO)$+Z%wVcW3kQp|JJs{-KaI>PILV05Df_6<8b`)BO?Pqdl@ia@*1`ECL~
zF^bjj!^eDPSmZ(Y=SYv2m{zr9f?o%8!
ziTIs`YPX^loSWsGdt;~0z;?OSQ|5b_HU069h=>Tok_7O=3DWO4uIO;IayuI5&otO)
z2;qz&Dew_2XrV$nxicw>f#obUI65L?kbnzP
z`%#Q;f)lX1(1p$@ug|mseo6;|1yF#UraL!i=efvJmXv#kfCyMcX%8U4a88j24uC?T
zZxci0ep>P+6CKOYtx>U;Y3StLJ}U4bY->@s$BxKKg#3r!!+Yu!W4R*hpdF~`v!;RE
z`l2xpIO|_7yx;Pj|4kDdg3L(-jlO1Mp_)k2UXHfA)nNSqa?V{ev>3++K9xb~j#=u@
zKMEOPzvPgX@0_u0e@M1Za-%;X$j;9G0>?rZyVgPuUL*H=2&l&g#4dE#XDT9g)q#M*
zOX^8_fSnZEY=_6f2m9OikK!Yp_}4?t$WG$xy5zgkJQGDl#U!G^&!FfzqT>W@kRIFz
zq0g+nf>ZSY?A)~Y?jLYg4%L1BF5tUW%}`EyU#)
zqb$UYGm^hGTsJ#Gw?
zr8c!aC&v-;@Bj84#dN73Z7+6j36JFKB|lW?*NY(DfKd0^+guI&Cwn!oTu9
z34{y&Jrdka9Y4S=UHPX&!qezLZP1DdFKe4R#4VRLo(n8fa3|E}M}=T1_g6Zn1QOtd
zR89yg3_j;X6yNdD_>qa&h)Fw|ot>S1+y0eY$7{qDHF1nHYbN=GIwWanea4q;%N}&6
zbHG~wK`_d|m3TjZr>9M^_Yj?O%-0M^KNr~N67BycpfUA9YO%AjK(%HE+S_1leuQmp
zbV#(Fi8gqxrS&I4?V$iTT7P0H+N=-fZGANA?IQp2|Cye~%0~{KjX1thlxuH(u)6~|
z?rgOmAEZ9N4hKROU)SU14c@ZZjK9zL1))xzBw%GZT!q{hA^+)Su|)y8u>riK7@%av
zE0L$?(SV=s!6RH~T<&+>3f8^9EN>VC82F6vsY`+}oX6H>dv$pxft?W%5rbu%;L}o2
zb}^1usLcz-M7-l4J;g^Ks0rFM4<7w9UyA@v^xS0Bb{9b=|M6T>USJC4^*^P)IkD&Y
z+aWw@2>6z2nW>KLuP=w$@BA~USziDV-*W8J#a9-Y4mGKfcdlqv#A_$klK)nEKn`*g
z$+J_2oX=qc@1u@)qqSwPWdN8~=8u-mnqs(o1#9`?q`1^6$^7q(duL3`{^(gTzfV>!
z82(S+WU6Qu7Fv?}A;dZdQlnjlX=c{`!JQTGb@A0Xty}_n$EWC%L|0VwP|@6K)V&O*
zx%obX=lCC=Qe=^cl_Z2XbWIQbL{98$>Z+pmt`D%PuMveWE7VqGrCsqt`ZP?=8
zk1BwSwNA{N&U%}i&YaDE=BjNh(Ct0G1tw)5vbKTS_W8))d{An**u{fC0Q57XW&yJ1
z>YN|--h|&I8^6!NMoF*cWH2)mtpXP4PCD`j8C8?+RM3Q{5ps}zB_cm{8@guy4q3V0
z0$b3`hcBQG+G*Sz;w2Ch7_|ZXI|%7ClT!de4BjYA7?0F@
zQ+!LCxK+3x`gw3Mw|hT~eceuMWy)?-g!8XVm-X0^#X`Au9(ke5dc?gAB7i)PzZfR(
zy$eB9?Pw(G^_gQZZySR4^_3mZHm-U&=gxi6(aSz)s%tDqgbBwRGn>m9S3!36kMT+)
zpFze?3cgvPa9?Ki*&vL;z$iFS5OgfQtW_K3b=RBvlc1L6X=I`T3y2HeB?d~HIKQ8B
zAH6ujMv1GMYCDOmw)iS4D*pade+Q6vlF0YDMhyj115?_n0jE8JmbjvZ|Rzd=}
zZa-vxTT={Vz2Y4VIP)tSmFVHjT!@F;gW;S2#1^-FE;mG-+(8FHfsJBkXaAU!2%aX8
z<@el;kfi+r>dy4R4td}=nS|i!E`~kx-hBtG&Mvc;2c^nv1>0dS2#QBV3&N$R|9pB$
zbOWPpzvpoHf_U5|ONf2t0k!4P2DHl
z89aH(rHr{HDN>M_5(hH#G{9n!05ve}VDEdsS6fN3hZHBIq+#mrpL{Ui_B$a>W>;ux
z{Sy}Do#$&wl5kBX?I(WaV;!j1txAujU#IGY?!IcSQ<_;?G`F&Yc}4wp;6tuhuQ!Zlmgb$B^SDpD+feRdn#xyS6rT3cT~~yVF(Njoabg
z@ID%6=`oyt)o-!XsloEn)!N|FiLtq8!G5?Cb15*l=H}to>{~UmG}E+;uJL7eLTw%P
zai6VDHlo8<;A^NPh3zI-xKq>i97EJ*WY2dQykXud$nu&
zt^c|4@xkRi%k5LfdxLC~-1XJ*liYH20D9Fn_YX{WdN|X`b)bhy)-kC2(PG{SV44t5
z;*@pcPrY8FCY_~K)
z?+wVJ8T#gj>Y&ex44CgWtEA+ti!dVRQ@1dUZ@@K
zb{ilQ3a~D`0x=(oR;?jtN;>
zL(=XrNLfYJZykL}fk=Jn1Mk{m0%#1i29$Si$GMw|FX3?h)qTzTPv<6F?ZTuy8}Ptt
zM8sp^lo33M(4UrWyZ}2u7^eOlqxhJ8lA|pr{>}U1-?6xf_DqE#>q))0+g}Tp2EW8)
zoHs@`&bc2pr$F{3z4ZrQPDs6q%;|cVo-!ZC{Pz&6#NgPKVKh3(Ss~-vC+I`(#KR;0
z>=R+u!fE$7UUeI7Bq}|vNsu#Tx3bfSj5g9$+)gKF)`O?eG4_wuDS-^#xibyRKw(C~
z1F?!|8vWeO=I0(UC2^~72JQz|W_naUzFODUTYlQQvb59?w@Kna7a}JIfJlfVLLd{^
z^KT*`Y|waCa*=1`5h{2tL41KqNKNqN@3JPYwA`m<^4I?PYkhtFi`lD=qjs7FGRe`KGMH-xj}r?GrD!Hd-}>Y6
z*G1h==r+tbNR7e_hHL(p4_z((&pO+})22i)UpP39g^d~Ig?{IBH-J19)sIz4a_
z6giOYcc{ko_E_4u%BLQWR3-?V!yfWi(X8=5F5ZtAc!|XW{IVqjE6b65k7^VJSP2DU
zwk%WMG7t)nK&xp;M9qF=ycFIjZ5S&mQOujwN)o7v^iI-V8gzAYt)n_1Xv|8%?W51@
zv)+HHrX5}YPdVD!UI9xEPawPg6|4;
z;PX3=`n5C5{RGPci1T5~Cv9=;@QF{I4=4_b$9g3BEMy+Sa>zqR-60`lpEUpi?e}uy
z-k`&ciMlWbExO4W}!4_BH1r0T45HxL^`wZ=lOK4~Sm
zPPwM8&px<`Wnxba1okB(L7TktCk1R^A<1`7HfxkS72)27jtWASjAE}zVoHRD4*MMk
zG)TNFzK35EA*VH{-!IWvFpA1)Ibk86)(Nosk4u@F26fSxQIDuu7mgFwrR`kOk)5=O
z;h>Gh896*Eoej)V?3F#cxl`P&j>rc>zT-W1hDU)vzr72Eyztncd6LE!|xoRpvzQEcH~yPVl7*jX>LXb>bM
z5!@?j63ca--oaUk+Pry2siwp8j=f!iWkExsB6!D--nWGA_-oX)OH#9XOTCrIc|i0g
zOioI4;^MAuNPNMajZ!4xC+szD;cX^U>h;yU+w5a^G||+rSSFpn+(fbrv$E0%qEeFA
zx=@7KZ%Kvu#HBw%Bmp?QuJ8ycO_;emtHAqR8r{&YbdFcrZT%j^CewFE8{VA}U#dbX
zY|ZSKV!O4sX-i6(JU5R9XD@%gBx)R>C;^Mh3If6K@io{FLLvORO{Z1g3)d=2F1dee
zeb8s{56WbtnlHnh50_+|3=>r@w>P}t)mzSBhZhbQj~+pD%`fADmZ&1D1-j65>zjbE
zz^S)P-VSX$v2pt8*waGS4>$3UFc%GIRc1WAd|zYaIr$(V6(nNswcIwl&V;48O4qLl
z;xlGBhNW1&%6T%P5b
z&E>863kF9-OdtwZT!5-7$NlpT5-ajt=Zu|Z11OTbWi}(HTN_IuQR)&N?p-1Ee;q4t
zSdY`9h}r&J*Tp80Rw9fOMeoa~x_5IQbzHEbS{(XX&Ikz$-?env4sv-qD3LU@oX`FC
zl{JlQ^y3Wu4Y`?+X4B{eP`h0jJ(yNG%tF7javtw6esb}%`3fU~W
zjvZ*YS+ZC)C?2a;H74`vPT}6ByXs<`ifm%w&|g*%kvarIhm>T8tqs7wUk#7nmq>!5
zD#EeWzDoRKo6xs^Hub}2ds*o4{q)Jb$L1UOCSNnxbG|c}*(SMCe}kB>*h2v$G0w&q
z3+*KvRzlBTVPj;$Cn`b2y&rc!yRtEa{W8wctu7nput?nJsifmetrh-BZemGEB~|n~
zb?!B4r1ymPX^JUml3GrS-hu#W$9Uom=p@Nnk_XDyK05qb&)si`%?r>iTW71jLKIqb
zVw;Y~)Olbil_GJh0K1oIV`1St$F9fn_|rUQPiREd1|(lQe0E7$)tr}d2tfDO#c@Y%cTd^v2>G}@#t0CZ|FCNG@BMJF$|-$YU84uSYgJdj44)5>
zF4L@UGtQ&KK5Awb)8?)zc}RWH_Fmg1i#ht;i@(X_K3_wOdGbAKy5qPcJTU*E*MvQL23>P2r*<21Iw``YrhVbt)1P`Yn;0Nb%=u>e$`g^`CgM
zsO&HscVuVlWP>p|j4TMfhkg&D@-<`W5anX^k#<-Ik}w>huP`h=cJb|xIr&6JyM_h5
zd9~CV!p4@w&mHUnI`#a_T;2S&WQ-?9&o=shM2Vx28`
z1?_cxg*z@%^?bi@aBDjP#Xp-$J8rCJW+#d}V)~+wEa6N(gnQjcQq(uR5OS90nI;$%
zc$Dq;l#J#F+cywA{&n=RA$~sh#;h7t;#N-cSj%1K25TO)7RPzlAv^@ihp22p(zMb(
z!G&+G3|2V`Yy92TjV=mUkL4bGyZPF`b#OE6%Qp~
zQt5T)O1kds&H=0J9ZWqUX?!Ij8bCrEQ#>^`^_ARm6+0jHS@@YVY#&`*Sh+#a)5KgcMhzy2a0U-)y%GAOQ3
zG!Qj&_V~VH`in-m!6!Ooib#Y=Mtf-o~$|zhQBg`ySZ({)t>?A7iJ56UWWDIvg8YIcM
zc6GmYAFb&2CQqSe1K{?6A~F26qs&Nn?g5JFeSs4l`^HJ`e*1@0X}!L(w9TCI8S)mU&F8@F~gb9#}n34(q*
z#6njZt{?glB;?*i`UtWX53EDH(3lqp1|@v0Z<0Wi<3|-$E>x##W3!!M)_&D)VCPbr
z2>dWI?AL3B{sk(TnH+_-cY^l6#s+u^aOjI1-1^4DMd<2A1HnVbdwj6Z34x#;`B*^4
zLWP56YQ8DQ5{M8delI#1`wD;XWFp-?i50RL9kZ5Be`JFz2U3e4yW5HUR&7R;3=tXn
z;wGm?;94%yz>c53*k~*zzvQ)xRLF1`c3$?zAxp3M!TBS~VdrC<5QlS;<-@Ic`SyJ2
z_TOo{_M`Ey*#P@z)@t~G!A^9)q+P5+bw|;2zqWVGVrroHBqz8y4E~5y|ZPiXndJ9Yx>@04Z|>}-!orY+9Jcg
z^qq<_ng3=J|E6cCNE#F%;d_0R8by)!#s#2ULPn7{!9FxyVpJNNasI%t?<_tcp|B
z-4}i2{aK$q`TIWYmvJjzA|S3Rgzq?)+s(0NeTE;SSmdzsx1CUGOXRa7wv06+mIzOx
zWZW3L0=0C~H`U>>iYq##XgSUs>bxFpHV`Q7HO_hwXV;HEe-I;6#KpucSmF}Xt1%)R
z660c2Scr(Fbrk2}mB?o|_(V85jc<*7Nb1W+2h*`-ap~UxL8_JpEB-uSP+nn@K+K$C
zLJGJEOZLz&JqEie0Z#d+F1HuucHuT^@z=yKYg5~Ko(x@g?r$PVqy=gKKmG=W3QhdtU66zu&kD(S7mc=UNO|?28KZ*DVY0WSnY9jy<*E
z%#U&t$9?iO;>|!RDT4!wb78rJx>;>|Dsr^?2^D+EF9^cYtsl!tr&s7310h(7GoW`n
zkiV|rJ3GIt@m1~_P!tueTn#Z8(9)!L3^BGp&*sz{9}JU`uB5Jn=+arf3uk;kXa7-8y{Jro
znDqE>+Owq(MHHsu_cxo;FcdYk0s9@Lw7TdKe7!-0pzn2q_41`IoL%}>i{x(A>qGRV
zC!agX;*yj4PS=BX46aMWb@vuTeV3teP=DvR`NOuJs=}LK9(5LG-3Cp|Iumi!cFRJi
z-{_>itnTD%bs2G|yPj!j%(-Pa3!VV}TEjRNeaUW;lDyU;hh9tCKAs=)IFwvu`lVQVw@
z?32uFZA@mJXp&E
z-VA~riyy8^*|F1@SYv}bUpx?9_;BjC-{;xflJ5V76*bEPlV5~_;u)tz3!J8cqF73F!_v>eZHG#sLl4s50J#(wyte@YE>jV`Quc4i~M?D-++KivaMwa&+vU7@I9B{|nKjE_!Ob!d{85r+^;M{Qm{M}3+NFy{
zQ%*tf06u9kUmKe5KD%T}ggdPY)xo@c#jj25IQk@alqvUZ-+7x+Ov{n1>#m?^@w+Bl
zn-bE&+BZSYSIs-3#eRQYX$uw0We`!Pl`~G%QrMJ)^?t@DY7cqf@Uj*}P)VI-Rb7E{kyigE_ti)>VqFvPlfPbxcrU%RR)g
z^F`&(58BCt$X8FKJ_}TJI%qGOW(5sUmwsU7gasdDX;#jy`B`N<>px-HO}c5YAKPwB
z^j8|EH%!shzf9DtyzJqbGUs+@Zx7RzF21pj9dPeimVp^38kDuC`7=GueVX&cb6hrA
ze9~AUVD|Fsu)SajYQ+9Y*Vfo7i~i-h{&rEl>ksAe=E~4vi>GllnLb1<7jB`IFqMf>
zF5Y^KPQMqrCU@V+JBey09hQ-n2{8)?bzuH|ClH2?E!73v8>rrP6kLAaiWPvDUYKmX8H-|
zB6XVaEWY{pE6$2c72b}=F%mb%0pn)}^%-gJ+L|8<3rOCD(Kx68!3XgBiZ7z*4josAsC)>_S@z^Gzz(h*Ys)=yg-Mx~7!EA7cgrE%W5_21x)
zPt&e^Vo3eM(HfPgTd5IS@S@eb&YDV_6|
zyX))8T)NaTMpPsFuXmO)el9JdGahS5`P`r?|Em3v5$z@BQO4n3n4lrfM((rrCv|1=
z$(+I5rQ3UQYg@gXOP!;IO^Y;KGiM4hD6IA`P33TwVfGtl}
z&cPp#mG;T)`D+eYWmcK2-272iH7!zM4=c#k0o~bq{0$ZrbaRtfn6*Zdi{ukp1AXcK
zS=el%Zn?SLq}6y@-u8pgnuo(Zj2PzHV!MY4LoxNVr(s{iC?!fx^+t%5N>~s7PB2eF2D9k~!pK5F&J3ZESB*c^
zN%1lR=lLI#v&~;C=6ux_HzLE9+sD-q|8R%%dninW4Pr|glBzR5e98VUCe171;}CbX
z5Zw#^Ug`LfycQCRZcXw+Rw3<5)-h6Ssb@>a{1K`GCcVCe64ytomo|WwDGnm?I$?7x
zrv61gUYd~Hhyv^N>57KpssDGsgD#C2R^m9<1c4{o=H=ndACxxQ*3Z+evznGZyA
z<$Nuis~^J_0YN!}dlbJFj6Xe4UQ!FB1!!q{0Xf-4;fENS!Xc2O(5MkZpxt}FsK*pK
zwuCj0s{V3KjyXIUuO1J3FES+QhzVWhg31tq86NZ{aoRP{J8@toU+HHzhB^9E*gB3=
z9DdMiddd%84uHRqJ;FFB|{
zE6c(w&B2n+Yk60Zm|^3JAB-q!L9Kc%cM44IVgx)SXeWsG$9Qn`G|xpi!CMO)WJRX*
z&x-N-vSBCtU4O+bI2Kv*#!vP~!Np=FEXJ!P0WJn#=>0VlGgQj*F$#+e)2c@G4z^iP
zrSW9&hLYjUqgaX~F_$7S?H@`dURr0%A6qKF>|sUBC7VAYWa|}Oj60g!FvetejD8n1
z39Mgi9D7)}Nh-ZGBYNa&I&-ArZX8zoTUtQbKw=5@Ct&S+_&It6V_tzV>RUwKlU)yCgeCpkL}hTUoGOKUpz+Fa9)ct~SO%aniS2^t(qq!GUH
zl`-cr%vo_)mBtZH`Eums)mU+182#37BOk;nz4F=CmQ|Q_5MdH1jvUuhvhgbR)
zIU=7AeIGx&ca}2Vt@fRdxSV2J~SKEdWhCSeFo
z!EH)v2*(DrV1Q4_|418);`Q6oc^BVO+u!*z`aHZ|%m0`9-H~n^0)2Z_pEq;b-A~9E
z0hjiychK`m1EfUB?aYl5BqD<{Nc0*A?CMMb+QJX>^&z$Ye7btD~!jFH&~3phWY(vCvhQkice<7RjuDKHILeNP!$3B
zj4Vt^Nl@f#P6e;RW(tgbs*KR>UC?V1MoH&2WLFK?q_@Qj(}`Mm-H$s~35i|1c)Tep
zIEb@eb)g+{#35@gf=xzG5Tm>`5>ga6v!4(mj2te*v$lYX=u-U2PQZQ|^Pq@el~IQe
zZJ3M)(}$)36teFPrConXM>#twEjEn@9JGpRI{%LN_ce##2cvTa2N~-&Mc+l($lpE?
zRKwy#hf7lTDrD35DV8foMOEBto-*UH+xQ&t1A${N-OpWOp6SY@boKzD?-7YDD7!G
z^bsbZ@V?F<#PeQ?p@j~}L;`q7MZDEg-XTURX1lj9h_
zpSMGoNz`lm=&`yA3l^eY3_C#ayP=ySqS+`$nz^6{*8i3`=v#yuqSHGK@TJ}6{FRvo
zNHQP4uqIeT1_rzJocun4m#%JNm>&zRpTG&E(O4&fS@##ez)|tKB=`K=FEU$hWIC}s%`bKn(BW_lz(SB9A3zF
ztR=e1q_(fH1eGNId5PA*3gs+35X-6_;O)T$u=^*)26%P7ac9;tVk6XmK1mCUp-xIu
z`BRo05WtD(9s+cKpCBaZOnY5n9{<@*3&%I%>nS_3ahTKZFp3iv7q;X}-L3MxXUWvQ
zm^&%{`_XS9S&W;I>H}6`JAy04-j>AFhs3`WIOs&%ZoZrebLRrccM9nHB7)h_w|5zaA
z?q`qMh!{xF=DH!2P5JS)ljNZwwsrAymx7>p18Tex*KNEjtms2Dv@z4t<=VeJEOGK}rh=U=W4|Gq-Rq?fhRG$Eujh{He`k2xhu7qT5a4km;${aV1*gXIOy9`AIw1
z%5T<6W*{S+)y&E=4mZmWxUlvQFL93>f6%rYU9g<&Khz-n*ABu72d)JDEX9}5gvD^w
z`nlkzURbwQm*tkZo>%^-fDsd#85&62=yPI^yX0Xr`~FM~j~dB>@wH}?H2jELZKxfWe7FWt)fIcR}siKY!&x$a>}Cg{CnKGGs6
zU7|eQr8{B(B66rISu&wPWrx`@1*;SzjN!lqdTjO0+JE$vbZf;x$1!R9_tK_U;~E&*
z$C7A!6_;w?H`&c)O4(03d4t;F#Xl-2r+i(0#aPuLND)eLJ@~9aX2J4?TDLca0!`84
zgB=7XvjPoGjfrSdhkWzE9KSE0q)-g2cDh%vxw6?qn`uZ}PkY<#^pA6q&KZN7sQ7dK
zPLn#OotfHg1F8;xb&~--14ZKk0lZt!2w&&c2nxq{b?W{3_e*z6MXzAzn^
zviLxMDI>ChX0P45m^LH>c3I?f_1(RsY*Y3Z)BCXT70;T~z8^YA5H)_z5w
zk>VeV+UwsF13LEi-1`3xfIKib#qb@1sTI4E#ERWZnxwK{g+H5ULK?5;93MG!3Q7>K
zB;^&Q{DzQu*qx90o_`vh=HICDuTFL;Q3
zJDpL{@6TT>c6E(r82od}@ncuMV&X@+{q|Tnot-7xqXyF>>p7{5KjG*i{GC&)=%a?Y
zERn)|7esxD^2foqfDRWaZ6MZ}t8ph%)UwzL*^=BHJj=P)X4)-T`Y8s*8aI_z(b49VE1wbeqF$W5lqt?+dm;}KQ&wGGT(wU4ZmMgAcSY+Vc!M$(9btu~<;im*WieSUgd{fWx5Cx!&_*A_6$8VMhvIvSoK^ar7e#^Jo5#IjqShe%%C~Oooq>8N-GoERsgL%%vK)UpN
zYB&%w>6^zmkOeu(FgEru^>4WOsdyMPs;Js~=dcSgk+kzb%r&Z+1uVQaR
zof?vvDE8u_qvV>KyR@Bk3ntndCVEYZQJYm*sjhJ*j{DBxd_#
z8c3_|Nsi=L(m?M*A1<*P>GabaVJ8O+oqOem(@kdK6)RrwQk>Vw+Y#@R>||RTug0RT
z7VGzlgSVoS5m%i*QZ@($MH|lTsTAFGK7}k~cWpz|H6B|QkR*#Odb$00j#3TRBwb02(+)hUia!83iIe-ztx+Tr
zE+&N?0z*9QjX{UQ5ZM+4)$Q%8p_+G@(_rT{q`X%v=A}tXA*u~Nk0upbLSNYo=1}Vv
z^Z(VUW4;jVrD8~?mo@S1fS1pUakF$}#FYnT(2D^P(=ob0{1_6?)TzM|*Twi?tn-p_
zeRbjM&&=MF&R`Cy=?FX-OC5hV4x3-v(U64(+yZ;yH(!+}=@{|&Y?bI;VfK%K0EJCz
zB1A?!5giIcJ{O~nyMJ-^a!j6RiVEVVh54-?{yu~iuAUnRHML_gl8>iH$@LH0vn>``
zK0ohNlO9KUC0Yjlm2a~YKzO^}cK1WDfXyIUKzo}NFJRa!^=4r^I{r>4+dKPIl$)&W
zvoA!Qb@8=q1p!pS|qZj`VPW+uq{3GP+V;^t_|AL_g1b
zCc+QZ>aeiLCrN3*LT?YlKa7m2l?heJi~N1eA5dMAa1!D>m}E9*%@hMWClfLjvwdqg
zfJZj&@CflB(}e&~1M-9sZSz$KRSMufQ_I-49Qh!8+?sJZ+Es+)#dq$_LY%AyTr|k<
znItNg)|2bZh~C%0GuP|T$i($O$JP0`JJi-h!?|%*3#p6H!qUNNehR%Qvj!#&T
zg_ndYB^%B1Kf%J=(mC$`;xtKm{}Ef4>+EAk(=*cj}>#k>OlHo9OBsKo@kgad5eCo1}!(z*q%`p}!Cl)7dBSg1UFs*~%
zz}eOI+|JjTl^77jVr4%gMPcu9Pn-Mg&axP+Gy;FZg<(SEUovA(vagbmR#A~dIGTJ-R=LY0{RYthH
zHj#<_80G=s_A|AA#+0e!1Wx}F=vVi}BNazsr6KPD%Jt)VPp()Tza8WM{3BrvC%`f3
zlBY*Jj5l24s3WRYct|;E_fn^$nQSQapSy)`?{F9}WY!b(t6jK&7*MZ)l`rLN=N>b&
zH$|K~=U%o7WjFD1GPI=L-A`kKPE#06COA}8GzlOQSWm_0ZBcsaCq&Yl@#L`)c;ts)
zeuKcs3D~nm%N-N4J{(nHu){UK;(0ZD8gQH}27u4TtE_CPcA2^Y=mMbzLdM^2AnnnW
z0la6z3xA?8Yz~1Bd-@r`!+>kghZS}__ydsO!=eJCYR{C_ph>yF$G{*i&>yl20o;Y4
zsp2zUx_)UcUtiLsTua@i=bSwgdOE;NKV4_RbQdawwWea>^G7dIYBwz>eCV7}1XdIv
zi^}scwddVru*K=m4AX9HhdjwNe{mNRD%>PPz=fdhA}K_lLbz_2Hsg!mV5>_qAx;BX
z5XPsth%X?z@PSE0szV1-81!!=`*sUE?c>ox(_ZZ+%2em)tlPWV3<>Mj2RyAU
z^`o*&Hw2Qke%J`m+6*9RU-s%L&idc+fg1w61KzbY=oYW3SD*Y2MC+E|xFr9JQ+5^N
z%d9?-1-v($fLiC*)BH8q=3Sc?PBdrhN&D*34yGdAI#ksz2s)&Ejufv1lMtY$0boKMF2$$%(@6gA
z>g-xRIkjdl-6HPn@@`IbG-Gc*@@*D8k`laHHGw1>lk@_Ti|xPm`uzpHo5On^vr!#9
z6})l=bJGI;v%_dAp3{$lo$khmiMvYlfHU|_V{LPsPczL$muHHLl|eZ&q$h*@2SFc1
zITJjq56P}#V($oz3vMIm2%|%N?Buo;`c{*2!#irz7I+e0d%+)*N*3^$#MCe9_z@E`
zmmfeDOP!-F;PHp7O2&y^Q{{FWo
zBCd<&$}m+=!87o^1UT_i`j5kMcLwQzUR?c&6%c#e!_k*UG$RZp`V8O2{pBi@1x9Mv
zR^Dp67w$Uszk|U&vcqef}%;
zF*`lKO6Sjr+5jM#k(KVfoqOZk3ddqdRIlfWN>|iqj;o>m9ilfs$B<4lF^kN=dVELb
zewq##UKn5kf-mfhxr6A1yWXU*1fwg|ic28D|J9
zF0}U2irKdRPEk1HQ}tCA;lqcSh4<9_`MD{FQenvZ8_QX79p^>hi5W~malnS!KeMc9qv2*2CVJ>`
z{?G^1_JbWQs#^&?9iO+>@Xi|c^Q~gJ*q3+j1dCtwC!*>*{&pJY#)dxY?%;t{
z_o&-0a!2AZ^G@slnT*P=nH2M|TkHVeXMQ0d1Yb^5nt51Mia8N8E5-b>d{g)uAR|J^
zu6Yrin9?rdpKEcd#|L#6$xgwA$Uk`}Py8>E%z`b-hH1n1(%sUrGzik&DIH38i%2&}
zEg>b{y@Y@u0@7X5EnNcA-LTZM-|;@*Zj1(m`r@-Kd#F4Iio9z+W(
zdP7hTGY^0Mabt-3Sr@3ji34
zzj-0xEBd@%{wcD-%h9kP6cTku%e+5o&r^m(_SAgkBDAD*c596WiD(h*kbPaqSku|d
zV`B9aYdPxwP|ddH<8q4YhlJ%{THCkXi;2%;?3mq4d*e=ML&Efz&|98pZ}Xlp{lWR>
z57>es)YnS8%T9xZpiOTi!v=u@j|zlR$g%D&e)D%%GyAmH3dvdWZHF=VnXtgNgzq}=
zw{w-fi$l8IUmfR@sM>4#vkrXUy+Q3#B5=oGh{xgM=7I{H?7$e_l+F&Ir_)zQ21Jbi
z@@5-@>hI-;#QWK^DVf_*U6?oME$(sVpxY-6pDOj+ljI->;~ly_Xed0Xdl&gf
zJz;Ds4MJ5M7vVJxmwqk#U-Z34W?Ig3t1gT7l8c;@3lJr+_PBRh($-FeA*z%e{_jEt
zlDL}tVBMI%27CuU<8Ix&2ncB18@u7|3fK2Gp+y**Tshw7A0Vs2)wjW)Hk6Ef-K4`7
zf+)$ThtYv`|NVLJQO&cz%C4y?>nLAbQO})%g)5Jc!e<@wG%(f=N?t8?Lm1FUsL(yL
z^s9;+gPbg!Y*Tdfv0?&xXN9r?b(oUs36Sr7UWe`SS?bbk^U?*bB!6~>l)6LlAp^NZ
zAWN@{OucWOD7gIEgM{|$S~y?g-}Ky$W)hwqh~ni?BS#LYscm&SV1czz$>D7bp&Jwb
zL7wtQL}^@~!TA{Vu^t?u2Gxd76=+iJuDN}6dO6Ye4@P|ZytVb14eTE0ID{rT;YaRQ
z&cE0IH9@sTr9^jue=1%JH+-)e<0QiNr#oTqN}2!M=rc2HBO8~D&T_)7+Amm{I)#wK
zZBHlIGV%&pCwuMaL{sYcO(eMNM+-j9MSh%(!xv#LPj#C0cb)fLi0@*-c)9knf@g#t
zYVQ(8^Z!6x4_{x&<~!xDf}ZTta-63{}@cT4Ozmu@1IorRAX&}g?nEk
zjqi(kh+QMZLXyc0R^m^UuVMjiZ%kq*=n4Hor}AwZ5JQ6YXi!pc97~ZaLdaLw!i&R)Q~VB`e4R9@u&IioyPiv+%7`R>j~4X6
zCr$9a&M6>on4@{^>-evRw~HCd{cjO{Vmvs?Go)D=(T)uRI%pUpMZy6TfckSd$nr2?
zGV|L^GP$rk85MyAKPalJL@34;-GZCoqnYHikH-37$O?jwONWORCnDQ9Z{*Hgn@U;z
z`Z?(Vb89>eF`awkJ^IG+g119C9Dwa1f%B@9na*kDV2MPnYABAz|xp
zh`8~j=}l8|Nl&i;hy8Yn7XGLCYZlnmc0<)-M=
zr{5OV$?T`tPsn%Y(4rPC{
zZCBq1)CbQ#K2RHZ`RO|yAP?)pA65ZCy>gK{)Z!L|mACE2nrd#u;bW5||I-A1g809F
zD^}Ztm996QYh1>2FTY$n3nqh)}qo
zKSe=e?rKyy9q>g{X~%7~mdm-SQz6k9M{9^7V6LAV(dv+|Tcg1bD=Lhv*$PmjksVhl
zUP=H+Nfu;_GcoqL;uPsWbt>1(#HtY0z%wwo-a)mUo63plRl-B}I0zghJfUMD)R+!s
z5#2tsrt%~2|)sWpnK0ej;28Czgy&{H|P*B
zMPf-Oe#?SuF?c(&EbM@F3T9d7HxPLPpyMS>@FQ50wx#v4DYYBgO>RTDf8hSQp{qm$
z%^q6FYM`qn)%MsVXUxER6;KSSqTo{0$arrlFXL}W)DmoH1KK6+h8`rFerUj-PBnTb
z16ZgduM3jBorsk&~u0*0c>C)%L*>e*hG^rl!
zxlXC@{CLX&YJLLSByzMlbV@+j*~zMmfEQ4|-40Q`M*2Wwq-GO*-!7>UYJG|Ro3`%U(+%=q$kB#S(pX5#}@}ENCqB*%Ok~Q86y2nkPRciT?`fVU!Zou#imvO
zT4UlxVHyqH{Ymwm3>ttUZy>x`&>)^YY!CXQ^OK3mdMzo)0*B)(CcidR<-oRUUo^*H
z`A>NslBx~_{T9G9@Id>u;8Nm*{^&u24Hd>#>@Z6b%`A)O*||PB`8)eJOIbSCN}Apa4(cDd=AB
zZ5p|bCE62+TQNJn)4=Of)H1M}AFNVP=sYeOH51^Xh9^)bma#J`!6^
zHa=Y3$G2M<0|ho{9J%DexV!k4##Q@SfWa?znP&N(Ah9UO8&nl5bn+lb|92?86mDQ0de=Xeq;!bU+gl86(U)Ayb0cTNK%7s1
zfcH~HX+3whH_O9Y`pc`i=bqT3JxBSK(DSgRQxZ)X7?zhU@9JV?H8F1a{W35dUNNVs<_CznnOHwum2JQv^Pz*Z?FzRW
zT-7XZ!p&l%0G&{Oric#7c%JpMUn5w5sku^piAQ3fmd-a=UW}4~K3QP_QIKz_
zGL$`k@jw>AVIPtA38q8COH3aTyWDv#|=-Py|Vm6KbJaw&+|1=&-3woPngYV;~C-^SqX{37X1BXG5g+iAyvL9re18E
zxVEPw`c^OeMjp1I0unAuW
z=r3IVCVfjxxuy-5-F~M7Ql4zLPjA%N>fq)RJ>LX|je$ZW0-2#Xo{i0wTK!5wNLuB`
zR6-r8RN^mj0jlfVvd|Rs7P0`?r;IN%Wm=jfZ;_v`o+vK22Y5Bu_-WRNf6pB50ArsNCTj9&
zH%TA3d{WGmKij+R*`r_EEb~p-tFGBd^uB#7dj5UQlHX4xo0lA=L26E|KqVu&m#|-~
z_upx{qZw_|9Y31!PLJ&e3N0#=V}xR&h~=`|jAefho>C*#Bx?%is4x0FQvNbk&@mJ7
z;-j_8CipcFjMPI95PUAIquJwvIFF|C!Yn??b(jbiM$ya+nL69RswqLo
z7`5DDV;=~-IJl+p$ClstHv=Y@V@d)MOEM(umElRuYc*|+y$Od1b$-)hvC5bY`xiX5
zdAFHrQ$>%jL?|t!R%wR6?kT*?&-eqS0DoFJP+FxJv?nMxN;`tRR+&h~5;68dM5h%i
zwaR>-c{|FWb#%^tNeDVO_^R?KWLUZw@!{1*Ggky84C#I(6;)zW8)%0?Lk6m_iLt)5!Tl(c~9Dr(#p#
zr}qnb#=-?ZIbSi2XR`)Ho-`D8=M49SB}7nt4Kw~?JHAy*_(1)-)n`BNTfM0pZdeaz
z5+k-lf94M%fzVD@(#a&PthB|1Br?DErR-SE_VxPce{^aZfsPsJI253={ttjL$)NY0
zA%-IB70tUt=KK)&XRE&Ir6t~Q8&`@N?;tk|KHW0$3D8d;by{l>saDnDYhVsZ<_YaF
z$-YK7pIlIno?dCz-R3g)2Wfu+o$&)Uh{O
zBDVR}zxLT=OpXmYHKbvL(x-5&4PQg%h!&8#XQZS`I8zfJnkUMpxwR%`YL4uO7GM87
z@iY-{((QsGxLIFHjAf|PEQ0D;QnQ+2`oue28Kn?oCX@lg=@H57#qVuonpbezy^dlg
zb8bS()p@L>Owi<^esVEI6*N%&6ThB0aoCZnRvNxc;D#6>(or46dG$>5KgUu0KsdvF
zZR%Z06jn~6+@vr1XSEDXcD=GOCZdD
z0F$rNV!gF_8&W}V-+6`^nQfUxPhNJ8lOPN1Q~-vJGq_J+k%pAE_?qv8{Eb9TJ<9?p
zFA8PlNgwG8zs4a;Vdgoy3fvdJ=9*?$o5J^sBJ`mPJH8G{gI($Y{Vrzms<;t>$)_y*
z$?2~YUY4YDaWQpGR?}$wEwgLnrfl}LY_l5Oa?>jPG%#&tkq$h89w$smusP?D(y2ae
zin`VX7}MTyAnB59C)hr2cm|<^8Dv29sF@_6-t}A%LPG-%CT_CWJ+pFb#)}Ejvwb-D
zq4V*{iyA0K;t8@U1K)}mD~t?L!%XjbKW9$j^Vs~tTz)Ne!Nd$QB&|iRgHT{uPQfzA
zvwhv=K;M|(Xz0SbzwZuyH9p&bX{bGkE=yOuDA^&7@Zm}u(&X^Jj4b~w-^-l;341vv
z?}x}Ebv!Bl+C&^|4j-I4c2X@7e+!{v>?7o(asRB2M<2JdQ6?D%0LNRl6#3eJe<2@B
ztAHsu1Vi^DXnXcsY%(i-`gi0-QNl?kcGZYnf0ZyMhZj$89nKbiX%j
zq|!aoFoR^MfzLcsM+EmYa`PXUxDVFqPHpcXcsPG`DOi`~sk-H4%#$CRVr0$nt;;D1
zS#<$|15rGPLci!cg}le5c-8(GQV1t#x>g}C-CHLunm7@`!9_>^CNcty`XXyBvm{@G
zA5Tj`&aR5brVLroYn7Gy$&pw)edhslGcy5Y5hU4xXoy7QL4iSds27GQsOoCQIktq$
z-Trjk$X8q2Cbl@aOvtRGDcE%)FR;`;DUYhpzX{DVsO
z@)SPCk8WRn*zHviCTqo7;i##6>GTPKXQId$>9RLWFy7IyP_Q?6yzPVnr?-B2q|8#D
z`9QwLg#kzHlik*qu0d&?
zv$r@*R~l=as$G5+a`cADM};43apGooGpCb_M!<1np46iF3>WIXM
zgd8yumTSlm{j%0z;KQ_BKj(K>=|_K0^h;$vP(iTJbRL_YKZ8KD&X9d|Bc5?JAIkiq
zpKP}*E-%$