From a34134e643abe2a9ccc1c79b84a5b0fe6e5b2095 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 19 Apr 2025 12:41:22 -0700 Subject: [PATCH] input: defind Command struct and default commands --- include/ghostty.h | 6 + src/input.zig | 2 + src/input/Binding.zig | 9 - src/input/command.zig | 393 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 401 insertions(+), 9 deletions(-) create mode 100644 src/input/command.zig diff --git a/include/ghostty.h b/include/ghostty.h index c4ef11930..06b812948 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -279,6 +279,12 @@ typedef struct { ghostty_input_mods_e mods; } ghostty_input_trigger_s; +typedef struct { + const char* action; + const char* title; + const char* description; +} ghostty_command_s; + typedef enum { GHOSTTY_BUILD_MODE_DEBUG, GHOSTTY_BUILD_MODE_RELEASE_SAFE, diff --git a/src/input.zig b/src/input.zig index 83be38d3d..caaf80509 100644 --- a/src/input.zig +++ b/src/input.zig @@ -5,6 +5,7 @@ const mouse = @import("input/mouse.zig"); const key = @import("input/key.zig"); const keyboard = @import("input/keyboard.zig"); +pub const command = @import("input/command.zig"); pub const function_keys = @import("input/function_keys.zig"); pub const keycodes = @import("input/keycodes.zig"); pub const kitty = @import("input/kitty.zig"); @@ -12,6 +13,7 @@ pub const kitty = @import("input/kitty.zig"); pub const ctrlOrSuper = key.ctrlOrSuper; pub const Action = key.Action; pub const Binding = @import("input/Binding.zig"); +pub const Command = command.Command; pub const Link = @import("input/Link.zig"); pub const Key = key.Key; pub const KeyboardLayout = keyboard.Layout; diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 244cd29cd..0b9ae1136 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -1017,15 +1017,6 @@ pub const Action = union(enum) { } }; -// A key for the C API to execute an action. This must be kept in sync -// with include/ghostty.h. -pub const Key = enum(c_int) { - copy_to_clipboard, - paste_from_clipboard, - new_tab, - new_window, -}; - /// 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. /// diff --git a/src/input/command.zig b/src/input/command.zig new file mode 100644 index 000000000..51bcbaad6 --- /dev/null +++ b/src/input/command.zig @@ -0,0 +1,393 @@ +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const Action = @import("binding.zig").Action; + +/// A command is a named binding action that can be executed from +/// something like a command palette. +/// +/// A command must be associated with a binding; all commands can be +/// mapped to traditional `keybind` configurations. This restriction +/// makes it so that there is nothing special about commands and likewise +/// it makes it trivial and consistent to define custom commands. +/// +/// For apprt implementers: a command palette doesn't have to make use +/// of all the fields here. We try to provide as much information as +/// possible to make it easier to implement a command palette in the way +/// that makes the most sense for the application. +pub const Command = struct { + action: Action, + title: [:0]const u8, + description: [:0]const u8, + + /// ghostty_command_s + pub const C = extern struct { + action: [*:0]const u8, + title: [*:0]const u8, + description: [*:0]const u8, + }; + + /// Convert this command to a C struct. + pub fn comptimeCval(self: Command) C { + assert(@inComptime()); + + return .{ + .action = std.fmt.comptimePrint("{s}", .{self.action}), + .title = self.title, + .description = self.description, + }; + } +}; + +pub const defaults: []const Command = defaults: { + var count: usize = 0; + for (@typeInfo(Action.Key).@"enum".fields) |field| { + const action = @field(Action.Key, field.name); + count += actionCommands(action).len; + } + + var result: [count]Command = undefined; + var i: usize = 0; + for (@typeInfo(Action.Key).@"enum".fields) |field| { + const action = @field(Action.Key, field.name); + const commands = actionCommands(action); + for (commands) |cmd| { + result[i] = cmd; + i += 1; + } + } + + assert(i == count); + const final = result; + break :defaults &final; +}; + +/// Defaults in C-compatible form. +pub const defaultsC: []const Command.C = defaults: { + var result: [defaults.len]Command.C = undefined; + for (defaults, 0..) |cmd, i| result[i] = cmd.comptimeCval(); + const final = result; + break :defaults &final; +}; + +/// Returns the set of commands associated with this action key by +/// default. Not all actions should have commands. As a general guideline, +/// an action should have a command only if it is useful and reasonable +/// to appear in a command palette. +fn actionCommands(action: Action.Key) []const Command { + // This is implemented as a function and switch rather than a + // flat comptime const because we want to ensure we get a compiler + // error when a new binding is added so that the contributor has + // to consider whether that new binding should have commands or not. + const result: []const Command = switch (action) { + // Note: the use of `comptime` prefix on the return values + // ensures that the data returned is all in the binary and + // and not pointing to the stack. + + .reset => comptime &.{.{ + .action = .reset, + .title = "Reset Terminal", + .description = "Reset the terminal to a clean state.", + }}, + + .copy_to_clipboard => comptime &.{.{ + .action = .copy_to_clipboard, + .title = "Copy to Clipboard", + .description = "Copy the selected text to the clipboard.", + }}, + + .copy_url_to_clipboard => comptime &.{.{ + .action = .copy_url_to_clipboard, + .title = "Copy URL to Clipboard", + .description = "Copy the URL under the cursor to the clipboard.", + }}, + + .paste_from_clipboard => comptime &.{.{ + .action = .paste_from_clipboard, + .title = "Paste from Clipboard", + .description = "Paste the contents of the clipboard.", + }}, + + .paste_from_selection => comptime &.{.{ + .action = .paste_from_selection, + .title = "Paste from Selection", + .description = "Paste the contents of the selection clipboard.", + }}, + + .increase_font_size => comptime &.{.{ + .action = .{ .increase_font_size = 1 }, + .title = "Increase Font Size", + .description = "Increase the font size by 1 point.", + }}, + + .decrease_font_size => comptime &.{.{ + .action = .{ .decrease_font_size = 1 }, + .title = "Decrease Font Size", + .description = "Decrease the font size by 1 point.", + }}, + + .reset_font_size => comptime &.{.{ + .action = .reset_font_size, + .title = "Reset Font Size", + .description = "Reset the font size to the default.", + }}, + + .clear_screen => comptime &.{.{ + .action = .clear_screen, + .title = "Clear Screen", + .description = "Clear the screen and scrollback.", + }}, + + .select_all => comptime &.{.{ + .action = .select_all, + .title = "Select All", + .description = "Select all text on the screen.", + }}, + + .scroll_to_top => comptime &.{.{ + .action = .scroll_to_top, + .title = "Scroll to Top", + .description = "Scroll to the top of the screen.", + }}, + + .scroll_to_bottom => comptime &.{.{ + .action = .scroll_to_bottom, + .title = "Scroll to Bottom", + .description = "Scroll to the bottom of the screen.", + }}, + + .scroll_page_up => comptime &.{.{ + .action = .scroll_page_up, + .title = "Scroll Page Up", + .description = "Scroll the screen up by a page.", + }}, + + .scroll_page_down => comptime &.{.{ + .action = .scroll_page_down, + .title = "Scroll Page Down", + .description = "Scroll the screen down by a page.", + }}, + + .write_screen_file => comptime &.{ + .{ + .action = .{ .write_screen_file = .paste }, + .title = "Copy Screen to Temporary File and Paste Path", + .description = "Copy the screen contents to a temporary file and paste the path to the file.", + }, + .{ + .action = .{ .write_screen_file = .open }, + .title = "Copy Screen to Temporary File and Open", + .description = "Copy the screen contents to a temporary file and open it.", + }, + }, + + .write_selection_file => comptime &.{ + .{ + .action = .{ .write_selection_file = .paste }, + .title = "Copy Selection to Temporary File and Paste Path", + .description = "Copy the selection contents to a temporary file and paste the path to the file.", + }, + .{ + .action = .{ .write_selection_file = .open }, + .title = "Copy Selection to Temporary File and Open", + .description = "Copy the selection contents to a temporary file and open it.", + }, + }, + + .new_window => comptime &.{.{ + .action = .new_window, + .title = "New Window", + .description = "Open a new window.", + }}, + + .new_tab => comptime &.{.{ + .action = .new_tab, + .title = "New Tab", + .description = "Open a new tab.", + }}, + + .move_tab => comptime &.{ + .{ + .action = .{ .move_tab = -1 }, + .title = "Move Tab Left", + .description = "Move the current tab to the left.", + }, + .{ + .action = .{ .move_tab = 1 }, + .title = "Move Tab Right", + .description = "Move the current tab to the right.", + }, + }, + + .toggle_tab_overview => comptime &.{.{ + .action = .toggle_tab_overview, + .title = "Toggle Tab Overview", + .description = "Toggle the tab overview.", + }}, + + .prompt_surface_title => comptime &.{.{ + .action = .prompt_surface_title, + .title = "Change Title...", + .description = "Prompt for a new title for the current terminal.", + }}, + + .new_split => comptime &.{ + .{ + .action = .{ .new_split = .left }, + .title = "Split Left", + .description = "Split the terminal to the left.", + }, + .{ + .action = .{ .new_split = .right }, + .title = "Split Right", + .description = "Split the terminal to the right.", + }, + .{ + .action = .{ .new_split = .up }, + .title = "Split Up", + .description = "Split the terminal up.", + }, + .{ + .action = .{ .new_split = .down }, + .title = "Split Down", + .description = "Split the terminal down.", + }, + }, + + .toggle_split_zoom => comptime &.{.{ + .action = .toggle_split_zoom, + .title = "Toggle Split Zoom", + .description = "Toggle the zoom state of the current split.", + }}, + + .equalize_splits => comptime &.{.{ + .action = .equalize_splits, + .title = "Equalize Splits", + .description = "Equalize the size of all splits.", + }}, + + .reset_window_size => comptime &.{.{ + .action = .reset_window_size, + .title = "Reset Window Size", + .description = "Reset the window size to the default.", + }}, + + .inspector => comptime &.{.{ + .action = .{ .inspector = .toggle }, + .title = "Toggle Inspector", + .description = "Toggle the inspector.", + }}, + + .open_config => comptime &.{.{ + .action = .open_config, + .title = "Open Config", + .description = "Open the config file.", + }}, + + .reload_config => comptime &.{.{ + .action = .reload_config, + .title = "Reload Config", + .description = "Reload the config file.", + }}, + + .close_surface => comptime &.{.{ + .action = .close_surface, + .title = "Close Terminal", + .description = "Close the current terminal.", + }}, + + .close_tab => comptime &.{.{ + .action = .close_tab, + .title = "Close Tab", + .description = "Close the current tab.", + }}, + + .close_window => comptime &.{.{ + .action = .close_window, + .title = "Close Window", + .description = "Close the current window.", + }}, + + .close_all_windows => comptime &.{.{ + .action = .close_all_windows, + .title = "Close All Windows", + .description = "Close all windows.", + }}, + + .toggle_maximize => comptime &.{.{ + .action = .toggle_maximize, + .title = "Toggle Maximize", + .description = "Toggle the maximized state of the current window.", + }}, + + .toggle_fullscreen => comptime &.{.{ + .action = .toggle_fullscreen, + .title = "Toggle Fullscreen", + .description = "Toggle the fullscreen state of the current window.", + }}, + + .toggle_window_decorations => comptime &.{.{ + .action = .toggle_window_decorations, + .title = "Toggle Window Decorations", + .description = "Toggle the window decorations.", + }}, + + .toggle_secure_input => comptime &.{.{ + .action = .toggle_secure_input, + .title = "Toggle Secure Input", + .description = "Toggle secure input mode.", + }}, + + .quit => comptime &.{.{ + .action = .quit, + .title = "Quit", + .description = "Quit the application.", + }}, + + // No commands because they're parameterized and there + // aren't obvious values users would use. It is possible that + // these may have commands in the future if there are very + // common values that users tend to use. + .csi, + .esc, + .text, + .cursor_key, + .scroll_page_fractional, + .scroll_page_lines, + .adjust_selection, + .jump_to_prompt, + .write_scrollback_file, + .goto_tab, + .goto_split, + .resize_split, + .crash, + => comptime &.{}, + + // No commands because I'm not sure they make sense in a command + // palette context. + .toggle_quick_terminal, + .toggle_visibility, + .previous_tab, + .next_tab, + .last_tab, + => comptime &.{}, + + // No commands for obvious reasons + .ignore, + .unbind, + => comptime &.{}, + }; + + // All generated commands should have the same action as the + // action passed in. + for (result) |cmd| assert(cmd.action == action); + + return result; +} + +test "command defaults" { + // This just ensures that defaults is analyzed and works. + const testing = std.testing; + try testing.expect(defaults.len > 0); + try testing.expectEqual(defaults.len, defaultsC.len); +}