# vm-manager A drawer for libvirt virtual machines: state, live statistics, the actions that make sense in the current state, and the snapshot list. It replaces the menu half of `rofi-qemu.sh`, which could only ever show a list of strings. ┌─ buildsystem ───────────────── ● running ─┐ │ CPU 4% RAM 0.7/16 GB Disk 60/130 GB │ │ 172.16.34.20 │ │ [Shutdown] [Reboot] [Suspend] [Reset] │ │ snapshots │ │ working 2026-03-06 10:27 │ └───────────────────────────────────────────┘ ## Running it qs -p . `SUPER+v` toggles it, through an IPC call, so the shell has to be running first. Both are wired in the Hyprland config: -- autostart.lua hl.exec_cmd("qs -p ~/Programming/GIT/quickshell/vm-manager") -- keybindings.lua hl.bind(mainMod .. " + v", hl.dsp.exec_cmd( "qs -p ~/Programming/GIT/quickshell/vm-manager ipc call panel toggle")) Write those two paths out in full in the real config: `exec_cmd` runs the command directly, with no shell to expand `~`. The drawer opens on the monitor named by the `monitor` property at the top of `VmPanel.qml`, defaulting to `DP-3`, and falls back to the first screen when that one is not connected. It dims the rest of that monitor, because the secondary screen usually has a real window on it, and takes keyboard focus so Escape closes it and a delete can be confirmed by typing. ## Where the numbers come from Everything shells out to `virsh`, the same commands the rofi script used. No libvirt bindings, no new dependency. | Row | Source | | ---------- | ------------------------------------------------- | | state | `virsh domstats --state` | | CPU | `cpu.time` sampled twice, divided by vCPU count | | RAM | `virsh dommemstat`: `actual - usable` | | Disk | guest agent `guest-get-fsinfo`, the `/` mount | | address | `virsh domifaddr --source agent` | | snapshots | `virsh snapshot-list` | Three of those need **qemu-guest-agent running inside the guest**. It is not up for the first few seconds after boot, and another VM might not have it at all, so those rows show `—` and the header says `agent starting` rather than substituting a host-side figure. That distinction matters: libvirt's own `balloon.current` is memory *allocated* to the VM, which on a VM that has booted reads as 100% forever, and `block.0.allocation` is qcow2 growth on the host, not usage inside the guest. Showing either in place of the real number would be quietly wrong, so they are not used as a fallback. ## Live without polling `virsh event --all --loop` streams lifecycle changes, and that process runs for the whole session, panel open or not. Starting a VM from `virsh` or virt-manager updates the drawer, and opening it shows current state rather than whatever was true last time. Statistics do need sampling, on a 2 second timer, but only while the drawer is open: `Virsh.sampling` follows the panel's `open`. A closed panel costs one idle process waiting on an event socket. ## Two things that bite **A config with no visible window exits.** The drawer is closed most of the time, so the shell holds itself open with a 1x1 transparent window with an empty mask, which is click-through and draws nothing. Without it the shell loads, reports no error and quits, and since `SUPER+v` reaches it over IPC, the key then has nothing to talk to and silently does nothing. **Key events reach an item, not a window.** Setting `keyboardFocus` on the layer shell is necessary but not sufficient: `Keys.onEscapePressed` on the PanelWindow itself never fires, because no item inside holds focus. A filled `Item` with `focus: true` catches it, and takes focus back when the TextInput in a delete confirmation gives it up. ## Destructive actions `Reset`, `Force stop`, snapshot `Revert` and snapshot `Delete` each take one confirmation click. `Delete VM` requires the machine's name to be typed, because it runs `virsh undefine --remove-all-storage`, which erases the disk image with no undo. The rofi script ran exactly that from a single menu selection with no confirmation at all. Failures are reported with `notify-send`, since a `virsh` error otherwise has nowhere to go: the process output is not attached to a terminal. ## Theme `Theme.qml` carries only fallbacks. The palette comes from `~/.cache/wal/udt-palette.qml`, generated by `udt-accent` from unified-desktop-theme's `palette.rasi`, and is watched: editing the palette there recolours a running drawer without restarting it. State dots use the green, yellow and red from that palette. The frosting is Hyprland's, matched on this window's namespace: hl.layer_rule({ name = "blur-vm-manager", match = { namespace = "^(quickshell-vm-manager)$" }, blur = true, xray = false, ignore_alpha = 0.1, })