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:
1 parent
5b8c92b98c
commit
e03432241b
21 files changed
+13413
-1
No files matched your search
@@ -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(¶m[..idx])?,
|
||||
emit: WriteScreenFormat::from_name(¶m[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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
Reference in new issue
Block a user