* feat(dashboard): add configurable performance resources - Add config options to show/hide Battery, GPU, CPU, Memory, Storage - Make dashboard responsive based on number of visible resources - Scale resource sizes and spacing dynamically for 3, 4, or 5 items - Battery shows charge status and time remaining/to full - Each resource can be individually toggled via config * fix(dashboard): add dynamic right margin for last visible resource Ensures the rightmost resource always has proper margin to prevent content from being cut off at the edge * fix(performance): comment out duplicated value2 properties for memory and storage resources * controlcenter: add settings for dashboard * feat: handle readonly properties and re-usable codes * Feature/performance tab rework (#5) * dashboard/performance: rework tab with card-based grid layout - Replace circular arc meters with card-based grid layout - CPU/GPU cards show hardware name, usage and temperature with horizontal bars - Memory card with 3/4 arc indicator and used/total at bottom - Storage card shows physical disks from lsblk with aggregated partition usage - Add cpuName, gpuName, cpuFreq, cpuMaxFreq, disks properties to SystemUsage - Clean hardware names (remove Intel/AMD/NVIDIA prefixes, TM/R symbols) * dashboard/performance: new hero card design * dashboard/performance: update storage indicators to be reponsive to the physical disks count * dashboard/performance: fix the overlay bounding issue * dashboard/perfromance: refactor code * dashboard/performance: add battery gauge * dashboard/performance: correct battery icon * dashboard/performance: configurable battery * dashboard/performance: update layout * dashboard/performance: move the "Usage" text on top and smaller the font size * dashboard/performance: add a lot of configurations * dashboard/performance: add network metrics * fix: issue with hot reload * chore: update default vaule for mainValueSpacing to 0 * chore: group settings into collapasible sections * chore: making GPU & Battery toggle not showing if not found * chore: fix network widget spacing & text * chore: remove old disk bars configs, add update interval * chore: remove old & unused value, functions * chore: network graph update smoothly when data points change * chore: refactor settings - de-flood settings, most of the font & size setting now follow the global Appearance config - Most of sliders are not needed anymore, only keep the update interval slider - clean up * chore: remove readonly properties from the controlcenter/dashboard. * chore: minor fix * fix: fix warning about onPercChange() * fix: network metrics negative number * fix: add minimal height & width, placeholder for none toggled * fix: network graph move smoothly (#6) * fix: network graph move smoothly * clean up * fix: graph animation even more smooth * fix: padding issue * chore: network icons short description * fix --------- Co-authored-by: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com>
339 lines
12 KiB
QML
339 lines
12 KiB
QML
pragma Singleton
|
|
|
|
import qs.config
|
|
import Quickshell
|
|
import Quickshell.Io
|
|
import QtQuick
|
|
|
|
Singleton {
|
|
id: root
|
|
|
|
// CPU properties
|
|
property string cpuName: ""
|
|
property real cpuPerc
|
|
property real cpuTemp
|
|
|
|
// GPU properties
|
|
readonly property string gpuType: Config.services.gpuType.toUpperCase() || autoGpuType
|
|
property string autoGpuType: "NONE"
|
|
property string gpuName: ""
|
|
property real gpuPerc
|
|
property real gpuTemp
|
|
|
|
// Memory properties
|
|
property real memUsed
|
|
property real memTotal
|
|
readonly property real memPerc: memTotal > 0 ? memUsed / memTotal : 0
|
|
|
|
// Storage properties (aggregated)
|
|
readonly property real storagePerc: {
|
|
let totalUsed = 0;
|
|
let totalSize = 0;
|
|
for (const disk of disks) {
|
|
totalUsed += disk.used;
|
|
totalSize += disk.total;
|
|
}
|
|
return totalSize > 0 ? totalUsed / totalSize : 0;
|
|
}
|
|
|
|
// Individual disks: Array of { mount, used, total, free, perc }
|
|
property var disks: []
|
|
|
|
property real lastCpuIdle
|
|
property real lastCpuTotal
|
|
|
|
property int refCount
|
|
|
|
function cleanCpuName(name: string): string {
|
|
return name.replace(/\(R\)/gi, "").replace(/\(TM\)/gi, "").replace(/CPU/gi, "").replace(/\d+th Gen /gi, "").replace(/\d+nd Gen /gi, "").replace(/\d+rd Gen /gi, "").replace(/\d+st Gen /gi, "").replace(/Core /gi, "").replace(/Processor/gi, "").replace(/\s+/g, " ").trim();
|
|
}
|
|
|
|
function cleanGpuName(name: string): string {
|
|
return name.replace(/NVIDIA GeForce /gi, "").replace(/NVIDIA /gi, "").replace(/AMD Radeon /gi, "").replace(/AMD /gi, "").replace(/Intel /gi, "").replace(/\(R\)/gi, "").replace(/\(TM\)/gi, "").replace(/Graphics/gi, "").replace(/\s+/g, " ").trim();
|
|
}
|
|
|
|
function formatKib(kib: real): var {
|
|
const mib = 1024;
|
|
const gib = 1024 ** 2;
|
|
const tib = 1024 ** 3;
|
|
|
|
if (kib >= tib)
|
|
return {
|
|
value: kib / tib,
|
|
unit: "TiB"
|
|
};
|
|
if (kib >= gib)
|
|
return {
|
|
value: kib / gib,
|
|
unit: "GiB"
|
|
};
|
|
if (kib >= mib)
|
|
return {
|
|
value: kib / mib,
|
|
unit: "MiB"
|
|
};
|
|
return {
|
|
value: kib,
|
|
unit: "KiB"
|
|
};
|
|
}
|
|
|
|
Timer {
|
|
running: root.refCount > 0
|
|
interval: Config.dashboard.resourceUpdateInterval
|
|
repeat: true
|
|
triggeredOnStart: true
|
|
onTriggered: {
|
|
stat.reload();
|
|
meminfo.reload();
|
|
storage.running = true;
|
|
gpuUsage.running = true;
|
|
sensors.running = true;
|
|
}
|
|
}
|
|
|
|
// One-time CPU info detection (name)
|
|
FileView {
|
|
id: cpuinfoInit
|
|
|
|
path: "/proc/cpuinfo"
|
|
onLoaded: {
|
|
const nameMatch = text().match(/model name\s*:\s*(.+)/);
|
|
if (nameMatch)
|
|
root.cpuName = root.cleanCpuName(nameMatch[1]);
|
|
}
|
|
}
|
|
|
|
FileView {
|
|
id: stat
|
|
|
|
path: "/proc/stat"
|
|
onLoaded: {
|
|
const data = text().match(/^cpu\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/);
|
|
if (data) {
|
|
const stats = data.slice(1).map(n => parseInt(n, 10));
|
|
const total = stats.reduce((a, b) => a + b, 0);
|
|
const idle = stats[3] + (stats[4] ?? 0);
|
|
|
|
const totalDiff = total - root.lastCpuTotal;
|
|
const idleDiff = idle - root.lastCpuIdle;
|
|
root.cpuPerc = totalDiff > 0 ? (1 - idleDiff / totalDiff) : 0;
|
|
|
|
root.lastCpuTotal = total;
|
|
root.lastCpuIdle = idle;
|
|
}
|
|
}
|
|
}
|
|
|
|
FileView {
|
|
id: meminfo
|
|
|
|
path: "/proc/meminfo"
|
|
onLoaded: {
|
|
const data = text();
|
|
root.memTotal = parseInt(data.match(/MemTotal: *(\d+)/)[1], 10) || 1;
|
|
root.memUsed = (root.memTotal - parseInt(data.match(/MemAvailable: *(\d+)/)[1], 10)) || 0;
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: storage
|
|
|
|
// Get physical disks with aggregated usage from their partitions
|
|
// lsblk outputs: NAME SIZE TYPE FSUSED FSSIZE in bytes
|
|
command: ["lsblk", "-b", "-o", "NAME,SIZE,TYPE,FSUSED,FSSIZE", "-P"]
|
|
stdout: StdioCollector {
|
|
onStreamFinished: {
|
|
const diskMap = {}; // Map disk name -> { name, totalSize, used, fsTotal }
|
|
const lines = text.trim().split("\n");
|
|
|
|
for (const line of lines) {
|
|
if (line.trim() === "")
|
|
continue;
|
|
|
|
// Parse KEY="VALUE" format
|
|
const nameMatch = line.match(/NAME="([^"]+)"/);
|
|
const sizeMatch = line.match(/SIZE="([^"]+)"/);
|
|
const typeMatch = line.match(/TYPE="([^"]+)"/);
|
|
const fsusedMatch = line.match(/FSUSED="([^"]*)"/);
|
|
const fssizeMatch = line.match(/FSSIZE="([^"]*)"/);
|
|
|
|
if (!nameMatch || !typeMatch)
|
|
continue;
|
|
|
|
const name = nameMatch[1];
|
|
const type = typeMatch[1];
|
|
const size = parseInt(sizeMatch?.[1] || "0", 10);
|
|
const fsused = parseInt(fsusedMatch?.[1] || "0", 10);
|
|
const fssize = parseInt(fssizeMatch?.[1] || "0", 10);
|
|
|
|
if (type === "disk") {
|
|
// Skip zram (swap) devices
|
|
if (name.startsWith("zram"))
|
|
continue;
|
|
|
|
// Initialize disk entry
|
|
if (!diskMap[name]) {
|
|
diskMap[name] = {
|
|
name: name,
|
|
totalSize: size,
|
|
used: 0,
|
|
fsTotal: 0
|
|
};
|
|
}
|
|
} else if (type === "part") {
|
|
// Find parent disk (remove trailing numbers/p+numbers)
|
|
let parentDisk = name.replace(/p?\d+$/, "");
|
|
// For nvme devices like nvme0n1p1, parent is nvme0n1
|
|
if (name.match(/nvme\d+n\d+p\d+/))
|
|
parentDisk = name.replace(/p\d+$/, "");
|
|
|
|
// Aggregate partition usage to parent disk
|
|
if (diskMap[parentDisk]) {
|
|
diskMap[parentDisk].used += fsused;
|
|
diskMap[parentDisk].fsTotal += fssize;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Convert map to sorted array
|
|
const diskList = [];
|
|
let totalUsed = 0;
|
|
let totalSize = 0;
|
|
|
|
for (const diskName of Object.keys(diskMap).sort()) {
|
|
const disk = diskMap[diskName];
|
|
// Use filesystem total if available, otherwise use disk size
|
|
const total = disk.fsTotal > 0 ? disk.fsTotal : disk.totalSize;
|
|
const used = disk.used;
|
|
const perc = total > 0 ? used / total : 0;
|
|
|
|
// Convert bytes to KiB for consistency with formatKib
|
|
diskList.push({
|
|
mount: disk.name // Using 'mount' property for compatibility
|
|
,
|
|
used: used / 1024,
|
|
total: total / 1024,
|
|
free: (total - used) / 1024,
|
|
perc: perc
|
|
});
|
|
|
|
totalUsed += used;
|
|
totalSize += total;
|
|
}
|
|
|
|
root.disks = diskList;
|
|
}
|
|
}
|
|
}
|
|
|
|
// GPU name detection (one-time)
|
|
Process {
|
|
id: gpuNameDetect
|
|
|
|
running: true
|
|
command: ["sh", "-c", "nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null || lspci 2>/dev/null | grep -i 'vga\\|3d\\|display' | head -1"]
|
|
stdout: StdioCollector {
|
|
onStreamFinished: {
|
|
const output = text.trim();
|
|
if (!output)
|
|
return;
|
|
|
|
// Check if it's from nvidia-smi (clean GPU name)
|
|
if (output.toLowerCase().includes("nvidia") || output.toLowerCase().includes("geforce") || output.toLowerCase().includes("rtx") || output.toLowerCase().includes("gtx")) {
|
|
root.gpuName = root.cleanGpuName(output);
|
|
} else {
|
|
// Parse lspci output: extract name from brackets or after colon
|
|
const bracketMatch = output.match(/\[([^\]]+)\]/);
|
|
if (bracketMatch) {
|
|
root.gpuName = root.cleanGpuName(bracketMatch[1]);
|
|
} else {
|
|
const colonMatch = output.match(/:\s*(.+)/);
|
|
if (colonMatch)
|
|
root.gpuName = root.cleanGpuName(colonMatch[1]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: gpuTypeCheck
|
|
|
|
running: !Config.services.gpuType
|
|
command: ["sh", "-c", "if command -v nvidia-smi &>/dev/null && nvidia-smi -L &>/dev/null; then echo NVIDIA; elif ls /sys/class/drm/card*/device/gpu_busy_percent 2>/dev/null | grep -q .; then echo GENERIC; else echo NONE; fi"]
|
|
stdout: StdioCollector {
|
|
onStreamFinished: root.autoGpuType = text.trim()
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: gpuUsage
|
|
|
|
command: root.gpuType === "GENERIC" ? ["sh", "-c", "cat /sys/class/drm/card*/device/gpu_busy_percent"] : root.gpuType === "NVIDIA" ? ["nvidia-smi", "--query-gpu=utilization.gpu,temperature.gpu", "--format=csv,noheader,nounits"] : ["echo"]
|
|
stdout: StdioCollector {
|
|
onStreamFinished: {
|
|
if (root.gpuType === "GENERIC") {
|
|
const percs = text.trim().split("\n");
|
|
const sum = percs.reduce((acc, d) => acc + parseInt(d, 10), 0);
|
|
root.gpuPerc = sum / percs.length / 100;
|
|
} else if (root.gpuType === "NVIDIA") {
|
|
const [usage, temp] = text.trim().split(",");
|
|
root.gpuPerc = parseInt(usage, 10) / 100;
|
|
root.gpuTemp = parseInt(temp, 10);
|
|
} else {
|
|
root.gpuPerc = 0;
|
|
root.gpuTemp = 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: sensors
|
|
|
|
command: ["sensors"]
|
|
environment: ({
|
|
LANG: "C.UTF-8",
|
|
LC_ALL: "C.UTF-8"
|
|
})
|
|
stdout: StdioCollector {
|
|
onStreamFinished: {
|
|
let cpuTemp = text.match(/(?:Package id [0-9]+|Tdie):\s+((\+|-)[0-9.]+)(°| )C/);
|
|
if (!cpuTemp)
|
|
// If AMD Tdie pattern failed, try fallback on Tctl
|
|
cpuTemp = text.match(/Tctl:\s+((\+|-)[0-9.]+)(°| )C/);
|
|
|
|
if (cpuTemp)
|
|
root.cpuTemp = parseFloat(cpuTemp[1]);
|
|
|
|
if (root.gpuType !== "GENERIC")
|
|
return;
|
|
|
|
let eligible = false;
|
|
let sum = 0;
|
|
let count = 0;
|
|
|
|
for (const line of text.trim().split("\n")) {
|
|
if (line === "Adapter: PCI adapter")
|
|
eligible = true;
|
|
else if (line === "")
|
|
eligible = false;
|
|
else if (eligible) {
|
|
let match = line.match(/^(temp[0-9]+|GPU core|edge)+:\s+\+([0-9]+\.[0-9]+)(°| )C/);
|
|
if (!match)
|
|
// Fall back to junction/mem if GPU doesn't have edge temp (for AMD GPUs)
|
|
match = line.match(/^(junction|mem)+:\s+\+([0-9]+\.[0-9]+)(°| )C/);
|
|
|
|
if (match) {
|
|
sum += parseFloat(match[2]);
|
|
count++;
|
|
}
|
|
}
|
|
}
|
|
|
|
root.gpuTemp = count > 0 ? sum / count : 0;
|
|
}
|
|
}
|
|
}
|
|
}
|