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:
+9
-1
@@ -4,7 +4,7 @@
|
||||
Do not edit this file. Run `quickshell/scripts/panama-settings-docs`
|
||||
after changing the schema; a contract fails when this copy is stale.
|
||||
|
||||
172 settings across 35 groups. 77 of them are applied to the compositor and confirmed by reading the value back.
|
||||
173 settings across 36 groups. 77 of them are applied to the compositor and confirmed by reading the value back.
|
||||
|
||||
## accessibility
|
||||
|
||||
@@ -289,6 +289,14 @@ Found on **Input › Mouse & Touchpad**.
|
||||
| **Hide pointer while typing**<br>`cursorHideWhileTyping` `cursor:hide_on_key_press` | false | The pointer vanishes on the next keystroke and returns when you move it |
|
||||
| **Jump pointer to the focused display**<br>`cursorWarpOnWorkspaceChange` `cursor:warp_on_change_workspace` | false | Moves the pointer to the last focused window after switching workspace |
|
||||
|
||||
## power
|
||||
|
||||
Found on **Power & Lock**.
|
||||
|
||||
| Setting | Default | What it does |
|
||||
|---|---|---|
|
||||
| **Pressing the power button**<br>`powerButtonAction` | menu | The system ignores the key; Panama decides — so a bumped button never yanks the plug Choices: Shows the power menu, Suspends, Powers off (two-press), Does nothing. |
|
||||
|
||||
## search
|
||||
|
||||
Found on **Applications › Applications**.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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