Skip to content

Commit

Permalink
Merge pull request #479 from CryZe/hotkey-docs
Browse files Browse the repository at this point in the history
Document the `livesplit-hotkey` crate
  • Loading branch information
CryZe authored Nov 16, 2021
2 parents 8d7bcaa + 4fb839e commit c5edf51
Show file tree
Hide file tree
Showing 8 changed files with 1,075 additions and 68 deletions.
1,046 changes: 988 additions & 58 deletions crates/livesplit-hotkey/src/key_code.rs

Large diffs are not rendered by default.

16 changes: 14 additions & 2 deletions crates/livesplit-hotkey/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
// For js! macro.
#![recursion_limit = "1024"]
#![warn(
clippy::complexity,
clippy::correctness,
clippy::perf,
clippy::style,
missing_docs,
rust_2018_idioms
)]
#![cfg_attr(not(feature = "std"), no_std)]

//! `livesplit-hotkey` is a crate that allows listening to hotkeys even when the
//! application is not in focus. The crate currently supports Windows, macOS,
//! Linux and the web via wasm-bindgen. On unsupported platforms the crate still
//! compiles but uses a stubbed out implementation instead that never receives
//! any hotkeys.

extern crate alloc;

cfg_if::cfg_if! {
Expand Down
21 changes: 18 additions & 3 deletions crates/livesplit-hotkey/src/linux/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,23 @@ use std::{
thread::{self, JoinHandle},
};

/// The error type for this crate.
#[derive(Debug, Copy, Clone, snafu::Snafu)]
#[non_exhaustive]
pub enum Error {
EPoll,
ThreadStopped,
/// 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,
/// The background thread stopped unexpectedly.
ThreadStopped,
}

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

enum Message {
Expand All @@ -33,6 +41,7 @@ enum Message {
// Low numbered tokens are allocated to devices.
const PING_TOKEN: Token = Token(usize::MAX);

/// A hook allows you to listen to hotkeys.
pub struct Hook {
sender: Sender<Message>,
waker: Waker,
Expand All @@ -49,14 +58,16 @@ impl Drop for Hook {
}
}

fn code_for(key: KeyCode) -> Option<Key> {
const fn code_for(key: KeyCode) -> Option<Key> {
// This mapping is based on all the different browsers. They however all use
// the X11 scan codes. Fortunately those have a trivial 1:1 mapping to evdev
// scan codes.
// https://github.com/freedesktop/xorg-xf86-input-evdev/blob/71036116be11b8c9d39ce153738875c44183cc60/src/evdev.c#L280
// You simply need to subtract 8 from the X11 scan code to get to the evdev
// scan code. So we take the mapping from the browsers, subtract 8 from each
// value and then use the named constant for that value.
// The USB HID to scan code translation in Linux is this table:
// https://github.com/torvalds/linux/blob/fe91c4725aeed35023ba4f7a1e1adfebb6878c23/drivers/hid/hid-input.c#L27-L44
use self::KeyCode::*;
Some(match key {
Escape => Key::KEY_ESC,
Expand Down Expand Up @@ -234,6 +245,7 @@ fn code_for(key: KeyCode) -> Option<Key> {
MailSend => Key::KEY_SEND, // Chrome only
MailReply => Key::KEY_REPLY, // Chrome only
MailForward => Key::KEY_FORWARDMAIL, // Chrome only
MicrophoneMuteToggle => Key::KEY_MICMUTE, // Chrome only
ZoomToggle => Key::KEY_ZOOM, // Chrome only
LaunchControlPanel => Key::KEY_CONTROLPANEL, // Chrome only
SelectTask => Key::KEY_APPSELECT, // Chrome only
Expand Down Expand Up @@ -272,6 +284,7 @@ fn code_for(key: KeyCode) -> Option<Key> {
}

impl Hook {
/// Creates a new hook.
pub fn new() -> Result<Self> {
let (sender, receiver) = channel();
let mut poll = Poll::new().map_err(|_| Error::EPoll)?;
Expand Down Expand Up @@ -353,6 +366,7 @@ impl Hook {
})
}

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

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

Expand Down
19 changes: 16 additions & 3 deletions crates/livesplit-hotkey/src/macos/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,25 @@ use std::{
thread,
};

/// The error type for this crate.
#[derive(Debug, snafu::Snafu)]
#[non_exhaustive]
pub enum Error {
/// The hotkey was already registered.
AlreadyRegistered,
/// The hotkey to unregister was not registered.
NotRegistered,
/// Failed creating the event tap.
CouldntCreateEventTap,
/// Failed creating the run loop source.
CouldntCreateRunLoopSource,
/// Failed getting the current run loop.
CouldntGetCurrentRunLoop,
/// The background thread stopped unexpectedly.
ThreadStoppedUnexpectedly,
}

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

struct Owned<T>(*mut T);
Expand All @@ -57,6 +66,7 @@ unsafe impl Send for RunLoop {}

type RegisteredKeys = Mutex<HashMap<KeyCode, Box<dyn FnMut() + Send + 'static>>>;

/// A hook allows you to listen to hotkeys.
pub struct Hook {
event_loop: RunLoop,
hotkeys: Arc<RegisteredKeys>,
Expand All @@ -75,6 +85,7 @@ impl Drop for Hook {
}

impl Hook {
/// Creates a new hook.
pub fn new() -> Result<Self> {
let hotkeys = Arc::new(Mutex::new(HashMap::new()));
let thread_hotkeys = hotkeys.clone();
Expand Down Expand Up @@ -135,6 +146,7 @@ impl Hook {
})
}

/// Registers a hotkey to listen to.
pub fn register<F>(&self, hotkey: KeyCode, callback: F) -> Result<()>
where
F: FnMut() + Send + 'static,
Expand All @@ -147,6 +159,7 @@ impl Hook {
}
}

/// Unregisters a previously registered hotkey.
pub fn unregister(&self, hotkey: KeyCode) -> Result<()> {
if self.hotkeys.lock().remove(&hotkey).is_some() {
Ok(())
Expand Down Expand Up @@ -281,8 +294,9 @@ unsafe extern "C" fn callback(
0x6E => KeyCode::ContextMenu, // Missing on MDN
0x6F => KeyCode::F12,
0x71 => KeyCode::F15,
// `Help` sometimes replaces the `Insert` key on mac keyboards, Chrome prefers `Insert`.
0x72 => KeyCode::Help,
// Apple hasn't been producing any keyboards with `Help` anymore since
// 2007. So this can be considered Insert instead.
0x72 => KeyCode::Insert,
0x73 => KeyCode::Home,
0x74 => KeyCode::PageUp,
0x75 => KeyCode::Delete,
Expand Down Expand Up @@ -337,7 +351,6 @@ pub(crate) fn try_resolve(key_code: KeyCode) -> Option<String> {
let key_code = match key_code {
KeyCode::Backquote => 0x32,
KeyCode::Backslash => 0x2A,
KeyCode::Backspace => 0x33,
KeyCode::BracketLeft => 0x21,
KeyCode::BracketRight => 0x1E,
KeyCode::Comma => 0x2B,
Expand Down
7 changes: 7 additions & 0 deletions crates/livesplit-hotkey/src/other/mod.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,32 @@
use crate::KeyCode;
use alloc::string::String;

/// The error type for this crate.
#[derive(Debug, snafu::Snafu)]
#[non_exhaustive]
pub enum Error {}

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

/// A hook allows you to listen to hotkeys.
pub struct Hook;

impl Hook {
/// Creates a new hook.
pub fn new() -> Result<Self> {
Ok(Hook)
}

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

/// Unregisters a previously registered hotkey.
pub fn unregister(&self, _: KeyCode) -> Result<()> {
Ok(())
}
Expand Down
9 changes: 9 additions & 0 deletions crates/livesplit-hotkey/src/wasm_unknown/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,22 @@ use std::{
sync::{Arc, Mutex},
};

/// The error type for this crate.
#[derive(Debug, snafu::Snafu)]
#[non_exhaustive]
pub enum Error {
/// The hotkey was already registered.
AlreadyRegistered,
/// The hotkey to unregister was not registered.
NotRegistered,
}

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

pub type EventListenerHandle = Box<dyn Fn(&str)>;

/// A hook allows you to listen to hotkeys.
pub struct Hook {
hotkeys: Arc<Mutex<HashMap<KeyCode, Box<dyn FnMut() + Send + 'static>>>>,
event: Option<Box<EventListenerHandle>>,
Expand Down Expand Up @@ -46,6 +52,7 @@ pub unsafe extern "C" fn HotkeyHook_callback(
}

impl Hook {
/// Creates a new hook.
pub fn new() -> Result<Self> {
let hotkeys = Arc::new(Mutex::new(HashMap::<
KeyCode,
Expand All @@ -71,6 +78,7 @@ impl Hook {
})
}

/// Registers a hotkey to listen to.
pub fn register<F>(&self, hotkey: KeyCode, callback: F) -> Result<()>
where
F: FnMut() + Send + 'static,
Expand All @@ -83,6 +91,7 @@ impl Hook {
}
}

/// Unregisters a previously registered hotkey.
pub fn unregister(&self, hotkey: KeyCode) -> Result<()> {
if self.hotkeys.lock().unwrap().remove(&hotkey).is_some() {
Ok(())
Expand Down
10 changes: 10 additions & 0 deletions crates/livesplit-hotkey/src/wasm_web/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,22 @@ use std::{
sync::{Arc, Mutex},
};

/// The error type for this crate.
#[derive(Debug, snafu::Snafu)]
#[non_exhaustive]
pub enum Error {
/// The hotkey was already registered.
AlreadyRegistered,
/// The hotkey to unregister was not registered.
NotRegistered,
/// Failed creating the hook.
FailedToCreateHook,
}

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

/// A hook allows you to listen to hotkeys.
pub struct Hook {
hotkeys: Arc<Mutex<HashMap<KeyCode, Box<dyn FnMut() + Send + 'static>>>>,
keyboard_callback: Closure<dyn FnMut(KeyboardEvent)>,
Expand Down Expand Up @@ -63,6 +70,7 @@ static GAMEPAD_BUTTONS: [KeyCode; TOTAL_BUTTONS] = [
];

impl Hook {
/// Creates a new hook.
pub fn new() -> Result<Self> {
let hotkeys = Arc::new(Mutex::new(HashMap::<
KeyCode,
Expand Down Expand Up @@ -130,6 +138,7 @@ impl Hook {
})
}

/// Registers a hotkey to listen to.
pub fn register<F>(&self, hotkey: KeyCode, callback: F) -> Result<()>
where
F: FnMut() + Send + 'static,
Expand All @@ -152,6 +161,7 @@ impl Hook {
}
}

/// Unregisters a previously registered hotkey.
pub fn unregister(&self, hotkey: KeyCode) -> Result<()> {
if self.hotkeys.lock().unwrap().remove(&hotkey).is_some() {
Ok(())
Expand Down
15 changes: 13 additions & 2 deletions crates/livesplit-hotkey/src/windows/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,26 @@ use winapi::{

const MSG_EXIT: UINT = 0x400;

/// The error type for this crate.
#[derive(Debug, snafu::Snafu)]
#[non_exhaustive]
pub enum Error {
/// The hotkey was already registered.
AlreadyRegistered,
/// The hotkey to unregister was not registered.
NotRegistered,
/// An error in the Windows API occurred.
WindowsHook,
/// The background thread stopped unexpectedly.
ThreadStopped,
/// An error occurred in the message loop.
MessageLoop,
}

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

/// A hook allows you to listen to hotkeys.
pub struct Hook {
thread_id: DWORD,
hotkeys: Arc<Mutex<HashMap<KeyCode, Box<dyn FnMut() + Send + 'static>>>>,
Expand All @@ -62,7 +71,7 @@ thread_local! {
static STATE: RefCell<Option<State>> = RefCell::new(None);
}

fn parse_scan_code(value: DWORD) -> Option<KeyCode> {
const fn parse_scan_code(value: DWORD) -> Option<KeyCode> {
// Windows uses PS/2 scan code set 1.
// https://www.avrfreaks.net/sites/default/files/PS2%20Keyboard.pdf Page 19
use self::KeyCode::*;
Expand Down Expand Up @@ -256,6 +265,7 @@ unsafe extern "system" fn callback_proc(code: c_int, wparam: WPARAM, lparam: LPA
}

impl Hook {
/// Creates a new hook.
pub fn new() -> Result<Self> {
let hotkeys = Arc::new(Mutex::new(HashMap::<
KeyCode,
Expand Down Expand Up @@ -329,6 +339,7 @@ impl Hook {
Ok(Hook { thread_id, hotkeys })
}

/// Registers a hotkey to listen to.
pub fn register<F>(&self, hotkey: KeyCode, callback: F) -> Result<()>
where
F: FnMut() + Send + 'static,
Expand All @@ -341,6 +352,7 @@ impl Hook {
}
}

/// Unregisters a previously registered hotkey.
pub fn unregister(&self, hotkey: KeyCode) -> Result<()> {
if self.hotkeys.lock().remove(&hotkey).is_some() {
Ok(())
Expand All @@ -355,7 +367,6 @@ pub(crate) fn try_resolve(key_code: KeyCode) -> Option<String> {
let scan_code = match key_code {
Backquote => 0x0029,
Backslash => 0x002B,
Backspace => 0x000E,
BracketLeft => 0x001A,
BracketRight => 0x001B,
Comma => 0x0033,
Expand Down

0 comments on commit c5edf51

Please sign in to comment.