Files
ghostty-rs/crates/ghostty-input/src/key.rs
T
ddidderr 452b782149 feat(macos): port Ghostty's macOS code paths (Metal, CoreText, Darwin)
Ports the macOS-specific Zig code behind cfg(target_os = "macos"):
- renderer: a GraphicsApi trait with generic::Renderer<G>; Metal backend
  (all renderer/metal/* files, shaders.metal verbatim, runtime library
  compile, IOSurface layer, CVDisplayLink), shadertoy -> MSL via naga;
  OpenGL remains the backend elsewhere.
- font: CoreText face, shaper and discovery with build.rs backend
  selection (coretext default; coretext-freetype / coretext-harfbuzz /
  coretext-noshape features), no fontconfig/freetype/harfbuzz on Apple.
- input: KeymapDarwin via UCKeyTranslate, option-as-alt, mac keycodes.
- os: macos, mach, cf_release_thread, NSProcessInfo args and macOS
  branches; config: macOS defaults, keybinds, Application Support paths.
- termio/core: login(1) wrapping, openpty/execvpe, Darwin child exit
  handling, os_log logging.

Compile-checked for aarch64/x86_64-apple-darwin (clippy -D warnings
clean) using the cross setup in docs/rewrite/notes/macos-M1.md; cannot
run here. The embedded runtime/C API (embedded.zig) is the next step.

Also includes the cross-platform integration edits shared by the crash,
verification, Windows and macOS work (Cargo target-gating, thread
crash-context hooks, apprt-gtk surface/IME refactor, termio/os cfg
gates), a cfg fix so ghostty-config builds on wasm32, and an allow for
the intentional recursion in the crash overflow test.

Test Plan:
- cargo test --workspace (3224 passed)
- cargo clippy --workspace --all-targets -- -D warnings
- cargo check --target aarch64-apple-darwin -p ghostty-renderer (with
  the documented cross env)
2026-10-12 06:02:39 +02:00

641 lines
21 KiB
Rust

//! 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<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 {
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());
}
}