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));
|
||||
}
|
||||
@@ -0,0 +1,590 @@
|
||||
//! Command palette entries (ZIG/src/input/command.zig).
|
||||
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use crate::binding::{
|
||||
Action, CloseTabMode, CopyToClipboard, GotoWindow, InspectorMode, NavigateSearch,
|
||||
SplitDirection, SplitFocusDirection, WriteScreen, WriteScreenAction, WriteScreenFormat,
|
||||
};
|
||||
|
||||
/// A command is a named binding action that can be executed from
|
||||
/// something like a command palette.
|
||||
///
|
||||
/// A command must be associated with a binding; all commands can be
|
||||
/// mapped to traditional `keybind` configurations. This restriction
|
||||
/// makes it so that there is nothing special about commands and likewise
|
||||
/// it makes it trivial and consistent to define custom commands.
|
||||
///
|
||||
/// For apprt implementers: a command palette doesn't have to make use
|
||||
/// of all the fields here. We try to provide as much information as
|
||||
/// possible to make it easier to implement a command palette in the way
|
||||
/// that makes the most sense for the application.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Command {
|
||||
pub action: Action,
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
impl Command {
|
||||
pub fn new(action: Action, title: &str, description: &str) -> Command {
|
||||
Command {
|
||||
action,
|
||||
title: title.to_string(),
|
||||
description: description.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn equal(&self, other: &Command) -> bool {
|
||||
if self.action.hash() != other.action.hash() {
|
||||
return false;
|
||||
}
|
||||
self.title == other.title && self.description == other.description
|
||||
}
|
||||
|
||||
/// Implements a comparison function for sorting. The sorting is defined
|
||||
/// by Ghostty to be what we prefer. If a caller wants some other
|
||||
/// sorting, they should do it themselves.
|
||||
pub fn less_than(lhs: &Command, rhs: &Command) -> bool {
|
||||
cmp_ignore_case(&lhs.title, &rhs.title) == std::cmp::Ordering::Less
|
||||
}
|
||||
|
||||
/// The `ghostty_command_s` equivalent: (action key name, action string,
|
||||
/// title, description).
|
||||
pub fn cval(&self) -> CommandC {
|
||||
CommandC {
|
||||
action_key: self.action.name().to_string(),
|
||||
action: self.action.to_string(),
|
||||
title: self.title.clone(),
|
||||
description: self.description.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Owned form of `Command.C`.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct CommandC {
|
||||
pub action_key: String,
|
||||
pub action: String,
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
/// `std.ascii.orderIgnoreCase`
|
||||
fn cmp_ignore_case(a: &str, b: &str) -> std::cmp::Ordering {
|
||||
let a = a.bytes().map(|c| c.to_ascii_lowercase());
|
||||
let b = b.bytes().map(|c| c.to_ascii_lowercase());
|
||||
a.cmp(b)
|
||||
}
|
||||
|
||||
/// The default command palette entries, sorted by title (ignoring case).
|
||||
pub fn defaults() -> &'static [Command] {
|
||||
static DEFAULTS: OnceLock<Vec<Command>> = OnceLock::new();
|
||||
DEFAULTS.get_or_init(|| {
|
||||
let mut result = default_commands();
|
||||
result.sort_by(|a, b| cmp_ignore_case(&a.title, &b.title));
|
||||
result
|
||||
})
|
||||
}
|
||||
|
||||
/// Defaults in C-compatible form.
|
||||
pub fn defaults_c() -> Vec<CommandC> {
|
||||
defaults().iter().map(Command::cval).collect()
|
||||
}
|
||||
|
||||
// The set of commands associated with each action key by default. Not all
|
||||
// actions should have commands. As a general guideline, an action should have
|
||||
// a command only if it is useful and reasonable to appear in a command
|
||||
// palette. Actions without commands (parameterized actions without obvious
|
||||
// values, ignore/unbind, etc.) are: csi, esc, cursor_key, set_font_size,
|
||||
// set_surface_title, set_tab_title, search, scroll_to_row,
|
||||
// scroll_page_fractional, scroll_page_lines, adjust_selection, jump_to_prompt,
|
||||
// write_scrollback_file, goto_tab, resize_split, activate_key_table,
|
||||
// activate_key_table_once, deactivate_key_table, deactivate_all_key_tables,
|
||||
// end_key_sequence, crash, toggle_command_palette, toggle_quick_terminal,
|
||||
// toggle_visibility, previous_tab, next_tab, last_tab, ignore, unbind.
|
||||
fn default_commands() -> Vec<Command> {
|
||||
vec![
|
||||
Command::new(
|
||||
Action::Reset,
|
||||
"Reset Terminal",
|
||||
"Reset the terminal to a clean state.",
|
||||
),
|
||||
Command::new(
|
||||
Action::CopyToClipboard(CopyToClipboard::Mixed),
|
||||
"Copy to Clipboard",
|
||||
"Copy the selected text to the clipboard in both plain and styled formats.",
|
||||
),
|
||||
Command::new(
|
||||
Action::CopyToClipboard(CopyToClipboard::Plain),
|
||||
"Copy Selection as Plain Text to Clipboard",
|
||||
"Copy the selected text as plain text to the clipboard.",
|
||||
),
|
||||
Command::new(
|
||||
Action::CopyToClipboard(CopyToClipboard::Vt),
|
||||
"Copy Selection as ANSI Sequences to Clipboard",
|
||||
"Copy the selected text as ANSI escape sequences to the clipboard.",
|
||||
),
|
||||
Command::new(
|
||||
Action::CopyToClipboard(CopyToClipboard::Html),
|
||||
"Copy Selection as HTML to Clipboard",
|
||||
"Copy the selected text as HTML to the clipboard.",
|
||||
),
|
||||
Command::new(
|
||||
Action::CopyUrlToClipboard,
|
||||
"Copy URL to Clipboard",
|
||||
"Copy the URL under the cursor to the clipboard.",
|
||||
),
|
||||
Command::new(
|
||||
Action::CopyTitleToClipboard,
|
||||
"Copy Terminal Title to Clipboard",
|
||||
"Copy the terminal title to the clipboard. If the terminal title is not set this has no effect.",
|
||||
),
|
||||
Command::new(
|
||||
Action::PasteFromClipboard,
|
||||
"Paste from Clipboard",
|
||||
"Paste the contents of the main clipboard.",
|
||||
),
|
||||
Command::new(
|
||||
Action::PasteFromSelection,
|
||||
"Paste from Selection",
|
||||
"Paste the contents of the selection clipboard.",
|
||||
),
|
||||
Command::new(
|
||||
Action::StartSearch,
|
||||
"Start Search",
|
||||
"Start a search if one isn't already active.",
|
||||
),
|
||||
Command::new(
|
||||
Action::SearchSelection,
|
||||
"Search Selection",
|
||||
"Start a search for the current text selection.",
|
||||
),
|
||||
Command::new(
|
||||
Action::EndSearch,
|
||||
"End Search",
|
||||
"End the current search if any and hide any GUI elements.",
|
||||
),
|
||||
Command::new(
|
||||
Action::NavigateSearch(NavigateSearch::Next),
|
||||
"Next Search Result",
|
||||
"Navigate to the next search result, if any.",
|
||||
),
|
||||
Command::new(
|
||||
Action::NavigateSearch(NavigateSearch::Previous),
|
||||
"Previous Search Result",
|
||||
"Navigate to the previous search result, if any.",
|
||||
),
|
||||
Command::new(
|
||||
Action::IncreaseFontSize(1.0),
|
||||
"Increase Font Size",
|
||||
"Increase the font size by 1 point.",
|
||||
),
|
||||
Command::new(
|
||||
Action::DecreaseFontSize(1.0),
|
||||
"Decrease Font Size",
|
||||
"Decrease the font size by 1 point.",
|
||||
),
|
||||
Command::new(
|
||||
Action::ResetFontSize,
|
||||
"Reset Font Size",
|
||||
"Reset the font size to the default.",
|
||||
),
|
||||
Command::new(
|
||||
Action::ClearScreen,
|
||||
"Clear Screen",
|
||||
"Clear the screen and scrollback.",
|
||||
),
|
||||
Command::new(
|
||||
Action::SelectAll,
|
||||
"Select All",
|
||||
"Select all text on the screen.",
|
||||
),
|
||||
Command::new(
|
||||
Action::ScrollToTop,
|
||||
"Scroll to Top",
|
||||
"Scroll to the top of the screen.",
|
||||
),
|
||||
Command::new(
|
||||
Action::ScrollToBottom,
|
||||
"Scroll to Bottom",
|
||||
"Scroll to the bottom of the screen.",
|
||||
),
|
||||
Command::new(
|
||||
Action::ScrollToSelection,
|
||||
"Scroll to Selection",
|
||||
"Scroll to the selected text.",
|
||||
),
|
||||
Command::new(
|
||||
Action::ScrollPageUp,
|
||||
"Scroll Page Up",
|
||||
"Scroll the screen up by a page.",
|
||||
),
|
||||
Command::new(
|
||||
Action::ScrollPageDown,
|
||||
"Scroll Page Down",
|
||||
"Scroll the screen down by a page.",
|
||||
),
|
||||
Command::new(
|
||||
Action::WriteScreenFile(WriteScreen::COPY),
|
||||
"Copy Screen to Temporary File and Copy Path",
|
||||
"Copy the screen contents to a temporary file and copy the path to the clipboard.",
|
||||
),
|
||||
Command::new(
|
||||
Action::WriteScreenFile(WriteScreen::PASTE),
|
||||
"Copy Screen to Temporary File and Paste Path",
|
||||
"Copy the screen contents to a temporary file and paste the path to the file.",
|
||||
),
|
||||
Command::new(
|
||||
Action::WriteScreenFile(WriteScreen::OPEN),
|
||||
"Copy Screen to Temporary File and Open",
|
||||
"Copy the screen contents to a temporary file and open it.",
|
||||
),
|
||||
Command::new(
|
||||
Action::WriteScreenFile(WriteScreen {
|
||||
action: WriteScreenAction::Copy,
|
||||
emit: WriteScreenFormat::Html,
|
||||
}),
|
||||
"Copy Screen as HTML to Temporary File and Copy Path",
|
||||
"Copy the screen contents as HTML to a temporary file and copy the path to the clipboard.",
|
||||
),
|
||||
Command::new(
|
||||
Action::WriteScreenFile(WriteScreen {
|
||||
action: WriteScreenAction::Paste,
|
||||
emit: WriteScreenFormat::Html,
|
||||
}),
|
||||
"Copy Screen as HTML to Temporary File and Paste Path",
|
||||
"Copy the screen contents as HTML to a temporary file and paste the path to the file.",
|
||||
),
|
||||
Command::new(
|
||||
Action::WriteScreenFile(WriteScreen {
|
||||
action: WriteScreenAction::Open,
|
||||
emit: WriteScreenFormat::Html,
|
||||
}),
|
||||
"Copy Screen as HTML to Temporary File and Open",
|
||||
"Copy the screen contents as HTML to a temporary file and open it.",
|
||||
),
|
||||
Command::new(
|
||||
Action::WriteScreenFile(WriteScreen {
|
||||
action: WriteScreenAction::Copy,
|
||||
emit: WriteScreenFormat::Vt,
|
||||
}),
|
||||
"Copy Screen as ANSI Sequences to Temporary File and Copy Path",
|
||||
"Copy the screen contents as ANSI escape sequences to a temporary file and copy the path to the clipboard.",
|
||||
),
|
||||
Command::new(
|
||||
Action::WriteScreenFile(WriteScreen {
|
||||
action: WriteScreenAction::Paste,
|
||||
emit: WriteScreenFormat::Vt,
|
||||
}),
|
||||
"Copy Screen as ANSI Sequences to Temporary File and Paste Path",
|
||||
"Copy the screen contents as ANSI escape sequences to a temporary file and paste the path to the file.",
|
||||
),
|
||||
Command::new(
|
||||
Action::WriteScreenFile(WriteScreen {
|
||||
action: WriteScreenAction::Open,
|
||||
emit: WriteScreenFormat::Vt,
|
||||
}),
|
||||
"Copy Screen as ANSI Sequences to Temporary File and Open",
|
||||
"Copy the screen contents as ANSI escape sequences to a temporary file and open it.",
|
||||
),
|
||||
Command::new(
|
||||
Action::WriteSelectionFile(WriteScreen::COPY),
|
||||
"Copy Selection to Temporary File and Copy Path",
|
||||
"Copy the selection contents to a temporary file and copy the path to the clipboard.",
|
||||
),
|
||||
Command::new(
|
||||
Action::WriteSelectionFile(WriteScreen::PASTE),
|
||||
"Copy Selection to Temporary File and Paste Path",
|
||||
"Copy the selection contents to a temporary file and paste the path to the file.",
|
||||
),
|
||||
Command::new(
|
||||
Action::WriteSelectionFile(WriteScreen::OPEN),
|
||||
"Copy Selection to Temporary File and Open",
|
||||
"Copy the selection contents to a temporary file and open it.",
|
||||
),
|
||||
Command::new(
|
||||
Action::WriteSelectionFile(WriteScreen {
|
||||
action: WriteScreenAction::Copy,
|
||||
emit: WriteScreenFormat::Html,
|
||||
}),
|
||||
"Copy Selection as HTML to Temporary File and Copy Path",
|
||||
"Copy the selection contents as HTML to a temporary file and copy the path to the clipboard.",
|
||||
),
|
||||
Command::new(
|
||||
Action::WriteSelectionFile(WriteScreen {
|
||||
action: WriteScreenAction::Paste,
|
||||
emit: WriteScreenFormat::Html,
|
||||
}),
|
||||
"Copy Selection as HTML to Temporary File and Paste Path",
|
||||
"Copy the selection contents as HTML to a temporary file and paste the path to the file.",
|
||||
),
|
||||
Command::new(
|
||||
Action::WriteSelectionFile(WriteScreen {
|
||||
action: WriteScreenAction::Open,
|
||||
emit: WriteScreenFormat::Html,
|
||||
}),
|
||||
"Copy Selection as HTML to Temporary File and Open",
|
||||
"Copy the selection contents as HTML to a temporary file and open it.",
|
||||
),
|
||||
Command::new(
|
||||
Action::WriteSelectionFile(WriteScreen {
|
||||
action: WriteScreenAction::Copy,
|
||||
emit: WriteScreenFormat::Vt,
|
||||
}),
|
||||
"Copy Selection as ANSI Sequences to Temporary File and Copy Path",
|
||||
"Copy the selection contents as ANSI escape sequences to a temporary file and copy the path to the clipboard.",
|
||||
),
|
||||
Command::new(
|
||||
Action::WriteSelectionFile(WriteScreen {
|
||||
action: WriteScreenAction::Paste,
|
||||
emit: WriteScreenFormat::Vt,
|
||||
}),
|
||||
"Copy Selection as ANSI Sequences to Temporary File and Paste Path",
|
||||
"Copy the selection contents as ANSI escape sequences to a temporary file and paste the path to the file.",
|
||||
),
|
||||
Command::new(
|
||||
Action::WriteSelectionFile(WriteScreen {
|
||||
action: WriteScreenAction::Open,
|
||||
emit: WriteScreenFormat::Vt,
|
||||
}),
|
||||
"Copy Selection as ANSI Sequences to Temporary File and Open",
|
||||
"Copy the selection contents as ANSI escape sequences to a temporary file and open it.",
|
||||
),
|
||||
Command::new(Action::NewWindow, "New Window", "Open a new window."),
|
||||
Command::new(Action::NewTab, "New Tab", "Open a new tab."),
|
||||
Command::new(
|
||||
Action::MoveTab(-1),
|
||||
"Move Tab Left",
|
||||
"Move the current tab to the left.",
|
||||
),
|
||||
Command::new(
|
||||
Action::MoveTab(1),
|
||||
"Move Tab Right",
|
||||
"Move the current tab to the right.",
|
||||
),
|
||||
Command::new(
|
||||
Action::ToggleTabOverview,
|
||||
"Toggle Tab Overview",
|
||||
"Toggle the tab overview.",
|
||||
),
|
||||
Command::new(
|
||||
Action::PromptSurfaceTitle,
|
||||
"Change Terminal Title…",
|
||||
"Prompt for a new title for the current terminal.",
|
||||
),
|
||||
Command::new(
|
||||
Action::PromptTabTitle,
|
||||
"Change Tab Title…",
|
||||
"Prompt for a new title for the current tab.",
|
||||
),
|
||||
Command::new(
|
||||
Action::NewSplit(SplitDirection::Left),
|
||||
"Split Left",
|
||||
"Split the terminal to the left.",
|
||||
),
|
||||
Command::new(
|
||||
Action::NewSplit(SplitDirection::Right),
|
||||
"Split Right",
|
||||
"Split the terminal to the right.",
|
||||
),
|
||||
Command::new(
|
||||
Action::NewSplit(SplitDirection::Up),
|
||||
"Split Up",
|
||||
"Split the terminal up.",
|
||||
),
|
||||
Command::new(
|
||||
Action::NewSplit(SplitDirection::Down),
|
||||
"Split Down",
|
||||
"Split the terminal down.",
|
||||
),
|
||||
Command::new(
|
||||
Action::GotoSplit(SplitFocusDirection::Previous),
|
||||
"Focus Split: Previous",
|
||||
"Focus the previous split, if any.",
|
||||
),
|
||||
Command::new(
|
||||
Action::GotoSplit(SplitFocusDirection::Next),
|
||||
"Focus Split: Next",
|
||||
"Focus the next split, if any.",
|
||||
),
|
||||
Command::new(
|
||||
Action::GotoSplit(SplitFocusDirection::Left),
|
||||
"Focus Split: Left",
|
||||
"Focus the split to the left, if it exists.",
|
||||
),
|
||||
Command::new(
|
||||
Action::GotoSplit(SplitFocusDirection::Right),
|
||||
"Focus Split: Right",
|
||||
"Focus the split to the right, if it exists.",
|
||||
),
|
||||
Command::new(
|
||||
Action::GotoSplit(SplitFocusDirection::Up),
|
||||
"Focus Split: Up",
|
||||
"Focus the split above, if it exists.",
|
||||
),
|
||||
Command::new(
|
||||
Action::GotoSplit(SplitFocusDirection::Down),
|
||||
"Focus Split: Down",
|
||||
"Focus the split below, if it exists.",
|
||||
),
|
||||
Command::new(
|
||||
Action::GotoWindow(GotoWindow::Previous),
|
||||
"Focus Window: Previous",
|
||||
"Focus the previous window, if any.",
|
||||
),
|
||||
Command::new(
|
||||
Action::GotoWindow(GotoWindow::Next),
|
||||
"Focus Window: Next",
|
||||
"Focus the next window, if any.",
|
||||
),
|
||||
Command::new(
|
||||
Action::ToggleSplitZoom,
|
||||
"Toggle Split Zoom",
|
||||
"Toggle the zoom state of the current split.",
|
||||
),
|
||||
Command::new(
|
||||
Action::ToggleReadonly,
|
||||
"Toggle Read-Only Mode",
|
||||
"Toggle read-only mode for the current surface.",
|
||||
),
|
||||
Command::new(
|
||||
Action::EqualizeSplits,
|
||||
"Equalize Splits",
|
||||
"Equalize the size of all splits.",
|
||||
),
|
||||
Command::new(
|
||||
Action::ResetWindowSize,
|
||||
"Reset Window Size",
|
||||
"Reset the window size to the default.",
|
||||
),
|
||||
Command::new(
|
||||
Action::Inspector(InspectorMode::Toggle),
|
||||
"Toggle Inspector",
|
||||
"Toggle the inspector.",
|
||||
),
|
||||
Command::new(
|
||||
Action::ShowGtkInspector,
|
||||
"Show the GTK Inspector",
|
||||
"Show the GTK inspector.",
|
||||
),
|
||||
Command::new(
|
||||
Action::ShowOnScreenKeyboard,
|
||||
"Show On-Screen Keyboard",
|
||||
"Show the on-screen keyboard if present.",
|
||||
),
|
||||
Command::new(Action::OpenConfig, "Open Config", "Open the config file."),
|
||||
Command::new(
|
||||
Action::ReloadConfig,
|
||||
"Reload Config",
|
||||
"Reload the config file.",
|
||||
),
|
||||
Command::new(
|
||||
Action::CloseSurface,
|
||||
"Close Terminal",
|
||||
"Close the current terminal.",
|
||||
),
|
||||
Command::new(
|
||||
Action::CloseTab(CloseTabMode::This),
|
||||
"Close Tab",
|
||||
"Close the current tab.",
|
||||
),
|
||||
Command::new(
|
||||
Action::CloseTab(CloseTabMode::Other),
|
||||
"Close Other Tabs",
|
||||
"Close all tabs in this window except the current one.",
|
||||
),
|
||||
Command::new(
|
||||
Action::CloseTab(CloseTabMode::Right),
|
||||
"Close Tabs to the Right",
|
||||
"Close all tabs to the right of the current one.",
|
||||
),
|
||||
Command::new(
|
||||
Action::CloseWindow,
|
||||
"Close Window",
|
||||
"Close the current window.",
|
||||
),
|
||||
Command::new(
|
||||
Action::CloseAllWindows,
|
||||
"Close All Windows",
|
||||
"Close all windows.",
|
||||
),
|
||||
Command::new(
|
||||
Action::ToggleMaximize,
|
||||
"Toggle Maximize",
|
||||
"Toggle the maximized state of the current window.",
|
||||
),
|
||||
Command::new(
|
||||
Action::ToggleFullscreen,
|
||||
"Toggle Fullscreen",
|
||||
"Toggle the fullscreen state of the current window.",
|
||||
),
|
||||
Command::new(
|
||||
Action::ToggleWindowDecorations,
|
||||
"Toggle Window Decorations",
|
||||
"Toggle the window decorations.",
|
||||
),
|
||||
Command::new(
|
||||
Action::ToggleWindowFloatOnTop,
|
||||
"Toggle Float on Top",
|
||||
"Toggle the float on top state of the current window.",
|
||||
),
|
||||
Command::new(
|
||||
Action::ToggleSecureInput,
|
||||
"Toggle Secure Input",
|
||||
"Toggle secure input mode.",
|
||||
),
|
||||
Command::new(
|
||||
Action::ToggleMouseReporting,
|
||||
"Toggle Mouse Reporting",
|
||||
"Toggle whether mouse events are reported to terminal applications.",
|
||||
),
|
||||
Command::new(
|
||||
Action::ToggleBackgroundOpacity,
|
||||
"Toggle Background Opacity",
|
||||
"Toggle the background opacity of a window that started transparent.",
|
||||
),
|
||||
Command::new(
|
||||
Action::CheckForUpdates,
|
||||
"Check for Updates",
|
||||
"Check for updates to the application.",
|
||||
),
|
||||
Command::new(Action::Undo, "Undo", "Undo the last action."),
|
||||
Command::new(Action::Redo, "Redo", "Redo the last undone action."),
|
||||
Command::new(Action::Quit, "Quit", "Quit the application."),
|
||||
Command::new(
|
||||
Action::Text("👻".to_string()),
|
||||
"Ghostty",
|
||||
"Put a little Ghostty in your terminal.",
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn command_defaults() {
|
||||
// This just ensures that defaults is analyzed and works.
|
||||
assert_ne!(defaults().len(), 0);
|
||||
assert_eq!(defaults().len(), defaults_c().len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_defaults_sorted_and_roundtrip() {
|
||||
let d = defaults();
|
||||
for w in d.windows(2) {
|
||||
assert_ne!(
|
||||
cmp_ignore_case(&w[0].title, &w[1].title),
|
||||
std::cmp::Ordering::Greater
|
||||
);
|
||||
}
|
||||
// Round trip every default action through its string form.
|
||||
// (`text` is stored raw and escaped on format, so it is excluded.)
|
||||
for c in d.iter().filter(|c| !matches!(c.action, Action::Text(_))) {
|
||||
let s = c.action.to_string();
|
||||
assert_eq!(Action::parse(&s).expect("parse"), c.action, "{s}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
//! Input-related config enums (ZIG/src/input/config.zig).
|
||||
|
||||
/// Determines the macOS option key behavior. See the config
|
||||
/// `macos-option-as-alt` for a lot more details.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
|
||||
#[repr(i32)]
|
||||
pub enum OptionAsAlt {
|
||||
#[default]
|
||||
False,
|
||||
True,
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
@@ -0,0 +1,616 @@
|
||||
//! This is the list of "PC style function keys" that xterm supports for
|
||||
//! the legacy keyboard protocols. These always take priority since at the
|
||||
//! time of writing this, even the most modern keyboard protocols still
|
||||
//! are backwards compatible with regards to these sequences.
|
||||
//!
|
||||
//! This is based on a variety of sources cross-referenced but mostly
|
||||
//! based on foot's keymap.h: https://codeberg.org/dnkl/foot/src/branch/master/keymap.h
|
||||
//! (ZIG/src/input/function_keys.zig)
|
||||
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use crate::key::{Key, Mods};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum CursorMode {
|
||||
Any,
|
||||
Normal,
|
||||
Application,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum KeypadMode {
|
||||
Any,
|
||||
Normal,
|
||||
Application,
|
||||
}
|
||||
|
||||
/// A bit confusing so I'll document this one: this is the "modify other keys"
|
||||
/// setting. We only change behavior for "set_other" which is ESC [ > 4; 2 m.
|
||||
/// So this can be "any" which means we don't care what's going on. Or it
|
||||
/// can be "set" which means modify keys must be set EXCEPT FOR "other keys"
|
||||
/// mode, and "set_other" which means modify keys must be set to "other keys"
|
||||
/// mode.
|
||||
///
|
||||
/// See: https://invisible-island.net/xterm/modified-keys.html
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ModifyKeys {
|
||||
Any,
|
||||
Set,
|
||||
SetOther,
|
||||
}
|
||||
|
||||
/// A single entry in the table of keys.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Entry {
|
||||
/// The exact set of modifiers that must be active for this entry to match.
|
||||
/// If mods_empty_is_any is true then empty mods means any set of mods can
|
||||
/// match. Otherwise, empty mods means no mods must be active.
|
||||
pub mods: Mods,
|
||||
pub mods_empty_is_any: bool,
|
||||
|
||||
/// The state required for cursor/keypad mode.
|
||||
pub cursor: CursorMode,
|
||||
pub keypad: KeypadMode,
|
||||
|
||||
/// Whether or not this entry should be used
|
||||
pub modify_other_keys: ModifyKeys,
|
||||
|
||||
/// The sequence to send to the pty if this entry matches.
|
||||
pub sequence: String,
|
||||
}
|
||||
|
||||
impl Entry {
|
||||
fn new(sequence: impl Into<String>) -> Entry {
|
||||
Entry {
|
||||
mods: Mods::default(),
|
||||
mods_empty_is_any: true,
|
||||
cursor: CursorMode::Any,
|
||||
keypad: KeypadMode::Any,
|
||||
modify_other_keys: ModifyKeys::Any,
|
||||
sequence: sequence.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fn m(shift: bool, ctrl: bool, alt: bool, super_: bool) -> Mods {
|
||||
Mods {
|
||||
shift,
|
||||
ctrl,
|
||||
alt,
|
||||
super_,
|
||||
caps_lock: false,
|
||||
num_lock: false,
|
||||
sides: crate::key_mods::Side {
|
||||
shift: crate::key_mods::ModSide::Left,
|
||||
ctrl: crate::key_mods::ModSide::Left,
|
||||
alt: crate::key_mods::ModSide::Left,
|
||||
super_: crate::key_mods::ModSide::Left,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// The list of modifier combinations for modify other key sequences.
|
||||
/// The mode value is index + 2.
|
||||
pub const MODIFIERS: &[Mods] = &[
|
||||
m(true, false, false, false),
|
||||
m(false, false, true, false),
|
||||
m(true, false, true, false),
|
||||
m(false, true, false, false),
|
||||
m(true, true, false, false),
|
||||
m(false, true, true, false),
|
||||
m(true, true, true, false),
|
||||
m(false, false, false, true),
|
||||
m(true, false, false, true),
|
||||
m(false, false, true, true),
|
||||
m(true, false, true, true),
|
||||
m(false, true, false, true),
|
||||
m(true, true, false, true),
|
||||
m(false, true, true, true),
|
||||
m(true, true, true, true),
|
||||
];
|
||||
|
||||
fn ent(mods: Mods, modify_other_keys: ModifyKeys, sequence: &str) -> Entry {
|
||||
Entry {
|
||||
mods,
|
||||
modify_other_keys,
|
||||
..Entry::new(sequence)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the entries for a key. Not every key has any entries. It is
|
||||
/// expected that you'll just use a for loop through the entry values, there
|
||||
/// are never more than a couple dozen or so.
|
||||
pub fn keys(k: Key) -> &'static [Entry] {
|
||||
&KEYS[k as usize]
|
||||
}
|
||||
|
||||
/// Iterate over the whole table (all keys, in `Key` order).
|
||||
pub fn all_keys() -> impl Iterator<Item = &'static [Entry]> {
|
||||
KEYS.iter().map(Vec::as_slice)
|
||||
}
|
||||
|
||||
static KEYS: LazyLock<Vec<Vec<Entry>>> = LazyLock::new(build_keys);
|
||||
|
||||
fn cat(mut a: Vec<Entry>, b: Vec<Entry>) -> Vec<Entry> {
|
||||
a.extend(b);
|
||||
a
|
||||
}
|
||||
|
||||
fn one(seq: &str) -> Vec<Entry> {
|
||||
vec![Entry::new(seq)]
|
||||
}
|
||||
|
||||
fn build_keys() -> Vec<Vec<Entry>> {
|
||||
let mut result: Vec<Vec<Entry>> = vec![Vec::new(); Key::ALL.len()];
|
||||
let mut set = |k: Key, v: Vec<Entry>| result[k as usize] = v;
|
||||
|
||||
set(
|
||||
Key::ArrowUp,
|
||||
cat(pc_style("\x1b[1;{}A"), cursor_key("\x1b[A", "\x1bOA")),
|
||||
);
|
||||
set(
|
||||
Key::ArrowDown,
|
||||
cat(pc_style("\x1b[1;{}B"), cursor_key("\x1b[B", "\x1bOB")),
|
||||
);
|
||||
set(
|
||||
Key::ArrowRight,
|
||||
cat(pc_style("\x1b[1;{}C"), cursor_key("\x1b[C", "\x1bOC")),
|
||||
);
|
||||
set(
|
||||
Key::ArrowLeft,
|
||||
cat(pc_style("\x1b[1;{}D"), cursor_key("\x1b[D", "\x1bOD")),
|
||||
);
|
||||
set(
|
||||
Key::Home,
|
||||
cat(pc_style("\x1b[1;{}H"), cursor_key("\x1b[H", "\x1bOH")),
|
||||
);
|
||||
set(
|
||||
Key::End,
|
||||
cat(pc_style("\x1b[1;{}F"), cursor_key("\x1b[F", "\x1bOF")),
|
||||
);
|
||||
set(Key::Insert, cat(pc_style("\x1b[2;{}~"), one("\x1B[2~")));
|
||||
set(Key::Delete, cat(pc_style("\x1b[3;{}~"), one("\x1B[3~")));
|
||||
set(Key::PageUp, cat(pc_style("\x1b[5;{}~"), one("\x1B[5~")));
|
||||
set(Key::PageDown, cat(pc_style("\x1b[6;{}~"), one("\x1B[6~")));
|
||||
|
||||
// Function Keys. todo: f13-f35 but we need to add to input.Key
|
||||
set(Key::F1, cat(pc_style("\x1b[1;{}P"), one("\x1BOP")));
|
||||
set(Key::F2, cat(pc_style("\x1b[1;{}Q"), one("\x1BOQ")));
|
||||
set(Key::F3, cat(pc_style("\x1b[13;{}~"), one("\x1BOR")));
|
||||
set(Key::F4, cat(pc_style("\x1b[1;{}S"), one("\x1BOS")));
|
||||
set(Key::F5, cat(pc_style("\x1b[15;{}~"), one("\x1B[15~")));
|
||||
set(Key::F6, cat(pc_style("\x1b[17;{}~"), one("\x1B[17~")));
|
||||
set(Key::F7, cat(pc_style("\x1b[18;{}~"), one("\x1B[18~")));
|
||||
set(Key::F8, cat(pc_style("\x1b[19;{}~"), one("\x1B[19~")));
|
||||
set(Key::F9, cat(pc_style("\x1b[20;{}~"), one("\x1B[20~")));
|
||||
set(Key::F10, cat(pc_style("\x1b[21;{}~"), one("\x1B[21~")));
|
||||
set(Key::F11, cat(pc_style("\x1b[23;{}~"), one("\x1B[23~")));
|
||||
set(Key::F12, cat(pc_style("\x1b[24;{}~"), one("\x1B[24~")));
|
||||
|
||||
// Keypad keys
|
||||
set(Key::Numpad0, kp_keys("p"));
|
||||
set(Key::Numpad1, kp_keys("q"));
|
||||
set(Key::Numpad2, kp_keys("r"));
|
||||
set(Key::Numpad3, kp_keys("s"));
|
||||
set(Key::Numpad4, kp_keys("t"));
|
||||
set(Key::Numpad5, kp_keys("u"));
|
||||
set(Key::Numpad6, kp_keys("v"));
|
||||
set(Key::Numpad7, kp_keys("w"));
|
||||
set(Key::Numpad8, kp_keys("x"));
|
||||
set(Key::Numpad9, kp_keys("y"));
|
||||
set(Key::NumpadDecimal, kp_keys("n"));
|
||||
set(Key::NumpadDivide, kp_keys("o"));
|
||||
set(Key::NumpadMultiply, kp_keys("j"));
|
||||
set(Key::NumpadSubtract, kp_keys("m"));
|
||||
set(Key::NumpadAdd, kp_keys("k"));
|
||||
set(Key::NumpadEnter, cat(kp_keys("M"), one("\r")));
|
||||
set(
|
||||
Key::NumpadUp,
|
||||
cat(pc_style("\x1b[1;{}A"), cursor_key("\x1b[A", "\x1bOA")),
|
||||
);
|
||||
set(
|
||||
Key::NumpadDown,
|
||||
cat(pc_style("\x1b[1;{}B"), cursor_key("\x1b[B", "\x1bOB")),
|
||||
);
|
||||
set(
|
||||
Key::NumpadRight,
|
||||
cat(pc_style("\x1b[1;{}C"), cursor_key("\x1b[C", "\x1bOC")),
|
||||
);
|
||||
set(
|
||||
Key::NumpadLeft,
|
||||
cat(pc_style("\x1b[1;{}D"), cursor_key("\x1b[D", "\x1bOD")),
|
||||
);
|
||||
set(
|
||||
Key::NumpadBegin,
|
||||
cat(pc_style("\x1b[1;{}E"), cursor_key("\x1b[E", "\x1bOE")),
|
||||
);
|
||||
set(
|
||||
Key::NumpadHome,
|
||||
cat(pc_style("\x1b[1;{}H"), cursor_key("\x1b[H", "\x1bOH")),
|
||||
);
|
||||
set(
|
||||
Key::NumpadEnd,
|
||||
cat(pc_style("\x1b[1;{}F"), cursor_key("\x1b[F", "\x1bOF")),
|
||||
);
|
||||
set(
|
||||
Key::NumpadInsert,
|
||||
cat(pc_style("\x1b[2;{}~"), one("\x1B[2~")),
|
||||
);
|
||||
set(
|
||||
Key::NumpadDelete,
|
||||
cat(pc_style("\x1b[3;{}~"), one("\x1B[3~")),
|
||||
);
|
||||
set(
|
||||
Key::NumpadPageUp,
|
||||
cat(pc_style("\x1b[5;{}~"), one("\x1B[5~")),
|
||||
);
|
||||
set(
|
||||
Key::NumpadPageDown,
|
||||
cat(pc_style("\x1b[6;{}~"), one("\x1B[6~")),
|
||||
);
|
||||
|
||||
set(
|
||||
Key::Backspace,
|
||||
vec![
|
||||
// Modify Keys Normal / Other
|
||||
ent(m(true, false, false, false), ModifyKeys::Set, "\x7f"),
|
||||
ent(m(false, false, true, false), ModifyKeys::Set, "\x1b\x7f"),
|
||||
ent(m(true, false, true, false), ModifyKeys::Set, "\x1b\x7f"),
|
||||
ent(m(true, true, false, false), ModifyKeys::Set, "\x08"),
|
||||
ent(m(false, true, true, false), ModifyKeys::Set, "\x1b\x08"),
|
||||
ent(m(false, false, false, true), ModifyKeys::Set, "\x7f"),
|
||||
ent(m(true, false, false, true), ModifyKeys::Set, "\x7f"),
|
||||
ent(m(false, false, true, true), ModifyKeys::Set, "\x1b\x7f"),
|
||||
ent(m(true, false, true, true), ModifyKeys::Set, "\x1b\x7f"),
|
||||
ent(m(false, true, false, true), ModifyKeys::Set, "\x08"),
|
||||
ent(m(true, true, false, true), ModifyKeys::Set, "\x08"),
|
||||
ent(m(false, true, true, true), ModifyKeys::Set, "\x1b\x08"),
|
||||
ent(m(true, true, true, true), ModifyKeys::Set, "\x1b\x08"),
|
||||
ent(
|
||||
m(true, false, false, false),
|
||||
ModifyKeys::SetOther,
|
||||
"\x1b[27;2;127~",
|
||||
),
|
||||
ent(
|
||||
m(false, false, true, false),
|
||||
ModifyKeys::SetOther,
|
||||
"\x1b[27;3;127~",
|
||||
),
|
||||
ent(
|
||||
m(true, false, true, false),
|
||||
ModifyKeys::SetOther,
|
||||
"\x1b[27;4;127~",
|
||||
),
|
||||
ent(
|
||||
m(true, true, false, false),
|
||||
ModifyKeys::SetOther,
|
||||
"\x1b[27;6;127~",
|
||||
),
|
||||
ent(
|
||||
m(false, true, true, false),
|
||||
ModifyKeys::SetOther,
|
||||
"\x1b[27;7;127~",
|
||||
),
|
||||
ent(
|
||||
m(true, true, true, false),
|
||||
ModifyKeys::SetOther,
|
||||
"\x1b[27;8;127~",
|
||||
),
|
||||
ent(
|
||||
m(false, false, false, true),
|
||||
ModifyKeys::SetOther,
|
||||
"\x1b[27;9;127~",
|
||||
),
|
||||
ent(
|
||||
m(true, false, false, true),
|
||||
ModifyKeys::SetOther,
|
||||
"\x1b[27;10;127~",
|
||||
),
|
||||
ent(
|
||||
m(false, false, true, true),
|
||||
ModifyKeys::SetOther,
|
||||
"\x1b[27;11;127~",
|
||||
),
|
||||
ent(
|
||||
m(true, false, true, true),
|
||||
ModifyKeys::SetOther,
|
||||
"\x1b[27;12;127~",
|
||||
),
|
||||
ent(
|
||||
m(false, true, false, true),
|
||||
ModifyKeys::SetOther,
|
||||
"\x1b[27;13;127~",
|
||||
),
|
||||
ent(
|
||||
m(true, true, false, true),
|
||||
ModifyKeys::SetOther,
|
||||
"\x1b[27;14;127~",
|
||||
),
|
||||
ent(
|
||||
m(false, true, true, true),
|
||||
ModifyKeys::SetOther,
|
||||
"\x1b[27;15;127~",
|
||||
),
|
||||
ent(
|
||||
m(true, true, true, true),
|
||||
ModifyKeys::SetOther,
|
||||
"\x1b[27;16;127~",
|
||||
),
|
||||
ent(m(false, true, false, false), ModifyKeys::Any, "\x08"),
|
||||
ent(m(false, false, false, false), ModifyKeys::Any, "\x7f"),
|
||||
ent(m(false, true, false, false), ModifyKeys::Any, "\x08"),
|
||||
ent(m(false, false, false, false), ModifyKeys::Any, "\x7f"),
|
||||
],
|
||||
);
|
||||
|
||||
set(
|
||||
Key::Tab,
|
||||
vec![
|
||||
ent(m(true, false, false, false), ModifyKeys::Set, "\x1b[Z"),
|
||||
ent(m(false, false, true, false), ModifyKeys::Set, "\x1b\t"),
|
||||
ent(
|
||||
m(true, false, false, false),
|
||||
ModifyKeys::SetOther,
|
||||
"\x1b[27;2;9~",
|
||||
),
|
||||
ent(
|
||||
m(false, false, true, false),
|
||||
ModifyKeys::SetOther,
|
||||
"\x1b[27;3;9~",
|
||||
),
|
||||
ent(m(true, false, true, false), ModifyKeys::Any, "\x1b[27;4;9~"),
|
||||
ent(
|
||||
m(false, true, false, false),
|
||||
ModifyKeys::Any,
|
||||
"\x1b[27;5;9~",
|
||||
),
|
||||
ent(m(true, true, false, false), ModifyKeys::Any, "\x1b[27;6;9~"),
|
||||
ent(m(false, true, true, false), ModifyKeys::Any, "\x1b[27;7;9~"),
|
||||
ent(m(true, true, true, false), ModifyKeys::Any, "\x1b[27;8;9~"),
|
||||
ent(
|
||||
m(false, false, false, true),
|
||||
ModifyKeys::Any,
|
||||
"\x1b[27;9;9~",
|
||||
),
|
||||
ent(
|
||||
m(true, false, false, true),
|
||||
ModifyKeys::Any,
|
||||
"\x1b[27;10;9~",
|
||||
),
|
||||
ent(
|
||||
m(false, false, true, true),
|
||||
ModifyKeys::Any,
|
||||
"\x1b[27;11;9~",
|
||||
),
|
||||
ent(m(true, false, true, true), ModifyKeys::Any, "\x1b[27;12;9~"),
|
||||
ent(
|
||||
m(false, true, false, true),
|
||||
ModifyKeys::Any,
|
||||
"\x1b[27;13;9~",
|
||||
),
|
||||
ent(m(true, true, false, true), ModifyKeys::Any, "\x1b[27;14;9~"),
|
||||
ent(m(false, true, true, true), ModifyKeys::Any, "\x1b[27;15;9~"),
|
||||
ent(m(true, true, true, true), ModifyKeys::Any, "\x1b[27;16;9~"),
|
||||
ent(m(false, false, false, false), ModifyKeys::Any, "\t"),
|
||||
ent(m(false, false, false, false), ModifyKeys::Any, "\t"),
|
||||
],
|
||||
);
|
||||
|
||||
set(
|
||||
Key::Enter,
|
||||
vec![
|
||||
ent(
|
||||
m(true, false, false, false),
|
||||
ModifyKeys::Any,
|
||||
"\x1b[27;2;13~",
|
||||
),
|
||||
ent(m(false, false, true, false), ModifyKeys::Set, "\x1b\r"),
|
||||
ent(
|
||||
m(false, false, true, false),
|
||||
ModifyKeys::SetOther,
|
||||
"\x1b[27;3;13~",
|
||||
),
|
||||
ent(
|
||||
m(true, false, true, false),
|
||||
ModifyKeys::Any,
|
||||
"\x1b[27;4;13~",
|
||||
),
|
||||
ent(
|
||||
m(false, true, false, false),
|
||||
ModifyKeys::Any,
|
||||
"\x1b[27;5;13~",
|
||||
),
|
||||
ent(
|
||||
m(true, true, false, false),
|
||||
ModifyKeys::Any,
|
||||
"\x1b[27;6;13~",
|
||||
),
|
||||
ent(
|
||||
m(false, true, true, false),
|
||||
ModifyKeys::Any,
|
||||
"\x1b[27;7;13~",
|
||||
),
|
||||
ent(m(true, true, true, false), ModifyKeys::Any, "\x1b[27;8;13~"),
|
||||
ent(
|
||||
m(false, false, false, true),
|
||||
ModifyKeys::Any,
|
||||
"\x1b[27;9;13~",
|
||||
),
|
||||
ent(
|
||||
m(true, false, false, true),
|
||||
ModifyKeys::Any,
|
||||
"\x1b[27;10;13~",
|
||||
),
|
||||
ent(
|
||||
m(false, false, true, true),
|
||||
ModifyKeys::Any,
|
||||
"\x1b[27;11;13~",
|
||||
),
|
||||
ent(
|
||||
m(true, false, true, true),
|
||||
ModifyKeys::Any,
|
||||
"\x1b[27;12;13~",
|
||||
),
|
||||
ent(
|
||||
m(false, true, false, true),
|
||||
ModifyKeys::Any,
|
||||
"\x1b[27;13;13~",
|
||||
),
|
||||
ent(
|
||||
m(true, true, false, true),
|
||||
ModifyKeys::Any,
|
||||
"\x1b[27;14;13~",
|
||||
),
|
||||
ent(
|
||||
m(false, true, true, true),
|
||||
ModifyKeys::Any,
|
||||
"\x1b[27;15;13~",
|
||||
),
|
||||
ent(m(true, true, true, true), ModifyKeys::Any, "\x1b[27;16;13~"),
|
||||
ent(m(false, false, false, false), ModifyKeys::Any, "\r"),
|
||||
ent(m(false, false, false, false), ModifyKeys::Any, "\r"),
|
||||
],
|
||||
);
|
||||
|
||||
set(
|
||||
Key::Escape,
|
||||
vec![
|
||||
ent(
|
||||
m(true, false, false, false),
|
||||
ModifyKeys::Any,
|
||||
"\x1b[27;2;27~",
|
||||
),
|
||||
ent(m(false, false, true, false), ModifyKeys::Any, "\x1b\x1b"),
|
||||
ent(
|
||||
m(true, false, true, false),
|
||||
ModifyKeys::Any,
|
||||
"\x1b[27;4;27~",
|
||||
),
|
||||
ent(
|
||||
m(false, true, false, false),
|
||||
ModifyKeys::Any,
|
||||
"\x1b[27;5;27~",
|
||||
),
|
||||
ent(
|
||||
m(true, true, false, false),
|
||||
ModifyKeys::Any,
|
||||
"\x1b[27;6;27~",
|
||||
),
|
||||
ent(
|
||||
m(false, true, true, false),
|
||||
ModifyKeys::Any,
|
||||
"\x1b[27;7;27~",
|
||||
),
|
||||
ent(m(true, true, true, false), ModifyKeys::Any, "\x1b[27;8;27~"),
|
||||
ent(
|
||||
m(false, false, false, true),
|
||||
ModifyKeys::Any,
|
||||
"\x1b[27;9;27~",
|
||||
),
|
||||
ent(
|
||||
m(true, false, false, true),
|
||||
ModifyKeys::Any,
|
||||
"\x1b[27;10;27~",
|
||||
),
|
||||
ent(
|
||||
m(false, false, true, true),
|
||||
ModifyKeys::Any,
|
||||
"\x1b[27;11;27~",
|
||||
),
|
||||
ent(
|
||||
m(true, false, true, true),
|
||||
ModifyKeys::Any,
|
||||
"\x1b[27;12;27~",
|
||||
),
|
||||
ent(
|
||||
m(false, true, false, true),
|
||||
ModifyKeys::Any,
|
||||
"\x1b[27;13;27~",
|
||||
),
|
||||
ent(
|
||||
m(true, true, false, true),
|
||||
ModifyKeys::Any,
|
||||
"\x1b[27;14;27~",
|
||||
),
|
||||
ent(
|
||||
m(false, true, true, true),
|
||||
ModifyKeys::Any,
|
||||
"\x1b[27;15;27~",
|
||||
),
|
||||
ent(m(true, true, true, true), ModifyKeys::Any, "\x1b[27;16;27~"),
|
||||
ent(m(false, false, false, false), ModifyKeys::Any, "\x1b"),
|
||||
ent(m(false, false, false, false), ModifyKeys::Any, "\x1b"),
|
||||
],
|
||||
);
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Returns the default keypad application mode entry.
|
||||
fn kp_default(suffix: &str) -> Vec<Entry> {
|
||||
vec![Entry {
|
||||
mods_empty_is_any: false,
|
||||
keypad: KeypadMode::Application,
|
||||
..Entry::new(format!("\x1bO{suffix}"))
|
||||
}]
|
||||
}
|
||||
|
||||
/// Returns the entries for a keypad key. The suffix is the final character
|
||||
/// of the sent sequence, such as "r" for kp_2.
|
||||
fn kp_keys(suffix: &str) -> Vec<Entry> {
|
||||
let mut pc = pc_style(&format!("\x1bO{{}}{suffix}"));
|
||||
for entry in &mut pc {
|
||||
entry.keypad = KeypadMode::Application;
|
||||
}
|
||||
cat(kp_default(suffix), pc)
|
||||
}
|
||||
|
||||
/// Returns entries that are dependent on cursor key settings.
|
||||
fn cursor_key(normal: &str, application: &str) -> Vec<Entry> {
|
||||
vec![
|
||||
Entry {
|
||||
cursor: CursorMode::Normal,
|
||||
..Entry::new(normal)
|
||||
},
|
||||
Entry {
|
||||
cursor: CursorMode::Application,
|
||||
..Entry::new(application)
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
/// Constructs a set of pcStyle function keys using the given format. The
|
||||
/// format should have exactly one "hole" (`{}`) for the mods code.
|
||||
/// Example: "\x1b[11;{}~" for F1.
|
||||
fn pc_style(fmt: &str) -> Vec<Entry> {
|
||||
MODIFIERS
|
||||
.iter()
|
||||
.zip(2..)
|
||||
.map(|(mods, code)| Entry {
|
||||
mods: *mods,
|
||||
..Entry::new(fmt.replace("{}", &code.to_string()))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn keys_table() {
|
||||
// All key sequences must fit into a termio array.
|
||||
// TODO(integration): termio's `Message.WriteReq.Small.Max` isn't
|
||||
// available here; use its documented limit (38 bytes).
|
||||
const MAX: usize = 38;
|
||||
let mut n = 0;
|
||||
for entries in all_keys() {
|
||||
for entry in entries {
|
||||
assert!(entry.sequence.len() <= MAX, "{:?}", entry.sequence);
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
assert!(n > 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
//! This module is a help generator for keybind actions documentation.
|
||||
//! It can generate documentation in different formats (plaintext for CLI,
|
||||
//! markdown for website) while maintaining consistent content.
|
||||
//! (ZIG/src/input/helpgen_actions.zig)
|
||||
//!
|
||||
//! The docs themselves live in [`ActionKey::docs`] (the Zig version reads
|
||||
//! them from the build-time generated `help_strings`).
|
||||
|
||||
use crate::binding::ActionKey;
|
||||
|
||||
/// Format options for generating keybind actions documentation
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Format {
|
||||
/// Plain text output with indentation
|
||||
Plaintext,
|
||||
/// Markdown formatted output
|
||||
Markdown,
|
||||
}
|
||||
|
||||
impl Format {
|
||||
fn format_field_name(self, w: &mut String, field_name: &str) {
|
||||
match self {
|
||||
Format::Plaintext => {
|
||||
w.push_str(field_name);
|
||||
w.push_str(":\n");
|
||||
}
|
||||
Format::Markdown => {
|
||||
w.push_str("## `");
|
||||
w.push_str(field_name);
|
||||
w.push_str("`\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn format_doc_line(self, w: &mut String, line: &str) {
|
||||
match self {
|
||||
Format::Plaintext => {
|
||||
w.push_str(" ");
|
||||
w.push_str(line);
|
||||
w.push('\n');
|
||||
}
|
||||
Format::Markdown => {
|
||||
w.push_str(line);
|
||||
w.push('\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn header(self) -> Option<&'static str> {
|
||||
match self {
|
||||
Format::Plaintext => None,
|
||||
Format::Markdown => Some(
|
||||
"---\n\
|
||||
title: Keybinding Action Reference\n\
|
||||
description: Reference of all Ghostty keybinding actions.\n\
|
||||
editOnGithubLink: https://github.com/ghostty-org/ghostty/edit/main/src/input/Binding.zig\n\
|
||||
---\n\
|
||||
\n\
|
||||
This is a reference of all Ghostty keybinding actions.\n\
|
||||
\n\
|
||||
\n",
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate keybind actions documentation with the specified format
|
||||
pub fn generate(writer: &mut String, format: Format, show_docs: bool) {
|
||||
if let Some(header) = format.header() {
|
||||
writer.push_str(header);
|
||||
}
|
||||
|
||||
let mut stream = String::new();
|
||||
|
||||
for &key in ActionKey::ALL {
|
||||
let name = key.name();
|
||||
if name.starts_with('_') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Write previously stored doc comment below all related actions
|
||||
// (every action has a doc comment).
|
||||
if show_docs {
|
||||
writer.push_str(&stream);
|
||||
writer.push('\n');
|
||||
stream.clear();
|
||||
}
|
||||
|
||||
if show_docs {
|
||||
format.format_field_name(writer, name);
|
||||
} else {
|
||||
writer.push_str(name);
|
||||
writer.push('\n');
|
||||
}
|
||||
|
||||
if show_docs {
|
||||
let mut iter = key.docs().split('\n').peekable();
|
||||
while let Some(s) = iter.next() {
|
||||
// If it is the last line and empty, then skip it.
|
||||
if iter.peek().is_none() && s.is_empty() {
|
||||
continue;
|
||||
}
|
||||
format.format_doc_line(&mut stream, s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write any remaining buffered documentation
|
||||
if !stream.is_empty() {
|
||||
writer.push_str(&stream);
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience: generate into a fresh string.
|
||||
pub fn generate_string(format: Format, show_docs: bool) -> String {
|
||||
let mut s = String::new();
|
||||
generate(&mut s, format, show_docs);
|
||||
s
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn plaintext_with_docs_lists_all_actions() {
|
||||
let s = generate_string(Format::Plaintext, true);
|
||||
assert!(s.starts_with("\nignore:\n"));
|
||||
assert!(s.contains("ignore:\n Ignore this key combination.\n"));
|
||||
for &k in ActionKey::ALL {
|
||||
assert!(s.contains(&format!("{}:\n", k.name())));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn names_only() {
|
||||
let s = generate_string(Format::Plaintext, false);
|
||||
assert_eq!(s.lines().count(), ActionKey::ALL.len());
|
||||
assert_eq!(s.lines().next(), Some("ignore"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn markdown_header_and_field() {
|
||||
let s = generate_string(Format::Markdown, true);
|
||||
assert!(s.starts_with("---\ntitle: Keybinding Action Reference\n"));
|
||||
assert!(s.contains("## `unbind`\n"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,611 @@
|
||||
//! Key events and key codes (ZIG/src/input/key.zig).
|
||||
//!
|
||||
//! The Zig `imguiKey` helper (cimgui constants) is intentionally not ported;
|
||||
//! the inspector is deferred (see notes).
|
||||
|
||||
pub use crate::key_mods::Mods;
|
||||
|
||||
/// A generic key input event. This is the information that is necessary
|
||||
/// regardless of apprt in order to generate the proper terminal
|
||||
/// control sequences for a given key press.
|
||||
///
|
||||
/// Some apprts may not be able to provide all of this information, such
|
||||
/// as GLFW. In this case, the apprt should provide as much information
|
||||
/// as it can and it should be expected that the terminal behavior
|
||||
/// will not be totally correct.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct KeyEvent<'a> {
|
||||
/// The action: press, release, etc.
|
||||
pub action: Action,
|
||||
|
||||
/// The keycode of the physical key that was pressed. This is agnostic
|
||||
/// to the layout. Layout-dependent matching can only be done via the
|
||||
/// UTF-8 or unshifted codepoint.
|
||||
pub key: Key,
|
||||
|
||||
/// Mods are the modifiers that are pressed.
|
||||
pub mods: Mods,
|
||||
|
||||
/// The mods that were consumed in order to generate the text
|
||||
/// in utf8. This has the mods set that were consumed, so to
|
||||
/// get the set of mods that are effective you must negate
|
||||
/// mods with this.
|
||||
///
|
||||
/// This field is meaningless if utf8 is empty.
|
||||
pub consumed_mods: Mods,
|
||||
|
||||
/// Composing is true when this key event is part of a dead key
|
||||
/// composition sequence and we're in the middle of it.
|
||||
pub composing: bool,
|
||||
|
||||
/// The utf8 sequence that was generated by this key event.
|
||||
/// This will be an empty string if there is no text generated.
|
||||
/// If composing is true and this is non-empty, this is preedit
|
||||
/// text.
|
||||
pub utf8: &'a str,
|
||||
|
||||
/// The codepoint for this key when it is unshifted. For example,
|
||||
/// shift+a is "A" in UTF-8 but unshifted would provide 'a'.
|
||||
pub unshifted_codepoint: u32,
|
||||
}
|
||||
|
||||
impl Default for KeyEvent<'_> {
|
||||
fn default() -> Self {
|
||||
KeyEvent {
|
||||
action: Action::Press,
|
||||
key: Key::Unidentified,
|
||||
mods: Mods::default(),
|
||||
consumed_mods: Mods::default(),
|
||||
composing: false,
|
||||
utf8: "",
|
||||
unshifted_codepoint: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyEvent<'_> {
|
||||
/// Returns the effective modifiers for this event. The effective
|
||||
/// modifiers are the mods that should be considered for keybindings.
|
||||
pub fn effective_mods(&self) -> Mods {
|
||||
if self.utf8.is_empty() {
|
||||
return self.mods;
|
||||
}
|
||||
self.mods.unset(self.consumed_mods)
|
||||
}
|
||||
|
||||
/// Returns a unique hash for this key event to be used for tracking
|
||||
/// uniquess specifically with bindings. This omits fields that are
|
||||
/// irrelevant for bindings.
|
||||
pub fn binding_hash(&self) -> u64 {
|
||||
use std::hash::{Hash, Hasher};
|
||||
let mut hasher = std::collections::hash_map::DefaultHasher::new();
|
||||
|
||||
// These are all the fields that are explicitly part of Trigger.
|
||||
self.key.hash(&mut hasher);
|
||||
self.unshifted_codepoint.hash(&mut hasher);
|
||||
self.mods.binding().hash(&mut hasher);
|
||||
|
||||
// Notes on unmapped things and why:
|
||||
//
|
||||
// - action: we don't have action-specific bindings right now
|
||||
// AND we want to know if a key resulted in a binding regardless
|
||||
// of action because a press should also ignore a release and so on.
|
||||
//
|
||||
// We can add to this if there is other confusion.
|
||||
|
||||
hasher.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// The action associated with an input event.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[repr(i32)]
|
||||
pub enum Action {
|
||||
Release,
|
||||
Press,
|
||||
Repeat,
|
||||
}
|
||||
|
||||
macro_rules! define_keys {
|
||||
($($variant:ident => $name:literal,)*) => {
|
||||
/// The set of key codes that Ghostty is aware of. These represent
|
||||
/// physical keys on the keyboard. The logical key (or key string)
|
||||
/// is the string that is generated by the key event and that is up
|
||||
/// to the apprt to provide.
|
||||
///
|
||||
/// Note that these are layout-independent. For example, the "a"
|
||||
/// key on a US keyboard is the same as the "ф" key on a Russian
|
||||
/// keyboard, but both will report the "a" enum value in the key
|
||||
/// event. These values are based on the W3C standard. See:
|
||||
/// https://www.w3.org/TR/uievents-code
|
||||
///
|
||||
/// Layout-dependent strings are provided in the KeyEvent struct as
|
||||
/// UTF-8 and are produced by the associated apprt. Ghostty core has
|
||||
/// no mechanism to map input events to strings without the apprt.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
#[repr(i32)]
|
||||
pub enum Key {
|
||||
$($variant,)*
|
||||
}
|
||||
|
||||
impl Key {
|
||||
/// All keys in declaration order.
|
||||
pub const ALL: &'static [Key] = &[$(Key::$variant,)*];
|
||||
|
||||
/// The Zig tag name (snake_case) of this key.
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
$(Key::$variant => $name,)*
|
||||
}
|
||||
}
|
||||
|
||||
/// Look up a key by its Zig tag name (snake_case).
|
||||
pub fn from_name(s: &str) -> Option<Key> {
|
||||
match s {
|
||||
$($name => Some(Key::$variant),)*
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
define_keys! {
|
||||
Unidentified => "unidentified",
|
||||
Backquote => "backquote",
|
||||
Backslash => "backslash",
|
||||
BracketLeft => "bracket_left",
|
||||
BracketRight => "bracket_right",
|
||||
Comma => "comma",
|
||||
Digit0 => "digit_0",
|
||||
Digit1 => "digit_1",
|
||||
Digit2 => "digit_2",
|
||||
Digit3 => "digit_3",
|
||||
Digit4 => "digit_4",
|
||||
Digit5 => "digit_5",
|
||||
Digit6 => "digit_6",
|
||||
Digit7 => "digit_7",
|
||||
Digit8 => "digit_8",
|
||||
Digit9 => "digit_9",
|
||||
Equal => "equal",
|
||||
IntlBackslash => "intl_backslash",
|
||||
IntlRo => "intl_ro",
|
||||
IntlYen => "intl_yen",
|
||||
KeyA => "key_a",
|
||||
KeyB => "key_b",
|
||||
KeyC => "key_c",
|
||||
KeyD => "key_d",
|
||||
KeyE => "key_e",
|
||||
KeyF => "key_f",
|
||||
KeyG => "key_g",
|
||||
KeyH => "key_h",
|
||||
KeyI => "key_i",
|
||||
KeyJ => "key_j",
|
||||
KeyK => "key_k",
|
||||
KeyL => "key_l",
|
||||
KeyM => "key_m",
|
||||
KeyN => "key_n",
|
||||
KeyO => "key_o",
|
||||
KeyP => "key_p",
|
||||
KeyQ => "key_q",
|
||||
KeyR => "key_r",
|
||||
KeyS => "key_s",
|
||||
KeyT => "key_t",
|
||||
KeyU => "key_u",
|
||||
KeyV => "key_v",
|
||||
KeyW => "key_w",
|
||||
KeyX => "key_x",
|
||||
KeyY => "key_y",
|
||||
KeyZ => "key_z",
|
||||
Minus => "minus",
|
||||
Period => "period",
|
||||
Quote => "quote",
|
||||
Semicolon => "semicolon",
|
||||
Slash => "slash",
|
||||
AltLeft => "alt_left",
|
||||
AltRight => "alt_right",
|
||||
Backspace => "backspace",
|
||||
CapsLock => "caps_lock",
|
||||
ContextMenu => "context_menu",
|
||||
ControlLeft => "control_left",
|
||||
ControlRight => "control_right",
|
||||
Enter => "enter",
|
||||
MetaLeft => "meta_left",
|
||||
MetaRight => "meta_right",
|
||||
ShiftLeft => "shift_left",
|
||||
ShiftRight => "shift_right",
|
||||
Space => "space",
|
||||
Tab => "tab",
|
||||
Convert => "convert",
|
||||
KanaMode => "kana_mode",
|
||||
NonConvert => "non_convert",
|
||||
Delete => "delete",
|
||||
End => "end",
|
||||
Help => "help",
|
||||
Home => "home",
|
||||
Insert => "insert",
|
||||
PageDown => "page_down",
|
||||
PageUp => "page_up",
|
||||
ArrowDown => "arrow_down",
|
||||
ArrowLeft => "arrow_left",
|
||||
ArrowRight => "arrow_right",
|
||||
ArrowUp => "arrow_up",
|
||||
NumLock => "num_lock",
|
||||
Numpad0 => "numpad_0",
|
||||
Numpad1 => "numpad_1",
|
||||
Numpad2 => "numpad_2",
|
||||
Numpad3 => "numpad_3",
|
||||
Numpad4 => "numpad_4",
|
||||
Numpad5 => "numpad_5",
|
||||
Numpad6 => "numpad_6",
|
||||
Numpad7 => "numpad_7",
|
||||
Numpad8 => "numpad_8",
|
||||
Numpad9 => "numpad_9",
|
||||
NumpadAdd => "numpad_add",
|
||||
NumpadBackspace => "numpad_backspace",
|
||||
NumpadClear => "numpad_clear",
|
||||
NumpadClearEntry => "numpad_clear_entry",
|
||||
NumpadComma => "numpad_comma",
|
||||
NumpadDecimal => "numpad_decimal",
|
||||
NumpadDivide => "numpad_divide",
|
||||
NumpadEnter => "numpad_enter",
|
||||
NumpadEqual => "numpad_equal",
|
||||
NumpadMemoryAdd => "numpad_memory_add",
|
||||
NumpadMemoryClear => "numpad_memory_clear",
|
||||
NumpadMemoryRecall => "numpad_memory_recall",
|
||||
NumpadMemoryStore => "numpad_memory_store",
|
||||
NumpadMemorySubtract => "numpad_memory_subtract",
|
||||
NumpadMultiply => "numpad_multiply",
|
||||
NumpadParenLeft => "numpad_paren_left",
|
||||
NumpadParenRight => "numpad_paren_right",
|
||||
NumpadSubtract => "numpad_subtract",
|
||||
NumpadSeparator => "numpad_separator",
|
||||
NumpadUp => "numpad_up",
|
||||
NumpadDown => "numpad_down",
|
||||
NumpadRight => "numpad_right",
|
||||
NumpadLeft => "numpad_left",
|
||||
NumpadBegin => "numpad_begin",
|
||||
NumpadHome => "numpad_home",
|
||||
NumpadEnd => "numpad_end",
|
||||
NumpadInsert => "numpad_insert",
|
||||
NumpadDelete => "numpad_delete",
|
||||
NumpadPageUp => "numpad_page_up",
|
||||
NumpadPageDown => "numpad_page_down",
|
||||
Escape => "escape",
|
||||
F1 => "f1",
|
||||
F2 => "f2",
|
||||
F3 => "f3",
|
||||
F4 => "f4",
|
||||
F5 => "f5",
|
||||
F6 => "f6",
|
||||
F7 => "f7",
|
||||
F8 => "f8",
|
||||
F9 => "f9",
|
||||
F10 => "f10",
|
||||
F11 => "f11",
|
||||
F12 => "f12",
|
||||
F13 => "f13",
|
||||
F14 => "f14",
|
||||
F15 => "f15",
|
||||
F16 => "f16",
|
||||
F17 => "f17",
|
||||
F18 => "f18",
|
||||
F19 => "f19",
|
||||
F20 => "f20",
|
||||
F21 => "f21",
|
||||
F22 => "f22",
|
||||
F23 => "f23",
|
||||
F24 => "f24",
|
||||
F25 => "f25",
|
||||
Fn => "fn",
|
||||
FnLock => "fn_lock",
|
||||
PrintScreen => "print_screen",
|
||||
ScrollLock => "scroll_lock",
|
||||
Pause => "pause",
|
||||
BrowserBack => "browser_back",
|
||||
BrowserFavorites => "browser_favorites",
|
||||
BrowserForward => "browser_forward",
|
||||
BrowserHome => "browser_home",
|
||||
BrowserRefresh => "browser_refresh",
|
||||
BrowserSearch => "browser_search",
|
||||
BrowserStop => "browser_stop",
|
||||
Eject => "eject",
|
||||
LaunchApp1 => "launch_app_1",
|
||||
LaunchApp2 => "launch_app_2",
|
||||
LaunchMail => "launch_mail",
|
||||
MediaPlayPause => "media_play_pause",
|
||||
MediaSelect => "media_select",
|
||||
MediaStop => "media_stop",
|
||||
MediaTrackNext => "media_track_next",
|
||||
MediaTrackPrevious => "media_track_previous",
|
||||
Power => "power",
|
||||
Sleep => "sleep",
|
||||
AudioVolumeDown => "audio_volume_down",
|
||||
AudioVolumeMute => "audio_volume_mute",
|
||||
AudioVolumeUp => "audio_volume_up",
|
||||
WakeUp => "wake_up",
|
||||
Copy => "copy",
|
||||
Cut => "cut",
|
||||
Paste => "paste",
|
||||
}
|
||||
|
||||
/// Codepoint <-> key map. Keypad entries must always be last so that
|
||||
/// `from_ascii` doesn't map them over normal numerics and other keys.
|
||||
const CODEPOINT_MAP: &[(u32, Key)] = &[
|
||||
('a' as u32, Key::KeyA),
|
||||
('b' as u32, Key::KeyB),
|
||||
('c' as u32, Key::KeyC),
|
||||
('d' as u32, Key::KeyD),
|
||||
('e' as u32, Key::KeyE),
|
||||
('f' as u32, Key::KeyF),
|
||||
('g' as u32, Key::KeyG),
|
||||
('h' as u32, Key::KeyH),
|
||||
('i' as u32, Key::KeyI),
|
||||
('j' as u32, Key::KeyJ),
|
||||
('k' as u32, Key::KeyK),
|
||||
('l' as u32, Key::KeyL),
|
||||
('m' as u32, Key::KeyM),
|
||||
('n' as u32, Key::KeyN),
|
||||
('o' as u32, Key::KeyO),
|
||||
('p' as u32, Key::KeyP),
|
||||
('q' as u32, Key::KeyQ),
|
||||
('r' as u32, Key::KeyR),
|
||||
('s' as u32, Key::KeyS),
|
||||
('t' as u32, Key::KeyT),
|
||||
('u' as u32, Key::KeyU),
|
||||
('v' as u32, Key::KeyV),
|
||||
('w' as u32, Key::KeyW),
|
||||
('x' as u32, Key::KeyX),
|
||||
('y' as u32, Key::KeyY),
|
||||
('z' as u32, Key::KeyZ),
|
||||
('0' as u32, Key::Digit0),
|
||||
('1' as u32, Key::Digit1),
|
||||
('2' as u32, Key::Digit2),
|
||||
('3' as u32, Key::Digit3),
|
||||
('4' as u32, Key::Digit4),
|
||||
('5' as u32, Key::Digit5),
|
||||
('6' as u32, Key::Digit6),
|
||||
('7' as u32, Key::Digit7),
|
||||
('8' as u32, Key::Digit8),
|
||||
('9' as u32, Key::Digit9),
|
||||
(';' as u32, Key::Semicolon),
|
||||
(' ' as u32, Key::Space),
|
||||
('\'' as u32, Key::Quote),
|
||||
(',' as u32, Key::Comma),
|
||||
('`' as u32, Key::Backquote),
|
||||
('.' as u32, Key::Period),
|
||||
('/' as u32, Key::Slash),
|
||||
('-' as u32, Key::Minus),
|
||||
('=' as u32, Key::Equal),
|
||||
('[' as u32, Key::BracketLeft),
|
||||
(']' as u32, Key::BracketRight),
|
||||
('\\' as u32, Key::Backslash),
|
||||
// Control characters
|
||||
('\t' as u32, Key::Tab),
|
||||
// Keypad entries. We just assume keypad with the numpad_ prefix
|
||||
// so that has some special meaning.
|
||||
('0' as u32, Key::Numpad0),
|
||||
('1' as u32, Key::Numpad1),
|
||||
('2' as u32, Key::Numpad2),
|
||||
('3' as u32, Key::Numpad3),
|
||||
('4' as u32, Key::Numpad4),
|
||||
('5' as u32, Key::Numpad5),
|
||||
('6' as u32, Key::Numpad6),
|
||||
('7' as u32, Key::Numpad7),
|
||||
('8' as u32, Key::Numpad8),
|
||||
('9' as u32, Key::Numpad9),
|
||||
('.' as u32, Key::NumpadDecimal),
|
||||
('/' as u32, Key::NumpadDivide),
|
||||
('*' as u32, Key::NumpadMultiply),
|
||||
('-' as u32, Key::NumpadSubtract),
|
||||
('+' as u32, Key::NumpadAdd),
|
||||
('=' as u32, Key::NumpadEqual),
|
||||
];
|
||||
|
||||
impl Key {
|
||||
/// Converts an ASCII character to a key, if possible. This returns
|
||||
/// None if the character is unknown.
|
||||
///
|
||||
/// Note that this can't distinguish between physical keys, i.e. '0'
|
||||
/// may be from the number row or the keypad, but it always maps
|
||||
/// to '.zero'.
|
||||
///
|
||||
/// This is what we want, we want people to create keybindings that
|
||||
/// are independent of the physical key.
|
||||
pub fn from_ascii(ch: u8) -> Option<Key> {
|
||||
CODEPOINT_MAP
|
||||
.iter()
|
||||
// No ASCII characters should ever map to a keypad key
|
||||
.filter(|e| !e.1.keypad())
|
||||
.find(|e| e.0 == u32::from(ch))
|
||||
.map(|e| e.1)
|
||||
}
|
||||
|
||||
/// Converts a W3C key code to a Ghostty key enum value.
|
||||
///
|
||||
/// All required W3C key codes are supported, but there are a number of
|
||||
/// non-standard key codes that are not supported. In the case the value is
|
||||
/// invalid or unsupported, this function will return None.
|
||||
pub fn from_w3c(code: &str) -> Option<Key> {
|
||||
// If the code is bigger than our buffer it can't possibly match.
|
||||
if code.len() > 128 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// First just check the whole thing lowercased, this is the simple case
|
||||
if let Some(k) = Key::from_name(&code.to_ascii_lowercase()) {
|
||||
return Some(k);
|
||||
}
|
||||
|
||||
// We need to convert FooBar to foo_bar
|
||||
let mut result = String::new();
|
||||
for (i, ch) in code.bytes().enumerate() {
|
||||
match ch {
|
||||
b'a'..=b'z' => result.push(ch as char),
|
||||
|
||||
// Caps and numbers trigger underscores
|
||||
b'A'..=b'Z' | b'0'..=b'9' => {
|
||||
if i > 0 {
|
||||
result.push('_');
|
||||
}
|
||||
result.push(ch.to_ascii_lowercase() as char);
|
||||
}
|
||||
|
||||
// We don't know of any key codes that aren't alphanumeric.
|
||||
_ => return None,
|
||||
}
|
||||
}
|
||||
|
||||
Key::from_name(&result)
|
||||
}
|
||||
|
||||
/// Converts a Ghostty key enum value to a W3C key code.
|
||||
pub fn w3c(self) -> String {
|
||||
let name = self.name();
|
||||
let mut out = String::with_capacity(name.len());
|
||||
let mut upper_next = true;
|
||||
for ch in name.chars() {
|
||||
if ch == '_' {
|
||||
upper_next = true;
|
||||
} else if upper_next {
|
||||
out.push(ch.to_ascii_uppercase());
|
||||
upper_next = false;
|
||||
} else {
|
||||
out.push(ch);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// True if this key represents a printable character.
|
||||
pub fn printable(self) -> bool {
|
||||
CODEPOINT_MAP.iter().any(|e| e.1 == self)
|
||||
}
|
||||
|
||||
/// True if this key is a modifier.
|
||||
pub fn modifier(self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Key::ShiftLeft
|
||||
| Key::ControlLeft
|
||||
| Key::AltLeft
|
||||
| Key::MetaLeft
|
||||
| Key::ShiftRight
|
||||
| Key::ControlRight
|
||||
| Key::AltRight
|
||||
| Key::MetaRight
|
||||
)
|
||||
}
|
||||
|
||||
/// Whether this key should be remappable by the operating system.
|
||||
///
|
||||
/// On certain OSes (namely Linux and the BSDs) certain keys like the
|
||||
/// functional keys are expected to be remappable by the user, such as
|
||||
/// in the very common use case of swapping the Caps Lock key with the
|
||||
/// Escape key with the XKB option `caps:swapescape`.
|
||||
///
|
||||
/// However, the way XKB implements this is by essentially acting as a
|
||||
/// software key remapper that destroys all information about the original
|
||||
/// physical key, leading to very annoying bugs like #7309 where the
|
||||
/// physical key `XKB_KEY_c` gets remapped into `XKB_KEY_Cyrillic_tse`,
|
||||
/// which causes all of our physical key handling to completely break down.
|
||||
/// _Very naughty._
|
||||
///
|
||||
/// As a compromise, given that writing system keys (§3.1.1) comprise the
|
||||
/// majority of keys that "change meaning [...] based on the current locale
|
||||
/// and keyboard layout", we allow all other keys to be remapped by default
|
||||
/// since they should be fairly harmless. We might consider making this
|
||||
/// configurable, but for now this should at least placate most people.
|
||||
pub fn should_be_remappable(self) -> bool {
|
||||
let v = self as i32;
|
||||
// "Writing System Keys" § 3.1.1 is the contiguous range
|
||||
// backquote ..= slash.
|
||||
!(Key::Backquote as i32..=Key::Slash as i32).contains(&v)
|
||||
}
|
||||
|
||||
/// Returns true if this is a keypad key.
|
||||
pub fn keypad(self) -> bool {
|
||||
self.name().starts_with("numpad_")
|
||||
}
|
||||
|
||||
/// Returns the codepoint representing this key, or None if the key is not
|
||||
/// printable.
|
||||
pub fn codepoint(self) -> Option<u32> {
|
||||
CODEPOINT_MAP.iter().find(|e| e.1 == self).map(|e| e.0)
|
||||
}
|
||||
|
||||
/// true if this key is one of the left or right versions of super (MacOS)
|
||||
/// or ctrl.
|
||||
pub fn ctrl_or_super(self) -> bool {
|
||||
// Non-darwin target.
|
||||
self == Key::ControlLeft || self == Key::ControlRight
|
||||
}
|
||||
|
||||
/// true if this key is either left or right shift.
|
||||
pub fn left_or_right_shift(self) -> bool {
|
||||
self == Key::ShiftLeft || self == Key::ShiftRight
|
||||
}
|
||||
|
||||
/// true if this key is either left or right alt.
|
||||
pub fn left_or_right_alt(self) -> bool {
|
||||
self == Key::AltLeft || self == Key::AltRight
|
||||
}
|
||||
}
|
||||
|
||||
/// This sets either "ctrl" or "super" to true (but not both)
|
||||
/// on mods depending on if the build target is Mac or not. On
|
||||
/// Mac, we default to super (i.e. super+c for copy) and on
|
||||
/// non-Mac we default to ctrl (i.e. ctrl+c for copy).
|
||||
pub fn ctrl_or_super(mods: Mods) -> Mods {
|
||||
let mut copy = mods;
|
||||
// Non-darwin target.
|
||||
copy.ctrl = true;
|
||||
copy
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn from_ascii_should_not_return_keypad_keys() {
|
||||
assert_eq!(Key::from_ascii(b'0'), Some(Key::Digit0));
|
||||
assert_eq!(Key::from_ascii(b'*'), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keypad_keys() {
|
||||
assert!(Key::Numpad0.keypad());
|
||||
assert!(!Key::Digit1.keypad());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn w3c() {
|
||||
// All our keys should convert to and from the W3C format.
|
||||
for &key in Key::ALL {
|
||||
assert_eq!(Some(key), Key::from_w3c(&key.w3c()), "{key:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn w3c_names() {
|
||||
assert_eq!(Key::KeyA.w3c(), "KeyA");
|
||||
assert_eq!(Key::ArrowUp.w3c(), "ArrowUp");
|
||||
assert_eq!(Key::Fn.w3c(), "Fn");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_be_remappable_range() {
|
||||
assert!(!Key::KeyA.should_be_remappable());
|
||||
assert!(!Key::Slash.should_be_remappable());
|
||||
assert!(Key::AltLeft.should_be_remappable());
|
||||
assert!(Key::Unidentified.should_be_remappable());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ctrl_or_super_test() {
|
||||
let m = ctrl_or_super(Mods::default());
|
||||
assert!(m.ctrl_or_super());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3161 @@
|
||||
//! Key event encoding: legacy (xterm/fixterms) and Kitty keyboard protocol
|
||||
//! (ZIG/src/input/key_encode.zig).
|
||||
|
||||
use std::io::{self, Write};
|
||||
|
||||
use ghostty_vt::kitty::key::Flags as KittyFlags;
|
||||
|
||||
use crate::{
|
||||
config::OptionAsAlt,
|
||||
function_keys::{self, CursorMode, KeypadMode, ModifyKeys},
|
||||
key::{self, Key, KeyEvent},
|
||||
kitty::{self as kitty_table, Entry as KittyEntry},
|
||||
};
|
||||
|
||||
/// Options that affect key encoding behavior. This is a mix of behavior
|
||||
/// from terminal state as well as application configuration.
|
||||
///
|
||||
/// TODO(integration): Zig has `Options.fromTerminal(*const Terminal)`; the
|
||||
/// terminal crate should construct this from its modes (`alt_esc_prefix`,
|
||||
/// `cursor_keys`, `keypad_keys`, `ignore_keypad_with_numlock`, flag
|
||||
/// `modify_other_keys_2`, active screen `kitty_keyboard.current()`).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub struct Options {
|
||||
/// Terminal DEC mode 1
|
||||
pub cursor_key_application: bool,
|
||||
|
||||
/// Terminal DEC mode 66
|
||||
pub keypad_key_application: bool,
|
||||
|
||||
/// Terminal DEC mode 1035
|
||||
pub ignore_keypad_with_numlock: bool,
|
||||
|
||||
/// Terminal DEC mode 1036
|
||||
pub alt_esc_prefix: bool,
|
||||
|
||||
/// xterm "modifyOtherKeys mode 2". Details here:
|
||||
/// https://invisible-island.net/xterm/modified-keys.html
|
||||
pub modify_other_keys_state_2: bool,
|
||||
|
||||
/// Kitty keyboard protocol flags.
|
||||
pub kitty_flags: KittyFlags,
|
||||
|
||||
/// Determines whether the "option" key on macOS is treated
|
||||
/// as "alt" or not. See the Ghostty `macos_option-as-alt` config
|
||||
/// docs for a more detailed description of why this is needed.
|
||||
pub macos_option_as_alt: OptionAsAlt,
|
||||
}
|
||||
|
||||
impl Options {
|
||||
pub const DEFAULT: Options = Options {
|
||||
cursor_key_application: false,
|
||||
keypad_key_application: false,
|
||||
ignore_keypad_with_numlock: false,
|
||||
alt_esc_prefix: false,
|
||||
modify_other_keys_state_2: false,
|
||||
kitty_flags: KittyFlags::DISABLED,
|
||||
macos_option_as_alt: OptionAsAlt::False,
|
||||
};
|
||||
}
|
||||
|
||||
/// Encode the key event to the writer in the proper format given
|
||||
/// the options. For example, this will properly encode a key press
|
||||
/// such as "ctrl+A" to Kitty format if Kitty encoding is enabled.
|
||||
///
|
||||
/// Not all key events will result in output. It is up to the caller
|
||||
/// to use a writer that can track whether any output was written if
|
||||
/// they care about that.
|
||||
pub fn encode<W: Write>(writer: &mut W, event: &KeyEvent, opts: &Options) -> io::Result<()> {
|
||||
if opts.kitty_flags.int() != 0 {
|
||||
kitty(writer, event, opts)
|
||||
} else {
|
||||
legacy(writer, event, opts)
|
||||
}
|
||||
}
|
||||
|
||||
fn is_enter_backspace_tab(k: Key) -> bool {
|
||||
matches!(k, Key::Enter | Key::Backspace | Key::Tab)
|
||||
}
|
||||
|
||||
/// Perform Kitty keyboard protocol encoding of the key event.
|
||||
fn kitty<W: Write>(writer: &mut W, event: &KeyEvent, opts: &Options) -> io::Result<()> {
|
||||
// This should never happen but we'll check anyway.
|
||||
if opts.kitty_flags.int() == 0 {
|
||||
return legacy(writer, event, opts);
|
||||
}
|
||||
|
||||
// We only processed "press" events unless report events is active
|
||||
if event.action == key::Action::Release {
|
||||
if !opts.kitty_flags.report_events {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Enter, backspace, and tab do not report release events unless "report
|
||||
// all" is set
|
||||
if !opts.kitty_flags.report_all && is_enter_backspace_tab(event.key) {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
let all_mods = event.mods;
|
||||
let effective_mods = event.effective_mods();
|
||||
let binding_mods = effective_mods.binding();
|
||||
|
||||
// Find the entry for this key in the kitty table.
|
||||
let entry_: Option<KittyEntry> = 'entry: {
|
||||
// Functional or predefined keys
|
||||
for entry in kitty_table::entries() {
|
||||
if entry.key == event.key {
|
||||
break 'entry Some(*entry);
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, we use our unicode codepoint from UTF8. We
|
||||
// always use the unshifted value.
|
||||
if event.unshifted_codepoint > 0 {
|
||||
break 'entry Some(KittyEntry {
|
||||
key: event.key,
|
||||
code: event.unshifted_codepoint,
|
||||
final_: b'u',
|
||||
modifier: false,
|
||||
});
|
||||
}
|
||||
|
||||
None
|
||||
};
|
||||
|
||||
'preprocessing: {
|
||||
// When composing, the only keys sent are plain modifiers.
|
||||
if event.composing {
|
||||
if let Some(entry) = &entry_
|
||||
&& entry.modifier
|
||||
{
|
||||
break 'preprocessing;
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// IME confirmation still sends an enter key so if we have enter
|
||||
// and UTF8 text we just send it directly since we assume that is
|
||||
// what's happening. See legacy()'s similar logic for more details
|
||||
// on how to verify this.
|
||||
if !event.utf8.is_empty() && matches!(event.key, Key::Enter | Key::Backspace) {
|
||||
// See legacy for why we handle this this way.
|
||||
if !is_control_utf8(event.utf8) {
|
||||
if event.key == Key::Backspace {
|
||||
return Ok(());
|
||||
}
|
||||
return writer.write_all(event.utf8.as_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
// If we're reporting all then we always send CSI sequences.
|
||||
if !opts.kitty_flags.report_all {
|
||||
// Quote:
|
||||
// The only exceptions are the Enter, Tab and Backspace keys which
|
||||
// still generate the same bytes as in legacy mode this is to allow the
|
||||
// user to type and execute commands in the shell such as reset after a
|
||||
// program that sets this mode crashes without clearing it.
|
||||
//
|
||||
// Quote ("report all" mode):
|
||||
// Note that all keys are reported as escape codes, including Enter,
|
||||
// Tab, Backspace etc.
|
||||
if binding_mods.empty() {
|
||||
match event.key {
|
||||
Key::Enter => return writer.write_all(b"\r"),
|
||||
Key::Tab => return writer.write_all(b"\t"),
|
||||
Key::Backspace => return writer.write_all(&[0x7F]),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Send plain-text non-modified text directly to the terminal.
|
||||
// We don't send release events because those are specially encoded.
|
||||
if !event.utf8.is_empty()
|
||||
&& binding_mods.empty()
|
||||
&& event.action != key::Action::Release
|
||||
{
|
||||
// We only do this for printable characters. We should
|
||||
// inspect the real unicode codepoint properties here but
|
||||
// the real world issue is usually control characters.
|
||||
if !event.utf8.chars().any(|c| is_control(u32::from(c))) {
|
||||
return writer.write_all(event.utf8.as_bytes());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let Some(entry) = entry_ else {
|
||||
// No entry found. If we have UTF-8 text this is a pure text event
|
||||
// (e.g. composed/IME text), so send it as-is so programs can
|
||||
// still receive it.
|
||||
if !event.utf8.is_empty() {
|
||||
return writer.write_all(event.utf8.as_bytes());
|
||||
}
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
// If this is just a modifier we require "report all" to send the sequence.
|
||||
if entry.modifier && !opts.kitty_flags.report_all {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let seq = {
|
||||
let mut seq = KittySequence {
|
||||
key: entry.code,
|
||||
final_: entry.final_,
|
||||
mods: KittyMods::from_input(event.action, event.key, all_mods),
|
||||
event: KittyEvent::None,
|
||||
alternates: [None, None],
|
||||
text: "",
|
||||
};
|
||||
|
||||
if opts.kitty_flags.report_events {
|
||||
seq.event = match event.action {
|
||||
key::Action::Press => KittyEvent::Press,
|
||||
key::Action::Release => KittyEvent::Release,
|
||||
key::Action::Repeat => KittyEvent::Repeat,
|
||||
};
|
||||
}
|
||||
|
||||
'alternates: {
|
||||
if !opts.kitty_flags.report_alternates {
|
||||
break 'alternates;
|
||||
}
|
||||
|
||||
// Break early if this is a control key
|
||||
if is_control(seq.key) {
|
||||
break 'alternates;
|
||||
}
|
||||
|
||||
let mut it = event.utf8.chars();
|
||||
|
||||
// If we have a codepoint in our UTF-8 sequence, then we can
|
||||
// report the shifted version.
|
||||
if let Some(cp1) = it.next().map(u32::from) {
|
||||
// Set the first alternate (shifted version)
|
||||
if cp1 != seq.key && seq.mods.shift {
|
||||
seq.alternates[0] = Some(cp1);
|
||||
}
|
||||
|
||||
// We want to know if there are additional codepoints because
|
||||
// our logic below depends on the utf8 being a single codepoint.
|
||||
let has_cp2 = it.next().is_some();
|
||||
|
||||
// Set the base layout key. We only report this if this codepoint
|
||||
// differs from our pressed key.
|
||||
if let Some(base) = event.key.codepoint()
|
||||
&& base != seq.key
|
||||
&& (cp1 != base && !has_cp2)
|
||||
{
|
||||
seq.alternates[1] = Some(base);
|
||||
}
|
||||
} else {
|
||||
// No UTF-8 so we can't report a shifted key but we can still
|
||||
// report a base layout key.
|
||||
if let Some(base) = event.key.codepoint()
|
||||
&& base != seq.key
|
||||
{
|
||||
seq.alternates[1] = Some(base);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if opts.kitty_flags.report_associated && seq.event != KittyEvent::Release {
|
||||
// Determine if the Alt modifier should be treated as an actual
|
||||
// modifier (in which case it prevents associated text) or as
|
||||
// the macOS Option key, which does not prevent associated text.
|
||||
// (Non-macOS: always true.)
|
||||
let alt_prevents_text = true;
|
||||
|
||||
if !seq.mods.prevents_text(alt_prevents_text) {
|
||||
seq.text = event.utf8;
|
||||
}
|
||||
}
|
||||
|
||||
seq
|
||||
};
|
||||
|
||||
seq.encode(writer)
|
||||
}
|
||||
|
||||
/// Perform legacy encoding of the key event. "Legacy" in this case
|
||||
/// is referring to the behavior of traditional terminals, plus
|
||||
/// xterm's `modifyOtherKeys`, plus Paul Evans's "fixterms" spec.
|
||||
/// These together combine the legacy protocol because they're all
|
||||
/// meant to be extensions that do not change any existing behavior
|
||||
/// and therefore safe to combine.
|
||||
fn legacy<W: Write>(writer: &mut W, event: &KeyEvent, opts: &Options) -> io::Result<()> {
|
||||
let all_mods = event.mods;
|
||||
let effective_mods = event.effective_mods();
|
||||
let binding_mods = effective_mods.binding();
|
||||
|
||||
// Legacy encoding only does press/repeat
|
||||
if event.action != key::Action::Press && event.action != key::Action::Repeat {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// If we're in a dead key state then we never emit a sequence.
|
||||
if event.composing {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// If we match a PC style function key then that is our result.
|
||||
if let Some(sequence) = pc_style_function_key(
|
||||
event.key,
|
||||
all_mods,
|
||||
opts.cursor_key_application,
|
||||
opts.keypad_key_application,
|
||||
opts.ignore_keypad_with_numlock,
|
||||
opts.modify_other_keys_state_2,
|
||||
) {
|
||||
// If we have UTF-8 text, then we never emit PC style function
|
||||
// keys. Many function keys (escape, enter, backspace) have
|
||||
// a specific meaning when dead keys are active and so we don't
|
||||
// want to send that to the terminal. Examples:
|
||||
//
|
||||
// - Japanese: escape clears the dead key state
|
||||
// - Korean: escape commits the dead key state
|
||||
// - Korean: backspace should delete a single preedit char
|
||||
//
|
||||
let mut skip_pc_style = false;
|
||||
if !event.utf8.is_empty()
|
||||
&& matches!(event.key, Key::Backspace | Key::Enter | Key::Escape)
|
||||
// We want to ignore control characters. This is because
|
||||
// some apprts (macOS) will send control characters as
|
||||
// UTF-8 encodings and we handle that manually.
|
||||
&& !is_control_utf8(event.utf8)
|
||||
{
|
||||
// Backspace encodes nothing because we modified IME.
|
||||
// Enter/escape don't encode the PC-style encoding
|
||||
// because we want to encode committed text.
|
||||
if event.key == Key::Backspace {
|
||||
return Ok(());
|
||||
}
|
||||
skip_pc_style = true;
|
||||
}
|
||||
|
||||
if !skip_pc_style {
|
||||
return writer.write_all(sequence.as_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
// If we match a control sequence, we output that directly. For
|
||||
// ctrlSeq we have to use all mods because we want it to only
|
||||
// match ctrl+<char>.
|
||||
if let Some(ch) = ctrl_seq(event.key, event.utf8, event.unshifted_codepoint, all_mods) {
|
||||
// C0 sequences support alt-as-esc prefixing.
|
||||
if binding_mods.alt {
|
||||
return writer.write_all(&[0x1B, ch]);
|
||||
}
|
||||
|
||||
return writer.write_all(&[ch]);
|
||||
}
|
||||
|
||||
// If we have no UTF8 text then the only possibility is the
|
||||
// alt-prefix handling of unshifted codepoints... so we process that.
|
||||
let utf8 = event.utf8;
|
||||
if utf8.is_empty() {
|
||||
if let Some(byte) = legacy_alt_prefix(event, binding_mods, all_mods, opts) {
|
||||
writer.write_all(&[0x1B, byte])?;
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// In modify other keys state 2, we send the CSI 27 sequence
|
||||
// for any char with a modifier. Ctrl sequences like Ctrl+a
|
||||
// are already handled above.
|
||||
if opts.modify_other_keys_state_2 {
|
||||
let mut it = utf8.chars();
|
||||
// We only do this if we have a single codepoint. There shouldn't
|
||||
// ever be a multi-codepoint sequence that triggers this.
|
||||
if let (Some(codepoint), None) = (it.next().map(u32::from), it.clone().next()) {
|
||||
// The mods we encode for this are just the binding mods (shift, ctrl,
|
||||
// super, alt unless it is actually option).
|
||||
let mods = event.mods.binding();
|
||||
|
||||
// This copies xterm's `ModifyOtherKeys` function that returns
|
||||
// whether modify other keys should be encoded for the given
|
||||
// input.
|
||||
let should_modify = 'should_modify: {
|
||||
// xterm IsControlInput
|
||||
if (0x40..=0x7F).contains(&codepoint) {
|
||||
break 'should_modify true;
|
||||
}
|
||||
|
||||
// If we have anything other than shift pressed, encode.
|
||||
let mut mods_no_shift = mods;
|
||||
mods_no_shift.shift = false;
|
||||
if !mods_no_shift.empty() {
|
||||
break 'should_modify true;
|
||||
}
|
||||
|
||||
// We only have shift pressed. We only allow space.
|
||||
if codepoint == u32::from(' ') {
|
||||
break 'should_modify true;
|
||||
}
|
||||
|
||||
// This logic isn't complete but I don't fully understand
|
||||
// the rest so I'm going to wait until we can have a
|
||||
// reasonable test scenario.
|
||||
false
|
||||
};
|
||||
|
||||
if should_modify {
|
||||
for (modset, code) in function_keys::MODIFIERS.iter().zip(2..) {
|
||||
if !mods.equal(*modset) {
|
||||
continue;
|
||||
}
|
||||
return write!(writer, "\x1B[27;{code};{codepoint}~");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Let's see if we should apply fixterms to this codepoint.
|
||||
// At this stage of key processing, we only need to apply fixterms
|
||||
// to unicode codepoints if we have ctrl set.
|
||||
if event.mods.ctrl {
|
||||
'csiu: {
|
||||
// Important: we want to use the original mods here, not the
|
||||
// effective mods. The fixterms spec states the shifted chars
|
||||
// should be sent uppercase but Kitty changes that behavior
|
||||
// so we'll send all the mods.
|
||||
let mut csi_u_mods = CsiUMods::from_input(event.mods);
|
||||
|
||||
// Get our codepoint. If we have more than one codepoint this
|
||||
// can't be valid CSIu.
|
||||
let mut it = event.utf8.chars();
|
||||
let Some(first) = it.next() else {
|
||||
break 'csiu;
|
||||
};
|
||||
if it.next().is_some() {
|
||||
break 'csiu;
|
||||
}
|
||||
let mut ch = u32::from(first);
|
||||
|
||||
// If our character is A to Z and we have shift set, then
|
||||
// we lowercase it. This is a Kitty-specific behavior that
|
||||
// we choose to follow and diverge from the fixterms spec.
|
||||
// This makes it easier for programs to detect shifted letters
|
||||
// for keybindings and is not just theoretical but used by
|
||||
// real programs.
|
||||
if (u32::from('A')..=u32::from('Z')).contains(&ch) && csi_u_mods.shift {
|
||||
// We want to rely on apprt to send us the correct
|
||||
// unshifted codepoint...
|
||||
ch += 32;
|
||||
}
|
||||
|
||||
// If our unshifted codepoint is identical to the shifted
|
||||
// then we consider shift. Otherwise, we do not because the
|
||||
// shift key was used to obtain the character. This is specified
|
||||
// by fixterms.
|
||||
if event.unshifted_codepoint != ch {
|
||||
csi_u_mods.shift = false;
|
||||
}
|
||||
|
||||
return write!(writer, "\x1B[{};{}u", ch, csi_u_mods.seq_int());
|
||||
}
|
||||
}
|
||||
|
||||
// If we have alt-pressed and alt-esc-prefix is enabled, then
|
||||
// we need to prefix the utf8 sequence with an esc.
|
||||
if let Some(byte) = legacy_alt_prefix(event, binding_mods, all_mods, opts) {
|
||||
return writer.write_all(&[0x1B, byte]);
|
||||
}
|
||||
|
||||
// (On macOS, command+keys do not encode text. On Linux we continue to
|
||||
// encode text because it is typical.)
|
||||
|
||||
writer.write_all(utf8.as_bytes())
|
||||
}
|
||||
|
||||
fn legacy_alt_prefix(
|
||||
event: &KeyEvent,
|
||||
binding_mods: key::Mods,
|
||||
_mods: key::Mods,
|
||||
opts: &Options,
|
||||
) -> Option<u8> {
|
||||
// This only takes effect with alt pressed
|
||||
if !binding_mods.alt || !opts.alt_esc_prefix {
|
||||
return None;
|
||||
}
|
||||
|
||||
// (macOS option-as-alt handling does not apply on Linux.)
|
||||
|
||||
// Otherwise, we require utf8 to already have the byte represented.
|
||||
let utf8 = event.utf8.as_bytes();
|
||||
if utf8.len() == 1 {
|
||||
return Some(utf8[0]);
|
||||
}
|
||||
|
||||
// If UTF8 isn't set, we will allow unshifted codepoints through.
|
||||
if event.unshifted_codepoint > 0
|
||||
&& let Ok(byte) = u8::try_from(event.unshifted_codepoint)
|
||||
{
|
||||
return Some(byte);
|
||||
}
|
||||
|
||||
// Else, we can't figure out the byte to alt-prefix so we
|
||||
// exit this handling.
|
||||
None
|
||||
}
|
||||
|
||||
/// Determines whether the key should be encoded in the xterm
|
||||
/// "PC-style Function Key" syntax (roughly). This is a hardcoded
|
||||
/// table of keys and modifiers that result in a specific sequence.
|
||||
fn pc_style_function_key(
|
||||
keyval: Key,
|
||||
mods: key::Mods,
|
||||
cursor_key_application: bool,
|
||||
keypad_key_application_req: bool,
|
||||
ignore_keypad_with_numlock: bool,
|
||||
modify_other_keys: bool, // True if state 2
|
||||
) -> Option<&'static str> {
|
||||
// We only want binding-sensitive mods because lock keys
|
||||
// and directional modifiers (left/right) don't matter for
|
||||
// pc-style function keys.
|
||||
let mods_int = mods.binding().int();
|
||||
|
||||
// Keypad application keymode isn't super straightforward.
|
||||
// On xterm, in VT220 mode, numlock alone is enough to trigger
|
||||
// application mode. But in more modern modes, numlock is
|
||||
// ignored by default via mode 1035 (default true). If mode
|
||||
// 1035 is on, we always are in numerical keypad mode. If
|
||||
// mode 1035 is off, we are in application mode if the
|
||||
// proper numlock state is pressed. The numlock state is implicitly
|
||||
// determined based on the keycode sent (i.e. 1 with numlock
|
||||
// on will be kp_end).
|
||||
//
|
||||
// If we're ignoring keypad then this is always false.
|
||||
// In other words, we're always in numerical keypad mode.
|
||||
let keypad_key_application = if ignore_keypad_with_numlock {
|
||||
false
|
||||
} else {
|
||||
keypad_key_application_req
|
||||
};
|
||||
|
||||
for entry in function_keys::keys(keyval) {
|
||||
match entry.cursor {
|
||||
CursorMode::Any => {}
|
||||
CursorMode::Normal => {
|
||||
if cursor_key_application {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
CursorMode::Application => {
|
||||
if !cursor_key_application {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match entry.keypad {
|
||||
KeypadMode::Any => {}
|
||||
KeypadMode::Normal => {
|
||||
if keypad_key_application {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
KeypadMode::Application => {
|
||||
if !keypad_key_application {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match entry.modify_other_keys {
|
||||
ModifyKeys::Any => {}
|
||||
ModifyKeys::Set => {
|
||||
if modify_other_keys {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
ModifyKeys::SetOther => {
|
||||
if !modify_other_keys {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let entry_mods_int = entry.mods.int();
|
||||
if entry_mods_int == 0 {
|
||||
if mods_int != 0 && !entry.mods_empty_is_any {
|
||||
continue;
|
||||
}
|
||||
// mods are either empty, or empty means any so we allow it.
|
||||
} else if entry_mods_int != mods_int {
|
||||
// any set mods require an exact match
|
||||
continue;
|
||||
}
|
||||
|
||||
return Some(entry.sequence.as_str());
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns the C0 byte for the key event if it should be used.
|
||||
/// This converts a key event into the expected terminal behavior
|
||||
/// such as Ctrl+C turning into 0x03, amongst many other translations.
|
||||
///
|
||||
/// This will return None if the key event should not be converted
|
||||
/// into a C0 byte. There are many cases for this and you should read
|
||||
/// the source code to understand them.
|
||||
fn ctrl_seq(logical_key: Key, utf8: &str, unshifted_codepoint: u32, mods: key::Mods) -> Option<u8> {
|
||||
let ctrl_only = key::Mods {
|
||||
ctrl: true,
|
||||
..key::Mods::default()
|
||||
}
|
||||
.int();
|
||||
|
||||
// If ctrl is not pressed then we never do anything.
|
||||
if !mods.ctrl {
|
||||
return None;
|
||||
}
|
||||
|
||||
// We need to only get binding modifiers so we strip lock
|
||||
// keys, sides, etc.
|
||||
let mut unset_mods = mods.binding();
|
||||
|
||||
// Remove alt from our modifiers because it does not impact whether
|
||||
// we are generating a ctrl sequence and we handle the ESC-prefix
|
||||
// logic separately.
|
||||
unset_mods.alt = false;
|
||||
|
||||
let mut ch: u8 = 'ch: {
|
||||
// If we have exactly one UTF8 byte, we assume that is the
|
||||
// character we want to convert to a C0 byte.
|
||||
if utf8.len() == 1 {
|
||||
break 'ch utf8.as_bytes()[0];
|
||||
}
|
||||
|
||||
// If we have a logical key that maps to a single byte
|
||||
// printable character, we use that. History to explain this:
|
||||
// this was added to support cyrillic keyboard layouts such
|
||||
// as Russian and Mongolian. These layouts have a `c` key that
|
||||
// maps to U+0441 (cyrillic small letter "c") but every
|
||||
// terminal I've tested encodes this as ctrl+c.
|
||||
if let Some(cp) = logical_key.codepoint()
|
||||
&& let Ok(byte) = u8::try_from(cp)
|
||||
{
|
||||
// For this specific case, we only map to the key if
|
||||
// we have exactly ctrl pressed. This is because shift
|
||||
// would modify the key and we don't know how to do that
|
||||
// properly here (don't have the layout). And we want
|
||||
// to encode shift as CSIu.
|
||||
if unset_mods.int() != ctrl_only {
|
||||
return None;
|
||||
}
|
||||
break 'ch byte;
|
||||
}
|
||||
|
||||
// Otherwise we don't have a character to convert that
|
||||
// we can reliably map to a C0 byte.
|
||||
return None;
|
||||
};
|
||||
|
||||
// Remove shift if we have something outside of the US letter
|
||||
// range. This is so that characters such as `ctrl+shift+-`
|
||||
// generate the correct ctrl-seq (used by emacs).
|
||||
if unset_mods.shift && !ch.is_ascii_uppercase() {
|
||||
// Special case for fixterms awkward case as specified.
|
||||
if ch != b'@' {
|
||||
unset_mods.shift = false;
|
||||
}
|
||||
}
|
||||
|
||||
// If the character is uppercase, we convert it to lowercase. We
|
||||
// rely on the unshifted codepoint to do this. This handles
|
||||
// the scenario where we have caps lock pressed. Note that
|
||||
// shifted characters are handled above, if we are just pressing
|
||||
// shift then the ctrl-only check will fail later and we won't
|
||||
// ctrl-seq encode.
|
||||
if ch.is_ascii_uppercase()
|
||||
&& unshifted_codepoint > 0
|
||||
&& let Ok(byte) = u8::try_from(unshifted_codepoint)
|
||||
{
|
||||
ch = byte;
|
||||
}
|
||||
|
||||
// An additional note on caps lock and shift interaction.
|
||||
// If we have caps lock set and an ASCII letter is pressed,
|
||||
// we lowercase it (above). If we have only control pressed,
|
||||
// we process it as a ctrl seq. For example ctrl+M with caps
|
||||
// lock but no shift will encode as 0x0D.
|
||||
//
|
||||
// But, if you press ctrl+shift+m, this will not encode as a
|
||||
// ctrl-seq and falls through to CSIu encoding. This lets programs
|
||||
// detect the difference between ctrl+M and ctrl+shift+M. This
|
||||
// diverges from the fixterms "spec" and most terminals. This
|
||||
// only matches Kitty in behavior. But I believe this is a
|
||||
// justified divergence because it's a useful distinction.
|
||||
|
||||
// After unsetting, we only continue if we have ONLY control set.
|
||||
if unset_mods.int() != ctrl_only {
|
||||
return None;
|
||||
}
|
||||
|
||||
// From Kitty's key encoding logic. I tried to discern the exact
|
||||
// behavior across different terminals but it's not clear, so I'm
|
||||
// just going to repeat what Kitty does.
|
||||
match ch {
|
||||
b' ' => Some(0),
|
||||
b'/' => Some(31),
|
||||
b'0' => Some(48),
|
||||
b'1' => Some(49),
|
||||
b'2' => Some(0),
|
||||
b'3' => Some(27),
|
||||
b'4' => Some(28),
|
||||
b'5' => Some(29),
|
||||
b'6' => Some(30),
|
||||
b'7' => Some(31),
|
||||
b'8' => Some(127),
|
||||
b'9' => Some(57),
|
||||
b'?' => Some(127),
|
||||
b'@' => Some(0),
|
||||
b'\\' => Some(28),
|
||||
b']' => Some(29),
|
||||
b'^' => Some(30),
|
||||
b'_' => Some(31),
|
||||
b'a' => Some(1),
|
||||
b'b' => Some(2),
|
||||
b'c' => Some(3),
|
||||
b'd' => Some(4),
|
||||
b'e' => Some(5),
|
||||
b'f' => Some(6),
|
||||
b'g' => Some(7),
|
||||
b'h' => Some(8),
|
||||
b'j' => Some(10),
|
||||
b'k' => Some(11),
|
||||
b'l' => Some(12),
|
||||
b'n' => Some(14),
|
||||
b'o' => Some(15),
|
||||
b'p' => Some(16),
|
||||
b'q' => Some(17),
|
||||
b'r' => Some(18),
|
||||
b's' => Some(19),
|
||||
b't' => Some(20),
|
||||
b'u' => Some(21),
|
||||
b'v' => Some(22),
|
||||
b'w' => Some(23),
|
||||
b'x' => Some(24),
|
||||
b'y' => Some(25),
|
||||
b'z' => Some(26),
|
||||
b'~' => Some(30),
|
||||
|
||||
// These are purposely NOT handled here because of the fixterms
|
||||
// specification: https://www.leonerd.org.uk/hacks/fixterms/
|
||||
// These are processed as CSI u.
|
||||
// 'i' => 0x09,
|
||||
// 'm' => 0x0D,
|
||||
// '[' => 0x1B,
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if this is an ASCII control character, matches libc implementation.
|
||||
fn is_control(cp: u32) -> bool {
|
||||
cp < 0x20 || cp == 0x7F
|
||||
}
|
||||
|
||||
/// Returns true if this string is comprised of a single
|
||||
/// control character. This returns false for multi-byte strings.
|
||||
fn is_control_utf8(s: &str) -> bool {
|
||||
s.len() == 1 && is_control(u32::from(s.as_bytes()[0]))
|
||||
}
|
||||
|
||||
/// This is the bitmask for fixterm CSI u modifiers.
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
struct CsiUMods {
|
||||
shift: bool,
|
||||
alt: bool,
|
||||
ctrl: bool,
|
||||
}
|
||||
|
||||
impl CsiUMods {
|
||||
/// Convert an input mods value into the CSI u mods value.
|
||||
fn from_input(mods: key::Mods) -> CsiUMods {
|
||||
CsiUMods {
|
||||
shift: mods.shift,
|
||||
alt: mods.alt,
|
||||
ctrl: mods.ctrl,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the raw int value of this packed struct.
|
||||
fn int(self) -> u8 {
|
||||
u8::from(self.shift) | u8::from(self.alt) << 1 | u8::from(self.ctrl) << 2
|
||||
}
|
||||
|
||||
/// Returns the integer value sent as part of the CSI u sequence.
|
||||
/// This adds 1 to the bitmask value as described in the spec.
|
||||
fn seq_int(self) -> u8 {
|
||||
self.int() + 1
|
||||
}
|
||||
}
|
||||
|
||||
/// This is the bitfields for Kitty modifiers.
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
#[allow(dead_code)]
|
||||
struct KittyMods {
|
||||
shift: bool,
|
||||
alt: bool,
|
||||
ctrl: bool,
|
||||
super_: bool,
|
||||
hyper: bool,
|
||||
meta: bool,
|
||||
caps_lock: bool,
|
||||
num_lock: bool,
|
||||
}
|
||||
|
||||
impl KittyMods {
|
||||
/// Convert an input mods value into the CSI u mods value.
|
||||
fn from_input(_action: key::Action, _k: Key, mods: key::Mods) -> KittyMods {
|
||||
KittyMods {
|
||||
shift: mods.shift,
|
||||
alt: mods.alt,
|
||||
ctrl: mods.ctrl,
|
||||
super_: mods.super_,
|
||||
hyper: false,
|
||||
meta: false,
|
||||
caps_lock: mods.caps_lock,
|
||||
num_lock: mods.num_lock,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the modifiers prevent printable text.
|
||||
///
|
||||
/// The alt_prevents_text parameter determines whether or not the Alt
|
||||
/// modifier prevents printable text. On Linux, this is always true. On
|
||||
/// macOS, this is only true if macos-option-as-alt is set.
|
||||
fn prevents_text(self, alt_prevents_text: bool) -> bool {
|
||||
(self.alt && alt_prevents_text) || self.ctrl || self.super_ || self.hyper || self.meta
|
||||
}
|
||||
|
||||
/// Returns the raw int value of this packed struct.
|
||||
fn int(self) -> u16 {
|
||||
u16::from(self.shift)
|
||||
| u16::from(self.alt) << 1
|
||||
| u16::from(self.ctrl) << 2
|
||||
| u16::from(self.super_) << 3
|
||||
| u16::from(self.hyper) << 4
|
||||
| u16::from(self.meta) << 5
|
||||
| u16::from(self.caps_lock) << 6
|
||||
| u16::from(self.num_lock) << 7
|
||||
}
|
||||
|
||||
/// Returns the integer value sent as part of the Kitty sequence.
|
||||
/// This adds 1 to the bitmask value as described in the spec.
|
||||
fn seq_int(self) -> u16 {
|
||||
self.int() + 1
|
||||
}
|
||||
}
|
||||
|
||||
/// Values for the event code (see "event-type" in the KittySequence comment).
|
||||
/// Note that Kitty omits the ":1" for the press event but other
|
||||
/// terminals include it. We'll include it.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum KittyEvent {
|
||||
None = 0,
|
||||
Press = 1,
|
||||
Repeat = 2,
|
||||
Release = 3,
|
||||
}
|
||||
|
||||
/// Represents a kitty key sequence and has helpers for encoding it.
|
||||
/// The sequence from the Kitty specification:
|
||||
///
|
||||
/// CSI unicode-key-code:alternate-key-codes ; modifiers:event-type ; text-as-codepoints u
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct KittySequence<'a> {
|
||||
key: u32,
|
||||
final_: u8,
|
||||
mods: KittyMods,
|
||||
event: KittyEvent,
|
||||
alternates: [Option<u32>; 2],
|
||||
text: &'a str,
|
||||
}
|
||||
|
||||
impl KittySequence<'_> {
|
||||
fn encode<W: Write>(&self, writer: &mut W) -> io::Result<()> {
|
||||
if self.final_ == b'u' || self.final_ == b'~' {
|
||||
return self.encode_full(writer);
|
||||
}
|
||||
self.encode_special(writer)
|
||||
}
|
||||
|
||||
fn encode_full<W: Write>(&self, writer: &mut W) -> io::Result<()> {
|
||||
// Key section
|
||||
write!(writer, "\x1B[{}", self.key)?;
|
||||
// Write our alternates
|
||||
if let Some(shifted) = self.alternates[0] {
|
||||
write!(writer, ":{shifted}")?;
|
||||
}
|
||||
if let Some(base) = self.alternates[1] {
|
||||
if self.alternates[0].is_none() {
|
||||
write!(writer, "::{base}")?;
|
||||
} else {
|
||||
write!(writer, ":{base}")?;
|
||||
}
|
||||
}
|
||||
|
||||
// Mods and events section
|
||||
let mods = self.mods.seq_int();
|
||||
let mut emit_prior = false;
|
||||
if self.event != KittyEvent::None && self.event != KittyEvent::Press {
|
||||
write!(writer, ";{}:{}", mods, self.event as u8)?;
|
||||
emit_prior = true;
|
||||
} else if mods > 1 {
|
||||
write!(writer, ";{mods}")?;
|
||||
emit_prior = true;
|
||||
}
|
||||
|
||||
// Text section
|
||||
if !self.text.is_empty() {
|
||||
let mut count = 0;
|
||||
for c in self.text.chars() {
|
||||
let cp = u32::from(c);
|
||||
// If the codepoint is non-printable ASCII character, skip.
|
||||
if is_control(cp) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// We need to add our ";". We need to add two if we didn't emit
|
||||
// the modifier section. We only do this initially.
|
||||
if count == 0 {
|
||||
if !emit_prior {
|
||||
writer.write_all(b";")?;
|
||||
}
|
||||
writer.write_all(b";")?;
|
||||
} else {
|
||||
writer.write_all(b":")?;
|
||||
}
|
||||
|
||||
write!(writer, "{cp}")?;
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
writer.write_all(&[self.final_])
|
||||
}
|
||||
|
||||
fn encode_special<W: Write>(&self, writer: &mut W) -> io::Result<()> {
|
||||
let mods = self.mods.seq_int();
|
||||
let f = self.final_ as char;
|
||||
if self.event != KittyEvent::None {
|
||||
return write!(writer, "\x1B[1;{}:{}{}", mods, self.event as u8, f);
|
||||
}
|
||||
|
||||
if mods > 1 {
|
||||
return write!(writer, "\x1B[1;{mods}{f}");
|
||||
}
|
||||
|
||||
write!(writer, "\x1B[{f}")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::key_mods::{ModSide, Mods, Side};
|
||||
|
||||
#[test]
|
||||
fn kitty_plain_text() {
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
kitty(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::KeyA,
|
||||
mods: Mods { ..Mods::default() },
|
||||
utf8: "abcd",
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
kitty_flags: KittyFlags {
|
||||
disambiguate: true,
|
||||
..KittyFlags::DISABLED
|
||||
},
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"abcd");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kitty_repeat_with_just_disambiguate() {
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
kitty(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::KeyA,
|
||||
action: key::Action::Repeat,
|
||||
mods: Mods { ..Mods::default() },
|
||||
utf8: "a",
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
kitty_flags: KittyFlags {
|
||||
disambiguate: true,
|
||||
..KittyFlags::DISABLED
|
||||
},
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"a");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kitty_enter_backspace_tab() {
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
kitty(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::Enter,
|
||||
mods: Mods { ..Mods::default() },
|
||||
utf8: "",
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
kitty_flags: KittyFlags {
|
||||
disambiguate: true,
|
||||
..KittyFlags::DISABLED
|
||||
},
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"\x0d");
|
||||
}
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
kitty(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::Backspace,
|
||||
mods: Mods { ..Mods::default() },
|
||||
utf8: "",
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
kitty_flags: KittyFlags {
|
||||
disambiguate: true,
|
||||
..KittyFlags::DISABLED
|
||||
},
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"\x7f");
|
||||
}
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
kitty(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::Tab,
|
||||
mods: Mods { ..Mods::default() },
|
||||
utf8: "",
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
kitty_flags: KittyFlags {
|
||||
disambiguate: true,
|
||||
..KittyFlags::DISABLED
|
||||
},
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"\x09");
|
||||
}
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
kitty(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
action: key::Action::Release,
|
||||
key: Key::Enter,
|
||||
mods: Mods { ..Mods::default() },
|
||||
utf8: "",
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
kitty_flags: KittyFlags {
|
||||
disambiguate: true,
|
||||
report_events: true,
|
||||
..KittyFlags::DISABLED
|
||||
},
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"");
|
||||
}
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
kitty(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
action: key::Action::Release,
|
||||
key: Key::Backspace,
|
||||
mods: Mods { ..Mods::default() },
|
||||
utf8: "",
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
kitty_flags: KittyFlags {
|
||||
disambiguate: true,
|
||||
report_events: true,
|
||||
..KittyFlags::DISABLED
|
||||
},
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"");
|
||||
}
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
kitty(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
action: key::Action::Release,
|
||||
key: Key::Tab,
|
||||
mods: Mods { ..Mods::default() },
|
||||
utf8: "",
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
kitty_flags: KittyFlags {
|
||||
disambiguate: true,
|
||||
report_events: true,
|
||||
..KittyFlags::DISABLED
|
||||
},
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"");
|
||||
}
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
kitty(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
action: key::Action::Release,
|
||||
key: Key::Enter,
|
||||
mods: Mods { ..Mods::default() },
|
||||
utf8: "",
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
kitty_flags: KittyFlags {
|
||||
disambiguate: true,
|
||||
report_events: true,
|
||||
report_all: true,
|
||||
..KittyFlags::DISABLED
|
||||
},
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"\x1b[13;1:3u");
|
||||
}
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
kitty(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
action: key::Action::Release,
|
||||
key: Key::Backspace,
|
||||
mods: Mods { ..Mods::default() },
|
||||
utf8: "",
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
kitty_flags: KittyFlags {
|
||||
disambiguate: true,
|
||||
report_events: true,
|
||||
report_all: true,
|
||||
..KittyFlags::DISABLED
|
||||
},
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"\x1b[127;1:3u");
|
||||
}
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
kitty(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
action: key::Action::Release,
|
||||
key: Key::Tab,
|
||||
mods: Mods { ..Mods::default() },
|
||||
utf8: "",
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
kitty_flags: KittyFlags {
|
||||
disambiguate: true,
|
||||
report_events: true,
|
||||
report_all: true,
|
||||
..KittyFlags::DISABLED
|
||||
},
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"\x1b[9;1:3u");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kitty_shift_backspace_emits_csi_u() {
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
kitty(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::Backspace,
|
||||
mods: Mods {
|
||||
shift: true,
|
||||
..Mods::default()
|
||||
},
|
||||
utf8: "",
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
kitty_flags: KittyFlags {
|
||||
disambiguate: true,
|
||||
..KittyFlags::DISABLED
|
||||
},
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"\x1b[127;2u");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kitty_shift_enter_emits_csi_u() {
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
kitty(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::Enter,
|
||||
mods: Mods {
|
||||
shift: true,
|
||||
..Mods::default()
|
||||
},
|
||||
utf8: "",
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
kitty_flags: KittyFlags {
|
||||
disambiguate: true,
|
||||
..KittyFlags::DISABLED
|
||||
},
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"\x1b[13;2u");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kitty_shift_tab_emits_csi_u() {
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
kitty(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::Tab,
|
||||
mods: Mods {
|
||||
shift: true,
|
||||
..Mods::default()
|
||||
},
|
||||
utf8: "",
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
kitty_flags: KittyFlags {
|
||||
disambiguate: true,
|
||||
..KittyFlags::DISABLED
|
||||
},
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"\x1b[9;2u");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kitty_enter_with_all_flags() {
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
kitty(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::Enter,
|
||||
mods: Mods { ..Mods::default() },
|
||||
utf8: "",
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
kitty_flags: KittyFlags {
|
||||
disambiguate: true,
|
||||
report_events: true,
|
||||
report_alternates: true,
|
||||
report_all: true,
|
||||
report_associated: true,
|
||||
},
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w[1..], b"[13u");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kitty_ctrl_with_all_flags() {
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
kitty(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::ControlLeft,
|
||||
mods: Mods {
|
||||
ctrl: true,
|
||||
..Mods::default()
|
||||
},
|
||||
utf8: "",
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
kitty_flags: KittyFlags {
|
||||
disambiguate: true,
|
||||
report_events: true,
|
||||
report_alternates: true,
|
||||
report_all: true,
|
||||
report_associated: true,
|
||||
},
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w[1..], b"[57442;5u");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kitty_ctrl_release_with_ctrl_mod_set() {
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
kitty(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
action: key::Action::Release,
|
||||
key: Key::ControlLeft,
|
||||
mods: Mods {
|
||||
ctrl: true,
|
||||
..Mods::default()
|
||||
},
|
||||
utf8: "",
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
kitty_flags: KittyFlags {
|
||||
disambiguate: true,
|
||||
report_events: true,
|
||||
report_alternates: true,
|
||||
report_all: true,
|
||||
report_associated: true,
|
||||
},
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w[1..], b"[57442;5:3u");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kitty_delete() {
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
kitty(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::Delete,
|
||||
mods: Mods { ..Mods::default() },
|
||||
utf8: "\u{7f}",
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
kitty_flags: KittyFlags {
|
||||
disambiguate: true,
|
||||
..KittyFlags::DISABLED
|
||||
},
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"\x1b[3~");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kitty_composing_with_no_modifier() {
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
kitty(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::KeyA,
|
||||
mods: Mods {
|
||||
shift: true,
|
||||
..Mods::default()
|
||||
},
|
||||
composing: true,
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
kitty_flags: KittyFlags {
|
||||
disambiguate: true,
|
||||
..KittyFlags::DISABLED
|
||||
},
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kitty_composing_with_modifier() {
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
kitty(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::ShiftLeft,
|
||||
mods: Mods {
|
||||
shift: true,
|
||||
..Mods::default()
|
||||
},
|
||||
composing: true,
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
kitty_flags: KittyFlags {
|
||||
disambiguate: true,
|
||||
report_all: true,
|
||||
..KittyFlags::DISABLED
|
||||
},
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"\x1b[57441;2u");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kitty_composed_text_with_report_all() {
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
kitty(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::Unidentified,
|
||||
mods: Mods { ..Mods::default() },
|
||||
utf8: "\u{fb}",
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
kitty_flags: KittyFlags {
|
||||
disambiguate: true,
|
||||
report_events: true,
|
||||
report_alternates: true,
|
||||
report_all: true,
|
||||
report_associated: true,
|
||||
},
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"\xc3\xbb");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kitty_shift_a_on_us_keyboard() {
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
kitty(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::KeyA,
|
||||
mods: Mods {
|
||||
shift: true,
|
||||
..Mods::default()
|
||||
},
|
||||
utf8: "A",
|
||||
unshifted_codepoint: 97,
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
kitty_flags: KittyFlags {
|
||||
disambiguate: true,
|
||||
report_alternates: true,
|
||||
..KittyFlags::DISABLED
|
||||
},
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"\x1b[97:65;2u");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kitty_matching_unshifted_codepoint() {
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
kitty(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::KeyA,
|
||||
mods: Mods {
|
||||
shift: true,
|
||||
..Mods::default()
|
||||
},
|
||||
utf8: "A",
|
||||
unshifted_codepoint: 65,
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
kitty_flags: KittyFlags {
|
||||
disambiguate: true,
|
||||
report_alternates: true,
|
||||
..KittyFlags::DISABLED
|
||||
},
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"\x1b[65::97;2u");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kitty_report_alternates_with_caps() {
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
kitty(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::KeyJ,
|
||||
mods: Mods {
|
||||
caps_lock: true,
|
||||
..Mods::default()
|
||||
},
|
||||
utf8: "J",
|
||||
unshifted_codepoint: 106,
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
kitty_flags: KittyFlags {
|
||||
disambiguate: true,
|
||||
report_all: true,
|
||||
report_alternates: true,
|
||||
report_associated: true,
|
||||
..KittyFlags::DISABLED
|
||||
},
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"\x1b[106;65;74u");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kitty_report_alternates_colon_shift() {
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
kitty(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::Semicolon,
|
||||
mods: Mods {
|
||||
shift: true,
|
||||
..Mods::default()
|
||||
},
|
||||
utf8: ":",
|
||||
unshifted_codepoint: u32::from(';'),
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
kitty_flags: KittyFlags {
|
||||
disambiguate: true,
|
||||
report_all: true,
|
||||
report_alternates: true,
|
||||
report_associated: true,
|
||||
..KittyFlags::DISABLED
|
||||
},
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"\x1b[59:58;2;58u");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kitty_report_alternates_with_ru_layout() {
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
kitty(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::Semicolon,
|
||||
mods: Mods { ..Mods::default() },
|
||||
utf8: "\u{447}",
|
||||
unshifted_codepoint: 1095,
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
kitty_flags: KittyFlags {
|
||||
disambiguate: true,
|
||||
report_all: true,
|
||||
report_alternates: true,
|
||||
report_associated: true,
|
||||
..KittyFlags::DISABLED
|
||||
},
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"\x1b[1095::59;;1095u");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kitty_report_alternates_with_ru_layout_shifted() {
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
kitty(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::Semicolon,
|
||||
mods: Mods {
|
||||
shift: true,
|
||||
..Mods::default()
|
||||
},
|
||||
utf8: "\u{427}",
|
||||
unshifted_codepoint: 1095,
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
kitty_flags: KittyFlags {
|
||||
disambiguate: true,
|
||||
report_all: true,
|
||||
report_alternates: true,
|
||||
report_associated: true,
|
||||
..KittyFlags::DISABLED
|
||||
},
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"\x1b[1095:1063:59;2;1063u");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kitty_report_alternates_with_ru_layout_caps_lock() {
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
kitty(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::Semicolon,
|
||||
mods: Mods {
|
||||
caps_lock: true,
|
||||
..Mods::default()
|
||||
},
|
||||
utf8: "\u{427}",
|
||||
unshifted_codepoint: 1095,
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
kitty_flags: KittyFlags {
|
||||
disambiguate: true,
|
||||
report_all: true,
|
||||
report_alternates: true,
|
||||
report_associated: true,
|
||||
..KittyFlags::DISABLED
|
||||
},
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"\x1b[1095::59;65;1063u");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kitty_report_alternates_with_hu_layout_release() {
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
kitty(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
action: key::Action::Release,
|
||||
key: Key::BracketLeft,
|
||||
mods: Mods {
|
||||
ctrl: true,
|
||||
..Mods::default()
|
||||
},
|
||||
utf8: "",
|
||||
unshifted_codepoint: 337,
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
kitty_flags: KittyFlags {
|
||||
disambiguate: true,
|
||||
report_all: true,
|
||||
report_alternates: true,
|
||||
report_associated: true,
|
||||
report_events: true,
|
||||
},
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w[1..], b"[337::91;5:3u");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kitty_up_arrow_with_utf8() {
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
kitty(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::ArrowUp,
|
||||
mods: Mods { ..Mods::default() },
|
||||
utf8: "\u{1e}",
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
kitty_flags: KittyFlags {
|
||||
disambiguate: true,
|
||||
..KittyFlags::DISABLED
|
||||
},
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"\x1b[A");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kitty_shift_tab() {
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
kitty(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::Tab,
|
||||
mods: Mods {
|
||||
shift: true,
|
||||
..Mods::default()
|
||||
},
|
||||
utf8: "",
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
kitty_flags: KittyFlags {
|
||||
disambiguate: true,
|
||||
report_alternates: true,
|
||||
..KittyFlags::DISABLED
|
||||
},
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"\x1b[9;2u");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kitty_left_shift() {
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
kitty(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::ShiftLeft,
|
||||
mods: Mods { ..Mods::default() },
|
||||
utf8: "",
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
kitty_flags: KittyFlags {
|
||||
disambiguate: true,
|
||||
report_alternates: true,
|
||||
..KittyFlags::DISABLED
|
||||
},
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kitty_left_shift_with_report_all() {
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
kitty(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::ShiftLeft,
|
||||
mods: Mods { ..Mods::default() },
|
||||
utf8: "",
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
kitty_flags: KittyFlags {
|
||||
disambiguate: true,
|
||||
report_all: true,
|
||||
..KittyFlags::DISABLED
|
||||
},
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"\x1b[57441u");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kitty_report_associated_with_modifiers() {
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
kitty(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::KeyJ,
|
||||
mods: Mods {
|
||||
ctrl: true,
|
||||
..Mods::default()
|
||||
},
|
||||
utf8: "j",
|
||||
unshifted_codepoint: 106,
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
kitty_flags: KittyFlags {
|
||||
disambiguate: true,
|
||||
report_all: true,
|
||||
report_alternates: true,
|
||||
report_associated: true,
|
||||
..KittyFlags::DISABLED
|
||||
},
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"\x1b[106;5u");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kitty_report_associated() {
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
kitty(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::KeyJ,
|
||||
mods: Mods {
|
||||
shift: true,
|
||||
..Mods::default()
|
||||
},
|
||||
utf8: "J",
|
||||
unshifted_codepoint: 106,
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
kitty_flags: KittyFlags {
|
||||
disambiguate: true,
|
||||
report_all: true,
|
||||
report_alternates: true,
|
||||
report_associated: true,
|
||||
..KittyFlags::DISABLED
|
||||
},
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"\x1b[106:74;2;74u");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kitty_report_associated_on_release() {
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
kitty(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
action: key::Action::Release,
|
||||
key: Key::KeyJ,
|
||||
mods: Mods {
|
||||
shift: true,
|
||||
..Mods::default()
|
||||
},
|
||||
utf8: "J",
|
||||
unshifted_codepoint: 106,
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
kitty_flags: KittyFlags {
|
||||
disambiguate: true,
|
||||
report_all: true,
|
||||
report_alternates: true,
|
||||
report_associated: true,
|
||||
report_events: true,
|
||||
},
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w[1..], b"[106:74;2:3u");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kitty_alternates_omit_control_characters() {
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
kitty(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::Delete,
|
||||
mods: Mods { ..Mods::default() },
|
||||
utf8: "\u{7f}",
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
kitty_flags: KittyFlags {
|
||||
disambiguate: true,
|
||||
report_alternates: true,
|
||||
report_all: true,
|
||||
..KittyFlags::DISABLED
|
||||
},
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"\x1b[3~");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kitty_enter_with_utf8_dead_key_state() {
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
kitty(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::Enter,
|
||||
utf8: "A",
|
||||
unshifted_codepoint: 0x0D,
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
kitty_flags: KittyFlags {
|
||||
disambiguate: true,
|
||||
report_alternates: true,
|
||||
report_all: true,
|
||||
..KittyFlags::DISABLED
|
||||
},
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"A");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kitty_keypad_number() {
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
kitty(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::Numpad1,
|
||||
mods: Mods { ..Mods::default() },
|
||||
utf8: "1",
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
kitty_flags: KittyFlags {
|
||||
disambiguate: true,
|
||||
report_events: true,
|
||||
report_alternates: true,
|
||||
report_all: true,
|
||||
report_associated: true,
|
||||
},
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w[1..], b"[57400;;49u");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kitty_backspace_with_utf8_dead_key_state() {
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
kitty(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::Backspace,
|
||||
utf8: "A",
|
||||
unshifted_codepoint: 0x0D,
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
kitty_flags: KittyFlags {
|
||||
disambiguate: true,
|
||||
report_events: true,
|
||||
report_alternates: true,
|
||||
report_all: true,
|
||||
report_associated: true,
|
||||
},
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_backspace_with_utf8_dead_key_state() {
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
legacy(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::Backspace,
|
||||
utf8: "A",
|
||||
unshifted_codepoint: 0x0D,
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_enter_with_utf8_dead_key_state() {
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
legacy(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::Enter,
|
||||
utf8: "A",
|
||||
unshifted_codepoint: 0x0D,
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"A");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_esc_with_utf8_dead_key_state() {
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
legacy(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::Escape,
|
||||
utf8: "A",
|
||||
unshifted_codepoint: 0x0D,
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"A");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_ctrl_shift_minus_underscore_on_us() {
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
legacy(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::Minus,
|
||||
mods: Mods {
|
||||
ctrl: true,
|
||||
shift: true,
|
||||
..Mods::default()
|
||||
},
|
||||
utf8: "_",
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"\x1f");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_ctrl_alt_c() {
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
legacy(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::KeyC,
|
||||
mods: Mods {
|
||||
ctrl: true,
|
||||
alt: true,
|
||||
..Mods::default()
|
||||
},
|
||||
utf8: "c",
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"\x1b\x03");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_alt_c() {
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
legacy(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::KeyC,
|
||||
utf8: "c",
|
||||
mods: Mods {
|
||||
alt: true,
|
||||
..Mods::default()
|
||||
},
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
alt_esc_prefix: true,
|
||||
macos_option_as_alt: OptionAsAlt::True,
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"\x1bc");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_alt_e_only_unshifted() {
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
legacy(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::KeyE,
|
||||
unshifted_codepoint: u32::from('e'),
|
||||
mods: Mods {
|
||||
alt: true,
|
||||
..Mods::default()
|
||||
},
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
alt_esc_prefix: true,
|
||||
macos_option_as_alt: OptionAsAlt::True,
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"\x1be");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_alt() {
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
legacy(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::KeyF,
|
||||
utf8: "\u{444}",
|
||||
mods: Mods {
|
||||
alt: true,
|
||||
..Mods::default()
|
||||
},
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
alt_esc_prefix: true,
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"\xd1\x84");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_ctrl_c() {
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
legacy(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::KeyC,
|
||||
mods: Mods {
|
||||
ctrl: true,
|
||||
..Mods::default()
|
||||
},
|
||||
utf8: "c",
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"\x03");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_ctrl_space() {
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
legacy(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::Space,
|
||||
mods: Mods {
|
||||
ctrl: true,
|
||||
..Mods::default()
|
||||
},
|
||||
utf8: " ",
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"\x00");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_ctrl_shift_backspace() {
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
legacy(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::Backspace,
|
||||
mods: Mods {
|
||||
ctrl: true,
|
||||
shift: true,
|
||||
..Mods::default()
|
||||
},
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"\x08");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_ctrl_shift_char_with_modify_other_state_2() {
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
legacy(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::KeyH,
|
||||
mods: Mods {
|
||||
ctrl: true,
|
||||
shift: true,
|
||||
..Mods::default()
|
||||
},
|
||||
utf8: "H",
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
modify_other_keys_state_2: true,
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"\x1b[27;6;72~");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_ctrl_shift_char_with_modify_other_state_2_and_consumed_mods() {
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
legacy(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::KeyH,
|
||||
mods: Mods {
|
||||
ctrl: true,
|
||||
shift: true,
|
||||
..Mods::default()
|
||||
},
|
||||
consumed_mods: Mods {
|
||||
shift: true,
|
||||
..Mods::default()
|
||||
},
|
||||
utf8: "H",
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
modify_other_keys_state_2: true,
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"\x1b[27;6;72~");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_alt_digit_with_modify_other_state_2() {
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
legacy(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::Digit8,
|
||||
mods: Mods {
|
||||
alt: true,
|
||||
..Mods::default()
|
||||
},
|
||||
consumed_mods: Mods { ..Mods::default() },
|
||||
utf8: "8",
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
modify_other_keys_state_2: true,
|
||||
macos_option_as_alt: OptionAsAlt::True,
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"\x1b[27;3;56~");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_fixterm_awkward_letters() {
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
legacy(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::KeyI,
|
||||
mods: Mods {
|
||||
ctrl: true,
|
||||
..Mods::default()
|
||||
},
|
||||
utf8: "i",
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"\x1b[105;5u");
|
||||
}
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
legacy(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::KeyM,
|
||||
mods: Mods {
|
||||
ctrl: true,
|
||||
..Mods::default()
|
||||
},
|
||||
utf8: "m",
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"\x1b[109;5u");
|
||||
}
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
legacy(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::BracketLeft,
|
||||
mods: Mods {
|
||||
ctrl: true,
|
||||
..Mods::default()
|
||||
},
|
||||
utf8: "[",
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"\x1b[91;5u");
|
||||
}
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
legacy(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::Digit2,
|
||||
mods: Mods {
|
||||
ctrl: true,
|
||||
shift: true,
|
||||
..Mods::default()
|
||||
},
|
||||
utf8: "@",
|
||||
unshifted_codepoint: u32::from('2'),
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"\x1b[64;5u");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_ctrl_shift_letter_ascii() {
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
legacy(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::KeyM,
|
||||
mods: Mods {
|
||||
ctrl: true,
|
||||
shift: true,
|
||||
..Mods::default()
|
||||
},
|
||||
utf8: "M",
|
||||
unshifted_codepoint: u32::from('m'),
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"\x1b[109;6u");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_shift_function_key_should_use_all_mods() {
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
legacy(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::ArrowUp,
|
||||
mods: Mods {
|
||||
shift: true,
|
||||
..Mods::default()
|
||||
},
|
||||
consumed_mods: Mods {
|
||||
shift: true,
|
||||
..Mods::default()
|
||||
},
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"\x1b[1;2A");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_keypad_enter() {
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
legacy(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::NumpadEnter,
|
||||
mods: Mods { ..Mods::default() },
|
||||
consumed_mods: Mods { ..Mods::default() },
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"\x0d");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_keypad_1() {
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
legacy(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::Numpad1,
|
||||
mods: Mods { ..Mods::default() },
|
||||
consumed_mods: Mods { ..Mods::default() },
|
||||
utf8: "1",
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"1");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_keypad_1_with_application_keypad() {
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
legacy(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::Numpad1,
|
||||
mods: Mods { ..Mods::default() },
|
||||
consumed_mods: Mods { ..Mods::default() },
|
||||
utf8: "1",
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
keypad_key_application: true,
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"\x1bOq");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_keypad_1_with_application_keypad_and_numlock() {
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
legacy(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::Numpad1,
|
||||
mods: Mods {
|
||||
num_lock: true,
|
||||
..Mods::default()
|
||||
},
|
||||
consumed_mods: Mods { ..Mods::default() },
|
||||
utf8: "1",
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
keypad_key_application: true,
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"\x1bOq");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_keypad_1_with_application_keypad_and_numlock_ignore() {
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
legacy(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::Numpad1,
|
||||
mods: Mods {
|
||||
num_lock: false,
|
||||
..Mods::default()
|
||||
},
|
||||
consumed_mods: Mods { ..Mods::default() },
|
||||
utf8: "1",
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
keypad_key_application: true,
|
||||
ignore_keypad_with_numlock: true,
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"1");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_f1() {
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
legacy(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::F1,
|
||||
mods: Mods {
|
||||
ctrl: true,
|
||||
..Mods::default()
|
||||
},
|
||||
consumed_mods: Mods { ..Mods::default() },
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"\x1b[1;5P");
|
||||
}
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
legacy(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::F2,
|
||||
mods: Mods {
|
||||
ctrl: true,
|
||||
..Mods::default()
|
||||
},
|
||||
consumed_mods: Mods { ..Mods::default() },
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"\x1b[1;5Q");
|
||||
}
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
legacy(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::F3,
|
||||
mods: Mods {
|
||||
ctrl: true,
|
||||
..Mods::default()
|
||||
},
|
||||
consumed_mods: Mods { ..Mods::default() },
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"\x1b[13;5~");
|
||||
}
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
legacy(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::F4,
|
||||
mods: Mods {
|
||||
ctrl: true,
|
||||
..Mods::default()
|
||||
},
|
||||
consumed_mods: Mods { ..Mods::default() },
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"\x1b[1;5S");
|
||||
}
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
legacy(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::F5,
|
||||
mods: Mods {
|
||||
ctrl: true,
|
||||
..Mods::default()
|
||||
},
|
||||
consumed_mods: Mods { ..Mods::default() },
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"\x1b[15;5~");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_left_shift_tab() {
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
legacy(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::Tab,
|
||||
mods: Mods {
|
||||
shift: true,
|
||||
sides: Side {
|
||||
shift: ModSide::Left,
|
||||
..Side::default()
|
||||
},
|
||||
..Mods::default()
|
||||
},
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"\x1b[Z");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_right_shift_tab() {
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
legacy(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::Tab,
|
||||
mods: Mods {
|
||||
shift: true,
|
||||
sides: Side {
|
||||
shift: ModSide::Right,
|
||||
..Side::default()
|
||||
},
|
||||
..Mods::default()
|
||||
},
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"\x1b[Z");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_hu_layout_ctrl_sends_proper_codepoint() {
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
legacy(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::BracketLeft,
|
||||
mods: Mods {
|
||||
ctrl: true,
|
||||
..Mods::default()
|
||||
},
|
||||
utf8: "\u{151}",
|
||||
unshifted_codepoint: 337,
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w[1..], b"[337;5u");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_backspace_with_del_utf8() {
|
||||
{
|
||||
let mut w = Vec::new();
|
||||
legacy(
|
||||
&mut w,
|
||||
&KeyEvent {
|
||||
key: Key::Backspace,
|
||||
utf8: "\u{7f}",
|
||||
unshifted_codepoint: 0x08,
|
||||
..KeyEvent::default()
|
||||
},
|
||||
&Options {
|
||||
..Options::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&w, b"\x7f");
|
||||
}
|
||||
}
|
||||
fn enc(seq: KittySequence) -> Vec<u8> {
|
||||
let mut w = Vec::new();
|
||||
seq.encode(&mut w).unwrap();
|
||||
w
|
||||
}
|
||||
|
||||
fn seq(key: u32, final_: u8) -> KittySequence<'static> {
|
||||
KittySequence {
|
||||
key,
|
||||
final_,
|
||||
mods: KittyMods::default(),
|
||||
event: KittyEvent::None,
|
||||
alternates: [None, None],
|
||||
text: "",
|
||||
}
|
||||
}
|
||||
|
||||
fn ctrl() -> Mods {
|
||||
Mods {
|
||||
ctrl: true,
|
||||
..Mods::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn csi_u_mods_modifier_sequence_values() {
|
||||
let c = |shift, alt, ctrl| CsiUMods { shift, alt, ctrl }.seq_int();
|
||||
assert_eq!(1, c(false, false, false));
|
||||
assert_eq!(2, c(true, false, false));
|
||||
assert_eq!(3, c(false, true, false));
|
||||
assert_eq!(5, c(false, false, true));
|
||||
assert_eq!(4, c(true, true, false));
|
||||
assert_eq!(6, c(true, false, true));
|
||||
assert_eq!(7, c(false, true, true));
|
||||
assert_eq!(8, c(true, true, true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kitty_mods_modifier_sequence_values() {
|
||||
let c = |shift, alt, ctrl| {
|
||||
KittyMods {
|
||||
shift,
|
||||
alt,
|
||||
ctrl,
|
||||
..KittyMods::default()
|
||||
}
|
||||
.seq_int()
|
||||
};
|
||||
assert_eq!(1, c(false, false, false));
|
||||
assert_eq!(2, c(true, false, false));
|
||||
assert_eq!(3, c(false, true, false));
|
||||
assert_eq!(5, c(false, false, true));
|
||||
assert_eq!(4, c(true, true, false));
|
||||
assert_eq!(6, c(true, false, true));
|
||||
assert_eq!(7, c(false, true, true));
|
||||
assert_eq!(8, c(true, true, true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kitty_sequence_backspace() {
|
||||
assert_eq!(enc(seq(127, b'u')), b"\x1B[127u");
|
||||
assert_eq!(
|
||||
enc(KittySequence {
|
||||
event: KittyEvent::Release,
|
||||
..seq(127, b'u')
|
||||
}),
|
||||
b"\x1B[127;1:3u"
|
||||
);
|
||||
assert_eq!(
|
||||
enc(KittySequence {
|
||||
mods: KittyMods {
|
||||
shift: true,
|
||||
..KittyMods::default()
|
||||
},
|
||||
..seq(127, b'u')
|
||||
}),
|
||||
b"\x1B[127;2u"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kitty_sequence_text() {
|
||||
assert_eq!(
|
||||
enc(KittySequence {
|
||||
text: "A",
|
||||
..seq(127, b'u')
|
||||
}),
|
||||
b"\x1B[127;;65u"
|
||||
);
|
||||
assert_eq!(
|
||||
enc(KittySequence {
|
||||
event: KittyEvent::Release,
|
||||
text: "A",
|
||||
..seq(127, b'u')
|
||||
}),
|
||||
b"\x1B[127;1:3;65u"
|
||||
);
|
||||
assert_eq!(
|
||||
enc(KittySequence {
|
||||
mods: KittyMods {
|
||||
shift: true,
|
||||
..KittyMods::default()
|
||||
},
|
||||
text: "A",
|
||||
..seq(127, b'u')
|
||||
}),
|
||||
b"\x1B[127;2;65u"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kitty_sequence_text_with_control_characters() {
|
||||
assert_eq!(
|
||||
enc(KittySequence {
|
||||
text: "\n",
|
||||
..seq(127, b'u')
|
||||
}),
|
||||
b"\x1b[127u"
|
||||
);
|
||||
assert_eq!(
|
||||
enc(KittySequence {
|
||||
text: "A\n",
|
||||
..seq(127, b'u')
|
||||
}),
|
||||
b"\x1b[127;;65u"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kitty_sequence_special_no_mods() {
|
||||
assert_eq!(enc(seq(1, b'A')), b"\x1B[A");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kitty_sequence_special_mods_only() {
|
||||
assert_eq!(
|
||||
enc(KittySequence {
|
||||
mods: KittyMods {
|
||||
shift: true,
|
||||
..KittyMods::default()
|
||||
},
|
||||
..seq(1, b'A')
|
||||
}),
|
||||
b"\x1B[1;2A"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kitty_sequence_special_mods_and_event() {
|
||||
assert_eq!(
|
||||
enc(KittySequence {
|
||||
event: KittyEvent::Release,
|
||||
mods: KittyMods {
|
||||
shift: true,
|
||||
..KittyMods::default()
|
||||
},
|
||||
..seq(1, b'A')
|
||||
}),
|
||||
b"\x1B[1;2:3A"
|
||||
);
|
||||
}
|
||||
|
||||
fn right_ctrl() -> Mods {
|
||||
Mods {
|
||||
ctrl: true,
|
||||
sides: Side {
|
||||
ctrl: ModSide::Right,
|
||||
..Side::default()
|
||||
},
|
||||
..Mods::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ctrlseq_normal_ctrl_c() {
|
||||
assert_eq!(
|
||||
ctrl_seq(Key::Unidentified, "c", 'c' as u32, ctrl()),
|
||||
Some(0x03)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ctrlseq_normal_ctrl_c_right_control() {
|
||||
assert_eq!(
|
||||
ctrl_seq(Key::Unidentified, "c", 'c' as u32, right_ctrl()),
|
||||
Some(0x03)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ctrlseq_alt_should_be_allowed() {
|
||||
let mods = Mods {
|
||||
alt: true,
|
||||
ctrl: true,
|
||||
..Mods::default()
|
||||
};
|
||||
assert_eq!(
|
||||
ctrl_seq(Key::Unidentified, "c", 'c' as u32, mods),
|
||||
Some(0x03)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ctrlseq_no_ctrl_does_nothing() {
|
||||
assert_eq!(
|
||||
ctrl_seq(Key::Unidentified, "c", 'c' as u32, Mods::default()),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ctrlseq_shifted_non_character() {
|
||||
let mods = Mods {
|
||||
ctrl: true,
|
||||
shift: true,
|
||||
..Mods::default()
|
||||
};
|
||||
assert_eq!(
|
||||
ctrl_seq(Key::Unidentified, "_", '-' as u32, mods),
|
||||
Some(0x1F)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ctrlseq_caps_ascii_letter() {
|
||||
let mods = Mods {
|
||||
ctrl: true,
|
||||
caps_lock: true,
|
||||
..Mods::default()
|
||||
};
|
||||
assert_eq!(
|
||||
ctrl_seq(Key::Unidentified, "C", 'c' as u32, mods),
|
||||
Some(0x03)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ctrlseq_shift_does_not_generate_ctrl_seq() {
|
||||
let shift = Mods {
|
||||
shift: true,
|
||||
..Mods::default()
|
||||
};
|
||||
assert_eq!(ctrl_seq(Key::Unidentified, "C", 'c' as u32, shift), None);
|
||||
let both = Mods {
|
||||
shift: true,
|
||||
ctrl: true,
|
||||
..Mods::default()
|
||||
};
|
||||
assert_eq!(ctrl_seq(Key::Unidentified, "C", 'c' as u32, both), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ctrlseq_russian_ctrl_c() {
|
||||
assert_eq!(ctrl_seq(Key::KeyC, "с", 0x0441, ctrl()), Some(0x03));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ctrlseq_russian_shifted_ctrl_c() {
|
||||
let mods = Mods {
|
||||
ctrl: true,
|
||||
shift: true,
|
||||
..Mods::default()
|
||||
};
|
||||
assert_eq!(ctrl_seq(Key::KeyC, "с", 0x0441, mods), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ctrlseq_russian_alt_ctrl_c() {
|
||||
let mods = Mods {
|
||||
ctrl: true,
|
||||
alt: true,
|
||||
..Mods::default()
|
||||
};
|
||||
assert_eq!(ctrl_seq(Key::KeyC, "с", 0x0441, mods), Some(0x03));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ctrlseq_right_ctrl_c() {
|
||||
assert_eq!(
|
||||
ctrl_seq(Key::KeyC, "с", 'c' as u32, right_ctrl()),
|
||||
Some(0x03)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,869 @@
|
||||
//! Key modifiers (ZIG/src/input/key_mods.zig).
|
||||
|
||||
use crate::config::OptionAsAlt;
|
||||
|
||||
/// Aliases for modifier names.
|
||||
pub const ALIAS: &[(&str, Mod)] = &[
|
||||
("cmd", Mod::Super),
|
||||
("command", Mod::Super),
|
||||
("opt", Mod::Alt),
|
||||
("option", Mod::Alt),
|
||||
("control", Mod::Ctrl),
|
||||
];
|
||||
|
||||
/// Single modifier
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum Mod {
|
||||
Shift,
|
||||
Ctrl,
|
||||
Alt,
|
||||
Super,
|
||||
}
|
||||
|
||||
impl Mod {
|
||||
fn from_name(s: &str) -> Option<Mod> {
|
||||
match s {
|
||||
"shift" => Some(Mod::Shift),
|
||||
"ctrl" => Some(Mod::Ctrl),
|
||||
"alt" => Some(Mod::Alt),
|
||||
"super" => Some(Mod::Super),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Which side of the keyboard a modifier is on (`Mod.Side`).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
|
||||
#[repr(u8)]
|
||||
pub enum ModSide {
|
||||
#[default]
|
||||
Left = 0,
|
||||
Right = 1,
|
||||
}
|
||||
|
||||
impl ModSide {
|
||||
fn from_name(s: &str) -> Option<ModSide> {
|
||||
match s {
|
||||
"left" => Some(ModSide::Left),
|
||||
"right" => Some(ModSide::Right),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The standard modifier keys only. Does not include the lock keys,
|
||||
/// only standard bindable keys. (`Mods.Keys`, a packed u4)
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
|
||||
pub struct Keys {
|
||||
pub shift: bool,
|
||||
pub ctrl: bool,
|
||||
pub alt: bool,
|
||||
pub super_: bool,
|
||||
}
|
||||
|
||||
impl Keys {
|
||||
pub fn int(self) -> u8 {
|
||||
u8::from(self.shift)
|
||||
| u8::from(self.ctrl) << 1
|
||||
| u8::from(self.alt) << 2
|
||||
| u8::from(self.super_) << 3
|
||||
}
|
||||
|
||||
pub fn from_int(v: u8) -> Keys {
|
||||
Keys {
|
||||
shift: v & 1 != 0,
|
||||
ctrl: v & 2 != 0,
|
||||
alt: v & 4 != 0,
|
||||
super_: v & 8 != 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Tracks the side that is active for any given modifier. Note
|
||||
/// that this doesn't confirm a modifier is pressed; you must check
|
||||
/// the bool for that in addition to this. (`Mods.Side`, a packed u4)
|
||||
///
|
||||
/// Not all platforms support this, check apprt for more info.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
|
||||
pub struct Side {
|
||||
pub shift: ModSide,
|
||||
pub ctrl: ModSide,
|
||||
pub alt: ModSide,
|
||||
pub super_: ModSide,
|
||||
}
|
||||
|
||||
impl Side {
|
||||
pub fn int(self) -> u8 {
|
||||
self.shift as u8 | (self.ctrl as u8) << 1 | (self.alt as u8) << 2 | (self.super_ as u8) << 3
|
||||
}
|
||||
|
||||
pub fn from_int(v: u8) -> Side {
|
||||
let f = |b: u8| {
|
||||
if v & b != 0 {
|
||||
ModSide::Right
|
||||
} else {
|
||||
ModSide::Left
|
||||
}
|
||||
};
|
||||
Side {
|
||||
shift: f(1),
|
||||
ctrl: f(2),
|
||||
alt: f(4),
|
||||
super_: f(8),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A bitmask for all key modifiers.
|
||||
///
|
||||
/// NOTE: Zig's field `super` is named `super_` here (`super` is a Rust
|
||||
/// keyword). The packed integer layout (u16) is available via `int()`.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
|
||||
pub struct Mods {
|
||||
pub shift: bool,
|
||||
pub ctrl: bool,
|
||||
pub alt: bool,
|
||||
pub super_: bool,
|
||||
pub caps_lock: bool,
|
||||
pub num_lock: bool,
|
||||
pub sides: Side,
|
||||
}
|
||||
|
||||
impl Mods {
|
||||
/// The mask that has all the side bits set.
|
||||
pub const SIDE_MASK: Mods = Mods {
|
||||
shift: false,
|
||||
ctrl: false,
|
||||
alt: false,
|
||||
super_: false,
|
||||
caps_lock: false,
|
||||
num_lock: false,
|
||||
sides: Side {
|
||||
shift: ModSide::Right,
|
||||
ctrl: ModSide::Right,
|
||||
alt: ModSide::Right,
|
||||
super_: ModSide::Right,
|
||||
},
|
||||
};
|
||||
|
||||
/// Integer value of this struct.
|
||||
pub fn int(self) -> u16 {
|
||||
u16::from(self.keys().int())
|
||||
| u16::from(self.caps_lock) << 4
|
||||
| u16::from(self.num_lock) << 5
|
||||
| u16::from(self.sides.int()) << 6
|
||||
}
|
||||
|
||||
pub fn from_int(v: u16) -> Mods {
|
||||
Mods {
|
||||
shift: v & 1 != 0,
|
||||
ctrl: v & 2 != 0,
|
||||
alt: v & 4 != 0,
|
||||
super_: v & 8 != 0,
|
||||
caps_lock: v & 16 != 0,
|
||||
num_lock: v & 32 != 0,
|
||||
sides: Side::from_int(((v >> 6) & 0xF) as u8),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if no modifiers are set.
|
||||
pub fn empty(self) -> bool {
|
||||
self.int() == 0
|
||||
}
|
||||
|
||||
/// Returns true if two mods are equal.
|
||||
pub fn equal(self, other: Mods) -> bool {
|
||||
self.int() == other.int()
|
||||
}
|
||||
|
||||
/// Returns only the keys.
|
||||
pub fn keys(self) -> Keys {
|
||||
Keys {
|
||||
shift: self.shift,
|
||||
ctrl: self.ctrl,
|
||||
alt: self.alt,
|
||||
super_: self.super_,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return mods that are only relevant for bindings.
|
||||
pub fn binding(self) -> Mods {
|
||||
Mods {
|
||||
shift: self.shift,
|
||||
ctrl: self.ctrl,
|
||||
alt: self.alt,
|
||||
super_: self.super_,
|
||||
..Mods::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Perform `self &~ other` to remove the other mods from self.
|
||||
pub fn unset(self, other: Mods) -> Mods {
|
||||
Mods::from_int(self.int() & !other.int())
|
||||
}
|
||||
|
||||
/// Returns the mods without locks set.
|
||||
pub fn without_locks(self) -> Mods {
|
||||
let mut copy = self;
|
||||
copy.caps_lock = false;
|
||||
copy.num_lock = false;
|
||||
copy
|
||||
}
|
||||
|
||||
/// Return the mods to use for key translation. This handles settings
|
||||
/// like macos-option-as-alt. The translation mods should be used for
|
||||
/// translation but never sent back in for the key callback.
|
||||
///
|
||||
/// Only macOS changes anything here; this port targets Linux so the
|
||||
/// mods are returned unchanged.
|
||||
pub fn translation(self, _option_as_alt: OptionAsAlt) -> Mods {
|
||||
self
|
||||
}
|
||||
|
||||
/// Checks to see if super is on (MacOS) or ctrl.
|
||||
pub fn ctrl_or_super(self) -> bool {
|
||||
// Non-darwin target.
|
||||
self.ctrl
|
||||
}
|
||||
}
|
||||
|
||||
/// Modifier remapping. See `key-remap` in Config.zig for detailed docs.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct RemapSet {
|
||||
/// Available mappings (insertion ordered, like an array hash map).
|
||||
pub map: Vec<(Mods, Mods)>,
|
||||
|
||||
/// The mask of remapped modifiers that can be used to quickly
|
||||
/// check if some input mods need remapping.
|
||||
pub mask: Mask,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
|
||||
pub enum ParseError {
|
||||
#[error("missing assignment")]
|
||||
MissingAssignment,
|
||||
#[error("invalid modifier")]
|
||||
InvalidMod,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
|
||||
pub enum ParseCliError {
|
||||
#[error("invalid value")]
|
||||
InvalidValue,
|
||||
}
|
||||
|
||||
impl RemapSet {
|
||||
pub const EMPTY: RemapSet = RemapSet {
|
||||
map: Vec::new(),
|
||||
mask: Mask {
|
||||
keys: Keys {
|
||||
shift: false,
|
||||
ctrl: false,
|
||||
alt: false,
|
||||
super_: false,
|
||||
},
|
||||
left_sides: Side {
|
||||
shift: ModSide::Left,
|
||||
ctrl: ModSide::Left,
|
||||
alt: ModSide::Left,
|
||||
super_: ModSide::Left,
|
||||
},
|
||||
right_sides: Side {
|
||||
shift: ModSide::Left,
|
||||
ctrl: ModSide::Left,
|
||||
alt: ModSide::Left,
|
||||
super_: ModSide::Left,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
fn put(&mut self, from: Mods, to: Mods) {
|
||||
if let Some(e) = self.map.iter_mut().find(|e| e.0 == from) {
|
||||
e.1 = to;
|
||||
} else {
|
||||
self.map.push((from, to));
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse from CLI input. Required by Config.
|
||||
pub fn parse_cli(&mut self, input: Option<&str>) -> Result<(), ParseCliError> {
|
||||
let value = input.unwrap_or("");
|
||||
|
||||
// Empty value resets the set
|
||||
if value.is_empty() {
|
||||
self.map.clear();
|
||||
self.mask = Mask::default();
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.parse(value).map_err(|_| ParseCliError::InvalidValue)
|
||||
}
|
||||
|
||||
/// Parse a modifier remap and add it to the set.
|
||||
pub fn parse(&mut self, input: &str) -> Result<(), ParseError> {
|
||||
// Find the assignment point ('=')
|
||||
let eql_idx = input.find('=').ok_or(ParseError::MissingAssignment)?;
|
||||
|
||||
// The to side defaults to "left" if no explicit side is given.
|
||||
// This is because this is the default unsided value provided by
|
||||
// the apprts in the current Mods layout.
|
||||
let to = {
|
||||
let raw = parse_mod(&input[eql_idx + 1..])?;
|
||||
init_mods(raw.0, raw.1.unwrap_or(ModSide::Left))
|
||||
};
|
||||
|
||||
// The from side, if sided, is easy and we put it directly into
|
||||
// the map.
|
||||
let from_raw = parse_mod(&input[..eql_idx])?;
|
||||
if let Some(from_side) = from_raw.1 {
|
||||
let from = init_mods(from_raw.0, from_side);
|
||||
self.put(from, to);
|
||||
self.mask.update(from);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// We need to do some combinatorial explosion here for unsided
|
||||
// from in order to assign all possible sides.
|
||||
let from_left = init_mods(from_raw.0, ModSide::Left);
|
||||
let from_right = init_mods(from_raw.0, ModSide::Right);
|
||||
self.put(from_left, to);
|
||||
self.put(from_right, to);
|
||||
self.mask.update(from_left);
|
||||
self.mask.update(from_right);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Must be called prior to any remappings so that the mapping
|
||||
/// is sorted properly. Otherwise, you will get invalid results.
|
||||
pub fn finalize(&mut self) {
|
||||
// Mods with any right sides prioritize. This mirrors the Zig
|
||||
// comparator (which ignores `b`) driven by an insertion sort.
|
||||
let side_mask = Mods::SIDE_MASK.int();
|
||||
let less = |a: &Mods| a.int() & side_mask != 0;
|
||||
for i in 1..self.map.len() {
|
||||
let mut j = i;
|
||||
while j > 0 && less(&self.map[j].0) {
|
||||
self.map.swap(j, j - 1);
|
||||
j -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Compare if two RemapSets are equal. Required by Config.
|
||||
pub fn equal(&self, other: &RemapSet) -> bool {
|
||||
if self.map.len() != other.map.len() {
|
||||
return false;
|
||||
}
|
||||
self.map.iter().all(|(k, v)| {
|
||||
other
|
||||
.map
|
||||
.iter()
|
||||
.find(|(ok, _)| ok == k)
|
||||
.is_some_and(|(_, ov)| v.equal(*ov))
|
||||
})
|
||||
}
|
||||
|
||||
/// Used by Formatter. Required by Config. Returns the values of each
|
||||
/// `key-remap = ...` line to print. An empty vec means the formatter
|
||||
/// should emit a single empty entry (`key-remap = `).
|
||||
pub fn format_entries(&self) -> Vec<String> {
|
||||
self.map
|
||||
.iter()
|
||||
.map(|(from, to)| {
|
||||
let mut s = String::new();
|
||||
format_mod(&mut s, *from);
|
||||
s.push('=');
|
||||
format_mod(&mut s, *to);
|
||||
s
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Returns true if the given mods need remapping.
|
||||
pub fn is_remapped(&self, mods: Mods) -> bool {
|
||||
self.mask.matches(mods)
|
||||
}
|
||||
|
||||
/// Apply a remap to the given mods.
|
||||
pub fn apply(&self, mods: Mods) -> Mods {
|
||||
if !self.is_remapped(mods) {
|
||||
return mods;
|
||||
}
|
||||
|
||||
let mods_binding = mods.keys().int();
|
||||
let mods_sides = mods.sides.int();
|
||||
|
||||
for (from, to) in &self.map {
|
||||
let from_binding = from.keys().int();
|
||||
if mods_binding & from_binding != from_binding {
|
||||
continue;
|
||||
}
|
||||
let from_sides = from.sides.int();
|
||||
if (mods_sides ^ from_sides) & from_binding != 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut mods_int = mods.int();
|
||||
mods_int &= !from.int();
|
||||
mods_int |= to.int();
|
||||
return Mods::from_int(mods_int);
|
||||
}
|
||||
|
||||
unreachable!("is_remapped true but no mapping matched")
|
||||
}
|
||||
}
|
||||
|
||||
fn format_mod(out: &mut String, mods: Mods) {
|
||||
// Check which mod is set and format it with optional side prefix
|
||||
let list = [
|
||||
("shift", mods.shift, mods.sides.shift),
|
||||
("ctrl", mods.ctrl, mods.sides.ctrl),
|
||||
("alt", mods.alt, mods.sides.alt),
|
||||
("super", mods.super_, mods.sides.super_),
|
||||
];
|
||||
for (name, set, side) in list {
|
||||
if set {
|
||||
if side == ModSide::Right {
|
||||
out.push_str("right_");
|
||||
} else {
|
||||
// Only write left_ if we need to distinguish
|
||||
// For now, always write left_ if it's a sided mapping
|
||||
out.push_str("left_");
|
||||
}
|
||||
out.push_str(name);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses a single mode in a single remapping string. E.g.
|
||||
/// `ctrl` or `left_shift`.
|
||||
fn parse_mod(input: &str) -> Result<(Mod, Option<ModSide>), ParseError> {
|
||||
let (side_str, mod_str) = match input.find('_') {
|
||||
Some(idx) => (&input[..idx], &input[idx + 1..]),
|
||||
None => ("", input),
|
||||
};
|
||||
|
||||
let m = match Mod::from_name(mod_str) {
|
||||
Some(m) => m,
|
||||
None => ALIAS
|
||||
.iter()
|
||||
.find(|p| p.0 == mod_str)
|
||||
.map(|p| p.1)
|
||||
.ok_or(ParseError::InvalidMod)?,
|
||||
};
|
||||
|
||||
let side = if side_str.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(ModSide::from_name(side_str).ok_or(ParseError::InvalidMod)?)
|
||||
};
|
||||
Ok((m, side))
|
||||
}
|
||||
|
||||
fn init_mods(m: Mod, side: ModSide) -> Mods {
|
||||
let mut mods = Mods::default();
|
||||
match m {
|
||||
Mod::Shift => {
|
||||
mods.shift = true;
|
||||
mods.sides.shift = side;
|
||||
}
|
||||
Mod::Ctrl => {
|
||||
mods.ctrl = true;
|
||||
mods.sides.ctrl = side;
|
||||
}
|
||||
Mod::Alt => {
|
||||
mods.alt = true;
|
||||
mods.sides.alt = side;
|
||||
}
|
||||
Mod::Super => {
|
||||
mods.super_ = true;
|
||||
mods.sides.super_ = side;
|
||||
}
|
||||
}
|
||||
mods
|
||||
}
|
||||
|
||||
/// Tracks which modifier keys and sides have remappings registered.
|
||||
/// Used as a fast pre-check before doing expensive map lookups.
|
||||
///
|
||||
/// The mask uses separate tracking for left and right sides because
|
||||
/// remappings can be side-specific (e.g., only remap left_ctrl).
|
||||
///
|
||||
/// Note: `left_sides` uses inverted logic where 1 means "left is remapped"
|
||||
/// even though `ModSide::Left = 0`. This allows efficient bitwise matching
|
||||
/// since we can AND directly with the side bits.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub struct Mask {
|
||||
/// Which modifier keys (shift/ctrl/alt/super) have any remapping.
|
||||
pub keys: Keys,
|
||||
/// Which modifiers have left-side remappings (inverted: 1 = left remapped).
|
||||
pub left_sides: Side,
|
||||
/// Which modifiers have right-side remappings (1 = right remapped).
|
||||
pub right_sides: Side,
|
||||
}
|
||||
|
||||
impl Mask {
|
||||
/// Adds a modifier to the mask, marking it as having a remapping.
|
||||
pub fn update(&mut self, mods: Mods) {
|
||||
let keys_int = mods.keys().int();
|
||||
|
||||
// OR the new keys into our existing keys mask.
|
||||
self.keys = Keys::from_int(self.keys.int() | keys_int);
|
||||
|
||||
// Both Keys and Side are u4 with matching bit positions.
|
||||
// This lets us use keys_int to select which side bits to update.
|
||||
let sides = mods.sides.int();
|
||||
let left_int = self.left_sides.int();
|
||||
let right_int = self.right_sides.int();
|
||||
|
||||
// Update left_sides: set bit if this key is active AND side is left.
|
||||
// Since Left=0, we invert sides so left becomes 1.
|
||||
self.left_sides = Side::from_int(left_int | (keys_int & !sides));
|
||||
|
||||
// Update right_sides: set bit if this key is active AND side is right.
|
||||
self.right_sides = Side::from_int(right_int | (keys_int & sides));
|
||||
}
|
||||
|
||||
/// Returns true if the given mods match any remapping in this mask.
|
||||
/// This is a fast check to avoid expensive map lookups when no
|
||||
/// remapping could possibly apply.
|
||||
///
|
||||
/// Checks both that the modifier key is remapped AND that the
|
||||
/// specific side (left/right) being pressed has a remapping.
|
||||
pub fn matches(&self, mods: Mods) -> bool {
|
||||
// Find which pressed keys have remappings registered.
|
||||
let active = mods.keys().int() & self.keys.int();
|
||||
if active == 0 {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the pressed side matches a remapped side.
|
||||
let sides = mods.sides.int();
|
||||
let left_int = self.left_sides.int();
|
||||
let right_int = self.right_sides.int();
|
||||
let side_match = (!sides & left_int) | (sides & right_int);
|
||||
|
||||
// Final check: is any active (pressed + remapped) key also side-matched?
|
||||
(active & side_match) != 0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn m(f: impl FnOnce(&mut Mods)) -> Mods {
|
||||
let mut x = Mods::default();
|
||||
f(&mut x);
|
||||
x
|
||||
}
|
||||
|
||||
// For our own understanding
|
||||
#[test]
|
||||
fn mods_layout() {
|
||||
assert_eq!(Mods::default().int(), 0);
|
||||
assert_eq!(
|
||||
Mods {
|
||||
shift: true,
|
||||
..Default::default()
|
||||
}
|
||||
.int(),
|
||||
1
|
||||
);
|
||||
assert_eq!(Mods::from_int(Mods::SIDE_MASK.int()), Mods::SIDE_MASK);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn translation_macos_option_as_alt() {
|
||||
// Darwin only upstream; on Linux it is the identity.
|
||||
let mods = Mods {
|
||||
alt: true,
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(mods.translation(OptionAsAlt::True), mods);
|
||||
}
|
||||
|
||||
fn left(k: &str) -> Mods {
|
||||
match k {
|
||||
"ctrl" => m(|x| x.ctrl = true),
|
||||
"alt" => m(|x| x.alt = true),
|
||||
"shift" => m(|x| x.shift = true),
|
||||
_ => m(|x| x.super_ = true),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remap_set_unsided_remap_creates_both_left_and_right_mappings() {
|
||||
let mut set = RemapSet::default();
|
||||
set.parse("ctrl=super").unwrap();
|
||||
set.finalize();
|
||||
let want = Mods {
|
||||
super_: true,
|
||||
sides: Side {
|
||||
super_: ModSide::Left,
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(
|
||||
want,
|
||||
set.apply(Mods {
|
||||
ctrl: true,
|
||||
sides: Side {
|
||||
ctrl: ModSide::Left,
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
want,
|
||||
set.apply(Mods {
|
||||
ctrl: true,
|
||||
sides: Side {
|
||||
ctrl: ModSide::Right,
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
fn sided(name: &str, side: ModSide) -> Mods {
|
||||
init_mods(
|
||||
match name {
|
||||
"shift" => Mod::Shift,
|
||||
"ctrl" => Mod::Ctrl,
|
||||
"alt" => Mod::Alt,
|
||||
_ => Mod::Super,
|
||||
},
|
||||
side,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remap_set_sided_from_only_maps_that_side() {
|
||||
let mut set = RemapSet::default();
|
||||
set.parse("left_alt=ctrl").unwrap();
|
||||
set.finalize();
|
||||
assert_eq!(
|
||||
sided("ctrl", ModSide::Left),
|
||||
set.apply(sided("alt", ModSide::Left))
|
||||
);
|
||||
let right_alt = sided("alt", ModSide::Right);
|
||||
assert_eq!(right_alt, set.apply(right_alt));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remap_set_sided_to() {
|
||||
let mut set = RemapSet::default();
|
||||
set.parse("ctrl=right_super").unwrap();
|
||||
set.finalize();
|
||||
assert_eq!(
|
||||
sided("super", ModSide::Right),
|
||||
set.apply(sided("ctrl", ModSide::Left))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remap_set_both_sides_specified() {
|
||||
let mut set = RemapSet::default();
|
||||
set.parse("left_shift=right_ctrl").unwrap();
|
||||
set.finalize();
|
||||
assert_eq!(
|
||||
sided("ctrl", ModSide::Right),
|
||||
set.apply(sided("shift", ModSide::Left))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remap_set_multiple_parses_accumulate() {
|
||||
let mut set = RemapSet::default();
|
||||
set.parse("left_ctrl=super").unwrap();
|
||||
set.parse("left_alt=ctrl").unwrap();
|
||||
set.finalize();
|
||||
assert_eq!(
|
||||
sided("super", ModSide::Left),
|
||||
set.apply(sided("ctrl", ModSide::Left))
|
||||
);
|
||||
assert_eq!(
|
||||
sided("ctrl", ModSide::Left),
|
||||
set.apply(sided("alt", ModSide::Left))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remap_set_error_on_missing_assignment() {
|
||||
let mut set = RemapSet::default();
|
||||
assert_eq!(set.parse("ctrl"), Err(ParseError::MissingAssignment));
|
||||
assert_eq!(set.parse(""), Err(ParseError::MissingAssignment));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remap_set_error_on_invalid_modifier() {
|
||||
let mut set = RemapSet::default();
|
||||
assert_eq!(set.parse("invalid=ctrl"), Err(ParseError::InvalidMod));
|
||||
assert_eq!(set.parse("ctrl=invalid"), Err(ParseError::InvalidMod));
|
||||
assert_eq!(set.parse("middle_ctrl=super"), Err(ParseError::InvalidMod));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remap_set_is_remapped_checks_mask() {
|
||||
let mut set = RemapSet::default();
|
||||
set.parse("ctrl=super").unwrap();
|
||||
set.finalize();
|
||||
assert!(set.is_remapped(left("ctrl")));
|
||||
assert!(!set.is_remapped(left("alt")));
|
||||
assert!(!set.is_remapped(left("shift")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remap_set_clone_creates_independent_copy() {
|
||||
let mut set = RemapSet::default();
|
||||
set.parse("ctrl=super").unwrap();
|
||||
set.finalize();
|
||||
let cloned = set.clone();
|
||||
assert!(set.equal(&cloned));
|
||||
assert!(cloned.is_remapped(left("ctrl")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remap_set_equal_compares_correctly() {
|
||||
let mut set1 = RemapSet::default();
|
||||
let mut set2 = RemapSet::default();
|
||||
assert!(set1.equal(&set2));
|
||||
set1.parse("ctrl=super").unwrap();
|
||||
assert!(!set1.equal(&set2));
|
||||
set2.parse("ctrl=super").unwrap();
|
||||
assert!(set1.equal(&set2));
|
||||
set1.parse("alt=shift").unwrap();
|
||||
assert!(!set1.equal(&set2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remap_set_parse_cli_basic() {
|
||||
let mut set = RemapSet::default();
|
||||
set.parse_cli(Some("ctrl=super")).unwrap();
|
||||
assert_eq!(set.map.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remap_set_parse_cli_empty_clears() {
|
||||
let mut set = RemapSet::default();
|
||||
set.parse_cli(Some("ctrl=super")).unwrap();
|
||||
assert_eq!(set.map.len(), 2);
|
||||
set.parse_cli(Some("")).unwrap();
|
||||
assert_eq!(set.map.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remap_set_parse_cli_invalid() {
|
||||
let mut set = RemapSet::default();
|
||||
assert_eq!(
|
||||
set.parse_cli(Some("foo=bar")),
|
||||
Err(ParseCliError::InvalidValue)
|
||||
);
|
||||
assert_eq!(
|
||||
set.parse_cli(Some("ctrl")),
|
||||
Err(ParseCliError::InvalidValue)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remap_set_parse_aliased_modifiers() {
|
||||
let mut set = RemapSet::default();
|
||||
set.parse("cmd=ctrl").unwrap();
|
||||
set.finalize();
|
||||
assert_eq!(
|
||||
sided("ctrl", ModSide::Left),
|
||||
set.apply(sided("super", ModSide::Left))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remap_set_parse_aliased_modifiers_command() {
|
||||
let mut set = RemapSet::default();
|
||||
set.parse("command=alt").unwrap();
|
||||
set.finalize();
|
||||
assert_eq!(
|
||||
sided("alt", ModSide::Left),
|
||||
set.apply(sided("super", ModSide::Left))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remap_set_parse_aliased_modifiers_opt_and_option() {
|
||||
let mut set = RemapSet::default();
|
||||
set.parse("opt=super").unwrap();
|
||||
set.finalize();
|
||||
assert_eq!(
|
||||
sided("super", ModSide::Left),
|
||||
set.apply(sided("alt", ModSide::Left))
|
||||
);
|
||||
|
||||
let mut set = RemapSet::default();
|
||||
set.parse("option=shift").unwrap();
|
||||
set.finalize();
|
||||
assert_eq!(
|
||||
sided("shift", ModSide::Left),
|
||||
set.apply(sided("alt", ModSide::Left))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remap_set_parse_aliased_modifiers_control() {
|
||||
let mut set = RemapSet::default();
|
||||
set.parse("control=super").unwrap();
|
||||
set.finalize();
|
||||
assert_eq!(
|
||||
sided("super", ModSide::Left),
|
||||
set.apply(sided("ctrl", ModSide::Left))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remap_set_parse_aliased_modifiers_on_target_side() {
|
||||
let mut set = RemapSet::default();
|
||||
set.parse("alt=cmd").unwrap();
|
||||
set.finalize();
|
||||
assert_eq!(
|
||||
sided("super", ModSide::Left),
|
||||
set.apply(sided("alt", ModSide::Left))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remap_set_format_entry_empty() {
|
||||
// The `key-remap = \n` rendering is done by the config formatter;
|
||||
// here an empty set yields no entries.
|
||||
assert_eq!(RemapSet::default().format_entries(), Vec::<String>::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remap_set_format_entry_single_sided() {
|
||||
let mut set = RemapSet::default();
|
||||
set.parse("left_ctrl=super").unwrap();
|
||||
set.finalize();
|
||||
assert_eq!(set.format_entries(), vec!["left_ctrl=left_super"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remap_set_format_entry_unsided_creates_two_entries() {
|
||||
let mut set = RemapSet::default();
|
||||
set.parse("ctrl=super").unwrap();
|
||||
set.finalize();
|
||||
let w = set.format_entries();
|
||||
assert!(w.contains(&"left_ctrl=left_super".to_string()));
|
||||
assert!(w.contains(&"right_ctrl=left_super".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remap_set_format_entry_right_sided() {
|
||||
let mut set = RemapSet::default();
|
||||
set.parse("left_alt=right_ctrl").unwrap();
|
||||
set.finalize();
|
||||
assert_eq!(set.format_entries(), vec!["left_alt=right_ctrl"]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
//! Keyboard layouts (ZIG/src/input/keyboard.zig).
|
||||
|
||||
use crate::config::OptionAsAlt;
|
||||
|
||||
/// Keyboard layouts.
|
||||
///
|
||||
/// These aren't heavily used in Ghostty and having a fully comprehensive
|
||||
/// list is not important. We only need to distinguish between a few
|
||||
/// different layouts for some nice-to-have features, such as setting a default
|
||||
/// value for "macos-option-as-alt".
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Layout {
|
||||
/// Unknown, unmapped layout. Ghostty should not make any assumptions
|
||||
/// about the layout of the keyboard.
|
||||
Unknown,
|
||||
|
||||
// The remaining should be fairly self-explanatory:
|
||||
UsStandard,
|
||||
UsInternational,
|
||||
}
|
||||
|
||||
impl Layout {
|
||||
/// Map an Apple keyboard layout ID to a value in this enum. The layout
|
||||
/// ID can be retrieved using Carbon's TIKeyboardLayoutGetInputSourceProperty
|
||||
/// function.
|
||||
///
|
||||
/// Even though our layout supports "unknown", we return None if we don't
|
||||
/// recognize the layout ID so callers can detect this scenario.
|
||||
pub fn map_apple_id(id: &str) -> Option<Layout> {
|
||||
match id {
|
||||
"com.apple.keylayout.US" => Some(Layout::UsStandard),
|
||||
"com.apple.keylayout.USInternational" => Some(Layout::UsInternational),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the default macos-option-as-alt value for this layout.
|
||||
///
|
||||
/// We apply some heuristics to change the default based on the keyboard
|
||||
/// layout if "macos-option-as-alt" is unset. We do this because on some
|
||||
/// keyboard layouts such as US standard layouts, users generally expect
|
||||
/// an input such as option-b to map to alt-b but macOS by default will
|
||||
/// convert it to the codepoint "∫".
|
||||
///
|
||||
/// This behavior however is desired on international layout where the
|
||||
/// option key is used for important, regularly used inputs.
|
||||
pub fn detect_option_as_alt(self) -> OptionAsAlt {
|
||||
match self {
|
||||
// On US standard, the option key is typically used as alt
|
||||
// and not as a modifier for other codepoints. For example,
|
||||
// option-B = ∫ but usually the user wants alt-B.
|
||||
Layout::UsStandard | Layout::UsInternational => OptionAsAlt::True,
|
||||
Layout::Unknown => OptionAsAlt::False,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,697 @@
|
||||
// Based on the Chromium source. The Chromium source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
// https://source.chromium.org/chromium/chromium/src/+/main:ui/events/keycodes/dom/dom_code_data.inc
|
||||
|
||||
//! Keycode table mapping USB/evdev/XKB/Windows/Mac codes to W3C DOM codes
|
||||
//! (ZIG/src/input/keycodes.zig). The table is data-driven.
|
||||
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use crate::key::Key;
|
||||
|
||||
/// Entry contains the USB code, native keycode, and W3C dom code for
|
||||
/// the current platform.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Entry {
|
||||
/// input key enum
|
||||
pub key: Key,
|
||||
/// USB HID usage code
|
||||
pub usb: u32,
|
||||
/// Native keycode
|
||||
pub native: u32,
|
||||
/// W3C DOM code, static memory
|
||||
pub code: &'static str,
|
||||
}
|
||||
|
||||
/// The full list of entries for the current platform (Linux: XKB codes).
|
||||
pub fn entries() -> &'static [Entry] {
|
||||
static ENTRIES: OnceLock<Vec<Entry>> = OnceLock::new();
|
||||
ENTRIES.get_or_init(|| {
|
||||
// Linux/FreeBSD use the xkb column (index 2).
|
||||
RAW_ENTRIES
|
||||
.iter()
|
||||
.map(|raw| Entry {
|
||||
key: code_to_key(raw.5).unwrap_or(Key::Unidentified),
|
||||
usb: raw.0,
|
||||
code: raw.5,
|
||||
native: raw.2,
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
/// A map from code to key. This isn't meant to be used at runtime
|
||||
/// (though it could), so it isn't exported. It it used to build the
|
||||
/// key value for Entry.
|
||||
fn code_to_key(code: &str) -> Option<Key> {
|
||||
CODE_TO_KEY.iter().find(|e| e.0 == code).map(|e| e.1)
|
||||
}
|
||||
|
||||
const CODE_TO_KEY: &[(&str, Key)] = &[
|
||||
("KeyA", Key::KeyA),
|
||||
("KeyB", Key::KeyB),
|
||||
("KeyC", Key::KeyC),
|
||||
("KeyD", Key::KeyD),
|
||||
("KeyE", Key::KeyE),
|
||||
("KeyF", Key::KeyF),
|
||||
("KeyG", Key::KeyG),
|
||||
("KeyH", Key::KeyH),
|
||||
("KeyI", Key::KeyI),
|
||||
("KeyJ", Key::KeyJ),
|
||||
("KeyK", Key::KeyK),
|
||||
("KeyL", Key::KeyL),
|
||||
("KeyM", Key::KeyM),
|
||||
("KeyN", Key::KeyN),
|
||||
("KeyO", Key::KeyO),
|
||||
("KeyP", Key::KeyP),
|
||||
("KeyQ", Key::KeyQ),
|
||||
("KeyR", Key::KeyR),
|
||||
("KeyS", Key::KeyS),
|
||||
("KeyT", Key::KeyT),
|
||||
("KeyU", Key::KeyU),
|
||||
("KeyV", Key::KeyV),
|
||||
("KeyW", Key::KeyW),
|
||||
("KeyX", Key::KeyX),
|
||||
("KeyY", Key::KeyY),
|
||||
("KeyZ", Key::KeyZ),
|
||||
("Digit1", Key::Digit1),
|
||||
("Digit2", Key::Digit2),
|
||||
("Digit3", Key::Digit3),
|
||||
("Digit4", Key::Digit4),
|
||||
("Digit5", Key::Digit5),
|
||||
("Digit6", Key::Digit6),
|
||||
("Digit7", Key::Digit7),
|
||||
("Digit8", Key::Digit8),
|
||||
("Digit9", Key::Digit9),
|
||||
("Digit0", Key::Digit0),
|
||||
("Enter", Key::Enter),
|
||||
("Escape", Key::Escape),
|
||||
("Backspace", Key::Backspace),
|
||||
("Tab", Key::Tab),
|
||||
("Space", Key::Space),
|
||||
("Minus", Key::Minus),
|
||||
("Equal", Key::Equal),
|
||||
("BracketLeft", Key::BracketLeft),
|
||||
("BracketRight", Key::BracketRight),
|
||||
("Backslash", Key::Backslash),
|
||||
("Semicolon", Key::Semicolon),
|
||||
("Quote", Key::Quote),
|
||||
("Backquote", Key::Backquote),
|
||||
("Comma", Key::Comma),
|
||||
("Period", Key::Period),
|
||||
("Slash", Key::Slash),
|
||||
("CapsLock", Key::CapsLock),
|
||||
("F1", Key::F1),
|
||||
("F2", Key::F2),
|
||||
("F3", Key::F3),
|
||||
("F4", Key::F4),
|
||||
("F5", Key::F5),
|
||||
("F6", Key::F6),
|
||||
("F7", Key::F7),
|
||||
("F8", Key::F8),
|
||||
("F9", Key::F9),
|
||||
("F10", Key::F10),
|
||||
("F11", Key::F11),
|
||||
("F12", Key::F12),
|
||||
("F13", Key::F13),
|
||||
("F14", Key::F14),
|
||||
("F15", Key::F15),
|
||||
("F16", Key::F16),
|
||||
("F17", Key::F17),
|
||||
("F18", Key::F18),
|
||||
("F19", Key::F19),
|
||||
("F20", Key::F20),
|
||||
("F21", Key::F21),
|
||||
("F22", Key::F22),
|
||||
("F23", Key::F23),
|
||||
("F24", Key::F24),
|
||||
("PrintScreen", Key::PrintScreen),
|
||||
("ScrollLock", Key::ScrollLock),
|
||||
("Pause", Key::Pause),
|
||||
("Insert", Key::Insert),
|
||||
("Home", Key::Home),
|
||||
("PageUp", Key::PageUp),
|
||||
("Delete", Key::Delete),
|
||||
("End", Key::End),
|
||||
("Copy", Key::Copy),
|
||||
("Cut", Key::Cut),
|
||||
("Paste", Key::Paste),
|
||||
("PageDown", Key::PageDown),
|
||||
("ArrowRight", Key::ArrowRight),
|
||||
("ArrowLeft", Key::ArrowLeft),
|
||||
("ArrowDown", Key::ArrowDown),
|
||||
("ArrowUp", Key::ArrowUp),
|
||||
("NumLock", Key::NumLock),
|
||||
("NumpadDivide", Key::NumpadDivide),
|
||||
("NumpadMultiply", Key::NumpadMultiply),
|
||||
("NumpadSubtract", Key::NumpadSubtract),
|
||||
("NumpadAdd", Key::NumpadAdd),
|
||||
("NumpadEnter", Key::NumpadEnter),
|
||||
("Numpad1", Key::Numpad1),
|
||||
("Numpad2", Key::Numpad2),
|
||||
("Numpad3", Key::Numpad3),
|
||||
("Numpad4", Key::Numpad4),
|
||||
("Numpad5", Key::Numpad5),
|
||||
("Numpad6", Key::Numpad6),
|
||||
("Numpad7", Key::Numpad7),
|
||||
("Numpad8", Key::Numpad8),
|
||||
("Numpad9", Key::Numpad9),
|
||||
("Numpad0", Key::Numpad0),
|
||||
("NumpadDecimal", Key::NumpadDecimal),
|
||||
("NumpadEqual", Key::NumpadEqual),
|
||||
("ContextMenu", Key::ContextMenu),
|
||||
("ControlLeft", Key::ControlLeft),
|
||||
("ShiftLeft", Key::ShiftLeft),
|
||||
("AltLeft", Key::AltLeft),
|
||||
("MetaLeft", Key::MetaLeft),
|
||||
("ControlRight", Key::ControlRight),
|
||||
("ShiftRight", Key::ShiftRight),
|
||||
("AltRight", Key::AltRight),
|
||||
("MetaRight", Key::MetaRight),
|
||||
];
|
||||
|
||||
/// The codes for the table from the Chromium data set. These are ALL the
|
||||
/// codes, not just the ones that are supported by the current platform.
|
||||
/// These are `pub` but you shouldn't use this because it uses way more
|
||||
/// memory than is necessary.
|
||||
///
|
||||
/// The format is: usb, evdev, xkb, win, mac, code
|
||||
pub type RawEntry = (u32, u32, u32, u32, u32, &'static str);
|
||||
|
||||
/// All of the full entries. This is marked pub but it should NOT be referenced
|
||||
/// directly because it contains too much data for normal usage. Use `entries`
|
||||
/// instead which contains just the relevant data for the target platform.
|
||||
#[rustfmt::skip]
|
||||
pub const RAW_ENTRIES: &[RawEntry] = &[
|
||||
// USB evdev XKB Win Mac Code
|
||||
(0x000000, 0x0000, 0x0000, 0x0000, 0xffff, ""),
|
||||
|
||||
// =========================================
|
||||
// Non-USB codes
|
||||
// =========================================
|
||||
|
||||
// USB evdev XKB Win Mac Code
|
||||
(0x000010, 0x0000, 0x0000, 0x0000, 0xffff, "Hyper"),
|
||||
(0x000011, 0x0000, 0x0000, 0x0000, 0xffff, "Super"),
|
||||
(0x000012, 0x0000, 0x0000, 0x0000, 0xffff, "Fn"),
|
||||
(0x000013, 0x0000, 0x0000, 0x0000, 0xffff, "FnLock"),
|
||||
(0x000014, 0x0000, 0x0000, 0x0000, 0xffff, "Suspend"),
|
||||
(0x000015, 0x0000, 0x0000, 0x0000, 0xffff, "Resume"),
|
||||
(0x000016, 0x0000, 0x0000, 0x0000, 0xffff, "Turbo"),
|
||||
|
||||
// =========================================
|
||||
// USB Usage Page 0x01: Generic Desktop Page
|
||||
// =========================================
|
||||
|
||||
// Sleep could be encoded as USB#0c0032, but there's no corresponding WakeUp
|
||||
// in the 0x0c USB page.
|
||||
// USB evdev XKB Win Mac
|
||||
(0x010082, 0x008e, 0x0096, 0xe05f, 0xffff, "Sleep"),
|
||||
(0x010083, 0x008f, 0x0097, 0xe063, 0xffff, "WakeUp"),
|
||||
(0x0100a9, 0x00f8, 0x0100, 0x0000, 0xffff, ""),
|
||||
(0x0100b5, 0x00e3, 0x00eb, 0x0000, 0xffff, "DisplayToggleIntExt"),
|
||||
|
||||
// =========================================
|
||||
// USB Usage Page 0x07: Keyboard/Keypad Page
|
||||
// =========================================
|
||||
|
||||
// TODO(garykac):
|
||||
// XKB#005c ISO Level3 Shift (AltGr)
|
||||
// XKB#005e <>||
|
||||
// XKB#006d Linefeed
|
||||
// XKB#008a SunProps cf. USB#0700a3 CrSel/Props
|
||||
// XKB#008e SunOpen
|
||||
// Mac#003f kVK_Function
|
||||
// Mac#000a kVK_ISO_Section (ISO keyboards only)
|
||||
// Mac#0066 kVK_JIS_Eisu (USB#07008a Henkan?)
|
||||
|
||||
// USB evdev XKB Win Mac
|
||||
(0x070000, 0x0000, 0x0000, 0x0000, 0xffff, ""),
|
||||
(0x070001, 0x0000, 0x0000, 0x00ff, 0xffff, ""),
|
||||
(0x070002, 0x0000, 0x0000, 0x00fc, 0xffff, ""),
|
||||
(0x070003, 0x0000, 0x0000, 0x0000, 0xffff, ""),
|
||||
(0x070004, 0x001e, 0x0026, 0x001e, 0x0000, "KeyA"),
|
||||
(0x070005, 0x0030, 0x0038, 0x0030, 0x000b, "KeyB"),
|
||||
(0x070006, 0x002e, 0x0036, 0x002e, 0x0008, "KeyC"),
|
||||
(0x070007, 0x0020, 0x0028, 0x0020, 0x0002, "KeyD"),
|
||||
|
||||
(0x070008, 0x0012, 0x001a, 0x0012, 0x000e, "KeyE"),
|
||||
(0x070009, 0x0021, 0x0029, 0x0021, 0x0003, "KeyF"),
|
||||
(0x07000a, 0x0022, 0x002a, 0x0022, 0x0005, "KeyG"),
|
||||
(0x07000b, 0x0023, 0x002b, 0x0023, 0x0004, "KeyH"),
|
||||
(0x07000c, 0x0017, 0x001f, 0x0017, 0x0022, "KeyI"),
|
||||
(0x07000d, 0x0024, 0x002c, 0x0024, 0x0026, "KeyJ"),
|
||||
(0x07000e, 0x0025, 0x002d, 0x0025, 0x0028, "KeyK"),
|
||||
(0x07000f, 0x0026, 0x002e, 0x0026, 0x0025, "KeyL"),
|
||||
|
||||
(0x070010, 0x0032, 0x003a, 0x0032, 0x002e, "KeyM"),
|
||||
(0x070011, 0x0031, 0x0039, 0x0031, 0x002d, "KeyN"),
|
||||
(0x070012, 0x0018, 0x0020, 0x0018, 0x001f, "KeyO"),
|
||||
(0x070013, 0x0019, 0x0021, 0x0019, 0x0023, "KeyP"),
|
||||
(0x070014, 0x0010, 0x0018, 0x0010, 0x000c, "KeyQ"),
|
||||
(0x070015, 0x0013, 0x001b, 0x0013, 0x000f, "KeyR"),
|
||||
(0x070016, 0x001f, 0x0027, 0x001f, 0x0001, "KeyS"),
|
||||
(0x070017, 0x0014, 0x001c, 0x0014, 0x0011, "KeyT"),
|
||||
|
||||
(0x070018, 0x0016, 0x001e, 0x0016, 0x0020, "KeyU"),
|
||||
(0x070019, 0x002f, 0x0037, 0x002f, 0x0009, "KeyV"),
|
||||
(0x07001a, 0x0011, 0x0019, 0x0011, 0x000d, "KeyW"),
|
||||
(0x07001b, 0x002d, 0x0035, 0x002d, 0x0007, "KeyX"),
|
||||
(0x07001c, 0x0015, 0x001d, 0x0015, 0x0010, "KeyY"),
|
||||
(0x07001d, 0x002c, 0x0034, 0x002c, 0x0006, "KeyZ"),
|
||||
(0x07001e, 0x0002, 0x000a, 0x0002, 0x0012, "Digit1"),
|
||||
(0x07001f, 0x0003, 0x000b, 0x0003, 0x0013, "Digit2"),
|
||||
|
||||
(0x070020, 0x0004, 0x000c, 0x0004, 0x0014, "Digit3"),
|
||||
(0x070021, 0x0005, 0x000d, 0x0005, 0x0015, "Digit4"),
|
||||
(0x070022, 0x0006, 0x000e, 0x0006, 0x0017, "Digit5"),
|
||||
(0x070023, 0x0007, 0x000f, 0x0007, 0x0016, "Digit6"),
|
||||
(0x070024, 0x0008, 0x0010, 0x0008, 0x001a, "Digit7"),
|
||||
(0x070025, 0x0009, 0x0011, 0x0009, 0x001c, "Digit8"),
|
||||
(0x070026, 0x000a, 0x0012, 0x000a, 0x0019, "Digit9"),
|
||||
(0x070027, 0x000b, 0x0013, 0x000b, 0x001d, "Digit0"),
|
||||
|
||||
(0x070028, 0x001c, 0x0024, 0x001c, 0x0024, "Enter"),
|
||||
(0x070029, 0x0001, 0x0009, 0x0001, 0x0035, "Escape"),
|
||||
(0x07002a, 0x000e, 0x0016, 0x000e, 0x0033, "Backspace"),
|
||||
(0x07002b, 0x000f, 0x0017, 0x000f, 0x0030, "Tab"),
|
||||
(0x07002c, 0x0039, 0x0041, 0x0039, 0x0031, "Space"),
|
||||
(0x07002d, 0x000c, 0x0014, 0x000c, 0x001b, "Minus"),
|
||||
(0x07002e, 0x000d, 0x0015, 0x000d, 0x0018, "Equal"),
|
||||
(0x07002f, 0x001a, 0x0022, 0x001a, 0x0021, "BracketLeft"),
|
||||
|
||||
(0x070030, 0x001b, 0x0023, 0x001b, 0x001e, "BracketRight"),
|
||||
(0x070031, 0x002b, 0x0033, 0x002b, 0x002a, "Backslash"),
|
||||
// USB#070032 never appears on keyboards that have USB#070031.
|
||||
// Platforms use the same scancode as for the two keys.
|
||||
// Hence this code can only be generated synthetically
|
||||
// (e.g. in a DOM Level 3 KeyboardEvent).
|
||||
// The keycap varies on international keyboards:
|
||||
// Dan: '* Dutch: <> Ger: #' UK: #~
|
||||
// TODO(garykac): Verify Mac intl keyboard.
|
||||
// .{ 0x070032, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
(0x070033, 0x0027, 0x002f, 0x0027, 0x0029, "Semicolon"),
|
||||
(0x070034, 0x0028, 0x0030, 0x0028, 0x0027, "Quote"),
|
||||
(0x070035, 0x0029, 0x0031, 0x0029, 0x0032, "Backquote"),
|
||||
(0x070036, 0x0033, 0x003b, 0x0033, 0x002b, "Comma"),
|
||||
(0x070037, 0x0034, 0x003c, 0x0034, 0x002f, "Period"),
|
||||
|
||||
(0x070038, 0x0035, 0x003d, 0x0035, 0x002c, "Slash"),
|
||||
// TODO(garykac): CapsLock requires special handling for each platform.
|
||||
(0x070039, 0x003a, 0x0042, 0x003a, 0x0039, "CapsLock"),
|
||||
(0x07003a, 0x003b, 0x0043, 0x003b, 0x007a, "F1"),
|
||||
(0x07003b, 0x003c, 0x0044, 0x003c, 0x0078, "F2"),
|
||||
(0x07003c, 0x003d, 0x0045, 0x003d, 0x0063, "F3"),
|
||||
(0x07003d, 0x003e, 0x0046, 0x003e, 0x0076, "F4"),
|
||||
(0x07003e, 0x003f, 0x0047, 0x003f, 0x0060, "F5"),
|
||||
(0x07003f, 0x0040, 0x0048, 0x0040, 0x0061, "F6"),
|
||||
|
||||
(0x070040, 0x0041, 0x0049, 0x0041, 0x0062, "F7"),
|
||||
(0x070041, 0x0042, 0x004a, 0x0042, 0x0064, "F8"),
|
||||
(0x070042, 0x0043, 0x004b, 0x0043, 0x0065, "F9"),
|
||||
(0x070043, 0x0044, 0x004c, 0x0044, 0x006d, "F10"),
|
||||
(0x070044, 0x0057, 0x005f, 0x0057, 0x0067, "F11"),
|
||||
(0x070045, 0x0058, 0x0060, 0x0058, 0x006f, "F12"),
|
||||
// PrintScreen is effectively F13 on Mac OS X.
|
||||
(0x070046, 0x0063, 0x006b, 0xe037, 0xffff, "PrintScreen"),
|
||||
(0x070047, 0x0046, 0x004e, 0x0046, 0xffff, "ScrollLock"),
|
||||
|
||||
(0x070048, 0x0077, 0x007f, 0x0045, 0xffff, "Pause"),
|
||||
// USB#0x070049 Insert, labeled "Help/Insert" on Mac -- see note M1 at top.
|
||||
(0x070049, 0x006e, 0x0076, 0xe052, 0x0072, "Insert"),
|
||||
(0x07004a, 0x0066, 0x006e, 0xe047, 0x0073, "Home"),
|
||||
(0x07004b, 0x0068, 0x0070, 0xe049, 0x0074, "PageUp"),
|
||||
// Delete (Forward Delete) named DEL because DELETE conflicts with <windows.h>
|
||||
(0x07004c, 0x006f, 0x0077, 0xe053, 0x0075, "Delete"),
|
||||
(0x07004d, 0x006b, 0x0073, 0xe04f, 0x0077, "End"),
|
||||
(0x07004e, 0x006d, 0x0075, 0xe051, 0x0079, "PageDown"),
|
||||
(0x07004f, 0x006a, 0x0072, 0xe04d, 0x007c, "ArrowRight"),
|
||||
|
||||
(0x070050, 0x0069, 0x0071, 0xe04b, 0x007b, "ArrowLeft"),
|
||||
(0x070051, 0x006c, 0x0074, 0xe050, 0x007d, "ArrowDown"),
|
||||
(0x070052, 0x0067, 0x006f, 0xe048, 0x007e, "ArrowUp"),
|
||||
(0x070053, 0x0045, 0x004d, 0xe045, 0x0047, "NumLock"),
|
||||
(0x070054, 0x0062, 0x006a, 0xe035, 0x004b, "NumpadDivide"),
|
||||
(0x070055, 0x0037, 0x003f, 0x0037, 0x0043, "NumpadMultiply"),
|
||||
(0x070056, 0x004a, 0x0052, 0x004a, 0x004e, "NumpadSubtract"),
|
||||
(0x070057, 0x004e, 0x0056, 0x004e, 0x0045, "NumpadAdd"),
|
||||
|
||||
(0x070058, 0x0060, 0x0068, 0xe01c, 0x004c, "NumpadEnter"),
|
||||
(0x070059, 0x004f, 0x0057, 0x004f, 0x0053, "Numpad1"), // +End
|
||||
(0x07005a, 0x0050, 0x0058, 0x0050, 0x0054, "Numpad2"), // +Down
|
||||
(0x07005b, 0x0051, 0x0059, 0x0051, 0x0055, "Numpad3"), // +PageDown
|
||||
(0x07005c, 0x004b, 0x0053, 0x004b, 0x0056, "Numpad4"), // +Left
|
||||
(0x07005d, 0x004c, 0x0054, 0x004c, 0x0057, "Numpad5"),
|
||||
(0x07005e, 0x004d, 0x0055, 0x004d, 0x0058, "Numpad6"), // +Right
|
||||
(0x07005f, 0x0047, 0x004f, 0x0047, 0x0059, "Numpad7"), // +Home
|
||||
(0x070060, 0x0048, 0x0050, 0x0048, 0x005b, "Numpad8"), // +Up
|
||||
(0x070061, 0x0049, 0x0051, 0x0049, 0x005c, "Numpad9"), // +PageUp
|
||||
(0x070062, 0x0052, 0x005a, 0x0052, 0x0052, "Numpad0"), // +Insert
|
||||
(0x070063, 0x0053, 0x005b, 0x0053, 0x0041, "NumpadDecimal"), // +Delete
|
||||
// USB#070064 is not present on US keyboard.
|
||||
// This key is typically located near LeftShift key.
|
||||
// The keycap varies on international keyboards:
|
||||
// Dan: <> Dutch: ][ Ger: <> UK: \|
|
||||
(0x070064, 0x0056, 0x005e, 0x0056, 0x000a, "IntlBackslash"),
|
||||
// USB#0x070065 Application Menu (next to RWin key) -- see note L2 at top.
|
||||
(0x070065, 0x007f, 0x0087, 0xe05d, 0x006e, "ContextMenu"),
|
||||
(0x070066, 0x0074, 0x007c, 0xe05e, 0xffff, "Power"),
|
||||
(0x070067, 0x0075, 0x007d, 0x0059, 0x0051, "NumpadEqual"),
|
||||
|
||||
(0x070068, 0x00b7, 0x00bf, 0x0064, 0x0069, "F13"),
|
||||
(0x070069, 0x00b8, 0x00c0, 0x0065, 0x006b, "F14"),
|
||||
(0x07006a, 0x00b9, 0x00c1, 0x0066, 0x0071, "F15"),
|
||||
(0x07006b, 0x00ba, 0x00c2, 0x0067, 0x006a, "F16"),
|
||||
(0x07006c, 0x00bb, 0x00c3, 0x0068, 0x0040, "F17"),
|
||||
(0x07006d, 0x00bc, 0x00c4, 0x0069, 0x004f, "F18"),
|
||||
(0x07006e, 0x00bd, 0x00c5, 0x006a, 0x0050, "F19"),
|
||||
(0x07006f, 0x00be, 0x00c6, 0x006b, 0x005a, "F20"),
|
||||
|
||||
(0x070070, 0x00bf, 0x00c7, 0x006c, 0xffff, "F21"),
|
||||
(0x070071, 0x00c0, 0x00c8, 0x006d, 0xffff, "F22"),
|
||||
(0x070072, 0x00c1, 0x00c9, 0x006e, 0xffff, "F23"),
|
||||
// USB#0x070073 -- see note W1 at top.
|
||||
(0x070073, 0x00c2, 0x00ca, 0x0076, 0xffff, "F24"),
|
||||
(0x070074, 0x0086, 0x008e, 0x0000, 0xffff, "Open"),
|
||||
// USB#0x070075 Help -- see note M1 at top.
|
||||
(0x070075, 0x008a, 0x0092, 0xe03b, 0xffff, "Help"),
|
||||
// USB#0x070076 Keyboard Menu -- see note L2 at top.
|
||||
// .{ 0x070076, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
(0x070077, 0x0084, 0x008c, 0x0000, 0xffff, "Select"),
|
||||
|
||||
// .{ 0x070078, 0x0080, 0x0088, 0x0000, 0xffff, ""},
|
||||
(0x070079, 0x0081, 0x0089, 0x0000, 0xffff, "Again"),
|
||||
(0x07007a, 0x0083, 0x008b, 0xe008, 0xffff, "Undo"),
|
||||
(0x07007b, 0x0089, 0x0091, 0xe017, 0xffff, "Cut"),
|
||||
(0x07007c, 0x0085, 0x008d, 0xe018, 0xffff, "Copy"),
|
||||
(0x07007d, 0x0087, 0x008f, 0xe00a, 0xffff, "Paste"),
|
||||
(0x07007e, 0x0088, 0x0090, 0x0000, 0xffff, "Find"),
|
||||
(0x07007f, 0x0071, 0x0079, 0xe020, 0x004a, "AudioVolumeMute"),
|
||||
|
||||
(0x070080, 0x0073, 0x007b, 0xe030, 0x0048, "AudioVolumeUp"),
|
||||
(0x070081, 0x0072, 0x007a, 0xe02e, 0x0049, "AudioVolumeDown"),
|
||||
// .{ 0x070082, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
// .{ 0x070083, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
// .{ 0x070084, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
(0x070085, 0x0079, 0x0081, 0x007e, 0x005f, "NumpadComma"),
|
||||
|
||||
// International1
|
||||
// USB#070086 is used on AS/400 keyboards. Standard Keypad_= is USB#070067.
|
||||
// .{ 0x070086, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
// USB#070087 is used for Brazilian /? and Japanese _ 'ro'.
|
||||
(0x070087, 0x0059, 0x0061, 0x0073, 0x005e, "IntlRo"),
|
||||
// International2
|
||||
// USB#070088 is used as Japanese Hiragana/Katakana key.
|
||||
(0x070088, 0x005d, 0x0065, 0x0070, 0xffff, "KanaMode"),
|
||||
// International3
|
||||
// USB#070089 is used as Japanese Yen key.
|
||||
(0x070089, 0x007c, 0x0084, 0x007d, 0x005d, "IntlYen"),
|
||||
// International4
|
||||
// USB#07008a is used as Japanese Henkan (Convert) key.
|
||||
(0x07008a, 0x005c, 0x0064, 0x0079, 0xffff, "Convert"),
|
||||
// International5
|
||||
// USB#07008b is used as Japanese Muhenkan (No-convert) key.
|
||||
(0x07008b, 0x005e, 0x0066, 0x007b, 0xffff, "NonConvert"),
|
||||
// .{ 0x07008c, 0x005f, 0x0067, 0x005c, 0xffff, ""},
|
||||
// .{ 0x07008d, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
// .{ 0x07008e, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
// .{ 0x07008f, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
|
||||
// LANG1
|
||||
// USB#070090 is used as Korean Hangul/English toggle key, and as the Kana key
|
||||
// on the Apple Japanese keyboard.
|
||||
(0x070090, 0x007a, 0x0082, 0x0072, 0x0068, "Lang1"),
|
||||
// LANG2
|
||||
// USB#070091 is used as Korean Hanja conversion key, and as the Eisu key on
|
||||
// the Apple Japanese keyboard.
|
||||
(0x070091, 0x007b, 0x0083, 0x0071, 0x0066, "Lang2"),
|
||||
// LANG3
|
||||
// USB#070092 is used as Japanese Katakana key.
|
||||
(0x070092, 0x005a, 0x0062, 0x0078, 0xffff, "Lang3"),
|
||||
// LANG4
|
||||
// USB#070093 is used as Japanese Hiragana key.
|
||||
(0x070093, 0x005b, 0x0063, 0x0077, 0xffff, "Lang4"),
|
||||
// LANG5
|
||||
// USB#070094 is used as Japanese Zenkaku/Hankaku (Fullwidth/halfwidth) key.
|
||||
// Not mapped on Windows -- see note W1 at top.
|
||||
(0x070094, 0x0055, 0x005d, 0x0000, 0xffff, "Lang5"),
|
||||
// .{ 0x070095, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
// .{ 0x070096, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
// .{ 0x070097, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
// .{ 0x070098, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
|
||||
// .{ 0x070099, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
// .{ 0x07009a, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
(0x07009b, 0x0000, 0x0000, 0x0000, 0xffff, "Abort"),
|
||||
// USB#0x07009c Keyboard Clear -- see note L1 at top.
|
||||
// .{ 0x07009c, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
// .{ 0x07009d, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
// .{ 0x07009e, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
// .{ 0x07009f, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
|
||||
// .{ 0x0700a0, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
// .{ 0x0700a1, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
// .{ 0x0700a2, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
// USB#0x0700a3 Props -- see note L2 at top.
|
||||
(0x0700a3, 0x0000, 0x0000, 0x0000, 0xffff, "Props"),
|
||||
// .{ 0x0700a4, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
|
||||
// .{ 0x0700b0, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
// .{ 0x0700b1, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
// .{ 0x0700b2, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
// .{ 0x0700b3, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
// .{ 0x0700b4, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
// .{ 0x0700b5, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
(0x0700b6, 0x00b3, 0x00bb, 0x0000, 0xffff, "NumpadParenLeft"),
|
||||
(0x0700b7, 0x00b4, 0x00bc, 0x0000, 0xffff, "NumpadParenRight"),
|
||||
|
||||
// .{ 0x0700b8, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
// .{ 0x0700b9, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
// .{ 0x0700ba, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
(0x0700bb, 0x0000, 0x0000, 0x0000, 0xffff, "NumpadBackspace"),
|
||||
// .{ 0x0700bc, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
// .{ 0x0700bd, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
// .{ 0x0700be, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
// .{ 0x0700bf, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
|
||||
// .{ 0x0700c0, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
// .{ 0x0700c1, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
// .{ 0x0700c2, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
// .{ 0x0700c3, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
// .{ 0x0700c4, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
// .{ 0x0700c5, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
// .{ 0x0700c6, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
// .{ 0x0700c7, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
|
||||
// .{ 0x0700c8, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
// .{ 0x0700c9, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
// .{ 0x0700ca, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
// NUMPAD_DOUBLE_VERTICAL_BAR), // Keypad_||
|
||||
// .{ 0x0700cb, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
// .{ 0x0700cc, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
// .{ 0x0700cd, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
// .{ 0x0700ce, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
// .{ 0x0700cf, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
|
||||
(0x0700d0, 0x0000, 0x0000, 0x0000, 0xffff, "NumpadMemoryStore"),
|
||||
(0x0700d1, 0x0000, 0x0000, 0x0000, 0xffff, "NumpadMemoryRecall"),
|
||||
(0x0700d2, 0x0000, 0x0000, 0x0000, 0xffff, "NumpadMemoryClear"),
|
||||
(0x0700d3, 0x0000, 0x0000, 0x0000, 0xffff, "NumpadMemoryAdd"),
|
||||
(0x0700d4, 0x0000, 0x0000, 0x0000, 0xffff, "NumpadMemorySubtract"),
|
||||
// .{ 0x0700d5, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
// .{ 0x0700d6, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
(0x0700d7, 0x0076, 0x007e, 0x0000, 0xffff, ""),
|
||||
// USB#0x0700d8 Keypad Clear -- see note L1 at top.
|
||||
(0x0700d8, 0x0000, 0x0000, 0x0000, 0xffff, "NumpadClear"),
|
||||
(0x0700d9, 0x0000, 0x0000, 0x0000, 0xffff, "NumpadClearEntry"),
|
||||
// .{ 0x0700da, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
// .{ 0x0700db, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
// .{ 0x0700dc, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
// .{ 0x0700dd, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
|
||||
// USB#0700de - #0700df are reserved.
|
||||
(0x0700e0, 0x001d, 0x0025, 0x001d, 0x003b, "ControlLeft"),
|
||||
(0x0700e1, 0x002a, 0x0032, 0x002a, 0x0038, "ShiftLeft"),
|
||||
// USB#0700e2: left Alt key (Mac left Option key).
|
||||
(0x0700e2, 0x0038, 0x0040, 0x0038, 0x003a, "AltLeft"),
|
||||
// USB#0700e3: left GUI key, e.g. Windows, Mac Command, ChromeOS Search.
|
||||
(0x0700e3, 0x007d, 0x0085, 0xe05b, 0x0037, "MetaLeft"),
|
||||
(0x0700e4, 0x0061, 0x0069, 0xe01d, 0x003e, "ControlRight"),
|
||||
(0x0700e5, 0x0036, 0x003e, 0x0036, 0x003c, "ShiftRight"),
|
||||
// USB#0700e6: right Alt key (Mac right Option key).
|
||||
(0x0700e6, 0x0064, 0x006c, 0xe038, 0x003d, "AltRight"),
|
||||
// USB#0700e7: right GUI key, e.g. Windows, Mac Command, ChromeOS Search.
|
||||
(0x0700e7, 0x007e, 0x0086, 0xe05c, 0x0036, "MetaRight"),
|
||||
|
||||
// USB#0700e8 - #07ffff are reserved
|
||||
|
||||
// ==================================
|
||||
// USB Usage Page 0x0c: Consumer Page
|
||||
// ==================================
|
||||
// AL = Application Launch
|
||||
// AC = Application Control
|
||||
|
||||
// TODO(garykac): Many XF86 keys have multiple scancodes mapping to them.
|
||||
// We need to map all of these into a canonical USB scancode without
|
||||
// confusing the reverse-lookup - most likely by simply returning the first
|
||||
// found match.
|
||||
|
||||
// TODO(garykac): Find appropriate mappings for:
|
||||
// Win#e03c Music - USB#0c0193 is AL_AVCapturePlayback
|
||||
// Win#e064 Pictures
|
||||
// XKB#0080 XF86LaunchA
|
||||
// XKB#0099 XF86Send
|
||||
// XKB#009b XF86Xfer
|
||||
// XKB#009c XF86Launch1
|
||||
// XKB#009d XF86Launch2
|
||||
// XKB... remaining XF86 keys
|
||||
|
||||
// KEY_BRIGHTNESS* added in Linux 3.16
|
||||
// http://www.usb.org/developers/hidpage/HUTRR41.pdf
|
||||
//
|
||||
// Keyboard backlight/illumination spec update.
|
||||
// https://www.usb.org/sites/default/files/hutrr73_-_fn_key_and_keyboard_backlight_brightness_0.pdf
|
||||
// USB evdev XKB Win Mac Code
|
||||
(0x0c0060, 0x0166, 0x016e, 0x0000, 0xffff, ""),
|
||||
(0x0c0061, 0x0172, 0x017a, 0x0000, 0xffff, ""),
|
||||
(0x0c006f, 0x00e1, 0x00e9, 0x0000, 0xffff, "BrightnessUp"),
|
||||
(0x0c0070, 0x00e0, 0x00e8, 0x0000, 0xffff, "BrightnessDown"),
|
||||
(0x0c0072, 0x01af, 0x01b7, 0x0000, 0xffff, ""),
|
||||
(0x0c0073, 0x0250, 0x0258, 0x0000, 0xffff, ""),
|
||||
(0x0c0074, 0x0251, 0x0259, 0x0000, 0xffff, ""),
|
||||
(0x0c0075, 0x00f4, 0x00fc, 0x0000, 0xffff, ""),
|
||||
(0x0c0079, 0x00e6, 0x00ee, 0x0000, 0xffff, ""),
|
||||
(0x0c007a, 0x00e5, 0x00ed, 0x0000, 0xffff, ""),
|
||||
(0x0c007c, 0x00e4, 0x00ec, 0x0000, 0xffff, ""),
|
||||
(0x0c0083, 0x0195, 0x019d, 0x0000, 0xffff, ""),
|
||||
(0x0c008c, 0x00a9, 0x00b1, 0x0000, 0xffff, ""),
|
||||
(0x0c008d, 0x016a, 0x0172, 0x0000, 0xffff, ""),
|
||||
(0x0c0094, 0x00ae, 0x00b6, 0x0000, 0xffff, ""),
|
||||
(0x0c009c, 0x019a, 0x01a2, 0x0000, 0xffff, ""),
|
||||
(0x0c009d, 0x019b, 0x01a3, 0x0000, 0xffff, ""),
|
||||
|
||||
// USB evdev XKB Win Mac
|
||||
(0x0c00b0, 0x00cf, 0x00d7, 0x0000, 0xffff, "MediaPlay"),
|
||||
(0x0c00b1, 0x00c9, 0x00d1, 0x0000, 0xffff, "MediaPause"),
|
||||
(0x0c00b2, 0x00a7, 0x00af, 0x0000, 0xffff, "MediaRecord"),
|
||||
(0x0c00b3, 0x00d0, 0x00d8, 0x0000, 0xffff, "MediaFastForward"),
|
||||
(0x0c00b4, 0x00a8, 0x00b0, 0x0000, 0xffff, "MediaRewind"),
|
||||
(0x0c00b5, 0x00a3, 0x00ab, 0xe019, 0xffff, "MediaTrackNext"),
|
||||
(0x0c00b6, 0x00a5, 0x00ad, 0xe010, 0xffff, "MediaTrackPrevious"),
|
||||
(0x0c00b7, 0x00a6, 0x00ae, 0xe024, 0xffff, "MediaStop"),
|
||||
(0x0c00b8, 0x00a1, 0x00a9, 0xe02c, 0xffff, "Eject"),
|
||||
(0x0c00cd, 0x00a4, 0x00ac, 0xe022, 0xffff, "MediaPlayPause"),
|
||||
(0x0c00cf, 0x0246, 0x024e, 0x0000, 0xffff, ""),
|
||||
(0x0c00d8, 0x024a, 0x0252, 0x0000, 0xffff, ""),
|
||||
(0x0c00d9, 0x0249, 0x0251, 0x0000, 0xffff, ""),
|
||||
(0x0c00e5, 0x00d1, 0x00d9, 0x0000, 0xffff, ""),
|
||||
// .{ 0x0c00e6, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
// .{ 0x0c0150, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
// .{ 0x0c0151, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
// .{ 0x0c0152, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
// .{ 0x0c0153, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
// .{ 0x0c0154, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
// .{ 0x0c0155, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
// USB#0c0183: AL Consumer Control Configuration
|
||||
(0x0c0183, 0x00ab, 0x00b3, 0xe06d, 0xffff, "MediaSelect"),
|
||||
(0x0c0184, 0x01a5, 0x01ad, 0x0000, 0xffff, ""),
|
||||
(0x0c0186, 0x01a7, 0x01af, 0x0000, 0xffff, ""),
|
||||
// USB#0x0c018a AL_EmailReader
|
||||
(0x0c018a, 0x009b, 0x00a3, 0xe06c, 0xffff, "LaunchMail"),
|
||||
// USB#0x0c018d: AL Contacts/Address Book
|
||||
(0x0c018d, 0x01ad, 0x01b5, 0x0000, 0xffff, ""),
|
||||
// USB#0x0c018e: AL Calendar/Schedule
|
||||
(0x0c018e, 0x018d, 0x0195, 0x0000, 0xffff, ""),
|
||||
// USB#0x0c018f AL Task/Project Manager
|
||||
// .{ 0x0c018f, 0x0241, 0x0249, 0x0000, 0xffff, ""},
|
||||
// USB#0x0c0190: AL Log/Journal/Timecard
|
||||
// .{ 0x0c0190, 0x0242, 0x024a, 0x0000, 0xffff, ""},
|
||||
// USB#0x0c0192: AL_Calculator
|
||||
(0x0c0192, 0x008c, 0x0094, 0xe021, 0xffff, "LaunchApp2"),
|
||||
// USB#0c0194: My Computer (AL_LocalMachineBrowser)
|
||||
(0x0c0194, 0x0090, 0x0098, 0xe06b, 0xffff, "LaunchApp1"),
|
||||
(0x0c0196, 0x0096, 0x009e, 0x0000, 0xffff, ""),
|
||||
(0x0c019c, 0x01b1, 0x01b9, 0x0000, 0xffff, ""),
|
||||
// USB#0x0c019e: AL Terminal Lock/Screensaver
|
||||
(0x0c019e, 0x0098, 0x00a0, 0x0000, 0xffff, ""),
|
||||
// USB#0x0c019f AL Control Panel
|
||||
(0x0c019f, 0x0243, 0x024b, 0x0000, 0xffff, "LaunchControlPanel"),
|
||||
// USB#0x0c01a2: AL Select Task/Application
|
||||
(0x0c01a2, 0x0244, 0x024c, 0x0000, 0xffff, "SelectTask"),
|
||||
// USB#0x0c01a7: AL_Documents
|
||||
(0x0c01a7, 0x00eb, 0x00f3, 0x0000, 0xffff, ""),
|
||||
(0x0c01ab, 0x01b0, 0x01b8, 0x0000, 0xffff, ""),
|
||||
// USB#0x0c01ae: AL Keyboard Layout
|
||||
(0x0c01ae, 0x0176, 0x017e, 0x0000, 0xffff, ""),
|
||||
(0x0c01b1, 0x0245, 0x024d, 0x0000, 0xffff, "LaunchScreenSaver"),
|
||||
(0x0c01cb, 0x0247, 0x024f, 0x0000, 0xffff, "LaunchAssistant"),
|
||||
// USB#0c01b4: Home Directory (AL_FileBrowser) (Explorer)
|
||||
// .{ 0x0c01b4, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
// USB#0x0c01b7: AL Audio Browser
|
||||
(0x0c01b7, 0x0188, 0x0190, 0x0000, 0xffff, ""),
|
||||
// USB#0x0c0201: AC New
|
||||
(0x0c0201, 0x00b5, 0x00bd, 0x0000, 0xffff, ""),
|
||||
// USB#0x0c0203: AC Close
|
||||
(0x0c0203, 0x00ce, 0x00d6, 0x0000, 0xffff, ""),
|
||||
// USB#0x0c0207: AC Close
|
||||
(0x0c0207, 0x00ea, 0x00f2, 0x0000, 0xffff, ""),
|
||||
// USB#0x0c0208: AC Print
|
||||
(0x0c0208, 0x00d2, 0x00da, 0x0000, 0xffff, ""),
|
||||
// USB#0x0c0221: AC_Search
|
||||
(0x0c0221, 0x00d9, 0x00e1, 0xe065, 0xffff, "BrowserSearch"),
|
||||
// USB#0x0c0223: AC_Home
|
||||
(0x0c0223, 0x00ac, 0x00b4, 0xe032, 0xffff, "BrowserHome"),
|
||||
// USB#0x0c0224: AC_Back
|
||||
(0x0c0224, 0x009e, 0x00a6, 0xe06a, 0xffff, "BrowserBack"),
|
||||
// USB#0x0c0225: AC_Forward
|
||||
(0x0c0225, 0x009f, 0x00a7, 0xe069, 0xffff, "BrowserForward"),
|
||||
// USB#0x0c0226: AC_Stop
|
||||
(0x0c0226, 0x0080, 0x0088, 0xe068, 0xffff, "BrowserStop"),
|
||||
// USB#0x0c0227: AC_Refresh (Reload)
|
||||
(0x0c0227, 0x00ad, 0x00b5, 0xe067, 0xffff, "BrowserRefresh"),
|
||||
// USB#0x0c022a: AC_Bookmarks (Favorites)
|
||||
(0x0c022a, 0x009c, 0x00a4, 0xe066, 0xffff, "BrowserFavorites"),
|
||||
(0x0c022d, 0x01a2, 0x01aa, 0x0000, 0xffff, ""),
|
||||
(0x0c022e, 0x01a3, 0x01ab, 0x0000, 0xffff, ""),
|
||||
// USB#0x0c0230: AC Full Screen View
|
||||
// .{ 0x0c0230, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
// USB#0x0c0231: AC Normal View
|
||||
// .{ 0x0c0231, 0x0000, 0x0000, 0x0000, 0xffff, ""},
|
||||
(0x0c0232, 0x0174, 0x017c, 0x0000, 0xffff, "ZoomToggle"),
|
||||
// USB#0x0c0279: AC Redo/Repeat
|
||||
(0x0c0279, 0x00b6, 0x00be, 0x0000, 0xffff, ""),
|
||||
// USB#0x0c0289: AC_Reply
|
||||
(0x0c0289, 0x00e8, 0x00f0, 0x0000, 0xffff, "MailReply"),
|
||||
// USB#0x0c028b: AC_ForwardMsg (MailForward)
|
||||
(0x0c028b, 0x00e9, 0x00f1, 0x0000, 0xffff, "MailForward"),
|
||||
// USB#0x0c028c: AC_Send
|
||||
(0x0c028c, 0x00e7, 0x00ef, 0x0000, 0xffff, "MailSend"),
|
||||
// USB#0x0c029d: AC Next Keyboard Layout Select
|
||||
(0x0c029d, 0x0248, 0x0250, 0x0000, 0xffff, "KeyboardLayoutSelect"),
|
||||
(0x0c029f, 0x0078, 0x0080, 0x0000, 0xffff, "ShowAllWindows"),
|
||||
(0x0c02a2, 0x00cc, 0x00d4, 0x0000, 0xffff, ""),
|
||||
(0x0c02d0, 0x0279, 0x0281, 0x0000, 0xffff, ""),
|
||||
];
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn entries_build() {
|
||||
let e = entries();
|
||||
assert_eq!(e.len(), RAW_ENTRIES.len());
|
||||
let a = e.iter().find(|e| e.code == "KeyA").unwrap();
|
||||
assert_eq!(a.key, Key::KeyA);
|
||||
assert_eq!(a.usb, 0x070004);
|
||||
assert_eq!(a.native, 0x0026);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn code_to_key_keys_are_w3c_names() {
|
||||
for (code, key) in CODE_TO_KEY {
|
||||
assert_eq!(Key::from_w3c(code), Some(*key), "{code}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
//! Kitty keyboard protocol key mapping data (ZIG/src/input/kitty.zig).
|
||||
|
||||
use crate::key::Key;
|
||||
|
||||
/// A single entry in the kitty keymap data. There are only ~100 entries
|
||||
/// so the recommendation is to just use a linear search to find the entry
|
||||
/// for a given key.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Entry {
|
||||
pub key: Key,
|
||||
pub code: u32,
|
||||
pub final_: u8,
|
||||
pub modifier: bool,
|
||||
}
|
||||
|
||||
const fn e(key: Key, code: u32, final_: u8, modifier: bool) -> Entry {
|
||||
Entry {
|
||||
key,
|
||||
code,
|
||||
final_,
|
||||
modifier,
|
||||
}
|
||||
}
|
||||
|
||||
/// The raw data for how to map keys to Kitty data. Based on the information:
|
||||
/// https://sw.kovidgoyal.net/kitty/keyboard-protocol/#functional-key-definitions
|
||||
/// And the exact table is ported from Foot:
|
||||
/// https://codeberg.org/dnkl/foot/src/branch/master/kitty-keymap.h
|
||||
///
|
||||
/// Note that we currently don't support all the same keysyms as Kitty,
|
||||
/// but we can add them as we add support.
|
||||
///
|
||||
/// These are stored in order of the above Kitty link, so it is easy to
|
||||
/// find the entry for a given key.
|
||||
pub const ENTRIES: &[Entry] = &[
|
||||
e(Key::Escape, 27, b'u', false),
|
||||
e(Key::Enter, 13, b'u', false),
|
||||
e(Key::Tab, 9, b'u', false),
|
||||
e(Key::Backspace, 127, b'u', false),
|
||||
e(Key::Insert, 2, b'~', false),
|
||||
e(Key::Delete, 3, b'~', false),
|
||||
e(Key::ArrowLeft, 1, b'D', false),
|
||||
e(Key::ArrowRight, 1, b'C', false),
|
||||
e(Key::ArrowUp, 1, b'A', false),
|
||||
e(Key::ArrowDown, 1, b'B', false),
|
||||
e(Key::PageUp, 5, b'~', false),
|
||||
e(Key::PageDown, 6, b'~', false),
|
||||
e(Key::Home, 1, b'H', false),
|
||||
e(Key::End, 1, b'F', false),
|
||||
e(Key::CapsLock, 57358, b'u', true),
|
||||
e(Key::ScrollLock, 57359, b'u', false),
|
||||
e(Key::NumLock, 57360, b'u', true),
|
||||
e(Key::PrintScreen, 57361, b'u', false),
|
||||
e(Key::Pause, 57362, b'u', false),
|
||||
e(Key::F1, 1, b'P', false),
|
||||
e(Key::F2, 1, b'Q', false),
|
||||
e(Key::F3, 13, b'~', false),
|
||||
e(Key::F4, 1, b'S', false),
|
||||
e(Key::F5, 15, b'~', false),
|
||||
e(Key::F6, 17, b'~', false),
|
||||
e(Key::F7, 18, b'~', false),
|
||||
e(Key::F8, 19, b'~', false),
|
||||
e(Key::F9, 20, b'~', false),
|
||||
e(Key::F10, 21, b'~', false),
|
||||
e(Key::F11, 23, b'~', false),
|
||||
e(Key::F12, 24, b'~', false),
|
||||
e(Key::F13, 57376, b'u', false),
|
||||
e(Key::F14, 57377, b'u', false),
|
||||
e(Key::F15, 57378, b'u', false),
|
||||
e(Key::F16, 57379, b'u', false),
|
||||
e(Key::F17, 57380, b'u', false),
|
||||
e(Key::F18, 57381, b'u', false),
|
||||
e(Key::F19, 57382, b'u', false),
|
||||
e(Key::F20, 57383, b'u', false),
|
||||
e(Key::F21, 57384, b'u', false),
|
||||
e(Key::F22, 57385, b'u', false),
|
||||
e(Key::F23, 57386, b'u', false),
|
||||
e(Key::F24, 57387, b'u', false),
|
||||
e(Key::F25, 57388, b'u', false),
|
||||
e(Key::Numpad0, 57399, b'u', false),
|
||||
e(Key::Numpad1, 57400, b'u', false),
|
||||
e(Key::Numpad2, 57401, b'u', false),
|
||||
e(Key::Numpad3, 57402, b'u', false),
|
||||
e(Key::Numpad4, 57403, b'u', false),
|
||||
e(Key::Numpad5, 57404, b'u', false),
|
||||
e(Key::Numpad6, 57405, b'u', false),
|
||||
e(Key::Numpad7, 57406, b'u', false),
|
||||
e(Key::Numpad8, 57407, b'u', false),
|
||||
e(Key::Numpad9, 57408, b'u', false),
|
||||
e(Key::NumpadDecimal, 57409, b'u', false),
|
||||
e(Key::NumpadDivide, 57410, b'u', false),
|
||||
e(Key::NumpadMultiply, 57411, b'u', false),
|
||||
e(Key::NumpadSubtract, 57412, b'u', false),
|
||||
e(Key::NumpadAdd, 57413, b'u', false),
|
||||
e(Key::NumpadEnter, 57414, b'u', false),
|
||||
e(Key::NumpadEqual, 57415, b'u', false),
|
||||
e(Key::NumpadSeparator, 57416, b'u', false),
|
||||
e(Key::NumpadLeft, 57417, b'u', false),
|
||||
e(Key::NumpadRight, 57418, b'u', false),
|
||||
e(Key::NumpadUp, 57419, b'u', false),
|
||||
e(Key::NumpadDown, 57420, b'u', false),
|
||||
e(Key::NumpadPageUp, 57421, b'u', false),
|
||||
e(Key::NumpadPageDown, 57422, b'u', false),
|
||||
e(Key::NumpadHome, 57423, b'u', false),
|
||||
e(Key::NumpadEnd, 57424, b'u', false),
|
||||
e(Key::NumpadInsert, 57425, b'u', false),
|
||||
e(Key::NumpadDelete, 57426, b'u', false),
|
||||
e(Key::NumpadBegin, 57427, b'u', false),
|
||||
e(Key::ShiftLeft, 57441, b'u', true),
|
||||
e(Key::ShiftRight, 57447, b'u', true),
|
||||
e(Key::ControlLeft, 57442, b'u', true),
|
||||
e(Key::ControlRight, 57448, b'u', true),
|
||||
e(Key::MetaLeft, 57444, b'u', true),
|
||||
e(Key::MetaRight, 57450, b'u', true),
|
||||
e(Key::AltLeft, 57443, b'u', true),
|
||||
e(Key::AltRight, 57449, b'u', true),
|
||||
];
|
||||
|
||||
/// The full list of entries for the current platform.
|
||||
pub fn entries() -> &'static [Entry] {
|
||||
ENTRIES
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn entries_table() {
|
||||
// To force evaluation of the table
|
||||
assert_ne!(entries().len(), 0);
|
||||
}
|
||||
}
|
||||
@@ -1 +1,26 @@
|
||||
//! ghostty-input: see docs/rewrite/ARCHITECTURE.md for the module map.
|
||||
//! ghostty-input: keyboard/mouse input handling (ZIG/src/input/*).
|
||||
|
||||
pub mod binding;
|
||||
pub mod command;
|
||||
pub mod config;
|
||||
pub mod function_keys;
|
||||
pub mod helpgen_actions;
|
||||
pub mod key;
|
||||
pub mod key_encode;
|
||||
pub mod key_mods;
|
||||
pub mod keyboard;
|
||||
pub mod keycodes;
|
||||
pub mod kitty;
|
||||
pub mod link;
|
||||
pub mod mouse;
|
||||
pub mod paste;
|
||||
|
||||
pub use config::OptionAsAlt;
|
||||
pub use key::{Action, Key, KeyEvent, ctrl_or_super};
|
||||
pub use key_mods::{Mods, RemapSet as KeyRemapSet};
|
||||
pub use keyboard::Layout as KeyboardLayout;
|
||||
pub use link::Link;
|
||||
pub use mouse::{
|
||||
Button as MouseButton, ButtonState as MouseButtonState, PressureStage as MousePressureStage,
|
||||
ScrollMods,
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
//! A link is a clickable element that can be used to trigger some action.
|
||||
//! A link is NOT just a URL that opens in a browser. A link is any generic
|
||||
//! regular expression match over terminal text that can trigger various
|
||||
//! action types. (ZIG/src/input/Link.zig)
|
||||
|
||||
use crate::key_mods::Mods;
|
||||
|
||||
/// The action that will be triggered when the link is clicked.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Action {
|
||||
/// Open the full matched value using the default open program.
|
||||
/// For example, on macOS this is "open" and on Linux this is "xdg-open".
|
||||
Open,
|
||||
|
||||
/// Open the OSC8 hyperlink under the mouse position. _-prefixed means
|
||||
/// this can't be user-specified, it's only used internally.
|
||||
_OpenOsc8,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Highlight {
|
||||
/// Always highlight the link.
|
||||
Always,
|
||||
|
||||
/// Only highlight the link when the mouse is hovering over it.
|
||||
Hover,
|
||||
|
||||
/// Highlight anytime the given mods are pressed, either when
|
||||
/// hovering or always. For always, all links will be highlighted
|
||||
/// when the mods are pressed regardless of if the mouse is hovering
|
||||
/// over them.
|
||||
///
|
||||
/// Note that if "shift" is specified here, this will NEVER match in
|
||||
/// TUI programs that capture mouse events. "Shift" with mouse capture
|
||||
/// escapes the mouse capture but strips the "shift" so it can't be
|
||||
/// detected.
|
||||
AlwaysMods(Mods),
|
||||
HoverMods(Mods),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Link {
|
||||
/// The regular expression that will be used to match the link.
|
||||
pub regex: String,
|
||||
|
||||
/// The action that will be triggered when the link is clicked.
|
||||
pub action: Action,
|
||||
|
||||
/// The situations in which the link will be highlighted. A link is only
|
||||
/// clickable by the mouse when it is highlighted, so this also controls
|
||||
/// when the link is clickable.
|
||||
pub highlight: Highlight,
|
||||
}
|
||||
|
||||
// TODO(integration): `oni_regex` (Zig `oniRegex`) builds an Oniguruma regex;
|
||||
// the regex engine choice is made by the consumer crate (config/core).
|
||||
|
||||
impl Link {
|
||||
/// Check if two links are equal.
|
||||
pub fn equal(&self, other: &Link) -> bool {
|
||||
self == other
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
//! Mouse input types (ZIG/src/input/mouse.zig).
|
||||
|
||||
/// The state of a mouse button.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[repr(i32)]
|
||||
pub enum ButtonState {
|
||||
Release,
|
||||
Press,
|
||||
}
|
||||
|
||||
/// Possible mouse buttons. We only track up to 11 because that's the maximum
|
||||
/// button input that terminal mouse tracking handles without becoming
|
||||
/// ambiguous.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[repr(i32)]
|
||||
pub enum Button {
|
||||
Unknown = 0,
|
||||
Left = 1,
|
||||
Right = 2,
|
||||
Middle = 3,
|
||||
Four = 4,
|
||||
Five = 5,
|
||||
Six = 6,
|
||||
Seven = 7,
|
||||
Eight = 8,
|
||||
Nine = 9,
|
||||
Ten = 10,
|
||||
Eleven = 11,
|
||||
}
|
||||
|
||||
impl Button {
|
||||
/// The maximum value in this enum. This can be used to create a densely
|
||||
/// packed array, for example.
|
||||
pub const MAX: i32 = 11;
|
||||
}
|
||||
|
||||
/// The "momentum" of a mouse scroll event. This matches the macOS events
|
||||
/// because it is the only reliable source right now of momentum events.
|
||||
/// This is used to handle "inertial scrolling" (i.e. flicking).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
|
||||
#[repr(u8)]
|
||||
pub enum Momentum {
|
||||
#[default]
|
||||
None = 0,
|
||||
Began = 1,
|
||||
Stationary = 2,
|
||||
Changed = 3,
|
||||
Ended = 4,
|
||||
Cancelled = 5,
|
||||
MayBegin = 6,
|
||||
}
|
||||
|
||||
impl Momentum {
|
||||
pub fn from_int(v: u8) -> Option<Momentum> {
|
||||
Some(match v {
|
||||
0 => Momentum::None,
|
||||
1 => Momentum::Began,
|
||||
2 => Momentum::Stationary,
|
||||
3 => Momentum::Changed,
|
||||
4 => Momentum::Ended,
|
||||
5 => Momentum::Cancelled,
|
||||
6 => Momentum::MayBegin,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// The pressure stage of a pressure-sensitive input device.
|
||||
///
|
||||
/// This currently only supports the stages that macOS supports.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
|
||||
#[repr(u8)]
|
||||
pub enum PressureStage {
|
||||
/// The input device is unpressed.
|
||||
#[default]
|
||||
None = 0,
|
||||
|
||||
/// The input device is pressed a normal amount. On macOS trackpads,
|
||||
/// this is after a "click".
|
||||
Normal = 1,
|
||||
|
||||
/// The input device is pressed a deep amount. On macOS trackpads,
|
||||
/// this is after a "force click".
|
||||
Deep = 2,
|
||||
}
|
||||
|
||||
/// The bitmask for mods for scroll events.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
|
||||
pub struct ScrollMods {
|
||||
/// True if this is a high-precision scroll event. For example, Apple
|
||||
/// devices such as Magic Mouse, trackpads, etc. are high-precision
|
||||
/// and send very detailed scroll events.
|
||||
pub precision: bool,
|
||||
|
||||
/// The momentum phase (if available, supported) of the scroll event.
|
||||
/// This is used to handle "inertial scrolling" (i.e. flicking).
|
||||
pub momentum: Momentum,
|
||||
}
|
||||
|
||||
impl ScrollMods {
|
||||
/// Packed `u8` representation (bit 0 precision, bits 1..4 momentum).
|
||||
pub fn int(self) -> u8 {
|
||||
u8::from(self.precision) | ((self.momentum as u8) << 1)
|
||||
}
|
||||
|
||||
pub fn from_int(v: u8) -> ScrollMods {
|
||||
ScrollMods {
|
||||
precision: v & 1 != 0,
|
||||
momentum: Momentum::from_int((v >> 1) & 0b111).unwrap_or(Momentum::None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// For our own understanding
|
||||
#[test]
|
||||
fn scroll_mods_layout() {
|
||||
assert_eq!(ScrollMods::default().int(), 0b0);
|
||||
assert_eq!(
|
||||
ScrollMods {
|
||||
precision: true,
|
||||
..Default::default()
|
||||
}
|
||||
.int(),
|
||||
0b0000_0001
|
||||
);
|
||||
assert_eq!(ScrollMods::from_int(0b0000_0011).momentum, Momentum::Began);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
//! Paste encoding (ZIG/src/input/paste.zig).
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
|
||||
pub enum Error {
|
||||
/// Returned if encoding requires a mutable copy of the data. This
|
||||
/// can only be returned if the input data type is const.
|
||||
#[error("mutable copy of the data is required")]
|
||||
MutableRequired,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Options {
|
||||
/// True if bracketed paste mode is on.
|
||||
pub bracketed: bool,
|
||||
}
|
||||
|
||||
impl Options {
|
||||
// TODO(integration): Zig has `fromTerminal(t: *const Terminal)` which
|
||||
// reads `t.modes.get(.bracketed_paste)`. Construct `Options` from the
|
||||
// terminal mode in ghostty-terminal instead.
|
||||
}
|
||||
|
||||
/// These are the set of byte values that are always replaced by
|
||||
/// a space (per xterm's behavior) for any text insertion method e.g.
|
||||
/// a paste, drag and drop, etc. These are copied directly from xterm's
|
||||
/// source.
|
||||
const STRIP: &[u8] = &[
|
||||
0x00, // NUL
|
||||
0x08, // BS
|
||||
0x05, // ENQ
|
||||
0x04, // EOT
|
||||
0x1B, // ESC
|
||||
0x7F, // DEL
|
||||
// These can be overridden by the running terminal program
|
||||
// via tcsetattr, so they aren't totally safe to hardcode like
|
||||
// this. In practice, I haven't seen modern programs change these
|
||||
// and its a much bigger architectural change to pass these through
|
||||
// so for now they're hardcoded.
|
||||
0x03, // VINTR (Ctrl+C)
|
||||
0x1C, // VQUIT (Ctrl+\)
|
||||
0x15, // VKILL (Ctrl+U)
|
||||
0x1A, // VSUSP (Ctrl+Z)
|
||||
0x11, // VSTART (Ctrl+Q)
|
||||
0x13, // VSTOP (Ctrl+S)
|
||||
0x17, // VWERASE (Ctrl+W)
|
||||
0x16, // VLNEXT (Ctrl+V)
|
||||
0x12, // VREPRINT (Ctrl+R)
|
||||
0x0F, // VDISCARD (Ctrl+O)
|
||||
];
|
||||
|
||||
/// Encode immutable (const) data for pasting. The resulting value can be
|
||||
/// written to the pty to perform a paste of the input data.
|
||||
///
|
||||
/// This returns `Error::MutableRequired` if the data needs modification
|
||||
/// (strip bytes or newline replacement); the caller can then make a mutable
|
||||
/// copy and use `encode_mut`. This calling style allows initially const data
|
||||
/// to be passed in without an allocation, since it is rare in normal use
|
||||
/// cases that the data will need to be modified.
|
||||
///
|
||||
/// The data is returned as a set of slices to limit allocations. The caller
|
||||
/// can combine the slices into a single buffer if desired.
|
||||
///
|
||||
/// WARNING: The input data is not checked for safety. See the `is_safe`
|
||||
/// function to check if the data is safe to paste.
|
||||
pub fn encode(data: &[u8], opts: Options) -> Result<[&[u8]; 3], Error> {
|
||||
let mut result: [&[u8]; 3] = [b"", data, b""];
|
||||
|
||||
if data.iter().any(|b| STRIP.contains(b)) {
|
||||
return Err(Error::MutableRequired);
|
||||
}
|
||||
|
||||
// Bracketed paste mode (mode 2004) wraps pasted data in
|
||||
// fenceposts so that the terminal can ignore things like newlines.
|
||||
if opts.bracketed {
|
||||
result[0] = b"\x1b[200~";
|
||||
result[2] = b"\x1b[201~";
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
// Non-bracketed. We have to replace newline with `\r`.
|
||||
if data.contains(&b'\n') {
|
||||
return Err(Error::MutableRequired);
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Like `encode` but modifies the data in place, so it can't fail.
|
||||
pub fn encode_mut(data: &mut [u8], opts: Options) -> [&[u8]; 3] {
|
||||
// If we have any of the strip values, then we need to replace them
|
||||
// with spaces. This is what xterm does and it does it regardless
|
||||
// of bracketed paste mode. This is a security measure to prevent pastes
|
||||
// from containing bytes that could be used to inject commands.
|
||||
for b in data.iter_mut() {
|
||||
if STRIP.contains(b) {
|
||||
*b = b' ';
|
||||
}
|
||||
}
|
||||
|
||||
let data: &[u8] = if opts.bracketed {
|
||||
data
|
||||
} else {
|
||||
// Non-bracketed. We have to replace newline with `\r`. This matches
|
||||
// the behavior of xterm and other terminals. For `\r\n` this will
|
||||
// result in `\r\r` which does match xterm.
|
||||
for b in data.iter_mut() {
|
||||
if *b == b'\n' {
|
||||
*b = b'\r';
|
||||
}
|
||||
}
|
||||
data
|
||||
};
|
||||
|
||||
let mut result: [&[u8]; 3] = [b"", data, b""];
|
||||
if opts.bracketed {
|
||||
result[0] = b"\x1b[200~";
|
||||
result[2] = b"\x1b[201~";
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Returns true if the data looks safe to paste. Data is considered
|
||||
/// unsafe if it contains any of the following:
|
||||
///
|
||||
/// - `\n`: Newlines can be used to inject commands.
|
||||
/// - `\x1b[201~`: This is the end of a bracketed paste. This cane be used
|
||||
/// to exit a bracketed paste and inject commands.
|
||||
///
|
||||
/// We consider any scenario unsafe regardless of current terminal state.
|
||||
/// For example, even if bracketed paste mode is not active, we still
|
||||
/// consider `\x1b[201~` unsafe. The existence of these types of bytes
|
||||
/// should raise suspicion that the producer of the paste data is
|
||||
/// acting strangely.
|
||||
pub fn is_safe(data: &[u8]) -> bool {
|
||||
!data.contains(&b'\n') && !data.windows(6).any(|w| w == b"\x1b[201~")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn is_safe_test() {
|
||||
assert!(is_safe(b"hello"));
|
||||
assert!(!is_safe(b"hello\n"));
|
||||
assert!(!is_safe(b"hello\nworld"));
|
||||
assert!(!is_safe(b"he\x1b[201~llo"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_bracketed() {
|
||||
let r = encode(b"hello", Options { bracketed: true }).unwrap();
|
||||
assert_eq!(r[0], b"\x1b[200~");
|
||||
assert_eq!(r[1], b"hello");
|
||||
assert_eq!(r[2], b"\x1b[201~");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_unbracketed_no_newlines() {
|
||||
let r = encode(b"hello", Options { bracketed: false }).unwrap();
|
||||
assert_eq!(r[0], b"");
|
||||
assert_eq!(r[1], b"hello");
|
||||
assert_eq!(r[2], b"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_unbracketed_newlines_const() {
|
||||
assert_eq!(
|
||||
encode(b"hello\nworld", Options { bracketed: false }),
|
||||
Err(Error::MutableRequired)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_unbracketed_newlines() {
|
||||
let mut data = b"hello\nworld".to_vec();
|
||||
let r = encode_mut(&mut data, Options { bracketed: false });
|
||||
assert_eq!(r[0], b"");
|
||||
assert_eq!(r[1], b"hello\rworld");
|
||||
assert_eq!(r[2], b"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_unbracketed_windows_stye_newline() {
|
||||
let mut data = b"hello\r\nworld".to_vec();
|
||||
let r = encode_mut(&mut data, Options { bracketed: false });
|
||||
assert_eq!(r[0], b"");
|
||||
assert_eq!(r[1], b"hello\r\rworld");
|
||||
assert_eq!(r[2], b"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_strip_unsafe_bytes_const() {
|
||||
assert_eq!(
|
||||
encode(b"hello\x00world", Options { bracketed: true }),
|
||||
Err(Error::MutableRequired)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_strip_unsafe_bytes_mutable_bracketed() {
|
||||
let mut data = b"hel\x1blo\x00world".to_vec();
|
||||
let r = encode_mut(&mut data, Options { bracketed: true });
|
||||
assert_eq!(r[0], b"\x1b[200~");
|
||||
assert_eq!(r[1], b"hel lo world");
|
||||
assert_eq!(r[2], b"\x1b[201~");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_strip_unsafe_bytes_mutable_unbracketed() {
|
||||
let mut data = b"hel\x03lo".to_vec();
|
||||
let r = encode_mut(&mut data, Options { bracketed: false });
|
||||
assert_eq!(r[0], b"");
|
||||
assert_eq!(r[1], b"hel lo");
|
||||
assert_eq!(r[2], b"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_strip_multiple_unsafe_bytes() {
|
||||
let mut data = b"\x00\x08\x7f".to_vec();
|
||||
let r = encode_mut(&mut data, Options { bracketed: true });
|
||||
assert_eq!(r[1], b" ");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user