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
612 lines
20 KiB
Rust
612 lines
20 KiB
Rust
//! 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());
|
|
}
|
|
}
|