Files
dotfiles/quickshell/services/Sys.qml
T

185 lines
6.6 KiB
QML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
// Hardware telemetry, sourced from the user's `fluxo` daemon. fluxo's hardware
// modules are configured to emit raw pipe-delimited values (see
// ~/.config/fluxo/config.toml); everything here is parsing and formatting.
//
// Fed by a single long-lived `fluxo stream` subscription. This used to be two
// Timers driving seven separate `Process` objects, each forking a `fluxo
// <module>` client — five forks every 2 s plus two more every 20 s, so about
// 2.5 process spawns a second, each one exec'ing and dynamically linking a 13 MB
// binary to read a number the daemon already had in memory, and each metric
// still up to one interval stale. `fluxo stream` keeps one connection open and
// pushes a JSON line only when a module's output actually changes: no forks, and
// values land as soon as the daemon has them.
Singleton {
id: root
// ------------------------------------------------------------------ cpu
property real cpuUsage: 0
property real cpuTemp: 0
property string cpuModel: ""
// ------------------------------------------------------------------ mem
property real memUsed: 0 // GB
property real memTotal: 0 // GB
readonly property real memPercent: memTotal > 0 ? memUsed / memTotal * 100 : 0
// ------------------------------------------------------------------ gpu
property bool gpuAvailable: false
property real gpuUsage: 0
property real gpuVramUsed: 0
property real gpuVramTotal: 0
property real gpuTemp: 0
property string gpuModel: ""
// ----------------------------------------------------------------- disk
property real diskUsed: 0 // GB
property real diskTotal: 0 // GB
property real diskPercent: 0
property string diskFree: ""
// ------------------------------------------------------------------ sys
property string uptime: ""
property real load1: 0
property real load5: 0
property real load15: 0
property int procs: 0
// ------------------------------------------------------------------ net
property string netInterface: ""
property string netIp: ""
property real netRx: 0 // MB/s
property real netTx: 0 // MB/s
readonly property bool netUp: netInterface !== "" && netInterface !== "none"
readonly property bool netWireless: netInterface.startsWith("wl")
// -------------------------------------------------------------- battery
// UPower drives the battery UI; fluxo supplies the TLP/AC detail line.
property string powerDetail: ""
// The one-shot `fluxo <module>` client wraps text in zero-width spaces so
// Waybar's proportional font stops reflowing. The stream deliberately does
// not, but stripping them costs nothing and keeps this parser usable against
// either source.
function _clean(s: string): string {
return (s || "").replace(//g, "").trim();
}
function _num(v): real {
const n = parseFloat(v);
return isNaN(n) ? 0 : n;
}
// ------------------------------------------------------------------ feed
// Modules fluxo pushes to us. `disk` streams the default mount, which fluxo
// resolves to "/" (see `signaler_default_args` in its registry).
readonly property var modules: ["cpu", "mem", "gpu", "net", "sys", "disk", "power"]
// One line of the stream: {"module":"cpu","text":"7.4|41.0","tooltip":...}.
function _ingest(line: string): void {
let ev;
try {
ev = JSON.parse(line);
} catch (e) {
return;
}
if (!ev || !ev.module)
return;
const parts = root._clean(ev.text).split("|");
const tip = ev.tooltip || "";
switch (ev.module) {
case "cpu":
root.cpuUsage = root._num(parts[0]);
root.cpuTemp = root._num(parts[1]);
root.cpuModel = tip;
break;
case "mem":
root.memUsed = root._num(parts[0]);
root.memTotal = root._num(parts[1]);
break;
case "gpu":
// fluxo emits a single non-numeric field ("No GPU") when it cannot
// find one, so field count is the availability test.
if (parts.length < 4) {
root.gpuAvailable = false;
break;
}
root.gpuAvailable = true;
root.gpuUsage = root._num(parts[0]);
root.gpuVramUsed = root._num(parts[1]);
root.gpuVramTotal = root._num(parts[2]);
root.gpuTemp = root._num(parts[3]);
const gm = /Model:\s*(.+)/.exec(tip);
if (gm)
root.gpuModel = gm[1];
break;
case "net":
root.netInterface = parts[0] || "";
root.netIp = parts[1] || "";
root.netRx = root._num(parts[2]);
root.netTx = root._num(parts[3]);
break;
case "sys":
root.uptime = parts[0] || "";
root.load1 = root._num(parts[1]);
root.load5 = root._num(parts[2]);
root.load15 = root._num(parts[3]);
const pm = /Processes:\s*(\d+)/.exec(tip);
if (pm)
root.procs = parseInt(pm[1]);
break;
case "disk":
root.diskUsed = root._num(parts[1]);
root.diskTotal = root._num(parts[2]);
root.diskPercent = ev.percentage || 0;
const fm = /Free:\s*(\S+)/.exec(tip);
root.diskFree = fm ? fm[1] : "";
break;
case "power":
root.powerDetail = tip;
break;
}
}
Process {
id: source
command: ["fluxo", "stream"].concat(root.modules)
running: true
stdout: SplitParser {
onRead: data => root._ingest(data)
}
// The daemon restarting (or not being up yet at login) drops us. Retry
// on a slow cadence: this is a reconnect, not a poll, so it only ever
// fires while the stream is actually down.
onExited: retry.restart()
}
Timer {
id: retry
interval: 2000
repeat: false
onTriggered: source.running = true
}
// ------------------------------------------------------------ formatting
function rate(mbps: real): string {
if (mbps >= 1) return mbps.toFixed(1) + " MB/s";
const kb = mbps * 1024;
if (kb >= 1) return Math.round(kb) + " KB/s";
return "idle";
}
function gb(v: real): string {
return v >= 100 ? Math.round(v) + "G" : v.toFixed(1) + "G";
}
}