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//!   -> send Command::Register { binding, reply }
16//!   -> engine processes the command
17//!   -> engine sends Result back on reply
18//!   -> manager returns BindingGuard or error
19//! ```
20
21use std::fmt;
22use std::path::PathBuf;
23use std::sync::Mutex;
24use std::sync::mpsc;
25
26use kbd::action::Action;
27use kbd::binding::Binding;
28use kbd::binding::BindingId;
29use kbd::binding::BindingOptions;
30use kbd::hotkey::HotkeyInput;
31use kbd::hotkey::ModifierSet;
32use kbd::introspection::ActiveLayerInfo;
33use kbd::introspection::BindingInfo;
34use kbd::introspection::ConflictInfo;
35use kbd::key::Key;
36use kbd::layer::Layer;
37use kbd::layer::LayerName;
38use kbd::sequence::PendingSequenceInfo;
39use kbd::sequence::SequenceInput;
40use kbd::sequence::SequenceOptions;
41
42use crate::backend::Backend;
43use crate::binding_guard::BindingGuard;
44use crate::engine::Command;
45use crate::engine::CommandSender;
46use crate::engine::EngineRuntime;
47use crate::engine::GrabState;
48use crate::error::ManagerStopped;
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51enum BackendSelection {
52    Auto,
53    Explicit(Backend),
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57enum GrabConfiguration {
58    Disabled,
59    Enabled,
60}
61
62/// Builder for explicit backend and runtime options.
63#[derive(Debug)]
64pub struct HotkeyManagerBuilder {
65    backend: BackendSelection,
66    grab: GrabConfiguration,
67}
68
69impl Default for HotkeyManagerBuilder {
70    fn default() -> Self {
71        Self {
72            backend: BackendSelection::Auto,
73            grab: GrabConfiguration::Disabled,
74        }
75    }
76}
77
78impl HotkeyManagerBuilder {
79    /// Force a specific backend instead of auto-detection.
80    #[must_use]
81    pub fn backend(mut self, backend: Backend) -> Self {
82        self.backend = BackendSelection::Explicit(backend);
83        self
84    }
85
86    /// Enable grab mode for exclusive device capture.
87    #[must_use]
88    pub fn grab(mut self) -> Self {
89        self.grab = GrabConfiguration::Enabled;
90        self
91    }
92
93    /// Build and start a new manager instance.
94    ///
95    /// Spawns the engine thread and begins listening for input device events.
96    ///
97    /// # Errors
98    ///
99    /// Returns an error if the backend cannot be initialized, grab mode
100    /// is requested without the `grab` feature, or the engine thread
101    /// fails to start.
102    pub fn build(self) -> Result<HotkeyManager, crate::error::StartupError> {
103        let backend = resolve_backend(self.backend)?;
104        validate_grab_configuration(backend, self.grab)?;
105
106        let grab_state = create_grab_state(self.grab)?;
107        let runtime = if let Some(input_directory) = internal_test_input_directory() {
108            EngineRuntime::spawn_with_input_dir(grab_state, &input_directory)?
109        } else {
110            EngineRuntime::spawn(grab_state)?
111        };
112        let commands = runtime.commands();
113
114        Ok(HotkeyManager {
115            backend,
116            commands,
117            runtime: Mutex::new(Some(runtime)),
118        })
119    }
120}
121
122/// Public manager API.
123pub struct HotkeyManager {
124    backend: Backend,
125    commands: CommandSender,
126    runtime: Mutex<Option<EngineRuntime>>,
127}
128
129impl fmt::Debug for HotkeyManager {
130    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
131        let running = self
132            .runtime
133            .lock()
134            .map(|guard| guard.is_some())
135            .unwrap_or(false);
136
137        f.debug_struct("HotkeyManager")
138            .field("backend", &self.backend)
139            .field("running", &running)
140            .finish_non_exhaustive()
141    }
142}
143
144impl HotkeyManager {
145    /// Create a manager with backend auto-detection.
146    ///
147    /// # Errors
148    ///
149    /// Returns an error if the backend cannot be initialized or input
150    /// devices are not accessible.
151    pub fn new() -> Result<Self, crate::error::StartupError> {
152        Self::builder().build()
153    }
154
155    /// Configure manager startup options.
156    #[must_use]
157    pub fn builder() -> HotkeyManagerBuilder {
158        HotkeyManagerBuilder::default()
159    }
160
161    /// Returns the backend this manager is using.
162    #[must_use]
163    pub const fn active_backend(&self) -> Backend {
164        self.backend
165    }
166
167    /// Register a simple hotkey callback.
168    ///
169    /// Accepts any type implementing [`HotkeyInput`]: a [`Hotkey`](kbd::hotkey::Hotkey), a
170    /// [`Key`], or a string (`&str` / `String`).
171    ///
172    /// # Errors
173    ///
174    /// Returns [`RegisterError::Parse`](crate::error::RegisterError::Parse) when
175    /// string input conversion fails,
176    /// [`RegisterError::AlreadyRegistered`](crate::error::RegisterError::AlreadyRegistered)
177    /// if the hotkey is already bound, or
178    /// [`RegisterError::ManagerStopped`](crate::error::RegisterError::ManagerStopped)
179    /// if the engine has shut down.
180    pub fn register<F>(
181        &self,
182        hotkey: impl HotkeyInput,
183        callback: F,
184    ) -> Result<BindingGuard, crate::error::RegisterError>
185    where
186        F: Fn() + Send + Sync + 'static,
187    {
188        self.register_with_options(hotkey, Action::from(callback), BindingOptions::default())
189    }
190
191    /// Register a multi-step sequence callback.
192    ///
193    /// # Errors
194    ///
195    /// Returns [`RegisterError::Parse`](crate::error::RegisterError::Parse) when
196    /// sequence input conversion fails,
197    /// [`RegisterError::AlreadyRegistered`](crate::error::RegisterError::AlreadyRegistered)
198    /// if the sequence is already bound, or
199    /// [`RegisterError::ManagerStopped`](crate::error::RegisterError::ManagerStopped)
200    /// if the engine has shut down.
201    pub fn register_sequence<F>(
202        &self,
203        sequence: impl SequenceInput,
204        callback: F,
205    ) -> Result<BindingGuard, crate::error::RegisterError>
206    where
207        F: Fn() + Send + Sync + 'static,
208    {
209        self.register_sequence_with_options(
210            sequence,
211            Action::from(callback),
212            SequenceOptions::default(),
213        )
214    }
215
216    /// Register a multi-step sequence with explicit action and options.
217    ///
218    /// # Errors
219    ///
220    /// Returns [`RegisterError::Parse`](crate::error::RegisterError::Parse) when
221    /// sequence input conversion fails,
222    /// [`RegisterError::AlreadyRegistered`](crate::error::RegisterError::AlreadyRegistered)
223    /// if the sequence is already bound, or
224    /// [`RegisterError::ManagerStopped`](crate::error::RegisterError::ManagerStopped)
225    /// if the engine has shut down.
226    pub fn register_sequence_with_options(
227        &self,
228        sequence: impl SequenceInput,
229        action: impl Into<Action>,
230        options: SequenceOptions,
231    ) -> Result<BindingGuard, crate::error::RegisterError> {
232        let sequence = sequence.into_sequence()?;
233        let action = action.into();
234        let id = self.request(|reply| Command::RegisterSequence {
235            sequence,
236            action,
237            options,
238            reply,
239        })??;
240        Ok(BindingGuard::new(id, self.commands.clone()))
241    }
242
243    /// Register a hotkey with an explicit action and binding options.
244    ///
245    /// Accepts any type implementing [`HotkeyInput`]: a [`Hotkey`](kbd::hotkey::Hotkey), a
246    /// [`Key`], or a string (`&str` / `String`).
247    ///
248    /// Use when you need metadata (description, overlay visibility) or
249    /// behavioral options beyond what [`register()`](Self::register) provides.
250    ///
251    /// # Errors
252    ///
253    /// Returns [`RegisterError::Parse`](crate::error::RegisterError::Parse) when
254    /// string input conversion fails,
255    /// [`RegisterError::AlreadyRegistered`](crate::error::RegisterError::AlreadyRegistered)
256    /// if the hotkey is already bound, or
257    /// [`RegisterError::ManagerStopped`](crate::error::RegisterError::ManagerStopped)
258    /// if the engine has shut down.
259    pub fn register_with_options(
260        &self,
261        hotkey: impl HotkeyInput,
262        action: impl Into<Action>,
263        options: BindingOptions,
264    ) -> Result<BindingGuard, crate::error::RegisterError> {
265        let id = BindingId::new();
266        let hotkey = hotkey.into_hotkey()?;
267        let binding = Binding::new(id, hotkey, action.into()).with_options(options);
268
269        self.request(|reply| Command::Register { binding, reply })??;
270        Ok(BindingGuard::new(id, self.commands.clone()))
271    }
272
273    /// Query whether a hotkey is currently registered.
274    ///
275    /// Accepts any type implementing [`HotkeyInput`]: a [`Hotkey`](kbd::hotkey::Hotkey), a
276    /// [`Key`], or a string (`&str` / `String`).
277    ///
278    /// # Errors
279    ///
280    /// Returns [`QueryError::Parse`](crate::error::QueryError::Parse) when string
281    /// input conversion fails, or
282    /// [`QueryError::ManagerStopped`](crate::error::QueryError::ManagerStopped) if
283    /// the engine has shut down.
284    pub fn is_registered(
285        &self,
286        hotkey: impl HotkeyInput,
287    ) -> Result<bool, crate::error::QueryError> {
288        let hotkey = hotkey.into_hotkey()?;
289        Ok(self.request(|reply| Command::IsRegistered { hotkey, reply })?)
290    }
291
292    /// Query whether a specific key is currently pressed on any device.
293    ///
294    /// # Errors
295    ///
296    /// Returns [`ManagerStopped`] if the engine has shut down.
297    pub fn is_key_pressed(&self, key: Key) -> Result<bool, ManagerStopped> {
298        self.request(|reply| Command::IsKeyPressed { key, reply })
299    }
300
301    /// Query the set of modifiers currently held, derived from key state.
302    ///
303    /// Left/right variants are canonicalized: if either `LeftCtrl` or `RightCtrl`
304    /// is held, `Modifier::Ctrl` is in the returned set.
305    ///
306    /// # Errors
307    ///
308    /// Returns [`ManagerStopped`] if the engine has shut down.
309    pub fn active_modifiers(&self) -> Result<ModifierSet, ManagerStopped> {
310        self.request(|reply| Command::ActiveModifiers { reply })
311    }
312
313    /// Define a named layer.
314    ///
315    /// Sends the layer definition to the engine for storage. The layer
316    /// is not active until explicitly pushed via [`push_layer()`](Self::push_layer).
317    ///
318    /// # Errors
319    ///
320    /// Returns [`LayerError::AlreadyDefined`](crate::error::LayerError::AlreadyDefined)
321    /// if a layer with the same name has already been defined, or
322    /// [`LayerError::ManagerStopped`](crate::error::LayerError::ManagerStopped)
323    /// if the engine has shut down.
324    pub fn define_layer(&self, layer: Layer) -> Result<(), crate::error::LayerError> {
325        self.request(|reply| Command::DefineLayer { layer, reply })?
326    }
327
328    /// Stop the manager and join the engine thread.
329    ///
330    /// All registered bindings are dropped. This is also called
331    /// automatically when the manager is dropped.
332    ///
333    /// # Errors
334    ///
335    /// Returns [`ShutdownError::Engine`](crate::error::ShutdownError::Engine) if
336    /// the engine thread panicked.
337    pub fn shutdown(self) -> Result<(), crate::error::ShutdownError> {
338        self.shutdown_inner()
339    }
340
341    /// Send a command that carries a reply channel and wait for the response.
342    ///
343    /// Encapsulates the channel-create → send → recv → map-error boilerplate
344    /// shared by every request/reply manager method.
345    fn request<T>(
346        &self,
347        build: impl FnOnce(mpsc::Sender<T>) -> Command,
348    ) -> Result<T, ManagerStopped> {
349        let (reply_tx, reply_rx) = mpsc::channel();
350        self.commands.send(build(reply_tx))?;
351        reply_rx.recv().map_err(|_| ManagerStopped)
352    }
353
354    fn shutdown_inner(&self) -> Result<(), crate::error::ShutdownError> {
355        let mut runtime = self
356            .runtime
357            .lock()
358            .map_err(|_| crate::error::ShutdownError::Engine)?;
359        if let Some(runtime) = runtime.take() {
360            return runtime.shutdown();
361        }
362
363        Ok(())
364    }
365
366    /// Push a named layer onto the layer stack.
367    ///
368    /// The layer must have been previously defined via [`define_layer`](Self::define_layer).
369    /// The pushed layer becomes the highest-priority layer for matching.
370    ///
371    /// # Errors
372    ///
373    /// Returns [`LayerError::NotDefined`](crate::error::LayerError::NotDefined) if
374    /// no layer with the given name exists, or
375    /// [`LayerError::ManagerStopped`](crate::error::LayerError::ManagerStopped) if
376    /// the engine has shut down.
377    pub fn push_layer(&self, name: impl Into<LayerName>) -> Result<(), crate::error::LayerError> {
378        self.request(|reply| Command::PushLayer {
379            name: name.into(),
380            reply,
381        })?
382    }
383
384    /// Pop the topmost layer from the layer stack.
385    ///
386    /// Returns the name of the popped layer.
387    ///
388    /// # Errors
389    ///
390    /// Returns [`LayerError::EmptyStack`](crate::error::LayerError::EmptyStack)
391    /// if no layers are active, or
392    /// [`LayerError::ManagerStopped`](crate::error::LayerError::ManagerStopped) if
393    /// the engine has shut down.
394    pub fn pop_layer(&self) -> Result<LayerName, crate::error::LayerError> {
395        self.request(|reply| Command::PopLayer { reply })?
396    }
397
398    /// Toggle a named layer on or off.
399    ///
400    /// If the layer is currently in the stack, it is removed.
401    /// If the layer is not in the stack, it is pushed.
402    ///
403    /// # Errors
404    ///
405    /// Returns [`LayerError::NotDefined`](crate::error::LayerError::NotDefined) if
406    /// no layer with the given name exists, or
407    /// [`LayerError::ManagerStopped`](crate::error::LayerError::ManagerStopped) if
408    /// the engine has shut down.
409    pub fn toggle_layer(&self, name: impl Into<LayerName>) -> Result<(), crate::error::LayerError> {
410        self.request(|reply| Command::ToggleLayer {
411            name: name.into(),
412            reply,
413        })?
414    }
415
416    /// List all registered bindings with current shadowed status.
417    ///
418    /// Returns global bindings and all layer bindings (active or not).
419    /// Each entry includes whether the binding is currently reachable
420    /// or shadowed by a higher-priority layer.
421    ///
422    /// # Errors
423    ///
424    /// Returns [`ManagerStopped`] if the engine has shut down.
425    pub fn list_bindings(&self) -> Result<Vec<BindingInfo>, ManagerStopped> {
426        self.request(|reply| Command::ListBindings { reply })
427    }
428
429    /// Query what would fire if the given hotkey were pressed now.
430    ///
431    /// Considers the current layer stack. Returns `None` if no binding
432    /// matches the hotkey.
433    ///
434    /// # Errors
435    ///
436    /// Returns [`QueryError::Parse`](crate::error::QueryError::Parse) when string
437    /// input conversion fails, or
438    /// [`QueryError::ManagerStopped`](crate::error::QueryError::ManagerStopped) if
439    /// the engine has shut down.
440    pub fn bindings_for_key(
441        &self,
442        hotkey: impl HotkeyInput,
443    ) -> Result<Option<BindingInfo>, crate::error::QueryError> {
444        let hotkey = hotkey.into_hotkey()?;
445        Ok(self.request(|reply| Command::BindingsForKey { hotkey, reply })?)
446    }
447
448    /// Query the current layer stack.
449    ///
450    /// Returns layers in stack order (bottom to top).
451    ///
452    /// # Errors
453    ///
454    /// Returns [`ManagerStopped`] if the engine has shut down.
455    pub fn active_layers(&self) -> Result<Vec<ActiveLayerInfo>, ManagerStopped> {
456        self.request(|reply| Command::ActiveLayers { reply })
457    }
458
459    /// Return current in-progress sequence state, if any.
460    ///
461    /// # Errors
462    ///
463    /// Returns [`ManagerStopped`] if the engine has shut down.
464    pub fn pending_sequence(&self) -> Result<Option<PendingSequenceInfo>, ManagerStopped> {
465        self.request(|reply| Command::PendingSequence { reply })
466    }
467
468    /// Register a tap-hold binding for a dual-function key.
469    ///
470    /// The `tap_action` fires when the key is pressed and released quickly
471    /// (before the threshold). The `hold_action` fires when the key is held
472    /// past the threshold or interrupted by another keypress.
473    ///
474    /// **Requires grab mode.** Tap-hold must intercept and buffer key events
475    /// before they reach other applications — without grab, the original key
476    /// event would be delivered immediately.
477    ///
478    /// # Errors
479    ///
480    /// Returns [`RegisterError::UnsupportedFeature`](crate::error::RegisterError::UnsupportedFeature)
481    /// if grab mode is not enabled,
482    /// [`RegisterError::AlreadyRegistered`](crate::error::RegisterError::AlreadyRegistered)
483    /// if the key already has a tap-hold binding, or
484    /// [`RegisterError::ManagerStopped`](crate::error::RegisterError::ManagerStopped)
485    /// if the engine has shut down.
486    pub fn register_tap_hold(
487        &self,
488        key: Key,
489        tap_action: impl Into<Action>,
490        hold_action: impl Into<Action>,
491        options: kbd::tap_hold::TapHoldOptions,
492    ) -> Result<BindingGuard, crate::error::RegisterError> {
493        let id = self.request(|reply| Command::RegisterTapHold {
494            key,
495            tap_action: tap_action.into(),
496            hold_action: hold_action.into(),
497            options,
498            reply,
499        })??;
500        Ok(BindingGuard::new(id, self.commands.clone()))
501    }
502
503    /// Find bindings that are shadowed by higher-priority layers.
504    ///
505    /// Returns conflict pairs: each entry shows the shadowed binding
506    /// and the binding that shadows it.
507    ///
508    /// # Errors
509    ///
510    /// Returns [`ManagerStopped`] if the engine has shut down.
511    pub fn conflicts(&self) -> Result<Vec<ConflictInfo>, ManagerStopped> {
512        self.request(|reply| Command::Conflicts { reply })
513    }
514}
515
516impl Drop for HotkeyManager {
517    fn drop(&mut self) {
518        let _ = self.shutdown_inner();
519    }
520}
521
522fn resolve_backend(selection: BackendSelection) -> Result<Backend, crate::error::StartupError> {
523    match selection {
524        BackendSelection::Auto => Ok(Backend::Evdev),
525        BackendSelection::Explicit(backend) => validate_explicit_backend(backend),
526    }
527}
528
529#[allow(clippy::unnecessary_wraps)]
530fn validate_explicit_backend(backend: Backend) -> Result<Backend, crate::error::StartupError> {
531    match backend {
532        Backend::Evdev => Ok(Backend::Evdev),
533    }
534}
535
536#[allow(clippy::unnecessary_wraps)]
537fn validate_grab_configuration(
538    _backend: Backend,
539    _grab: GrabConfiguration,
540) -> Result<(), crate::error::StartupError> {
541    Ok(())
542}
543
544fn internal_test_input_directory() -> Option<PathBuf> {
545    std::env::var_os("_KBD_GLOBAL_INTERNAL_TEST_INPUT_DIR").map(PathBuf::from)
546}
547
548fn create_grab_state(grab: GrabConfiguration) -> Result<GrabState, crate::error::StartupError> {
549    match grab {
550        GrabConfiguration::Disabled => Ok(GrabState::Disabled),
551        GrabConfiguration::Enabled => {
552            #[cfg(feature = "grab")]
553            {
554                let forwarder = crate::engine::forwarder::UinputForwarder::new()?;
555                Ok(GrabState::Enabled {
556                    forwarder: Box::new(forwarder),
557                })
558            }
559            #[cfg(not(feature = "grab"))]
560            {
561                Err(crate::error::StartupError::UnsupportedFeature)
562            }
563        }
564    }
565}