livesplit_hotkey/
lib.rs

1#![warn(
2    clippy::complexity,
3    clippy::correctness,
4    clippy::perf,
5    clippy::style,
6    missing_docs,
7    rust_2018_idioms
8)]
9#![forbid(clippy::incompatible_msrv)]
10#![cfg_attr(not(feature = "std"), no_std)]
11
12//! `livesplit-hotkey` is a crate that allows listening to hotkeys even when the
13//! application is not in focus. The crate currently supports Windows, macOS,
14//! Linux and the web via wasm-bindgen. On unsupported platforms the crate still
15//! compiles but uses a stubbed out implementation instead that never receives
16//! any hotkeys.
17
18extern crate alloc;
19
20cfg_if::cfg_if! {
21    if #[cfg(not(feature = "std"))] {
22        mod other;
23        use self::other as platform;
24    } else if #[cfg(windows)] {
25        mod windows;
26        use self::windows as platform;
27    } else if #[cfg(target_os = "linux")] {
28        mod linux;
29        use self::linux as platform;
30    } else if #[cfg(target_os = "macos")] {
31        mod macos;
32        use self::macos as platform;
33    } else if #[cfg(all(target_family = "wasm", target_os = "unknown", feature = "wasm-web"))] {
34        mod wasm_web;
35        use self::wasm_web as platform;
36    } else {
37        mod other;
38        use self::other as platform;
39    }
40}
41
42mod hotkey;
43mod key_code;
44mod modifiers;
45use core::fmt;
46
47pub use self::{hotkey::*, key_code::*, modifiers::*};
48
49/// A hook allows you to listen to hotkeys.
50#[repr(transparent)]
51pub struct Hook(platform::Hook);
52
53/// The preference of whether the hotkeys should be consumed or not. Consuming a
54/// hotkey means that the hotkey won't be passed on to the application that is
55/// currently in focus.
56#[derive(Copy, Clone, PartialEq, Eq, Hash)]
57pub enum ConsumePreference {
58    /// There is no preference, the crate chooses the most suitable implementation.
59    NoPreference,
60    /// Prefers the hotkeys to be consumed, but does not require it.
61    PreferConsume,
62    /// Prefers the hotkeys to not be consumed, but does not require it.
63    PreferNoConsume,
64    /// Requires the hotkeys to be consumed, the [`Hook`] won't be created otherwise.
65    MustConsume,
66    /// Requires the hotkeys to not be consumed, the [`Hook`] won't be created
67    /// otherwise.
68    MustNotConsume,
69}
70
71impl Hook {
72    /// Creates a new hook without any preference of whether the hotkeys should
73    /// be consumed or not.
74    pub fn new() -> Result<Self> {
75        Ok(Self(platform::Hook::new(ConsumePreference::NoPreference)?))
76    }
77
78    /// Creates a new hook with a specific preference of whether the hotkeys
79    /// should be consumed or not.
80    pub fn with_consume_preference(consume: ConsumePreference) -> Result<Self> {
81        Ok(Self(platform::Hook::new(consume)?))
82    }
83
84    /// Registers a hotkey to listen to.
85    pub fn register<F>(&self, hotkey: Hotkey, callback: F) -> Result<()>
86    where
87        F: FnMut() + Send + 'static,
88    {
89        self.0.register(hotkey, callback)
90    }
91
92    /// Unregisters a previously registered hotkey.
93    pub fn unregister(&self, hotkey: Hotkey) -> Result<()> {
94        self.0.unregister(hotkey)
95    }
96}
97
98/// The result type for this crate.
99pub type Result<T> = core::result::Result<T, Error>;
100
101/// The error type for this crate.
102#[derive(Debug)]
103#[non_exhaustive]
104pub enum Error {
105    /// The consume preference could not be met on the current platform.
106    UnmatchedPreference,
107    /// The hotkey was already registered.
108    AlreadyRegistered,
109    /// The hotkey to unregister was not registered.
110    NotRegistered,
111    /// A platform specific error occurred.
112    Platform(platform::Error),
113}
114
115// FIXME: Impl core::error::Error once it's stable.
116#[cfg(feature = "std")]
117impl std::error::Error for Error {}
118
119impl fmt::Display for Error {
120    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
121        f.write_str(match self {
122            Self::UnmatchedPreference => {
123                "The consume preference could not be met on the current platform."
124            }
125            Self::AlreadyRegistered => "The hotkey was already registered.",
126            Self::NotRegistered => "The hotkey to unregister was not registered.",
127            Self::Platform(e) => return fmt::Display::fmt(e, f),
128        })
129    }
130}
131
132#[cfg(not(all(target_family = "wasm", target_os = "unknown", feature = "wasm-web")))]
133const _: () = {
134    #[allow(unused)]
135    const fn assert_thread_safe<T: Send + Sync>() {}
136    assert_thread_safe::<Hook>();
137};
138
139#[cfg(test)]
140mod tests {
141    use std::{thread, time::Duration};
142
143    use super::*;
144
145    #[test]
146    fn test() {
147        let hook = Hook::new().unwrap();
148
149        hook.register(KeyCode::Numpad1.with_modifiers(Modifiers::SHIFT), || {
150            println!("A")
151        })
152        .unwrap();
153        println!("Press Shift + Numpad1");
154        thread::sleep(Duration::from_secs(5));
155        hook.unregister(KeyCode::Numpad1.with_modifiers(Modifiers::SHIFT))
156            .unwrap();
157
158        hook.register(KeyCode::KeyN.into(), || println!("B"))
159            .unwrap();
160        println!("Press KeyN");
161        thread::sleep(Duration::from_secs(5));
162        hook.unregister(KeyCode::KeyN.into()).unwrap();
163
164        hook.register(KeyCode::Numpad1.into(), || println!("C"))
165            .unwrap();
166        println!("Press Numpad1");
167        thread::sleep(Duration::from_secs(5));
168        hook.unregister(KeyCode::Numpad1.into()).unwrap();
169    }
170
171    #[test]
172    fn resolve() {
173        let hook = Hook::new().unwrap();
174
175        // Based on German keyboard layout.
176        println!("ß: {}", KeyCode::Minus.resolve(&hook));
177        println!("ü: {}", KeyCode::BracketLeft.resolve(&hook));
178        println!("#: {}", KeyCode::Backslash.resolve(&hook));
179        println!("+: {}", KeyCode::BracketRight.resolve(&hook));
180        println!("z: {}", KeyCode::KeyY.resolve(&hook));
181        println!("^: {}", KeyCode::Backquote.resolve(&hook));
182        println!("<: {}", KeyCode::IntlBackslash.resolve(&hook));
183        println!("Yen: {}", KeyCode::IntlYen.resolve(&hook));
184        println!("Enter: {}", KeyCode::Enter.resolve(&hook));
185        println!("Space: {}", KeyCode::Space.resolve(&hook));
186        println!("Tab: {}", KeyCode::Tab.resolve(&hook));
187        println!("Numpad0: {}", KeyCode::Numpad0.resolve(&hook));
188    }
189}