Skip to main content

kbd_global/
manager.rs

1//! [`HotkeyManager`] — the public API entry point.
2//!
3//! Thin. Sends commands to the engine, returns handles. Does not own
4//! mutable state — the engine owns everything.
5//!
6//! # Architecture
7//!
8//! The manager holds a command channel sender and a wake mechanism.
9//! Every public method translates to a `Command` sent to the engine.
10//! Operations that can fail (register, `define_layer`) use a reply
11//! channel to return `Result` synchronously to the caller.
12//!
13//! ```text
14//! HotkeyManager::register()
15//!   → sends Command::Register { id, binding, reply_tx }
16//!   → engine processes command, sends Result back on reply_tx
17//!   → manager returns BindingGuard or Error to caller
18//! ```
19
20use std::fmt;
21use std::sync::Mutex;
22use std::sync::mpsc;
23
24use kbd::action::Action;
25use kbd::binding::BindingId;
26use kbd::binding::BindingOptions;
27use kbd::binding::RegisteredBinding;
28use kbd::hotkey::Hotkey;
29use kbd::hotkey::Modifier;
30use kbd::introspection::ActiveLayerInfo;
31use kbd::introspection::BindingInfo;
32use kbd::introspection::ConflictInfo;
33use kbd::key::Key;
34use kbd::layer::Layer;
35use kbd::layer::LayerName;
36
37use crate::Error;
38use crate::backend::Backend;
39use crate::binding_guard::BindingGuard;
40use crate::engine::Command;
41use crate::engine::CommandSender;
42use crate::engine::EngineRuntime;
43use crate::engine::GrabState;
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46enum BackendSelection {
47    Auto,
48    Explicit(Backend),
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52enum GrabConfiguration {
53    Disabled,
54    Enabled,
55}
56
57/// Builder for explicit backend and runtime options.
58#[derive(Debug)]
59pub struct HotkeyManagerBuilder {
60    backend: BackendSelection,
61    grab: GrabConfiguration,
62}
63
64impl Default for HotkeyManagerBuilder {
65    fn default() -> Self {
66        Self {
67            backend: BackendSelection::Auto,
68            grab: GrabConfiguration::Disabled,
69        }
70    }
71}
72
73impl HotkeyManagerBuilder {
74    /// Force a specific backend instead of auto-detection.
75    #[must_use]
76    pub fn backend(mut self, backend: Backend) -> Self {
77        self.backend = BackendSelection::Explicit(backend);
78        self
79    }
80
81    /// Enable grab mode for exclusive device capture.
82    #[must_use]
83    pub fn grab(mut self) -> Self {
84        self.grab = GrabConfiguration::Enabled;
85        self
86    }
87
88    /// Build and start a new manager instance.
89    ///
90    /// Spawns the engine thread and begins listening for input device events.
91    ///
92    /// # Errors
93    ///
94    /// Returns an error if the backend cannot be initialized, grab mode
95    /// is requested without the `grab` feature, or the engine thread
96    /// fails to start.
97    pub fn build(self) -> Result<HotkeyManager, Error> {
98        let backend = resolve_backend(self.backend)?;
99        validate_grab_configuration(backend, self.grab)?;
100
101        let grab_state = create_grab_state(self.grab)?;
102        let runtime = EngineRuntime::spawn(grab_state)?;
103        let commands = runtime.commands();
104
105        Ok(HotkeyManager {
106            backend,
107            commands,
108            runtime: Mutex::new(Some(runtime)),
109        })
110    }
111}
112
113/// Public manager API.
114pub struct HotkeyManager {
115    backend: Backend,
116    commands: CommandSender,
117    runtime: Mutex<Option<EngineRuntime>>,
118}
119
120impl fmt::Debug for HotkeyManager {
121    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122        let running = self
123            .runtime
124            .lock()
125            .map(|guard| guard.is_some())
126            .unwrap_or(false);
127
128        f.debug_struct("HotkeyManager")
129            .field("backend", &self.backend)
130            .field("running", &running)
131            .finish_non_exhaustive()
132    }
133}
134
135impl HotkeyManager {
136    /// Create a manager with backend auto-detection.
137    ///
138    /// # Errors
139    ///
140    /// Returns an error if the backend cannot be initialized or input
141    /// devices are not accessible.
142    pub fn new() -> Result<Self, Error> {
143        Self::builder().build()
144    }
145
146    /// Configure manager startup options.
147    #[must_use]
148    pub fn builder() -> HotkeyManagerBuilder {
149        HotkeyManagerBuilder::default()
150    }
151
152    /// Returns the backend this manager is using.
153    #[must_use]
154    pub const fn active_backend(&self) -> Backend {
155        self.backend
156    }
157
158    /// Register a simple hotkey callback.
159    ///
160    /// # Errors
161    ///
162    /// Returns [`Error::AlreadyRegistered`] if the hotkey is already bound,
163    /// or [`Error::ManagerStopped`] if the engine has shut down.
164    pub fn register<F>(&self, hotkey: impl Into<Hotkey>, callback: F) -> Result<BindingGuard, Error>
165    where
166        F: Fn() + Send + Sync + 'static,
167    {
168        self.register_action(hotkey.into(), Action::from(callback))
169    }
170
171    /// Register a hotkey with an explicit action and binding options.
172    ///
173    /// Use when you need metadata (description, overlay visibility) or
174    /// behavioral options beyond what [`register()`](Self::register) provides.
175    ///
176    /// # Errors
177    ///
178    /// Returns [`Error::AlreadyRegistered`] if the hotkey is already bound,
179    /// or [`Error::ManagerStopped`] if the engine has shut down.
180    pub fn register_with_options(
181        &self,
182        hotkey: impl Into<Hotkey>,
183        action: impl Into<Action>,
184        options: BindingOptions,
185    ) -> Result<BindingGuard, Error> {
186        let id = BindingId::new();
187        let binding =
188            RegisteredBinding::new(id, hotkey.into(), action.into()).with_options(options);
189        let (reply_tx, reply_rx) = mpsc::channel();
190
191        self.commands.send(Command::Register {
192            binding,
193            reply: reply_tx,
194        })?;
195
196        match reply_rx.recv().map_err(|_| Error::ManagerStopped)? {
197            Ok(()) => Ok(BindingGuard::new(id, self.commands.clone())),
198            Err(error) => Err(error),
199        }
200    }
201
202    /// Query whether a hotkey is currently registered.
203    ///
204    /// # Errors
205    ///
206    /// Returns [`Error::ManagerStopped`] if the engine has shut down.
207    pub fn is_registered(&self, hotkey: impl Into<Hotkey>) -> Result<bool, Error> {
208        let hotkey = hotkey.into();
209        let (reply_tx, reply_rx) = mpsc::channel();
210
211        self.commands.send(Command::IsRegistered {
212            hotkey,
213            reply: reply_tx,
214        })?;
215
216        reply_rx.recv().map_err(|_| Error::ManagerStopped)
217    }
218
219    /// Query whether a specific key is currently pressed on any device.
220    ///
221    /// # Errors
222    ///
223    /// Returns [`Error::ManagerStopped`] if the engine has shut down.
224    pub fn is_key_pressed(&self, key: Key) -> Result<bool, Error> {
225        let (reply_tx, reply_rx) = mpsc::channel();
226
227        self.commands.send(Command::IsKeyPressed {
228            key,
229            reply: reply_tx,
230        })?;
231
232        reply_rx.recv().map_err(|_| Error::ManagerStopped)
233    }
234
235    /// Query the set of modifiers currently held, derived from key state.
236    ///
237    /// Left/right variants are canonicalized: if either `LeftCtrl` or `RightCtrl`
238    /// is held, `Modifier::Ctrl` is in the returned set.
239    ///
240    /// # Errors
241    ///
242    /// Returns [`Error::ManagerStopped`] if the engine has shut down.
243    pub fn active_modifiers(&self) -> Result<Vec<Modifier>, Error> {
244        let (reply_tx, reply_rx) = mpsc::channel();
245
246        self.commands
247            .send(Command::ActiveModifiers { reply: reply_tx })?;
248
249        reply_rx.recv().map_err(|_| Error::ManagerStopped)
250    }
251
252    /// Define a named layer.
253    ///
254    /// Sends the layer definition to the engine for storage. The layer
255    /// is not active until explicitly pushed via [`push_layer()`](Self::push_layer).
256    ///
257    /// # Errors
258    ///
259    /// Returns [`Error::LayerAlreadyDefined`] if a layer with the same
260    /// name has already been defined, or [`Error::ManagerStopped`] if
261    /// the engine has shut down.
262    pub fn define_layer(&self, layer: Layer) -> Result<(), Error> {
263        let (reply_tx, reply_rx) = mpsc::channel();
264
265        self.commands.send(Command::DefineLayer {
266            layer,
267            reply: reply_tx,
268        })?;
269
270        reply_rx.recv().map_err(|_| Error::ManagerStopped)?
271    }
272
273    /// Stop the manager and join the engine thread.
274    ///
275    /// All registered bindings are dropped. This is also called
276    /// automatically when the manager is dropped.
277    ///
278    /// # Errors
279    ///
280    /// Returns [`Error::EngineError`] if the engine thread panicked.
281    pub fn shutdown(self) -> Result<(), Error> {
282        self.shutdown_inner()
283    }
284
285    fn register_action(&self, hotkey: Hotkey, action: Action) -> Result<BindingGuard, Error> {
286        self.register_with_options(hotkey, action, BindingOptions::default())
287    }
288
289    fn shutdown_inner(&self) -> Result<(), Error> {
290        let mut runtime = self.runtime.lock().map_err(|_| Error::EngineError)?;
291        if let Some(runtime) = runtime.take() {
292            return runtime.shutdown();
293        }
294
295        Ok(())
296    }
297
298    /// Push a named layer onto the layer stack.
299    ///
300    /// The layer must have been previously defined via [`define_layer`](Self::define_layer).
301    /// The pushed layer becomes the highest-priority layer for matching.
302    ///
303    /// # Errors
304    ///
305    /// Returns [`Error::LayerNotDefined`] if no layer with the given name exists,
306    /// or [`Error::ManagerStopped`] if the engine has shut down.
307    pub fn push_layer(&self, name: impl Into<LayerName>) -> Result<(), Error> {
308        let (reply_tx, reply_rx) = mpsc::channel();
309
310        self.commands.send(Command::PushLayer {
311            name: name.into(),
312            reply: reply_tx,
313        })?;
314
315        reply_rx.recv().map_err(|_| Error::ManagerStopped)?
316    }
317
318    /// Pop the topmost layer from the layer stack.
319    ///
320    /// Returns the name of the popped layer.
321    ///
322    /// # Errors
323    ///
324    /// Returns [`Error::EmptyLayerStack`] if no layers are active,
325    /// or [`Error::ManagerStopped`] if the engine has shut down.
326    pub fn pop_layer(&self) -> Result<LayerName, Error> {
327        let (reply_tx, reply_rx) = mpsc::channel();
328
329        self.commands.send(Command::PopLayer { reply: reply_tx })?;
330
331        reply_rx.recv().map_err(|_| Error::ManagerStopped)?
332    }
333
334    /// Toggle a named layer on or off.
335    ///
336    /// If the layer is currently in the stack, it is removed.
337    /// If the layer is not in the stack, it is pushed.
338    ///
339    /// # Errors
340    ///
341    /// Returns [`Error::LayerNotDefined`] if no layer with the given name exists,
342    /// or [`Error::ManagerStopped`] if the engine has shut down.
343    pub fn toggle_layer(&self, name: impl Into<LayerName>) -> Result<(), Error> {
344        let (reply_tx, reply_rx) = mpsc::channel();
345
346        self.commands.send(Command::ToggleLayer {
347            name: name.into(),
348            reply: reply_tx,
349        })?;
350
351        reply_rx.recv().map_err(|_| Error::ManagerStopped)?
352    }
353
354    /// List all registered bindings with current shadowed status.
355    ///
356    /// Returns global bindings and all layer bindings (active or not).
357    /// Each entry includes whether the binding is currently reachable
358    /// or shadowed by a higher-priority layer.
359    ///
360    /// # Errors
361    ///
362    /// Returns [`Error::ManagerStopped`] if the engine has shut down.
363    pub fn list_bindings(&self) -> Result<Vec<BindingInfo>, Error> {
364        let (reply_tx, reply_rx) = mpsc::channel();
365
366        self.commands
367            .send(Command::ListBindings { reply: reply_tx })?;
368
369        reply_rx.recv().map_err(|_| Error::ManagerStopped)
370    }
371
372    /// Query what would fire if the given hotkey were pressed now.
373    ///
374    /// Considers the current layer stack. Returns `None` if no binding
375    /// matches the hotkey.
376    ///
377    /// # Errors
378    ///
379    /// Returns [`Error::ManagerStopped`] if the engine has shut down.
380    pub fn bindings_for_key(
381        &self,
382        hotkey: impl Into<Hotkey>,
383    ) -> Result<Option<BindingInfo>, Error> {
384        let (reply_tx, reply_rx) = mpsc::channel();
385
386        self.commands.send(Command::BindingsForKey {
387            hotkey: hotkey.into(),
388            reply: reply_tx,
389        })?;
390
391        reply_rx.recv().map_err(|_| Error::ManagerStopped)
392    }
393
394    /// Query the current layer stack.
395    ///
396    /// Returns layers in stack order (bottom to top).
397    ///
398    /// # Errors
399    ///
400    /// Returns [`Error::ManagerStopped`] if the engine has shut down.
401    pub fn active_layers(&self) -> Result<Vec<ActiveLayerInfo>, Error> {
402        let (reply_tx, reply_rx) = mpsc::channel();
403
404        self.commands
405            .send(Command::ActiveLayers { reply: reply_tx })?;
406
407        reply_rx.recv().map_err(|_| Error::ManagerStopped)
408    }
409
410    // TODO: register_sequence() — multi-step hotkey
411    // TODO: register_tap_hold() — dual-function key
412
413    /// Find bindings that are shadowed by higher-priority layers.
414    ///
415    /// Returns conflict pairs: each entry shows the shadowed binding
416    /// and the binding that shadows it.
417    ///
418    /// # Errors
419    ///
420    /// Returns [`Error::ManagerStopped`] if the engine has shut down.
421    pub fn conflicts(&self) -> Result<Vec<ConflictInfo>, Error> {
422        let (reply_tx, reply_rx) = mpsc::channel();
423
424        self.commands.send(Command::Conflicts { reply: reply_tx })?;
425
426        reply_rx.recv().map_err(|_| Error::ManagerStopped)
427    }
428}
429
430impl Drop for HotkeyManager {
431    fn drop(&mut self) {
432        let _ = self.shutdown_inner();
433    }
434}
435
436fn resolve_backend(selection: BackendSelection) -> Result<Backend, Error> {
437    match selection {
438        BackendSelection::Auto => Ok(Backend::Evdev),
439        BackendSelection::Explicit(backend) => validate_explicit_backend(backend),
440    }
441}
442
443#[allow(clippy::unnecessary_wraps)]
444fn validate_explicit_backend(backend: Backend) -> Result<Backend, Error> {
445    match backend {
446        Backend::Evdev => Ok(Backend::Evdev),
447    }
448}
449
450#[allow(clippy::unnecessary_wraps)]
451fn validate_grab_configuration(_backend: Backend, _grab: GrabConfiguration) -> Result<(), Error> {
452    Ok(())
453}
454
455fn create_grab_state(grab: GrabConfiguration) -> Result<GrabState, Error> {
456    match grab {
457        GrabConfiguration::Disabled => Ok(GrabState::Disabled),
458        GrabConfiguration::Enabled => {
459            #[cfg(feature = "grab")]
460            {
461                let forwarder = crate::engine::forwarder::UinputForwarder::new()?;
462                Ok(GrabState::Enabled {
463                    forwarder: Box::new(forwarder),
464                })
465            }
466            #[cfg(not(feature = "grab"))]
467            {
468                Err(Error::UnsupportedFeature)
469            }
470        }
471    }
472}