pragma Singleton // Storage: what is in this machine, how full it is, and what is filling it. // // Two reads with very different costs, kept apart on purpose: // // refresh() layout, usage, and drive health. Around 90ms -- lsblk plus one // udisks call -- so the page opens with it and re-reads freely. // scan() what is using the space. Measuring a folder means walking it, // and a Steam library alone can be a terabyte, so this happens // only when asked and the answer is kept until asked again. // // Not a stored preference: every value here is the machine's, not the user's. // // Deliberately absent: partitioning and formatting. Those stay in GNOME Disks, // which the page can launch. import Quickshell import Quickshell.Io import QtQuick Singleton { id: root readonly property string helperPath: Quickshell.shellDir + "/scripts/panama-disks" property var drives: [] property var filesystems: [] property var swap: [] property bool scanned: false property string lastError: "" // The expensive half. property var folders: [] property var containers: null property bool scanning: false property bool folderScanTruncated: false // Empty until a scan has completed, which is what the page shows a prompt // for rather than an empty list -- "nothing here" and "not measured yet" // are different answers. property bool foldersMeasured: false readonly property var primaryDrive: root.drives.length > 0 ? root.drives[0] : null // The filesystem the user means when they ask how full the machine is. readonly property var rootFilesystem: { for (const filesystem of root.filesystems) { if ((filesystem.mountpoints ?? []).includes("/")) return filesystem; } return root.filesystems.length > 0 ? root.filesystems[0] : null; } // Bytes, in the units people actually read. Binary units with decimal-style // names would be a lie in the other direction; this matches what df -h and // the drive's own packaging say. function formatBytes(bytes: real): string { if (!(bytes > 0)) return "0 B"; const units = ["B", "KB", "MB", "GB", "TB"]; let value = bytes; let index = 0; while (value >= 1000 && index < units.length - 1) { value /= 1000; index += 1; } const decimals = value < 10 && index > 1 ? 1 : 0; return value.toFixed(decimals) + " " + units[index]; } function usedFraction(filesystem: var): real { const size = Number(filesystem?.sizeBytes ?? 0); if (!(size > 0)) return 0; return Math.max(0, Math.min(1, Number(filesystem.usedBytes ?? 0) / size)); } // "/ and /home" rather than two rows: btrfs subvolumes share one pool of // free space, and showing them separately doubles it on screen. function mountLabel(filesystem: var): string { const points = filesystem?.mountpoints ?? []; if (points.length === 0) return String(filesystem?.device ?? ""); if (points.length === 1) return points[0]; return points.slice(0, -1).join(", ") + " and " + points[points.length - 1]; } function healthSummary(drive: var): string { if (!drive) return ""; if (drive.healthy === false) return (drive.warnings ?? []).length > 0 ? "Reporting " + drive.warnings.join(", ") : "Reporting a failure"; if (drive.healthy === true) return "No warnings"; return "Health not reported"; } function refresh(): void { if (!query.running) query.running = true; } function scan(): void { if (root.scanning) return; root.scanning = true; folderScan.running = true; } function unmount(devicePath: string): void { root.runMedia(["unmount", devicePath]); } function eject(devicePath: string): void { root.runMedia(["eject", devicePath]); } function runMedia(arguments: var): void { if (media.running) return; root.lastError = ""; media.command = [root.helperPath].concat(arguments); media.running = true; } Process { id: query command: [root.helperPath, "snapshot"] stdout: StdioCollector { onStreamFinished: { try { const parsed = JSON.parse(this.text); root.drives = Array.isArray(parsed.drives) ? parsed.drives : []; root.filesystems = Array.isArray(parsed.filesystems) ? parsed.filesystems : []; root.swap = Array.isArray(parsed.swap) ? parsed.swap : []; root.lastError = ""; } catch (error) { root.drives = []; root.filesystems = []; root.lastError = "Could not read the storage helper's output."; console.warn("Disks: could not parse helper output:", error); } root.scanned = true; } } stderr: StdioCollector { onStreamFinished: if (this.text.trim() !== "") root.lastError = this.text.trim() } } Process { id: folderScan command: [root.helperPath, "scan"] stdout: StdioCollector { onStreamFinished: { try { const parsed = JSON.parse(this.text); root.folders = Array.isArray(parsed.folders) ? parsed.folders : []; root.containers = parsed.containers ?? null; root.folderScanTruncated = parsed.truncated === true; root.foldersMeasured = true; } catch (error) { root.lastError = "Could not measure what is using the drive."; console.warn("Disks: could not parse scan output:", error); } } } onExited: root.scanning = false } Process { id: media stderr: StdioCollector { onStreamFinished: if (this.text.trim() !== "") root.lastError = this.text.trim() } // Re-read rather than assuming: a device may refuse to unmount because // something still has a file open on it. onExited: root.refresh() } }