Skip to main content

ashlar_native/
lib.rs

1//! Native OS features behind one small, safe API: file dialogs, the menu bar, the clipboard,
2//! keep-awake, files opened from the file manager, single instance, notifications, dock / taskbar
3//! progress, the OS keychain and system preferences.
4//!
5//! Every feature is behind a cargo feature and degrades cleanly: on a platform where it is not
6//! implemented (or on the web) a call returns [`NativeError::Unsupported`]; nothing panics. The
7//! crate never depends on a UI toolkit, so apps wire it to their own event loop: activation events
8//! arrive on channels, and callbacks take a waker so an idle app wakes only when something
9//! happened.
10//!
11//! | Module | macOS | Windows | Linux | Web |
12//! |---|---|---|---|---|
13//! | [`dialogs`] | rfd | rfd | rfd (portal) | rfd (bytes only) |
14//! | [`menu`] | muda | muda (per window) | spec only | spec only |
15//! | [`clipboard`] | arboard + NSPasteboard types | arboard | arboard | – |
16//! | [`power`] | NSProcessInfo activity | SetThreadExecutionState | logind inhibit | – |
17//! | [`open_files`] | Apple events + argv | argv | argv | – |
18//! | [`single_instance`] | Unix socket | loopback + token | Unix socket | – |
19//! | [`notify`] | UNUserNotificationCenter (bundled apps) | – | D-Bus notifications | – |
20//! | [`progress`] | NSDockTile | ITaskbarList3 | – | – |
21//! | [`secrets`] | Keychain | Credential Manager | Secret Service | – |
22//! | [`system`] | AppKit | registry / SPI | portal settings | media queries |
23//! | [`cli_tool`] | link + admin prompt | – | link (no prompt) | – |
24//!
25//! [`modal`] (always compiled) is a hook around every blocking dialog, for hang watchdogs.
26//! [`terminate`] (always compiled) runs a hook when the process is asked to stop (SIGINT, SIGTERM,
27//! SIGHUP).
28//!
29//! `unsafe` is denied in this crate and allowed only in the FFI modules that need it, each block
30//! with a `// SAFETY:` comment (docs/adr/0001-native-ffi.md).
31
32mod error;
33pub use error::{NativeError, Result};
34pub mod modal;
35pub mod terminate;
36
37#[cfg(feature = "cli-tool")]
38pub mod cli_tool;
39#[cfg(feature = "clipboard")]
40pub mod clipboard;
41#[cfg(feature = "dialogs")]
42pub mod dialogs;
43#[cfg(feature = "menu")]
44pub mod menu;
45#[cfg(feature = "notify")]
46pub mod notify;
47#[cfg(feature = "open-files")]
48pub mod open_files;
49#[cfg(feature = "power")]
50pub mod power;
51#[cfg(feature = "progress")]
52pub mod progress;
53#[cfg(feature = "secrets")]
54pub mod secrets;
55#[cfg(feature = "single-instance")]
56pub mod single_instance;
57#[cfg(feature = "system")]
58pub mod system;
59
60/// Something that wakes the app's event loop (an egui `request_repaint`, a winit proxy event).
61/// Called from background threads after a new event was queued.
62pub type Waker = std::sync::Arc<dyn Fn() + Send + Sync>;
63
64/// A waker that does nothing, for apps that poll.
65pub fn no_waker() -> Waker {
66    std::sync::Arc::new(|| {})
67}
68
69/// Run `f`, turning a panic inside a third-party binding into an error instead of unwinding into
70/// the app.
71#[allow(dead_code)]
72pub(crate) fn guarded<T>(what: &str, f: impl FnOnce() -> Result<T>) -> Result<T> {
73    std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)).unwrap_or_else(|_| Err(NativeError::Os(format!("{what} failed inside the OS binding"))))
74}
75
76/// The OS's own words for a failure, made safe to show: one line, at most 200 characters.
77#[allow(dead_code)]
78pub(crate) fn os_text(e: impl std::fmt::Display) -> String {
79    let text: String = e.to_string().chars().map(|c| if c.is_control() { ' ' } else { c }).take(200).collect();
80    let text = text.trim();
81    if text.is_empty() { "no reason given".into() } else { text.to_string() }
82}