const std = @import("std"); const assert = @import("../quirks.zig").inlineAssert; const Allocator = std.mem.Allocator; const Action = @import("Binding.zig").Action; const i18n = @import("../os/i18n.zig"); /// 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_key: [*:0]const u8, action: [*:0]const u8, title: [*:0]const u8, description: [*:0]const u8, }; pub fn clone(self: *const Command, alloc: Allocator) Allocator.Error!Command { return .{ .action = try self.action.clone(alloc), .title = try alloc.dupeZ(u8, self.title), .description = try alloc.dupeZ(u8, self.description), }; } pub fn equal(self: Command, other: Command) bool { if (self.action.hash() != other.action.hash()) return false; if (!std.mem.eql(u8, self.title, other.title)) return false; if (!std.mem.eql(u8, self.description, other.description)) return false; return true; } /// Convert this command to a C struct at comptime. pub fn comptimeCval(self: Command) C { assert(@inComptime()); return .{ .action_key = @tagName(self.action), .action = std.fmt.comptimePrint("{f}", .{self.action}), .title = self.title, .description = self.description, }; } /// Convert this command to a C struct at runtime. /// /// This shares memory with the original command. /// /// The action string is allocated using the provided allocator. You can /// free the slice directly if you need to but we recommend an arena /// for this. pub fn cval(self: Command, alloc: Allocator) Allocator.Error!C { var buf: std.Io.Writer.Allocating = .init(alloc); defer buf.deinit(); self.action.format(&buf.writer) catch return error.OutOfMemory; const action = try buf.toOwnedSliceSentinel(0); return .{ .action_key = @tagName(self.action), .action = action.ptr, .title = self.title, .description = self.description, }; } pub fn translated(self: Command) Command { return .{ .action = self.action, .title = std.mem.span(i18n._(self.title)), .description = std.mem.span(i18n._(self.description)), }; } /// Implements a comparison function for std.mem.sortUnstable /// and similar functions. The sorting is defined by Ghostty /// to be what we prefer. If a caller wants some other sorting, /// they should do it themselves. pub fn lessThan(_: void, lhs: Command, rhs: Command) bool { return std.ascii.orderIgnoreCase(lhs.title, rhs.title) == .lt; } }; pub const defaults: []const Command = defaults: { @setEvalBranchQuota(100_000); 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; } } std.mem.sortUnstable(Command, &result, {}, Command.lessThan); assert(i == count); const final = result; break :defaults &final; }; /// Defaults in C-compatible form. pub const defaultsC: []const Command.C = defaults: { @setEvalBranchQuota(100_000); 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 = i18n.N_("Reset Terminal"), .description = i18n.N_("Reset the terminal to a clean state."), }}, .copy_to_clipboard => comptime &.{ .{ .action = .{ .copy_to_clipboard = .mixed }, .title = i18n.N_("Copy to Clipboard"), .description = i18n.N_("Copy the selected text to the clipboard in both plain and styled formats."), }, .{ .action = .{ .copy_to_clipboard = .plain }, .title = i18n.N_("Copy Selection as Plain Text to Clipboard"), .description = i18n.N_("Copy the selected text as plain text to the clipboard."), }, .{ .action = .{ .copy_to_clipboard = .vt }, .title = i18n.N_("Copy Selection as ANSI Sequences to Clipboard"), .description = i18n.N_("Copy the selected text as ANSI escape sequences to the clipboard."), }, .{ .action = .{ .copy_to_clipboard = .html }, .title = i18n.N_("Copy Selection as HTML to Clipboard"), .description = i18n.N_("Copy the selected text as HTML to the clipboard."), } }, .copy_url_to_clipboard => comptime &.{.{ .action = .copy_url_to_clipboard, .title = i18n.N_("Copy URL to Clipboard"), .description = i18n.N_("Copy the URL under the cursor to the clipboard."), }}, .copy_title_to_clipboard => comptime &.{.{ .action = .copy_title_to_clipboard, .title = i18n.N_("Copy Terminal Title to Clipboard"), .description = i18n.N_("Copy the terminal title to the clipboard. If the terminal title is not set this has no effect."), }}, .paste_from_clipboard => comptime &.{.{ .action = .paste_from_clipboard, .title = i18n.N_("Paste from Clipboard"), .description = i18n.N_("Paste the contents of the main clipboard."), }}, .paste_from_selection => comptime &.{.{ .action = .paste_from_selection, .title = i18n.N_("Paste from Selection"), .description = i18n.N_("Paste the contents of the selection clipboard."), }}, .start_search => comptime &.{.{ .action = .start_search, .title = i18n.N_("Start Search"), .description = i18n.N_("Start a search if one isn't already active."), }}, .search_selection => comptime &.{.{ .action = .search_selection, .title = i18n.N_("Search Selection"), .description = i18n.N_("Start a search for the current text selection."), }}, .end_search => comptime &.{.{ .action = .end_search, .title = i18n.N_("End Search"), .description = i18n.N_("End the current search if any and hide any GUI elements."), }}, .navigate_search => comptime &.{ .{ .action = .{ .navigate_search = .next }, .title = i18n.N_("Next Search Result"), .description = i18n.N_("Navigate to the next search result, if any."), }, .{ .action = .{ .navigate_search = .previous }, .title = i18n.N_("Previous Search Result"), .description = i18n.N_("Navigate to the previous search result, if any."), } }, .increase_font_size => comptime &.{.{ .action = .{ .increase_font_size = 1 }, .title = i18n.N_("Increase Font Size"), .description = i18n.N_("Increase the font size by 1 point."), }}, .decrease_font_size => comptime &.{.{ .action = .{ .decrease_font_size = 1 }, .title = i18n.N_("Decrease Font Size"), .description = i18n.N_("Decrease the font size by 1 point."), }}, .reset_font_size => comptime &.{.{ .action = .reset_font_size, .title = i18n.N_("Reset Font Size"), .description = i18n.N_("Reset the font size to the default."), }}, .clear_screen => comptime &.{.{ .action = .clear_screen, .title = i18n.N_("Clear Screen"), .description = i18n.N_("Clear the screen and scrollback."), }}, .select_all => comptime &.{.{ .action = .select_all, .title = i18n.N_("Select All"), .description = i18n.N_("Select all text on the screen."), }}, .scroll_to_top => comptime &.{.{ .action = .scroll_to_top, .title = i18n.N_("Scroll to Top"), .description = i18n.N_("Scroll to the top of the screen."), }}, .scroll_to_bottom => comptime &.{.{ .action = .scroll_to_bottom, .title = i18n.N_("Scroll to Bottom"), .description = i18n.N_("Scroll to the bottom of the screen."), }}, .scroll_to_selection => comptime &.{.{ .action = .scroll_to_selection, .title = i18n.N_("Scroll to Selection"), .description = i18n.N_("Scroll to the selected text."), }}, .scroll_page_up => comptime &.{.{ .action = .scroll_page_up, .title = i18n.N_("Scroll Page Up"), .description = i18n.N_("Scroll the screen up by a page."), }}, .scroll_page_down => comptime &.{.{ .action = .scroll_page_down, .title = i18n.N_("Scroll Page Down"), .description = i18n.N_("Scroll the screen down by a page."), }}, .write_screen_file => comptime &.{ .{ .action = .{ .write_screen_file = .copy }, .title = i18n.N_("Copy Screen to Temporary File and Copy Path"), .description = i18n.N_("Copy the screen contents to a temporary file and copy the path to the clipboard."), }, .{ .action = .{ .write_screen_file = .paste }, .title = i18n.N_("Copy Screen to Temporary File and Paste Path"), .description = i18n.N_("Copy the screen contents to a temporary file and paste the path to the file."), }, .{ .action = .{ .write_screen_file = .open }, .title = i18n.N_("Copy Screen to Temporary File and Open"), .description = i18n.N_("Copy the screen contents to a temporary file and open it."), }, .{ .action = .{ .write_screen_file = .{ .action = .copy, .emit = .html, } }, .title = i18n.N_("Copy Screen as HTML to Temporary File and Copy Path"), .description = i18n.N_("Copy the screen contents as HTML to a temporary file and copy the path to the clipboard."), }, .{ .action = .{ .write_screen_file = .{ .action = .paste, .emit = .html, } }, .title = i18n.N_("Copy Screen as HTML to Temporary File and Paste Path"), .description = i18n.N_("Copy the screen contents as HTML to a temporary file and paste the path to the file."), }, .{ .action = .{ .write_screen_file = .{ .action = .open, .emit = .html, } }, .title = i18n.N_("Copy Screen as HTML to Temporary File and Open"), .description = i18n.N_("Copy the screen contents as HTML to a temporary file and open it."), }, .{ .action = .{ .write_screen_file = .{ .action = .copy, .emit = .vt, } }, .title = i18n.N_("Copy Screen as ANSI Sequences to Temporary File and Copy Path"), .description = i18n.N_("Copy the screen contents as ANSI escape sequences to a temporary file and copy the path to the clipboard."), }, .{ .action = .{ .write_screen_file = .{ .action = .paste, .emit = .vt, } }, .title = i18n.N_("Copy Screen as ANSI Sequences to Temporary File and Paste Path"), .description = i18n.N_("Copy the screen contents as ANSI escape sequences to a temporary file and paste the path to the file."), }, .{ .action = .{ .write_screen_file = .{ .action = .open, .emit = .vt, } }, .title = i18n.N_("Copy Screen as ANSI Sequences to Temporary File and Open"), .description = i18n.N_("Copy the screen contents as ANSI escape sequences to a temporary file and open it."), }, }, .write_selection_file => comptime &.{ .{ .action = .{ .write_selection_file = .copy }, .title = i18n.N_("Copy Selection to Temporary File and Copy Path"), .description = i18n.N_("Copy the selection contents to a temporary file and copy the path to the clipboard."), }, .{ .action = .{ .write_selection_file = .paste }, .title = i18n.N_("Copy Selection to Temporary File and Paste Path"), .description = i18n.N_("Copy the selection contents to a temporary file and paste the path to the file."), }, .{ .action = .{ .write_selection_file = .open }, .title = i18n.N_("Copy Selection to Temporary File and Open"), .description = i18n.N_("Copy the selection contents to a temporary file and open it."), }, .{ .action = .{ .write_selection_file = .{ .action = .copy, .emit = .html, } }, .title = i18n.N_("Copy Selection as HTML to Temporary File and Copy Path"), .description = i18n.N_("Copy the selection contents as HTML to a temporary file and copy the path to the clipboard."), }, .{ .action = .{ .write_selection_file = .{ .action = .paste, .emit = .html, } }, .title = i18n.N_("Copy Selection as HTML to Temporary File and Paste Path"), .description = i18n.N_("Copy the selection contents as HTML to a temporary file and paste the path to the file."), }, .{ .action = .{ .write_selection_file = .{ .action = .open, .emit = .html, } }, .title = i18n.N_("Copy Selection as HTML to Temporary File and Open"), .description = i18n.N_("Copy the selection contents as HTML to a temporary file and open it."), }, .{ .action = .{ .write_selection_file = .{ .action = .copy, .emit = .vt, } }, .title = i18n.N_("Copy Selection as ANSI Sequences to Temporary File and Copy Path"), .description = i18n.N_("Copy the selection contents as ANSI escape sequences to a temporary file and copy the path to the clipboard."), }, .{ .action = .{ .write_selection_file = .{ .action = .paste, .emit = .vt, } }, .title = i18n.N_("Copy Selection as ANSI Sequences to Temporary File and Paste Path"), .description = i18n.N_("Copy the selection contents as ANSI escape sequences to a temporary file and paste the path to the file."), }, .{ .action = .{ .write_selection_file = .{ .action = .open, .emit = .vt, } }, .title = i18n.N_("Copy Selection as ANSI Sequences to Temporary File and Open"), .description = i18n.N_("Copy the selection contents as ANSI escape sequences to a temporary file and open it."), }, }, .new_window => comptime &.{.{ .action = .new_window, .title = i18n.N_("New Window"), .description = i18n.N_("Open a new window."), }}, .new_tab => comptime &.{.{ .action = .new_tab, .title = i18n.N_("New Tab"), .description = i18n.N_("Open a new tab."), }}, .move_tab => comptime &.{ .{ .action = .{ .move_tab = -1 }, .title = i18n.N_("Move Tab Left"), .description = i18n.N_("Move the current tab to the left."), }, .{ .action = .{ .move_tab = 1 }, .title = i18n.N_("Move Tab Right"), .description = i18n.N_("Move the current tab to the right."), }, }, .move_tab_to_new_window => comptime &.{.{ .action = .move_tab_to_new_window, .title = i18n.N_("Move Tab to New Window"), .description = i18n.N_("Move the current tab to a new window."), }}, .toggle_tab_overview => comptime &.{.{ .action = .toggle_tab_overview, .title = i18n.N_("Toggle Tab Overview"), .description = i18n.N_("Toggle the tab overview."), }}, .prompt_surface_title => comptime &.{.{ .action = .prompt_surface_title, .title = i18n.N_("Change Terminal Title…"), .description = i18n.N_("Prompt for a new title for the current terminal."), }}, .prompt_tab_title => comptime &.{.{ .action = .prompt_tab_title, .title = i18n.N_("Change Tab Title…"), .description = i18n.N_("Prompt for a new title for the current tab."), }}, .prompt_window_title => comptime &.{.{ .action = .prompt_window_title, .title = "Change Window Title…", .description = "Prompt for a new title for the current window.", }}, .new_split => comptime &.{ .{ .action = .{ .new_split = .left }, .title = i18n.N_("Split Left"), .description = i18n.N_("Split the terminal to the left."), }, .{ .action = .{ .new_split = .right }, .title = i18n.N_("Split Right"), .description = i18n.N_("Split the terminal to the right."), }, .{ .action = .{ .new_split = .up }, .title = i18n.N_("Split Up"), .description = i18n.N_("Split the terminal up."), }, .{ .action = .{ .new_split = .down }, .title = i18n.N_("Split Down"), .description = i18n.N_("Split the terminal down."), }, }, .goto_split => comptime &.{ .{ .action = .{ .goto_split = .previous }, .title = i18n.N_("Focus Split: Previous"), .description = i18n.N_("Focus the previous split, if any."), }, .{ .action = .{ .goto_split = .next }, .title = i18n.N_("Focus Split: Next"), .description = i18n.N_("Focus the next split, if any."), }, .{ .action = .{ .goto_split = .left }, .title = i18n.N_("Focus Split: Left"), .description = i18n.N_("Focus the split to the left, if it exists."), }, .{ .action = .{ .goto_split = .right }, .title = i18n.N_("Focus Split: Right"), .description = i18n.N_("Focus the split to the right, if it exists."), }, .{ .action = .{ .goto_split = .up }, .title = i18n.N_("Focus Split: Up"), .description = i18n.N_("Focus the split above, if it exists."), }, .{ .action = .{ .goto_split = .down }, .title = i18n.N_("Focus Split: Down"), .description = i18n.N_("Focus the split below, if it exists."), }, }, .goto_window => comptime &.{ .{ .action = .{ .goto_window = .previous }, .title = i18n.N_("Focus Window: Previous"), .description = i18n.N_("Focus the previous window, if any."), }, .{ .action = .{ .goto_window = .next }, .title = i18n.N_("Focus Window: Next"), .description = i18n.N_("Focus the next window, if any."), }, }, .toggle_split_zoom => comptime &.{.{ .action = .toggle_split_zoom, .title = i18n.N_("Toggle Split Zoom"), .description = i18n.N_("Toggle the zoom state of the current split."), }}, .toggle_readonly => comptime &.{.{ .action = .toggle_readonly, .title = i18n.N_("Toggle Read-Only Mode"), .description = i18n.N_("Toggle read-only mode for the current surface."), }}, .equalize_splits => comptime &.{.{ .action = .equalize_splits, .title = i18n.N_("Equalize Splits"), .description = i18n.N_("Equalize the size of all splits."), }}, .reset_window_size => comptime &.{.{ .action = .reset_window_size, .title = i18n.N_("Reset Window Size"), .description = i18n.N_("Reset the window size to the default."), }}, .inspector => comptime &.{.{ .action = .{ .inspector = .toggle }, .title = i18n.N_("Toggle Inspector"), .description = i18n.N_("Toggle the inspector."), }}, .show_gtk_inspector => comptime &.{.{ .action = .show_gtk_inspector, .title = i18n.N_("Show the GTK Inspector"), .description = i18n.N_("Show the GTK inspector."), }}, .show_on_screen_keyboard => comptime &.{.{ .action = .show_on_screen_keyboard, .title = i18n.N_("Show On-Screen Keyboard"), .description = i18n.N_("Show the on-screen keyboard if present."), }}, .open_config => comptime &.{ .{ .action = .{ .open_config = .os_open }, .title = i18n.N_("Open Config Using OS editor"), .description = i18n.N_("Open the config file with the OS's default editor."), }, .{ .action = .{ .open_config = .new_window }, .title = i18n.N_("Open Config in New Terminal Window"), .description = i18n.N_("Open the config file in a new window using $EDITOR or $VISUAL."), }, }, .reload_config => comptime &.{.{ .action = .reload_config, .title = i18n.N_("Reload Config"), .description = i18n.N_("Reload the config file."), }}, .close_surface => comptime &.{.{ .action = .close_surface, .title = i18n.N_("Close Terminal"), .description = i18n.N_("Close the current terminal."), }}, .close_tab => comptime &.{ .{ .action = .{ .close_tab = .this }, .title = i18n.N_("Close Tab"), .description = i18n.N_("Close the current tab."), }, .{ .action = .{ .close_tab = .other }, .title = i18n.N_("Close Other Tabs"), .description = i18n.N_("Close all tabs in this window except the current one."), }, .{ .action = .{ .close_tab = .right }, .title = i18n.N_("Close Tabs to the Right"), .description = i18n.N_("Close all tabs to the right of the current one."), }, }, .close_window => comptime &.{.{ .action = .close_window, .title = i18n.N_("Close Window"), .description = i18n.N_("Close the current window."), }}, .close_all_windows => comptime &.{.{ .action = .close_all_windows, .title = i18n.N_("Close All Windows"), .description = i18n.N_("Close all windows."), }}, .toggle_maximize => comptime &.{.{ .action = .toggle_maximize, .title = i18n.N_("Toggle Maximize"), .description = i18n.N_("Toggle the maximized state of the current window."), }}, .toggle_fullscreen => comptime &.{.{ .action = .toggle_fullscreen, .title = i18n.N_("Toggle Fullscreen"), .description = i18n.N_("Toggle the fullscreen state of the current window."), }}, .toggle_window_decorations => comptime &.{.{ .action = .toggle_window_decorations, .title = i18n.N_("Toggle Window Decorations"), .description = i18n.N_("Toggle the window decorations."), }}, .toggle_window_float_on_top => comptime &.{.{ .action = .toggle_window_float_on_top, .title = i18n.N_("Toggle Float on Top"), .description = i18n.N_("Toggle the float on top state of the current window."), }}, .toggle_secure_input => comptime &.{.{ .action = .toggle_secure_input, .title = i18n.N_("Toggle Secure Input"), .description = i18n.N_("Toggle secure input mode."), }}, .toggle_mouse_reporting => comptime &.{.{ .action = .toggle_mouse_reporting, .title = i18n.N_("Toggle Mouse Reporting"), .description = i18n.N_("Toggle whether mouse events are reported to terminal applications."), }}, .toggle_background_opacity => comptime &.{.{ .action = .toggle_background_opacity, .title = i18n.N_("Toggle Background Opacity"), .description = i18n.N_("Toggle the background opacity of a window that started transparent."), }}, .check_for_updates => comptime &.{.{ .action = .check_for_updates, .title = i18n.N_("Check for Updates"), .description = i18n.N_("Check for updates to the application."), }}, .undo => comptime &.{.{ .action = .undo, .title = i18n.N_("Undo"), .description = i18n.N_("Undo the last action."), }}, .redo => comptime &.{.{ .action = .redo, .title = i18n.N_("Redo"), .description = i18n.N_("Redo the last undone action."), }}, .quit => comptime &.{.{ .action = .quit, .title = i18n.N_("Quit"), .description = i18n.N_("Quit the application."), }}, .text => comptime &.{.{ .action = .{ .text = "👻" }, .title = i18n.N_("Ghostty"), .description = i18n.N_("Put a little Ghostty in your terminal."), }}, // 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, .cursor_key, .set_font_size, .set_surface_title, .set_tab_title, .set_window_title, .search, .scroll_to_row, .scroll_page_fractional, .scroll_page_lines, .adjust_selection, .jump_to_prompt, .write_scrollback_file, .goto_tab, .resize_split, .activate_key_table, .activate_key_table_once, .deactivate_key_table, .deactivate_all_key_tables, .end_key_sequence, .crash, => comptime &.{}, // No commands because I'm not sure they make sense in a command // palette context. .toggle_command_palette, .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); }