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
3323c3f525
commit
3afe938b95
21 files changed
+13413
-1
No files matched your search
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user