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

Generated
+1
View File
@@ -623,6 +623,7 @@ version = "0.1.0"
dependencies = [
"base64",
"log",
"memchr",
"thiserror",
]
+2
View File
@@ -6,6 +6,8 @@ license.workspace = true
[dependencies]
ghostty-vt.workspace = true
indexmap = "2.14.2"
thiserror = "2.0.21"
[lints]
workspace = true
+3004
View File
@@ -0,0 +1,3004 @@
//! The set of actions that a keybinding can take (`Binding.Action` in
//! ZIG/src/input/Binding.zig).
#![allow(clippy::needless_raw_string_hashes)]
use std::fmt;
use std::hash::{Hash, Hasher};
use super::Error;
/// Define a fieldless enum whose variants are named in snake_case in the
/// Zig source (and therefore in configuration / `+list-actions`).
macro_rules! str_enum {
(
$(#[$meta:meta])*
pub enum $name:ident {
$($(#[$vmeta:meta])* $variant:ident => $s:literal,)*
}
) => {
$(#[$meta])*
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum $name {
$($(#[$vmeta])* $variant,)*
}
impl $name {
/// The Zig tag name.
pub fn name(self) -> &'static str {
match self {
$($name::$variant => $s,)*
}
}
/// Look up by Zig tag name.
pub fn from_name(s: &str) -> Option<$name> {
match s {
$($s => Some($name::$variant),)*
_ => None,
}
}
}
};
}
/// Implements `parse_param` for plain string enums.
macro_rules! plain_param {
($name:ident) => {
impl $name {
pub(super) fn parse_param(s: &str) -> Result<$name, Error> {
$name::from_name(s).ok_or(Error::InvalidFormat)
}
}
};
}
/// The scope of an action. The scope is the context in which an action
/// must be executed.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Scope {
App,
Surface,
}
str_enum! {
pub enum CrashThread {
Main => "main",
Io => "io",
Render => "render",
}
}
plain_param!(CrashThread);
/// Data to send depending on whether cursor key mode is enabled.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CursorKey {
pub normal: String,
pub application: String,
}
str_enum! {
pub enum NavigateSearch {
Previous => "previous",
Next => "next",
}
}
plain_param!(NavigateSearch);
str_enum! {
pub enum AdjustSelection {
Left => "left",
Right => "right",
Up => "up",
Down => "down",
PageUp => "page_up",
PageDown => "page_down",
Home => "home",
End => "end",
BeginningOfLine => "beginning_of_line",
EndOfLine => "end_of_line",
}
}
plain_param!(AdjustSelection);
str_enum! {
pub enum SplitDirection {
Right => "right",
Down => "down",
Left => "left",
Up => "up",
/// splits along the larger direction
Auto => "auto",
}
}
plain_param!(SplitDirection);
impl SplitDirection {
pub const DEFAULT: SplitDirection = SplitDirection::Auto;
}
str_enum! {
pub enum SplitFocusDirection {
Previous => "previous",
Next => "next",
Up => "up",
Left => "left",
Down => "down",
Right => "right",
}
}
impl SplitFocusDirection {
pub fn parse(input: &str) -> Result<SplitFocusDirection, Error> {
if let Some(v) = SplitFocusDirection::from_name(input) {
return Ok(v);
}
// For backwards compatibility we map "top" and "bottom" onto the enum
// values "up" and "down"
match input {
"top" => Ok(SplitFocusDirection::Up),
"bottom" => Ok(SplitFocusDirection::Down),
_ => Err(Error::InvalidFormat),
}
}
pub(super) fn parse_param(s: &str) -> Result<SplitFocusDirection, Error> {
SplitFocusDirection::parse(s)
}
}
str_enum! {
pub enum SplitResizeDirection {
Up => "up",
Down => "down",
Left => "left",
Right => "right",
}
}
plain_param!(SplitResizeDirection);
str_enum! {
pub enum GotoWindow {
Previous => "previous",
Next => "next",
}
}
plain_param!(GotoWindow);
/// Zig: a tuple `struct { SplitResizeDirection, u16 }`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SplitResizeParameter(pub SplitResizeDirection, pub u16);
impl SplitResizeParameter {
pub(super) fn parse_param(param: &str) -> Result<SplitResizeParameter, Error> {
let mut it = param.split(',');
let dir = SplitResizeDirection::parse_param(it.next().ok_or(Error::InvalidFormat)?)?;
let amt = it
.next()
.ok_or(Error::InvalidFormat)?
.parse::<u16>()
.map_err(|_| Error::InvalidFormat)?;
// If we have extra parameters it is an error
if it.next().is_some() {
return Err(Error::InvalidFormat);
}
Ok(SplitResizeParameter(dir, amt))
}
pub(super) fn format_into(self, out: &mut String) {
out.push_str(self.0.name());
out.push(',');
out.push_str(&self.1.to_string());
}
}
str_enum! {
pub enum CopyToClipboard {
Plain => "plain",
Vt => "vt",
Html => "html",
/// This type will mix multiple distinct types with a set content-type
/// such as text/html for html, so that the OS/application can choose
/// what is best when pasting.
Mixed => "mixed",
}
}
plain_param!(CopyToClipboard);
impl CopyToClipboard {
pub const DEFAULT: CopyToClipboard = CopyToClipboard::Mixed;
}
str_enum! {
pub enum WriteScreenAction {
Copy => "copy",
Paste => "paste",
Open => "open",
}
}
str_enum! {
pub enum WriteScreenFormat {
Plain => "plain",
Vt => "vt",
Html => "html",
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct WriteScreen {
pub action: WriteScreenAction,
pub emit: WriteScreenFormat,
}
impl WriteScreen {
pub const COPY: WriteScreen = WriteScreen {
action: WriteScreenAction::Copy,
emit: WriteScreenFormat::Plain,
};
pub const PASTE: WriteScreen = WriteScreen {
action: WriteScreenAction::Paste,
emit: WriteScreenFormat::Plain,
};
pub const OPEN: WriteScreen = WriteScreen {
action: WriteScreenAction::Open,
emit: WriteScreenFormat::Plain,
};
pub fn parse(param: &str) -> Result<WriteScreen, Error> {
let pe = |s: &str| WriteScreenAction::from_name(s).ok_or(Error::InvalidFormat);
// If we don't have a `,`, default to the plain format. This is
// also very important for backwards compatibility before Ghostty
// 1.3 which didn't support output formats.
let Some(idx) = param.find(',') else {
return Ok(WriteScreen {
action: pe(param)?,
emit: WriteScreenFormat::Plain,
});
};
Ok(WriteScreen {
action: pe(&param[..idx])?,
emit: WriteScreenFormat::from_name(&param[idx + 1..]).ok_or(Error::InvalidFormat)?,
})
}
pub(super) fn parse_param(s: &str) -> Result<WriteScreen, Error> {
WriteScreen::parse(s)
}
pub(super) fn format_into(self, out: &mut String) {
out.push_str(self.action.name());
out.push(',');
out.push_str(self.emit.name());
}
}
str_enum! {
pub enum InspectorMode {
Toggle => "toggle",
Show => "show",
Hide => "hide",
}
}
plain_param!(InspectorMode);
str_enum! {
pub enum CloseTabMode {
This => "this",
Other => "other",
Right => "right",
}
}
plain_param!(CloseTabMode);
impl CloseTabMode {
pub const DEFAULT: CloseTabMode = CloseTabMode::This;
}
/// Integer / float parameter parsing (`parseInt`/`parseFloat`).
pub(super) trait ParseNum: Sized {
fn parse_param(s: &str) -> Result<Self, Error>;
}
macro_rules! num_param {
($($t:ty),*) => {$(
impl ParseNum for $t {
fn parse_param(s: &str) -> Result<$t, Error> {
s.parse::<$t>().map_err(|_| Error::InvalidFormat)
}
}
)*};
}
num_param!(f32, usize, isize, i16);
/// Escape in the style of Zig's `std.zig.stringEscape`.
pub(super) fn string_escape(s: &str, out: &mut String) {
use std::fmt::Write;
for &b in s.as_bytes() {
match b {
b'\n' => out.push_str("\\n"),
b'\r' => out.push_str("\\r"),
b'\t' => out.push_str("\\t"),
b'\\' => out.push_str("\\\\"),
b'"' => out.push_str("\\\""),
b'\'' | b' ' | b'!' | b'#'..=b'&' | b'('..=b'[' | b']'..=b'~' => out.push(b as char),
_ => {
let _ = write!(out, "\\x{b:02x}");
}
}
}
}
impl Eq for Action {}
impl Hash for Action {
fn hash<H: Hasher>(&self, state: &mut H) {
self.hash_into(state);
}
}
impl fmt::Display for Action {
/// Encodes the action back into the format used by `parse`.
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut s = String::new();
self.fmt_into(&mut s);
f.write_str(&s)
}
}
impl Action {
/// Parse an action in the format of "key=value" where key is the
/// action name and value is the action parameter. The parameter
/// is optional depending on the action.
pub fn parse(input: &str) -> Result<Action, Error> {
// Split our action by colon. A colon may not exist for some
// actions so it is optional. The part preceding the colon is the
// action name.
let colon_idx = input.find(':');
let action = &input[..colon_idx.unwrap_or(input.len())];
// An action name is always required
if action.is_empty() {
return Err(Error::InvalidFormat);
}
Action::parse_named(action, colon_idx.map(|i| &input[i + 1..]))
}
/// The action name (Zig `@tagName`).
pub fn name(&self) -> &'static str {
self.key().name()
}
/// The doc comment of this action.
pub fn docs(&self) -> &'static str {
self.key().docs()
}
/// A hash code that can be used to uniquely identify this action.
pub fn hash(&self) -> u64 {
let mut h = std::collections::hash_map::DefaultHasher::new();
self.hash_into(&mut h);
h.finish()
}
/// Compares two actions for equality.
pub fn equal(&self, other: &Action) -> bool {
self == other
}
/// Returns the action if it is of the given scope (Zig `scoped`).
///
/// DEVIATION: Zig returns a distinct comptime-generated union type; here
/// the same `Action` type is returned.
pub fn scoped(&self, s: Scope) -> Option<&Action> {
(self.scope() == s).then_some(self)
}
}
// @generated-once from ZIG/src/input/Binding.zig `Action` (see gen script in notes); now maintained by hand.
/// The set of actions that a keybinding can take.
///
/// The per-action documentation (printed by `ghostty +list-actions --docs`)
/// is available through [`Action::docs`] / [`ActionKey::docs`].
#[derive(Debug, Clone, PartialEq)]
pub enum Action {
// Ignore this key combination.
//
// Ghostty will not process this combination nor forward it to the child
// process within the terminal, but it may still be processed by the OS or
// other applications.
Ignore,
// Unbind a previously bound key binding.
//
// This cannot unbind bindings that were not bound by Ghostty or the user
// (e.g. bindings set by the OS or some other application).
Unbind,
// Send a CSI sequence.
//
// The value should be the CSI sequence without the CSI header (`ESC [` or
// `\x1b[`).
//
// For example, `csi:0m` can be sent to reset all styles of the current text.
Csi(String),
// Send an `ESC` sequence.
Esc(String),
// Send the specified text.
//
// Uses Zig string literal syntax. This is currently not validated.
// If the text is invalid (i.e. contains an invalid escape sequence),
// the error will currently only show up in logs.
Text(String),
// Send data to the pty depending on whether cursor key mode is enabled
// (`application`) or disabled (`normal`).
CursorKey(CursorKey),
// Reset the terminal.
//
// This can fix a lot of issues when a running program puts the terminal
// into a broken state, equivalent to running the `reset` command.
//
// If you do this while in a TUI program such as vim, this may break
// the program. If you do this while in a shell, you may have to press
// enter after to get a new prompt.
Reset,
// Copy the selected text to the clipboard.
CopyToClipboard(CopyToClipboard),
// Paste the contents of the default clipboard.
PasteFromClipboard,
// Paste the contents of the selection clipboard.
PasteFromSelection,
// If there is a URL under the cursor, copy it to the default clipboard.
CopyUrlToClipboard,
// Copy the terminal title to the clipboard. If the terminal title is not
// set or is empty this has no effect.
CopyTitleToClipboard,
// Increase the font size by the specified amount in points (pt).
//
// For example, `increase_font_size:1.5` will increase the font size
// by 1.5 points.
IncreaseFontSize(f32),
// Decrease the font size by the specified amount in points (pt).
//
// For example, `decrease_font_size:1.5` will decrease the font size
// by 1.5 points.
DecreaseFontSize(f32),
// Reset the font size to the original configured size.
ResetFontSize,
// Set the font size to the specified size in points (pt).
//
// For example, `set_font_size:14.5` will set the font size
// to 14.5 points.
SetFontSize(f32),
// Start a search for the given text. If the text is empty, then
// the search is canceled. A canceled search will not disable any GUI
// elements showing search. For that, the explicit end_search binding
// should be used.
//
// If a previous search is active, it is replaced.
Search(String),
// Start a search for the current text selection. If there is no
// selection, this does nothing. If a search is already active, this
// changes the search terms.
SearchSelection,
// Navigate the search results. If there is no active search, this
// is not performed.
NavigateSearch(NavigateSearch),
// Start a search if it isn't started already. This doesn't set any
// search terms, but opens the UI for searching.
StartSearch,
// End the current search if any and hide any GUI elements.
EndSearch,
// Clear the screen and all scrollback.
ClearScreen,
// Select all text on the screen.
SelectAll,
// Scroll to the top of the screen.
ScrollToTop,
// Scroll to the bottom of the screen.
ScrollToBottom,
// Scroll to the selected text.
ScrollToSelection,
// Scroll to the given absolute row in the screen with 0 being
// the first row.
ScrollToRow(usize),
// Scroll the screen up by one page.
ScrollPageUp,
// Scroll the screen down by one page.
ScrollPageDown,
// Scroll the screen by the specified fraction of a page.
//
// Positive values scroll downwards, and negative values scroll upwards.
//
// For example, `scroll_page_fractional:0.5` would scroll the screen
// downwards by half a page, while `scroll_page_fractional:-1.5` would
// scroll it upwards by one and a half pages.
ScrollPageFractional(f32),
// Scroll the screen by the specified amount of lines.
//
// Positive values scroll downwards, and negative values scroll upwards.
//
// For example, `scroll_page_lines:3` would scroll the screen downwards
// by 3 lines, while `scroll_page_lines:-10` would scroll it upwards by 10
// lines.
ScrollPageLines(i16),
// Adjust the current selection in the given direction or position,
// relative to the cursor.
//
// WARNING: This does not create a new selection, and does nothing when
// there currently isn't one.
//
// Valid arguments are:
//
// - `left`, `right`
//
// Adjust the selection one cell to the left or right respectively.
//
// - `up`, `down`
//
// Adjust the selection one line upwards or downwards respectively.
//
// - `page_up`, `page_down`
//
// Adjust the selection one page upwards or downwards respectively.
//
// - `home`, `end`
//
// Adjust the selection to the top-left or the bottom-right corner
// of the screen respectively.
//
// - `beginning_of_line`, `end_of_line`
//
// Adjust the selection to the beginning or the end of the line
// respectively.
//
AdjustSelection(AdjustSelection),
// Jump the viewport forward or back by the given number of prompts.
//
// Requires shell integration.
//
// Positive values scroll downwards, and negative values scroll upwards.
JumpToPrompt(i16),
// Write the entire scrollback into a temporary file with the specified
// action. The action determines what to do with the filepath.
//
// Valid actions are:
//
// - `copy`
//
// Copy the file path into the clipboard.
//
// - `paste`
//
// Paste the file path into the terminal.
//
// - `open`
//
// Open the file in the default OS editor for text files.
//
// The default OS editor is determined by using `open` on macOS
// and `xdg-open` on Linux.
//
WriteScrollbackFile(WriteScreen),
// Write the contents of the screen into a temporary file with the
// specified action.
//
// See `write_scrollback_file` for possible actions.
WriteScreenFile(WriteScreen),
// Write the currently selected text into a temporary file with the
// specified action.
//
// See `write_scrollback_file` for possible actions.
//
// Does nothing when no text is selected.
WriteSelectionFile(WriteScreen),
// Open a new window.
//
// If the application isn't currently focused,
// this will bring it to the front.
NewWindow,
// Open a new tab.
NewTab,
// Go to the previous tab.
PreviousTab,
// Go to the next tab.
NextTab,
// Go to the last tab.
LastTab,
// Go to the tab with the specific index, starting from 1.
//
// If the tab number is higher than the number of tabs,
// this will go to the last tab.
GotoTab(usize),
// Moves a tab by a relative offset.
//
// Positive values move the tab forwards, and negative values move it
// backwards. If the new position is out of bounds, it is wrapped around
// cyclically within the tab list.
//
// For example, `move_tab:1` moves the tab one position forwards, and if
// it was already the last tab in the list, it wraps around and becomes
// the first tab in the list. Likewise, `move_tab:-1` moves the tab one
// position backwards, and if it was the first tab, then it will become
// the last tab.
MoveTab(isize),
// Toggle the tab overview.
//
// This is only supported on Linux and when the system's libadwaita
// version is 1.4 or newer. The current libadwaita version can be
// found by running `ghostty +version`.
ToggleTabOverview,
// Change the title of the current focused surface via a pop-up prompt.
PromptSurfaceTitle,
// Change the title of the current tab via a pop-up prompt. The
// title set via this prompt overrides any title set by the terminal
// and persists across focus changes within the tab.
PromptTabTitle,
// Set the title for the current focused surface.
//
// If the title is empty, the surface title is reset to an empty title.
SetSurfaceTitle(String),
// Set the title for the current focused tab.
//
// If the title is empty, the tab title override is cleared.
SetTabTitle(String),
// Create a new split in the specified direction.
//
// Valid arguments:
//
// - `right`, `down`, `left`, `up`
//
// Creates a new split in the corresponding direction.
//
// - `auto`
//
// Creates a new split along the larger direction.
// For example, if the parent split is currently wider than it is tall,
// then a left-right split would be created, and vice versa.
//
NewSplit(SplitDirection),
// Focus on a split either in the specified direction (`right`, `down`,
// `left` and `up`), or in the adjacent split in the order of creation
// (`previous` and `next`).
GotoSplit(SplitFocusDirection),
// Focus on either the previous window or the next one ('previous', 'next')
GotoWindow(GotoWindow),
// Zoom in or out of the current split.
//
// When a split is zoomed into, it will take up the entire space in
// the current tab, hiding other splits. The tab or tab bar would also
// reflect this by displaying an icon indicating the zoomed state.
ToggleSplitZoom,
// Toggle read-only mode for the current surface.
//
// When a surface is in read-only mode:
// - No input is sent to the PTY (mouse events, key encoding)
// - Input can still be used at the terminal level to make selections,
// copy/paste (keybinds), scroll, etc.
// - Warn before quit is always enabled in this state even if an active
// process is not running
ToggleReadonly,
// Resize the current split in the specified direction and amount in
// pixels. The two arguments should be joined with a comma (`,`),
// like in `resize_split:up,10`.
ResizeSplit(SplitResizeParameter),
// Equalize the size of all splits in the current window.
EqualizeSplits,
// Reset the window to the default size. The "default size" is the
// size that a new window would be created with. This has no effect
// if the window is fullscreen.
//
// Only implemented on macOS.
ResetWindowSize,
// Control the visibility of the terminal inspector.
//
// Valid arguments: `toggle`, `show`, `hide`.
Inspector(InspectorMode),
// Show the GTK inspector.
//
// Has no effect on macOS.
ShowGtkInspector,
// Show the on-screen keyboard if one is present.
//
// Only implemented on Linux (GTK). On GNOME, the "Screen Keyboard"
// accessibility feature must be turned on, which can be found under
// Settings > Accessibility > Typing. Other platforms are as of now
// untested.
ShowOnScreenKeyboard,
// Open the configuration file in the default OS editor.
//
// If your default OS editor isn't configured then this will fail.
// Currently, any failures to open the configuration will show up only in
// the logs.
OpenConfig,
// Reload the configuration.
//
// The exact meaning depends on the app runtime in use, but this usually
// involves re-reading the configuration file and applying any changes
// Note that not all changes can be applied at runtime.
ReloadConfig,
// Close the current "surface", whether that is a window, tab, split, etc.
//
// This might trigger a close confirmation popup, depending on the value
// of the `confirm-close-surface` configuration setting.
CloseSurface,
// Close the current tab and all splits therein, close all other tabs, or
// close every tab to the right of the current one depending on the mode.
//
// If the mode is not specified, defaults to closing the current tab.
//
// This might trigger a close confirmation popup, depending on the value
// of the `confirm-close-surface` configuration setting.
CloseTab(CloseTabMode),
// Close the current window and all tabs and splits therein.
//
// This might trigger a close confirmation popup, depending on the value
// of the `confirm-close-surface` configuration setting.
CloseWindow,
// Close all windows.
//
// WARNING: This action has been deprecated and has no effect on either
// Linux or macOS. Users are instead encouraged to use `all:close_window`
// instead.
CloseAllWindows,
// Maximize or unmaximize the current window.
//
// This has no effect on macOS as it does not have the concept of
// maximized windows.
ToggleMaximize,
// Fullscreen or unfullscreen the current window.
ToggleFullscreen,
// Toggle window decorations (titlebar, buttons, etc.) for the current window.
//
// Only implemented on Linux.
ToggleWindowDecorations,
// Toggle whether the terminal window should always float on top of other
// windows even when unfocused.
//
// Terminal windows always start as normal (not float-on-top) windows.
//
// Only implemented on macOS.
ToggleWindowFloatOnTop,
// Toggle secure input mode.
//
// This is used to prevent apps from monitoring your keyboard input
// when entering passwords or other sensitive information.
//
// This applies to the entire application, not just the focused terminal.
// You must manually untoggle it or quit Ghostty entirely to disable it.
//
// Only implemented on macOS, as this uses a built-in system API.
ToggleSecureInput,
// Toggle mouse reporting on or off.
//
// When mouse reporting is disabled, mouse events will not be reported to
// terminal applications even if they request it. This allows you to always
// use the mouse for selection and other terminal UI interactions without
// applications capturing mouse input.
//
// This can also be controlled via the `mouse-reporting` configuration
// option.
ToggleMouseReporting,
// Toggle the command palette.
//
// The command palette is a popup that lets you see what actions
// you can perform, their associated keybindings (if any), a search bar
// to filter the actions, and the ability to then execute the action.
//
// This requires libadwaita 1.5 or newer on Linux. The current libadwaita
// version can be found by running `ghostty +version`.
ToggleCommandPalette,
// Toggle the quick terminal.
//
// The quick terminal, also known as the "Quake-style" or drop-down
// terminal, is a terminal window that appears on demand from a keybinding,
// often sliding in from a screen edge such as the top. This is useful for
// quick access to a terminal without having to open a new window or tab.
//
// The terminal state is preserved between appearances, so showing the
// quick terminal after it was already hidden would display the same
// window instead of creating a new one.
//
// As quick terminals are often useful when other windows are currently
// focused, they are best used with *global* keybinds. For example, one
// can define the following key bind to toggle the quick terminal from
// anywhere within the system by pressing `` Cmd+` ``:
//
// ```ini
// keybind = global:cmd+backquote=toggle_quick_terminal
// ```
//
// The quick terminal has some limitations:
//
// - Only one quick terminal instance can exist at a time.
//
// - Unlike normal terminal windows, the quick terminal will not be
// restored when the application is restarted on systems that support
// window restoration like macOS.
//
// - On Linux, the quick terminal is only supported on Wayland and not
// X11, and only on Wayland compositors that support the `wlr-layer-shell-v1`
// protocol. In practice, this means that only GNOME users would not be
// able to use this feature.
//
// - On Linux, slide-in animations are only supported on KDE, and when
// the "Sliding Popups" KWin plugin is enabled.
//
// If you do not have this plugin enabled, open System Settings > Apps
// & Windows > Window Management > Desktop Effects, and enable the
// plugin in the plugin list. Ghostty would then need to be restarted
// fully for this to take effect.
//
// - Quick terminal tabs are only supported on Linux and not on macOS.
// This is because tabs on macOS require a title bar.
//
// - On macOS, a fullscreened quick terminal will always be in non-native
// fullscreen mode. This is a requirement due to how the quick terminal
// is rendered.
//
// See the various configurations for the quick terminal in the
// configuration file to customize its behavior.
ToggleQuickTerminal,
// Show or hide all windows. If all windows become shown, we also ensure
// Ghostty becomes focused. When hiding all windows, focus is yielded
// to the next application as determined by the OS.
//
// Note: When the focused surface is fullscreen, this method does nothing.
//
// Only implemented on macOS.
ToggleVisibility,
// Toggle the window background opacity between transparent and opaque.
//
// This does nothing when `background-opacity` is set to 1 or above.
//
// When `background-opacity` is less than 1, this action will either make
// the window transparent or not depending on its current transparency state.
//
// Only implemented on macOS.
ToggleBackgroundOpacity,
// Check for updates.
//
// Only implemented on macOS.
CheckForUpdates,
// Undo the last undoable action for the focused surface or terminal,
// if possible. This can undo actions such as closing tabs or
// windows.
//
// Not every action in Ghostty can be undone or redone. The list
// of actions support undo/redo is currently limited to:
//
// - New window, close window
// - New tab, close tab
// - New split, close split
//
// All actions are only undoable/redoable for a limited time.
// For example, restoring a closed split can only be done for
// some number of seconds since the split was closed. The exact
// amount is configured with the `undo-timeout` configuration settings.
//
// The undo/redo actions being limited ensures that there is
// bounded memory usage over time, closed surfaces don't continue running
// in the background indefinitely, and the keybinds become available
// for terminal applications to use.
//
// Only implemented on macOS.
Undo,
// Redo the last undoable action for the focused surface or terminal,
// if possible. See "undo" for more details on what can and cannot
// be undone or redone.
Redo,
// End the currently active key sequence, if any, and flush the
// keys up to this point to the terminal, excluding the key that
// triggered this action.
//
// For example: `ctrl+w>escape=end_key_sequence` would encode
// `ctrl+w` to the terminal and exit the key sequence.
//
// Normally, an invalid sequence will reset the key sequence and
// flush all data including the invalid key. This action allows
// you to flush only the prior keys, which is useful when you want
// to bind something like a control key (`ctrl+w`) but not send
// additional inputs.
EndKeySequence,
// Activate a named key table (see `keybind` configuration documentation).
// The named key table will remain active until `deactivate_key_table`
// is called. If you want a one-shot key table activation, use the
// `activate_key_table_once` action instead.
//
// If the named key table does not exist, this action has no effect
// and performable will report false.
//
// If the named key table is already the currently active key table,
// this action has no effect and performable will report false.
ActivateKeyTable(String),
// Same as activate_key_table, but the key table will only be active
// until the first valid keybinding from that table is used (including
// any defined `catch_all` bindings).
//
// The "once" check is only done if this is the currently active
// key table. If another key table is activated later, then this
// table will remain active until it pops back out to being the
// active key table.
ActivateKeyTableOnce(String),
// Deactivate the currently active key table, if any. The next most
// recently activated key table (if any) will become active again.
// If no key table is active, this action has no effect.
DeactivateKeyTable,
// Deactivate all active key tables. If no active key table exists,
// this will report performable as false.
DeactivateAllKeyTables,
// Quit Ghostty.
Quit,
// Crash Ghostty in the desired thread for the focused surface.
//
// WARNING: This is a hard crash (panic) and data can be lost.
//
// The purpose of this action is to test crash handling. For some
// users, it may be useful to test crash reporting functionality in
// order to determine if it all works as expected.
//
// The value determines the crash location:
//
// - `main`
//
// Crash on the main (GUI) thread.
//
// - `io`
//
// Crash on the IO thread for the focused surface.
//
// - `render`
//
// Crash on the render thread for the focused surface.
//
Crash(CrashThread),
}
/// The tag of an [`Action`] (Zig `Action.Key`).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum ActionKey {
Ignore,
Unbind,
Csi,
Esc,
Text,
CursorKey,
Reset,
CopyToClipboard,
PasteFromClipboard,
PasteFromSelection,
CopyUrlToClipboard,
CopyTitleToClipboard,
IncreaseFontSize,
DecreaseFontSize,
ResetFontSize,
SetFontSize,
Search,
SearchSelection,
NavigateSearch,
StartSearch,
EndSearch,
ClearScreen,
SelectAll,
ScrollToTop,
ScrollToBottom,
ScrollToSelection,
ScrollToRow,
ScrollPageUp,
ScrollPageDown,
ScrollPageFractional,
ScrollPageLines,
AdjustSelection,
JumpToPrompt,
WriteScrollbackFile,
WriteScreenFile,
WriteSelectionFile,
NewWindow,
NewTab,
PreviousTab,
NextTab,
LastTab,
GotoTab,
MoveTab,
ToggleTabOverview,
PromptSurfaceTitle,
PromptTabTitle,
SetSurfaceTitle,
SetTabTitle,
NewSplit,
GotoSplit,
GotoWindow,
ToggleSplitZoom,
ToggleReadonly,
ResizeSplit,
EqualizeSplits,
ResetWindowSize,
Inspector,
ShowGtkInspector,
ShowOnScreenKeyboard,
OpenConfig,
ReloadConfig,
CloseSurface,
CloseTab,
CloseWindow,
CloseAllWindows,
ToggleMaximize,
ToggleFullscreen,
ToggleWindowDecorations,
ToggleWindowFloatOnTop,
ToggleSecureInput,
ToggleMouseReporting,
ToggleCommandPalette,
ToggleQuickTerminal,
ToggleVisibility,
ToggleBackgroundOpacity,
CheckForUpdates,
Undo,
Redo,
EndKeySequence,
ActivateKeyTable,
ActivateKeyTableOnce,
DeactivateKeyTable,
DeactivateAllKeyTables,
Quit,
Crash,
}
impl ActionKey {
/// All action keys in declaration order.
pub const ALL: &'static [ActionKey] = &[
ActionKey::Ignore,
ActionKey::Unbind,
ActionKey::Csi,
ActionKey::Esc,
ActionKey::Text,
ActionKey::CursorKey,
ActionKey::Reset,
ActionKey::CopyToClipboard,
ActionKey::PasteFromClipboard,
ActionKey::PasteFromSelection,
ActionKey::CopyUrlToClipboard,
ActionKey::CopyTitleToClipboard,
ActionKey::IncreaseFontSize,
ActionKey::DecreaseFontSize,
ActionKey::ResetFontSize,
ActionKey::SetFontSize,
ActionKey::Search,
ActionKey::SearchSelection,
ActionKey::NavigateSearch,
ActionKey::StartSearch,
ActionKey::EndSearch,
ActionKey::ClearScreen,
ActionKey::SelectAll,
ActionKey::ScrollToTop,
ActionKey::ScrollToBottom,
ActionKey::ScrollToSelection,
ActionKey::ScrollToRow,
ActionKey::ScrollPageUp,
ActionKey::ScrollPageDown,
ActionKey::ScrollPageFractional,
ActionKey::ScrollPageLines,
ActionKey::AdjustSelection,
ActionKey::JumpToPrompt,
ActionKey::WriteScrollbackFile,
ActionKey::WriteScreenFile,
ActionKey::WriteSelectionFile,
ActionKey::NewWindow,
ActionKey::NewTab,
ActionKey::PreviousTab,
ActionKey::NextTab,
ActionKey::LastTab,
ActionKey::GotoTab,
ActionKey::MoveTab,
ActionKey::ToggleTabOverview,
ActionKey::PromptSurfaceTitle,
ActionKey::PromptTabTitle,
ActionKey::SetSurfaceTitle,
ActionKey::SetTabTitle,
ActionKey::NewSplit,
ActionKey::GotoSplit,
ActionKey::GotoWindow,
ActionKey::ToggleSplitZoom,
ActionKey::ToggleReadonly,
ActionKey::ResizeSplit,
ActionKey::EqualizeSplits,
ActionKey::ResetWindowSize,
ActionKey::Inspector,
ActionKey::ShowGtkInspector,
ActionKey::ShowOnScreenKeyboard,
ActionKey::OpenConfig,
ActionKey::ReloadConfig,
ActionKey::CloseSurface,
ActionKey::CloseTab,
ActionKey::CloseWindow,
ActionKey::CloseAllWindows,
ActionKey::ToggleMaximize,
ActionKey::ToggleFullscreen,
ActionKey::ToggleWindowDecorations,
ActionKey::ToggleWindowFloatOnTop,
ActionKey::ToggleSecureInput,
ActionKey::ToggleMouseReporting,
ActionKey::ToggleCommandPalette,
ActionKey::ToggleQuickTerminal,
ActionKey::ToggleVisibility,
ActionKey::ToggleBackgroundOpacity,
ActionKey::CheckForUpdates,
ActionKey::Undo,
ActionKey::Redo,
ActionKey::EndKeySequence,
ActionKey::ActivateKeyTable,
ActionKey::ActivateKeyTableOnce,
ActionKey::DeactivateKeyTable,
ActionKey::DeactivateAllKeyTables,
ActionKey::Quit,
ActionKey::Crash,
];
/// The action name as used in keybind configuration.
pub fn name(self) -> &'static str {
match self {
ActionKey::Ignore => "ignore",
ActionKey::Unbind => "unbind",
ActionKey::Csi => "csi",
ActionKey::Esc => "esc",
ActionKey::Text => "text",
ActionKey::CursorKey => "cursor_key",
ActionKey::Reset => "reset",
ActionKey::CopyToClipboard => "copy_to_clipboard",
ActionKey::PasteFromClipboard => "paste_from_clipboard",
ActionKey::PasteFromSelection => "paste_from_selection",
ActionKey::CopyUrlToClipboard => "copy_url_to_clipboard",
ActionKey::CopyTitleToClipboard => "copy_title_to_clipboard",
ActionKey::IncreaseFontSize => "increase_font_size",
ActionKey::DecreaseFontSize => "decrease_font_size",
ActionKey::ResetFontSize => "reset_font_size",
ActionKey::SetFontSize => "set_font_size",
ActionKey::Search => "search",
ActionKey::SearchSelection => "search_selection",
ActionKey::NavigateSearch => "navigate_search",
ActionKey::StartSearch => "start_search",
ActionKey::EndSearch => "end_search",
ActionKey::ClearScreen => "clear_screen",
ActionKey::SelectAll => "select_all",
ActionKey::ScrollToTop => "scroll_to_top",
ActionKey::ScrollToBottom => "scroll_to_bottom",
ActionKey::ScrollToSelection => "scroll_to_selection",
ActionKey::ScrollToRow => "scroll_to_row",
ActionKey::ScrollPageUp => "scroll_page_up",
ActionKey::ScrollPageDown => "scroll_page_down",
ActionKey::ScrollPageFractional => "scroll_page_fractional",
ActionKey::ScrollPageLines => "scroll_page_lines",
ActionKey::AdjustSelection => "adjust_selection",
ActionKey::JumpToPrompt => "jump_to_prompt",
ActionKey::WriteScrollbackFile => "write_scrollback_file",
ActionKey::WriteScreenFile => "write_screen_file",
ActionKey::WriteSelectionFile => "write_selection_file",
ActionKey::NewWindow => "new_window",
ActionKey::NewTab => "new_tab",
ActionKey::PreviousTab => "previous_tab",
ActionKey::NextTab => "next_tab",
ActionKey::LastTab => "last_tab",
ActionKey::GotoTab => "goto_tab",
ActionKey::MoveTab => "move_tab",
ActionKey::ToggleTabOverview => "toggle_tab_overview",
ActionKey::PromptSurfaceTitle => "prompt_surface_title",
ActionKey::PromptTabTitle => "prompt_tab_title",
ActionKey::SetSurfaceTitle => "set_surface_title",
ActionKey::SetTabTitle => "set_tab_title",
ActionKey::NewSplit => "new_split",
ActionKey::GotoSplit => "goto_split",
ActionKey::GotoWindow => "goto_window",
ActionKey::ToggleSplitZoom => "toggle_split_zoom",
ActionKey::ToggleReadonly => "toggle_readonly",
ActionKey::ResizeSplit => "resize_split",
ActionKey::EqualizeSplits => "equalize_splits",
ActionKey::ResetWindowSize => "reset_window_size",
ActionKey::Inspector => "inspector",
ActionKey::ShowGtkInspector => "show_gtk_inspector",
ActionKey::ShowOnScreenKeyboard => "show_on_screen_keyboard",
ActionKey::OpenConfig => "open_config",
ActionKey::ReloadConfig => "reload_config",
ActionKey::CloseSurface => "close_surface",
ActionKey::CloseTab => "close_tab",
ActionKey::CloseWindow => "close_window",
ActionKey::CloseAllWindows => "close_all_windows",
ActionKey::ToggleMaximize => "toggle_maximize",
ActionKey::ToggleFullscreen => "toggle_fullscreen",
ActionKey::ToggleWindowDecorations => "toggle_window_decorations",
ActionKey::ToggleWindowFloatOnTop => "toggle_window_float_on_top",
ActionKey::ToggleSecureInput => "toggle_secure_input",
ActionKey::ToggleMouseReporting => "toggle_mouse_reporting",
ActionKey::ToggleCommandPalette => "toggle_command_palette",
ActionKey::ToggleQuickTerminal => "toggle_quick_terminal",
ActionKey::ToggleVisibility => "toggle_visibility",
ActionKey::ToggleBackgroundOpacity => "toggle_background_opacity",
ActionKey::CheckForUpdates => "check_for_updates",
ActionKey::Undo => "undo",
ActionKey::Redo => "redo",
ActionKey::EndKeySequence => "end_key_sequence",
ActionKey::ActivateKeyTable => "activate_key_table",
ActionKey::ActivateKeyTableOnce => "activate_key_table_once",
ActionKey::DeactivateKeyTable => "deactivate_key_table",
ActionKey::DeactivateAllKeyTables => "deactivate_all_key_tables",
ActionKey::Quit => "quit",
ActionKey::Crash => "crash",
}
}
/// The doc comment of the action (Zig `help_strings.KeybindAction`).
pub fn docs(self) -> &'static str {
match self {
ActionKey::Ignore => {
r##"Ignore this key combination.
Ghostty will not process this combination nor forward it to the child
process within the terminal, but it may still be processed by the OS or
other applications.
"##
}
ActionKey::Unbind => {
r##"Unbind a previously bound key binding.
This cannot unbind bindings that were not bound by Ghostty or the user
(e.g. bindings set by the OS or some other application).
"##
}
ActionKey::Csi => {
r##"Send a CSI sequence.
The value should be the CSI sequence without the CSI header (`ESC [` or
`\x1b[`).
For example, `csi:0m` can be sent to reset all styles of the current text.
"##
}
ActionKey::Esc => {
r##"Send an `ESC` sequence.
"##
}
ActionKey::Text => {
r##"Send the specified text.
Uses Zig string literal syntax. This is currently not validated.
If the text is invalid (i.e. contains an invalid escape sequence),
the error will currently only show up in logs.
"##
}
ActionKey::CursorKey => {
r##"Send data to the pty depending on whether cursor key mode is enabled
(`application`) or disabled (`normal`).
"##
}
ActionKey::Reset => {
r##"Reset the terminal.
This can fix a lot of issues when a running program puts the terminal
into a broken state, equivalent to running the `reset` command.
If you do this while in a TUI program such as vim, this may break
the program. If you do this while in a shell, you may have to press
enter after to get a new prompt.
"##
}
ActionKey::CopyToClipboard => {
r##"Copy the selected text to the clipboard.
"##
}
ActionKey::PasteFromClipboard => {
r##"Paste the contents of the default clipboard.
"##
}
ActionKey::PasteFromSelection => {
r##"Paste the contents of the selection clipboard.
"##
}
ActionKey::CopyUrlToClipboard => {
r##"If there is a URL under the cursor, copy it to the default clipboard.
"##
}
ActionKey::CopyTitleToClipboard => {
r##"Copy the terminal title to the clipboard. If the terminal title is not
set or is empty this has no effect.
"##
}
ActionKey::IncreaseFontSize => {
r##"Increase the font size by the specified amount in points (pt).
For example, `increase_font_size:1.5` will increase the font size
by 1.5 points.
"##
}
ActionKey::DecreaseFontSize => {
r##"Decrease the font size by the specified amount in points (pt).
For example, `decrease_font_size:1.5` will decrease the font size
by 1.5 points.
"##
}
ActionKey::ResetFontSize => {
r##"Reset the font size to the original configured size.
"##
}
ActionKey::SetFontSize => {
r##"Set the font size to the specified size in points (pt).
For example, `set_font_size:14.5` will set the font size
to 14.5 points.
"##
}
ActionKey::Search => {
r##"Start a search for the given text. If the text is empty, then
the search is canceled. A canceled search will not disable any GUI
elements showing search. For that, the explicit end_search binding
should be used.
If a previous search is active, it is replaced.
"##
}
ActionKey::SearchSelection => {
r##"Start a search for the current text selection. If there is no
selection, this does nothing. If a search is already active, this
changes the search terms.
"##
}
ActionKey::NavigateSearch => {
r##"Navigate the search results. If there is no active search, this
is not performed.
"##
}
ActionKey::StartSearch => {
r##"Start a search if it isn't started already. This doesn't set any
search terms, but opens the UI for searching.
"##
}
ActionKey::EndSearch => {
r##"End the current search if any and hide any GUI elements.
"##
}
ActionKey::ClearScreen => {
r##"Clear the screen and all scrollback.
"##
}
ActionKey::SelectAll => {
r##"Select all text on the screen.
"##
}
ActionKey::ScrollToTop => {
r##"Scroll to the top of the screen.
"##
}
ActionKey::ScrollToBottom => {
r##"Scroll to the bottom of the screen.
"##
}
ActionKey::ScrollToSelection => {
r##"Scroll to the selected text.
"##
}
ActionKey::ScrollToRow => {
r##"Scroll to the given absolute row in the screen with 0 being
the first row.
"##
}
ActionKey::ScrollPageUp => {
r##"Scroll the screen up by one page.
"##
}
ActionKey::ScrollPageDown => {
r##"Scroll the screen down by one page.
"##
}
ActionKey::ScrollPageFractional => {
r##"Scroll the screen by the specified fraction of a page.
Positive values scroll downwards, and negative values scroll upwards.
For example, `scroll_page_fractional:0.5` would scroll the screen
downwards by half a page, while `scroll_page_fractional:-1.5` would
scroll it upwards by one and a half pages.
"##
}
ActionKey::ScrollPageLines => {
r##"Scroll the screen by the specified amount of lines.
Positive values scroll downwards, and negative values scroll upwards.
For example, `scroll_page_lines:3` would scroll the screen downwards
by 3 lines, while `scroll_page_lines:-10` would scroll it upwards by 10
lines.
"##
}
ActionKey::AdjustSelection => {
r##"Adjust the current selection in the given direction or position,
relative to the cursor.
WARNING: This does not create a new selection, and does nothing when
there currently isn't one.
Valid arguments are:
- `left`, `right`
Adjust the selection one cell to the left or right respectively.
- `up`, `down`
Adjust the selection one line upwards or downwards respectively.
- `page_up`, `page_down`
Adjust the selection one page upwards or downwards respectively.
- `home`, `end`
Adjust the selection to the top-left or the bottom-right corner
of the screen respectively.
- `beginning_of_line`, `end_of_line`
Adjust the selection to the beginning or the end of the line
respectively.
"##
}
ActionKey::JumpToPrompt => {
r##"Jump the viewport forward or back by the given number of prompts.
Requires shell integration.
Positive values scroll downwards, and negative values scroll upwards.
"##
}
ActionKey::WriteScrollbackFile => {
r##"Write the entire scrollback into a temporary file with the specified
action. The action determines what to do with the filepath.
Valid actions are:
- `copy`
Copy the file path into the clipboard.
- `paste`
Paste the file path into the terminal.
- `open`
Open the file in the default OS editor for text files.
The default OS editor is determined by using `open` on macOS
and `xdg-open` on Linux.
"##
}
ActionKey::WriteScreenFile => {
r##"Write the contents of the screen into a temporary file with the
specified action.
See `write_scrollback_file` for possible actions.
"##
}
ActionKey::WriteSelectionFile => {
r##"Write the currently selected text into a temporary file with the
specified action.
See `write_scrollback_file` for possible actions.
Does nothing when no text is selected.
"##
}
ActionKey::NewWindow => {
r##"Open a new window.
If the application isn't currently focused,
this will bring it to the front.
"##
}
ActionKey::NewTab => {
r##"Open a new tab.
"##
}
ActionKey::PreviousTab => {
r##"Go to the previous tab.
"##
}
ActionKey::NextTab => {
r##"Go to the next tab.
"##
}
ActionKey::LastTab => {
r##"Go to the last tab.
"##
}
ActionKey::GotoTab => {
r##"Go to the tab with the specific index, starting from 1.
If the tab number is higher than the number of tabs,
this will go to the last tab.
"##
}
ActionKey::MoveTab => {
r##"Moves a tab by a relative offset.
Positive values move the tab forwards, and negative values move it
backwards. If the new position is out of bounds, it is wrapped around
cyclically within the tab list.
For example, `move_tab:1` moves the tab one position forwards, and if
it was already the last tab in the list, it wraps around and becomes
the first tab in the list. Likewise, `move_tab:-1` moves the tab one
position backwards, and if it was the first tab, then it will become
the last tab.
"##
}
ActionKey::ToggleTabOverview => {
r##"Toggle the tab overview.
This is only supported on Linux and when the system's libadwaita
version is 1.4 or newer. The current libadwaita version can be
found by running `ghostty +version`.
"##
}
ActionKey::PromptSurfaceTitle => {
r##"Change the title of the current focused surface via a pop-up prompt.
"##
}
ActionKey::PromptTabTitle => {
r##"Change the title of the current tab via a pop-up prompt. The
title set via this prompt overrides any title set by the terminal
and persists across focus changes within the tab.
"##
}
ActionKey::SetSurfaceTitle => {
r##"Set the title for the current focused surface.
If the title is empty, the surface title is reset to an empty title.
"##
}
ActionKey::SetTabTitle => {
r##"Set the title for the current focused tab.
If the title is empty, the tab title override is cleared.
"##
}
ActionKey::NewSplit => {
r##"Create a new split in the specified direction.
Valid arguments:
- `right`, `down`, `left`, `up`
Creates a new split in the corresponding direction.
- `auto`
Creates a new split along the larger direction.
For example, if the parent split is currently wider than it is tall,
then a left-right split would be created, and vice versa.
"##
}
ActionKey::GotoSplit => {
r##"Focus on a split either in the specified direction (`right`, `down`,
`left` and `up`), or in the adjacent split in the order of creation
(`previous` and `next`).
"##
}
ActionKey::GotoWindow => {
r##"Focus on either the previous window or the next one ('previous', 'next')
"##
}
ActionKey::ToggleSplitZoom => {
r##"Zoom in or out of the current split.
When a split is zoomed into, it will take up the entire space in
the current tab, hiding other splits. The tab or tab bar would also
reflect this by displaying an icon indicating the zoomed state.
"##
}
ActionKey::ToggleReadonly => {
r##"Toggle read-only mode for the current surface.
When a surface is in read-only mode:
- No input is sent to the PTY (mouse events, key encoding)
- Input can still be used at the terminal level to make selections,
copy/paste (keybinds), scroll, etc.
- Warn before quit is always enabled in this state even if an active
process is not running
"##
}
ActionKey::ResizeSplit => {
r##"Resize the current split in the specified direction and amount in
pixels. The two arguments should be joined with a comma (`,`),
like in `resize_split:up,10`.
"##
}
ActionKey::EqualizeSplits => {
r##"Equalize the size of all splits in the current window.
"##
}
ActionKey::ResetWindowSize => {
r##"Reset the window to the default size. The "default size" is the
size that a new window would be created with. This has no effect
if the window is fullscreen.
Only implemented on macOS.
"##
}
ActionKey::Inspector => {
r##"Control the visibility of the terminal inspector.
Valid arguments: `toggle`, `show`, `hide`.
"##
}
ActionKey::ShowGtkInspector => {
r##"Show the GTK inspector.
Has no effect on macOS.
"##
}
ActionKey::ShowOnScreenKeyboard => {
r##"Show the on-screen keyboard if one is present.
Only implemented on Linux (GTK). On GNOME, the "Screen Keyboard"
accessibility feature must be turned on, which can be found under
Settings > Accessibility > Typing. Other platforms are as of now
untested.
"##
}
ActionKey::OpenConfig => {
r##"Open the configuration file in the default OS editor.
If your default OS editor isn't configured then this will fail.
Currently, any failures to open the configuration will show up only in
the logs.
"##
}
ActionKey::ReloadConfig => {
r##"Reload the configuration.
The exact meaning depends on the app runtime in use, but this usually
involves re-reading the configuration file and applying any changes
Note that not all changes can be applied at runtime.
"##
}
ActionKey::CloseSurface => {
r##"Close the current "surface", whether that is a window, tab, split, etc.
This might trigger a close confirmation popup, depending on the value
of the `confirm-close-surface` configuration setting.
"##
}
ActionKey::CloseTab => {
r##"Close the current tab and all splits therein, close all other tabs, or
close every tab to the right of the current one depending on the mode.
If the mode is not specified, defaults to closing the current tab.
This might trigger a close confirmation popup, depending on the value
of the `confirm-close-surface` configuration setting.
"##
}
ActionKey::CloseWindow => {
r##"Close the current window and all tabs and splits therein.
This might trigger a close confirmation popup, depending on the value
of the `confirm-close-surface` configuration setting.
"##
}
ActionKey::CloseAllWindows => {
r##"Close all windows.
WARNING: This action has been deprecated and has no effect on either
Linux or macOS. Users are instead encouraged to use `all:close_window`
instead.
"##
}
ActionKey::ToggleMaximize => {
r##"Maximize or unmaximize the current window.
This has no effect on macOS as it does not have the concept of
maximized windows.
"##
}
ActionKey::ToggleFullscreen => {
r##"Fullscreen or unfullscreen the current window.
"##
}
ActionKey::ToggleWindowDecorations => {
r##"Toggle window decorations (titlebar, buttons, etc.) for the current window.
Only implemented on Linux.
"##
}
ActionKey::ToggleWindowFloatOnTop => {
r##"Toggle whether the terminal window should always float on top of other
windows even when unfocused.
Terminal windows always start as normal (not float-on-top) windows.
Only implemented on macOS.
"##
}
ActionKey::ToggleSecureInput => {
r##"Toggle secure input mode.
This is used to prevent apps from monitoring your keyboard input
when entering passwords or other sensitive information.
This applies to the entire application, not just the focused terminal.
You must manually untoggle it or quit Ghostty entirely to disable it.
Only implemented on macOS, as this uses a built-in system API.
"##
}
ActionKey::ToggleMouseReporting => {
r##"Toggle mouse reporting on or off.
When mouse reporting is disabled, mouse events will not be reported to
terminal applications even if they request it. This allows you to always
use the mouse for selection and other terminal UI interactions without
applications capturing mouse input.
This can also be controlled via the `mouse-reporting` configuration
option.
"##
}
ActionKey::ToggleCommandPalette => {
r##"Toggle the command palette.
The command palette is a popup that lets you see what actions
you can perform, their associated keybindings (if any), a search bar
to filter the actions, and the ability to then execute the action.
This requires libadwaita 1.5 or newer on Linux. The current libadwaita
version can be found by running `ghostty +version`.
"##
}
ActionKey::ToggleQuickTerminal => {
r##"Toggle the quick terminal.
The quick terminal, also known as the "Quake-style" or drop-down
terminal, is a terminal window that appears on demand from a keybinding,
often sliding in from a screen edge such as the top. This is useful for
quick access to a terminal without having to open a new window or tab.
The terminal state is preserved between appearances, so showing the
quick terminal after it was already hidden would display the same
window instead of creating a new one.
As quick terminals are often useful when other windows are currently
focused, they are best used with *global* keybinds. For example, one
can define the following key bind to toggle the quick terminal from
anywhere within the system by pressing `` Cmd+` ``:
```ini
keybind = global:cmd+backquote=toggle_quick_terminal
```
The quick terminal has some limitations:
- Only one quick terminal instance can exist at a time.
- Unlike normal terminal windows, the quick terminal will not be
restored when the application is restarted on systems that support
window restoration like macOS.
- On Linux, the quick terminal is only supported on Wayland and not
X11, and only on Wayland compositors that support the `wlr-layer-shell-v1`
protocol. In practice, this means that only GNOME users would not be
able to use this feature.
- On Linux, slide-in animations are only supported on KDE, and when
the "Sliding Popups" KWin plugin is enabled.
If you do not have this plugin enabled, open System Settings > Apps
& Windows > Window Management > Desktop Effects, and enable the
plugin in the plugin list. Ghostty would then need to be restarted
fully for this to take effect.
- Quick terminal tabs are only supported on Linux and not on macOS.
This is because tabs on macOS require a title bar.
- On macOS, a fullscreened quick terminal will always be in non-native
fullscreen mode. This is a requirement due to how the quick terminal
is rendered.
See the various configurations for the quick terminal in the
configuration file to customize its behavior.
"##
}
ActionKey::ToggleVisibility => {
r##"Show or hide all windows. If all windows become shown, we also ensure
Ghostty becomes focused. When hiding all windows, focus is yielded
to the next application as determined by the OS.
Note: When the focused surface is fullscreen, this method does nothing.
Only implemented on macOS.
"##
}
ActionKey::ToggleBackgroundOpacity => {
r##"Toggle the window background opacity between transparent and opaque.
This does nothing when `background-opacity` is set to 1 or above.
When `background-opacity` is less than 1, this action will either make
the window transparent or not depending on its current transparency state.
Only implemented on macOS.
"##
}
ActionKey::CheckForUpdates => {
r##"Check for updates.
Only implemented on macOS.
"##
}
ActionKey::Undo => {
r##"Undo the last undoable action for the focused surface or terminal,
if possible. This can undo actions such as closing tabs or
windows.
Not every action in Ghostty can be undone or redone. The list
of actions support undo/redo is currently limited to:
- New window, close window
- New tab, close tab
- New split, close split
All actions are only undoable/redoable for a limited time.
For example, restoring a closed split can only be done for
some number of seconds since the split was closed. The exact
amount is configured with the `undo-timeout` configuration settings.
The undo/redo actions being limited ensures that there is
bounded memory usage over time, closed surfaces don't continue running
in the background indefinitely, and the keybinds become available
for terminal applications to use.
Only implemented on macOS.
"##
}
ActionKey::Redo => {
r##"Redo the last undoable action for the focused surface or terminal,
if possible. See "undo" for more details on what can and cannot
be undone or redone.
"##
}
ActionKey::EndKeySequence => {
r##"End the currently active key sequence, if any, and flush the
keys up to this point to the terminal, excluding the key that
triggered this action.
For example: `ctrl+w>escape=end_key_sequence` would encode
`ctrl+w` to the terminal and exit the key sequence.
Normally, an invalid sequence will reset the key sequence and
flush all data including the invalid key. This action allows
you to flush only the prior keys, which is useful when you want
to bind something like a control key (`ctrl+w`) but not send
additional inputs.
"##
}
ActionKey::ActivateKeyTable => {
r##"Activate a named key table (see `keybind` configuration documentation).
The named key table will remain active until `deactivate_key_table`
is called. If you want a one-shot key table activation, use the
`activate_key_table_once` action instead.
If the named key table does not exist, this action has no effect
and performable will report false.
If the named key table is already the currently active key table,
this action has no effect and performable will report false.
"##
}
ActionKey::ActivateKeyTableOnce => {
r##"Same as activate_key_table, but the key table will only be active
until the first valid keybinding from that table is used (including
any defined `catch_all` bindings).
The "once" check is only done if this is the currently active
key table. If another key table is activated later, then this
table will remain active until it pops back out to being the
active key table.
"##
}
ActionKey::DeactivateKeyTable => {
r##"Deactivate the currently active key table, if any. The next most
recently activated key table (if any) will become active again.
If no key table is active, this action has no effect.
"##
}
ActionKey::DeactivateAllKeyTables => {
r##"Deactivate all active key tables. If no active key table exists,
this will report performable as false.
"##
}
ActionKey::Quit => {
r##"Quit Ghostty.
"##
}
ActionKey::Crash => {
r##"Crash Ghostty in the desired thread for the focused surface.
WARNING: This is a hard crash (panic) and data can be lost.
The purpose of this action is to test crash handling. For some
users, it may be useful to test crash reporting functionality in
order to determine if it all works as expected.
The value determines the crash location:
- `main`
Crash on the main (GUI) thread.
- `io`
Crash on the IO thread for the focused surface.
- `render`
Crash on the render thread for the focused surface.
"##
}
}
}
}
impl Action {
/// The tag of this action.
pub fn key(&self) -> ActionKey {
match self {
Action::Ignore => ActionKey::Ignore,
Action::Unbind => ActionKey::Unbind,
Action::Csi(_) => ActionKey::Csi,
Action::Esc(_) => ActionKey::Esc,
Action::Text(_) => ActionKey::Text,
Action::CursorKey(_) => ActionKey::CursorKey,
Action::Reset => ActionKey::Reset,
Action::CopyToClipboard(_) => ActionKey::CopyToClipboard,
Action::PasteFromClipboard => ActionKey::PasteFromClipboard,
Action::PasteFromSelection => ActionKey::PasteFromSelection,
Action::CopyUrlToClipboard => ActionKey::CopyUrlToClipboard,
Action::CopyTitleToClipboard => ActionKey::CopyTitleToClipboard,
Action::IncreaseFontSize(_) => ActionKey::IncreaseFontSize,
Action::DecreaseFontSize(_) => ActionKey::DecreaseFontSize,
Action::ResetFontSize => ActionKey::ResetFontSize,
Action::SetFontSize(_) => ActionKey::SetFontSize,
Action::Search(_) => ActionKey::Search,
Action::SearchSelection => ActionKey::SearchSelection,
Action::NavigateSearch(_) => ActionKey::NavigateSearch,
Action::StartSearch => ActionKey::StartSearch,
Action::EndSearch => ActionKey::EndSearch,
Action::ClearScreen => ActionKey::ClearScreen,
Action::SelectAll => ActionKey::SelectAll,
Action::ScrollToTop => ActionKey::ScrollToTop,
Action::ScrollToBottom => ActionKey::ScrollToBottom,
Action::ScrollToSelection => ActionKey::ScrollToSelection,
Action::ScrollToRow(_) => ActionKey::ScrollToRow,
Action::ScrollPageUp => ActionKey::ScrollPageUp,
Action::ScrollPageDown => ActionKey::ScrollPageDown,
Action::ScrollPageFractional(_) => ActionKey::ScrollPageFractional,
Action::ScrollPageLines(_) => ActionKey::ScrollPageLines,
Action::AdjustSelection(_) => ActionKey::AdjustSelection,
Action::JumpToPrompt(_) => ActionKey::JumpToPrompt,
Action::WriteScrollbackFile(_) => ActionKey::WriteScrollbackFile,
Action::WriteScreenFile(_) => ActionKey::WriteScreenFile,
Action::WriteSelectionFile(_) => ActionKey::WriteSelectionFile,
Action::NewWindow => ActionKey::NewWindow,
Action::NewTab => ActionKey::NewTab,
Action::PreviousTab => ActionKey::PreviousTab,
Action::NextTab => ActionKey::NextTab,
Action::LastTab => ActionKey::LastTab,
Action::GotoTab(_) => ActionKey::GotoTab,
Action::MoveTab(_) => ActionKey::MoveTab,
Action::ToggleTabOverview => ActionKey::ToggleTabOverview,
Action::PromptSurfaceTitle => ActionKey::PromptSurfaceTitle,
Action::PromptTabTitle => ActionKey::PromptTabTitle,
Action::SetSurfaceTitle(_) => ActionKey::SetSurfaceTitle,
Action::SetTabTitle(_) => ActionKey::SetTabTitle,
Action::NewSplit(_) => ActionKey::NewSplit,
Action::GotoSplit(_) => ActionKey::GotoSplit,
Action::GotoWindow(_) => ActionKey::GotoWindow,
Action::ToggleSplitZoom => ActionKey::ToggleSplitZoom,
Action::ToggleReadonly => ActionKey::ToggleReadonly,
Action::ResizeSplit(_) => ActionKey::ResizeSplit,
Action::EqualizeSplits => ActionKey::EqualizeSplits,
Action::ResetWindowSize => ActionKey::ResetWindowSize,
Action::Inspector(_) => ActionKey::Inspector,
Action::ShowGtkInspector => ActionKey::ShowGtkInspector,
Action::ShowOnScreenKeyboard => ActionKey::ShowOnScreenKeyboard,
Action::OpenConfig => ActionKey::OpenConfig,
Action::ReloadConfig => ActionKey::ReloadConfig,
Action::CloseSurface => ActionKey::CloseSurface,
Action::CloseTab(_) => ActionKey::CloseTab,
Action::CloseWindow => ActionKey::CloseWindow,
Action::CloseAllWindows => ActionKey::CloseAllWindows,
Action::ToggleMaximize => ActionKey::ToggleMaximize,
Action::ToggleFullscreen => ActionKey::ToggleFullscreen,
Action::ToggleWindowDecorations => ActionKey::ToggleWindowDecorations,
Action::ToggleWindowFloatOnTop => ActionKey::ToggleWindowFloatOnTop,
Action::ToggleSecureInput => ActionKey::ToggleSecureInput,
Action::ToggleMouseReporting => ActionKey::ToggleMouseReporting,
Action::ToggleCommandPalette => ActionKey::ToggleCommandPalette,
Action::ToggleQuickTerminal => ActionKey::ToggleQuickTerminal,
Action::ToggleVisibility => ActionKey::ToggleVisibility,
Action::ToggleBackgroundOpacity => ActionKey::ToggleBackgroundOpacity,
Action::CheckForUpdates => ActionKey::CheckForUpdates,
Action::Undo => ActionKey::Undo,
Action::Redo => ActionKey::Redo,
Action::EndKeySequence => ActionKey::EndKeySequence,
Action::ActivateKeyTable(_) => ActionKey::ActivateKeyTable,
Action::ActivateKeyTableOnce(_) => ActionKey::ActivateKeyTableOnce,
Action::DeactivateKeyTable => ActionKey::DeactivateKeyTable,
Action::DeactivateAllKeyTables => ActionKey::DeactivateAllKeyTables,
Action::Quit => ActionKey::Quit,
Action::Crash(_) => ActionKey::Crash,
}
}
/// Parse the action given its name, the optional parameter (text after the first colon).
fn parse_named(name: &str, param: Option<&str>) -> Result<Action, Error> {
match name {
"ignore" => {
if param.is_some() {
return Err(Error::InvalidFormat);
}
Ok(Action::Ignore)
}
"unbind" => {
if param.is_some() {
return Err(Error::InvalidFormat);
}
Ok(Action::Unbind)
}
"csi" => {
let p = param.ok_or(Error::InvalidFormat)?;
Ok(Action::Csi(p.to_string()))
}
"esc" => {
let p = param.ok_or(Error::InvalidFormat)?;
Ok(Action::Esc(p.to_string()))
}
"text" => {
let p = param.ok_or(Error::InvalidFormat)?;
Ok(Action::Text(p.to_string()))
}
"cursor_key" => Err(Error::InvalidAction),
"reset" => {
if param.is_some() {
return Err(Error::InvalidFormat);
}
Ok(Action::Reset)
}
"copy_to_clipboard" => {
let Some(p) = param else {
return Ok(Action::CopyToClipboard(CopyToClipboard::DEFAULT));
};
Ok(Action::CopyToClipboard(CopyToClipboard::parse_param(p)?))
}
"paste_from_clipboard" => {
if param.is_some() {
return Err(Error::InvalidFormat);
}
Ok(Action::PasteFromClipboard)
}
"paste_from_selection" => {
if param.is_some() {
return Err(Error::InvalidFormat);
}
Ok(Action::PasteFromSelection)
}
"copy_url_to_clipboard" => {
if param.is_some() {
return Err(Error::InvalidFormat);
}
Ok(Action::CopyUrlToClipboard)
}
"copy_title_to_clipboard" => {
if param.is_some() {
return Err(Error::InvalidFormat);
}
Ok(Action::CopyTitleToClipboard)
}
"increase_font_size" => {
let Some(p) = param else {
return Err(Error::InvalidFormat);
};
Ok(Action::IncreaseFontSize(f32::parse_param(p)?))
}
"decrease_font_size" => {
let Some(p) = param else {
return Err(Error::InvalidFormat);
};
Ok(Action::DecreaseFontSize(f32::parse_param(p)?))
}
"reset_font_size" => {
if param.is_some() {
return Err(Error::InvalidFormat);
}
Ok(Action::ResetFontSize)
}
"set_font_size" => {
let Some(p) = param else {
return Err(Error::InvalidFormat);
};
Ok(Action::SetFontSize(f32::parse_param(p)?))
}
"search" => {
let p = param.ok_or(Error::InvalidFormat)?;
Ok(Action::Search(p.to_string()))
}
"search_selection" => {
if param.is_some() {
return Err(Error::InvalidFormat);
}
Ok(Action::SearchSelection)
}
"navigate_search" => {
let Some(p) = param else {
return Err(Error::InvalidFormat);
};
Ok(Action::NavigateSearch(NavigateSearch::parse_param(p)?))
}
"start_search" => {
if param.is_some() {
return Err(Error::InvalidFormat);
}
Ok(Action::StartSearch)
}
"end_search" => {
if param.is_some() {
return Err(Error::InvalidFormat);
}
Ok(Action::EndSearch)
}
"clear_screen" => {
if param.is_some() {
return Err(Error::InvalidFormat);
}
Ok(Action::ClearScreen)
}
"select_all" => {
if param.is_some() {
return Err(Error::InvalidFormat);
}
Ok(Action::SelectAll)
}
"scroll_to_top" => {
if param.is_some() {
return Err(Error::InvalidFormat);
}
Ok(Action::ScrollToTop)
}
"scroll_to_bottom" => {
if param.is_some() {
return Err(Error::InvalidFormat);
}
Ok(Action::ScrollToBottom)
}
"scroll_to_selection" => {
if param.is_some() {
return Err(Error::InvalidFormat);
}
Ok(Action::ScrollToSelection)
}
"scroll_to_row" => {
let Some(p) = param else {
return Err(Error::InvalidFormat);
};
Ok(Action::ScrollToRow(usize::parse_param(p)?))
}
"scroll_page_up" => {
if param.is_some() {
return Err(Error::InvalidFormat);
}
Ok(Action::ScrollPageUp)
}
"scroll_page_down" => {
if param.is_some() {
return Err(Error::InvalidFormat);
}
Ok(Action::ScrollPageDown)
}
"scroll_page_fractional" => {
let Some(p) = param else {
return Err(Error::InvalidFormat);
};
Ok(Action::ScrollPageFractional(f32::parse_param(p)?))
}
"scroll_page_lines" => {
let Some(p) = param else {
return Err(Error::InvalidFormat);
};
Ok(Action::ScrollPageLines(i16::parse_param(p)?))
}
"adjust_selection" => {
let Some(p) = param else {
return Err(Error::InvalidFormat);
};
Ok(Action::AdjustSelection(AdjustSelection::parse_param(p)?))
}
"jump_to_prompt" => {
let Some(p) = param else {
return Err(Error::InvalidFormat);
};
Ok(Action::JumpToPrompt(i16::parse_param(p)?))
}
"write_scrollback_file" => {
let Some(p) = param else {
return Err(Error::InvalidFormat);
};
Ok(Action::WriteScrollbackFile(WriteScreen::parse_param(p)?))
}
"write_screen_file" => {
let Some(p) = param else {
return Err(Error::InvalidFormat);
};
Ok(Action::WriteScreenFile(WriteScreen::parse_param(p)?))
}
"write_selection_file" => {
let Some(p) = param else {
return Err(Error::InvalidFormat);
};
Ok(Action::WriteSelectionFile(WriteScreen::parse_param(p)?))
}
"new_window" => {
if param.is_some() {
return Err(Error::InvalidFormat);
}
Ok(Action::NewWindow)
}
"new_tab" => {
if param.is_some() {
return Err(Error::InvalidFormat);
}
Ok(Action::NewTab)
}
"previous_tab" => {
if param.is_some() {
return Err(Error::InvalidFormat);
}
Ok(Action::PreviousTab)
}
"next_tab" => {
if param.is_some() {
return Err(Error::InvalidFormat);
}
Ok(Action::NextTab)
}
"last_tab" => {
if param.is_some() {
return Err(Error::InvalidFormat);
}
Ok(Action::LastTab)
}
"goto_tab" => {
let Some(p) = param else {
return Err(Error::InvalidFormat);
};
Ok(Action::GotoTab(usize::parse_param(p)?))
}
"move_tab" => {
let Some(p) = param else {
return Err(Error::InvalidFormat);
};
Ok(Action::MoveTab(isize::parse_param(p)?))
}
"toggle_tab_overview" => {
if param.is_some() {
return Err(Error::InvalidFormat);
}
Ok(Action::ToggleTabOverview)
}
"prompt_surface_title" => {
if param.is_some() {
return Err(Error::InvalidFormat);
}
Ok(Action::PromptSurfaceTitle)
}
"prompt_tab_title" => {
if param.is_some() {
return Err(Error::InvalidFormat);
}
Ok(Action::PromptTabTitle)
}
"set_surface_title" => {
let p = param.ok_or(Error::InvalidFormat)?;
Ok(Action::SetSurfaceTitle(p.to_string()))
}
"set_tab_title" => {
let p = param.ok_or(Error::InvalidFormat)?;
Ok(Action::SetTabTitle(p.to_string()))
}
"new_split" => {
let Some(p) = param else {
return Ok(Action::NewSplit(SplitDirection::DEFAULT));
};
Ok(Action::NewSplit(SplitDirection::parse_param(p)?))
}
"goto_split" => {
let Some(p) = param else {
return Err(Error::InvalidFormat);
};
Ok(Action::GotoSplit(SplitFocusDirection::parse_param(p)?))
}
"goto_window" => {
let Some(p) = param else {
return Err(Error::InvalidFormat);
};
Ok(Action::GotoWindow(GotoWindow::parse_param(p)?))
}
"toggle_split_zoom" => {
if param.is_some() {
return Err(Error::InvalidFormat);
}
Ok(Action::ToggleSplitZoom)
}
"toggle_readonly" => {
if param.is_some() {
return Err(Error::InvalidFormat);
}
Ok(Action::ToggleReadonly)
}
"resize_split" => {
let Some(p) = param else {
return Err(Error::InvalidFormat);
};
Ok(Action::ResizeSplit(SplitResizeParameter::parse_param(p)?))
}
"equalize_splits" => {
if param.is_some() {
return Err(Error::InvalidFormat);
}
Ok(Action::EqualizeSplits)
}
"reset_window_size" => {
if param.is_some() {
return Err(Error::InvalidFormat);
}
Ok(Action::ResetWindowSize)
}
"inspector" => {
let Some(p) = param else {
return Err(Error::InvalidFormat);
};
Ok(Action::Inspector(InspectorMode::parse_param(p)?))
}
"show_gtk_inspector" => {
if param.is_some() {
return Err(Error::InvalidFormat);
}
Ok(Action::ShowGtkInspector)
}
"show_on_screen_keyboard" => {
if param.is_some() {
return Err(Error::InvalidFormat);
}
Ok(Action::ShowOnScreenKeyboard)
}
"open_config" => {
if param.is_some() {
return Err(Error::InvalidFormat);
}
Ok(Action::OpenConfig)
}
"reload_config" => {
if param.is_some() {
return Err(Error::InvalidFormat);
}
Ok(Action::ReloadConfig)
}
"close_surface" => {
if param.is_some() {
return Err(Error::InvalidFormat);
}
Ok(Action::CloseSurface)
}
"close_tab" => {
let Some(p) = param else {
return Ok(Action::CloseTab(CloseTabMode::DEFAULT));
};
Ok(Action::CloseTab(CloseTabMode::parse_param(p)?))
}
"close_window" => {
if param.is_some() {
return Err(Error::InvalidFormat);
}
Ok(Action::CloseWindow)
}
"close_all_windows" => {
if param.is_some() {
return Err(Error::InvalidFormat);
}
Ok(Action::CloseAllWindows)
}
"toggle_maximize" => {
if param.is_some() {
return Err(Error::InvalidFormat);
}
Ok(Action::ToggleMaximize)
}
"toggle_fullscreen" => {
if param.is_some() {
return Err(Error::InvalidFormat);
}
Ok(Action::ToggleFullscreen)
}
"toggle_window_decorations" => {
if param.is_some() {
return Err(Error::InvalidFormat);
}
Ok(Action::ToggleWindowDecorations)
}
"toggle_window_float_on_top" => {
if param.is_some() {
return Err(Error::InvalidFormat);
}
Ok(Action::ToggleWindowFloatOnTop)
}
"toggle_secure_input" => {
if param.is_some() {
return Err(Error::InvalidFormat);
}
Ok(Action::ToggleSecureInput)
}
"toggle_mouse_reporting" => {
if param.is_some() {
return Err(Error::InvalidFormat);
}
Ok(Action::ToggleMouseReporting)
}
"toggle_command_palette" => {
if param.is_some() {
return Err(Error::InvalidFormat);
}
Ok(Action::ToggleCommandPalette)
}
"toggle_quick_terminal" => {
if param.is_some() {
return Err(Error::InvalidFormat);
}
Ok(Action::ToggleQuickTerminal)
}
"toggle_visibility" => {
if param.is_some() {
return Err(Error::InvalidFormat);
}
Ok(Action::ToggleVisibility)
}
"toggle_background_opacity" => {
if param.is_some() {
return Err(Error::InvalidFormat);
}
Ok(Action::ToggleBackgroundOpacity)
}
"check_for_updates" => {
if param.is_some() {
return Err(Error::InvalidFormat);
}
Ok(Action::CheckForUpdates)
}
"undo" => {
if param.is_some() {
return Err(Error::InvalidFormat);
}
Ok(Action::Undo)
}
"redo" => {
if param.is_some() {
return Err(Error::InvalidFormat);
}
Ok(Action::Redo)
}
"end_key_sequence" => {
if param.is_some() {
return Err(Error::InvalidFormat);
}
Ok(Action::EndKeySequence)
}
"activate_key_table" => {
let p = param.ok_or(Error::InvalidFormat)?;
Ok(Action::ActivateKeyTable(p.to_string()))
}
"activate_key_table_once" => {
let p = param.ok_or(Error::InvalidFormat)?;
Ok(Action::ActivateKeyTableOnce(p.to_string()))
}
"deactivate_key_table" => {
if param.is_some() {
return Err(Error::InvalidFormat);
}
Ok(Action::DeactivateKeyTable)
}
"deactivate_all_key_tables" => {
if param.is_some() {
return Err(Error::InvalidFormat);
}
Ok(Action::DeactivateAllKeyTables)
}
"quit" => {
if param.is_some() {
return Err(Error::InvalidFormat);
}
Ok(Action::Quit)
}
"crash" => {
let Some(p) = param else {
return Err(Error::InvalidFormat);
};
Ok(Action::Crash(CrashThread::parse_param(p)?))
}
_ => Err(Error::InvalidAction),
}
}
/// Format only the name and parameter pieces. Used by Display.
fn fmt_into(&self, out: &mut String) {
out.push_str(self.key().name());
match self {
Action::Ignore
| Action::Unbind
| Action::Reset
| Action::PasteFromClipboard
| Action::PasteFromSelection
| Action::CopyUrlToClipboard
| Action::CopyTitleToClipboard
| Action::ResetFontSize
| Action::SearchSelection
| Action::StartSearch
| Action::EndSearch
| Action::ClearScreen
| Action::SelectAll
| Action::ScrollToTop
| Action::ScrollToBottom
| Action::ScrollToSelection
| Action::ScrollPageUp
| Action::ScrollPageDown
| Action::NewWindow
| Action::NewTab
| Action::PreviousTab
| Action::NextTab
| Action::LastTab
| Action::ToggleTabOverview
| Action::PromptSurfaceTitle
| Action::PromptTabTitle
| Action::ToggleSplitZoom
| Action::ToggleReadonly
| Action::EqualizeSplits
| Action::ResetWindowSize
| Action::ShowGtkInspector
| Action::ShowOnScreenKeyboard
| Action::OpenConfig
| Action::ReloadConfig
| Action::CloseSurface
| Action::CloseWindow
| Action::CloseAllWindows
| Action::ToggleMaximize
| Action::ToggleFullscreen
| Action::ToggleWindowDecorations
| Action::ToggleWindowFloatOnTop
| Action::ToggleSecureInput
| Action::ToggleMouseReporting
| Action::ToggleCommandPalette
| Action::ToggleQuickTerminal
| Action::ToggleVisibility
| Action::ToggleBackgroundOpacity
| Action::CheckForUpdates
| Action::Undo
| Action::Redo
| Action::EndKeySequence
| Action::DeactivateKeyTable
| Action::DeactivateAllKeyTables
| Action::Quit => {}
Action::Csi(v) => {
out.push(':');
string_escape(v, out);
}
Action::Esc(v) => {
out.push(':');
string_escape(v, out);
}
Action::Text(v) => {
out.push(':');
string_escape(v, out);
}
Action::CursorKey(_) => panic!("formatting not supported"),
Action::CopyToClipboard(v) => {
out.push(':');
out.push_str(v.name());
}
Action::IncreaseFontSize(v) => {
out.push(':');
out.push_str(&v.to_string());
}
Action::DecreaseFontSize(v) => {
out.push(':');
out.push_str(&v.to_string());
}
Action::SetFontSize(v) => {
out.push(':');
out.push_str(&v.to_string());
}
Action::Search(v) => {
out.push(':');
string_escape(v, out);
}
Action::NavigateSearch(v) => {
out.push(':');
out.push_str(v.name());
}
Action::ScrollToRow(v) => {
out.push(':');
out.push_str(&v.to_string());
}
Action::ScrollPageFractional(v) => {
out.push(':');
out.push_str(&v.to_string());
}
Action::ScrollPageLines(v) => {
out.push(':');
out.push_str(&v.to_string());
}
Action::AdjustSelection(v) => {
out.push(':');
out.push_str(v.name());
}
Action::JumpToPrompt(v) => {
out.push(':');
out.push_str(&v.to_string());
}
Action::WriteScrollbackFile(v) => {
out.push(':');
v.format_into(out);
}
Action::WriteScreenFile(v) => {
out.push(':');
v.format_into(out);
}
Action::WriteSelectionFile(v) => {
out.push(':');
v.format_into(out);
}
Action::GotoTab(v) => {
out.push(':');
out.push_str(&v.to_string());
}
Action::MoveTab(v) => {
out.push(':');
out.push_str(&v.to_string());
}
Action::SetSurfaceTitle(v) => {
out.push(':');
string_escape(v, out);
}
Action::SetTabTitle(v) => {
out.push(':');
string_escape(v, out);
}
Action::NewSplit(v) => {
out.push(':');
out.push_str(v.name());
}
Action::GotoSplit(v) => {
out.push(':');
out.push_str(v.name());
}
Action::GotoWindow(v) => {
out.push(':');
out.push_str(v.name());
}
Action::ResizeSplit(v) => {
out.push(':');
v.format_into(out);
}
Action::Inspector(v) => {
out.push(':');
out.push_str(v.name());
}
Action::CloseTab(v) => {
out.push(':');
out.push_str(v.name());
}
Action::ActivateKeyTable(v) => {
out.push(':');
string_escape(v, out);
}
Action::ActivateKeyTableOnce(v) => {
out.push(':');
string_escape(v, out);
}
Action::Crash(v) => {
out.push(':');
out.push_str(v.name());
}
}
}
pub fn hash_into<H: std::hash::Hasher>(&self, h: &mut H) {
use std::hash::Hash;
self.key().hash(h);
match self {
Action::Ignore
| Action::Unbind
| Action::Reset
| Action::PasteFromClipboard
| Action::PasteFromSelection
| Action::CopyUrlToClipboard
| Action::CopyTitleToClipboard
| Action::ResetFontSize
| Action::SearchSelection
| Action::StartSearch
| Action::EndSearch
| Action::ClearScreen
| Action::SelectAll
| Action::ScrollToTop
| Action::ScrollToBottom
| Action::ScrollToSelection
| Action::ScrollPageUp
| Action::ScrollPageDown
| Action::NewWindow
| Action::NewTab
| Action::PreviousTab
| Action::NextTab
| Action::LastTab
| Action::ToggleTabOverview
| Action::PromptSurfaceTitle
| Action::PromptTabTitle
| Action::ToggleSplitZoom
| Action::ToggleReadonly
| Action::EqualizeSplits
| Action::ResetWindowSize
| Action::ShowGtkInspector
| Action::ShowOnScreenKeyboard
| Action::OpenConfig
| Action::ReloadConfig
| Action::CloseSurface
| Action::CloseWindow
| Action::CloseAllWindows
| Action::ToggleMaximize
| Action::ToggleFullscreen
| Action::ToggleWindowDecorations
| Action::ToggleWindowFloatOnTop
| Action::ToggleSecureInput
| Action::ToggleMouseReporting
| Action::ToggleCommandPalette
| Action::ToggleQuickTerminal
| Action::ToggleVisibility
| Action::ToggleBackgroundOpacity
| Action::CheckForUpdates
| Action::Undo
| Action::Redo
| Action::EndKeySequence
| Action::DeactivateKeyTable
| Action::DeactivateAllKeyTables
| Action::Quit => {}
Action::Csi(v) => v.hash(h),
Action::Esc(v) => v.hash(h),
Action::Text(v) => v.hash(h),
Action::CursorKey(v) => v.hash(h),
Action::CopyToClipboard(v) => v.hash(h),
Action::IncreaseFontSize(v) => v.to_bits().hash(h),
Action::DecreaseFontSize(v) => v.to_bits().hash(h),
Action::SetFontSize(v) => v.to_bits().hash(h),
Action::Search(v) => v.hash(h),
Action::NavigateSearch(v) => v.hash(h),
Action::ScrollToRow(v) => v.hash(h),
Action::ScrollPageFractional(v) => v.to_bits().hash(h),
Action::ScrollPageLines(v) => v.hash(h),
Action::AdjustSelection(v) => v.hash(h),
Action::JumpToPrompt(v) => v.hash(h),
Action::WriteScrollbackFile(v) => v.hash(h),
Action::WriteScreenFile(v) => v.hash(h),
Action::WriteSelectionFile(v) => v.hash(h),
Action::GotoTab(v) => v.hash(h),
Action::MoveTab(v) => v.hash(h),
Action::SetSurfaceTitle(v) => v.hash(h),
Action::SetTabTitle(v) => v.hash(h),
Action::NewSplit(v) => v.hash(h),
Action::GotoSplit(v) => v.hash(h),
Action::GotoWindow(v) => v.hash(h),
Action::ResizeSplit(v) => v.hash(h),
Action::Inspector(v) => v.hash(h),
Action::CloseTab(v) => v.hash(h),
Action::ActivateKeyTable(v) => v.hash(h),
Action::ActivateKeyTableOnce(v) => v.hash(h),
Action::Crash(v) => v.hash(h),
}
}
/// Returns the scope of an action.
pub fn scope(&self) -> Scope {
self.key().scope()
}
}
impl ActionKey {
/// Returns the scope of an action.
pub fn scope(self) -> Scope {
match self {
ActionKey::Ignore
| ActionKey::Unbind
| ActionKey::OpenConfig
| ActionKey::ReloadConfig
| ActionKey::CloseAllWindows
| ActionKey::Quit
| ActionKey::ToggleQuickTerminal
| ActionKey::ToggleVisibility
| ActionKey::CheckForUpdates
| ActionKey::ShowGtkInspector
| ActionKey::NewWindow
| ActionKey::Undo
| ActionKey::Redo => Scope::App,
ActionKey::Csi
| ActionKey::Esc
| ActionKey::Text
| ActionKey::CursorKey
| ActionKey::Search
| ActionKey::NavigateSearch
| ActionKey::SearchSelection
| ActionKey::StartSearch
| ActionKey::EndSearch
| ActionKey::Reset
| ActionKey::CopyToClipboard
| ActionKey::CopyUrlToClipboard
| ActionKey::CopyTitleToClipboard
| ActionKey::PasteFromClipboard
| ActionKey::PasteFromSelection
| ActionKey::IncreaseFontSize
| ActionKey::DecreaseFontSize
| ActionKey::ResetFontSize
| ActionKey::SetFontSize
| ActionKey::PromptSurfaceTitle
| ActionKey::PromptTabTitle
| ActionKey::SetSurfaceTitle
| ActionKey::SetTabTitle
| ActionKey::ClearScreen
| ActionKey::SelectAll
| ActionKey::ScrollToTop
| ActionKey::ScrollToBottom
| ActionKey::ScrollToSelection
| ActionKey::ScrollToRow
| ActionKey::ScrollPageUp
| ActionKey::ScrollPageDown
| ActionKey::ScrollPageFractional
| ActionKey::ScrollPageLines
| ActionKey::AdjustSelection
| ActionKey::JumpToPrompt
| ActionKey::WriteScrollbackFile
| ActionKey::WriteScreenFile
| ActionKey::WriteSelectionFile
| ActionKey::CloseSurface
| ActionKey::CloseTab
| ActionKey::CloseWindow
| ActionKey::ToggleMaximize
| ActionKey::ToggleFullscreen
| ActionKey::ToggleWindowDecorations
| ActionKey::ToggleWindowFloatOnTop
| ActionKey::ToggleSecureInput
| ActionKey::ToggleMouseReporting
| ActionKey::ToggleCommandPalette
| ActionKey::ToggleBackgroundOpacity
| ActionKey::ShowOnScreenKeyboard
| ActionKey::ResetWindowSize
| ActionKey::ActivateKeyTable
| ActionKey::ActivateKeyTableOnce
| ActionKey::DeactivateKeyTable
| ActionKey::DeactivateAllKeyTables
| ActionKey::EndKeySequence
| ActionKey::Crash
| ActionKey::NewTab
| ActionKey::PreviousTab
| ActionKey::NextTab
| ActionKey::LastTab
| ActionKey::GotoTab
| ActionKey::MoveTab
| ActionKey::ToggleTabOverview
| ActionKey::NewSplit
| ActionKey::GotoSplit
| ActionKey::GotoWindow
| ActionKey::ToggleSplitZoom
| ActionKey::ToggleReadonly
| ActionKey::ResizeSplit
| ActionKey::EqualizeSplits
| ActionKey::Inspector => Scope::Surface,
}
}
/// Look up an action key by name.
pub fn from_name(s: &str) -> Option<ActionKey> {
match s {
"ignore" => Some(ActionKey::Ignore),
"unbind" => Some(ActionKey::Unbind),
"csi" => Some(ActionKey::Csi),
"esc" => Some(ActionKey::Esc),
"text" => Some(ActionKey::Text),
"cursor_key" => Some(ActionKey::CursorKey),
"reset" => Some(ActionKey::Reset),
"copy_to_clipboard" => Some(ActionKey::CopyToClipboard),
"paste_from_clipboard" => Some(ActionKey::PasteFromClipboard),
"paste_from_selection" => Some(ActionKey::PasteFromSelection),
"copy_url_to_clipboard" => Some(ActionKey::CopyUrlToClipboard),
"copy_title_to_clipboard" => Some(ActionKey::CopyTitleToClipboard),
"increase_font_size" => Some(ActionKey::IncreaseFontSize),
"decrease_font_size" => Some(ActionKey::DecreaseFontSize),
"reset_font_size" => Some(ActionKey::ResetFontSize),
"set_font_size" => Some(ActionKey::SetFontSize),
"search" => Some(ActionKey::Search),
"search_selection" => Some(ActionKey::SearchSelection),
"navigate_search" => Some(ActionKey::NavigateSearch),
"start_search" => Some(ActionKey::StartSearch),
"end_search" => Some(ActionKey::EndSearch),
"clear_screen" => Some(ActionKey::ClearScreen),
"select_all" => Some(ActionKey::SelectAll),
"scroll_to_top" => Some(ActionKey::ScrollToTop),
"scroll_to_bottom" => Some(ActionKey::ScrollToBottom),
"scroll_to_selection" => Some(ActionKey::ScrollToSelection),
"scroll_to_row" => Some(ActionKey::ScrollToRow),
"scroll_page_up" => Some(ActionKey::ScrollPageUp),
"scroll_page_down" => Some(ActionKey::ScrollPageDown),
"scroll_page_fractional" => Some(ActionKey::ScrollPageFractional),
"scroll_page_lines" => Some(ActionKey::ScrollPageLines),
"adjust_selection" => Some(ActionKey::AdjustSelection),
"jump_to_prompt" => Some(ActionKey::JumpToPrompt),
"write_scrollback_file" => Some(ActionKey::WriteScrollbackFile),
"write_screen_file" => Some(ActionKey::WriteScreenFile),
"write_selection_file" => Some(ActionKey::WriteSelectionFile),
"new_window" => Some(ActionKey::NewWindow),
"new_tab" => Some(ActionKey::NewTab),
"previous_tab" => Some(ActionKey::PreviousTab),
"next_tab" => Some(ActionKey::NextTab),
"last_tab" => Some(ActionKey::LastTab),
"goto_tab" => Some(ActionKey::GotoTab),
"move_tab" => Some(ActionKey::MoveTab),
"toggle_tab_overview" => Some(ActionKey::ToggleTabOverview),
"prompt_surface_title" => Some(ActionKey::PromptSurfaceTitle),
"prompt_tab_title" => Some(ActionKey::PromptTabTitle),
"set_surface_title" => Some(ActionKey::SetSurfaceTitle),
"set_tab_title" => Some(ActionKey::SetTabTitle),
"new_split" => Some(ActionKey::NewSplit),
"goto_split" => Some(ActionKey::GotoSplit),
"goto_window" => Some(ActionKey::GotoWindow),
"toggle_split_zoom" => Some(ActionKey::ToggleSplitZoom),
"toggle_readonly" => Some(ActionKey::ToggleReadonly),
"resize_split" => Some(ActionKey::ResizeSplit),
"equalize_splits" => Some(ActionKey::EqualizeSplits),
"reset_window_size" => Some(ActionKey::ResetWindowSize),
"inspector" => Some(ActionKey::Inspector),
"show_gtk_inspector" => Some(ActionKey::ShowGtkInspector),
"show_on_screen_keyboard" => Some(ActionKey::ShowOnScreenKeyboard),
"open_config" => Some(ActionKey::OpenConfig),
"reload_config" => Some(ActionKey::ReloadConfig),
"close_surface" => Some(ActionKey::CloseSurface),
"close_tab" => Some(ActionKey::CloseTab),
"close_window" => Some(ActionKey::CloseWindow),
"close_all_windows" => Some(ActionKey::CloseAllWindows),
"toggle_maximize" => Some(ActionKey::ToggleMaximize),
"toggle_fullscreen" => Some(ActionKey::ToggleFullscreen),
"toggle_window_decorations" => Some(ActionKey::ToggleWindowDecorations),
"toggle_window_float_on_top" => Some(ActionKey::ToggleWindowFloatOnTop),
"toggle_secure_input" => Some(ActionKey::ToggleSecureInput),
"toggle_mouse_reporting" => Some(ActionKey::ToggleMouseReporting),
"toggle_command_palette" => Some(ActionKey::ToggleCommandPalette),
"toggle_quick_terminal" => Some(ActionKey::ToggleQuickTerminal),
"toggle_visibility" => Some(ActionKey::ToggleVisibility),
"toggle_background_opacity" => Some(ActionKey::ToggleBackgroundOpacity),
"check_for_updates" => Some(ActionKey::CheckForUpdates),
"undo" => Some(ActionKey::Undo),
"redo" => Some(ActionKey::Redo),
"end_key_sequence" => Some(ActionKey::EndKeySequence),
"activate_key_table" => Some(ActionKey::ActivateKeyTable),
"activate_key_table_once" => Some(ActionKey::ActivateKeyTableOnce),
"deactivate_key_table" => Some(ActionKey::DeactivateKeyTable),
"deactivate_all_key_tables" => Some(ActionKey::DeactivateAllKeyTables),
"quit" => Some(ActionKey::Quit),
"crash" => Some(ActionKey::Crash),
_ => None,
}
}
}
+787
View File
@@ -0,0 +1,787 @@
//! A binding maps some input trigger to an action. When the trigger
//! occurs, the action is performed. (ZIG/src/input/Binding.zig)
pub mod action;
pub mod set;
use std::collections::hash_map::DefaultHasher;
use std::fmt;
use std::hash::{Hash, Hasher};
pub use action::{
Action, ActionKey, AdjustSelection, CloseTabMode, CopyToClipboard, CrashThread, CursorKey,
GotoWindow, InspectorMode, NavigateSearch, Scope, SplitDirection, SplitFocusDirection,
SplitResizeDirection, SplitResizeParameter, WriteScreen, WriteScreenAction, WriteScreenFormat,
};
pub use set::Set;
use crate::key;
use crate::key_mods::{self, Mods};
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum Error {
#[error("invalid format")]
InvalidFormat,
#[error("invalid action")]
InvalidAction,
}
/// A binding: trigger, action and per-binding flags.
#[derive(Debug, Clone, PartialEq)]
pub struct Binding {
/// The trigger that needs to be performed to execute the action.
pub trigger: Trigger,
/// The action to take if this binding matches
pub action: Action,
/// Boolean flags that can be set per binding.
pub flags: Flags,
}
/// Flags the full binding-scoped flags that can be set per binding.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Flags {
/// True if this binding should consume the input when the
/// action is triggered.
pub consumed: bool,
/// True if this binding should be forwarded to all active surfaces
/// in the application.
pub all: bool,
/// True if this binding is global. Global bindings should work system-wide
/// and not just while Ghostty is focused. This may not work on all platforms.
/// See the keybind config documentation for more information.
pub global: bool,
/// True if this binding should only be triggered if the action can be
/// performed. If the action can't be performed then the binding acts as
/// if it doesn't exist.
pub performable: bool,
}
impl Default for Flags {
fn default() -> Self {
Flags {
consumed: true,
all: false,
global: false,
performable: false,
}
}
}
impl Flags {
/// Converts this to a C-compatible value (packed bits, `consumed` = bit 0).
pub fn cval(self) -> u8 {
u8::from(self.consumed)
| u8::from(self.all) << 1
| u8::from(self.global) << 2
| u8::from(self.performable) << 3
}
}
/// Elements yielded by the [`Parser`].
#[derive(Debug, Clone, PartialEq)]
pub enum Elem {
/// A leader trigger in a sequence.
Leader(Trigger),
/// The final trigger and action in a sequence.
Binding(Binding),
/// A chained action `chain=<action>` that should be appended
/// to the previous binding. Note that any action is parsed, including
/// invalid actions for chains such as `unbind`. We expect downstream
/// consumers to validate that the action is valid for chaining.
Chain(Action),
}
/// Full binding parser. The binding parser is implemented as an iterator
/// which yields elements to support multi-key sequences.
#[derive(Debug, Clone)]
pub struct Parser<'a> {
trigger_it: SequenceIterator<'a>,
action: Action,
flags: Flags,
chain: bool,
}
impl<'a> Parser<'a> {
pub fn new(raw_input: &'a str) -> Result<Parser<'a>, Error> {
let (flags, start_idx) = parse_flags(raw_input)?;
let input = &raw_input[start_idx..];
// Find the equal sign. This is more complicated than it seems on
// the surface because we need to ignore equal signs that are
// part of the trigger.
let eql_idx: usize = 'eql: {
// TODO: We should change this parser into a real state machine
// based parser that parses the trigger fully, then yields the
// action after. The loop below is a total mess.
let mut offset = 0usize;
let bytes = input.as_bytes();
while let Some(offset_idx) = input[offset..].find('=') {
// Find: '=+ctrl' or '==action'
let idx = offset + offset_idx;
if idx + 1 < bytes.len() && (bytes[idx + 1] == b'+' || bytes[idx + 1] == b'=') {
offset += offset_idx + 1;
continue;
}
// Looks like the real equal sign.
break 'eql idx;
}
return Err(Error::InvalidFormat);
};
// Detect chains. Chains must not have flags.
let chain = &input[..eql_idx] == "chain";
if chain && start_idx > 0 {
return Err(Error::InvalidFormat);
}
// Sequence iterator goes up to the equal, action is after. We can
// parse the action now.
Ok(Parser {
trigger_it: SequenceIterator {
// This is kind of hacky but we put a dummy trigger
// for chained inputs. The `next` will never yield this
// because we have chain set.
input: if chain { "a" } else { &input[..eql_idx] },
i: 0,
},
action: Action::parse(&input[eql_idx + 1..])?,
flags,
chain,
})
}
pub fn next_elem(&mut self) -> Result<Option<Elem>, Error> {
// Get our trigger. If we're out of triggers then we're done.
let Some(trigger) = self.trigger_it.next_trigger()? else {
return Ok(None);
};
// If this is our last trigger then it is our final binding.
if !self.trigger_it.done() {
// Global/all bindings can't be sequences
if self.flags.global || self.flags.all {
return Err(Error::InvalidFormat);
}
return Ok(Some(Elem::Leader(trigger)));
}
// If we're a chain then return it as-is.
if self.chain {
return Ok(Some(Elem::Chain(self.action.clone())));
}
// Out of triggers, yield the final action.
Ok(Some(Elem::Binding(Binding {
trigger,
action: self.action.clone(),
flags: self.flags,
})))
}
pub fn reset(&mut self) {
self.trigger_it.i = 0;
}
}
fn parse_flags(raw_input: &str) -> Result<(Flags, usize), Error> {
let mut flags = Flags::default();
let mut start_idx = 0usize;
let mut input = raw_input;
// Find the next prefix
while let Some(idx) = input.find(':') {
let prefix = &input[..idx];
// If the prefix is one of our flags then set it.
match prefix {
"all" => {
if flags.all {
return Err(Error::InvalidFormat);
}
flags.all = true;
}
"global" => {
if flags.global {
return Err(Error::InvalidFormat);
}
flags.global = true;
}
"unconsumed" => {
if !flags.consumed {
return Err(Error::InvalidFormat);
}
flags.consumed = false;
}
"performable" => {
if flags.performable {
return Err(Error::InvalidFormat);
}
flags.performable = true;
}
// If we don't recognize the prefix then we're done. We
// let any unknown prefix fallthrough to trigger-specific
// parsing in case there are trigger-specific prefixes
// (none currently but historically there was `physical:`
// at one point). Breaking here lets us always implement new
// prefixes.
_ => break,
}
// Move past the prefix
start_idx += idx + 1;
input = &input[idx + 1..];
}
Ok((flags, start_idx))
}
/// An iterator that yields each trigger in a sequence of triggers. For
/// example, the sequence "ctrl+a>ctrl+b" would yield "ctrl+a" and then
/// "ctrl+b".
#[derive(Debug, Clone)]
pub struct SequenceIterator<'a> {
/// The input of triggers. This is expected to be ONLY triggers. Things
/// like the "unconsumed:" prefix or action must be stripped before
/// passing to this iterator.
pub input: &'a str,
pub i: usize,
}
impl<'a> SequenceIterator<'a> {
pub fn new(input: &'a str) -> Self {
SequenceIterator { input, i: 0 }
}
/// Returns the next trigger in the sequence if there is no parsing error.
pub fn next_trigger(&mut self) -> Result<Option<Trigger>, Error> {
if self.done() {
return Ok(None);
}
let rem = &self.input[self.i..];
let idx = rem.find('>').unwrap_or(rem.len());
self.i += idx + 1;
Ok(Some(Trigger::parse(&rem[..idx])?))
}
/// Returns true if there are no more triggers to parse.
pub fn done(&self) -> bool {
self.i > self.input.len()
}
}
/// Parse a single, non-sequenced binding. To support sequences you must
/// use parse. This is a convenience function for single bindings aimed
/// primarily at tests.
///
/// This doesn't support `chain` either, since chaining requires some
/// stateful concept of a prior binding.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ParseSingleError {
Error(Error),
UnexpectedChain,
UnexpectedSequence,
}
impl From<Error> for ParseSingleError {
fn from(e: Error) -> Self {
ParseSingleError::Error(e)
}
}
pub fn parse_single(raw_input: &str) -> Result<Binding, ParseSingleError> {
let mut p = Parser::new(raw_input)?;
let elem = p.next_elem()?.ok_or(Error::InvalidFormat)?;
match elem {
Elem::Leader(_) => Err(ParseSingleError::UnexpectedSequence),
Elem::Binding(b) => Ok(b),
Elem::Chain(_) => Err(ParseSingleError::UnexpectedChain),
}
}
/// Returns true if lhs should be sorted before rhs
pub fn less_than(lhs: &Binding, rhs: &Binding) -> bool {
let count = |m: Mods| {
usize::from(m.super_) + usize::from(m.ctrl) + usize::from(m.shift) + usize::from(m.alt)
};
let lhs_count = count(lhs.trigger.mods);
let rhs_count = count(rhs.trigger.mods);
if lhs_count != rhs_count {
return lhs_count > rhs_count;
}
if lhs.trigger.mods.int() != rhs.trigger.mods.int() {
return lhs.trigger.mods.int() > rhs.trigger.mods.int();
}
let k = |t: &Trigger| -> i64 {
match t.key {
TriggerKey::Physical(k) => k as i64,
TriggerKey::Unicode(cp) => i64::from(cp),
// Zig reads the C-union tag's key here (undefined); treat as 0.
TriggerKey::CatchAll => 0,
}
};
k(&lhs.trigger) < k(&rhs.trigger)
}
/// The key part of a [`Trigger`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TriggerKey {
/// key is the "physical" version. This is the same as mapped for
/// standard US keyboard layouts. For non-US keyboard layouts, this
/// is used to bind to a physical key location rather than a translated
/// key.
Physical(key::Key),
/// This is used for binding to keys that produce a certain unicode
/// codepoint. This is useful for binding to keys that don't have a
/// registered keycode with Ghostty.
Unicode(u32),
/// A catch-all key that matches any key press that is otherwise
/// unbound.
CatchAll,
}
/// Trigger is the associated key state that can trigger an action.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Trigger {
/// The key that has to be pressed for a binding to take action.
pub key: TriggerKey,
/// The key modifiers that must be active for this to match.
pub mods: Mods,
}
impl Default for Trigger {
fn default() -> Self {
Trigger {
key: TriggerKey::Physical(key::Key::Unidentified),
mods: Mods::default(),
}
}
}
/// Backwards compatible key names from Ghostty 1.1.x. Ghostty 1.2+ doesn't
/// support these anymore since we moved to W3C key codes.
pub const BACKWARDS_COMPATIBLE_KEYS: &[(&str, TriggerKey)] = {
use TriggerKey::{Physical as P, Unicode as U};
use key::Key as K;
&[
("zero", U('0' as u32)),
("one", U('1' as u32)),
("two", U('2' as u32)),
("three", U('3' as u32)),
("four", U('4' as u32)),
("five", U('5' as u32)),
("six", U('6' as u32)),
("seven", U('7' as u32)),
("eight", U('8' as u32)),
("nine", U('9' as u32)),
("plus", U('+' as u32)),
("apostrophe", U('\'' as u32)),
("grave_accent", P(K::Backquote)),
("left_bracket", P(K::BracketLeft)),
("right_bracket", P(K::BracketRight)),
("up", P(K::ArrowUp)),
("down", P(K::ArrowDown)),
("left", P(K::ArrowLeft)),
("right", P(K::ArrowRight)),
("kp_0", P(K::Numpad0)),
("kp_1", P(K::Numpad1)),
("kp_2", P(K::Numpad2)),
("kp_3", P(K::Numpad3)),
("kp_4", P(K::Numpad4)),
("kp_5", P(K::Numpad5)),
("kp_6", P(K::Numpad6)),
("kp_7", P(K::Numpad7)),
("kp_8", P(K::Numpad8)),
("kp_9", P(K::Numpad9)),
("kp_add", P(K::NumpadAdd)),
("kp_subtract", P(K::NumpadSubtract)),
("kp_multiply", P(K::NumpadMultiply)),
("kp_divide", P(K::NumpadDivide)),
("kp_decimal", P(K::NumpadDecimal)),
("kp_enter", P(K::NumpadEnter)),
("kp_equal", P(K::NumpadEqual)),
("kp_separator", P(K::NumpadSeparator)),
("kp_left", P(K::NumpadLeft)),
("kp_right", P(K::NumpadRight)),
("kp_up", P(K::NumpadUp)),
("kp_down", P(K::NumpadDown)),
("kp_page_up", P(K::NumpadPageUp)),
("kp_page_down", P(K::NumpadPageDown)),
("kp_home", P(K::NumpadHome)),
("kp_end", P(K::NumpadEnd)),
("kp_insert", P(K::NumpadInsert)),
("kp_delete", P(K::NumpadDelete)),
("kp_begin", P(K::NumpadBegin)),
("left_shift", P(K::ShiftLeft)),
("right_shift", P(K::ShiftRight)),
("left_control", P(K::ControlLeft)),
("right_control", P(K::ControlRight)),
("left_alt", P(K::AltLeft)),
("right_alt", P(K::AltRight)),
("left_super", P(K::MetaLeft)),
("right_super", P(K::MetaRight)),
// Physical variants. This is a blunt approach to this but its
// glue for backwards compatibility so I'm not too worried about
// making this super nice.
("physical:zero", P(K::Digit0)),
("physical:one", P(K::Digit1)),
("physical:two", P(K::Digit2)),
("physical:three", P(K::Digit3)),
("physical:four", P(K::Digit4)),
("physical:five", P(K::Digit5)),
("physical:six", P(K::Digit6)),
("physical:seven", P(K::Digit7)),
("physical:eight", P(K::Digit8)),
("physical:nine", P(K::Digit9)),
("physical:apostrophe", P(K::Quote)),
("physical:grave_accent", P(K::Backquote)),
("physical:left_bracket", P(K::BracketLeft)),
("physical:right_bracket", P(K::BracketRight)),
("physical:up", P(K::ArrowUp)),
("physical:down", P(K::ArrowDown)),
("physical:left", P(K::ArrowLeft)),
("physical:right", P(K::ArrowRight)),
("physical:kp_0", P(K::Numpad0)),
("physical:kp_1", P(K::Numpad1)),
("physical:kp_2", P(K::Numpad2)),
("physical:kp_3", P(K::Numpad3)),
("physical:kp_4", P(K::Numpad4)),
("physical:kp_5", P(K::Numpad5)),
("physical:kp_6", P(K::Numpad6)),
("physical:kp_7", P(K::Numpad7)),
("physical:kp_8", P(K::Numpad8)),
("physical:kp_9", P(K::Numpad9)),
("physical:kp_add", P(K::NumpadAdd)),
("physical:kp_subtract", P(K::NumpadSubtract)),
("physical:kp_multiply", P(K::NumpadMultiply)),
("physical:kp_divide", P(K::NumpadDivide)),
("physical:kp_decimal", P(K::NumpadDecimal)),
("physical:kp_enter", P(K::NumpadEnter)),
("physical:kp_equal", P(K::NumpadEqual)),
("physical:kp_separator", P(K::NumpadSeparator)),
("physical:kp_left", P(K::NumpadLeft)),
("physical:kp_right", P(K::NumpadRight)),
("physical:kp_up", P(K::NumpadUp)),
("physical:kp_down", P(K::NumpadDown)),
("physical:kp_page_up", P(K::NumpadPageUp)),
("physical:kp_page_down", P(K::NumpadPageDown)),
("physical:kp_home", P(K::NumpadHome)),
("physical:kp_end", P(K::NumpadEnd)),
("physical:kp_insert", P(K::NumpadInsert)),
("physical:kp_delete", P(K::NumpadDelete)),
("physical:kp_begin", P(K::NumpadBegin)),
("physical:left_shift", P(K::ShiftLeft)),
("physical:right_shift", P(K::ShiftRight)),
("physical:left_control", P(K::ControlLeft)),
("physical:right_control", P(K::ControlRight)),
("physical:left_alt", P(K::AltLeft)),
("physical:right_alt", P(K::AltRight)),
("physical:left_super", P(K::MetaLeft)),
("physical:right_super", P(K::MetaRight)),
]
};
/// The codepoint we use for comparisons. Case folding can result
/// in more codepoints so we need to use a 3 element array.
///
/// DEVIATION: Zig uses uucode's full case folding table. Here we use
/// `char::to_lowercase` (single-char results only) plus a few simple-folding
/// special cases; multi-codepoint foldings return the codepoint as-is exactly
/// like Zig does.
fn folded_codepoint(cp: u32) -> [u32; 3] {
// ASCII fast path
if let Some(c) = char::from_u32(cp)
&& c.is_ascii_alphabetic()
{
return [u32::from(c.to_ascii_lowercase()), 0, 0];
}
// Unicode slow path. Case folding can result in more codepoints.
// If more codepoints are produced then we return the codepoint
// as-is which isn't correct but until we have a failing test
// then I don't want to handle this.
let Some(c) = char::from_u32(cp) else {
return [cp, 0, 0];
};
// Simple-fold differences from lowercase.
match c {
'\u{00B5}' => return [0x03BC, 0, 0],
'\u{017F}' => return ['s' as u32, 0, 0],
'\u{03C2}' => return [0x03C3, 0, 0],
_ => {}
}
let mut it = c.to_lowercase();
match (it.next(), it.next()) {
(Some(l), None) => [l as u32, 0, 0],
_ => [cp, 0, 0],
}
}
impl Trigger {
/// Parse a single trigger. The input is expected to be ONLY the trigger
/// (i.e. in the sequence `a=ignore` input is only `a`). The trigger may
/// not be part of a sequence (i.e. `a>b`). This parses exactly a single
/// trigger.
pub fn parse(input: &str) -> Result<Trigger, Error> {
if input.is_empty() {
return Err(Error::InvalidFormat);
}
let mut result = Trigger::default();
let mut rem: &str = input;
'outer: while !rem.is_empty() {
let idx = rem.find('+').unwrap_or(rem.len());
let part = &rem[..idx];
rem = if idx >= rem.len() {
""
} else {
&rem[idx + 1..]
};
// Check if its a modifier
{
let slot: Option<&mut bool> = match part {
"shift" => Some(&mut result.mods.shift),
"ctrl" => Some(&mut result.mods.ctrl),
"alt" => Some(&mut result.mods.alt),
"super" => Some(&mut result.mods.super_),
"caps_lock" => Some(&mut result.mods.caps_lock),
"num_lock" => Some(&mut result.mods.num_lock),
_ => None,
};
if let Some(slot) = slot {
// Repeat not allowed
if *slot {
return Err(Error::InvalidFormat);
}
*slot = true;
continue 'outer;
}
}
// Alias modifiers
for (alias, m) in key_mods::ALIAS {
if part == *alias {
// Repeat not allowed
let slot = match m {
key_mods::Mod::Shift => &mut result.mods.shift,
key_mods::Mod::Ctrl => &mut result.mods.ctrl,
key_mods::Mod::Alt => &mut result.mods.alt,
key_mods::Mod::Super => &mut result.mods.super_,
};
if *slot {
return Err(Error::InvalidFormat);
}
*slot = true;
continue 'outer;
}
}
// Anything after this point is a key and we only support
// single keys.
if !result.is_key_unset() {
return Err(Error::InvalidFormat);
}
// If the part is empty it means that it is actually
// a literal `+`, which we treat as a Unicode character.
if part.is_empty() {
result.key = TriggerKey::Unicode('+' as u32);
continue;
}
// Check if its a key
if part != "unidentified"
&& let Some(k) = key::Key::from_name(part)
{
result.key = TriggerKey::Physical(k);
continue;
}
// If we're still unset and we have exactly one unicode
// character then we can use that as a key.
{
let mut it = part.chars();
if let (Some(c), None) = (it.next(), it.next()) {
result.key = TriggerKey::Unicode(c as u32);
continue;
}
}
// Look for a matching w3c name next.
if let Some(w3c_key) = key::Key::from_w3c(part) {
result.key = TriggerKey::Physical(w3c_key);
continue;
}
// Check for catch_all. We do this near the end since its unlikely
// in most cases that we're setting a catch-all key.
if part == "catch_all" {
result.key = TriggerKey::CatchAll;
continue;
}
// If we're still unset then we look for backwards compatible
// keys with Ghostty 1.1.x. We do this last so its least likely
// to impact performance for modern users.
if let Some((_, old_key)) = BACKWARDS_COMPATIBLE_KEYS.iter().find(|e| e.0 == part) {
result.key = *old_key;
continue;
}
// We didn't recognize this value
return Err(Error::InvalidFormat);
}
Ok(result)
}
/// Returns true if this trigger has no key set.
pub fn is_key_unset(&self) -> bool {
match self.key {
TriggerKey::Physical(v) => v == key::Key::Unidentified,
TriggerKey::Unicode(_) | TriggerKey::CatchAll => false,
}
}
/// Returns a hash code that can be used to uniquely identify this trigger.
pub fn hash(&self) -> u64 {
let mut h = DefaultHasher::new();
self.hash_incremental(&mut h);
h.finish()
}
/// Hash the trigger into the given hasher (case-folded for unicode keys).
pub fn hash_incremental<H: Hasher>(&self, h: &mut H) {
std::mem::discriminant(&self.key).hash(h);
match self.key {
TriggerKey::Physical(v) => v.hash(h),
TriggerKey::Unicode(cp) => folded_codepoint(cp).hash(h),
TriggerKey::CatchAll => {}
}
self.mods.binding().hash(h);
}
/// Returns true if two triggers are equal.
pub fn equal(&self, other: &Trigger) -> bool {
self == other
}
/// Returns true if two triggers are equal using folded codepoints.
pub fn folded_equal(&self, other: &Trigger) -> bool {
if self.mods != other.mods {
return false;
}
match (self.key, other.key) {
(TriggerKey::Physical(a), TriggerKey::Physical(b)) => a == b,
(TriggerKey::Unicode(a), TriggerKey::Unicode(b)) => {
folded_codepoint(a) == folded_codepoint(b)
}
(TriggerKey::CatchAll, TriggerKey::CatchAll) => true,
_ => false,
}
}
/// Convert the trigger to a C API compatible trigger.
pub fn cval(&self) -> TriggerC {
match self.key {
TriggerKey::Physical(v) => TriggerC {
tag: TriggerCTag::Physical,
key: v as u32,
mods: self.mods,
},
TriggerKey::Unicode(v) => TriggerC {
tag: TriggerCTag::Unicode,
key: v,
mods: self.mods,
},
TriggerKey::CatchAll => TriggerC {
tag: TriggerCTag::CatchAll,
key: 0,
mods: self.mods,
},
}
}
}
/// C API tag for triggers.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum TriggerCTag {
Physical,
Unicode,
CatchAll,
}
/// The struct used for triggers in the C API (`Trigger.C`).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TriggerC {
pub tag: TriggerCTag,
/// Physical key enum value or unicode codepoint (undefined for catch_all).
pub key: u32,
pub mods: Mods,
}
impl fmt::Display for Trigger {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// Modifiers first
if self.mods.super_ {
f.write_str("super+")?;
}
if self.mods.ctrl {
f.write_str("ctrl+")?;
}
if self.mods.alt {
f.write_str("alt+")?;
}
if self.mods.shift {
f.write_str("shift+")?;
}
// Key
match self.key {
TriggerKey::Physical(k) => f.write_str(k.name()),
TriggerKey::Unicode(c) => {
// Zig `{u}` writes the codepoint as UTF-8.
match char::from_u32(c) {
Some(ch) => write!(f, "{ch}"),
None => f.write_str("\u{FFFD}"),
}
}
TriggerKey::CatchAll => f.write_str("catch_all"),
}
}
}
/// Hash-map key wrapper for triggers: equality and hashing use the
/// case-folded comparison (`Trigger.bindingSetEqual`).
#[derive(Debug, Clone, Copy)]
pub struct FoldedTrigger(pub Trigger);
impl PartialEq for FoldedTrigger {
fn eq(&self, other: &Self) -> bool {
self.0.folded_equal(&other.0)
}
}
impl Eq for FoldedTrigger {}
impl Hash for FoldedTrigger {
fn hash<H: Hasher>(&self, state: &mut H) {
self.0.hash_incremental(state);
}
}
#[cfg(test)]
mod tests;
+681
View File
@@ -0,0 +1,681 @@
//! A structure that contains a set of bindings and focuses on fast lookup.
//! The use case is that this will be called on EVERY key input to look
//! for an associated action so it must be fast. (`Binding.Set`)
use std::fmt::Write as _;
use indexmap::IndexMap;
use super::{Action, Elem, Error, Flags, FoldedTrigger, Parser, Trigger, TriggerKey};
use crate::key::KeyEvent;
/// TODO(integration): stand-in for `config/formatter.zig` `EntryFormatter`.
/// The config crate implements this to write `keybind = <value>` lines.
pub trait EntryFormatter {
/// Format a single `key = value` entry where `value` is a string.
fn format_entry(&mut self, value: &str);
}
/// Errors from [`Set::append_chain`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum AppendChainError {
#[error("no chain parent")]
NoChainParent,
}
/// Leaf node of a set is an action to trigger. This is a "leaf" compared
/// to the inner nodes which are "leaders" for sequences.
#[derive(Debug, Clone, PartialEq)]
pub struct Leaf {
pub action: Action,
pub flags: Flags,
}
impl Leaf {
pub fn hash(&self) -> u64 {
use std::hash::{Hash, Hasher};
let mut h = std::collections::hash_map::DefaultHasher::new();
self.action.hash_into(&mut h);
self.flags.hash(&mut h);
h.finish()
}
pub fn generic(&self) -> GenericLeaf<'_> {
GenericLeaf {
flags: self.flags,
actions: std::slice::from_ref(&self.action),
}
}
}
/// Leaf node of a set that triggers multiple actions in sequence.
#[derive(Debug, Clone, PartialEq)]
pub struct LeafChained {
pub actions: Vec<Action>,
pub flags: Flags,
}
impl LeafChained {
pub fn generic(&self) -> GenericLeaf<'_> {
GenericLeaf {
flags: self.flags,
actions: &self.actions,
}
}
}
/// A generic leaf node that can be used to unify the handling of
/// leaf and leaf_chained.
#[derive(Debug, Clone, Copy)]
pub struct GenericLeaf<'a> {
pub flags: Flags,
pub actions: &'a [Action],
}
impl GenericLeaf<'_> {
pub fn actions_slice(&self) -> &[Action] {
self.actions
}
}
/// The entry type for the forward mapping of trigger to action.
#[derive(Debug, Clone)]
pub enum Value {
/// This key is a leader key in a sequence. You must follow the given
/// set to find the next key in the sequence.
Leader(Box<Set>),
/// This trigger completes a sequence and the value is the action
/// to take along with the flags that may define binding behavior.
Leaf(Leaf),
/// A set of actions to take in response to a trigger.
LeafChained(LeafChained),
}
impl Value {
pub fn as_leaf(&self) -> Option<&Leaf> {
match self {
Value::Leaf(l) => Some(l),
_ => None,
}
}
pub fn as_leader(&self) -> Option<&Set> {
match self {
Value::Leader(s) => Some(s),
_ => None,
}
}
pub fn as_leaf_chained(&self) -> Option<&LeafChained> {
match self {
Value::LeafChained(l) => Some(l),
_ => None,
}
}
/// Implements the formatter for the fmt package. This encodes the
/// action back into the format used by parse.
///
/// NOTE: the Zig version has no `leaf_chained` prong (it would not
/// compile if used); here only the first action is written.
pub fn format(&self) -> String {
let mut out = String::new();
match self {
Value::Leader(set) => {
// the leader key was already printed.
for (k, v) in &set.bindings {
let _ = write!(out, ">{}{}", k.0, v.format());
}
}
Value::Leaf(leaf) => {
// action implements the format
let _ = write!(out, "={}", leaf.action);
}
Value::LeafChained(leaf) => {
if let Some(a) = leaf.actions.first() {
let _ = write!(out, "={a}");
}
}
}
out
}
/// Writes the configuration entries for the binding
/// that this value is part of.
///
/// The value may be part of multiple configuration entries
/// if they're all part of the same prefix sequence (e.g. 'a>b', 'a>c').
/// These will result in multiple separate entries in the configuration.
///
/// `buffer` is temporary storage that is shared between calls to
/// nested levels of the set. For example, 'a>b>c=x' and 'a>b>d=y' will
/// reuse the 'a>b' written to the buffer before flushing it to the
/// formatter with 'c=x' and 'd=y'.
pub fn format_entries(&self, buffer: &mut String, formatter: &mut dyn EntryFormatter) {
match self {
Value::Leader(set) => {
// We'll rewind to this position after each sub-entry,
// sharing the prefix between siblings.
let pos = buffer.len();
for (k, v) in &set.bindings {
buffer.truncate(pos);
let _ = write!(buffer, ">{}", k.0);
v.format_entries(buffer, formatter);
}
buffer.truncate(pos);
}
Value::Leaf(leaf) => {
// When we get to the leaf, the buffer contains
// the full sequence of keys needed to reach this action.
let pos = buffer.len();
let _ = write!(buffer, "={}", leaf.action);
formatter.format_entry(buffer);
buffer.truncate(pos);
}
Value::LeafChained(leaf) => {
let pos = buffer.len();
for (i, action) in leaf.actions.iter().enumerate() {
if i == 0 {
let _ = write!(buffer, "={action}");
formatter.format_entry(buffer);
buffer.truncate(pos);
} else {
// DEVIATION: Zig rewinds the shared buffer to 0 and
// overwrites the sequence prefix; we use a scratch
// string so sibling entries keep their prefix.
let line = format!("chain={action}");
formatter.format_entry(&line);
}
}
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum RecurseError {
SequenceUnbind,
NoChainParent,
UnexpectedEndOfInput,
InvalidChainAction,
}
/// A set of bindings with fast lookup.
#[derive(Debug, Default)]
pub struct Set {
/// The set of bindings.
pub bindings: IndexMap<FoldedTrigger, Value>,
/// The reverse mapping of action to binding. Note that multiple
/// bindings can map to the same action and this map will only have
/// the most recently added binding for an action.
///
/// Sequenced triggers are never present in the reverse map at this time.
/// This is a conscious decision since the primary use case of the reverse
/// map is to support GUI toolkit keyboard accelerators and no mainstream
/// GUI toolkit supports sequences.
///
/// Performable triggers are also not present in the reverse map. This
/// is so that GUI toolkits don't register performable triggers as
/// menu shortcuts (the primary use case of the reverse map). GUI toolkits
/// such as GTK handle menu shortcuts too early in the event lifecycle
/// for performable to work so this is a conscious decision to ease the
/// integration with GUI toolkits.
reverse: IndexMap<Action, Trigger>,
/// The chain parent is the information necessary to attach a chained
/// action to the proper location in our mapping.
///
/// DEVIATION: Zig stores raw pointers; here it is the path of triggers
/// (through leader sets) from this set to the parent entry. The last
/// element is the parent's own trigger.
chain_parent: Option<Vec<Trigger>>,
}
impl Clone for Set {
/// Deep clone the set. The clone has no chain parent.
fn clone(&self) -> Self {
Set {
bindings: self.bindings.clone(),
reverse: self.reverse.clone(),
chain_parent: None,
}
}
}
impl Set {
pub fn new() -> Set {
Set::default()
}
/// Deep clone the set (see [`Clone`]).
pub fn clone_set(&self) -> Set {
self.clone()
}
/// The trigger path of the current chain parent, if any.
pub fn chain_parent(&self) -> Option<&[Trigger]> {
self.chain_parent.as_deref()
}
/// The (last) trigger of the chain parent, if any.
pub fn chain_parent_trigger(&self) -> Option<Trigger> {
self.chain_parent.as_ref().and_then(|p| p.last().copied())
}
/// The value of the chain parent, if any.
pub fn chain_parent_value(&self) -> Option<&Value> {
let path = self.chain_parent.as_ref()?;
let (last, prefix) = path.split_last()?;
let mut cur = self;
for t in prefix {
cur = cur.bindings.get(&FoldedTrigger(*t))?.as_leader()?;
}
cur.bindings.get(&FoldedTrigger(*last))
}
/// Parse a user input binding and add it to the set. This will handle
/// the "unbind" case, ensure consumed/unconsumed fields are set correctly,
/// handle sequences, etc.
///
/// Any Error returned is validated before any set modifications are made.
pub fn parse_and_put(&mut self, input: &str) -> Result<(), Error> {
// To make cleanup easier, we ensure that the full sequence is
// valid before making any set modifications.
let mut it = Parser::new(input)?;
while it.next_elem()?.is_some() {}
it.reset();
match Set::parse_and_put_recurse(self, &mut it, true) {
// If we have an updated set (a binding was added) then we store
// it for our chain parent.
Ok(Some(path)) => self.chain_parent = Some(path),
// If we didn't update a set then we clear our chain parent since
// chaining is no longer valid until a valid binding is saved.
// SequenceUnbind reaching the root means we've unbound all the
// way up and this put was a success.
Ok(None) | Err(RecurseError::SequenceUnbind) => self.chain_parent = None,
// If our parser input was too short then the format
// is invalid because we handle all valid cases. A chain without
// a parent or an invalid chain action are format errors too.
Err(
RecurseError::UnexpectedEndOfInput
| RecurseError::NoChainParent
| RecurseError::InvalidChainAction,
) => return Err(Error::InvalidFormat),
}
Ok(())
}
/// Returns the path (relative to `set`) of the leaf that was
/// ultimately updated if a binding was added.
fn parse_and_put_recurse(
set: &mut Set,
it: &mut Parser<'_>,
root: bool,
) -> Result<Option<Vec<Trigger>>, RecurseError> {
let elem = it
.next_elem()
.ok()
.flatten()
.ok_or(RecurseError::UnexpectedEndOfInput)?;
match elem {
Elem::Leader(t) => {
// If we have a leader, we need to upsert a set for it.
enum Old {
None,
Leaf(Leaf),
Chained(LeafChained),
}
let key = FoldedTrigger(t);
let mut old = Old::None;
if let Some(entry) = set.bindings.get_mut(&key) {
match entry {
// We have an existing leader for this key already
// so recurse into this set.
Value::Leader(s) => {
let r = Set::parse_and_put_recurse(s, it, false);
return match r {
Ok(path) => Ok(path.map(|p| prepend(t, p))),
// Our child put unbound. If our set is empty we
// need to dealloc and continue up. If our set is
// not empty then we're done.
Err(RecurseError::SequenceUnbind) => {
if s.bindings.is_empty() {
set.remove(t);
Err(RecurseError::SequenceUnbind)
} else {
Ok(None)
}
}
Err(e) => Err(e),
};
}
Value::Leaf(l) => old = Old::Leaf(l.clone()),
Value::LeafChained(l) => old = Old::Chained(l.clone()),
}
// Remove the existing action. Fallthrough as if
// we don't have a leader.
set.remove(t);
}
// Create our new set for this leader and insert the leader entry
set.bindings.insert(key, Value::Leader(Box::default()));
// Recurse
let r = {
let Some(Value::Leader(next)) = set.bindings.get_mut(&key) else {
unreachable!("leader was just inserted");
};
Set::parse_and_put_recurse(next, it, false)
};
match r {
Ok(path) => Ok(path.map(|p| prepend(t, p))),
// If our action was to unbind, we restore the old
// action if we have it.
Err(RecurseError::SequenceUnbind) => {
set.remove(t);
match old {
Old::None => {}
Old::Leaf(leaf) => set.put_flags(t, leaf.action, leaf.flags),
Old::Chained(leaf) => {
// Rebuild our chain
let mut actions = leaf.actions.into_iter();
if let Some(first) = actions.next() {
set.put_flags(t, first, leaf.flags);
for action in actions {
if set.append_chain(action).is_err() {
set.remove(t);
break;
}
}
}
}
}
Ok(None)
}
Err(e) => Err(e),
}
}
Elem::Binding(b) => match b.action {
Action::Unbind => {
set.remove(b.trigger);
Err(RecurseError::SequenceUnbind)
}
action => {
set.put_flags(b.trigger, action, b.flags);
Ok(set.chain_parent.clone())
}
},
Elem::Chain(action) => {
// Chains can only happen on the root.
debug_assert!(root);
// Unbind is not valid for chains.
if action == Action::Unbind {
return Err(RecurseError::InvalidChainAction);
}
set.append_chain(action)
.map_err(|_| RecurseError::NoChainParent)?;
Ok(set.chain_parent.clone())
}
}
}
/// Add a binding to the set. If the binding already exists then
/// this will overwrite it.
pub fn put(&mut self, t: Trigger, action: Action) {
self.put_flags(t, action, Flags::default());
}
/// Add a binding to the set with explicit flags.
pub fn put_flags(&mut self, t: Trigger, action: Action, flags: Flags) {
// unbind should never go into the set, it should be handled prior
assert_ne!(action, Action::Unbind);
// This is true if we're going to track this entry as
// a reverse mapping. There are certain scenarios we don't.
// See the reverse map docs for more information.
let track_reverse = !flags.performable;
self.chain_parent = None;
let key = FoldedTrigger(t);
// If we have an existing binding for this trigger, we have to
// update the reverse mapping to remove the old action. A leader or
// chained leaf is simply replaced (chained leaves aren't in the
// reverse mapping).
if track_reverse && matches!(self.bindings.get(&key), Some(Value::Leaf(_))) {
let t_hash = t.hash();
if let Some(i) = self.reverse.values().position(|v| v.hash() == t_hash) {
self.reverse.swap_remove_index(i);
}
}
// `insert` keeps the existing key and position (like getOrPut).
self.bindings.insert(
key,
Value::Leaf(Leaf {
action: action.clone(),
flags,
}),
);
if track_reverse {
self.reverse.insert(action, t);
}
let stored = self.bindings.get_key_value(&key).map_or(t, |(k, _)| k.0);
self.chain_parent = Some(vec![stored]);
}
/// Append a chained action to the prior set action.
///
/// It is an error if there is no valid prior chain parent.
pub fn append_chain(&mut self, action: Action) -> Result<(), AppendChainError> {
// Unbind is not a valid chain action; callers must check this.
assert_ne!(action, Action::Unbind);
let path = self
.chain_parent
.clone()
.ok_or(AppendChainError::NoChainParent)?;
let (last, prefix) = path.split_last().ok_or(AppendChainError::NoChainParent)?;
// Navigate to the set that owns the parent entry.
let mut parent_set: &mut Set = self;
for t in prefix {
match parent_set.bindings.get_mut(&FoldedTrigger(*t)) {
Some(Value::Leader(s)) => parent_set = s,
_ => return Err(AppendChainError::NoChainParent),
}
}
let key = FoldedTrigger(*last);
let Some((idx, stored_key, value)) = parent_set.bindings.get_full_mut(&key) else {
return Err(AppendChainError::NoChainParent);
};
let stored_key = stored_key.0;
let _ = idx;
match value {
// Leader can never be a chain parent. Verified through various
// assertions and unit tests.
Value::Leader(_) => unreachable!("leader can never be a chain parent"),
// If it is already a chained action, we just append the
// action. Easy!
Value::LeafChained(leaf) => leaf.actions.push(action),
// If it is a leaf, we need to convert it to a leaf_chained.
// We also need to be careful to remove any prior reverse
// mappings for this action since chained actions are not
// part of the reverse mapping.
Value::Leaf(leaf) => {
let first = leaf.action.clone();
let flags = leaf.flags;
// Convert to leaf_chained first, before fixing up reverse
// mapping, so the fixup doesn't find this (now chained) entry.
*value = Value::LeafChained(LeafChained {
actions: vec![first.clone(), action],
flags,
});
// Clean up our reverse mapping. Chained actions are not
// part of the reverse mapping, so we need to fix up the
// reverse map (possibly restoring another trigger for the
// same action).
parent_set.fixup_reverse_for_action(&first, stored_key);
}
}
Ok(())
}
/// Get a binding for a given trigger.
pub fn get(&self, t: Trigger) -> Option<&Value> {
self.bindings.get(&FoldedTrigger(t))
}
/// Get the stored key and value for a given trigger.
pub fn get_entry(&self, t: Trigger) -> Option<(&Trigger, &Value)> {
self.bindings
.get_key_value(&FoldedTrigger(t))
.map(|(k, v)| (&k.0, v))
}
/// Get a trigger for the given action. An action can have multiple
/// triggers so this will return the first one found.
pub fn get_trigger(&self, a: &Action) -> Option<Trigger> {
self.reverse.get(a).copied()
}
/// Get an entry for the given key event. This will attempt to find
/// a binding using multiple parts of the event in the following order:
///
/// 1. Translated key (event.key)
/// 2. Physical key (event.physical_key)
/// 3. Unshifted Unicode codepoint (event.unshifted_codepoint)
pub fn get_event(&self, event: &KeyEvent<'_>) -> Option<(&Trigger, &Value)> {
let mut trigger = Trigger {
mods: event.mods.binding(),
key: TriggerKey::Physical(event.key),
};
if let Some(v) = self.get_entry(trigger) {
return Some(v);
}
// If our UTF-8 text is exactly one codepoint, we try to match that.
if !event.utf8.is_empty() {
let mut it = event.utf8.chars();
// No codepoints or multiple codepoints drops to invalid format
if let (Some(cp), None) = (it.next(), it.next()) {
trigger.key = TriggerKey::Unicode(cp as u32);
if let Some(v) = self.get_entry(trigger) {
return Some(v);
}
}
}
// Finally fallback to the full unshifted codepoint if we have one.
// Question: should we be doing this if we have UTF-8 text? I
// suspect "no" but we don't currently have any failing scenarios
// to verify this.
if event.unshifted_codepoint > 0 {
trigger.key = TriggerKey::Unicode(event.unshifted_codepoint);
if let Some(v) = self.get_entry(trigger) {
return Some(v);
}
}
// Fallback to catch_all with modifiers first, then without modifiers.
trigger.key = TriggerKey::CatchAll;
if let Some(v) = self.get_entry(trigger) {
return Some(v);
}
if !trigger.mods.empty() {
trigger.mods = crate::key_mods::Mods::default();
if let Some(v) = self.get_entry(trigger) {
return Some(v);
}
}
None
}
/// Remove a binding for a given trigger.
pub fn remove(&mut self, t: Trigger) {
// Removal always resets our chain parent. We could make this
// finer grained but the way it is documented is that chaining
// must happen directly after sets so this works.
self.chain_parent = None;
let Some(entry) = self.bindings.swap_remove(&FoldedTrigger(t)) else {
return;
};
match entry {
// For a leader removal, the child set is dropped. Leaders are
// never part of reverse maps so no other accounting is needed.
Value::Leader(_) => {}
// For an action we need to fix up the reverse mapping.
Value::Leaf(leaf) => self.fixup_reverse_for_action(&leaf.action, t),
// Chained leaves are never in our reverse mapping so no
// cleanup is required.
Value::LeafChained(_) => {}
}
}
/// Fix up the reverse mapping after removing an action.
///
/// When an action is removed from a binding (either by removal or by
/// converting to a chained action), we need to update the reverse mapping.
/// If another binding has the same action, we update the reverse mapping
/// to point to that binding. Otherwise, we remove the action from the
/// reverse mapping entirely.
///
/// The `old` parameter is the trigger that was previously bound to this
/// action. It is used to check if the reverse mapping still points to
/// this trigger; if not, no fixup is needed since the reverse map already
/// points to a different trigger for this action.
fn fixup_reverse_for_action(&mut self, action: &Action, old: Trigger) {
let Some(entry) = self.reverse.get_mut(action) else {
return;
};
// If our value is not the same as the old trigger, we can
// ignore it because our reverse mapping points somewhere else.
if !entry.equal(&old) {
return;
}
// It is the same trigger, so let's now go through our bindings
// and try to find another trigger that maps to the same action.
let action_hash = action.hash();
for (k, v) in &self.bindings {
if let Value::Leaf(leaf_search) = v
&& leaf_search.action.hash() == action_hash
{
*entry = k.0;
return;
}
}
// No other trigger points to this action so we remove
// the reverse mapping completely.
self.reverse.swap_remove(action);
}
}
fn prepend(t: Trigger, mut p: Vec<Trigger>) -> Vec<Trigger> {
p.insert(0, t);
p
}
+1560
View File
@@ -0,0 +1,1560 @@
#![allow(clippy::unwrap_used)]
use super::set::{AppendChainError, EntryFormatter, Value};
use super::*;
use crate::key::{Key, KeyEvent};
use crate::key_mods::Mods;
fn uni(c: char) -> Trigger {
Trigger {
key: TriggerKey::Unicode(c as u32),
..Trigger::default()
}
}
fn phys(k: Key) -> TriggerKey {
TriggerKey::Physical(k)
}
fn ctrl() -> Mods {
Mods {
ctrl: true,
..Mods::default()
}
}
fn shift() -> Mods {
Mods {
shift: true,
..Mods::default()
}
}
fn bind(trigger: Trigger, action: Action) -> Binding {
Binding {
trigger,
action,
flags: Flags::default(),
}
}
fn bind_flags(trigger: Trigger, action: Action, flags: Flags) -> Binding {
Binding {
trigger,
action,
flags,
}
}
fn trig(mods: Mods, key: TriggerKey) -> Trigger {
Trigger { key, mods }
}
fn ps(s: &str) -> Binding {
parse_single(s).unwrap()
}
fn ps_err(s: &str) -> ParseSingleError {
parse_single(s).unwrap_err()
}
const INVALID: ParseSingleError = ParseSingleError::Error(Error::InvalidFormat);
#[test]
fn flags_cval() {
assert_eq!(Flags::default().cval(), 0b0001);
assert_eq!(
Flags {
consumed: false,
..Flags::default()
}
.cval(),
0b0000
);
assert_eq!(
Flags {
all: true,
..Flags::default()
}
.cval(),
0b0011
);
assert_eq!(
Flags {
global: true,
..Flags::default()
}
.cval(),
0b0101
);
assert_eq!(
Flags {
performable: true,
..Flags::default()
}
.cval(),
0b1001
);
assert_eq!(
Flags {
consumed: true,
all: true,
global: true,
performable: true
}
.cval(),
0b1111
);
}
#[test]
fn split_focus_direction_parse() {
use SplitFocusDirection as D;
assert_eq!(D::parse("previous"), Ok(D::Previous));
assert_eq!(D::parse("next"), Ok(D::Next));
assert_eq!(D::parse("up"), Ok(D::Up));
assert_eq!(D::parse("left"), Ok(D::Left));
assert_eq!(D::parse("down"), Ok(D::Down));
assert_eq!(D::parse("right"), Ok(D::Right));
assert_eq!(D::parse("top"), Ok(D::Up));
assert_eq!(D::parse("bottom"), Ok(D::Down));
assert_eq!(D::parse(""), Err(Error::InvalidFormat));
assert_eq!(D::parse("green"), Err(Error::InvalidFormat));
}
#[test]
fn parse_triggers() {
// single character
assert_eq!(ps("a=ignore"), bind(uni('a'), Action::Ignore));
// single modifier
assert_eq!(
ps("shift+a=ignore"),
bind(
trig(shift(), TriggerKey::Unicode('a' as u32)),
Action::Ignore
)
);
assert_eq!(
ps("ctrl+a=ignore"),
bind(
trig(ctrl(), TriggerKey::Unicode('a' as u32)),
Action::Ignore
)
);
// multiple modifier
assert_eq!(
ps("shift+ctrl+a=ignore"),
bind(
trig(
Mods {
shift: true,
ctrl: true,
..Mods::default()
},
TriggerKey::Unicode('a' as u32)
),
Action::Ignore
)
);
// key can come before modifier
assert_eq!(
ps("a+shift=ignore"),
bind(
trig(shift(), TriggerKey::Unicode('a' as u32)),
Action::Ignore
)
);
// physical keys
assert_eq!(
ps("shift+key_a=ignore"),
bind(trig(shift(), phys(Key::KeyA)), Action::Ignore)
);
// unicode keys
assert_eq!(
ps("shift+ö=ignore"),
bind(
trig(shift(), TriggerKey::Unicode('ö' as u32)),
Action::Ignore
)
);
// unconsumed keys
let unconsumed = Flags {
consumed: false,
..Flags::default()
};
assert_eq!(
ps("unconsumed:shift+a=ignore"),
bind_flags(
trig(shift(), TriggerKey::Unicode('a' as u32)),
Action::Ignore,
unconsumed
)
);
// unconsumed physical keys
assert_eq!(
ps("unconsumed:key_a+shift=ignore"),
bind_flags(trig(shift(), phys(Key::KeyA)), Action::Ignore, unconsumed)
);
// performable keys
assert_eq!(
ps("performable:shift+a=ignore"),
bind_flags(
trig(shift(), TriggerKey::Unicode('a' as u32)),
Action::Ignore,
Flags {
performable: true,
..Flags::default()
}
)
);
// invalid key
assert_eq!(ps_err("foo=ignore"), INVALID);
// repeated control
assert_eq!(ps_err("shift+shift+a=ignore"), INVALID);
// multiple character
assert_eq!(ps_err("a+b=ignore"), INVALID);
}
#[test]
fn parse_w3c_key_names() {
// Exact match
assert_eq!(
ps("KeyA=ignore"),
bind(trig(Mods::default(), phys(Key::KeyA)), Action::Ignore)
);
// Case-sensitive
assert_eq!(ps_err("Keya=ignore"), INVALID);
}
#[test]
fn parse_catch_all() {
assert_eq!(
ps("catch_all=ignore"),
bind(trig(Mods::default(), TriggerKey::CatchAll), Action::Ignore)
);
assert_eq!(
ps("ctrl+catch_all=ignore"),
bind(trig(ctrl(), TriggerKey::CatchAll), Action::Ignore)
);
}
#[test]
fn parse_plus_sign() {
assert_eq!(ps("+=ignore"), bind(uni('+'), Action::Ignore));
// Modifier
assert_eq!(
ps("ctrl++=ignore"),
bind(
trig(ctrl(), TriggerKey::Unicode('+' as u32)),
Action::Ignore
)
);
assert_eq!(ps_err("++=ignore"), INVALID);
}
#[test]
fn parse_equals_sign() {
assert_eq!(ps("==ignore"), bind(uni('='), Action::Ignore));
// Modifier
assert_eq!(
ps("ctrl+==ignore"),
bind(
trig(ctrl(), TriggerKey::Unicode('=' as u32)),
Action::Ignore
)
);
assert_eq!(ps_err("=ignore"), INVALID);
}
#[test]
fn parse_text_action_equals_sign() {
let b = ps("==text:=");
assert_eq!(b.trigger, uni('='));
assert_eq!(b.action, Action::Text("=".into()));
let b = ps("==text:=hello");
assert_eq!(b.trigger, uni('='));
assert_eq!(b.action, Action::Text("=hello".into()));
let b = ps("ctrl+==text:=hello");
assert_eq!(b.trigger, trig(ctrl(), TriggerKey::Unicode('=' as u32)));
assert_eq!(b.action, Action::Text("=hello".into()));
let b = ps("=+ctrl=text:=hello");
assert_eq!(b.trigger, trig(ctrl(), TriggerKey::Unicode('=' as u32)));
assert_eq!(b.action, Action::Text("=hello".into()));
}
// For Ghostty 1.2+ we changed our key names to match the W3C and removed
// `physical:`. This tests the backwards compatibility with the old format.
#[test]
fn parse_backwards_compatibility_with_le_1_1_x() {
// simple, for sanity
assert_eq!(ps("zero=ignore"), bind(uni('0'), Action::Ignore));
assert_eq!(
ps("physical:zero=ignore"),
bind(trig(Mods::default(), phys(Key::Digit0)), Action::Ignore)
);
// duplicates
assert_eq!(ps_err("zero+one=ignore"), INVALID);
// test our full map
for (k, v) in BACKWARDS_COMPATIBLE_KEYS {
assert_eq!(
ps(&format!("{k}=ignore")),
bind(trig(Mods::default(), *v), Action::Ignore),
"{k}"
);
}
}
#[test]
fn parse_global_triggers() {
let g = Flags {
global: true,
..Flags::default()
};
assert_eq!(
ps("global:shift+a=ignore"),
bind_flags(
trig(shift(), TriggerKey::Unicode('a' as u32)),
Action::Ignore,
g
)
);
assert_eq!(
ps("global:key_a+shift=ignore"),
bind_flags(trig(shift(), phys(Key::KeyA)), Action::Ignore, g)
);
assert_eq!(
ps("unconsumed:global:a+shift=ignore"),
bind_flags(
trig(shift(), TriggerKey::Unicode('a' as u32)),
Action::Ignore,
Flags {
global: true,
consumed: false,
..Flags::default()
}
)
);
// global sequences not allowed
let mut p = Parser::new("global:a>b=ignore").unwrap();
assert_eq!(p.next_elem(), Err(Error::InvalidFormat));
}
#[test]
fn parse_all_triggers() {
let a = Flags {
all: true,
..Flags::default()
};
assert_eq!(
ps("all:shift+a=ignore"),
bind_flags(
trig(shift(), TriggerKey::Unicode('a' as u32)),
Action::Ignore,
a
)
);
assert_eq!(
ps("all:key_a+shift=ignore"),
bind_flags(trig(shift(), phys(Key::KeyA)), Action::Ignore, a)
);
assert_eq!(
ps("unconsumed:all:a+shift=ignore"),
bind_flags(
trig(shift(), TriggerKey::Unicode('a' as u32)),
Action::Ignore,
Flags {
all: true,
consumed: false,
..Flags::default()
}
)
);
// all sequences not allowed
let mut p = Parser::new("all:a>b=ignore").unwrap();
assert_eq!(p.next_elem(), Err(Error::InvalidFormat));
}
#[test]
fn trigger_equal() {
let t = |key, mods| Trigger { key, mods };
let up = phys(Key::ArrowUp);
let down = phys(Key::ArrowDown);
// Equal physical keys
assert!(t(up, ctrl()).equal(&t(up, ctrl())));
// Different physical keys
assert!(!t(up, ctrl()).equal(&t(down, ctrl())));
// Different mods
assert!(!t(up, ctrl()).equal(&t(up, shift())));
// Equal unicode keys
let ua = TriggerKey::Unicode('a' as u32);
let ub = TriggerKey::Unicode('b' as u32);
assert!(t(ua, Mods::default()).equal(&t(ua, Mods::default())));
// Different unicode keys
assert!(!t(ua, Mods::default()).equal(&t(ub, Mods::default())));
// Different key types
assert!(!t(ua, Mods::default()).equal(&t(phys(Key::KeyA), Mods::default())));
// catch_all
let ca = TriggerKey::CatchAll;
assert!(t(ca, Mods::default()).equal(&t(ca, Mods::default())));
// catch_all with different mods
let alt = Mods {
alt: true,
..Mods::default()
};
assert!(!t(ca, Mods::default()).equal(&t(ca, alt)));
}
#[test]
fn parse_modifier_aliases() {
let sup = Mods {
super_: true,
..Mods::default()
};
let alt = Mods {
alt: true,
..Mods::default()
};
let a = TriggerKey::Unicode('a' as u32);
assert_eq!(ps("cmd+a=ignore"), bind(trig(sup, a), Action::Ignore));
assert_eq!(ps("command+a=ignore"), bind(trig(sup, a), Action::Ignore));
assert_eq!(ps("opt+a=ignore"), bind(trig(alt, a), Action::Ignore));
assert_eq!(ps("option+a=ignore"), bind(trig(alt, a), Action::Ignore));
assert_eq!(
ps("control+a=ignore"),
bind(trig(ctrl(), a), Action::Ignore)
);
}
#[test]
fn parse_action_invalid() {
// invalid action
assert_eq!(
ps_err("a=nopenopenope"),
ParseSingleError::Error(Error::InvalidAction)
);
}
#[test]
fn parse_action_no_parameters() {
assert_eq!(ps("a=ignore"), bind(uni('a'), Action::Ignore));
assert_eq!(ps_err("a=ignore:A"), INVALID);
}
#[test]
fn parse_action_with_string() {
assert_eq!(ps("a=csi:A").action, Action::Csi("A".into()));
assert_eq!(ps("a=esc:A").action, Action::Esc("A".into()));
assert_eq!(
ps("a=set_surface_title:surface").action,
Action::SetSurfaceTitle("surface".into())
);
assert_eq!(
ps("a=set_tab_title:tab").action,
Action::SetTabTitle("tab".into())
);
}
#[test]
fn parse_action_with_enum() {
assert_eq!(
ps("a=new_split:right").action,
Action::NewSplit(SplitDirection::Right)
);
}
#[test]
fn parse_action_with_enum_with_default() {
assert_eq!(
ps("a=new_split").action,
Action::NewSplit(SplitDirection::Auto)
);
}
#[test]
fn parse_action_with_int() {
assert_eq!(ps("a=jump_to_prompt:-1").action, Action::JumpToPrompt(-1));
assert_eq!(ps("a=jump_to_prompt:10").action, Action::JumpToPrompt(10));
}
#[test]
fn parse_action_with_float() {
assert_eq!(
ps("a=scroll_page_fractional:-0.5").action,
Action::ScrollPageFractional(-0.5)
);
assert_eq!(
ps("a=scroll_page_fractional:+0.5").action,
Action::ScrollPageFractional(0.5)
);
}
#[test]
fn parse_action_with_a_tuple() {
assert_eq!(
ps("a=resize_split:up,10").action,
Action::ResizeSplit(SplitResizeParameter(SplitResizeDirection::Up, 10))
);
// missing parameter
assert_eq!(ps_err("a=resize_split:up"), INVALID);
// too many
assert_eq!(ps_err("a=resize_split:up,10,12"), INVALID);
// invalid type
assert_eq!(ps_err("a=resize_split:up,four"), INVALID);
}
#[test]
fn parse_chain() {
// Valid
{
let mut p = Parser::new("chain=new_tab").unwrap();
assert_eq!(p.next_elem(), Ok(Some(Elem::Chain(Action::NewTab))));
assert_eq!(p.next_elem(), Ok(None));
}
// Chain can't have flags
assert_eq!(
Parser::new("global:chain=ignore").unwrap_err(),
Error::InvalidFormat
);
// Chain can't be part of a sequence
{
let mut p = Parser::new("a>chain=ignore").unwrap();
let _ = p.next_elem();
assert_eq!(p.next_elem(), Err(Error::InvalidFormat));
}
}
#[test]
fn sequence_iterator() {
// single character
{
let mut it = SequenceIterator::new("a");
assert_eq!(it.next_trigger(), Ok(Some(uni('a'))));
assert_eq!(it.next_trigger(), Ok(None));
}
// multi character
{
let mut it = SequenceIterator::new("a>b");
assert_eq!(it.next_trigger(), Ok(Some(uni('a'))));
assert_eq!(it.next_trigger(), Ok(Some(uni('b'))));
assert_eq!(it.next_trigger(), Ok(None));
}
// empty
{
let mut it = SequenceIterator::new("");
assert_eq!(it.next_trigger(), Err(Error::InvalidFormat));
}
// empty starting sequence
{
let mut it = SequenceIterator::new(">a");
assert_eq!(it.next_trigger(), Err(Error::InvalidFormat));
}
// empty ending sequence
{
let mut it = SequenceIterator::new("a>");
assert_eq!(it.next_trigger(), Ok(Some(uni('a'))));
assert_eq!(it.next_trigger(), Err(Error::InvalidFormat));
}
}
#[test]
fn parse_sequences() {
// single character
{
let mut p = Parser::new("ctrl+a=ignore").unwrap();
assert_eq!(
p.next_elem(),
Ok(Some(Elem::Binding(bind(
trig(ctrl(), TriggerKey::Unicode('a' as u32)),
Action::Ignore
))))
);
assert_eq!(p.next_elem(), Ok(None));
}
// sequence
{
let mut p = Parser::new("a>b=ignore").unwrap();
assert_eq!(p.next_elem(), Ok(Some(Elem::Leader(uni('a')))));
assert_eq!(
p.next_elem(),
Ok(Some(Elem::Binding(bind(uni('b'), Action::Ignore))))
);
assert_eq!(p.next_elem(), Ok(None));
}
}
fn leaf(s: &Set, t: Trigger) -> &set::Leaf {
s.get(t).unwrap().as_leaf().unwrap()
}
#[test]
fn set_parse_and_put_typical_binding() {
let mut s = Set::new();
s.parse_and_put("a=new_window").unwrap();
// Creates forward mapping
let l = leaf(&s, uni('a'));
assert_eq!(l.action, Action::NewWindow);
assert_eq!(l.flags, Flags::default());
// Creates reverse mapping
let trigger = s.get_trigger(&Action::NewWindow).unwrap();
assert_eq!(trigger.key, TriggerKey::Unicode('a' as u32));
// Sets up the chain parent properly
assert!(s.chain_parent().is_some());
assert_eq!(s.chain_parent_trigger().unwrap().to_string(), "a");
}
#[test]
fn set_parse_and_put_unconsumed_binding() {
let mut s = Set::new();
s.parse_and_put("unconsumed:a=new_window").unwrap();
let l = leaf(&s, uni('a'));
assert_eq!(l.action, Action::NewWindow);
assert_eq!(
l.flags,
Flags {
consumed: false,
..Flags::default()
}
);
let trigger = s.get_trigger(&Action::NewWindow).unwrap();
assert_eq!(trigger.key, TriggerKey::Unicode('a' as u32));
assert!(s.chain_parent().is_some());
assert_eq!(s.chain_parent_trigger().unwrap().to_string(), "a");
}
#[test]
fn set_parse_and_put_removed_binding() {
let mut s = Set::new();
s.parse_and_put("a=new_window").unwrap();
s.parse_and_put("a=unbind").unwrap();
assert!(s.get(uni('a')).is_none());
assert!(s.get_trigger(&Action::NewWindow).is_none());
assert!(s.chain_parent().is_none());
}
#[test]
fn set_put_sets_chain_parent() {
let mut s = Set::new();
s.put(uni('a'), Action::NewWindow);
assert!(s.chain_parent().is_some());
assert_eq!(s.chain_parent_trigger().unwrap().to_string(), "a");
assert!(matches!(s.chain_parent_value(), Some(Value::Leaf(_))));
}
#[test]
fn set_put_flags_sets_chain_parent() {
let mut s = Set::new();
s.put_flags(
uni('a'),
Action::NewWindow,
Flags {
consumed: false,
..Flags::default()
},
);
assert_eq!(s.chain_parent_trigger().unwrap().to_string(), "a");
let v = s.chain_parent_value().unwrap().as_leaf().unwrap();
assert!(!v.flags.consumed);
}
#[test]
fn set_sequence_sets_chain_parent_to_final_leaf() {
let mut s = Set::new();
s.parse_and_put("a>b=new_window").unwrap();
assert_eq!(s.chain_parent_trigger().unwrap().to_string(), "b");
let v = s.chain_parent_value().unwrap().as_leaf().unwrap();
assert_eq!(v.action, Action::NewWindow);
}
#[test]
fn set_multiple_leaves_under_leader_updates_chain_parent() {
let mut s = Set::new();
s.parse_and_put("a>b=new_window").unwrap();
assert_eq!(s.chain_parent_trigger().unwrap().to_string(), "b");
s.parse_and_put("a>c=new_tab").unwrap();
assert_eq!(s.chain_parent_trigger().unwrap().to_string(), "c");
assert_eq!(
s.chain_parent_value().unwrap().as_leaf().unwrap().action,
Action::NewTab
);
}
#[test]
fn set_sequence_unbind_clears_chain_parent() {
let mut s = Set::new();
s.parse_and_put("a>b=new_window").unwrap();
assert!(s.chain_parent().is_some());
s.parse_and_put("a>b=unbind").unwrap();
assert!(s.chain_parent().is_none());
}
#[test]
fn set_sequence_unbind_with_remaining_leaves_clears_chain_parent() {
let mut s = Set::new();
s.parse_and_put("a>b=new_window").unwrap();
s.parse_and_put("a>c=new_tab").unwrap();
s.parse_and_put("a>b=unbind").unwrap();
// After unbind, chain_parent should be cleared even though 'c' remains
assert!(s.chain_parent().is_none());
// But 'c' should still exist
let inner = s.get(uni('a')).unwrap().as_leader().unwrap();
assert!(inner.get(uni('c')).is_some());
}
#[test]
fn set_direct_remove_clears_chain_parent() {
let mut s = Set::new();
s.put(uni('a'), Action::NewWindow);
assert!(s.chain_parent().is_some());
s.remove(uni('a'));
assert!(s.chain_parent().is_none());
}
#[test]
fn set_invalid_format_preserves_chain_parent() {
let mut s = Set::new();
s.parse_and_put("a=new_window").unwrap();
let before = s.chain_parent().map(<[Trigger]>::to_vec);
// Try an invalid parse - should fail
assert_eq!(
s.parse_and_put("a=invalid_action_xyz"),
Err(Error::InvalidAction)
);
// chain_parent should be unchanged
assert!(s.chain_parent().is_some());
assert_eq!(s.chain_parent().map(<[Trigger]>::to_vec), before);
}
#[test]
fn set_clone_produces_null_chain_parent() {
let mut s = Set::new();
s.parse_and_put("a=new_window").unwrap();
assert!(s.chain_parent().is_some());
let cloned = s.clone_set();
assert!(cloned.chain_parent().is_none());
assert!(cloned.get(uni('a')).is_some());
}
#[test]
fn set_clone_with_leaf_chained() {
let mut s = Set::new();
s.parse_and_put("a=new_window").unwrap();
s.parse_and_put("chain=new_tab").unwrap();
let entry = s.get(uni('a')).unwrap();
assert_eq!(entry.as_leaf_chained().unwrap().actions.len(), 2);
let cloned = s.clone_set();
let ce = cloned.get(uni('a')).unwrap().as_leaf_chained().unwrap();
assert_eq!(ce.actions.len(), 2);
assert_eq!(ce.actions[0], Action::NewWindow);
assert_eq!(ce.actions[1], Action::NewTab);
}
#[test]
fn set_clone_with_leaf_chained_containing_allocated_data() {
let mut s = Set::new();
s.parse_and_put("a=text:hello").unwrap();
s.parse_and_put("chain=text:world").unwrap();
assert!(s.get(uni('a')).unwrap().as_leaf_chained().is_some());
let cloned = s.clone_set();
let ce = cloned.get(uni('a')).unwrap().as_leaf_chained().unwrap();
assert_eq!(ce.actions[0], Action::Text("hello".into()));
assert_eq!(ce.actions[1], Action::Text("world".into()));
// Truly cloned, not shared.
let (Action::Text(orig), Action::Text(copy)) = (
&s.get(uni('a')).unwrap().as_leaf_chained().unwrap().actions[0],
&ce.actions[0],
) else {
panic!("expected text");
};
assert_ne!(orig.as_ptr(), copy.as_ptr());
}
#[test]
fn set_parse_and_put_sequence() {
let mut s = Set::new();
s.parse_and_put("a>b=new_window").unwrap();
let inner = s.get(uni('a')).unwrap().as_leader().unwrap();
let l = leaf(inner, uni('b'));
assert_eq!(l.action, Action::NewWindow);
assert_eq!(l.flags, Flags::default());
}
#[test]
fn set_parse_and_put_sequence_with_two_actions() {
let mut s = Set::new();
s.parse_and_put("a>b=new_window").unwrap();
s.parse_and_put("a>c=new_tab").unwrap();
let inner = s.get(uni('a')).unwrap().as_leader().unwrap();
let b = leaf(inner, uni('b'));
assert_eq!(b.action, Action::NewWindow);
assert_eq!(b.flags, Flags::default());
let c = leaf(inner, uni('c'));
assert_eq!(c.action, Action::NewTab);
assert_eq!(c.flags, Flags::default());
}
#[test]
fn set_parse_and_put_overwrite_sequence() {
let mut s = Set::new();
s.parse_and_put("a>b=new_tab").unwrap();
s.parse_and_put("a>b=new_window").unwrap();
let inner = s.get(uni('a')).unwrap().as_leader().unwrap();
let b = leaf(inner, uni('b'));
assert_eq!(b.action, Action::NewWindow);
assert_eq!(b.flags, Flags::default());
}
#[test]
fn set_parse_and_put_overwrite_leader() {
let mut s = Set::new();
s.parse_and_put("a=new_tab").unwrap();
s.parse_and_put("a>b=new_window").unwrap();
let inner = s.get(uni('a')).unwrap().as_leader().unwrap();
let b = leaf(inner, uni('b'));
assert_eq!(b.action, Action::NewWindow);
assert_eq!(b.flags, Flags::default());
}
#[test]
fn set_parse_and_put_unbind_sequence_unbinds_leader() {
let mut s = Set::new();
s.parse_and_put("a>b=new_window").unwrap();
s.parse_and_put("a>b=unbind").unwrap();
assert!(s.get(uni('a')).is_none());
}
#[test]
fn set_parse_and_put_unbind_sequence_unbinds_leader_if_not_set() {
let mut s = Set::new();
s.parse_and_put("a>b=unbind").unwrap();
assert!(s.get(uni('a')).is_none());
}
#[test]
fn set_parse_and_put_unbind_sequence_restores_previous_leaf() {
// Extra: a failed leader insert must restore the old leaf binding.
let mut s = Set::new();
s.parse_and_put("a=new_tab").unwrap();
s.parse_and_put("a>b=unbind").unwrap();
assert_eq!(leaf(&s, uni('a')).action, Action::NewTab);
assert!(s.chain_parent().is_none());
}
#[test]
fn set_parse_and_put_sequence_preserves_reverse_mapping() {
let mut s = Set::new();
s.parse_and_put("a=new_window").unwrap();
s.parse_and_put("ctrl+a>b=new_window").unwrap();
let trigger = s.get_trigger(&Action::NewWindow).unwrap();
assert_eq!(trigger.key, TriggerKey::Unicode('a' as u32));
}
#[test]
fn set_put_overwrites_sequence() {
let mut s = Set::new();
s.parse_and_put("ctrl+a>b=new_window").unwrap();
s.put(
trig(ctrl(), TriggerKey::Unicode('a' as u32)),
Action::NewWindow,
);
let trigger = s.get_trigger(&Action::NewWindow).unwrap();
assert_eq!(trigger.key, TriggerKey::Unicode('a' as u32));
}
#[test]
fn set_maintains_reverse_mapping() {
let mut s = Set::new();
s.put(uni('a'), Action::NewWindow);
assert_eq!(
s.get_trigger(&Action::NewWindow).unwrap().key,
TriggerKey::Unicode('a' as u32)
);
// should be most recent
s.put(uni('b'), Action::NewWindow);
assert_eq!(
s.get_trigger(&Action::NewWindow).unwrap().key,
TriggerKey::Unicode('b' as u32)
);
// removal should replace
s.remove(uni('b'));
assert_eq!(
s.get_trigger(&Action::NewWindow).unwrap().key,
TriggerKey::Unicode('a' as u32)
);
}
#[test]
fn set_performable_is_not_part_of_reverse_mappings() {
let mut s = Set::new();
s.put(uni('a'), Action::NewWindow);
assert_eq!(
s.get_trigger(&Action::NewWindow).unwrap().key,
TriggerKey::Unicode('a' as u32)
);
// trigger should be non-performable
s.put_flags(
uni('b'),
Action::NewWindow,
Flags {
performable: true,
..Flags::default()
},
);
assert_eq!(
s.get_trigger(&Action::NewWindow).unwrap().key,
TriggerKey::Unicode('a' as u32)
);
// removal of performable should do nothing
s.remove(uni('b'));
assert_eq!(
s.get_trigger(&Action::NewWindow).unwrap().key,
TriggerKey::Unicode('a' as u32)
);
}
#[test]
fn set_overriding_a_mapping_updates_reverse() {
let mut s = Set::new();
s.put(uni('a'), Action::NewWindow);
assert_eq!(
s.get_trigger(&Action::NewWindow).unwrap().key,
TriggerKey::Unicode('a' as u32)
);
// should be most recent
s.put(uni('a'), Action::NewTab);
assert!(s.get_trigger(&Action::NewWindow).is_none());
}
#[test]
fn set_consumed_state() {
let mut s = Set::new();
s.put(uni('a'), Action::NewWindow);
assert!(leaf(&s, uni('a')).flags.consumed);
s.put_flags(
uni('a'),
Action::NewWindow,
Flags {
consumed: false,
..Flags::default()
},
);
assert!(!leaf(&s, uni('a')).flags.consumed);
s.put(uni('a'), Action::NewWindow);
assert!(leaf(&s, uni('a')).flags.consumed);
}
#[test]
fn set_parse_and_put_chain() {
let mut s = Set::new();
s.parse_and_put("a=new_window").unwrap();
s.parse_and_put("chain=new_tab").unwrap();
// Creates forward mapping as leaf_chained
let chained = s.get(uni('a')).unwrap().as_leaf_chained().unwrap();
assert_eq!(chained.actions.len(), 2);
assert_eq!(chained.actions[0], Action::NewWindow);
assert_eq!(chained.actions[1], Action::NewTab);
// Does not create reverse mapping, because reverse mappings are only for
// non-chained actions.
assert!(s.get_trigger(&Action::NewWindow).is_none());
}
#[test]
fn set_parse_and_put_chain_without_parent_is_error() {
let mut s = Set::new();
assert_eq!(s.parse_and_put("chain=new_tab"), Err(Error::InvalidFormat));
}
#[test]
fn set_parse_and_put_chain_multiple_times() {
let mut s = Set::new();
s.parse_and_put("a=new_window").unwrap();
s.parse_and_put("chain=new_tab").unwrap();
s.parse_and_put("chain=close_surface").unwrap();
let chained = s.get(uni('a')).unwrap().as_leaf_chained().unwrap();
assert_eq!(chained.actions.len(), 3);
assert_eq!(chained.actions[0], Action::NewWindow);
assert_eq!(chained.actions[1], Action::NewTab);
assert_eq!(chained.actions[2], Action::CloseSurface);
}
#[test]
fn set_parse_and_put_chain_preserves_flags() {
let mut s = Set::new();
s.parse_and_put("unconsumed:a=new_window").unwrap();
s.parse_and_put("chain=new_tab").unwrap();
let chained = s.get(uni('a')).unwrap().as_leaf_chained().unwrap();
assert!(!chained.flags.consumed);
assert_eq!(chained.actions.len(), 2);
}
#[test]
fn set_parse_and_put_chain_after_unbind_is_error() {
let mut s = Set::new();
s.parse_and_put("a=new_window").unwrap();
s.parse_and_put("a=unbind").unwrap();
// Chain after unbind should fail because chain_parent is cleared
assert_eq!(s.parse_and_put("chain=new_tab"), Err(Error::InvalidFormat));
}
#[test]
fn set_parse_and_put_chain_on_sequence() {
let mut s = Set::new();
s.parse_and_put("a>b=new_window").unwrap();
s.parse_and_put("chain=new_tab").unwrap();
let inner = s.get(uni('a')).unwrap().as_leader().unwrap();
let chained = inner.get(uni('b')).unwrap().as_leaf_chained().unwrap();
assert_eq!(chained.actions.len(), 2);
assert_eq!(chained.actions[0], Action::NewWindow);
assert_eq!(chained.actions[1], Action::NewTab);
}
#[test]
fn set_parse_and_put_chain_with_unbind_is_error() {
let mut s = Set::new();
s.parse_and_put("a=new_window").unwrap();
// chain=unbind is not valid
assert_eq!(s.parse_and_put("chain=unbind"), Err(Error::InvalidFormat));
// Original binding should still exist
assert_eq!(leaf(&s, uni('a')).action, Action::NewWindow);
}
fn ev(key: Key, mods: Mods, utf8: &str, unshifted: u32) -> KeyEvent<'_> {
KeyEvent {
key,
mods,
utf8,
unshifted_codepoint: unshifted,
..KeyEvent::default()
}
}
fn ev_action(s: &Set, e: &KeyEvent<'_>) -> Option<Action> {
s.get_event(e)
.map(|(_, v)| v.as_leaf().unwrap().action.clone())
}
#[test]
fn set_get_event_physical() {
let mut s = Set::new();
s.parse_and_put("ctrl+quote=new_window").unwrap();
// Physical matches on physical
assert_eq!(
ev_action(&s, &ev(Key::Quote, ctrl(), "", 0)),
Some(Action::NewWindow)
);
// Physical does not match on UTF8/codepoint
assert_eq!(
ev_action(&s, &ev(Key::KeyA, ctrl(), "'", '\'' as u32)),
None
);
}
#[test]
fn set_get_event_codepoint() {
let mut s = Set::new();
s.parse_and_put("ctrl+'=new_window").unwrap();
// Matches on codepoint
assert_eq!(
ev_action(&s, &ev(Key::KeyA, ctrl(), "", '\'' as u32)),
Some(Action::NewWindow)
);
// Matches on UTF-8
assert_eq!(
ev_action(&s, &ev(Key::KeyA, ctrl(), "'", 0)),
Some(Action::NewWindow)
);
// Doesn't match on physical
assert_eq!(ev_action(&s, &ev(Key::KeyA, ctrl(), "", 0)), None);
}
#[test]
fn set_get_event_codepoint_case_folding() {
let mut s = Set::new();
s.parse_and_put("ctrl+A=new_window").unwrap();
// Lowercase codepoint
assert_eq!(
ev_action(&s, &ev(Key::KeyJ, ctrl(), "", 'a' as u32)),
Some(Action::NewWindow)
);
// Uppercase codepoint
assert_eq!(
ev_action(&s, &ev(Key::KeyA, ctrl(), "", 'A' as u32)),
Some(Action::NewWindow)
);
// Negative case for sanity
assert_eq!(ev_action(&s, &ev(Key::KeyJ, ctrl(), "", 0)), None);
}
#[test]
fn set_get_event_catch_all_fallback() {
let mut s = Set::new();
s.parse_and_put("catch_all=ignore").unwrap();
// Matches unbound key without modifiers
assert_eq!(
ev_action(&s, &ev(Key::KeyA, Mods::default(), "", 0)),
Some(Action::Ignore)
);
// Matches unbound key with modifiers (falls back to catch_all without mods)
assert_eq!(
ev_action(&s, &ev(Key::KeyA, ctrl(), "", 0)),
Some(Action::Ignore)
);
// Specific binding takes precedence over catch_all
s.parse_and_put("ctrl+b=new_window").unwrap();
assert_eq!(
ev_action(&s, &ev(Key::KeyB, ctrl(), "", 'b' as u32)),
Some(Action::NewWindow)
);
}
#[test]
fn set_get_event_catch_all_with_modifiers() {
let mut s = Set::new();
s.parse_and_put("ctrl+catch_all=close_surface").unwrap();
s.parse_and_put("catch_all=ignore").unwrap();
// Key with ctrl matches catch_all with ctrl modifier
assert_eq!(
ev_action(&s, &ev(Key::KeyA, ctrl(), "", 0)),
Some(Action::CloseSurface)
);
// Key without mods matches catch_all without mods
assert_eq!(
ev_action(&s, &ev(Key::KeyA, Mods::default(), "", 0)),
Some(Action::Ignore)
);
// Key with different mods falls back to catch_all without mods
let alt = Mods {
alt: true,
..Mods::default()
};
assert_eq!(
ev_action(&s, &ev(Key::KeyA, alt, "", 0)),
Some(Action::Ignore)
);
}
#[test]
fn action_clone() {
let a = Action::Ignore;
assert_eq!(a.clone(), Action::Ignore);
let a = Action::Text("foo".into());
assert_eq!(a.clone(), Action::Text("foo".into()));
}
#[test]
fn parse_increase_font_size() {
assert_eq!(
ps("a=increase_font_size:1.5").action,
Action::IncreaseFontSize(1.5)
);
}
#[test]
fn parse_decrease_font_size() {
assert_eq!(
ps("a=decrease_font_size:2.5").action,
Action::DecreaseFontSize(2.5)
);
}
#[test]
fn parse_reset_font_size() {
assert_eq!(ps("a=reset_font_size").action, Action::ResetFontSize);
}
#[test]
fn parse_set_font_size() {
assert_eq!(ps("a=set_font_size:13.5").action, Action::SetFontSize(13.5));
}
#[test]
fn parse_copy_to_clipboard_default() {
assert_eq!(
ps("a=copy_to_clipboard").action,
Action::CopyToClipboard(CopyToClipboard::Mixed)
);
}
#[test]
fn parse_copy_to_clipboard_explicit() {
assert_eq!(
ps("a=copy_to_clipboard:html").action,
Action::CopyToClipboard(CopyToClipboard::Html)
);
}
#[test]
fn parse_write_screen_file_no_format() {
assert_eq!(
ps("a=write_screen_file:copy").action,
Action::WriteScreenFile(WriteScreen::COPY)
);
}
#[test]
fn parse_write_screen_file_format() {
assert_eq!(
ps("a=write_screen_file:copy,html").action,
Action::WriteScreenFile(WriteScreen {
action: WriteScreenAction::Copy,
emit: WriteScreenFormat::Html,
})
);
}
#[test]
fn parse_write_screen_file_format_as_string() {
let b = ps("a=write_screen_file:copy,html");
assert_eq!(b.action.to_string(), "write_screen_file:copy,html");
}
#[test]
fn parse_write_screen_file_invalid() {
assert_eq!(ps_err("a=write_screen_file:"), INVALID);
assert_eq!(ps_err("a=write_screen_file:,"), INVALID);
assert_eq!(ps_err("a=write_screen_file:copy,"), INVALID);
assert_eq!(ps_err("a=write_screen_file:copy,html,extra"), INVALID);
}
#[test]
fn action_format() {
let a = Action::Text("👻".into());
assert_eq!(a.to_string(), "text:\\xf0\\x9f\\x91\\xbb");
}
#[test]
fn action_format_set_title() {
let a = Action::SetTabTitle("foo bar".into());
assert_eq!(a.to_string(), "set_tab_title:foo bar");
}
#[test]
fn set_append_chain_with_no_parent_returns_error() {
let mut s = Set::new();
assert_eq!(
s.append_chain(Action::NewTab),
Err(AppendChainError::NoChainParent)
);
}
#[test]
fn set_append_chain_after_put_converts_to_leaf_chained() {
let mut s = Set::new();
s.put(uni('a'), Action::NewWindow);
s.append_chain(Action::NewTab).unwrap();
let chained = s.get(uni('a')).unwrap().as_leaf_chained().unwrap();
assert_eq!(chained.actions.len(), 2);
assert_eq!(chained.actions[0], Action::NewWindow);
assert_eq!(chained.actions[1], Action::NewTab);
}
#[test]
fn set_append_chain_after_put_flags_preserves_flags() {
let mut s = Set::new();
s.put_flags(
uni('a'),
Action::NewWindow,
Flags {
consumed: false,
..Flags::default()
},
);
s.append_chain(Action::NewTab).unwrap();
let chained = s.get(uni('a')).unwrap().as_leaf_chained().unwrap();
assert!(!chained.flags.consumed);
assert_eq!(chained.actions.len(), 2);
assert_eq!(chained.actions[0], Action::NewWindow);
assert_eq!(chained.actions[1], Action::NewTab);
}
#[test]
fn set_append_chain_multiple_times() {
let mut s = Set::new();
s.put(uni('a'), Action::NewWindow);
s.append_chain(Action::NewTab).unwrap();
s.append_chain(Action::CloseSurface).unwrap();
let chained = s.get(uni('a')).unwrap().as_leaf_chained().unwrap();
assert_eq!(chained.actions.len(), 3);
assert_eq!(chained.actions[0], Action::NewWindow);
assert_eq!(chained.actions[1], Action::NewTab);
assert_eq!(chained.actions[2], Action::CloseSurface);
}
#[test]
fn set_append_chain_removes_reverse_mapping() {
let mut s = Set::new();
s.put(uni('a'), Action::NewWindow);
assert!(s.get_trigger(&Action::NewWindow).is_some());
// Chaining should remove the reverse mapping
s.append_chain(Action::NewTab).unwrap();
assert!(s.get_trigger(&Action::NewWindow).is_none());
}
#[test]
fn set_append_chain_with_performable_does_not_affect_reverse_mapping() {
let mut s = Set::new();
s.put(uni('b'), Action::NewWindow);
assert!(s.get_trigger(&Action::NewWindow).is_some());
// Add a performable binding (not in reverse map) and chain it
s.put_flags(
uni('a'),
Action::CloseSurface,
Flags {
performable: true,
..Flags::default()
},
);
assert!(s.get_trigger(&Action::CloseSurface).is_none());
// Chaining the performable binding should not crash or affect anything
s.append_chain(Action::NewTab).unwrap();
// The non-performable new_window binding should still be in reverse map
assert!(s.get_trigger(&Action::NewWindow).is_some());
}
#[test]
fn set_append_chain_restores_next_valid_reverse_mapping() {
let mut s = Set::new();
s.put(uni('a'), Action::NewWindow);
s.put(uni('b'), Action::NewWindow);
// Reverse mapping should point to 'b' (most recent)
assert_eq!(
s.get_trigger(&Action::NewWindow).unwrap().key,
TriggerKey::Unicode('b' as u32)
);
// Chain an action to 'b', which should restore 'a' in the reverse map
s.append_chain(Action::NewTab).unwrap();
assert_eq!(
s.get_trigger(&Action::NewWindow).unwrap().key,
TriggerKey::Unicode('a' as u32)
);
}
/// Stand-in for `formatterpkg.entryFormatter("keybind", ...)`.
struct KeybindFormatter(String);
impl EntryFormatter for KeybindFormatter {
fn format_entry(&mut self, value: &str) {
self.0.push_str("keybind = ");
self.0.push_str(value);
self.0.push('\n');
}
}
fn format_set_entry(s: &Set, t: Trigger) -> String {
let (k, v) = s.get_entry(t).unwrap();
let mut buf = k.to_string();
let mut f = KeybindFormatter(String::new());
v.format_entries(&mut buf, &mut f);
f.0
}
#[test]
fn set_format_entries_leaf_chained() {
let mut s = Set::new();
s.parse_and_put("a=new_window").unwrap();
s.parse_and_put("chain=new_tab").unwrap();
assert!(s.get(uni('a')).unwrap().as_leaf_chained().is_some());
assert_eq!(
format_set_entry(&s, uni('a')),
"keybind = a=new_window\nkeybind = chain=new_tab\n"
);
}
#[test]
fn set_format_entries_leaf_chained_multiple_chains() {
let mut s = Set::new();
s.parse_and_put("ctrl+a=new_window").unwrap();
s.parse_and_put("chain=new_tab").unwrap();
s.parse_and_put("chain=close_surface").unwrap();
let t = trig(ctrl(), TriggerKey::Unicode('a' as u32));
assert_eq!(
s.get(t).unwrap().as_leaf_chained().unwrap().actions.len(),
3
);
assert_eq!(
format_set_entry(&s, t),
"keybind = ctrl+a=new_window\nkeybind = chain=new_tab\nkeybind = chain=close_surface\n"
);
}
#[test]
fn set_format_entries_leaf_chained_with_text_action() {
let mut s = Set::new();
s.parse_and_put("a=text:hello").unwrap();
s.parse_and_put("chain=text:world").unwrap();
assert_eq!(
format_set_entry(&s, uni('a')),
"keybind = a=text:hello\nkeybind = chain=text:world\n"
);
}
#[test]
fn set_format_entries_sequence_shares_prefix() {
// Extra: sequences share the prefix and siblings are not corrupted by
// chained leaves.
let mut s = Set::new();
s.parse_and_put("a>b=new_window").unwrap();
s.parse_and_put("chain=new_tab").unwrap();
s.parse_and_put("a>c=close_surface").unwrap();
assert_eq!(
format_set_entry(&s, uni('a')),
"keybind = a>b=new_window\nkeybind = chain=new_tab\nkeybind = a>c=close_surface\n"
);
}
#[test]
fn trigger_hash_folds_case() {
assert_eq!(uni('a').hash(), uni('A').hash());
assert!(uni('a').folded_equal(&uni('A')));
assert!(!uni('a').equal(&uni('A')));
}
#[test]
fn trigger_display_roundtrip() {
for s in ["super+ctrl+alt+shift+a", "catch_all", "ctrl+arrow_up", "+"] {
let t = Trigger::parse(s).unwrap();
assert_eq!(t.to_string(), s);
}
}
#[test]
fn action_docs_and_scope() {
assert!(
Action::Ignore
.docs()
.starts_with("Ignore this key combination.")
);
assert_eq!(Action::NewWindow.scope(), Scope::App);
assert_eq!(Action::NewTab.scope(), Scope::Surface);
assert!(Action::NewTab.scoped(Scope::App).is_none());
assert!(Action::NewWindow.scoped(Scope::App).is_some());
}
#[test]
fn action_roundtrip_all_default_forms() {
for s in [
"csi:0m",
"text:hello world",
"increase_font_size:1.5",
"scroll_to_row:5",
"goto_tab:3",
"move_tab:-1",
"adjust_selection:left",
"resize_split:up,10",
"close_tab:other",
"inspector:toggle",
"crash:main",
"activate_key_table:foo",
] {
assert_eq!(Action::parse(s).unwrap().to_string(), s);
}
}
#[test]
fn less_than_orders_by_mod_count() {
let a = bind(
trig(ctrl(), TriggerKey::Unicode('a' as u32)),
Action::Ignore,
);
let b = bind(uni('a'), Action::Ignore);
assert!(less_than(&a, &b));
assert!(!less_than(&b, &a));
}
+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}");
}
}
}
+13
View File
@@ -0,0 +1,13 @@
//! Input-related config enums (ZIG/src/input/config.zig).
/// Determines the macOS option key behavior. See the config
/// `macos-option-as-alt` for a lot more details.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[repr(i32)]
pub enum OptionAsAlt {
#[default]
False,
True,
Left,
Right,
}
+616
View File
@@ -0,0 +1,616 @@
//! This is the list of "PC style function keys" that xterm supports for
//! the legacy keyboard protocols. These always take priority since at the
//! time of writing this, even the most modern keyboard protocols still
//! are backwards compatible with regards to these sequences.
//!
//! This is based on a variety of sources cross-referenced but mostly
//! based on foot's keymap.h: https://codeberg.org/dnkl/foot/src/branch/master/keymap.h
//! (ZIG/src/input/function_keys.zig)
use std::sync::LazyLock;
use crate::key::{Key, Mods};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CursorMode {
Any,
Normal,
Application,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KeypadMode {
Any,
Normal,
Application,
}
/// A bit confusing so I'll document this one: this is the "modify other keys"
/// setting. We only change behavior for "set_other" which is ESC [ > 4; 2 m.
/// So this can be "any" which means we don't care what's going on. Or it
/// can be "set" which means modify keys must be set EXCEPT FOR "other keys"
/// mode, and "set_other" which means modify keys must be set to "other keys"
/// mode.
///
/// See: https://invisible-island.net/xterm/modified-keys.html
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ModifyKeys {
Any,
Set,
SetOther,
}
/// A single entry in the table of keys.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Entry {
/// The exact set of modifiers that must be active for this entry to match.
/// If mods_empty_is_any is true then empty mods means any set of mods can
/// match. Otherwise, empty mods means no mods must be active.
pub mods: Mods,
pub mods_empty_is_any: bool,
/// The state required for cursor/keypad mode.
pub cursor: CursorMode,
pub keypad: KeypadMode,
/// Whether or not this entry should be used
pub modify_other_keys: ModifyKeys,
/// The sequence to send to the pty if this entry matches.
pub sequence: String,
}
impl Entry {
fn new(sequence: impl Into<String>) -> Entry {
Entry {
mods: Mods::default(),
mods_empty_is_any: true,
cursor: CursorMode::Any,
keypad: KeypadMode::Any,
modify_other_keys: ModifyKeys::Any,
sequence: sequence.into(),
}
}
}
const fn m(shift: bool, ctrl: bool, alt: bool, super_: bool) -> Mods {
Mods {
shift,
ctrl,
alt,
super_,
caps_lock: false,
num_lock: false,
sides: crate::key_mods::Side {
shift: crate::key_mods::ModSide::Left,
ctrl: crate::key_mods::ModSide::Left,
alt: crate::key_mods::ModSide::Left,
super_: crate::key_mods::ModSide::Left,
},
}
}
/// The list of modifier combinations for modify other key sequences.
/// The mode value is index + 2.
pub const MODIFIERS: &[Mods] = &[
m(true, false, false, false),
m(false, false, true, false),
m(true, false, true, false),
m(false, true, false, false),
m(true, true, false, false),
m(false, true, true, false),
m(true, true, true, false),
m(false, false, false, true),
m(true, false, false, true),
m(false, false, true, true),
m(true, false, true, true),
m(false, true, false, true),
m(true, true, false, true),
m(false, true, true, true),
m(true, true, true, true),
];
fn ent(mods: Mods, modify_other_keys: ModifyKeys, sequence: &str) -> Entry {
Entry {
mods,
modify_other_keys,
..Entry::new(sequence)
}
}
/// Returns the entries for a key. Not every key has any entries. It is
/// expected that you'll just use a for loop through the entry values, there
/// are never more than a couple dozen or so.
pub fn keys(k: Key) -> &'static [Entry] {
&KEYS[k as usize]
}
/// Iterate over the whole table (all keys, in `Key` order).
pub fn all_keys() -> impl Iterator<Item = &'static [Entry]> {
KEYS.iter().map(Vec::as_slice)
}
static KEYS: LazyLock<Vec<Vec<Entry>>> = LazyLock::new(build_keys);
fn cat(mut a: Vec<Entry>, b: Vec<Entry>) -> Vec<Entry> {
a.extend(b);
a
}
fn one(seq: &str) -> Vec<Entry> {
vec![Entry::new(seq)]
}
fn build_keys() -> Vec<Vec<Entry>> {
let mut result: Vec<Vec<Entry>> = vec![Vec::new(); Key::ALL.len()];
let mut set = |k: Key, v: Vec<Entry>| result[k as usize] = v;
set(
Key::ArrowUp,
cat(pc_style("\x1b[1;{}A"), cursor_key("\x1b[A", "\x1bOA")),
);
set(
Key::ArrowDown,
cat(pc_style("\x1b[1;{}B"), cursor_key("\x1b[B", "\x1bOB")),
);
set(
Key::ArrowRight,
cat(pc_style("\x1b[1;{}C"), cursor_key("\x1b[C", "\x1bOC")),
);
set(
Key::ArrowLeft,
cat(pc_style("\x1b[1;{}D"), cursor_key("\x1b[D", "\x1bOD")),
);
set(
Key::Home,
cat(pc_style("\x1b[1;{}H"), cursor_key("\x1b[H", "\x1bOH")),
);
set(
Key::End,
cat(pc_style("\x1b[1;{}F"), cursor_key("\x1b[F", "\x1bOF")),
);
set(Key::Insert, cat(pc_style("\x1b[2;{}~"), one("\x1B[2~")));
set(Key::Delete, cat(pc_style("\x1b[3;{}~"), one("\x1B[3~")));
set(Key::PageUp, cat(pc_style("\x1b[5;{}~"), one("\x1B[5~")));
set(Key::PageDown, cat(pc_style("\x1b[6;{}~"), one("\x1B[6~")));
// Function Keys. todo: f13-f35 but we need to add to input.Key
set(Key::F1, cat(pc_style("\x1b[1;{}P"), one("\x1BOP")));
set(Key::F2, cat(pc_style("\x1b[1;{}Q"), one("\x1BOQ")));
set(Key::F3, cat(pc_style("\x1b[13;{}~"), one("\x1BOR")));
set(Key::F4, cat(pc_style("\x1b[1;{}S"), one("\x1BOS")));
set(Key::F5, cat(pc_style("\x1b[15;{}~"), one("\x1B[15~")));
set(Key::F6, cat(pc_style("\x1b[17;{}~"), one("\x1B[17~")));
set(Key::F7, cat(pc_style("\x1b[18;{}~"), one("\x1B[18~")));
set(Key::F8, cat(pc_style("\x1b[19;{}~"), one("\x1B[19~")));
set(Key::F9, cat(pc_style("\x1b[20;{}~"), one("\x1B[20~")));
set(Key::F10, cat(pc_style("\x1b[21;{}~"), one("\x1B[21~")));
set(Key::F11, cat(pc_style("\x1b[23;{}~"), one("\x1B[23~")));
set(Key::F12, cat(pc_style("\x1b[24;{}~"), one("\x1B[24~")));
// Keypad keys
set(Key::Numpad0, kp_keys("p"));
set(Key::Numpad1, kp_keys("q"));
set(Key::Numpad2, kp_keys("r"));
set(Key::Numpad3, kp_keys("s"));
set(Key::Numpad4, kp_keys("t"));
set(Key::Numpad5, kp_keys("u"));
set(Key::Numpad6, kp_keys("v"));
set(Key::Numpad7, kp_keys("w"));
set(Key::Numpad8, kp_keys("x"));
set(Key::Numpad9, kp_keys("y"));
set(Key::NumpadDecimal, kp_keys("n"));
set(Key::NumpadDivide, kp_keys("o"));
set(Key::NumpadMultiply, kp_keys("j"));
set(Key::NumpadSubtract, kp_keys("m"));
set(Key::NumpadAdd, kp_keys("k"));
set(Key::NumpadEnter, cat(kp_keys("M"), one("\r")));
set(
Key::NumpadUp,
cat(pc_style("\x1b[1;{}A"), cursor_key("\x1b[A", "\x1bOA")),
);
set(
Key::NumpadDown,
cat(pc_style("\x1b[1;{}B"), cursor_key("\x1b[B", "\x1bOB")),
);
set(
Key::NumpadRight,
cat(pc_style("\x1b[1;{}C"), cursor_key("\x1b[C", "\x1bOC")),
);
set(
Key::NumpadLeft,
cat(pc_style("\x1b[1;{}D"), cursor_key("\x1b[D", "\x1bOD")),
);
set(
Key::NumpadBegin,
cat(pc_style("\x1b[1;{}E"), cursor_key("\x1b[E", "\x1bOE")),
);
set(
Key::NumpadHome,
cat(pc_style("\x1b[1;{}H"), cursor_key("\x1b[H", "\x1bOH")),
);
set(
Key::NumpadEnd,
cat(pc_style("\x1b[1;{}F"), cursor_key("\x1b[F", "\x1bOF")),
);
set(
Key::NumpadInsert,
cat(pc_style("\x1b[2;{}~"), one("\x1B[2~")),
);
set(
Key::NumpadDelete,
cat(pc_style("\x1b[3;{}~"), one("\x1B[3~")),
);
set(
Key::NumpadPageUp,
cat(pc_style("\x1b[5;{}~"), one("\x1B[5~")),
);
set(
Key::NumpadPageDown,
cat(pc_style("\x1b[6;{}~"), one("\x1B[6~")),
);
set(
Key::Backspace,
vec![
// Modify Keys Normal / Other
ent(m(true, false, false, false), ModifyKeys::Set, "\x7f"),
ent(m(false, false, true, false), ModifyKeys::Set, "\x1b\x7f"),
ent(m(true, false, true, false), ModifyKeys::Set, "\x1b\x7f"),
ent(m(true, true, false, false), ModifyKeys::Set, "\x08"),
ent(m(false, true, true, false), ModifyKeys::Set, "\x1b\x08"),
ent(m(false, false, false, true), ModifyKeys::Set, "\x7f"),
ent(m(true, false, false, true), ModifyKeys::Set, "\x7f"),
ent(m(false, false, true, true), ModifyKeys::Set, "\x1b\x7f"),
ent(m(true, false, true, true), ModifyKeys::Set, "\x1b\x7f"),
ent(m(false, true, false, true), ModifyKeys::Set, "\x08"),
ent(m(true, true, false, true), ModifyKeys::Set, "\x08"),
ent(m(false, true, true, true), ModifyKeys::Set, "\x1b\x08"),
ent(m(true, true, true, true), ModifyKeys::Set, "\x1b\x08"),
ent(
m(true, false, false, false),
ModifyKeys::SetOther,
"\x1b[27;2;127~",
),
ent(
m(false, false, true, false),
ModifyKeys::SetOther,
"\x1b[27;3;127~",
),
ent(
m(true, false, true, false),
ModifyKeys::SetOther,
"\x1b[27;4;127~",
),
ent(
m(true, true, false, false),
ModifyKeys::SetOther,
"\x1b[27;6;127~",
),
ent(
m(false, true, true, false),
ModifyKeys::SetOther,
"\x1b[27;7;127~",
),
ent(
m(true, true, true, false),
ModifyKeys::SetOther,
"\x1b[27;8;127~",
),
ent(
m(false, false, false, true),
ModifyKeys::SetOther,
"\x1b[27;9;127~",
),
ent(
m(true, false, false, true),
ModifyKeys::SetOther,
"\x1b[27;10;127~",
),
ent(
m(false, false, true, true),
ModifyKeys::SetOther,
"\x1b[27;11;127~",
),
ent(
m(true, false, true, true),
ModifyKeys::SetOther,
"\x1b[27;12;127~",
),
ent(
m(false, true, false, true),
ModifyKeys::SetOther,
"\x1b[27;13;127~",
),
ent(
m(true, true, false, true),
ModifyKeys::SetOther,
"\x1b[27;14;127~",
),
ent(
m(false, true, true, true),
ModifyKeys::SetOther,
"\x1b[27;15;127~",
),
ent(
m(true, true, true, true),
ModifyKeys::SetOther,
"\x1b[27;16;127~",
),
ent(m(false, true, false, false), ModifyKeys::Any, "\x08"),
ent(m(false, false, false, false), ModifyKeys::Any, "\x7f"),
ent(m(false, true, false, false), ModifyKeys::Any, "\x08"),
ent(m(false, false, false, false), ModifyKeys::Any, "\x7f"),
],
);
set(
Key::Tab,
vec![
ent(m(true, false, false, false), ModifyKeys::Set, "\x1b[Z"),
ent(m(false, false, true, false), ModifyKeys::Set, "\x1b\t"),
ent(
m(true, false, false, false),
ModifyKeys::SetOther,
"\x1b[27;2;9~",
),
ent(
m(false, false, true, false),
ModifyKeys::SetOther,
"\x1b[27;3;9~",
),
ent(m(true, false, true, false), ModifyKeys::Any, "\x1b[27;4;9~"),
ent(
m(false, true, false, false),
ModifyKeys::Any,
"\x1b[27;5;9~",
),
ent(m(true, true, false, false), ModifyKeys::Any, "\x1b[27;6;9~"),
ent(m(false, true, true, false), ModifyKeys::Any, "\x1b[27;7;9~"),
ent(m(true, true, true, false), ModifyKeys::Any, "\x1b[27;8;9~"),
ent(
m(false, false, false, true),
ModifyKeys::Any,
"\x1b[27;9;9~",
),
ent(
m(true, false, false, true),
ModifyKeys::Any,
"\x1b[27;10;9~",
),
ent(
m(false, false, true, true),
ModifyKeys::Any,
"\x1b[27;11;9~",
),
ent(m(true, false, true, true), ModifyKeys::Any, "\x1b[27;12;9~"),
ent(
m(false, true, false, true),
ModifyKeys::Any,
"\x1b[27;13;9~",
),
ent(m(true, true, false, true), ModifyKeys::Any, "\x1b[27;14;9~"),
ent(m(false, true, true, true), ModifyKeys::Any, "\x1b[27;15;9~"),
ent(m(true, true, true, true), ModifyKeys::Any, "\x1b[27;16;9~"),
ent(m(false, false, false, false), ModifyKeys::Any, "\t"),
ent(m(false, false, false, false), ModifyKeys::Any, "\t"),
],
);
set(
Key::Enter,
vec![
ent(
m(true, false, false, false),
ModifyKeys::Any,
"\x1b[27;2;13~",
),
ent(m(false, false, true, false), ModifyKeys::Set, "\x1b\r"),
ent(
m(false, false, true, false),
ModifyKeys::SetOther,
"\x1b[27;3;13~",
),
ent(
m(true, false, true, false),
ModifyKeys::Any,
"\x1b[27;4;13~",
),
ent(
m(false, true, false, false),
ModifyKeys::Any,
"\x1b[27;5;13~",
),
ent(
m(true, true, false, false),
ModifyKeys::Any,
"\x1b[27;6;13~",
),
ent(
m(false, true, true, false),
ModifyKeys::Any,
"\x1b[27;7;13~",
),
ent(m(true, true, true, false), ModifyKeys::Any, "\x1b[27;8;13~"),
ent(
m(false, false, false, true),
ModifyKeys::Any,
"\x1b[27;9;13~",
),
ent(
m(true, false, false, true),
ModifyKeys::Any,
"\x1b[27;10;13~",
),
ent(
m(false, false, true, true),
ModifyKeys::Any,
"\x1b[27;11;13~",
),
ent(
m(true, false, true, true),
ModifyKeys::Any,
"\x1b[27;12;13~",
),
ent(
m(false, true, false, true),
ModifyKeys::Any,
"\x1b[27;13;13~",
),
ent(
m(true, true, false, true),
ModifyKeys::Any,
"\x1b[27;14;13~",
),
ent(
m(false, true, true, true),
ModifyKeys::Any,
"\x1b[27;15;13~",
),
ent(m(true, true, true, true), ModifyKeys::Any, "\x1b[27;16;13~"),
ent(m(false, false, false, false), ModifyKeys::Any, "\r"),
ent(m(false, false, false, false), ModifyKeys::Any, "\r"),
],
);
set(
Key::Escape,
vec![
ent(
m(true, false, false, false),
ModifyKeys::Any,
"\x1b[27;2;27~",
),
ent(m(false, false, true, false), ModifyKeys::Any, "\x1b\x1b"),
ent(
m(true, false, true, false),
ModifyKeys::Any,
"\x1b[27;4;27~",
),
ent(
m(false, true, false, false),
ModifyKeys::Any,
"\x1b[27;5;27~",
),
ent(
m(true, true, false, false),
ModifyKeys::Any,
"\x1b[27;6;27~",
),
ent(
m(false, true, true, false),
ModifyKeys::Any,
"\x1b[27;7;27~",
),
ent(m(true, true, true, false), ModifyKeys::Any, "\x1b[27;8;27~"),
ent(
m(false, false, false, true),
ModifyKeys::Any,
"\x1b[27;9;27~",
),
ent(
m(true, false, false, true),
ModifyKeys::Any,
"\x1b[27;10;27~",
),
ent(
m(false, false, true, true),
ModifyKeys::Any,
"\x1b[27;11;27~",
),
ent(
m(true, false, true, true),
ModifyKeys::Any,
"\x1b[27;12;27~",
),
ent(
m(false, true, false, true),
ModifyKeys::Any,
"\x1b[27;13;27~",
),
ent(
m(true, true, false, true),
ModifyKeys::Any,
"\x1b[27;14;27~",
),
ent(
m(false, true, true, true),
ModifyKeys::Any,
"\x1b[27;15;27~",
),
ent(m(true, true, true, true), ModifyKeys::Any, "\x1b[27;16;27~"),
ent(m(false, false, false, false), ModifyKeys::Any, "\x1b"),
ent(m(false, false, false, false), ModifyKeys::Any, "\x1b"),
],
);
result
}
/// Returns the default keypad application mode entry.
fn kp_default(suffix: &str) -> Vec<Entry> {
vec![Entry {
mods_empty_is_any: false,
keypad: KeypadMode::Application,
..Entry::new(format!("\x1bO{suffix}"))
}]
}
/// Returns the entries for a keypad key. The suffix is the final character
/// of the sent sequence, such as "r" for kp_2.
fn kp_keys(suffix: &str) -> Vec<Entry> {
let mut pc = pc_style(&format!("\x1bO{{}}{suffix}"));
for entry in &mut pc {
entry.keypad = KeypadMode::Application;
}
cat(kp_default(suffix), pc)
}
/// Returns entries that are dependent on cursor key settings.
fn cursor_key(normal: &str, application: &str) -> Vec<Entry> {
vec![
Entry {
cursor: CursorMode::Normal,
..Entry::new(normal)
},
Entry {
cursor: CursorMode::Application,
..Entry::new(application)
},
]
}
/// Constructs a set of pcStyle function keys using the given format. The
/// format should have exactly one "hole" (`{}`) for the mods code.
/// Example: "\x1b[11;{}~" for F1.
fn pc_style(fmt: &str) -> Vec<Entry> {
MODIFIERS
.iter()
.zip(2..)
.map(|(mods, code)| Entry {
mods: *mods,
..Entry::new(fmt.replace("{}", &code.to_string()))
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn keys_table() {
// All key sequences must fit into a termio array.
// TODO(integration): termio's `Message.WriteReq.Small.Max` isn't
// available here; use its documented limit (38 bytes).
const MAX: usize = 38;
let mut n = 0;
for entries in all_keys() {
for entry in entries {
assert!(entry.sequence.len() <= MAX, "{:?}", entry.sequence);
n += 1;
}
}
assert!(n > 0);
}
}
+148
View File
@@ -0,0 +1,148 @@
//! This module is a help generator for keybind actions documentation.
//! It can generate documentation in different formats (plaintext for CLI,
//! markdown for website) while maintaining consistent content.
//! (ZIG/src/input/helpgen_actions.zig)
//!
//! The docs themselves live in [`ActionKey::docs`] (the Zig version reads
//! them from the build-time generated `help_strings`).
use crate::binding::ActionKey;
/// Format options for generating keybind actions documentation
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Format {
/// Plain text output with indentation
Plaintext,
/// Markdown formatted output
Markdown,
}
impl Format {
fn format_field_name(self, w: &mut String, field_name: &str) {
match self {
Format::Plaintext => {
w.push_str(field_name);
w.push_str(":\n");
}
Format::Markdown => {
w.push_str("## `");
w.push_str(field_name);
w.push_str("`\n");
}
}
}
fn format_doc_line(self, w: &mut String, line: &str) {
match self {
Format::Plaintext => {
w.push_str(" ");
w.push_str(line);
w.push('\n');
}
Format::Markdown => {
w.push_str(line);
w.push('\n');
}
}
}
fn header(self) -> Option<&'static str> {
match self {
Format::Plaintext => None,
Format::Markdown => Some(
"---\n\
title: Keybinding Action Reference\n\
description: Reference of all Ghostty keybinding actions.\n\
editOnGithubLink: https://github.com/ghostty-org/ghostty/edit/main/src/input/Binding.zig\n\
---\n\
\n\
This is a reference of all Ghostty keybinding actions.\n\
\n\
\n",
),
}
}
}
/// Generate keybind actions documentation with the specified format
pub fn generate(writer: &mut String, format: Format, show_docs: bool) {
if let Some(header) = format.header() {
writer.push_str(header);
}
let mut stream = String::new();
for &key in ActionKey::ALL {
let name = key.name();
if name.starts_with('_') {
continue;
}
// Write previously stored doc comment below all related actions
// (every action has a doc comment).
if show_docs {
writer.push_str(&stream);
writer.push('\n');
stream.clear();
}
if show_docs {
format.format_field_name(writer, name);
} else {
writer.push_str(name);
writer.push('\n');
}
if show_docs {
let mut iter = key.docs().split('\n').peekable();
while let Some(s) = iter.next() {
// If it is the last line and empty, then skip it.
if iter.peek().is_none() && s.is_empty() {
continue;
}
format.format_doc_line(&mut stream, s);
}
}
}
// Write any remaining buffered documentation
if !stream.is_empty() {
writer.push_str(&stream);
}
}
/// Convenience: generate into a fresh string.
pub fn generate_string(format: Format, show_docs: bool) -> String {
let mut s = String::new();
generate(&mut s, format, show_docs);
s
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn plaintext_with_docs_lists_all_actions() {
let s = generate_string(Format::Plaintext, true);
assert!(s.starts_with("\nignore:\n"));
assert!(s.contains("ignore:\n Ignore this key combination.\n"));
for &k in ActionKey::ALL {
assert!(s.contains(&format!("{}:\n", k.name())));
}
}
#[test]
fn names_only() {
let s = generate_string(Format::Plaintext, false);
assert_eq!(s.lines().count(), ActionKey::ALL.len());
assert_eq!(s.lines().next(), Some("ignore"));
}
#[test]
fn markdown_header_and_field() {
let s = generate_string(Format::Markdown, true);
assert!(s.starts_with("---\ntitle: Keybinding Action Reference\n"));
assert!(s.contains("## `unbind`\n"));
}
}
+611
View File
@@ -0,0 +1,611 @@
//! Key events and key codes (ZIG/src/input/key.zig).
//!
//! The Zig `imguiKey` helper (cimgui constants) is intentionally not ported;
//! the inspector is deferred (see notes).
pub use crate::key_mods::Mods;
/// A generic key input event. This is the information that is necessary
/// regardless of apprt in order to generate the proper terminal
/// control sequences for a given key press.
///
/// Some apprts may not be able to provide all of this information, such
/// as GLFW. In this case, the apprt should provide as much information
/// as it can and it should be expected that the terminal behavior
/// will not be totally correct.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct KeyEvent<'a> {
/// The action: press, release, etc.
pub action: Action,
/// The keycode of the physical key that was pressed. This is agnostic
/// to the layout. Layout-dependent matching can only be done via the
/// UTF-8 or unshifted codepoint.
pub key: Key,
/// Mods are the modifiers that are pressed.
pub mods: Mods,
/// The mods that were consumed in order to generate the text
/// in utf8. This has the mods set that were consumed, so to
/// get the set of mods that are effective you must negate
/// mods with this.
///
/// This field is meaningless if utf8 is empty.
pub consumed_mods: Mods,
/// Composing is true when this key event is part of a dead key
/// composition sequence and we're in the middle of it.
pub composing: bool,
/// The utf8 sequence that was generated by this key event.
/// This will be an empty string if there is no text generated.
/// If composing is true and this is non-empty, this is preedit
/// text.
pub utf8: &'a str,
/// The codepoint for this key when it is unshifted. For example,
/// shift+a is "A" in UTF-8 but unshifted would provide 'a'.
pub unshifted_codepoint: u32,
}
impl Default for KeyEvent<'_> {
fn default() -> Self {
KeyEvent {
action: Action::Press,
key: Key::Unidentified,
mods: Mods::default(),
consumed_mods: Mods::default(),
composing: false,
utf8: "",
unshifted_codepoint: 0,
}
}
}
impl KeyEvent<'_> {
/// Returns the effective modifiers for this event. The effective
/// modifiers are the mods that should be considered for keybindings.
pub fn effective_mods(&self) -> Mods {
if self.utf8.is_empty() {
return self.mods;
}
self.mods.unset(self.consumed_mods)
}
/// Returns a unique hash for this key event to be used for tracking
/// uniquess specifically with bindings. This omits fields that are
/// irrelevant for bindings.
pub fn binding_hash(&self) -> u64 {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
// These are all the fields that are explicitly part of Trigger.
self.key.hash(&mut hasher);
self.unshifted_codepoint.hash(&mut hasher);
self.mods.binding().hash(&mut hasher);
// Notes on unmapped things and why:
//
// - action: we don't have action-specific bindings right now
// AND we want to know if a key resulted in a binding regardless
// of action because a press should also ignore a release and so on.
//
// We can add to this if there is other confusion.
hasher.finish()
}
}
/// The action associated with an input event.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(i32)]
pub enum Action {
Release,
Press,
Repeat,
}
macro_rules! define_keys {
($($variant:ident => $name:literal,)*) => {
/// The set of key codes that Ghostty is aware of. These represent
/// physical keys on the keyboard. The logical key (or key string)
/// is the string that is generated by the key event and that is up
/// to the apprt to provide.
///
/// Note that these are layout-independent. For example, the "a"
/// key on a US keyboard is the same as the "ф" key on a Russian
/// keyboard, but both will report the "a" enum value in the key
/// event. These values are based on the W3C standard. See:
/// https://www.w3.org/TR/uievents-code
///
/// Layout-dependent strings are provided in the KeyEvent struct as
/// UTF-8 and are produced by the associated apprt. Ghostty core has
/// no mechanism to map input events to strings without the apprt.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[repr(i32)]
pub enum Key {
$($variant,)*
}
impl Key {
/// All keys in declaration order.
pub const ALL: &'static [Key] = &[$(Key::$variant,)*];
/// The Zig tag name (snake_case) of this key.
pub fn name(self) -> &'static str {
match self {
$(Key::$variant => $name,)*
}
}
/// Look up a key by its Zig tag name (snake_case).
pub fn from_name(s: &str) -> Option<Key> {
match s {
$($name => Some(Key::$variant),)*
_ => None,
}
}
}
};
}
define_keys! {
Unidentified => "unidentified",
Backquote => "backquote",
Backslash => "backslash",
BracketLeft => "bracket_left",
BracketRight => "bracket_right",
Comma => "comma",
Digit0 => "digit_0",
Digit1 => "digit_1",
Digit2 => "digit_2",
Digit3 => "digit_3",
Digit4 => "digit_4",
Digit5 => "digit_5",
Digit6 => "digit_6",
Digit7 => "digit_7",
Digit8 => "digit_8",
Digit9 => "digit_9",
Equal => "equal",
IntlBackslash => "intl_backslash",
IntlRo => "intl_ro",
IntlYen => "intl_yen",
KeyA => "key_a",
KeyB => "key_b",
KeyC => "key_c",
KeyD => "key_d",
KeyE => "key_e",
KeyF => "key_f",
KeyG => "key_g",
KeyH => "key_h",
KeyI => "key_i",
KeyJ => "key_j",
KeyK => "key_k",
KeyL => "key_l",
KeyM => "key_m",
KeyN => "key_n",
KeyO => "key_o",
KeyP => "key_p",
KeyQ => "key_q",
KeyR => "key_r",
KeyS => "key_s",
KeyT => "key_t",
KeyU => "key_u",
KeyV => "key_v",
KeyW => "key_w",
KeyX => "key_x",
KeyY => "key_y",
KeyZ => "key_z",
Minus => "minus",
Period => "period",
Quote => "quote",
Semicolon => "semicolon",
Slash => "slash",
AltLeft => "alt_left",
AltRight => "alt_right",
Backspace => "backspace",
CapsLock => "caps_lock",
ContextMenu => "context_menu",
ControlLeft => "control_left",
ControlRight => "control_right",
Enter => "enter",
MetaLeft => "meta_left",
MetaRight => "meta_right",
ShiftLeft => "shift_left",
ShiftRight => "shift_right",
Space => "space",
Tab => "tab",
Convert => "convert",
KanaMode => "kana_mode",
NonConvert => "non_convert",
Delete => "delete",
End => "end",
Help => "help",
Home => "home",
Insert => "insert",
PageDown => "page_down",
PageUp => "page_up",
ArrowDown => "arrow_down",
ArrowLeft => "arrow_left",
ArrowRight => "arrow_right",
ArrowUp => "arrow_up",
NumLock => "num_lock",
Numpad0 => "numpad_0",
Numpad1 => "numpad_1",
Numpad2 => "numpad_2",
Numpad3 => "numpad_3",
Numpad4 => "numpad_4",
Numpad5 => "numpad_5",
Numpad6 => "numpad_6",
Numpad7 => "numpad_7",
Numpad8 => "numpad_8",
Numpad9 => "numpad_9",
NumpadAdd => "numpad_add",
NumpadBackspace => "numpad_backspace",
NumpadClear => "numpad_clear",
NumpadClearEntry => "numpad_clear_entry",
NumpadComma => "numpad_comma",
NumpadDecimal => "numpad_decimal",
NumpadDivide => "numpad_divide",
NumpadEnter => "numpad_enter",
NumpadEqual => "numpad_equal",
NumpadMemoryAdd => "numpad_memory_add",
NumpadMemoryClear => "numpad_memory_clear",
NumpadMemoryRecall => "numpad_memory_recall",
NumpadMemoryStore => "numpad_memory_store",
NumpadMemorySubtract => "numpad_memory_subtract",
NumpadMultiply => "numpad_multiply",
NumpadParenLeft => "numpad_paren_left",
NumpadParenRight => "numpad_paren_right",
NumpadSubtract => "numpad_subtract",
NumpadSeparator => "numpad_separator",
NumpadUp => "numpad_up",
NumpadDown => "numpad_down",
NumpadRight => "numpad_right",
NumpadLeft => "numpad_left",
NumpadBegin => "numpad_begin",
NumpadHome => "numpad_home",
NumpadEnd => "numpad_end",
NumpadInsert => "numpad_insert",
NumpadDelete => "numpad_delete",
NumpadPageUp => "numpad_page_up",
NumpadPageDown => "numpad_page_down",
Escape => "escape",
F1 => "f1",
F2 => "f2",
F3 => "f3",
F4 => "f4",
F5 => "f5",
F6 => "f6",
F7 => "f7",
F8 => "f8",
F9 => "f9",
F10 => "f10",
F11 => "f11",
F12 => "f12",
F13 => "f13",
F14 => "f14",
F15 => "f15",
F16 => "f16",
F17 => "f17",
F18 => "f18",
F19 => "f19",
F20 => "f20",
F21 => "f21",
F22 => "f22",
F23 => "f23",
F24 => "f24",
F25 => "f25",
Fn => "fn",
FnLock => "fn_lock",
PrintScreen => "print_screen",
ScrollLock => "scroll_lock",
Pause => "pause",
BrowserBack => "browser_back",
BrowserFavorites => "browser_favorites",
BrowserForward => "browser_forward",
BrowserHome => "browser_home",
BrowserRefresh => "browser_refresh",
BrowserSearch => "browser_search",
BrowserStop => "browser_stop",
Eject => "eject",
LaunchApp1 => "launch_app_1",
LaunchApp2 => "launch_app_2",
LaunchMail => "launch_mail",
MediaPlayPause => "media_play_pause",
MediaSelect => "media_select",
MediaStop => "media_stop",
MediaTrackNext => "media_track_next",
MediaTrackPrevious => "media_track_previous",
Power => "power",
Sleep => "sleep",
AudioVolumeDown => "audio_volume_down",
AudioVolumeMute => "audio_volume_mute",
AudioVolumeUp => "audio_volume_up",
WakeUp => "wake_up",
Copy => "copy",
Cut => "cut",
Paste => "paste",
}
/// Codepoint <-> key map. Keypad entries must always be last so that
/// `from_ascii` doesn't map them over normal numerics and other keys.
const CODEPOINT_MAP: &[(u32, Key)] = &[
('a' as u32, Key::KeyA),
('b' as u32, Key::KeyB),
('c' as u32, Key::KeyC),
('d' as u32, Key::KeyD),
('e' as u32, Key::KeyE),
('f' as u32, Key::KeyF),
('g' as u32, Key::KeyG),
('h' as u32, Key::KeyH),
('i' as u32, Key::KeyI),
('j' as u32, Key::KeyJ),
('k' as u32, Key::KeyK),
('l' as u32, Key::KeyL),
('m' as u32, Key::KeyM),
('n' as u32, Key::KeyN),
('o' as u32, Key::KeyO),
('p' as u32, Key::KeyP),
('q' as u32, Key::KeyQ),
('r' as u32, Key::KeyR),
('s' as u32, Key::KeyS),
('t' as u32, Key::KeyT),
('u' as u32, Key::KeyU),
('v' as u32, Key::KeyV),
('w' as u32, Key::KeyW),
('x' as u32, Key::KeyX),
('y' as u32, Key::KeyY),
('z' as u32, Key::KeyZ),
('0' as u32, Key::Digit0),
('1' as u32, Key::Digit1),
('2' as u32, Key::Digit2),
('3' as u32, Key::Digit3),
('4' as u32, Key::Digit4),
('5' as u32, Key::Digit5),
('6' as u32, Key::Digit6),
('7' as u32, Key::Digit7),
('8' as u32, Key::Digit8),
('9' as u32, Key::Digit9),
(';' as u32, Key::Semicolon),
(' ' as u32, Key::Space),
('\'' as u32, Key::Quote),
(',' as u32, Key::Comma),
('`' as u32, Key::Backquote),
('.' as u32, Key::Period),
('/' as u32, Key::Slash),
('-' as u32, Key::Minus),
('=' as u32, Key::Equal),
('[' as u32, Key::BracketLeft),
(']' as u32, Key::BracketRight),
('\\' as u32, Key::Backslash),
// Control characters
('\t' as u32, Key::Tab),
// Keypad entries. We just assume keypad with the numpad_ prefix
// so that has some special meaning.
('0' as u32, Key::Numpad0),
('1' as u32, Key::Numpad1),
('2' as u32, Key::Numpad2),
('3' as u32, Key::Numpad3),
('4' as u32, Key::Numpad4),
('5' as u32, Key::Numpad5),
('6' as u32, Key::Numpad6),
('7' as u32, Key::Numpad7),
('8' as u32, Key::Numpad8),
('9' as u32, Key::Numpad9),
('.' as u32, Key::NumpadDecimal),
('/' as u32, Key::NumpadDivide),
('*' as u32, Key::NumpadMultiply),
('-' as u32, Key::NumpadSubtract),
('+' as u32, Key::NumpadAdd),
('=' as u32, Key::NumpadEqual),
];
impl Key {
/// Converts an ASCII character to a key, if possible. This returns
/// None if the character is unknown.
///
/// Note that this can't distinguish between physical keys, i.e. '0'
/// may be from the number row or the keypad, but it always maps
/// to '.zero'.
///
/// This is what we want, we want people to create keybindings that
/// are independent of the physical key.
pub fn from_ascii(ch: u8) -> Option<Key> {
CODEPOINT_MAP
.iter()
// No ASCII characters should ever map to a keypad key
.filter(|e| !e.1.keypad())
.find(|e| e.0 == u32::from(ch))
.map(|e| e.1)
}
/// Converts a W3C key code to a Ghostty key enum value.
///
/// All required W3C key codes are supported, but there are a number of
/// non-standard key codes that are not supported. In the case the value is
/// invalid or unsupported, this function will return None.
pub fn from_w3c(code: &str) -> Option<Key> {
// If the code is bigger than our buffer it can't possibly match.
if code.len() > 128 {
return None;
}
// First just check the whole thing lowercased, this is the simple case
if let Some(k) = Key::from_name(&code.to_ascii_lowercase()) {
return Some(k);
}
// We need to convert FooBar to foo_bar
let mut result = String::new();
for (i, ch) in code.bytes().enumerate() {
match ch {
b'a'..=b'z' => result.push(ch as char),
// Caps and numbers trigger underscores
b'A'..=b'Z' | b'0'..=b'9' => {
if i > 0 {
result.push('_');
}
result.push(ch.to_ascii_lowercase() as char);
}
// We don't know of any key codes that aren't alphanumeric.
_ => return None,
}
}
Key::from_name(&result)
}
/// Converts a Ghostty key enum value to a W3C key code.
pub fn w3c(self) -> String {
let name = self.name();
let mut out = String::with_capacity(name.len());
let mut upper_next = true;
for ch in name.chars() {
if ch == '_' {
upper_next = true;
} else if upper_next {
out.push(ch.to_ascii_uppercase());
upper_next = false;
} else {
out.push(ch);
}
}
out
}
/// True if this key represents a printable character.
pub fn printable(self) -> bool {
CODEPOINT_MAP.iter().any(|e| e.1 == self)
}
/// True if this key is a modifier.
pub fn modifier(self) -> bool {
matches!(
self,
Key::ShiftLeft
| Key::ControlLeft
| Key::AltLeft
| Key::MetaLeft
| Key::ShiftRight
| Key::ControlRight
| Key::AltRight
| Key::MetaRight
)
}
/// Whether this key should be remappable by the operating system.
///
/// On certain OSes (namely Linux and the BSDs) certain keys like the
/// functional keys are expected to be remappable by the user, such as
/// in the very common use case of swapping the Caps Lock key with the
/// Escape key with the XKB option `caps:swapescape`.
///
/// However, the way XKB implements this is by essentially acting as a
/// software key remapper that destroys all information about the original
/// physical key, leading to very annoying bugs like #7309 where the
/// physical key `XKB_KEY_c` gets remapped into `XKB_KEY_Cyrillic_tse`,
/// which causes all of our physical key handling to completely break down.
/// _Very naughty._
///
/// As a compromise, given that writing system keys (§3.1.1) comprise the
/// majority of keys that "change meaning [...] based on the current locale
/// and keyboard layout", we allow all other keys to be remapped by default
/// since they should be fairly harmless. We might consider making this
/// configurable, but for now this should at least placate most people.
pub fn should_be_remappable(self) -> bool {
let v = self as i32;
// "Writing System Keys" § 3.1.1 is the contiguous range
// backquote ..= slash.
!(Key::Backquote as i32..=Key::Slash as i32).contains(&v)
}
/// Returns true if this is a keypad key.
pub fn keypad(self) -> bool {
self.name().starts_with("numpad_")
}
/// Returns the codepoint representing this key, or None if the key is not
/// printable.
pub fn codepoint(self) -> Option<u32> {
CODEPOINT_MAP.iter().find(|e| e.1 == self).map(|e| e.0)
}
/// true if this key is one of the left or right versions of super (MacOS)
/// or ctrl.
pub fn ctrl_or_super(self) -> bool {
// Non-darwin target.
self == Key::ControlLeft || self == Key::ControlRight
}
/// true if this key is either left or right shift.
pub fn left_or_right_shift(self) -> bool {
self == Key::ShiftLeft || self == Key::ShiftRight
}
/// true if this key is either left or right alt.
pub fn left_or_right_alt(self) -> bool {
self == Key::AltLeft || self == Key::AltRight
}
}
/// This sets either "ctrl" or "super" to true (but not both)
/// on mods depending on if the build target is Mac or not. On
/// Mac, we default to super (i.e. super+c for copy) and on
/// non-Mac we default to ctrl (i.e. ctrl+c for copy).
pub fn ctrl_or_super(mods: Mods) -> Mods {
let mut copy = mods;
// Non-darwin target.
copy.ctrl = true;
copy
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn from_ascii_should_not_return_keypad_keys() {
assert_eq!(Key::from_ascii(b'0'), Some(Key::Digit0));
assert_eq!(Key::from_ascii(b'*'), None);
}
#[test]
fn keypad_keys() {
assert!(Key::Numpad0.keypad());
assert!(!Key::Digit1.keypad());
}
#[test]
fn w3c() {
// All our keys should convert to and from the W3C format.
for &key in Key::ALL {
assert_eq!(Some(key), Key::from_w3c(&key.w3c()), "{key:?}");
}
}
#[test]
fn w3c_names() {
assert_eq!(Key::KeyA.w3c(), "KeyA");
assert_eq!(Key::ArrowUp.w3c(), "ArrowUp");
assert_eq!(Key::Fn.w3c(), "Fn");
}
#[test]
fn should_be_remappable_range() {
assert!(!Key::KeyA.should_be_remappable());
assert!(!Key::Slash.should_be_remappable());
assert!(Key::AltLeft.should_be_remappable());
assert!(Key::Unidentified.should_be_remappable());
}
#[test]
fn ctrl_or_super_test() {
let m = ctrl_or_super(Mods::default());
assert!(m.ctrl_or_super());
}
}
+3161
View File
@@ -0,0 +1,3161 @@
//! Key event encoding: legacy (xterm/fixterms) and Kitty keyboard protocol
//! (ZIG/src/input/key_encode.zig).
use std::io::{self, Write};
use ghostty_vt::kitty::key::Flags as KittyFlags;
use crate::{
config::OptionAsAlt,
function_keys::{self, CursorMode, KeypadMode, ModifyKeys},
key::{self, Key, KeyEvent},
kitty::{self as kitty_table, Entry as KittyEntry},
};
/// Options that affect key encoding behavior. This is a mix of behavior
/// from terminal state as well as application configuration.
///
/// TODO(integration): Zig has `Options.fromTerminal(*const Terminal)`; the
/// terminal crate should construct this from its modes (`alt_esc_prefix`,
/// `cursor_keys`, `keypad_keys`, `ignore_keypad_with_numlock`, flag
/// `modify_other_keys_2`, active screen `kitty_keyboard.current()`).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Options {
/// Terminal DEC mode 1
pub cursor_key_application: bool,
/// Terminal DEC mode 66
pub keypad_key_application: bool,
/// Terminal DEC mode 1035
pub ignore_keypad_with_numlock: bool,
/// Terminal DEC mode 1036
pub alt_esc_prefix: bool,
/// xterm "modifyOtherKeys mode 2". Details here:
/// https://invisible-island.net/xterm/modified-keys.html
pub modify_other_keys_state_2: bool,
/// Kitty keyboard protocol flags.
pub kitty_flags: KittyFlags,
/// Determines whether the "option" key on macOS is treated
/// as "alt" or not. See the Ghostty `macos_option-as-alt` config
/// docs for a more detailed description of why this is needed.
pub macos_option_as_alt: OptionAsAlt,
}
impl Options {
pub const DEFAULT: Options = Options {
cursor_key_application: false,
keypad_key_application: false,
ignore_keypad_with_numlock: false,
alt_esc_prefix: false,
modify_other_keys_state_2: false,
kitty_flags: KittyFlags::DISABLED,
macos_option_as_alt: OptionAsAlt::False,
};
}
/// Encode the key event to the writer in the proper format given
/// the options. For example, this will properly encode a key press
/// such as "ctrl+A" to Kitty format if Kitty encoding is enabled.
///
/// Not all key events will result in output. It is up to the caller
/// to use a writer that can track whether any output was written if
/// they care about that.
pub fn encode<W: Write>(writer: &mut W, event: &KeyEvent, opts: &Options) -> io::Result<()> {
if opts.kitty_flags.int() != 0 {
kitty(writer, event, opts)
} else {
legacy(writer, event, opts)
}
}
fn is_enter_backspace_tab(k: Key) -> bool {
matches!(k, Key::Enter | Key::Backspace | Key::Tab)
}
/// Perform Kitty keyboard protocol encoding of the key event.
fn kitty<W: Write>(writer: &mut W, event: &KeyEvent, opts: &Options) -> io::Result<()> {
// This should never happen but we'll check anyway.
if opts.kitty_flags.int() == 0 {
return legacy(writer, event, opts);
}
// We only processed "press" events unless report events is active
if event.action == key::Action::Release {
if !opts.kitty_flags.report_events {
return Ok(());
}
// Enter, backspace, and tab do not report release events unless "report
// all" is set
if !opts.kitty_flags.report_all && is_enter_backspace_tab(event.key) {
return Ok(());
}
}
let all_mods = event.mods;
let effective_mods = event.effective_mods();
let binding_mods = effective_mods.binding();
// Find the entry for this key in the kitty table.
let entry_: Option<KittyEntry> = 'entry: {
// Functional or predefined keys
for entry in kitty_table::entries() {
if entry.key == event.key {
break 'entry Some(*entry);
}
}
// Otherwise, we use our unicode codepoint from UTF8. We
// always use the unshifted value.
if event.unshifted_codepoint > 0 {
break 'entry Some(KittyEntry {
key: event.key,
code: event.unshifted_codepoint,
final_: b'u',
modifier: false,
});
}
None
};
'preprocessing: {
// When composing, the only keys sent are plain modifiers.
if event.composing {
if let Some(entry) = &entry_
&& entry.modifier
{
break 'preprocessing;
}
return Ok(());
}
// IME confirmation still sends an enter key so if we have enter
// and UTF8 text we just send it directly since we assume that is
// what's happening. See legacy()'s similar logic for more details
// on how to verify this.
if !event.utf8.is_empty() && matches!(event.key, Key::Enter | Key::Backspace) {
// See legacy for why we handle this this way.
if !is_control_utf8(event.utf8) {
if event.key == Key::Backspace {
return Ok(());
}
return writer.write_all(event.utf8.as_bytes());
}
}
// If we're reporting all then we always send CSI sequences.
if !opts.kitty_flags.report_all {
// Quote:
// The only exceptions are the Enter, Tab and Backspace keys which
// still generate the same bytes as in legacy mode this is to allow the
// user to type and execute commands in the shell such as reset after a
// program that sets this mode crashes without clearing it.
//
// Quote ("report all" mode):
// Note that all keys are reported as escape codes, including Enter,
// Tab, Backspace etc.
if binding_mods.empty() {
match event.key {
Key::Enter => return writer.write_all(b"\r"),
Key::Tab => return writer.write_all(b"\t"),
Key::Backspace => return writer.write_all(&[0x7F]),
_ => {}
}
}
// Send plain-text non-modified text directly to the terminal.
// We don't send release events because those are specially encoded.
if !event.utf8.is_empty()
&& binding_mods.empty()
&& event.action != key::Action::Release
{
// We only do this for printable characters. We should
// inspect the real unicode codepoint properties here but
// the real world issue is usually control characters.
if !event.utf8.chars().any(|c| is_control(u32::from(c))) {
return writer.write_all(event.utf8.as_bytes());
}
}
}
}
let Some(entry) = entry_ else {
// No entry found. If we have UTF-8 text this is a pure text event
// (e.g. composed/IME text), so send it as-is so programs can
// still receive it.
if !event.utf8.is_empty() {
return writer.write_all(event.utf8.as_bytes());
}
return Ok(());
};
// If this is just a modifier we require "report all" to send the sequence.
if entry.modifier && !opts.kitty_flags.report_all {
return Ok(());
}
let seq = {
let mut seq = KittySequence {
key: entry.code,
final_: entry.final_,
mods: KittyMods::from_input(event.action, event.key, all_mods),
event: KittyEvent::None,
alternates: [None, None],
text: "",
};
if opts.kitty_flags.report_events {
seq.event = match event.action {
key::Action::Press => KittyEvent::Press,
key::Action::Release => KittyEvent::Release,
key::Action::Repeat => KittyEvent::Repeat,
};
}
'alternates: {
if !opts.kitty_flags.report_alternates {
break 'alternates;
}
// Break early if this is a control key
if is_control(seq.key) {
break 'alternates;
}
let mut it = event.utf8.chars();
// If we have a codepoint in our UTF-8 sequence, then we can
// report the shifted version.
if let Some(cp1) = it.next().map(u32::from) {
// Set the first alternate (shifted version)
if cp1 != seq.key && seq.mods.shift {
seq.alternates[0] = Some(cp1);
}
// We want to know if there are additional codepoints because
// our logic below depends on the utf8 being a single codepoint.
let has_cp2 = it.next().is_some();
// Set the base layout key. We only report this if this codepoint
// differs from our pressed key.
if let Some(base) = event.key.codepoint()
&& base != seq.key
&& (cp1 != base && !has_cp2)
{
seq.alternates[1] = Some(base);
}
} else {
// No UTF-8 so we can't report a shifted key but we can still
// report a base layout key.
if let Some(base) = event.key.codepoint()
&& base != seq.key
{
seq.alternates[1] = Some(base);
}
}
}
if opts.kitty_flags.report_associated && seq.event != KittyEvent::Release {
// Determine if the Alt modifier should be treated as an actual
// modifier (in which case it prevents associated text) or as
// the macOS Option key, which does not prevent associated text.
// (Non-macOS: always true.)
let alt_prevents_text = true;
if !seq.mods.prevents_text(alt_prevents_text) {
seq.text = event.utf8;
}
}
seq
};
seq.encode(writer)
}
/// Perform legacy encoding of the key event. "Legacy" in this case
/// is referring to the behavior of traditional terminals, plus
/// xterm's `modifyOtherKeys`, plus Paul Evans's "fixterms" spec.
/// These together combine the legacy protocol because they're all
/// meant to be extensions that do not change any existing behavior
/// and therefore safe to combine.
fn legacy<W: Write>(writer: &mut W, event: &KeyEvent, opts: &Options) -> io::Result<()> {
let all_mods = event.mods;
let effective_mods = event.effective_mods();
let binding_mods = effective_mods.binding();
// Legacy encoding only does press/repeat
if event.action != key::Action::Press && event.action != key::Action::Repeat {
return Ok(());
}
// If we're in a dead key state then we never emit a sequence.
if event.composing {
return Ok(());
}
// If we match a PC style function key then that is our result.
if let Some(sequence) = pc_style_function_key(
event.key,
all_mods,
opts.cursor_key_application,
opts.keypad_key_application,
opts.ignore_keypad_with_numlock,
opts.modify_other_keys_state_2,
) {
// If we have UTF-8 text, then we never emit PC style function
// keys. Many function keys (escape, enter, backspace) have
// a specific meaning when dead keys are active and so we don't
// want to send that to the terminal. Examples:
//
// - Japanese: escape clears the dead key state
// - Korean: escape commits the dead key state
// - Korean: backspace should delete a single preedit char
//
let mut skip_pc_style = false;
if !event.utf8.is_empty()
&& matches!(event.key, Key::Backspace | Key::Enter | Key::Escape)
// We want to ignore control characters. This is because
// some apprts (macOS) will send control characters as
// UTF-8 encodings and we handle that manually.
&& !is_control_utf8(event.utf8)
{
// Backspace encodes nothing because we modified IME.
// Enter/escape don't encode the PC-style encoding
// because we want to encode committed text.
if event.key == Key::Backspace {
return Ok(());
}
skip_pc_style = true;
}
if !skip_pc_style {
return writer.write_all(sequence.as_bytes());
}
}
// If we match a control sequence, we output that directly. For
// ctrlSeq we have to use all mods because we want it to only
// match ctrl+<char>.
if let Some(ch) = ctrl_seq(event.key, event.utf8, event.unshifted_codepoint, all_mods) {
// C0 sequences support alt-as-esc prefixing.
if binding_mods.alt {
return writer.write_all(&[0x1B, ch]);
}
return writer.write_all(&[ch]);
}
// If we have no UTF8 text then the only possibility is the
// alt-prefix handling of unshifted codepoints... so we process that.
let utf8 = event.utf8;
if utf8.is_empty() {
if let Some(byte) = legacy_alt_prefix(event, binding_mods, all_mods, opts) {
writer.write_all(&[0x1B, byte])?;
}
return Ok(());
}
// In modify other keys state 2, we send the CSI 27 sequence
// for any char with a modifier. Ctrl sequences like Ctrl+a
// are already handled above.
if opts.modify_other_keys_state_2 {
let mut it = utf8.chars();
// We only do this if we have a single codepoint. There shouldn't
// ever be a multi-codepoint sequence that triggers this.
if let (Some(codepoint), None) = (it.next().map(u32::from), it.clone().next()) {
// The mods we encode for this are just the binding mods (shift, ctrl,
// super, alt unless it is actually option).
let mods = event.mods.binding();
// This copies xterm's `ModifyOtherKeys` function that returns
// whether modify other keys should be encoded for the given
// input.
let should_modify = 'should_modify: {
// xterm IsControlInput
if (0x40..=0x7F).contains(&codepoint) {
break 'should_modify true;
}
// If we have anything other than shift pressed, encode.
let mut mods_no_shift = mods;
mods_no_shift.shift = false;
if !mods_no_shift.empty() {
break 'should_modify true;
}
// We only have shift pressed. We only allow space.
if codepoint == u32::from(' ') {
break 'should_modify true;
}
// This logic isn't complete but I don't fully understand
// the rest so I'm going to wait until we can have a
// reasonable test scenario.
false
};
if should_modify {
for (modset, code) in function_keys::MODIFIERS.iter().zip(2..) {
if !mods.equal(*modset) {
continue;
}
return write!(writer, "\x1B[27;{code};{codepoint}~");
}
}
}
}
// Let's see if we should apply fixterms to this codepoint.
// At this stage of key processing, we only need to apply fixterms
// to unicode codepoints if we have ctrl set.
if event.mods.ctrl {
'csiu: {
// Important: we want to use the original mods here, not the
// effective mods. The fixterms spec states the shifted chars
// should be sent uppercase but Kitty changes that behavior
// so we'll send all the mods.
let mut csi_u_mods = CsiUMods::from_input(event.mods);
// Get our codepoint. If we have more than one codepoint this
// can't be valid CSIu.
let mut it = event.utf8.chars();
let Some(first) = it.next() else {
break 'csiu;
};
if it.next().is_some() {
break 'csiu;
}
let mut ch = u32::from(first);
// If our character is A to Z and we have shift set, then
// we lowercase it. This is a Kitty-specific behavior that
// we choose to follow and diverge from the fixterms spec.
// This makes it easier for programs to detect shifted letters
// for keybindings and is not just theoretical but used by
// real programs.
if (u32::from('A')..=u32::from('Z')).contains(&ch) && csi_u_mods.shift {
// We want to rely on apprt to send us the correct
// unshifted codepoint...
ch += 32;
}
// If our unshifted codepoint is identical to the shifted
// then we consider shift. Otherwise, we do not because the
// shift key was used to obtain the character. This is specified
// by fixterms.
if event.unshifted_codepoint != ch {
csi_u_mods.shift = false;
}
return write!(writer, "\x1B[{};{}u", ch, csi_u_mods.seq_int());
}
}
// If we have alt-pressed and alt-esc-prefix is enabled, then
// we need to prefix the utf8 sequence with an esc.
if let Some(byte) = legacy_alt_prefix(event, binding_mods, all_mods, opts) {
return writer.write_all(&[0x1B, byte]);
}
// (On macOS, command+keys do not encode text. On Linux we continue to
// encode text because it is typical.)
writer.write_all(utf8.as_bytes())
}
fn legacy_alt_prefix(
event: &KeyEvent,
binding_mods: key::Mods,
_mods: key::Mods,
opts: &Options,
) -> Option<u8> {
// This only takes effect with alt pressed
if !binding_mods.alt || !opts.alt_esc_prefix {
return None;
}
// (macOS option-as-alt handling does not apply on Linux.)
// Otherwise, we require utf8 to already have the byte represented.
let utf8 = event.utf8.as_bytes();
if utf8.len() == 1 {
return Some(utf8[0]);
}
// If UTF8 isn't set, we will allow unshifted codepoints through.
if event.unshifted_codepoint > 0
&& let Ok(byte) = u8::try_from(event.unshifted_codepoint)
{
return Some(byte);
}
// Else, we can't figure out the byte to alt-prefix so we
// exit this handling.
None
}
/// Determines whether the key should be encoded in the xterm
/// "PC-style Function Key" syntax (roughly). This is a hardcoded
/// table of keys and modifiers that result in a specific sequence.
fn pc_style_function_key(
keyval: Key,
mods: key::Mods,
cursor_key_application: bool,
keypad_key_application_req: bool,
ignore_keypad_with_numlock: bool,
modify_other_keys: bool, // True if state 2
) -> Option<&'static str> {
// We only want binding-sensitive mods because lock keys
// and directional modifiers (left/right) don't matter for
// pc-style function keys.
let mods_int = mods.binding().int();
// Keypad application keymode isn't super straightforward.
// On xterm, in VT220 mode, numlock alone is enough to trigger
// application mode. But in more modern modes, numlock is
// ignored by default via mode 1035 (default true). If mode
// 1035 is on, we always are in numerical keypad mode. If
// mode 1035 is off, we are in application mode if the
// proper numlock state is pressed. The numlock state is implicitly
// determined based on the keycode sent (i.e. 1 with numlock
// on will be kp_end).
//
// If we're ignoring keypad then this is always false.
// In other words, we're always in numerical keypad mode.
let keypad_key_application = if ignore_keypad_with_numlock {
false
} else {
keypad_key_application_req
};
for entry in function_keys::keys(keyval) {
match entry.cursor {
CursorMode::Any => {}
CursorMode::Normal => {
if cursor_key_application {
continue;
}
}
CursorMode::Application => {
if !cursor_key_application {
continue;
}
}
}
match entry.keypad {
KeypadMode::Any => {}
KeypadMode::Normal => {
if keypad_key_application {
continue;
}
}
KeypadMode::Application => {
if !keypad_key_application {
continue;
}
}
}
match entry.modify_other_keys {
ModifyKeys::Any => {}
ModifyKeys::Set => {
if modify_other_keys {
continue;
}
}
ModifyKeys::SetOther => {
if !modify_other_keys {
continue;
}
}
}
let entry_mods_int = entry.mods.int();
if entry_mods_int == 0 {
if mods_int != 0 && !entry.mods_empty_is_any {
continue;
}
// mods are either empty, or empty means any so we allow it.
} else if entry_mods_int != mods_int {
// any set mods require an exact match
continue;
}
return Some(entry.sequence.as_str());
}
None
}
/// Returns the C0 byte for the key event if it should be used.
/// This converts a key event into the expected terminal behavior
/// such as Ctrl+C turning into 0x03, amongst many other translations.
///
/// This will return None if the key event should not be converted
/// into a C0 byte. There are many cases for this and you should read
/// the source code to understand them.
fn ctrl_seq(logical_key: Key, utf8: &str, unshifted_codepoint: u32, mods: key::Mods) -> Option<u8> {
let ctrl_only = key::Mods {
ctrl: true,
..key::Mods::default()
}
.int();
// If ctrl is not pressed then we never do anything.
if !mods.ctrl {
return None;
}
// We need to only get binding modifiers so we strip lock
// keys, sides, etc.
let mut unset_mods = mods.binding();
// Remove alt from our modifiers because it does not impact whether
// we are generating a ctrl sequence and we handle the ESC-prefix
// logic separately.
unset_mods.alt = false;
let mut ch: u8 = 'ch: {
// If we have exactly one UTF8 byte, we assume that is the
// character we want to convert to a C0 byte.
if utf8.len() == 1 {
break 'ch utf8.as_bytes()[0];
}
// If we have a logical key that maps to a single byte
// printable character, we use that. History to explain this:
// this was added to support cyrillic keyboard layouts such
// as Russian and Mongolian. These layouts have a `c` key that
// maps to U+0441 (cyrillic small letter "c") but every
// terminal I've tested encodes this as ctrl+c.
if let Some(cp) = logical_key.codepoint()
&& let Ok(byte) = u8::try_from(cp)
{
// For this specific case, we only map to the key if
// we have exactly ctrl pressed. This is because shift
// would modify the key and we don't know how to do that
// properly here (don't have the layout). And we want
// to encode shift as CSIu.
if unset_mods.int() != ctrl_only {
return None;
}
break 'ch byte;
}
// Otherwise we don't have a character to convert that
// we can reliably map to a C0 byte.
return None;
};
// Remove shift if we have something outside of the US letter
// range. This is so that characters such as `ctrl+shift+-`
// generate the correct ctrl-seq (used by emacs).
if unset_mods.shift && !ch.is_ascii_uppercase() {
// Special case for fixterms awkward case as specified.
if ch != b'@' {
unset_mods.shift = false;
}
}
// If the character is uppercase, we convert it to lowercase. We
// rely on the unshifted codepoint to do this. This handles
// the scenario where we have caps lock pressed. Note that
// shifted characters are handled above, if we are just pressing
// shift then the ctrl-only check will fail later and we won't
// ctrl-seq encode.
if ch.is_ascii_uppercase()
&& unshifted_codepoint > 0
&& let Ok(byte) = u8::try_from(unshifted_codepoint)
{
ch = byte;
}
// An additional note on caps lock and shift interaction.
// If we have caps lock set and an ASCII letter is pressed,
// we lowercase it (above). If we have only control pressed,
// we process it as a ctrl seq. For example ctrl+M with caps
// lock but no shift will encode as 0x0D.
//
// But, if you press ctrl+shift+m, this will not encode as a
// ctrl-seq and falls through to CSIu encoding. This lets programs
// detect the difference between ctrl+M and ctrl+shift+M. This
// diverges from the fixterms "spec" and most terminals. This
// only matches Kitty in behavior. But I believe this is a
// justified divergence because it's a useful distinction.
// After unsetting, we only continue if we have ONLY control set.
if unset_mods.int() != ctrl_only {
return None;
}
// From Kitty's key encoding logic. I tried to discern the exact
// behavior across different terminals but it's not clear, so I'm
// just going to repeat what Kitty does.
match ch {
b' ' => Some(0),
b'/' => Some(31),
b'0' => Some(48),
b'1' => Some(49),
b'2' => Some(0),
b'3' => Some(27),
b'4' => Some(28),
b'5' => Some(29),
b'6' => Some(30),
b'7' => Some(31),
b'8' => Some(127),
b'9' => Some(57),
b'?' => Some(127),
b'@' => Some(0),
b'\\' => Some(28),
b']' => Some(29),
b'^' => Some(30),
b'_' => Some(31),
b'a' => Some(1),
b'b' => Some(2),
b'c' => Some(3),
b'd' => Some(4),
b'e' => Some(5),
b'f' => Some(6),
b'g' => Some(7),
b'h' => Some(8),
b'j' => Some(10),
b'k' => Some(11),
b'l' => Some(12),
b'n' => Some(14),
b'o' => Some(15),
b'p' => Some(16),
b'q' => Some(17),
b'r' => Some(18),
b's' => Some(19),
b't' => Some(20),
b'u' => Some(21),
b'v' => Some(22),
b'w' => Some(23),
b'x' => Some(24),
b'y' => Some(25),
b'z' => Some(26),
b'~' => Some(30),
// These are purposely NOT handled here because of the fixterms
// specification: https://www.leonerd.org.uk/hacks/fixterms/
// These are processed as CSI u.
// 'i' => 0x09,
// 'm' => 0x0D,
// '[' => 0x1B,
_ => None,
}
}
/// Returns true if this is an ASCII control character, matches libc implementation.
fn is_control(cp: u32) -> bool {
cp < 0x20 || cp == 0x7F
}
/// Returns true if this string is comprised of a single
/// control character. This returns false for multi-byte strings.
fn is_control_utf8(s: &str) -> bool {
s.len() == 1 && is_control(u32::from(s.as_bytes()[0]))
}
/// This is the bitmask for fixterm CSI u modifiers.
#[derive(Debug, Clone, Copy, Default)]
struct CsiUMods {
shift: bool,
alt: bool,
ctrl: bool,
}
impl CsiUMods {
/// Convert an input mods value into the CSI u mods value.
fn from_input(mods: key::Mods) -> CsiUMods {
CsiUMods {
shift: mods.shift,
alt: mods.alt,
ctrl: mods.ctrl,
}
}
/// Returns the raw int value of this packed struct.
fn int(self) -> u8 {
u8::from(self.shift) | u8::from(self.alt) << 1 | u8::from(self.ctrl) << 2
}
/// Returns the integer value sent as part of the CSI u sequence.
/// This adds 1 to the bitmask value as described in the spec.
fn seq_int(self) -> u8 {
self.int() + 1
}
}
/// This is the bitfields for Kitty modifiers.
#[derive(Debug, Clone, Copy, Default)]
#[allow(dead_code)]
struct KittyMods {
shift: bool,
alt: bool,
ctrl: bool,
super_: bool,
hyper: bool,
meta: bool,
caps_lock: bool,
num_lock: bool,
}
impl KittyMods {
/// Convert an input mods value into the CSI u mods value.
fn from_input(_action: key::Action, _k: Key, mods: key::Mods) -> KittyMods {
KittyMods {
shift: mods.shift,
alt: mods.alt,
ctrl: mods.ctrl,
super_: mods.super_,
hyper: false,
meta: false,
caps_lock: mods.caps_lock,
num_lock: mods.num_lock,
}
}
/// Returns true if the modifiers prevent printable text.
///
/// The alt_prevents_text parameter determines whether or not the Alt
/// modifier prevents printable text. On Linux, this is always true. On
/// macOS, this is only true if macos-option-as-alt is set.
fn prevents_text(self, alt_prevents_text: bool) -> bool {
(self.alt && alt_prevents_text) || self.ctrl || self.super_ || self.hyper || self.meta
}
/// Returns the raw int value of this packed struct.
fn int(self) -> u16 {
u16::from(self.shift)
| u16::from(self.alt) << 1
| u16::from(self.ctrl) << 2
| u16::from(self.super_) << 3
| u16::from(self.hyper) << 4
| u16::from(self.meta) << 5
| u16::from(self.caps_lock) << 6
| u16::from(self.num_lock) << 7
}
/// Returns the integer value sent as part of the Kitty sequence.
/// This adds 1 to the bitmask value as described in the spec.
fn seq_int(self) -> u16 {
self.int() + 1
}
}
/// Values for the event code (see "event-type" in the KittySequence comment).
/// Note that Kitty omits the ":1" for the press event but other
/// terminals include it. We'll include it.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum KittyEvent {
None = 0,
Press = 1,
Repeat = 2,
Release = 3,
}
/// Represents a kitty key sequence and has helpers for encoding it.
/// The sequence from the Kitty specification:
///
/// CSI unicode-key-code:alternate-key-codes ; modifiers:event-type ; text-as-codepoints u
#[derive(Debug, Clone, Copy)]
struct KittySequence<'a> {
key: u32,
final_: u8,
mods: KittyMods,
event: KittyEvent,
alternates: [Option<u32>; 2],
text: &'a str,
}
impl KittySequence<'_> {
fn encode<W: Write>(&self, writer: &mut W) -> io::Result<()> {
if self.final_ == b'u' || self.final_ == b'~' {
return self.encode_full(writer);
}
self.encode_special(writer)
}
fn encode_full<W: Write>(&self, writer: &mut W) -> io::Result<()> {
// Key section
write!(writer, "\x1B[{}", self.key)?;
// Write our alternates
if let Some(shifted) = self.alternates[0] {
write!(writer, ":{shifted}")?;
}
if let Some(base) = self.alternates[1] {
if self.alternates[0].is_none() {
write!(writer, "::{base}")?;
} else {
write!(writer, ":{base}")?;
}
}
// Mods and events section
let mods = self.mods.seq_int();
let mut emit_prior = false;
if self.event != KittyEvent::None && self.event != KittyEvent::Press {
write!(writer, ";{}:{}", mods, self.event as u8)?;
emit_prior = true;
} else if mods > 1 {
write!(writer, ";{mods}")?;
emit_prior = true;
}
// Text section
if !self.text.is_empty() {
let mut count = 0;
for c in self.text.chars() {
let cp = u32::from(c);
// If the codepoint is non-printable ASCII character, skip.
if is_control(cp) {
continue;
}
// We need to add our ";". We need to add two if we didn't emit
// the modifier section. We only do this initially.
if count == 0 {
if !emit_prior {
writer.write_all(b";")?;
}
writer.write_all(b";")?;
} else {
writer.write_all(b":")?;
}
write!(writer, "{cp}")?;
count += 1;
}
}
writer.write_all(&[self.final_])
}
fn encode_special<W: Write>(&self, writer: &mut W) -> io::Result<()> {
let mods = self.mods.seq_int();
let f = self.final_ as char;
if self.event != KittyEvent::None {
return write!(writer, "\x1B[1;{}:{}{}", mods, self.event as u8, f);
}
if mods > 1 {
return write!(writer, "\x1B[1;{mods}{f}");
}
write!(writer, "\x1B[{f}")
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use crate::key_mods::{ModSide, Mods, Side};
#[test]
fn kitty_plain_text() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
key: Key::KeyA,
mods: Mods { ..Mods::default() },
utf8: "abcd",
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"abcd");
}
}
#[test]
fn kitty_repeat_with_just_disambiguate() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
key: Key::KeyA,
action: key::Action::Repeat,
mods: Mods { ..Mods::default() },
utf8: "a",
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"a");
}
}
#[test]
fn kitty_enter_backspace_tab() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
key: Key::Enter,
mods: Mods { ..Mods::default() },
utf8: "",
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x0d");
}
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
key: Key::Backspace,
mods: Mods { ..Mods::default() },
utf8: "",
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x7f");
}
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
key: Key::Tab,
mods: Mods { ..Mods::default() },
utf8: "",
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x09");
}
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
action: key::Action::Release,
key: Key::Enter,
mods: Mods { ..Mods::default() },
utf8: "",
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
report_events: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"");
}
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
action: key::Action::Release,
key: Key::Backspace,
mods: Mods { ..Mods::default() },
utf8: "",
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
report_events: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"");
}
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
action: key::Action::Release,
key: Key::Tab,
mods: Mods { ..Mods::default() },
utf8: "",
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
report_events: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"");
}
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
action: key::Action::Release,
key: Key::Enter,
mods: Mods { ..Mods::default() },
utf8: "",
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
report_events: true,
report_all: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[13;1:3u");
}
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
action: key::Action::Release,
key: Key::Backspace,
mods: Mods { ..Mods::default() },
utf8: "",
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
report_events: true,
report_all: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[127;1:3u");
}
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
action: key::Action::Release,
key: Key::Tab,
mods: Mods { ..Mods::default() },
utf8: "",
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
report_events: true,
report_all: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[9;1:3u");
}
}
#[test]
fn kitty_shift_backspace_emits_csi_u() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
key: Key::Backspace,
mods: Mods {
shift: true,
..Mods::default()
},
utf8: "",
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[127;2u");
}
}
#[test]
fn kitty_shift_enter_emits_csi_u() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
key: Key::Enter,
mods: Mods {
shift: true,
..Mods::default()
},
utf8: "",
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[13;2u");
}
}
#[test]
fn kitty_shift_tab_emits_csi_u() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
key: Key::Tab,
mods: Mods {
shift: true,
..Mods::default()
},
utf8: "",
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[9;2u");
}
}
#[test]
fn kitty_enter_with_all_flags() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
key: Key::Enter,
mods: Mods { ..Mods::default() },
utf8: "",
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
report_events: true,
report_alternates: true,
report_all: true,
report_associated: true,
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w[1..], b"[13u");
}
}
#[test]
fn kitty_ctrl_with_all_flags() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
key: Key::ControlLeft,
mods: Mods {
ctrl: true,
..Mods::default()
},
utf8: "",
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
report_events: true,
report_alternates: true,
report_all: true,
report_associated: true,
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w[1..], b"[57442;5u");
}
}
#[test]
fn kitty_ctrl_release_with_ctrl_mod_set() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
action: key::Action::Release,
key: Key::ControlLeft,
mods: Mods {
ctrl: true,
..Mods::default()
},
utf8: "",
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
report_events: true,
report_alternates: true,
report_all: true,
report_associated: true,
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w[1..], b"[57442;5:3u");
}
}
#[test]
fn kitty_delete() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
key: Key::Delete,
mods: Mods { ..Mods::default() },
utf8: "\u{7f}",
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[3~");
}
}
#[test]
fn kitty_composing_with_no_modifier() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
key: Key::KeyA,
mods: Mods {
shift: true,
..Mods::default()
},
composing: true,
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"");
}
}
#[test]
fn kitty_composing_with_modifier() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
key: Key::ShiftLeft,
mods: Mods {
shift: true,
..Mods::default()
},
composing: true,
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
report_all: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[57441;2u");
}
}
#[test]
fn kitty_composed_text_with_report_all() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
key: Key::Unidentified,
mods: Mods { ..Mods::default() },
utf8: "\u{fb}",
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
report_events: true,
report_alternates: true,
report_all: true,
report_associated: true,
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\xc3\xbb");
}
}
#[test]
fn kitty_shift_a_on_us_keyboard() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
key: Key::KeyA,
mods: Mods {
shift: true,
..Mods::default()
},
utf8: "A",
unshifted_codepoint: 97,
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
report_alternates: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[97:65;2u");
}
}
#[test]
fn kitty_matching_unshifted_codepoint() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
key: Key::KeyA,
mods: Mods {
shift: true,
..Mods::default()
},
utf8: "A",
unshifted_codepoint: 65,
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
report_alternates: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[65::97;2u");
}
}
#[test]
fn kitty_report_alternates_with_caps() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
key: Key::KeyJ,
mods: Mods {
caps_lock: true,
..Mods::default()
},
utf8: "J",
unshifted_codepoint: 106,
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
report_all: true,
report_alternates: true,
report_associated: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[106;65;74u");
}
}
#[test]
fn kitty_report_alternates_colon_shift() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
key: Key::Semicolon,
mods: Mods {
shift: true,
..Mods::default()
},
utf8: ":",
unshifted_codepoint: u32::from(';'),
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
report_all: true,
report_alternates: true,
report_associated: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[59:58;2;58u");
}
}
#[test]
fn kitty_report_alternates_with_ru_layout() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
key: Key::Semicolon,
mods: Mods { ..Mods::default() },
utf8: "\u{447}",
unshifted_codepoint: 1095,
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
report_all: true,
report_alternates: true,
report_associated: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[1095::59;;1095u");
}
}
#[test]
fn kitty_report_alternates_with_ru_layout_shifted() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
key: Key::Semicolon,
mods: Mods {
shift: true,
..Mods::default()
},
utf8: "\u{427}",
unshifted_codepoint: 1095,
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
report_all: true,
report_alternates: true,
report_associated: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[1095:1063:59;2;1063u");
}
}
#[test]
fn kitty_report_alternates_with_ru_layout_caps_lock() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
key: Key::Semicolon,
mods: Mods {
caps_lock: true,
..Mods::default()
},
utf8: "\u{427}",
unshifted_codepoint: 1095,
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
report_all: true,
report_alternates: true,
report_associated: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[1095::59;65;1063u");
}
}
#[test]
fn kitty_report_alternates_with_hu_layout_release() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
action: key::Action::Release,
key: Key::BracketLeft,
mods: Mods {
ctrl: true,
..Mods::default()
},
utf8: "",
unshifted_codepoint: 337,
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
report_all: true,
report_alternates: true,
report_associated: true,
report_events: true,
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w[1..], b"[337::91;5:3u");
}
}
#[test]
fn kitty_up_arrow_with_utf8() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
key: Key::ArrowUp,
mods: Mods { ..Mods::default() },
utf8: "\u{1e}",
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[A");
}
}
#[test]
fn kitty_shift_tab() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
key: Key::Tab,
mods: Mods {
shift: true,
..Mods::default()
},
utf8: "",
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
report_alternates: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[9;2u");
}
}
#[test]
fn kitty_left_shift() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
key: Key::ShiftLeft,
mods: Mods { ..Mods::default() },
utf8: "",
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
report_alternates: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"");
}
}
#[test]
fn kitty_left_shift_with_report_all() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
key: Key::ShiftLeft,
mods: Mods { ..Mods::default() },
utf8: "",
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
report_all: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[57441u");
}
}
#[test]
fn kitty_report_associated_with_modifiers() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
key: Key::KeyJ,
mods: Mods {
ctrl: true,
..Mods::default()
},
utf8: "j",
unshifted_codepoint: 106,
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
report_all: true,
report_alternates: true,
report_associated: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[106;5u");
}
}
#[test]
fn kitty_report_associated() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
key: Key::KeyJ,
mods: Mods {
shift: true,
..Mods::default()
},
utf8: "J",
unshifted_codepoint: 106,
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
report_all: true,
report_alternates: true,
report_associated: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[106:74;2;74u");
}
}
#[test]
fn kitty_report_associated_on_release() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
action: key::Action::Release,
key: Key::KeyJ,
mods: Mods {
shift: true,
..Mods::default()
},
utf8: "J",
unshifted_codepoint: 106,
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
report_all: true,
report_alternates: true,
report_associated: true,
report_events: true,
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w[1..], b"[106:74;2:3u");
}
}
#[test]
fn kitty_alternates_omit_control_characters() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
key: Key::Delete,
mods: Mods { ..Mods::default() },
utf8: "\u{7f}",
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
report_alternates: true,
report_all: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[3~");
}
}
#[test]
fn kitty_enter_with_utf8_dead_key_state() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
key: Key::Enter,
utf8: "A",
unshifted_codepoint: 0x0D,
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
report_alternates: true,
report_all: true,
..KittyFlags::DISABLED
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"A");
}
}
#[test]
fn kitty_keypad_number() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
key: Key::Numpad1,
mods: Mods { ..Mods::default() },
utf8: "1",
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
report_events: true,
report_alternates: true,
report_all: true,
report_associated: true,
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w[1..], b"[57400;;49u");
}
}
#[test]
fn kitty_backspace_with_utf8_dead_key_state() {
{
let mut w = Vec::new();
kitty(
&mut w,
&KeyEvent {
key: Key::Backspace,
utf8: "A",
unshifted_codepoint: 0x0D,
..KeyEvent::default()
},
&Options {
kitty_flags: KittyFlags {
disambiguate: true,
report_events: true,
report_alternates: true,
report_all: true,
report_associated: true,
},
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"");
}
}
#[test]
fn legacy_backspace_with_utf8_dead_key_state() {
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::Backspace,
utf8: "A",
unshifted_codepoint: 0x0D,
..KeyEvent::default()
},
&Options {
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"");
}
}
#[test]
fn legacy_enter_with_utf8_dead_key_state() {
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::Enter,
utf8: "A",
unshifted_codepoint: 0x0D,
..KeyEvent::default()
},
&Options {
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"A");
}
}
#[test]
fn legacy_esc_with_utf8_dead_key_state() {
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::Escape,
utf8: "A",
unshifted_codepoint: 0x0D,
..KeyEvent::default()
},
&Options {
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"A");
}
}
#[test]
fn legacy_ctrl_shift_minus_underscore_on_us() {
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::Minus,
mods: Mods {
ctrl: true,
shift: true,
..Mods::default()
},
utf8: "_",
..KeyEvent::default()
},
&Options {
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1f");
}
}
#[test]
fn legacy_ctrl_alt_c() {
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::KeyC,
mods: Mods {
ctrl: true,
alt: true,
..Mods::default()
},
utf8: "c",
..KeyEvent::default()
},
&Options {
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b\x03");
}
}
#[test]
fn legacy_alt_c() {
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::KeyC,
utf8: "c",
mods: Mods {
alt: true,
..Mods::default()
},
..KeyEvent::default()
},
&Options {
alt_esc_prefix: true,
macos_option_as_alt: OptionAsAlt::True,
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1bc");
}
}
#[test]
fn legacy_alt_e_only_unshifted() {
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::KeyE,
unshifted_codepoint: u32::from('e'),
mods: Mods {
alt: true,
..Mods::default()
},
..KeyEvent::default()
},
&Options {
alt_esc_prefix: true,
macos_option_as_alt: OptionAsAlt::True,
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1be");
}
}
#[test]
fn legacy_alt() {
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::KeyF,
utf8: "\u{444}",
mods: Mods {
alt: true,
..Mods::default()
},
..KeyEvent::default()
},
&Options {
alt_esc_prefix: true,
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\xd1\x84");
}
}
#[test]
fn legacy_ctrl_c() {
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::KeyC,
mods: Mods {
ctrl: true,
..Mods::default()
},
utf8: "c",
..KeyEvent::default()
},
&Options {
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x03");
}
}
#[test]
fn legacy_ctrl_space() {
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::Space,
mods: Mods {
ctrl: true,
..Mods::default()
},
utf8: " ",
..KeyEvent::default()
},
&Options {
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x00");
}
}
#[test]
fn legacy_ctrl_shift_backspace() {
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::Backspace,
mods: Mods {
ctrl: true,
shift: true,
..Mods::default()
},
..KeyEvent::default()
},
&Options {
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x08");
}
}
#[test]
fn legacy_ctrl_shift_char_with_modify_other_state_2() {
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::KeyH,
mods: Mods {
ctrl: true,
shift: true,
..Mods::default()
},
utf8: "H",
..KeyEvent::default()
},
&Options {
modify_other_keys_state_2: true,
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[27;6;72~");
}
}
#[test]
fn legacy_ctrl_shift_char_with_modify_other_state_2_and_consumed_mods() {
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::KeyH,
mods: Mods {
ctrl: true,
shift: true,
..Mods::default()
},
consumed_mods: Mods {
shift: true,
..Mods::default()
},
utf8: "H",
..KeyEvent::default()
},
&Options {
modify_other_keys_state_2: true,
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[27;6;72~");
}
}
#[test]
fn legacy_alt_digit_with_modify_other_state_2() {
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::Digit8,
mods: Mods {
alt: true,
..Mods::default()
},
consumed_mods: Mods { ..Mods::default() },
utf8: "8",
..KeyEvent::default()
},
&Options {
modify_other_keys_state_2: true,
macos_option_as_alt: OptionAsAlt::True,
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[27;3;56~");
}
}
#[test]
fn legacy_fixterm_awkward_letters() {
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::KeyI,
mods: Mods {
ctrl: true,
..Mods::default()
},
utf8: "i",
..KeyEvent::default()
},
&Options {
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[105;5u");
}
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::KeyM,
mods: Mods {
ctrl: true,
..Mods::default()
},
utf8: "m",
..KeyEvent::default()
},
&Options {
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[109;5u");
}
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::BracketLeft,
mods: Mods {
ctrl: true,
..Mods::default()
},
utf8: "[",
..KeyEvent::default()
},
&Options {
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[91;5u");
}
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::Digit2,
mods: Mods {
ctrl: true,
shift: true,
..Mods::default()
},
utf8: "@",
unshifted_codepoint: u32::from('2'),
..KeyEvent::default()
},
&Options {
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[64;5u");
}
}
#[test]
fn legacy_ctrl_shift_letter_ascii() {
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::KeyM,
mods: Mods {
ctrl: true,
shift: true,
..Mods::default()
},
utf8: "M",
unshifted_codepoint: u32::from('m'),
..KeyEvent::default()
},
&Options {
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[109;6u");
}
}
#[test]
fn legacy_shift_function_key_should_use_all_mods() {
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::ArrowUp,
mods: Mods {
shift: true,
..Mods::default()
},
consumed_mods: Mods {
shift: true,
..Mods::default()
},
..KeyEvent::default()
},
&Options {
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[1;2A");
}
}
#[test]
fn legacy_keypad_enter() {
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::NumpadEnter,
mods: Mods { ..Mods::default() },
consumed_mods: Mods { ..Mods::default() },
..KeyEvent::default()
},
&Options {
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x0d");
}
}
#[test]
fn legacy_keypad_1() {
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::Numpad1,
mods: Mods { ..Mods::default() },
consumed_mods: Mods { ..Mods::default() },
utf8: "1",
..KeyEvent::default()
},
&Options {
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"1");
}
}
#[test]
fn legacy_keypad_1_with_application_keypad() {
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::Numpad1,
mods: Mods { ..Mods::default() },
consumed_mods: Mods { ..Mods::default() },
utf8: "1",
..KeyEvent::default()
},
&Options {
keypad_key_application: true,
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1bOq");
}
}
#[test]
fn legacy_keypad_1_with_application_keypad_and_numlock() {
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::Numpad1,
mods: Mods {
num_lock: true,
..Mods::default()
},
consumed_mods: Mods { ..Mods::default() },
utf8: "1",
..KeyEvent::default()
},
&Options {
keypad_key_application: true,
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1bOq");
}
}
#[test]
fn legacy_keypad_1_with_application_keypad_and_numlock_ignore() {
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::Numpad1,
mods: Mods {
num_lock: false,
..Mods::default()
},
consumed_mods: Mods { ..Mods::default() },
utf8: "1",
..KeyEvent::default()
},
&Options {
keypad_key_application: true,
ignore_keypad_with_numlock: true,
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"1");
}
}
#[test]
fn legacy_f1() {
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::F1,
mods: Mods {
ctrl: true,
..Mods::default()
},
consumed_mods: Mods { ..Mods::default() },
..KeyEvent::default()
},
&Options {
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[1;5P");
}
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::F2,
mods: Mods {
ctrl: true,
..Mods::default()
},
consumed_mods: Mods { ..Mods::default() },
..KeyEvent::default()
},
&Options {
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[1;5Q");
}
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::F3,
mods: Mods {
ctrl: true,
..Mods::default()
},
consumed_mods: Mods { ..Mods::default() },
..KeyEvent::default()
},
&Options {
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[13;5~");
}
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::F4,
mods: Mods {
ctrl: true,
..Mods::default()
},
consumed_mods: Mods { ..Mods::default() },
..KeyEvent::default()
},
&Options {
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[1;5S");
}
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::F5,
mods: Mods {
ctrl: true,
..Mods::default()
},
consumed_mods: Mods { ..Mods::default() },
..KeyEvent::default()
},
&Options {
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[15;5~");
}
}
#[test]
fn legacy_left_shift_tab() {
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::Tab,
mods: Mods {
shift: true,
sides: Side {
shift: ModSide::Left,
..Side::default()
},
..Mods::default()
},
..KeyEvent::default()
},
&Options {
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[Z");
}
}
#[test]
fn legacy_right_shift_tab() {
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::Tab,
mods: Mods {
shift: true,
sides: Side {
shift: ModSide::Right,
..Side::default()
},
..Mods::default()
},
..KeyEvent::default()
},
&Options {
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x1b[Z");
}
}
#[test]
fn legacy_hu_layout_ctrl_sends_proper_codepoint() {
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::BracketLeft,
mods: Mods {
ctrl: true,
..Mods::default()
},
utf8: "\u{151}",
unshifted_codepoint: 337,
..KeyEvent::default()
},
&Options {
..Options::default()
},
)
.unwrap();
assert_eq!(&w[1..], b"[337;5u");
}
}
#[test]
fn legacy_backspace_with_del_utf8() {
{
let mut w = Vec::new();
legacy(
&mut w,
&KeyEvent {
key: Key::Backspace,
utf8: "\u{7f}",
unshifted_codepoint: 0x08,
..KeyEvent::default()
},
&Options {
..Options::default()
},
)
.unwrap();
assert_eq!(&w, b"\x7f");
}
}
fn enc(seq: KittySequence) -> Vec<u8> {
let mut w = Vec::new();
seq.encode(&mut w).unwrap();
w
}
fn seq(key: u32, final_: u8) -> KittySequence<'static> {
KittySequence {
key,
final_,
mods: KittyMods::default(),
event: KittyEvent::None,
alternates: [None, None],
text: "",
}
}
fn ctrl() -> Mods {
Mods {
ctrl: true,
..Mods::default()
}
}
#[test]
fn csi_u_mods_modifier_sequence_values() {
let c = |shift, alt, ctrl| CsiUMods { shift, alt, ctrl }.seq_int();
assert_eq!(1, c(false, false, false));
assert_eq!(2, c(true, false, false));
assert_eq!(3, c(false, true, false));
assert_eq!(5, c(false, false, true));
assert_eq!(4, c(true, true, false));
assert_eq!(6, c(true, false, true));
assert_eq!(7, c(false, true, true));
assert_eq!(8, c(true, true, true));
}
#[test]
fn kitty_mods_modifier_sequence_values() {
let c = |shift, alt, ctrl| {
KittyMods {
shift,
alt,
ctrl,
..KittyMods::default()
}
.seq_int()
};
assert_eq!(1, c(false, false, false));
assert_eq!(2, c(true, false, false));
assert_eq!(3, c(false, true, false));
assert_eq!(5, c(false, false, true));
assert_eq!(4, c(true, true, false));
assert_eq!(6, c(true, false, true));
assert_eq!(7, c(false, true, true));
assert_eq!(8, c(true, true, true));
}
#[test]
fn kitty_sequence_backspace() {
assert_eq!(enc(seq(127, b'u')), b"\x1B[127u");
assert_eq!(
enc(KittySequence {
event: KittyEvent::Release,
..seq(127, b'u')
}),
b"\x1B[127;1:3u"
);
assert_eq!(
enc(KittySequence {
mods: KittyMods {
shift: true,
..KittyMods::default()
},
..seq(127, b'u')
}),
b"\x1B[127;2u"
);
}
#[test]
fn kitty_sequence_text() {
assert_eq!(
enc(KittySequence {
text: "A",
..seq(127, b'u')
}),
b"\x1B[127;;65u"
);
assert_eq!(
enc(KittySequence {
event: KittyEvent::Release,
text: "A",
..seq(127, b'u')
}),
b"\x1B[127;1:3;65u"
);
assert_eq!(
enc(KittySequence {
mods: KittyMods {
shift: true,
..KittyMods::default()
},
text: "A",
..seq(127, b'u')
}),
b"\x1B[127;2;65u"
);
}
#[test]
fn kitty_sequence_text_with_control_characters() {
assert_eq!(
enc(KittySequence {
text: "\n",
..seq(127, b'u')
}),
b"\x1b[127u"
);
assert_eq!(
enc(KittySequence {
text: "A\n",
..seq(127, b'u')
}),
b"\x1b[127;;65u"
);
}
#[test]
fn kitty_sequence_special_no_mods() {
assert_eq!(enc(seq(1, b'A')), b"\x1B[A");
}
#[test]
fn kitty_sequence_special_mods_only() {
assert_eq!(
enc(KittySequence {
mods: KittyMods {
shift: true,
..KittyMods::default()
},
..seq(1, b'A')
}),
b"\x1B[1;2A"
);
}
#[test]
fn kitty_sequence_special_mods_and_event() {
assert_eq!(
enc(KittySequence {
event: KittyEvent::Release,
mods: KittyMods {
shift: true,
..KittyMods::default()
},
..seq(1, b'A')
}),
b"\x1B[1;2:3A"
);
}
fn right_ctrl() -> Mods {
Mods {
ctrl: true,
sides: Side {
ctrl: ModSide::Right,
..Side::default()
},
..Mods::default()
}
}
#[test]
fn ctrlseq_normal_ctrl_c() {
assert_eq!(
ctrl_seq(Key::Unidentified, "c", 'c' as u32, ctrl()),
Some(0x03)
);
}
#[test]
fn ctrlseq_normal_ctrl_c_right_control() {
assert_eq!(
ctrl_seq(Key::Unidentified, "c", 'c' as u32, right_ctrl()),
Some(0x03)
);
}
#[test]
fn ctrlseq_alt_should_be_allowed() {
let mods = Mods {
alt: true,
ctrl: true,
..Mods::default()
};
assert_eq!(
ctrl_seq(Key::Unidentified, "c", 'c' as u32, mods),
Some(0x03)
);
}
#[test]
fn ctrlseq_no_ctrl_does_nothing() {
assert_eq!(
ctrl_seq(Key::Unidentified, "c", 'c' as u32, Mods::default()),
None
);
}
#[test]
fn ctrlseq_shifted_non_character() {
let mods = Mods {
ctrl: true,
shift: true,
..Mods::default()
};
assert_eq!(
ctrl_seq(Key::Unidentified, "_", '-' as u32, mods),
Some(0x1F)
);
}
#[test]
fn ctrlseq_caps_ascii_letter() {
let mods = Mods {
ctrl: true,
caps_lock: true,
..Mods::default()
};
assert_eq!(
ctrl_seq(Key::Unidentified, "C", 'c' as u32, mods),
Some(0x03)
);
}
#[test]
fn ctrlseq_shift_does_not_generate_ctrl_seq() {
let shift = Mods {
shift: true,
..Mods::default()
};
assert_eq!(ctrl_seq(Key::Unidentified, "C", 'c' as u32, shift), None);
let both = Mods {
shift: true,
ctrl: true,
..Mods::default()
};
assert_eq!(ctrl_seq(Key::Unidentified, "C", 'c' as u32, both), None);
}
#[test]
fn ctrlseq_russian_ctrl_c() {
assert_eq!(ctrl_seq(Key::KeyC, "с", 0x0441, ctrl()), Some(0x03));
}
#[test]
fn ctrlseq_russian_shifted_ctrl_c() {
let mods = Mods {
ctrl: true,
shift: true,
..Mods::default()
};
assert_eq!(ctrl_seq(Key::KeyC, "с", 0x0441, mods), None);
}
#[test]
fn ctrlseq_russian_alt_ctrl_c() {
let mods = Mods {
ctrl: true,
alt: true,
..Mods::default()
};
assert_eq!(ctrl_seq(Key::KeyC, "с", 0x0441, mods), Some(0x03));
}
#[test]
fn ctrlseq_right_ctrl_c() {
assert_eq!(
ctrl_seq(Key::KeyC, "с", 'c' as u32, right_ctrl()),
Some(0x03)
);
}
}
+869
View File
@@ -0,0 +1,869 @@
//! Key modifiers (ZIG/src/input/key_mods.zig).
use crate::config::OptionAsAlt;
/// Aliases for modifier names.
pub const ALIAS: &[(&str, Mod)] = &[
("cmd", Mod::Super),
("command", Mod::Super),
("opt", Mod::Alt),
("option", Mod::Alt),
("control", Mod::Ctrl),
];
/// Single modifier
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Mod {
Shift,
Ctrl,
Alt,
Super,
}
impl Mod {
fn from_name(s: &str) -> Option<Mod> {
match s {
"shift" => Some(Mod::Shift),
"ctrl" => Some(Mod::Ctrl),
"alt" => Some(Mod::Alt),
"super" => Some(Mod::Super),
_ => None,
}
}
}
/// Which side of the keyboard a modifier is on (`Mod.Side`).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[repr(u8)]
pub enum ModSide {
#[default]
Left = 0,
Right = 1,
}
impl ModSide {
fn from_name(s: &str) -> Option<ModSide> {
match s {
"left" => Some(ModSide::Left),
"right" => Some(ModSide::Right),
_ => None,
}
}
}
/// The standard modifier keys only. Does not include the lock keys,
/// only standard bindable keys. (`Mods.Keys`, a packed u4)
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct Keys {
pub shift: bool,
pub ctrl: bool,
pub alt: bool,
pub super_: bool,
}
impl Keys {
pub fn int(self) -> u8 {
u8::from(self.shift)
| u8::from(self.ctrl) << 1
| u8::from(self.alt) << 2
| u8::from(self.super_) << 3
}
pub fn from_int(v: u8) -> Keys {
Keys {
shift: v & 1 != 0,
ctrl: v & 2 != 0,
alt: v & 4 != 0,
super_: v & 8 != 0,
}
}
}
/// Tracks the side that is active for any given modifier. Note
/// that this doesn't confirm a modifier is pressed; you must check
/// the bool for that in addition to this. (`Mods.Side`, a packed u4)
///
/// Not all platforms support this, check apprt for more info.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct Side {
pub shift: ModSide,
pub ctrl: ModSide,
pub alt: ModSide,
pub super_: ModSide,
}
impl Side {
pub fn int(self) -> u8 {
self.shift as u8 | (self.ctrl as u8) << 1 | (self.alt as u8) << 2 | (self.super_ as u8) << 3
}
pub fn from_int(v: u8) -> Side {
let f = |b: u8| {
if v & b != 0 {
ModSide::Right
} else {
ModSide::Left
}
};
Side {
shift: f(1),
ctrl: f(2),
alt: f(4),
super_: f(8),
}
}
}
/// A bitmask for all key modifiers.
///
/// NOTE: Zig's field `super` is named `super_` here (`super` is a Rust
/// keyword). The packed integer layout (u16) is available via `int()`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct Mods {
pub shift: bool,
pub ctrl: bool,
pub alt: bool,
pub super_: bool,
pub caps_lock: bool,
pub num_lock: bool,
pub sides: Side,
}
impl Mods {
/// The mask that has all the side bits set.
pub const SIDE_MASK: Mods = Mods {
shift: false,
ctrl: false,
alt: false,
super_: false,
caps_lock: false,
num_lock: false,
sides: Side {
shift: ModSide::Right,
ctrl: ModSide::Right,
alt: ModSide::Right,
super_: ModSide::Right,
},
};
/// Integer value of this struct.
pub fn int(self) -> u16 {
u16::from(self.keys().int())
| u16::from(self.caps_lock) << 4
| u16::from(self.num_lock) << 5
| u16::from(self.sides.int()) << 6
}
pub fn from_int(v: u16) -> Mods {
Mods {
shift: v & 1 != 0,
ctrl: v & 2 != 0,
alt: v & 4 != 0,
super_: v & 8 != 0,
caps_lock: v & 16 != 0,
num_lock: v & 32 != 0,
sides: Side::from_int(((v >> 6) & 0xF) as u8),
}
}
/// Returns true if no modifiers are set.
pub fn empty(self) -> bool {
self.int() == 0
}
/// Returns true if two mods are equal.
pub fn equal(self, other: Mods) -> bool {
self.int() == other.int()
}
/// Returns only the keys.
pub fn keys(self) -> Keys {
Keys {
shift: self.shift,
ctrl: self.ctrl,
alt: self.alt,
super_: self.super_,
}
}
/// Return mods that are only relevant for bindings.
pub fn binding(self) -> Mods {
Mods {
shift: self.shift,
ctrl: self.ctrl,
alt: self.alt,
super_: self.super_,
..Mods::default()
}
}
/// Perform `self &~ other` to remove the other mods from self.
pub fn unset(self, other: Mods) -> Mods {
Mods::from_int(self.int() & !other.int())
}
/// Returns the mods without locks set.
pub fn without_locks(self) -> Mods {
let mut copy = self;
copy.caps_lock = false;
copy.num_lock = false;
copy
}
/// Return the mods to use for key translation. This handles settings
/// like macos-option-as-alt. The translation mods should be used for
/// translation but never sent back in for the key callback.
///
/// Only macOS changes anything here; this port targets Linux so the
/// mods are returned unchanged.
pub fn translation(self, _option_as_alt: OptionAsAlt) -> Mods {
self
}
/// Checks to see if super is on (MacOS) or ctrl.
pub fn ctrl_or_super(self) -> bool {
// Non-darwin target.
self.ctrl
}
}
/// Modifier remapping. See `key-remap` in Config.zig for detailed docs.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct RemapSet {
/// Available mappings (insertion ordered, like an array hash map).
pub map: Vec<(Mods, Mods)>,
/// The mask of remapped modifiers that can be used to quickly
/// check if some input mods need remapping.
pub mask: Mask,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum ParseError {
#[error("missing assignment")]
MissingAssignment,
#[error("invalid modifier")]
InvalidMod,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum ParseCliError {
#[error("invalid value")]
InvalidValue,
}
impl RemapSet {
pub const EMPTY: RemapSet = RemapSet {
map: Vec::new(),
mask: Mask {
keys: Keys {
shift: false,
ctrl: false,
alt: false,
super_: false,
},
left_sides: Side {
shift: ModSide::Left,
ctrl: ModSide::Left,
alt: ModSide::Left,
super_: ModSide::Left,
},
right_sides: Side {
shift: ModSide::Left,
ctrl: ModSide::Left,
alt: ModSide::Left,
super_: ModSide::Left,
},
},
};
fn put(&mut self, from: Mods, to: Mods) {
if let Some(e) = self.map.iter_mut().find(|e| e.0 == from) {
e.1 = to;
} else {
self.map.push((from, to));
}
}
/// Parse from CLI input. Required by Config.
pub fn parse_cli(&mut self, input: Option<&str>) -> Result<(), ParseCliError> {
let value = input.unwrap_or("");
// Empty value resets the set
if value.is_empty() {
self.map.clear();
self.mask = Mask::default();
return Ok(());
}
self.parse(value).map_err(|_| ParseCliError::InvalidValue)
}
/// Parse a modifier remap and add it to the set.
pub fn parse(&mut self, input: &str) -> Result<(), ParseError> {
// Find the assignment point ('=')
let eql_idx = input.find('=').ok_or(ParseError::MissingAssignment)?;
// The to side defaults to "left" if no explicit side is given.
// This is because this is the default unsided value provided by
// the apprts in the current Mods layout.
let to = {
let raw = parse_mod(&input[eql_idx + 1..])?;
init_mods(raw.0, raw.1.unwrap_or(ModSide::Left))
};
// The from side, if sided, is easy and we put it directly into
// the map.
let from_raw = parse_mod(&input[..eql_idx])?;
if let Some(from_side) = from_raw.1 {
let from = init_mods(from_raw.0, from_side);
self.put(from, to);
self.mask.update(from);
return Ok(());
}
// We need to do some combinatorial explosion here for unsided
// from in order to assign all possible sides.
let from_left = init_mods(from_raw.0, ModSide::Left);
let from_right = init_mods(from_raw.0, ModSide::Right);
self.put(from_left, to);
self.put(from_right, to);
self.mask.update(from_left);
self.mask.update(from_right);
Ok(())
}
/// Must be called prior to any remappings so that the mapping
/// is sorted properly. Otherwise, you will get invalid results.
pub fn finalize(&mut self) {
// Mods with any right sides prioritize. This mirrors the Zig
// comparator (which ignores `b`) driven by an insertion sort.
let side_mask = Mods::SIDE_MASK.int();
let less = |a: &Mods| a.int() & side_mask != 0;
for i in 1..self.map.len() {
let mut j = i;
while j > 0 && less(&self.map[j].0) {
self.map.swap(j, j - 1);
j -= 1;
}
}
}
/// Compare if two RemapSets are equal. Required by Config.
pub fn equal(&self, other: &RemapSet) -> bool {
if self.map.len() != other.map.len() {
return false;
}
self.map.iter().all(|(k, v)| {
other
.map
.iter()
.find(|(ok, _)| ok == k)
.is_some_and(|(_, ov)| v.equal(*ov))
})
}
/// Used by Formatter. Required by Config. Returns the values of each
/// `key-remap = ...` line to print. An empty vec means the formatter
/// should emit a single empty entry (`key-remap = `).
pub fn format_entries(&self) -> Vec<String> {
self.map
.iter()
.map(|(from, to)| {
let mut s = String::new();
format_mod(&mut s, *from);
s.push('=');
format_mod(&mut s, *to);
s
})
.collect()
}
/// Returns true if the given mods need remapping.
pub fn is_remapped(&self, mods: Mods) -> bool {
self.mask.matches(mods)
}
/// Apply a remap to the given mods.
pub fn apply(&self, mods: Mods) -> Mods {
if !self.is_remapped(mods) {
return mods;
}
let mods_binding = mods.keys().int();
let mods_sides = mods.sides.int();
for (from, to) in &self.map {
let from_binding = from.keys().int();
if mods_binding & from_binding != from_binding {
continue;
}
let from_sides = from.sides.int();
if (mods_sides ^ from_sides) & from_binding != 0 {
continue;
}
let mut mods_int = mods.int();
mods_int &= !from.int();
mods_int |= to.int();
return Mods::from_int(mods_int);
}
unreachable!("is_remapped true but no mapping matched")
}
}
fn format_mod(out: &mut String, mods: Mods) {
// Check which mod is set and format it with optional side prefix
let list = [
("shift", mods.shift, mods.sides.shift),
("ctrl", mods.ctrl, mods.sides.ctrl),
("alt", mods.alt, mods.sides.alt),
("super", mods.super_, mods.sides.super_),
];
for (name, set, side) in list {
if set {
if side == ModSide::Right {
out.push_str("right_");
} else {
// Only write left_ if we need to distinguish
// For now, always write left_ if it's a sided mapping
out.push_str("left_");
}
out.push_str(name);
return;
}
}
}
/// Parses a single mode in a single remapping string. E.g.
/// `ctrl` or `left_shift`.
fn parse_mod(input: &str) -> Result<(Mod, Option<ModSide>), ParseError> {
let (side_str, mod_str) = match input.find('_') {
Some(idx) => (&input[..idx], &input[idx + 1..]),
None => ("", input),
};
let m = match Mod::from_name(mod_str) {
Some(m) => m,
None => ALIAS
.iter()
.find(|p| p.0 == mod_str)
.map(|p| p.1)
.ok_or(ParseError::InvalidMod)?,
};
let side = if side_str.is_empty() {
None
} else {
Some(ModSide::from_name(side_str).ok_or(ParseError::InvalidMod)?)
};
Ok((m, side))
}
fn init_mods(m: Mod, side: ModSide) -> Mods {
let mut mods = Mods::default();
match m {
Mod::Shift => {
mods.shift = true;
mods.sides.shift = side;
}
Mod::Ctrl => {
mods.ctrl = true;
mods.sides.ctrl = side;
}
Mod::Alt => {
mods.alt = true;
mods.sides.alt = side;
}
Mod::Super => {
mods.super_ = true;
mods.sides.super_ = side;
}
}
mods
}
/// Tracks which modifier keys and sides have remappings registered.
/// Used as a fast pre-check before doing expensive map lookups.
///
/// The mask uses separate tracking for left and right sides because
/// remappings can be side-specific (e.g., only remap left_ctrl).
///
/// Note: `left_sides` uses inverted logic where 1 means "left is remapped"
/// even though `ModSide::Left = 0`. This allows efficient bitwise matching
/// since we can AND directly with the side bits.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Mask {
/// Which modifier keys (shift/ctrl/alt/super) have any remapping.
pub keys: Keys,
/// Which modifiers have left-side remappings (inverted: 1 = left remapped).
pub left_sides: Side,
/// Which modifiers have right-side remappings (1 = right remapped).
pub right_sides: Side,
}
impl Mask {
/// Adds a modifier to the mask, marking it as having a remapping.
pub fn update(&mut self, mods: Mods) {
let keys_int = mods.keys().int();
// OR the new keys into our existing keys mask.
self.keys = Keys::from_int(self.keys.int() | keys_int);
// Both Keys and Side are u4 with matching bit positions.
// This lets us use keys_int to select which side bits to update.
let sides = mods.sides.int();
let left_int = self.left_sides.int();
let right_int = self.right_sides.int();
// Update left_sides: set bit if this key is active AND side is left.
// Since Left=0, we invert sides so left becomes 1.
self.left_sides = Side::from_int(left_int | (keys_int & !sides));
// Update right_sides: set bit if this key is active AND side is right.
self.right_sides = Side::from_int(right_int | (keys_int & sides));
}
/// Returns true if the given mods match any remapping in this mask.
/// This is a fast check to avoid expensive map lookups when no
/// remapping could possibly apply.
///
/// Checks both that the modifier key is remapped AND that the
/// specific side (left/right) being pressed has a remapping.
pub fn matches(&self, mods: Mods) -> bool {
// Find which pressed keys have remappings registered.
let active = mods.keys().int() & self.keys.int();
if active == 0 {
return false;
}
// Check if the pressed side matches a remapped side.
let sides = mods.sides.int();
let left_int = self.left_sides.int();
let right_int = self.right_sides.int();
let side_match = (!sides & left_int) | (sides & right_int);
// Final check: is any active (pressed + remapped) key also side-matched?
(active & side_match) != 0
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
fn m(f: impl FnOnce(&mut Mods)) -> Mods {
let mut x = Mods::default();
f(&mut x);
x
}
// For our own understanding
#[test]
fn mods_layout() {
assert_eq!(Mods::default().int(), 0);
assert_eq!(
Mods {
shift: true,
..Default::default()
}
.int(),
1
);
assert_eq!(Mods::from_int(Mods::SIDE_MASK.int()), Mods::SIDE_MASK);
}
#[test]
fn translation_macos_option_as_alt() {
// Darwin only upstream; on Linux it is the identity.
let mods = Mods {
alt: true,
..Default::default()
};
assert_eq!(mods.translation(OptionAsAlt::True), mods);
}
fn left(k: &str) -> Mods {
match k {
"ctrl" => m(|x| x.ctrl = true),
"alt" => m(|x| x.alt = true),
"shift" => m(|x| x.shift = true),
_ => m(|x| x.super_ = true),
}
}
#[test]
fn remap_set_unsided_remap_creates_both_left_and_right_mappings() {
let mut set = RemapSet::default();
set.parse("ctrl=super").unwrap();
set.finalize();
let want = Mods {
super_: true,
sides: Side {
super_: ModSide::Left,
..Default::default()
},
..Default::default()
};
assert_eq!(
want,
set.apply(Mods {
ctrl: true,
sides: Side {
ctrl: ModSide::Left,
..Default::default()
},
..Default::default()
})
);
assert_eq!(
want,
set.apply(Mods {
ctrl: true,
sides: Side {
ctrl: ModSide::Right,
..Default::default()
},
..Default::default()
})
);
}
fn sided(name: &str, side: ModSide) -> Mods {
init_mods(
match name {
"shift" => Mod::Shift,
"ctrl" => Mod::Ctrl,
"alt" => Mod::Alt,
_ => Mod::Super,
},
side,
)
}
#[test]
fn remap_set_sided_from_only_maps_that_side() {
let mut set = RemapSet::default();
set.parse("left_alt=ctrl").unwrap();
set.finalize();
assert_eq!(
sided("ctrl", ModSide::Left),
set.apply(sided("alt", ModSide::Left))
);
let right_alt = sided("alt", ModSide::Right);
assert_eq!(right_alt, set.apply(right_alt));
}
#[test]
fn remap_set_sided_to() {
let mut set = RemapSet::default();
set.parse("ctrl=right_super").unwrap();
set.finalize();
assert_eq!(
sided("super", ModSide::Right),
set.apply(sided("ctrl", ModSide::Left))
);
}
#[test]
fn remap_set_both_sides_specified() {
let mut set = RemapSet::default();
set.parse("left_shift=right_ctrl").unwrap();
set.finalize();
assert_eq!(
sided("ctrl", ModSide::Right),
set.apply(sided("shift", ModSide::Left))
);
}
#[test]
fn remap_set_multiple_parses_accumulate() {
let mut set = RemapSet::default();
set.parse("left_ctrl=super").unwrap();
set.parse("left_alt=ctrl").unwrap();
set.finalize();
assert_eq!(
sided("super", ModSide::Left),
set.apply(sided("ctrl", ModSide::Left))
);
assert_eq!(
sided("ctrl", ModSide::Left),
set.apply(sided("alt", ModSide::Left))
);
}
#[test]
fn remap_set_error_on_missing_assignment() {
let mut set = RemapSet::default();
assert_eq!(set.parse("ctrl"), Err(ParseError::MissingAssignment));
assert_eq!(set.parse(""), Err(ParseError::MissingAssignment));
}
#[test]
fn remap_set_error_on_invalid_modifier() {
let mut set = RemapSet::default();
assert_eq!(set.parse("invalid=ctrl"), Err(ParseError::InvalidMod));
assert_eq!(set.parse("ctrl=invalid"), Err(ParseError::InvalidMod));
assert_eq!(set.parse("middle_ctrl=super"), Err(ParseError::InvalidMod));
}
#[test]
fn remap_set_is_remapped_checks_mask() {
let mut set = RemapSet::default();
set.parse("ctrl=super").unwrap();
set.finalize();
assert!(set.is_remapped(left("ctrl")));
assert!(!set.is_remapped(left("alt")));
assert!(!set.is_remapped(left("shift")));
}
#[test]
fn remap_set_clone_creates_independent_copy() {
let mut set = RemapSet::default();
set.parse("ctrl=super").unwrap();
set.finalize();
let cloned = set.clone();
assert!(set.equal(&cloned));
assert!(cloned.is_remapped(left("ctrl")));
}
#[test]
fn remap_set_equal_compares_correctly() {
let mut set1 = RemapSet::default();
let mut set2 = RemapSet::default();
assert!(set1.equal(&set2));
set1.parse("ctrl=super").unwrap();
assert!(!set1.equal(&set2));
set2.parse("ctrl=super").unwrap();
assert!(set1.equal(&set2));
set1.parse("alt=shift").unwrap();
assert!(!set1.equal(&set2));
}
#[test]
fn remap_set_parse_cli_basic() {
let mut set = RemapSet::default();
set.parse_cli(Some("ctrl=super")).unwrap();
assert_eq!(set.map.len(), 2);
}
#[test]
fn remap_set_parse_cli_empty_clears() {
let mut set = RemapSet::default();
set.parse_cli(Some("ctrl=super")).unwrap();
assert_eq!(set.map.len(), 2);
set.parse_cli(Some("")).unwrap();
assert_eq!(set.map.len(), 0);
}
#[test]
fn remap_set_parse_cli_invalid() {
let mut set = RemapSet::default();
assert_eq!(
set.parse_cli(Some("foo=bar")),
Err(ParseCliError::InvalidValue)
);
assert_eq!(
set.parse_cli(Some("ctrl")),
Err(ParseCliError::InvalidValue)
);
}
#[test]
fn remap_set_parse_aliased_modifiers() {
let mut set = RemapSet::default();
set.parse("cmd=ctrl").unwrap();
set.finalize();
assert_eq!(
sided("ctrl", ModSide::Left),
set.apply(sided("super", ModSide::Left))
);
}
#[test]
fn remap_set_parse_aliased_modifiers_command() {
let mut set = RemapSet::default();
set.parse("command=alt").unwrap();
set.finalize();
assert_eq!(
sided("alt", ModSide::Left),
set.apply(sided("super", ModSide::Left))
);
}
#[test]
fn remap_set_parse_aliased_modifiers_opt_and_option() {
let mut set = RemapSet::default();
set.parse("opt=super").unwrap();
set.finalize();
assert_eq!(
sided("super", ModSide::Left),
set.apply(sided("alt", ModSide::Left))
);
let mut set = RemapSet::default();
set.parse("option=shift").unwrap();
set.finalize();
assert_eq!(
sided("shift", ModSide::Left),
set.apply(sided("alt", ModSide::Left))
);
}
#[test]
fn remap_set_parse_aliased_modifiers_control() {
let mut set = RemapSet::default();
set.parse("control=super").unwrap();
set.finalize();
assert_eq!(
sided("super", ModSide::Left),
set.apply(sided("ctrl", ModSide::Left))
);
}
#[test]
fn remap_set_parse_aliased_modifiers_on_target_side() {
let mut set = RemapSet::default();
set.parse("alt=cmd").unwrap();
set.finalize();
assert_eq!(
sided("super", ModSide::Left),
set.apply(sided("alt", ModSide::Left))
);
}
#[test]
fn remap_set_format_entry_empty() {
// The `key-remap = \n` rendering is done by the config formatter;
// here an empty set yields no entries.
assert_eq!(RemapSet::default().format_entries(), Vec::<String>::new());
}
#[test]
fn remap_set_format_entry_single_sided() {
let mut set = RemapSet::default();
set.parse("left_ctrl=super").unwrap();
set.finalize();
assert_eq!(set.format_entries(), vec!["left_ctrl=left_super"]);
}
#[test]
fn remap_set_format_entry_unsided_creates_two_entries() {
let mut set = RemapSet::default();
set.parse("ctrl=super").unwrap();
set.finalize();
let w = set.format_entries();
assert!(w.contains(&"left_ctrl=left_super".to_string()));
assert!(w.contains(&"right_ctrl=left_super".to_string()));
}
#[test]
fn remap_set_format_entry_right_sided() {
let mut set = RemapSet::default();
set.parse("left_alt=right_ctrl").unwrap();
set.finalize();
assert_eq!(set.format_entries(), vec!["left_alt=right_ctrl"]);
}
}
+56
View File
@@ -0,0 +1,56 @@
//! Keyboard layouts (ZIG/src/input/keyboard.zig).
use crate::config::OptionAsAlt;
/// Keyboard layouts.
///
/// These aren't heavily used in Ghostty and having a fully comprehensive
/// list is not important. We only need to distinguish between a few
/// different layouts for some nice-to-have features, such as setting a default
/// value for "macos-option-as-alt".
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Layout {
/// Unknown, unmapped layout. Ghostty should not make any assumptions
/// about the layout of the keyboard.
Unknown,
// The remaining should be fairly self-explanatory:
UsStandard,
UsInternational,
}
impl Layout {
/// Map an Apple keyboard layout ID to a value in this enum. The layout
/// ID can be retrieved using Carbon's TIKeyboardLayoutGetInputSourceProperty
/// function.
///
/// Even though our layout supports "unknown", we return None if we don't
/// recognize the layout ID so callers can detect this scenario.
pub fn map_apple_id(id: &str) -> Option<Layout> {
match id {
"com.apple.keylayout.US" => Some(Layout::UsStandard),
"com.apple.keylayout.USInternational" => Some(Layout::UsInternational),
_ => None,
}
}
/// Returns the default macos-option-as-alt value for this layout.
///
/// We apply some heuristics to change the default based on the keyboard
/// layout if "macos-option-as-alt" is unset. We do this because on some
/// keyboard layouts such as US standard layouts, users generally expect
/// an input such as option-b to map to alt-b but macOS by default will
/// convert it to the codepoint "∫".
///
/// This behavior however is desired on international layout where the
/// option key is used for important, regularly used inputs.
pub fn detect_option_as_alt(self) -> OptionAsAlt {
match self {
// On US standard, the option key is typically used as alt
// and not as a modifier for other codepoints. For example,
// option-B = ∫ but usually the user wants alt-B.
Layout::UsStandard | Layout::UsInternational => OptionAsAlt::True,
Layout::Unknown => OptionAsAlt::False,
}
}
}
+697
View File
@@ -0,0 +1,697 @@
// Based on the Chromium source. The Chromium source code is governed by a
// BSD-style license that can be found in the LICENSE file.
// https://source.chromium.org/chromium/chromium/src/+/main:ui/events/keycodes/dom/dom_code_data.inc
//! Keycode table mapping USB/evdev/XKB/Windows/Mac codes to W3C DOM codes
//! (ZIG/src/input/keycodes.zig). The table is data-driven.
use std::sync::OnceLock;
use crate::key::Key;
/// Entry contains the USB code, native keycode, and W3C dom code for
/// the current platform.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Entry {
/// input key enum
pub key: Key,
/// USB HID usage code
pub usb: u32,
/// Native keycode
pub native: u32,
/// W3C DOM code, static memory
pub code: &'static str,
}
/// The full list of entries for the current platform (Linux: XKB codes).
pub fn entries() -> &'static [Entry] {
static ENTRIES: OnceLock<Vec<Entry>> = OnceLock::new();
ENTRIES.get_or_init(|| {
// Linux/FreeBSD use the xkb column (index 2).
RAW_ENTRIES
.iter()
.map(|raw| Entry {
key: code_to_key(raw.5).unwrap_or(Key::Unidentified),
usb: raw.0,
code: raw.5,
native: raw.2,
})
.collect()
})
}
/// A map from code to key. This isn't meant to be used at runtime
/// (though it could), so it isn't exported. It it used to build the
/// key value for Entry.
fn code_to_key(code: &str) -> Option<Key> {
CODE_TO_KEY.iter().find(|e| e.0 == code).map(|e| e.1)
}
const CODE_TO_KEY: &[(&str, Key)] = &[
("KeyA", Key::KeyA),
("KeyB", Key::KeyB),
("KeyC", Key::KeyC),
("KeyD", Key::KeyD),
("KeyE", Key::KeyE),
("KeyF", Key::KeyF),
("KeyG", Key::KeyG),
("KeyH", Key::KeyH),
("KeyI", Key::KeyI),
("KeyJ", Key::KeyJ),
("KeyK", Key::KeyK),
("KeyL", Key::KeyL),
("KeyM", Key::KeyM),
("KeyN", Key::KeyN),
("KeyO", Key::KeyO),
("KeyP", Key::KeyP),
("KeyQ", Key::KeyQ),
("KeyR", Key::KeyR),
("KeyS", Key::KeyS),
("KeyT", Key::KeyT),
("KeyU", Key::KeyU),
("KeyV", Key::KeyV),
("KeyW", Key::KeyW),
("KeyX", Key::KeyX),
("KeyY", Key::KeyY),
("KeyZ", Key::KeyZ),
("Digit1", Key::Digit1),
("Digit2", Key::Digit2),
("Digit3", Key::Digit3),
("Digit4", Key::Digit4),
("Digit5", Key::Digit5),
("Digit6", Key::Digit6),
("Digit7", Key::Digit7),
("Digit8", Key::Digit8),
("Digit9", Key::Digit9),
("Digit0", Key::Digit0),
("Enter", Key::Enter),
("Escape", Key::Escape),
("Backspace", Key::Backspace),
("Tab", Key::Tab),
("Space", Key::Space),
("Minus", Key::Minus),
("Equal", Key::Equal),
("BracketLeft", Key::BracketLeft),
("BracketRight", Key::BracketRight),
("Backslash", Key::Backslash),
("Semicolon", Key::Semicolon),
("Quote", Key::Quote),
("Backquote", Key::Backquote),
("Comma", Key::Comma),
("Period", Key::Period),
("Slash", Key::Slash),
("CapsLock", Key::CapsLock),
("F1", Key::F1),
("F2", Key::F2),
("F3", Key::F3),
("F4", Key::F4),
("F5", Key::F5),
("F6", Key::F6),
("F7", Key::F7),
("F8", Key::F8),
("F9", Key::F9),
("F10", Key::F10),
("F11", Key::F11),
("F12", Key::F12),
("F13", Key::F13),
("F14", Key::F14),
("F15", Key::F15),
("F16", Key::F16),
("F17", Key::F17),
("F18", Key::F18),
("F19", Key::F19),
("F20", Key::F20),
("F21", Key::F21),
("F22", Key::F22),
("F23", Key::F23),
("F24", Key::F24),
("PrintScreen", Key::PrintScreen),
("ScrollLock", Key::ScrollLock),
("Pause", Key::Pause),
("Insert", Key::Insert),
("Home", Key::Home),
("PageUp", Key::PageUp),
("Delete", Key::Delete),
("End", Key::End),
("Copy", Key::Copy),
("Cut", Key::Cut),
("Paste", Key::Paste),
("PageDown", Key::PageDown),
("ArrowRight", Key::ArrowRight),
("ArrowLeft", Key::ArrowLeft),
("ArrowDown", Key::ArrowDown),
("ArrowUp", Key::ArrowUp),
("NumLock", Key::NumLock),
("NumpadDivide", Key::NumpadDivide),
("NumpadMultiply", Key::NumpadMultiply),
("NumpadSubtract", Key::NumpadSubtract),
("NumpadAdd", Key::NumpadAdd),
("NumpadEnter", Key::NumpadEnter),
("Numpad1", Key::Numpad1),
("Numpad2", Key::Numpad2),
("Numpad3", Key::Numpad3),
("Numpad4", Key::Numpad4),
("Numpad5", Key::Numpad5),
("Numpad6", Key::Numpad6),
("Numpad7", Key::Numpad7),
("Numpad8", Key::Numpad8),
("Numpad9", Key::Numpad9),
("Numpad0", Key::Numpad0),
("NumpadDecimal", Key::NumpadDecimal),
("NumpadEqual", Key::NumpadEqual),
("ContextMenu", Key::ContextMenu),
("ControlLeft", Key::ControlLeft),
("ShiftLeft", Key::ShiftLeft),
("AltLeft", Key::AltLeft),
("MetaLeft", Key::MetaLeft),
("ControlRight", Key::ControlRight),
("ShiftRight", Key::ShiftRight),
("AltRight", Key::AltRight),
("MetaRight", Key::MetaRight),
];
/// The codes for the table from the Chromium data set. These are ALL the
/// codes, not just the ones that are supported by the current platform.
/// These are `pub` but you shouldn't use this because it uses way more
/// memory than is necessary.
///
/// The format is: usb, evdev, xkb, win, mac, code
pub type RawEntry = (u32, u32, u32, u32, u32, &'static str);
/// All of the full entries. This is marked pub but it should NOT be referenced
/// directly because it contains too much data for normal usage. Use `entries`
/// instead which contains just the relevant data for the target platform.
#[rustfmt::skip]
pub const RAW_ENTRIES: &[RawEntry] = &[
// USB evdev XKB Win Mac Code
(0x000000, 0x0000, 0x0000, 0x0000, 0xffff, ""),
// =========================================
// Non-USB codes
// =========================================
// USB evdev XKB Win Mac Code
(0x000010, 0x0000, 0x0000, 0x0000, 0xffff, "Hyper"),
(0x000011, 0x0000, 0x0000, 0x0000, 0xffff, "Super"),
(0x000012, 0x0000, 0x0000, 0x0000, 0xffff, "Fn"),
(0x000013, 0x0000, 0x0000, 0x0000, 0xffff, "FnLock"),
(0x000014, 0x0000, 0x0000, 0x0000, 0xffff, "Suspend"),
(0x000015, 0x0000, 0x0000, 0x0000, 0xffff, "Resume"),
(0x000016, 0x0000, 0x0000, 0x0000, 0xffff, "Turbo"),
// =========================================
// USB Usage Page 0x01: Generic Desktop Page
// =========================================
// Sleep could be encoded as USB#0c0032, but there's no corresponding WakeUp
// in the 0x0c USB page.
// USB evdev XKB Win Mac
(0x010082, 0x008e, 0x0096, 0xe05f, 0xffff, "Sleep"),
(0x010083, 0x008f, 0x0097, 0xe063, 0xffff, "WakeUp"),
(0x0100a9, 0x00f8, 0x0100, 0x0000, 0xffff, ""),
(0x0100b5, 0x00e3, 0x00eb, 0x0000, 0xffff, "DisplayToggleIntExt"),
// =========================================
// USB Usage Page 0x07: Keyboard/Keypad Page
// =========================================
// TODO(garykac):
// XKB#005c ISO Level3 Shift (AltGr)
// XKB#005e <>||
// XKB#006d Linefeed
// XKB#008a SunProps cf. USB#0700a3 CrSel/Props
// XKB#008e SunOpen
// Mac#003f kVK_Function
// Mac#000a kVK_ISO_Section (ISO keyboards only)
// Mac#0066 kVK_JIS_Eisu (USB#07008a Henkan?)
// USB evdev XKB Win Mac
(0x070000, 0x0000, 0x0000, 0x0000, 0xffff, ""),
(0x070001, 0x0000, 0x0000, 0x00ff, 0xffff, ""),
(0x070002, 0x0000, 0x0000, 0x00fc, 0xffff, ""),
(0x070003, 0x0000, 0x0000, 0x0000, 0xffff, ""),
(0x070004, 0x001e, 0x0026, 0x001e, 0x0000, "KeyA"),
(0x070005, 0x0030, 0x0038, 0x0030, 0x000b, "KeyB"),
(0x070006, 0x002e, 0x0036, 0x002e, 0x0008, "KeyC"),
(0x070007, 0x0020, 0x0028, 0x0020, 0x0002, "KeyD"),
(0x070008, 0x0012, 0x001a, 0x0012, 0x000e, "KeyE"),
(0x070009, 0x0021, 0x0029, 0x0021, 0x0003, "KeyF"),
(0x07000a, 0x0022, 0x002a, 0x0022, 0x0005, "KeyG"),
(0x07000b, 0x0023, 0x002b, 0x0023, 0x0004, "KeyH"),
(0x07000c, 0x0017, 0x001f, 0x0017, 0x0022, "KeyI"),
(0x07000d, 0x0024, 0x002c, 0x0024, 0x0026, "KeyJ"),
(0x07000e, 0x0025, 0x002d, 0x0025, 0x0028, "KeyK"),
(0x07000f, 0x0026, 0x002e, 0x0026, 0x0025, "KeyL"),
(0x070010, 0x0032, 0x003a, 0x0032, 0x002e, "KeyM"),
(0x070011, 0x0031, 0x0039, 0x0031, 0x002d, "KeyN"),
(0x070012, 0x0018, 0x0020, 0x0018, 0x001f, "KeyO"),
(0x070013, 0x0019, 0x0021, 0x0019, 0x0023, "KeyP"),
(0x070014, 0x0010, 0x0018, 0x0010, 0x000c, "KeyQ"),
(0x070015, 0x0013, 0x001b, 0x0013, 0x000f, "KeyR"),
(0x070016, 0x001f, 0x0027, 0x001f, 0x0001, "KeyS"),
(0x070017, 0x0014, 0x001c, 0x0014, 0x0011, "KeyT"),
(0x070018, 0x0016, 0x001e, 0x0016, 0x0020, "KeyU"),
(0x070019, 0x002f, 0x0037, 0x002f, 0x0009, "KeyV"),
(0x07001a, 0x0011, 0x0019, 0x0011, 0x000d, "KeyW"),
(0x07001b, 0x002d, 0x0035, 0x002d, 0x0007, "KeyX"),
(0x07001c, 0x0015, 0x001d, 0x0015, 0x0010, "KeyY"),
(0x07001d, 0x002c, 0x0034, 0x002c, 0x0006, "KeyZ"),
(0x07001e, 0x0002, 0x000a, 0x0002, 0x0012, "Digit1"),
(0x07001f, 0x0003, 0x000b, 0x0003, 0x0013, "Digit2"),
(0x070020, 0x0004, 0x000c, 0x0004, 0x0014, "Digit3"),
(0x070021, 0x0005, 0x000d, 0x0005, 0x0015, "Digit4"),
(0x070022, 0x0006, 0x000e, 0x0006, 0x0017, "Digit5"),
(0x070023, 0x0007, 0x000f, 0x0007, 0x0016, "Digit6"),
(0x070024, 0x0008, 0x0010, 0x0008, 0x001a, "Digit7"),
(0x070025, 0x0009, 0x0011, 0x0009, 0x001c, "Digit8"),
(0x070026, 0x000a, 0x0012, 0x000a, 0x0019, "Digit9"),
(0x070027, 0x000b, 0x0013, 0x000b, 0x001d, "Digit0"),
(0x070028, 0x001c, 0x0024, 0x001c, 0x0024, "Enter"),
(0x070029, 0x0001, 0x0009, 0x0001, 0x0035, "Escape"),
(0x07002a, 0x000e, 0x0016, 0x000e, 0x0033, "Backspace"),
(0x07002b, 0x000f, 0x0017, 0x000f, 0x0030, "Tab"),
(0x07002c, 0x0039, 0x0041, 0x0039, 0x0031, "Space"),
(0x07002d, 0x000c, 0x0014, 0x000c, 0x001b, "Minus"),
(0x07002e, 0x000d, 0x0015, 0x000d, 0x0018, "Equal"),
(0x07002f, 0x001a, 0x0022, 0x001a, 0x0021, "BracketLeft"),
(0x070030, 0x001b, 0x0023, 0x001b, 0x001e, "BracketRight"),
(0x070031, 0x002b, 0x0033, 0x002b, 0x002a, "Backslash"),
// USB#070032 never appears on keyboards that have USB#070031.
// Platforms use the same scancode as for the two keys.
// Hence this code can only be generated synthetically
// (e.g. in a DOM Level 3 KeyboardEvent).
// The keycap varies on international keyboards:
// Dan: '* Dutch: <> Ger: #' UK: #~
// TODO(garykac): Verify Mac intl keyboard.
// .{ 0x070032, 0x0000, 0x0000, 0x0000, 0xffff, ""},
(0x070033, 0x0027, 0x002f, 0x0027, 0x0029, "Semicolon"),
(0x070034, 0x0028, 0x0030, 0x0028, 0x0027, "Quote"),
(0x070035, 0x0029, 0x0031, 0x0029, 0x0032, "Backquote"),
(0x070036, 0x0033, 0x003b, 0x0033, 0x002b, "Comma"),
(0x070037, 0x0034, 0x003c, 0x0034, 0x002f, "Period"),
(0x070038, 0x0035, 0x003d, 0x0035, 0x002c, "Slash"),
// TODO(garykac): CapsLock requires special handling for each platform.
(0x070039, 0x003a, 0x0042, 0x003a, 0x0039, "CapsLock"),
(0x07003a, 0x003b, 0x0043, 0x003b, 0x007a, "F1"),
(0x07003b, 0x003c, 0x0044, 0x003c, 0x0078, "F2"),
(0x07003c, 0x003d, 0x0045, 0x003d, 0x0063, "F3"),
(0x07003d, 0x003e, 0x0046, 0x003e, 0x0076, "F4"),
(0x07003e, 0x003f, 0x0047, 0x003f, 0x0060, "F5"),
(0x07003f, 0x0040, 0x0048, 0x0040, 0x0061, "F6"),
(0x070040, 0x0041, 0x0049, 0x0041, 0x0062, "F7"),
(0x070041, 0x0042, 0x004a, 0x0042, 0x0064, "F8"),
(0x070042, 0x0043, 0x004b, 0x0043, 0x0065, "F9"),
(0x070043, 0x0044, 0x004c, 0x0044, 0x006d, "F10"),
(0x070044, 0x0057, 0x005f, 0x0057, 0x0067, "F11"),
(0x070045, 0x0058, 0x0060, 0x0058, 0x006f, "F12"),
// PrintScreen is effectively F13 on Mac OS X.
(0x070046, 0x0063, 0x006b, 0xe037, 0xffff, "PrintScreen"),
(0x070047, 0x0046, 0x004e, 0x0046, 0xffff, "ScrollLock"),
(0x070048, 0x0077, 0x007f, 0x0045, 0xffff, "Pause"),
// USB#0x070049 Insert, labeled "Help/Insert" on Mac -- see note M1 at top.
(0x070049, 0x006e, 0x0076, 0xe052, 0x0072, "Insert"),
(0x07004a, 0x0066, 0x006e, 0xe047, 0x0073, "Home"),
(0x07004b, 0x0068, 0x0070, 0xe049, 0x0074, "PageUp"),
// Delete (Forward Delete) named DEL because DELETE conflicts with <windows.h>
(0x07004c, 0x006f, 0x0077, 0xe053, 0x0075, "Delete"),
(0x07004d, 0x006b, 0x0073, 0xe04f, 0x0077, "End"),
(0x07004e, 0x006d, 0x0075, 0xe051, 0x0079, "PageDown"),
(0x07004f, 0x006a, 0x0072, 0xe04d, 0x007c, "ArrowRight"),
(0x070050, 0x0069, 0x0071, 0xe04b, 0x007b, "ArrowLeft"),
(0x070051, 0x006c, 0x0074, 0xe050, 0x007d, "ArrowDown"),
(0x070052, 0x0067, 0x006f, 0xe048, 0x007e, "ArrowUp"),
(0x070053, 0x0045, 0x004d, 0xe045, 0x0047, "NumLock"),
(0x070054, 0x0062, 0x006a, 0xe035, 0x004b, "NumpadDivide"),
(0x070055, 0x0037, 0x003f, 0x0037, 0x0043, "NumpadMultiply"),
(0x070056, 0x004a, 0x0052, 0x004a, 0x004e, "NumpadSubtract"),
(0x070057, 0x004e, 0x0056, 0x004e, 0x0045, "NumpadAdd"),
(0x070058, 0x0060, 0x0068, 0xe01c, 0x004c, "NumpadEnter"),
(0x070059, 0x004f, 0x0057, 0x004f, 0x0053, "Numpad1"), // +End
(0x07005a, 0x0050, 0x0058, 0x0050, 0x0054, "Numpad2"), // +Down
(0x07005b, 0x0051, 0x0059, 0x0051, 0x0055, "Numpad3"), // +PageDown
(0x07005c, 0x004b, 0x0053, 0x004b, 0x0056, "Numpad4"), // +Left
(0x07005d, 0x004c, 0x0054, 0x004c, 0x0057, "Numpad5"),
(0x07005e, 0x004d, 0x0055, 0x004d, 0x0058, "Numpad6"), // +Right
(0x07005f, 0x0047, 0x004f, 0x0047, 0x0059, "Numpad7"), // +Home
(0x070060, 0x0048, 0x0050, 0x0048, 0x005b, "Numpad8"), // +Up
(0x070061, 0x0049, 0x0051, 0x0049, 0x005c, "Numpad9"), // +PageUp
(0x070062, 0x0052, 0x005a, 0x0052, 0x0052, "Numpad0"), // +Insert
(0x070063, 0x0053, 0x005b, 0x0053, 0x0041, "NumpadDecimal"), // +Delete
// USB#070064 is not present on US keyboard.
// This key is typically located near LeftShift key.
// The keycap varies on international keyboards:
// Dan: <> Dutch: ][ Ger: <> UK: \|
(0x070064, 0x0056, 0x005e, 0x0056, 0x000a, "IntlBackslash"),
// USB#0x070065 Application Menu (next to RWin key) -- see note L2 at top.
(0x070065, 0x007f, 0x0087, 0xe05d, 0x006e, "ContextMenu"),
(0x070066, 0x0074, 0x007c, 0xe05e, 0xffff, "Power"),
(0x070067, 0x0075, 0x007d, 0x0059, 0x0051, "NumpadEqual"),
(0x070068, 0x00b7, 0x00bf, 0x0064, 0x0069, "F13"),
(0x070069, 0x00b8, 0x00c0, 0x0065, 0x006b, "F14"),
(0x07006a, 0x00b9, 0x00c1, 0x0066, 0x0071, "F15"),
(0x07006b, 0x00ba, 0x00c2, 0x0067, 0x006a, "F16"),
(0x07006c, 0x00bb, 0x00c3, 0x0068, 0x0040, "F17"),
(0x07006d, 0x00bc, 0x00c4, 0x0069, 0x004f, "F18"),
(0x07006e, 0x00bd, 0x00c5, 0x006a, 0x0050, "F19"),
(0x07006f, 0x00be, 0x00c6, 0x006b, 0x005a, "F20"),
(0x070070, 0x00bf, 0x00c7, 0x006c, 0xffff, "F21"),
(0x070071, 0x00c0, 0x00c8, 0x006d, 0xffff, "F22"),
(0x070072, 0x00c1, 0x00c9, 0x006e, 0xffff, "F23"),
// USB#0x070073 -- see note W1 at top.
(0x070073, 0x00c2, 0x00ca, 0x0076, 0xffff, "F24"),
(0x070074, 0x0086, 0x008e, 0x0000, 0xffff, "Open"),
// USB#0x070075 Help -- see note M1 at top.
(0x070075, 0x008a, 0x0092, 0xe03b, 0xffff, "Help"),
// USB#0x070076 Keyboard Menu -- see note L2 at top.
// .{ 0x070076, 0x0000, 0x0000, 0x0000, 0xffff, ""},
(0x070077, 0x0084, 0x008c, 0x0000, 0xffff, "Select"),
// .{ 0x070078, 0x0080, 0x0088, 0x0000, 0xffff, ""},
(0x070079, 0x0081, 0x0089, 0x0000, 0xffff, "Again"),
(0x07007a, 0x0083, 0x008b, 0xe008, 0xffff, "Undo"),
(0x07007b, 0x0089, 0x0091, 0xe017, 0xffff, "Cut"),
(0x07007c, 0x0085, 0x008d, 0xe018, 0xffff, "Copy"),
(0x07007d, 0x0087, 0x008f, 0xe00a, 0xffff, "Paste"),
(0x07007e, 0x0088, 0x0090, 0x0000, 0xffff, "Find"),
(0x07007f, 0x0071, 0x0079, 0xe020, 0x004a, "AudioVolumeMute"),
(0x070080, 0x0073, 0x007b, 0xe030, 0x0048, "AudioVolumeUp"),
(0x070081, 0x0072, 0x007a, 0xe02e, 0x0049, "AudioVolumeDown"),
// .{ 0x070082, 0x0000, 0x0000, 0x0000, 0xffff, ""},
// .{ 0x070083, 0x0000, 0x0000, 0x0000, 0xffff, ""},
// .{ 0x070084, 0x0000, 0x0000, 0x0000, 0xffff, ""},
(0x070085, 0x0079, 0x0081, 0x007e, 0x005f, "NumpadComma"),
// International1
// USB#070086 is used on AS/400 keyboards. Standard Keypad_= is USB#070067.
// .{ 0x070086, 0x0000, 0x0000, 0x0000, 0xffff, ""},
// USB#070087 is used for Brazilian /? and Japanese _ 'ro'.
(0x070087, 0x0059, 0x0061, 0x0073, 0x005e, "IntlRo"),
// International2
// USB#070088 is used as Japanese Hiragana/Katakana key.
(0x070088, 0x005d, 0x0065, 0x0070, 0xffff, "KanaMode"),
// International3
// USB#070089 is used as Japanese Yen key.
(0x070089, 0x007c, 0x0084, 0x007d, 0x005d, "IntlYen"),
// International4
// USB#07008a is used as Japanese Henkan (Convert) key.
(0x07008a, 0x005c, 0x0064, 0x0079, 0xffff, "Convert"),
// International5
// USB#07008b is used as Japanese Muhenkan (No-convert) key.
(0x07008b, 0x005e, 0x0066, 0x007b, 0xffff, "NonConvert"),
// .{ 0x07008c, 0x005f, 0x0067, 0x005c, 0xffff, ""},
// .{ 0x07008d, 0x0000, 0x0000, 0x0000, 0xffff, ""},
// .{ 0x07008e, 0x0000, 0x0000, 0x0000, 0xffff, ""},
// .{ 0x07008f, 0x0000, 0x0000, 0x0000, 0xffff, ""},
// LANG1
// USB#070090 is used as Korean Hangul/English toggle key, and as the Kana key
// on the Apple Japanese keyboard.
(0x070090, 0x007a, 0x0082, 0x0072, 0x0068, "Lang1"),
// LANG2
// USB#070091 is used as Korean Hanja conversion key, and as the Eisu key on
// the Apple Japanese keyboard.
(0x070091, 0x007b, 0x0083, 0x0071, 0x0066, "Lang2"),
// LANG3
// USB#070092 is used as Japanese Katakana key.
(0x070092, 0x005a, 0x0062, 0x0078, 0xffff, "Lang3"),
// LANG4
// USB#070093 is used as Japanese Hiragana key.
(0x070093, 0x005b, 0x0063, 0x0077, 0xffff, "Lang4"),
// LANG5
// USB#070094 is used as Japanese Zenkaku/Hankaku (Fullwidth/halfwidth) key.
// Not mapped on Windows -- see note W1 at top.
(0x070094, 0x0055, 0x005d, 0x0000, 0xffff, "Lang5"),
// .{ 0x070095, 0x0000, 0x0000, 0x0000, 0xffff, ""},
// .{ 0x070096, 0x0000, 0x0000, 0x0000, 0xffff, ""},
// .{ 0x070097, 0x0000, 0x0000, 0x0000, 0xffff, ""},
// .{ 0x070098, 0x0000, 0x0000, 0x0000, 0xffff, ""},
// .{ 0x070099, 0x0000, 0x0000, 0x0000, 0xffff, ""},
// .{ 0x07009a, 0x0000, 0x0000, 0x0000, 0xffff, ""},
(0x07009b, 0x0000, 0x0000, 0x0000, 0xffff, "Abort"),
// USB#0x07009c Keyboard Clear -- see note L1 at top.
// .{ 0x07009c, 0x0000, 0x0000, 0x0000, 0xffff, ""},
// .{ 0x07009d, 0x0000, 0x0000, 0x0000, 0xffff, ""},
// .{ 0x07009e, 0x0000, 0x0000, 0x0000, 0xffff, ""},
// .{ 0x07009f, 0x0000, 0x0000, 0x0000, 0xffff, ""},
// .{ 0x0700a0, 0x0000, 0x0000, 0x0000, 0xffff, ""},
// .{ 0x0700a1, 0x0000, 0x0000, 0x0000, 0xffff, ""},
// .{ 0x0700a2, 0x0000, 0x0000, 0x0000, 0xffff, ""},
// USB#0x0700a3 Props -- see note L2 at top.
(0x0700a3, 0x0000, 0x0000, 0x0000, 0xffff, "Props"),
// .{ 0x0700a4, 0x0000, 0x0000, 0x0000, 0xffff, ""},
// .{ 0x0700b0, 0x0000, 0x0000, 0x0000, 0xffff, ""},
// .{ 0x0700b1, 0x0000, 0x0000, 0x0000, 0xffff, ""},
// .{ 0x0700b2, 0x0000, 0x0000, 0x0000, 0xffff, ""},
// .{ 0x0700b3, 0x0000, 0x0000, 0x0000, 0xffff, ""},
// .{ 0x0700b4, 0x0000, 0x0000, 0x0000, 0xffff, ""},
// .{ 0x0700b5, 0x0000, 0x0000, 0x0000, 0xffff, ""},
(0x0700b6, 0x00b3, 0x00bb, 0x0000, 0xffff, "NumpadParenLeft"),
(0x0700b7, 0x00b4, 0x00bc, 0x0000, 0xffff, "NumpadParenRight"),
// .{ 0x0700b8, 0x0000, 0x0000, 0x0000, 0xffff, ""},
// .{ 0x0700b9, 0x0000, 0x0000, 0x0000, 0xffff, ""},
// .{ 0x0700ba, 0x0000, 0x0000, 0x0000, 0xffff, ""},
(0x0700bb, 0x0000, 0x0000, 0x0000, 0xffff, "NumpadBackspace"),
// .{ 0x0700bc, 0x0000, 0x0000, 0x0000, 0xffff, ""},
// .{ 0x0700bd, 0x0000, 0x0000, 0x0000, 0xffff, ""},
// .{ 0x0700be, 0x0000, 0x0000, 0x0000, 0xffff, ""},
// .{ 0x0700bf, 0x0000, 0x0000, 0x0000, 0xffff, ""},
// .{ 0x0700c0, 0x0000, 0x0000, 0x0000, 0xffff, ""},
// .{ 0x0700c1, 0x0000, 0x0000, 0x0000, 0xffff, ""},
// .{ 0x0700c2, 0x0000, 0x0000, 0x0000, 0xffff, ""},
// .{ 0x0700c3, 0x0000, 0x0000, 0x0000, 0xffff, ""},
// .{ 0x0700c4, 0x0000, 0x0000, 0x0000, 0xffff, ""},
// .{ 0x0700c5, 0x0000, 0x0000, 0x0000, 0xffff, ""},
// .{ 0x0700c6, 0x0000, 0x0000, 0x0000, 0xffff, ""},
// .{ 0x0700c7, 0x0000, 0x0000, 0x0000, 0xffff, ""},
// .{ 0x0700c8, 0x0000, 0x0000, 0x0000, 0xffff, ""},
// .{ 0x0700c9, 0x0000, 0x0000, 0x0000, 0xffff, ""},
// .{ 0x0700ca, 0x0000, 0x0000, 0x0000, 0xffff, ""},
// NUMPAD_DOUBLE_VERTICAL_BAR), // Keypad_||
// .{ 0x0700cb, 0x0000, 0x0000, 0x0000, 0xffff, ""},
// .{ 0x0700cc, 0x0000, 0x0000, 0x0000, 0xffff, ""},
// .{ 0x0700cd, 0x0000, 0x0000, 0x0000, 0xffff, ""},
// .{ 0x0700ce, 0x0000, 0x0000, 0x0000, 0xffff, ""},
// .{ 0x0700cf, 0x0000, 0x0000, 0x0000, 0xffff, ""},
(0x0700d0, 0x0000, 0x0000, 0x0000, 0xffff, "NumpadMemoryStore"),
(0x0700d1, 0x0000, 0x0000, 0x0000, 0xffff, "NumpadMemoryRecall"),
(0x0700d2, 0x0000, 0x0000, 0x0000, 0xffff, "NumpadMemoryClear"),
(0x0700d3, 0x0000, 0x0000, 0x0000, 0xffff, "NumpadMemoryAdd"),
(0x0700d4, 0x0000, 0x0000, 0x0000, 0xffff, "NumpadMemorySubtract"),
// .{ 0x0700d5, 0x0000, 0x0000, 0x0000, 0xffff, ""},
// .{ 0x0700d6, 0x0000, 0x0000, 0x0000, 0xffff, ""},
(0x0700d7, 0x0076, 0x007e, 0x0000, 0xffff, ""),
// USB#0x0700d8 Keypad Clear -- see note L1 at top.
(0x0700d8, 0x0000, 0x0000, 0x0000, 0xffff, "NumpadClear"),
(0x0700d9, 0x0000, 0x0000, 0x0000, 0xffff, "NumpadClearEntry"),
// .{ 0x0700da, 0x0000, 0x0000, 0x0000, 0xffff, ""},
// .{ 0x0700db, 0x0000, 0x0000, 0x0000, 0xffff, ""},
// .{ 0x0700dc, 0x0000, 0x0000, 0x0000, 0xffff, ""},
// .{ 0x0700dd, 0x0000, 0x0000, 0x0000, 0xffff, ""},
// USB#0700de - #0700df are reserved.
(0x0700e0, 0x001d, 0x0025, 0x001d, 0x003b, "ControlLeft"),
(0x0700e1, 0x002a, 0x0032, 0x002a, 0x0038, "ShiftLeft"),
// USB#0700e2: left Alt key (Mac left Option key).
(0x0700e2, 0x0038, 0x0040, 0x0038, 0x003a, "AltLeft"),
// USB#0700e3: left GUI key, e.g. Windows, Mac Command, ChromeOS Search.
(0x0700e3, 0x007d, 0x0085, 0xe05b, 0x0037, "MetaLeft"),
(0x0700e4, 0x0061, 0x0069, 0xe01d, 0x003e, "ControlRight"),
(0x0700e5, 0x0036, 0x003e, 0x0036, 0x003c, "ShiftRight"),
// USB#0700e6: right Alt key (Mac right Option key).
(0x0700e6, 0x0064, 0x006c, 0xe038, 0x003d, "AltRight"),
// USB#0700e7: right GUI key, e.g. Windows, Mac Command, ChromeOS Search.
(0x0700e7, 0x007e, 0x0086, 0xe05c, 0x0036, "MetaRight"),
// USB#0700e8 - #07ffff are reserved
// ==================================
// USB Usage Page 0x0c: Consumer Page
// ==================================
// AL = Application Launch
// AC = Application Control
// TODO(garykac): Many XF86 keys have multiple scancodes mapping to them.
// We need to map all of these into a canonical USB scancode without
// confusing the reverse-lookup - most likely by simply returning the first
// found match.
// TODO(garykac): Find appropriate mappings for:
// Win#e03c Music - USB#0c0193 is AL_AVCapturePlayback
// Win#e064 Pictures
// XKB#0080 XF86LaunchA
// XKB#0099 XF86Send
// XKB#009b XF86Xfer
// XKB#009c XF86Launch1
// XKB#009d XF86Launch2
// XKB... remaining XF86 keys
// KEY_BRIGHTNESS* added in Linux 3.16
// http://www.usb.org/developers/hidpage/HUTRR41.pdf
//
// Keyboard backlight/illumination spec update.
// https://www.usb.org/sites/default/files/hutrr73_-_fn_key_and_keyboard_backlight_brightness_0.pdf
// USB evdev XKB Win Mac Code
(0x0c0060, 0x0166, 0x016e, 0x0000, 0xffff, ""),
(0x0c0061, 0x0172, 0x017a, 0x0000, 0xffff, ""),
(0x0c006f, 0x00e1, 0x00e9, 0x0000, 0xffff, "BrightnessUp"),
(0x0c0070, 0x00e0, 0x00e8, 0x0000, 0xffff, "BrightnessDown"),
(0x0c0072, 0x01af, 0x01b7, 0x0000, 0xffff, ""),
(0x0c0073, 0x0250, 0x0258, 0x0000, 0xffff, ""),
(0x0c0074, 0x0251, 0x0259, 0x0000, 0xffff, ""),
(0x0c0075, 0x00f4, 0x00fc, 0x0000, 0xffff, ""),
(0x0c0079, 0x00e6, 0x00ee, 0x0000, 0xffff, ""),
(0x0c007a, 0x00e5, 0x00ed, 0x0000, 0xffff, ""),
(0x0c007c, 0x00e4, 0x00ec, 0x0000, 0xffff, ""),
(0x0c0083, 0x0195, 0x019d, 0x0000, 0xffff, ""),
(0x0c008c, 0x00a9, 0x00b1, 0x0000, 0xffff, ""),
(0x0c008d, 0x016a, 0x0172, 0x0000, 0xffff, ""),
(0x0c0094, 0x00ae, 0x00b6, 0x0000, 0xffff, ""),
(0x0c009c, 0x019a, 0x01a2, 0x0000, 0xffff, ""),
(0x0c009d, 0x019b, 0x01a3, 0x0000, 0xffff, ""),
// USB evdev XKB Win Mac
(0x0c00b0, 0x00cf, 0x00d7, 0x0000, 0xffff, "MediaPlay"),
(0x0c00b1, 0x00c9, 0x00d1, 0x0000, 0xffff, "MediaPause"),
(0x0c00b2, 0x00a7, 0x00af, 0x0000, 0xffff, "MediaRecord"),
(0x0c00b3, 0x00d0, 0x00d8, 0x0000, 0xffff, "MediaFastForward"),
(0x0c00b4, 0x00a8, 0x00b0, 0x0000, 0xffff, "MediaRewind"),
(0x0c00b5, 0x00a3, 0x00ab, 0xe019, 0xffff, "MediaTrackNext"),
(0x0c00b6, 0x00a5, 0x00ad, 0xe010, 0xffff, "MediaTrackPrevious"),
(0x0c00b7, 0x00a6, 0x00ae, 0xe024, 0xffff, "MediaStop"),
(0x0c00b8, 0x00a1, 0x00a9, 0xe02c, 0xffff, "Eject"),
(0x0c00cd, 0x00a4, 0x00ac, 0xe022, 0xffff, "MediaPlayPause"),
(0x0c00cf, 0x0246, 0x024e, 0x0000, 0xffff, ""),
(0x0c00d8, 0x024a, 0x0252, 0x0000, 0xffff, ""),
(0x0c00d9, 0x0249, 0x0251, 0x0000, 0xffff, ""),
(0x0c00e5, 0x00d1, 0x00d9, 0x0000, 0xffff, ""),
// .{ 0x0c00e6, 0x0000, 0x0000, 0x0000, 0xffff, ""},
// .{ 0x0c0150, 0x0000, 0x0000, 0x0000, 0xffff, ""},
// .{ 0x0c0151, 0x0000, 0x0000, 0x0000, 0xffff, ""},
// .{ 0x0c0152, 0x0000, 0x0000, 0x0000, 0xffff, ""},
// .{ 0x0c0153, 0x0000, 0x0000, 0x0000, 0xffff, ""},
// .{ 0x0c0154, 0x0000, 0x0000, 0x0000, 0xffff, ""},
// .{ 0x0c0155, 0x0000, 0x0000, 0x0000, 0xffff, ""},
// USB#0c0183: AL Consumer Control Configuration
(0x0c0183, 0x00ab, 0x00b3, 0xe06d, 0xffff, "MediaSelect"),
(0x0c0184, 0x01a5, 0x01ad, 0x0000, 0xffff, ""),
(0x0c0186, 0x01a7, 0x01af, 0x0000, 0xffff, ""),
// USB#0x0c018a AL_EmailReader
(0x0c018a, 0x009b, 0x00a3, 0xe06c, 0xffff, "LaunchMail"),
// USB#0x0c018d: AL Contacts/Address Book
(0x0c018d, 0x01ad, 0x01b5, 0x0000, 0xffff, ""),
// USB#0x0c018e: AL Calendar/Schedule
(0x0c018e, 0x018d, 0x0195, 0x0000, 0xffff, ""),
// USB#0x0c018f AL Task/Project Manager
// .{ 0x0c018f, 0x0241, 0x0249, 0x0000, 0xffff, ""},
// USB#0x0c0190: AL Log/Journal/Timecard
// .{ 0x0c0190, 0x0242, 0x024a, 0x0000, 0xffff, ""},
// USB#0x0c0192: AL_Calculator
(0x0c0192, 0x008c, 0x0094, 0xe021, 0xffff, "LaunchApp2"),
// USB#0c0194: My Computer (AL_LocalMachineBrowser)
(0x0c0194, 0x0090, 0x0098, 0xe06b, 0xffff, "LaunchApp1"),
(0x0c0196, 0x0096, 0x009e, 0x0000, 0xffff, ""),
(0x0c019c, 0x01b1, 0x01b9, 0x0000, 0xffff, ""),
// USB#0x0c019e: AL Terminal Lock/Screensaver
(0x0c019e, 0x0098, 0x00a0, 0x0000, 0xffff, ""),
// USB#0x0c019f AL Control Panel
(0x0c019f, 0x0243, 0x024b, 0x0000, 0xffff, "LaunchControlPanel"),
// USB#0x0c01a2: AL Select Task/Application
(0x0c01a2, 0x0244, 0x024c, 0x0000, 0xffff, "SelectTask"),
// USB#0x0c01a7: AL_Documents
(0x0c01a7, 0x00eb, 0x00f3, 0x0000, 0xffff, ""),
(0x0c01ab, 0x01b0, 0x01b8, 0x0000, 0xffff, ""),
// USB#0x0c01ae: AL Keyboard Layout
(0x0c01ae, 0x0176, 0x017e, 0x0000, 0xffff, ""),
(0x0c01b1, 0x0245, 0x024d, 0x0000, 0xffff, "LaunchScreenSaver"),
(0x0c01cb, 0x0247, 0x024f, 0x0000, 0xffff, "LaunchAssistant"),
// USB#0c01b4: Home Directory (AL_FileBrowser) (Explorer)
// .{ 0x0c01b4, 0x0000, 0x0000, 0x0000, 0xffff, ""},
// USB#0x0c01b7: AL Audio Browser
(0x0c01b7, 0x0188, 0x0190, 0x0000, 0xffff, ""),
// USB#0x0c0201: AC New
(0x0c0201, 0x00b5, 0x00bd, 0x0000, 0xffff, ""),
// USB#0x0c0203: AC Close
(0x0c0203, 0x00ce, 0x00d6, 0x0000, 0xffff, ""),
// USB#0x0c0207: AC Close
(0x0c0207, 0x00ea, 0x00f2, 0x0000, 0xffff, ""),
// USB#0x0c0208: AC Print
(0x0c0208, 0x00d2, 0x00da, 0x0000, 0xffff, ""),
// USB#0x0c0221: AC_Search
(0x0c0221, 0x00d9, 0x00e1, 0xe065, 0xffff, "BrowserSearch"),
// USB#0x0c0223: AC_Home
(0x0c0223, 0x00ac, 0x00b4, 0xe032, 0xffff, "BrowserHome"),
// USB#0x0c0224: AC_Back
(0x0c0224, 0x009e, 0x00a6, 0xe06a, 0xffff, "BrowserBack"),
// USB#0x0c0225: AC_Forward
(0x0c0225, 0x009f, 0x00a7, 0xe069, 0xffff, "BrowserForward"),
// USB#0x0c0226: AC_Stop
(0x0c0226, 0x0080, 0x0088, 0xe068, 0xffff, "BrowserStop"),
// USB#0x0c0227: AC_Refresh (Reload)
(0x0c0227, 0x00ad, 0x00b5, 0xe067, 0xffff, "BrowserRefresh"),
// USB#0x0c022a: AC_Bookmarks (Favorites)
(0x0c022a, 0x009c, 0x00a4, 0xe066, 0xffff, "BrowserFavorites"),
(0x0c022d, 0x01a2, 0x01aa, 0x0000, 0xffff, ""),
(0x0c022e, 0x01a3, 0x01ab, 0x0000, 0xffff, ""),
// USB#0x0c0230: AC Full Screen View
// .{ 0x0c0230, 0x0000, 0x0000, 0x0000, 0xffff, ""},
// USB#0x0c0231: AC Normal View
// .{ 0x0c0231, 0x0000, 0x0000, 0x0000, 0xffff, ""},
(0x0c0232, 0x0174, 0x017c, 0x0000, 0xffff, "ZoomToggle"),
// USB#0x0c0279: AC Redo/Repeat
(0x0c0279, 0x00b6, 0x00be, 0x0000, 0xffff, ""),
// USB#0x0c0289: AC_Reply
(0x0c0289, 0x00e8, 0x00f0, 0x0000, 0xffff, "MailReply"),
// USB#0x0c028b: AC_ForwardMsg (MailForward)
(0x0c028b, 0x00e9, 0x00f1, 0x0000, 0xffff, "MailForward"),
// USB#0x0c028c: AC_Send
(0x0c028c, 0x00e7, 0x00ef, 0x0000, 0xffff, "MailSend"),
// USB#0x0c029d: AC Next Keyboard Layout Select
(0x0c029d, 0x0248, 0x0250, 0x0000, 0xffff, "KeyboardLayoutSelect"),
(0x0c029f, 0x0078, 0x0080, 0x0000, 0xffff, "ShowAllWindows"),
(0x0c02a2, 0x00cc, 0x00d4, 0x0000, 0xffff, ""),
(0x0c02d0, 0x0279, 0x0281, 0x0000, 0xffff, ""),
];
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn entries_build() {
let e = entries();
assert_eq!(e.len(), RAW_ENTRIES.len());
let a = e.iter().find(|e| e.code == "KeyA").unwrap();
assert_eq!(a.key, Key::KeyA);
assert_eq!(a.usb, 0x070004);
assert_eq!(a.native, 0x0026);
}
#[test]
fn code_to_key_keys_are_w3c_names() {
for (code, key) in CODE_TO_KEY {
assert_eq!(Key::from_w3c(code), Some(*key), "{code}");
}
}
}
+134
View File
@@ -0,0 +1,134 @@
//! Kitty keyboard protocol key mapping data (ZIG/src/input/kitty.zig).
use crate::key::Key;
/// A single entry in the kitty keymap data. There are only ~100 entries
/// so the recommendation is to just use a linear search to find the entry
/// for a given key.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Entry {
pub key: Key,
pub code: u32,
pub final_: u8,
pub modifier: bool,
}
const fn e(key: Key, code: u32, final_: u8, modifier: bool) -> Entry {
Entry {
key,
code,
final_,
modifier,
}
}
/// The raw data for how to map keys to Kitty data. Based on the information:
/// https://sw.kovidgoyal.net/kitty/keyboard-protocol/#functional-key-definitions
/// And the exact table is ported from Foot:
/// https://codeberg.org/dnkl/foot/src/branch/master/kitty-keymap.h
///
/// Note that we currently don't support all the same keysyms as Kitty,
/// but we can add them as we add support.
///
/// These are stored in order of the above Kitty link, so it is easy to
/// find the entry for a given key.
pub const ENTRIES: &[Entry] = &[
e(Key::Escape, 27, b'u', false),
e(Key::Enter, 13, b'u', false),
e(Key::Tab, 9, b'u', false),
e(Key::Backspace, 127, b'u', false),
e(Key::Insert, 2, b'~', false),
e(Key::Delete, 3, b'~', false),
e(Key::ArrowLeft, 1, b'D', false),
e(Key::ArrowRight, 1, b'C', false),
e(Key::ArrowUp, 1, b'A', false),
e(Key::ArrowDown, 1, b'B', false),
e(Key::PageUp, 5, b'~', false),
e(Key::PageDown, 6, b'~', false),
e(Key::Home, 1, b'H', false),
e(Key::End, 1, b'F', false),
e(Key::CapsLock, 57358, b'u', true),
e(Key::ScrollLock, 57359, b'u', false),
e(Key::NumLock, 57360, b'u', true),
e(Key::PrintScreen, 57361, b'u', false),
e(Key::Pause, 57362, b'u', false),
e(Key::F1, 1, b'P', false),
e(Key::F2, 1, b'Q', false),
e(Key::F3, 13, b'~', false),
e(Key::F4, 1, b'S', false),
e(Key::F5, 15, b'~', false),
e(Key::F6, 17, b'~', false),
e(Key::F7, 18, b'~', false),
e(Key::F8, 19, b'~', false),
e(Key::F9, 20, b'~', false),
e(Key::F10, 21, b'~', false),
e(Key::F11, 23, b'~', false),
e(Key::F12, 24, b'~', false),
e(Key::F13, 57376, b'u', false),
e(Key::F14, 57377, b'u', false),
e(Key::F15, 57378, b'u', false),
e(Key::F16, 57379, b'u', false),
e(Key::F17, 57380, b'u', false),
e(Key::F18, 57381, b'u', false),
e(Key::F19, 57382, b'u', false),
e(Key::F20, 57383, b'u', false),
e(Key::F21, 57384, b'u', false),
e(Key::F22, 57385, b'u', false),
e(Key::F23, 57386, b'u', false),
e(Key::F24, 57387, b'u', false),
e(Key::F25, 57388, b'u', false),
e(Key::Numpad0, 57399, b'u', false),
e(Key::Numpad1, 57400, b'u', false),
e(Key::Numpad2, 57401, b'u', false),
e(Key::Numpad3, 57402, b'u', false),
e(Key::Numpad4, 57403, b'u', false),
e(Key::Numpad5, 57404, b'u', false),
e(Key::Numpad6, 57405, b'u', false),
e(Key::Numpad7, 57406, b'u', false),
e(Key::Numpad8, 57407, b'u', false),
e(Key::Numpad9, 57408, b'u', false),
e(Key::NumpadDecimal, 57409, b'u', false),
e(Key::NumpadDivide, 57410, b'u', false),
e(Key::NumpadMultiply, 57411, b'u', false),
e(Key::NumpadSubtract, 57412, b'u', false),
e(Key::NumpadAdd, 57413, b'u', false),
e(Key::NumpadEnter, 57414, b'u', false),
e(Key::NumpadEqual, 57415, b'u', false),
e(Key::NumpadSeparator, 57416, b'u', false),
e(Key::NumpadLeft, 57417, b'u', false),
e(Key::NumpadRight, 57418, b'u', false),
e(Key::NumpadUp, 57419, b'u', false),
e(Key::NumpadDown, 57420, b'u', false),
e(Key::NumpadPageUp, 57421, b'u', false),
e(Key::NumpadPageDown, 57422, b'u', false),
e(Key::NumpadHome, 57423, b'u', false),
e(Key::NumpadEnd, 57424, b'u', false),
e(Key::NumpadInsert, 57425, b'u', false),
e(Key::NumpadDelete, 57426, b'u', false),
e(Key::NumpadBegin, 57427, b'u', false),
e(Key::ShiftLeft, 57441, b'u', true),
e(Key::ShiftRight, 57447, b'u', true),
e(Key::ControlLeft, 57442, b'u', true),
e(Key::ControlRight, 57448, b'u', true),
e(Key::MetaLeft, 57444, b'u', true),
e(Key::MetaRight, 57450, b'u', true),
e(Key::AltLeft, 57443, b'u', true),
e(Key::AltRight, 57449, b'u', true),
];
/// The full list of entries for the current platform.
pub fn entries() -> &'static [Entry] {
ENTRIES
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn entries_table() {
// To force evaluation of the table
assert_ne!(entries().len(), 0);
}
}
+26 -1
View File
@@ -1 +1,26 @@
//! ghostty-input: see docs/rewrite/ARCHITECTURE.md for the module map.
//! ghostty-input: keyboard/mouse input handling (ZIG/src/input/*).
pub mod binding;
pub mod command;
pub mod config;
pub mod function_keys;
pub mod helpgen_actions;
pub mod key;
pub mod key_encode;
pub mod key_mods;
pub mod keyboard;
pub mod keycodes;
pub mod kitty;
pub mod link;
pub mod mouse;
pub mod paste;
pub use config::OptionAsAlt;
pub use key::{Action, Key, KeyEvent, ctrl_or_super};
pub use key_mods::{Mods, RemapSet as KeyRemapSet};
pub use keyboard::Layout as KeyboardLayout;
pub use link::Link;
pub use mouse::{
Button as MouseButton, ButtonState as MouseButtonState, PressureStage as MousePressureStage,
ScrollMods,
};
+63
View File
@@ -0,0 +1,63 @@
//! A link is a clickable element that can be used to trigger some action.
//! A link is NOT just a URL that opens in a browser. A link is any generic
//! regular expression match over terminal text that can trigger various
//! action types. (ZIG/src/input/Link.zig)
use crate::key_mods::Mods;
/// The action that will be triggered when the link is clicked.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Action {
/// Open the full matched value using the default open program.
/// For example, on macOS this is "open" and on Linux this is "xdg-open".
Open,
/// Open the OSC8 hyperlink under the mouse position. _-prefixed means
/// this can't be user-specified, it's only used internally.
_OpenOsc8,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Highlight {
/// Always highlight the link.
Always,
/// Only highlight the link when the mouse is hovering over it.
Hover,
/// Highlight anytime the given mods are pressed, either when
/// hovering or always. For always, all links will be highlighted
/// when the mods are pressed regardless of if the mouse is hovering
/// over them.
///
/// Note that if "shift" is specified here, this will NEVER match in
/// TUI programs that capture mouse events. "Shift" with mouse capture
/// escapes the mouse capture but strips the "shift" so it can't be
/// detected.
AlwaysMods(Mods),
HoverMods(Mods),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Link {
/// The regular expression that will be used to match the link.
pub regex: String,
/// The action that will be triggered when the link is clicked.
pub action: Action,
/// The situations in which the link will be highlighted. A link is only
/// clickable by the mouse when it is highlighted, so this also controls
/// when the link is clickable.
pub highlight: Highlight,
}
// TODO(integration): `oni_regex` (Zig `oniRegex`) builds an Oniguruma regex;
// the regex engine choice is made by the consumer crate (config/core).
impl Link {
/// Check if two links are equal.
pub fn equal(&self, other: &Link) -> bool {
self == other
}
}
+132
View File
@@ -0,0 +1,132 @@
//! Mouse input types (ZIG/src/input/mouse.zig).
/// The state of a mouse button.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(i32)]
pub enum ButtonState {
Release,
Press,
}
/// Possible mouse buttons. We only track up to 11 because that's the maximum
/// button input that terminal mouse tracking handles without becoming
/// ambiguous.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(i32)]
pub enum Button {
Unknown = 0,
Left = 1,
Right = 2,
Middle = 3,
Four = 4,
Five = 5,
Six = 6,
Seven = 7,
Eight = 8,
Nine = 9,
Ten = 10,
Eleven = 11,
}
impl Button {
/// The maximum value in this enum. This can be used to create a densely
/// packed array, for example.
pub const MAX: i32 = 11;
}
/// The "momentum" of a mouse scroll event. This matches the macOS events
/// because it is the only reliable source right now of momentum events.
/// This is used to handle "inertial scrolling" (i.e. flicking).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[repr(u8)]
pub enum Momentum {
#[default]
None = 0,
Began = 1,
Stationary = 2,
Changed = 3,
Ended = 4,
Cancelled = 5,
MayBegin = 6,
}
impl Momentum {
pub fn from_int(v: u8) -> Option<Momentum> {
Some(match v {
0 => Momentum::None,
1 => Momentum::Began,
2 => Momentum::Stationary,
3 => Momentum::Changed,
4 => Momentum::Ended,
5 => Momentum::Cancelled,
6 => Momentum::MayBegin,
_ => return None,
})
}
}
/// The pressure stage of a pressure-sensitive input device.
///
/// This currently only supports the stages that macOS supports.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[repr(u8)]
pub enum PressureStage {
/// The input device is unpressed.
#[default]
None = 0,
/// The input device is pressed a normal amount. On macOS trackpads,
/// this is after a "click".
Normal = 1,
/// The input device is pressed a deep amount. On macOS trackpads,
/// this is after a "force click".
Deep = 2,
}
/// The bitmask for mods for scroll events.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct ScrollMods {
/// True if this is a high-precision scroll event. For example, Apple
/// devices such as Magic Mouse, trackpads, etc. are high-precision
/// and send very detailed scroll events.
pub precision: bool,
/// The momentum phase (if available, supported) of the scroll event.
/// This is used to handle "inertial scrolling" (i.e. flicking).
pub momentum: Momentum,
}
impl ScrollMods {
/// Packed `u8` representation (bit 0 precision, bits 1..4 momentum).
pub fn int(self) -> u8 {
u8::from(self.precision) | ((self.momentum as u8) << 1)
}
pub fn from_int(v: u8) -> ScrollMods {
ScrollMods {
precision: v & 1 != 0,
momentum: Momentum::from_int((v >> 1) & 0b111).unwrap_or(Momentum::None),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
// For our own understanding
#[test]
fn scroll_mods_layout() {
assert_eq!(ScrollMods::default().int(), 0b0);
assert_eq!(
ScrollMods {
precision: true,
..Default::default()
}
.int(),
0b0000_0001
);
assert_eq!(ScrollMods::from_int(0b0000_0011).momentum, Momentum::Began);
}
}
+225
View File
@@ -0,0 +1,225 @@
//! Paste encoding (ZIG/src/input/paste.zig).
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum Error {
/// Returned if encoding requires a mutable copy of the data. This
/// can only be returned if the input data type is const.
#[error("mutable copy of the data is required")]
MutableRequired,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Options {
/// True if bracketed paste mode is on.
pub bracketed: bool,
}
impl Options {
// TODO(integration): Zig has `fromTerminal(t: *const Terminal)` which
// reads `t.modes.get(.bracketed_paste)`. Construct `Options` from the
// terminal mode in ghostty-terminal instead.
}
/// These are the set of byte values that are always replaced by
/// a space (per xterm's behavior) for any text insertion method e.g.
/// a paste, drag and drop, etc. These are copied directly from xterm's
/// source.
const STRIP: &[u8] = &[
0x00, // NUL
0x08, // BS
0x05, // ENQ
0x04, // EOT
0x1B, // ESC
0x7F, // DEL
// These can be overridden by the running terminal program
// via tcsetattr, so they aren't totally safe to hardcode like
// this. In practice, I haven't seen modern programs change these
// and its a much bigger architectural change to pass these through
// so for now they're hardcoded.
0x03, // VINTR (Ctrl+C)
0x1C, // VQUIT (Ctrl+\)
0x15, // VKILL (Ctrl+U)
0x1A, // VSUSP (Ctrl+Z)
0x11, // VSTART (Ctrl+Q)
0x13, // VSTOP (Ctrl+S)
0x17, // VWERASE (Ctrl+W)
0x16, // VLNEXT (Ctrl+V)
0x12, // VREPRINT (Ctrl+R)
0x0F, // VDISCARD (Ctrl+O)
];
/// Encode immutable (const) data for pasting. The resulting value can be
/// written to the pty to perform a paste of the input data.
///
/// This returns `Error::MutableRequired` if the data needs modification
/// (strip bytes or newline replacement); the caller can then make a mutable
/// copy and use `encode_mut`. This calling style allows initially const data
/// to be passed in without an allocation, since it is rare in normal use
/// cases that the data will need to be modified.
///
/// The data is returned as a set of slices to limit allocations. The caller
/// can combine the slices into a single buffer if desired.
///
/// WARNING: The input data is not checked for safety. See the `is_safe`
/// function to check if the data is safe to paste.
pub fn encode(data: &[u8], opts: Options) -> Result<[&[u8]; 3], Error> {
let mut result: [&[u8]; 3] = [b"", data, b""];
if data.iter().any(|b| STRIP.contains(b)) {
return Err(Error::MutableRequired);
}
// Bracketed paste mode (mode 2004) wraps pasted data in
// fenceposts so that the terminal can ignore things like newlines.
if opts.bracketed {
result[0] = b"\x1b[200~";
result[2] = b"\x1b[201~";
return Ok(result);
}
// Non-bracketed. We have to replace newline with `\r`.
if data.contains(&b'\n') {
return Err(Error::MutableRequired);
}
Ok(result)
}
/// Like `encode` but modifies the data in place, so it can't fail.
pub fn encode_mut(data: &mut [u8], opts: Options) -> [&[u8]; 3] {
// If we have any of the strip values, then we need to replace them
// with spaces. This is what xterm does and it does it regardless
// of bracketed paste mode. This is a security measure to prevent pastes
// from containing bytes that could be used to inject commands.
for b in data.iter_mut() {
if STRIP.contains(b) {
*b = b' ';
}
}
let data: &[u8] = if opts.bracketed {
data
} else {
// Non-bracketed. We have to replace newline with `\r`. This matches
// the behavior of xterm and other terminals. For `\r\n` this will
// result in `\r\r` which does match xterm.
for b in data.iter_mut() {
if *b == b'\n' {
*b = b'\r';
}
}
data
};
let mut result: [&[u8]; 3] = [b"", data, b""];
if opts.bracketed {
result[0] = b"\x1b[200~";
result[2] = b"\x1b[201~";
}
result
}
/// Returns true if the data looks safe to paste. Data is considered
/// unsafe if it contains any of the following:
///
/// - `\n`: Newlines can be used to inject commands.
/// - `\x1b[201~`: This is the end of a bracketed paste. This cane be used
/// to exit a bracketed paste and inject commands.
///
/// We consider any scenario unsafe regardless of current terminal state.
/// For example, even if bracketed paste mode is not active, we still
/// consider `\x1b[201~` unsafe. The existence of these types of bytes
/// should raise suspicion that the producer of the paste data is
/// acting strangely.
pub fn is_safe(data: &[u8]) -> bool {
!data.contains(&b'\n') && !data.windows(6).any(|w| w == b"\x1b[201~")
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn is_safe_test() {
assert!(is_safe(b"hello"));
assert!(!is_safe(b"hello\n"));
assert!(!is_safe(b"hello\nworld"));
assert!(!is_safe(b"he\x1b[201~llo"));
}
#[test]
fn encode_bracketed() {
let r = encode(b"hello", Options { bracketed: true }).unwrap();
assert_eq!(r[0], b"\x1b[200~");
assert_eq!(r[1], b"hello");
assert_eq!(r[2], b"\x1b[201~");
}
#[test]
fn encode_unbracketed_no_newlines() {
let r = encode(b"hello", Options { bracketed: false }).unwrap();
assert_eq!(r[0], b"");
assert_eq!(r[1], b"hello");
assert_eq!(r[2], b"");
}
#[test]
fn encode_unbracketed_newlines_const() {
assert_eq!(
encode(b"hello\nworld", Options { bracketed: false }),
Err(Error::MutableRequired)
);
}
#[test]
fn encode_unbracketed_newlines() {
let mut data = b"hello\nworld".to_vec();
let r = encode_mut(&mut data, Options { bracketed: false });
assert_eq!(r[0], b"");
assert_eq!(r[1], b"hello\rworld");
assert_eq!(r[2], b"");
}
#[test]
fn encode_unbracketed_windows_stye_newline() {
let mut data = b"hello\r\nworld".to_vec();
let r = encode_mut(&mut data, Options { bracketed: false });
assert_eq!(r[0], b"");
assert_eq!(r[1], b"hello\r\rworld");
assert_eq!(r[2], b"");
}
#[test]
fn encode_strip_unsafe_bytes_const() {
assert_eq!(
encode(b"hello\x00world", Options { bracketed: true }),
Err(Error::MutableRequired)
);
}
#[test]
fn encode_strip_unsafe_bytes_mutable_bracketed() {
let mut data = b"hel\x1blo\x00world".to_vec();
let r = encode_mut(&mut data, Options { bracketed: true });
assert_eq!(r[0], b"\x1b[200~");
assert_eq!(r[1], b"hel lo world");
assert_eq!(r[2], b"\x1b[201~");
}
#[test]
fn encode_strip_unsafe_bytes_mutable_unbracketed() {
let mut data = b"hel\x03lo".to_vec();
let r = encode_mut(&mut data, Options { bracketed: false });
assert_eq!(r[0], b"");
assert_eq!(r[1], b"hel lo");
assert_eq!(r[2], b"");
}
#[test]
fn encode_strip_multiple_unsafe_bytes() {
let mut data = b"\x00\x08\x7f".to_vec();
let r = encode_mut(&mut data, Options { bracketed: true });
assert_eq!(r[1], b" ");
}
}
+37
View File
@@ -0,0 +1,37 @@
# ghostty-input port notes
## File mapping (ZIG/src/input -> crates/ghostty-input/src)
- input.zig -> lib.rs (re-exports); config.zig -> config.rs; keyboard.zig -> keyboard.rs
- key.zig -> key.rs (`Key` generated by macro with name()/from_name(); `KeyEvent<'a>` borrows utf8)
- key_mods.zig -> key_mods.rs (`Mods` is a plain bool struct + `int()/from_int()` u16 layout; `RemapSet` is an insertion-ordered Vec)
- keycodes.zig -> keycodes.rs (data table `RAW_ENTRIES`, `entries()` built lazily, Linux = xkb column)
- function_keys.zig -> function_keys.rs; key_encode.zig -> key_encode.rs (legacy + kitty)
- Binding.zig -> binding/{mod,action,set,tests}.rs; command.zig -> command.rs
- helpgen_actions.zig -> helpgen_actions.rs (`generate`/`generate_string`)
- mouse.zig, kitty.zig, paste.zig, Link.zig -> same-named modules
- Skipped: KeymapDarwin.zig, KeymapNoop.zig (macOS only / no-op).
## Naming deviations
- `Mods.super` -> `super_` (Rust keyword). Zig `final` -> `final_` in kitty::Entry.
- Binding Action doc comments are kept as `//` comments (indented lists would be doctests); text is exposed via `ActionKey::docs()` / `Action::docs()`.
- `Action::scoped(Scope)` returns `Option<&Action>`.
- paste: `encode(&[u8])` (const, may return MutableRequired) and `encode_mut(&mut [u8])` replace the comptime-typed Zig function.
- `Set.chain_parent` is a trigger path instead of raw pointers.
- Case folding: `char::to_lowercase` plus 3 special cases rather than uucode tables.
- `Value::format_entries` fixes a Zig bug (shared-buffer rewind corrupting sibling prefixes for chained leaves in sequences); test `set_format_entries_sequence_shares_prefix`.
- macOS-only branches (option-as-alt translation, ctrlOrSuper on Darwin) are compiled out; Linux behaviour only.
## Skipped tests
- 7 macOS-only key_encode tests (kitty/legacy with macos option/super text, `alt+x macos`, etc.) and Mods `translation macos-option-as-alt` (ported as a Linux identity test).
- RemapSet `formatEntry` tests use `format_entries()` (list of values) since the config formatter lives in ghostty-config.
- Zig `imguiKey` (cimgui) not ported; inspector deferred.
- `Link.oniRegex` not ported (regex engine chosen by consumer).
## TODO(integration)
- `paste::Options::fromTerminal`, `key_encode::Options` fromTerminal: build from terminal modes in ghostty-terminal.
- `binding::set::EntryFormatter` trait stands in for config/formatter.zig; config crate should implement it.
- `Command::C` / `defaultsC` are plain structs, not C ABI.
- function_keys test uses a hardcoded 38-byte limit (Zig reads from termio).
## Status
220 tests pass; clippy -D warnings and fmt clean.