aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/modules/vm/Docker.qml
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-10-22 11:18:54 +0200
committerDanilo M. <danix@danix.xyz>2026-10-22 11:18:54 +0200
commit38b64dda38c76fa11424cba8ed012cd94a56362e (patch)
tree7ce20fcb308c72d91f699db4ed15e779112f9e7e /desktop/modules/vm/Docker.qml
parent761c1d31d80b3fd72602bb771f9eb2e86c31d20c (diff)
downloadquickshell-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/Docker.qml')
-rw-r--r--desktop/modules/vm/Docker.qml149
1 files changed, 149 insertions, 0 deletions
diff --git a/desktop/modules/vm/Docker.qml b/desktop/modules/vm/Docker.qml
new file mode 100644
index 0000000..10199b2
--- /dev/null
+++ b/desktop/modules/vm/Docker.qml
@@ -0,0 +1,149 @@
+// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+pragma Singleton
+
+import Quickshell
+import Quickshell.Io
+import QtQuick
+
+// The docker half of the vm page, shaped like Virsh: the docker CLI, an
+// event stream that always runs, and a stats poll only while the page is up.
+// Containers are grouped by compose project, because one compose stack is a
+// dozen containers that start and stop together; a container outside any
+// project is a group of one.
+Singleton {
+ id: root
+
+ // project -> [{ id, name, state, status, image }], names sorted
+ property var groups: ({})
+ property var projects: []
+ property var stats: ({}) // short id -> { cpu, mem, memTotal }
+
+ property bool sampling: false
+
+ signal actionFailed(string target, string action, string message)
+
+ // --- discovery -------------------------------------------------------
+
+ Process {
+ id: listProc
+ command: ["docker", "ps", "-a", "--format",
+ '{{.ID}}\t{{.Names}}\t{{.State}}\t{{.Status}}\t{{.Label "com.docker.compose.project"}}\t{{.Image}}']
+ stdout: StdioCollector {
+ onStreamFinished: {
+ const g = {};
+ for (const line of text.split("\n")) {
+ const f = line.split("\t");
+ if (f.length < 6) continue;
+ const p = f[4] || f[1];
+ (g[p] = g[p] ?? []).push({ id: f[0], name: f[1], state: f[2],
+ status: f[3], image: f[5] });
+ }
+ for (const p in g) g[p].sort((a, b) => a.name.localeCompare(b.name));
+ root.groups = g;
+ root.projects = Object.keys(g).sort();
+ }
+ }
+ // No daemon, or no permission on the socket: the section is empty.
+ onExited: code => { if (code !== 0) { root.groups = {}; root.projects = []; } }
+ }
+
+ // --- stats -----------------------------------------------------------
+ //
+ // `docker stats --no-stream` samples for about 2s before printing, which
+ // is the same cadence as the timer; a tick that lands while it is still
+ // running is a no-op. CPU is docker's own figure, 100% per core.
+
+ function _bytes(s) {
+ const m = s.match(/([\d.]+)\s*([kKMGT]?i?B)/);
+ if (!m) return -1;
+ const k = { B: 1, kB: 1e3, KB: 1e3, MB: 1e6, GB: 1e9, TB: 1e12,
+ KiB: 1024, MiB: 1024 ** 2, GiB: 1024 ** 3, TiB: 1024 ** 4 }[m[2]] ?? 1;
+ return parseFloat(m[1]) * k;
+ }
+
+ Process {
+ id: statsProc
+ command: ["docker", "stats", "--no-stream", "--format",
+ "{{.ID}}\t{{.CPUPerc}}\t{{.MemUsage}}"]
+ stdout: StdioCollector {
+ onStreamFinished: {
+ const s = {};
+ for (const line of text.split("\n")) {
+ const f = line.split("\t");
+ if (f.length < 3) continue;
+ const [used, total] = f[2].split("/");
+ s[f[0]] = { cpu: parseFloat(f[1]), mem: root._bytes(used),
+ memTotal: root._bytes(total ?? "") };
+ }
+ root.stats = s;
+ }
+ }
+ }
+
+ // --- actions ---------------------------------------------------------
+
+ // ids: the containers to act on; target names them in a failure notice.
+ function act(ids, target, action) {
+ actProc.target = target; actProc.action = action;
+ actProc.command = ["docker", action].concat(ids);
+ actProc.running = true;
+ }
+
+ Process {
+ id: actProc
+ property string target: ""
+ property string action: ""
+ stderr: StdioCollector { id: actErr }
+ onExited: code => {
+ if (code !== 0)
+ root.actionFailed(actProc.target, actProc.action,
+ actErr.text.trim() || `exited ${code}`);
+ root.refreshList();
+ }
+ }
+
+ // --- refresh ---------------------------------------------------------
+
+ function refreshList() { listProc.running = true; }
+
+ Timer {
+ interval: 2000
+ running: root.sampling
+ repeat: true
+ triggeredOnStart: true
+ onTriggered: statsProc.running = true
+ }
+
+ // Only lifecycle events: health checks fire exec_* events every few
+ // seconds per container, which would turn this into a poll. The stream
+ // dies with the daemon, so it is retried rather than left dead.
+ Process {
+ id: events
+ running: true
+ command: ["docker", "events", "--filter", "type=container",
+ "--filter", "event=create", "--filter", "event=start",
+ "--filter", "event=die", "--filter", "event=pause",
+ "--filter", "event=unpause", "--filter", "event=destroy",
+ "--filter", "event=health_status",
+ "--format", "{{.Action}}"]
+ stdout: SplitParser { onRead: root.refreshList() }
+ onRunningChanged: if (running) root.refreshList()
+ onExited: retry.start()
+ }
+
+ Timer {
+ id: retry
+ interval: 10000
+ onTriggered: events.running = true
+ }
+}