Your Termix fleet in the Omarchy bar.
Omix is a read-mostly fleet status surface for Omarchy Quattro. Termix already has a web UI, a desktop app, a mobile app and a CLI — this is not a fifth client. Its job is to tell you when something needs attention, and get you to the right place in Termix in one click.
Quiet when everything is fine. That is the whole design.
| State | Trigger | Looks like |
|---|---|---|
| Quiet | No active alerts, every host reachable, every tunnel healthy | The glyph in your theme's foreground, no count, no dot |
| Warning | Any warning-severity alert, or any tunnel down | Amber glyph and dot, with a count |
| Critical | Any critical alert, or any host down | Red glyph and dot, with a count |
| Unknown | Termix unreachable, a rejected key, or the first poll has not returned | Dimmed glyph with a neutral dot, no count |
Hover for a one-line summary: 2/3 hosts up, 2 alerts, 1 of 2 tunnels down.
Left click toggles the panel, middle click forces a refresh. Right click is
deliberately unbound.
omarchy plugin add https://github.com/dreadnought-0/omix.git --enable --yesThen set at least a Termix URL and a way to reach an API key, either through
Setup → Plugins → Omix or by editing the widget's entry in
~/.config/omarchy/shell.json:
{ "id": "io.github.dreadnought-0.omix",
"baseUrl": "https://termix.example.lan",
"apiKeyCommand": "pass show homelab/termix-api-key" }Omarchy stores every plugin's settings inline on its bar-layout entry, so there
is no separate config file and no config: sub-object.
Omix bundles no binaries and installs nothing. It needs:
| Requirement | Why | Where it comes from |
|---|---|---|
| Omarchy Quattro (tested on 4.0.1) | Host shell | Already present |
curl |
Every HTTP request. The API key is passed as configuration on stdin, never as an argument. | core/curl, present on every Omarchy install |
omarchy-launch-browser |
Opens deep links back into Termix | Ships with Omarchy |
| A reachable Termix instance and an API key | The thing being monitored | Yours |
Optional: whatever command you use to fetch the key — pass, secret-tool,
infisical, keyctl. Omix runs the command you configure and reads its stdout;
it depends on none of them specifically.
Omix requires no privilege escalation, adds no systemd units, writes nothing outside its own state file, and never starts a second Quickshell process.
omarchy plugin remove io.github.dreadnought-0.omixThat removes the checkout and takes the widget out of your bar layout. Two things it does not touch, so remove them yourself if you want a clean slate:
rm -rf ~/.local/state/omix # cached fleet state, written 0600and the API key, wherever you put it — Omix never copies it anywhere, so
deleting the plugin leaves your pass entry or key file exactly as it was.
If you used the plaintext apiKey setting, removing the widget from
shell.json removes the key with it; rotate that key anyway, since it has
been sitting in a config file.
To disable without removing:
omarchy plugin disable io.github.dreadnought-0.omixDisabling a bar widget discards its settings. Omarchy disables a widget by
taking it out of the bar layout, and the settings live on that layout entry, so
re-enabling gives you a fresh widget with defaults — your Termix URL and key
path are gone. Copy the entry out of shell.json first if you plan to come
back. This is Omarchy's behaviour for every bar widget, not something Omix
chooses.
Disabling stops the heartbeats immediately. Termix reaps any metrics viewer whose last heartbeat is over 120 seconds old, so collection stops on its own within two minutes; if you re-enable or restart the shell before then, Omix sweeps its own registrations at startup and it stops at once. See Live metrics for why it works that way rather than firing a final request on the way out.
Built and tested against Termix 2.7.1 (Termix-SSH/Termix, commit 42f8270,
2026-09-02).
The Termix API moves fast and the published reference lags the code. Omix was built by reading the route definitions in that release, not the docs, and it capability-probes every domain at startup rather than assuming an endpoint exists. A section whose endpoints are missing on your release is hidden with a reason, not left to throw errors on every poll. See CHANGELOG.md for the endpoints whose response shape had to be inferred rather than confirmed.
Termix API keys are tmx_-prefixed, scoped to one user, and can be given an
expiry date. Creating one is an admin-only operation in Termix 2.7.1.
- Sign in to Termix as an admin.
- Go to Settings → Admin → API Keys.
- Create a key. Give it a name that says what it is for (
omix-bar-widget), scope it to the user whose hosts you want on the bar, and set an expiry date — a year is a reasonable ceiling for something living in a desktop. - Copy the token. Termix shows it exactly once.
- Store it somewhere Omix can fetch it (below). Do not paste it into
shell.jsonif you can avoid it.
A Termix API key is not a read-only monitoring token, and Termix has no read-only scope. The key you give Omix can do everything its user can do through the API — and Termix holds the SSH credentials for your entire fleet. The same key that reads "14/15 hosts up" can enroll hosts, create credentials, open sessions and share them.
Omix itself never calls any of those endpoints — see What Omix will not do — but the key does not know that. Treat it as a fleet-wide credential:
- Give it an expiry, and rotate it.
- Scope it to a user that only has the hosts you actually want on the bar.
- Keep it out of
shell.json. - If you turn on actions, that key now needs write access to do them, and a compromise of your desktop session is a compromise of your fleet. That is a real trade for a one-click container restart, and it is off by default for that reason.
If a read-only or monitoring-scoped key ever lands in Termix, this plugin should be reconfigured to demand it.
Resolved in this order, first hit wins.
A command printing the key on stdout. Run once at startup and on explicit
refresh. The key never touches shell.json.
{ "apiKeyCommand": "pass show homelab/termix-api-key" }Works with anything that prints a secret:
{ "apiKeyCommand": "secret-tool lookup service termix" }
{ "apiKeyCommand": "infisical secrets get TERMIX_API_KEY --plain" }
{ "apiKeyCommand": "keyctl print %user:termix" }A worked pass setup:
pass insert homelab/termix-api-key # paste the tmx_… token, then Ctrl-D
pass show homelab/termix-api-key # confirm it prints one lineThen set apiKeyCommand to pass show homelab/termix-api-key. If your password
store needs an unlocked GPG agent, unlock it before the shell starts, or Omix
reports the command's failure in the panel and retries on refresh.
A file containing only the key.
{ "apiKeyFile": "~/.config/omarchy/secrets/termix-api-key" }Omix refuses to read it unless its permissions are 0600 or tighter, and
tells you the exact chmod to run. A key file your group can read is a
fleet-wide credential sitting in the open.
install -m 600 /dev/null ~/.config/omarchy/secrets/termix-api-key
printf '%s' 'tmx_…' > ~/.config/omarchy/secrets/termix-api-key{ "apiKey": "tmx_…" }This stores a fleet-wide credential in plaintext in ~/.config/omarchy/shell.json.
Anything that can read your config can read your fleet — every other plugin in the
shell, any backup of your dotfiles, any config repo you push. Omix supports it,
warns once in the panel while it is in use, and this README says it plainly:
prefer the command.
- It is passed to
curlas configuration on stdin, never as a command-line argument. Anything inargvis readable from/procby every local user for as long as the process runs, and a widget polling every 30 seconds would publish your fleet key on a loop. - It is never logged, never rendered, never put in a tooltip, and never written
to the state cache. Every string that can reach the UI — including
curl's own error text — is passed through a redactor first. 401responses are told apart: an expired key, a revoked key and a locked data key each produce a different, actionable message.
Every key below is set on the widget's entry in shell.json, or through
Setup → Plugins → Omix.
| Key | Default | What it does |
|---|---|---|
baseUrl |
"" |
Termix API base URL. The only host Omix ever contacts. |
apiKeyCommand |
"" |
Command printing the key. Preferred. |
apiKeyFile |
"" |
File holding the key. Must be 0600 or tighter. |
apiKey |
"" |
Plaintext key. Warned about, in use. |
verifyTls |
true |
Turn off only if you cannot supply a CA bundle. |
caBundle |
"" |
PEM bundle for a private CA. The right fix for self-signed. |
webUrl |
baseUrl |
Where deep links open. Set it when the API and UI hosts differ. |
openWith |
browser |
browser or desktop. See Deep links. |
pollIntervalSeconds |
30 |
Background cadence while the panel is closed. |
panelPollIntervalSeconds |
10 |
Cadence while the panel is open. |
barContent |
worst |
worst, alerts, hosts, combined, or glyph. |
enableLiveMetrics |
true |
See Live metrics. |
allowActions |
false |
See Actions. |
ignoreHosts |
"" |
Comma-separated host names to leave out of everything. |
onlyFleets |
"" |
Comma-separated fleet names to scope the whole plugin to. |
certExpiryWarnDays |
21 |
Certificates expiring sooner are called out. |
cpuWarnPercent |
90 |
Tints a CPU bar. Does not create alerts. |
memWarnPercent |
90 |
Tints a memory bar. |
diskWarnPercent |
85 |
Tints a disk bar. |
sectionAlerts … sectionActivity |
true (activity: false) |
Turn whole panel sections off. |
A section turned off issues zero requests, rather than being fetched and hidden. Turning off Docker means Omix never opens an SSH session to list containers; turning off Certificates means it never reads a host's certbot inventory.
Two deliberate exceptions: alerts and tunnel statuses are polled whether or not their sections are on, because they are two of the bar's three signals. Turning off the Alerts section hides a panel view; it must not blind the bar, or the state table at the top of this README would quietly stop being true. Both are single fleet-wide requests, so this costs nothing extra.
sectionActivity is off by default: the audit log is admin-only on most
instances, and it is the noisiest thing Termix will hand you.
Scopes every count and list to the named fleets. Useful when your Termix instance covers far more hosts than you want represented by one bar glyph.
Fleet membership is only known once the Fleets section has loaded. Until then the filter is not applied — applying half of it would silently hide hosts and make the bar read as healthier than it is.
Termix's Host Metrics domain is viewer-driven: the server collects metrics for a host only while something is registered as watching it. That makes a careless bar widget a permanent load on every machine in your fleet.
With enableLiveMetrics: true (default), Omix:
- registers as a viewer only while the panel is open on the Hosts section, and only for the host rows actually on screen — never the whole fleet;
- debounces registration behind scrolling, so dragging through a long list does not start collection on forty machines;
- sends heartbeats while those rows stay visible;
- unregisters on every exit path it can complete one on: panel close, section change, scrolling a row away, and session idle;
- writes its registrations to a journal on disk before it makes them, so a
shell that is killed outright (
kill -9, a crash, a power cut) leaves a record behind — and the next startup sweeps the sessions it orphaned.
That last point is the one that matters, and it is why the journal exists: a destructor cannot run if the process is killed, so correctness cannot depend on one.
There is one path Omix cannot close cleanly, and it is worth being plain about.
When the plugin is disabled or removed, its subprocesses are destroyed with
it — a final unregister request is killed before curl can finish. Two things
cover that: heartbeats stop instantly, and Termix reaps any viewer that has not
heartbeated for 120 seconds. So collection stops within two minutes rather than
immediately, and immediately if the shell comes back and sweeps the journal.
Handing the request to a detached process would close the gap, but only by
putting a fleet-wide key into a command line or a file on the way out, which is
a bad trade for ninety seconds.
With enableLiveMetrics: false, Omix never registers a viewer at all and
runs on host statuses, alerts and tunnels alone. This is a fully supported mode,
not a degraded afterthought: you lose per-host CPU/memory/disk and the
sparklines, and you keep every other signal including the entire bar state
model. If you would rather your fleet never collected metrics on a bar widget's
behalf, this is the setting.
Twelve sections. Every screenshot below is the real widget against a fleet of ten hosts — one down, two reachable-but-sessionless, a failed unit, an expired certificate and a failing backup job.
Termix down, a rejected key, a TLS failure and an expired key each produce a different, actionable message. The bar goes to Unknown — never to all-clear — and polling backs off to a five-minute ceiling rather than hammering a machine that is already down. Opening the panel retries at once.
Overview — hosts up over total, active alerts by severity, tunnel health, and the per-host counts once their sections have been opened. Every tile links to its section. Satisfiable from three requests regardless of fleet size.
Alerts — active firings with severity, host, message and when they fired. Dismiss inline. Omix does no threshold evaluation of its own; Termix's alert engine already did it, with your durations, your cooldowns and your notification channels. A second opinion here would just disagree with the alerts you configured.
Hosts — every host with reachability, tags, and live CPU/memory/disk with a sparkline when metrics are on. Text filter and a problems-only filter. Problems sort to the top.
Tunnels — every SSH tunnel with state, failure reason and retry count. Presets listed separately.
Docker — containers grouped by host, on demand.
Proxmox — guests and cached node stats, read from Termix's own Proxmox sync.
Omix never talks to Proxmox directly. This section hides itself when a
dedicated Proxmox plugin is installed (it probes omarchy plugin list --json
for proxmox/omaprox ids), because those cover PBS jobs, per-storage detail,
task history and quorum state that Termix does not expose — and two shallow
copies of the same guest list help nobody.
Services — failed units, and enabled units that are not running, by exception. A full unit list is hundreds of rows that answer no question. Top processes load separately, per host, on request.
Network — Tailscale state and IPs, WireGuard interfaces and peers.
Certificates — certbot and acme.sh certificates, soonest expiry first, with your warning threshold. Quietly one of the most useful views here: in a homelab behind a reverse proxy, the thing that takes everything down at once is a certificate nobody was watching. Termix has no alerting for it.
Automations — trigger type, last run and last result, failures first.
Fleets — groupings and their members.
Activity — recent audit entries. Off by default.
The highest-value thing here. Omix does not need to be a terminal; it needs to get you to one in a click. Each host row opens Termix's terminal, metrics or file manager view for that host, and the panel header opens Termix itself.
openWith: "browser" opens ?view=…&hostId=… in your browser and lands on the
exact host.
openWith: "desktop" launches the Termix desktop app and stops there. It
cannot target a host, for two reasons that are Termix's rather than ours: Termix
registers no URL scheme, and the desktop app talks to its own embedded local
backend, whose host ids belong to a different database than your server's.
Browser mode is the default because it is the one that actually works.
All actions sit behind allowActions: true, which is off by default. With
it off, no action control is reachable anywhere in the UI — not disabled, not
greyed out: absent, so there is nothing to mis-click.
With it on, Omix can:
- restart a systemd service on a host
- restart a Docker container
- bring an SSH tunnel up or down
- bring a WireGuard interface up or down
- connect or disconnect Tailscale on a host
- trigger an automation
Every one of them shows a confirmation naming the exact target
(Restart nginx.service?) before it fires, and reports its result inline at the
top of the panel. Nothing is bound to a bare click.
Dismissing an alert is allowed even with actions off: it changes your own alert list and touches no machine.
Actions are deliberately not exposed over IPC. A confirmation needs someone to confirm to.
Not at any setting, not behind any flag. Some of these are dangerous from a bar click; the rest are Termix's job, and the deep link takes you there.
- Embed or proxy a terminal, RDP, VNC, Telnet or serial session
- File manager operations of any kind
- Read, create or modify credentials, or touch the Vault integration
- Create or modify SSH hosts, enroll hosts, or handle Termix ID keys
- RBAC changes, host sharing, user management, or API key management
- Create session share links
- Send signals to processes, including kill
- Anything involving Termix's AI assistant endpoints
- Guacamole token generation
Omix contacts the Termix instance you configure and nothing else. No
telemetry, no analytics, no update checks, no third-party requests of any kind.
The only other processes it runs are your apiKeyCommand, curl,
omarchy-launch-browser for deep links, and omarchy plugin list --json once at
startup to check whether a dedicated Proxmox plugin is installed.
Omix does not use sudo or pkexec. It never writes outside its own state file, and never starts a second Quickshell process.
The state cache lives at ~/.local/state/omix/state.json, written 0600. It
holds rendered state — host names, counts, the viewer journal — so that
reopening the panel is instant. It never holds a credential.
TLS verification failed. Omix says so and names the fix. Point caBundle at
your CA's PEM file:
"caBundle": "~/.config/omarchy/secrets/homelab-ca.pem". Setting
verifyTls: false also works and is a bad idea — your API key rides every
request, and without verification you cannot tell your Termix instance from
anything else answering that address.
"Termix API key has expired" / "Termix rejected the API key". These are different on purpose. The first means the key hit its expiry date: make a new one. The second means Termix does not recognise it: it was deleted, deactivated, or mistyped.
"API key file is readable by other users". Run the chmod 600 the message
gives you. Omix will not read a key file your group or the world can read.
A section is missing. Either you turned it off, or its endpoints are not present on your Termix release and the capability probe hid it. Omix prefers hiding a section to spamming errors on every poll. Turn on the Overview and check the section list.
Live metrics unavailable, TOTP mentioned. Your instance gates metrics behind
a TOTP challenge. A bar widget has nowhere to prompt for a code and must not
try, so Omix stops registering viewers and says so. Host status, alerts and
tunnels are unaffected. Set enableLiveMetrics: false to stop it asking.
The API and UI are on different hostnames (reverse proxy, split DNS). Set
webUrl to the UI hostname; baseUrl stays the API one. Deep links use webUrl.
Termix unreachable. The bar goes to Unknown — never to "all clear" — the panel shows why, and polling backs off exponentially to a five-minute ceiling rather than hammering a machine that is down. Opening the panel retries at once.
Nothing happens after editing a .js file under the plugin. Omarchy
hot-reloads .qml but not .js. Run omarchy restart shell.
node tests/model.test.js # pure JS: parsing, redaction, bar state, viewer lifecycle
./tests/integration.sh # the real Service.qml against a stub Termix, headless
./tests/crash-recovery.sh # kill -9 mid-session, assert the next start cleans up
omarchy plugin validate .
qmllint -I <import-root> *.qml sections/*.qml components/*.qmlThe integration test runs the actual Service.qml under Quickshell against a
stub Termix (tests/stub-termix.js) without touching your running bar. It
asserts the things unit tests cannot reach: that the key never appears in a
process argument, that a 404 endpoint hides its section, that every per-host
section actually populates, and — checked against the stub server's own view
rather than the plugin's — that no metrics viewer registration survives the
panel closing.
crash-recovery.sh goes further and kills the harness with SIGKILL partway
through, then asserts that a second run releases what the first one orphaned.
It resolves its key through a command rather than inline on purpose: that is
the asynchronous path, the cache file loads before the key does, and an
implementation that spends its one sweep before the key arrives silently never
cleans up. That bug existed and this test is why it does not.
Everything under api/ and model/ is plain JavaScript with no QML imports, so
it can be exercised in node directly.
Termix aggregates Proxmox behind its own API, so Omix overlaps with a dedicated Proxmox plugin — but it does not replace one. Termix's Proxmox coverage is guest discovery plus cached node stats: no PBS backup job results, no per-storage detail, no cluster task history, no quorum state. Run both if you want depth on Proxmox and breadth across the fleet; Omix stands its own Proxmox section down automatically when it finds one installed.
MIT. See LICENSE.
Termix is Apache 2.0, by Luke Gustafson. Omix is an independent third-party plugin and is not affiliated with or endorsed by the Termix project.
















