feat(input): port keyboard, keybinding and mouse input handling

Ports ZIG/src/input (except macOS keymaps) into ghostty-input:
- key, key_mods (incl. RemapSet), the physical keycode table
- key_encode: legacy, modifyOtherKeys and kitty keyboard protocol
  encoders, function key tables
- Binding: all keybinding actions with their user-visible docs (for
  +list-actions), triggers, sets with sequences, chains, leader keys and
  key tables, and formatting back to config strings
- command palette defaults, mouse, paste safety checks, kitty helpers,
  links, helpgen action docs

Deviations (docs/rewrite/notes/ghostty-input.md): Mods.super is named
super_; case folding uses char::to_lowercase plus Ghostty's special
cases instead of uucode tables; chain parents are trigger paths instead
of pointers; a Zig bug where chained leaves inside sequences corrupted the
shared prefix when formatting is fixed (covered by a test).

Integration left: building encode/paste Options from terminal modes,
and implementing the EntryFormatter trait in ghostty-config.

Test Plan:
- cargo test -p ghostty-input (220 tests; 8 macOS-only skipped)
- cargo clippy -p ghostty-input --all-targets -- -D warnings
This commit is contained in:
ddidderr committed 2026-10-11 21:56:07 +02:00
1 parent 3323c3f525
commit 3afe938b95
21 files changed
+13413 -1

No files matched your search

+590
View File
@@ -0,0 +1,590 @@
//! Command palette entries (ZIG/src/input/command.zig).
use std::sync::OnceLock;
use crate::binding::{
Action, CloseTabMode, CopyToClipboard, GotoWindow, InspectorMode, NavigateSearch,
SplitDirection, SplitFocusDirection, WriteScreen, WriteScreenAction, WriteScreenFormat,
};
/// 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.
#[derive(Debug, Clone, PartialEq)]
pub struct Command {
pub action: Action,
pub title: String,
pub description: String,
}
impl Command {
pub fn new(action: Action, title: &str, description: &str) -> Command {
Command {
action,
title: title.to_string(),
description: description.to_string(),
}
}
pub fn equal(&self, other: &Command) -> bool {
if self.action.hash() != other.action.hash() {
return false;
}
self.title == other.title && self.description == other.description
}
/// Implements a comparison function for sorting. 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 less_than(lhs: &Command, rhs: &Command) -> bool {
cmp_ignore_case(&lhs.title, &rhs.title) == std::cmp::Ordering::Less
}
/// The `ghostty_command_s` equivalent: (action key name, action string,
/// title, description).
pub fn cval(&self) -> CommandC {
CommandC {
action_key: self.action.name().to_string(),
action: self.action.to_string(),
title: self.title.clone(),
description: self.description.clone(),
}
}
}
/// Owned form of `Command.C`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CommandC {
pub action_key: String,
pub action: String,
pub title: String,
pub description: String,
}
/// `std.ascii.orderIgnoreCase`
fn cmp_ignore_case(a: &str, b: &str) -> std::cmp::Ordering {
let a = a.bytes().map(|c| c.to_ascii_lowercase());
let b = b.bytes().map(|c| c.to_ascii_lowercase());
a.cmp(b)
}
/// The default command palette entries, sorted by title (ignoring case).
pub fn defaults() -> &'static [Command] {
static DEFAULTS: OnceLock<Vec<Command>> = OnceLock::new();
DEFAULTS.get_or_init(|| {
let mut result = default_commands();
result.sort_by(|a, b| cmp_ignore_case(&a.title, &b.title));
result
})
}
/// Defaults in C-compatible form.
pub fn defaults_c() -> Vec<CommandC> {
defaults().iter().map(Command::cval).collect()
}
// The set of commands associated with each 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. Actions without commands (parameterized actions without obvious
// values, ignore/unbind, etc.) are: csi, esc, cursor_key, set_font_size,
// set_surface_title, set_tab_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, toggle_command_palette, toggle_quick_terminal,
// toggle_visibility, previous_tab, next_tab, last_tab, ignore, unbind.
fn default_commands() -> Vec<Command> {
vec![
Command::new(
Action::Reset,
"Reset Terminal",
"Reset the terminal to a clean state.",
),
Command::new(
Action::CopyToClipboard(CopyToClipboard::Mixed),
"Copy to Clipboard",
"Copy the selected text to the clipboard in both plain and styled formats.",
),
Command::new(
Action::CopyToClipboard(CopyToClipboard::Plain),
"Copy Selection as Plain Text to Clipboard",
"Copy the selected text as plain text to the clipboard.",
),
Command::new(
Action::CopyToClipboard(CopyToClipboard::Vt),
"Copy Selection as ANSI Sequences to Clipboard",
"Copy the selected text as ANSI escape sequences to the clipboard.",
),
Command::new(
Action::CopyToClipboard(CopyToClipboard::Html),
"Copy Selection as HTML to Clipboard",
"Copy the selected text as HTML to the clipboard.",
),
Command::new(
Action::CopyUrlToClipboard,
"Copy URL to Clipboard",
"Copy the URL under the cursor to the clipboard.",
),
Command::new(
Action::CopyTitleToClipboard,
"Copy Terminal Title to Clipboard",
"Copy the terminal title to the clipboard. If the terminal title is not set this has no effect.",
),
Command::new(
Action::PasteFromClipboard,
"Paste from Clipboard",
"Paste the contents of the main clipboard.",
),
Command::new(
Action::PasteFromSelection,
"Paste from Selection",
"Paste the contents of the selection clipboard.",
),
Command::new(
Action::StartSearch,
"Start Search",
"Start a search if one isn't already active.",
),
Command::new(
Action::SearchSelection,
"Search Selection",
"Start a search for the current text selection.",
),
Command::new(
Action::EndSearch,
"End Search",
"End the current search if any and hide any GUI elements.",
),
Command::new(
Action::NavigateSearch(NavigateSearch::Next),
"Next Search Result",
"Navigate to the next search result, if any.",
),
Command::new(
Action::NavigateSearch(NavigateSearch::Previous),
"Previous Search Result",
"Navigate to the previous search result, if any.",
),
Command::new(
Action::IncreaseFontSize(1.0),
"Increase Font Size",
"Increase the font size by 1 point.",
),
Command::new(
Action::DecreaseFontSize(1.0),
"Decrease Font Size",
"Decrease the font size by 1 point.",
),
Command::new(
Action::ResetFontSize,
"Reset Font Size",
"Reset the font size to the default.",
),
Command::new(
Action::ClearScreen,
"Clear Screen",
"Clear the screen and scrollback.",
),
Command::new(
Action::SelectAll,
"Select All",
"Select all text on the screen.",
),
Command::new(
Action::ScrollToTop,
"Scroll to Top",
"Scroll to the top of the screen.",
),
Command::new(
Action::ScrollToBottom,
"Scroll to Bottom",
"Scroll to the bottom of the screen.",
),
Command::new(
Action::ScrollToSelection,
"Scroll to Selection",
"Scroll to the selected text.",
),
Command::new(
Action::ScrollPageUp,
"Scroll Page Up",
"Scroll the screen up by a page.",
),
Command::new(
Action::ScrollPageDown,
"Scroll Page Down",
"Scroll the screen down by a page.",
),
Command::new(
Action::WriteScreenFile(WriteScreen::COPY),
"Copy Screen to Temporary File and Copy Path",
"Copy the screen contents to a temporary file and copy the path to the clipboard.",
),
Command::new(
Action::WriteScreenFile(WriteScreen::PASTE),
"Copy Screen to Temporary File and Paste Path",
"Copy the screen contents to a temporary file and paste the path to the file.",
),
Command::new(
Action::WriteScreenFile(WriteScreen::OPEN),
"Copy Screen to Temporary File and Open",
"Copy the screen contents to a temporary file and open it.",
),
Command::new(
Action::WriteScreenFile(WriteScreen {
action: WriteScreenAction::Copy,
emit: WriteScreenFormat::Html,
}),
"Copy Screen as HTML to Temporary File and Copy Path",
"Copy the screen contents as HTML to a temporary file and copy the path to the clipboard.",
),
Command::new(
Action::WriteScreenFile(WriteScreen {
action: WriteScreenAction::Paste,
emit: WriteScreenFormat::Html,
}),
"Copy Screen as HTML to Temporary File and Paste Path",
"Copy the screen contents as HTML to a temporary file and paste the path to the file.",
),
Command::new(
Action::WriteScreenFile(WriteScreen {
action: WriteScreenAction::Open,
emit: WriteScreenFormat::Html,
}),
"Copy Screen as HTML to Temporary File and Open",
"Copy the screen contents as HTML to a temporary file and open it.",
),
Command::new(
Action::WriteScreenFile(WriteScreen {
action: WriteScreenAction::Copy,
emit: WriteScreenFormat::Vt,
}),
"Copy Screen as ANSI Sequences to Temporary File and Copy Path",
"Copy the screen contents as ANSI escape sequences to a temporary file and copy the path to the clipboard.",
),
Command::new(
Action::WriteScreenFile(WriteScreen {
action: WriteScreenAction::Paste,
emit: WriteScreenFormat::Vt,
}),
"Copy Screen as ANSI Sequences to Temporary File and Paste Path",
"Copy the screen contents as ANSI escape sequences to a temporary file and paste the path to the file.",
),
Command::new(
Action::WriteScreenFile(WriteScreen {
action: WriteScreenAction::Open,
emit: WriteScreenFormat::Vt,
}),
"Copy Screen as ANSI Sequences to Temporary File and Open",
"Copy the screen contents as ANSI escape sequences to a temporary file and open it.",
),
Command::new(
Action::WriteSelectionFile(WriteScreen::COPY),
"Copy Selection to Temporary File and Copy Path",
"Copy the selection contents to a temporary file and copy the path to the clipboard.",
),
Command::new(
Action::WriteSelectionFile(WriteScreen::PASTE),
"Copy Selection to Temporary File and Paste Path",
"Copy the selection contents to a temporary file and paste the path to the file.",
),
Command::new(
Action::WriteSelectionFile(WriteScreen::OPEN),
"Copy Selection to Temporary File and Open",
"Copy the selection contents to a temporary file and open it.",
),
Command::new(
Action::WriteSelectionFile(WriteScreen {
action: WriteScreenAction::Copy,
emit: WriteScreenFormat::Html,
}),
"Copy Selection as HTML to Temporary File and Copy Path",
"Copy the selection contents as HTML to a temporary file and copy the path to the clipboard.",
),
Command::new(
Action::WriteSelectionFile(WriteScreen {
action: WriteScreenAction::Paste,
emit: WriteScreenFormat::Html,
}),
"Copy Selection as HTML to Temporary File and Paste Path",
"Copy the selection contents as HTML to a temporary file and paste the path to the file.",
),
Command::new(
Action::WriteSelectionFile(WriteScreen {
action: WriteScreenAction::Open,
emit: WriteScreenFormat::Html,
}),
"Copy Selection as HTML to Temporary File and Open",
"Copy the selection contents as HTML to a temporary file and open it.",
),
Command::new(
Action::WriteSelectionFile(WriteScreen {
action: WriteScreenAction::Copy,
emit: WriteScreenFormat::Vt,
}),
"Copy Selection as ANSI Sequences to Temporary File and Copy Path",
"Copy the selection contents as ANSI escape sequences to a temporary file and copy the path to the clipboard.",
),
Command::new(
Action::WriteSelectionFile(WriteScreen {
action: WriteScreenAction::Paste,
emit: WriteScreenFormat::Vt,
}),
"Copy Selection as ANSI Sequences to Temporary File and Paste Path",
"Copy the selection contents as ANSI escape sequences to a temporary file and paste the path to the file.",
),
Command::new(
Action::WriteSelectionFile(WriteScreen {
action: WriteScreenAction::Open,
emit: WriteScreenFormat::Vt,
}),
"Copy Selection as ANSI Sequences to Temporary File and Open",
"Copy the selection contents as ANSI escape sequences to a temporary file and open it.",
),
Command::new(Action::NewWindow, "New Window", "Open a new window."),
Command::new(Action::NewTab, "New Tab", "Open a new tab."),
Command::new(
Action::MoveTab(-1),
"Move Tab Left",
"Move the current tab to the left.",
),
Command::new(
Action::MoveTab(1),
"Move Tab Right",
"Move the current tab to the right.",
),
Command::new(
Action::ToggleTabOverview,
"Toggle Tab Overview",
"Toggle the tab overview.",
),
Command::new(
Action::PromptSurfaceTitle,
"Change Terminal Title…",
"Prompt for a new title for the current terminal.",
),
Command::new(
Action::PromptTabTitle,
"Change Tab Title…",
"Prompt for a new title for the current tab.",
),
Command::new(
Action::NewSplit(SplitDirection::Left),
"Split Left",
"Split the terminal to the left.",
),
Command::new(
Action::NewSplit(SplitDirection::Right),
"Split Right",
"Split the terminal to the right.",
),
Command::new(
Action::NewSplit(SplitDirection::Up),
"Split Up",
"Split the terminal up.",
),
Command::new(
Action::NewSplit(SplitDirection::Down),
"Split Down",
"Split the terminal down.",
),
Command::new(
Action::GotoSplit(SplitFocusDirection::Previous),
"Focus Split: Previous",
"Focus the previous split, if any.",
),
Command::new(
Action::GotoSplit(SplitFocusDirection::Next),
"Focus Split: Next",
"Focus the next split, if any.",
),
Command::new(
Action::GotoSplit(SplitFocusDirection::Left),
"Focus Split: Left",
"Focus the split to the left, if it exists.",
),
Command::new(
Action::GotoSplit(SplitFocusDirection::Right),
"Focus Split: Right",
"Focus the split to the right, if it exists.",
),
Command::new(
Action::GotoSplit(SplitFocusDirection::Up),
"Focus Split: Up",
"Focus the split above, if it exists.",
),
Command::new(
Action::GotoSplit(SplitFocusDirection::Down),
"Focus Split: Down",
"Focus the split below, if it exists.",
),
Command::new(
Action::GotoWindow(GotoWindow::Previous),
"Focus Window: Previous",
"Focus the previous window, if any.",
),
Command::new(
Action::GotoWindow(GotoWindow::Next),
"Focus Window: Next",
"Focus the next window, if any.",
),
Command::new(
Action::ToggleSplitZoom,
"Toggle Split Zoom",
"Toggle the zoom state of the current split.",
),
Command::new(
Action::ToggleReadonly,
"Toggle Read-Only Mode",
"Toggle read-only mode for the current surface.",
),
Command::new(
Action::EqualizeSplits,
"Equalize Splits",
"Equalize the size of all splits.",
),
Command::new(
Action::ResetWindowSize,
"Reset Window Size",
"Reset the window size to the default.",
),
Command::new(
Action::Inspector(InspectorMode::Toggle),
"Toggle Inspector",
"Toggle the inspector.",
),
Command::new(
Action::ShowGtkInspector,
"Show the GTK Inspector",
"Show the GTK inspector.",
),
Command::new(
Action::ShowOnScreenKeyboard,
"Show On-Screen Keyboard",
"Show the on-screen keyboard if present.",
),
Command::new(Action::OpenConfig, "Open Config", "Open the config file."),
Command::new(
Action::ReloadConfig,
"Reload Config",
"Reload the config file.",
),
Command::new(
Action::CloseSurface,
"Close Terminal",
"Close the current terminal.",
),
Command::new(
Action::CloseTab(CloseTabMode::This),
"Close Tab",
"Close the current tab.",
),
Command::new(
Action::CloseTab(CloseTabMode::Other),
"Close Other Tabs",
"Close all tabs in this window except the current one.",
),
Command::new(
Action::CloseTab(CloseTabMode::Right),
"Close Tabs to the Right",
"Close all tabs to the right of the current one.",
),
Command::new(
Action::CloseWindow,
"Close Window",
"Close the current window.",
),
Command::new(
Action::CloseAllWindows,
"Close All Windows",
"Close all windows.",
),
Command::new(
Action::ToggleMaximize,
"Toggle Maximize",
"Toggle the maximized state of the current window.",
),
Command::new(
Action::ToggleFullscreen,
"Toggle Fullscreen",
"Toggle the fullscreen state of the current window.",
),
Command::new(
Action::ToggleWindowDecorations,
"Toggle Window Decorations",
"Toggle the window decorations.",
),
Command::new(
Action::ToggleWindowFloatOnTop,
"Toggle Float on Top",
"Toggle the float on top state of the current window.",
),
Command::new(
Action::ToggleSecureInput,
"Toggle Secure Input",
"Toggle secure input mode.",
),
Command::new(
Action::ToggleMouseReporting,
"Toggle Mouse Reporting",
"Toggle whether mouse events are reported to terminal applications.",
),
Command::new(
Action::ToggleBackgroundOpacity,
"Toggle Background Opacity",
"Toggle the background opacity of a window that started transparent.",
),
Command::new(
Action::CheckForUpdates,
"Check for Updates",
"Check for updates to the application.",
),
Command::new(Action::Undo, "Undo", "Undo the last action."),
Command::new(Action::Redo, "Redo", "Redo the last undone action."),
Command::new(Action::Quit, "Quit", "Quit the application."),
Command::new(
Action::Text("👻".to_string()),
"Ghostty",
"Put a little Ghostty in your terminal.",
),
]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn command_defaults() {
// This just ensures that defaults is analyzed and works.
assert_ne!(defaults().len(), 0);
assert_eq!(defaults().len(), defaults_c().len());
}
#[test]
fn command_defaults_sorted_and_roundtrip() {
let d = defaults();
for w in d.windows(2) {
assert_ne!(
cmp_ignore_case(&w[0].title, &w[1].title),
std::cmp::Ordering::Greater
);
}
// Round trip every default action through its string form.
// (`text` is stored raw and escaped on format, so it is excluded.)
for c in d.iter().filter(|c| !matches!(c.action, Action::Text(_))) {
let s = c.action.to_string();
assert_eq!(Action::parse(&s).expect("parse"), c.action, "{s}");
}
}
}