From 4a3490dde74b462c4259100f96e1068c3a029192 Mon Sep 17 00:00:00 2001 From: SpikeHD <25207995+SpikeHD@users.noreply.github.com> Date: Sun, 17 Aug 2025 12:39:57 -0700 Subject: [PATCH 01/10] feat: linux impl --- crates/livesplit-hotkey/src/lib.rs | 9 +++ .../livesplit-hotkey/src/linux/evdev_impl.rs | 57 +++++++++++++------ crates/livesplit-hotkey/src/linux/mod.rs | 21 +++++++ crates/livesplit-hotkey/src/linux/x11_impl.rs | 34 ++++++++--- 4 files changed, 96 insertions(+), 25 deletions(-) diff --git a/crates/livesplit-hotkey/src/lib.rs b/crates/livesplit-hotkey/src/lib.rs index 74a3432a1..9e9ee5bd6 100644 --- a/crates/livesplit-hotkey/src/lib.rs +++ b/crates/livesplit-hotkey/src/lib.rs @@ -89,6 +89,15 @@ impl Hook { self.0.register(hotkey, callback) } + /// Registers a specific hotkey to listen to, which allows you to + /// differentiate between key presses and releases. + pub fn register_specific(&self, hotkey: Hotkey, callback: F) -> Result<()> + where + F: FnMut(bool) + Send + 'static, + { + self.0.register_specific(hotkey, callback) + } + /// Unregisters a previously registered hotkey. pub fn unregister(&self, hotkey: Hotkey) -> Result<()> { self.0.unregister(hotkey) diff --git a/crates/livesplit-hotkey/src/linux/evdev_impl.rs b/crates/livesplit-hotkey/src/linux/evdev_impl.rs index d9b26eb8e..aab268fed 100644 --- a/crates/livesplit-hotkey/src/linux/evdev_impl.rs +++ b/crates/livesplit-hotkey/src/linux/evdev_impl.rs @@ -252,12 +252,13 @@ pub fn new() -> Result { } let join_handle = thread::spawn(move || -> Result<()> { - let mut result = Ok(()); - let mut events = Events::with_capacity(1024); - let mut hotkeys: HashMap<(Key, Modifiers), Box> = HashMap::new(); - let mut modifiers = Modifiers::empty(); + let mut result = Ok(()); + let mut events = Events::with_capacity(1024); + let mut hotkeys: HashMap<(Key, Modifiers), Box> = HashMap::new(); + let mut specific_hotkeys: HashMap<(Key, Modifiers), Box> = HashMap::new(); + let mut modifiers = Modifiers::empty(); - let (mut xlib, mut display) = (None, None); + let (mut xlib, mut display) = (None, None); 'event_loop: loop { if poll.poll(&mut events, None).is_err() { @@ -274,6 +275,9 @@ pub fn new() -> Result { const PRESSED: i32 = 1; match ev.value() { PRESSED => { + if let Some(callback) = specific_hotkeys.get_mut(&(k, modifiers)) { + callback(true); + } if let Some(callback) = hotkeys.get_mut(&(k, modifiers)) { callback(); } @@ -293,21 +297,26 @@ pub fn new() -> Result { _ => {} } } - RELEASED => match k { - Key::KEY_LEFTALT | Key::KEY_RIGHTALT => { - modifiers.remove(Modifiers::ALT); - } - Key::KEY_LEFTCTRL | Key::KEY_RIGHTCTRL => { - modifiers.remove(Modifiers::CONTROL); - } - Key::KEY_LEFTMETA | Key::KEY_RIGHTMETA => { - modifiers.remove(Modifiers::META); + RELEASED => { + if let Some(callback) = specific_hotkeys.get_mut(&(k, modifiers)) { + callback(false); } - Key::KEY_LEFTSHIFT | Key::KEY_RIGHTSHIFT => { - modifiers.remove(Modifiers::SHIFT); + match k { + Key::KEY_LEFTALT | Key::KEY_RIGHTALT => { + modifiers.remove(Modifiers::ALT); + } + Key::KEY_LEFTCTRL | Key::KEY_RIGHTCTRL => { + modifiers.remove(Modifiers::CONTROL); + } + Key::KEY_LEFTMETA | Key::KEY_RIGHTMETA => { + modifiers.remove(Modifiers::META); + } + Key::KEY_LEFTSHIFT | Key::KEY_RIGHTSHIFT => { + modifiers.remove(Modifiers::SHIFT); + } + _ => {} } - _ => {} - }, + } _ => {} // Ignore repeating } } @@ -327,6 +336,18 @@ pub fn new() -> Result { }, ); } + Message::RegisterSpecific(key, callback, promise) => { + promise.set( + if code_for(key.key_code) + .and_then(|k| specific_hotkeys.insert((k, key.modifiers), callback)) + .is_some() + { + Err(crate::Error::AlreadyRegistered) + } else { + Ok(()) + }, + ); + } Message::Unregister(key, promise) => promise.set( code_for(key.key_code) .and_then(|k| hotkeys.remove(&(k, key.modifiers)).map(drop)) diff --git a/crates/livesplit-hotkey/src/linux/mod.rs b/crates/livesplit-hotkey/src/linux/mod.rs index b9179fc26..e88b8e5ef 100644 --- a/crates/livesplit-hotkey/src/linux/mod.rs +++ b/crates/livesplit-hotkey/src/linux/mod.rs @@ -43,6 +43,12 @@ enum Message { Box, Promise>, ), + // Tracks both press and release + RegisterSpecific( + Hotkey, + Box, + Promise>, + ), Unregister(Hotkey, Promise>), Resolve(KeyCode, Promise>), End, @@ -105,6 +111,21 @@ impl Hook { future.value().ok_or(Error::ThreadStopped)? } + pub fn register_specific(&self, hotkey: Hotkey, callback: F) -> Result<()> + where + F: FnMut(bool) + Send + 'static, + { + let (future, promise) = future_promise(); + + self.sender + .send(Message::RegisterSpecific(hotkey, Box::new(callback), promise)) + .map_err(|_| Error::ThreadStopped)?; + + self.waker.wake().map_err(|_| Error::ThreadStopped)?; + + future.value().ok_or(Error::ThreadStopped)? + } + pub fn unregister(&self, hotkey: Hotkey) -> Result<()> { let (future, promise) = future_promise(); diff --git a/crates/livesplit-hotkey/src/linux/x11_impl.rs b/crates/livesplit-hotkey/src/linux/x11_impl.rs index 10bb5b01d..21950fd82 100644 --- a/crates/livesplit-hotkey/src/linux/x11_impl.rs +++ b/crates/livesplit-hotkey/src/linux/x11_impl.rs @@ -7,8 +7,7 @@ use std::{ use mio::{Events, Interest, Poll, Token, Waker, unix::SourceFd}; use x11_dl::xlib::{ - _XDisplay, AnyKey, AnyModifier, ControlMask, Display, GrabModeAsync, KeyPress, LockMask, - Mod1Mask, Mod2Mask, Mod3Mask, Mod4Mask, ShiftMask, XErrorEvent, XKeyEvent, Xlib, + AnyKey, AnyModifier, ControlMask, Display, GrabModeAsync, KeyPress, KeyRelease, LockMask, Mod1Mask, Mod2Mask, Mod3Mask, Mod4Mask, ShiftMask, XErrorEvent, XKeyEvent, Xlib, _XDisplay }; use super::{Error, Hook, Message}; @@ -151,6 +150,7 @@ pub fn new() -> Result { let mut result = Ok(()); let mut events = Events::with_capacity(1024); let mut hotkeys = HashMap::new(); + let mut specific_hotkeys = HashMap::new(); // For some reason we need to call this once for any KeyGrabs to // actually do anything. @@ -179,6 +179,18 @@ pub fn new() -> Result { Ok(()) }); } + Message::RegisterSpecific(key, callback, promise) => { + promise.set(if let Some(code) = code_for(key.key_code) { + if specific_hotkeys.insert((code, key.modifiers), callback).is_some() { + Err(crate::Error::AlreadyRegistered) + } else { + grab_key(&xlib, display, code, key.modifiers, false); + Ok(()) + } + } else { + Ok(()) + }); + } Message::Unregister(key, promise) => { let res = if let Some(code) = code_for(key.key_code) { let res = hotkeys @@ -208,7 +220,8 @@ pub fn new() -> Result { let err_code = (xlib.XNextEvent)(display, event.as_mut_ptr()); if err_code == 0 { let event = event.assume_init(); - if event.get_type() == KeyPress { + let event_type = event.get_type(); + if event_type == KeyPress || event_type == KeyRelease { let event: &XKeyEvent = event.as_ref(); let mut modifiers = Modifiers::empty(); @@ -225,10 +238,17 @@ pub fn new() -> Result { modifiers.insert(Modifiers::META); } - if let Some(callback) = - hotkeys.get_mut(&(event.keycode, modifiers)) - { - callback(); + let is_press = event_type == KeyPress; + + if let Some(callback) = specific_hotkeys.get_mut(&(event.keycode, modifiers)) { + callback(is_press); + } + + // Existing hotkeys (press only) + if is_press { + if let Some(callback) = hotkeys.get_mut(&(event.keycode, modifiers)) { + callback(); + } } } } From 11ede1561e5353ef362556fbb2bfd1aa8db50b98 Mon Sep 17 00:00:00 2001 From: SpikeHD <25207995+SpikeHD@users.noreply.github.com> Date: Sun, 17 Aug 2025 12:47:24 -0700 Subject: [PATCH 02/10] fix: tweak comment --- crates/livesplit-hotkey/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/livesplit-hotkey/src/lib.rs b/crates/livesplit-hotkey/src/lib.rs index 9e9ee5bd6..949c40f39 100644 --- a/crates/livesplit-hotkey/src/lib.rs +++ b/crates/livesplit-hotkey/src/lib.rs @@ -89,8 +89,8 @@ impl Hook { self.0.register(hotkey, callback) } - /// Registers a specific hotkey to listen to, which allows you to - /// differentiate between key presses and releases. + /// Registers a hotkey to listen to, but with specific handling for + /// press and release events. pub fn register_specific(&self, hotkey: Hotkey, callback: F) -> Result<()> where F: FnMut(bool) + Send + 'static, From 14ece49fb5b529879294202b4d252dce23401a3d Mon Sep 17 00:00:00 2001 From: SpikeHD <25207995+SpikeHD@users.noreply.github.com> Date: Sun, 17 Aug 2025 12:56:48 -0700 Subject: [PATCH 03/10] feat: completely blind window implementation --- crates/livesplit-hotkey/src/other/mod.rs | 8 +++++ crates/livesplit-hotkey/src/windows/mod.rs | 42 ++++++++++++++++++---- 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/crates/livesplit-hotkey/src/other/mod.rs b/crates/livesplit-hotkey/src/other/mod.rs index 3e2cb7406..87136a6dd 100644 --- a/crates/livesplit-hotkey/src/other/mod.rs +++ b/crates/livesplit-hotkey/src/other/mod.rs @@ -28,6 +28,14 @@ impl Hook { Ok(()) } + #[inline] + pub fn register_specific(&self, _: Hotkey, _: F) -> Result<()> + where + F: FnMut(bool) + Send + 'static, + { + Ok(()) + } + #[inline] pub fn unregister(&self, _: Hotkey) -> Result<()> { Ok(()) diff --git a/crates/livesplit-hotkey/src/windows/mod.rs b/crates/livesplit-hotkey/src/windows/mod.rs index acadd05e0..0a3fe49b8 100644 --- a/crates/livesplit-hotkey/src/windows/mod.rs +++ b/crates/livesplit-hotkey/src/windows/mod.rs @@ -46,10 +46,12 @@ impl fmt::Display for Error { } type Callback = Box; +type PressReleaseCallback = Box; pub struct Hook { thread_id: u32, hotkeys: Arc>>, + specific_hotkeys: Arc>>, } impl Drop for Hook { @@ -62,7 +64,7 @@ impl Drop for Hook { struct State { hook: HHOOK, - events: Sender, + events: Sender<(Hotkey, bool)>, modifiers: Modifiers, // FIXME: Use variant count when it's stable. // https://github.com/rust-lang/rust/issues/73662 @@ -294,10 +296,10 @@ unsafe extern "system" fn callback_proc(code: i32, wparam: WPARAM, lparam: LPARA state .events - .send(Hotkey { + .send((Hotkey { key_code, modifiers: state.modifiers, - }) + }, true)) .expect("Callback Thread disconnected"); match key_code { @@ -347,6 +349,14 @@ unsafe extern "system" fn callback_proc(code: i32, wparam: WPARAM, lparam: LPARA let (idx, bit) = key_idx(key_code); state.key_state[idx as usize] &= !bit; + state + .events + .send((Hotkey { + key_code, + modifiers: state.modifiers, + }, false)) + .expect("Callback Thread disconnected"); + match key_code { KeyCode::AltLeft | KeyCode::AltRight => { state.modifiers.remove(Modifiers::ALT); @@ -390,6 +400,10 @@ impl Hook { Hotkey, Box, >::new())); + let specific_hotkeys = Arc::new(Mutex::new(HashMap::< + (KeyCode, Modifiers), + Box, + >::new())); let (initialized_tx, initialized_rx) = channel(); let (events_tx, events_rx) = channel(); @@ -446,12 +460,16 @@ impl Hook { }); let hotkey_map = hotkeys.clone(); + let specific_hotkeys = specific_hotkeys.clone(); thread::spawn(move || { - while let Ok(key) = events_rx.recv() { - if let Some(callback) = hotkey_map.lock().unwrap().get_mut(&key) { + while let Ok((key, is_press)) = events_rx.recv() { + if let Some(callback) = hotkey_map.lock().unwrap().get_mut(&key) && is_press { callback(); } + if let Some(callback) = specific_hotkey_map.lock().unwrap().get_mut(&(key.key_code, key.modifiers)) { + callback(is_press); + } } }); @@ -459,7 +477,7 @@ impl Hook { .recv() .map_err(|_| crate::Error::Platform(Error::ThreadStopped))??; - Ok(Hook { thread_id, hotkeys }) + Ok(Hook { thread_id, hotkeys, specific_hotkeys }) } pub fn register(&self, hotkey: Hotkey, callback: F) -> Result<()> @@ -474,6 +492,18 @@ impl Hook { } } + pub fn register_specific(&self, hotkey: Hotkey, callback: F) -> Result<()> + where + F: FnMut(bool) + Send + 'static, + { + if let Entry::Vacant(vacant) = self.specific_hotkeys.lock().unwrap().entry((hotkey.key_code, hotkey.modifiers)) { + vacant.insert(Box::new(callback)); + Ok(()) + } else { + Err(crate::Error::AlreadyRegistered) + } + } + pub fn unregister(&self, hotkey: Hotkey) -> Result<()> { if self.hotkeys.lock().unwrap().remove(&hotkey).is_some() { Ok(()) From 060a1d2f2b88d05faaa789e96f169c889eb0864d Mon Sep 17 00:00:00 2001 From: SpikeHD <25207995+SpikeHD@users.noreply.github.com> Date: Sun, 17 Aug 2025 13:03:47 -0700 Subject: [PATCH 04/10] fix: windows impl (on my windows machine) --- crates/livesplit-hotkey/src/windows/mod.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/livesplit-hotkey/src/windows/mod.rs b/crates/livesplit-hotkey/src/windows/mod.rs index 0a3fe49b8..f6d6dd7af 100644 --- a/crates/livesplit-hotkey/src/windows/mod.rs +++ b/crates/livesplit-hotkey/src/windows/mod.rs @@ -460,12 +460,15 @@ impl Hook { }); let hotkey_map = hotkeys.clone(); - let specific_hotkeys = specific_hotkeys.clone(); + let specific_hotkey_map = specific_hotkeys.clone(); thread::spawn(move || { while let Ok((key, is_press)) = events_rx.recv() { - if let Some(callback) = hotkey_map.lock().unwrap().get_mut(&key) && is_press { - callback(); + // TODO make single expression once this is stable + if is_press { + if let Some(callback) = hotkey_map.lock().unwrap().get_mut(&key) { + callback(); + } } if let Some(callback) = specific_hotkey_map.lock().unwrap().get_mut(&(key.key_code, key.modifiers)) { callback(is_press); From bad71421a1c32ac2eed4ab66427db9b03e1396d3 Mon Sep 17 00:00:00 2001 From: SpikeHD <25207995+SpikeHD@users.noreply.github.com> Date: Sun, 17 Aug 2025 13:18:32 -0700 Subject: [PATCH 05/10] feat: macos implementation --- crates/livesplit-hotkey/src/macos/cg.rs | 2 ++ crates/livesplit-hotkey/src/macos/mod.rs | 29 +++++++++++++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/crates/livesplit-hotkey/src/macos/cg.rs b/crates/livesplit-hotkey/src/macos/cg.rs index a8d61d234..9d279e2fa 100644 --- a/crates/livesplit-hotkey/src/macos/cg.rs +++ b/crates/livesplit-hotkey/src/macos/cg.rs @@ -223,4 +223,6 @@ unsafe extern "C" { pub fn CGEventTapEnable(tap: MachPortRef, enable: bool); pub fn CGEventGetIntegerValueField(event: EventRef, field: EventField) -> i64; + + pub fn CGEventGetType(event: EventRef) -> EventType; } diff --git a/crates/livesplit-hotkey/src/macos/mod.rs b/crates/livesplit-hotkey/src/macos/mod.rs index 6a4eea481..49d2a63a1 100644 --- a/crates/livesplit-hotkey/src/macos/mod.rs +++ b/crates/livesplit-hotkey/src/macos/mod.rs @@ -76,6 +76,7 @@ unsafe impl Sync for RunLoop {} struct State { hotkeys: Mutex>>, + specific_hotkeys: Mutex>>, } /// A hook allows you to listen to hotkeys. @@ -105,6 +106,7 @@ impl Hook { let state = Arc::new(State { hotkeys: Mutex::new(HashMap::new()), + specific_hotkeys: Mutex::new(HashMap::new()), }); let thread_state = state.clone(); @@ -131,7 +133,7 @@ impl Hook { } else { EventTapOptions::LISTEN_ONLY }, - EventMask::KEY_DOWN, + EventMask::KEY_DOWN | EventMask::KEY_UP, Some(callback), state_ptr as *mut c_void, ); @@ -185,6 +187,18 @@ impl Hook { } } + pub fn register_specific(&self, hotkey: Hotkey, callback: F) -> Result<()> + where + F: FnMut(bool) + Send + 'static, + { + if let Entry::Vacant(vacant) = self.state.specific_hotkeys.lock().unwrap().entry(hotkey) { + vacant.insert(Box::new(callback)); + Ok(()) + } else { + Err(crate::Error::AlreadyRegistered) + } + } + pub fn unregister(&self, hotkey: Hotkey) -> Result<()> { let _ = self .state @@ -491,6 +505,19 @@ unsafe extern "C" fn callback( // If we handled the event and the hook is consuming, we should return // null so the system deletes the event. If the hook is not consuming // the return value will be ignored, so return null anyway. + return null_mut(); + } + + if let Some(callback) = state + .specific_hotkeys + .lock() + .unwrap() + .get_mut(&key_code.with_modifiers(modifiers)) + { + // Determine if this is a key down or key up event + let is_key_up = unsafe { cg::CGEventGetType(event) } == cg::EventType::KEY_UP; + + callback(!is_key_up); null_mut() } else { event From 7f82de15a14eb6d03f241fe168a84009aab112e8 Mon Sep 17 00:00:00 2001 From: SpikeHD <25207995+SpikeHD@users.noreply.github.com> Date: Sun, 17 Aug 2025 13:49:29 -0700 Subject: [PATCH 06/10] feat: untested wasm implementation --- crates/livesplit-hotkey/src/wasm_web/mod.rs | 36 +++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/crates/livesplit-hotkey/src/wasm_web/mod.rs b/crates/livesplit-hotkey/src/wasm_web/mod.rs index d08f0ed51..bc871994f 100644 --- a/crates/livesplit-hotkey/src/wasm_web/mod.rs +++ b/crates/livesplit-hotkey/src/wasm_web/mod.rs @@ -27,6 +27,7 @@ impl fmt::Display for Error { pub struct Hook { hotkeys: Arc>>>, + specific_hotkeys: Arc>>>, keyboard_callback: Closure, gamepad_callback: Closure, interval_id: Cell>, @@ -41,6 +42,10 @@ impl Drop for Hook { "keydown", self.keyboard_callback.as_ref().unchecked_ref(), ); + let _ = window.remove_event_listener_with_callback( + "keyup", + self.keyboard_callback.as_ref().unchecked_ref(), + ); if let Some(interval_id) = self.interval_id.get() { window.clear_interval_with_handle(interval_id); } @@ -131,6 +136,18 @@ impl Hook { event.prevent_default(); } } + + if let Some(callback) = self + .specific_hotkeys + .lock() + .unwrap() + .get_mut(&(code, modifiers)) + { + callback(event.type_() == "keydown"); + if prevent_default { + event.prevent_default(); + } + } } } }) as Box); @@ -139,6 +156,10 @@ impl Hook { .add_event_listener_with_callback("keydown", keyboard_callback.as_ref().unchecked_ref()) .map_err(|_| crate::Error::Platform(Error::FailedToCreateHook))?; + window + .add_event_listener_with_callback("keyup", keyboard_callback.as_ref().unchecked_ref()) + .map_err(|_| crate::Error::Platform(Error::FailedToCreateHook))?; + let hotkey_map = hotkeys.clone(); let mut states = Vec::new(); @@ -237,6 +258,21 @@ impl Hook { } } + pub fn register_specific(&self, hotkey: Hotkey, callback: F) -> Result<()> + where + F: FnMut(bool) + Send + 'static, + { + if let Entry::Vacant(vacant) = self.specific_hotkeys.lock().unwrap().entry(( + hotkey.key_code, + hotkey.modifiers, + )) { + vacant.insert(Box::new(callback)); + Ok(()) + } else { + Err(crate::Error::AlreadyRegistered) + } + } + pub fn unregister(&self, hotkey: Hotkey) -> Result<()> { if self.hotkeys.lock().unwrap().remove(&hotkey).is_some() { Ok(()) From ad9e775a027a2ee9dc5e5c4587a4c8b9c28cb056 Mon Sep 17 00:00:00 2001 From: SpikeHD <25207995+SpikeHD@users.noreply.github.com> Date: Sun, 17 Aug 2025 14:01:43 -0700 Subject: [PATCH 07/10] fix: feature-gate --- crates/livesplit-hotkey/Cargo.toml | 1 + crates/livesplit-hotkey/src/lib.rs | 3 +++ .../livesplit-hotkey/src/linux/evdev_impl.rs | 4 +++ crates/livesplit-hotkey/src/linux/mod.rs | 3 ++- crates/livesplit-hotkey/src/linux/x11_impl.rs | 3 +++ crates/livesplit-hotkey/src/macos/mod.rs | 4 +++ crates/livesplit-hotkey/src/other/mod.rs | 1 + crates/livesplit-hotkey/src/wasm_web/mod.rs | 26 +++++++++++++++++-- crates/livesplit-hotkey/src/windows/mod.rs | 12 ++++++++- 9 files changed, 53 insertions(+), 4 deletions(-) diff --git a/crates/livesplit-hotkey/Cargo.toml b/crates/livesplit-hotkey/Cargo.toml index 0b2d9022b..b42714f30 100644 --- a/crates/livesplit-hotkey/Cargo.toml +++ b/crates/livesplit-hotkey/Cargo.toml @@ -59,4 +59,5 @@ std = [ "windows-sys", "x11-dl", ] +press_and_release = [] wasm-web = ["wasm-bindgen", "web-sys", "js-sys"] diff --git a/crates/livesplit-hotkey/src/lib.rs b/crates/livesplit-hotkey/src/lib.rs index 949c40f39..01f03b8d1 100644 --- a/crates/livesplit-hotkey/src/lib.rs +++ b/crates/livesplit-hotkey/src/lib.rs @@ -91,6 +91,9 @@ impl Hook { /// Registers a hotkey to listen to, but with specific handling for /// press and release events. + /// + /// Requires the `press_and_release` feature to be enabled. + #[cfg(feature = "press_and_release")] pub fn register_specific(&self, hotkey: Hotkey, callback: F) -> Result<()> where F: FnMut(bool) + Send + 'static, diff --git a/crates/livesplit-hotkey/src/linux/evdev_impl.rs b/crates/livesplit-hotkey/src/linux/evdev_impl.rs index aab268fed..15339a345 100644 --- a/crates/livesplit-hotkey/src/linux/evdev_impl.rs +++ b/crates/livesplit-hotkey/src/linux/evdev_impl.rs @@ -255,6 +255,7 @@ pub fn new() -> Result { let mut result = Ok(()); let mut events = Events::with_capacity(1024); let mut hotkeys: HashMap<(Key, Modifiers), Box> = HashMap::new(); + #[cfg(feature = "press_and_release")] let mut specific_hotkeys: HashMap<(Key, Modifiers), Box> = HashMap::new(); let mut modifiers = Modifiers::empty(); @@ -275,6 +276,7 @@ pub fn new() -> Result { const PRESSED: i32 = 1; match ev.value() { PRESSED => { + #[cfg(feature = "press_and_release")] if let Some(callback) = specific_hotkeys.get_mut(&(k, modifiers)) { callback(true); } @@ -298,6 +300,7 @@ pub fn new() -> Result { } } RELEASED => { + #[cfg(feature = "press_and_release")] if let Some(callback) = specific_hotkeys.get_mut(&(k, modifiers)) { callback(false); } @@ -336,6 +339,7 @@ pub fn new() -> Result { }, ); } + #[cfg(feature = "press_and_release")] Message::RegisterSpecific(key, callback, promise) => { promise.set( if code_for(key.key_code) diff --git a/crates/livesplit-hotkey/src/linux/mod.rs b/crates/livesplit-hotkey/src/linux/mod.rs index e88b8e5ef..f13dc989a 100644 --- a/crates/livesplit-hotkey/src/linux/mod.rs +++ b/crates/livesplit-hotkey/src/linux/mod.rs @@ -43,7 +43,7 @@ enum Message { Box, Promise>, ), - // Tracks both press and release + #[cfg(feature = "press_and_release")] RegisterSpecific( Hotkey, Box, @@ -111,6 +111,7 @@ impl Hook { future.value().ok_or(Error::ThreadStopped)? } + #[cfg(feature = "press_and_release")] pub fn register_specific(&self, hotkey: Hotkey, callback: F) -> Result<()> where F: FnMut(bool) + Send + 'static, diff --git a/crates/livesplit-hotkey/src/linux/x11_impl.rs b/crates/livesplit-hotkey/src/linux/x11_impl.rs index 21950fd82..89152c7d7 100644 --- a/crates/livesplit-hotkey/src/linux/x11_impl.rs +++ b/crates/livesplit-hotkey/src/linux/x11_impl.rs @@ -150,6 +150,7 @@ pub fn new() -> Result { let mut result = Ok(()); let mut events = Events::with_capacity(1024); let mut hotkeys = HashMap::new(); + #[cfg(feature = "press_and_release")] let mut specific_hotkeys = HashMap::new(); // For some reason we need to call this once for any KeyGrabs to @@ -179,6 +180,7 @@ pub fn new() -> Result { Ok(()) }); } + #[cfg(feature = "press_and_release")] Message::RegisterSpecific(key, callback, promise) => { promise.set(if let Some(code) = code_for(key.key_code) { if specific_hotkeys.insert((code, key.modifiers), callback).is_some() { @@ -240,6 +242,7 @@ pub fn new() -> Result { let is_press = event_type == KeyPress; + #[cfg(feature = "press_and_release")] if let Some(callback) = specific_hotkeys.get_mut(&(event.keycode, modifiers)) { callback(is_press); } diff --git a/crates/livesplit-hotkey/src/macos/mod.rs b/crates/livesplit-hotkey/src/macos/mod.rs index 49d2a63a1..7ec3c33a5 100644 --- a/crates/livesplit-hotkey/src/macos/mod.rs +++ b/crates/livesplit-hotkey/src/macos/mod.rs @@ -76,6 +76,7 @@ unsafe impl Sync for RunLoop {} struct State { hotkeys: Mutex>>, + #[cfg(feature = "press_and_release")] specific_hotkeys: Mutex>>, } @@ -106,6 +107,7 @@ impl Hook { let state = Arc::new(State { hotkeys: Mutex::new(HashMap::new()), + #[cfg(feature = "press_and_release")] specific_hotkeys: Mutex::new(HashMap::new()), }); let thread_state = state.clone(); @@ -187,6 +189,7 @@ impl Hook { } } + #[cfg(feature = "press_and_release")] pub fn register_specific(&self, hotkey: Hotkey, callback: F) -> Result<()> where F: FnMut(bool) + Send + 'static, @@ -508,6 +511,7 @@ unsafe extern "C" fn callback( return null_mut(); } + #[cfg(feature = "press_and_release")] if let Some(callback) = state .specific_hotkeys .lock() diff --git a/crates/livesplit-hotkey/src/other/mod.rs b/crates/livesplit-hotkey/src/other/mod.rs index 87136a6dd..26af8d375 100644 --- a/crates/livesplit-hotkey/src/other/mod.rs +++ b/crates/livesplit-hotkey/src/other/mod.rs @@ -29,6 +29,7 @@ impl Hook { } #[inline] + #[cfg(feature = "press_and_release")] pub fn register_specific(&self, _: Hotkey, _: F) -> Result<()> where F: FnMut(bool) + Send + 'static, diff --git a/crates/livesplit-hotkey/src/wasm_web/mod.rs b/crates/livesplit-hotkey/src/wasm_web/mod.rs index bc871994f..720ec3979 100644 --- a/crates/livesplit-hotkey/src/wasm_web/mod.rs +++ b/crates/livesplit-hotkey/src/wasm_web/mod.rs @@ -27,6 +27,7 @@ impl fmt::Display for Error { pub struct Hook { hotkeys: Arc>>>, + #[cfg(feature = "press_and_release")] specific_hotkeys: Arc>>>, keyboard_callback: Closure, gamepad_callback: Closure, @@ -42,6 +43,7 @@ impl Drop for Hook { "keydown", self.keyboard_callback.as_ref().unchecked_ref(), ); + #[cfg(feature = "press_and_release")] let _ = window.remove_event_listener_with_callback( "keyup", self.keyboard_callback.as_ref().unchecked_ref(), @@ -88,10 +90,17 @@ impl Hook { Hotkey, Box, >::new())); + #[cfg(feature = "press_and_release")] + let specific_hotkeys = Arc::new(Mutex::new(HashMap::< + (KeyCode, Modifiers), + Box, + >::new())); let window = window().ok_or(crate::Error::Platform(Error::FailedToCreateHook))?; let hotkey_map = hotkeys.clone(); + #[cfg(feature = "press_and_release")] + let specific_hotkey_map = specific_hotkeys.clone(); let keyboard_callback = Closure::wrap(Box::new(move |event: MaybeKeyboardEvent| { // Despite all sorts of documentation claiming that `keydown` events // pass you a `KeyboardEvent`, this is not actually always the case @@ -137,8 +146,8 @@ impl Hook { } } - if let Some(callback) = self - .specific_hotkeys + #[cfg(feature = "press_and_release")] + if let Some(callback) = specific_hotkey_map .lock() .unwrap() .get_mut(&(code, modifiers)) @@ -156,11 +165,14 @@ impl Hook { .add_event_listener_with_callback("keydown", keyboard_callback.as_ref().unchecked_ref()) .map_err(|_| crate::Error::Platform(Error::FailedToCreateHook))?; + #[cfg(feature = "press_and_release")] window .add_event_listener_with_callback("keyup", keyboard_callback.as_ref().unchecked_ref()) .map_err(|_| crate::Error::Platform(Error::FailedToCreateHook))?; let hotkey_map = hotkeys.clone(); + #[cfg(feature = "press_and_release")] + let specific_hotkey_map = specific_hotkeys.clone(); let mut states = Vec::new(); let navigator = window.navigator(); @@ -217,6 +229,15 @@ impl Hook { { callback(); } + } else if !pressed && *state { + #[cfg(feature = "press_and_release")] + if let Some(callback) = specific_hotkey_map + .lock() + .unwrap() + .get_mut(&(code, Modifiers::empty())) + { + callback(false); + } } *state = pressed; } @@ -258,6 +279,7 @@ impl Hook { } } + #[cfg(feature = "press_and_release")] pub fn register_specific(&self, hotkey: Hotkey, callback: F) -> Result<()> where F: FnMut(bool) + Send + 'static, diff --git a/crates/livesplit-hotkey/src/windows/mod.rs b/crates/livesplit-hotkey/src/windows/mod.rs index f6d6dd7af..3e2cc2c95 100644 --- a/crates/livesplit-hotkey/src/windows/mod.rs +++ b/crates/livesplit-hotkey/src/windows/mod.rs @@ -51,6 +51,7 @@ type PressReleaseCallback = Box; pub struct Hook { thread_id: u32, hotkeys: Arc>>, + #[cfg(feature = "press_and_release")] specific_hotkeys: Arc>>, } @@ -400,6 +401,7 @@ impl Hook { Hotkey, Box, >::new())); + #[cfg(feature = "press_and_release")] let specific_hotkeys = Arc::new(Mutex::new(HashMap::< (KeyCode, Modifiers), Box, @@ -460,6 +462,7 @@ impl Hook { }); let hotkey_map = hotkeys.clone(); + #[cfg(feature = "press_and_release")] let specific_hotkey_map = specific_hotkeys.clone(); thread::spawn(move || { @@ -470,6 +473,7 @@ impl Hook { callback(); } } + #[cfg(feature = "press_and_release")] if let Some(callback) = specific_hotkey_map.lock().unwrap().get_mut(&(key.key_code, key.modifiers)) { callback(is_press); } @@ -480,7 +484,12 @@ impl Hook { .recv() .map_err(|_| crate::Error::Platform(Error::ThreadStopped))??; - Ok(Hook { thread_id, hotkeys, specific_hotkeys }) + Ok(Hook { + thread_id, + hotkeys, + #[cfg(feature = "press_and_release")] + specific_hotkeys + }) } pub fn register(&self, hotkey: Hotkey, callback: F) -> Result<()> @@ -495,6 +504,7 @@ impl Hook { } } + #[cfg(feature = "press_and_release")] pub fn register_specific(&self, hotkey: Hotkey, callback: F) -> Result<()> where F: FnMut(bool) + Send + 'static, From 6b85b9f916e1a4201d8322c0441145eff9dedccc Mon Sep 17 00:00:00 2001 From: SpikeHD <25207995+SpikeHD@users.noreply.github.com> Date: Sun, 17 Aug 2025 14:05:23 -0700 Subject: [PATCH 08/10] fix: unused type --- crates/livesplit-hotkey/src/windows/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/livesplit-hotkey/src/windows/mod.rs b/crates/livesplit-hotkey/src/windows/mod.rs index 3e2cc2c95..776c4a4be 100644 --- a/crates/livesplit-hotkey/src/windows/mod.rs +++ b/crates/livesplit-hotkey/src/windows/mod.rs @@ -46,6 +46,7 @@ impl fmt::Display for Error { } type Callback = Box; +#[cfg(feature = "press_and_release")] type PressReleaseCallback = Box; pub struct Hook { From 4d2c23d8585b9eab23fa28069ff0a5364ee2a64c Mon Sep 17 00:00:00 2001 From: SpikeHD <25207995+SpikeHD@users.noreply.github.com> Date: Sun, 17 Aug 2025 14:08:12 -0700 Subject: [PATCH 09/10] fix: macos build --- crates/livesplit-hotkey/src/macos/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/livesplit-hotkey/src/macos/mod.rs b/crates/livesplit-hotkey/src/macos/mod.rs index 7ec3c33a5..166f04a2e 100644 --- a/crates/livesplit-hotkey/src/macos/mod.rs +++ b/crates/livesplit-hotkey/src/macos/mod.rs @@ -522,8 +522,8 @@ unsafe extern "C" fn callback( let is_key_up = unsafe { cg::CGEventGetType(event) } == cg::EventType::KEY_UP; callback(!is_key_up); - null_mut() - } else { - event + return null_mut(); } + + event } From f47830a666be6fa861fad0eedc631025a802a90b Mon Sep 17 00:00:00 2001 From: SpikeHD <25207995+SpikeHD@users.noreply.github.com> Date: Sun, 17 Aug 2025 14:11:56 -0700 Subject: [PATCH 10/10] fix: fmt --- crates/livesplit-hotkey/src/lib.rs | 2 +- .../livesplit-hotkey/src/linux/evdev_impl.rs | 33 ++++++++----- crates/livesplit-hotkey/src/linux/mod.rs | 10 ++-- crates/livesplit-hotkey/src/linux/x11_impl.rs | 16 ++++-- crates/livesplit-hotkey/src/macos/mod.rs | 2 +- .../livesplit-hotkey/src/macos/permission.rs | 8 +-- crates/livesplit-hotkey/src/wasm_web/mod.rs | 13 +++-- crates/livesplit-hotkey/src/windows/mod.rs | 49 ++++++++++++------- 8 files changed, 85 insertions(+), 48 deletions(-) diff --git a/crates/livesplit-hotkey/src/lib.rs b/crates/livesplit-hotkey/src/lib.rs index 01f03b8d1..936e53f1e 100644 --- a/crates/livesplit-hotkey/src/lib.rs +++ b/crates/livesplit-hotkey/src/lib.rs @@ -91,7 +91,7 @@ impl Hook { /// Registers a hotkey to listen to, but with specific handling for /// press and release events. - /// + /// /// Requires the `press_and_release` feature to be enabled. #[cfg(feature = "press_and_release")] pub fn register_specific(&self, hotkey: Hotkey, callback: F) -> Result<()> diff --git a/crates/livesplit-hotkey/src/linux/evdev_impl.rs b/crates/livesplit-hotkey/src/linux/evdev_impl.rs index 15339a345..e455aa530 100644 --- a/crates/livesplit-hotkey/src/linux/evdev_impl.rs +++ b/crates/livesplit-hotkey/src/linux/evdev_impl.rs @@ -1,10 +1,10 @@ use std::{collections::hash_map::HashMap, os::unix::prelude::AsRawFd, ptr, thread}; use evdev::{Device, EventType, InputEventKind, Key}; -use mio::{unix::SourceFd, Events, Interest, Poll, Token, Waker}; -use x11_dl::xlib::{Xlib, _XDisplay}; +use mio::{Events, Interest, Poll, Token, Waker, unix::SourceFd}; +use x11_dl::xlib::{_XDisplay, Xlib}; -use super::{x11_impl, Error, Hook, Message}; +use super::{Error, Hook, Message, x11_impl}; use crate::{KeyCode, Modifiers, Result}; // Low numbered tokens are allocated to devices. @@ -252,14 +252,15 @@ pub fn new() -> Result { } let join_handle = thread::spawn(move || -> Result<()> { - let mut result = Ok(()); - let mut events = Events::with_capacity(1024); - let mut hotkeys: HashMap<(Key, Modifiers), Box> = HashMap::new(); - #[cfg(feature = "press_and_release")] - let mut specific_hotkeys: HashMap<(Key, Modifiers), Box> = HashMap::new(); - let mut modifiers = Modifiers::empty(); + let mut result = Ok(()); + let mut events = Events::with_capacity(1024); + let mut hotkeys: HashMap<(Key, Modifiers), Box> = HashMap::new(); + #[cfg(feature = "press_and_release")] + let mut specific_hotkeys: HashMap<(Key, Modifiers), Box> = + HashMap::new(); + let mut modifiers = Modifiers::empty(); - let (mut xlib, mut display) = (None, None); + let (mut xlib, mut display) = (None, None); 'event_loop: loop { if poll.poll(&mut events, None).is_err() { @@ -277,7 +278,9 @@ pub fn new() -> Result { match ev.value() { PRESSED => { #[cfg(feature = "press_and_release")] - if let Some(callback) = specific_hotkeys.get_mut(&(k, modifiers)) { + if let Some(callback) = + specific_hotkeys.get_mut(&(k, modifiers)) + { callback(true); } if let Some(callback) = hotkeys.get_mut(&(k, modifiers)) { @@ -301,7 +304,9 @@ pub fn new() -> Result { } RELEASED => { #[cfg(feature = "press_and_release")] - if let Some(callback) = specific_hotkeys.get_mut(&(k, modifiers)) { + if let Some(callback) = + specific_hotkeys.get_mut(&(k, modifiers)) + { callback(false); } match k { @@ -343,7 +348,9 @@ pub fn new() -> Result { Message::RegisterSpecific(key, callback, promise) => { promise.set( if code_for(key.key_code) - .and_then(|k| specific_hotkeys.insert((k, key.modifiers), callback)) + .and_then(|k| { + specific_hotkeys.insert((k, key.modifiers), callback) + }) .is_some() { Err(crate::Error::AlreadyRegistered) diff --git a/crates/livesplit-hotkey/src/linux/mod.rs b/crates/livesplit-hotkey/src/linux/mod.rs index f13dc989a..4f0d53e5a 100644 --- a/crates/livesplit-hotkey/src/linux/mod.rs +++ b/crates/livesplit-hotkey/src/linux/mod.rs @@ -3,8 +3,8 @@ use std::{fmt, thread::JoinHandle}; use crate::{ConsumePreference, Hotkey, KeyCode, Result}; use crossbeam_channel::Sender; use mio::Waker; -use nix::unistd::{getgroups, Group}; -use promising_future::{future_promise, Promise}; +use nix::unistd::{Group, getgroups}; +use promising_future::{Promise, future_promise}; mod evdev_impl; mod x11_impl; @@ -119,7 +119,11 @@ impl Hook { let (future, promise) = future_promise(); self.sender - .send(Message::RegisterSpecific(hotkey, Box::new(callback), promise)) + .send(Message::RegisterSpecific( + hotkey, + Box::new(callback), + promise, + )) .map_err(|_| Error::ThreadStopped)?; self.waker.wake().map_err(|_| Error::ThreadStopped)?; diff --git a/crates/livesplit-hotkey/src/linux/x11_impl.rs b/crates/livesplit-hotkey/src/linux/x11_impl.rs index 89152c7d7..504d91a17 100644 --- a/crates/livesplit-hotkey/src/linux/x11_impl.rs +++ b/crates/livesplit-hotkey/src/linux/x11_impl.rs @@ -7,7 +7,8 @@ use std::{ use mio::{Events, Interest, Poll, Token, Waker, unix::SourceFd}; use x11_dl::xlib::{ - AnyKey, AnyModifier, ControlMask, Display, GrabModeAsync, KeyPress, KeyRelease, LockMask, Mod1Mask, Mod2Mask, Mod3Mask, Mod4Mask, ShiftMask, XErrorEvent, XKeyEvent, Xlib, _XDisplay + _XDisplay, AnyKey, AnyModifier, ControlMask, Display, GrabModeAsync, KeyPress, KeyRelease, + LockMask, Mod1Mask, Mod2Mask, Mod3Mask, Mod4Mask, ShiftMask, XErrorEvent, XKeyEvent, Xlib, }; use super::{Error, Hook, Message}; @@ -183,7 +184,10 @@ pub fn new() -> Result { #[cfg(feature = "press_and_release")] Message::RegisterSpecific(key, callback, promise) => { promise.set(if let Some(code) = code_for(key.key_code) { - if specific_hotkeys.insert((code, key.modifiers), callback).is_some() { + if specific_hotkeys + .insert((code, key.modifiers), callback) + .is_some() + { Err(crate::Error::AlreadyRegistered) } else { grab_key(&xlib, display, code, key.modifiers, false); @@ -243,13 +247,17 @@ pub fn new() -> Result { let is_press = event_type == KeyPress; #[cfg(feature = "press_and_release")] - if let Some(callback) = specific_hotkeys.get_mut(&(event.keycode, modifiers)) { + if let Some(callback) = + specific_hotkeys.get_mut(&(event.keycode, modifiers)) + { callback(is_press); } // Existing hotkeys (press only) if is_press { - if let Some(callback) = hotkeys.get_mut(&(event.keycode, modifiers)) { + if let Some(callback) = + hotkeys.get_mut(&(event.keycode, modifiers)) + { callback(); } } diff --git a/crates/livesplit-hotkey/src/macos/mod.rs b/crates/livesplit-hotkey/src/macos/mod.rs index 166f04a2e..8e6bb16b1 100644 --- a/crates/livesplit-hotkey/src/macos/mod.rs +++ b/crates/livesplit-hotkey/src/macos/mod.rs @@ -510,7 +510,7 @@ unsafe extern "C" fn callback( // the return value will be ignored, so return null anyway. return null_mut(); } - + #[cfg(feature = "press_and_release")] if let Some(callback) = state .specific_hotkeys diff --git a/crates/livesplit-hotkey/src/macos/permission.rs b/crates/livesplit-hotkey/src/macos/permission.rs index dac2e57be..e804d530d 100644 --- a/crates/livesplit-hotkey/src/macos/permission.rs +++ b/crates/livesplit-hotkey/src/macos/permission.rs @@ -1,10 +1,10 @@ use super::{ - ax::{kAXTrustedCheckOptionPrompt, AXIsProcessTrustedWithOptions}, + Owned, + ax::{AXIsProcessTrustedWithOptions, kAXTrustedCheckOptionPrompt}, cf::{ - kCFAllocatorDefault, kCFBooleanTrue, kCFTypeDictionaryKeyCallBacks, - kCFTypeDictionaryValueCallBacks, CFDictionaryCreate, + CFDictionaryCreate, kCFAllocatorDefault, kCFBooleanTrue, kCFTypeDictionaryKeyCallBacks, + kCFTypeDictionaryValueCallBacks, }, - Owned, }; use std::ffi::c_void; diff --git a/crates/livesplit-hotkey/src/wasm_web/mod.rs b/crates/livesplit-hotkey/src/wasm_web/mod.rs index 720ec3979..49fd5e9c0 100644 --- a/crates/livesplit-hotkey/src/wasm_web/mod.rs +++ b/crates/livesplit-hotkey/src/wasm_web/mod.rs @@ -28,7 +28,8 @@ impl fmt::Display for Error { pub struct Hook { hotkeys: Arc>>>, #[cfg(feature = "press_and_release")] - specific_hotkeys: Arc>>>, + specific_hotkeys: + Arc>>>, keyboard_callback: Closure, gamepad_callback: Closure, interval_id: Cell>, @@ -284,10 +285,12 @@ impl Hook { where F: FnMut(bool) + Send + 'static, { - if let Entry::Vacant(vacant) = self.specific_hotkeys.lock().unwrap().entry(( - hotkey.key_code, - hotkey.modifiers, - )) { + if let Entry::Vacant(vacant) = self + .specific_hotkeys + .lock() + .unwrap() + .entry((hotkey.key_code, hotkey.modifiers)) + { vacant.insert(Box::new(callback)); Ok(()) } else { diff --git a/crates/livesplit-hotkey/src/windows/mod.rs b/crates/livesplit-hotkey/src/windows/mod.rs index 776c4a4be..cef625c0c 100644 --- a/crates/livesplit-hotkey/src/windows/mod.rs +++ b/crates/livesplit-hotkey/src/windows/mod.rs @@ -298,10 +298,13 @@ unsafe extern "system" fn callback_proc(code: i32, wparam: WPARAM, lparam: LPARA state .events - .send((Hotkey { - key_code, - modifiers: state.modifiers, - }, true)) + .send(( + Hotkey { + key_code, + modifiers: state.modifiers, + }, + true, + )) .expect("Callback Thread disconnected"); match key_code { @@ -353,10 +356,13 @@ unsafe extern "system" fn callback_proc(code: i32, wparam: WPARAM, lparam: LPARA state .events - .send((Hotkey { - key_code, - modifiers: state.modifiers, - }, false)) + .send(( + Hotkey { + key_code, + modifiers: state.modifiers, + }, + false, + )) .expect("Callback Thread disconnected"); match key_code { @@ -470,12 +476,16 @@ impl Hook { while let Ok((key, is_press)) = events_rx.recv() { // TODO make single expression once this is stable if is_press { - if let Some(callback) = hotkey_map.lock().unwrap().get_mut(&key) { - callback(); - } + if let Some(callback) = hotkey_map.lock().unwrap().get_mut(&key) { + callback(); + } } #[cfg(feature = "press_and_release")] - if let Some(callback) = specific_hotkey_map.lock().unwrap().get_mut(&(key.key_code, key.modifiers)) { + if let Some(callback) = specific_hotkey_map + .lock() + .unwrap() + .get_mut(&(key.key_code, key.modifiers)) + { callback(is_press); } } @@ -486,10 +496,10 @@ impl Hook { .map_err(|_| crate::Error::Platform(Error::ThreadStopped))??; Ok(Hook { - thread_id, - hotkeys, - #[cfg(feature = "press_and_release")] - specific_hotkeys + thread_id, + hotkeys, + #[cfg(feature = "press_and_release")] + specific_hotkeys, }) } @@ -510,7 +520,12 @@ impl Hook { where F: FnMut(bool) + Send + 'static, { - if let Entry::Vacant(vacant) = self.specific_hotkeys.lock().unwrap().entry((hotkey.key_code, hotkey.modifiers)) { + if let Entry::Vacant(vacant) = self + .specific_hotkeys + .lock() + .unwrap() + .entry((hotkey.key_code, hotkey.modifiers)) + { vacant.insert(Box::new(callback)); Ok(()) } else {