Draw idle as one timeline, and let the power button answer to its owner

Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
Gabriel Brown
2026-08-24 20:33:44 -04:00
parent 6f0ce639d9
commit e1ff25fc66
23 changed files with 2655 additions and 174 deletions
@@ -52,6 +52,21 @@ Singleton {
property int chargeLimit: 0
property bool chargeLimitSupported: false
// How much of its design capacity the pack still holds, as a whole
// percent, and how many full cycles it has been through.
//
// Both are `null` rather than 0 on the very many machines whose firmware
// does not report them -- every desktop, and a fair number of laptops that
// export cycle_count as a permanent zero. Null is the value the Power page
// renders as an em dash; a zero would render as a dead battery that has
// never been charged, which is the same class of lie as the time-to-empty
// estimate this service deliberately does not compute.
//
// `var` rather than `int` so that null survives: an int property would
// coerce it to 0 and put the lie back.
property var healthPercent: null
property var cycleCount: null
readonly property bool charging: root.status === "Charging"
readonly property bool low: root.available && !root.acOnline
&& root.percent <= Settings.batteryLowPercent
@@ -81,6 +96,20 @@ Singleton {
thresholdFile.reload();
}
// Wear, read through the helper rather than from sysfs directly.
//
// It is the one reading here that needs arithmetic across a variable set of
// files -- energy_full or charge_full, against a design capacity that may
// not exist, summed over however many packs the machine has -- which is
// exactly what the helper already does for `status`. Kept off the 20-second
// poll: health moves over months and cycles over days, so this runs once
// when the paths resolve and again whenever the Power page asks.
function refreshHealth(): void {
if (root.batteryPath === "" || healthQuery.running)
return;
healthQuery.running = true;
}
function setChargeLimit(percent: int): void {
if (!root.chargeLimitSupported || applyLimit.running)
return;
@@ -118,9 +147,33 @@ Singleton {
}
if (root.batteryPath === "") {
root.available = false;
root.healthPercent = null;
root.cycleCount = null;
return;
}
root.refresh();
root.refreshHealth();
}
}
}
Process {
id: healthQuery
command: [root.helperPath, "status"]
stdout: StdioCollector {
onStreamFinished: {
try {
const state = JSON.parse(this.text);
// Anything that is not a number -- null, absent, a string
// from a future field -- is "this machine does not say".
root.healthPercent = typeof state.healthPercent === "number"
? state.healthPercent : null;
root.cycleCount = typeof state.cycleCount === "number"
? state.cycleCount : null;
} catch (error) {
root.healthPercent = null;
root.cycleCount = null;
}
}
}
}
@@ -149,6 +202,10 @@ Singleton {
onLoadFailed: {
root.available = false;
root.located = false;
// The pack that these described is gone; keep no wear figures for
// a battery that is no longer there.
root.healthPercent = null;
root.cycleCount = null;
}
}