Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell
This commit is contained in:
commit
835f88d71e
17 changed files with 890 additions and 283 deletions
49
Services/BatteryService.qml
Normal file
49
Services/BatteryService.qml
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Services.UPower
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
// Choose icon based on charge and charging state
|
||||
function getIcon(percent, charging, isReady) {
|
||||
if (!isReady) {
|
||||
return "battery_error"
|
||||
}
|
||||
|
||||
if (charging) {
|
||||
if (percent >= 95)
|
||||
return "battery_full"
|
||||
if (percent >= 85)
|
||||
return "battery_charging_90"
|
||||
if (percent >= 65)
|
||||
return "battery_charging_80"
|
||||
if (percent >= 55)
|
||||
return "battery_charging_60"
|
||||
if (percent >= 45)
|
||||
return "battery_charging_50"
|
||||
if (percent >= 25)
|
||||
return "battery_charging_30"
|
||||
if (percent >= 0)
|
||||
return "battery_charging_20"
|
||||
} else {
|
||||
if (percent >= 95)
|
||||
return "battery_full"
|
||||
if (percent >= 85)
|
||||
return "battery_6_bar"
|
||||
if (percent >= 70)
|
||||
return "battery_5_bar"
|
||||
if (percent >= 55)
|
||||
return "battery_4_bar"
|
||||
if (percent >= 40)
|
||||
return "battery_3_bar"
|
||||
if (percent >= 25)
|
||||
return "battery_2_bar"
|
||||
if (percent >= 10)
|
||||
return "battery_1_bar"
|
||||
if (percent >= 0)
|
||||
return "battery_0_bar"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -110,9 +110,43 @@ Singleton {
|
|||
property real lastBrightness: 0
|
||||
property real queuedBrightness: NaN
|
||||
|
||||
// For internal displays - store the backlight device path
|
||||
property string backlightDevice: ""
|
||||
property string brightnessPath: ""
|
||||
property string maxBrightnessPath: ""
|
||||
property int maxBrightness: 100
|
||||
property bool ignoreNextChange: false
|
||||
|
||||
// Signal for brightness changes
|
||||
signal brightnessUpdated(real newBrightness)
|
||||
|
||||
// FileView to watch for external brightness changes (internal displays only)
|
||||
readonly property FileView brightnessWatcher: FileView {
|
||||
id: brightnessWatcher
|
||||
// Only set path for internal displays with a valid brightness path
|
||||
path: (!monitor.isDdc && !monitor.isAppleDisplay && monitor.brightnessPath !== "") ? monitor.brightnessPath : ""
|
||||
watchChanges: path !== ""
|
||||
onFileChanged: {
|
||||
reload()
|
||||
if (monitor.ignoreNextChange) {
|
||||
monitor.ignoreNextChange = false
|
||||
return
|
||||
}
|
||||
if (text() === "")
|
||||
return
|
||||
var current = parseInt(text().trim())
|
||||
if (!isNaN(current) && monitor.maxBrightness > 0) {
|
||||
var newBrightness = current / monitor.maxBrightness
|
||||
// Only update if it's actually different (avoid feedback loops)
|
||||
if (Math.abs(newBrightness - monitor.brightness) > 0.01) {
|
||||
monitor.brightness = newBrightness
|
||||
monitor.brightnessUpdated(monitor.brightness)
|
||||
//Logger.log("Brightness", "External change detected:", monitor.modelData.name, monitor.brightness)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize brightness
|
||||
readonly property Process initProc: Process {
|
||||
stdout: StdioCollector {
|
||||
|
|
@ -121,8 +155,8 @@ Singleton {
|
|||
if (dataText === "") {
|
||||
return
|
||||
}
|
||||
Logger.log("Brightness", "Raw brightness data for", monitor.modelData.name + ":", dataText)
|
||||
|
||||
//Logger.log("Brightness", "Raw brightness data for", monitor.modelData.name + ":", dataText)
|
||||
if (monitor.isAppleDisplay) {
|
||||
var val = parseInt(dataText)
|
||||
if (!isNaN(val)) {
|
||||
|
|
@ -140,14 +174,20 @@ Singleton {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
// Internal backlight
|
||||
var parts = dataText.split(" ")
|
||||
if (parts.length >= 2) {
|
||||
var current = parseInt(parts[0])
|
||||
var max = parseInt(parts[1])
|
||||
// Internal backlight - parse the response which includes device path
|
||||
var lines = dataText.split("\n")
|
||||
if (lines.length >= 3) {
|
||||
monitor.backlightDevice = lines[0]
|
||||
monitor.brightnessPath = monitor.backlightDevice + "/brightness"
|
||||
monitor.maxBrightnessPath = monitor.backlightDevice + "/max_brightness"
|
||||
|
||||
var current = parseInt(lines[1])
|
||||
var max = parseInt(lines[2])
|
||||
if (!isNaN(current) && !isNaN(max) && max > 0) {
|
||||
monitor.maxBrightness = max
|
||||
monitor.brightness = current / max
|
||||
Logger.log("Brightness", "Internal brightness:", current + "/" + max + " =", monitor.brightness)
|
||||
Logger.log("Brightness", "Using backlight device:", monitor.backlightDevice)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -171,7 +211,7 @@ Singleton {
|
|||
|
||||
function increaseBrightness(): void {
|
||||
var stepSize = Settings.data.brightness.brightnessStep / 100.0
|
||||
setBrightnessDebounced(brightness + stepSize)
|
||||
setBrightnessDebounced(monitor.brightness + stepSize)
|
||||
}
|
||||
|
||||
function decreaseBrightness(): void {
|
||||
|
|
@ -183,22 +223,23 @@ Singleton {
|
|||
value = Math.max(0, Math.min(1, value))
|
||||
var rounded = Math.round(value * 100)
|
||||
|
||||
if (Math.round(brightness * 100) === rounded)
|
||||
if (Math.round(monitor.brightness * 100) === rounded)
|
||||
return
|
||||
|
||||
if (isDdc && timer.running) {
|
||||
queuedBrightness = value
|
||||
monitor.queuedBrightness = value
|
||||
return
|
||||
}
|
||||
|
||||
brightness = value
|
||||
brightnessUpdated(brightness)
|
||||
monitor.brightness = value
|
||||
brightnessUpdated(monitor.brightness)
|
||||
|
||||
if (isAppleDisplay) {
|
||||
Quickshell.execDetached(["asdbctl", "set", rounded])
|
||||
} else if (isDdc) {
|
||||
Quickshell.execDetached(["ddcutil", "-b", busNum, "setvcp", "10", rounded])
|
||||
} else {
|
||||
monitor.ignoreNextChange = true
|
||||
Quickshell.execDetached(["brightnessctl", "s", rounded + "%"])
|
||||
}
|
||||
|
||||
|
|
@ -208,7 +249,7 @@ Singleton {
|
|||
}
|
||||
|
||||
function setBrightnessDebounced(value: real): void {
|
||||
queuedBrightness = value
|
||||
monitor.queuedBrightness = value
|
||||
timer.restart()
|
||||
}
|
||||
|
||||
|
|
@ -218,8 +259,11 @@ Singleton {
|
|||
} else if (isDdc) {
|
||||
initProc.command = ["ddcutil", "-b", busNum, "getvcp", "10", "--brief"]
|
||||
} else {
|
||||
// Internal backlight - try to find the first available backlight device
|
||||
initProc.command = ["sh", "-c", "for dev in /sys/class/backlight/*; do if [ -f \"$dev/brightness\" ] && [ -f \"$dev/max_brightness\" ]; then echo \"$(cat $dev/brightness) $(cat $dev/max_brightness)\"; break; fi; done"]
|
||||
// Internal backlight - find the first available backlight device and get its info
|
||||
// This now returns: device_path, current_brightness, max_brightness (on separate lines)
|
||||
initProc.command = ["sh", "-c", "for dev in /sys/class/backlight/*; do "
|
||||
+ " if [ -f \"$dev/brightness\" ] && [ -f \"$dev/max_brightness\" ]; then " + " echo \"$dev\"; "
|
||||
+ " cat \"$dev/brightness\"; " + " cat \"$dev/max_brightness\"; " + " break; " + " fi; " + "done"]
|
||||
}
|
||||
initProc.running = true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,9 @@ Singleton {
|
|||
property bool connecting: false
|
||||
property string connectingTo: ""
|
||||
property string lastError: ""
|
||||
property bool ethernet: false
|
||||
property bool ethernetConnected: false
|
||||
property string disconnectingFrom: ""
|
||||
property string forgettingNetwork: ""
|
||||
|
||||
// Persistent cache
|
||||
property string cacheFile: Settings.cacheDir + "network.json"
|
||||
|
|
@ -38,10 +40,21 @@ Singleton {
|
|||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Settings.data.network
|
||||
function onWifiEnabledChanged() {
|
||||
if (Settings.data.network.wifiEnabled) {
|
||||
ToastService.showNotice("Wi-Fi", "Enabled")
|
||||
} else {
|
||||
ToastService.showNotice("Wi-Fi", "Disabled")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
Logger.log("Network", "Service initialized")
|
||||
syncWifiState()
|
||||
refresh()
|
||||
scan()
|
||||
}
|
||||
|
||||
// Save cache with debounce
|
||||
|
|
@ -55,22 +68,23 @@ Singleton {
|
|||
saveDebounce.restart()
|
||||
}
|
||||
|
||||
// Single refresh timer for periodic scans
|
||||
Timer {
|
||||
id: refreshTimer
|
||||
interval: 30000
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: refresh()
|
||||
}
|
||||
|
||||
// Delayed scan timer for WiFi enable
|
||||
// Delayed scan timer
|
||||
Timer {
|
||||
id: delayedScanTimer
|
||||
interval: 7000
|
||||
onTriggered: scan()
|
||||
}
|
||||
|
||||
// Ethernet check timer
|
||||
// Always running every 30s
|
||||
Timer {
|
||||
id: ethernetCheckTimer
|
||||
interval: 30000
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: ethernetStateProcess.running = true
|
||||
}
|
||||
|
||||
// Core functions
|
||||
function syncWifiState() {
|
||||
wifiStateProcess.running = true
|
||||
|
|
@ -83,14 +97,6 @@ Singleton {
|
|||
wifiToggleProcess.running = true
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
ethernetStateProcess.running = true
|
||||
|
||||
if (Settings.data.network.wifiEnabled) {
|
||||
scan()
|
||||
}
|
||||
}
|
||||
|
||||
function scan() {
|
||||
if (scanning)
|
||||
return
|
||||
|
|
@ -98,6 +104,7 @@ Singleton {
|
|||
scanning = true
|
||||
lastError = ""
|
||||
scanProcess.running = true
|
||||
Logger.log("Network", "Wi-Fi scan in progress...")
|
||||
}
|
||||
|
||||
function connect(ssid, password = "") {
|
||||
|
|
@ -123,11 +130,14 @@ Singleton {
|
|||
}
|
||||
|
||||
function disconnect(ssid) {
|
||||
disconnectingFrom = ssid
|
||||
disconnectProcess.ssid = ssid
|
||||
disconnectProcess.running = true
|
||||
}
|
||||
|
||||
function forget(ssid) {
|
||||
forgettingNetwork = ssid
|
||||
|
||||
// Remove from cache
|
||||
let known = cacheAdapter.knownNetworks
|
||||
delete known[ssid]
|
||||
|
|
@ -144,6 +154,40 @@ Singleton {
|
|||
forgetProcess.running = true
|
||||
}
|
||||
|
||||
// Helper function to immediately update network status
|
||||
function updateNetworkStatus(ssid, connected) {
|
||||
let nets = networks
|
||||
|
||||
// Update all networks connected status
|
||||
for (let key in nets) {
|
||||
if (nets[key].connected && key !== ssid) {
|
||||
nets[key].connected = false
|
||||
}
|
||||
}
|
||||
|
||||
// Update the target network if it exists
|
||||
if (nets[ssid]) {
|
||||
nets[ssid].connected = connected
|
||||
nets[ssid].existing = true
|
||||
nets[ssid].cached = true
|
||||
} else if (connected) {
|
||||
// Create a temporary entry if network doesn't exist yet
|
||||
nets[ssid] = {
|
||||
"ssid": ssid,
|
||||
"security": "--",
|
||||
"signal": 100,
|
||||
"connected"// Default to good signal until real scan
|
||||
: true,
|
||||
"existing": true,
|
||||
"cached": true
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger property change notification
|
||||
networks = ({})
|
||||
networks = nets
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
function signalIcon(signal) {
|
||||
if (signal >= 80)
|
||||
|
|
@ -164,17 +208,19 @@ Singleton {
|
|||
// Processes
|
||||
Process {
|
||||
id: ethernetStateProcess
|
||||
running: false
|
||||
running: true
|
||||
command: ["nmcli", "-t", "-f", "DEVICE,TYPE,STATE", "device"]
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
|
||||
root.ethernet = text.split("\n").some(line => {
|
||||
const parts = line.split(":")
|
||||
return parts[1] === "ethernet" && parts[2] === "connected"
|
||||
})
|
||||
Logger.log("Network", "Ethernet connected:", root.ethernet)
|
||||
const connected = text.split("\n").some(line => {
|
||||
const parts = line.split(":")
|
||||
return parts[1] === "ethernet" && parts[2] === "connected"
|
||||
})
|
||||
if (root.ethernetConnected !== connected) {
|
||||
root.ethernetConnected = connected
|
||||
Logger.log("Network", "Ethernet connected:", root.ethernetConnected)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -187,7 +233,7 @@ Singleton {
|
|||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const enabled = text.trim() === "enabled"
|
||||
Logger.log("Network", "Wifi enabled:", enabled)
|
||||
Logger.log("Network", "Wi-Fi enabled:", enabled)
|
||||
if (Settings.data.network.wifiEnabled !== enabled) {
|
||||
Settings.data.network.wifiEnabled = enabled
|
||||
}
|
||||
|
|
@ -227,11 +273,11 @@ Singleton {
|
|||
id: scanProcess
|
||||
running: false
|
||||
command: ["sh", "-c", `
|
||||
# Get existing profiles
|
||||
profiles=$(nmcli -t -f NAME,TYPE connection show | grep ':802-11-wireless' | cut -d: -f1)
|
||||
# Get list of saved connection profiles (just the names)
|
||||
profiles=$(nmcli -t -f NAME connection show | tr '\n' '|')
|
||||
|
||||
# Get WiFi networks
|
||||
nmcli -t -f SSID,SECURITY,SIGNAL,IN-USE device wifi list | while read line; do
|
||||
nmcli -t -f SSID,SECURITY,SIGNAL,IN-USE device wifi list --rescan yes | while read line; do
|
||||
ssid=$(echo "$line" | cut -d: -f1)
|
||||
security=$(echo "$line" | cut -d: -f2)
|
||||
signal=$(echo "$line" | cut -d: -f3)
|
||||
|
|
@ -242,8 +288,10 @@ Singleton {
|
|||
continue
|
||||
fi
|
||||
|
||||
# Check if SSID matches any profile name (simple check)
|
||||
# This covers most cases where profile name equals or contains the SSID
|
||||
existing=false
|
||||
if echo "$profiles" | grep -q "^$ssid$"; then
|
||||
if echo "$profiles" | grep -qF "$ssid|"; then
|
||||
existing=true
|
||||
fi
|
||||
|
||||
|
|
@ -286,9 +334,25 @@ Singleton {
|
|||
}
|
||||
}
|
||||
|
||||
// For logging purpose only
|
||||
Logger.log("Network", "Wi-Fi scan completed")
|
||||
const oldSSIDs = Object.keys(root.networks)
|
||||
const newSSIDs = Object.keys(nets)
|
||||
const newNetworks = newSSIDs.filter(ssid => !oldSSIDs.includes(ssid))
|
||||
const lostNetworks = oldSSIDs.filter(ssid => !newSSIDs.includes(ssid))
|
||||
if (newNetworks.length > 0 || lostNetworks.length > 0) {
|
||||
if (newNetworks.length > 0) {
|
||||
Logger.log("Network", "New Wi-Fi SSID discovered:", newNetworks.join(", "))
|
||||
}
|
||||
if (lostNetworks.length > 0) {
|
||||
Logger.log("Network", "Wi-Fi SSID disappeared:", lostNetworks.join(", "))
|
||||
}
|
||||
Logger.log("Network", "Total Wi-Fi SSIDs:", Object.keys(nets).length)
|
||||
}
|
||||
|
||||
// Assign the results
|
||||
root.networks = nets
|
||||
root.scanning = false
|
||||
Logger.log("Network", "Discovered", Object.keys(root.networks).length, "Wi-Fi networks")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -338,12 +402,15 @@ Singleton {
|
|||
cacheAdapter.lastConnected = connectProcess.ssid
|
||||
saveCache()
|
||||
|
||||
// Immediately update the UI before scanning
|
||||
root.updateNetworkStatus(connectProcess.ssid, true)
|
||||
|
||||
root.connecting = false
|
||||
root.connectingTo = ""
|
||||
Logger.log("Network", "Connected to " + connectProcess.ssid)
|
||||
Logger.log("Network", `Connected to network: "${connectProcess.ssid}"`)
|
||||
|
||||
// Rescan to update status
|
||||
delayedScanTimer.interval = 1000
|
||||
// Still do a scan to get accurate signal and security info
|
||||
delayedScanTimer.interval = 5000
|
||||
delayedScanTimer.restart()
|
||||
}
|
||||
}
|
||||
|
|
@ -378,8 +445,27 @@ Singleton {
|
|||
running: false
|
||||
command: ["nmcli", "connection", "down", "id", ssid]
|
||||
|
||||
onRunningChanged: {
|
||||
if (!running) {
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
Logger.log("Network", `Disconnected from network: "${disconnectProcess.ssid}"`)
|
||||
|
||||
// Immediately update UI on successful disconnect
|
||||
root.updateNetworkStatus(disconnectProcess.ssid, false)
|
||||
root.disconnectingFrom = ""
|
||||
|
||||
// Do a scan to refresh the list
|
||||
delayedScanTimer.interval = 1000
|
||||
delayedScanTimer.restart()
|
||||
}
|
||||
}
|
||||
|
||||
stderr: StdioCollector {
|
||||
onStreamFinished: {
|
||||
root.disconnectingFrom = ""
|
||||
if (text.trim()) {
|
||||
Logger.warn("Network", "Disconnect error: " + text)
|
||||
}
|
||||
// Still trigger a scan even on error
|
||||
delayedScanTimer.interval = 1000
|
||||
delayedScanTimer.restart()
|
||||
}
|
||||
|
|
@ -390,10 +476,67 @@ Singleton {
|
|||
id: forgetProcess
|
||||
property string ssid: ""
|
||||
running: false
|
||||
command: ["nmcli", "connection", "delete", "id", ssid]
|
||||
|
||||
onRunningChanged: {
|
||||
if (!running) {
|
||||
// Try multiple common profile name patterns
|
||||
command: ["sh", "-c", `
|
||||
ssid="$1"
|
||||
deleted=false
|
||||
|
||||
# Try exact SSID match first
|
||||
if nmcli connection delete id "$ssid" 2>/dev/null; then
|
||||
echo "Deleted profile: $ssid"
|
||||
deleted=true
|
||||
fi
|
||||
|
||||
# Try "Auto <SSID>" pattern
|
||||
if nmcli connection delete id "Auto $ssid" 2>/dev/null; then
|
||||
echo "Deleted profile: Auto $ssid"
|
||||
deleted=true
|
||||
fi
|
||||
|
||||
# Try "<SSID> 1", "<SSID> 2", etc. patterns
|
||||
for i in 1 2 3; do
|
||||
if nmcli connection delete id "$ssid $i" 2>/dev/null; then
|
||||
echo "Deleted profile: $ssid $i"
|
||||
deleted=true
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$deleted" = "false" ]; then
|
||||
echo "No profiles found for SSID: $ssid"
|
||||
fi
|
||||
`, "--", ssid]
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
Logger.log("Network", `Forget network: "${forgetProcess.ssid}"`)
|
||||
Logger.log("Network", text.trim().replace(/[\r\n]/g, " "))
|
||||
|
||||
// Update both cached and existing status immediately
|
||||
let nets = root.networks
|
||||
if (nets[forgetProcess.ssid]) {
|
||||
nets[forgetProcess.ssid].cached = false
|
||||
nets[forgetProcess.ssid].existing = false
|
||||
// Trigger property change
|
||||
root.networks = ({})
|
||||
root.networks = nets
|
||||
}
|
||||
|
||||
root.forgettingNetwork = ""
|
||||
|
||||
// Scan to verify the profile is gone
|
||||
delayedScanTimer.interval = 5000
|
||||
delayedScanTimer.restart()
|
||||
}
|
||||
}
|
||||
|
||||
stderr: StdioCollector {
|
||||
onStreamFinished: {
|
||||
root.forgettingNetwork = ""
|
||||
if (text.trim() && !text.includes("No profiles found")) {
|
||||
Logger.warn("Network", "Forget error: " + text)
|
||||
}
|
||||
// Still Trigger a scan even on error
|
||||
delayedScanTimer.interval = 1000
|
||||
delayedScanTimer.restart()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import QtQuick
|
|||
import Qt.labs.folderlistmodel
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
|
@ -11,12 +12,313 @@ Singleton {
|
|||
// Public values
|
||||
property real cpuUsage: 0
|
||||
property real cpuTemp: 0
|
||||
property real memoryUsageGb: 0
|
||||
property real memoryUsagePer: 0
|
||||
property real diskUsage: 0
|
||||
property real memGb: 0
|
||||
property real memPercent: 0
|
||||
property real diskPercent: 0
|
||||
property real rxSpeed: 0
|
||||
property real txSpeed: 0
|
||||
|
||||
// Configuration
|
||||
property int sleepDuration: 3000
|
||||
|
||||
// Internal state for CPU calculation
|
||||
property var prevCpuStats: null
|
||||
|
||||
// Internal state for network speed calculation
|
||||
// Previous Bytes need to be stored as 'real' as they represent the total of bytes transfered
|
||||
// since the computer started, so their value will easily overlfow a 32bit int.
|
||||
property real prevRxBytes: 0
|
||||
property real prevTxBytes: 0
|
||||
property real prevTime: 0
|
||||
|
||||
// Cpu temperature is the most complex
|
||||
readonly property var supportedTempCpuSensorNames: ["coretemp", "k10temp", "zenpower"]
|
||||
property string cpuTempSensorName: ""
|
||||
property string cpuTempHwmonPath: ""
|
||||
// For Intel coretemp averaging of all cores/sensors
|
||||
property var intelTempValues: []
|
||||
property int intelTempFilesChecked: 0
|
||||
property int intelTempMaxFiles: 20 // Will test up to temp20_input
|
||||
|
||||
// --------------------------------------------
|
||||
Component.onCompleted: {
|
||||
Logger.log("SystemStat", "Service started with interval:", root.sleepDuration, "ms")
|
||||
|
||||
// Kickoff the cpu name detection for temperature
|
||||
cpuTempNameReader.checkNext()
|
||||
}
|
||||
|
||||
// --------------------------------------------
|
||||
// Timer for periodic updates
|
||||
Timer {
|
||||
id: updateTimer
|
||||
interval: root.sleepDuration
|
||||
repeat: true
|
||||
running: true
|
||||
triggeredOnStart: true
|
||||
onTriggered: {
|
||||
// Trigger all direct system files reads
|
||||
memInfoFile.reload()
|
||||
cpuStatFile.reload()
|
||||
netDevFile.reload()
|
||||
|
||||
// Run df (disk free) one time
|
||||
dfProcess.running = true
|
||||
|
||||
updateCpuTemperature()
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------
|
||||
// FileView components for reading system files
|
||||
FileView {
|
||||
id: memInfoFile
|
||||
path: "/proc/meminfo"
|
||||
onLoaded: parseMemoryInfo(text())
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: cpuStatFile
|
||||
path: "/proc/stat"
|
||||
onLoaded: calculateCpuUsage(text())
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: netDevFile
|
||||
path: "/proc/net/dev"
|
||||
onLoaded: calculateNetworkSpeed(text())
|
||||
}
|
||||
|
||||
// --------------------------------------------
|
||||
// Process to fetch disk usage in percent
|
||||
// Uses 'df' aka 'disk free'
|
||||
Process {
|
||||
id: dfProcess
|
||||
command: ["df", "--output=pcent", "/"]
|
||||
running: false
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const lines = text.trim().split('\n')
|
||||
if (lines.length >= 2) {
|
||||
const percent = lines[1].replace(/[^0-9]/g, '')
|
||||
root.diskPercent = parseInt(percent) || 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------
|
||||
// CPU Temperature
|
||||
// It's more complex.
|
||||
// ----
|
||||
// #1 - Find a common cpu sensor name ie: "coretemp", "k10temp", "zenpower"
|
||||
FileView {
|
||||
id: cpuTempNameReader
|
||||
property int currentIndex: 0
|
||||
|
||||
function checkNext() {
|
||||
if (currentIndex >= 10) {
|
||||
// Check up to hwmon10
|
||||
Logger.warn("No supported temperature sensor found")
|
||||
return
|
||||
}
|
||||
|
||||
//Logger.log("SystemStat", "---- Probing: hwmon", currentIndex)
|
||||
cpuTempNameReader.path = `/sys/class/hwmon/hwmon${currentIndex}/name`
|
||||
cpuTempNameReader.reload()
|
||||
}
|
||||
|
||||
onLoaded: {
|
||||
const name = text().trim()
|
||||
if (root.supportedTempCpuSensorNames.includes(name)) {
|
||||
root.cpuTempSensorName = name
|
||||
root.cpuTempHwmonPath = `/sys/class/hwmon/hwmon${currentIndex}`
|
||||
Logger.log("SystemStat", `Found ${root.cpuTempSensorName} CPU thermal sensor at ${root.cpuTempHwmonPath}`)
|
||||
} else {
|
||||
currentIndex++
|
||||
Qt.callLater(() => {
|
||||
// Qt.callLater is mandatory
|
||||
checkNext()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onLoadFailed: function (error) {
|
||||
currentIndex++
|
||||
Qt.callLater(() => {
|
||||
// Qt.callLater is mandatory
|
||||
checkNext()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ----
|
||||
// #2 - Read sensor value
|
||||
FileView {
|
||||
id: cpuTempReader
|
||||
printErrors: false
|
||||
|
||||
onLoaded: {
|
||||
const data = text().trim()
|
||||
if (root.cpuTempSensorName === "coretemp") {
|
||||
// For Intel, collect all temperature values
|
||||
const temp = parseInt(data) / 1000.0
|
||||
//console.log(temp, cpuTempReader.path)
|
||||
root.intelTempValues.push(temp)
|
||||
Qt.callLater(() => {
|
||||
// Qt.callLater is mandatory
|
||||
checkNextIntelTemp()
|
||||
})
|
||||
} else {
|
||||
// For AMD sensors (k10temp and zenpower), directly set the temperature
|
||||
root.cpuTemp = Math.round(parseInt(data) / 1000.0)
|
||||
}
|
||||
}
|
||||
onLoadFailed: function (error) {
|
||||
Qt.callLater(() => {
|
||||
// Qt.callLater is mandatory
|
||||
checkNextIntelTemp()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------
|
||||
// Parse memory info from /proc/meminfo
|
||||
function parseMemoryInfo(text) {
|
||||
if (!text)
|
||||
return
|
||||
|
||||
const lines = text.split('\n')
|
||||
let memTotal = 0
|
||||
let memAvailable = 0
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('MemTotal:')) {
|
||||
memTotal = parseInt(line.split(/\s+/)[1]) || 0
|
||||
} else if (line.startsWith('MemAvailable:')) {
|
||||
memAvailable = parseInt(line.split(/\s+/)[1]) || 0
|
||||
}
|
||||
}
|
||||
|
||||
if (memTotal > 0) {
|
||||
const usageKb = memTotal - memAvailable
|
||||
root.memGb = (usageKb / 1000000).toFixed(1)
|
||||
root.memPercent = Math.round((usageKb / memTotal) * 100)
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------
|
||||
// Calculate CPU usage from /proc/stat
|
||||
function calculateCpuUsage(text) {
|
||||
if (!text)
|
||||
return
|
||||
|
||||
const lines = text.split('\n')
|
||||
const cpuLine = lines[0]
|
||||
|
||||
// First line is total CPU
|
||||
if (!cpuLine.startsWith('cpu '))
|
||||
return
|
||||
|
||||
const parts = cpuLine.split(/\s+/)
|
||||
const stats = {
|
||||
"user": parseInt(parts[1]) || 0,
|
||||
"nice": parseInt(parts[2]) || 0,
|
||||
"system": parseInt(parts[3]) || 0,
|
||||
"idle": parseInt(parts[4]) || 0,
|
||||
"iowait": parseInt(parts[5]) || 0,
|
||||
"irq": parseInt(parts[6]) || 0,
|
||||
"softirq": parseInt(parts[7]) || 0,
|
||||
"steal": parseInt(parts[8]) || 0,
|
||||
"guest": parseInt(parts[9]) || 0,
|
||||
"guestNice": parseInt(parts[10]) || 0
|
||||
}
|
||||
const totalIdle = stats.idle + stats.iowait
|
||||
const total = Object.values(stats).reduce((sum, val) => sum + val, 0)
|
||||
|
||||
if (root.prevCpuStats) {
|
||||
const prevTotalIdle = root.prevCpuStats.idle + root.prevCpuStats.iowait
|
||||
const prevTotal = Object.values(root.prevCpuStats).reduce((sum, val) => sum + val, 0)
|
||||
|
||||
const diffTotal = total - prevTotal
|
||||
const diffIdle = totalIdle - prevTotalIdle
|
||||
|
||||
if (diffTotal > 0) {
|
||||
root.cpuUsage = (((diffTotal - diffIdle) / diffTotal) * 100).toFixed(1)
|
||||
}
|
||||
}
|
||||
|
||||
root.prevCpuStats = stats
|
||||
}
|
||||
|
||||
// -------------------------------------------------------
|
||||
// Calculate RX and TX speed from /proc/net/dev
|
||||
// Average speed of all interfaces excepted 'lo'
|
||||
function calculateNetworkSpeed(text) {
|
||||
if (!text) {
|
||||
return
|
||||
}
|
||||
|
||||
const currentTime = Date.now() / 1000
|
||||
const lines = text.split('\n')
|
||||
|
||||
let totalRx = 0
|
||||
let totalTx = 0
|
||||
|
||||
for (var i = 2; i < lines.length; i++) {
|
||||
const line = lines[i].trim()
|
||||
if (!line) {
|
||||
continue
|
||||
}
|
||||
|
||||
const colonIndex = line.indexOf(':')
|
||||
if (colonIndex === -1) {
|
||||
continue
|
||||
}
|
||||
|
||||
const iface = line.substring(0, colonIndex).trim()
|
||||
if (iface === 'lo') {
|
||||
continue
|
||||
}
|
||||
|
||||
const statsLine = line.substring(colonIndex + 1).trim()
|
||||
const stats = statsLine.split(/\s+/)
|
||||
|
||||
const rxBytes = parseInt(stats[0], 10) || 0
|
||||
const txBytes = parseInt(stats[8], 10) || 0
|
||||
|
||||
totalRx += rxBytes
|
||||
totalTx += txBytes
|
||||
}
|
||||
|
||||
// Compute only if we have a previous run to compare to.
|
||||
if (root.prevTime > 0) {
|
||||
const timeDiff = currentTime - root.prevTime
|
||||
|
||||
// Avoid division by zero if time hasn't passed.
|
||||
if (timeDiff > 0) {
|
||||
let rxDiff = totalRx - root.prevRxBytes
|
||||
let txDiff = totalTx - root.prevTxBytes
|
||||
|
||||
// Handle counter resets (e.g., WiFi reconnect), which would cause a negative value.
|
||||
if (rxDiff < 0) {
|
||||
rxDiff = 0
|
||||
}
|
||||
if (txDiff < 0) {
|
||||
txDiff = 0
|
||||
}
|
||||
|
||||
root.rxSpeed = Math.round(rxDiff / timeDiff) // Speed in Bytes/s
|
||||
root.txSpeed = Math.round(txDiff / timeDiff)
|
||||
}
|
||||
}
|
||||
|
||||
root.prevRxBytes = totalRx
|
||||
root.prevTxBytes = totalTx
|
||||
root.prevTime = currentTime
|
||||
}
|
||||
|
||||
// -------------------------------------------------------
|
||||
// Helper function to format network speeds
|
||||
function formatSpeed(bytesPerSecond) {
|
||||
if (bytesPerSecond < 1024) {
|
||||
|
|
@ -30,27 +332,44 @@ Singleton {
|
|||
}
|
||||
}
|
||||
|
||||
// Background process emitting one JSON line per sample
|
||||
Process {
|
||||
id: reader
|
||||
running: true
|
||||
command: ["sh", "-c", Quickshell.shellDir + "/Bin/system-stats.sh"]
|
||||
stdout: SplitParser {
|
||||
onRead: function (line) {
|
||||
try {
|
||||
const data = JSON.parse(line)
|
||||
root.cpuUsage = data.cpu
|
||||
root.cpuTemp = data.cputemp
|
||||
root.memoryUsageGb = data.memgb
|
||||
root.memoryUsagePer = data.memper
|
||||
root.diskUsage = data.diskper
|
||||
root.rxSpeed = parseFloat(data.rx_speed) || 0
|
||||
root.txSpeed = parseFloat(data.tx_speed) || 0
|
||||
} catch (e) {
|
||||
|
||||
// ignore malformed lines
|
||||
}
|
||||
}
|
||||
// -------------------------------------------------------
|
||||
// Function to start fetching and computing the cpu temperature
|
||||
function updateCpuTemperature() {
|
||||
// For AMD sensors (k10temp and zenpower), only use Tctl sensor
|
||||
// temp1_input corresponds to Tctl (Temperature Control) on these sensors
|
||||
if (root.cpuTempSensorName === "k10temp" || root.cpuTempSensorName === "zenpower") {
|
||||
cpuTempReader.path = `${root.cpuTempHwmonPath}/temp1_input`
|
||||
cpuTempReader.reload()
|
||||
} // For Intel coretemp, start averaging all available sensors/cores
|
||||
else if (root.cpuTempSensorName === "coretemp") {
|
||||
root.intelTempValues = []
|
||||
root.intelTempFilesChecked = 0
|
||||
checkNextIntelTemp()
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------
|
||||
// Function to check next Intel temperature sensor
|
||||
function checkNextIntelTemp() {
|
||||
if (root.intelTempFilesChecked >= root.intelTempMaxFiles) {
|
||||
// Calculate average of all found temperatures
|
||||
if (root.intelTempValues.length > 0) {
|
||||
let sum = 0
|
||||
for (var i = 0; i < root.intelTempValues.length; i++) {
|
||||
sum += root.intelTempValues[i]
|
||||
}
|
||||
root.cpuTemp = Math.round(sum / root.intelTempValues.length)
|
||||
//Logger.log("SystemStat", `Averaged ${root.intelTempValues.length} CPU thermal sensors: ${root.cpuTemp}°C`)
|
||||
} else {
|
||||
Logger.warn("SystemStat", "No temperature sensors found for coretemp")
|
||||
root.cpuTemp = 0
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Check next temperature file
|
||||
root.intelTempFilesChecked++
|
||||
cpuTempReader.path = `${root.cpuTempHwmonPath}/temp${root.intelTempFilesChecked}_input`
|
||||
cpuTempReader.reload()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue