Krolyxon
Commit a466635

Branches

Refs →

aurora.configquickshellauroraBattery.qml

159 lines · 6.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() } }
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()
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 property bool hasProfiles: false
100 Process {
101 command: ["busctl", "--system", "status", "org.freedesktop.UPower.PowerProfiles"]
102 running: true
103 stdout: StdioCollector {}
104 onExited: code => root.hasProfiles = code === 0
105 }
106
107 // ---- low battery: one warning per level per discharge, then the critical action after 60s
108 property bool warnedLow: false
109 property bool warnedCritical: false
110 function checkLow() {
111 if (!started || (hasBat && pct <= 0)) return
112 if (!hasBat || onAc) {
113 warnedLow = warnedCritical = false
114 if (critical.running) { critical.stop(); critNote.running = false }
115 return
116 }
117 // re-arm once above a level again (a threshold changed in settings)
118 if (pct > Settings.batLow) warnedLow = false
119 if (pct > Settings.batCritical) {
120 warnedCritical = false
121 if (critical.running) { critical.stop(); critNote.running = false }
122 }
123 if (pct <= Settings.batCritical && !warnedCritical) {
124 warnedCritical = warnedLow = true
125 var act = Settings.batCriticalAction
126 if (act === "none") {
127 Quickshell.execDetached(["notify-send", "-a", "aurora", "-u", "critical", "-i", "battery-caution",
128 "battery critical", pct + "% left, plug in now"])
129 } else {
130 critNote.command = ["notify-send", "-a", "aurora", "-u", "critical", "-i", "battery-caution",
131 "-A", "cancel=cancel", "battery critical",
132 pct + "% left: " + (act === "suspend" ? "suspending" : "powering off")
133 + " in 60s unless plugged in"]
134 critNote.running = true
135 critical.restart()
136 }
137 } else if (pct <= Settings.batLow && !warnedLow) {
138 warnedLow = true
139 Quickshell.execDetached(["notify-send", "-a", "aurora", "-u", "critical", "-i", "battery-caution",
140 "battery low", pct + "% left"])
141 }
142 }
143
144 Timer {
145 id: critical
146 interval: 60000
147 onTriggered: {
148 critNote.running = false
149 Session.run(Settings.batCriticalAction === "suspend" ? "sleep" : "shutdown")
150 }
151 }
152 // the critical warning's "cancel" button
153 Process {
154 id: critNote
155 stdout: StdioCollector {
156 onStreamFinished: if (text.trim() === "cancel") critical.stop()
157 }
158 }
159}