Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/livesplit-hotkey/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,5 @@ std = [
"windows-sys",
"x11-dl",
]
press_and_release = []
wasm-web = ["wasm-bindgen", "web-sys", "js-sys"]
12 changes: 12 additions & 0 deletions crates/livesplit-hotkey/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,18 @@ impl Hook {
self.0.register(hotkey, callback)
}

/// 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<F>(&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)
Expand Down
64 changes: 48 additions & 16 deletions crates/livesplit-hotkey/src/linux/evdev_impl.rs
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -255,6 +255,9 @@ pub fn new() -> Result<Hook> {
let mut result = Ok(());
let mut events = Events::with_capacity(1024);
let mut hotkeys: HashMap<(Key, Modifiers), Box<dyn FnMut() + Send>> = HashMap::new();
#[cfg(feature = "press_and_release")]
let mut specific_hotkeys: HashMap<(Key, Modifiers), Box<dyn FnMut(bool) + Send>> =
HashMap::new();
let mut modifiers = Modifiers::empty();

let (mut xlib, mut display) = (None, None);
Expand All @@ -274,6 +277,12 @@ pub fn new() -> Result<Hook> {
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);
}
if let Some(callback) = hotkeys.get_mut(&(k, modifiers)) {
callback();
}
Expand All @@ -293,21 +302,29 @@ pub fn new() -> Result<Hook> {
_ => {}
}
}
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 => {
#[cfg(feature = "press_and_release")]
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
}
}
Expand All @@ -327,6 +344,21 @@ pub fn new() -> Result<Hook> {
},
);
}
#[cfg(feature = "press_and_release")]
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))
Expand Down
30 changes: 28 additions & 2 deletions crates/livesplit-hotkey/src/linux/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -43,6 +43,12 @@ enum Message {
Box<dyn FnMut() + Send + 'static>,
Promise<Result<()>>,
),
#[cfg(feature = "press_and_release")]
RegisterSpecific(
Hotkey,
Box<dyn FnMut(bool) + Send + 'static>,
Promise<Result<()>>,
),
Unregister(Hotkey, Promise<Result<()>>),
Resolve(KeyCode, Promise<Option<char>>),
End,
Expand Down Expand Up @@ -105,6 +111,26 @@ impl Hook {
future.value().ok_or(Error::ThreadStopped)?
}

#[cfg(feature = "press_and_release")]
pub fn register_specific<F>(&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();

Expand Down
41 changes: 36 additions & 5 deletions crates/livesplit-hotkey/src/linux/x11_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ 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,
_XDisplay, AnyKey, AnyModifier, ControlMask, Display, GrabModeAsync, KeyPress, KeyRelease,
LockMask, Mod1Mask, Mod2Mask, Mod3Mask, Mod4Mask, ShiftMask, XErrorEvent, XKeyEvent, Xlib,
};

use super::{Error, Hook, Message};
Expand Down Expand Up @@ -151,6 +151,8 @@ pub fn new() -> Result<Hook> {
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
// actually do anything.
Expand Down Expand Up @@ -179,6 +181,22 @@ pub fn new() -> Result<Hook> {
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()
{
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
Expand Down Expand Up @@ -208,7 +226,8 @@ pub fn new() -> Result<Hook> {
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();
Expand All @@ -225,10 +244,22 @@ pub fn new() -> Result<Hook> {
modifiers.insert(Modifiers::META);
}

let is_press = event_type == KeyPress;

#[cfg(feature = "press_and_release")]
if let Some(callback) =
hotkeys.get_mut(&(event.keycode, modifiers))
specific_hotkeys.get_mut(&(event.keycode, modifiers))
{
callback();
callback(is_press);
}

// Existing hotkeys (press only)
if is_press {
if let Some(callback) =
hotkeys.get_mut(&(event.keycode, modifiers))
{
callback();
}
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions crates/livesplit-hotkey/src/macos/cg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
39 changes: 35 additions & 4 deletions crates/livesplit-hotkey/src/macos/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ unsafe impl Sync for RunLoop {}

struct State {
hotkeys: Mutex<HashMap<Hotkey, Box<dyn FnMut() + Send + 'static>>>,
#[cfg(feature = "press_and_release")]
specific_hotkeys: Mutex<HashMap<Hotkey, Box<dyn FnMut(bool) + Send + 'static>>>,
}

/// A hook allows you to listen to hotkeys.
Expand Down Expand Up @@ -105,6 +107,8 @@ 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();

Expand All @@ -131,7 +135,7 @@ impl Hook {
} else {
EventTapOptions::LISTEN_ONLY
},
EventMask::KEY_DOWN,
EventMask::KEY_DOWN | EventMask::KEY_UP,
Some(callback),
state_ptr as *mut c_void,
);
Expand Down Expand Up @@ -185,6 +189,19 @@ impl Hook {
}
}

#[cfg(feature = "press_and_release")]
pub fn register_specific<F>(&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
Expand Down Expand Up @@ -491,8 +508,22 @@ 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.
null_mut()
} else {
event
return null_mut();
}

#[cfg(feature = "press_and_release")]
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);
return null_mut();
}

event
}
8 changes: 4 additions & 4 deletions crates/livesplit-hotkey/src/macos/permission.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down
9 changes: 9 additions & 0 deletions crates/livesplit-hotkey/src/other/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@ impl Hook {
Ok(())
}

#[inline]
#[cfg(feature = "press_and_release")]
pub fn register_specific<F>(&self, _: Hotkey, _: F) -> Result<()>
where
F: FnMut(bool) + Send + 'static,
{
Ok(())
}

#[inline]
pub fn unregister(&self, _: Hotkey) -> Result<()> {
Ok(())
Expand Down
Loading