diff --git a/.gitignore b/.gitignore index b8675e3..af2e9a5 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ Colors.qml modules/wlogout/scripts/helper.sh config/path/ConfigPath.qml config/path/WallpaperPath.qml -modules/notification/assets/notification.ogg \ No newline at end of file +modules/notification/assets/notification.ogg +*.json \ No newline at end of file diff --git a/modules/bar/states/Battery/BatteryState.qml b/modules/bar/states/Battery/BatteryState.qml index 9ac8a93..52c4ebf 100644 --- a/modules/bar/states/Battery/BatteryState.qml +++ b/modules/bar/states/Battery/BatteryState.qml @@ -1,33 +1,96 @@ pragma Singleton import QtQuick import Quickshell +import Quickshell.Io import Quickshell.Services.UPower +import "../../../../config" Item { id: root - property bool available: UPower.displayDevice.isPresent + // show widget for laptop battery *or* UPS (your main PC), hide otherwise + // displayDevice is aggregate - for UPS it is type Ups, isLaptopBattery is false + property bool available: { + const d = UPower.displayDevice + if (d && d.ready && d.isPresent && (d.isLaptopBattery || d.type === UPowerDeviceType.Ups)) return true + // fallback: scan all devices (some systems don't set DisplayDevice to UPS) + const devs = UPower.devices.values + for (let i = 0; i < devs.length; i++) { + const dev = devs[i] + if (dev.isPresent && (dev.isLaptopBattery || dev.type === UPowerDeviceType.Ups)) return true + } + return false + } property real percentage: UPower.displayDevice.percentage*100 - property string state: UPowerDeviceState.toString(UPower.displayDevice.state) - property bool charging: state == "Charging" - property bool full: state == "FullyCharged" + // 'state' shadows Item.state (qmllint) -> use chargeState + property string chargeState: UPowerDeviceState.toString(UPower.displayDevice.state) + // keep alias via getter would still shadow, so expose via function if needed; prefer chargeState + property bool charging: chargeState == "Charging" + property bool full: chargeState == "FullyCharged" property real energy: UPower.displayDevice.energy property real energy_formated: Math.round(energy*10)/10 property real time_to_empty: UPower.displayDevice.timeToEmpty + property real time_to_full: UPower.displayDevice.timeToFull property real hour_to_empty: Math.floor(time_to_empty/3600) property real min_to_empty: Math.floor((time_to_empty%3600)/60) + property real hour_to_full: Math.floor(time_to_full/3600) + property real min_to_full: Math.floor((time_to_full%3600)/60) property real energy_rate: UPower.displayDevice.changeRate property real energy_rate_formated: Math.round(energy_rate*10)/10 property bool healthSupported: UPower.displayDevice.healthSupported property real health: healthSupported ? UPower.displayDevice.healthPercentage : NaN - property int _maxHistory: 60*60 + property int _maxHistory: 60*60 // 1h window property var percentageHistory: [{time: Date.now() - _maxHistory*1000, value: 0}, {time: Date.now(), value: 0}] - property var percentageHistoryExists: true + property var percentageHistoryExists: false property var energyHistory: [{time: Date.now() - _maxHistory*1000, value: 0}, {time: Date.now(), value: 0}] - property var energyHistoryExists: true + property var energyHistoryExists: false property var usageHistory: [{time: Date.now() - _maxHistory*1000, value: 0}, {time: Date.now(), value: 0}] - property var usageHistoryExists: true + property var usageHistoryExists: false + + // ---- persistence: survives reload AND close (FileView -> disk) ---- + // use Config.configDir (base dir) so path is stable and visible in repo + readonly property string _historyPath: Config.configDir + "/modules/bar/states/Battery/battery_history.json" + FileView { + id: historyFile + path: _historyPath + printErrors: true + // adapter holds json, synced to disk via writeAdapter() + adapter: JsonAdapter { + property var percentageHistory: [] + property var energyHistory: [] + property var usageHistory: [] + } + onLoaded: { + function prune(arr) { + if (!arr || typeof arr.length !== "number" || arr.length===0) return null + const f = arr.filter(p => p && typeof p.time==="number" && typeof p.value==="number") + if (f.length < 2) return null + return f.length > root._maxHistory ? f.slice(-root._maxHistory) : f + } + const p = prune(adapter.percentageHistory) + if (p) { root.percentageHistory = p; root.percentageHistoryExists = p.some(i=>i.value!==0) } + const e = prune(adapter.energyHistory) + if (e) { root.energyHistory = e; root.energyHistoryExists = e.some(i=>i.value!==0) } + const u = prune(adapter.usageHistory) + if (u) { root.usageHistory = u; root.usageHistoryExists = u.some(i=>i.value!==0) } + } + onLoadFailed: {} // first run, file missing -> keep defaults + } + + // periodic save every 10s (debounce-restart would never fire while statsTimer pushes every 1s) + Timer { + id: saveTimer + interval: 10000 + repeat: true + running: true + onTriggered: { + historyFile.adapter.percentageHistory = root.percentageHistory + historyFile.adapter.energyHistory = root.energyHistory + historyFile.adapter.usageHistory = root.usageHistory + historyFile.writeAdapter() + } + } Timer { id: statsTimer @@ -35,28 +98,38 @@ Item { repeat: true running: true onTriggered: { - var now = Date.now() + const now = Date.now() // Creating percentage History - var pHist = root.percentageHistory + let pHist = root.percentageHistory.slice() pHist.push({time: now, value: percentage}) if (pHist.length > root._maxHistory) pHist.shift() - percentageHistoryExists = pHist.some(item => item.value !== 0 && item.value !== undefined) + root.percentageHistoryExists = pHist.some(item => item.value !== 0 && item.value !== undefined) root.percentageHistory = pHist - // Creating percentage History - var eHist = root.energyHistory + // Creating energy History + let eHist = root.energyHistory.slice() eHist.push({time: now, value: energy_formated}) if (eHist.length > root._maxHistory) eHist.shift() - energyHistoryExists = eHist.some(item => item.value !== 0 && item.value !== undefined) + root.energyHistoryExists = eHist.some(item => item.value !== 0 && item.value !== undefined) root.energyHistory = eHist - // Creating percentage History - var uHist = root.usageHistory + // Creating usage History + let uHist = root.usageHistory.slice() uHist.push({time: now, value: energy_rate_formated}) if (uHist.length > root._maxHistory) uHist.shift() - usageHistoryExists = uHist.some(item => item.value !== 0 && item.value !== undefined) + root.usageHistoryExists = uHist.some(item => item.value !== 0 && item.value !== undefined) root.usageHistory = uHist } } -} + + // ensure save on clean close / reload (best-effort, reload preserves via FileView anyway) + Component.onDestruction: { + if (historyFile.loaded) { + historyFile.adapter.percentageHistory = root.percentageHistory + historyFile.adapter.energyHistory = root.energyHistory + historyFile.adapter.usageHistory = root.usageHistory + historyFile.writeAdapter() + } + } +} \ No newline at end of file diff --git a/modules/bar/states/Network/NetworkState.qml b/modules/bar/states/Network/NetworkState.qml index d9a072c..d6f8f36 100644 --- a/modules/bar/states/Network/NetworkState.qml +++ b/modules/bar/states/Network/NetworkState.qml @@ -3,6 +3,7 @@ import QtQuick import Quickshell import Quickshell.Io import Quickshell.Networking +import "../../../../config" QtObject { id: root @@ -38,6 +39,55 @@ QtObject { readonly property var networking: Networking + // ---- persistence: survives reload AND close (disk) ---- + // use Config.configDir (base dir) so path is stable and visible in repo + readonly property string _historyPath: Config.configDir + "/modules/bar/states/Network/network_history.json" + // QtObject has no default property, so hold FileView as typed property + property FileView historyFile: FileView { + path: _historyPath + printErrors: true + adapter: JsonAdapter { + property var downloadHistory: [] + property var uploadHistory: [] + property var pingHistory: [] + } + onLoaded: { + function prune(arr) { + if (!arr || typeof arr.length !== "number" || arr.length===0) return null + const f = arr.filter(p => p && typeof p.time==="number" && typeof p.value==="number") + if (f.length < 2) return null + return f.length > root._maxHistory ? f.slice(-root._maxHistory) : f + } + const d = prune(adapter.downloadHistory) + if (d) root.downloadHistory = d + const u = prune(adapter.uploadHistory) + if (u) root.uploadHistory = u + const p = prune(adapter.pingHistory) + if (p) root.pingHistory = p + } + onLoadFailed: {} // first run + } + // periodic save every 10s (restart-debounce would never fire while statsTimer pushes every 1s) + property Timer saveTimer: Timer { + interval: 10000 + repeat: true + running: true + onTriggered: { + historyFile.adapter.downloadHistory = root.downloadHistory + historyFile.adapter.uploadHistory = root.uploadHistory + historyFile.adapter.pingHistory = root.pingHistory + historyFile.writeAdapter() + } + } + Component.onDestruction: { + if (historyFile.loaded) { + historyFile.adapter.downloadHistory = root.downloadHistory + historyFile.adapter.uploadHistory = root.uploadHistory + historyFile.adapter.pingHistory = root.pingHistory + historyFile.writeAdapter() + } + } + function findAllConnectedDevices() { var devs = networking.devices.values var result = [] @@ -70,7 +120,8 @@ QtObject { var nets = dev.networks.values for (var j = 0; j < nets.length; j++) { if (nets[j].connected) { - sig += ":" + nets[j].name + // include signalStrength & name so hover updates live on signal change + sig += ":" + nets[j].name + ":" + Math.round(nets[j].signalStrength*100) break } } @@ -354,4 +405,4 @@ QtObject { Component.onCompleted: { root.updateAll() } -} +} \ No newline at end of file diff --git a/modules/bar/widgets/Battery.qml b/modules/bar/widgets/Battery.qml index 9df2b68..aecbb32 100644 --- a/modules/bar/widgets/Battery.qml +++ b/modules/bar/widgets/Battery.qml @@ -1,6 +1,5 @@ import QtQuick import Quickshell -import Quickshell.Hyprland import "../../../config" import "../states/Battery" @@ -13,7 +12,7 @@ MouseArea { visible: BatteryState.available property bool expanded: false - + Rectangle { anchors.fill: parent color: batteryButton.containsMouse ? Colors.tertiary : Qt.alpha(Colors.tertiaryContainer, 0.25) @@ -24,7 +23,7 @@ MouseArea { Text { id: batteryText - visible : !expanded + visible : !batteryButton.expanded anchors.centerIn: parent text:( BatteryState.percentage >= 90 ? "" : @@ -37,7 +36,7 @@ MouseArea { font.family: Config.font color: ( BatteryState.charging ? ( - BatteryState.full ? Colors.secondary : + BatteryState.full ? Colors.secondary : Colors.tertiary ) : BatteryState.percentage <= 5 ? Colors.errorContainer : @@ -49,7 +48,7 @@ MouseArea { Text { id: chargeIcon - visible : !expanded && BatteryState.charging + visible : !batteryButton.expanded && BatteryState.charging anchors.centerIn: parent text: "󱐋" font.pixelSize: Config.iconSize(batteryButton.height) @@ -61,7 +60,7 @@ MouseArea { Row { id: expandedRow - visible: expanded + visible: batteryButton.expanded anchors.centerIn: parent spacing: Config.bar_spacing_fun(batteryButton.height) @@ -89,7 +88,7 @@ MouseArea { color: ( BatteryState.percentage <= 5 ? Colors.errorContainer : BatteryState.charging ? ( - BatteryState.full ? Colors.secondary : + BatteryState.full ? Colors.secondary : Colors.tertiary ) : BatteryState.percentage < 10 ? "yellow" : @@ -100,7 +99,7 @@ MouseArea { Text { id: batteryPercentageText - text: BatteryState.percentage + "%" + text: Math.round(BatteryState.percentage) + "%" font.pixelSize: Config.labelSize(batteryButton.height) font.family: Config.font @@ -125,9 +124,9 @@ MouseArea { radiusValue: Config.radius_fun(batteryButton.height) Column { - spacing: parent.spacing + spacing: batteryPopup.spacingValue Text { - text: "State: " + BatteryState.state + text: "State: " + BatteryState.chargeState wrapMode: Text.WordWrap color: Colors.primary anchors.horizontalCenter: parent.horizontalCenter @@ -140,7 +139,7 @@ MouseArea { visible: BatteryState.percentageHistoryExists Text { - text: "percentage: " + BatteryState.percentage + "%" + text: "percentage: " + Math.round(BatteryState.percentage) + "%" color: Colors.primary anchors.horizontalCenter: parent.horizontalCenter font.pixelSize: Config.font_size_tiny(bar.height) @@ -160,7 +159,7 @@ MouseArea { visible: BatteryState.energyHistoryExists Text { - text: "energy: " + Math.round(BatteryState.energy*10)/10 + " W" + text: "energy: " + BatteryState.energy_formated + " Wh" color: Colors.primary anchors.horizontalCenter: parent.horizontalCenter font.pixelSize: Config.font_size_tiny(bar.height) @@ -195,10 +194,17 @@ MouseArea { } } Text { - text: ( - BatteryState.hour_to_empty > 0 ? "Remaining: " + BatteryState.hour_to_empty + "h " + BatteryState.min_to_empty + "min" : - BatteryState.min_to_empty > 0 ? "Remaining: " + BatteryState.min_to_empty + "min" : - "NaN") + text: { + if (BatteryState.charging) { + if (BatteryState.hour_to_full > 0) return "Remaining: " + BatteryState.hour_to_full + "h " + BatteryState.min_to_full + "min (to full)" + if (BatteryState.min_to_full > 0) return "Remaining: " + BatteryState.min_to_full + "min (to full)" + return BatteryState.full ? "Fully charged" : "Calculating..." + } else { + if (BatteryState.hour_to_empty > 0) return "Remaining: " + BatteryState.hour_to_empty + "h " + BatteryState.min_to_empty + "min" + if (BatteryState.min_to_empty > 0) return "Remaining: " + BatteryState.min_to_empty + "min" + return "Calculating..." + } + } wrapMode: Text.WordWrap color: Colors.primary anchors.horizontalCenter: parent.horizontalCenter @@ -206,7 +212,7 @@ MouseArea { font.family: Config.font } Text { - text: "Health: " + BatteryState.health + " %" + text: "Health: " + Math.round(BatteryState.health) + " %" visible: BatteryState.healthSupported wrapMode: Text.WordWrap color: Colors.primary