Watch
1
0
Fork
You've already forked SouveraineOS
0

tasks 37-39: back verb, pill swipe osk swap, audio levels as state

This commit is contained in:
Fimeg 2026-07-27 09:29:10 -04:00
commit 4973393736
3 changed files with 263 additions and 0 deletions

View 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).

View 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).

View 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).