From 558ca359b80cbfe4331c2f667935da668bd25a08 Mon Sep 17 00:00:00 2001 From: fufesou Date: Sun, 4 Oct 2026 20:48:56 -0700 Subject: [PATCH] fix: distinguish AltGraph from ordinary Alt shortcuts Signed-off-by: fufesou --- src/keyboard/shortcuts.rs | 27 ++++++++++++++++++++++----- src/platform/linux.rs | 29 +++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/src/keyboard/shortcuts.rs b/src/keyboard/shortcuts.rs index 5423af5a3..23544f30b 100644 --- a/src/keyboard/shortcuts.rs +++ b/src/keyboard/shortcuts.rs @@ -150,10 +150,6 @@ pub fn match_normalized<'a>(key: &str, mods: &[Modifier], b: &'a Bindings) -> Op pub fn normalize_modifiers(alt: bool, ctrl: bool, shift: bool, command: bool) -> Vec { // iOS shares Apple's keyboard semantics with macOS — recording dialog // already treats iOS as `_isMac`, so the matcher must too. - // - // AltGr conflation: `get_modifiers_state` ORs Alt and AltGr, so an - // AltGr+key press satisfies `Modifier::Alt`. Theoretical collision only; - // fix at `get_modifiers_state` if a real bug surfaces. let mut v = Vec::new(); if cfg!(any(target_os = "macos", target_os = "ios")) { if command { v.push(Modifier::Primary); } @@ -333,7 +329,28 @@ pub fn match_event(event: &rdev::Event) -> Option { let (alt, ctrl, shift, command) = crate::keyboard::client::get_modifiers_state(false, false, false, false); let mods = normalize_modifiers(alt, ctrl, shift, command); - match_normalized(&key_name, &mods, &bindings).map(str::to_owned) + let action = match_normalized(&key_name, &mods, &bindings)?; + // AltGraph is text input, not the ordinary Alt recorded in a shortcut. + // Keep the shared modifier snapshot unchanged for remote key encoding. + #[cfg(target_os = "windows")] + if unsafe { super::IS_0X021D_DOWN } + && super::MODIFIERS_STATE.lock().unwrap().get(&rdev::Key::AltGr) == Some(&true) + { + return None; + } + #[cfg(target_os = "linux")] + if super::MODIFIERS_STATE.lock().unwrap().get(&rdev::Key::AltGr) == Some(&true) { + match crate::platform::linux::right_alt_is_level3_shift() { + Ok(true) => return None, + Ok(false) => {} + Err(err) => { + hbb_common::throttled_log!(std::time::Duration::from_secs(5), warn, + "Cannot identify AltGraph for keyboard shortcuts: {}", err); + return None; + } + } + } + Some(action.to_owned()) } #[cfg(feature = "flutter")] diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 169e63021..8f6f3068c 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -302,6 +302,35 @@ thread_local! { static DISPLAY: RefCell<*mut c_void> = RefCell::new(unsafe { XOpenDisplay(std::ptr::null())}); } +pub fn right_alt_is_level3_shift() -> ResultType { + use hbb_common::x11::{keysym, xlib}; + const XKB_USE_CORE_KBD: c_uint = 0x0100; + // A fresh connection avoids Xlib's stale keymap cache after a layout switch. + // This query runs only for a matched chord with physical Right Alt held. + let display = unsafe { xlib::XOpenDisplay(std::ptr::null()) }; + if display.is_null() { + bail!("X display unavailable"); + } + let result = (|| { + let code = rdev::linux_keycode_from_key(rdev::Key::AltGr) + .ok_or_else(|| anyhow!("Right Alt has no X keycode"))?; + let mut state: xlib::XkbStateRec = unsafe { std::mem::zeroed() }; + let status = unsafe { xlib::XkbGetState(display, XKB_USE_CORE_KBD, &mut state) }; + if status != c_int::from(xlib::Success) { + bail!("XkbGetState failed: {}", status); + } + let symbol = unsafe { + xlib::XkbKeycodeToKeysym(display, code as _, state.group.into(), 0) + }; + // Physical Right Alt is ordinary Alt on layouts such as US English. + Ok(matches!(symbol as u32, keysym::XK_ISO_Level3_Shift + | keysym::XK_ISO_Level3_Latch | keysym::XK_ISO_Level3_Lock + | keysym::XK_Mode_switch)) + })(); + unsafe { xlib::XCloseDisplay(display) }; + result +} + // X11 error event structure for the custom error handler. // See: https://www.x.org/releases/current/doc/libX11/libX11/libX11.html#Using-the-Default-Error-Handlers #[repr(C)]