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:
@@ -1302,3 +1302,171 @@ documented").
|
||||
one (`secrets-contract`, which lists the live keyring), then the harness ones
|
||||
(`settings-search-contract`, `lock-screen-settings-contract`), and
|
||||
`settings-pages-contract` last, as before.
|
||||
|
||||
## Phase 13 (Power & Lock) — append below
|
||||
|
||||
Spec: `2026-08-24-power-lock-redesign.md`. The Power page stopped being four
|
||||
cards that each said a true thing and together said nothing. Two idle cards
|
||||
holding the same three concepts became one, with a timeline drawn above the
|
||||
sliders and the ordering warning rendered on it rather than appearing as a
|
||||
fifth card. The power button became adjustable without root — logind keeps
|
||||
`HandlePowerKey=ignore`, and the compositor bind branches on a new
|
||||
`powerButtonAction` preference. The lid's `open` stopped clobbering the panel's
|
||||
configuration. Battery grew health, and the idle card grew a list of what is
|
||||
holding the machine awake.
|
||||
|
||||
Three agents edited the tree concurrently; everything below was reconciled
|
||||
against the landed files rather than against the spec's pinned shapes.
|
||||
|
||||
### New contracts (1)
|
||||
|
||||
`quickshell/power-page-contract`. The README count line moves **173 → 174**;
|
||||
`setup/readme-contract` was run and passes ("174 contracts, as documented").
|
||||
|
||||
The lid emission fix went into the EXISTING `setup/lid-contract` as a fifth
|
||||
section rather than into a file of its own. It drives the same helper the other
|
||||
four sections drive, through the same stub directory, and splitting it out
|
||||
would have meant a second copy of that scaffolding for no separation anyone
|
||||
benefits from.
|
||||
|
||||
### Run and passing
|
||||
|
||||
- **`setup/lid-contract` — RUN END TO END, PASS.** Hermetic throughout: a
|
||||
stubbed `panama-hw` answers the predicates, a stubbed `hyprctl` answers the
|
||||
monitor query from a fixture and records the rule instead of applying it, and
|
||||
`XDG_CONFIG_HOME` points at a scratch settings store. Nothing reached the
|
||||
compositor. The four existing sections are unchanged and still pass. The new
|
||||
one pins:
|
||||
- **The full record survives the round trip.** A fixture panel with mode,
|
||||
scale, transform, stored x/y, VRR, bit depth, colour profile and both SDR
|
||||
trims comes back as an `hl.monitor` rule carrying every one of them, with
|
||||
`position` built from the stored coordinates rather than `"auto"`.
|
||||
- **The old four-key emission fails it**, verified by running the check
|
||||
against the pre-fix `cmd_open` before A's rewrite landed: eight findings,
|
||||
naming the position and every extended field it dropped.
|
||||
- **The two failure directions stay different**, which is the part worth a
|
||||
contract. A bad colour profile, VRR, bit depth or SDR value drops on its
|
||||
own and the geometry stands; a bad transform, scale or mode refuses the
|
||||
WHOLE record and the panel comes back on `mode = "preferred"`, never
|
||||
half-honoured. Each of the three geometry fixtures was confirmed to be
|
||||
valid JSON differing from the good one in exactly one field, so none of
|
||||
them passes by failing to parse.
|
||||
- **No stored coordinates means `position = "auto"`**, and a half-written
|
||||
position (an `x` with no `y`) is refused the same way `monitors.lua`
|
||||
refuses it.
|
||||
- **`null` never reaches the rule** — jq prints an absent key as the string
|
||||
"null", and a rule carrying one is the shape this whole fix exists to avoid.
|
||||
- **It stays `hyprctl eval`, never `keyword`**, so even a wrong rule dies at
|
||||
the next reload.
|
||||
- **`quickshell/battery-contract` — RUN END TO END, PASS.** Existing pins
|
||||
unchanged; still driven against fixture sysfs trees. New: health is read
|
||||
from `energy_full`/`energy_full_design` AND from the `charge_` spelling (a
|
||||
driver that reports charge is not a machine without a battery), the cycle
|
||||
count is carried through, and — the half that matters — a battery whose
|
||||
firmware reports neither gets `null`, not a confident `100%` and `0 cycles`
|
||||
on hardware that never said. Checked four ways: the laptop fixture without
|
||||
the files, the desktop fixture with no battery at all, a zero design capacity
|
||||
that would otherwise divide by it, and the JSON staying parseable in every
|
||||
one of those cases.
|
||||
- **`quickshell/power-page-contract` — RUN END TO END, PASS.** Static
|
||||
apart from one stubbed run: `panama-idle inhibitors` against a fake `busctl`,
|
||||
which is the only part of the row nothing else covers. What it pins:
|
||||
- **The power button cannot power the machine off.** Said three ways, because
|
||||
this is the assertion the redesign turns on: no `systemctl poweroff` on the
|
||||
bind path (comments stripped first — keybinds.lua explains at length that it
|
||||
deliberately does not, and a contract must not fail over prose that agrees
|
||||
with it); the powermenu IPC handler in `shell.qml` runs no command of its
|
||||
own; and the menu still disarms its destructive entries, which is what makes
|
||||
powering off two presses.
|
||||
- **Every option the schema offers is branched on**, with the option values
|
||||
read OUT of the schema rather than restated — a contract listing the four
|
||||
would pass on the day a fifth was added and did nothing. Plus: the fallback
|
||||
arm opens the menu, so a hand-edited settings file cannot decide what the
|
||||
power key does, and the bind stays `locked = true`.
|
||||
- **`powerButtonAction` carries no `hypr` block**: it is read by keybinds.lua
|
||||
the way workspace rules are, and a hypr block would send Hyprland a keyword
|
||||
that does not exist.
|
||||
- **One idle card, structurally.** Not by title — the AC and battery sliders
|
||||
for each of the three timings must live inside the SAME `SettingsCard`
|
||||
block, sliced by brace depth. A rename cannot satisfy this and a second
|
||||
card cannot hide from it.
|
||||
- **The inhibitor list is filtered, ordered, and honest about failure.** A
|
||||
`shutdown` hold says nothing about whether the screen will blank and is
|
||||
dropped; sleep, idle and handle-lid-switch holds are kept, Panama's own lid
|
||||
inhibitor included; `delay` inhibitors sort below `block` ones, because a
|
||||
delay does not keep a machine awake; and a logind that cannot be reached
|
||||
exits non-zero WITHOUT printing `[]`, so "could not look" never reads as
|
||||
"nothing is holding it". The ordering assertion was checked against a
|
||||
delay-first list to confirm it discriminates.
|
||||
- **The page's own components count as the page.** The file list is derived —
|
||||
PowerPage.qml plus every settings component it instantiates that no other
|
||||
settings page does — so a card lifted into a component of its own keeps
|
||||
being checked instead of quietly falling out of scope.
|
||||
- **`hypr/idle-config-contract`, `hypr/idle-defaults-contract`,
|
||||
`quickshell/powermenu-contract` — RUN, PASS, unchanged.** Verified rather
|
||||
than assumed: the `inhibitors` verb is additive (it adds no `read_setting`
|
||||
call, which is what `idle-defaults-contract` walks) and the powermenu's
|
||||
pre-select argument left `logoutScript` alone.
|
||||
- **`quickshell/search-routing-contract` — RUN, PASS** (145 routed settings,
|
||||
up from 144: `powerButtonAction` is the new one). Also RUN and passing:
|
||||
`schema-hypr-shape-contract` (77 mapped options), `enum-hypr-map-contract`
|
||||
(11 mapped enums), `preference-schema-contract`,
|
||||
`settings-ownership-contract`, `setup/readme-contract`.
|
||||
- **Schema docs and Vicinae commands regenerated and passing.**
|
||||
`quickshell/scripts/panama-settings-docs` and `panama-settings-commands` were
|
||||
both run without `--check` and their output committed:
|
||||
`docs/settings.md` goes 172 → **173 settings across 36 groups**, with the new
|
||||
`power` group rendering as "Found on **Power & Lock**" — the group comment
|
||||
shape the spec asked C to verify, which comes from the `"power": "power"`
|
||||
route added to `SettingsSearch.qml`. `settings-power`'s Vicinae keywords
|
||||
gained "pressing the power button", "hibernate" and "power profile".
|
||||
`quickshell/settings-docs-contract` — RUN, PASS (173 settings documented);
|
||||
`quickshell/panama-commands-contract` — RUN, PASS (77 commands).
|
||||
- **Six search entries added**: Hibernate, Power profile, Power button, Lid,
|
||||
Suspend, Sleep, all routing to `power`, plus the `"power": "power"` group
|
||||
route for the new schema group. Hibernate is the one worth having: 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.
|
||||
|
||||
### Deferred, and why
|
||||
|
||||
- **`quickshell/settings-search-contract` — NOT RUN**: it daemonizes a
|
||||
Quickshell harness. The six new entries were checked statically and route to
|
||||
`power`, which `settings-nav-contract` confirms is a leaf.
|
||||
- **`quickshell/settings-pages-contract`, `settings-write-sweep-contract`,
|
||||
`qmldir-registration-contract`, `settings-buttons-contract`,
|
||||
`settings-nav-contract`, `settings-jump-contract`,
|
||||
`settings-hardcoded-values-contract` — NOT RUN**: they load the QML or drive
|
||||
the live settings window. The Power page was rebuilt this phase and needs all
|
||||
of them at the end-of-redesign sweep.
|
||||
- **`quickshell/lock-screen-settings-contract` — NOT RUN** (same harness reason
|
||||
as phase 12); its source-only half still expects `lockMinutes`,
|
||||
`lockMinutesBattery` and `lockOnSleep` bound on Power, which the rebuilt page
|
||||
keeps.
|
||||
- **One thing the two agents disagree about, left as it landed.** A's
|
||||
`IdleLock` exposes `inhibitorsKnown` beside `inhibitors`, so a logind that
|
||||
could not be reached is distinguishable from one holding nothing — the
|
||||
helper's own contract pins that distinction, and the verb exits non-zero
|
||||
without printing `[]` precisely so the caller can tell. B's page does not
|
||||
read it: an empty list renders as "Nothing — no application holds a wake
|
||||
lock" whether the probe came back empty or never came back, which the page
|
||||
argues in a comment is also the honest thing to say before the first reading
|
||||
lands. That is defensible for the first-read case and wrong for the failure
|
||||
case, and it is a copy decision rather than a wiring one, so it is reported
|
||||
rather than pinned. Worth a look on the rendered page.
|
||||
- **The whole live half of the power button.** Nothing here presses it. The
|
||||
contract proves the bind branches and that no branch powers off; whether the
|
||||
compositor picks the new bind up needs a `hyprctl reload`, which this phase
|
||||
deliberately did not run.
|
||||
- **Hibernate is dormant on this machine.** `CanHibernate` answers no under
|
||||
zram swap, so the gated row is pinned by its wiring rather than by having
|
||||
been seen.
|
||||
- **Battery health is dormant too**, for the same reason the rest of the
|
||||
battery surface is: this is a desktop. Every health assertion runs against a
|
||||
fixture sysfs tree.
|
||||
- Run order for this phase: the hermetic ones first (`lid-contract`,
|
||||
`battery-contract`, `power-page-contract`, `idle-config-contract`,
|
||||
`idle-defaults-contract`), then the source-only ones (`powermenu-contract`,
|
||||
`search-routing-contract`, `schema-hypr-shape-contract`,
|
||||
`preference-schema-contract`, `settings-docs-contract`,
|
||||
`readme-contract`), then the harness ones last.
|
||||
|
||||
Reference in New Issue
Block a user