From 90999e8ac1fcf675f68d9191f68463536afc70db Mon Sep 17 00:00:00 2001 From: bbedward Date: Tue, 15 Jul 2025 15:57:12 -0400 Subject: [PATCH] battery: use native UPower service --- Services/BatteryService.qml | 280 ++++++------------ Widgets/BatteryWidget.qml | 4 +- .../CenterCommandCenter/MediaPlayerWidget.qml | 25 +- Widgets/TopBar/TopBar.qml | 4 + 4 files changed, 111 insertions(+), 202 deletions(-) diff --git a/Services/BatteryService.qml b/Services/BatteryService.qml index 25a84c76..fca00943 100644 --- a/Services/BatteryService.qml +++ b/Services/BatteryService.qml @@ -1,206 +1,120 @@ import QtQuick import Quickshell -import Quickshell.Io +import Quickshell.Services.UPower pragma Singleton pragma ComponentBehavior: Bound Singleton { id: root - property bool batteryAvailable: false - property int batteryLevel: 0 - property string batteryStatus: "Unknown" - property int timeRemaining: 0 - property bool isCharging: false - property bool isLowBattery: false - property int batteryHealth: 100 - property string batteryTechnology: "Unknown" - property int cycleCount: 0 - property int batteryCapacity: 0 - property var powerProfiles: [] - property string activePowerProfile: "" - Process { - id: batteryAvailabilityChecker - command: ["bash", "-c", "ls /sys/class/power_supply/ | grep -E '^BAT' | head -1"] - running: true - - stdout: SplitParser { - splitMarker: "\n" - onRead: (data) => { - if (data.trim()) { - root.batteryAvailable = true - console.log("Battery found:", data.trim()) - batteryStatusChecker.running = true - } else { - root.batteryAvailable = false - console.log("No battery found - this appears to be a desktop system") - } - } - } - } + // Debug mode for testing on desktop systems without batteries + property bool debugMode: false // Set to true to enable fake battery for testing - // Battery status checker - Process { - id: batteryStatusChecker - command: ["bash", "-c", "if [ -d /sys/class/power_supply/BAT0 ] || [ -d /sys/class/power_supply/BAT1 ]; then upower -i $(upower -e | grep 'BAT') | grep -E 'state|percentage|time to|energy|technology|cycle-count' || acpi -b 2>/dev/null || echo 'fallback'; else echo 'no-battery'; fi"] - running: false - - stdout: StdioCollector { - onStreamFinished: { - if (text.trim() === "no-battery") { - root.batteryAvailable = false - return - } - - if (text.trim() && text.trim() !== "fallback") { - parseBatteryInfo(text.trim()) - } else { - fallbackBatteryChecker.running = true - } - } - } - - onExited: (exitCode) => { - if (exitCode !== 0) { - console.warn("Battery status check failed, trying fallback methods") - fallbackBatteryChecker.running = true - } - } - } + // Debug fake battery data + property int debugBatteryLevel: 65 + property string debugBatteryStatus: "Discharging" + property int debugTimeRemaining: 7200 // 2 hours in seconds + property bool debugIsCharging: false + property int debugBatteryHealth: 88 + property string debugBatteryTechnology: "Li-ion" + property int debugBatteryCapacity: 45000 // 45 Wh in mWh - Process { - id: fallbackBatteryChecker - command: ["bash", "-c", "if [ -f /sys/class/power_supply/BAT0/capacity ]; then BAT=BAT0; elif [ -f /sys/class/power_supply/BAT1/capacity ]; then BAT=BAT1; else echo 'no-battery'; exit 1; fi; echo \"percentage: $(cat /sys/class/power_supply/$BAT/capacity)%\"; echo \"state: $(cat /sys/class/power_supply/$BAT/status 2>/dev/null || echo Unknown)\"; if [ -f /sys/class/power_supply/$BAT/technology ]; then echo \"technology: $(cat /sys/class/power_supply/$BAT/technology)\"; fi; if [ -f /sys/class/power_supply/$BAT/cycle_count ]; then echo \"cycle-count: $(cat /sys/class/power_supply/$BAT/cycle_count)\"; fi"] - running: false + property bool batteryAvailable: debugMode || (battery.ready && battery.isLaptopBattery) + property int batteryLevel: debugMode ? debugBatteryLevel : Math.round(battery.percentage) + property string batteryStatus: debugMode ? debugBatteryStatus : UPowerDeviceState.toString(battery.state) + property int timeRemaining: debugMode ? debugTimeRemaining : (battery.timeToEmpty || battery.timeToFull) + property bool isCharging: debugMode ? debugIsCharging : (battery.state === UPowerDeviceState.Charging) + property bool isLowBattery: debugMode ? (debugBatteryLevel <= 20) : (battery.percentage <= 20) + property int batteryHealth: debugMode ? debugBatteryHealth : (battery.healthSupported ? Math.round(battery.healthPercentage) : 100) + property string batteryTechnology: { + if (debugMode) return debugBatteryTechnology - stdout: StdioCollector { - onStreamFinished: { - if (text.trim() !== "no-battery") { - parseBatteryInfo(text.trim()) + // Try to get technology from any available laptop battery + for (let i = 0; i < UPower.devices.length; i++) { + let device = UPower.devices[i] + if (device.isLaptopBattery && device.ready) { + // UPower doesn't expose technology directly, but we can get it from the model + let model = device.model || "" + if (model.toLowerCase().includes("li-ion") || model.toLowerCase().includes("lithium")) { + return "Li-ion" + } else if (model.toLowerCase().includes("li-po") || model.toLowerCase().includes("polymer")) { + return "Li-polymer" + } else if (model.toLowerCase().includes("nimh")) { + return "NiMH" } } } + return "Unknown" } + property int cycleCount: 0 // UPower doesn't expose cycle count + property int batteryCapacity: debugMode ? debugBatteryCapacity : Math.round(battery.energyCapacity * 1000) + property var powerProfiles: availableProfiles + property string activePowerProfile: PowerProfile.toString(PowerProfiles.profile) - Process { - id: powerProfilesChecker - command: ["bash", "-c", "if command -v powerprofilesctl > /dev/null; then powerprofilesctl list 2>/dev/null; else echo 'not-available'; fi"] - running: false - - stdout: StdioCollector { - onStreamFinished: { - if (text.trim() !== "not-available") { - parsePowerProfiles(text.trim()) - } - } - } - } + property var battery: UPower.displayDevice - function parseBatteryInfo(batteryText) { - let lines = batteryText.split('\n') - - for (let line of lines) { - line = line.trim().toLowerCase() - - if (line.includes('percentage:') || line.includes('capacity:')) { - let match = line.match(/(\d+)%?/) - if (match) { - root.batteryLevel = parseInt(match[1]) - root.isLowBattery = root.batteryLevel <= 20 - } - } else if (line.includes('state:') || line.includes('status:')) { - let statusPart = line.split(':')[1]?.trim().toLowerCase() || line - console.log("Raw battery status line:", line, "extracted status:", statusPart) - - if (statusPart === 'charging') { - root.batteryStatus = "Charging" - root.isCharging = true - console.log("Battery is charging") - } else if (statusPart === 'discharging') { - root.batteryStatus = "Discharging" - root.isCharging = false - console.log("Battery is discharging") - } else if (statusPart === 'full') { - root.batteryStatus = "Full" - root.isCharging = false - console.log("Battery is full") - } else if (statusPart === 'not charging') { - root.batteryStatus = "Not charging" - root.isCharging = false - console.log("Battery is not charging") - } else { - root.batteryStatus = statusPart.charAt(0).toUpperCase() + statusPart.slice(1) || "Unknown" - root.isCharging = false - console.log("Battery status unknown:", statusPart) - } - } else if (line.includes('time to')) { - let match = line.match(/(\d+):(\d+)/) - if (match) { - root.timeRemaining = parseInt(match[1]) * 60 + parseInt(match[2]) - } - } else if (line.includes('technology:')) { - let tech = line.split(':')[1]?.trim() || "Unknown" - root.batteryTechnology = tech.charAt(0).toUpperCase() + tech.slice(1) - } else if (line.includes('cycle-count:')) { - let match = line.match(/(\d+)/) - if (match) { - root.cycleCount = parseInt(match[1]) - } - } else if (line.includes('energy-full:') || line.includes('capacity:')) { - let match = line.match(/([\d.]+)\s*wh/i) - if (match) { - root.batteryCapacity = Math.round(parseFloat(match[1]) * 1000) // Convert to mWh - } - } - } - - console.log("Battery status updated:", root.batteryLevel + "%", root.batteryStatus) - } - - function parsePowerProfiles(profileText) { - let lines = profileText.split('\n') + property var availableProfiles: { let profiles = [] - - for (let line of lines) { - line = line.trim() - if (line.includes('*')) { - let profileName = line.replace('*', '').trim() - if (profileName.includes(':')) { - profileName = profileName.split(':')[0].trim() - } - root.activePowerProfile = profileName - profiles.push(profileName) - } else if (line && !line.includes(':') && line.length > 0) { - profiles.push(line) + if (PowerProfiles.profile !== undefined) { + profiles.push("power-saver") + profiles.push("balanced") + if (PowerProfiles.hasPerformanceProfile) { + profiles.push("performance") } } - - root.powerProfiles = profiles - console.log("Power profiles available:", profiles, "Active:", root.activePowerProfile) + return profiles + } + + // Timer to simulate battery changes in debug mode + Timer { + id: debugTimer + interval: 5000 // Update every 5 seconds + running: debugMode + repeat: true + onTriggered: { + // Simulate battery discharge/charge + if (debugIsCharging) { + debugBatteryLevel = Math.min(100, debugBatteryLevel + 1) + if (debugBatteryLevel >= 100) { + debugBatteryStatus = "Full" + debugIsCharging = false + } + } else { + debugBatteryLevel = Math.max(0, debugBatteryLevel - 1) + if (debugBatteryLevel <= 15) { + debugBatteryStatus = "Charging" + debugIsCharging = true + } + } + + // Update time remaining + debugTimeRemaining = debugIsCharging ? + Math.max(0, debugTimeRemaining - 300) : // 5 minutes less to full + Math.max(0, debugTimeRemaining - 300) // 5 minutes less remaining + } } function setBatteryProfile(profileName) { - if (!root.powerProfiles.includes(profileName)) { + let profile = PowerProfile.Balanced + + if (profileName === "power-saver") { + profile = PowerProfile.PowerSaver + } else if (profileName === "balanced") { + profile = PowerProfile.Balanced + } else if (profileName === "performance") { + if (PowerProfiles.hasPerformanceProfile) { + profile = PowerProfile.Performance + } else { + console.warn("Performance profile not available") + return + } + } else { console.warn("Invalid power profile:", profileName) return } console.log("Setting power profile to:", profileName) - let profileProcess = Qt.createQmlObject(` - import Quickshell.Io - Process { - command: ["powerprofilesctl", "set", "${profileName}"] - running: true - onExited: (exitCode) => { - if (exitCode === 0) { - console.log("Power profile changed to:", "${profileName}") - root.activePowerProfile = "${profileName}" - } else { - console.warn("Failed to change power profile") - } - } - } - `, root) + PowerProfiles.profile = profile } function getBatteryIcon() { @@ -230,8 +144,8 @@ Singleton { function formatTimeRemaining() { if (root.timeRemaining <= 0) return "Unknown" - let hours = Math.floor(root.timeRemaining / 60) - let minutes = root.timeRemaining % 60 + let hours = Math.floor(root.timeRemaining / 3600) + let minutes = Math.floor((root.timeRemaining % 3600) / 60) if (hours > 0) { return hours + "h " + minutes + "m" @@ -239,16 +153,4 @@ Singleton { return minutes + "m" } } - - - Timer { - interval: 30000 - running: root.batteryAvailable - repeat: true - triggeredOnStart: true - onTriggered: { - batteryStatusChecker.running = true - powerProfilesChecker.running = true - } - } } \ No newline at end of file diff --git a/Widgets/BatteryWidget.qml b/Widgets/BatteryWidget.qml index 0335dabd..3a3d7043 100644 --- a/Widgets/BatteryWidget.qml +++ b/Widgets/BatteryWidget.qml @@ -7,6 +7,8 @@ Rectangle { property bool batteryPopupVisible: false + signal toggleBatteryPopup() + width: 70 // Increased width to accommodate percentage text height: 30 radius: Theme.cornerRadius @@ -63,7 +65,7 @@ Rectangle { cursorShape: Qt.PointingHandCursor onClicked: { - batteryPopupVisible = !batteryPopupVisible + toggleBatteryPopup() } } diff --git a/Widgets/CenterCommandCenter/MediaPlayerWidget.qml b/Widgets/CenterCommandCenter/MediaPlayerWidget.qml index e390eed7..04dbf42d 100644 --- a/Widgets/CenterCommandCenter/MediaPlayerWidget.qml +++ b/Widgets/CenterCommandCenter/MediaPlayerWidget.qml @@ -78,14 +78,14 @@ Rectangle { } Column { - anchors.centerIn: parent - width: parent.width - theme.spacingM * 2 - spacing: theme.spacingM + anchors.fill: parent + anchors.margins: theme.spacingS + spacing: theme.spacingS // Show different content based on whether we have active media Item { width: parent.width - height: 80 + height: 60 // Placeholder when no media Column { @@ -117,8 +117,8 @@ Rectangle { // Album Art Rectangle { - width: 80 - height: 80 + width: 60 + height: 60 radius: theme.cornerRadius color: Qt.rgba(theme.surfaceVariant.r, theme.surfaceVariant.g, theme.surfaceVariant.b, 0.3) @@ -152,7 +152,7 @@ Rectangle { // Track Info Column { - width: parent.width - 80 - theme.spacingM + width: parent.width - 60 - theme.spacingM spacing: theme.spacingXS anchors.verticalCenter: parent.verticalCenter @@ -189,7 +189,7 @@ Rectangle { Item { id: progressBarContainer width: parent.width - height: 32 + height: 24 Rectangle { id: progressBarBackground @@ -305,8 +305,9 @@ Rectangle { // Control buttons - always visible Row { anchors.horizontalCenter: parent.horizontalCenter - spacing: theme.spacingL + spacing: theme.spacingM visible: activePlayer !== null + height: 32 // Previous button Rectangle { @@ -344,9 +345,9 @@ Rectangle { // Play/Pause button Rectangle { - width: 36 - height: 36 - radius: 18 + width: 32 + height: 32 + radius: 16 color: theme.primary Text { diff --git a/Widgets/TopBar/TopBar.qml b/Widgets/TopBar/TopBar.qml index 39cb7133..9130177e 100644 --- a/Widgets/TopBar/TopBar.qml +++ b/Widgets/TopBar/TopBar.qml @@ -315,6 +315,10 @@ PanelWindow { // Battery Widget BatteryWidget { anchors.verticalCenter: parent.verticalCenter + batteryPopupVisible: topBar.shellRoot.batteryPopupVisible + onToggleBatteryPopup: { + topBar.shellRoot.batteryPopupVisible = !topBar.shellRoot.batteryPopupVisible + } } ControlCenterButton {