mirror of
https://github.com/feschber/lan-mouse.git
synced 2026-05-17 19:44:44 +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>
162 lines
5.2 KiB
Rust
162 lines
5.2 KiB
Rust
use thiserror::Error;
|
|
|
|
#[derive(Debug, Error)]
|
|
pub enum InputCaptureError {
|
|
#[error("error creating input-capture: `{0}`")]
|
|
Create(#[from] CaptureCreationError),
|
|
#[error("error while capturing input: `{0}`")]
|
|
Capture(#[from] CaptureError),
|
|
}
|
|
|
|
#[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
|
|
use std::io;
|
|
#[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
|
|
use wayland_client::{
|
|
ConnectError, DispatchError,
|
|
backend::WaylandError,
|
|
globals::{BindError, GlobalError},
|
|
};
|
|
|
|
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
|
|
use ashpd::desktop::ResponseError;
|
|
|
|
#[cfg(target_os = "macos")]
|
|
use core_graphics::base::CGError;
|
|
|
|
#[derive(Debug, Error)]
|
|
pub enum CaptureError {
|
|
#[error("activation stream closed unexpectedly")]
|
|
ActivationClosed,
|
|
#[error("libei stream was closed")]
|
|
EndOfStream,
|
|
#[error("io error: `{0}`")]
|
|
Io(#[from] std::io::Error),
|
|
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
|
|
#[error("libei error: `{0}`")]
|
|
Reis(#[from] reis::Error),
|
|
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
|
|
#[error(transparent)]
|
|
Portal(#[from] ashpd::Error),
|
|
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
|
|
#[error("libei disconnected - reason: `{0}`")]
|
|
Disconnected(String),
|
|
#[cfg(target_os = "macos")]
|
|
#[error("failed to warp mouse cursor: `{0}`")]
|
|
WarpCursor(CGError),
|
|
#[cfg(target_os = "macos")]
|
|
#[error("reset_mouse_position called without a connected client")]
|
|
ResetMouseWithoutClient,
|
|
#[cfg(target_os = "macos")]
|
|
#[error("core-graphics error: {0}")]
|
|
CoreGraphics(CGError),
|
|
#[cfg(target_os = "macos")]
|
|
#[error("unable to map key event: {0}")]
|
|
KeyMapError(i64),
|
|
#[cfg(target_os = "macos")]
|
|
#[error("Event tap disabled")]
|
|
EventTapDisabled,
|
|
}
|
|
|
|
#[derive(Debug, Error)]
|
|
pub enum CaptureCreationError {
|
|
#[error("no backend available")]
|
|
NoAvailableBackend,
|
|
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
|
|
#[error("error creating input-capture-portal backend: `{0}`")]
|
|
Libei(#[from] LibeiCaptureCreationError),
|
|
#[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
|
|
#[error("error creating layer-shell capture backend: `{0}`")]
|
|
LayerShell(#[from] LayerShellCaptureCreationError),
|
|
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
|
|
#[error("error creating x11 capture backend: `{0}`")]
|
|
X11(#[from] X11InputCaptureCreationError),
|
|
#[cfg(windows)]
|
|
#[error("error creating windows capture backend")]
|
|
Windows,
|
|
#[cfg(target_os = "macos")]
|
|
#[error("error creating macos capture backend: `{0}`")]
|
|
MacOS(#[from] MacosCaptureCreationError),
|
|
}
|
|
|
|
impl CaptureCreationError {
|
|
/// request was intentionally denied by the user
|
|
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
|
|
pub(crate) fn cancelled_by_user(&self) -> bool {
|
|
matches!(
|
|
self,
|
|
CaptureCreationError::Libei(LibeiCaptureCreationError::Ashpd(ashpd::Error::Response(
|
|
ResponseError::Cancelled
|
|
)))
|
|
)
|
|
}
|
|
#[cfg(not(all(unix, feature = "libei", not(target_os = "macos"))))]
|
|
pub(crate) fn cancelled_by_user(&self) -> bool {
|
|
false
|
|
}
|
|
}
|
|
|
|
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
|
|
#[derive(Debug, Error)]
|
|
pub enum LibeiCaptureCreationError {
|
|
#[error("xdg-desktop-portal: `{0}`")]
|
|
Ashpd(#[from] ashpd::Error),
|
|
}
|
|
|
|
#[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
|
|
#[derive(Debug, Error)]
|
|
#[error("{protocol} protocol not supported: {inner}")]
|
|
pub struct WaylandBindError {
|
|
inner: BindError,
|
|
protocol: &'static str,
|
|
}
|
|
|
|
#[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
|
|
impl WaylandBindError {
|
|
pub(crate) fn new(inner: BindError, protocol: &'static str) -> Self {
|
|
Self { inner, protocol }
|
|
}
|
|
}
|
|
|
|
#[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
|
|
#[derive(Debug, Error)]
|
|
pub enum LayerShellCaptureCreationError {
|
|
#[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] io::Error),
|
|
}
|
|
|
|
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
|
|
#[derive(Debug, Error)]
|
|
pub enum X11InputCaptureCreationError {
|
|
#[error("X11 input capture is not yet implemented :(")]
|
|
NotImplemented,
|
|
}
|
|
|
|
#[cfg(target_os = "macos")]
|
|
#[derive(Debug, Error)]
|
|
pub enum MacosCaptureCreationError {
|
|
#[error("event source creation failed!")]
|
|
EventSourceCreation,
|
|
#[cfg(target_os = "macos")]
|
|
#[error("event tap creation failed")]
|
|
EventTapCreation,
|
|
#[error("accessibility permission is required")]
|
|
AccessibilityPermission,
|
|
#[error("input monitoring permission is required")]
|
|
InputMonitoringPermission,
|
|
#[error("failed to set CG Cursor property")]
|
|
CGCursorProperty,
|
|
#[cfg(target_os = "macos")]
|
|
#[error("failed to get display ids: {0}")]
|
|
ActiveDisplays(CGError),
|
|
}
|