tasks 37-39: back verb, pill swipe osk swap, audio levels as state
This commit is contained in:
parent
ed21db0f44
commit
4973393736
3 changed files with 263 additions and 0 deletions
113
docs/tasks/37-navigation-back-verb.md
Normal file
113
docs/tasks/37-navigation-back-verb.md
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
# TASK 37 — "Back" as a first-class verb
|
||||
|
||||
**Status:** open, opened 2026-07-27. Prompted by @wiredwireless's `xdg_navigation`
|
||||
sketch (Matrix, 2026-07-24) and Casey's ask to work out how it fits here.
|
||||
**Size:** our half is small and worth doing now; the protocol half is upstream's
|
||||
problem and should not block us.
|
||||
|
||||
## The outside idea, in one line
|
||||
|
||||
An app declares "my current view has somewhere to go back to"; the compositor
|
||||
decides how the user expresses "back" (edge swipe, key, mouse button 8,
|
||||
accessibility) and invokes it. Later: gesture *progress* with commit/cancel, so
|
||||
the app can drag the outgoing page as Android's predictive back does.
|
||||
|
||||
## Verdict: the concept is sound. The framing needs work before it ships anywhere.
|
||||
|
||||
The idea is right and the pieces exist. Three things would decide whether it
|
||||
survives contact with freedesktop, and they are worth getting right regardless
|
||||
of whether we ever send it upstream.
|
||||
|
||||
**1. The killer objection is "just send `XF86Back`."** Apps already handle that
|
||||
keysym and mouse button 8. A compositor can synthesise it today, with no
|
||||
protocol at all. Any proposal that leads with the discrete `back()` call walks
|
||||
straight into this and loses.
|
||||
|
||||
**2. The answer is `can_go_back` — the state channel, not the verb.** A
|
||||
synthesised key is fire-and-forget: the compositor learns nothing about whether
|
||||
anything happened. It therefore cannot decide what to do when the app has
|
||||
nowhere to go — close the window? switch workspace? nothing? — so in practice
|
||||
it does nothing, and "back" is unreliable, which is exactly why it never caught
|
||||
on. Declared availability is the new capability, and it is what lets the
|
||||
compositor own the *fallback* policy while the app owns only its own stack.
|
||||
|
||||
Android is the precedent, and it cuts in this proposal's favour: `onBackPressed()`
|
||||
was the key-shaped model, and Android 13 replaced it with `OnBackInvokedCallback`
|
||||
— a *registered* callback with declared availability — precisely because the
|
||||
key model could not express delegation or predictive back. The platform being
|
||||
cited already tried the cheap version and migrated off it.
|
||||
|
||||
**3. Lead with predictive back, not with back.** Progress + commit + cancel
|
||||
cannot ride a key event under any encoding. That makes it a mechanism argument
|
||||
("here is a thing input events structurally cannot do") rather than a policy
|
||||
argument ("here is a nicer way to do a thing you can already do"). Discrete
|
||||
back then falls out as the degenerate case: progress 0 → 1 in one step.
|
||||
Mechanism arguments are the register Wayland review accepts; policy arguments
|
||||
are the ones that summon the metaphysics thread.
|
||||
|
||||
### Two design questions to answer before posting, not after
|
||||
|
||||
- **What happens at the root**, when `can_go_back` is false. This is where the
|
||||
"what does back *mean*" fight actually lives, and the design already answers
|
||||
it — the compositor knows, so the compositor decides (close / switch / do
|
||||
nothing), and no app ever needs an opinion. Saying so up front removes a
|
||||
whole class of objection.
|
||||
- **Scope.** One verb, one state, one progress channel. No `forward`, no `up`,
|
||||
no `home`. Each extra verb multiplies the semantic argument and none of them
|
||||
have back's near-universal agreement. Also: this belongs on `xdg_toplevel`
|
||||
(precedent: xdg-dialog, xdg-toplevel-icon), not on bare `wl_surface`, and
|
||||
dialogs/popups need a stated answer.
|
||||
|
||||
## How it fits here — we have already built the compositor half, twice
|
||||
|
||||
This is not an analogy; the shapes line up exactly.
|
||||
|
||||
- **`services/Gestures.qml` is the routing table in the sketch.** It already
|
||||
maps gesture → named semantic action (`squeezeAction`, `edgeAction`, a
|
||||
`bind(gesture, action)` IPC) rather than gesture → hardcoded behaviour, which
|
||||
`INTERFACE-ARCHITECTURE` §4 requires of every input. Adding `back` as an
|
||||
action is one table entry.
|
||||
- **`can_go_back` is the dial's `enabled`, and its refusal reason is the dial's
|
||||
`reason`.** TASK-30's verb tables and TASK-31's radial dial already carry
|
||||
"this verb exists but is not available right now, and here is why, in the
|
||||
words the refusal used". A back verb is that shape with a one-bit state.
|
||||
Whatever we build for back should feed the same table, or we will have three
|
||||
places that enumerate capabilities instead of one — the exact drift TASK-31
|
||||
was written to prevent.
|
||||
|
||||
**What we do not have is the app half**, and that is the whole point of doing
|
||||
this ourselves: our first-party apps are ours. culver already pushes a stack on
|
||||
narrow layouts (contacts, conversation) and pops it; souveraine-settings is a
|
||||
page tree; the player has views. Every one of them already knows its own
|
||||
answer to "where does back go" and has no way to say so.
|
||||
|
||||
## The move
|
||||
|
||||
Implement the *semantics* over our own IPC now, shaped like the protocol so a
|
||||
later swap is a transport change and not a redesign:
|
||||
|
||||
1. A `navigation` service in the shell holding, per focused app: `canGoBack`,
|
||||
and `back()` / `backProgress(f)` / `backCommit()` / `backCancel()`.
|
||||
2. First-party apps report their stack depth into it. culver first — it is the
|
||||
app where the missing gesture is felt daily.
|
||||
3. `back` becomes a gesture-table action like any other (TASK-13's squeeze,
|
||||
the pill, an edge swipe), never a hardcoded binding.
|
||||
4. Predictive back last, and only after the discrete version feels right.
|
||||
|
||||
**Third-party apps** (Firefox, GTK apps) cannot participate and will not until
|
||||
something lands upstream. They get the legacy `XF86Back` key. This is a
|
||||
*declared two-tier policy*, not a silent fallback: the compositor knows exactly
|
||||
which tier a surface is in, and in tier 2 it does not claim to know whether
|
||||
anything happened. It must never present tier-2 behaviour as if it were tier 1.
|
||||
|
||||
The by-product matters as much as the feature: doing this first means that if
|
||||
the idea does go to freedesktop, it arrives as a working implementation with a
|
||||
device behind it, not as a proposal. That is the difference between being
|
||||
argued with and being eaten.
|
||||
|
||||
## Connects to
|
||||
|
||||
TASK-30 (verb tables — back is a verb and belongs in them), TASK-31 (the dial —
|
||||
same enabled/reason shape, same routing table), TASK-13 (squeeze — another
|
||||
gesture wanting a semantic destination), `INTERFACE-ARCHITECTURE.md` §4,
|
||||
TASK-32 (pill and OSK as apps — the pill is one place a back gesture lands).
|
||||
65
docs/tasks/38-pill-swipe-osk-swap.md
Normal file
65
docs/tasks/38-pill-swipe-osk-swap.md
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
# TASK 38 — Pill swipe-to-max swaps the keyboard
|
||||
|
||||
**Status:** open, 2026-07-27 (Casey). **Size:** micro. **Repo:** `souveraine`
|
||||
(`surfaces/quickshell`) + `Pixel3Arch` (osk-switch).
|
||||
|
||||
## What Casey asked for
|
||||
|
||||
When the keyboard is out, an *additional* swipe up on the nav pill — past the
|
||||
point where it changes to the pink square, i.e. the existing swipe-to-maximum
|
||||
detent — swaps the on-screen keyboard to squeekboard: the smaller, denser,
|
||||
terminal-style layout with arrows and fn.
|
||||
|
||||
## Why this exists now
|
||||
|
||||
stevia 0.56.0-9 (2026-07-27) dropped its terminal layout: the layout-menu entry
|
||||
and the `POS_INPUT_METHOD_PURPOSE_TERMINAL` auto-switch are both gone, so
|
||||
opening a terminal no longer pulls up a second layout inside stevia. The split
|
||||
is now clean and deliberate:
|
||||
|
||||
- **stevia** — the daily keyboard. Word completion, dictation mic.
|
||||
- **squeekboard** — the terminal keyboard. Arrows, fn, dense grid.
|
||||
|
||||
Until this task lands there is **no way to reach squeekboard by gesture**, so
|
||||
terminals get the daily layout. That is the gap this closes, and it is the
|
||||
reason the terminal drop and this task belong to each other.
|
||||
|
||||
## What already exists
|
||||
|
||||
- `/usr/local/bin/osk-switch` toggles which OSK runs. Both packages are
|
||||
installed and co-installable (stevia has no `conflicts`, both keep
|
||||
`provides=(phosh-osk-provider)`, neither ships `mobi.phosh.OSK.service`); the
|
||||
rootfs overlay owns `stevia.service` + `squeekboard.service` as static units
|
||||
and only one claims `sm.puri.OSK0` at a time.
|
||||
- The pill already has a swipe-to-maximum detent with its own visual state (the
|
||||
pink square) — the gesture and its feedback exist, only the extra step and
|
||||
its action are missing.
|
||||
- `services/Gestures.qml` is the routing table. Per `INTERFACE-ARCHITECTURE`
|
||||
§4 this must register there as a named action, **not** as a hardcoded pill
|
||||
binding.
|
||||
|
||||
## Shape
|
||||
|
||||
1. A named action (`oskSwap` or similar) in the gesture table that calls
|
||||
osk-switch.
|
||||
2. The pill's swipe-to-max gains a further detent, live only while an OSK is
|
||||
visible, that invokes it.
|
||||
3. Haptic detent on the extra step, same as the dial's ring — the gesture has
|
||||
to be findable without looking, which is the whole argument for it.
|
||||
4. Swapping while a text field is focused must not lose focus or the field's
|
||||
contents.
|
||||
|
||||
## Watch for
|
||||
|
||||
- **`osk-switch` lives in `/usr/local/bin` and is owned by no package** —
|
||||
exactly what TASK-25 exists to stop. If this task starts depending on it,
|
||||
package it first or the dependency is unowned.
|
||||
- Do not hardcode the pill→osk-switch call. It goes through the table.
|
||||
- The swap is not instant (a unit stops, another starts). Decide what the
|
||||
surface does in the gap rather than letting it flicker.
|
||||
|
||||
## Connects to
|
||||
|
||||
TASK-24 (the keyboard layout work that produced the stevia/squeekboard split),
|
||||
TASK-32 (pill and OSK as apps), TASK-25 (packaging osk-switch),
|
||||
`INTERFACE-ARCHITECTURE.md` §4, TASK-37 (the other gesture wanting a home).
|
||||
85
docs/tasks/39-audio-levels-as-state.md
Normal file
85
docs/tasks/39-audio-levels-as-state.md
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
# TASK 39 — Audio levels as tracked state, and sound roles
|
||||
|
||||
**Status:** open, 2026-07-27 (Casey, explicitly "for later unless it folds
|
||||
right in"). **Size:** decide the model first; the persistence is small once the
|
||||
model is right. **Repo:** `souveraine` (sessiond + surfaces), `Pixel3Arch`.
|
||||
|
||||
## What Casey asked
|
||||
|
||||
> "we now are monitoring / tracking the brightness before shutdown/reboots but
|
||||
> we should probably add sound levels to that; or at least consider the
|
||||
> approaches later on how we might separate some notification level sounds vs
|
||||
> others. Frankly most times I think the phone should be expected to have audio
|
||||
> up to around 70%; but I may be errant."
|
||||
|
||||
## Check this before building anything
|
||||
|
||||
**WirePlumber already restores stream and device volumes across boots** (its
|
||||
`restore-stream` / `restore-device` state under `~/.local/state/wireplumber`).
|
||||
If volume is not coming back the way Casey expects, the bug is plausibly there
|
||||
— a state file not being written, or a device whose name changes between boots
|
||||
so the restore never matches. Building a second persistence layer on top of a
|
||||
first one that is silently failing would produce two writers for one value,
|
||||
which is the same shape as the hypridle/sessiond competing-writer bug that cost
|
||||
the 2026-07-25 session.
|
||||
|
||||
So: confirm what WirePlumber is and is not restoring on this device **first**,
|
||||
and only then decide whether sessiond needs to own anything here.
|
||||
|
||||
## The real question is not persistence, it is the stream model
|
||||
|
||||
"Sound level" is not one number, and choosing to persist it as one number bakes
|
||||
in a model we would then have to undo. Every phone platform splits audio by
|
||||
**role**, because a single slider cannot express the thing people actually
|
||||
want — ringer loud while media is quiet, or media loud while notifications stay
|
||||
out of the way. Android's stream split (music / notification / ring / alarm /
|
||||
voice call) is the reference, the same way it was for TASK-37.
|
||||
|
||||
PipeWire already carries the mechanism: per-stream volumes plus
|
||||
`media.role` / `media.category` on the stream. What is missing is *our policy* —
|
||||
which roles exist on this device, what each defaults to, and which ones a
|
||||
volume gesture actually moves.
|
||||
|
||||
Casey's "notification level sounds vs others" is exactly this split, and it is
|
||||
the part worth designing. The persistence falls out of it: you restore per
|
||||
role, not one master.
|
||||
|
||||
## This touches AUDIO-PRIVACY, not just ergonomics
|
||||
|
||||
Which sounds are audible **while the screen is locked** is not a volume
|
||||
question, it is the same tier question as visual redaction: a notification that
|
||||
speaks its contents aloud on a locked phone leaks exactly what the lockscreen
|
||||
redaction rules exist to prevent. Any role model here has to state what each
|
||||
role does at each lock tier, or we will have solved audibility and reopened the
|
||||
leak we already closed on the display side.
|
||||
|
||||
## On the 70%
|
||||
|
||||
Worth measuring rather than adopting. Two reasons to be careful:
|
||||
|
||||
- Volume scales are perceptual, not linear — "70%" on a slider is not 70% of
|
||||
loudness, and the mapping differs between the PipeWire volume, the UI slider,
|
||||
and the codec's own gain. A number picked in one of those three does not mean
|
||||
the same thing in the others.
|
||||
- The relevant default is probably per role, not global. A 70% that is right
|
||||
for media is very likely wrong for a ringtone that has to be heard from
|
||||
another room.
|
||||
|
||||
Casey flagged he may be errant here, and the honest answer is that the number
|
||||
should come out of the role model, not go into it.
|
||||
|
||||
## Shape, once the above is settled
|
||||
|
||||
1. Confirm what WirePlumber restores today. Fix it there if that is the bug.
|
||||
2. Define the roles this device has and each one's default and lock-tier
|
||||
behaviour.
|
||||
3. Decide who owns the value. Default to WirePlumber; sessiond only takes it if
|
||||
there is a device-state reason (there may not be — volume is preference, not
|
||||
the security-relevant device state the state machine was built for).
|
||||
4. Volume gestures and the settings UI move roles, not a master number.
|
||||
|
||||
## Connects to
|
||||
|
||||
`AUDIO-PRIVACY.md` (the lock-tier half), `SETTINGS-AUTHORITY.md`,
|
||||
TASK-08 (device state manager — and the question of whether audio belongs in
|
||||
it at all), TASK-19 (settings/control centre — where roles would surface).
|
||||
Loading…
Reference in a new issue