mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
gtk: implement command palette
This commit is contained in:
@ -599,6 +599,7 @@ typedef enum {
|
|||||||
GHOSTTY_ACTION_COLOR_CHANGE,
|
GHOSTTY_ACTION_COLOR_CHANGE,
|
||||||
GHOSTTY_ACTION_RELOAD_CONFIG,
|
GHOSTTY_ACTION_RELOAD_CONFIG,
|
||||||
GHOSTTY_ACTION_CONFIG_CHANGE,
|
GHOSTTY_ACTION_CONFIG_CHANGE,
|
||||||
|
GHOSTTY_ACTION_TOGGLE_COMMAND_PALETTE,
|
||||||
} ghostty_action_tag_e;
|
} ghostty_action_tag_e;
|
||||||
|
|
||||||
typedef union {
|
typedef union {
|
||||||
|
@ -4224,6 +4224,12 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
|||||||
{},
|
{},
|
||||||
),
|
),
|
||||||
|
|
||||||
|
.toggle_command_palette => return try self.rt_app.performAction(
|
||||||
|
.{ .surface = self },
|
||||||
|
.toggle_command_palette,
|
||||||
|
{},
|
||||||
|
),
|
||||||
|
|
||||||
.toggle_secure_input => return try self.rt_app.performAction(
|
.toggle_secure_input => return try self.rt_app.performAction(
|
||||||
.{ .surface = self },
|
.{ .surface = self },
|
||||||
.secure_input,
|
.secure_input,
|
||||||
|
@ -230,6 +230,9 @@ pub const Action = union(Key) {
|
|||||||
/// for changes.
|
/// for changes.
|
||||||
config_change: ConfigChange,
|
config_change: ConfigChange,
|
||||||
|
|
||||||
|
/// Toggle the command palette
|
||||||
|
toggle_command_palette,
|
||||||
|
|
||||||
/// Sync with: ghostty_action_tag_e
|
/// Sync with: ghostty_action_tag_e
|
||||||
pub const Key = enum(c_int) {
|
pub const Key = enum(c_int) {
|
||||||
quit,
|
quit,
|
||||||
@ -271,6 +274,7 @@ pub const Action = union(Key) {
|
|||||||
color_change,
|
color_change,
|
||||||
reload_config,
|
reload_config,
|
||||||
config_change,
|
config_change,
|
||||||
|
toggle_command_palette,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Sync with: ghostty_action_u
|
/// Sync with: ghostty_action_u
|
||||||
|
@ -222,6 +222,7 @@ pub const App = struct {
|
|||||||
.close_tab,
|
.close_tab,
|
||||||
.toggle_tab_overview,
|
.toggle_tab_overview,
|
||||||
.toggle_window_decorations,
|
.toggle_window_decorations,
|
||||||
|
.toggle_command_palette,
|
||||||
.toggle_quick_terminal,
|
.toggle_quick_terminal,
|
||||||
.toggle_visibility,
|
.toggle_visibility,
|
||||||
.goto_tab,
|
.goto_tab,
|
||||||
|
@ -481,6 +481,7 @@ pub fn performAction(
|
|||||||
.toggle_tab_overview => self.toggleTabOverview(target),
|
.toggle_tab_overview => self.toggleTabOverview(target),
|
||||||
.toggle_split_zoom => self.toggleSplitZoom(target),
|
.toggle_split_zoom => self.toggleSplitZoom(target),
|
||||||
.toggle_window_decorations => self.toggleWindowDecorations(target),
|
.toggle_window_decorations => self.toggleWindowDecorations(target),
|
||||||
|
.toggle_command_palette => return try self.toggleCommandPalette(target),
|
||||||
.quit_timer => self.quitTimer(value),
|
.quit_timer => self.quitTimer(value),
|
||||||
|
|
||||||
// Unimplemented
|
// Unimplemented
|
||||||
@ -739,7 +740,7 @@ fn toggleWindowDecorations(
|
|||||||
.surface => |v| {
|
.surface => |v| {
|
||||||
const window = v.rt_surface.container.window() orelse {
|
const window = v.rt_surface.container.window() orelse {
|
||||||
log.info(
|
log.info(
|
||||||
"toggleFullscreen invalid for container={s}",
|
"toggleWindowDecorations invalid for container={s}",
|
||||||
.{@tagName(v.rt_surface.container)},
|
.{@tagName(v.rt_surface.container)},
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
@ -750,6 +751,27 @@ fn toggleWindowDecorations(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn toggleCommandPalette(
|
||||||
|
_: *App,
|
||||||
|
target: apprt.Target,
|
||||||
|
) !bool {
|
||||||
|
switch (target) {
|
||||||
|
.app => return false,
|
||||||
|
.surface => |v| {
|
||||||
|
const window = v.rt_surface.container.window() orelse {
|
||||||
|
log.info(
|
||||||
|
"toggleCommandPalette invalid for container={s}",
|
||||||
|
.{@tagName(v.rt_surface.container)},
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
window.toggleCommandPalette();
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn quitTimer(self: *App, mode: apprt.action.QuitTimer) void {
|
fn quitTimer(self: *App, mode: apprt.action.QuitTimer) void {
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
.start => self.startQuitTimer(),
|
.start => self.startQuitTimer(),
|
||||||
|
@ -24,6 +24,7 @@ const adwaita = @import("adwaita.zig");
|
|||||||
const gtk_key = @import("key.zig");
|
const gtk_key = @import("key.zig");
|
||||||
const TabView = @import("TabView.zig");
|
const TabView = @import("TabView.zig");
|
||||||
const HeaderBar = @import("headerbar.zig");
|
const HeaderBar = @import("headerbar.zig");
|
||||||
|
const CommandPalette = @import("command_palette.zig").CommandPalette;
|
||||||
const version = @import("version.zig");
|
const version = @import("version.zig");
|
||||||
const winproto = @import("winproto.zig");
|
const winproto = @import("winproto.zig");
|
||||||
|
|
||||||
@ -49,6 +50,8 @@ context_menu: *c.GtkWidget,
|
|||||||
/// The libadwaita widget for receiving toast send requests.
|
/// The libadwaita widget for receiving toast send requests.
|
||||||
toast_overlay: *c.GtkWidget,
|
toast_overlay: *c.GtkWidget,
|
||||||
|
|
||||||
|
command_palette: ?*CommandPalette = null,
|
||||||
|
|
||||||
/// See adwTabOverviewOpen for why we have this.
|
/// See adwTabOverviewOpen for why we have this.
|
||||||
adw_tab_overview_focus_timer: ?c.guint = null,
|
adw_tab_overview_focus_timer: ?c.guint = null,
|
||||||
|
|
||||||
@ -226,6 +229,15 @@ pub fn init(self: *Window, app: *App) !void {
|
|||||||
);
|
);
|
||||||
c.gtk_box_append(@ptrCast(box), self.toast_overlay);
|
c.gtk_box_append(@ptrCast(box), self.toast_overlay);
|
||||||
|
|
||||||
|
if (adwaita.versionAtLeast(1, 5, 0)) {
|
||||||
|
const command_palette = try CommandPalette.new(self);
|
||||||
|
// We manually reference the command palette here
|
||||||
|
// to prevent it from being destroyed when the dialog closes
|
||||||
|
command_palette.ref();
|
||||||
|
self.command_palette = command_palette;
|
||||||
|
}
|
||||||
|
errdefer if (self.command_palette) |palette| palette.unref();
|
||||||
|
|
||||||
// If we have a tab overview then we can set it on our notebook.
|
// If we have a tab overview then we can set it on our notebook.
|
||||||
if (self.tab_overview) |tab_overview| {
|
if (self.tab_overview) |tab_overview| {
|
||||||
if (!adwaita.versionAtLeast(1, 4, 0)) unreachable;
|
if (!adwaita.versionAtLeast(1, 4, 0)) unreachable;
|
||||||
@ -427,6 +439,7 @@ pub fn deinit(self: *Window) void {
|
|||||||
c.gtk_widget_unparent(@ptrCast(self.context_menu));
|
c.gtk_widget_unparent(@ptrCast(self.context_menu));
|
||||||
|
|
||||||
self.winproto.deinit(self.app.core_app.alloc);
|
self.winproto.deinit(self.app.core_app.alloc);
|
||||||
|
if (self.command_palette) |palette| palette.unref();
|
||||||
|
|
||||||
if (self.adw_tab_overview_focus_timer) |timer| {
|
if (self.adw_tab_overview_focus_timer) |timer| {
|
||||||
_ = c.g_source_remove(timer);
|
_ = c.g_source_remove(timer);
|
||||||
@ -544,6 +557,11 @@ pub fn toggleWindowDecorations(self: *Window) void {
|
|||||||
self.updateConfig(&self.app.config) catch {};
|
self.updateConfig(&self.app.config) catch {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Toggle the command palette for this window.
|
||||||
|
pub fn toggleCommandPalette(self: *Window) void {
|
||||||
|
if (self.command_palette) |palette| palette.present();
|
||||||
|
}
|
||||||
|
|
||||||
/// Grabs focus on the currently selected tab.
|
/// Grabs focus on the currently selected tab.
|
||||||
pub fn focusCurrentTab(self: *Window) void {
|
pub fn focusCurrentTab(self: *Window) void {
|
||||||
const tab = self.notebook.currentTab() orelse return;
|
const tab = self.notebook.currentTab() orelse return;
|
||||||
@ -1018,7 +1036,7 @@ fn gtkActionReset(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the surface to use for an action.
|
/// Returns the surface to use for an action.
|
||||||
fn actionSurface(self: *Window) ?*CoreSurface {
|
pub fn actionSurface(self: *Window) ?*CoreSurface {
|
||||||
const tab = self.notebook.currentTab() orelse return null;
|
const tab = self.notebook.currentTab() orelse return null;
|
||||||
const surface = tab.focus_child orelse return null;
|
const surface = tab.focus_child orelse return null;
|
||||||
return &surface.core_surface;
|
return &surface.core_surface;
|
||||||
|
519
src/apprt/gtk/command_palette.zig
Normal file
519
src/apprt/gtk/command_palette.zig
Normal file
@ -0,0 +1,519 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const Binding = @import("../../input/Binding.zig");
|
||||||
|
const Window = @import("Window.zig");
|
||||||
|
const key = @import("key.zig");
|
||||||
|
const zf = @import("zf");
|
||||||
|
|
||||||
|
const gobject = @import("gobject");
|
||||||
|
const gio = @import("gio");
|
||||||
|
const gtk = @import("gtk");
|
||||||
|
const adw = @import("adw");
|
||||||
|
|
||||||
|
const log = std.log.scoped(.command_palette);
|
||||||
|
|
||||||
|
/// List of "example" commands/queries
|
||||||
|
const example_queries = [_][:0]const u8{
|
||||||
|
"new window",
|
||||||
|
"inspector",
|
||||||
|
"window decoration",
|
||||||
|
"clear",
|
||||||
|
"reload config",
|
||||||
|
"new split",
|
||||||
|
"tab overview",
|
||||||
|
"maximize",
|
||||||
|
"fullscreen",
|
||||||
|
"zoom",
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The command palette, which provides a pop-up dialog for searching
|
||||||
|
/// and running a list of possible "commands", or pre-parametrized actions.
|
||||||
|
pub const CommandPalette = extern struct {
|
||||||
|
parent: Parent,
|
||||||
|
|
||||||
|
pub const Parent = adw.Dialog;
|
||||||
|
|
||||||
|
const Private = struct {
|
||||||
|
window: *Window,
|
||||||
|
alloc: std.heap.ArenaAllocator,
|
||||||
|
list: *CommandListModel,
|
||||||
|
|
||||||
|
examples: @TypeOf(example_queries),
|
||||||
|
example_idx: usize = 0,
|
||||||
|
|
||||||
|
// To be filled in during template population
|
||||||
|
stack: *adw.ViewStack,
|
||||||
|
search: *gtk.SearchEntry,
|
||||||
|
actions: *gtk.ListView,
|
||||||
|
example: *adw.ActionRow,
|
||||||
|
|
||||||
|
var offset: c_int = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const getGObjectType = gobject.ext.defineClass(CommandPalette, .{
|
||||||
|
.instanceInit = init,
|
||||||
|
.classInit = Class.init,
|
||||||
|
.parent_class = &Class.parent,
|
||||||
|
.private = .{ .Type = Private, .offset = &Private.offset },
|
||||||
|
});
|
||||||
|
|
||||||
|
pub fn new(window: *Window) !*CommandPalette {
|
||||||
|
const self = gobject.ext.newInstance(CommandPalette, .{});
|
||||||
|
const priv = self.private();
|
||||||
|
|
||||||
|
// TODO: This is bad style in GObject. This should be in `init`, but
|
||||||
|
// I have no way of smuggling a window pointer through.
|
||||||
|
priv.window = window;
|
||||||
|
priv.alloc = std.heap.ArenaAllocator.init(window.app.core_app.alloc);
|
||||||
|
|
||||||
|
priv.list = try CommandListModel.new(&priv.alloc);
|
||||||
|
try priv.list.private().commands.updateBindings(priv.alloc.allocator(), window.app.config.keybind.set);
|
||||||
|
priv.actions.setModel(priv.list.as(gtk.SelectionModel));
|
||||||
|
|
||||||
|
priv.examples = example_queries;
|
||||||
|
std.crypto.random.shuffle([:0]const u8, &priv.examples);
|
||||||
|
|
||||||
|
priv.search.setKeyCaptureWidget(self.as(gtk.Widget));
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn present(self: *CommandPalette) void {
|
||||||
|
self.refreshExample();
|
||||||
|
self.as(adw.Dialog).present(@ptrCast(self.private().window.window));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Boilerplate
|
||||||
|
pub fn as(self: *CommandPalette, comptime T: type) *T {
|
||||||
|
return gobject.ext.as(T, self);
|
||||||
|
}
|
||||||
|
pub fn ref(self: *CommandPalette) void {
|
||||||
|
_ = self.as(gobject.Object).ref();
|
||||||
|
}
|
||||||
|
pub fn unref(self: *CommandPalette) void {
|
||||||
|
self.as(gobject.Object).unref();
|
||||||
|
}
|
||||||
|
fn private(self: *CommandPalette) *Private {
|
||||||
|
return gobject.ext.impl_helpers.getPrivate(self, Private, Private.offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn refreshExample(self: *CommandPalette) void {
|
||||||
|
const priv = self.private();
|
||||||
|
const example = priv.examples[priv.example_idx];
|
||||||
|
priv.example.as(adw.PreferencesRow).setTitle(example);
|
||||||
|
|
||||||
|
priv.example_idx += 1;
|
||||||
|
if (priv.example_idx == priv.examples.len) priv.example_idx = 0;
|
||||||
|
}
|
||||||
|
fn setQuery(self: *CommandPalette, query: [:0]const u8) void {
|
||||||
|
const priv = self.private();
|
||||||
|
priv.list.setQuery(query);
|
||||||
|
|
||||||
|
const page = switch (priv.list.state()) {
|
||||||
|
.prompt => "prompt",
|
||||||
|
.items => items: {
|
||||||
|
priv.actions.scrollTo(0, .{ .focus = true }, null);
|
||||||
|
break :items "items";
|
||||||
|
},
|
||||||
|
.not_found => "not-found",
|
||||||
|
};
|
||||||
|
|
||||||
|
priv.stack.setVisibleChildName(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lifecycle functions
|
||||||
|
fn init(self: *CommandPalette, _: *Class) callconv(.C) void {
|
||||||
|
self.as(gtk.Widget).initTemplate();
|
||||||
|
}
|
||||||
|
fn dispose(self: *CommandPalette) callconv(.C) void {
|
||||||
|
self.as(gtk.Widget).disposeTemplate(getGObjectType());
|
||||||
|
gobject.Object.virtual_methods.dispose.call(Class.parent, self.as(Parent));
|
||||||
|
}
|
||||||
|
fn finalize(self: *CommandPalette) callconv(.C) void {
|
||||||
|
self.private().alloc.deinit();
|
||||||
|
gobject.Object.virtual_methods.finalize.call(Class.parent, self.as(Parent));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Callbacks
|
||||||
|
fn searchChanged(self: *CommandPalette, _: *gtk.SearchEntry) callconv(.C) void {
|
||||||
|
const text = self.private().search.as(gtk.Editable).getText();
|
||||||
|
self.setQuery(std.mem.span(text));
|
||||||
|
}
|
||||||
|
fn searchStopped(self: *CommandPalette, _: *gtk.SearchEntry) callconv(.C) void {
|
||||||
|
_ = self.as(Parent).close();
|
||||||
|
}
|
||||||
|
fn searchActivated(self: *CommandPalette, _: *gtk.SearchEntry) callconv(.C) void {
|
||||||
|
self.activateAction(0, self.private().actions);
|
||||||
|
}
|
||||||
|
fn exampleActivated(self: *CommandPalette, row: *adw.ActionRow) callconv(.C) void {
|
||||||
|
const text = row.as(adw.PreferencesRow).getTitle();
|
||||||
|
self.private().search.as(gtk.Editable).setText(text);
|
||||||
|
self.setQuery(std.mem.span(text));
|
||||||
|
self.refreshExample();
|
||||||
|
}
|
||||||
|
fn activateAction(self: *CommandPalette, pos: c_uint, _: *gtk.ListView) callconv(.C) void {
|
||||||
|
const priv = self.private();
|
||||||
|
const action = priv.list.getAction(pos) orelse return;
|
||||||
|
const action_surface = priv.window.actionSurface() orelse return;
|
||||||
|
|
||||||
|
const performed = action_surface.performBindingAction(action) catch |err| {
|
||||||
|
log.err("failed to perform binding action={}", .{err});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!performed) {
|
||||||
|
log.warn("binding action was not performed", .{});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = self.as(Parent).close();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const Class = extern struct {
|
||||||
|
parent: Parent.Class,
|
||||||
|
|
||||||
|
var parent: *Parent.Class = undefined;
|
||||||
|
|
||||||
|
pub const Instance = CommandPalette;
|
||||||
|
|
||||||
|
fn init(class: *Class) callconv(.C) void {
|
||||||
|
gobject.Object.virtual_methods.dispose.implement(class, dispose);
|
||||||
|
gobject.Object.virtual_methods.finalize.implement(class, finalize);
|
||||||
|
|
||||||
|
const widget_class = class.as(gtk.WidgetClass);
|
||||||
|
widget_class.setTemplateFromResource("/com/mitchellh/ghostty/ui/command_palette.ui");
|
||||||
|
widget_class.bindTemplateCallbackFull("searchChanged", @ptrCast(&searchChanged));
|
||||||
|
widget_class.bindTemplateCallbackFull("searchStopped", @ptrCast(&searchStopped));
|
||||||
|
widget_class.bindTemplateCallbackFull("searchActivated", @ptrCast(&searchActivated));
|
||||||
|
widget_class.bindTemplateCallbackFull("exampleActivated", @ptrCast(&exampleActivated));
|
||||||
|
widget_class.bindTemplateCallbackFull("activateAction", @ptrCast(&activateAction));
|
||||||
|
|
||||||
|
class.bindTemplateChildPrivate("actions", .{});
|
||||||
|
class.bindTemplateChildPrivate("search", .{});
|
||||||
|
class.bindTemplateChildPrivate("stack", .{});
|
||||||
|
class.bindTemplateChildPrivate("example", .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as(self: *Class, comptime T: type) *T {
|
||||||
|
return gobject.ext.as(T, self);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bindTemplateChildPrivate(class: *Class, comptime name: [:0]const u8, comptime options: gtk.ext.BindTemplateChildOptions) void {
|
||||||
|
gtk.ext.impl_helpers.bindTemplateChildPrivate(class, name, Private, Private.offset, options);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const CommandListModel = extern struct {
|
||||||
|
parent: Parent,
|
||||||
|
|
||||||
|
pub const Parent = gobject.Object;
|
||||||
|
pub const Implements = [_]type{ gio.ListModel, gtk.SelectionModel };
|
||||||
|
|
||||||
|
const Private = struct {
|
||||||
|
alloc: *std.heap.ArenaAllocator,
|
||||||
|
tokens: std.ArrayListUnmanaged([]const u8),
|
||||||
|
commands: CommandList,
|
||||||
|
|
||||||
|
var offset: c_int = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const State = enum {
|
||||||
|
prompt,
|
||||||
|
items,
|
||||||
|
not_found,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const getGObjectType = gobject.ext.defineClass(CommandListModel, .{
|
||||||
|
.classInit = Class.init,
|
||||||
|
.implements = &.{
|
||||||
|
gobject.ext.implement(gio.ListModel, .{ .init = Class.initListModel }),
|
||||||
|
gobject.ext.implement(gtk.SelectionModel, .{ .init = Class.initSelectionModel }),
|
||||||
|
},
|
||||||
|
.private = .{ .Type = Private, .offset = &Private.offset },
|
||||||
|
});
|
||||||
|
|
||||||
|
pub const properties = struct {
|
||||||
|
pub const query = struct {
|
||||||
|
pub const name = "query";
|
||||||
|
const impl = gobject.ext.defineProperty(name, CommandListModel, ?[:0]const u8, .{
|
||||||
|
.default = "",
|
||||||
|
.accessor = .{ .getter = @ptrCast(&noop), .setter = setQuery },
|
||||||
|
.flags = .{ .writable = true },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn new(alloc: *std.heap.ArenaAllocator) !*CommandListModel {
|
||||||
|
const self = gobject.ext.newInstance(CommandListModel, .{});
|
||||||
|
self.private().* = .{
|
||||||
|
.alloc = alloc,
|
||||||
|
.tokens = .{},
|
||||||
|
.commands = try CommandList.init(alloc.allocator()),
|
||||||
|
};
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getAction(self: *CommandListModel, pos: c_uint) ?Binding.Action {
|
||||||
|
const commands = self.private().commands;
|
||||||
|
if (pos >= commands.len) return null;
|
||||||
|
|
||||||
|
return commands.list.items(.command)[@intCast(pos)].action;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn state(self: *CommandListModel) State {
|
||||||
|
const priv = self.private();
|
||||||
|
|
||||||
|
return if (priv.tokens.items.len > 0)
|
||||||
|
if (priv.commands.len > 0) .items else .not_found
|
||||||
|
else
|
||||||
|
.prompt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Boilerplate
|
||||||
|
pub fn as(self: *CommandListModel, comptime T: type) *T {
|
||||||
|
return gobject.ext.as(T, self);
|
||||||
|
}
|
||||||
|
fn private(self: *CommandListModel) *Private {
|
||||||
|
return gobject.ext.impl_helpers.getPrivate(self, Private, Private.offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lifecycle
|
||||||
|
fn finalize(self: *CommandListModel) callconv(.C) void {
|
||||||
|
const priv = self.private();
|
||||||
|
priv.tokens.deinit(priv.alloc.allocator());
|
||||||
|
priv.commands.deinit(priv.alloc.allocator());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Properties
|
||||||
|
pub fn setQuery(self: *CommandListModel, query: ?[:0]const u8) void {
|
||||||
|
const priv = self.private();
|
||||||
|
|
||||||
|
priv.tokens.clearRetainingCapacity();
|
||||||
|
if (query) |q| {
|
||||||
|
var it = std.mem.tokenizeScalar(u8, q, ' ');
|
||||||
|
while (it.next()) |token|
|
||||||
|
priv.tokens.append(priv.alloc.allocator(), token) catch @panic("OOM");
|
||||||
|
}
|
||||||
|
|
||||||
|
const old_len = priv.commands.sortAndFilter(priv.tokens.items);
|
||||||
|
self.as(gio.ListModel).itemsChanged(0, old_len, priv.commands.len);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListModel interface
|
||||||
|
fn getItem(list: *gio.ListModel, pos: c_uint) callconv(.C) ?*gobject.Object {
|
||||||
|
const self = gobject.ext.cast(CommandListModel, list) orelse return null;
|
||||||
|
const priv = self.private();
|
||||||
|
if (pos >= priv.commands.len) return null;
|
||||||
|
|
||||||
|
const item = priv.commands.list.get(@intCast(pos));
|
||||||
|
return gobject.ext.as(gobject.Object, CommandObject.new(item));
|
||||||
|
}
|
||||||
|
fn getItemType(_: *gio.ListModel) callconv(.C) gobject.Type {
|
||||||
|
return CommandObject.getGObjectType();
|
||||||
|
}
|
||||||
|
fn getNItems(list: *gio.ListModel) callconv(.C) c_uint {
|
||||||
|
const self = gobject.ext.cast(CommandListModel, list) orelse return 0;
|
||||||
|
return self.private().commands.len;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelectionModel interface
|
||||||
|
fn isSelected(_: *gtk.SelectionModel, pos: c_uint) callconv(.C) c_int {
|
||||||
|
// Always select the first item (purely visual)
|
||||||
|
return @intFromBool(pos == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const Class = extern struct {
|
||||||
|
parent: Parent.Class,
|
||||||
|
|
||||||
|
pub const Instance = CommandListModel;
|
||||||
|
|
||||||
|
fn init(class: *Class) callconv(.C) void {
|
||||||
|
gobject.ext.registerProperties(class, &.{
|
||||||
|
properties.query.impl,
|
||||||
|
});
|
||||||
|
gobject.Object.virtual_methods.finalize.implement(class, finalize);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn initListModel(iface: *gio.ListModel.Iface) callconv(.C) void {
|
||||||
|
gio.ListModel.virtual_methods.get_item.implement(iface, getItem);
|
||||||
|
gio.ListModel.virtual_methods.get_item_type.implement(iface, getItemType);
|
||||||
|
gio.ListModel.virtual_methods.get_n_items.implement(iface, getNItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn initSelectionModel(iface: *gtk.SelectionModel.Iface) callconv(.C) void {
|
||||||
|
gtk.SelectionModel.virtual_methods.is_selected.implement(iface, isSelected);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const CommandList = struct {
|
||||||
|
list: std.MultiArrayList(Item) = .{},
|
||||||
|
len: c_uint = 0,
|
||||||
|
|
||||||
|
const Item = struct {
|
||||||
|
command: Binding.Command,
|
||||||
|
accelerator: ?[:0]const u8 = null,
|
||||||
|
rank: ?f64 = null,
|
||||||
|
var offset: c_int = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
fn init(alloc: std.mem.Allocator) !CommandList {
|
||||||
|
var list: std.MultiArrayList(Item) = .{};
|
||||||
|
try list.ensureTotalCapacity(alloc, Binding.commands.len);
|
||||||
|
|
||||||
|
for (Binding.commands) |command| {
|
||||||
|
switch (command.action) {
|
||||||
|
// macOS-only
|
||||||
|
.prompt_surface_title,
|
||||||
|
.close_all_windows,
|
||||||
|
.toggle_secure_input,
|
||||||
|
.toggle_quick_terminal,
|
||||||
|
=> continue,
|
||||||
|
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
list.appendAssumeCapacity(.{ .command = command });
|
||||||
|
}
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.list = list,
|
||||||
|
.len = @intCast(list.len),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
fn deinit(self: *CommandList, alloc: std.mem.Allocator) void {
|
||||||
|
self.list.deinit(alloc);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sort and filter the current list of commands based on a slice of tokens.
|
||||||
|
/// Returns the old size of the command list.
|
||||||
|
fn sortAndFilter(self: *CommandList, tokens: [][]const u8) c_uint {
|
||||||
|
const old_len = self.len;
|
||||||
|
|
||||||
|
if (tokens.len == 0) {
|
||||||
|
self.len = @intCast(self.list.len);
|
||||||
|
return old_len;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cmds = self.list.items(.command);
|
||||||
|
const ranks = self.list.items(.rank);
|
||||||
|
for (cmds, ranks) |cmd, *rank| {
|
||||||
|
rank.* = zf.rank(cmd.title, tokens, .{
|
||||||
|
.to_lower = true,
|
||||||
|
.plain = true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
self.list.sort(self);
|
||||||
|
|
||||||
|
// Limit new length to the items that are not null
|
||||||
|
// (null rank correspond to non-matches)
|
||||||
|
const new_len = std.mem.indexOfScalar(?f64, ranks, null);
|
||||||
|
self.len = @intCast(new_len orelse self.list.len);
|
||||||
|
return old_len;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn updateBindings(self: *CommandList, alloc: std.mem.Allocator, set: Binding.Set) !void {
|
||||||
|
var buf: [256]u8 = undefined;
|
||||||
|
for (self.list.items(.command), self.list.items(.accelerator)) |cmd, *accel| {
|
||||||
|
if (accel.*) |accel_| alloc.free(accel_);
|
||||||
|
|
||||||
|
accel.* = accel: {
|
||||||
|
const trigger = set.getTrigger(cmd.action) orelse break :accel null;
|
||||||
|
const accel_ = try key.accelFromTrigger(&buf, trigger) orelse break :accel null;
|
||||||
|
break :accel try alloc.dupeZ(u8, accel_);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A custom sorting criterion that guarantees that nulls
|
||||||
|
// (non-matches) always sink to the bottom of the list,
|
||||||
|
// so we can easily exclude them
|
||||||
|
pub fn lessThan(self: *CommandList, a: usize, b: usize) bool {
|
||||||
|
const ranks = self.list.items(.rank);
|
||||||
|
|
||||||
|
// If a is null, then always place a (null) after b
|
||||||
|
const rank_a = ranks[a] orelse return false;
|
||||||
|
// If a is non-null and b is null, then put b (null) after a
|
||||||
|
const rank_b = ranks[b] orelse return true;
|
||||||
|
|
||||||
|
// Normal ranking logic
|
||||||
|
return rank_a < rank_b;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const CommandObject = extern struct {
|
||||||
|
parent: Parent,
|
||||||
|
|
||||||
|
pub const Parent = gobject.Object;
|
||||||
|
const Private = CommandList.Item;
|
||||||
|
|
||||||
|
pub const getGObjectType = gobject.ext.defineClass(CommandObject, .{
|
||||||
|
.classInit = Class.init,
|
||||||
|
.private = .{ .Type = Private, .offset = &Private.offset },
|
||||||
|
});
|
||||||
|
|
||||||
|
pub const properties = struct {
|
||||||
|
pub const title = struct {
|
||||||
|
pub const name = "title";
|
||||||
|
const impl = gobject.ext.defineProperty(name, CommandObject, ?[:0]const u8, .{
|
||||||
|
.default = "",
|
||||||
|
.accessor = .{ .getter = getTitle, .setter = @ptrCast(&noop) },
|
||||||
|
.flags = .{ .readable = true },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
pub const description = struct {
|
||||||
|
pub const name = "description";
|
||||||
|
const impl = gobject.ext.defineProperty(name, CommandObject, ?[:0]const u8, .{
|
||||||
|
.default = "",
|
||||||
|
.accessor = .{ .getter = getDescription, .setter = @ptrCast(&noop) },
|
||||||
|
.flags = .{ .readable = true },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
pub const accelerator = struct {
|
||||||
|
pub const name = "accelerator";
|
||||||
|
const impl = gobject.ext.defineProperty(name, CommandObject, ?[:0]const u8, .{
|
||||||
|
.default = "",
|
||||||
|
.accessor = .{ .getter = getAccelerator, .setter = @ptrCast(&noop) },
|
||||||
|
.flags = .{ .readable = true },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn new(item: CommandList.Item) *CommandObject {
|
||||||
|
const self = gobject.ext.newInstance(CommandObject, .{});
|
||||||
|
self.private().* = item;
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
fn private(self: *CommandObject) *Private {
|
||||||
|
return gobject.ext.impl_helpers.getPrivate(self, Private, Private.offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getTitle(self: *CommandObject) ?[:0]const u8 {
|
||||||
|
return self.private().command.title;
|
||||||
|
}
|
||||||
|
fn getDescription(self: *CommandObject) ?[:0]const u8 {
|
||||||
|
return self.private().command.description;
|
||||||
|
}
|
||||||
|
fn getAccelerator(self: *CommandObject) ?[:0]const u8 {
|
||||||
|
return self.private().accelerator;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const Class = extern struct {
|
||||||
|
parent: Parent.Class,
|
||||||
|
|
||||||
|
pub const Instance = CommandObject;
|
||||||
|
|
||||||
|
fn init(class: *Class) callconv(.C) void {
|
||||||
|
gobject.ext.registerProperties(class, &.{
|
||||||
|
properties.title.impl,
|
||||||
|
properties.description.impl,
|
||||||
|
properties.accelerator.impl,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// For some bizarre reason zig-gobject forces you to set a setter for a read-only field...
|
||||||
|
fn noop() callconv(.C) void {
|
||||||
|
unreachable;
|
||||||
|
}
|
@ -54,7 +54,9 @@ const icons = [_]struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub const ui_files = [_][]const u8{};
|
pub const ui_files = [_][]const u8{};
|
||||||
pub const blueprint_files = [_][]const u8{};
|
pub const blueprint_files = [_][]const u8{
|
||||||
|
"command_palette",
|
||||||
|
};
|
||||||
|
|
||||||
pub fn main() !void {
|
pub fn main() !void {
|
||||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||||
|
143
src/apprt/gtk/ui/command_palette.blp
Normal file
143
src/apprt/gtk/ui/command_palette.blp
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
using Gtk 4.0;
|
||||||
|
using Adw 1;
|
||||||
|
|
||||||
|
template $CommandPalette: Adw.Dialog {
|
||||||
|
content-width: 700;
|
||||||
|
|
||||||
|
Adw.ToolbarView {
|
||||||
|
top-bar-style: flat;
|
||||||
|
|
||||||
|
[top]
|
||||||
|
Adw.HeaderBar {
|
||||||
|
[title]
|
||||||
|
SearchEntry search {
|
||||||
|
hexpand: true;
|
||||||
|
placeholder-text: _("Search for Action");
|
||||||
|
search-changed => $searchChanged() swapped;
|
||||||
|
stop-search => $searchStopped() swapped;
|
||||||
|
activate => $searchActivated() swapped;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Adw.ViewStack stack {
|
||||||
|
visible-child-name: "prompt";
|
||||||
|
|
||||||
|
Adw.ViewStackPage {
|
||||||
|
name: "prompt";
|
||||||
|
|
||||||
|
child: Adw.StatusPage {
|
||||||
|
styles [
|
||||||
|
"compact"
|
||||||
|
]
|
||||||
|
|
||||||
|
title: _("Type to Search");
|
||||||
|
description: _("Find any command by searching for its name");
|
||||||
|
|
||||||
|
child: ListBox {
|
||||||
|
styles [
|
||||||
|
"boxed-list"
|
||||||
|
]
|
||||||
|
|
||||||
|
margin-start: 50;
|
||||||
|
margin-end: 50;
|
||||||
|
|
||||||
|
Adw.ActionRow example {
|
||||||
|
title: "new window";
|
||||||
|
selectable: false;
|
||||||
|
activatable: true;
|
||||||
|
activated => $exampleActivated() swapped;
|
||||||
|
|
||||||
|
[prefix]
|
||||||
|
Image {
|
||||||
|
icon-name: "system-search-symbolic";
|
||||||
|
}
|
||||||
|
[suffix]
|
||||||
|
Image {
|
||||||
|
icon-name: "keyboard-enter-symbolic";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Adw.ViewStackPage {
|
||||||
|
name: "not-found";
|
||||||
|
|
||||||
|
child: Adw.StatusPage {
|
||||||
|
styles [
|
||||||
|
"compact"
|
||||||
|
]
|
||||||
|
|
||||||
|
icon-name: "system-search-symbolic";
|
||||||
|
title: _("No Results Found");
|
||||||
|
description: _("Try a different search");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Adw.ViewStackPage {
|
||||||
|
name: "items";
|
||||||
|
|
||||||
|
child: ScrolledWindow {
|
||||||
|
// min-content-height: 200;
|
||||||
|
|
||||||
|
ListView actions {
|
||||||
|
show-separators: true;
|
||||||
|
single-click-activate: true;
|
||||||
|
activate => $activateAction() swapped;
|
||||||
|
|
||||||
|
styles [
|
||||||
|
"rich-list"
|
||||||
|
]
|
||||||
|
|
||||||
|
factory: BuilderListItemFactory {
|
||||||
|
template ListItem {
|
||||||
|
child: Box {
|
||||||
|
orientation: horizontal;
|
||||||
|
spacing: 10;
|
||||||
|
|
||||||
|
Box {
|
||||||
|
orientation: vertical;
|
||||||
|
hexpand: true;
|
||||||
|
|
||||||
|
Label {
|
||||||
|
ellipsize: end;
|
||||||
|
halign: start;
|
||||||
|
wrap: false;
|
||||||
|
single-line-mode: true;
|
||||||
|
|
||||||
|
styles [
|
||||||
|
"title"
|
||||||
|
]
|
||||||
|
|
||||||
|
label: bind template.item as <$CommandObject>.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
ellipsize: end;
|
||||||
|
halign: start;
|
||||||
|
wrap: false;
|
||||||
|
single-line-mode: true;
|
||||||
|
|
||||||
|
styles [
|
||||||
|
"subtitle"
|
||||||
|
]
|
||||||
|
|
||||||
|
label: bind template.item as <$CommandObject>.description;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ShortcutLabel {
|
||||||
|
accelerator: bind template.item as <$CommandObject>.accelerator;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -479,6 +479,11 @@ pub const Action = union(enum) {
|
|||||||
/// This currently only works on macOS.
|
/// This currently only works on macOS.
|
||||||
toggle_visibility: void,
|
toggle_visibility: void,
|
||||||
|
|
||||||
|
/// Toggles the command palette.
|
||||||
|
///
|
||||||
|
/// Currently only supported on Linux.
|
||||||
|
toggle_command_palette,
|
||||||
|
|
||||||
/// Quit ghostty.
|
/// Quit ghostty.
|
||||||
quit: void,
|
quit: void,
|
||||||
|
|
||||||
@ -772,6 +777,7 @@ pub const Action = union(enum) {
|
|||||||
.toggle_maximize,
|
.toggle_maximize,
|
||||||
.toggle_fullscreen,
|
.toggle_fullscreen,
|
||||||
.toggle_window_decorations,
|
.toggle_window_decorations,
|
||||||
|
.toggle_command_palette,
|
||||||
.toggle_secure_input,
|
.toggle_secure_input,
|
||||||
.crash,
|
.crash,
|
||||||
=> .surface,
|
=> .surface,
|
||||||
@ -855,6 +861,258 @@ pub const Action = union(enum) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns zero or more commands associated to this action.
|
||||||
|
/// Actions that don't make sense in a GUI context would have
|
||||||
|
/// zero commands, while some parameterized actions (e.g. splitting)
|
||||||
|
/// would have multiple associated commands corresponding to
|
||||||
|
/// each value of the parameter (e.g. split direction).
|
||||||
|
pub fn commands(self: Action) []const Command {
|
||||||
|
// GENERAL RULES OF THUMB TO ASSIGNING COMMANDS TO AN ACTION:
|
||||||
|
//
|
||||||
|
// 1. Does this action require arbitrary user input,
|
||||||
|
// or parameters that don't have obvious default values?
|
||||||
|
// If so, then it shouldn't have any commands.
|
||||||
|
//
|
||||||
|
// 2. Is it easier for the user to activate the toggle command
|
||||||
|
// palette binding, type in the name of the action and select it,
|
||||||
|
// compared to just using its keybinding?
|
||||||
|
// If not, then it shouldn't have any commands.
|
||||||
|
// (For example, why go through all of that just to navigate to the next tab?)
|
||||||
|
//
|
||||||
|
// 3. Do you think that most people will access this action via
|
||||||
|
// the action palette, instead of using a keybind?
|
||||||
|
// (Potential reasons being that the keybind is too awkward or too infrequent)
|
||||||
|
// If so, then definitely add a command. If not, maybe think twice.
|
||||||
|
|
||||||
|
// For the vast majority of cases we only have one command,
|
||||||
|
// so we avoid a lot of syntactic noise by using an optional here
|
||||||
|
// to stand in for a zero-or-one item list.
|
||||||
|
const command: ?Command = switch (self) {
|
||||||
|
.new_tab => .{
|
||||||
|
.action = .new_tab,
|
||||||
|
.title = "New Tab",
|
||||||
|
.description = "Open a new tab in the current window.",
|
||||||
|
},
|
||||||
|
.close_tab => .{
|
||||||
|
.action = .close_tab,
|
||||||
|
.title = "Close Tab",
|
||||||
|
.description = "Close the current tab.",
|
||||||
|
},
|
||||||
|
.new_window => .{
|
||||||
|
.action = .new_window,
|
||||||
|
.title = "New Window",
|
||||||
|
.description = "Open a new window.",
|
||||||
|
},
|
||||||
|
.close_window => .{
|
||||||
|
.action = .close_window,
|
||||||
|
.title = "Close Window",
|
||||||
|
.description = "Close the current window.",
|
||||||
|
},
|
||||||
|
.close_all_windows => .{
|
||||||
|
.action = .close_all_windows,
|
||||||
|
.title = "Close All Windows",
|
||||||
|
.description = "Close all currently open windows.",
|
||||||
|
},
|
||||||
|
.close_surface => .{
|
||||||
|
.action = .close_surface,
|
||||||
|
.title = "Close Surface",
|
||||||
|
.description = "Closes the current terminal surface.",
|
||||||
|
},
|
||||||
|
|
||||||
|
.new_split => return &.{
|
||||||
|
.{
|
||||||
|
.action = .{ .new_split = .up },
|
||||||
|
.title = "Split Upwards",
|
||||||
|
.description = "Split and create a new surface upwards.",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.action = .{ .new_split = .down },
|
||||||
|
.title = "Split Downwards",
|
||||||
|
.description = "Split and create a new surface downwards.",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.action = .{ .new_split = .left },
|
||||||
|
.title = "Split Leftwards",
|
||||||
|
.description = "Split and create a new surface leftwards.",
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.action = .{ .new_split = .right },
|
||||||
|
.title = "Split Rightwards",
|
||||||
|
.description = "Split and create a new surface rightwards.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
.equalize_splits => .{
|
||||||
|
.action = .equalize_splits,
|
||||||
|
.title = "Equalize Splits",
|
||||||
|
.description = "Equalize the sizes of the current and adjacent surfaces.",
|
||||||
|
},
|
||||||
|
|
||||||
|
.prompt_surface_title => .{
|
||||||
|
.action = .prompt_surface_title,
|
||||||
|
.title = "Rename Surface Title",
|
||||||
|
.description = "Rename the title of the current surface.",
|
||||||
|
},
|
||||||
|
|
||||||
|
.open_config => .{
|
||||||
|
.action = .open_config,
|
||||||
|
.title = "Open Configuration",
|
||||||
|
.description = "Open Ghostty's configuration.",
|
||||||
|
},
|
||||||
|
.reload_config => .{
|
||||||
|
.action = .reload_config,
|
||||||
|
.title = "Reload Configuration",
|
||||||
|
.description = "Reload Ghostty's configuration.",
|
||||||
|
},
|
||||||
|
|
||||||
|
.copy_to_clipboard => .{
|
||||||
|
.action = .copy_to_clipboard,
|
||||||
|
.title = "Copy to Clipboard",
|
||||||
|
.description = "Copy the current selection to the clipboard.",
|
||||||
|
},
|
||||||
|
.copy_url_to_clipboard => .{
|
||||||
|
.action = .copy_url_to_clipboard,
|
||||||
|
.title = "Copy URL to Clipboard",
|
||||||
|
.description = "Copy the URL under the cursor to the clipboard.",
|
||||||
|
},
|
||||||
|
.paste_from_clipboard => .{
|
||||||
|
.action = .paste_from_clipboard,
|
||||||
|
.title = "Paste from Clipboard",
|
||||||
|
.description = "Paste from the clipboard.",
|
||||||
|
},
|
||||||
|
.paste_from_selection => .{
|
||||||
|
.action = .paste_from_selection,
|
||||||
|
.title = "Paste from Selection Clipboard",
|
||||||
|
.description = "Paste from the selection clipboard.",
|
||||||
|
},
|
||||||
|
.select_all => .{
|
||||||
|
.action = .select_all,
|
||||||
|
.title = "Select All",
|
||||||
|
.description = "Select everything in the current terminal.",
|
||||||
|
},
|
||||||
|
|
||||||
|
.scroll_to_top => .{
|
||||||
|
.action = .scroll_to_top,
|
||||||
|
.title = "Scroll to Top",
|
||||||
|
.description = "Scroll to the top of the terminal.",
|
||||||
|
},
|
||||||
|
.scroll_to_bottom => .{
|
||||||
|
.action = .scroll_to_bottom,
|
||||||
|
.title = "Scroll to Bottom",
|
||||||
|
.description = "Scroll to the bottom of the terminal.",
|
||||||
|
},
|
||||||
|
|
||||||
|
.reset => .{
|
||||||
|
.action = .reset,
|
||||||
|
.title = "Reset Terminal",
|
||||||
|
.description = "Reset the terminal.",
|
||||||
|
},
|
||||||
|
.clear_screen => .{
|
||||||
|
.action = .clear_screen,
|
||||||
|
.title = "Clear Screen",
|
||||||
|
.description = "Clear the terminal screen.",
|
||||||
|
},
|
||||||
|
|
||||||
|
.increase_font_size => .{
|
||||||
|
.action = .{ .increase_font_size = 1 },
|
||||||
|
.title = "Increase Font Size",
|
||||||
|
.description = "Increase the font size by 1pt.",
|
||||||
|
},
|
||||||
|
.decrease_font_size => .{
|
||||||
|
.action = .{ .decrease_font_size = 1 },
|
||||||
|
.title = "Decrease Font Size",
|
||||||
|
.description = "Decrease the font size by 1pt.",
|
||||||
|
},
|
||||||
|
.reset_font_size => .{
|
||||||
|
.action = .reset_font_size,
|
||||||
|
.title = "Reset Font Size",
|
||||||
|
.description = "Reset the font size to its original value.",
|
||||||
|
},
|
||||||
|
|
||||||
|
.inspector => .{
|
||||||
|
.action = .{ .inspector = .toggle },
|
||||||
|
.title = "Toggle Inspector",
|
||||||
|
.description = "Toggle the terminal inspector.",
|
||||||
|
},
|
||||||
|
.toggle_quick_terminal => .{
|
||||||
|
.action = .toggle_quick_terminal,
|
||||||
|
.title = "Toggle Quick Terminal",
|
||||||
|
.description = "Toggle the quick terminal.",
|
||||||
|
},
|
||||||
|
.toggle_maximize => .{
|
||||||
|
.action = .toggle_maximize,
|
||||||
|
.title = "Toggle Maximize",
|
||||||
|
.description = "Toggle the maximized state of the current window.",
|
||||||
|
},
|
||||||
|
.toggle_fullscreen => .{
|
||||||
|
.action = .toggle_fullscreen,
|
||||||
|
.title = "Toggle Fullscreen",
|
||||||
|
.description = "Toggle the fullscreened state of the current window.",
|
||||||
|
},
|
||||||
|
.toggle_tab_overview => .{
|
||||||
|
.action = .toggle_tab_overview,
|
||||||
|
.title = "Toggle Tab Overview",
|
||||||
|
.description = "Toggle the tab overview.",
|
||||||
|
},
|
||||||
|
.toggle_split_zoom => .{
|
||||||
|
.action = .toggle_split_zoom,
|
||||||
|
.title = "Toggle Split Zoom",
|
||||||
|
.description = "Zoom in or out of the current split.",
|
||||||
|
},
|
||||||
|
.toggle_window_decorations => .{
|
||||||
|
.action = .toggle_window_decorations,
|
||||||
|
.title = "Toggle Window Decorations",
|
||||||
|
.description = "Toggle the window decorations.",
|
||||||
|
},
|
||||||
|
.toggle_secure_input => .{
|
||||||
|
.action = .toggle_secure_input,
|
||||||
|
.title = "Toggle Secure Input",
|
||||||
|
.description = "Toggle the secure input state.",
|
||||||
|
},
|
||||||
|
|
||||||
|
.quit => .{
|
||||||
|
.action = .quit,
|
||||||
|
.title = "Quit",
|
||||||
|
.description = "Quit Ghostty.",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Requires a parameter that requires arbitrary
|
||||||
|
// input or has no good default - see (1)
|
||||||
|
.ignore,
|
||||||
|
.unbind,
|
||||||
|
.csi,
|
||||||
|
.esc,
|
||||||
|
.text,
|
||||||
|
.cursor_key,
|
||||||
|
.goto_tab,
|
||||||
|
.resize_split,
|
||||||
|
.scroll_page_up,
|
||||||
|
.scroll_page_down,
|
||||||
|
.scroll_page_fractional,
|
||||||
|
.scroll_page_lines,
|
||||||
|
.adjust_selection,
|
||||||
|
.jump_to_prompt,
|
||||||
|
.write_scrollback_file,
|
||||||
|
.write_screen_file,
|
||||||
|
.write_selection_file,
|
||||||
|
|
||||||
|
// A lot easier to perform via existing GUI controls
|
||||||
|
// than using the command palette - see (2)
|
||||||
|
.previous_tab,
|
||||||
|
.next_tab,
|
||||||
|
.last_tab,
|
||||||
|
.goto_split,
|
||||||
|
.move_tab,
|
||||||
|
|
||||||
|
// Special reasons
|
||||||
|
.toggle_command_palette, // Just hit Esc or press the close button
|
||||||
|
.toggle_visibility, // You have to already be visible to trigger the palette
|
||||||
|
.crash, // Why would you want to do this?
|
||||||
|
=> null,
|
||||||
|
};
|
||||||
|
|
||||||
|
return if (command) |cmd| &.{cmd} else &.{};
|
||||||
|
}
|
||||||
|
|
||||||
/// Implements the formatter for the fmt package. This encodes the
|
/// Implements the formatter for the fmt package. This encodes the
|
||||||
/// action back into the format used by parse.
|
/// action back into the format used by parse.
|
||||||
pub fn format(
|
pub fn format(
|
||||||
@ -1006,6 +1264,38 @@ pub const Key = enum(c_int) {
|
|||||||
new_window,
|
new_window,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// A command that can be executed in the command palette.
|
||||||
|
///
|
||||||
|
/// This is essentially a subset of binding actions that actually make *sense*
|
||||||
|
/// to be run by a user in a GUI interface (e.g. no control sequences or
|
||||||
|
/// arbitrary text), and has a translatable title attached to it.
|
||||||
|
pub const Command = struct {
|
||||||
|
action: Action,
|
||||||
|
title: [:0]const u8,
|
||||||
|
description: [:0]const u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const commands = commands: {
|
||||||
|
var size: usize = 0;
|
||||||
|
|
||||||
|
for (@typeInfo(Action).Union.fields) |field| {
|
||||||
|
// The value is ignored anyway, so this is safe.
|
||||||
|
const action = @unionInit(Binding.Action, field.name, undefined);
|
||||||
|
size += action.commands().len;
|
||||||
|
}
|
||||||
|
|
||||||
|
var list: [size]Command = undefined;
|
||||||
|
var i: usize = 0;
|
||||||
|
|
||||||
|
for (@typeInfo(Action).Union.fields) |field| {
|
||||||
|
const action = @unionInit(Binding.Action, field.name, undefined);
|
||||||
|
const cmds = action.commands();
|
||||||
|
@memcpy(list[i..][0..cmds.len], cmds);
|
||||||
|
i += cmds.len;
|
||||||
|
}
|
||||||
|
|
||||||
|
break :commands list;
|
||||||
|
};
|
||||||
/// Trigger is the associated key state that can trigger an action.
|
/// Trigger is the associated key state that can trigger an action.
|
||||||
/// This is an extern struct because this is also used in the C API.
|
/// This is an extern struct because this is also used in the C API.
|
||||||
///
|
///
|
||||||
|
Reference in New Issue
Block a user