Skip to content

Commit

Permalink
Allow hotkeys to be consumed
Browse files Browse the repository at this point in the history
This adds a new type to `livesplit-hotkey` that allows you to specify
what your preference is for the hotkeys. You can now specify that you
want the hotkeys to be consumed, which means that the hotkeys will not
be forwarded to the application that is in focus. This is currently not
implemented for all the platforms, but the basic API for it is here now.
  • Loading branch information
CryZe committed Jan 16, 2024
1 parent edcdc6f commit 72d9567
Show file tree
Hide file tree
Showing 9 changed files with 174 additions and 131 deletions.
2 changes: 1 addition & 1 deletion crates/livesplit-hotkey/src/key_code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1845,7 +1845,7 @@ impl KeyCode {
pub fn resolve(self, hook: &Hook) -> Cow<'static, str> {
let class = self.classify();
if class == KeyCodeClass::WritingSystem {
if let Some(resolved) = hook.try_resolve(self) {
if let Some(resolved) = hook.0.try_resolve(self) {
let uppercase = if resolved != "ß" {
resolved.to_uppercase()
} else {
Expand Down
85 changes: 84 additions & 1 deletion crates/livesplit-hotkey/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,90 @@ cfg_if::cfg_if! {
mod hotkey;
mod key_code;
mod modifiers;
pub use self::{hotkey::*, key_code::*, modifiers::*, platform::*};
use core::fmt;

pub use self::{hotkey::*, key_code::*, modifiers::*};

/// A hook allows you to listen to hotkeys.
#[repr(transparent)]
pub struct Hook(platform::Hook);

/// The preference of whether the hotkeys should be consumed or not. Consuming a
/// hotkey means that the hotkey won't be passed on to the application that is
/// currently in focus.
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
pub enum ConsumePreference {
/// There is no preference, the crate chooses the most suitable implementation.
NoPreference,
/// Prefers the hotkeys to be consumed, but does not require it.
PreferConsume,
/// Prefers the hotkeys to not be consumed, but does not require it.
PreferNoConsume,
/// Requires the hotkeys to be consumed, the [`Hook`] won't be created otherwise.
MustConsume,
/// Requires the hotkeys to not be consumed, the [`Hook`] won't be created
/// otherwise.
MustNotConsume,
}

impl Hook {
/// Creates a new hook without any preference of whether the hotkeys should
/// be consumed or not.
pub fn new() -> Result<Self> {
Ok(Self(platform::Hook::new(ConsumePreference::NoPreference)?))
}

/// Creates a new hook with a specific preference of whether the hotkeys
/// should be consumed or not.
pub fn with_consume_preference(consume: ConsumePreference) -> Result<Self> {
Ok(Self(platform::Hook::new(consume)?))
}

/// Registers a hotkey to listen to.
pub fn register<F>(&self, hotkey: Hotkey, callback: F) -> Result<()>
where
F: FnMut() + Send + 'static,
{
self.0.register(hotkey, callback)
}

/// Unregisters a previously registered hotkey.
pub fn unregister(&self, hotkey: Hotkey) -> Result<()> {
self.0.unregister(hotkey)
}
}

/// The result type for this crate.
pub type Result<T> = core::result::Result<T, Error>;

/// The error type for this crate.
#[derive(Debug)]
#[non_exhaustive]
pub enum Error {
/// The consume preference could not be met on the current platform.
UnmatchedPreference,
/// The hotkey was already registered.
AlreadyRegistered,
/// The hotkey to unregister was not registered.
NotRegistered,
/// A platform specific error occurred.
Platform(platform::Error),
}

impl std::error::Error for Error {}

impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::UnmatchedPreference => {
"The consume preference could not be met on the current platform."
}
Self::AlreadyRegistered => "The hotkey was already registered.",
Self::NotRegistered => "The hotkey to unregister was not registered.",
Self::Platform(e) => return fmt::Display::fmt(e, f),
})
}
}

#[cfg(not(all(target_family = "wasm", target_os = "unknown", feature = "wasm-web")))]
const _: () = {
Expand Down
10 changes: 5 additions & 5 deletions crates/livesplit-hotkey/src/linux/evdev_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ use evdev::{Device, EventType, InputEventKind, Key};
use mio::{unix::SourceFd, Events, Interest, Poll, Token, Waker};
use x11_dl::xlib::{Xlib, _XDisplay};

use super::{x11_impl, Message};
use crate::{Error, Hook, KeyCode, Modifiers, Result};
use super::{x11_impl, Error, Hook, Message};
use crate::{KeyCode, Modifiers, Result};

// Low numbered tokens are allocated to devices.
const PING_TOKEN: Token = Token(usize::MAX);
Expand Down Expand Up @@ -321,7 +321,7 @@ pub fn new() -> Result<Hook> {
.and_then(|k| hotkeys.insert((k, key.modifiers), callback))
.is_some()
{
Err(Error::AlreadyRegistered)
Err(crate::Error::AlreadyRegistered)
} else {
Ok(())
},
Expand All @@ -330,7 +330,7 @@ pub fn new() -> Result<Hook> {
Message::Unregister(key, promise) => promise.set(
code_for(key.key_code)
.and_then(|k| hotkeys.remove(&(k, key.modifiers)).map(drop))
.ok_or(Error::NotRegistered),
.ok_or(crate::Error::NotRegistered),
),
Message::Resolve(key_code, promise) => {
promise.set(resolve(&mut xlib, &mut display, key_code))
Expand All @@ -348,7 +348,7 @@ pub fn new() -> Result<Hook> {
unsafe { (xlib.XCloseDisplay)(display) };
}

result
result.map_err(Into::into)
});

Ok(Hook {
Expand Down
48 changes: 22 additions & 26 deletions crates/livesplit-hotkey/src/linux/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::{fmt, thread::JoinHandle};

use crate::{Hotkey, KeyCode};
use crate::{ConsumePreference, Hotkey, KeyCode, Result};
use crossbeam_channel::Sender;
use mio::Waker;
use nix::unistd::{getgroups, Group};
Expand All @@ -9,33 +9,25 @@ use promising_future::{future_promise, Promise};
mod evdev_impl;
mod x11_impl;

/// The error type for this crate.
#[derive(Debug, Copy, Clone)]
#[non_exhaustive]
pub enum Error {
/// The hotkey was already registered.
AlreadyRegistered,
/// The hotkey to unregister was not registered.
NotRegistered,
/// Failed fetching events from evdev.
EvDev,
/// Failed polling the event file descriptors.
EPoll,
/// Failed dynamically linking to X11.
NoXLib,
/// Failed opening a connection to the X11 server.
OpenXServerConnection,
/// The background thread stopped unexpectedly.
ThreadStopped,
}

impl std::error::Error for Error {}
impl From<Error> for crate::Error {
fn from(e: Error) -> Self {
Self::Platform(e)
}
}

impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::AlreadyRegistered => "The hotkey was already registered.",
Self::NotRegistered => "The hotkey to unregister was not registered.",
Self::EvDev => "Failed fetching events from evdev.",
Self::EPoll => "Failed polling the event file descriptors.",
Self::NoXLib => "Failed dynamically linking to X11.",
Expand All @@ -45,9 +37,6 @@ impl fmt::Display for Error {
}
}

/// The result type for this crate.
pub type Result<T> = std::result::Result<T, Error>;

enum Message {
Register(
Hotkey,
Expand All @@ -59,7 +48,6 @@ enum Message {
End,
}

/// A hook allows you to listen to hotkeys.
pub struct Hook {
sender: Sender<Message>,
waker: Waker,
Expand All @@ -83,16 +71,25 @@ fn can_use_evdev() -> Option<()> {
}

impl Hook {
/// Creates a new hook.
pub fn new() -> Result<Self> {
if can_use_evdev().is_some() {
evdev_impl::new()
pub fn new(consume: ConsumePreference) -> Result<Self> {
if matches!(consume, ConsumePreference::PreferConsume) {
if let Ok(x11) = x11_impl::new() {
return Ok(x11);
}
}

if !matches!(consume, ConsumePreference::MustConsume) && can_use_evdev().is_some() {
evdev_impl::new().map_err(Into::into)
} else if !matches!(
consume,
ConsumePreference::MustNotConsume | ConsumePreference::PreferConsume
) {
x11_impl::new().map_err(Into::into)
} else {
x11_impl::new()
Err(crate::Error::UnmatchedPreference)
}
}

/// Registers a hotkey to listen to.
pub fn register<F>(&self, hotkey: Hotkey, callback: F) -> Result<()>
where
F: FnMut() + Send + 'static,
Expand All @@ -108,7 +105,6 @@ impl Hook {
future.value().ok_or(Error::ThreadStopped)?
}

/// Unregisters a previously registered hotkey.
pub fn unregister(&self, hotkey: Hotkey) -> Result<()> {
let (future, promise) = future_promise();

Expand All @@ -121,7 +117,7 @@ impl Hook {
future.value().ok_or(Error::ThreadStopped)?
}

pub(crate) fn try_resolve(&self, key_code: KeyCode) -> Option<String> {
pub fn try_resolve(&self, key_code: KeyCode) -> Option<String> {
let (future, promise) = future_promise();

self.sender.send(Message::Resolve(key_code, promise)).ok()?;
Expand Down
15 changes: 7 additions & 8 deletions crates/livesplit-hotkey/src/linux/x11_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ use x11_dl::xlib::{
ShiftMask, XErrorEvent, XKeyEvent, Xlib, _XDisplay,
};

use super::Message;
use crate::{Error, Hook, KeyCode, Modifiers, Result};
use super::{Error, Hook, Message};
use crate::{KeyCode, Modifiers, Result};

unsafe fn ungrab_all(xlib: &Xlib, display: *mut Display) {
let screencount = (xlib.XScreenCount)(display);
Expand Down Expand Up @@ -81,7 +81,7 @@ pub fn new() -> Result<Hook> {

let display = (xlib.XOpenDisplay)(ptr::null());
if display.is_null() {
return Err(Error::OpenXServerConnection);
return Err(Error::OpenXServerConnection.into());
}

let fd = (xlib.XConnectionNumber)(display) as std::os::unix::io::RawFd;
Expand All @@ -103,8 +103,7 @@ pub fn new() -> Result<Hook> {

let join_handle = thread::spawn(move || -> Result<()> {
// Force the whole XData to be moved.
let xdata = xdata;
let XData(xlib, display) = xdata;
let XData(xlib, display) = { xdata };

let mut result = Ok(());
let mut events = Events::with_capacity(1024);
Expand Down Expand Up @@ -132,7 +131,7 @@ pub fn new() -> Result<Hook> {
})
.is_some()
{
Err(Error::AlreadyRegistered)
Err(crate::Error::AlreadyRegistered)
} else {
let keys = hotkeys.keys().copied().collect::<Vec<_>>();
grab_all(&xlib, display, &keys);
Expand All @@ -143,7 +142,7 @@ pub fn new() -> Result<Hook> {
Message::Unregister(key, promise) => {
let res = code_for(key.key_code)
.and_then(|k| hotkeys.remove(&(k, key.modifiers)).map(drop))
.ok_or(Error::NotRegistered);
.ok_or(crate::Error::NotRegistered);
if res.is_ok() {
let keys = hotkeys.keys().copied().collect::<Vec<_>>();
grab_all(&xlib, display, &keys);
Expand Down Expand Up @@ -199,7 +198,7 @@ pub fn new() -> Result<Hook> {

(xlib.XCloseDisplay)(display);

result
result.map_err(Into::into)
});

Ok(Hook {
Expand Down
Loading

0 comments on commit 72d9567

Please sign in to comment.