Device capabilities (parts)

A part exposes a platform capability without a UI, such as battery status, storage, or sensors. It provides a shared Rust API with an implementation selected at compile time for the target operating system. Parts are Cargo dependencies and require no runtime plugin registry.

The catalog

CrateWhat it doesReference
day-part-batterycharge level and charging statebattery
day-part-clipboardread/write the system clipboard (text)clipboard
day-part-prefssmall key-value preference storage in the platform’s conventional locationprefs
day-part-fsapp-local file storage: read, write, remove, list (sync and async)fs
day-part-local-notifylocal notifications: post now or schedule, channels, tap-to-routenotify
day-part-networkconnectivity statusnetwork
day-part-deviceinfodevice model, OS versiondeviceinfo
day-part-sensorsaccelerometer and friends, as a live streamsensors
day-part-httpHTTP through each platform’s networking stackhttp
day-part-permissionsask the OS for the camera, location, notifications … and declare them at build timepermissions
day-part-locationthe device’s position, once or as a live streamlocation
day-part-hapticshaptic feedbackhaptics
day-part-soundsound effectssound
day-part-wakelockkeeping the screen onwakelock
day-part-speechtext to speech through each platform’s speech API; in a separate repositoryday-part-speech

Using parts

The APIs are small. Here are some examples from the crates:

// Battery
if let Some(b) = day_part_battery::status() {
    println!("{:?}, {:?}%", b.state, b.percent());   // Charging, Some(80)
}

// Clipboard
day_part_clipboard::set_text("hello");
let text = day_part_clipboard::get_text();           // Option<String>

// Preferences — strings in, strings out, stored where the platform expects
day_part_prefs::set("theme", "dark");
let theme = day_part_prefs::get("theme");            // Option<String>

Wiring a part into UI is the usual reactive pattern (read into a signal, bind the signal):

let battery = Signal::new(day_part_battery::status());

column((
    label(move || match battery.get() {
        Some(b) => format!("{}%", b.percent().unwrap_or(0)),
        None => tr("battery_unknown").format(),
    }),
    button(tr("refresh")).action(move || battery.set(day_part_battery::status())),
))

Returns are Option/bool rather than panics: a desktop without a battery reports None, a denied clipboard read reports None, and your UI decides what that means. Check each part’s reference page for the per-platform support matrix; not every capability exists everywhere.

Implementing a part

When you need a platform API Day doesn’t cover (Bluetooth, a payment SDK, notification badges), you write a part. The implementation depends on the platform API:

  • Pure-Rust platforms are a #[cfg] branch and a system crate (objc2 on Apple, windows on Windows, sysfs/D-Bus on Linux).
  • Android usually needs a small Java shim; a part can carry its own Java sources, Android resources, Gradle dependencies, ProGuard keep rules, and even manifest components (a BroadcastReceiver for a scheduled notification), all declared in Cargo metadata and aggregated into the app’s Gradle project by day build, so the scaffold needs no manual edits.
  • The same channel covers the other platforms: system frameworks and Swift for iOS and macOS ([package.metadata.day.ios] / [package.metadata.day.macos]), ArkTS sources for HarmonyOS.
  • Or write the platform half inline in your Rust file, one arm per platform, and let the build generate both sides of the call (see below).
  • Permissions a part needs (say, vibration) are declared in the part’s metadata and merged into each platform’s manifest the same way.

day new part my-part scaffolds that layout with per-OS stubs. The part tutorial walks through a complete real example (a battery part with six platform implementations) and is the best template for your own.

Foreign code, inline

A part whose platform half is a function rather than a directory of shims can declare it once in Rust and provide platform implementations in the same file:

day_bridge::bridge! {
    #[day_bridge::declare]
    extern "day" {
        fn speak_native(text: &str) -> Result<(), day_bridge::Error>;
    }

    #[day_bridge::impl(swift, platforms = [ios, macos])]
    swift!(
        prelude = r#"
            import AVFoundation
        "#,
        body = r#"
            func speak_native(text: String) throws { … }
        "#,
    );

    #[day_bridge::impl(rust, platforms = [other])]
    fn speak_native(_text: &str) -> Result<(), day_bridge::Error> {
        Err(day_bridge::Error::Unsupported)
    }
}

The build generates the Swift adapter, the JNI binding, the ES module, or the C translation unit, plus the Rust that calls it. The crate still compiles with plain cargo test on a machine with none of those toolchains, because the last arm answers everywhere else. day-part-speech carries six languages in one file this way; the bridge reference is the contract, and day-part-speech is the worked example, in its own repository.

Parts are for headless capabilities only. The moment your capability needs to render something, it’s a piece, and a different set of tools applies.