etyOS

Nirvana: writing applets

Background work and the system

Threads and the handle that wakes the window, streams for live components, and the SDK's helpers for preferences, applications, the machine and the compositor.

render and event must return quickly: while they run, the window waits. Anything that can take a moment — a tool that is run, the network, a folder that is read — happens on a thread.

The handle

run gives the applet a Handle when it is built. It is Clone, and it is the one thing a thread needs:

use std::{sync::{Arc, Mutex}, thread, time::Duration};
use nirvana_protocol::prelude::*;

struct Monitor {
    load: Arc<Mutex<Option<String>>>,
}

impl Monitor {
    fn new(handle: Handle) -> Self {
        let load = Arc::new(Mutex::new(None));
        let shared = load.clone();

        thread::spawn(move || loop {
            let value = std::fs::read_to_string("/proc/loadavg").ok();

            *shared.lock().unwrap() = value;
            handle.invalidate();                  // "render again, please"
            thread::sleep(Duration::from_secs(2));
        });

        Self { load }
    }
}

impl Applet for Monitor {
    fn render(&mut self, _window: &Window) -> Node {
        let group = match self.load.lock().unwrap().as_deref() {
            Some(load) => Group::new().child(Row::new(tr("Load")).value(load.trim())),
            None => Group::new().loading(true),       // nothing yet: say so
        };

        Page::new().title(tr("Monitor")).child(group).into()
    }

    fn event(&mut self, _event: &Event, _commands: &mut Commands) {}
}

fn main() {
    run(Monitor::new);
}
CallWhat it does
handle.invalidate()Asks Nirvana to call render again for every open window. Cheap; call it whenever shared state changed.
handle.show_window()Shows a kept-alive window that is hidden.
handle.close_window()Closes the applet’s windows from a thread (the program a terminal ran has ended).
handle.stream(id, bytes)Sends bytes to a live component (below).

The pattern is always the same: shared state behind a Mutex, a thread that fills it and invalidates, and a render that reads whatever is there now — a spinner while it is None, the content after.

Do not hold the lock while doing the slow thing: lock, swap the value in, unlock.

Streams

A live component’s content does not travel in scenes. A terminal’s output is streamed to it:

// a thread reading the pseudo-terminal:
handle.stream("term", &buffer[..read]);

and what is typed comes back as data events on the same id. Send large amounts in pieces of some kilobytes.

State on disk

There is no storage API: an applet keeps its state in files, where a desktop program should.

WhatWhere
The person’s choices~/.config/etyde/<applet> ($XDG_CONFIG_HOME)
What the applet remembers (history, last folder)~/.local/state/etyde/<applet> ($XDG_STATE_HOME)
What can be fetched again~/.cache/etyde/…

prefs::write_atomically(path, text) writes a file so that a reader never sees half of it. prefs::read_lines(name) and prefs::write_lines(name, …) keep a list of lines under ~/.config/etyde/.

The SDK’s helpers

Besides scene, runtime and i18n, the crate has modules for what applets keep needing. They go through the tools a desktop already has, and each returns “nothing there” instead of failing when a tool or a device is missing. All of them may be slow: call them from a thread, never from render.

prefs: the shell’s preferences

One file, ~/.config/etyde/shell, of key = value lines: what Settings writes and the shell reads.

use nirvana_protocol::prefs::{self, Prefs};

let prefs = Prefs::load();
let size = prefs.number("dock.icon_size", 54, 32, 80);   // default, min, max
let labels = prefs.flag("dock.labels", true);
let align = prefs.choice("dock.align", &["start", "center", "end"], "center");

prefs::set("dock.labels", "false")?;

Every reader names its own default, so a missing file or key means “as shipped”. To follow changes, poll prefs::modified(&prefs::shell_path()) and load again when it moves. prefs::yawc_value and prefs::set_yawc_value read and edit the compositor’s config in place, keeping its comments.

apps: installed applications

use nirvana_protocol::apps;

let all = apps::installed();                 // from .desktop entries
let for_pdf = apps::handlers("application/pdf");
let owner = apps::for_window(&all, "org.mozilla.firefox");

system: the machine

Sound (pactl), the network (nmcli), the battery and the backlight (sysfs), packages (dpkg, apt): snapshots to show, and the few actions Settings and the shell take. Actions that need the administrator go through run_as_root, which asks with the system’s own prompt.

yawc: the compositor

A client for the control socket.

use nirvana_protocol::yawc;

let mut compositor = yawc::connect()?;
compositor.spawn("etyde-applet settings")?;       // start something in the session
compositor.set_clipboard("copied text")?;
let screens = compositor.outputs()?;

watch_windows follows the list of open windows; hotkeys and set_hotkey read and change key bindings; activate, minimize and close act on a window.

weather, widgets

The forecast from open-meteo.com, kept for half an hour, and the glanceable cards the lock screen and the desktop show.

Running other programs

An applet’s process is an ordinary program: std::process::Command works. Three things to keep in mind:

  • The applet’s environment is a short list unless its registry entry says "environment": "full".
  • To start an application for the person (so that it outlives the applet and belongs to the session), ask the compositor: yawc::connect()?.spawn(…).
  • Never show a tool’s raw output as an error. Say what failed and what to do next, in a Banner.

Something wrong or missing on this page? Say so in the tracker (choose “Site and docs”).

Esc