aboutsummaryrefslogtreecommitdiffstats
path: root/vm-manager/VmPanel.qml
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-09-14 12:32:19 +0200
committerDanilo M. <danix@danix.xyz>2026-09-14 12:32:19 +0200
commit315becb78eb48be186f2370ae6c5efd3e967c12d (patch)
tree66dad581d03ebbc4f49680099e955fe6c62612e2 /vm-manager/VmPanel.qml
parent350b6358e29699721c636c18f8928acb68a05ade (diff)
downloadquickshell-315becb78eb48be186f2370ae6c5efd3e967c12d.tar.gz
quickshell-315becb78eb48be186f2370ae6c5efd3e967c12d.zip
feat(desktop): move vm-manager in as the vm module
Virsh.qml is unchanged; VmPanel's body becomes the page and the stat row becomes a Flow, because four stats with 150px bars do not fit a 600px drawer as one row. The page starts and stops Virsh.sampling, so the 2s poll runs only while something is looking at it; the lifecycle event stream still runs always, which is what keeps the tile's dots current.
Diffstat (limited to 'vm-manager/VmPanel.qml')
-rw-r--r--vm-manager/VmPanel.qml457
1 files changed, 0 insertions, 457 deletions
diff --git a/vm-manager/VmPanel.qml b/vm-manager/VmPanel.qml
deleted file mode 100644
index 5960a1a..0000000
--- a/vm-manager/VmPanel.qml
+++ /dev/null
@@ -1,457 +0,0 @@
-// 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.
-
-import Quickshell
-import Quickshell.Wayland
-import QtQuick
-
-Scope {
- id: root
-
- // The screen the drawer opens on. Falls back to the focused one when
- // this monitor is not connected, so the panel is never invisible.
- property string monitor: "DP-3"
-
- property bool open: false
- property string selected: ""
-
- // A pending destructive action, shown as a confirm step instead of the
- // action list: { kind, vm, snap }. Null when nothing is being confirmed.
- property var confirming: null
- property string typed: ""
-
- readonly property var screenObj:
- Quickshell.screens.find(s => s.name === root.monitor) ?? Quickshell.screens[0]
-
- function toggle() { root.open = !root.open; }
- function close() { root.open = false; root.confirming = null; root.typed = ""; }
-
- onOpenChanged: {
- Virsh.sampling = open;
- confirming = null;
- typed = "";
- if (open) {
- Virsh.refreshList();
- if (!selected && Virsh.names.length) selected = Virsh.names[0];
- if (selected) Virsh.loadSnapshots(selected);
- }
- }
-
- onSelectedChanged: if (selected) Virsh.loadSnapshots(selected)
-
- function fmtBytes(b) {
- if (b < 0) return "—";
- const g = b / (1024 * 1024 * 1024);
- return g >= 10 ? g.toFixed(0) + " GB" : g.toFixed(1) + " GB";
- }
-
- function stateColor(s) {
- if (s === "running") return Theme.green;
- if (s === "paused" || s === "suspended" || s === "shutting down") return Theme.yellow;
- if (s === "crashed") return Theme.red;
- return Theme.overlay;
- }
-
- // Which verbs make sense in the current state, mirroring the states the
- // old rofi script switched on.
- function actionsFor(s, saved) {
- if (s === "running")
- return [["shutdown", "Shutdown"], ["reboot", "Reboot"], ["suspend", "Suspend"],
- ["reset", "Reset"], ["destroy", "Force stop"]];
- if (s === "paused" || s === "suspended")
- return [["resume", "Resume"], ["shutdown", "Shutdown"], ["destroy", "Force stop"]];
- // Only worth offering when a saved image actually exists: without one
- // managedsave-remove fails, and the button would be noise on every
- // other VM.
- if (saved)
- return [["start", "Start"], ["discardsave", "Discard saved state"]];
- return [["start", "Start"]];
- }
-
- function isDestructive(a) { return a === "reset" || a === "destroy" || a === "discardsave"; }
-
- function run(vm, action) {
- if (isDestructive(action)) root.confirming = { kind: action, vm: vm, snap: "" };
- else Virsh.act(vm, action);
- }
-
- // Quickshell exits once no window is visible, and the drawer is closed
- // most of the time, so a 1x1 transparent window holds the process open.
- // Without it the shell quits and the keybind, which reaches it over IPC,
- // has nothing left to talk to.
- PanelWindow {
- visible: true
- implicitWidth: 1
- implicitHeight: 1
- color: "transparent"
- exclusionMode: ExclusionMode.Ignore
- mask: Region {}
- WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
- }
-
- LazyLoader {
- active: root.open
-
- PanelWindow {
- id: win
- screen: root.screenObj
-
- anchors { top: true; left: true; right: true; bottom: true }
- color: "transparent"
- exclusionMode: ExclusionMode.Ignore
- WlrLayershell.layer: WlrLayer.Overlay
- WlrLayershell.namespace: "quickshell-vm-manager"
- // The panel needs keys: Escape to close, typing to confirm a delete.
- WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
-
- // Dim the screen behind: the secondary monitor usually has an
- // editor or a chat window on it, and this says the drawer has focus.
- Rectangle {
- anchors.fill: parent
- color: "#000000"
- opacity: 0.45
- MouseArea { anchors.fill: parent; onClicked: root.close() }
- }
-
- // Keys reach an item, not the window, so something inside has to
- // hold focus. The TextInput in a delete confirmation takes it when
- // it appears; this takes it back whenever that is gone.
- Item {
- id: keyCatcher
- anchors.fill: parent
- focus: true
-
- Keys.onEscapePressed: {
- if (root.confirming) root.confirming = null;
- else root.close();
- }
-
- Connections {
- target: root
- function onConfirmingChanged() {
- if (!root.confirming) keyCatcher.forceActiveFocus();
- }
- }
- }
-
- Rectangle {
- id: drawer
- anchors { top: parent.top; left: parent.left; right: parent.right }
- height: Math.min(content.implicitHeight + 40, win.height - 60)
- color: Qt.alpha(Theme.base, 0.72)
- bottomLeftRadius: 14
- bottomRightRadius: 14
- border.width: 1
- border.color: Qt.alpha(Theme.text, 0.12)
-
- // Clicks on the drawer must not fall through to the dimmer.
- MouseArea { anchors.fill: parent }
-
- Column {
- id: content
- anchors { left: parent.left; right: parent.right; top: parent.top; margins: 20 }
- spacing: 16
-
- Row {
- spacing: 10
- Text {
- text: "Virtual machines"
- font { family: Theme.fontFamily; pixelSize: Theme.fontSize + 2; bold: true }
- color: Theme.text
- }
- Text {
- anchors.verticalCenter: parent.verticalCenter
- text: "Esc to close"
- font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 }
- color: Theme.overlay
- }
- }
-
- // One row per VM, so several VMs stay readable at a glance.
- Repeater {
- model: Virsh.names
-
- Rectangle {
- required property string modelData
- readonly property var vm: Virsh.vms[modelData] ?? ({})
- readonly property bool isSel: root.selected === modelData
-
- width: content.width
- implicitHeight: vmCol.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: root.selected = modelData
- }
-
- Column {
- id: vmCol
- 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: root.stateColor(vm.state ?? "")
- }
- Text {
- text: modelData
- font { family: Theme.fontFamily; pixelSize: Theme.fontSize; bold: true }
- color: Theme.text
- }
- Text {
- anchors.verticalCenter: parent.verticalCenter
- text: vm.state ?? ""
- font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 }
- color: Theme.subtext
- }
- Text {
- anchors.verticalCenter: parent.verticalCenter
- text: (vm.vcpus ?? 0) + " vCPU"
- font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 }
- color: Theme.overlay
- }
- Text {
- anchors.verticalCenter: parent.verticalCenter
- visible: vm.state === "running" && !(vm.agent ?? false)
- text: "agent starting"
- font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 }
- color: Theme.overlay
- }
- }
-
- // Stats only mean anything while the VM runs.
- Row {
- visible: vm.state === "running"
- spacing: 24
-
- Stat {
- label: "CPU"
- value: (vm.cpu ?? -1) < 0 ? "—" : (vm.cpu).toFixed(0) + "%"
- fraction: (vm.cpu ?? 0) / 100
- }
- Stat {
- label: "RAM"
- value: (vm.memUsed ?? -1) < 0 ? "—"
- : root.fmtBytes(vm.memUsed) + " / " + root.fmtBytes(vm.memTotal)
- fraction: (vm.memUsed ?? -1) < 0 ? -1 : vm.memUsed / vm.memTotal
- }
- Stat {
- label: "Disk"
- value: (vm.fsUsed ?? -1) < 0 ? "—"
- : root.fmtBytes(vm.fsUsed) + " / " + root.fmtBytes(vm.fsTotal)
- fraction: (vm.fsUsed ?? -1) < 0 ? -1 : vm.fsUsed / vm.fsTotal
- }
- Stat {
- label: "Address"
- value: (vm.ip ?? "") === "" ? "—" : vm.ip
- fraction: -1
- }
- }
-
- // Actions and snapshots, for the selected VM only.
- Loader {
- active: isSel
- width: parent.width
- sourceComponent: detail
- property string vmName: modelData
- property string vmState: vm.state ?? ""
- property bool vmSaved: vm.saved ?? false
- }
- }
- }
- }
- }
- }
- }
- }
-
- Component {
- id: detail
-
- Column {
- spacing: 12
-
- readonly property string vmName: parent.vmName
- readonly property string vmState: parent.vmState
- readonly property bool vmSaved: parent.vmSaved
-
- Rectangle { width: parent.width; height: 1; color: Qt.alpha(Theme.text, 0.08) }
-
- // Confirm step replaces the buttons, so the action cannot be
- // clicked again while it is being confirmed.
- Loader {
- active: root.confirming !== null && root.confirming.vm === vmName
- width: parent.width
- sourceComponent: confirmUi
- }
-
- Flow {
- visible: !(root.confirming !== null && root.confirming.vm === vmName)
- width: parent.width
- spacing: 8
-
- Repeater {
- model: root.actionsFor(vmState, vmSaved)
- Button {
- required property var modelData
- text: modelData[1]
- danger: root.isDestructive(modelData[0])
- onClicked: root.run(vmName, modelData[0])
- }
- }
- Button {
- text: "Snapshot"
- onClicked: Virsh.snapshotCreate(vmName)
- }
- Button {
- text: "Delete VM"
- danger: true
- onClicked: root.confirming = { kind: "delete", vm: vmName, snap: "" }
- }
- }
-
- Text {
- visible: (Virsh.snapshots[vmName] ?? []).length > 0
- text: "Snapshots"
- font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2; bold: true }
- color: Theme.subtext
- }
-
- Repeater {
- model: Virsh.snapshots[vmName] ?? []
-
- Row {
- required property var modelData
- width: parent.width
- spacing: 10
-
- Text {
- anchors.verticalCenter: parent.verticalCenter
- width: 260
- elide: Text.ElideRight
- text: modelData.name
- font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 }
- color: Theme.text
- }
- Text {
- anchors.verticalCenter: parent.verticalCenter
- width: 150
- text: modelData.created
- font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 }
- color: Theme.overlay
- }
- Text {
- anchors.verticalCenter: parent.verticalCenter
- width: 70
- text: modelData.state
- font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 }
- color: Theme.overlay
- }
- Button {
- text: "Revert"
- danger: true
- onClicked: root.confirming = { kind: "revert", vm: vmName, snap: modelData.name }
- }
- Button {
- text: "Delete"
- danger: true
- onClicked: root.confirming = { kind: "snapdelete", vm: vmName, snap: modelData.name }
- }
- }
- }
- }
- }
-
- Component {
- id: confirmUi
-
- Column {
- spacing: 10
- readonly property var c: root.confirming
- // Deleting a VM erases its disk image, so that one asks for the
- // name to be typed. The rest are recoverable enough for a click.
- readonly property bool needsTyping: c && c.kind === "delete"
-
- Text {
- width: parent.width
- wrapMode: Text.Wrap
- font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 1 }
- color: Theme.red
- text: {
- if (!c) return "";
- if (c.kind === "delete") return `Delete ${c.vm}? This erases its disk image and cannot be undone.`;
- if (c.kind === "revert") return `Revert ${c.vm} to "${c.snap}"? Changes since that snapshot are lost.`;
- if (c.kind === "snapdelete") return `Delete snapshot "${c.snap}"?`;
- if (c.kind === "destroy") return `Force stop ${c.vm}? This is a power cut, not a shutdown.`;
- if (c.kind === "discardsave") return `Discard the saved state of ${c.vm}? Its memory image is deleted and the next start boots cold. The disk is untouched.`;
- if (c.kind === "reset") return `Reset ${c.vm}? This is a hard reset, not a reboot.`;
- return "";
- }
- }
-
- TextInput {
- id: nameField
- visible: needsTyping
- width: 260
- text: root.typed
- onTextChanged: root.typed = text
- font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 1 }
- color: Theme.text
- focus: needsTyping
- Component.onCompleted: if (needsTyping) forceActiveFocus()
-
- Rectangle {
- anchors.fill: parent
- anchors.margins: -6
- z: -1
- radius: 6
- color: Qt.alpha(Theme.surface, 0.8)
- border.width: 1
- border.color: Qt.alpha(Theme.text, 0.15)
- }
- Text {
- visible: !nameField.text
- text: "type the VM name"
- font: nameField.font
- color: Theme.overlay
- }
- }
-
- Row {
- spacing: 8
- Button {
- text: "Confirm"
- danger: true
- enabled: !needsTyping || root.typed === c.vm
- onClicked: {
- if (c.kind === "delete") Virsh.deleteVm(c.vm);
- else if (c.kind === "revert") Virsh.snapshotRevert(c.vm, c.snap);
- else if (c.kind === "snapdelete") Virsh.snapshotDelete(c.vm, c.snap);
- else Virsh.act(c.vm, c.kind);
- root.confirming = null;
- root.typed = "";
- }
- }
- Button {
- text: "Cancel"
- onClicked: { root.confirming = null; root.typed = ""; }
- }
- }
- }
- }
-}