141 lines
9.5 KiB
Markdown
141 lines
9.5 KiB
Markdown
# 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 <entryId>` 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.
|