//! Key events and key codes (ZIG/src/input/key.zig). //! //! The Zig `imguiKey` helper (cimgui constants) lives in the inspector crate //! (`ghostty_inspector::keys::imgui_key`) so that this crate stays free of //! the ImGui dependency. 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 { 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 { 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 { // 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 { 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 { if crate::platform::is_darwin() { return self == Key::MetaLeft || self == Key::MetaRight; } 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 { ctrl_or_super_for(mods, crate::platform::is_darwin()) } /// [`ctrl_or_super`] for an explicit platform (`darwin == true` sets super). pub fn ctrl_or_super_for(mods: Mods, darwin: bool) -> Mods { let mut copy = mods; if darwin { copy.super_ = true; } else { 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 key_ctrl_or_super_follows_platform() { crate::platform::with_darwin(true, || { assert!(Key::MetaLeft.ctrl_or_super()); assert!(Key::MetaRight.ctrl_or_super()); assert!(!Key::ControlLeft.ctrl_or_super()); assert!(ctrl_or_super(Mods::default()).super_); assert!(!ctrl_or_super(Mods::default()).ctrl); }); crate::platform::with_darwin(false, || { assert!(Key::ControlLeft.ctrl_or_super()); assert!(!Key::MetaLeft.ctrl_or_super()); assert!(ctrl_or_super(Mods::default()).ctrl); assert!(!ctrl_or_super(Mods::default()).super_); }); } #[test] fn ctrl_or_super_test() { let m = ctrl_or_super(Mods::default()); assert!(m.ctrl_or_super()); } }