Modules
A module is a data source that the daemon polls on its own interval and caches. Each implements the Module { kind, capabilities, collect } contract from modules/mod.rs (see Architecture).
Built-in modules
| Module | Source | Interval | Notes |
|---|---|---|---|
system |
/proc/stat, /proc/meminfo, sysfs hwmon |
2s | always compiled |
gpu |
NVML (libnvidia-ml) |
5s | nvidia feature |
audio |
wpctl / pactl subprocesses |
2s | always compiled |
media |
D-Bus MPRIS via zbus |
5s | media feature |
network |
/sys/class/net |
5s | always compiled |
storage |
statvfs + sysfs hwmon |
30s | always compiled |
llama |
HTTP localhost:8080/metrics |
2s | always compiled |
ytmd |
YTMD companion HTTP + Socket.IO | 5s | always compiled |
hyprland |
Hyprland IPC | 1s | always compiled |
ModuleOutput
collect() returns a ModuleOutput:
- waybar fields —
text,tooltip,class,percentage stale: boolalert_data: HashMap<String, f64>— structured numerics consumed by the alert engine and state-transition logicmeta: HashMap<String, String>— structured strings (artist,title,album,sink, …) consumed by the state-transition logic
State transitions and alerts read alert_data / meta, never the human-facing tooltip. If you want a module's changes logged to the journal, populate those maps.
Adding a module
Adding a module means touching all of these (they must stay in sync):
ModuleKind(alfred-core::types) — add the variant, and update itsDisplay/FromStr/all(). The enum is dep-free and never feature-gated.- Config struct — add the per-module config in
alfred-core::config(#[serde(deny_unknown_fields)]; updateConfig::validate()). - Spawn macro — add the
spawn_if_enabled!line inmodules/mod.rs. - Control actions live in two places that must match:
- the module's
capabilities()— what the daemon registers and serves at/modules(i.e. whatalfred controlaccepts), - the static table in
client/list.rs::builtin_metadata— whatalfred listprints without a running daemon.
- the module's
Optional-dependency modules (Cargo feature)
If a module's only heavy/platform-specific dependency is self-contained to its own file, give it a default-on Cargo feature that gates both dep:<crate> and pub mod <module>; + its spawn_if_enabled! line — see nvidia (gpu / nvml-wrapper) and media (zbus).
Because ModuleKind is never gated, a compiled-out module just never spawns — its /control handler and alfred list row still exist.
Blocking work (subprocess calls, sysfs/statvfs, sqlite, NVML) belongs in
tokio::task::spawn_blocking, never inline in an asynccollect().
Seeded from the repo's README.md and CLAUDE.md — those remain the in-tree source of truth.