From 315becb78eb48be186f2370ae6c5efd3e967c12d Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Mon, 14 Sep 2026 12:32:19 +0200 Subject: 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. --- vm-manager/VmPanel.qml | 457 ------------------------------------------------- 1 file changed, 457 deletions(-) delete mode 100644 vm-manager/VmPanel.qml (limited to 'vm-manager/VmPanel.qml') 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. -// -// 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 = ""; } - } - } - } - } -} -- cgit v1.2.3