runlatch-core 0.2.0

Core data model, provider trait, and built-in providers for runlatch.
Documentation

runlatch-core

The data model, provider abstraction, built-in providers, and registry that power runlatch, a modular Linux autostart manager.

Linux autostart is spread across systemd units, XDG .desktop files, and DE-specific session configs. This crate unifies them behind one trait, AutostartProvider, aggregated by a Registry. New backends (OpenRC, KDE, GNOME, …) are added by implementing the trait — no changes to the core.

Quick start

Enumerate every autostart entry on the machine across all built-in providers:

use runlatch_core::Registry;

# async fn run() -> anyhow::Result<()> {
let registry = Registry::with_defaults();
let result = registry.all_entries().await;

for entry in &result.entries {
    let mark = if entry.enabled { "" } else { "" };
    println!("{mark} {}:{}  {}", entry.source, entry.id, entry.command);
}

// Aggregation never fails as a whole: a provider that errored is reported here
// while every other provider's entries are still returned above.
for err in &result.errors {
    eprintln!("warning: provider '{}' failed: {:#}", err.source, err.error);
}
# Ok(())
# }

The crate is runtime-agnostic — it only ever .awaits, so the binary brings its own runtime (see Architecture).

Writing a provider

A backend is any type implementing AutostartProvider. It must be Send + Sync so the registry can hold it as a Box<dyn AutostartProvider> and share it across tasks. Implement the seven methods, then hand an instance to Registry::new:

use anyhow::Result;
use async_trait::async_trait;
use runlatch_core::{AutostartEntry, AutostartProvider, Registry, Scope};

struct MyProvider;

#[async_trait]
impl AutostartProvider for MyProvider {
    /// Stable id, also the left side of a `source:id` address.
    fn id(&self) -> &'static str { "my-provider" }

    fn scope(&self) -> Scope { Scope::User }

    /// Real runtime detection. Return `false` (never panic) when the backend
    /// isn't present — the registry then silently skips this provider.
    async fn is_available(&self) -> bool { true }

    async fn entries(&self) -> Result<Vec<AutostartEntry>> { Ok(vec![]) }
    async fn enable(&self, _id: &str) -> Result<()> { Ok(()) }
    async fn disable(&self, _id: &str) -> Result<()> { Ok(()) }
    async fn add(&self, _entry: &AutostartEntry) -> Result<()> { Ok(()) }
    async fn remove(&self, _id: &str) -> Result<()> { Ok(()) }
}

# async fn run() {
// Mix your provider in with the built-ins, or run it standalone.
let registry = Registry::new(vec![Box::new(MyProvider)]);
let _ = registry.all_entries().await;
# }

Each AutostartEntry you return is tagged with your id as its source, so callers can address it unambiguously as my-provider:<entry-id>.

Architecture

Async on purpose. The I/O methods are async because probing and querying some backends (notably systemd over D-Bus) can block for a noticeable time. Keeping the core async and runtime-agnostic means it only ever .awaits — it never spins up a runtime or block_ons — so a GUI front-end can drive it without pinning its render thread on a slow bus call.

Aggregation never fails as a whole. Registry::all_entries collects a failing provider's error into AggregateResult::errors while still returning every other provider's good results. One broken backend never blanks the listing.

Order-preserving .desktop editing. The XDG provider toggles entries by setting/removing the Hidden key via DesktopFile, which edits a single key while leaving comments, ordering, and unrelated keys byte-for-byte intact.

License

Licensed under the MIT license.