From cf77897388c24ee29933e6e65912c477c51cc1d3 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Tue, 5 Aug 2025 10:33:08 -0500 Subject: [PATCH 1/2] gtk-ng: port the command palette --- src/apprt/gtk-ng/build/gresource.zig | 1 + src/apprt/gtk-ng/class.zig | 6 + src/apprt/gtk-ng/class/application.zig | 11 +- src/apprt/gtk-ng/class/command_palette.zig | 575 ++++++++++++++++++++ src/apprt/gtk-ng/class/surface.zig | 24 + src/apprt/gtk-ng/class/window.zig | 78 +++ src/apprt/gtk-ng/css/style.css | 13 + src/apprt/gtk-ng/key.zig | 2 +- src/apprt/gtk-ng/ui/1.5/command-palette.blp | 109 ++++ valgrind.supp | 20 + 10 files changed, 837 insertions(+), 2 deletions(-) create mode 100644 src/apprt/gtk-ng/class/command_palette.zig create mode 100644 src/apprt/gtk-ng/ui/1.5/command-palette.blp diff --git a/src/apprt/gtk-ng/build/gresource.zig b/src/apprt/gtk-ng/build/gresource.zig index 2c835a172..f5b91ce48 100644 --- a/src/apprt/gtk-ng/build/gresource.zig +++ b/src/apprt/gtk-ng/build/gresource.zig @@ -44,6 +44,7 @@ pub const blueprints: []const Blueprint = &.{ .{ .major = 1, .minor = 3, .name = "surface-child-exited" }, .{ .major = 1, .minor = 5, .name = "tab" }, .{ .major = 1, .minor = 5, .name = "window" }, + .{ .major = 1, .minor = 5, .name = "command-palette" }, }; /// CSS files in css_path diff --git a/src/apprt/gtk-ng/class.zig b/src/apprt/gtk-ng/class.zig index dc024c5cf..170df1acb 100644 --- a/src/apprt/gtk-ng/class.zig +++ b/src/apprt/gtk-ng/class.zig @@ -29,6 +29,12 @@ pub fn Common( return @ptrCast(@alignCast(gobject.Object.ref(self.as(gobject.Object)))); } + /// If the reference count is 1 and the object is floating, clear the + /// floating attribute. Otherwise, increase the reference count by 1. + pub fn refSink(self: *Self) *Self { + return @ptrCast(@alignCast(gobject.Object.refSink(self.as(gobject.Object)))); + } + /// Decrease the reference count of the object. pub fn unref(self: *Self) void { gobject.Object.unref(self.as(gobject.Object)); diff --git a/src/apprt/gtk-ng/class/application.zig b/src/apprt/gtk-ng/class/application.zig index d653d3e99..91359ee7c 100644 --- a/src/apprt/gtk-ng/class/application.zig +++ b/src/apprt/gtk-ng/class/application.zig @@ -607,10 +607,10 @@ pub const Application = extern struct { .toggle_quick_terminal => return Action.toggleQuickTerminal(self), .toggle_tab_overview => return Action.toggleTabOverview(target), .toggle_window_decorations => return Action.toggleWindowDecorations(target), + .toggle_command_palette => return Action.toggleCommandPalette(target), // Unimplemented but todo on gtk-ng branch .prompt_title, - .toggle_command_palette, .inspector, // TODO: splits .new_split, @@ -2111,6 +2111,15 @@ const Action = struct { }, } } + + pub fn toggleCommandPalette(target: apprt.Target) bool { + switch (target) { + .app => return false, + .surface => |surface| { + return surface.rt_surface.gobj().toggleCommandPalette(); + }, + } + } }; /// This sets various GTK-related environment variables as necessary diff --git a/src/apprt/gtk-ng/class/command_palette.zig b/src/apprt/gtk-ng/class/command_palette.zig new file mode 100644 index 000000000..5b274cad9 --- /dev/null +++ b/src/apprt/gtk-ng/class/command_palette.zig @@ -0,0 +1,575 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const ArenaAllocator = std.heap.ArenaAllocator; + +const adw = @import("adw"); +const gio = @import("gio"); +const gobject = @import("gobject"); +const gtk = @import("gtk"); + +const input = @import("../../../input.zig"); +const gresource = @import("../build/gresource.zig"); +const key = @import("../key.zig"); +const Common = @import("../class.zig").Common; +const Application = @import("application.zig").Application; +const Window = @import("window.zig").Window; +const Config = @import("config.zig").Config; + +const log = std.log.scoped(.gtk_ghostty_command_palette); + +pub const CommandPalette = extern struct { + const Self = @This(); + parent_instance: Parent, + pub const Parent = adw.Bin; + pub const getGObjectType = gobject.ext.defineClass(Self, .{ + .name = "GhosttyCommandPalette", + .instanceInit = &init, + .classInit = &Class.init, + .parent_class = &Class.parent, + .private = .{ .Type = Private, .offset = &Private.offset }, + }); + + pub const properties = struct { + pub const config = struct { + pub const name = "config"; + const impl = gobject.ext.defineProperty( + name, + Self, + ?*Config, + .{ + .nick = "Config", + .blurb = "The configuration that this command palette is using.", + .accessor = C.privateObjFieldAccessor("config"), + }, + ); + }; + }; + + pub const signals = struct { + /// Emitted when a command from the command palette is activated. The + /// action contains pointers to allocated data so if a receiver of this + /// signal needs to keep the action around it will need to clone the + /// action or there may be use-after-free errors. + pub const tigger = struct { + pub const name = "trigger"; + pub const connect = impl.connect; + const impl = gobject.ext.defineSignal( + name, + Self, + &.{*const input.Binding.Action}, + void, + ); + }; + }; + + const Private = struct { + /// The configuration that this command palette is using. + config: ?*Config = null, + + /// The dialog object containing the palette UI. + dialog: *adw.Dialog, + + /// The search input text field. + search: *gtk.SearchEntry, + + /// The view containing each result row. + view: *gtk.ListView, + + /// The model that provides filtered data for the view to display. + model: *gtk.SingleSelection, + + /// The list that serves as the data source of the model. + /// This is where all command data is ultimately stored. + source: *gio.ListStore, + + pub var offset: c_int = 0; + }; + + /// Create a new instance of the command palette. The caller will own a + /// reference to the object. + pub fn new() *Self { + const self = gobject.ext.newInstance(Self, .{}); + + // Sink ourselves so that we aren't floating anymore. We'll unref + // ourselves when the palette is closed or an action is activated. + _ = self.refSink(); + + // Bump the ref so that the caller has a reference. + return self.ref(); + } + + //--------------------------------------------------------------- + // Virtual Methods + + fn init(self: *Self, _: *Class) callconv(.c) void { + gtk.Widget.initTemplate(self.as(gtk.Widget)); + + // Listen for any changes to our config. + _ = gobject.Object.signals.notify.connect( + self, + ?*anyopaque, + propConfig, + null, + .{ + .detail = "config", + }, + ); + } + + fn dispose(self: *Self) callconv(.c) void { + const priv = self.private(); + + priv.source.removeAll(); + + if (priv.config) |config| { + config.unref(); + priv.config = null; + } + + gtk.Widget.disposeTemplate( + self.as(gtk.Widget), + getGObjectType(), + ); + + gobject.Object.virtual_methods.dispose.call( + Class.parent, + self.as(Parent), + ); + } + + //--------------------------------------------------------------- + // Signal Handlers + + fn propConfig(self: *CommandPalette, _: *gobject.ParamSpec, _: ?*anyopaque) callconv(.c) void { + const priv = self.private(); + + const config = priv.config orelse { + log.warn("command palette does not have a config!", .{}); + return; + }; + + const cfg = config.get(); + + // Clear existing binds + priv.source.removeAll(); + + 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 => {}, + } + + const cmd = Command.new(config, command); + const cmd_ref = cmd.as(gobject.Object); + priv.source.append(cmd_ref); + cmd_ref.unref(); + } + } + + fn searchStopped(_: *gtk.SearchEntry, self: *CommandPalette) callconv(.c) void { + // ESC was pressed - close the palette + const priv = self.private(); + _ = priv.dialog.close(); + self.unref(); + } + + fn searchActivated(_: *gtk.SearchEntry, self: *CommandPalette) callconv(.c) void { + // If Enter is pressed, activate the selected entry + const priv = self.private(); + self.activated(priv.model.getSelected()); + } + + fn rowActivated(_: *gtk.ListView, pos: c_uint, self: *CommandPalette) callconv(.c) void { + self.activated(pos); + } + + //--------------------------------------------------------------- + + /// Show or hide the command palette dialog. If the dialog is shown it will + /// be modal over the given window. + pub fn toggle(self: *CommandPalette, window: *Window) void { + const priv = self.private(); + + // If the dialog has been shown, close it and unref ourselves so all of + // our memory is reclaimed. + if (priv.dialog.as(gtk.Widget).getRealized() != 0) { + _ = priv.dialog.close(); + self.unref(); + return; + } + + // Show the dialog + priv.dialog.present(window.as(gtk.Widget)); + + // Focus on the search bar when opening the dialog + _ = priv.search.as(gtk.Widget).grabFocus(); + } + + /// Helper function to send a signal containing the action that should be + /// performed. + fn activated(self: *CommandPalette, pos: c_uint) void { + const priv = self.private(); + + // Close before running the action in order to avoid being replaced by + // another dialog (such as the change title dialog). If that occurs then + // the command palette dialog won't be counted as having closed properly + // and cannot receive focus when reopened. + _ = priv.dialog.close(); + + // We are always done with the command palette when this finishes, even + // if there were errors. + defer self.unref(); + + // Use priv.model and not priv.source here to use the list of *visible* results + const object = priv.model.as(gio.ListModel).getObject(pos) orelse return; + defer object.unref(); + + const cmd = gobject.ext.cast(Command, object) orelse return; + + const action = cmd.getAction() orelse return; + + // Signal that an an action has been selected. Signals are synchronous + // so we shouldn't need to worry about cloning the action. + signals.tigger.impl.emit( + self, + null, + .{&action}, + null, + ); + } + + const C = Common(Self, Private); + pub const as = C.as; + pub const ref = C.ref; + pub const refSink = C.refSink; + pub const unref = C.unref; + const private = C.private; + + pub const Class = extern struct { + parent_class: Parent.Class, + var parent: *Parent.Class = undefined; + pub const Instance = Self; + + fn init(class: *Class) callconv(.c) void { + gobject.ext.ensureType(Command); + gtk.Widget.Class.setTemplateFromResource( + class.as(gtk.Widget.Class), + comptime gresource.blueprint(.{ + .major = 1, + .minor = 5, + .name = "command-palette", + }), + ); + + // Bindings + class.bindTemplateChildPrivate("dialog", .{}); + class.bindTemplateChildPrivate("search", .{}); + class.bindTemplateChildPrivate("view", .{}); + class.bindTemplateChildPrivate("model", .{}); + class.bindTemplateChildPrivate("source", .{}); + + // Template Callbacks + class.bindTemplateCallback("notify_config", &propConfig); + class.bindTemplateCallback("search_stopped", &searchStopped); + class.bindTemplateCallback("search_activated", &searchActivated); + class.bindTemplateCallback("row_activated", &rowActivated); + + // Properties + gobject.ext.registerProperties(class, &.{ + properties.config.impl, + }); + + // Signals + signals.tigger.impl.register(.{}); + + // Virtual methods + gobject.Object.virtual_methods.dispose.implement(class, &dispose); + } + + pub const as = C.Class.as; + pub const bindTemplateChildPrivate = C.Class.bindTemplateChildPrivate; + pub const bindTemplateCallback = C.Class.bindTemplateCallback; + }; +}; + +/// Object that wraps around a command. +/// +/// As GTK list models only accept objects that are within the GObject hierarchy, +/// we have to construct a wrapper to be easily consumed by the list model. +const Command = extern struct { + pub const Self = @This(); + pub const Parent = gobject.Object; + parent: Parent, + + pub const getGObjectType = gobject.ext.defineClass(Self, .{ + .name = "GhosttyCommand", + .instanceInit = &init, + .classInit = Class.init, + .parent_class = &Class.parent, + .private = .{ .Type = Private, .offset = &Private.offset }, + }); + + const properties = struct { + pub const config = struct { + pub const name = "config"; + const impl = gobject.ext.defineProperty( + name, + Self, + ?*Config, + .{ + .nick = "Config", + .blurb = "The configuration that this command palette is using.", + .accessor = C.privateObjFieldAccessor("config"), + }, + ); + }; + + pub const action_key = struct { + pub const name = "action-key"; + const impl = gobject.ext.defineProperty( + name, + Self, + ?[:0]const u8, + .{ + .nick = "Action Key", + .default = null, + .accessor = gobject.ext.typedAccessor( + Self, + ?[:0]const u8, + .{ + .getter = propGetActionKey, + .getter_transfer = .none, + }, + ), + }, + ); + }; + + pub const action = struct { + pub const name = "action"; + const impl = gobject.ext.defineProperty( + name, + Self, + ?[:0]const u8, + .{ + .nick = "Action", + .default = null, + .accessor = gobject.ext.typedAccessor( + Self, + ?[:0]const u8, + .{ + .getter = propGetAction, + .getter_transfer = .none, + }, + ), + }, + ); + }; + + pub const title = struct { + pub const name = "title"; + const impl = gobject.ext.defineProperty( + name, + Self, + ?[:0]const u8, + .{ + .nick = "Title", + .default = null, + .accessor = gobject.ext.typedAccessor( + Self, + ?[:0]const u8, + .{ + .getter = propGetTitle, + .getter_transfer = .none, + }, + ), + }, + ); + }; + + pub const description = struct { + pub const name = "description"; + const impl = gobject.ext.defineProperty( + name, + Self, + ?[:0]const u8, + .{ + .nick = "Description", + .default = null, + .accessor = gobject.ext.typedAccessor( + Self, + ?[:0]const u8, + .{ + .getter = propGetDescription, + .getter_transfer = .none, + }, + ), + }, + ); + }; + }; + + 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, + + pub var offset: c_int = 0; + }; + + pub fn new(config: *Config, command: input.Command) *Self { + const self = gobject.ext.newInstance(Self, .{ + .config = config, + }); + + const priv = self.private(); + priv.command = command.clone(priv.arena.allocator()) catch null; + + return self; + } + + fn init(self: *Self, _: *Class) callconv(.c) void { + // NOTE: we do not watch for changes to the config here as the command + // palette will destroy and recreate this object if/when the config + // changes. + + const priv = self.private(); + priv.arena = .init(Application.default().allocator()); + } + + fn dispose(self: *Self) callconv(.c) void { + const priv = self.private(); + + if (priv.config) |config| { + config.unref(); + priv.config = null; + } + + gobject.Object.virtual_methods.dispose.call( + Class.parent, + self.as(Parent), + ); + } + + fn finalize(self: *Self) callconv(.c) void { + const priv = self.private(); + + priv.arena.deinit(); + + gobject.Object.virtual_methods.finalize.call( + Class.parent, + self.as(Parent), + ); + } + + //--------------------------------------------------------------- + + fn propGetActionKey(self: *Self) ?[:0]const u8 { + const priv = self.private(); + + if (priv.action_key) |action_key| return action_key; + + const command = priv.command orelse return null; + + priv.action_key = std.fmt.allocPrintZ( + priv.arena.allocator(), + "{}", + .{command.action}, + ) catch null; + + return priv.action_key; + } + + fn propGetAction(self: *Self) ?[:0]const u8 { + const priv = self.private(); + + if (priv.action) |action| return action; + + const command = priv.command orelse return null; + + const cfg = if (priv.config) |config| config.get() else return null; + const keybinds = cfg.keybind.set; + + const alloc = priv.arena.allocator(); + + priv.action = action: { + var buf: [64]u8 = undefined; + const trigger = keybinds.getTrigger(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; + } + + fn propGetTitle(self: *Self) ?[:0]const u8 { + const priv = self.private(); + const command = priv.command orelse return null; + return command.title; + } + + fn propGetDescription(self: *Self) ?[:0]const u8 { + const priv = self.private(); + const command = priv.command orelse return null; + return command.description; + } + + //--------------------------------------------------------------- + + /// Return a copy of the action. Callers must ensure that they do not use + /// the action beyond the lifetime of this object because it has internally + /// 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; + } + + //--------------------------------------------------------------- + + const C = Common(Self, Private); + pub const as = C.as; + pub const ref = C.ref; + pub const unref = C.unref; + const private = C.private; + + pub const Class = extern struct { + parent_class: Parent.Class, + var parent: *Parent.Class = undefined; + pub const Instance = Self; + + fn init(class: *Class) callconv(.c) void { + gobject.ext.registerProperties(class, &.{ + properties.config.impl, + properties.action_key.impl, + properties.action.impl, + properties.title.impl, + properties.description.impl, + }); + + gobject.Object.virtual_methods.dispose.implement(class, &dispose); + gobject.Object.virtual_methods.finalize.implement(class, &finalize); + } + }; +}; diff --git a/src/apprt/gtk-ng/class/surface.zig b/src/apprt/gtk-ng/class/surface.zig index ecddb6e79..4223a4653 100644 --- a/src/apprt/gtk-ng/class/surface.zig +++ b/src/apprt/gtk-ng/class/surface.zig @@ -377,6 +377,19 @@ pub const Surface = extern struct { void, ); }; + + /// Emitted when this surface requests that the command palette be + /// toggled. + pub const @"toggle-command-palette" = struct { + pub const name = "toggle-command-palette"; + pub const connect = impl.connect; + const impl = gobject.ext.defineSignal( + name, + Self, + &.{}, + void, + ); + }; }; const Private = struct { @@ -566,6 +579,16 @@ pub const Surface = extern struct { ); } + pub fn toggleCommandPalette(self: *Self) bool { + signals.@"toggle-command-palette".impl.emit( + self, + null, + .{}, + null, + ); + return true; + } + /// Set the current progress report state. pub fn setProgressReport( self: *Self, @@ -2362,6 +2385,7 @@ pub const Surface = extern struct { signals.@"present-request".impl.register(.{}); signals.@"toggle-fullscreen".impl.register(.{}); signals.@"toggle-maximize".impl.register(.{}); + signals.@"toggle-command-palette".impl.register(.{}); // Virtual methods gobject.Object.virtual_methods.dispose.implement(class, &dispose); diff --git a/src/apprt/gtk-ng/class/window.zig b/src/apprt/gtk-ng/class/window.zig index 7e2348785..403701f58 100644 --- a/src/apprt/gtk-ng/class/window.zig +++ b/src/apprt/gtk-ng/class/window.zig @@ -25,6 +25,7 @@ const CloseConfirmationDialog = @import("close_confirmation_dialog.zig").CloseCo const Surface = @import("surface.zig").Surface; const Tab = @import("tab.zig").Tab; const DebugWarning = @import("debug_warning.zig").DebugWarning; +const CommandPalette = @import("command_palette.zig").CommandPalette; const log = std.log.scoped(.gtk_ghostty_window); @@ -244,6 +245,9 @@ pub const Window = extern struct { /// See tabOverviewOpen for why we have this. tab_overview_focus_timer: ?c_uint = null, + /// A weak reference to a command palette. + command_palette: gobject.WeakRef = std.mem.zeroes(gobject.WeakRef), + // Template bindings tab_overview: *adw.TabOverview, tab_bar: *adw.TabBar, @@ -332,6 +336,7 @@ pub const Window = extern struct { .{ "paste", actionPaste, null }, .{ "reset", actionReset, null }, .{ "clear", actionClear, null }, + .{ "toggle-command-palette", actionToggleCommandPalette, null }, }; const action_map = self.as(gio.ActionMap); @@ -1208,6 +1213,13 @@ pub const Window = extern struct { self, .{}, ); + _ = Surface.signals.@"toggle-command-palette".connect( + surface, + *Self, + surfaceToggleCommandPalette, + self, + .{}, + ); // If we've never had a surface initialize yet, then we register // this signal. Its theoretically possible to launch multiple surfaces @@ -1418,6 +1430,15 @@ pub const Window = extern struct { // We react to the changes in the propMaximized callback } + /// React to a signal from a surface requesting that the command palette + /// be toggled. + fn surfaceToggleCommandPalette( + _: *Surface, + self: *Self, + ) callconv(.c) void { + self.toggleCommandPalette(); + } + fn surfaceInit( surface: *Surface, self: *Self, @@ -1551,6 +1572,63 @@ pub const Window = extern struct { self.performBindingAction(.clear_screen); } + /// Toggle the command palette. + fn toggleCommandPalette(self: *Window) void { + const priv = self.private(); + // Get a reference to a command palette. First check the weak reference + // that we save to see if we already have stored. If we don't then + // create a new one. + const command_palette = gobject.ext.cast(CommandPalette, priv.command_palette.get()) orelse command_palette: { + // Create a fresh command palette. + const command_palette = CommandPalette.new(); + + // Synchronize our config to the command palette's config. + _ = gobject.Object.bindProperty( + self.as(gobject.Object), + "config", + command_palette.as(gobject.Object), + "config", + .{ .sync_create = true }, + ); + + // Listen to the activate signal to know if the user selected an option in + // the command palette. + _ = CommandPalette.signals.tigger.connect( + command_palette, + *Window, + signalCommandPaletteTrigger, + self, + .{}, + ); + + break :command_palette command_palette; + }; + defer command_palette.unref(); + + // Save a weak reference to the command palette. We use a weak reference to avoid + // reference counting cycles that might cause problems later. + priv.command_palette.set(command_palette.as(gobject.Object)); + + // Tell the command palette to toggle itself. If the dialog gets + // presented (instead of hidden) it will be modal over our window. + command_palette.toggle(self); + } + + // React to a signal from a command palette asking an action to be performed. + fn signalCommandPaletteTrigger(_: *CommandPalette, action: *const input.Binding.Action, self: *Self) callconv(.c) void { + // If the activation actually has an action, perform it. + self.performBindingAction(action.*); + } + + /// React to a GTK action requesting that the command palette be toggled. + fn actionToggleCommandPalette( + _: *gio.SimpleAction, + _: ?*glib.Variant, + self: *Window, + ) callconv(.c) void { + self.toggleCommandPalette(); + } + const C = Common(Self, Private); pub const as = C.as; pub const ref = C.ref; diff --git a/src/apprt/gtk-ng/css/style.css b/src/apprt/gtk-ng/css/style.css index 1e3e09d9f..970c91b03 100644 --- a/src/apprt/gtk-ng/css/style.css +++ b/src/apprt/gtk-ng/css/style.css @@ -101,3 +101,16 @@ label.resize-overlay { /* after GTK 4.16 is a requirement, switch to the following: */ /* background-color: color-mix(in srgb, var(--error-bg-color), transparent); */ } + +/* + * Command Palette + */ +.command-palette-search > image:first-child { + margin-left: 8px; + margin-right: 4px; +} + +.command-palette-search > image:last-child { + margin-left: 4px; + margin-right: 8px; +} diff --git a/src/apprt/gtk-ng/key.zig b/src/apprt/gtk-ng/key.zig index 344d7de43..a00b0312e 100644 --- a/src/apprt/gtk-ng/key.zig +++ b/src/apprt/gtk-ng/key.zig @@ -60,7 +60,7 @@ pub fn xdgShortcutFromTrigger( return slice[0 .. slice.len - 1 :0]; } -fn writeTriggerKey(writer: anytype, trigger: input.Binding.Trigger) !bool { +fn writeTriggerKey(writer: anytype, trigger: input.Binding.Trigger) error{NoSpaceLeft}!bool { switch (trigger.key) { .physical => |k| { const keyval = keyvalFromKey(k) orelse return false; diff --git a/src/apprt/gtk-ng/ui/1.5/command-palette.blp b/src/apprt/gtk-ng/ui/1.5/command-palette.blp new file mode 100644 index 000000000..0ccae1f0a --- /dev/null +++ b/src/apprt/gtk-ng/ui/1.5/command-palette.blp @@ -0,0 +1,109 @@ +using Gtk 4.0; +using Gio 2.0; +using Adw 1; + +Adw.Dialog dialog { + content-width: 700; + + Adw.ToolbarView { + top-bar-style: flat; + + [top] + Adw.HeaderBar { + [title] + Gtk.SearchEntry search { + hexpand: true; + placeholder-text: _("Execute a command…"); + stop-search => $search_stopped(); + activate => $search_activated(); + + styles [ + "command-palette-search", + ] + } + } + + Gtk.ScrolledWindow { + min-content-height: 300; + + Gtk.ListView view { + show-separators: true; + single-click-activate: true; + activate => $row_activated(); + + model: Gtk.SingleSelection model { + model: Gtk.FilterListModel { + incremental: true; + + filter: Gtk.AnyFilter { + Gtk.StringFilter { + expression: expr item as <$GhosttyCommand>.title; + search: bind search.text; + } + + Gtk.StringFilter { + expression: expr item as <$GhosttyCommand>.action-key; + search: bind search.text; + } + }; + + model: Gio.ListStore source { + item-type: typeof<$GhosttyCommand>; + }; + }; + }; + + styles [ + "rich-list", + ] + + factory: Gtk.BuilderListItemFactory { + template Gtk.ListItem { + child: Gtk.Box { + orientation: horizontal; + spacing: 10; + tooltip-text: bind template.item as <$GhosttyCommand>.description; + + Gtk.Box { + orientation: vertical; + hexpand: true; + + Gtk.Label { + ellipsize: end; + halign: start; + wrap: false; + single-line-mode: true; + + styles [ + "title", + ] + + label: bind template.item as <$GhosttyCommand>.title; + } + + Gtk.Label { + ellipsize: end; + halign: start; + wrap: false; + single-line-mode: true; + + styles [ + "subtitle", + "monospace", + ] + + label: bind template.item as <$GhosttyCommand>.action-key; + } + } + + Gtk.ShortcutLabel { + accelerator: bind template.item as <$GhosttyCommand>.action; + valign: center; + } + }; + } + }; + } + } + } +} diff --git a/valgrind.supp b/valgrind.supp index 3535ecc45..2a0d2b9fa 100644 --- a/valgrind.supp +++ b/valgrind.supp @@ -840,6 +840,26 @@ fun:FcConfigSubstituteWithPat } +{ + FcConfigValues + Memcheck:Leak + match-leak-kinds: possible + fun:malloc + obj:/usr/lib*/libfontconfig.so* + obj:/usr/lib*/libfontconfig.so* + fun:FcConfigValues +} + +{ + FcValueSave + Memcheck:Leak + match-leak-kinds: possible + fun:malloc + obj:/usr/lib*/libfontconfig.so* + obj:/usr/lib*/libfontconfig.so* + fun:FcValueSave +} + # Pixman { pixman_image_composite32 From 19fde96d30fe49eb8b29e1efa31fb97780df79c9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 5 Aug 2025 09:38:08 -0700 Subject: [PATCH 2/2] funny typos --- src/apprt/gtk-ng/class/command_palette.zig | 6 +++--- src/apprt/gtk-ng/class/window.zig | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/apprt/gtk-ng/class/command_palette.zig b/src/apprt/gtk-ng/class/command_palette.zig index 5b274cad9..2dc3e1a74 100644 --- a/src/apprt/gtk-ng/class/command_palette.zig +++ b/src/apprt/gtk-ng/class/command_palette.zig @@ -50,7 +50,7 @@ pub const CommandPalette = extern struct { /// action contains pointers to allocated data so if a receiver of this /// signal needs to keep the action around it will need to clone the /// action or there may be use-after-free errors. - pub const tigger = struct { + pub const trigger = struct { pub const name = "trigger"; pub const connect = impl.connect; const impl = gobject.ext.defineSignal( @@ -240,7 +240,7 @@ pub const CommandPalette = extern struct { // Signal that an an action has been selected. Signals are synchronous // so we shouldn't need to worry about cloning the action. - signals.tigger.impl.emit( + signals.trigger.impl.emit( self, null, .{&action}, @@ -290,7 +290,7 @@ pub const CommandPalette = extern struct { }); // Signals - signals.tigger.impl.register(.{}); + signals.trigger.impl.register(.{}); // Virtual methods gobject.Object.virtual_methods.dispose.implement(class, &dispose); diff --git a/src/apprt/gtk-ng/class/window.zig b/src/apprt/gtk-ng/class/window.zig index 403701f58..7c036e2f7 100644 --- a/src/apprt/gtk-ng/class/window.zig +++ b/src/apprt/gtk-ng/class/window.zig @@ -1593,7 +1593,7 @@ pub const Window = extern struct { // Listen to the activate signal to know if the user selected an option in // the command palette. - _ = CommandPalette.signals.tigger.connect( + _ = CommandPalette.signals.trigger.connect( command_palette, *Window, signalCommandPaletteTrigger,