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

Document the livesplit-hotkey crate #479

Merged
merged 1 commit into from
Nov 16, 2021
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
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