macos: drop the CapturePane / EmulationPane enums

Now that we always route Reenable clicks to the Accessibility pane on
macOS 13+ (AX transitively covers Input Monitoring listen-only and
Post Event, and the bundle isn't listed in the separate panes anyway),
the CapturePane / EmulationPane enums and their non-Accessibility
variants are dead weight. Remove them along with:

- `missing_capture_pane` / `missing_emulation_pane`
- `open_input_monitoring_settings` / `open_post_event_settings`
- `input_monitoring_granted` / `post_event_granted` preflight wrappers
- the `CGPreflightListenEventAccess` / `CGPreflightPostEventAccess`
  FFI declarations in lan-mouse-gtk (the daemon crates keep their own)

`handle_capture` / `handle_emulation` collapse to a single helper that
opens the Accessibility pane if AX is missing, otherwise just retries.
`ensure_listed_in_input_monitoring` is kept because it still has a
side effect on macOS 13/14, where Input Monitoring is a separately-
granted category.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jon Kinney
2026-04-24 08:35:10 -05:00
committed by Ferdinand Schober
parent b3cade9bac
commit 8a444f98dd
2 changed files with 18 additions and 113 deletions

View File

@@ -2,10 +2,12 @@
//! Tiny macOS Privacy-pane helpers used by the GUI. //! Tiny macOS Privacy-pane helpers used by the GUI.
//! //!
//! Clicking "Reenable" on the capture/emulation warning row should take the //! On macOS 13+, the Accessibility grant transitively confers the
//! user to whichever Privacy pane is actually missing a grant — opening the //! listen-only event-tap privilege that Input Monitoring gates and the
//! Accessibility pane when the user has already granted Accessibility (and //! synthesize-event privilege that Post Event gates, and the bundle
//! only needs Input Monitoring) is confusing and hides the real request. //! typically isn't even listed in those separate panes. So the single
//! user-facing action for any missing-capture or missing-emulation
//! scenario is "re-toggle Accessibility" — we don't route elsewhere.
use std::ffi::{c_uchar, c_void}; use std::ffi::{c_uchar, c_void};
use std::process::Command; use std::process::Command;
@@ -48,9 +50,7 @@ extern "C" {
#[link(name = "CoreGraphics", kind = "framework")] #[link(name = "CoreGraphics", kind = "framework")]
extern "C" { extern "C" {
fn CGPreflightListenEventAccess() -> c_uchar;
fn CGRequestListenEventAccess() -> c_uchar; fn CGRequestListenEventAccess() -> c_uchar;
fn CGPreflightPostEventAccess() -> c_uchar;
fn CGRequestPostEventAccess() -> c_uchar; fn CGRequestPostEventAccess() -> c_uchar;
// CFMachPortRef CGEventTapCreate( // CFMachPortRef CGEventTapCreate(
@@ -73,67 +73,6 @@ pub fn accessibility_granted() -> bool {
raw != 0 raw != 0
} }
pub fn input_monitoring_granted() -> bool {
let raw = unsafe { CGPreflightListenEventAccess() };
log::debug!("CGPreflightListenEventAccess() = {raw}");
raw != 0
}
pub fn post_event_granted() -> bool {
let raw = unsafe { CGPreflightPostEventAccess() };
log::debug!("CGPreflightPostEventAccess() = {raw}");
raw != 0
}
// Variants `InputMonitoring` and `PostEvent` are currently never returned
// by `missing_capture_pane` / `missing_emulation_pane` — on macOS 13+ those
// categories auto-grant via Accessibility and the bundle typically isn't
// listed in those separate panes, so routing users there is a dead end.
// Kept in the enum so older-macOS behavior can be restored without a
// structural change.
#[allow(dead_code)]
pub enum CapturePane {
Accessibility,
InputMonitoring,
/// Everything is already granted; the caller should just retry.
None,
}
#[allow(dead_code)]
pub enum EmulationPane {
Accessibility,
PostEvent,
None,
}
pub fn missing_capture_pane() -> CapturePane {
if !accessibility_granted() {
CapturePane::Accessibility
} else if !input_monitoring_granted() {
// On macOS 13+, Accessibility trust confers the listen-only
// event-tap privilege that Input Monitoring gates, and on Sequoia
// the bundle typically isn't listed in the Input Monitoring pane
// at all. The actionable fix when IM preflight is still 0 is to
// re-toggle Accessibility, so send the user there rather than to
// an empty IM list.
CapturePane::Accessibility
} else {
CapturePane::None
}
}
pub fn missing_emulation_pane() -> EmulationPane {
if !accessibility_granted() {
EmulationPane::Accessibility
} else if !post_event_granted() {
// Post Event is nested under Accessibility on modern macOS and
// auto-grants alongside it. Point the user back to Accessibility
// for the same reason as the capture case above.
EmulationPane::Accessibility
} else {
EmulationPane::None
}
}
/// Poll for an Accessibility grant transition. Starts a 1-second GLib /// Poll for an Accessibility grant transition. Starts a 1-second GLib
/// timer that fires `on_granted` once, the first time /// timer that fires `on_granted` once, the first time
@@ -165,20 +104,6 @@ pub fn open_accessibility_settings() {
open_url("x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility"); open_url("x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility");
} }
pub fn open_input_monitoring_settings() {
unsafe {
ensure_listed_in_input_monitoring();
}
open_url("x-apple.systempreferences:com.apple.preference.security?Privacy_ListenEvent");
}
pub fn open_post_event_settings() {
unsafe {
CGRequestPostEventAccess();
}
open_url("x-apple.systempreferences:com.apple.preference.security?Privacy_PostEvent");
}
/// Make sure the app appears in System Settings → Privacy → Input Monitoring. /// Make sure the app appears in System Settings → Privacy → Input Monitoring.
/// ///
/// `CGRequestListenEventAccess()` is *supposed* to register the app in the /// `CGRequestListenEventAccess()` is *supposed* to register the app in the

View File

@@ -10,6 +10,16 @@ use lan_mouse_ipc::{DEFAULT_PORT, FrontendRequestWriter};
use crate::authorization_window::AuthorizationWindow; use crate::authorization_window::AuthorizationWindow;
#[cfg(target_os = "macos")]
fn open_accessibility_if_missing(ctx: &str) {
if crate::macos_privacy::accessibility_granted() {
log::info!("{ctx}: Accessibility already granted, retry only");
} else {
log::info!("{ctx}: opening Accessibility pane");
crate::macos_privacy::open_accessibility_settings();
}
}
#[derive(CompositeTemplate, Default)] #[derive(CompositeTemplate, Default)]
#[template(resource = "/de/feschber/LanMouse/window.ui")] #[template(resource = "/de/feschber/LanMouse/window.ui")]
pub struct Window { pub struct Window {
@@ -143,44 +153,14 @@ impl Window {
#[template_callback] #[template_callback]
fn handle_emulation(&self) { fn handle_emulation(&self) {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
{ open_accessibility_if_missing("Reenable emulation");
use crate::macos_privacy::{self, EmulationPane};
match macos_privacy::missing_emulation_pane() {
EmulationPane::Accessibility => {
log::info!("Reenable emulation: opening Accessibility pane");
macos_privacy::open_accessibility_settings();
}
EmulationPane::PostEvent => {
log::info!("Reenable emulation: opening Post Event pane");
macos_privacy::open_post_event_settings();
}
EmulationPane::None => {
log::info!("Reenable emulation: both grants present, retry only");
}
}
}
self.obj().request_emulation(); self.obj().request_emulation();
} }
#[template_callback] #[template_callback]
fn handle_capture(&self) { fn handle_capture(&self) {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
{ open_accessibility_if_missing("Reenable capture");
use crate::macos_privacy::{self, CapturePane};
match macos_privacy::missing_capture_pane() {
CapturePane::Accessibility => {
log::info!("Reenable capture: opening Accessibility pane");
macos_privacy::open_accessibility_settings();
}
CapturePane::InputMonitoring => {
log::info!("Reenable capture: opening Input Monitoring pane");
macos_privacy::open_input_monitoring_settings();
}
CapturePane::None => {
log::info!("Reenable capture: both grants present, retry only");
}
}
}
self.obj().request_capture(); self.obj().request_capture();
} }