309 lines
12 KiB
QML
309 lines
12 KiB
QML
pragma ComponentBehavior: Bound
|
|
|
|
import Quickshell
|
|
import Quickshell.Io
|
|
import QtQuick
|
|
import QtQuick.Layouts
|
|
import ".."
|
|
|
|
// System stats strip: Volume · Network · CPU load · Memory · Temperature · Brightness · Battery
|
|
// All stats refreshed every 5 s via a single bash process.
|
|
// Requires: pactl, ip, awk, nproc, acpi, coreutils. Battery/backlight items hide when unavailable.
|
|
|
|
RowLayout {
|
|
id: root
|
|
spacing: 16 // space between stat groups
|
|
|
|
// ── State ─────────────────────────────────────────────────────────────────
|
|
property int volPct: 0
|
|
property bool volMuted: false
|
|
property real loadAvg: 0
|
|
property int cpuCores: 1
|
|
property real memUsedGB: 0
|
|
property real memTotalGB: 0
|
|
readonly property real memRatio: memTotalGB > 0 ? memUsedGB / memTotalGB : 0
|
|
property int tempC: 0
|
|
property int brightPct: 100
|
|
property int batPct: -1 // -1 means no battery detected
|
|
property string batStatus: ""
|
|
property string batTime: "" // "H:MM" remaining, empty when unknown
|
|
property string netIp: ""
|
|
property string powerProfile: "balanced"
|
|
|
|
readonly property var profileIcons: ({
|
|
"power-saver": "",
|
|
"balanced": "",
|
|
"performance": ""
|
|
})
|
|
|
|
readonly property var profileColors: ({
|
|
"power-saver": Colors.accent,
|
|
"balanced": Colors.fg,
|
|
"performance": Colors.warning
|
|
})
|
|
|
|
// ── Line parser ───────────────────────────────────────────────────────────
|
|
function parseLine(line) {
|
|
if (line.startsWith("vol:")) {
|
|
const p = line.split(":");
|
|
root.volPct = parseInt(p[1]) || 0;
|
|
root.volMuted = p[2] === "1";
|
|
|
|
} else if (line.startsWith("cpu:")) {
|
|
const p = line.split(":");
|
|
root.loadAvg = parseFloat(p[1]) || 0;
|
|
root.cpuCores = parseInt(p[2]) || 1;
|
|
|
|
} else if (line.startsWith("mem:")) {
|
|
const p = line.split(":");
|
|
root.memUsedGB = parseFloat(p[1]) || 0;
|
|
root.memTotalGB = parseFloat(p[2]) || 0;
|
|
|
|
} else if (line.startsWith("temp:")) {
|
|
root.tempC = parseInt(line.slice(5)) || 0;
|
|
|
|
} else if (line.startsWith("bright:")) {
|
|
root.brightPct = parseInt(line.slice(7)) || 100;
|
|
|
|
} else if (line.startsWith("bat:")) {
|
|
const p = line.split(":");
|
|
const cap = parseInt(p[1]);
|
|
root.batPct = isNaN(cap) ? -1 : cap;
|
|
root.batStatus = p[2] || "";
|
|
root.batTime = p.slice(3).join(":") || ""; // rejoin H:MM split by delimiter
|
|
|
|
} else if (line.startsWith("net:")) {
|
|
root.netIp = line.slice(4).trim();
|
|
}
|
|
}
|
|
|
|
// ── Power profile ─────────────────────────────────────────────────────────
|
|
Process {
|
|
id: getPowerProc
|
|
command: ["powerprofilesctl", "get"]
|
|
running: true
|
|
stdout: SplitParser {
|
|
onRead: data => {
|
|
const p = data.trim();
|
|
if (p.length > 0) root.powerProfile = p;
|
|
}
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: setPowerProc
|
|
property string target: ""
|
|
command: ["powerprofilesctl", "set", setPowerProc.target]
|
|
onExited: (code, status) => {
|
|
if (code === 0) getPowerProc.running = true;
|
|
}
|
|
}
|
|
|
|
function cycleProfile() {
|
|
const profiles = ["power-saver", "balanced", "performance"];
|
|
const idx = profiles.indexOf(powerProfile);
|
|
const next = profiles[(idx + 1) % profiles.length];
|
|
powerProfile = next;
|
|
setPowerProc.target = next;
|
|
setPowerProc.running = true;
|
|
}
|
|
|
|
// ── Process ───────────────────────────────────────────────────────────────
|
|
Process {
|
|
id: statsProc
|
|
command: [
|
|
"bash", "-c",
|
|
// Volume
|
|
"vol=$(pactl get-sink-volume @DEFAULT_SINK@ 2>/dev/null | grep -oP '\\d+(?=%)' | head -1); " +
|
|
"muted=$(pactl get-sink-mute @DEFAULT_SINK@ 2>/dev/null | grep -q yes && echo 1 || echo 0); " +
|
|
// CPU: 1-min load average + core count
|
|
"load=$(cut -d' ' -f1 /proc/loadavg); " +
|
|
"cores=$(nproc); " +
|
|
// Memory: used and total in GB
|
|
"mem=$(awk '/MemTotal/{t=$2}/MemAvailable/{a=$2}END{printf \"%.2f:%.2f\",(t-a)/1048576,t/1048576}' /proc/meminfo); " +
|
|
// Temperature (highest thermal zone; falls back to hwmon)
|
|
"temps=$(cat /sys/class/thermal/thermal_zone*/temp 2>/dev/null | sort -n | tail -1); " +
|
|
"[ -z \"$temps\" ] && temps=$(cat /sys/class/hwmon/hwmon*/temp1_input 2>/dev/null | sort -n | tail -1); " +
|
|
// Brightness
|
|
"bright_cur=$(cat /sys/class/backlight/*/brightness 2>/dev/null | head -1); " +
|
|
"bright_max=$(cat /sys/class/backlight/*/max_brightness 2>/dev/null | head -1); " +
|
|
"if [ -n \"$bright_cur\" ] && [ -n \"$bright_max\" ] && [ \"$bright_max\" -gt 0 ]; " +
|
|
"then bright=$((bright_cur*100/bright_max)); else bright=100; fi; " +
|
|
// Battery capacity + status + estimated time (via acpi, H:MM only)
|
|
"bat=$(cat /sys/class/power_supply/BAT*/capacity 2>/dev/null | head -1); " +
|
|
"bat_st=$(cat /sys/class/power_supply/BAT*/status 2>/dev/null | head -1); " +
|
|
"bat_time=$(acpi -b 2>/dev/null | grep -oP '\\d+:\\d+:\\d+' | head -1 | cut -d: -f1-2); " +
|
|
// Network (outbound IP)
|
|
"net=$(ip route get 1.1.1.1 2>/dev/null | grep -oP 'src \\K[\\d.]+' | head -1); " +
|
|
// Normalise
|
|
"temp=$([ -n \"$temps\" ] && echo $((temps/1000)) || echo 0); " +
|
|
"[ -z \"$vol\" ] && vol=0; [ -z \"$bat\" ] && bat=-1; " +
|
|
// Output
|
|
"printf \"vol:%s:%s\\ncpu:%s:%s\\nmem:%s\\ntemp:%s\\nbright:%s\\nbat:%s:%s:%s\\nnet:%s\\n\" " +
|
|
"\"$vol\" \"$muted\" \"$load\" \"$cores\" \"$mem\" \"$temp\" \"$bright\" \"$bat\" \"$bat_st\" \"$bat_time\" \"$net\""
|
|
]
|
|
stdout: SplitParser {
|
|
onRead: function(line) { root.parseLine(line) }
|
|
}
|
|
}
|
|
|
|
Timer {
|
|
interval: 5000
|
|
running: true
|
|
repeat: true
|
|
triggeredOnStart: true
|
|
onTriggered: {
|
|
statsProc.running = false
|
|
statsProc.running = true
|
|
}
|
|
}
|
|
|
|
// ── Volume ────────────────────────────────────────────────────────────────
|
|
Item {
|
|
implicitWidth: volRow.implicitWidth
|
|
implicitHeight: volRow.implicitHeight
|
|
|
|
Process { id: pavucontrolProc; command: ["pavucontrol"] }
|
|
Process { id: volUpProc; command: ["pactl", "set-sink-volume", "@DEFAULT_SINK@", "+5%"] }
|
|
Process { id: volDownProc; command: ["pactl", "set-sink-volume", "@DEFAULT_SINK@", "-5%"] }
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
acceptedButtons: Qt.LeftButton
|
|
onClicked: if (!pavucontrolProc.running) pavucontrolProc.running = true
|
|
onWheel: function(wheel) {
|
|
if (wheel.angleDelta.y > 0) {
|
|
volUpProc.running = false
|
|
volUpProc.running = true
|
|
root.volPct = Math.min(100, root.volPct + 5)
|
|
} else {
|
|
volDownProc.running = false
|
|
volDownProc.running = true
|
|
root.volPct = Math.max(0, root.volPct - 5)
|
|
}
|
|
}
|
|
}
|
|
|
|
RowLayout {
|
|
id: volRow
|
|
anchors.fill: parent
|
|
spacing: 4
|
|
|
|
Text {
|
|
text: root.volMuted ? "" : (root.volPct > 50 ? "" : root.volPct > 0 ? "" : "")
|
|
color: root.volMuted ? Colors.fgDim : Colors.fg
|
|
font.pixelSize: 15
|
|
}
|
|
Text {
|
|
text: root.volMuted ? "mute" : root.volPct + "%"
|
|
color: Colors.fg
|
|
font.pixelSize: 13
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Network ───────────────────────────────────────────────────────────────
|
|
RowLayout {
|
|
spacing: 4
|
|
Text {
|
|
text: root.netIp ? "" : ""
|
|
color: root.netIp ? Colors.fg : Colors.fgDim
|
|
font.pixelSize: 15
|
|
}
|
|
Text {
|
|
visible: root.netIp !== ""
|
|
text: root.netIp
|
|
color: Colors.fg
|
|
font.pixelSize: 13
|
|
}
|
|
}
|
|
|
|
// ── CPU load average + power profile ─────────────────────────────────────
|
|
Item {
|
|
implicitWidth: cpuRow.implicitWidth
|
|
implicitHeight: cpuRow.implicitHeight
|
|
|
|
TapHandler {
|
|
onTapped: root.cycleProfile()
|
|
}
|
|
|
|
RowLayout {
|
|
id: cpuRow
|
|
anchors.fill: parent
|
|
spacing: 4
|
|
|
|
Text {
|
|
text: ""
|
|
color: Colors.fg
|
|
font.pixelSize: 15
|
|
}
|
|
Text {
|
|
text: root.profileIcons[root.powerProfile] ?? ""
|
|
color: root.profileColors[root.powerProfile] ?? Colors.fg
|
|
font.pixelSize: 13
|
|
Behavior on color { ColorAnimation { duration: 80 } }
|
|
}
|
|
Text {
|
|
text: root.loadAvg.toFixed(2)
|
|
color: root.loadAvg > root.cpuCores ? Colors.critical :
|
|
root.loadAvg > root.cpuCores * 0.7 ? Colors.warning : Colors.fg
|
|
font.pixelSize: 13
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Memory ────────────────────────────────────────────────────────────────
|
|
RowLayout {
|
|
spacing: 4
|
|
Text { text: ""; color: Colors.fg; font.pixelSize: 15 }
|
|
Text {
|
|
text: root.memUsedGB.toFixed(1) + "/" + root.memTotalGB.toFixed(1) + "G"
|
|
color: root.memRatio > 0.8 ? Colors.critical :
|
|
root.memRatio > 0.6 ? Colors.warning : Colors.fg
|
|
font.pixelSize: 13
|
|
}
|
|
}
|
|
|
|
// ── Temperature ───────────────────────────────────────────────────────────
|
|
RowLayout {
|
|
spacing: 4
|
|
Text { text: ""; color: Colors.fg; font.pixelSize: 15 }
|
|
Text {
|
|
text: root.tempC + "°"
|
|
color: root.tempC > 85 ? Colors.critical : root.tempC > 65 ? Colors.warning : Colors.fg
|
|
font.pixelSize: 13
|
|
}
|
|
}
|
|
|
|
// ── Brightness ────────────────────────────────────────────────────────────
|
|
RowLayout {
|
|
spacing: 4
|
|
Text { text: ""; color: Colors.fg; font.pixelSize: 15 }
|
|
Text {
|
|
text: root.brightPct + "%"
|
|
color: Colors.fg
|
|
font.pixelSize: 13
|
|
}
|
|
}
|
|
|
|
// ── Battery (hidden on desktops without a battery) ────────────────────────
|
|
RowLayout {
|
|
spacing: 4
|
|
visible: root.batPct >= 0
|
|
Text {
|
|
text: root.batStatus === "Charging" ? "" :
|
|
root.batPct > 80 ? "" :
|
|
root.batPct > 60 ? "" :
|
|
root.batPct > 40 ? "" :
|
|
root.batPct > 20 ? "" : ""
|
|
color: root.batPct < 20 && root.batStatus !== "Charging" ? Colors.critical : Colors.fg
|
|
font.pixelSize: 15
|
|
}
|
|
Text {
|
|
text: root.batTime ? root.batPct + "% · " + root.batTime : root.batPct + "%"
|
|
color: root.batPct < 20 && root.batStatus !== "Charging" ? Colors.critical : Colors.fg
|
|
font.pixelSize: 13
|
|
}
|
|
}
|
|
}
|