aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/island
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-10-16 09:34:51 +0200
committerDanilo M. <danix@danix.xyz>2026-10-16 09:35:05 +0200
commitcb936e93be75bae93fbd5a81f39376cd31142e71 (patch)
tree89df0e88f53b87170c6f1e4e24236c68b87ee20e /desktop/island
parentcf89a5d88fb738883e9663b9e44afcc7b7f98807 (diff)
downloadquickshell-cb936e93be75bae93fbd5a81f39376cd31142e71.tar.gz
quickshell-cb936e93be75bae93fbd5a81f39376cd31142e71.zip
docs(spec): user details in the drawer
A drawer row greets the login user and opens a page that edits name, email, avatar, language and location, written where other programs already read them: AccountsService (and through it the passwd GECOS LibreOffice reads), KDE's emaildefaults and the global git config, with the surname shortened to an initial for git. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'desktop/island')
-rw-r--r--desktop/island/perf/Perf.js187
-rw-r--r--desktop/island/perf/Perf.qml303
-rw-r--r--desktop/island/perf/PerfPanel.qml336
-rw-r--r--desktop/island/perf/test_perf.js153
4 files changed, 0 insertions, 979 deletions
diff --git a/desktop/island/perf/Perf.js b/desktop/island/perf/Perf.js
deleted file mode 100644
index b853f50..0000000
--- a/desktop/island/perf/Perf.js
+++ /dev/null
@@ -1,187 +0,0 @@
-.pragma library
-
-// 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.
-
-// The performance panel's parsing and arithmetic, kept pure so it runs under
-// node (test_perf.js). Perf.qml does the reading; everything here takes text
-// or numbers and returns values. A reading that cannot be made is null, and
-// the panel draws null as a dash.
-
-const UNITS = { "": 1, B: 1, KiB: 1024, MiB: 1024 ** 2, GiB: 1024 ** 3 };
-
-// A public IP older than this is not shown: by then the fetch has failed
-// for hours, and an address that may no longer be ours is worse than a dash.
-const STALE = 4 * 3600;
-
-// One {total, idle} per cpu line of /proc/stat; index 0 is the aggregate,
-// then the cores in file order. idle counts iowait, as conky did. Only the
-// first eight fields are summed: guest and guest_nice are already inside
-// user and nice, and adding them again overstates load while VMs run.
-function cpuTimes(stat) {
- const out = [];
- for (const line of stat.split("\n")) {
- const m = /^cpu\d*\s+(.*)$/.exec(line);
- if (!m) continue;
- const v = m[1].trim().split(/\s+/).map(Number);
- if (v.length < 5) continue;
- out.push({ total: v.slice(0, 8).reduce((a, b) => a + b, 0), idle: v[3] + v[4] });
- }
- return out;
-}
-
-// Percent busy between two samples.
-function load(prev, cur) {
- if (!prev || !cur) return null;
- const dt = cur.total - prev.total;
- if (dt <= 0) return null;
- return Math.max(0, Math.min(100, 100 * (1 - (cur.idle - prev.idle) / dt)));
-}
-
-// {total, used} in bytes; used is MemTotal - MemAvailable.
-function meminfo(text) {
- const kb = key => {
- const m = new RegExp("^" + key + ":\\s+(\\d+)", "m").exec(text);
- return m ? Number(m[1]) * 1024 : null;
- };
- const total = kb("MemTotal"), avail = kb("MemAvailable");
- if (total === null || avail === null) return null;
- return { total: total, used: total - avail };
-}
-
-// Rows of `df -P -B1`. The header fails the percent match and is skipped.
-// host is the NFS server from the device field, "" for a local disk, so the
-// panel labels shares without an address in the repo.
-function df(text) {
- const out = [];
- for (const line of text.split("\n")) {
- const m = /^(\S+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)%\s+(\S+)\s*$/.exec(line);
- if (!m) continue;
- const h = /^([^/:]+):/.exec(m[1]);
- out.push({ mount: m[6], host: h ? h[1] : "", size: Number(m[2]),
- used: Number(m[3]), pct: Number(m[5]) });
- }
- return out;
-}
-
-// VRAM in bytes from the fdinfo scan: drm-resident-vram0 of every xe fd,
-// counted once per drm-client-id because a client holds several fds.
-function vram(text) {
- const fds = {};
- for (const line of text.split("\n")) {
- const m = /^([^:]*):(drm-[\w-]+):\s*(.*)$/.exec(line);
- if (!m) continue;
- (fds[m[1]] = fds[m[1]] || {})[m[2]] = m[3];
- }
- const seen = {};
- let total = null;
- for (const path in fds) {
- const fd = fds[path];
- const id = fd["drm-client-id"];
- const n = /^(\d+)\s*(\w*)/.exec(fd["drm-resident-vram0"] || "");
- if (fd["drm-driver"] !== "xe" || !id || !n || seen[id]) continue;
- seen[id] = true;
- total = (total || 0) + Number(n[1]) * (UNITS[n[2]] || 1);
- }
- return total;
-}
-
-// Bytes per second between two counter readings. A counter that went
-// backwards (interface reset) gives null rather than a negative spike.
-function rate(prev, cur, secs) {
- if (prev === null || cur === null || !(secs > 0) || cur < prev) return null;
- return (cur - prev) / secs;
-}
-
-// `grep -H . /sys/class/hwmon/hwmon*/name` -> {chip: dir}. hwmonN numbers
-// change across boots, so chips are found by name.
-function hwmonMap(text) {
- const map = {};
- for (const line of text.split("\n")) {
- const m = /^(.*)\/name:(.+)$/.exec(line);
- if (m && !(m[2] in map)) map[m[2]] = m[1];
- }
- return map;
-}
-
-// `ip -4 -o addr show <iface>` -> the first address, without its prefix.
-function lanAddr(text) {
- const m = /\binet (\d+\.\d+\.\d+\.\d+)\//.exec(text);
- return m ? m[1] : "";
-}
-
-// The first model name in /proc/cpuinfo, trimmed the way conky trimmed it.
-function cpuModel(cpuinfo) {
- const m = /^model name\s*:\s*(.+)$/m.exec(cpuinfo);
- if (!m) return "";
- return m[1].replace(/\((R|TM|tm)\)/g, "")
- .replace(/^\s*\d+th Gen\s+/, "")
- .replace(/^\s*(AMD|Intel)\s+/, "")
- .replace(/\s+\d+-Core Processor.*$/, "")
- .replace(/\s+/g, " ").trim();
-}
-
-// "ok", "warn", "crit", or "" for a value that could not be read.
-function band(v, warn, crit) {
- if (v === null || v === undefined) return "";
- return v >= crit ? "crit" : v >= warn ? "warn" : "ok";
-}
-
-// The worst of a list of bands; unknowns never raise it.
-function worst(bands) {
- if (bands.indexOf("crit") >= 0) return "crit";
- if (bands.indexOf("warn") >= 0) return "warn";
- return "ok";
-}
-
-function isIPv4(s) {
- return /^(\d{1,3}\.){3}\d{1,3}$/.test(s) && s.split(".").every(o => Number(o) <= 255);
-}
-
-// conky's cache format: "ip <addr>" and "fetched <epoch>" lines.
-function parsePubip(text) {
- const ip = /^ip (\S+)$/m.exec(text), at = /^fetched (\d+)$/m.exec(text);
- return ip && at && isIPv4(ip[1]) ? { ip: ip[1], fetched: Number(at[1]) } : null;
-}
-
-function formatPubip(ip, fetched) {
- return "ip " + ip + "\nfetched " + fetched + "\n";
-}
-
-// What the panel shows and whether the icon warns. Only a fresh value can
-// mismatch: a failed fetch leaves the old one to go stale, never to alarm.
-function ipCheck(pub, now, pin) {
- const fresh = !!pub && now - pub.fetched < STALE;
- return { ip: fresh ? pub.ip : "", mismatch: fresh && !!pin && pub.ip !== pin };
-}
-
-// Whether a fetch is due: no value yet, or one at least `every` seconds old.
-function due(pub, now, every) {
- return !pub || now - pub.fetched >= every;
-}
-
-// A history with v appended, oldest dropped past n.
-function push(list, v, n) {
- const out = list.concat([v]);
- return out.length > n ? out.slice(out.length - n) : out;
-}
-
-function bytes(n) {
- if (n === null || n === undefined) return "--";
- const u = ["B", "K", "M", "G", "T"];
- let i = 0;
- while (n >= 1024 && i < u.length - 1) { n /= 1024; i++; }
- return (i ? n.toFixed(1) : String(Math.round(n))) + u[i];
-}
-
-function speed(n) {
- return n === null || n === undefined ? "--" : bytes(n) + "/s";
-}
diff --git a/desktop/island/perf/Perf.qml b/desktop/island/perf/Perf.qml
deleted file mode 100644
index 53d8670..0000000
--- a/desktop/island/perf/Perf.qml
+++ /dev/null
@@ -1,303 +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.
-
-pragma Singleton
-
-import Quickshell
-import Quickshell.Io
-import QtQuick
-import QtQml
-import "../.."
-import "Perf.js" as P
-
-// The numbers behind the performance panel and the status pill's icon. CPU,
-// RAM, temperatures and disks are sampled always, so the icon can warn while
-// the panel is closed; per-core load, the network rate, VRAM and the fan only
-// while `detailed`, which is any PerfPanel existing (island or drawer).
-// /proc and /sys are FileViews reloaded on a timer, no subprocess; df, the
-// fdinfo scan, ip and curl are Processes.
-Singleton {
- id: root
-
- // PerfPanel instances alive; each counts itself in and out.
- property int viewers: 0
- readonly property bool detailed: viewers > 0
-
- // ponytail: host constants, as conky had them. One mount per NFS server
- // (Library and Slackware are one export, so are shared and backup); br0
- // is a bridge, so VM-to-host copies show in the graph; xe puts no VRAM
- // capacity in sysfs. Edit here when the hardware changes.
- readonly property var mounts: ["/", "/home", "/data", "/mnt/nfs/Library", "/mnt/nfs/shared"]
- readonly property string iface: "br0"
- readonly property real vramTotal: 12 * 1024 * 1024 * 1024
- readonly property string gpuName: "ARC B580"
-
- // Conky's table. Ceilings are per sensor: 70C is unremarkable on a CPU
- // package and alarming on an NVMe. GPU and VRAM are the GPU card's; the
- // rest are the CPU card's rows.
- readonly property var sensors: [
- { chip: "k10temp", file: "temp1_input", label: "CPU", warn: 75, crit: 90 },
- { chip: "k10temp", file: "temp3_input", label: "CCD1", warn: 75, crit: 90 },
- { chip: "nvme", file: "temp1_input", label: "NVME", warn: 60, crit: 70 },
- { chip: "gigabyte_wmi", file: "temp2_input", label: "BOARD", warn: 60, crit: 70 },
- { chip: "gigabyte_wmi", file: "temp3_input", label: "BOARD", warn: 60, crit: 70 },
- { chip: "xe", file: "temp2_input", label: "GPU", warn: 75, crit: 85 },
- { chip: "xe", file: "temp3_input", label: "VRAM", warn: 80, crit: 90 },
- ]
-
- property var hwmon: ({}) // chip name -> /sys/class/hwmon/hwmonN
- property string cpuModel: ""
- property var load: null // aggregate percent
- property var cores: [] // percent per core, while detailed
- property var mem: null // {total, used} bytes
- property var temps: [] // [{label, value, warn, crit}], value null if unread
- property var disks: [] // Perf.js df rows
- property var vram: null // bytes, while detailed
- property var fan: null // rpm, while detailed
- property var rx: [] // bytes/s, newest last, while detailed
- property var tx: []
- property string lan: ""
- property var pub: null // {ip, fetched}
- property real now: Date.now() / 1000
-
- readonly property var ip: P.ipCheck(root.pub, root.now, Prefs.pinnedIp)
-
- // The icon's dot. CPU load is left out: spikes would flicker it, and
- // sustained load shows up as heat.
- readonly property string severity: P.worst(
- root.temps.map(t => P.band(t.value, t.warn, t.crit))
- .concat([P.band(root.mem ? 100 * root.mem.used / root.mem.total : null, 85, 95)])
- .concat(root.disks.map(d => P.band(d.pct, 90, 95)))
- .concat([root.ip.mismatch ? "crit" : "ok"]))
-
- readonly property string pubipPath:
- (Quickshell.env("XDG_CACHE_HOME") || Quickshell.env("HOME") + "/.cache") + "/udt/pubip.txt"
-
- property var prevCpu: []
- property var prevNet: null // {rx, tx, t}
-
- function num(view) {
- const v = parseInt(view.text(), 10);
- return isNaN(v) ? null : v;
- }
-
- // Measured from the last attempt, not the last success, so a failing
- // fetch (rate limited, offline) still waits 30 minutes; and never over
- // one already running, which would kill it before it could answer.
- property real pubAttempt: 0
- function fetchPubip() {
- const now = Date.now() / 1000;
- if (pubProc.running || !P.due(root.pub, now, 1800) || now - root.pubAttempt < 1800) return;
- root.pubAttempt = now;
- pubProc.running = true;
- }
-
- onDetailedChanged: {
- if (detailed) {
- if (!root.lan) lanProc.running = true;
- root.fetchPubip();
- } else {
- root.cores = [];
- root.rx = [];
- root.tx = [];
- root.prevNet = null;
- root.vram = null;
- root.fan = null;
- }
- }
-
- // --- /proc and /sys ---------------------------------------------------
-
- FileView { id: stat; path: "/proc/stat"; blockLoading: true }
- FileView { id: meminfo; path: "/proc/meminfo"; blockLoading: true }
- FileView {
- path: "/proc/cpuinfo"
- blockLoading: true
- onLoaded: root.cpuModel = P.cpuModel(text())
- }
-
- Instantiator {
- id: tempViews
- model: root.sensors
- delegate: FileView {
- required property var modelData
- path: root.hwmon[modelData.chip] ? root.hwmon[modelData.chip] + "/" + modelData.file : ""
- blockLoading: true
- printErrors: false
- }
- }
-
- FileView {
- id: fanView
- path: root.hwmon.xe ? root.hwmon.xe + "/fan1_input" : ""
- blockLoading: true
- printErrors: false
- }
-
- FileView {
- id: rxView
- path: "/sys/class/net/" + root.iface + "/statistics/rx_bytes"
- blockLoading: true
- printErrors: false
- }
-
- FileView {
- id: txView
- path: "/sys/class/net/" + root.iface + "/statistics/tx_bytes"
- blockLoading: true
- printErrors: false
- }
-
- Process {
- running: true
- command: ["sh", "-c", "grep -H . /sys/class/hwmon/hwmon*/name"]
- stdout: StdioCollector { onStreamFinished: root.hwmon = P.hwmonMap(text) }
- }
-
- // CPU, RAM and temperatures always; the fan and VRAM while detailed.
- Timer {
- interval: 2000
- running: true
- repeat: true
- triggeredOnStart: true
- onTriggered: {
- root.now = Date.now() / 1000;
-
- stat.reload();
- const cur = P.cpuTimes(stat.text());
- root.load = P.load(root.prevCpu[0], cur[0]);
- root.cores = root.detailed
- ? cur.slice(1).map((c, i) => P.load(root.prevCpu[i + 1], c) ?? 0)
- : [];
- root.prevCpu = cur;
-
- meminfo.reload();
- root.mem = P.meminfo(meminfo.text());
-
- const t = [];
- for (let i = 0; i < root.sensors.length; i++) {
- const s = root.sensors[i];
- const v = tempViews.objectAt(i);
- let value = null;
- if (v && v.path) {
- v.reload();
- const n = root.num(v);
- value = n === null ? null : n / 1000;
- }
- t.push({ label: s.label, value: value, warn: s.warn, crit: s.crit });
- }
- root.temps = t;
-
- if (root.detailed) {
- if (fanView.path) { fanView.reload(); root.fan = root.num(fanView); }
- if (!vramProc.running) vramProc.running = true;
- }
- }
- }
-
- // ponytail: a shell glob over every fd on the system, ~14ms; only while
- // the panel is open. Only the user's own processes are readable, so a
- // system service's GPU use is not counted, as in conky.
- Process {
- id: vramProc
- command: ["sh", "-c", "grep -sH -e '^drm-driver' -e '^drm-client-id' "
- + "-e '^drm-resident-vram0' /proc/[0-9]*/fdinfo/* 2>/dev/null"]
- stdout: StdioCollector { onStreamFinished: root.vram = P.vram(text) }
- }
-
- // --- network ------------------------------------------------------------
-
- Timer {
- interval: 1000
- running: root.detailed
- repeat: true
- triggeredOnStart: true
- onTriggered: {
- rxView.reload();
- txView.reload();
- const cur = { rx: root.num(rxView), tx: root.num(txView), t: Date.now() / 1000 };
- const p = root.prevNet;
- if (p) {
- const r = P.rate(p.rx, cur.rx, cur.t - p.t);
- const s = P.rate(p.tx, cur.tx, cur.t - p.t);
- if (r !== null) root.rx = P.push(root.rx, r, 60);
- if (s !== null) root.tx = P.push(root.tx, s, 60);
- }
- root.prevNet = cur;
- }
- }
-
- Process {
- id: lanProc
- command: ["ip", "-4", "-o", "addr", "show", root.iface]
- stdout: StdioCollector { onStreamFinished: root.lan = P.lanAddr(text) }
- }
-
- // --- disks --------------------------------------------------------------
-
- // df under timeout: an unreachable NFS server blocks statfs, and the
- // whole call then yields nothing, so the last rows are kept.
- Timer {
- interval: 60000
- running: true
- repeat: true
- triggeredOnStart: true
- onTriggered: { dfProc.running = false; dfProc.running = true; }
- }
-
- Process {
- id: dfProc
- command: ["timeout", "-k", "2", "5", "/usr/bin/df", "-P", "-B1"].concat(root.mounts)
- stdout: StdioCollector {
- onStreamFinished: {
- const rows = P.df(text);
- if (rows.length) root.disks = rows;
- }
- }
- }
-
- // --- public IP ----------------------------------------------------------
-
- // conky's cache file, so the address survives a restart and the fetch
- // keeps conky's 30 minute pace. Checked every minute, fetched when due.
- FileView {
- id: pubFile
- path: root.pubipPath
- atomicWrites: true
- watchChanges: true
- printErrors: false
- onFileChanged: reload()
- onLoaded: root.pub = P.parsePubip(text())
- }
-
- Timer {
- interval: 60000
- running: true
- repeat: true
- onTriggered: root.fetchPubip()
- }
-
- // Anything but an IPv4 address (a failure, an error page) leaves the
- // cache alone: the old value then goes stale rather than mismatching.
- Process {
- id: pubProc
- command: ["curl", "-4fsS", "--max-time", "10", "https://ipinfo.io/ip"]
- stdout: StdioCollector {
- onStreamFinished: {
- const ip = text.trim();
- if (!P.isIPv4(ip)) return;
- const at = Math.floor(Date.now() / 1000);
- root.pub = { ip: ip, fetched: at };
- pubFile.setText(P.formatPubip(ip, at));
- }
- }
- }
-}
diff --git a/desktop/island/perf/PerfPanel.qml b/desktop/island/perf/PerfPanel.qml
deleted file mode 100644
index f149d82..0000000
--- a/desktop/island/perf/PerfPanel.qml
+++ /dev/null
@@ -1,336 +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 QtQuick
-import QtQuick.Layouts
-import "../.."
-import "Perf.js" as P
-
-// CPU, GPU, network and disks as cards in three columns: CPU down the left,
-// GPU and network beside it, disks under those two. Numbers come from Perf;
-// colours follow conky's bands, so they read as they did there.
-GridLayout {
- id: panel
-
- columns: 3
- columnSpacing: 8
- rowSpacing: 8
-
- readonly property real col: (width - 2 * columnSpacing) / 3
- readonly property var gpuTemp: sensor("GPU")
- readonly property var vramTemp: sensor("VRAM")
-
- function tone(v, warn, crit) {
- const b = P.band(v, warn, crit);
- return b === "crit" ? Theme.red : b === "warn" ? Theme.yellow : b === "ok" ? Theme.green : Theme.subtext;
- }
- function deg(v) { return v === null ? "--" : Math.round(v) + "°"; }
- function sensor(label) {
- return Perf.temps.find(t => t.label === label) || { value: null, warn: 0, crit: 0 };
- }
- // Detailed sampling (per-core, network, VRAM, fan) runs while any panel
- // exists. The island loads one only while open and the drawer page only
- // while shown, so counting instances is the whole gate, and two of them
- // never clear it under each other.
- Component.onCompleted: Perf.viewers++
- Component.onDestruction: Perf.viewers--
-
- function pct(part, whole) { return part === null || !whole ? null : 100 * part / whole; }
- function last(list) { return list.length ? list[list.length - 1] : null; }
- // Conky's glyphs, re-checked in Inconsolata Nerd Font.
- function glyph(d) {
- if (d.host) return String.fromCodePoint(0xF06F3); // network share
- return String.fromCodePoint({ "/": 0xF318, "/home": 0xF015 }[d.mount] ?? 0xF02CA);
- }
-
- component Label: Text {
- font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 }
- color: Theme.text
- }
-
- // A label left, a value right, an optional glyph before the label.
- component Pair: Item {
- property string glyph: ""
- property string label: ""
- property string value: ""
- property color labelColor: Theme.subtext
- property color valueColor: Theme.text
- width: parent ? parent.width : 0
- implicitHeight: valueText.implicitHeight
-
- Row {
- spacing: 6
- Text {
- visible: text !== ""
- text: parent.parent.glyph
- font { family: Theme.iconFamily; pixelSize: Theme.fontSize - 2 }
- color: parent.parent.labelColor
- }
- Label { text: parent.parent.label; color: parent.parent.labelColor }
- }
- Label {
- id: valueText
- anchors.right: parent.right
- text: parent.value
- color: parent.valueColor
- }
- }
-
- component Bar: Rectangle {
- property real frac: 0
- property color fill: Theme.green
- width: parent ? parent.width : 0
- height: 5
- radius: 3
- color: Theme.surfaceAlt
- Rectangle {
- width: parent.width * Math.max(0, Math.min(1, parent.frac))
- height: parent.height
- radius: 3
- color: parent.fill
- }
- }
-
- component Card: Rectangle {
- default property alias content: body.data
- property string title: ""
- property string headline: ""
- property color headlineColor: Theme.text
- radius: 10
- color: Qt.alpha(Theme.surface, 0.6)
- implicitHeight: body.implicitHeight + 20
- Layout.fillHeight: true
-
- Column {
- id: body
- x: 10
- y: 10
- width: parent.width - 20
- spacing: 5
-
- Item {
- width: parent.width
- height: Math.max(titleText.implicitHeight, headText.implicitHeight)
- Text {
- id: titleText
- anchors.verticalCenter: parent.verticalCenter
- text: parent.parent.parent.title
- font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4; bold: true }
- color: Theme.subtext
- }
- Text {
- id: headText
- anchors.right: parent.right
- text: parent.parent.parent.headline
- font { family: Theme.fontFamily; pixelSize: Theme.fontSize + 4; bold: true }
- color: parent.parent.parent.headlineColor
- }
- }
- }
- }
-
- // --- CPU ----------------------------------------------------------------
-
- Card {
- Layout.row: 0
- Layout.column: 0
- Layout.rowSpan: 2
- Layout.preferredWidth: panel.col
- title: "CPU"
- headline: Perf.load === null ? "--" : Math.round(Perf.load) + "%"
- headlineColor: panel.tone(Perf.load, 25, 75)
-
- Label {
- width: parent.width
- text: Perf.cpuModel
- color: Theme.subtext
- elide: Text.ElideRight
- }
-
- // One bar per core, rising from a common baseline.
- Row {
- id: eq
- width: parent.width
- height: 40
- spacing: 2
- Repeater {
- model: Perf.cores
- delegate: Item {
- required property var modelData
- width: (eq.width - eq.spacing * (Perf.cores.length - 1)) / Perf.cores.length
- height: eq.height
- Rectangle {
- anchors.bottom: parent.bottom
- width: parent.width
- height: Math.max(1, parent.height * modelData / 100)
- radius: 1
- color: panel.tone(modelData, 25, 75)
- }
- }
- }
- }
-
- Pair {
- label: "RAM"
- value: Perf.mem ? P.bytes(Perf.mem.used) + " / " + P.bytes(Perf.mem.total) : "--"
- }
- Bar {
- frac: Perf.mem ? Perf.mem.used / Perf.mem.total : 0
- fill: panel.tone(panel.pct(Perf.mem ? Perf.mem.used : null, Perf.mem ? Perf.mem.total : 0), 25, 75)
- }
-
- Repeater {
- model: Perf.temps.filter(t => t.label !== "GPU" && t.label !== "VRAM")
- delegate: Pair {
- required property var modelData
- label: modelData.label
- value: panel.deg(modelData.value)
- valueColor: panel.tone(modelData.value, modelData.warn, modelData.crit)
- }
- }
- }
-
- // --- GPU ----------------------------------------------------------------
-
- // No utilisation: xe exposes no busy percentage.
- Card {
- Layout.row: 0
- Layout.column: 1
- Layout.preferredWidth: panel.col
- title: "GPU · " + Perf.gpuName
- headline: panel.deg(panel.gpuTemp.value)
- headlineColor: panel.tone(panel.gpuTemp.value, panel.gpuTemp.warn, panel.gpuTemp.crit)
-
- Pair {
- label: "VRAM"
- value: Perf.vram === null ? "--" : P.bytes(Perf.vram) + " / " + P.bytes(Perf.vramTotal)
- }
- Bar {
- frac: (Perf.vram ?? 0) / Perf.vramTotal
- fill: panel.tone(panel.pct(Perf.vram, Perf.vramTotal), 25, 75)
- }
- Pair {
- label: "VRAM temp"
- value: panel.deg(panel.vramTemp.value)
- valueColor: panel.tone(panel.vramTemp.value, panel.vramTemp.warn, panel.vramTemp.crit)
- }
- Pair {
- label: "Fan"
- value: Perf.fan === null ? "--" : Perf.fan + " rpm"
- }
- }
-
- // --- network ------------------------------------------------------------
-
- Card {
- Layout.row: 0
- Layout.column: 2
- Layout.preferredWidth: panel.col
- title: "NET · " + Perf.iface
-
- Canvas {
- id: graph
- width: parent.width
- height: 44
- onPaint: {
- const ctx = getContext("2d");
- ctx.reset();
- const max = Math.max(1, ...Perf.rx, ...Perf.tx);
- const line = (data, color) => {
- ctx.strokeStyle = color;
- ctx.lineWidth = 1.5;
- ctx.beginPath();
- data.forEach((v, i) => {
- const x = width * i / 59;
- const y = height - 1 - (height - 2) * v / max;
- if (i) ctx.lineTo(x, y); else ctx.moveTo(x, y);
- });
- ctx.stroke();
- };
- line(Perf.rx, Theme.teal);
- line(Perf.tx, Theme.peach);
- }
- Connections {
- target: Perf
- function onRxChanged() { graph.requestPaint(); }
- function onTxChanged() { graph.requestPaint(); }
- }
- }
-
- Pair {
- label: "↓ " + P.speed(panel.last(Perf.rx))
- labelColor: Theme.teal
- value: "↑ " + P.speed(panel.last(Perf.tx))
- valueColor: Theme.peach
- }
- Pair { label: "LAN"; value: Perf.lan || "--" }
-
- // The public IP, red when it differs from the pinned one. The pin
- // stores the address shown; it is lit while they are the same.
- Item {
- width: parent.width
- height: pubText.implicitHeight
-
- Label { text: "Public"; color: Theme.subtext }
- Label {
- id: pubText
- anchors { right: pin.left; rightMargin: 6 }
- text: Perf.ip.ip || "--"
- color: Perf.ip.mismatch ? Theme.red : Theme.text
- }
- Text {
- id: pin
- anchors { right: parent.right; verticalCenter: pubText.verticalCenter }
- text: String.fromCodePoint(0xF0403)
- font { family: Theme.iconFamily; pixelSize: Theme.fontSize - 2 }
- color: Perf.ip.ip !== "" && Perf.ip.ip === Prefs.pinnedIp ? Theme.text : Theme.overlay
-
- MouseArea {
- anchors.fill: parent
- anchors.margins: -4
- enabled: Perf.ip.ip !== ""
- cursorShape: Qt.PointingHandCursor
- onClicked: Prefs.setPinnedIp(Perf.ip.ip)
- }
- }
- }
- }
-
- // --- disks --------------------------------------------------------------
-
- Card {
- Layout.row: 1
- Layout.column: 1
- Layout.columnSpan: 2
- Layout.preferredWidth: 2 * panel.col + panel.columnSpacing
- title: "DISKS"
-
- Repeater {
- model: Perf.disks
- delegate: Column {
- required property var modelData
- width: parent.width
- spacing: 2
- Pair {
- glyph: panel.glyph(modelData)
- label: modelData.host || modelData.mount
- value: modelData.pct + "% · " + P.bytes(modelData.used) + " / " + P.bytes(modelData.size)
- valueColor: panel.tone(modelData.pct, 25, 75)
- }
- Bar {
- frac: modelData.pct / 100
- fill: panel.tone(modelData.pct, 25, 75)
- }
- }
- }
- }
-}
diff --git a/desktop/island/perf/test_perf.js b/desktop/island/perf/test_perf.js
deleted file mode 100644
index 5f39f5d..0000000
--- a/desktop/island/perf/test_perf.js
+++ /dev/null
@@ -1,153 +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.
-
-// Run: node desktop/island/perf/test_perf.js
-// Perf.js is a QML library, so its .pragma line is stripped before node
-// evaluates it, as test_controls.js does for Controls.js.
-
-const fs = require("fs");
-const assert = require("assert");
-
-const src = fs.readFileSync(__dirname + "/Perf.js", "utf8")
- .replace(/^\.pragma library$/m, "");
-const P = new Function(src + "\nreturn { cpuTimes, load, meminfo, df, vram, rate, " +
- "hwmonMap, lanAddr, cpuModel, band, worst, isIPv4, parsePubip, formatPubip, " +
- "ipCheck, due, push, bytes, speed };")();
-
-// cpuTimes: aggregate first, then cores; idle is idle + iowait.
-const stat = [
- "cpu 100 0 50 800 50 0 0 0 0 0",
- "cpu0 50 0 25 400 25 0 0 0 0 0",
- "cpu1 50 0 25 400 25 0 0 0 0 0",
- "intr 12345",
- "ctxt 999",
-].join("\n");
-const t = P.cpuTimes(stat);
-assert.strictEqual(t.length, 3);
-assert.deepStrictEqual(t[0], { total: 1000, idle: 850 });
-assert.deepStrictEqual(t[1], { total: 500, idle: 425 });
-assert.deepStrictEqual(P.cpuTimes(""), []);
-// guest and guest_nice are already inside user and nice: not added twice.
-assert.deepStrictEqual(P.cpuTimes("cpu 100 0 0 100 0 0 0 0 80 0"), [{ total: 200, idle: 100 }]);
-
-// load: busy share of the delta; no prev, or no time passed, is null.
-assert.strictEqual(P.load({ total: 1000, idle: 850 }, { total: 1100, idle: 925 }), 25);
-assert.strictEqual(P.load(undefined, { total: 1, idle: 1 }), null);
-assert.strictEqual(P.load({ total: 5, idle: 5 }, { total: 5, idle: 5 }), null);
-assert.strictEqual(P.load({ total: 0, idle: 0 }, { total: 100, idle: 0 }), 100);
-
-// meminfo: used is total - available, in bytes.
-const mem = "MemTotal: 32000000 kB\nMemFree: 1000 kB\nMemAvailable: 24000000 kB\n";
-assert.deepStrictEqual(P.meminfo(mem), { total: 32000000 * 1024, used: 8000000 * 1024 });
-assert.strictEqual(P.meminfo("MemTotal: 5 kB\n"), null);
-
-// df: header skipped, NFS host from the device field, bytes as numbers.
-const dfOut = [
- "Filesystem 1-blocks Used Available Capacity Mounted on",
- "/dev/mapper/vg-root 263088050176 143921766400 105727733760 58% /",
- "192.0.2.11:/Volume1/Library 3926310780928 3185272160256 541516365824 86% /mnt/nfs/Library",
-].join("\n");
-const d = P.df(dfOut);
-assert.strictEqual(d.length, 2);
-assert.deepStrictEqual(d[0], { mount: "/", host: "", size: 263088050176, used: 143921766400, pct: 58 });
-assert.strictEqual(d[1].host, "192.0.2.11");
-assert.strictEqual(d[1].mount, "/mnt/nfs/Library");
-assert.deepStrictEqual(P.df(""), []);
-
-// vram: summed once per xe client id, units honoured, other drivers ignored.
-const fd = [
- "/proc/1/fdinfo/7:drm-driver:\txe",
- "/proc/1/fdinfo/7:drm-client-id:\t38",
- "/proc/1/fdinfo/7:drm-resident-vram0:\t1024 KiB",
- "/proc/1/fdinfo/10:drm-driver:\txe",
- "/proc/1/fdinfo/10:drm-client-id:\t38",
- "/proc/1/fdinfo/10:drm-resident-vram0:\t1024 KiB",
- "/proc/2/fdinfo/5:drm-driver:\txe",
- "/proc/2/fdinfo/5:drm-client-id:\t40",
- "/proc/2/fdinfo/5:drm-resident-vram0:\t2 MiB",
- "/proc/3/fdinfo/4:drm-driver:\tamdgpu",
- "/proc/3/fdinfo/4:drm-client-id:\t1",
- "/proc/3/fdinfo/4:drm-resident-vram0:\t9 GiB",
-].join("\n");
-assert.strictEqual(P.vram(fd), 1024 * 1024 + 2 * 1024 * 1024);
-assert.strictEqual(P.vram(""), null);
-
-// rate: bytes per second; a counter that went backwards is null.
-assert.strictEqual(P.rate(1000, 3000, 2), 1000);
-assert.strictEqual(P.rate(3000, 1000, 1), null);
-assert.strictEqual(P.rate(null, 1000, 1), null);
-assert.strictEqual(P.rate(1000, 2000, 0), null);
-
-// hwmonMap: chip name -> directory, first one wins.
-const names = "/sys/class/hwmon/hwmon1/name:nvme\n/sys/class/hwmon/hwmon5/name:xe\n/sys/class/hwmon/hwmon7/name:nvme\n";
-assert.deepStrictEqual(P.hwmonMap(names),
- { nvme: "/sys/class/hwmon/hwmon1", xe: "/sys/class/hwmon/hwmon5" });
-
-// lanAddr: the address without its prefix length.
-assert.strictEqual(P.lanAddr("4: br0 inet 192.0.2.5/24 brd 192.0.2.255 scope global br0\n"), "192.0.2.5");
-assert.strictEqual(P.lanAddr(""), "");
-
-// cpuModel: vendor, trademarks and the core-count tail dropped.
-assert.strictEqual(P.cpuModel("processor\t: 0\nmodel name\t: AMD Ryzen 9 7950X 16-Core Processor\n"), "Ryzen 9 7950X");
-assert.strictEqual(P.cpuModel("model name\t: 11th Gen Intel(R) Core(TM) i7-1165G7 @ 2.80GHz\n"), "Core i7-1165G7 @ 2.80GHz");
-assert.strictEqual(P.cpuModel(""), "");
-
-// band and worst: null is "" (unknown), which never raises the worst.
-assert.strictEqual(P.band(10, 25, 75), "ok");
-assert.strictEqual(P.band(25, 25, 75), "warn");
-assert.strictEqual(P.band(75, 25, 75), "crit");
-assert.strictEqual(P.band(null, 25, 75), "");
-assert.strictEqual(P.worst(["ok", "", "warn"]), "warn");
-assert.strictEqual(P.worst(["warn", "crit", "ok"]), "crit");
-assert.strictEqual(P.worst(["", ""]), "ok");
-assert.strictEqual(P.worst([]), "ok");
-
-// isIPv4
-assert.ok(P.isIPv4("203.0.113.5"));
-assert.ok(!P.isIPv4("203.0.113.256"));
-assert.ok(!P.isIPv4("<html>"));
-assert.ok(!P.isIPv4(""));
-
-// The pubip cache, in conky's format, both ways.
-assert.deepStrictEqual(P.parsePubip("ip 203.0.113.5\nfetched 1700000000\n"), { ip: "203.0.113.5", fetched: 1700000000 });
-assert.strictEqual(P.parsePubip("ip nonsense\nfetched 1\n"), null);
-assert.strictEqual(P.parsePubip(""), null);
-assert.strictEqual(P.formatPubip("203.0.113.5", 1700000000), "ip 203.0.113.5\nfetched 1700000000\n");
-assert.deepStrictEqual(P.parsePubip(P.formatPubip("198.51.100.7", 42)), { ip: "198.51.100.7", fetched: 42 });
-
-// ipCheck: no pin ok; match ok; mismatch only when fresh; stale is a dash.
-const now = 1700010000;
-const fresh = { ip: "203.0.113.5", fetched: now - 60 };
-const stale = { ip: "198.51.100.7", fetched: now - 5 * 3600 };
-assert.deepStrictEqual(P.ipCheck(fresh, now, ""), { ip: "203.0.113.5", mismatch: false });
-assert.deepStrictEqual(P.ipCheck(fresh, now, "203.0.113.5"), { ip: "203.0.113.5", mismatch: false });
-assert.deepStrictEqual(P.ipCheck(fresh, now, "198.51.100.7"), { ip: "203.0.113.5", mismatch: true });
-assert.deepStrictEqual(P.ipCheck(stale, now, "203.0.113.5"), { ip: "", mismatch: false });
-assert.deepStrictEqual(P.ipCheck(null, now, "203.0.113.5"), { ip: "", mismatch: false });
-
-// due: no value, or one at least `every` seconds old.
-assert.ok(P.due(null, now, 1800));
-assert.ok(P.due({ ip: "x", fetched: now - 1800 }, now, 1800));
-assert.ok(!P.due({ ip: "x", fetched: now - 1799 }, now, 1800));
-
-// push: newest last, capped.
-assert.deepStrictEqual(P.push([1, 2, 3], 4, 3), [2, 3, 4]);
-assert.deepStrictEqual(P.push([], 1, 3), [1]);
-
-// bytes and speed
-assert.strictEqual(P.bytes(512), "512B");
-assert.strictEqual(P.bytes(1536), "1.5K");
-assert.strictEqual(P.bytes(9.1 * 1024 ** 3), "9.1G");
-assert.strictEqual(P.bytes(null), "--");
-assert.strictEqual(P.speed(1024 * 1024 * 1.2), "1.2M/s");
-assert.strictEqual(P.speed(null), "--");
-
-console.log("test_perf: ok");