From d7972cb8b7219c0a9a489a8f46abf4939dfb34d4 Mon Sep 17 00:00:00 2001 From: Tommy Brunn Date: Sat, 3 Jan 2026 18:36:43 +0100 Subject: [PATCH 1/6] gtk: Session Search Gtk implementation of #9945. Fixes #9948. This adds session search to the command palette on Gtk, allowing you to jump to any surface by title or working directory. The main difference to the Mac OS implementation is that tabs do not have colors by which to search. --- src/apprt/gtk/class/application.zig | 30 ++ src/apprt/gtk/class/command_palette.zig | 398 +++++++++++++++++++++--- src/apprt/gtk/class/split_tree.zig | 6 + src/apprt/gtk/class/window.zig | 52 ++++ 4 files changed, 440 insertions(+), 46 deletions(-) diff --git a/src/apprt/gtk/class/application.zig b/src/apprt/gtk/class/application.zig index b16bce049..eb83fa8a2 100644 --- a/src/apprt/gtk/class/application.zig +++ b/src/apprt/gtk/class/application.zig @@ -1176,6 +1176,36 @@ pub const Application = extern struct { return self.private().config.ref(); } + /// Collect all surfaces from all windows in the application. + /// The caller must unref each surface and window and deinit the list. + pub fn collectAllSurfaces( + self: *Self, + alloc: Allocator, + ) !std.ArrayList(Window.SurfaceInfo) { + var all_surfaces: std.ArrayList(Window.SurfaceInfo) = .{}; + errdefer { + for (all_surfaces.items) |info| { + info.surface.unref(); + info.window.unref(); + } + all_surfaces.deinit(alloc); + } + + const windows = self.as(gtk.Application).getWindows(); + var it: ?*glib.List = windows; + while (it) |node| : (it = node.f_next) { + const window_widget = @as(*gtk.Window, @ptrCast(@alignCast(node.f_data))); + const window = gobject.ext.cast(Window, window_widget) orelse continue; + + var window_surfaces = try window.collectSurfaces(alloc); + defer window_surfaces.deinit(alloc); + + try all_surfaces.appendSlice(alloc, window_surfaces.items); + } + + return all_surfaces; + } + /// Set the configuration for this application. The reference count /// is increased on the new configuration and the old one is /// unreferenced. diff --git a/src/apprt/gtk/class/command_palette.zig b/src/apprt/gtk/class/command_palette.zig index 6da49115e..b5d4b34d4 100644 --- a/src/apprt/gtk/class/command_palette.zig +++ b/src/apprt/gtk/class/command_palette.zig @@ -13,6 +13,8 @@ const key = @import("../key.zig"); const Common = @import("../class.zig").Common; const Application = @import("application.zig").Application; const Window = @import("window.zig").Window; +const Surface = @import("surface.zig").Surface; +const Tab = @import("tab.zig").Tab; const Config = @import("config.zig").Config; const log = std.log.scoped(.gtk_ghostty_command_palette); @@ -146,34 +148,155 @@ pub const CommandPalette = extern struct { return; }; - const cfg = config.get(); - // Clear existing binds priv.source.removeAll(); + const alloc = Application.default().allocator(); + var commands: std.ArrayList(*Command) = .{}; + defer { + for (commands.items) |cmd| cmd.unref(); + commands.deinit(alloc); + } + + self.collectJumpCommands(config, &commands) catch |err| { + log.warn("failed to collect jump commands: {}", .{err}); + }; + + self.collectRegularCommands(config, &commands, alloc); + + // Sort commands + std.mem.sort(*Command, commands.items, {}, struct { + fn lessThan(_: void, a: *Command, b: *Command) bool { + return compareCommands(a, b); + } + }.lessThan); + + for (commands.items) |cmd| { + const cmd_ref = cmd.as(gobject.Object); + priv.source.append(cmd_ref); + } + } + + /// Collect regular commands from configuration, filtering out unsupported actions. + fn collectRegularCommands( + self: *CommandPalette, + config: *Config, + commands: *std.ArrayList(*Command), + alloc: std.mem.Allocator, + ) void { + _ = self; + const cfg = config.get(); + for (cfg.@"command-palette-entry".value.items) |command| { // Filter out actions that are not implemented or don't make sense // for GTK. - switch (command.action) { - .close_all_windows, - .toggle_secure_input, - .check_for_updates, - .redo, - .undo, - .reset_window_size, - .toggle_window_float_on_top, - => continue, - - else => {}, - } + if (!isActionSupportedOnGtk(command.action)) continue; const cmd = Command.new(config, command); - const cmd_ref = cmd.as(gobject.Object); - priv.source.append(cmd_ref); - cmd_ref.unref(); + commands.append(alloc, cmd) catch |err| { + log.warn("failed to add command to list: {}", .{err}); + cmd.unref(); + continue; + }; } } + /// Check if an action is supported on GTK. + fn isActionSupportedOnGtk(action: input.Binding.Action) bool { + return switch (action) { + .close_all_windows, + .toggle_secure_input, + .check_for_updates, + .redo, + .undo, + .reset_window_size, + .toggle_window_float_on_top, + => false, + + else => true, + }; + } + + /// Collect jump commands for all surfaces across all windows. + fn collectJumpCommands( + self: *CommandPalette, + config: *Config, + commands: *std.ArrayList(*Command), + ) !void { + _ = self; + const app = Application.default(); + const alloc = app.allocator(); + + // Collect all surfaces from all windows + var surfaces = app.collectAllSurfaces(alloc) catch |err| { + log.warn("failed to collect surfaces for jump commands: {}", .{err}); + return; + }; + defer { + for (surfaces.items) |info| { + info.surface.unref(); + info.window.unref(); + } + surfaces.deinit(alloc); + } + + for (surfaces.items) |info| { + const cmd = Command.newJump(config, info.surface, info.window); + errdefer cmd.unref(); + try commands.append(alloc, cmd); + } + } + + /// Compare two commands for sorting. + /// Sorts alphabetically by title (case-insensitive), with colon normalization + /// so "Foo:" sorts before "Foo Bar:". Uses sort_key as tie-breaker. + fn compareCommands(a: *Command, b: *Command) bool { + const a_title = a.propGetTitle() orelse return false; + const b_title = b.propGetTitle() orelse return true; + + // Compare case-insensitively with colon normalization + var i: usize = 0; + var j: usize = 0; + while (i < a_title.len and j < b_title.len) { + // Get characters, replacing ':' with '\t' + const a_char = if (a_title[i] == ':') '\t' else a_title[i]; + const b_char = if (b_title[j] == ':') '\t' else b_title[j]; + + const a_lower = std.ascii.toLower(a_char); + const b_lower = std.ascii.toLower(b_char); + + if (a_lower != b_lower) { + return a_lower < b_lower; + } + + i += 1; + j += 1; + } + + // If one title is a prefix of the other, shorter one comes first + if (a_title.len != b_title.len) { + return a_title.len < b_title.len; + } + + // Titles are equal - use sort_key as tie-breaker if both have one + const a_priv = a.private(); + const b_priv = b.private(); + + const a_sort_key = switch (a_priv.data) { + .regular => 0, + .jump => |*ja| ja.sort_key, + }; + const b_sort_key = switch (b_priv.data) { + .regular => 0, + .jump => |*jb| jb.sort_key, + }; + + if (a_sort_key != 0 and b_sort_key != 0) { + return a_sort_key < b_sort_key; + } + return false; + } + fn close(self: *CommandPalette) void { const priv = self.private(); _ = priv.dialog.close(); @@ -234,6 +357,16 @@ pub const CommandPalette = extern struct { self.close(); const cmd = gobject.ext.cast(Command, object_ orelse return) orelse return; + + // Handle jump commands differently + if (cmd.isJump()) { + const surface = cmd.getJumpSurface() orelse return; + const window = cmd.getJumpWindow() orelse return; + focusSurface(surface, window); + return; + } + + // Regular command - emit trigger signal const action = cmd.getAction() orelse return; // Signal that an an action has been selected. Signals are synchronous @@ -413,22 +546,30 @@ const Command = extern struct { }; pub const Private = struct { - /// The configuration we should use to get keybindings. config: ?*Config = null, - - /// Arena used to manage our allocations. arena: ArenaAllocator, - - /// The command. - command: ?input.Command = null, - - /// Cache the formatted action. - action: ?[:0]const u8 = null, - - /// Cache the formatted action_key. - action_key: ?[:0]const u8 = null, + data: CommandData, pub var offset: c_int = 0; + + pub const CommandData = union(enum) { + regular: RegularData, + jump: JumpData, + }; + + pub const RegularData = struct { + command: input.Command, + action: ?[:0]const u8 = null, + action_key: ?[:0]const u8 = null, + }; + + pub const JumpData = struct { + surface: *Surface, + window: *Window, + title: ?[:0]const u8 = null, + description: ?[:0]const u8 = null, + sort_key: usize, + }; }; pub fn new(config: *Config, command: input.Command) *Self { @@ -437,7 +578,34 @@ const Command = extern struct { }); const priv = self.private(); - priv.command = command.clone(priv.arena.allocator()) catch null; + const cloned = command.clone(priv.arena.allocator()) catch { + self.unref(); + return undefined; + }; + + priv.data = .{ + .regular = .{ + .command = cloned, + }, + }; + + return self; + } + + /// Create a new jump command that focuses a specific surface. + pub fn newJump(config: *Config, surface: *Surface, window: *Window) *Self { + const self = gobject.ext.newInstance(Self, .{ + .config = config, + }); + + const priv = self.private(); + priv.data = .{ + .jump = .{ + .surface = surface.ref(), + .window = window.ref(), + .sort_key = @intFromPtr(surface), + }, + }; return self; } @@ -459,6 +627,14 @@ const Command = extern struct { priv.config = null; } + switch (priv.data) { + .regular => {}, + .jump => |*j| { + j.surface.unref(); + j.window.unref(); + }, + } + gobject.Object.virtual_methods.dispose.call( Class.parent, self.as(Parent), @@ -481,52 +657,116 @@ const Command = extern struct { fn propGetActionKey(self: *Self) ?[:0]const u8 { const priv = self.private(); - if (priv.action_key) |action_key| return action_key; + const regular = switch (priv.data) { + .regular => |*r| r, + .jump => return null, + }; - const command = priv.command orelse return null; + if (regular.action_key) |action_key| return action_key; - priv.action_key = std.fmt.allocPrintSentinel( + regular.action_key = std.fmt.allocPrintSentinel( priv.arena.allocator(), "{f}", - .{command.action}, + .{regular.command.action}, 0, ) catch null; - return priv.action_key; + return regular.action_key; } fn propGetAction(self: *Self) ?[:0]const u8 { const priv = self.private(); - if (priv.action) |action| return action; + const regular = switch (priv.data) { + .regular => |*r| r, + .jump => return null, + }; - const command = priv.command orelse return null; + if (regular.action) |action| return action; const cfg = if (priv.config) |config| config.get() else return null; const keybinds = cfg.keybind.set; const alloc = priv.arena.allocator(); - priv.action = action: { + regular.action = action: { var buf: [64]u8 = undefined; - const trigger = keybinds.getTrigger(command.action) orelse break :action null; + const trigger = keybinds.getTrigger(regular.command.action) orelse break :action null; const accel = (key.accelFromTrigger(&buf, trigger) catch break :action null) orelse break :action null; break :action alloc.dupeZ(u8, accel) catch return null; }; - return priv.action; + return regular.action; } fn propGetTitle(self: *Self) ?[:0]const u8 { const priv = self.private(); - const command = priv.command orelse return null; - return command.title; + + switch (priv.data) { + .regular => |*r| return r.command.title, + .jump => |*j| { + if (j.title) |title| return title; + + const alloc = priv.arena.allocator(); + + var val = gobject.ext.Value.new(?[:0]const u8); + defer val.unset(); + gobject.Object.getProperty( + j.surface.as(gobject.Object), + "title", + &val, + ); + const surface_title = gobject.ext.Value.get(&val, ?[:0]const u8) orelse "Untitled"; + + j.title = std.fmt.allocPrintSentinel( + alloc, + "Focus: {s}", + .{surface_title}, + 0, + ) catch null; + + return j.title; + }, + } } fn propGetDescription(self: *Self) ?[:0]const u8 { const priv = self.private(); - const command = priv.command orelse return null; - return command.description; + + switch (priv.data) { + .regular => |*r| return r.command.description, + .jump => |*j| { + if (j.description) |desc| return desc; + + const alloc = priv.arena.allocator(); + + var title_val = gobject.ext.Value.new(?[:0]const u8); + defer title_val.unset(); + gobject.Object.getProperty( + j.surface.as(gobject.Object), + "title", + &title_val, + ); + const title = gobject.ext.Value.get(&title_val, ?[:0]const u8) orelse "Untitled"; + + var pwd_val = gobject.ext.Value.new(?[:0]const u8); + defer pwd_val.unset(); + gobject.Object.getProperty( + j.surface.as(gobject.Object), + "pwd", + &pwd_val, + ); + const pwd = gobject.ext.Value.get(&pwd_val, ?[:0]const u8); + + if (pwd) |p| { + if (std.mem.indexOf(u8, title, p) == null) { + j.description = alloc.dupeZ(u8, p) catch null; + } + } + + return j.description; + }, + } } //--------------------------------------------------------------- @@ -536,8 +776,34 @@ const Command = extern struct { /// allocated data that will be freed when this object is. pub fn getAction(self: *Self) ?input.Binding.Action { const priv = self.private(); - const command = priv.command orelse return null; - return command.action; + return switch (priv.data) { + .regular => |*r| r.command.action, + .jump => null, + }; + } + + /// Check if this is a jump command. + pub fn isJump(self: *Self) bool { + const priv = self.private(); + return priv.data == .jump; + } + + /// Get the jump surface. + pub fn getJumpSurface(self: *Self) ?*Surface { + const priv = self.private(); + return switch (priv.data) { + .regular => null, + .jump => |*j| j.surface, + }; + } + + /// Get the jump window. + pub fn getJumpWindow(self: *Self) ?*Window { + const priv = self.private(); + return switch (priv.data) { + .regular => null, + .jump => |*j| j.window, + }; } //--------------------------------------------------------------- @@ -567,3 +833,43 @@ const Command = extern struct { } }; }; + +/// Focus a surface in a window, bringing the window to front and switching +/// to the appropriate tab if needed. +fn focusSurface(surface: *Surface, window: *Window) void { + window.as(gtk.Window).present(); + + // Find the tab containing this surface + const tab_view = window.getTabView(); + const n = tab_view.getNPages(); + if (n < 0) return; + + for (0..@intCast(n)) |i| { + const page = tab_view.getNthPage(@intCast(i)); + const child = page.getChild(); + const tab = gobject.ext.cast(Tab, child) orelse continue; + + // Check if this tab contains the surface + const tree = tab.getSurfaceTree() orelse continue; + var it = tree.iterator(); + var found = false; + while (it.next()) |entry| { + if (entry.view == surface) { + found = true; + break; + } + } + + if (found) { + // Switch to this tab + tab_view.setSelectedPage(page); + + // Look up the split tree and update last focused surface + const split_tree = tab.getSplitTree(); + split_tree.setLastFocusedSurface(surface); + + surface.grabFocus(); + break; + } + } +} diff --git a/src/apprt/gtk/class/split_tree.zig b/src/apprt/gtk/class/split_tree.zig index 46b3268d9..e203879ca 100644 --- a/src/apprt/gtk/class/split_tree.zig +++ b/src/apprt/gtk/class/split_tree.zig @@ -478,6 +478,12 @@ pub const SplitTree = extern struct { return surface; } + /// Sets the last focused surface in the tree. This is used to track + /// which surface should be considered "active" for the split tree. + pub fn setLastFocusedSurface(self: *Self, surface: ?*Surface) void { + self.private().last_focused.set(surface); + } + /// Returns whether any of the surfaces in the tree have a parent. /// This is important because we can only rebuild the widget tree /// when every surface has no parent. diff --git a/src/apprt/gtk/class/window.zig b/src/apprt/gtk/class/window.zig index 77fd2eea5..638db87b8 100644 --- a/src/apprt/gtk/class/window.zig +++ b/src/apprt/gtk/class/window.zig @@ -804,6 +804,58 @@ pub const Window = extern struct { return self.private().config; } + /// Information about a surface in a window. + pub const SurfaceInfo = struct { + surface: *Surface, + window: *Window, + }; + + /// Collect all surfaces from all tabs in this window. + /// The caller must unref each surface and window when done. + pub fn collectSurfaces( + self: *Self, + alloc: std.mem.Allocator, + ) !std.ArrayList(SurfaceInfo) { + var surfaces: std.ArrayList(SurfaceInfo) = .{}; + errdefer { + for (surfaces.items) |info| { + info.surface.unref(); + info.window.unref(); + } + surfaces.deinit(alloc); + } + + const priv = self.private(); + const n = priv.tab_view.getNPages(); + if (n < 0) return surfaces; + + for (0..@intCast(n)) |i| { + const page = priv.tab_view.getNthPage(@intCast(i)); + const child = page.getChild(); + const tab = gobject.ext.cast(Tab, child) orelse { + log.warn("unexpected non-Tab child in tab view", .{}); + continue; + }; + + const tree = tab.getSurfaceTree() orelse continue; + + var it = tree.iterator(); + while (it.next()) |entry| { + try surfaces.append(alloc, .{ + .surface = entry.view.ref(), + .window = self.ref(), + }); + } + } + + return surfaces; + } + + /// Get the tab view for this window. + pub fn getTabView(self: *Self) *adw.TabView { + return self.private().tab_view; + } + /// Get the current window decoration value for this window. pub fn getWindowDecoration(self: *Self) configpkg.WindowDecoration { const priv = self.private(); From 8754c53e0e538feb5ea74b9dac274784465f6ba0 Mon Sep 17 00:00:00 2001 From: Tommy Brunn Date: Sat, 3 Jan 2026 22:07:57 +0100 Subject: [PATCH 2/6] gtk: Get jump command title from Surface title --- src/apprt/gtk/class/command_palette.zig | 29 +++---------------------- 1 file changed, 3 insertions(+), 26 deletions(-) diff --git a/src/apprt/gtk/class/command_palette.zig b/src/apprt/gtk/class/command_palette.zig index b5d4b34d4..f022f13a7 100644 --- a/src/apprt/gtk/class/command_palette.zig +++ b/src/apprt/gtk/class/command_palette.zig @@ -708,15 +708,7 @@ const Command = extern struct { if (j.title) |title| return title; const alloc = priv.arena.allocator(); - - var val = gobject.ext.Value.new(?[:0]const u8); - defer val.unset(); - gobject.Object.getProperty( - j.surface.as(gobject.Object), - "title", - &val, - ); - const surface_title = gobject.ext.Value.get(&val, ?[:0]const u8) orelse "Untitled"; + const surface_title = j.surface.getTitle() orelse "Untitled"; j.title = std.fmt.allocPrintSentinel( alloc, @@ -740,23 +732,8 @@ const Command = extern struct { const alloc = priv.arena.allocator(); - var title_val = gobject.ext.Value.new(?[:0]const u8); - defer title_val.unset(); - gobject.Object.getProperty( - j.surface.as(gobject.Object), - "title", - &title_val, - ); - const title = gobject.ext.Value.get(&title_val, ?[:0]const u8) orelse "Untitled"; - - var pwd_val = gobject.ext.Value.new(?[:0]const u8); - defer pwd_val.unset(); - gobject.Object.getProperty( - j.surface.as(gobject.Object), - "pwd", - &pwd_val, - ); - const pwd = gobject.ext.Value.get(&pwd_val, ?[:0]const u8); + const title = j.surface.getTitle() orelse "Untitled"; + const pwd = j.surface.getPwd(); if (pwd) |p| { if (std.mem.indexOf(u8, title, p) == null) { From d3aa68413974c6e5b932d52fdf04372c6e494c8b Mon Sep 17 00:00:00 2001 From: Tommy Brunn Date: Sat, 3 Jan 2026 22:15:23 +0100 Subject: [PATCH 3/6] gtk: Remove window reference from jump commands Removes redundant implementations that is already present in the core application to work with surfaces. --- src/apprt/gtk/class/application.zig | 30 -------- src/apprt/gtk/class/command_palette.zig | 93 +++++-------------------- src/apprt/gtk/class/split_tree.zig | 6 -- src/apprt/gtk/class/window.zig | 47 ------------- 4 files changed, 16 insertions(+), 160 deletions(-) diff --git a/src/apprt/gtk/class/application.zig b/src/apprt/gtk/class/application.zig index eb83fa8a2..b16bce049 100644 --- a/src/apprt/gtk/class/application.zig +++ b/src/apprt/gtk/class/application.zig @@ -1176,36 +1176,6 @@ pub const Application = extern struct { return self.private().config.ref(); } - /// Collect all surfaces from all windows in the application. - /// The caller must unref each surface and window and deinit the list. - pub fn collectAllSurfaces( - self: *Self, - alloc: Allocator, - ) !std.ArrayList(Window.SurfaceInfo) { - var all_surfaces: std.ArrayList(Window.SurfaceInfo) = .{}; - errdefer { - for (all_surfaces.items) |info| { - info.surface.unref(); - info.window.unref(); - } - all_surfaces.deinit(alloc); - } - - const windows = self.as(gtk.Application).getWindows(); - var it: ?*glib.List = windows; - while (it) |node| : (it = node.f_next) { - const window_widget = @as(*gtk.Window, @ptrCast(@alignCast(node.f_data))); - const window = gobject.ext.cast(Window, window_widget) orelse continue; - - var window_surfaces = try window.collectSurfaces(alloc); - defer window_surfaces.deinit(alloc); - - try all_surfaces.appendSlice(alloc, window_surfaces.items); - } - - return all_surfaces; - } - /// Set the configuration for this application. The reference count /// is increased on the new configuration and the old one is /// unreferenced. diff --git a/src/apprt/gtk/class/command_palette.zig b/src/apprt/gtk/class/command_palette.zig index f022f13a7..5e942e4a3 100644 --- a/src/apprt/gtk/class/command_palette.zig +++ b/src/apprt/gtk/class/command_palette.zig @@ -192,10 +192,14 @@ pub const CommandPalette = extern struct { // for GTK. if (!isActionSupportedOnGtk(command.action)) continue; - const cmd = Command.new(config, command); + const cmd = Command.new(config, command) catch |err| { + log.warn("failed to create command: {}", .{err}); + continue; + }; + errdefer cmd.unref(); + commands.append(alloc, cmd) catch |err| { log.warn("failed to add command to list: {}", .{err}); - cmd.unref(); continue; }; } @@ -227,21 +231,11 @@ pub const CommandPalette = extern struct { const app = Application.default(); const alloc = app.allocator(); - // Collect all surfaces from all windows - var surfaces = app.collectAllSurfaces(alloc) catch |err| { - log.warn("failed to collect surfaces for jump commands: {}", .{err}); - return; - }; - defer { - for (surfaces.items) |info| { - info.surface.unref(); - info.window.unref(); - } - surfaces.deinit(alloc); - } - - for (surfaces.items) |info| { - const cmd = Command.newJump(config, info.surface, info.window); + // Get all surfaces from the core app + const core_app = app.core(); + for (core_app.surfaces.items) |apprt_surface| { + const surface = apprt_surface.gobj(); + const cmd = Command.newJump(config, surface); errdefer cmd.unref(); try commands.append(alloc, cmd); } @@ -361,8 +355,7 @@ pub const CommandPalette = extern struct { // Handle jump commands differently if (cmd.isJump()) { const surface = cmd.getJumpSurface() orelse return; - const window = cmd.getJumpWindow() orelse return; - focusSurface(surface, window); + surface.present(); return; } @@ -565,23 +558,20 @@ const Command = extern struct { pub const JumpData = struct { surface: *Surface, - window: *Window, title: ?[:0]const u8 = null, description: ?[:0]const u8 = null, sort_key: usize, }; }; - pub fn new(config: *Config, command: input.Command) *Self { + pub fn new(config: *Config, command: input.Command) Allocator.Error!*Self { const self = gobject.ext.newInstance(Self, .{ .config = config, }); + errdefer self.unref(); const priv = self.private(); - const cloned = command.clone(priv.arena.allocator()) catch { - self.unref(); - return undefined; - }; + const cloned = try command.clone(priv.arena.allocator()); priv.data = .{ .regular = .{ @@ -593,7 +583,7 @@ const Command = extern struct { } /// Create a new jump command that focuses a specific surface. - pub fn newJump(config: *Config, surface: *Surface, window: *Window) *Self { + pub fn newJump(config: *Config, surface: *Surface) *Self { const self = gobject.ext.newInstance(Self, .{ .config = config, }); @@ -602,7 +592,6 @@ const Command = extern struct { priv.data = .{ .jump = .{ .surface = surface.ref(), - .window = window.ref(), .sort_key = @intFromPtr(surface), }, }; @@ -631,7 +620,6 @@ const Command = extern struct { .regular => {}, .jump => |*j| { j.surface.unref(); - j.window.unref(); }, } @@ -774,15 +762,6 @@ const Command = extern struct { }; } - /// Get the jump window. - pub fn getJumpWindow(self: *Self) ?*Window { - const priv = self.private(); - return switch (priv.data) { - .regular => null, - .jump => |*j| j.window, - }; - } - //--------------------------------------------------------------- const C = Common(Self, Private); @@ -810,43 +789,3 @@ const Command = extern struct { } }; }; - -/// Focus a surface in a window, bringing the window to front and switching -/// to the appropriate tab if needed. -fn focusSurface(surface: *Surface, window: *Window) void { - window.as(gtk.Window).present(); - - // Find the tab containing this surface - const tab_view = window.getTabView(); - const n = tab_view.getNPages(); - if (n < 0) return; - - for (0..@intCast(n)) |i| { - const page = tab_view.getNthPage(@intCast(i)); - const child = page.getChild(); - const tab = gobject.ext.cast(Tab, child) orelse continue; - - // Check if this tab contains the surface - const tree = tab.getSurfaceTree() orelse continue; - var it = tree.iterator(); - var found = false; - while (it.next()) |entry| { - if (entry.view == surface) { - found = true; - break; - } - } - - if (found) { - // Switch to this tab - tab_view.setSelectedPage(page); - - // Look up the split tree and update last focused surface - const split_tree = tab.getSplitTree(); - split_tree.setLastFocusedSurface(surface); - - surface.grabFocus(); - break; - } - } -} diff --git a/src/apprt/gtk/class/split_tree.zig b/src/apprt/gtk/class/split_tree.zig index e203879ca..46b3268d9 100644 --- a/src/apprt/gtk/class/split_tree.zig +++ b/src/apprt/gtk/class/split_tree.zig @@ -478,12 +478,6 @@ pub const SplitTree = extern struct { return surface; } - /// Sets the last focused surface in the tree. This is used to track - /// which surface should be considered "active" for the split tree. - pub fn setLastFocusedSurface(self: *Self, surface: ?*Surface) void { - self.private().last_focused.set(surface); - } - /// Returns whether any of the surfaces in the tree have a parent. /// This is important because we can only rebuild the widget tree /// when every surface has no parent. diff --git a/src/apprt/gtk/class/window.zig b/src/apprt/gtk/class/window.zig index 638db87b8..8df250447 100644 --- a/src/apprt/gtk/class/window.zig +++ b/src/apprt/gtk/class/window.zig @@ -804,53 +804,6 @@ pub const Window = extern struct { return self.private().config; } - /// Information about a surface in a window. - pub const SurfaceInfo = struct { - surface: *Surface, - window: *Window, - }; - - /// Collect all surfaces from all tabs in this window. - /// The caller must unref each surface and window when done. - pub fn collectSurfaces( - self: *Self, - alloc: std.mem.Allocator, - ) !std.ArrayList(SurfaceInfo) { - var surfaces: std.ArrayList(SurfaceInfo) = .{}; - errdefer { - for (surfaces.items) |info| { - info.surface.unref(); - info.window.unref(); - } - surfaces.deinit(alloc); - } - - const priv = self.private(); - const n = priv.tab_view.getNPages(); - if (n < 0) return surfaces; - - for (0..@intCast(n)) |i| { - const page = priv.tab_view.getNthPage(@intCast(i)); - const child = page.getChild(); - const tab = gobject.ext.cast(Tab, child) orelse { - log.warn("unexpected non-Tab child in tab view", .{}); - continue; - }; - - const tree = tab.getSurfaceTree() orelse continue; - - var it = tree.iterator(); - while (it.next()) |entry| { - try surfaces.append(alloc, .{ - .surface = entry.view.ref(), - .window = self.ref(), - }); - } - } - - return surfaces; - } - /// Get the tab view for this window. pub fn getTabView(self: *Self) *adw.TabView { return self.private().tab_view; From f5d7108c51240724e71ed85de06ee64d8cba014f Mon Sep 17 00:00:00 2001 From: Tommy Brunn Date: Sun, 4 Jan 2026 08:36:40 +0100 Subject: [PATCH 4/6] gtk: Remove strong reference to surface from command palette --- src/apprt/gtk/class/command_palette.zig | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/apprt/gtk/class/command_palette.zig b/src/apprt/gtk/class/command_palette.zig index 5e942e4a3..af1ef8e5e 100644 --- a/src/apprt/gtk/class/command_palette.zig +++ b/src/apprt/gtk/class/command_palette.zig @@ -10,6 +10,7 @@ const gtk = @import("gtk"); const input = @import("../../../input.zig"); const gresource = @import("../build/gresource.zig"); const key = @import("../key.zig"); +const WeakRef = @import("../weak_ref.zig").WeakRef; const Common = @import("../class.zig").Common; const Application = @import("application.zig").Application; const Window = @import("window.zig").Window; @@ -355,6 +356,7 @@ pub const CommandPalette = extern struct { // Handle jump commands differently if (cmd.isJump()) { const surface = cmd.getJumpSurface() orelse return; + defer surface.unref(); surface.present(); return; } @@ -557,7 +559,7 @@ const Command = extern struct { }; pub const JumpData = struct { - surface: *Surface, + surface: WeakRef(Surface) = .empty, title: ?[:0]const u8 = null, description: ?[:0]const u8 = null, sort_key: usize, @@ -591,10 +593,10 @@ const Command = extern struct { const priv = self.private(); priv.data = .{ .jump = .{ - .surface = surface.ref(), .sort_key = @intFromPtr(surface), }, }; + priv.data.jump.surface.set(surface); return self; } @@ -619,7 +621,7 @@ const Command = extern struct { switch (priv.data) { .regular => {}, .jump => |*j| { - j.surface.unref(); + j.surface.set(null); }, } @@ -695,8 +697,11 @@ const Command = extern struct { .jump => |*j| { if (j.title) |title| return title; + const surface = j.surface.get() orelse return null; + defer surface.unref(); + const alloc = priv.arena.allocator(); - const surface_title = j.surface.getTitle() orelse "Untitled"; + const surface_title = surface.getTitle() orelse "Untitled"; j.title = std.fmt.allocPrintSentinel( alloc, @@ -718,10 +723,13 @@ const Command = extern struct { .jump => |*j| { if (j.description) |desc| return desc; + const surface = j.surface.get() orelse return null; + defer surface.unref(); + const alloc = priv.arena.allocator(); - const title = j.surface.getTitle() orelse "Untitled"; - const pwd = j.surface.getPwd(); + const title = surface.getTitle() orelse "Untitled"; + const pwd = surface.getPwd(); if (pwd) |p| { if (std.mem.indexOf(u8, title, p) == null) { @@ -753,12 +761,13 @@ const Command = extern struct { return priv.data == .jump; } - /// Get the jump surface. + /// Get the jump surface. Returns a strong reference that the caller + /// must unref when done, or null if the surface has been destroyed. pub fn getJumpSurface(self: *Self) ?*Surface { const priv = self.private(); return switch (priv.data) { .regular => null, - .jump => |*j| j.surface, + .jump => |*j| j.surface.get(), }; } From f2b5a9192a85c78b94fff03869fce147856a9d41 Mon Sep 17 00:00:00 2001 From: Tommy Brunn Date: Tue, 20 Jan 2026 17:05:14 +0100 Subject: [PATCH 5/6] gtk: Clean up title sorting --- src/apprt/gtk/class/command_palette.zig | 27 ++++++++----------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/src/apprt/gtk/class/command_palette.zig b/src/apprt/gtk/class/command_palette.zig index af1ef8e5e..ea214bf6d 100644 --- a/src/apprt/gtk/class/command_palette.zig +++ b/src/apprt/gtk/class/command_palette.zig @@ -250,12 +250,10 @@ pub const CommandPalette = extern struct { const b_title = b.propGetTitle() orelse return true; // Compare case-insensitively with colon normalization - var i: usize = 0; - var j: usize = 0; - while (i < a_title.len and j < b_title.len) { + for (0..@min(a_title.len, b_title.len)) |i| { // Get characters, replacing ':' with '\t' const a_char = if (a_title[i] == ':') '\t' else a_title[i]; - const b_char = if (b_title[j] == ':') '\t' else b_title[j]; + const b_char = if (b_title[i] == ':') '\t' else b_title[i]; const a_lower = std.ascii.toLower(a_char); const b_lower = std.ascii.toLower(b_char); @@ -263,9 +261,6 @@ pub const CommandPalette = extern struct { if (a_lower != b_lower) { return a_lower < b_lower; } - - i += 1; - j += 1; } // If one title is a prefix of the other, shorter one comes first @@ -273,23 +268,17 @@ pub const CommandPalette = extern struct { return a_title.len < b_title.len; } - // Titles are equal - use sort_key as tie-breaker if both have one - const a_priv = a.private(); - const b_priv = b.private(); - - const a_sort_key = switch (a_priv.data) { - .regular => 0, + // Titles are equal - use sort_key as tie-breaker if both are jump commands + const a_sort_key = switch (a.private().data) { + .regular => return false, .jump => |*ja| ja.sort_key, }; - const b_sort_key = switch (b_priv.data) { - .regular => 0, + const b_sort_key = switch (b.private().data) { + .regular => return false, .jump => |*jb| jb.sort_key, }; - if (a_sort_key != 0 and b_sort_key != 0) { - return a_sort_key < b_sort_key; - } - return false; + return a_sort_key < b_sort_key; } fn close(self: *CommandPalette) void { From 0c8b51c7ab95633a0c5c5f6e6397136736960d18 Mon Sep 17 00:00:00 2001 From: Tommy Brunn Date: Tue, 20 Jan 2026 17:09:15 +0100 Subject: [PATCH 6/6] gtk: Add todo for replacing jump sort key with surface id Using a pointer for this is a bit icky. Once Ghostty adds unique ids to surfaces, we can sort by that id instead. This can potentially also be used to navigate to the surface instead of having the command palette reference the surfaces directly. --- src/apprt/gtk/class/command_palette.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/src/apprt/gtk/class/command_palette.zig b/src/apprt/gtk/class/command_palette.zig index ea214bf6d..0d91c43b2 100644 --- a/src/apprt/gtk/class/command_palette.zig +++ b/src/apprt/gtk/class/command_palette.zig @@ -582,6 +582,7 @@ const Command = extern struct { const priv = self.private(); priv.data = .{ .jump = .{ + // TODO: Replace with surface id whenever Ghostty adds one .sort_key = @intFromPtr(surface), }, };