Krolyxon
Commit 8fda16f

Branches

Refs →

aurora.configquickshellauroraBattery.qml

199 lines · 8.6 KB · QML

1pragma Singleton
2import QtQuick
3import Quickshell
4import Quickshell.Io
5import Quickshell.Services.UPower
6
7// Battery care: charge limit and low-battery warnings.
8// The Legion firmware only has a fixed cap ("conservation mode", sysfs charge_types Long_Life). UPower
9// switches it without sudo (EnableChargeThreshold, polkit allow_active) but Quickshell's UPower doesn't
10// expose that, hence busctl. Settings.chargeMode:
11// off normal charging
12// conservation firmware cap on all the time (stops at ~80%, kept by the EC, works without aurora)
13// (anything else that isn't "off", like the old "limit80", counts as conservation)
14Singleton {
15 id: root
16
17 property string path: "" // the battery's UPower object, found with `upower -e`
18 property bool supported: false // ChargeThresholdSupported
19 property bool capOn: false // ChargeThresholdEnabled (conservation mode right now)
20 readonly property bool ready: path !== ""
21 // UPower reports 0% for a moment at startup: no decisions before this
22 property bool started: false
23 Timer { interval: 4000; running: true; onTriggered: { root.started = true; root.evaluate(); root.checkLow(); root.applyAuto() } }
24
25 readonly property var bat: UPower.displayDevice
26 readonly property bool hasBat: bat !== null && bat.isPresent
27 readonly property int pct: hasBat ? Math.round(bat.percentage > 1 ? bat.percentage : bat.percentage * 100) : 100
28 readonly property bool onAc: !UPower.onBattery
29 // plugged in but not charging because of the cap
30 readonly property bool held: hasBat && onAc && capOn && bat.state !== UPowerDeviceState.Charging
31
32 // ---- charge limit
33 function busctl(args) {
34 return ["busctl", "--system"].concat(args.slice(0, 1), ["org.freedesktop.UPower", root.path,
35 "org.freedesktop.UPower.Device"], args.slice(1))
36 }
37 function setCap(on) {
38 if (!ready || !supported || setProc.running) return
39 setProc.want = on
40 setProc.command = busctl(["call", "EnableChargeThreshold", "b", on ? "true" : "false"])
41 setProc.running = true
42 }
43 // keep the firmware cap in line with the setting
44 function evaluate() {
45 if (!started || !ready || !supported || !hasBat) return
46 var want = Settings.chargeMode !== "off"
47 if (want !== capOn) setCap(want)
48 }
49
50 onPctChanged: checkLow()
51 onOnAcChanged: { checkLow(); applyAuto() }
52 Connections {
53 target: Settings
54 function onChargeModeChanged() { root.evaluate() }
55 function onBatLowChanged() { root.checkLow() }
56 function onBatCriticalChanged() { root.checkLow() }
57 function onBatCriticalActionChanged() { root.warnedCritical = false; root.checkLow() }
58 }
59
60 Process {
61 command: ["upower", "-e"]
62 running: true
63 stdout: StdioCollector {
64 onStreamFinished: {
65 var l = text.split("\n")
66 for (var i = 0; i < l.length; i++)
67 if (l[i].indexOf("/battery_") >= 0) { root.path = l[i].trim(); break }
68 if (root.ready) getProc.running = true
69 }
70 }
71 }
72 // "b true\nb false"
73 Process {
74 id: getProc
75 command: root.busctl(["get-property", "ChargeThresholdSupported", "ChargeThresholdEnabled"])
76 stdout: StdioCollector {
77 onStreamFinished: {
78 var l = text.trim().split("\n")
79 if (l.length < 2) return
80 root.supported = l[0].trim() === "b true"
81 root.capOn = l[1].trim() === "b true"
82 root.evaluate()
83 }
84 }
85 }
86 Process {
87 id: setProc
88 property bool want: false
89 stderr: StdioCollector { id: setErr }
90 onExited: code => {
91 if (code !== 0)
92 Quickshell.execDetached(["notify-send", "-a", "aurora", "-i", "battery", "charge limit",
93 "couldn't switch conservation mode: " + setErr.text.trim()])
94 getProc.running = true // read back the real state
95 }
96 }
97
98 // ---- power-profiles-daemon present? (Quickshell's PowerProfiles has no "available" flag)
99 // Don't touch PowerProfiles before it is: reading it D-Bus-activates ppd and blocks the whole shell until it
100 // runs, and ppd is After=multi-user.target (which waits on network-online), so that can be 30s+ after login.
101 property bool hasProfiles: false
102 readonly property bool hasPerf: hasProfiles && PowerProfiles.hasPerformanceProfile
103 property int ppdTries: 0
104 Process {
105 id: ppdCheck
106 command: ["busctl", "--system", "status", "org.freedesktop.UPower.PowerProfiles"]
107 running: true
108 stdout: StdioCollector {}
109 onExited: code => { root.ppdTries++; root.hasProfiles = code === 0; root.applyAuto() }
110 }
111 // keep looking for ~2 min after start
112 Timer {
113 interval: 5000
114 repeat: true
115 running: !root.hasProfiles && root.ppdTries < 24
116 onTriggered: if (!ppdCheck.running) ppdCheck.running = true
117 }
118
119 // ---- power profile: saver | balanced | performance (set in ppd, which remembers it), or auto =
120 // Settings.powerAc / powerBat, switched when the power source changes
121 readonly property string profile: Settings.powerAuto ? "auto" : hasProfiles ? nameOf(PowerProfiles.profile) : "balanced"
122 function nameOf(p) {
123 return p === PowerProfile.PowerSaver ? "saver" : p === PowerProfile.Performance ? "performance" : "balanced"
124 }
125 function profileOf(name) {
126 if (name === "saver") return PowerProfile.PowerSaver
127 if (name === "performance" && hasPerf) return PowerProfile.Performance
128 return PowerProfile.Balanced
129 }
130 function setProfile(name) {
131 Settings.powerAuto = name === "auto"
132 if (name === "auto") applyAuto()
133 else if (hasProfiles) PowerProfiles.profile = profileOf(name)
134 }
135 function applyAuto() {
136 if (!started || !hasProfiles || !Settings.powerAuto) return
137 var p = profileOf(onAc ? Settings.powerAc : Settings.powerBat)
138 if (PowerProfiles.profile !== p) PowerProfiles.profile = p
139 }
140 Connections {
141 target: Settings
142 function onPowerAutoChanged() { root.applyAuto() }
143 function onPowerAcChanged() { root.applyAuto() }
144 function onPowerBatChanged() { root.applyAuto() }
145 }
146
147 // ---- low battery: one warning per level per discharge, then the critical action after 60s
148 property bool warnedLow: false
149 property bool warnedCritical: false
150 function checkLow() {
151 if (!started || (hasBat && pct <= 0)) return
152 if (!hasBat || onAc) {
153 warnedLow = warnedCritical = false
154 if (critical.running) { critical.stop(); critNote.running = false }
155 return
156 }
157 // re-arm once above a level again (a threshold changed in settings)
158 if (pct > Settings.batLow) warnedLow = false
159 if (pct > Settings.batCritical) {
160 warnedCritical = false
161 if (critical.running) { critical.stop(); critNote.running = false }
162 }
163 if (pct <= Settings.batCritical && !warnedCritical) {
164 warnedCritical = warnedLow = true
165 var act = Settings.batCriticalAction
166 if (act === "none") {
167 Quickshell.execDetached(["notify-send", "-a", "aurora", "-u", "critical", "-i", "battery-caution",
168 "battery critical", pct + "% left, plug in now"])
169 } else {
170 critNote.command = ["notify-send", "-a", "aurora", "-u", "critical", "-i", "battery-caution",
171 "-A", "cancel=cancel", "battery critical",
172 pct + "% left: " + (act === "suspend" ? "suspending" : "powering off")
173 + " in 60s unless plugged in"]
174 critNote.running = true
175 critical.restart()
176 }
177 } else if (pct <= Settings.batLow && !warnedLow) {
178 warnedLow = true
179 Quickshell.execDetached(["notify-send", "-a", "aurora", "-u", "critical", "-i", "battery-caution",
180 "battery low", pct + "% left"])
181 }
182 }
183
184 Timer {
185 id: critical
186 interval: 60000
187 onTriggered: {
188 critNote.running = false
189 Session.run(Settings.batCriticalAction === "suspend" ? "sleep" : "shutdown")
190 }
191 }
192 // the critical warning's "cancel" button
193 Process {
194 id: critNote
195 stdout: StdioCollector {
196 onStreamFinished: if (text.trim() === "cancel") critical.stop()
197 }
198 }
199}