An Omarchy bar widget for Docker: start, restart, stop and remove containers, and open a desktop session on a Windows (or macOS) VM built from the dockur images.
| Dependency | Needed for | Notes |
|---|---|---|
docker CLI |
everything | Your user must be able to run docker without sudo (usually via the docker group). Not there yet? The panel offers to fix it — see Starting the daemon. |
omarchy-windows-vm |
the session button on the omarchy-windows container |
Ships with Omarchy. |
xfreerdp3 |
the session button on any other VM container | Package freerdp. Omarchy installs it with omarchy-windows-vm install. |
omarchy-launch-browser or xdg-open |
the web viewer button | One of the two is present on any desktop. |
uwsm |
detaching launched clients into their own scope | Optional; falls back to setsid. |
Everything else is stock Omarchy: the shell, bash, and the plugin's own
files. Nothing is downloaded at runtime.
omarchy plugin add https://github.com/dicemans/omarchy-plugin-docker-vms.git --enableThe widget lands in the bar's right section. Move it with:
omarchy bar move io.github.dicemans.docker-vms --section leftManual install
git clone https://github.com/dicemans/omarchy-plugin-docker-vms.git \
~/.config/omarchy/plugins/io.github.dicemans.docker-vms
omarchy-shell shell rescanPlugins
omarchy plugin enable io.github.dicemans.docker-vms --section rightomarchy plugin remove io.github.dicemans.docker-vmsThat deletes the plugin directory and drops its entry from
~/.config/omarchy/shell.json. Nothing else is left behind: the plugin writes
no state of its own, and it never touches your containers on the way out.
| Icon | Action | Shown when |
|---|---|---|
| | Open desktop session (RDP) | the container is a VM image and has an RDP port published |
| | Open the web viewer in the browser | port 8006 is published |
| | Restart | the container is running |
| | Stop | the container is running |
| | Start | docker can start it — created or exited |
| | Remove container | docker will let it go — created, exited or dead |
The session and viewer buttons start a stopped VM before connecting, but they
ask first: a click meant as "connect" should not silently boot several
gigabytes of Windows. Answer Start and it is a single step from "off" to "at
the Windows desktop"; the question is skipped entirely when the container is
already running. The same confirmation applies to the rdp and viewer IPC
calls, so a keybinding cannot power on a VM without asking either. For the container Omarchy
installs (omarchy-windows) it delegates to omarchy-windows-vm launch -k,
which already knows the compose file, the credentials, and the display
scaling — and -k means closing the RDP window leaves the VM running. Any
other VM container is connected to directly with xfreerdp3, using the
USERNAME / PASSWORD the container was created with.
Clicking a row opens its menu. Starting, stopping and restarting already have their own buttons, so making the row a shortcut for one of them only created a place where a stray click did something unexpected.
Under each name sits a second line: how the container is behaving — health, CPU and memory when the monitor is on, compose project, restart count, published ports. What it is — the image — lives in the menu header, where there is room to read it without eliding.
A click on the row, or m from the keyboard. Everything that does not earn a
permanent button lives here, and it is built from the container's state, so it
never offers something docker would refuse:
| Entry | Shown when |
|---|---|
| View logs | always — docker logs -f --tail 200 in a terminal that waits for a key before closing, so a stopped container's logs stay readable instead of flashing past |
| Open a shell | any running container. On a VM it is labelled (container), because it lands in the container running QEMU rather than inside the guest — which is exactly where you look when a VM will not boot |
| Pause / Resume | running / paused |
| Kill now | running or restarting |
| Open 127.0.0.1:port | one entry per published port, while the container is running — a published port survives a stop, but nothing is listening on it, so the entry would only ever open a dead tab |
| Open folder name | one entry per bind mount |
| Copy name, Copy port | always |
Off by default. The switch sits next to the CONTAINERS heading, labelled
CPU & RAM, and s toggles it from the keyboard.
The reason it is opt-in is measurable: on the machine this was built on,
docker stats --no-stream costs a flat ~2000 ms whether there are zero,
one or five containers — it waits for two samples a second apart to compute a
percentage — against ~60 ms for the entire listing. The cost is a wait, not
CPU: the whole refresh loop is under 0.2% of an eight-core machine either way.
But a two-second wait inside the refresh would make the panel feel broken, so
when the monitor is on the numbers come from a second, slower timer in its own
process, and the list keeps its own pace.
Two things never happen on a single click.
Removing a container. The dialog names it, and the answer starts on
Cancel so a stray Enter cannot delete. Removal is offered only where docker
will actually allow it, and it runs a plain docker rm — never -f, never
-v — so the image and every volume survive, including the bind mount that
holds a VM's disk. Recreating the container over the same volume gets the
machine back.
Starting a stopped VM. The session and viewer buttons still take you from "off" to the Windows desktop in one step, but they ask first: a click meant as "connect" should not silently boot several gigabytes of Windows. Here the answer starts on Start, because you did just ask for it and nothing is destroyed — and the accent colour, rather than the warning red used for removal, says the same thing.
Neither question is something a caller can skip. The IPC remove, rdp and
viewer calls all put the dialog on screen instead of acting, so a keybinding
obeys the same gate as a click — including for a container the panel has not
listed yet.
The panel follows docker's states rather than a running/stopped guess, because the guess produced wrong offers: a paused container used to be handed Start (which cannot resume it) and Remove (which docker refuses), and one stuck in a restart loop was counted as stopped — the bar said "0 running" while a container thrashed.
| State | Offered |
|---|---|
running, paused, restarting |
Restart, Stop |
created, exited |
Start, Remove |
dead |
Remove |
removing, anything unrecognised |
nothing |
A container failing its healthcheck, stuck restarting, or dead is painted in
the urgent colour, in the row and in the bar. The bar swaps the whale for an
alert glyph when the docker daemon cannot be reached, so a dead daemon no longer
looks like an idle one.
A docker the panel cannot reach is the one listing failure it can fix itself,
so it does not stop at the diagnosis: a Start Docker button appears
where the container list would be (Enter takes it too), and it repairs
whatever actually stands in the way — through polkit, in a single
authorization, because the shell has no terminal to put a sudo prompt in:
- the daemon is down →
systemctl start docker.service. For this session only; it never enables the service at boot. - the socket refuses your user (the button then reads Fix Docker access)
→ your user joins the
dockergroup, the fix docker's own documentation prescribes, plus an ACL on the socket so it works now rather than at your next login. The ACL dies with the daemon; the group is what remains. - a fresh machine has both problems at once → both fixes, still one dialog.
The tooltip says which of these the click will do before you click. After the
authorization the helper waits up to 30 seconds for the socket to answer, so
the next refresh paints the container list rather than the error it just
fixed. A dismissed authorization dialog is reported as exactly that, not as a
failure — and if the ACL half of the grant could not land (no setfacl on
the machine), the panel says the part that is left: log out and back in.
Arrow keys (or hjkl) move between rows; left/right steps through a row's
action buttons, Enter activates, x asks to remove the selected container,
r refreshes now, c opens the desktop session and v the web viewer,
m (or Enter on the row) opens the menu, s toggles the performance
monitor, Esc closes,
Tab moves to the next bar panel. Every action the panel offers is reachable
without a mouse. While the confirmation is up
it owns the keys: left/right switch the answer, Enter takes it, Esc
cancels.
The first key press only reveals the cursor, so a panel summoned by keyboard never acts on a row you have not looked at yet.
Set these on the widget's entry in ~/.config/omarchy/shell.json, or through
Setup > Plugins.
| Key | Default | Meaning |
|---|---|---|
refreshIntervalSec |
5 |
Refresh cadence while the panel is open. Closed, it backs off to 30s. |
nameFilter |
"" |
Only list containers whose name contains this text. |
vmsOnly |
false |
Hide plain containers and list only Windows/macOS VMs. |
stopTimeoutSec |
60 |
Seconds docker may spend on a clean shutdown before killing the container. Docker's own default without this is 10 seconds — a power cut for a virtual machine. |
rdpTimeoutSec |
120 |
Seconds to wait for a cold VM to answer on RDP before giving up. |
showStats |
false |
Show CPU and memory. See the note above on why this is opt-in. |
statsIntervalSec |
15 |
How often CPU and memory are sampled while the monitor is on. |
showCount |
true |
Paint the number of active containers next to the bar glyph. |
Every action is scriptable, which is what makes it bindable to a key:
omarchy-shell io.github.dicemans.docker-vms toggle
omarchy-shell io.github.dicemans.docker-vms rdp omarchy-windows
omarchy-shell io.github.dicemans.docker-vms viewer omarchy-windows
omarchy-shell io.github.dicemans.docker-vms start|stop|restart <container>
omarchy-shell io.github.dicemans.docker-vms remove <container> # asks, never deletes outright
omarchy-shell io.github.dicemans.docker-vms startDocker # start the daemon; polkit still asksWhen an action fails, the panel shows docker's own first line — "port is already allocated", "cannot start a paused container, try unpause instead" — rather than a generic "could not start the container".
The action calls answer ok or busy (another action is still in flight) —
never a silent success. remove answers confirm once the question is on
screen, or container is running when it refuses outright. A name that does
not exist is reported by the helper and shown in the panel.
For example, in ~/.config/hypr/bindings.lua:
o.bind("SUPER SHIFT", "W", "Windows VM", "omarchy-shell io.github.dicemans.docker-vms rdp omarchy-windows")Omarchy plugins run unsandboxed inside the long-running omarchy-shell
process, with your user's permissions. What this one does with them:
- No
sudo, nopkexec, no polkit. Every docker call is the plaindockerCLI run as you. If your user cannot talk to the docker socket, the panel says so and does nothing. - Note that membership in the
dockergroup is effectively root on the host — that is a property of docker itself, not of this plugin, but it is the privilege boundary this widget sits on. - No second Quickshell process. The widget lives in the shell that is already running.
- It writes nothing outside its own directory and changes no user
configuration. Adding the widget to your bar is done by
omarchy plugin enable, at your request. - Nothing is fetched at runtime — no network calls, no downloads, no
telemetry. The only outbound connection is the RDP client you asked for,
to
127.0.0.1. - The RDP password never reaches the argument vector.
/proc/<pid>/cmdlineis world-readable, so a credential passed as/p:secretis handed to every other user on the machine. The plugin instead invokesxfreerdp3 /args-from:stdinand writes the whole argument list — the password included — down a pipe, so the process list shows only the flag. The secret is unset as soon as the pipe owns it. This is also why the client is detached withsetsidrather thanuwsmon that path:uwsmhands the launch to a daemon, and the pipe would not reach the process that must read it. Foromarchy-windowsthe plugin delegates toomarchy-windows-vmand never handles the password at all. - Every read from docker is bounded while it is read. A host with thousands
of containers, or a container with a megabyte-long name, cannot grow the
long-running shell process: each
dockerinvocation is capped at 256 KiB and 200 rows by a consumer-sidehead(the producer is stopped by SIGPIPE), each field is clipped to 512 characters, and diagnostics to 400. The same caps are applied again inModel.jsbefore anything reaches the panel. When the row cap is hit the list still works and the panel says so in a footnote. - Deletion is narrow by construction: stopped containers only, plain
docker rm, behind a confirmation dialog. - One privileged path, declared up front. The only thing the plugin ever
does with root is the fix behind the Start Docker / Fix Docker access
button: start
docker.service, join thedockergroup, ACL the socket — nothing else, and never without a polkit authorization. The button's tooltip states what the click will do before it is clicked, and the username reaches the root script as a positional argument, never interpolated, so no name can be read as shell.
manifest.json plugin manifest (bar-widget, entry point, settings schema)
Panel.qml bar button + panel
Model.js parsing and per-row action rules, no QML types
bin/docker-vm-ctl every docker call, port lookup, and client launch
preview.png the panel screenshot
menu.png the menu screenshot
confirm.png the confirmation screenshot
LICENSE MIT
bin/docker-vm-ctl is a plain script and is the place to look when something
misbehaves — run it in a terminal:
~/.config/omarchy/plugins/io.github.dicemans.docker-vms/bin/docker-vm-ctl listIts remove refuses a running container regardless of what the panel
believes, so the guard holds even if the list on screen is a few seconds
stale.
list prints one TSV line per container: name, image, state, status, kind,
RDP port, web port, compose project, health, restart count, published ports,
bind mounts. Ports are read from HostConfig.PortBindings, which
survives a stop — docker ps reports no ports at all for a stopped container,
which would otherwise hide the connect button on exactly the VMs you want to
start.
MIT — see LICENSE.


