Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow hotkeys to be consumed #757

Merged
merged 1 commit into from
Jan 16, 2024
Merged
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
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
87 changes: 86 additions & 1 deletion crates/livesplit-hotkey/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,92 @@ 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),
}

// FIXME: Impl core::error::Error once it's stable.
#[cfg(feature = "std")]
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
Loading