Skip to main content

handy_keys/
listener.rs

1//! Keyboard listener for streaming raw key events
2//!
3//! This module provides a `KeyboardListener` that streams all keyboard events,
4//! useful for implementing "record hotkey" UI flows.
5//!
6//! # Platform Notes
7//!
8//! - **macOS**: Uses CGEventTap. Requires accessibility permissions.
9//! - **Windows**: Uses low-level keyboard hooks. Clean thread shutdown.
10//! - **Linux**: Uses rdev. On Wayland, blocking may not work due to
11//!   compositor restrictions. Thread cleanup is limited.
12
13use std::sync::atomic::{AtomicBool, Ordering};
14use std::sync::mpsc::{Receiver, TryRecvError};
15use std::sync::Arc;
16use std::thread::JoinHandle;
17use std::time::Duration;
18
19use crate::error::{Error, Result};
20use crate::types::KeyEvent;
21
22pub use crate::platform::state::BlockingHotkeys;
23
24/// Platform-agnostic Keyboard Listener
25///
26/// Streams all keyboard events. Can optionally block events that match
27/// registered hotkeys.
28pub struct KeyboardListener {
29    event_receiver: Receiver<KeyEvent>,
30    _thread_handle: Option<JoinHandle<()>>,
31    running: Arc<AtomicBool>,
32    blocking_hotkeys: Option<BlockingHotkeys>,
33}
34
35impl KeyboardListener {
36    /// Create a new KeyboardListener (non-blocking mode)
37    ///
38    /// Events are observed but not blocked. Use this for "record hotkey" UI flows.
39    ///
40    /// On macOS, this will check for accessibility permissions and fail if not granted.
41    pub fn new() -> Result<Self> {
42        Self::new_internal(None)
43    }
44
45    /// Create a new KeyboardListener with blocking support
46    ///
47    /// Events matching hotkeys in the provided set will be blocked from reaching
48    /// other applications. The set can be modified after creation to add/remove
49    /// hotkeys dynamically.
50    ///
51    /// Note: On Wayland, blocking may not work due to compositor restrictions.
52    pub fn new_with_blocking(blocking_hotkeys: BlockingHotkeys) -> Result<Self> {
53        Self::new_internal(Some(blocking_hotkeys))
54    }
55
56    fn new_internal(blocking_hotkeys: Option<BlockingHotkeys>) -> Result<Self> {
57        #[cfg(target_os = "macos")]
58        {
59            use crate::platform::macos::listener;
60            let state = listener::spawn(blocking_hotkeys)?;
61            Ok(KeyboardListener {
62                event_receiver: state.event_receiver,
63                _thread_handle: state.thread_handle,
64                running: state.running,
65                blocking_hotkeys: state.blocking_hotkeys,
66            })
67        }
68
69        #[cfg(target_os = "windows")]
70        {
71            use crate::platform::windows::listener;
72            let state = listener::spawn(blocking_hotkeys)?;
73            Ok(KeyboardListener {
74                event_receiver: state.event_receiver,
75                _thread_handle: state.thread_handle,
76                running: state.running,
77                blocking_hotkeys: state.blocking_hotkeys,
78            })
79        }
80
81        #[cfg(target_os = "linux")]
82        {
83            use crate::platform::linux::listener;
84            let state = listener::spawn(blocking_hotkeys)?;
85            Ok(KeyboardListener {
86                event_receiver: state.event_receiver,
87                _thread_handle: state.thread_handle,
88                running: state.running,
89                blocking_hotkeys: state.blocking_hotkeys,
90            })
91        }
92    }
93
94    /// Get a reference to the blocking hotkeys set (if blocking is enabled)
95    pub fn blocking_hotkeys(&self) -> Option<&BlockingHotkeys> {
96        self.blocking_hotkeys.as_ref()
97    }
98
99    /// Blocking receive for key events
100    ///
101    /// Blocks until a key event is received or the listener stops.
102    pub fn recv(&self) -> Result<KeyEvent> {
103        self.event_receiver
104            .recv()
105            .map_err(|_| Error::EventLoopNotRunning)
106    }
107
108    /// Blocking receive with timeout
109    ///
110    /// Blocks until a key event is received, the timeout expires, or the listener stops.
111    pub fn recv_timeout(&self, timeout: Duration) -> Result<KeyEvent> {
112        self.event_receiver
113            .recv_timeout(timeout)
114            .map_err(|e| match e {
115                std::sync::mpsc::RecvTimeoutError::Timeout => Error::Timeout,
116                std::sync::mpsc::RecvTimeoutError::Disconnected => Error::EventLoopNotRunning,
117            })
118    }
119
120    /// Non-blocking receive for key events
121    ///
122    /// Returns `Some(event)` if an event is available, `None` otherwise.
123    pub fn try_recv(&self) -> Option<KeyEvent> {
124        match self.event_receiver.try_recv() {
125            Ok(event) => Some(event),
126            Err(TryRecvError::Empty) => None,
127            Err(TryRecvError::Disconnected) => None,
128        }
129    }
130}
131
132impl Drop for KeyboardListener {
133    fn drop(&mut self) {
134        self.running.store(false, Ordering::SeqCst);
135
136        // On macOS and Windows, we can join the thread for clean shutdown.
137        // On Linux (rdev), the thread continues running but becomes idle
138        // because rdev::grab() blocks indefinitely.
139        #[cfg(any(target_os = "macos", target_os = "windows"))]
140        if let Some(handle) = self._thread_handle.take() {
141            let _ = handle.join();
142        }
143    }
144}