C+
Packages · View as Markdown

permissions

Ask the platform for access, and know the answer without asking.

[dependencies]
permissions = "*"

[macos.dependencies]
objc = "*"      # the Apple half's msgSend; brings -framework Foundation

[ios.dependencies]
objc = "*"

[android.dependencies]
jni          = "*"   # checkSelfPermission / requestPermissions, reflectively
android_view = "*"
facet        = "*"   # app_events: the Activity and the JavaVM
flex_layout  = "*"   # facet's closure, restated
events       = "*"

Name only the platforms you build for. The resolver validates every import against ONE flat set taken from your manifest — it does not read a dependency's own — so a package's transitive deps are named again here. Miss one and the link says which symbol.

import "permissions/permissions" as permissions;

Common case

Read a state, ask when it can be asked, answer through a callback.

fn answered(name: str, s: permissions::State, ctx: *u8) {
    if s == permissions::State::Granted { start_capture(); }
}

match permissions::state(of: permissions::CAMERA) {
    permissions::State::Granted => { start_capture(); }
    permissions::State::Blocked => { let _ = permissions::open_settings(pane: permissions::CAMERA); }
    _ => { let _s = permissions::request(permissions::CAMERA, on_answer: answered); }
}

A permission is a name, not an enum: the constants exist so a typo is a compile error, and a bare string is the escape hatch for anything this package has never heard of (permissions::state(of: "android.permission.NFC")).

Two things that will bite you

  • The app's own manifest is a hard prerequisite and nothing enforces it. On Apple, a missing Info.plist usage-description key leaves state working normally and makes request kill the process — asynchronously, after the call has already returned. On Android a missing <uses-permission> makes the read answer denied forever with no dialog. See guide.md.
  • Denied and Blocked are different. Denied means asking again shows a dialog; Blocked means it does nothing and the only road left is open_settings. Collapsing them produces a button that does nothing.

Docs

Need File
Use it in minutes docs/tutorial.md
How / why / gotchas docs/guide.md
Exact signatures docs/ref.md

Platforms

Platform State Diverges by
macOS, iOS full one file; notifications need a signed bundle, so macOS answers Unsupported for it
Android full, including location one dialog per batch, a persisted "have asked" bit, an API 33 fork for notifications
Windows NOTIFICATIONS real, everything else Unsupported; open_settings is real a desktop process has no per-app authorization object to read — see the guide
Linux Unsupported for everything xdg-desktop-portal is parked, and it has no check-without-asking

Tests

Unit tests are #[test] fns beside the code, with src/test_main.cplus as the discovery root:

cd vendor/permissions && cpc test          # 183 checks, macOS host
tools/run_embedded_plist_probe.sh          # 2 rows, one build each

The probe covers what a #[test] cannot: cpc test links a bare binary and never embeds macos/Info.plist, so the row where an embedded plist gave this package a bundle identifier without a bundle — and turned a guarded refusal into SIGABRT — takes a real cpc build to reach. It builds one project twice and checks both runs survive and both still answer Unsupported.

tests/ holds the iOS runner — a package rather than flat files, because the checks need UIKit and a bundle, so they have to be an app. It is driven by tools/run_ios_tests.sh, which also grants and revokes through xcrun simctl privacy and asserts the reads follow. playground/permprobe is the Android probe for the one thing no harness can do: tap Deny twice.