1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-07 22:15:38 -05:00
Files
DankMaterialShell/Services/SysMonitorService.qml
2025-08-08 22:53:03 -04:00

1013 lines
32 KiB
QML

pragma Singleton
pragma ComponentBehavior
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Services
import qs.Common
Singleton {
id: root
property int refCount: 0
property int updateInterval: refCount > 0 ? 3000 : 30000
property int maxProcesses: 100
property bool isUpdating: false
property bool staticDataInitialized: false
property var processes: []
property string sortBy: "cpu"
property bool sortDescending: true
property var lastProcTicks: ({})
property real lastTotalJiffies: -1
property real cpuUsage: 0
property real totalCpuUsage: 0
property int cpuCores: 1
property int cpuCount: 1
property string cpuModel: ""
property real cpuFrequency: 0
property real cpuTemperature: -1
property var perCoreCpuUsage: []
property var lastCpuStats: null
property var lastPerCoreStats: null
property real memoryUsage: 0
property real totalMemoryMB: 0
property real usedMemoryMB: 0
property real freeMemoryMB: 0
property real availableMemoryMB: 0
property int totalMemoryKB: 0
property int usedMemoryKB: 0
property int totalSwapKB: 0
property int usedSwapKB: 0
property real networkRxRate: 0
property real networkTxRate: 0
property var lastNetworkStats: null
property real diskReadRate: 0
property real diskWriteRate: 0
property var lastDiskStats: null
property var diskMounts: []
property int historySize: 60
property var cpuHistory: []
property var memoryHistory: []
property var networkHistory: ({
"rx": [],
"tx": []
})
property var diskHistory: ({
"read": [],
"write": []
})
property string kernelVersion: ""
property string distribution: ""
property string hostname: ""
property string architecture: ""
property string loadAverage: ""
property int processCount: 0
property int threadCount: 0
property string bootTime: ""
property string motherboard: ""
property string biosVersion: ""
property var availableGpus: []
// Check if any GPU temperature widgets are configured
function hasGpuTempWidgets() {
const allWidgets = [...(SettingsData.topBarLeftWidgets || []),
...(SettingsData.topBarCenterWidgets || []),
...(SettingsData.topBarRightWidgets || [])]
return allWidgets.some(widget => {
const widgetId = typeof widget === "string" ? widget : widget.id
const widgetEnabled = typeof widget === "string" ? true : (widget.enabled !== false)
return widgetId === "gpuTemp" && widgetEnabled
})
}
// Check if any NVIDIA GPU temperature widgets are configured
function hasNvidiaGpuTempWidgets() {
if (!hasGpuTempWidgets()) return false
const allWidgets = [...(SettingsData.topBarLeftWidgets || []),
...(SettingsData.topBarCenterWidgets || []),
...(SettingsData.topBarRightWidgets || [])]
return allWidgets.some(widget => {
const widgetId = typeof widget === "string" ? widget : widget.id
const widgetEnabled = typeof widget === "string" ? true : (widget.enabled !== false)
if (widgetId !== "gpuTemp" || !widgetEnabled) return false
const selectedGpuIndex = typeof widget === "string" ? 0 : (widget.selectedGpuIndex || 0)
if (availableGpus && availableGpus[selectedGpuIndex]) {
return availableGpus[selectedGpuIndex].driver === "nvidia"
}
return false
})
}
// Check if any non-NVIDIA GPU temperature widgets are configured
function hasNonNvidiaGpuTempWidgets() {
if (!hasGpuTempWidgets()) return false
const allWidgets = [...(SettingsData.topBarLeftWidgets || []),
...(SettingsData.topBarCenterWidgets || []),
...(SettingsData.topBarRightWidgets || [])]
return allWidgets.some(widget => {
const widgetId = typeof widget === "string" ? widget : widget.id
const widgetEnabled = typeof widget === "string" ? true : (widget.enabled !== false)
if (widgetId !== "gpuTemp" || !widgetEnabled) return false
const selectedGpuIndex = typeof widget === "string" ? 0 : (widget.selectedGpuIndex || 0)
if (availableGpus && availableGpus[selectedGpuIndex]) {
return availableGpus[selectedGpuIndex].driver !== "nvidia"
}
return true // Default to true if GPU not found yet (static data might not be loaded)
})
}
function addRef() {
refCount++
if (refCount === 1) {
if (!staticDataInitialized) {
initializeStaticData()
}
updateAllStats()
}
}
function removeRef() {
refCount = Math.max(0, refCount - 1)
}
function initializeStaticData() {
if (!staticDataInitialized) {
staticDataInitialized = true
staticDataProcess.running = true
}
}
function updateAllStats() {
if (refCount > 0) {
isUpdating = true
dynamicStatsProcess.running = true
}
}
function setSortBy(newSortBy) {
if (newSortBy !== sortBy) {
sortBy = newSortBy
sortProcessesInPlace()
}
}
function toggleSortOrder() {
sortDescending = !sortDescending
sortProcessesInPlace()
}
function sortProcessesInPlace() {
if (processes.length === 0)
return
const sortedProcesses = [...processes]
sortedProcesses.sort((a, b) => {
let aVal, bVal
switch (sortBy) {
case "cpu":
aVal = parseFloat(a.cpu) || 0
bVal = parseFloat(b.cpu) || 0
break
case "memory":
aVal = parseFloat(a.memoryPercent) || 0
bVal = parseFloat(b.memoryPercent) || 0
break
case "name":
aVal = a.command || ""
bVal = b.command || ""
break
case "pid":
aVal = parseInt(a.pid) || 0
bVal = parseInt(b.pid) || 0
break
default:
aVal = parseFloat(a.cpu) || 0
bVal = parseFloat(b.cpu) || 0
}
if (typeof aVal === "string") {
return sortDescending ? bVal.localeCompare(
aVal) : aVal.localeCompare(
bVal)
} else {
return sortDescending ? bVal - aVal : aVal - bVal
}
})
processes = sortedProcesses
}
function killProcess(pid) {
if (pid > 0) {
Quickshell.execDetached("kill", [pid.toString()])
}
}
function addToHistory(array, value) {
array.push(value)
if (array.length > historySize)
array.shift()
}
function calculateCpuUsage(currentStats, lastStats) {
if (!lastStats || !currentStats || currentStats.length < 4) {
return 0
}
const currentTotal = currentStats.reduce((sum, val) => sum + val, 0)
const lastTotal = lastStats.reduce((sum, val) => sum + val, 0)
const totalDiff = currentTotal - lastTotal
if (totalDiff <= 0)
return 0
const currentIdle = currentStats[3]
const lastIdle = lastStats[3]
const idleDiff = currentIdle - lastIdle
const usedDiff = totalDiff - idleDiff
return Math.max(0, Math.min(100, (usedDiff / totalDiff) * 100))
}
function parseStaticData(data) {
if (data.cpu) {
cpuCores = data.cpu.count || 1
cpuCount = data.cpu.count || 1
cpuModel = data.cpu.model || ""
}
if (data.system) {
kernelVersion = data.system.kernel || ""
distribution = data.system.distro || ""
hostname = data.system.hostname || ""
architecture = data.system.arch || ""
motherboard = data.system.motherboard || ""
biosVersion = data.system.bios || ""
}
if (data.gpus) {
const gpuList = []
for (const gpu of data.gpus) {
// Parse the display name and PCI ID from rawLine
let displayName = ""
let fullName = ""
let pciId = ""
if (gpu.rawLine) {
// Extract PCI ID [vvvv:dddd]
const pciMatch = gpu.rawLine.match(/\[([0-9a-f]{4}:[0-9a-f]{4})\]/i)
if (pciMatch) {
pciId = pciMatch[1]
}
// Remove BDF and class prefix
let s = gpu.rawLine.replace(/^[^:]+: /, "")
// Remove PCI ID [vvvv:dddd] and everything after
s = s.replace(/\[[0-9a-f]{4}:[0-9a-f]{4}\].*$/i, "")
// Try to extract text after last ']'
const afterBracket = s.match(/\]\s*([^\[]+)$/)
if (afterBracket && afterBracket[1].trim()) {
displayName = afterBracket[1].trim()
} else {
// Try to get last bracketed text
const lastBracket = s.match(/\[([^\]]+)\]([^\[]*$)/)
if (lastBracket) {
displayName = lastBracket[1]
} else {
displayName = s
}
}
// Remove vendor prefixes
displayName = displayName
.replace(/^NVIDIA Corporation\s+/i, "")
.replace(/^NVIDIA\s+/i, "")
.replace(/^Advanced Micro Devices, Inc\.\s+/i, "")
.replace(/^AMD\/ATI\s+/i, "")
.replace(/^AMD\s+/i, "")
.replace(/^ATI\s+/i, "")
.replace(/^Intel Corporation\s+/i, "")
.replace(/^Intel\s+/i, "")
.trim()
} else if (gpu.rawLine && gpu.rawLine.startsWith("NVIDIA")) {
// nvidia-smi fallback case
displayName = gpu.rawLine.replace(/^NVIDIA\s+/, "")
} else {
displayName = "Unknown"
}
// Build full name with vendor prefix
switch(gpu.vendor) {
case "NVIDIA": fullName = "NVIDIA " + displayName; break
case "AMD": fullName = "AMD " + displayName; break
case "Intel": fullName = "Intel " + displayName; break
default: fullName = displayName
}
gpuList.push({
"driver": gpu.driver,
"vendor": gpu.vendor,
"displayName": displayName,
"fullName": fullName,
"pciId": pciId,
"temperature": 0,
"hwmon": "unknown"
})
}
availableGpus = gpuList
}
}
function parseDynamicStats(data) {
updateGpuTemperatures(data.gputemps || [])
parseUnifiedStats(JSON.stringify(data))
}
function updateGpuTemperatures(tempData) {
if (availableGpus.length === 0 || tempData.length === 0) return
const updatedGpus = []
for (let i = 0; i < availableGpus.length; i++) {
const gpu = availableGpus[i]
const tempInfo = tempData.find(t => t.driver === gpu.driver)
if (tempInfo) {
updatedGpus.push({
"driver": gpu.driver,
"vendor": gpu.vendor,
"displayName": gpu.displayName,
"fullName": gpu.fullName,
"pciId": gpu.pciId,
"temperature": tempInfo.temperature || 0,
"hwmon": tempInfo.hwmon || "unknown"
})
} else {
updatedGpus.push({
"driver": gpu.driver,
"vendor": gpu.vendor,
"displayName": gpu.displayName,
"fullName": gpu.fullName,
"pciId": gpu.pciId,
"temperature": gpu.temperature || 0,
"hwmon": gpu.hwmon || "unknown"
})
}
}
availableGpus = updatedGpus
}
function parseUnifiedStats(text) {
function num(x) {
return (typeof x === "number" && !isNaN(x)) ? x : 0
}
let data
try {
data = JSON.parse(text)
} catch (error) {
isUpdating = false
return
}
if (data.memory) {
const m = data.memory
totalMemoryKB = num(m.total)
const free = num(m.free)
const buf = num(m.buffers)
const cached = num(m.cached)
const shared = num(m.shared)
usedMemoryKB = totalMemoryKB - free - buf - cached
totalSwapKB = num(m.swaptotal)
usedSwapKB = num(m.swaptotal) - num(m.swapfree)
totalMemoryMB = totalMemoryKB / 1024
usedMemoryMB = usedMemoryKB / 1024
freeMemoryMB = (totalMemoryKB - usedMemoryKB) / 1024
availableMemoryMB = num(
m.available) ? num(
m.available) / 1024 : (free + buf + cached) / 1024
memoryUsage = totalMemoryKB > 0 ? (usedMemoryKB / totalMemoryKB) * 100 : 0
}
if (data.cpu) {
cpuFrequency = data.cpu.frequency || 0
cpuTemperature = data.cpu.temperature || 0
if (data.cpu.total && data.cpu.total.length >= 8) {
const currentStats = data.cpu.total
const usage = calculateCpuUsage(currentStats, lastCpuStats)
cpuUsage = usage
totalCpuUsage = usage
lastCpuStats = [...currentStats]
}
if (data.cpu.cores) {
const coreUsages = []
for (var i = 0; i < data.cpu.cores.length; i++) {
const currentCoreStats = data.cpu.cores[i]
if (currentCoreStats && currentCoreStats.length >= 8) {
let lastCoreStats = null
if (lastPerCoreStats && lastPerCoreStats[i]) {
lastCoreStats = lastPerCoreStats[i]
}
const usage = calculateCpuUsage(currentCoreStats, lastCoreStats)
coreUsages.push(usage)
}
}
if (JSON.stringify(perCoreCpuUsage) !== JSON.stringify(coreUsages)) {
perCoreCpuUsage = coreUsages
}
lastPerCoreStats = data.cpu.cores.map(core => [...core])
}
}
if (data.network) {
let totalRx = 0
let totalTx = 0
for (const iface of data.network) {
totalRx += iface.rx
totalTx += iface.tx
}
if (lastNetworkStats) {
const timeDiff = updateInterval / 1000
const rxDiff = totalRx - lastNetworkStats.rx
const txDiff = totalTx - lastNetworkStats.tx
networkRxRate = Math.max(0, rxDiff / timeDiff)
networkTxRate = Math.max(0, txDiff / timeDiff)
addToHistory(networkHistory.rx, networkRxRate / 1024)
addToHistory(networkHistory.tx, networkTxRate / 1024)
}
lastNetworkStats = {
"rx": totalRx,
"tx": totalTx
}
}
if (data.disk) {
let totalRead = 0
let totalWrite = 0
for (const disk of data.disk) {
totalRead += disk.read * 512
totalWrite += disk.write * 512
}
if (lastDiskStats) {
const timeDiff = updateInterval / 1000
const readDiff = totalRead - lastDiskStats.read
const writeDiff = totalWrite - lastDiskStats.write
diskReadRate = Math.max(0, readDiff / timeDiff)
diskWriteRate = Math.max(0, writeDiff / timeDiff)
addToHistory(diskHistory.read, diskReadRate / (1024 * 1024))
addToHistory(diskHistory.write, diskWriteRate / (1024 * 1024))
}
lastDiskStats = {
"read": totalRead,
"write": totalWrite
}
}
let totalDiff = 0
if (data.cpu && data.cpu.total && data.cpu.total.length >= 4) {
const currentTotal = data.cpu.total.reduce((s, v) => s + v, 0)
if (lastTotalJiffies > 0)
totalDiff = currentTotal - lastTotalJiffies
lastTotalJiffies = currentTotal
}
if (data.processes) {
const newProcesses = []
for (const proc of data.processes) {
const pid = proc.pid
const pticks = Number(proc.pticks) || 0
const prev = lastProcTicks[pid] ?? null
let cpuShare = 0
if (prev !== null && totalDiff > 0) {
// Per share all CPUs (matches gnome system monitor)
//cpuShare = 100 * Math.max(0, pticks - prev) / totalDiff
// per-share per-core
cpuShare = 100 * cpuCores * Math.max(0, pticks - prev) / totalDiff
}
lastProcTicks[pid] = pticks // update cache
newProcesses.push({
"pid": pid,
"ppid": proc.ppid,
"cpu": cpuShare,
"memoryPercent": proc.pssPercent
?? proc.memoryPercent,
"memoryKB": proc.pssKB ?? proc.memoryKB,
"command": proc.command,
"fullCommand": proc.fullCommand,
"displayName": (proc.command && proc.command.length
> 15) ? proc.command.substring(
0,
15) + "..." : proc.command
})
}
processes = newProcesses
sortProcessesInPlace()
}
if (data.system) {
loadAverage = data.system.loadavg || ""
processCount = data.system.processes || 0
threadCount = data.system.threads || 0
bootTime = data.system.boottime || ""
}
if (data.diskmounts) {
diskMounts = data.diskmounts
}
addToHistory(cpuHistory, cpuUsage)
addToHistory(memoryHistory, memoryUsage)
isUpdating = false
}
function getProcessIcon(command) {
const cmd = command.toLowerCase()
if (cmd.includes("firefox") || cmd.includes("chrome") || cmd.includes(
"browser"))
return "web"
if (cmd.includes("code") || cmd.includes("editor") || cmd.includes("vim"))
return "code"
if (cmd.includes("terminal") || cmd.includes("bash") || cmd.includes("zsh"))
return "terminal"
if (cmd.includes("music") || cmd.includes("audio") || cmd.includes(
"spotify"))
return "music_note"
if (cmd.includes("video") || cmd.includes("vlc") || cmd.includes("mpv"))
return "play_circle"
if (cmd.includes("systemd") || cmd.includes("kernel") || cmd.includes(
"kthread"))
return "settings"
return "memory"
}
function formatCpuUsage(cpu) {
return (cpu || 0).toFixed(1) + "%"
}
function formatMemoryUsage(memoryKB) {
const mem = memoryKB || 0
if (mem < 1024)
return mem.toFixed(0) + " KB"
else if (mem < 1024 * 1024)
return (mem / 1024).toFixed(1) + " MB"
else
return (mem / (1024 * 1024)).toFixed(1) + " GB"
}
function formatSystemMemory(memoryKB) {
const mem = memoryKB || 0
if (mem < 1024 * 1024)
return (mem / 1024).toFixed(0) + " MB"
else
return (mem / (1024 * 1024)).toFixed(1) + " GB"
}
Timer {
id: updateTimer
interval: root.updateInterval
running: root.refCount > 0
repeat: true
triggeredOnStart: true
onTriggered: root.updateAllStats()
}
readonly property string staticDataScript: `exec 2>/dev/null
set -o pipefail
json_escape() { sed -e 's/\\\\/\\\\\\\\/g' -e 's/"/\\\\"/g' -e ':a;N;$!ba;s/\\n/\\\\n/g'; }
printf "{"
cpu_count=$(nproc)
cpu_model=$(grep -m1 'model name' /proc/cpuinfo | cut -d: -f2- | sed 's/^ *//' | json_escape || echo 'Unknown')
printf '"cpu":{"count":%d,"model":"%s"},' "$cpu_count" "$cpu_model"
dmip="/sys/class/dmi/id"
[ -d "$dmip" ] || dmip="/sys/devices/virtual/dmi/id"
mb_vendor=$([ -r "$dmip/board_vendor" ] && cat "$dmip/board_vendor" | json_escape || echo "Unknown")
mb_name=$([ -r "$dmip/board_name" ] && cat "$dmip/board_name" | json_escape || echo "")
bios_ver=$([ -r "$dmip/bios_version" ] && cat "$dmip/bios_version" | json_escape || echo "Unknown")
bios_date=$([ -r "$dmip/bios_date" ] && cat "$dmip/bios_date" | json_escape || echo "")
kern_ver=$(uname -r | json_escape)
distro=$(grep PRETTY_NAME /etc/os-release 2>/dev/null | cut -d= -f2- | tr -d '"' | json_escape || echo 'Unknown')
host_name=$(hostname | json_escape)
arch_name=$(uname -m)
printf '"system":{"kernel":"%s","distro":"%s","hostname":"%s","arch":"%s","motherboard":"%s %s","bios":"%s %s"},' \\
"$kern_ver" "$distro" "$host_name" "$arch_name" "$mb_vendor" "$mb_name" "$bios_ver" "$bios_date"
printf '"gpus":['
gfirst=1
tmp_gpu=$(mktemp)
# Map driver -> vendor, else infer from lspci line
infer_vendor() {
case "$1" in
nvidia|nouveau) echo NVIDIA ;;
amdgpu|radeon) echo AMD ;;
i915|xe) echo Intel ;;
*) case "$2" in
*NVIDIA*|*Nvidia*|*nvidia*) echo NVIDIA ;;
*AMD*|*ATI*|*amd*|*ati*) echo AMD ;;
*Intel*|*intel*) echo Intel ;;
*) echo Unknown ;;
esac ;;
esac
}
# Priority for sorting (nvidia first, then dGPU AMD, then iGPU AMD/Intel)
prio_of() {
local drv="$1" bdf="$2"
case "$drv" in
nvidia) echo 3 ;;
amdgpu|radeon)
# crude: device number from BDF 0000:BB:DD.F -> DD
local dd="\${bdf##*:}"; dd="\${dd%%.*}"
[ "$dd" = "00" ] && echo 1 || echo 2
;;
i915|xe) echo 0 ;;
*) echo 0 ;;
esac
}
# Enumerate all VGA/3D/2D/Display devices (domain-aware)
LC_ALL=C lspci -nnD 2>/dev/null | grep -iE ' VGA| 3D| 2D| Display' | while IFS= read -r line; do
bdf="\${line%% *}" # 0000:BB:DD.F
short_bdf="\${bdf#0000:}"
# kernel driver in use
drv=""; vendor="Unknown"
if [ -e "/sys/bus/pci/devices/\$bdf/driver" ]; then
drv="$(basename "$(readlink -f "/sys/bus/pci/devices/\$bdf/driver")")"
fi
vendor="$(infer_vendor "\$drv" "\$line")"
# Just pass the raw line, we'll parse it in JavaScript
raw_line="$(printf '%s' "\$line" | json_escape)"
# priority for sorting
prio="$(prio_of "\$drv" "\$bdf")"
printf '%s|%s|%s|%s\\n' "\$prio" "\$drv" "\$vendor" "\$raw_line" >> "\$tmp_gpu"
done
# Output JSON
if [ -s "\$tmp_gpu" ]; then
while IFS='|' read -r pr drv vendor raw_line; do
[ \$gfirst -eq 1 ] || printf ","
printf '{"driver":"%s","vendor":"%s","rawLine":"%s"}' \\
"\$drv" "\$vendor" "\$raw_line"
gfirst=0
done < <(sort -t'|' -k1,1nr -k2,2 "\$tmp_gpu")
fi
rm -f "\$tmp_gpu"
printf ']'
printf "}\\n"`
readonly property string dynamicDataScript: `
sort_key=\${1:-cpu}
max_procs=\${2:-20}
collect_gpu_temps=\${3:-0}
collect_nvidia_only=\${4:-0}
collect_non_nvidia=\${5:-1}
json_escape() { sed -e 's/\\\\/\\\\\\\\/g' -e 's/"/\\\\"/g' -e ':a;N;$!ba;s/\\n/\\\\n/g'; }
printf "{"
mem_line="$(awk '/^MemTotal:/{t=$2}
/^MemFree:/{f=$2}
/^MemAvailable:/{a=$2}
/^Buffers:/{b=$2}
/^Cached:/{c=$2}
/^Shmem:/{s=$2}
/^SwapTotal:/{st=$2}
/^SwapFree:/{sf=$2}
END{printf "%d %d %d %d %d %d %d %d",t,f,a,b,c,s,st,sf}' /proc/meminfo)"
read -r MT MF MA BU CA SH ST SF <<< "$mem_line"
printf '"memory":{"total":%d,"free":%d,"available":%d,"buffers":%d,"cached":%d,"shared":%d,"swaptotal":%d,"swapfree":%d},' \\
"$MT" "$MF" "$MA" "$BU" "$CA" "$SH" "$ST" "$SF"
# Get pss per pid
get_pss_kb() {
# Read PSS with zero external processes.
# 1) Prefer smaps_rollup (fast, single file)
# 2) Fallback to summing PSS in smaps
# Return 0 if unavailable.
local pid="$1" f total v k _
f="/proc/$pid/smaps_rollup"
if [ -r "$f" ]; then
# smaps_rollup has one Pss: line — read it directly
while read -r k v _; do
if [ "$k" = "Pss:" ]; then
printf '%s\n' "\${v:-0}"
return
fi
done < "$f"
printf '0\n'
return
fi
f="/proc/$pid/smaps"
if [ -r "$f" ]; then
total=0
while read -r k v _; do
[ "$k" = "Pss:" ] && total=$(( total + (v:-0) ))
done < "$f"
printf '%s\n' "$total"
return
fi
printf '0\n'
}
cpu_count=$(nproc)
cpu_model=$(grep -m1 'model name' /proc/cpuinfo | cut -d: -f2- | sed 's/^ *//' | json_escape || echo 'Unknown')
cpu_freq=$(awk -F: '/cpu MHz/{gsub(/ /,"",$2);print $2;exit}' /proc/cpuinfo || echo 0)
cpu_temp=0
for hwmon_dir in /sys/class/hwmon/hwmon*/; do
[ -d "$hwmon_dir" ] || continue
name_file="\${hwmon_dir}name"
[ -r "$name_file" ] || continue
# Check if this hwmon is for CPU temperature
if grep -qE 'coretemp|k10temp|k8temp|cpu_thermal|soc_thermal' "$name_file" 2>/dev/null; then
# Look for temperature files without using wildcards in quotes
for temp_file in "\${hwmon_dir}"temp*_input; do
if [ -r "$temp_file" ]; then
cpu_temp=$(awk '{printf "%.1f", $1/1000}' "$temp_file" 2>/dev/null || echo 0)
break 2 # Break both loops
fi
done
fi
done
printf '"cpu":{"count":%d,"model":"%s","frequency":%s,"temperature":%s,' \\
"$cpu_count" "$cpu_model" "$cpu_freq" "$cpu_temp"
printf '"total":'
awk 'NR==1 {
printf "[";
for(i=2; i<=NF; i++) {
if(i>2) printf ",";
printf "%d", $i;
}
printf "]";
exit
}' /proc/stat
printf ',"cores":['
cpu_cores=$(nproc)
awk -v n="$cpu_cores" 'BEGIN{c=0}
/^cpu[0-9]+/ {
if(c>0) printf ",";
printf "[%d,%d,%d,%d,%d,%d,%d,%d]", $2,$3,$4,$5,$6,$7,$8,$9;
c++;
if(c==n) exit
}' /proc/stat
printf ']},'
printf '"network":['
tmp_net=$(mktemp)
grep -E '(wlan|eth|enp|wlp|ens|eno)' /proc/net/dev > "$tmp_net" || true
nfirst=1
while IFS= read -r line; do
[ -z "$line" ] && continue
iface=$(echo "$line" | awk '{print $1}' | sed 's/://')
rx_bytes=$(echo "$line" | awk '{print $2}')
tx_bytes=$(echo "$line" | awk '{print $10}')
[ $nfirst -eq 1 ] || printf ","
printf '{"name":"%s","rx":%d,"tx":%d}' "$iface" "$rx_bytes" "$tx_bytes"
nfirst=0
done < "$tmp_net"
rm -f "$tmp_net"
printf '],'
printf '"disk":['
tmp_disk=$(mktemp)
grep -E ' (sd[a-z]+|nvme[0-9]+n[0-9]+|vd[a-z]+|dm-[0-9]+|mmcblk[0-9]+) ' /proc/diskstats > "$tmp_disk" || true
dfirst=1
while IFS= read -r line; do
[ -z "$line" ] && continue
name=$(echo "$line" | awk '{print $3}')
read_sectors=$(echo "$line" | awk '{print $6}')
write_sectors=$(echo "$line" | awk '{print $10}')
[ $dfirst -eq 1 ] || printf ","
printf '{"name":"%s","read":%d,"write":%d}' "$name" "$read_sectors" "$write_sectors"
dfirst=0
done < "$tmp_disk"
rm -f "$tmp_disk"
printf '],'
printf '"processes":['
case "$sort_key" in
cpu) SORT_OPT="--sort=-pcpu" ;;
memory) SORT_OPT="--sort=-pmem" ;;
name) SORT_OPT="--sort=+comm" ;;
pid) SORT_OPT="--sort=+pid" ;;
*) SORT_OPT="--sort=-pcpu" ;;
esac
tmp_ps=$(mktemp)
ps -eo pid,ppid,pcpu,pmem,rss,comm,cmd --no-headers $SORT_OPT | head -n "$max_procs" > "$tmp_ps" || true
pfirst=1
while IFS=' ' read -r pid ppid cpu pmem_rss rss_kib comm rest; do
[ -z "$pid" ] && continue
# Per-process CPU ticks (utime+stime)
pticks=$(awk '{print $14+$15}' "/proc/$pid/stat" 2>/dev/null || echo 0)
if [ "\${rss_kib:-0}" -eq 0 ]; then pss_kib=0; else pss_kib=$(get_pss_kb "$pid"); fi
case "$pss_kib" in (''|*[!0-9]*) pss_kib=0 ;; esac
pss_pct=$(LC_ALL=C awk -v p="$pss_kib" -v t="$MT" 'BEGIN{if(t>0) printf "%.2f", (100*p)/t; else printf "0.00"}')
cmd=$(printf "%s %s" "$comm" "\${rest:-}" | json_escape)
comm_esc=$(printf "%s" "$comm" | json_escape)
[ $pfirst -eq 1 ] || printf ","
printf '{"pid":%s,"ppid":%s,"cpu":%s,"pticks":%s,"memoryPercent":%s,"memoryKB":%s,"pssKB":%s,"pssPercent":%s,"command":"%s","fullCommand":"%s"}' \
"$pid" "$ppid" "$cpu" "$pticks" "$pss_pct" "$rss_kib" "$pss_kib" "$pss_pct" "$comm_esc" "$cmd"
pfirst=0
done < "$tmp_ps"
rm -f "$tmp_ps"
printf '],'
load_avg=$(cut -d' ' -f1-3 /proc/loadavg)
proc_count=$(ls -Ud /proc/[0-9]* 2>/dev/null | wc -l)
thread_count=$(ls -Ud /proc/[0-9]*/task/[0-9]* 2>/dev/null | wc -l)
boot_time=$(who -b 2>/dev/null | awk '{print $3, $4}' | json_escape || echo 'Unknown')
printf '"system":{"loadavg":"%s","processes":%d,"threads":%d,"boottime":"%s"},' \\
"$load_avg" "$proc_count" "$thread_count" "$boot_time"
printf '"diskmounts":['
tmp_mounts=$(mktemp)
df -h --output=source,target,fstype,size,used,avail,pcent | tail -n +2 | grep -vE '^(tmpfs|devtmpfs)' | head -n 10 > "$tmp_mounts" || true
mfirst=1
while IFS= read -r line; do
[ -z "$line" ] && continue
device=$(echo "$line" | awk '{print $1}' | json_escape)
mount=$(echo "$line" | awk '{print $2}' | json_escape)
fstype=$(echo "$line" | awk '{print $3}')
size=$(echo "$line" | awk '{print $4}')
used=$(echo "$line" | awk '{print $5}')
avail=$(echo "$line" | awk '{print $6}')
percent=$(echo "$line" | awk '{print $7}')
[ $mfirst -eq 1 ] || printf ","
printf '{"device":"%s","mount":"%s","fstype":"%s","size":"%s","used":"%s","avail":"%s","percent":"%s"}' \\
"$device" "$mount" "$fstype" "$size" "$used" "$avail" "$percent"
mfirst=0
done < "$tmp_mounts"
rm -f "$tmp_mounts"
printf ']',
printf '"gputemps":['
if [ "$collect_gpu_temps" = "1" ]; then
gfirst=1
tmp_gpu=$(mktemp)
# Gather GPU temperatures only
for card in /sys/class/drm/card*; do
[ -e "$card/device/driver" ] || continue
drv=$(basename "$(readlink -f "$card/device/driver")")
drv=\${drv##*/}
# Temperature (only scan hwmon for non-NVIDIA GPUs)
hw=""; temp="0"
if [ "$collect_non_nvidia" = "1" ]; then
for h in "$card/device"/hwmon/hwmon*; do
[ -e "$h/temp1_input" ] || continue
hw=$(basename "$h")
temp=$(awk '{printf "%.1f",$1/1000}' "$h/temp1_input" 2>/dev/null || echo "0")
break
done
fi
# NVIDIA temperature fallback
if [ "$drv" = "nvidia" ] && [ "$temp" = "0" ] && [ "$collect_nvidia_only" = "1" ] && command -v nvidia-smi >/dev/null 2>&1; then
t=$(nvidia-smi --query-gpu=temperature.gpu --format=csv,noheader,nounits 2>/dev/null | head -1)
[ -n "$t" ] && { temp="$t"; hw="\${hw:-nvidia}"; }
fi
[ "$temp" != "0" ] && {
[ $gfirst -eq 1 ] || printf ","
printf '{"driver":"%s","hwmon":"%s","temperature":%s}' "$drv" "\${hw:-unknown}" "\${temp:-0}"
gfirst=0
}
done
# Fallback if no DRM cards found but nvidia-smi is available
if [ $gfirst -eq 1 ] && [ \"$collect_nvidia_only\" = \"1\" ]; then
if command -v nvidia-smi >/dev/null 2>&1; then
temp=$(nvidia-smi --query-gpu=temperature.gpu --format=csv,noheader,nounits 2>/dev/null | head -1)
[ -n "$temp" ] && {
printf '{"driver":"nvidia","hwmon":"nvidia","temperature":%s}' "$temp"
gfirst=0
}
fi
fi
rm -f "$tmp_gpu"
fi
printf ']'
printf "}\\n"`
Process {
id: staticDataProcess
command: ["bash", "-c", "bash -s <<'QS_EOF'\n"
+ root.staticDataScript + "\nQS_EOF\n"]
running: false
onExited: exitCode => {
if (exitCode !== 0) {
console.warn("Static data collection failed with exit code:", exitCode)
}
}
stdout: StdioCollector {
onStreamFinished: {
if (text.trim()) {
const fullText = text.trim()
const lastBraceIndex = fullText.lastIndexOf('}')
if (lastBraceIndex === -1) {
console.warn("Invalid static data JSON")
return
}
const jsonText = fullText.substring(0, lastBraceIndex + 1)
try {
const data = JSON.parse(jsonText)
parseStaticData(data)
} catch (e) {
console.warn("Failed to parse static data JSON:", e)
return
}
}
}
}
}
Process {
id: dynamicStatsProcess
command: ["bash", "-c", "bash -s \"$1\" \"$2\" \"$3\" \"$4\" \"$5\" <<'QS_EOF'\n"
+ root.dynamicDataScript + "\nQS_EOF\n", "-", root.sortBy, String(root.maxProcesses), root.hasGpuTempWidgets() ? "1" : "0", root.hasNvidiaGpuTempWidgets() ? "1" : "0", root.hasNonNvidiaGpuTempWidgets() ? "1" : "0"]
running: false
onExited: exitCode => {
if (exitCode !== 0) {
isUpdating = false
}
}
stdout: StdioCollector {
onStreamFinished: {
if (text.trim()) {
const fullText = text.trim()
const lastBraceIndex = fullText.lastIndexOf('}')
if (lastBraceIndex === -1) {
isUpdating = false
return
}
const jsonText = fullText.substring(0, lastBraceIndex + 1)
try {
const data = JSON.parse(jsonText)
parseDynamicStats(data)
} catch (e) {
isUpdating = false
return
}
}
}
}
}
}