car-desktop 0.9.0

OS-level screen capture, accessibility inspection, and input synthesis for Common Agent Runtime
Documentation
//! macOS TCC permission checks + prompts for Screen Recording and
//! Accessibility.
//!
//! Both permissions are per-app, user-granted via System Settings
//! > Privacy & Security. Neither prompts can be bypassed
//! programmatically (the OS doesn't allow it), so the request API
//! here opens the prompt the first time, after which the user
//! must return to System Settings and relaunch the app — that's a
//! kernel-level TCC constraint, not something we can paper over.

use std::ffi::c_void;

use core_foundation::base::TCFType;
use core_foundation::boolean::CFBoolean;
use core_foundation::dictionary::CFDictionary;
use core_foundation::string::CFString;

use crate::errors::Result;
use crate::models::{PermissionRequest, PermissionSnapshot};

/// Read current TCC state for Screen Recording + Accessibility
/// without prompting. Safe to call any time, including at app
/// startup before the Bevy camera spins up.
pub fn permissions_impl() -> Result<PermissionSnapshot> {
    let screen_recording = cg_preflight_screen_capture_access();
    let accessibility = ax_is_process_trusted(false);
    Ok(PermissionSnapshot {
        screen_recording,
        accessibility,
        // `permissions()` doesn't prompt so a fresh grant can't
        // have happened via this call; the flag is only set on
        // the request path when the OS accepts a new grant.
        needs_restart: false,
    })
}

/// Trigger the OS permission prompts for anything currently
/// missing. Returns the post-prompt snapshot. Note: macOS will
/// not apply a newly-granted Screen Recording scope until the
/// app is quit and relaunched — we detect that transition via
/// the `needs_restart` flag in the returned snapshot so the
/// caller can render a blocking "please relaunch" UI.
pub fn request_permissions_impl(needs: PermissionRequest) -> Result<PermissionSnapshot> {
    let before = permissions_impl()?;

    if needs.screen_recording && !before.screen_recording {
        // CGRequestScreenCaptureAccess() opens the prompt and
        // returns true if already granted, false if the user
        // needs to approve in System Settings.
        cg_request_screen_capture_access();
    }
    if needs.accessibility && !before.accessibility {
        // Passing the "prompt" option to AXIsProcessTrustedWithOptions
        // surfaces the Accessibility prompt.
        ax_is_process_trusted(true);
    }

    let after = PermissionSnapshot {
        screen_recording: cg_preflight_screen_capture_access(),
        accessibility: ax_is_process_trusted(false),
        // If Screen Recording was denied before AND still denied
        // after (meaning the prompt just appeared), the user needs
        // to flip the switch + relaunch. Accessibility takes
        // effect immediately once granted, so it doesn't force a
        // relaunch on its own.
        needs_restart: needs.screen_recording && !before.screen_recording,
    };
    Ok(after)
}

// ─── Low-level TCC wrappers ────────────────────────────────────

/// `CGPreflightScreenCaptureAccess` — returns the current
/// Screen Recording authorization state without prompting.
/// Available on macOS 10.15+.
fn cg_preflight_screen_capture_access() -> bool {
    unsafe { ffi::CGPreflightScreenCaptureAccess() }
}

/// `CGRequestScreenCaptureAccess` — prompts on first call.
/// Subsequent calls with an approved state return true;
/// subsequent calls with a denied state do NOT re-prompt
/// (macOS won't allow repeat prompts). Available on macOS 10.15+.
fn cg_request_screen_capture_access() -> bool {
    unsafe { ffi::CGRequestScreenCaptureAccess() }
}

/// `AXIsProcessTrustedWithOptions` — either preflights (prompt =
/// false) or prompts (prompt = true). The option key is
/// `kAXTrustedCheckOptionPrompt`.
fn ax_is_process_trusted(prompt: bool) -> bool {
    let key = CFString::new("AXTrustedCheckOptionPrompt");
    let value = CFBoolean::from(prompt);
    let dict = CFDictionary::from_CFType_pairs(&[(key.as_CFType(), value.as_CFType())]);
    unsafe {
        ffi::AXIsProcessTrustedWithOptions(dict.as_concrete_TypeRef() as *const c_void)
    }
}

// ─── Raw FFI bindings ──────────────────────────────────────────

mod ffi {
    use std::ffi::c_void;

    #[link(name = "CoreGraphics", kind = "framework")]
    extern "C" {
        pub fn CGPreflightScreenCaptureAccess() -> bool;
        pub fn CGRequestScreenCaptureAccess() -> bool;
    }

    #[link(name = "ApplicationServices", kind = "framework")]
    extern "C" {
        pub fn AXIsProcessTrustedWithOptions(options: *const c_void) -> bool;
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    /// Preflight is side-effect-free; calling it from a test
    /// always succeeds regardless of the actual permission state.
    #[test]
    fn preflight_returns_a_boolean() {
        let _ = cg_preflight_screen_capture_access();
        let _ = ax_is_process_trusted(false);
    }

    /// permissions_impl wraps the preflights with the snapshot
    /// shape; never errors under normal conditions.
    #[test]
    fn permissions_impl_returns_well_shaped_snapshot() {
        let snap = permissions_impl().expect("permissions_impl never errors");
        // needs_restart is always false on the preflight path.
        assert!(!snap.needs_restart);
    }
}