- Rust 99.9%
The waybar text read "WAIT" both when the router had no model loaded and when a model was loaded but not generating. "WAIT" suggests something is blocked, when in fact the server is available in both cases. Split the two states: no model loaded now renders "IDLE" (new `idle` class, tooltip explains a model loads on the first request), and the loaded-but-quiet state renders "READY". The latter keeps its `wait` CSS class so existing waybar styling is unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| .forgejo/workflows | ||
| .githooks | ||
| alfred | ||
| alfred-core | ||
| alfred-matrix | ||
| alfred-ytm | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| CLAUDE.md | ||
| README.md | ||
alfred
A personal system telemetry daemon for the Linux desktop.
Architecture
alfred serve # persistent daemon, managed by systemd --user
alfred get <module> # one-shot query for scripts/waybar/debugging
alfred stream <module> # optional long-lived NDJSON client
alfred status # check if daemon is running
alfred notify "message" # send an outbound Matrix notification
alfred completions zsh > _alfred # generate shell completion
Modules
| Module | Source | Interval |
|---|---|---|
system |
/proc/stat, /proc/meminfo, sysfs hwmon |
2s |
gpu |
NVML (libnvidia-ml) |
5s |
audio |
wpctl / pactl subprocesses |
2s |
media |
D-Bus MPRIS via zbus |
5s |
network |
/sys/class/net |
5s |
storage |
statvfs + sysfs hwmon |
30s |
llama |
HTTP localhost:8080/metrics |
2s |
ytmd |
YTMD companion HTTP + Socket.IO | 5s |
hyprland |
Hyprland IPC | 1s |
API
GET /:module → latest cached JSON (one-shot)
GET /stream/:module → newline-delimited JSON stream (push on update)
POST /control/:module/:action → trigger an action, forces immediate refresh
POST /notify → dispatch a notification through the router (channels/backends)
Config
~/.config/alfred/config.toml
Matrix notifications
alfred can post m.room.message events to a Matrix room — from the CLI or from
any internal module (now-playing, "job done", alerts). Send-only, into
encrypted or unencrypted rooms.
alfred notify "hello from alfred"
alfred notify --channel alerts --level warn "disk almost full"
alfred notify --room "#alfred:server" --level error --title "Backup" --html "<b>failed</b>"
--level (info|warn|error) adds a leading emoji and, for warn/error, an
HTML colour (and, on desktop backends, a notify-send urgency). --channel
targets a [notify.channels] group (which may fan out to several backends);
--room sends directly to one Matrix room (friendly name from [matrix.rooms],
or a literal !id / #alias). With neither, it uses [notify].default_channel,
falling back to [matrix].default_room.
Notification router (channels & backends)
Notifications flow through a small router so producers (the CLI, alerts,
internal modules) don't care how a message is delivered. A channel is a
named group of backends; a backend is either matrix (E2EE-capable, below)
or exec (a shell command template).
[notify]
default_channel = "alerts" # used when nothing else is specified
[notify.backends.desktop]
kind = "exec"
command = "notify-send -u {{urgency}} '{{title}}' '{{body}}'"
# exec template vars: {{title}} {{body}} {{level}} {{urgency}} {{icon}}
[notify.backends.matrix-music]
kind = "matrix"
room = "now-playing" # a [matrix.rooms] name, or an id/alias
[notify.channels.alerts]
backends = ["matrix", "desktop"] # "matrix" is built-in → [matrix].default_room
[notify.channels.music]
backends = ["matrix-music"]
alfred notify --channel alerts … (or an alert's notify = "alerts") then goes
to both Matrix and desktop. The CLI prints a per-backend result and exits
non-zero only if every backend failed.
Alerts can drive it directly instead of shelling out — an alert may set exec,
notify, or both:
[[alerts]]
name = "cpu-hot"
condition = "cpu_temp > 85"
notify = "alerts" # channel name
level = "error"
title = "CPU hot"
message = "Temperature: {{cpu_temp}}°C"
recover_message = "CPU back to normal" # optional; sent as info on recover
# exec = "..." # still supported, and can coexist
How it works (E2EE)
The lean daemon does not link matrix-rust-sdk. It resolves the bot token,
looks up the bot's user/device via whoami, builds the message, and forwards
the send to a small alfred-matrix worker subprocess (same pattern as the
alfred-ytm music helper). That worker links matrix-rust-sdk with Olm/Megolm
and owns a persistent crypto store, so it encrypts transparently when the
target room has encryption enabled and sends plaintext otherwise.
Notes:
- The bot token must be minted via
/login(it needs adevice_id); a/register-only token has no device and E2EE can't work — alfred rejects it with a clear error. - The crypto store lives at
${XDG_STATE_HOME:-~/.local/state}/alfred-matrix/<device_id>/(dir mode700). Keep it: deleting it or rotating the token (→ new device) forces new keys, and other clients may show the bot's device as unverified (they still decrypt — messages are shared with all member devices by default). - The bot must be a member of the room; if it has been invited it will join on first send.
- First send after (re)start does an initial sync to bootstrap keys, so it can be slower; subsequent sends reuse the warm client.
The daemon never logs the token or the auth header. If the homeserver is unreachable it returns an error — the daemon stays up and the CLI reports a clear message.
Config
[matrix]
enabled = true
homeserver = "https://matrix.mydomain" # base URL, no trailing path
# Provide the bot access token via ONE of these. Prefer a file or command so the
# secret never lives in a committed config; a plaintext access_token warns.
# access_token = "syt_…" # discouraged
access_token_file = "/home/me/.secrets/alfred-matrix.token" # must be chmod 600
# access_token_command = "gpg -q -d ~/.secrets/alfred-matrix.gpg"
default_room = "!roomid:mydomain" # id or #alias, or a name below
[matrix.rooms] # friendly name → room id/alias
alerts = "!abcdef:mydomain"
now-playing = "#music:mydomain"
Commands from Matrix (inbound)
The bot can also answer you: message !alfred <command> in a watched room and
alfred replies there. This is opt-in and locked down — only allowlisted users
are ever acted on, the bot ignores its own messages, and messages sent before
the listener started (e.g. after a restart) are dropped so history never
re-triggers.
[matrix.commands]
enabled = true
prefix = "!alfred" # message prefix that marks a command
allow_from = ["@me:mydomain"] # MXIDs allowed to issue commands
rooms = ["!abcdef:mydomain"] # watched rooms; defaults to default_room
allow_control = false # true → allow mute/skip/etc. over Matrix
Commands:
!alfred help # list commands
!alfred status # one line per module
!alfred get <module> # a module's text + tooltip
!alfred modules # modules and their control actions
!alfred events [n] # recent journal events (max 25)
!alfred control <module> <action> [args…] # requires allow_control = true
With allow_control = false (the default) the bot is a read-only dashboard;
control verbs are refused. Delivery is E2EE when the room is encrypted, same as
outbound. Internally: the alfred-matrix worker watches its sync stream and
forwards prefix-matched messages to the daemon, which runs the query/action and
replies in the same room.
Minting a bot token against conduwuit
Register a dedicated bot user, then log in once to obtain a long-lived access token (do this from a trusted shell; the token grants the bot's full access):
# 1. Register the bot user (conduwuit: needs a registration token if configured,
# or use the admin room / your homeserver's registration flow).
curl -s -XPOST 'https://matrix.mydomain/_matrix/client/v3/register' \
-H 'Content-Type: application/json' \
-d '{
"username": "alfred",
"password": "CHOOSE_A_STRONG_PASSWORD",
"auth": { "type": "m.login.dummy" }
}'
# 2. Log in to mint an access token.
curl -s -XPOST 'https://matrix.mydomain/_matrix/client/v3/login' \
-H 'Content-Type: application/json' \
-d '{
"type": "m.login.password",
"identifier": { "type": "m.id.user", "user": "alfred" },
"password": "CHOOSE_A_STRONG_PASSWORD",
"initial_device_display_name": "alfred-daemon"
}'
# → { "access_token": "syt_…", "user_id": "@alfred:mydomain", ... }
Store that access_token where access_token_file/access_token_command
points, invite @alfred:mydomain to the room (encrypted or not — the bot joins
on first send), and put the room id in default_room / [matrix.rooms]. Get a
room id from your client's room settings, or:
curl -s 'https://matrix.mydomain/_matrix/client/v3/directory/room/%23music%3Amydomain' \
-H 'Authorization: Bearer syt_…' # resolves #music:mydomain → { "room_id": "!…" }
Waybar integration
Waybar 0.15.0 has been more reliable here with normal polling than with
long-lived interval: 0 stream consumers, so the recommended setup is:
"custom/system": {
"interval": 2,
"exec": "alfred get system",
"return-type": "json",
"format": "{}"
}
alfred stream <module> is still available for other long-lived consumers.
Systemd
~/.config/systemd/user/alfred.service
Shell completions
Generate a zsh completion file:
alfred completions zsh > ~/.local/share/zsh/site-functions/_alfred
Then make sure that directory is in fpath before compinit, e.g. in ~/.zshrc:
fpath=(~/.local/share/zsh/site-functions $fpath)
autoload -U compinit
compinit