Make every settings row reachable, and every accessibility switch honest
Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
+3
-2
@@ -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.
|
||||
|
||||
173 settings across 36 groups. 77 of them are applied to the compositor and confirmed by reading the value back.
|
||||
174 settings across 36 groups. 77 of them are applied to the compositor and confirmed by reading the value back.
|
||||
|
||||
## accessibility
|
||||
|
||||
@@ -12,11 +12,12 @@ Found on **Accessibility**.
|
||||
|
||||
| Setting | Default | What it does |
|
||||
|---|---|---|
|
||||
| **Magnifier**<br>`magnifierFactor` `cursor:zoom_factor` | 1.0 | Magnifies the screen around the pointer. 1.0 is off. Range 1.0–5.0. |
|
||||
| **Magnifier**<br>`magnifierFactor` `cursor:zoom_factor` | 1.0 × | Magnifies the screen around the pointer. 1.00 × is off. Range 1.0–5.0. |
|
||||
| **Magnifier follows in steps**<br>`magnifierRigid` `cursor:zoom_rigid` | false | Moves the magnified view in increments rather than gliding with the pointer |
|
||||
| **High contrast**<br>`highContrast` | false | Increases contrast in applications that support it. Modern GTK applications read this from the desktop portal and restyle themselves; older ones need a high-contrast theme, which is not installed here. |
|
||||
| **Dim inactive windows**<br>`dimInactive` `decoration:dim_inactive` | false | Darkens every window except the focused one, so the active window is unmistakable |
|
||||
| **Dim amount**<br>`dimStrength` `decoration:dim_strength` | 0.5 | How much darker unfocused windows are. Range 0.05–0.9. |
|
||||
| **Flash the screen for notifications**<br>`visualAlerts` | false | A single flash at the edges of every screen when a notification arrives that would ring the bell |
|
||||
| **Pointer size**<br>`cursorSize` | 24 px | Applies to the compositor and to applications. Range 16–64. |
|
||||
| **Text size**<br>`textScale` | 1.0 | Scales interface text everywhere; 1.00 is the design size. Range 0.75–2.0. |
|
||||
|
||||
|
||||
@@ -1470,3 +1470,205 @@ benefits from.
|
||||
`search-routing-contract`, `schema-hypr-shape-contract`,
|
||||
`preference-schema-contract`, `settings-docs-contract`,
|
||||
`readme-contract`), then the harness ones last.
|
||||
|
||||
## Phase 14 (Accessibility) — append below
|
||||
|
||||
Spec: `2026-08-24-accessibility-redesign.md`. The Accessibility page stopped
|
||||
being six cards of controls sorted by mechanism plus one card that apologised,
|
||||
and became five sorted by what is being accommodated: Vision, Motion, Hearing,
|
||||
Keyboard & pointer, Screen reader. Behind them, the four things an
|
||||
accessibility page exists for and this one did not have: the magnifier gained
|
||||
keybinds routed through the shell, Reduce motion became true of the shell's own
|
||||
bar, dock, panels and OSD rather than only of the compositor, a visual
|
||||
alternative to the notification bell arrived, and Orca became a process that is
|
||||
reported as running or not instead of a button that launched it and forgot.
|
||||
Invisible and largest: the nine shared row primitives gained screen-reader
|
||||
names, roles, Tab stops and key handling, which reaches all fourteen categories
|
||||
at once.
|
||||
|
||||
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/accessibility-contract`. The README count line moves **174 → 175**;
|
||||
`setup/readme-contract` was run and passes ("175 contracts, as documented").
|
||||
|
||||
Entirely static, on purpose. Every failure it guards against is one a live run
|
||||
would report as working: `hyprctl keyword` exits 0 on a Lua-configured Hyprland
|
||||
while refusing the write, a gsettings `screen-reader-enabled` write succeeds and
|
||||
starts nothing, and a row with no `Accessible.name` looks and behaves exactly
|
||||
like one that has it. Nothing here starts a shell, applies a zoom, starts or
|
||||
stops Orca, or writes a gsettings key.
|
||||
|
||||
What it pins, in the order the file argues them:
|
||||
|
||||
- **The zoom path commits through the preference and never touches `hyprctl`.**
|
||||
Asserted over the PATH rather than over `stepZoom`'s own text: the slice is
|
||||
`stepZoom` plus every function in `Accessibility.qml` it hands work to
|
||||
(`showZoomOsd`, `zoomLabel`), collected transitively, so splitting the OSD
|
||||
call into a helper moves the assertions with it rather than out from under
|
||||
them. Within that path: no `hyprctl`, no `keyword`, a
|
||||
`SystemSettings.commitPreference` call, an `OsdState` post, and clamp bounds
|
||||
that either read `PreferenceSchema.spec` or match the schema's own 1.0–5.0.
|
||||
The `accessibility` IPC handler in `shell.qml` is checked the same way.
|
||||
- **The three binds, matched by target and verb rather than by chord.**
|
||||
`SUPER+=` was already "Reset split", so which free chords the zoom keys take
|
||||
was a decision made against `keybinds.lua` rather than against the mock — a
|
||||
contract naming `SUPER+=` would have been wrong on the day it was written.
|
||||
A separate check fails on any chord bound twice, which is the actual hazard:
|
||||
Hyprland takes the last bind and the earlier action stops working silently.
|
||||
- **The visual bell is one-shot.** No `loops: Animation.Infinite`, no `loops:
|
||||
-1`, no Timer with `repeat: true` anywhere behind it, and it reads
|
||||
`visualAlerts`.
|
||||
- **The flash follows bell eligibility EXCEPT the event-sounds gate.** The
|
||||
signal name is read out of `VisualBell.qml`'s `Connections { target: Notifs }`
|
||||
rather than guessed, which also pins that the two are really wired. Two
|
||||
structural checks then run over `Notifs.qml`: no `SoundFeedback.eventSounds`
|
||||
early return may stand between the enclosing function's first line and the
|
||||
emit, and the emit must sit on a path that DOES carry the shared gates
|
||||
(`appRule(...).sound`, low urgency, `suppress-sound`) — otherwise the flash
|
||||
would not be the bell seen, but a second louder notifier ignoring every
|
||||
per-application rule somebody set. A third check fails if the file stops
|
||||
explaining the exception, since an undocumented exception is one the next
|
||||
reader helpfully removes.
|
||||
- **Orca is a process, never a gsettings key.** `screen-reader-enabled` is
|
||||
grepped for repo-wide across the shell's QML and JS, comment-stripped first.
|
||||
Plus: `orcaRunning` exists, something `pgrep`s, and the probe is not on a
|
||||
repeating Timer — a poll that runs all session to answer a question nobody
|
||||
asked is a cost with no reader.
|
||||
- **The nine primitives.** `Accessible.name`, `Accessible.role` and
|
||||
`activeFocusOnTab` in each of `SliderRow`, `ToggleRow`, `SwitchRow`,
|
||||
`ActionRow`, `ChoiceRow`, `SegmentRow`, `SettingRow`, `OptionPickerRow`,
|
||||
`PickerRow`, resolved through the QML inheritance chain (a name inherited
|
||||
from `SettingRow` is a name the row really has) — with one exception that is
|
||||
NOT inheritable: a file drawing its own `MouseArea`/`SettingsToggle`/
|
||||
`SettingsButton`/`ValueSlider` must declare its own `activeFocusOnTab`,
|
||||
because `SettingRow` is a Tab stop only when `activatable`, which a
|
||||
`ToggleRow` is not. Then: Space/Enter activation on the four rows that
|
||||
activate, Left/Right on the two that hold a range, a focus indicator gated on
|
||||
`activeFocus`, and — the visual-at-rest promise — no focus border painted
|
||||
unconditionally, since these rows are used by every page.
|
||||
- **The page, per the approved mock.** The five card titles, the five retired
|
||||
ones absent, `dimStrength` gated on `dimInactive`, the mono-audio row present
|
||||
and marked not-yet with somewhere to go meanwhile, the Keyboard jump going
|
||||
through `openSettings()` rather than assigning `settingsPage` by hand, and no
|
||||
`Process` on the page or its own components.
|
||||
- **The honesty box is a statement, not an alarm.** It must name Hyprland (not
|
||||
"Wayland"), say the absence was probed rather than assumed, and explain why
|
||||
the GNOME switches are inert here — and it must NOT be painted in
|
||||
`Theme.danger`/`Theme.warn` or use the words Warning, Error, Unsupported,
|
||||
unfortunately, sorry, Broken. An accessibility page is the last page that can
|
||||
afford to open with an alarm.
|
||||
- **The dead zero label, swept across every settings page rather than only this
|
||||
one.** `SliderRow.display()` substitutes `zeroLabel` at exactly 0, so a row
|
||||
setting one for a setting whose schema minimum is above 0 is copy that can
|
||||
never appear — which is what `zeroLabel: "Off"` was doing under a magnifier
|
||||
whose minimum IS 1.0. Any page reintroducing that class of bug now fails.
|
||||
- **Reduce motion is true of the shell**: `Theme.motionEnabled` reads
|
||||
`Settings.animationsEnabled`, and all four duration tokens collapse to 0.
|
||||
- **The cross-goal collision, which is silent.** `VisualBell` must not time its
|
||||
flash with a `Theme.dur*` token: those now collapse to 0 under Reduce motion,
|
||||
so a flash timed with `Theme.durFast` would be instantaneous and therefore
|
||||
invisible — switching on Reduce motion would silently switch OFF Visual
|
||||
alerts, for somebody quite likely to want both. A landed and pinned.
|
||||
|
||||
### Reconciled
|
||||
|
||||
- **`quickshell/notification-app-rules-contract` — RUN, PASS after a fix.** It
|
||||
broke on the visual-alerts refactor: the bell's shared gates moved out of
|
||||
`playBell` into the new `bellWouldRing` predicate, and the contract read only
|
||||
`playBell`'s literal body, so it reported that the per-application sound
|
||||
switch and the effective-urgency check had been dropped when both had merely
|
||||
moved one call up. Now asserted over the bell's DECISION PATH (`playBell` +
|
||||
`bellWouldRing`), plus a new assertion that `playBell` actually consults the
|
||||
predicate — without which every gate would sit in a function nothing runs,
|
||||
which reads exactly like a passing contract.
|
||||
- **`quickshell/osd-model-contract` — RUN, PASS.** Gained the magnifier's OSD
|
||||
kind: `iconFor('zoom', 0.4)` is `zoom-in-symbolic` and `iconFor('zoom', 0)` is
|
||||
`zoom-original-symbolic`, since an empty bar is 1.00 ×, which is off rather
|
||||
than barely magnified. Without a mapping the kind falls through to its own
|
||||
name and the OSD draws the generic fallback glyph — for the one shortcut
|
||||
whose whole job is telling somebody who cannot read the screen what the
|
||||
magnification now is.
|
||||
- **No contract pinned the Theme duration literals.** Checked by grep across
|
||||
`tests/` for `durFast`/`durNormal`/`durSlow`/`durDockReveal` and
|
||||
`motionEnabled`: zero hits before the change, so gating them broke nothing.
|
||||
`accessibility-contract` now pins the gating itself, which is the first time
|
||||
those tokens have been pinned anywhere.
|
||||
- **`quickshell/settings-ownership-contract` — RUN, PASS unchanged.** Its
|
||||
duplicate table expects exactly `animationsEnabled`, `cursorInactiveTimeout`,
|
||||
`cursorSize` and `inactiveOpacity` to appear on two pages, and the rebuilt
|
||||
page keeps all four, so the mirror set did not move.
|
||||
|
||||
### Seven search entries added
|
||||
|
||||
Magnifier zoom, Zoom in and out, Reduce motion, Visual alerts, Screen reader,
|
||||
Orca, Sticky keys — all routing to `accessibility`. The schema already covers
|
||||
the switches by their own labels, so these are only the words people arrive
|
||||
with that no label uses: the verb rather than the noun ("zoom in", not
|
||||
"Magnifier"), the application's name rather than its category ("Orca", not
|
||||
"Screen reader"), and the schema's own wording gap ("Reduce motion", where the
|
||||
label reads "Animations"; "Visual alerts", where it reads "Flash the screen for
|
||||
notifications").
|
||||
|
||||
**High contrast was deliberately NOT added**, and the contract fails if it is:
|
||||
it is already a schema label, and the index covers every schema label, so a
|
||||
second copy would list the same setting twice in one result. The contract also
|
||||
fails if any of these entries routes anywhere but `accessibility`.
|
||||
|
||||
### Schema docs and launcher commands regenerated
|
||||
|
||||
`quickshell/scripts/panama-settings-docs` and `panama-settings-commands` both
|
||||
run without `--check` and their output committed. `docs/settings.md` goes 173 →
|
||||
**174 settings across 36 groups**: `visualAlerts` renders in the accessibility
|
||||
group, and `magnifierFactor` picks up its new `×` unit in both the default
|
||||
column ("1.0 ×") and the detail ("1.00 × is off"). `settings-accessibility`'s
|
||||
Vicinae keywords gained "flash the screen for notifications", "magnifier zoom",
|
||||
"zoom in and out", "reduce motion" and "visual alerts".
|
||||
`quickshell/settings-docs-contract` — RUN, PASS (174 settings documented);
|
||||
`quickshell/panama-commands-contract` — RUN, PASS (77 commands; the
|
||||
settings generator writes 38 of them).
|
||||
|
||||
### Deferred, and why
|
||||
|
||||
- **`quickshell/settings-pages-contract`, `settings-write-sweep-contract`,
|
||||
`settings-search-contract`, `settings-buttons-contract` (live half),
|
||||
`settings-preferences-contract`, `settings-commit-reset-contract`,
|
||||
`settings-system-contract`, `settings-hyprland-write-contract` — NOT RUN**:
|
||||
they daemonize a Quickshell harness or drive the live settings window. The
|
||||
Accessibility page was rebuilt this phase, `SettingsNote.qml` is new, and all
|
||||
nine shared row primitives changed, so this set is the end-of-redesign sweep
|
||||
and matters more here than it did in any previous phase — a broken
|
||||
`SliderRow` takes every settings page down, and only these would see it.
|
||||
`qmldir-registration-contract` (static) was run and passes, so
|
||||
`SettingsNote` at least resolves.
|
||||
- **The whole live half of the zoom keybinds.** Nothing here presses a key. The
|
||||
contract proves the binds exist, name the IPC verb, sit on unoccupied chords,
|
||||
and that the path behind them commits through the preference; whether
|
||||
Hyprland picks the new binds up needs a `hyprctl reload`, which this phase
|
||||
deliberately did not run.
|
||||
- **The flash has not been seen.** `VisualBell` is pinned structurally — one
|
||||
animation, no loops, no repeating timer, gated on the preference, not timed
|
||||
with a motion-gated token — but nobody has watched a notification arrive with
|
||||
Visual alerts on. That is the one assertion here a screenshot would settle
|
||||
and a grep cannot.
|
||||
- **Orca has not been started or stopped.** The service is pinned as a process
|
||||
probe with on-demand polling; whether `pgrep -x orca` matches the installed
|
||||
Orca is A's verified claim in a comment (it calls `set_process_name("orca")`,
|
||||
which renames `comm`), not something this contract re-checks.
|
||||
- **The screen reader has not read the page.** `Accessible.name`, `role` and
|
||||
`activeFocusOnTab` are present in all nine files and the key handlers are
|
||||
there, but no AT-SPI client has walked the settings window to confirm the
|
||||
names come out in a useful order. That needs Orca running against a live
|
||||
shell.
|
||||
- **`quickshell/sound-page-contract` fails, and it is not this work.** Verified
|
||||
by stashing the whole working tree and running it against a clean checkout:
|
||||
it fails identically ("DictationPage does not report whether the speech
|
||||
server is installed"), so it was already red before this phase began.
|
||||
- Run order for this phase: the static ones first (`accessibility-contract`,
|
||||
`osd-model-contract`, `preference-schema-contract`,
|
||||
`schema-hypr-shape-contract`, `settings-ownership-contract`,
|
||||
`search-routing-contract`, `ipc-targets-contract`, `settings-jump-contract`,
|
||||
`settings-docs-contract`, `readme-contract`, all eight of `tests/hypr/`),
|
||||
then the sandboxed-harness ones, then the live settings window last.
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
# Accessibility redesign — everything that's real
|
||||
|
||||
Approved mock: `home-mocks/accessibility.html` (scratchpad, :8642). Spec wins over mock on
|
||||
conflict. Tabless leaf stays.
|
||||
|
||||
## Goals
|
||||
|
||||
1. **Magnifier keybinds**: Super+= / Super+- / Super+0 step the zoom from anywhere, through the
|
||||
shell so the stored preference stays truthful and the OSD shows the level. Mechanism: the
|
||||
binds call `qs ipc call accessibility zoom in|out|reset`; a new IPC target routes to
|
||||
`Accessibility.stepZoom()`, which computes (×1.25 steps, clamp to the schema's 1.0–5.0),
|
||||
commits through the normal verified-preference path, and posts the OSD. No new script; a
|
||||
dead shell means dead zoom keys, which is honest (everything else in the shell is dead too).
|
||||
2. **Reduce motion becomes true**: `Theme.qml`'s `dur*` tokens gate on
|
||||
`Settings.animationsEnabled` (0 when off) so the bar/dock/OSD/panels genuinely still.
|
||||
3. **Visual alerts**: new schema key `visualAlerts` (bool, def false, group `accessibility`);
|
||||
when on, a one-shot screen-edge flash (per-screen overlay, single animation per event,
|
||||
never looping) fires wherever `Notifs.playBell` decides a notification sounds (including
|
||||
when the audible bell is skipped for `sound: false` apps? No — the flash follows the same
|
||||
eligibility as the bell EXCEPT the eventSounds gate: visual alerts are for people who can't
|
||||
hear the bell, so the flash fires on bell-eligible notifications even when event sounds are
|
||||
off. Pin that.)
|
||||
4. **Honest Orca control**: running state (pgrep poll on page open + after actions),
|
||||
Start/Stop; the a11y-bus readiness line. No gsettings screen-reader toggle (gsd-owned,
|
||||
inert — refused).
|
||||
5. **The primitives get accessible**: `SliderRow`, `ToggleRow`, `SwitchRow`, `ActionRow`,
|
||||
`ChoiceRow`, `SegmentRow`, `SettingRow` (activatable), `OptionPickerRow`, `PickerRow`
|
||||
gain `Accessible.role`/`Accessible.name` (from label; detail as description),
|
||||
`activeFocusOnTab` on the interactive element, key handling (Space/Enter activates,
|
||||
Left/Right steps sliders and segments), and a visible focus indicator (the titlebar
|
||||
close-button border pattern). No API or visual-at-rest changes.
|
||||
6. **Page rebuilt** per mock: Vision / Motion / Hearing / Keyboard & pointer / Screen reader;
|
||||
`dimStrength` gated on `dimInactive`; magnifier "1× is off" honesty (schema gains
|
||||
`unit: "×"`-style display via the page detail — fix the dead `zeroLabel`); the honesty box
|
||||
for sticky/slow/bounce with the probed-not-assumed copy; mono-audio NOT YET row; "Open
|
||||
Keyboard" jump.
|
||||
7. Cleanups: `DESKTOP-PARITY.md`'s stale AccessX claim corrected; search entries for
|
||||
magnifier/zoom/orca/screen reader/high contrast/reduce motion/visual alerts.
|
||||
|
||||
Non-goals: mono audio (deferred with the on-page honesty), sticky/slow/bounce/hover/mouse
|
||||
keys (refused with receipts), gsettings `enable-animations`/`screen-reader-enabled` writes
|
||||
(inert), shell text scaling.
|
||||
|
||||
## Ownership
|
||||
|
||||
- **A**: `config/PreferenceSchema.qml` (visualAlerts; magnifierFactor detail/unit fix),
|
||||
`config/dot/hypr/keybinds.lua` (three zoom binds — CHECK for collisions with existing
|
||||
Super+=/-/0 binds first; pick free chords and update the mock copy via B if needed),
|
||||
`shell.qml` (accessibility IPC target), `services/Accessibility.qml` (stepZoom, orcaRunning
|
||||
+ start/stop, poll discipline), `services/Notifs.qml` (flash trigger signal per goal 3),
|
||||
NEW `modules/notifications/VisualBell.qml` (+ per-screen wiring in shell.qml),
|
||||
`config/dot/hypr/DESKTOP-PARITY.md` (stale claim), `services/OsdState.qml`/`OsdModel.js`
|
||||
only if the zoom OSD needs a new kind (coordinate with C on osd-model-contract).
|
||||
- **B**: the nine row primitives (accessibility additions only — zero API/visual-at-rest
|
||||
change), `modules/settings/AccessibilityPage.qml` rebuild (+ any new components + qmldir).
|
||||
Reuse KeycapChord for the chord display.
|
||||
- **C**: `services/SettingsSearch.qml`, NEW `tests/quickshell/accessibility-contract` (page
|
||||
structure; zoom IPC path commits through the verified-preference path and never calls
|
||||
hyprctl keyword; visual bell one-shot — no `loops: Animation.Infinite`, no Timer-driven
|
||||
repeat; flash follows bell eligibility but not the eventSounds gate; primitives carry
|
||||
Accessible.name + activeFocusOnTab — sweep all nine files; Orca control uses process state,
|
||||
never gsettings), `osd-model-contract` (zoom kind if added), reconcile any contract pinning
|
||||
Theme durations or the primitives' file contents, backlog Phase 14, README count
|
||||
(174 → 175 expected), docs/commands regen at the end (schema changed).
|
||||
|
||||
Hard rules: no live mutations (no zoom application, no orca start/stop, no gsettings writes,
|
||||
no hyprctl keyword/reload); hermetic stubs only; valid QML at every save (primitives are used
|
||||
by EVERY page — a broken SliderRow takes the whole settings app down, so B edits them one at
|
||||
a time with a reload-check between each). B programs against A's pinned APIs; A updates this
|
||||
spec before changing them.
|
||||
Reference in New Issue
Block a user