Skip to main content

oximemo_capture/
lib.rs

1//! macOS global Option double-tap monitor (ยง6.1).
2//!
3//! The macOS implementation spawns a dedicated thread with its own
4//! `NSRunLoop` and registers a passive `NSEvent` global monitor for
5//! `.flagsChanged` events. The Option key keeps working normally in every
6//! other app; we only observe.
7//!
8//! On non-macOS targets, [`CaptureMonitor::start`] returns
9//! [`CaptureError::Os`] so dependent crates compile everywhere.
10
11#[cfg(target_os = "macos")]
12mod macos;
13
14#[cfg(target_os = "macos")]
15pub use macos::CaptureMonitorImpl;
16
17/// A handle to the running global monitor. Dropping it stops monitoring.
18pub struct CaptureMonitor {
19    #[cfg(target_os = "macos")]
20    inner: Option<macos::CaptureMonitorImpl>,
21}
22
23/// Errors from starting the monitor.
24#[derive(Debug)]
25pub enum CaptureError {
26    /// macOS Accessibility/Input Monitoring permission not granted.
27    PermissionDenied,
28    /// Underlying OS error.
29    Os(String),
30}
31
32impl std::fmt::Display for CaptureError {
33    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34        match self {
35            Self::PermissionDenied => {
36                f.write_str("macOS Accessibility/Input Monitoring permission denied")
37            }
38            Self::Os(s) => write!(f, "capture OS error: {s}"),
39        }
40    }
41}
42
43impl std::error::Error for CaptureError {}
44
45impl CaptureMonitor {
46    /// Start watching for an Option-key double-tap.
47    ///
48    /// - `threshold_ms`: max interval between two Option-only press/release
49    ///   pairs to count as a double-tap.
50    /// - `on_trigger`: invoked on the monitor's thread when a double-tap is
51    ///   detected. Implementations should hand off to the main app (e.g.
52    ///   emit a Tauri event) rather than do heavy work inline.
53    #[cfg(target_os = "macos")]
54    pub fn start(
55        threshold_ms: u32,
56        on_trigger: Box<dyn Fn() + Send + 'static>,
57    ) -> Result<Self, CaptureError> {
58        let inner = macos::CaptureMonitorImpl::start(threshold_ms, on_trigger)?;
59        Ok(Self { inner: Some(inner) })
60    }
61
62    #[cfg(not(target_os = "macos"))]
63    pub fn start(
64        _threshold_ms: u32,
65        _on_trigger: Box<dyn Fn() + Send + 'static>,
66    ) -> Result<Self, CaptureError> {
67        Err(CaptureError::Os("capture only supported on macOS".into()))
68    }
69}
70
71impl Drop for CaptureMonitor {
72    fn drop(&mut self) {
73        #[cfg(target_os = "macos")]
74        {
75            self.inner.take();
76        }
77    }
78}