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:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,28 @@ import QtQuick
|
||||
import qs.config
|
||||
|
||||
Singleton {
|
||||
// Whether logind says this machine can resume from disk. Probed once --
|
||||
// the answer changes only when swap is reconfigured. The power menu keeps
|
||||
// its own copy of this probe for its Hibernate entry; this one exists so
|
||||
// the Power page can state the machine's answer rather than describing
|
||||
// the menu's behavior from a distance.
|
||||
property bool canHibernate: false
|
||||
property bool canHibernateKnown: false
|
||||
|
||||
Process {
|
||||
id: hibernateProbe
|
||||
command: ["busctl", "call", "org.freedesktop.login1",
|
||||
"/org/freedesktop/login1", "org.freedesktop.login1.Manager",
|
||||
"CanHibernate"]
|
||||
running: true
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
canHibernate = this.text.includes('"yes"');
|
||||
canHibernateKnown = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
id: root
|
||||
|
||||
readonly property string helperPath: Quickshell.shellDir + "/scripts/panama-idle"
|
||||
@@ -35,6 +57,49 @@ Singleton {
|
||||
|
||||
readonly property bool busy: statusQuery.running || applyRun.running
|
||||
|
||||
// What is holding sleep or idle off right now: an array of
|
||||
// { who, why, what, mode }, newest read wins, blocks sorted before delays.
|
||||
//
|
||||
// hypridle has no conditional listener -- there is no way to say "not while
|
||||
// a video is playing" -- so rather than inventing rules about when not to
|
||||
// sleep, the Power page shows who is already saying it. `mode` is the part
|
||||
// that matters when reading the list: a `delay` inhibitor holds sleep for a
|
||||
// few seconds on the way down and nothing more, while a `block` is what
|
||||
// actually keeps a machine awake. NetworkManager, UPower and hypridle
|
||||
// itself hold delays permanently, so a list that did not distinguish them
|
||||
// would report a machine as pinned awake at all times.
|
||||
property var inhibitors: []
|
||||
|
||||
// False until logind has actually answered. "Nothing holds the machine
|
||||
// awake" and "nobody could be asked" are different claims and the page
|
||||
// must not make the first one on the strength of the second.
|
||||
property bool inhibitorsKnown: false
|
||||
|
||||
function refreshInhibitors(): void {
|
||||
if (!inhibitorQuery.running)
|
||||
inhibitorQuery.running = true;
|
||||
}
|
||||
|
||||
Process {
|
||||
id: inhibitorQuery
|
||||
command: [root.helperPath, "inhibitors"]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
try {
|
||||
const parsed = JSON.parse(this.text);
|
||||
root.inhibitors = Array.isArray(parsed) ? parsed : [];
|
||||
root.inhibitorsKnown = true;
|
||||
} catch (error) {
|
||||
// The helper prints nothing at all when logind could not
|
||||
// be asked, precisely so this lands here rather than
|
||||
// parsing an empty array and believing it.
|
||||
root.inhibitors = [];
|
||||
root.inhibitorsKnown = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The values as stored. They only describe what is running when `managed`.
|
||||
readonly property int blankMinutes: DesktopPreferences.get("screenBlankMinutes")
|
||||
readonly property int lockMinutes: DesktopPreferences.get("lockMinutes")
|
||||
|
||||
@@ -34,6 +34,7 @@ Singleton {
|
||||
"datetime": "datetime",
|
||||
"battery": "power",
|
||||
"idleBattery": "power",
|
||||
"power": "power",
|
||||
"typography": "appearance",
|
||||
"themes": "appearance",
|
||||
"titlebar": "appearance",
|
||||
@@ -148,6 +149,18 @@ Singleton {
|
||||
{ label: "Performance overlay", detail: "Frame rate and sensors on top of the game", page: "gaming" },
|
||||
{ label: "Proton", detail: "Compatibility tools available to Steam", page: "gaming" },
|
||||
{ label: "Graphics card", detail: "Temperature, power draw, and video memory", page: "gaming" },
|
||||
// Power & Lock. The sliders come from the schema, but the subjects
|
||||
// people arrive with are the physical ones — a button, a lid, sleep —
|
||||
// and none of those is the label of a preference. Hibernate is the
|
||||
// sharpest case: it has no control at all, only an honest row saying
|
||||
// why this machine will not do it, and a search that found nothing
|
||||
// would read as the desktop having no opinion.
|
||||
{ label: "Hibernate", detail: "Whether this machine can hibernate, and why zram swap means it does not", page: "power" },
|
||||
{ label: "Power profile", detail: "Power saver, balanced, or performance, applied system-wide", page: "power" },
|
||||
{ label: "Power button", detail: "What pressing it does: the power menu, suspend, power off, or nothing", page: "power" },
|
||||
{ label: "Lid", detail: "What closing the lid does, and why an external display changes it", page: "power" },
|
||||
{ label: "Suspend", detail: "When the machine sleeps on its own, on wall power and on battery", page: "power" },
|
||||
{ label: "Sleep", detail: "The idle timeline: screen off, then lock, then suspend", page: "power" },
|
||||
{ label: "Software update", detail: "Packages, applications, and firmware", page: "updates" },
|
||||
{ label: "Updates", detail: "What is waiting to be installed", page: "updates" },
|
||||
{ label: "Firmware", detail: "Updates for the hardware itself", page: "updates" },
|
||||
|
||||
Reference in New Issue
Block a user