aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/modules/vm/VmPage.qml
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-10-22 18:22:06 +0200
committerDanilo M. <danix@danix.xyz>2026-10-22 18:22:06 +0200
commit8cab337fdd895cb754de56199f219705be2ec61a (patch)
treea7b6fc891720b62da6df69150eef3795c7ec02a0 /desktop/modules/vm/VmPage.qml
parentaf956df74341308aea5f19eb9077ae1664f8a202 (diff)
downloadquickshell-8cab337fdd895cb754de56199f219705be2ec61a.tar.gz
quickshell-8cab337fdd895cb754de56199f219705be2ec61a.zip
vm: update, open and prune for docker containers
Update runs `docker compose -p <project> -f <files> up -d --pull always` with the files read from the com.docker.compose.project.config_files label, so only stacks compose itself made get the button: a plain `docker run` container has no recipe to recreate it from, and the supabase CLI labels the project and nothing else. It has its own Process because a pull runs for minutes and must not be clobbered by a start or stop meanwhile. Published TCP ports become localhost:<port> buttons opened by the default browser. Docker cannot say which port is a web UI, so every published one gets a button and a port ending in 443 is guessed https. Prune images runs `docker image prune -a -f` behind a confirm step, since a re-pull can be gigabytes. The images it would delete are listed under the header from `docker image ls`'s {{.Containers}} count, which docker 29 fills in; images of stopped containers are kept. That count reads 0 for images only used by `--rm` containers, such as the test-build ones, so the list is the warning. 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.qml92
1 files changed, 82 insertions, 10 deletions
diff --git a/desktop/modules/vm/VmPage.qml b/desktop/modules/vm/VmPage.qml
index 9c731f5..2ac57f0 100644
--- a/desktop/modules/vm/VmPage.qml
+++ b/desktop/modules/vm/VmPage.qml
@@ -189,7 +189,8 @@ Column {
//
// 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.
+ // Nothing per container is destructive enough to confirm: no rm. Image
+ // prune is, since a re-pull can be gigabytes, so it asks first.
property string selectedProject: ""
@@ -199,11 +200,54 @@ Column {
return [["start", "Start"]];
}
- Text {
+ property bool pruneArmed: false
+
+ Row {
visible: Docker.projects.length > 0
- text: "Containers"
- font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2; bold: true }
- color: Theme.subtext
+ spacing: 10
+ Text {
+ anchors.verticalCenter: parent.verticalCenter
+ text: "Containers"
+ font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2; bold: true }
+ color: Theme.subtext
+ }
+ Button {
+ visible: !page.pruneArmed && (Docker.unused.length > 0 || Docker.pruning)
+ enabled: !Docker.pruning
+ text: Docker.pruning ? "Pruning…" : "Prune images"
+ onClicked: page.pruneArmed = true
+ }
+ Button {
+ visible: page.pruneArmed
+ danger: true
+ text: "Delete unused images"
+ onClicked: { page.pruneArmed = false; Docker.prune(); }
+ }
+ Button {
+ visible: page.pruneArmed
+ text: "Cancel"
+ onClicked: page.pruneArmed = false
+ }
+ Text {
+ anchors.verticalCenter: parent.verticalCenter
+ visible: !page.pruneArmed && Docker.pruned !== ""
+ text: Docker.pruned
+ font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 }
+ color: Theme.overlay
+ }
+ }
+
+ // What prune would delete: images no container uses.
+ Repeater {
+ model: Docker.unused
+ Text {
+ required property var modelData
+ width: page.width
+ elide: Text.ElideMiddle
+ text: `${modelData.name} ${modelData.size}`
+ font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 }
+ color: Theme.overlay
+ }
}
Repeater {
@@ -284,23 +328,30 @@ Column {
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.
+ // file is needed; pointless for a group of one. Update
+ // is the exception: it needs the compose files.
Flow {
- visible: cs.length > 1
+ visible: cs.length > 1 || !!cs[0]?.files
width: parent.width
spacing: 8
Button {
- visible: up < cs.length
+ visible: !!cs[0]?.files
+ enabled: Docker.updating === ""
+ text: Docker.updating === modelData ? "Updating…" : "Update"
+ onClicked: Docker.update(modelData, cs[0].files)
+ }
+ Button {
+ visible: cs.length > 1 && up < cs.length
text: "Start all"
onClicked: Docker.act(cs.map(c => c.id), modelData, "start")
}
Button {
- visible: up > 0
+ visible: cs.length > 1 && up > 0
text: "Stop all"
onClicked: Docker.act(cs.map(c => c.id), modelData, "stop")
}
Button {
- visible: up > 0
+ visible: cs.length > 1 && up > 0
text: "Restart all"
onClicked: Docker.act(cs.map(c => c.id), modelData, "restart")
}
@@ -352,6 +403,27 @@ Column {
}
}
}
+
+ // Published ports, opened by the default
+ // browser (a tab in a running one).
+ // ponytail: docker cannot say which port is a
+ // web UI, so every published port gets a
+ // button, and a port ending in 443 is guessed
+ // https.
+ Flow {
+ visible: modelData.ports.length > 0
+ width: parent.width
+ spacing: 8
+ Repeater {
+ model: ctr.modelData.ports
+ Button {
+ required property int modelData
+ text: `localhost:${modelData}`
+ onClicked: Qt.openUrlExternally(
+ `${modelData % 1000 === 443 ? "https" : "http"}://localhost:${modelData}`)
+ }
+ }
+ }
}
}
}