Files
omarchy/shell/plugins/services/battery/Service.qml
T
David Heinemeier HanssonandClaude Fable 5.1 55799c4ab0 Poll the active power profile over D-Bus instead of powerprofilesctl
The battery service ran `powerprofilesctl get` every two seconds to keep the
active profile visible to the wallpaper and lock services. That command is a
PyGObject script, so the shell spawned a Python interpreter for it tens of
thousands of times a day. Roughly once a day one of those exits into a CPython
3.14 finalization race (python/cpython#124619): the GLib D-Bus worker thread
calls PyGILState_Ensure after the interpreter is torn down and the process
dies with SIGSEGV, leaving a core dump and a crash notification behind.

Read the ActiveProfile property straight from power-profiles-daemon with
busctl, the same way omarchy-powerprofiles-set already reads UPower. The
output is JSON, so an empty or malformed reply when the daemon is not running
still reads as no active profile, matching the previous behaviour.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011gbfh4Mi9dK6SAd1P2xMTi
2026-09-13 08:56:39 +02:00

127 lines
3.7 KiB
QML

import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Services.UPower
import "BatteryModel.js" as BatteryModel
Item {
id: root
property var shell: null
property string omarchyPath: Quickshell.env("OMARCHY_PATH")
readonly property int batteryThreshold: 10
property string pendingPowerSource: ""
property string activePowerProfile: ""
readonly property bool powerSaverOnBattery: UPower.onBattery && activePowerProfile === "power-saver"
PersistentProperties {
id: persisted
reloadableId: "omarchy-battery"
property bool notifiedLowBattery: false
}
function batteryPercentage() {
return BatteryModel.batteryPercentage(UPower.displayDevice)
}
function isDischarging() {
return BatteryModel.isDischarging(UPower.displayDevice, UPower.onBattery, UPowerDeviceState.Discharging)
}
function checkBattery() {
var state = BatteryModel.shouldWarnLowBattery(UPower.displayDevice, UPower.onBattery, UPowerDeviceState.Discharging, batteryThreshold, persisted.notifiedLowBattery)
persisted.notifiedLowBattery = state.notifiedLowBattery
if (state.notify) sendLowBatteryWarning(state.level)
}
function sendLowBatteryWarning(level) {
if (warningProcess.running) return
warningProcess.command = [
"omarchy-battery-low",
String(level)
]
warningProcess.running = true
}
function applyPowerProfile() {
pendingPowerSource = UPower.onBattery ? "battery" : "ac"
if (!powerProfileProcess.running) runPendingPowerProfile()
}
function runPendingPowerProfile() {
powerProfileProcess.command = ["omarchy-powerprofiles-set", pendingPowerSource]
pendingPowerSource = ""
powerProfileProcess.running = true
}
function refreshPowerProfile() {
if (!powerProfileReadProcess.running) powerProfileReadProcess.running = true
}
function parseActiveProfile(text) {
// busctl --json=short prints {"type":"s","data":"balanced"}; an empty or
// malformed reply (daemon not running) reads as no active profile.
try {
return String(JSON.parse(text).data || "").trim()
} catch (e) {
return ""
}
}
Process { id: warningProcess }
Process {
id: powerProfileProcess
onExited: {
if (root.pendingPowerSource !== "") root.runPendingPowerProfile()
root.refreshPowerProfile()
}
}
Process {
id: powerProfileReadProcess
// Read the property straight from the daemon rather than via
// `powerprofilesctl get`. That is a PyGObject script, and spawning a Python
// interpreter for it every two seconds trips a CPython 3.14 shutdown race
// (python/cpython#124619): the GLib D-Bus worker thread re-enters the
// interpreter after finalization and the process dies with SIGSEGV,
// leaving a core dump and a crash notification behind roughly daily.
command: ["busctl", "--json=short", "get-property", "net.hadess.PowerProfiles", "/net/hadess/PowerProfiles", "net.hadess.PowerProfiles", "ActiveProfile"]
stdout: StdioCollector {
waitForEnd: true
onStreamFinished: root.activePowerProfile = root.parseActiveProfile(text)
}
}
Timer {
// There is no portable way to subscribe to profile changes from QML; keep
// them visible to consumers such as the wallpaper service without requiring
// the power panel to be open.
interval: 2000
running: true
repeat: true
triggeredOnStart: true
onTriggered: root.refreshPowerProfile()
}
Timer {
interval: 30000
running: true
repeat: true
triggeredOnStart: true
onTriggered: root.checkBattery()
}
Connections {
target: UPower
function onOnBatteryChanged() {
root.checkBattery()
root.applyPowerProfile()
root.refreshPowerProfile()
}
}
Component.onCompleted: root.refreshPowerProfile()
}