use std::ffi::{c_uchar, c_void};
use std::process::Command;
use std::sync::Once;
use gtk::glib;
#[link(name = "ApplicationServices", kind = "framework")]
extern "C" {
fn AXIsProcessTrusted() -> c_uchar;
fn AXIsProcessTrustedWithOptions(options: *const c_void) -> c_uchar;
}
#[link(name = "CoreFoundation", kind = "framework")]
extern "C" {
static kCFAllocatorDefault: *const c_void;
static kCFTypeDictionaryKeyCallBacks: *const c_void;
static kCFTypeDictionaryValueCallBacks: *const c_void;
static kCFBooleanTrue: *const c_void;
fn CFDictionaryCreate(
allocator: *const c_void,
keys: *const *const c_void,
values: *const *const c_void,
num: isize,
key_callbacks: *const c_void,
value_callbacks: *const c_void,
) -> *const c_void;
fn CFRelease(cf: *const c_void);
}
#[link(name = "ApplicationServices", kind = "framework")]
extern "C" {
static kAXTrustedCheckOptionPrompt: *const c_void;
}
#[link(name = "CoreGraphics", kind = "framework")]
extern "C" {
fn CGRequestListenEventAccess() -> c_uchar;
fn CGRequestPostEventAccess() -> c_uchar;
fn CGEventTapCreate(
tap: u32,
place: u32,
options: u32,
events_of_interest: u64,
callback: *const c_void,
user_info: *const c_void,
) -> *const c_void;
}
pub fn accessibility_granted() -> bool {
let raw = unsafe { AXIsProcessTrusted() };
log::debug!("AXIsProcessTrusted() = {raw}");
raw != 0
}
pub enum AccessibilityChange {
Granted,
Revoked,
}
pub fn watch_accessibility_state<F>(mut on_change: F)
where
F: FnMut(AccessibilityChange) + 'static,
{
let mut last = accessibility_granted();
log::info!("watching Accessibility state (initial = {last})");
glib::timeout_add_seconds_local(1, move || {
let current = accessibility_granted();
if current != last {
log::info!("Accessibility state flip: {last} -> {current}");
on_change(if current {
AccessibilityChange::Granted
} else {
AccessibilityChange::Revoked
});
last = current;
}
glib::ControlFlow::Continue
});
}
pub fn open_accessibility_settings() {
open_url("x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility");
}
pub fn relaunch_bundle() {
let Ok(exe) = std::env::current_exe() else {
return;
};
let Some(bundle) = exe
.parent()
.and_then(std::path::Path::parent)
.and_then(std::path::Path::parent)
else {
return;
};
let cmd = format!("(sleep 1 && open {bundle:?}) &");
let _ = Command::new("sh").arg("-c").arg(cmd).spawn();
}
unsafe fn ensure_listed_in_input_monitoring() {
let req = CGRequestListenEventAccess();
log::debug!("CGRequestListenEventAccess() = {req}");
let cb = input_monitoring_noop_tap_callback as *const c_void;
let tap = CGEventTapCreate(1, 0, 1, 1 << 10, cb, std::ptr::null());
log::debug!("CGEventTapCreate(kCGSessionEventTap) -> {tap:?}");
if !tap.is_null() {
CFRelease(tap);
}
}
extern "C" fn input_monitoring_noop_tap_callback(
_proxy: *const c_void,
_ty: u32,
event: *const c_void,
_refcon: *const c_void,
) -> *const c_void {
event
}
fn open_url(url: &str) {
if let Err(e) = Command::new("open").arg(url).spawn() {
log::warn!("failed to open {url}: {e}");
}
}
pub fn fire_initial_prompts() {
static FIRED: Once = Once::new();
FIRED.call_once(fire_initial_prompts_inner);
}
fn fire_initial_prompts_inner() {
if !accessibility_granted() {
log::info!("firing first-launch Accessibility prompt");
unsafe {
let key = kAXTrustedCheckOptionPrompt;
let value = kCFBooleanTrue;
let options = CFDictionaryCreate(
kCFAllocatorDefault,
&key as *const _,
&value as *const _,
1,
kCFTypeDictionaryKeyCallBacks,
kCFTypeDictionaryValueCallBacks,
);
AXIsProcessTrustedWithOptions(options);
CFRelease(options);
}
return;
}
log::info!("ensuring Lan Mouse is listed under Input Monitoring");
unsafe {
ensure_listed_in_input_monitoring();
}
log::info!("ensuring Lan Mouse is listed under Accessibility > Post Event");
unsafe {
CGRequestPostEventAccess();
}
}