replace device_query with tauri API so no need for accessibility perm
on darwin
This commit is contained in:
@@ -9,7 +9,6 @@ struct ApplicationState {
|
||||
|
||||
pub async fn init(handle: &AppHandle) -> Result<(), String> {
|
||||
handle.manage(ApplicationState::default());
|
||||
let accessibility_permission_granted = crate::macos::init(handle).await?;
|
||||
let settings = crate::settings::get(&handle.state())
|
||||
.await
|
||||
.map_err(crate::db::command_error)?;
|
||||
@@ -17,17 +16,14 @@ pub async fn init(handle: &AppHandle) -> Result<(), String> {
|
||||
crate::ui::splashscreen::close(handle).await?;
|
||||
|
||||
if settings.onboarding_done {
|
||||
start(handle).await;
|
||||
if cfg!(target_os = "macos") && !accessibility_permission_granted {
|
||||
crate::ui::onboarding::show_accessibility_page(handle).await?;
|
||||
}
|
||||
start(handle);
|
||||
} else {
|
||||
crate::ui::onboarding::show_initial(handle).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn start(handle: &AppHandle) {
|
||||
pub fn start(handle: &AppHandle) {
|
||||
let state = handle.state::<ApplicationState>();
|
||||
if state
|
||||
.started
|
||||
@@ -37,8 +33,10 @@ pub async fn start(handle: &AppHandle) {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Err(error) = crate::cursor::start_tracking(handle) {
|
||||
eprintln!("failed to initialize cursor tracking: {error}");
|
||||
}
|
||||
crate::ui::init(handle);
|
||||
reconcile_cursor(handle).await;
|
||||
}
|
||||
|
||||
pub fn is_started(handle: &AppHandle) -> bool {
|
||||
@@ -47,13 +45,3 @@ pub fn is_started(handle: &AppHandle) -> bool {
|
||||
.started
|
||||
.load(Ordering::Acquire)
|
||||
}
|
||||
|
||||
pub async fn reconcile_cursor(handle: &AppHandle) {
|
||||
if !is_started(handle) || !crate::macos::accessibility_permission_granted(handle) {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Err(error) = crate::cursor::start_tracking(handle).await {
|
||||
eprintln!("failed to initialize cursor tracking: {error}");
|
||||
}
|
||||
}
|
||||
|
||||
+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(())
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ mod images;
|
||||
mod interactions;
|
||||
mod keypair;
|
||||
mod live_data;
|
||||
mod macos;
|
||||
mod network;
|
||||
mod onboarding;
|
||||
mod profile;
|
||||
@@ -114,9 +113,7 @@ fn specta_builder() -> tauri_specta::Builder<tauri::Wry> {
|
||||
puppet::list_puppet_states,
|
||||
scene_configuration::get_scene_configuration,
|
||||
scene_configuration::update_scene_configuration,
|
||||
onboarding::get_onboarding_status,
|
||||
onboarding::complete_onboarding,
|
||||
macos::request_accessibility_permission,
|
||||
settings::autostart::get_autostart_enabled,
|
||||
settings::autostart::set_autostart_enabled,
|
||||
settings::get_locale_settings,
|
||||
@@ -136,7 +133,6 @@ fn specta_builder() -> tauri_specta::Builder<tauri::Wry> {
|
||||
interactions::FriendInteractionReceived,
|
||||
puppet::PuppetStatesChanged,
|
||||
scene_configuration::SceneConfigurationChanged,
|
||||
onboarding::OnboardingStatus,
|
||||
settings::LocaleChanged,
|
||||
])
|
||||
}
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
use std::{
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use tauri::{AppHandle, Manager};
|
||||
|
||||
pub struct AccessibilityPermissionState(AtomicBool);
|
||||
|
||||
impl AccessibilityPermissionState {
|
||||
fn new(granted: bool) -> Self {
|
||||
Self(AtomicBool::new(granted))
|
||||
}
|
||||
|
||||
fn replace(&self, granted: bool) -> bool {
|
||||
self.0.swap(granted, Ordering::AcqRel) != granted
|
||||
}
|
||||
|
||||
fn granted(&self) -> bool {
|
||||
self.0.load(Ordering::Acquire)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn system_accessibility_permission_granted() -> bool {
|
||||
macos_accessibility_client::accessibility::application_is_trusted()
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
fn system_accessibility_permission_granted() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn request_system_accessibility_permission() -> bool {
|
||||
macos_accessibility_client::accessibility::application_is_trusted_with_prompt()
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
fn request_system_accessibility_permission() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
pub fn accessibility_permission_granted(handle: &AppHandle) -> bool {
|
||||
if !cfg!(target_os = "macos") {
|
||||
return true;
|
||||
}
|
||||
handle.state::<AccessibilityPermissionState>().granted()
|
||||
}
|
||||
|
||||
async fn apply_permission_state(handle: &AppHandle, granted: bool) -> Result<bool, String> {
|
||||
if !handle
|
||||
.state::<AccessibilityPermissionState>()
|
||||
.replace(granted)
|
||||
{
|
||||
return Ok(false);
|
||||
}
|
||||
let database = handle.state();
|
||||
let status = crate::onboarding::emit_status(handle, &database).await?;
|
||||
if status.onboarding_done {
|
||||
if granted {
|
||||
crate::application::reconcile_cursor(handle).await;
|
||||
} else {
|
||||
crate::ui::onboarding::show_accessibility_page(handle).await?;
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub async fn init(handle: &AppHandle) -> Result<bool, String> {
|
||||
let granted = system_accessibility_permission_granted();
|
||||
handle.manage(AccessibilityPermissionState::new(granted));
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let handle = handle.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let mut interval = tokio::time::interval(Duration::from_secs(1));
|
||||
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
|
||||
interval.tick().await;
|
||||
|
||||
loop {
|
||||
interval.tick().await;
|
||||
let granted = system_accessibility_permission_granted();
|
||||
if let Err(error) = apply_permission_state(&handle, granted).await {
|
||||
eprintln!("failed to update macOS Accessibility permission: {error}");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Ok(granted)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn request_accessibility_permission(handle: AppHandle) -> Result<bool, String> {
|
||||
let granted = tauri::async_runtime::spawn_blocking(request_system_accessibility_permission)
|
||||
.await
|
||||
.map_err(|error| error.to_string())?;
|
||||
apply_permission_state(&handle, granted).await?;
|
||||
Ok(granted)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn permission_state_reports_only_actual_transitions() {
|
||||
let state = AccessibilityPermissionState::new(false);
|
||||
|
||||
assert!(!state.replace(false));
|
||||
assert!(state.replace(true));
|
||||
assert!(!state.replace(true));
|
||||
assert!(state.replace(false));
|
||||
}
|
||||
}
|
||||
@@ -1,56 +1,6 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use tauri::{AppHandle, State};
|
||||
use tauri_specta::Event;
|
||||
|
||||
use crate::db::{self, AppDatabase};
|
||||
use crate::settings::AppSettings;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Type, Event)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct OnboardingStatus {
|
||||
pub onboarding_done: bool,
|
||||
pub macos_accessibility_permission_granted: bool,
|
||||
pub requires_accessibility_permission: bool,
|
||||
}
|
||||
|
||||
impl OnboardingStatus {
|
||||
fn new(handle: &AppHandle, settings: AppSettings) -> Self {
|
||||
Self {
|
||||
onboarding_done: settings.onboarding_done,
|
||||
macos_accessibility_permission_granted: crate::macos::accessibility_permission_granted(
|
||||
handle,
|
||||
),
|
||||
requires_accessibility_permission: cfg!(target_os = "macos"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn emit_status(
|
||||
handle: &AppHandle,
|
||||
database: &AppDatabase,
|
||||
) -> Result<OnboardingStatus, String> {
|
||||
let status = OnboardingStatus::new(
|
||||
handle,
|
||||
crate::settings::get(database)
|
||||
.await
|
||||
.map_err(db::command_error)?,
|
||||
);
|
||||
status.clone().emit(handle).map_err(db::command_error)?;
|
||||
Ok(status)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_onboarding_status(
|
||||
handle: AppHandle,
|
||||
database: State<'_, AppDatabase>,
|
||||
) -> Result<OnboardingStatus, String> {
|
||||
let settings = crate::settings::get(&database)
|
||||
.await
|
||||
.map_err(db::command_error)?;
|
||||
Ok(OnboardingStatus::new(&handle, settings))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
@@ -58,15 +8,10 @@ pub async fn complete_onboarding(
|
||||
handle: AppHandle,
|
||||
database: State<'_, AppDatabase>,
|
||||
) -> Result<(), String> {
|
||||
if cfg!(target_os = "macos") && !crate::macos::accessibility_permission_granted(&handle) {
|
||||
return Err("Accessibility access must be granted before setup can finish.".to_owned());
|
||||
}
|
||||
|
||||
crate::settings::set_onboarding_done(&database, true)
|
||||
.await
|
||||
.map_err(db::command_error)?;
|
||||
emit_status(&handle, &database).await?;
|
||||
crate::application::start(&handle).await;
|
||||
crate::application::start(&handle);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -75,6 +20,7 @@ mod tests {
|
||||
use sqlx::sqlite::SqlitePoolOptions;
|
||||
|
||||
use super::*;
|
||||
use crate::settings::AppSettings;
|
||||
|
||||
async fn database() -> AppDatabase {
|
||||
let pool = SqlitePoolOptions::new()
|
||||
|
||||
@@ -2,37 +2,30 @@ use tauri::{AppHandle, Manager, WebviewUrl};
|
||||
|
||||
const WINDOW_LABEL: &str = "onboarding";
|
||||
|
||||
async fn show(handle: &AppHandle, accessibility_only: bool) -> Result<(), String> {
|
||||
pub async fn show_initial(handle: &AppHandle) -> Result<(), String> {
|
||||
if let Some(window) = handle.get_webview_window(WINDOW_LABEL) {
|
||||
if accessibility_only {
|
||||
window
|
||||
.eval("window.location.href = '/onboarding?step=accessibility'")
|
||||
.map_err(|error| error.to_string())?;
|
||||
}
|
||||
window.unminimize().map_err(|error| error.to_string())?;
|
||||
window.show().map_err(|error| error.to_string())?;
|
||||
return window.set_focus().map_err(|error| error.to_string());
|
||||
}
|
||||
|
||||
let page = if accessibility_only {
|
||||
"/onboarding?step=accessibility"
|
||||
} else {
|
||||
"/onboarding"
|
||||
};
|
||||
let window =
|
||||
tauri::WebviewWindowBuilder::new(handle, WINDOW_LABEL, WebviewUrl::App(page.into()))
|
||||
.title(crate::settings::text(
|
||||
handle,
|
||||
crate::settings::NativeText::OnboardingTitle,
|
||||
))
|
||||
.inner_size(680.0, 520.0)
|
||||
.min_inner_size(680.0, 520.0)
|
||||
.transparent(true)
|
||||
.resizable(false)
|
||||
.maximizable(false)
|
||||
.center()
|
||||
.build()
|
||||
.map_err(|error| error.to_string())?;
|
||||
let window = tauri::WebviewWindowBuilder::new(
|
||||
handle,
|
||||
WINDOW_LABEL,
|
||||
WebviewUrl::App("/onboarding".into()),
|
||||
)
|
||||
.title(crate::settings::text(
|
||||
handle,
|
||||
crate::settings::NativeText::OnboardingTitle,
|
||||
))
|
||||
.inner_size(680.0, 520.0)
|
||||
.min_inner_size(680.0, 520.0)
|
||||
.transparent(true)
|
||||
.resizable(false)
|
||||
.maximizable(false)
|
||||
.center()
|
||||
.build()
|
||||
.map_err(|error| error.to_string())?;
|
||||
|
||||
let handle = handle.clone();
|
||||
window.on_window_event(move |event| {
|
||||
@@ -44,11 +37,3 @@ async fn show(handle: &AppHandle, accessibility_only: bool) -> Result<(), String
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn show_initial(handle: &AppHandle) -> Result<(), String> {
|
||||
show(handle, false).await
|
||||
}
|
||||
|
||||
pub async fn show_accessibility_page(handle: &AppHandle) -> Result<(), String> {
|
||||
show(handle, true).await
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::{sync::RwLock, time::Duration};
|
||||
use std::sync::RwLock;
|
||||
|
||||
use serde::Deserialize;
|
||||
use specta::Type;
|
||||
@@ -82,15 +82,22 @@ pub const WINDOW_LABEL: &str = "scene";
|
||||
fn track_scene_hitboxes(app_handle: AppHandle, window: tauri::WebviewWindow) {
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let mut ignores_cursor = true;
|
||||
let mut interval = tokio::time::interval(Duration::from_millis(16));
|
||||
let mut interval = tokio::time::interval(crate::cursor::SYSTEM_CURSOR_POLL_INTERVAL);
|
||||
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
|
||||
|
||||
loop {
|
||||
interval.tick().await;
|
||||
|
||||
let cursor = match app_handle.cursor_position() {
|
||||
Ok(cursor) => cursor,
|
||||
let cursor = match app_handle
|
||||
.state::<crate::cursor::CursorPositionProvider>()
|
||||
.latest()
|
||||
{
|
||||
Ok(Some(cursor)) => cursor,
|
||||
Ok(None) => continue,
|
||||
Err(error) => {
|
||||
eprintln!("Failed to read cursor position for scene hit-testing: {error}");
|
||||
eprintln!(
|
||||
"Failed to read shared cursor position for scene hit-testing: {error}"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user