Skip to main content

origin_platform/
shortcut.rs

1//! Global shortcut contract (B5).
2//!
3//! A global shortcut fires while the application is in the background — the "quick
4//! capture" pattern. Registration is the product's choice; the host owns the native
5//! binding. Like the tray, this is present only when the product declares it.
6
7use async_trait::async_trait;
8use origin_domain::Result;
9use std::fmt::Debug;
10
11/// One accelerator the product wants to own.
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct Shortcut {
14    /// Stable id so the product can tell which shortcut fired.
15    pub id: String,
16    /// Platform-neutral accelerator, e.g. `"CmdOrCtrl+Shift+Space"`.
17    pub accelerator: String,
18}
19
20impl Shortcut {
21    pub fn new(id: impl Into<String>, accelerator: impl Into<String>) -> Self {
22        Self {
23            id: id.into(),
24            accelerator: accelerator.into(),
25        }
26    }
27}
28
29/// Registers global shortcuts.
30///
31/// The host reports a press as a typed event (ARCHITECTURE.md rule 9), never as a
32/// callback the product registers by string. A headless or CLI build gets a no-op.
33#[async_trait]
34pub trait GlobalShortcutService: Debug + Send + Sync + 'static {
35    /// Register `shortcut`, or replace an existing one with the same id.
36    async fn register(&self, shortcut: Shortcut) -> Result<()>;
37
38    /// Unregister by id. Unregistering an unknown id succeeds.
39    async fn unregister(&self, id: &str) -> Result<()>;
40}
41
42/// Does nothing. For headless runs, CLI builds and tests.
43#[derive(Debug, Clone, Copy, Default)]
44pub struct NoopGlobalShortcutService;
45
46#[async_trait]
47impl GlobalShortcutService for NoopGlobalShortcutService {
48    async fn register(&self, shortcut: Shortcut) -> Result<()> {
49        tracing::debug!(id = %shortcut.id, accelerator = %shortcut.accelerator, "global shortcut — dropped (noop)");
50        Ok(())
51    }
52
53    async fn unregister(&self, id: &str) -> Result<()> {
54        tracing::debug!(id, "global shortcut unregistered — dropped (noop)");
55        Ok(())
56    }
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62
63    #[tokio::test]
64    async fn the_noop_service_accepts_registration_and_removal() {
65        let shortcuts: &dyn GlobalShortcutService = &NoopGlobalShortcutService;
66
67        shortcuts
68            .register(Shortcut::new("quick-capture", "CmdOrCtrl+Shift+Space"))
69            .await
70            .unwrap();
71        shortcuts.unregister("quick-capture").await.unwrap();
72        shortcuts.unregister("never-registered").await.unwrap();
73    }
74}