mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2025-12-07 14:05:38 -05:00
battery: use native UPower service
This commit is contained in:
@@ -1,206 +1,120 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Io
|
import Quickshell.Services.UPower
|
||||||
pragma Singleton
|
pragma Singleton
|
||||||
pragma ComponentBehavior: Bound
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property bool batteryAvailable: false
|
// Debug mode for testing on desktop systems without batteries
|
||||||
property int batteryLevel: 0
|
property bool debugMode: false // Set to true to enable fake battery for testing
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Battery status checker
|
// Debug fake battery data
|
||||||
Process {
|
property int debugBatteryLevel: 65
|
||||||
id: batteryStatusChecker
|
property string debugBatteryStatus: "Discharging"
|
||||||
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"]
|
property int debugTimeRemaining: 7200 // 2 hours in seconds
|
||||||
running: false
|
property bool debugIsCharging: false
|
||||||
|
property int debugBatteryHealth: 88
|
||||||
stdout: StdioCollector {
|
property string debugBatteryTechnology: "Li-ion"
|
||||||
onStreamFinished: {
|
property int debugBatteryCapacity: 45000 // 45 Wh in mWh
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
property bool batteryAvailable: debugMode || (battery.ready && battery.isLaptopBattery)
|
||||||
id: fallbackBatteryChecker
|
property int batteryLevel: debugMode ? debugBatteryLevel : Math.round(battery.percentage)
|
||||||
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"]
|
property string batteryStatus: debugMode ? debugBatteryStatus : UPowerDeviceState.toString(battery.state)
|
||||||
running: false
|
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 {
|
// Try to get technology from any available laptop battery
|
||||||
onStreamFinished: {
|
for (let i = 0; i < UPower.devices.length; i++) {
|
||||||
if (text.trim() !== "no-battery") {
|
let device = UPower.devices[i]
|
||||||
parseBatteryInfo(text.trim())
|
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 {
|
property var battery: UPower.displayDevice
|
||||||
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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseBatteryInfo(batteryText) {
|
property var availableProfiles: {
|
||||||
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')
|
|
||||||
let profiles = []
|
let profiles = []
|
||||||
|
if (PowerProfiles.profile !== undefined) {
|
||||||
for (let line of lines) {
|
profiles.push("power-saver")
|
||||||
line = line.trim()
|
profiles.push("balanced")
|
||||||
if (line.includes('*')) {
|
if (PowerProfiles.hasPerformanceProfile) {
|
||||||
let profileName = line.replace('*', '').trim()
|
profiles.push("performance")
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return profiles
|
||||||
root.powerProfiles = profiles
|
}
|
||||||
console.log("Power profiles available:", profiles, "Active:", root.activePowerProfile)
|
|
||||||
|
// 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) {
|
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)
|
console.warn("Invalid power profile:", profileName)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Setting power profile to:", profileName)
|
console.log("Setting power profile to:", profileName)
|
||||||
let profileProcess = Qt.createQmlObject(`
|
PowerProfiles.profile = profile
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getBatteryIcon() {
|
function getBatteryIcon() {
|
||||||
@@ -230,8 +144,8 @@ Singleton {
|
|||||||
function formatTimeRemaining() {
|
function formatTimeRemaining() {
|
||||||
if (root.timeRemaining <= 0) return "Unknown"
|
if (root.timeRemaining <= 0) return "Unknown"
|
||||||
|
|
||||||
let hours = Math.floor(root.timeRemaining / 60)
|
let hours = Math.floor(root.timeRemaining / 3600)
|
||||||
let minutes = root.timeRemaining % 60
|
let minutes = Math.floor((root.timeRemaining % 3600) / 60)
|
||||||
|
|
||||||
if (hours > 0) {
|
if (hours > 0) {
|
||||||
return hours + "h " + minutes + "m"
|
return hours + "h " + minutes + "m"
|
||||||
@@ -239,16 +153,4 @@ Singleton {
|
|||||||
return minutes + "m"
|
return minutes + "m"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
interval: 30000
|
|
||||||
running: root.batteryAvailable
|
|
||||||
repeat: true
|
|
||||||
triggeredOnStart: true
|
|
||||||
onTriggered: {
|
|
||||||
batteryStatusChecker.running = true
|
|
||||||
powerProfilesChecker.running = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,8 @@ Rectangle {
|
|||||||
|
|
||||||
property bool batteryPopupVisible: false
|
property bool batteryPopupVisible: false
|
||||||
|
|
||||||
|
signal toggleBatteryPopup()
|
||||||
|
|
||||||
width: 70 // Increased width to accommodate percentage text
|
width: 70 // Increased width to accommodate percentage text
|
||||||
height: 30
|
height: 30
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
@@ -63,7 +65,7 @@ Rectangle {
|
|||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
batteryPopupVisible = !batteryPopupVisible
|
toggleBatteryPopup()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -78,14 +78,14 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
anchors.centerIn: parent
|
anchors.fill: parent
|
||||||
width: parent.width - theme.spacingM * 2
|
anchors.margins: theme.spacingS
|
||||||
spacing: theme.spacingM
|
spacing: theme.spacingS
|
||||||
|
|
||||||
// Show different content based on whether we have active media
|
// Show different content based on whether we have active media
|
||||||
Item {
|
Item {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: 80
|
height: 60
|
||||||
|
|
||||||
// Placeholder when no media
|
// Placeholder when no media
|
||||||
Column {
|
Column {
|
||||||
@@ -117,8 +117,8 @@ Rectangle {
|
|||||||
|
|
||||||
// Album Art
|
// Album Art
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: 80
|
width: 60
|
||||||
height: 80
|
height: 60
|
||||||
radius: theme.cornerRadius
|
radius: theme.cornerRadius
|
||||||
color: Qt.rgba(theme.surfaceVariant.r, theme.surfaceVariant.g, theme.surfaceVariant.b, 0.3)
|
color: Qt.rgba(theme.surfaceVariant.r, theme.surfaceVariant.g, theme.surfaceVariant.b, 0.3)
|
||||||
|
|
||||||
@@ -152,7 +152,7 @@ Rectangle {
|
|||||||
|
|
||||||
// Track Info
|
// Track Info
|
||||||
Column {
|
Column {
|
||||||
width: parent.width - 80 - theme.spacingM
|
width: parent.width - 60 - theme.spacingM
|
||||||
spacing: theme.spacingXS
|
spacing: theme.spacingXS
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
@@ -189,7 +189,7 @@ Rectangle {
|
|||||||
Item {
|
Item {
|
||||||
id: progressBarContainer
|
id: progressBarContainer
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: 32
|
height: 24
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: progressBarBackground
|
id: progressBarBackground
|
||||||
@@ -305,8 +305,9 @@ Rectangle {
|
|||||||
// Control buttons - always visible
|
// Control buttons - always visible
|
||||||
Row {
|
Row {
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
spacing: theme.spacingL
|
spacing: theme.spacingM
|
||||||
visible: activePlayer !== null
|
visible: activePlayer !== null
|
||||||
|
height: 32
|
||||||
|
|
||||||
// Previous button
|
// Previous button
|
||||||
Rectangle {
|
Rectangle {
|
||||||
@@ -344,9 +345,9 @@ Rectangle {
|
|||||||
|
|
||||||
// Play/Pause button
|
// Play/Pause button
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: 36
|
width: 32
|
||||||
height: 36
|
height: 32
|
||||||
radius: 18
|
radius: 16
|
||||||
color: theme.primary
|
color: theme.primary
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
|||||||
@@ -315,6 +315,10 @@ PanelWindow {
|
|||||||
// Battery Widget
|
// Battery Widget
|
||||||
BatteryWidget {
|
BatteryWidget {
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
batteryPopupVisible: topBar.shellRoot.batteryPopupVisible
|
||||||
|
onToggleBatteryPopup: {
|
||||||
|
topBar.shellRoot.batteryPopupVisible = !topBar.shellRoot.batteryPopupVisible
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ControlCenterButton {
|
ControlCenterButton {
|
||||||
|
|||||||
Reference in New Issue
Block a user