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.plistusage-description key leavesstateworking normally and makesrequestkill 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. DeniedandBlockedare different.Deniedmeans asking again shows a dialog;Blockedmeans it does nothing and the only road left isopen_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.