replace device_query with tauri API so no need for accessibility perm
on darwin
This commit is contained in:
+151
-202
@@ -1,5 +1,3 @@
|
||||
use device_query::{DeviceEvents, DeviceEventsHandler, DeviceQuery, DeviceState};
|
||||
use lazy_static::lazy_static;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use std::collections::HashMap;
|
||||
@@ -8,7 +6,10 @@ use std::sync::RwLock;
|
||||
use std::time::Duration;
|
||||
use tauri::{AppHandle, Manager};
|
||||
use tauri_specta::Event;
|
||||
use tokio::sync::{mpsc, oneshot, watch};
|
||||
use tokio::sync::watch;
|
||||
|
||||
pub(crate) const SYSTEM_CURSOR_POLL_INTERVAL: Duration = Duration::from_millis(250);
|
||||
const CURSOR_BROADCAST_INTERVAL: Duration = Duration::from_millis(500);
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Type)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -71,121 +72,87 @@ impl CursorState {
|
||||
}
|
||||
}
|
||||
|
||||
fn current_position(handle: &AppHandle) -> Result<CursorPositions, String> {
|
||||
let monitor = handle
|
||||
.primary_monitor()
|
||||
.map_err(|error| error.to_string())?
|
||||
.ok_or("Primary monitor is unavailable")?;
|
||||
let position = DeviceState::checked_new()
|
||||
.ok_or_else(|| "System cursor access is unavailable".to_owned())?
|
||||
.get_mouse()
|
||||
.coords;
|
||||
fn read_system_cursor_position(handle: &AppHandle) -> Result<CursorPosition, String> {
|
||||
handle
|
||||
.cursor_position()
|
||||
.map(|position| CursorPosition {
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
})
|
||||
.map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let raw = CursorPosition {
|
||||
x: position.0 as f64,
|
||||
y: position.1 as f64,
|
||||
};
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let raw = CursorPosition {
|
||||
x: position.0 as f64 * monitor.scale_factor(),
|
||||
y: position.1 as f64 * monitor.scale_factor(),
|
||||
};
|
||||
|
||||
Ok(CursorPositions {
|
||||
mapped: transform_cursor_pos(&raw, true, &monitor),
|
||||
fn positions_from_raw(raw: CursorPosition, monitor: &tauri::Monitor) -> CursorPositions {
|
||||
CursorPositions {
|
||||
mapped: transform_cursor_pos(&raw, true, monitor),
|
||||
raw,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Was private, but for some reason LSP
|
||||
// complains even when there's no external references.
|
||||
// Possibly because of `lazy_static!`.
|
||||
// Just leave it public I guess.
|
||||
struct CursorTask {
|
||||
stop_tx: watch::Sender<bool>,
|
||||
task: tauri::async_runtime::JoinHandle<()>,
|
||||
}
|
||||
|
||||
enum CursorTracker {
|
||||
Starting,
|
||||
Running(CursorTask),
|
||||
#[derive(Default)]
|
||||
pub(crate) struct CursorPositionProvider {
|
||||
latest: RwLock<Option<CursorPosition>>,
|
||||
tracker: Mutex<Option<CursorTask>>,
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref CURSOR_TRACKER: Mutex<Option<CursorTracker>> = Mutex::new(None);
|
||||
impl CursorPositionProvider {
|
||||
pub(crate) fn latest(&self) -> Result<Option<CursorPosition>, String> {
|
||||
self.latest
|
||||
.read()
|
||||
.map(|position| position.clone())
|
||||
.map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
fn update(&self, position: CursorPosition) -> Result<(), String> {
|
||||
let mut latest = self.latest.write().map_err(|error| error.to_string())?;
|
||||
if latest.as_ref() != Some(&position) {
|
||||
*latest = Some(position);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(app: &AppHandle) {
|
||||
app.manage(CursorState::default());
|
||||
app.manage(CursorPositionProvider::default());
|
||||
}
|
||||
|
||||
/// Starts cursor tracking after Accessibility permission is available.
|
||||
pub async fn start_tracking(app: &AppHandle) -> Result<(), String> {
|
||||
if !crate::macos::accessibility_permission_granted(app) {
|
||||
return Err("macOS Accessibility permission has not been granted".to_owned());
|
||||
}
|
||||
|
||||
/// Starts the shared cursor provider and change-based cursor broadcasting.
|
||||
pub fn start_tracking(app: &AppHandle) -> Result<(), String> {
|
||||
let primary_monitor = app
|
||||
.primary_monitor()
|
||||
.map_err(|error| error.to_string())?
|
||||
.ok_or_else(|| "Failed to resolve primary monitor".to_owned())?;
|
||||
|
||||
{
|
||||
let mut tracker = CURSOR_TRACKER
|
||||
.lock()
|
||||
.map_err(|error| format!("Failed to lock cursor tracker state: {error}"))?;
|
||||
if tracker.is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
*tracker = Some(CursorTracker::Starting);
|
||||
let provider = app.state::<CursorPositionProvider>();
|
||||
let mut tracker = provider
|
||||
.tracker
|
||||
.lock()
|
||||
.map_err(|error| format!("Failed to lock cursor tracker state: {error}"))?;
|
||||
if tracker.is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!("Initializing cursor tracking...");
|
||||
match current_position(app) {
|
||||
Ok(positions) => update_cursor_position(app, positions),
|
||||
Err(error) => eprintln!("Failed to resolve current cursor position: {error}"),
|
||||
}
|
||||
let initial_position = read_system_cursor_position(app)
|
||||
.map_err(|error| format!("Failed to resolve current cursor position: {error}"))?;
|
||||
provider.update(initial_position.clone())?;
|
||||
update_cursor_position(app, positions_from_raw(initial_position, &primary_monitor));
|
||||
|
||||
let (stop_tx, stop_rx) = watch::channel(false);
|
||||
let (ready_tx, ready_rx) = oneshot::channel();
|
||||
|
||||
let handle = app.clone();
|
||||
let task = tauri::async_runtime::spawn(async move {
|
||||
if let Err(e) = init_cursor_tracking_i(stop_rx, primary_monitor, handle, ready_tx).await {
|
||||
println!("Failed to initialize cursor tracking: {}", e);
|
||||
if let Err(error) = track_cursor(stop_rx, primary_monitor, handle).await {
|
||||
eprintln!("Cursor tracking stopped with an error: {error}");
|
||||
}
|
||||
});
|
||||
|
||||
let startup = ready_rx
|
||||
.await
|
||||
.unwrap_or_else(|_| Err("Cursor tracking task stopped during initialization".to_owned()));
|
||||
if let Err(error) = startup {
|
||||
if let Ok(mut tracker) = CURSOR_TRACKER.lock()
|
||||
&& matches!(*tracker, Some(CursorTracker::Starting))
|
||||
{
|
||||
*tracker = None;
|
||||
}
|
||||
let _ = task.await;
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
let installed = {
|
||||
let mut tracker = CURSOR_TRACKER
|
||||
.lock()
|
||||
.map_err(|error| format!("Failed to lock cursor tracker state: {error}"))?;
|
||||
if matches!(*tracker, Some(CursorTracker::Starting)) {
|
||||
*tracker = Some(CursorTracker::Running(CursorTask { stop_tx, task }));
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
if !installed {
|
||||
return Err("Cursor tracking startup was cancelled".to_owned());
|
||||
}
|
||||
*tracker = Some(CursorTask { stop_tx, task });
|
||||
|
||||
println!("EVENT: Cursor Tracker Enabled");
|
||||
Ok(())
|
||||
@@ -265,6 +232,86 @@ fn transform_coords(pos: &CursorPosition, to_normalized: bool, w: f64, h: f64) -
|
||||
}
|
||||
}
|
||||
|
||||
/// Stop cursor tracking and unregister all listeners.
|
||||
#[allow(dead_code)] // TODO: get rid of this when app teardown sequence is introduced.
|
||||
pub async fn stop_cursor_tracking(app: &AppHandle) {
|
||||
println!("stop_cursor_tracking called");
|
||||
|
||||
let tracker = match app.state::<CursorPositionProvider>().tracker.lock() {
|
||||
Ok(mut tracker) => tracker.take(),
|
||||
Err(e) => {
|
||||
println!("Failed to lock cursor tracker state: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let tracker = match tracker {
|
||||
Some(tracker) => tracker,
|
||||
None => {
|
||||
println!("Cursor tracking is not running");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = tracker.stop_tx.send(true) {
|
||||
println!("Failed to signal cursor tracking stop: {}", e);
|
||||
}
|
||||
|
||||
if let Err(e) = tracker.task.await {
|
||||
println!("Cursor tracking task join failed: {}", e);
|
||||
}
|
||||
|
||||
println!("EVENT: Cursor Tracker Disabled");
|
||||
}
|
||||
|
||||
async fn track_cursor(
|
||||
mut stop_rx: watch::Receiver<bool>,
|
||||
monitor: tauri::Monitor,
|
||||
handle: AppHandle,
|
||||
) -> Result<(), String> {
|
||||
let provider = handle.state::<CursorPositionProvider>();
|
||||
let mut last_broadcast_position = provider.latest()?;
|
||||
let mut poll_interval = tokio::time::interval(SYSTEM_CURSOR_POLL_INTERVAL);
|
||||
let mut broadcast_interval = tokio::time::interval(CURSOR_BROADCAST_INTERVAL);
|
||||
poll_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
|
||||
broadcast_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
|
||||
|
||||
// The initial position was already sampled and broadcast by start_tracking.
|
||||
poll_interval.tick().await;
|
||||
broadcast_interval.tick().await;
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = poll_interval.tick() => {
|
||||
match read_system_cursor_position(&handle) {
|
||||
Ok(position) => provider.update(position)?,
|
||||
Err(error) => eprintln!("Failed to read system cursor position: {error}"),
|
||||
}
|
||||
}
|
||||
_ = broadcast_interval.tick() => {
|
||||
let latest = provider.latest()?;
|
||||
if latest != last_broadcast_position {
|
||||
if let Some(position) = latest.as_ref() {
|
||||
update_cursor_position(
|
||||
&handle,
|
||||
positions_from_raw(position.clone(), &monitor),
|
||||
);
|
||||
}
|
||||
last_broadcast_position = latest;
|
||||
}
|
||||
}
|
||||
changed = stop_rx.changed() => {
|
||||
if changed.is_err() || *stop_rx.borrow() {
|
||||
println!("Stopping cursor tracking");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -318,124 +365,26 @@ mod tests {
|
||||
assert_eq!(two_x.x, one_x.x);
|
||||
assert_eq!(two_x.y, one_x.y);
|
||||
}
|
||||
}
|
||||
|
||||
/// Stop cursor tracking and unregister all listeners.
|
||||
#[allow(dead_code)] // TODO: get rid of this when app teardown sequence is introduced.
|
||||
pub async fn stop_cursor_tracking() {
|
||||
println!("stop_cursor_tracking called");
|
||||
#[test]
|
||||
fn cursor_provider_keeps_the_latest_system_position() {
|
||||
let provider = CursorPositionProvider::default();
|
||||
assert_eq!(provider.latest().unwrap(), None);
|
||||
|
||||
let tracker = match CURSOR_TRACKER.lock() {
|
||||
Ok(mut tracker) => tracker.take(),
|
||||
Err(e) => {
|
||||
println!("Failed to lock cursor tracker state: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
provider
|
||||
.update(CursorPosition { x: 120.0, y: 80.0 })
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
provider.latest().unwrap(),
|
||||
Some(CursorPosition { x: 120.0, y: 80.0 })
|
||||
);
|
||||
|
||||
let tracker = match tracker {
|
||||
Some(CursorTracker::Running(tracker)) => tracker,
|
||||
Some(CursorTracker::Starting) => {
|
||||
println!("Cursor tracking startup cancelled");
|
||||
return;
|
||||
}
|
||||
None => {
|
||||
println!("Cursor tracking is not running");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = tracker.stop_tx.send(true) {
|
||||
println!("Failed to signal cursor tracking stop: {}", e);
|
||||
provider
|
||||
.update(CursorPosition { x: 240.0, y: 160.0 })
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
provider.latest().unwrap(),
|
||||
Some(CursorPosition { x: 240.0, y: 160.0 })
|
||||
);
|
||||
}
|
||||
|
||||
if let Err(e) = tracker.task.await {
|
||||
println!("Cursor tracking task join failed: {}", e);
|
||||
}
|
||||
|
||||
println!("EVENT: Cursor Tracker Disabled");
|
||||
}
|
||||
|
||||
async fn init_cursor_tracking_i(
|
||||
mut stop_rx: watch::Receiver<bool>,
|
||||
monitor: tauri::Monitor,
|
||||
handle: AppHandle,
|
||||
ready_tx: oneshot::Sender<Result<(), String>>,
|
||||
) -> Result<(), String> {
|
||||
// Create a channel to decouple event generation (producer) from processing (consumer).
|
||||
// Capacity 100 is plenty for 500ms polling (2Hz).
|
||||
let (tx, mut rx) = mpsc::channel::<CursorPositions>(100);
|
||||
let permission_handle = handle.clone();
|
||||
|
||||
// Spawn the consumer task
|
||||
// This task handles WebSocket reporting and local position projection updates.
|
||||
// It runs independently of the device event loop.
|
||||
tauri::async_runtime::spawn(async move {
|
||||
println!("Cursor event consumer started");
|
||||
|
||||
while let Some(positions) = rx.recv().await {
|
||||
update_cursor_position(&handle, positions);
|
||||
}
|
||||
println!("Cursor event consumer stopped (channel closed)");
|
||||
});
|
||||
|
||||
let device_state = match DeviceEventsHandler::new(Duration::from_millis(500)) {
|
||||
Some(device_state) => device_state,
|
||||
None => {
|
||||
let error = "Failed to create device event handler (already running?)".to_owned();
|
||||
let _ = ready_tx.send(Err(error.clone()));
|
||||
return Err(error);
|
||||
}
|
||||
};
|
||||
|
||||
println!("Device event handler created successfully");
|
||||
println!("Setting up mouse move handler for event broadcasting...");
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let scale_factor = monitor.scale_factor();
|
||||
|
||||
// The producer closure moves `tx` into it.
|
||||
// device_query runs this closure on its own thread.
|
||||
let _guard = device_state.on_mouse_move(move |position: &(i32, i32)| {
|
||||
if !crate::macos::accessibility_permission_granted(&permission_handle) {
|
||||
return;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let raw = CursorPosition {
|
||||
x: position.0 as f64,
|
||||
y: position.1 as f64,
|
||||
};
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let raw = CursorPosition {
|
||||
x: position.0 as f64 * scale_factor,
|
||||
y: position.1 as f64 * scale_factor,
|
||||
};
|
||||
|
||||
let mapped = transform_cursor_pos(&raw, true, &monitor);
|
||||
|
||||
let positions = CursorPositions { raw, mapped };
|
||||
|
||||
// Send to consumer channel (non-blocking)
|
||||
if let Err(e) = tx.try_send(positions) {
|
||||
println!("Failed to send cursor position to channel: {:?}", e);
|
||||
}
|
||||
});
|
||||
|
||||
ready_tx
|
||||
.send(Ok(()))
|
||||
.map_err(|_| "Cursor tracking startup was cancelled".to_owned())?;
|
||||
println!("Mouse move handler registered - now broadcasting cursor events to all windows");
|
||||
|
||||
// Keep the handler alive while tracking is enabled.
|
||||
// This loop is necessary to keep `_guard` and `device_state` in scope.
|
||||
while stop_rx.changed().await.is_ok() {
|
||||
if *stop_rx.borrow() {
|
||||
println!("Stopping cursor tracking event handler");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user