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);
}
| Call | What 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.
| What | Where |
|---|---|
| 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.