# 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 1. **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. 2. **Power profile tiles** (existing service; presentation only, "via tuned" honesty). 3. **Battery health**: design-capacity %, charge cycles, and a charge-stop readback tile — dormant here, real on the laptop. 4. **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. 5. **Honesty rows**: lid card presents its inhibitor design; hibernate row states the zram reason, gated on the live `CanHibernate` probe; Management shows the generated path. 6. **The `panama-lid` clobber dies**: `cmd_open` emits the full stored record (position from stored x/y when present, scale, transform, vrr, bitdepth, cm, sdr fields — conditional, mirroring `monitors.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`**: the `XF86PowerOff` bind branches on the preference: menu → existing power-menu IPC; suspend → `systemctl suspend`; poweroff → power-menu IPC with a `poweroff` preselect argument; nothing → no-op bind (still `locked = 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 ` calls `PowerMenu.preselect(entryId)`, which opens the menu and calls the entry's own `trigger()` — 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. `toggle` is unchanged. - **`scripts/panama-idle`**: new `inhibitors` verb → `[{ who, why, what, mode }]` (sleep/idle/handle-lid-switch holds only, Panama's own lid inhibitor included honestly); `services/IdleLock.qml` exposes `inhibitors` + `inhibitorsKnown` + `refreshInhibitors()`, which B calls on page open. - **Settled (A), two additions to the pinned shape.** Source is logind's `ListInhibitors` over `busctl --json=short`, not `systemd-inhibit --list`: that table is padded display output whose `why` column contains spaces and whose `who` column is a free string the inhibiting program picks, so no column split is reliable. And each row carries `mode`, because `delay` and `block` are not the same claim — NetworkManager, UPower and hypridle hold permanent `delay` holds on every machine, and a list that did not distinguish them would report the desktop as pinned awake at all times. **Only `block` keeps a machine awake; B should read the empty state off the blocks, not the row count.** Rows sort blocks first, then by `who`. On failure the verb prints NOTHING and exits non-zero (never `[]`), which is what `inhibitorsKnown` is derived from. - **`scripts/panama-battery`**: `status` gains `healthPercent` (energy_full/energy_full_design or charge_ equivalents, summed across packs), `cycleCount` — both JSON `null` when sysfs lacks them, and a reported `cycle_count` of 0 counts as absent rather than as a new battery; `services/Battery.qml` exposes `healthPercent`/`cycleCount` as `property var` defaulting to `null` (an `int` property would coerce null back to 0), filled by a new `refreshHealth()` that runs once when the sysfs paths resolve and again whenever the page asks — never on the 20-second poll. - **`scripts/panama-lid`**: `cmd_open` reads the stored record's full field set via jq and emits `hl.monitor({...})` with every valid field, conditional per key, position from stored x/y when both present else `"auto"` — validation mirroring `monitors.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 to `powerButtonAction`, generatedPath rendered); `powerButtonAction` consumed in keybinds.lua (all four values branch); the poweroff branch goes through the power menu (никогда a direct poweroff — pin that `systemctl poweroff` appears nowhere in the bind path). - `lid-contract` / a new static half: fixture settings.json with extended display fields → the generated `hl.monitor` line 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`, NEW `IdleTimeline.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.