Files
ghostty-rs/crates/ghostty-apprt-gtk/src/key.rs
T
ddidderr 8bcb85a29a style: apply nightly rustfmt import grouping across the workspace
rustfmt.toml sets group_imports, imports_granularity and imports_layout,
which are nightly-only; agents formatted with stable rustfmt, which
ignored them. This is a formatting-only pass with `cargo +nightly fmt`.

Test Plan:
- cargo clippy --workspace --all-targets -- -D warnings
- cargo test --workspace
2026-10-12 05:03:37 +02:00

528 lines
16 KiB
Rust

//! GDK key <-> Ghostty input translation (port of `apprt/gtk/key.zig`).
use std::fmt::Write as _;
use ghostty_input::{
binding::{Trigger, TriggerKey},
key::{Action, Key as K},
key_mods::{ModSide, Mods},
};
use gtk::{gdk, gdk::prelude::DeviceExt, glib::translate::IntoGlib, prelude::*};
/// Name of a keyval (`gdk.keyvalName`).
fn keyval_name(keyval: u32) -> Option<String> {
#[allow(unsafe_code)]
// SAFETY: `gdk::Key` is a transparent newtype over the guint keyval;
// every u32 is a valid value (unknown keyvals simply have no name).
let key = unsafe { <gdk::Key as gtk::glib::translate::FromGlib<u32>>::from_glib(keyval) };
key.name().map(|s| s.to_string())
}
/// Returns a GTK accelerator string from a trigger, or None if the trigger
/// can't be represented (e.g. catch_all or unmapped key).
pub fn accel_from_trigger(trigger: Trigger) -> Option<String> {
let mut s = String::new();
if trigger.mods.shift {
s.push_str("<Shift>");
}
if trigger.mods.ctrl {
s.push_str("<Ctrl>");
}
if trigger.mods.alt {
s.push_str("<Alt>");
}
if trigger.mods.super_ {
s.push_str("<Super>");
}
write_trigger_key(&mut s, trigger).then_some(s)
}
/// Returns a XDG-compliant shortcuts string from a trigger.
/// Spec: <https://specifications.freedesktop.org/shortcuts-spec/latest/>
pub fn xdg_shortcut_from_trigger(trigger: Trigger) -> Option<String> {
let mut s = String::new();
if trigger.mods.shift {
s.push_str("SHIFT+");
}
if trigger.mods.ctrl {
s.push_str("CTRL+");
}
if trigger.mods.alt {
s.push_str("ALT+");
}
if trigger.mods.super_ {
s.push_str("LOGO+");
}
// NOTE: the spec expects libxkbcommon keysyms; GTK's are identical to
// X11's, which works for now (same assumption as upstream).
write_trigger_key(&mut s, trigger).then_some(s)
}
fn write_trigger_key(w: &mut String, trigger: Trigger) -> bool {
match trigger.key {
TriggerKey::Physical(k) => {
let Some(keyval) = keyval_from_key(k) else {
return false;
};
let Some(name) = keyval_name(keyval) else {
return false;
};
w.push_str(&name);
}
TriggerKey::Unicode(cp) => {
if let Some(name) = keyval_name(cp) {
w.push_str(&name);
} else if let Some(c) = char::from_u32(cp) {
w.push(c);
}
}
TriggerKey::CatchAll => return false,
}
true
}
pub fn translate_mods(state: gdk::ModifierType) -> Mods {
Mods {
shift: state.contains(gdk::ModifierType::SHIFT_MASK),
ctrl: state.contains(gdk::ModifierType::CONTROL_MASK),
alt: state.contains(gdk::ModifierType::ALT_MASK),
super_: state.contains(gdk::ModifierType::SUPER_MASK),
// Lock is dependent on the X settings but we just assume caps lock.
caps_lock: state.contains(gdk::ModifierType::LOCK_MASK),
..Mods::default()
}
}
/// Get the unshifted unicode value of the keyval. This is used by the Kitty
/// keyboard protocol. Returns 0 if unknown.
pub fn keyval_unicode_unshifted(
widget: &impl IsA<gtk::Widget>,
event: &gdk::KeyEvent,
keycode: u32,
) -> u32 {
// We need the currently active keyboard layout so we know what group to
// look at.
keyval_unicode_unshifted_for_layout(widget, event.layout(), keycode)
}
/// Like [`keyval_unicode_unshifted`] with an explicit keyboard layout (group)
/// instead of a `GdkKeyEvent`; used by the debug input replay.
pub fn keyval_unicode_unshifted_for_layout(
widget: &impl IsA<gtk::Widget>,
layout: u32,
keycode: u32,
) -> u32 {
let display = widget.as_ref().display();
// All possible keyboard mappings for this keycode (the physical key).
let Some(entries) = display.map_keycode(keycode) else {
return 0;
};
for (key, keyval) in entries {
if key.group() == layout.cast_signed() && key.level() == 0 {
return keyval.to_unicode().map_or(0, |c| c as u32);
}
}
0
}
/// Returns the mods to use for a key event from a GTK event. This requires
/// a lot of context because the GdkEvent doesn't contain enough on its own.
///
/// `winproto_event_mods` is `winproto::App::event_mods(device, gtk_mods)`.
pub fn event_mods(
event: &gdk::Event,
physical_key: K,
gtk_mods: gdk::ModifierType,
action: Action,
winproto_event_mods: impl FnOnce(Option<&gdk::Device>, gdk::ModifierType) -> Mods,
) -> Mods {
event_mods_for_device(
event.device().as_ref(),
physical_key,
gtk_mods,
action,
winproto_event_mods,
)
}
/// Like [`event_mods`] but with the (optional) device instead of the event.
pub fn event_mods_for_device(
device: Option<&gdk::Device>,
physical_key: K,
gtk_mods: gdk::ModifierType,
action: Action,
winproto_event_mods: impl FnOnce(Option<&gdk::Device>, gdk::ModifierType) -> Mods,
) -> Mods {
let mut mods = winproto_event_mods(device, gtk_mods);
mods.num_lock = device.is_some_and(DeviceExt::is_num_locked);
// We use the physical key to determine sided modifiers. As far as I can
// tell there's no other way to reliably determine this.
//
// We also set the main modifier to true if either side is true, since on
// both X11/Wayland, GTK doesn't set the main modifier if only the
// modifier key is pressed, but our core logic relies on it.
let down = action != Action::Release;
match physical_key {
K::ShiftLeft => {
mods.shift = down;
mods.sides.shift = ModSide::Left;
}
K::ShiftRight => {
mods.shift = down;
mods.sides.shift = ModSide::Right;
}
K::ControlLeft => {
mods.ctrl = down;
mods.sides.ctrl = ModSide::Left;
}
K::ControlRight => {
mods.ctrl = down;
mods.sides.ctrl = ModSide::Right;
}
K::AltLeft => {
mods.alt = down;
mods.sides.alt = ModSide::Left;
}
K::AltRight => {
mods.alt = down;
mods.sides.alt = ModSide::Right;
}
K::MetaLeft => {
mods.super_ = down;
mods.sides.super_ = ModSide::Left;
}
K::MetaRight => {
mods.super_ = down;
mods.sides.super_ = ModSide::Right;
}
_ => {}
}
mods
}
/// Returns an input key from a keyval or None if we don't have a mapping.
pub fn key_from_keyval(keyval: u32) -> Option<K> {
KEYMAP
.iter()
.find(|(g, _)| g.into_glib() == keyval)
.map(|(_, k)| *k)
}
/// Returns a keyval from an input key or None if we don't have a mapping.
pub fn keyval_from_key(key: K) -> Option<u32> {
KEYMAP
.iter()
.find(|(_, k)| *k == key)
.map(|(g, _)| g.into_glib())
}
/// Converts a trigger to a human-readable label for display in UI, using
/// GTK accelerator-style formatting (e.g. "Ctrl+Shift+A"). Returns None if
/// the trigger cannot be formatted (e.g. catch_all).
pub fn label_from_trigger(trigger: Trigger) -> Option<String> {
let mut s = String::new();
if trigger.mods.super_ {
s.push_str("Super+");
}
if trigger.mods.ctrl {
s.push_str("Ctrl+");
}
if trigger.mods.alt {
s.push_str("Alt+");
}
if trigger.mods.shift {
s.push_str("Shift+");
}
write_trigger_key_label(&mut s, trigger).then_some(s)
}
fn push_capitalized(w: &mut String, name: &str) {
let mut chars = name.chars();
if let Some(first) = chars.next() {
if first.is_ascii_lowercase() {
w.push(first.to_ascii_uppercase());
w.push_str(chars.as_str());
} else {
w.push_str(name);
}
}
}
/// Writes the key portion of a trigger in human-readable format.
fn write_trigger_key_label(w: &mut String, trigger: Trigger) -> bool {
match trigger.key {
TriggerKey::Physical(k) => {
let Some(keyval) = keyval_from_key(k) else {
return false;
};
let Some(name) = keyval_name(keyval) else {
return false;
};
push_capitalized(w, &name);
}
TriggerKey::Unicode(cp) => {
if let Some(name) = keyval_name(cp) {
push_capitalized(w, &name);
} else if let Some(c) = char::from_u32(cp) {
let _ = write!(w, "{c}");
}
}
TriggerKey::CatchAll => return false,
}
true
}
/// The keymap: mappings between GDK keyvals and our own key enum.
// TODO: media keys
const KEYMAP: &[(gdk::Key, K)] = &[
(gdk::Key::a, K::KeyA),
(gdk::Key::b, K::KeyB),
(gdk::Key::c, K::KeyC),
(gdk::Key::d, K::KeyD),
(gdk::Key::e, K::KeyE),
(gdk::Key::f, K::KeyF),
(gdk::Key::g, K::KeyG),
(gdk::Key::h, K::KeyH),
(gdk::Key::i, K::KeyI),
(gdk::Key::j, K::KeyJ),
(gdk::Key::k, K::KeyK),
(gdk::Key::l, K::KeyL),
(gdk::Key::m, K::KeyM),
(gdk::Key::n, K::KeyN),
(gdk::Key::o, K::KeyO),
(gdk::Key::p, K::KeyP),
(gdk::Key::q, K::KeyQ),
(gdk::Key::r, K::KeyR),
(gdk::Key::s, K::KeyS),
(gdk::Key::t, K::KeyT),
(gdk::Key::u, K::KeyU),
(gdk::Key::v, K::KeyV),
(gdk::Key::w, K::KeyW),
(gdk::Key::x, K::KeyX),
(gdk::Key::y, K::KeyY),
(gdk::Key::z, K::KeyZ),
(gdk::Key::_0, K::Digit0),
(gdk::Key::_1, K::Digit1),
(gdk::Key::_2, K::Digit2),
(gdk::Key::_3, K::Digit3),
(gdk::Key::_4, K::Digit4),
(gdk::Key::_5, K::Digit5),
(gdk::Key::_6, K::Digit6),
(gdk::Key::_7, K::Digit7),
(gdk::Key::_8, K::Digit8),
(gdk::Key::_9, K::Digit9),
(gdk::Key::semicolon, K::Semicolon),
(gdk::Key::space, K::Space),
(gdk::Key::apostrophe, K::Quote),
(gdk::Key::comma, K::Comma),
(gdk::Key::grave, K::Backquote),
(gdk::Key::period, K::Period),
(gdk::Key::slash, K::Slash),
(gdk::Key::minus, K::Minus),
(gdk::Key::equal, K::Equal),
(gdk::Key::bracketleft, K::BracketLeft),
(gdk::Key::bracketright, K::BracketRight),
(gdk::Key::backslash, K::Backslash),
(gdk::Key::Up, K::ArrowUp),
(gdk::Key::Down, K::ArrowDown),
(gdk::Key::Right, K::ArrowRight),
(gdk::Key::Left, K::ArrowLeft),
(gdk::Key::Home, K::Home),
(gdk::Key::End, K::End),
(gdk::Key::Insert, K::Insert),
(gdk::Key::Delete, K::Delete),
(gdk::Key::Caps_Lock, K::CapsLock),
(gdk::Key::Scroll_Lock, K::ScrollLock),
(gdk::Key::Num_Lock, K::NumLock),
(gdk::Key::Page_Up, K::PageUp),
(gdk::Key::Page_Down, K::PageDown),
(gdk::Key::Escape, K::Escape),
(gdk::Key::Return, K::Enter),
(gdk::Key::Tab, K::Tab),
(gdk::Key::BackSpace, K::Backspace),
(gdk::Key::Print, K::PrintScreen),
(gdk::Key::Pause, K::Pause),
(gdk::Key::F1, K::F1),
(gdk::Key::F2, K::F2),
(gdk::Key::F3, K::F3),
(gdk::Key::F4, K::F4),
(gdk::Key::F5, K::F5),
(gdk::Key::F6, K::F6),
(gdk::Key::F7, K::F7),
(gdk::Key::F8, K::F8),
(gdk::Key::F9, K::F9),
(gdk::Key::F10, K::F10),
(gdk::Key::F11, K::F11),
(gdk::Key::F12, K::F12),
(gdk::Key::F13, K::F13),
(gdk::Key::F14, K::F14),
(gdk::Key::F15, K::F15),
(gdk::Key::F16, K::F16),
(gdk::Key::F17, K::F17),
(gdk::Key::F18, K::F18),
(gdk::Key::F19, K::F19),
(gdk::Key::F20, K::F20),
(gdk::Key::F21, K::F21),
(gdk::Key::F22, K::F22),
(gdk::Key::F23, K::F23),
(gdk::Key::F24, K::F24),
(gdk::Key::F25, K::F25),
(gdk::Key::KP_0, K::Numpad0),
(gdk::Key::KP_1, K::Numpad1),
(gdk::Key::KP_2, K::Numpad2),
(gdk::Key::KP_3, K::Numpad3),
(gdk::Key::KP_4, K::Numpad4),
(gdk::Key::KP_5, K::Numpad5),
(gdk::Key::KP_6, K::Numpad6),
(gdk::Key::KP_7, K::Numpad7),
(gdk::Key::KP_8, K::Numpad8),
(gdk::Key::KP_9, K::Numpad9),
(gdk::Key::KP_Decimal, K::NumpadDecimal),
(gdk::Key::KP_Divide, K::NumpadDivide),
(gdk::Key::KP_Multiply, K::NumpadMultiply),
(gdk::Key::KP_Subtract, K::NumpadSubtract),
(gdk::Key::KP_Add, K::NumpadAdd),
(gdk::Key::KP_Enter, K::NumpadEnter),
(gdk::Key::KP_Equal, K::NumpadEqual),
(gdk::Key::KP_Separator, K::NumpadSeparator),
(gdk::Key::KP_Left, K::NumpadLeft),
(gdk::Key::KP_Right, K::NumpadRight),
(gdk::Key::KP_Up, K::NumpadUp),
(gdk::Key::KP_Down, K::NumpadDown),
(gdk::Key::KP_Page_Up, K::NumpadPageUp),
(gdk::Key::KP_Page_Down, K::NumpadPageDown),
(gdk::Key::KP_Home, K::NumpadHome),
(gdk::Key::KP_End, K::NumpadEnd),
(gdk::Key::KP_Insert, K::NumpadInsert),
(gdk::Key::KP_Delete, K::NumpadDelete),
(gdk::Key::KP_Begin, K::NumpadBegin),
(gdk::Key::Copy, K::Copy),
(gdk::Key::Cut, K::Cut),
(gdk::Key::Paste, K::Paste),
(gdk::Key::Shift_L, K::ShiftLeft),
(gdk::Key::Control_L, K::ControlLeft),
(gdk::Key::Alt_L, K::AltLeft),
(gdk::Key::Super_L, K::MetaLeft),
(gdk::Key::Shift_R, K::ShiftRight),
(gdk::Key::Control_R, K::ControlRight),
(gdk::Key::Alt_R, K::AltRight),
(gdk::Key::Super_R, K::MetaRight),
];
#[cfg(test)]
mod tests {
use super::*;
fn uni(mods: Mods, cp: u32) -> Trigger {
Trigger {
mods,
key: TriggerKey::Unicode(cp),
}
}
fn all_mods() -> Mods {
Mods {
ctrl: true,
alt: true,
super_: true,
shift: true,
..Mods::default()
}
}
#[test]
fn accel_from_trigger_test() {
let m = Mods {
super_: true,
..Mods::default()
};
assert_eq!(
accel_from_trigger(uni(m, 'q' as u32)).as_deref(),
Some("<Super>q")
);
assert_eq!(
accel_from_trigger(uni(all_mods(), 92)).as_deref(),
Some("<Shift><Ctrl><Alt><Super>backslash")
);
}
#[test]
fn xdg_shortcut_from_trigger_test() {
let m = Mods {
super_: true,
..Mods::default()
};
assert_eq!(
xdg_shortcut_from_trigger(uni(m, 'q' as u32)).as_deref(),
Some("LOGO+q")
);
assert_eq!(
xdg_shortcut_from_trigger(uni(all_mods(), 92)).as_deref(),
Some("SHIFT+CTRL+ALT+LOGO+backslash")
);
}
#[test]
fn label_from_trigger_test() {
let m = Mods {
super_: true,
..Mods::default()
};
assert_eq!(
label_from_trigger(uni(m, 'q' as u32)).as_deref(),
Some("Super+Q")
);
assert_eq!(
label_from_trigger(uni(all_mods(), 92)).as_deref(),
Some("Super+Ctrl+Alt+Shift+Backslash")
);
let ctrl = Mods {
ctrl: true,
..Mods::default()
};
assert_eq!(
label_from_trigger(Trigger {
mods: ctrl,
key: TriggerKey::Physical(K::KeyA)
})
.as_deref(),
Some("Ctrl+A")
);
assert_eq!(
label_from_trigger(Trigger {
mods: Mods::default(),
key: TriggerKey::Physical(K::Escape)
})
.as_deref(),
Some("Escape")
);
assert_eq!(
label_from_trigger(Trigger {
mods: Mods::default(),
key: TriggerKey::CatchAll
}),
None
);
}
#[test]
fn keymap_roundtrip() {
for (g, k) in KEYMAP {
assert_eq!(key_from_keyval(g.into_glib()), Some(*k));
}
assert_eq!(keyval_from_key(K::KeyA), Some(gdk::Key::a.into_glib()));
assert_eq!(keyval_from_key(K::Unidentified), None);
}
#[test]
fn translate_mods_test() {
let m = translate_mods(gdk::ModifierType::SHIFT_MASK | gdk::ModifierType::LOCK_MASK);
assert!(m.shift && m.caps_lock && !m.ctrl && !m.alt && !m.super_);
}
}