mirror of
https://github.com/feschber/lan-mouse.git
synced 2026-05-08 07:08:05 +03:00
On macOS the three TCC grants (Accessibility, Input Monitoring, Post Event) live in separate Privacy panes. Before this change the "Reenable" row sent the user to Accessibility regardless of which grant was actually missing, and the daemon's own permission checks re-fired the Accessibility prompt on every retry. - lan-mouse-gtk/src/macos_privacy.rs: new module that exposes silent preflight checks (AXIsProcessTrusted, CGPreflightListenEventAccess, CGPreflightPostEventAccess), per-pane URL-scheme navigation, and a Once-guarded fire_initial_prompts() called from build_ui. The initial-prompt path only fires the Accessibility prompt if AX is missing and then returns; secondary registrations run only after AX is granted, which prevents a double Accessibility alert on Sequoia where Post Event is nested under Accessibility. - Input Monitoring registration attempts CGEventTapCreate at kCGSessionEventTap (not kCGHIDEventTap) so a failure surfaces as an Input Monitoring signal rather than triggering an Accessibility prompt as a side effect. - lan-mouse-gtk/src/window/imp.rs: handle_capture / handle_emulation switch on the missing-pane enum and navigate to the specific pane via x-apple.systempreferences:... URLs before re-requesting. - lan-mouse-gtk/resources/window.ui: pill class on the Reenable buttons so the hover padding matches the rest of libadwaita. - input-capture/src/macos.rs, input-emulation/src/macos.rs: make request_*_permission() a silent preflight (AXIsProcessTrusted / CGPreflightListenEventAccess / CGPreflightPostEventAccess), so the daemon no longer fires TCC prompts on retry — all prompting is owned by the GUI. - input-capture/src/error.rs, input-emulation/src/error.rs: new error variants so the GUI can distinguish missing-AX from missing-IM / missing-PostEvent for pane routing. Verified on macOS 15.5: first launch fires a single AX prompt; second launch (AX granted) registers under Input Monitoring via the session-tap attempt and requests Post Event. Sequoia auto-grants the listen-only path via AX so the IM list may stay empty, which is the intended OS behavior and no longer blocks capture. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
166 lines
4.9 KiB
Rust
166 lines
4.9 KiB
Rust
#[derive(Debug, Error)]
|
|
pub enum InputEmulationError {
|
|
#[error("error creating input-emulation: `{0}`")]
|
|
Create(#[from] EmulationCreationError),
|
|
#[error("error emulating input: `{0}`")]
|
|
Emulate(#[from] EmulationError),
|
|
}
|
|
|
|
#[cfg(all(
|
|
unix,
|
|
any(feature = "remote_desktop_portal", feature = "libei"),
|
|
not(target_os = "macos")
|
|
))]
|
|
use ashpd::{Error::Response, desktop::ResponseError};
|
|
use std::io;
|
|
use thiserror::Error;
|
|
|
|
#[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
|
|
use wayland_client::{
|
|
ConnectError, DispatchError,
|
|
backend::WaylandError,
|
|
globals::{BindError, GlobalError},
|
|
};
|
|
|
|
#[derive(Debug, Error)]
|
|
pub enum EmulationError {
|
|
#[error("event stream closed")]
|
|
EndOfStream,
|
|
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
|
|
#[error("libei error: `{0}`")]
|
|
Libei(#[from] reis::Error),
|
|
#[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
|
|
#[error("wayland error: `{0}`")]
|
|
Wayland(#[from] wayland_client::backend::WaylandError),
|
|
#[cfg(all(
|
|
unix,
|
|
any(feature = "remote_desktop_portal", feature = "libei"),
|
|
not(target_os = "macos")
|
|
))]
|
|
#[error("xdg-desktop-portal: `{0}`")]
|
|
Ashpd(#[from] ashpd::Error),
|
|
#[error("io error: `{0}`")]
|
|
Io(#[from] io::Error),
|
|
}
|
|
|
|
#[derive(Debug, Error)]
|
|
pub enum EmulationCreationError {
|
|
#[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
|
|
#[error("wlroots backend: `{0}`")]
|
|
Wlroots(#[from] WlrootsEmulationCreationError),
|
|
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
|
|
#[error("libei backend: `{0}`")]
|
|
Libei(#[from] LibeiEmulationCreationError),
|
|
#[cfg(all(unix, feature = "remote_desktop_portal", not(target_os = "macos")))]
|
|
#[error("xdg-desktop-portal: `{0}`")]
|
|
Xdp(#[from] XdpEmulationCreationError),
|
|
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
|
|
#[error("x11: `{0}`")]
|
|
X11(#[from] X11EmulationCreationError),
|
|
#[cfg(target_os = "macos")]
|
|
#[error("macos: `{0}`")]
|
|
MacOs(#[from] MacOSEmulationCreationError),
|
|
#[cfg(windows)]
|
|
#[error("windows: `{0}`")]
|
|
Windows(#[from] WindowsEmulationCreationError),
|
|
#[error("capture error")]
|
|
NoAvailableBackend,
|
|
}
|
|
|
|
impl EmulationCreationError {
|
|
/// request was intentionally denied by the user
|
|
pub(crate) fn cancelled_by_user(&self) -> bool {
|
|
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
|
|
if matches!(
|
|
self,
|
|
EmulationCreationError::Libei(LibeiEmulationCreationError::Ashpd(Response(
|
|
ResponseError::Cancelled,
|
|
)))
|
|
) {
|
|
return true;
|
|
}
|
|
#[cfg(all(unix, feature = "remote_desktop_portal", not(target_os = "macos")))]
|
|
if matches!(
|
|
self,
|
|
EmulationCreationError::Xdp(XdpEmulationCreationError::Ashpd(Response(
|
|
ResponseError::Cancelled,
|
|
)))
|
|
) {
|
|
return true;
|
|
}
|
|
false
|
|
}
|
|
}
|
|
|
|
#[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
|
|
#[derive(Debug, Error)]
|
|
pub enum WlrootsEmulationCreationError {
|
|
#[error(transparent)]
|
|
Connect(#[from] ConnectError),
|
|
#[error(transparent)]
|
|
Global(#[from] GlobalError),
|
|
#[error(transparent)]
|
|
Wayland(#[from] WaylandError),
|
|
#[error(transparent)]
|
|
Bind(#[from] WaylandBindError),
|
|
#[error(transparent)]
|
|
Dispatch(#[from] DispatchError),
|
|
#[error(transparent)]
|
|
Io(#[from] std::io::Error),
|
|
}
|
|
|
|
#[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
|
|
#[derive(Debug, Error)]
|
|
#[error("wayland protocol \"{protocol}\" not supported: {inner}")]
|
|
pub struct WaylandBindError {
|
|
inner: BindError,
|
|
protocol: &'static str,
|
|
}
|
|
|
|
#[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
|
|
impl WaylandBindError {
|
|
pub(crate) fn new(inner: BindError, protocol: &'static str) -> Self {
|
|
Self { inner, protocol }
|
|
}
|
|
}
|
|
|
|
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
|
|
#[derive(Debug, Error)]
|
|
pub enum LibeiEmulationCreationError {
|
|
#[error(transparent)]
|
|
Ashpd(#[from] ashpd::Error),
|
|
#[error(transparent)]
|
|
Io(#[from] std::io::Error),
|
|
#[error(transparent)]
|
|
Reis(#[from] reis::Error),
|
|
}
|
|
|
|
#[cfg(all(unix, feature = "remote_desktop_portal", not(target_os = "macos")))]
|
|
#[derive(Debug, Error)]
|
|
pub enum XdpEmulationCreationError {
|
|
#[error(transparent)]
|
|
Ashpd(#[from] ashpd::Error),
|
|
}
|
|
|
|
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
|
|
#[derive(Debug, Error)]
|
|
pub enum X11EmulationCreationError {
|
|
#[error("could not open display")]
|
|
OpenDisplay,
|
|
}
|
|
|
|
#[cfg(target_os = "macos")]
|
|
#[derive(Debug, Error)]
|
|
pub enum MacOSEmulationCreationError {
|
|
#[error("could not create event source")]
|
|
EventSourceCreation,
|
|
#[error("accessibility permission is required")]
|
|
AccessibilityPermission,
|
|
#[error("input control permission is required")]
|
|
InputControlPermission,
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
#[derive(Debug, Error)]
|
|
pub enum WindowsEmulationCreationError {}
|