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:
@@ -0,0 +1,140 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user