diff --git a/po/com.mitchellh.ghostty.pot b/po/com.mitchellh.ghostty.pot index 3892d14d8e..d6a99d01df 100644 --- a/po/com.mitchellh.ghostty.pot +++ b/po/com.mitchellh.ghostty.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2025-04-22 08:57-0700\n" +"POT-Creation-Date: 2025-04-23 16:58+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -50,7 +50,7 @@ msgstr "" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:10 #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97 -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:100 msgid "Reload Configuration" msgstr "" @@ -78,6 +78,10 @@ msgstr "" msgid "Split Right" msgstr "" +#: src/apprt/gtk/ui/1.5/command-palette.blp:16 +msgid "Execute a command…" +msgstr "" + #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 msgid "Copy" @@ -115,7 +119,7 @@ msgstr "" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:62 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30 -#: src/apprt/gtk/Window.zig:248 +#: src/apprt/gtk/Window.zig:255 msgid "New Tab" msgstr "" @@ -143,20 +147,24 @@ msgid "Config" msgstr "" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:92 -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95 msgid "Open Configuration" msgstr "" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 +msgid "Command Palette" +msgstr "" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 msgid "Terminal Inspector" msgstr "" -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:102 -#: src/apprt/gtk/Window.zig:1003 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +#: src/apprt/gtk/Window.zig:1024 msgid "About Ghostty" msgstr "" -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:112 msgid "Quit" msgstr "" @@ -197,31 +205,35 @@ msgid "" "commands may be executed." msgstr "" -#: src/apprt/gtk/Window.zig:201 +#: src/apprt/gtk/Window.zig:208 msgid "Main Menu" msgstr "" -#: src/apprt/gtk/Window.zig:222 +#: src/apprt/gtk/Window.zig:229 msgid "View Open Tabs" msgstr "" -#: src/apprt/gtk/Window.zig:249 +#: src/apprt/gtk/Window.zig:256 msgid "New Split" msgstr "" -#: src/apprt/gtk/Window.zig:312 +#: src/apprt/gtk/Window.zig:319 msgid "" "⚠️ You're running a debug build of Ghostty! Performance will be degraded." msgstr "" -#: src/apprt/gtk/Window.zig:744 +#: src/apprt/gtk/Window.zig:765 msgid "Reloaded the configuration" msgstr "" -#: src/apprt/gtk/Window.zig:984 +#: src/apprt/gtk/Window.zig:1005 msgid "Ghostty Developers" msgstr "" +#: src/apprt/gtk/inspector.zig:144 +msgid "Ghostty: Terminal Inspector" +msgstr "" + #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "" @@ -261,7 +273,3 @@ msgstr "" #: src/apprt/gtk/Surface.zig:1243 msgid "Copied to clipboard" msgstr "" - -#: src/apprt/gtk/inspector.zig:144 -msgid "Ghostty: Terminal Inspector" -msgstr "" diff --git a/po/zh_CN.UTF-8.po b/po/zh_CN.UTF-8.po index 80c3766aa5..ee2c51362e 100644 --- a/po/zh_CN.UTF-8.po +++ b/po/zh_CN.UTF-8.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2025-04-22 08:57-0700\n" +"POT-Creation-Date: 2025-04-23 16:58+0800\n" "PO-Revision-Date: 2025-02-27 09:16+0100\n" "Last-Translator: Leah \n" "Language-Team: Chinese (simplified) \n" @@ -51,7 +51,7 @@ msgstr "忽略" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:10 #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97 -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:100 msgid "Reload Configuration" msgstr "重新加载配置" @@ -79,6 +79,10 @@ msgstr "向左分屏" msgid "Split Right" msgstr "向右分屏" +#: src/apprt/gtk/ui/1.5/command-palette.blp:16 +msgid "Execute a command…" +msgstr "选择要执行的命令……" + #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 msgid "Copy" @@ -116,7 +120,7 @@ msgstr "标签页" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:62 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30 -#: src/apprt/gtk/Window.zig:248 +#: src/apprt/gtk/Window.zig:255 msgid "New Tab" msgstr "新建标签页" @@ -144,20 +148,24 @@ msgid "Config" msgstr "配置" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:92 -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95 msgid "Open Configuration" msgstr "打开配置文件" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 +msgid "Command Palette" +msgstr "命令面板" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 msgid "Terminal Inspector" msgstr "终端调试器" -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:102 -#: src/apprt/gtk/Window.zig:1003 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +#: src/apprt/gtk/Window.zig:1024 msgid "About Ghostty" msgstr "关于 Ghostty" -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:112 msgid "Quit" msgstr "退出" @@ -198,31 +206,35 @@ msgid "" "commands may be executed." msgstr "将以下内容粘贴至终端内将可能执行有害命令。" -#: src/apprt/gtk/Window.zig:201 +#: src/apprt/gtk/Window.zig:208 msgid "Main Menu" msgstr "主菜单" -#: src/apprt/gtk/Window.zig:222 +#: src/apprt/gtk/Window.zig:229 msgid "View Open Tabs" msgstr "浏览标签页" -#: src/apprt/gtk/Window.zig:249 +#: src/apprt/gtk/Window.zig:256 msgid "New Split" -msgstr "" +msgstr "新建分屏" -#: src/apprt/gtk/Window.zig:312 +#: src/apprt/gtk/Window.zig:319 msgid "" "⚠️ You're running a debug build of Ghostty! Performance will be degraded." msgstr "⚠️ Ghostty 正在以调试模式运行!性能将大打折扣。" -#: src/apprt/gtk/Window.zig:744 +#: src/apprt/gtk/Window.zig:765 msgid "Reloaded the configuration" msgstr "已重新加载配置" -#: src/apprt/gtk/Window.zig:984 +#: src/apprt/gtk/Window.zig:1005 msgid "Ghostty Developers" msgstr "Ghostty 开发团队" +#: src/apprt/gtk/inspector.zig:144 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty 终端调试器" + #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "关闭" @@ -262,7 +274,3 @@ msgstr "分屏内正在运行中的进程将被终止。" #: src/apprt/gtk/Surface.zig:1243 msgid "Copied to clipboard" msgstr "已复制至剪贴板" - -#: src/apprt/gtk/inspector.zig:144 -msgid "Ghostty: Terminal Inspector" -msgstr "Ghostty 终端调试器" diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 72c0d75092..a83146e99a 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -485,10 +485,10 @@ pub fn performAction( .toggle_quick_terminal => return try self.toggleQuickTerminal(), .secure_input => self.setSecureInput(target, value), .ring_bell => try self.ringBell(target), + .toggle_command_palette => try self.toggleCommandPalette(target), // Unimplemented .close_all_windows, - .toggle_command_palette, .toggle_visibility, .cell_size, .key_sequence, @@ -742,7 +742,7 @@ fn toggleWindowDecorations( .surface => |v| { const window = v.rt_surface.container.window() orelse { log.info( - "toggleFullscreen invalid for container={s}", + "toggleWindowDecorations invalid for container={s}", .{@tagName(v.rt_surface.container)}, ); return; @@ -784,6 +784,23 @@ fn ringBell(_: *App, target: apprt.Target) !void { } } +fn toggleCommandPalette(_: *App, target: apprt.Target) !void { + switch (target) { + .app => {}, + .surface => |surface| { + const window = surface.rt_surface.container.window() orelse { + log.info( + "toggleCommandPalette invalid for container={s}", + .{@tagName(surface.rt_surface.container)}, + ); + return; + }; + + window.toggleCommandPalette(); + }, + } +} + fn quitTimer(self: *App, mode: apprt.action.QuitTimer) void { switch (mode) { .start => self.startQuitTimer(), @@ -1022,6 +1039,7 @@ fn syncActionAccelerators(self: *App) !void { try self.syncActionAccelerator("app.open-config", .{ .open_config = {} }); try self.syncActionAccelerator("app.reload-config", .{ .reload_config = {} }); try self.syncActionAccelerator("win.toggle-inspector", .{ .inspector = .toggle }); + try self.syncActionAccelerator("win.toggle-command-palette", .toggle_command_palette); try self.syncActionAccelerator("win.close", .{ .close_window = {} }); try self.syncActionAccelerator("win.new-window", .{ .new_window = {} }); try self.syncActionAccelerator("win.new-tab", .{ .new_tab = {} }); diff --git a/src/apprt/gtk/CommandPalette.zig b/src/apprt/gtk/CommandPalette.zig new file mode 100644 index 0000000000..d9052dd0ad --- /dev/null +++ b/src/apprt/gtk/CommandPalette.zig @@ -0,0 +1,238 @@ +const CommandPalette = @This(); + +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const adw = @import("adw"); +const gio = @import("gio"); +const gobject = @import("gobject"); +const gtk = @import("gtk"); + +const configpkg = @import("../../config.zig"); +const inputpkg = @import("../../input.zig"); +const key = @import("key.zig"); +const Builder = @import("Builder.zig"); +const Window = @import("Window.zig"); + +const log = std.log.scoped(.command_palette); + +window: *Window, + +arena: std.heap.ArenaAllocator, + +/// 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 fn init(self: *CommandPalette, window: *Window) !void { + // Register the custom command type *before* initializing the builder + // If we don't do this now, the builder will complain that it doesn't know + // about this type and fail to initialize + _ = Command.getGObjectType(); + + var builder = Builder.init("command-palette", 1, 5, .blp); + + self.* = .{ + .window = window, + .arena = .init(window.app.core_app.alloc), + .dialog = builder.getObject(adw.Dialog, "command-palette").?, + .search = builder.getObject(gtk.SearchEntry, "search").?, + .view = builder.getObject(gtk.ListView, "view").?, + .model = builder.getObject(gtk.SingleSelection, "model").?, + .source = builder.getObject(gio.ListStore, "source").?, + }; + + // Manually take a reference here so that the dialog + // remains in memory after closing + self.dialog.ref(); + errdefer self.dialog.unref(); + + _ = gtk.SearchEntry.signals.stop_search.connect( + self.search, + *CommandPalette, + searchStopped, + self, + .{}, + ); + + _ = gtk.SearchEntry.signals.activate.connect( + self.search, + *CommandPalette, + searchActivated, + self, + .{}, + ); + + _ = gtk.ListView.signals.activate.connect( + self.view, + *CommandPalette, + rowActivated, + self, + .{}, + ); + + try self.updateConfig(&self.window.app.config); +} + +pub fn deinit(self: *CommandPalette) void { + self.arena.deinit(); + self.dialog.unref(); +} + +pub fn toggle(self: *CommandPalette) void { + self.dialog.present(self.window.window.as(gtk.Widget)); +} + +pub fn updateConfig(self: *CommandPalette, config: *const configpkg.Config) !void { + // Clear existing binds and clear allocated data + self.source.removeAll(); + _ = self.arena.reset(.retain_capacity); + + // TODO: Allow user-configured palette entries + for (inputpkg.command.defaults) |command| { + // Filter out actions that are not implemented + // or don't make sense for GTK + switch (command.action) { + .close_all_windows, + .toggle_secure_input, + => continue, + + else => {}, + } + + const cmd = try Command.new( + self.arena.allocator(), + command, + config.keybind.set, + ); + self.source.append(cmd.as(gobject.Object)); + } +} + +fn activated(self: *CommandPalette, pos: c_uint) void { + // Use self.model and not self.source here to use the list of *visible* results + const object = self.model.as(gio.ListModel).getObject(pos) orelse return; + const cmd = gobject.ext.cast(Command, object) orelse return; + + const action = inputpkg.Binding.Action.parse( + std.mem.span(cmd.cmd_c.action_key), + ) catch |err| { + log.err("got invalid action={s} ({})", .{ cmd.cmd_c.action_key, err }); + return; + }; + + self.window.performBindingAction(action); + _ = self.dialog.close(); +} + +fn searchStopped(_: *gtk.SearchEntry, self: *CommandPalette) callconv(.c) void { + // ESC was pressed - close the palette + _ = self.dialog.close(); +} + +fn searchActivated(_: *gtk.SearchEntry, self: *CommandPalette) callconv(.c) void { + // If Enter is pressed, activate the selected entry + self.activated(self.model.getSelected()); +} + +fn rowActivated(_: *gtk.ListView, pos: c_uint, self: *CommandPalette) callconv(.c) void { + self.activated(pos); +} + +/// 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 { + parent: Parent, + cmd_c: inputpkg.Command.C, + + pub const getGObjectType = gobject.ext.defineClass(Command, .{ + .name = "GhosttyCommand", + .classInit = Class.init, + }); + + pub fn new(alloc: Allocator, cmd: inputpkg.Command, keybinds: inputpkg.Binding.Set) !*Command { + const self = gobject.ext.newInstance(Command, .{}); + var buf: [64]u8 = undefined; + + const action = action: { + const trigger = keybinds.getTrigger(cmd.action) orelse break :action null; + const accel = try key.accelFromTrigger(&buf, trigger) orelse break :action null; + break :action try alloc.dupeZ(u8, accel); + }; + + self.cmd_c = .{ + .title = cmd.title.ptr, + .description = cmd.description.ptr, + .action = if (action) |v| v.ptr else "", + .action_key = try std.fmt.allocPrintZ(alloc, "{}", .{cmd.action}), + }; + + return self; + } + + fn as(self: *Command, comptime T: type) *T { + return gobject.ext.as(T, self); + } + + pub const Parent = gobject.Object; + + pub const Class = extern struct { + parent: Parent.Class, + + pub const Instance = Command; + + pub fn init(class: *Class) callconv(.c) void { + const info = @typeInfo(inputpkg.Command.C).@"struct"; + + // Expose all fields on the Command.C struct as properties + // that can be accessed by the GObject type system + // (and by extension, blueprints) + const properties = comptime props: { + var props: [info.fields.len]type = undefined; + + for (info.fields, 0..) |field, i| { + const accessor = struct { + fn getter(cmd: *Command) ?[:0]const u8 { + return std.mem.span(@field(cmd.cmd_c, field.name)); + } + }; + + // "Canonicalize" field names into the format GObject expects + const prop_name = prop_name: { + var buf: [field.name.len:0]u8 = undefined; + _ = std.mem.replace(u8, field.name, "_", "-", &buf); + break :prop_name buf; + }; + + props[i] = gobject.ext.defineProperty( + &prop_name, + Command, + ?[:0]const u8, + .{ + .default = null, + .accessor = .{ .getter = &accessor.getter }, + }, + ); + } + + break :props props; + }; + + gobject.ext.registerProperties(class, &properties); + } + }; +}; diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index 20ac3d9557..3f230525bc 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -34,6 +34,7 @@ const gtk_key = @import("key.zig"); const TabView = @import("TabView.zig"); const HeaderBar = @import("headerbar.zig"); const CloseDialog = @import("CloseDialog.zig"); +const CommandPalette = @import("CommandPalette.zig"); const winprotopkg = @import("winproto.zig"); const gtk_version = @import("gtk_version.zig"); const adw_version = @import("adw_version.zig"); @@ -67,6 +68,9 @@ titlebar_menu: Menu(Window, "titlebar_menu", true), /// The libadwaita widget for receiving toast send requests. toast_overlay: *adw.ToastOverlay, +/// The command palette. +command_palette: CommandPalette, + /// See adwTabOverviewOpen for why we have this. adw_tab_overview_focus_timer: ?c_uint = null, @@ -139,6 +143,7 @@ pub fn init(self: *Window, app: *App) !void { .notebook = undefined, .titlebar_menu = undefined, .toast_overlay = undefined, + .command_palette = undefined, .winproto = .none, }; @@ -167,6 +172,8 @@ pub fn init(self: *Window, app: *App) !void { // Setup our notebook self.notebook.init(self); + if (adw_version.supportsDialogs()) try self.command_palette.init(self); + // If we are using Adwaita, then we can support the tab overview. self.tab_overview = if (adw_version.supportsTabOverview()) overview: { const tab_overview = adw.TabOverview.new(); @@ -460,6 +467,9 @@ pub fn updateConfig( // We always resync our appearance whenever the config changes. try self.syncAppearance(); + + // Update binds inside the command palette + try self.command_palette.updateConfig(config); } /// Updates appearance based on config settings. Will be called once upon window @@ -577,6 +587,7 @@ fn initActions(self: *Window) void { .{ "split-left", gtkActionSplitLeft }, .{ "split-up", gtkActionSplitUp }, .{ "toggle-inspector", gtkActionToggleInspector }, + .{ "toggle-command-palette", gtkActionToggleCommandPalette }, .{ "copy", gtkActionCopy }, .{ "paste", gtkActionPaste }, .{ "reset", gtkActionReset }, @@ -600,6 +611,7 @@ fn initActions(self: *Window) void { pub fn deinit(self: *Window) void { self.winproto.deinit(self.app.core_app.alloc); + if (adw_version.supportsDialogs()) self.command_palette.deinit(); if (self.adw_tab_overview_focus_timer) |timer| { _ = glib.Source.remove(timer); @@ -729,6 +741,15 @@ pub fn toggleWindowDecorations(self: *Window) void { }; } +/// Toggle the window decorations for this window. +pub fn toggleCommandPalette(self: *Window) void { + if (adw_version.supportsDialogs()) { + self.command_palette.toggle(); + } else { + log.warn("libadwaita 1.5+ is required for the command palette", .{}); + } +} + /// Grabs focus on the currently selected tab. pub fn focusCurrentTab(self: *Window) void { const tab = self.notebook.currentTab() orelse return; @@ -820,7 +841,7 @@ fn gtkWindowUpdateScaleFactor( } /// Perform a binding action on the window's action surface. -fn performBindingAction(self: *Window, action: input.Binding.Action) void { +pub fn performBindingAction(self: *Window, action: input.Binding.Action) void { const surface = self.actionSurface() orelse return; _ = surface.performBindingAction(action) catch |err| { log.warn("error performing binding action error={}", .{err}); @@ -1082,6 +1103,14 @@ fn gtkActionToggleInspector( self.performBindingAction(.{ .inspector = .toggle }); } +fn gtkActionToggleCommandPalette( + _: *gio.SimpleAction, + _: ?*glib.Variant, + self: *Window, +) callconv(.C) void { + self.performBindingAction(.toggle_command_palette); +} + fn gtkActionCopy( _: *gio.SimpleAction, _: ?*glib.Variant, diff --git a/src/apprt/gtk/gresource.zig b/src/apprt/gtk/gresource.zig index 7ced9fc45a..5ddb4854ff 100644 --- a/src/apprt/gtk/gresource.zig +++ b/src/apprt/gtk/gresource.zig @@ -76,6 +76,7 @@ pub const blueprint_files = [_]VersionedBlueprint{ .{ .major = 1, .minor = 5, .name = "prompt-title-dialog" }, .{ .major = 1, .minor = 5, .name = "config-errors-dialog" }, .{ .major = 1, .minor = 0, .name = "menu-headerbar-split_menu" }, + .{ .major = 1, .minor = 5, .name = "command-palette" }, .{ .major = 1, .minor = 0, .name = "menu-surface-context_menu" }, .{ .major = 1, .minor = 0, .name = "menu-window-titlebar_menu" }, .{ .major = 1, .minor = 5, .name = "ccw-osc-52-read" }, diff --git a/src/apprt/gtk/style.css b/src/apprt/gtk/style.css index ecaef6b332..7c4b53d03a 100644 --- a/src/apprt/gtk/style.css +++ b/src/apprt/gtk/style.css @@ -73,3 +73,19 @@ window.ssd.no-border-radius { filter: blur(5px); transition: filter 0.3s ease; } + +.command-palette-search { + font-size: 1.25rem; + padding: 4px; + -gtk-icon-size: 20px; +} + +.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/ui/1.0/menu-window-titlebar_menu.blp b/src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp index 71e7d060cd..3273aa81ce 100644 --- a/src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp +++ b/src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp @@ -81,6 +81,11 @@ menu menu { } section { + item { + label: _("Command Palette"); + action: "win.toggle-command-palette"; + } + item { label: _("Terminal Inspector"); action: "win.toggle-inspector"; diff --git a/src/apprt/gtk/ui/1.5/command-palette.blp b/src/apprt/gtk/ui/1.5/command-palette.blp new file mode 100644 index 0000000000..a844820910 --- /dev/null +++ b/src/apprt/gtk/ui/1.5/command-palette.blp @@ -0,0 +1,106 @@ +using Gtk 4.0; +using Gio 2.0; +using Adw 1; + +Adw.Dialog command-palette { + content-width: 700; + + Adw.ToolbarView { + top-bar-style: flat; + + [top] + Adw.HeaderBar { + [title] + SearchEntry search { + hexpand: true; + placeholder-text: _("Execute a command…"); + + styles [ + "command-palette-search", + ] + } + } + + ScrolledWindow { + min-content-height: 300; + + ListView view { + show-separators: true; + single-click-activate: true; + + model: SingleSelection model { + model: FilterListModel { + incremental: true; + + filter: AnyFilter { + StringFilter { + expression: expr item as <$GhosttyCommand>.title; + search: bind search.text; + } + + StringFilter { + expression: expr item as <$GhosttyCommand>.action-key; + search: bind search.text; + } + }; + + model: Gio.ListStore source { + item-type: typeof<$GhosttyCommand>; + }; + }; + }; + + styles [ + "rich-list", + ] + + factory: BuilderListItemFactory { + template ListItem { + child: Box { + orientation: horizontal; + spacing: 10; + tooltip-text: bind template.item as <$GhosttyCommand>.description; + + Box { + orientation: vertical; + hexpand: true; + + Label { + ellipsize: end; + halign: start; + wrap: false; + single-line-mode: true; + + styles [ + "title", + ] + + label: bind template.item as <$GhosttyCommand>.title; + } + + Label { + ellipsize: end; + halign: start; + wrap: false; + single-line-mode: true; + + styles [ + "subtitle", + "monospace", + ] + + label: bind template.item as <$GhosttyCommand>.action-key; + } + } + + ShortcutLabel { + accelerator: bind template.item as <$GhosttyCommand>.action; + valign: center; + } + }; + } + }; + } + } + } +} diff --git a/src/config/Config.zig b/src/config/Config.zig index f71e0972db..0e66111d86 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -4694,6 +4694,13 @@ pub const Keybinds = struct { .{ .toggle_split_zoom = {} }, ); + // Toggle command palette, matches VSCode + try self.set.put( + alloc, + .{ .key = .{ .translated = .p }, .mods = inputpkg.ctrlOrSuper(.{ .shift = true }) }, + .toggle_command_palette, + ); + // Mac-specific keyboard bindings. if (comptime builtin.target.os.tag.isDarwin()) { try self.set.put( @@ -4866,13 +4873,6 @@ pub const Keybinds = struct { .{ .jump_to_prompt = 1 }, ); - // Toggle command palette, matches VSCode - try self.set.put( - alloc, - .{ .key = .{ .translated = .p }, .mods = .{ .super = true, .shift = true } }, - .{ .toggle_command_palette = {} }, - ); - // Inspector, matching Chromium try self.set.put( alloc, diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 1a2961a532..a702ad6ebc 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -445,8 +445,6 @@ pub const Action = union(enum) { /// that lets you see what actions you can perform, their associated /// keybindings (if any), a search bar to filter the actions, and /// the ability to then execute the action. - /// - /// This only works on macOS. toggle_command_palette, /// Toggle the "quick" terminal. The quick terminal is a terminal that