aboutsummaryrefslogtreecommitdiffstats
path: root/vm-manager/README.md
blob: 8d817f90e2cc2cad88dbb75262866edaf8c66697 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
# 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,
    })