9.5 KiB
Power & Lock redesign — one timeline, honest sleep
Approved mock: home-mocks/power.html (scratchpad, :8642). Spec wins over mock on conflict.
Single tabless page stays.
Goals
- Idle timeline: one visualization (Active → Screen off → Lock → Suspend) above the sliders, switching wall-power/battery by live source; the ordering warnings become part of the picture. A "Keeping the machine awake right now" row lists live inhibitors.
- Power profile tiles (existing service; presentation only, "via tuned" honesty).
- Battery health: design-capacity %, charge cycles, and a charge-stop readback tile — dormant here, real on the laptop.
- Power button adjustable without root: logind keeps
HandlePowerKey=ignore; the compositor bind reads a new preference. Options: Shows the power menu (default) / Suspends / Powers off (two-press: opens the power menu with Power Off pre-selected, so the second press fires it) / Does nothing. - Honesty rows: lid card presents its inhibitor design; hibernate row states the zram
reason, gated on the live
CanHibernateprobe; Management shows the generated path. - The
panama-lidclobber dies:cmd_openemits the full stored record (position from stored x/y when present, scale, transform, vrr, bitdepth, cm, sdr fields — conditional, mirroringmonitors.lua's validation) instead of the 4-key rule that wipes extended fields.
Non-goals: lid behavior override (deliberately read-only, per LidPolicy's argued design), scheduled shutdown, suspend-then-hibernate, time-to-empty (deliberate omission stays), per-app power, conditional suspend rules (hypridle has no conditional listener — the inhibitor row is the honest substitute).
Schema (A)
New group power (groupPages → power): powerButtonAction — enum, def "menu", values
menu | suspend | poweroff | nothing, label "Pressing the power button", detail per mock. No
hypr block (consumed by keybinds.lua via prefs, like workspace rules — reload-applied is fine
for a power-button preference; if the keybind can read it live via a dispatcher branch,
better — A investigates which pattern keybinds.lua supports and documents the choice).
Settled (A): per-press, no reload. The bind is hl.dsp.exec_cmd of a one-line shell
case over jq -r '.powerButtonAction // empty' against the settings file, so the branch is
evaluated when the key goes down rather than when the config is read. All four values branch
inside keybinds.lua; a missing jq, missing file, malformed file or unknown value falls through
to *) and opens the menu. prefs.get was rejected because prefs.lua loads the store once at
config time, which would have made this the one Power-page control that needs hyprctl reload.
Plumbing (A)
config/dot/hypr/keybinds.lua: theXF86PowerOffbind branches on the preference: menu → existing power-menu IPC; suspend →systemctl suspend; poweroff → power-menu IPC with apoweroffpreselect argument; nothing → no-op bind (stilllocked = true).shell.qml+modules/powermenu/PowerMenu.qml: the powermenu IPC accepts an optional entry id to pre-select (arming Power Off so one more press fires — the existing two-press semantics preserved); no other menu behavior changes.- Settled (A). Each entry gains a stable
entryId:lock | logout | suspend | hibernate | restart | poweroff.qs ipc call powermenu open <entryId>callsPowerMenu.preselect(entryId), which opens the menu and calls the entry's owntrigger()— the same one a click calls — so a destructive entry arms on the first press and fires on the second, inside the existing 4-second confirm window. Nothing bypasses the confirm; an id this machine has no entry for (hibernate without swap) just opens the menu.toggleis unchanged.
- Settled (A). Each entry gains a stable
scripts/panama-idle: newinhibitorsverb →[{ who, why, what, mode }](sleep/idle/handle-lid-switch holds only, Panama's own lid inhibitor included honestly);services/IdleLock.qmlexposesinhibitors+inhibitorsKnown+refreshInhibitors(), which B calls on page open.- Settled (A), two additions to the pinned shape. Source is logind's
ListInhibitorsoverbusctl --json=short, notsystemd-inhibit --list: that table is padded display output whosewhycolumn contains spaces and whosewhocolumn is a free string the inhibiting program picks, so no column split is reliable. And each row carriesmode, becausedelayandblockare not the same claim — NetworkManager, UPower and hypridle hold permanentdelayholds on every machine, and a list that did not distinguish them would report the desktop as pinned awake at all times. Onlyblockkeeps a machine awake; B should read the empty state off the blocks, not the row count. Rows sort blocks first, then bywho. On failure the verb prints NOTHING and exits non-zero (never[]), which is whatinhibitorsKnownis derived from.
- Settled (A), two additions to the pinned shape. Source is logind's
scripts/panama-battery:statusgainshealthPercent(energy_full/energy_full_design or charge_ equivalents, summed across packs),cycleCount— both JSONnullwhen sysfs lacks them, and a reportedcycle_countof 0 counts as absent rather than as a new battery;services/Battery.qmlexposeshealthPercent/cycleCountasproperty vardefaulting tonull(anintproperty would coerce null back to 0), filled by a newrefreshHealth()that runs once when the sysfs paths resolve and again whenever the page asks — never on the 20-second poll.scripts/panama-lid:cmd_openreads the stored record's full field set via jq and emitshl.monitor({...})with every valid field, conditional per key, position from stored x/y when both present else"auto"— validation mirroringmonitors.lua(regex-safe values only; invalid extended fields drop per-field, geometry survives). Header's "NOT YET VERIFIED ON A LAPTOP" honesty stays.
UI (B)
PowerPage.qml rebuilt per mock: Power profile tiles (icon, label, one-line detail, active
highlight, degraded note); Idle card (NEW IdleTimeline.qml: proportional stops on a
piecewise scale with labels, driven by the same schema values — draggable with snap-to-step
if robust, read-only visualization otherwise [flag which]; the source header "on wall power" /
"on battery" from Battery.acOnline; the battery-variant sliders swap in below when on
battery — one card, not two; ordering warnings rendered inline on the timeline rather than a
separate card); Lock before sleeping toggle; inhibitors row ("Nothing — no application holds a
wake lock" empty state); Battery card (health tiles + charge-stop readback + existing
low/critical/action rows; charge-limit slider keeps firmware-confirm detail); Lid card
(read-only, DOCKED/WILL SUSPEND badge from LidPolicy, the inhibitor-design prose); Power
button card (OptionPickerRow on powerButtonAction, the mock's detail); Session card (Lock
now — through a service or execDetached as today, hibernate row gated !canHibernate showing
the zram reason [read the live probe from PowerMenu's existing CanHibernate source — lift it
into a small service property if needed], power menu pointer row); Management card (managed
toggle + IdleLock.generatedPath + service state in the detail).
Search & docs (C)
Extra entries: Hibernate, Power profile, Power button, Lid, Suspend, Sleep → power. Schema regen: docs + commands after the new key (orchestrator can also run it; C verifies the group comment shape).
Contracts (C — write; hermetic runs only)
- NEW
power-page-contract: page structure (profile tiles, IdleTimeline present and fed by the schema keys, one idle card not two, inhibitors row with empty state, hibernate row gated on the live probe with the zram copy, power button row bound topowerButtonAction, generatedPath rendered);powerButtonActionconsumed in keybinds.lua (all four values branch); the poweroff branch goes through the power menu (никогда a direct poweroff — pin thatsystemctl poweroffappears nowhere in the bind path). lid-contract/ a new static half: fixture settings.json with extended display fields → the generatedhl.monitorline carries them; invalid values drop per-field; stored x/y become position; absent → auto. The old 4-key emission must FAIL it.battery-contract: health fields present when sysfs provides them, absent-tolerant when not (desktop fixture), never fabricated.idle-config-contract/idle-defaults-contract: verify unaffected (inhibitors verb is additive).- Backlog Phase 13; README count (173 → 174 expected).
Agent ownership (parallel)
- A:
config/PreferenceSchema.qml,config/dot/hypr/keybinds.lua,shell.qml,modules/powermenu/PowerMenu.qml,scripts/panama-idle,scripts/panama-battery,scripts/panama-lid,services/IdleLock.qml,services/Battery.qml. - B:
modules/settings/PowerPage.qml, NEWIdleTimeline.qml, other components (+ qmldir). - C:
services/SettingsSearch.qml, contracts above, backlog, README count line.
Hard rules: NO live mutations — no systemctl suspend/poweroff, no logind writes, no threshold
writes, no hyprctl keyword/eval, no powermenu triggering; keybinds.lua edits must keep
hyprctl reload UNRUN (the user reloads naturally later — but the shell hot-reloads QML, so
PowerMenu/shell.qml edits land live and must stay valid). Read-only probes and stubs only.
B programs against the pinned APIs; A updates this spec before changing them.