use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use core_graphics::event::{
CGEvent, CGEventTap, CGEventTapLocation, CGEventTapOptions, CGEventTapPlacement,
CGEventTapProxy, CGEventType, CallbackResult,
};
use tracing::warn;
pub(super) const PROBE_HEARTBEAT: Duration = Duration::from_secs(5);
pub(super) fn can_filter_events() -> bool {
CGEventTap::new(
CGEventTapLocation::HID,
CGEventTapPlacement::HeadInsertEventTap,
CGEventTapOptions::Default,
vec![CGEventType::Null],
|_proxy: CGEventTapProxy, _etype: CGEventType, _event: &CGEvent| CallbackResult::Keep,
)
.is_ok()
}
pub(super) enum ProbeCue {
EverySlice,
OnWake {
due: Arc<AtomicBool>,
_watch: axwatch::Watch,
},
}
impl ProbeCue {
pub(super) fn arm() -> Self {
let due = Arc::new(AtomicBool::new(true));
let flag = Arc::clone(&due);
match axwatch::Watch::start(Some(PROBE_HEARTBEAT), move |_| {
flag.store(true, Ordering::Release);
}) {
Ok(watch) => Self::OnWake { due, _watch: watch },
Err(error) => {
warn!(
%error,
"could not watch the Accessibility grant — probing it every slice instead"
);
Self::EverySlice
}
}
}
pub(super) fn take(&self) -> bool {
match self {
Self::EverySlice => true,
Self::OnWake { due, .. } => due.swap(false, Ordering::AcqRel),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_wake_driven_cue_probes_on_the_first_slice_and_once_per_wake() {
let due = Arc::new(AtomicBool::new(true));
let cue = ProbeCue::OnWake {
due: Arc::clone(&due),
_watch: axwatch::Watch::start(None, |_| {}).expect("registration needs no run loop"),
};
assert!(cue.take(), "the slice after arming probes");
assert!(!cue.take(), "nothing woke the watch since");
due.store(true, Ordering::Release);
assert!(cue.take(), "a wake buys exactly one probe");
assert!(!cue.take());
}
}