diff options
| author | Danilo M. <danix@danix.xyz> | 2026-10-22 11:18:54 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-10-22 11:18:54 +0200 |
| commit | 38b64dda38c76fa11424cba8ed012cd94a56362e (patch) | |
| tree | 7ce20fcb308c72d91f699db4ed15e779112f9e7e /desktop/modules/vm/VmPage.qml | |
| parent | 761c1d31d80b3fd72602bb771f9eb2e86c31d20c (diff) | |
| download | quickshell-38b64dda38c76fa11424cba8ed012cd94a56362e.tar.gz quickshell-38b64dda38c76fa11424cba8ed012cd94a56362e.zip | |
vm: show docker containers beside the VMs
Docker.qml mirrors Virsh: the docker CLI, an event stream that runs for the
whole session, and `docker stats --no-stream` on the 2s timer only while the
page is up. Containers are grouped by compose project, since a stack is a
dozen containers that start and stop together; a container outside any
project is a group of one. Verbs are start, stop, restart and resume, by
container id, so no compose file is needed and nothing removes a container.
The event stream is filtered to lifecycle events: health checks emit exec_*
events every few seconds per container, which would make the stream a poll.
It dies with the daemon, so it is retried every 10s.
An inactive Loader keeps its last height and a Column still reserves it, so
collapsing one card left an empty box where its detail had been. Both the
container and VM detail loaders now hide while inactive; the VM one had the
same bug, unseen with a single VM.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'desktop/modules/vm/VmPage.qml')
| -rw-r--r-- | desktop/modules/vm/VmPage.qml | 182 |
1 files changed, 181 insertions, 1 deletions
diff --git a/desktop/modules/vm/VmPage.qml b/desktop/modules/vm/VmPage.qml index 4f9e0aa..9c731f5 100644 --- a/desktop/modules/vm/VmPage.qml +++ b/desktop/modules/vm/VmPage.qml @@ -31,11 +31,13 @@ Column { // correct the moment the page appears. Virsh.sampling = true; Virsh.refreshList(); + Docker.sampling = true; + Docker.refreshList(); if (!page.selected && Virsh.names.length) page.selected = Virsh.names[0]; if (page.selected) Virsh.loadSnapshots(page.selected); } - Component.onDestruction: Virsh.sampling = false + Component.onDestruction: { Virsh.sampling = false; Docker.sampling = false; } onSelectedChanged: if (selected) Virsh.loadSnapshots(selected) @@ -170,6 +172,9 @@ Column { // Actions and snapshots, for the selected VM only. Loader { active: isSel + // An inactive Loader keeps its last height, and Column + // would still reserve it; hidden children take no space. + visible: active width: parent.width sourceComponent: detail property string vmName: modelData @@ -180,6 +185,181 @@ Column { } } + // --- containers -------------------------------------------------------- + // + // One card per compose project, shaped like a VM row: a dot, the + // aggregate stats, and the containers with their verbs when selected. + // Nothing here is destructive enough to confirm: no rm, no prune. + + property string selectedProject: "" + + function containerActions(s) { + if (s === "running") return [["stop", "Stop"], ["restart", "Restart"]]; + if (s === "paused") return [["unpause", "Resume"]]; + return [["start", "Start"]]; + } + + Text { + visible: Docker.projects.length > 0 + text: "Containers" + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2; bold: true } + color: Theme.subtext + } + + Repeater { + model: Docker.projects + + Rectangle { + id: card + required property string modelData + readonly property var cs: Docker.groups[modelData] ?? [] + readonly property int up: cs.filter(c => c.state === "running").length + readonly property bool isSel: page.selectedProject === modelData + readonly property var st: cs.map(c => Docker.stats[c.id]).filter(s => s) + readonly property real cpu: st.reduce((a, s) => a + s.cpu, 0) + readonly property real mem: st.reduce((a, s) => a + Math.max(0, s.mem), 0) + readonly property real memTotal: st.length ? st[0].memTotal : -1 + + width: page.width + implicitHeight: cCol.implicitHeight + 24 + radius: 10 + color: isSel ? Qt.alpha(Theme.surface, 0.7) : Qt.alpha(Theme.surface, 0.35) + border.width: 1 + border.color: isSel ? Qt.alpha(Theme.accent, 0.5) : "transparent" + + MouseArea { + anchors.fill: parent + onClicked: page.selectedProject = isSel ? "" : modelData + } + + Column { + id: cCol + anchors { left: parent.left; right: parent.right; top: parent.top; margins: 12 } + spacing: 10 + + Row { + spacing: 10 + Rectangle { + anchors.verticalCenter: parent.verticalCenter + width: 9; height: 9; radius: 5 + color: up === 0 ? Theme.overlay : up === cs.length ? Theme.green : Theme.yellow + } + Text { + text: modelData + font { family: Theme.fontFamily; pixelSize: Theme.fontSize; bold: true } + color: Theme.text + } + Text { + anchors.verticalCenter: parent.verticalCenter + text: cs.length > 1 ? `${up}/${cs.length} running` : (cs[0]?.status ?? "") + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 } + color: Theme.subtext + } + } + + Flow { + visible: up > 0 + width: parent.width + spacing: 20 + + Stat { + label: "CPU" + value: st.length ? cpu.toFixed(0) + "%" : "—" + fraction: st.length ? cpu / 100 : -1 + } + Stat { + label: "RAM" + value: st.length ? page.fmtBytes(mem) : "—" + fraction: memTotal > 0 ? mem / memTotal : -1 + } + } + + Loader { + active: isSel + visible: active + width: parent.width + sourceComponent: Column { + spacing: 8 + + Rectangle { width: parent.width; height: 1; color: Qt.alpha(Theme.text, 0.08) } + + // Whole-project verbs, by container id, so no compose + // file is needed. Pointless for a group of one. + Flow { + visible: cs.length > 1 + width: parent.width + spacing: 8 + Button { + visible: up < cs.length + text: "Start all" + onClicked: Docker.act(cs.map(c => c.id), modelData, "start") + } + Button { + visible: up > 0 + text: "Stop all" + onClicked: Docker.act(cs.map(c => c.id), modelData, "stop") + } + Button { + visible: up > 0 + text: "Restart all" + onClicked: Docker.act(cs.map(c => c.id), modelData, "restart") + } + } + + Repeater { + model: cs + + Column { + id: ctr + required property var modelData + readonly property var s: Docker.stats[modelData.id] + width: parent.width + spacing: 4 + + Row { + width: parent.width + spacing: 8 + Rectangle { + anchors.verticalCenter: parent.verticalCenter + width: 7; height: 7; radius: 4 + color: page.stateColor(modelData.state) + } + Text { + width: parent.width - 15 + elide: Text.ElideRight + text: modelData.name + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 } + color: Theme.text + } + } + + Row { + spacing: 10 + Text { + anchors.verticalCenter: parent.verticalCenter + text: modelData.status + + (s && modelData.state === "running" + ? ` ${s.cpu.toFixed(0)}% ${page.fmtBytes(s.mem)}` : "") + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 } + color: Theme.overlay + } + Repeater { + model: page.containerActions(modelData.state) + Button { + required property var modelData + text: modelData[1] + onClicked: Docker.act([ctr.modelData.id], ctr.modelData.name, modelData[0]) + } + } + } + } + } + } + } + } + } + } + Component { id: detail |
