Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions apps/desktop/src-tauri/binaries/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
char-chrome-native-host-*
check-permissions-*
34 changes: 34 additions & 0 deletions apps/desktop/src-tauri/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,39 @@ fn main() {
#[cfg(target_os = "macos")]
println!("cargo:rustc-link-arg=-fapple-link-rtlib");

#[cfg(target_os = "macos")]
build_check_permissions();

tauri_build::build()
}

#[cfg(target_os = "macos")]
fn build_check_permissions() {
let triple = std::env::var("TAURI_ENV_TARGET_TRIPLE")
.unwrap_or_else(|_| "aarch64-apple-darwin".to_string());

let manifest_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
let swift_src = manifest_dir.join("../../../plugins/permissions/swift/check-permissions.swift");
let binaries_dir = manifest_dir.join("binaries");
let dst = binaries_dir.join(format!("check-permissions-{triple}"));

println!("cargo:rerun-if-changed={}", swift_src.display());

if dst.exists() {
return;
}

std::fs::create_dir_all(&binaries_dir).expect("create binaries/");

let status = std::process::Command::new("swiftc")
.args(["-O", "-o"])
.arg(&dst)
.arg(&swift_src)
.status()
.expect("failed to run swiftc");

assert!(
status.success(),
"swiftc failed to compile check-permissions"
);
}
3 changes: 3 additions & 0 deletions apps/desktop/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@
"active": true,
"createUpdaterArtifacts": true,
"targets": "all",
"externalBin": [
"binaries/check-permissions"
],
"icon": [
"icons/stable/32x32.png",
"icons/stable/128x128.png",
Expand Down
1 change: 1 addition & 0 deletions plugins/permissions/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ hypr-audio = { workspace = true }

tauri = { workspace = true, features = ["specta", "test"] }
tauri-plugin-shell = { workspace = true }
tauri-plugin-sidecar2 = { workspace = true }

specta = { workspace = true }
tauri-specta = { workspace = true, features = ["derive", "typescript"] }
Expand Down
80 changes: 80 additions & 0 deletions plugins/permissions/src/ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,22 @@ impl<'a, R: tauri::Runtime, M: tauri::Manager<R>> Permissions<'a, R, M> {
}

pub async fn check(&self, permission: Permission) -> Result<PermissionStatus, crate::Error> {
#[cfg(target_os = "macos")]
{
if matches!(permission, Permission::SystemAudio) {
return self.check_system_audio().await;
}

if let Some(status) = self.check_sidecar(permission).await {
return Ok(status);
}

tracing::warn!(
?permission,
"sidecar unavailable, falling back to in-process check"
);
}

match permission {
Permission::Calendar => self.check_calendar().await,
Permission::Contacts => self.check_contacts().await,
Expand All @@ -56,6 +72,70 @@ impl<'a, R: tauri::Runtime, M: tauri::Manager<R>> Permissions<'a, R, M> {
}
}

#[cfg(target_os = "macos")]
async fn check_sidecar(&self, permission: Permission) -> Option<PermissionStatus> {
use tauri_plugin_sidecar2::Sidecar2PluginExt;

let arg = match permission {
Permission::Calendar => "calendar",
Permission::Contacts => "contacts",
Permission::Microphone => "microphone",
Permission::Accessibility => "accessibility",
Permission::SystemAudio => unreachable!(),
};

let cmd = self
.manager
.sidecar2()
.sidecar("check-permissions")
.ok()?
.args([arg]);

let output = cmd.output().await.ok()?;

if !output.status.success() {
tracing::warn!(
status = ?output.status,
stderr = %String::from_utf8_lossy(&output.stderr),
"check-permissions binary failed"
);
return None;
}

let value = String::from_utf8(output.stdout).ok()?;
let value = value.trim();

let status = match permission {
Permission::Calendar => match value {
"notDetermined" => PermissionStatus::NeverRequested,
"fullAccess" => PermissionStatus::Authorized,
// "restricted" | "denied" | "writeOnly" | _
_ => PermissionStatus::Denied,
},
Permission::Contacts => match value {
"notDetermined" => PermissionStatus::NeverRequested,
"authorized" => PermissionStatus::Authorized,
// "restricted" | "denied" | _
_ => PermissionStatus::Denied,
},
Permission::Microphone => match value {
"notDetermined" => PermissionStatus::NeverRequested,
"authorized" => PermissionStatus::Authorized,
// "restricted" | "denied" | _
_ => PermissionStatus::Denied,
},
Permission::Accessibility => match value {
"trusted" => PermissionStatus::Authorized,
// "untrusted" | _
_ => PermissionStatus::Denied,
},
Permission::SystemAudio => unreachable!(),
};

tracing::debug!(permission = arg, %value, ?status, "check via sidecar");
Some(status)
}

pub async fn request(&self, permission: Permission) -> Result<(), crate::Error> {
match permission {
Permission::Calendar => self.request_calendar().await,
Expand Down
44 changes: 44 additions & 0 deletions plugins/permissions/swift/check-permissions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import AVFoundation
import Contacts
import EventKit
import Foundation

guard CommandLine.arguments.count > 1 else {
fputs("Usage: check-permissions <calendar|contacts|microphone|accessibility>\n", stderr)
exit(1)
}

let permissionType = CommandLine.arguments[1]

switch permissionType {
case "calendar":
switch EKEventStore.authorizationStatus(for: .event) {
case .notDetermined: print("notDetermined")
case .restricted: print("restricted")
case .denied: print("denied")
case .fullAccess: print("fullAccess")
case .writeOnly: print("writeOnly")
@unknown default: print("unknown")
}
case "contacts":
switch CNContactStore.authorizationStatus(for: .contacts) {
case .notDetermined: print("notDetermined")
case .restricted: print("restricted")
case .denied: print("denied")
case .authorized: print("authorized")
@unknown default: print("unknown")
}
case "microphone":
switch AVCaptureDevice.authorizationStatus(for: .audio) {
case .notDetermined: print("notDetermined")
case .restricted: print("restricted")
case .denied: print("denied")
case .authorized: print("authorized")
@unknown default: print("unknown")
}
case "accessibility":
print(AXIsProcessTrusted() ? "trusted" : "untrusted")
default:
fputs("Unknown permission type: \(permissionType)\n", stderr)
exit(1)
}
Loading