use objc2_application_services::{
AXIsProcessTrusted, AXIsProcessTrustedWithOptions, kAXTrustedCheckOptionPrompt,
};
use objc2_core_foundation::{CFBoolean, CFDictionary, CFRetained, CFString};
use objc2_core_graphics::{CGPreflightScreenCaptureAccess, CGRequestScreenCaptureAccess};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Permission {
Accessibility,
ScreenRecording,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PermissionStatus {
Granted,
Denied,
}
impl PermissionStatus {
#[must_use]
pub const fn is_granted(self) -> bool {
matches!(self, Self::Granted)
}
}
impl Permission {
#[must_use]
pub const fn settings_url(self) -> &'static str {
match self {
Self::Accessibility => {
"x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility"
}
Self::ScreenRecording => {
"x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture"
}
}
}
#[must_use]
pub fn hint(self) -> String {
let what = match self {
Self::Accessibility => "Accessibility",
Self::ScreenRecording => "Screen Recording",
};
format!(
"grant {what} to this program in System Settings > Privacy & Security > {what}, \
then run it again. Open that pane with:\n open \"{}\"\n\
Note the grant is tied to the binary's path and signature, so a rebuilt \
binary may need granting again.",
self.settings_url()
)
}
}
#[must_use]
pub fn check(permission: Permission) -> PermissionStatus {
let granted = match permission {
Permission::Accessibility => unsafe { AXIsProcessTrusted() },
Permission::ScreenRecording => CGPreflightScreenCaptureAccess(),
};
if granted {
PermissionStatus::Granted
} else {
PermissionStatus::Denied
}
}
pub fn request(permission: Permission) -> PermissionStatus {
match permission {
Permission::Accessibility => {
let key: &CFString = unsafe { kAXTrustedCheckOptionPrompt };
let yes = CFBoolean::new(true);
let options: CFRetained<CFDictionary<CFString, CFBoolean>> =
CFDictionary::from_slices(&[key], &[yes]);
let granted = unsafe { AXIsProcessTrustedWithOptions(Some(options.as_opaque())) };
if granted {
PermissionStatus::Granted
} else {
PermissionStatus::Denied
}
}
Permission::ScreenRecording => {
if CGRequestScreenCaptureAccess() {
PermissionStatus::Granted
} else {
PermissionStatus::Denied
}
}
}
}
#[allow(
dead_code,
reason = "used by the AX-backed operations landing in phase 5"
)]
pub(crate) fn require(permission: Permission) -> crate::Result<()> {
if check(permission).is_granted() {
return Ok(());
}
Err(crate::Error::PermissionDenied {
permission,
hint: permission.hint(),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn settings_urls_are_distinct_and_well_formed() {
let a = Permission::Accessibility.settings_url();
let s = Permission::ScreenRecording.settings_url();
assert_ne!(a, s);
for url in [a, s] {
assert!(url.starts_with("x-apple.systempreferences:"));
}
}
#[test]
fn the_hint_is_actionable_not_merely_accurate() {
let h = Permission::Accessibility.hint();
assert!(h.contains("System Settings"), "{h}");
assert!(h.contains("open \"x-apple.systempreferences:"), "{h}");
assert!(
h.contains("rebuilt"),
"the rebuild gotcha must be mentioned"
);
}
#[test]
fn checking_never_panics_whatever_the_grant_state() {
let _ = check(Permission::Accessibility);
let _ = check(Permission::ScreenRecording);
}
}