Design Phase 2 expectation gaps

This commit is contained in:
Gabriel Brown
2026-08-18 15:00:18 -04:00
parent 1360a80f07
commit bfa9c58b09
@@ -0,0 +1,436 @@
# Phase 2: Close the Expectation Gaps
## Purpose
Phase 2 completes four ordinary desktop capabilities that are conspicuous when
they are absent: per-application volume, multi-monitor arrangement, lock-screen
appearance, and wallpaper automation. The goal is not to make Panama broader;
it is to make the parts people expect in their first week feel native, safe,
and complete.
The approved visual direction is **A — Continuity**. New controls use Panama's
existing two-pane Settings window, restrained cards, Prism selection marks, and
Tokyo Night Moon tokens. The monitor canvas and lock-screen preview are the only
new visual forms. There is no inspector sidebar, dashboard-within-a-dashboard,
or new navigation level.
## Product boundaries
Phase 2 includes:
- Live volume and mute controls grouped by playback application.
- Positioning connected displays, selecting Panama's primary display, and
preserving the existing mode, scale, rotation, confirmation, and rollback
behavior.
- Choosing the lock-screen background treatment and visibility of its clock,
date, user label, and idle password field.
- Single-image, slideshow, and per-monitor wallpaper modes, including an
explicit slideshow collection, shuffle, and interval.
It does not include audio routing between applications, equalizers, PipeWire
profiles, disabling displays, display mirroring, full-time HDR, animated
wallpapers, per-workspace wallpapers, a lock-screen plugin system, or arbitrary
hyprlock configuration. Those would expand four expectation-closing features
into four general-purpose configuration tools.
Phase 3 remains responsible for Control Center mirrors and contextual
"configure this" affordances. Phase 4 remains responsible for user accents and
named themes.
## Shared principles
Each area has one source of truth:
- PipeWire owns live stream volume. Panama does not persist ephemeral streams.
- `DesktopPreferences.displays` owns confirmed display layout overrides.
- Panama's schema owns lock appearance and generates the effective hyprlock
file under the user state directory.
- Panama's schema owns wallpaper policy; hyprpaper owns the pixels currently
displayed.
Settings components call typed service methods. They never construct shell
commands. External writes use argument arrays, validate user-authored values,
and verify observable state where the target exposes readback.
No feature adds a continuous animation or short polling loop. PipeWire and the
screen model are event-driven. Wallpaper rotation wakes only at its configured
interval. Lock configuration regenerates only after a relevant preference
change. Display reads happen when the page opens, after an operation, and when
the connected-screen set changes.
## Application volume
### State and grouping
`services/AudioDevices.qml` gains a playback-stream view derived from
`Pipewire.nodes.values`. A playback stream is a ready `PwNode` whose type
contains `PwNodeType.AudioOutStream`, has an audio interface, and is not a
physical device.
Streams are grouped into applications using the first non-empty stable identity
from PipeWire's properties:
1. `application.id`
2. `application.process.binary`
3. `application.name`
4. the node ID as a final per-stream fallback
The display label prefers `application.name`, then `node.description`,
`media.name`, and finally **Unknown application**. The icon prefers
`application.icon_name`; the delegate falls back to a generic audio-application
symbolic icon. Property values are presentation data only and never become
commands or file paths.
One application row may own several simultaneous streams. Its displayed volume
is the arithmetic mean of the tracked stream volumes. Moving the row writes the
same requested level to every stream in the group. The row is muted only when
every stream is muted; pressing mute applies one state to all streams. Moving a
slider always unmutes every stream, matching the existing device controls.
New streams appear and closed streams disappear through PipeWire's node model.
There is no saved per-application volume map: persisting a browser tab or media
session identity would restore stale state to unrelated future streams.
### Settings experience
Sound gains an **Applications** card beneath Output and Input. Each active
playback application has an icon, application name, optional stream-count or
media detail, mute button, slider, and percentage. Rows remain stable while a
stream's properties update.
When nothing is playing, the card remains visible with the quiet direction:
**Applications playing sound will appear here.** PipeWire discovery failure is
distinct and directs the user to System Health.
The existing Fedora handoff becomes **Device profiles**. Panama now owns
application volume, while Fedora's panel remains the advanced route for codec
and hardware profile selection.
### Failure behavior
- A node that disappears during a drag is ignored without affecting surviving
streams in the group.
- A stream without writable audio state remains visible as unavailable rather
than crashing the model.
- Mixed mute state is represented as unmuted; the next explicit mute action
makes the group consistent.
- No stream metadata is written to logs or the settings store.
## Multi-monitor arrangement
### Persisted layout
The existing `displays` JSON remains the single preference and keeps its
backward-compatible per-output shape. Confirmed entries gain three fields:
```json
{
"DP-2": {
"mode": "[email protected]",
"scale": 1.5,
"transform": 0,
"x": 0,
"y": 0,
"primary": true
}
}
```
Old entries without `x`, `y`, or `primary` remain valid. They use automatic
placement until the first confirmed arrangement. Exactly one connected output
is primary in a confirmed multi-monitor layout. Single-monitor layouts make the
only output primary automatically.
Wayland has no universal primary-display protocol. **Primary** is therefore an
honest Panama role: it anchors the saved coordinate system at logical `0,0` and
is listed first in Panama's display and per-monitor wallpaper selectors.
Panama's current overlays continue following the focused monitor; this phase
does not silently move them. The role does not claim to force arbitrary
third-party Wayland applications to open on a particular output.
Positions are integer logical pixels after scale and rotation. Before apply,
the service normalizes every coordinate relative to the selected primary
display. Negative coordinates are allowed for displays physically left of or
above the primary.
### Apply, verify, and rollback
`services/Displays.qml` moves from a one-output pending record to a whole-layout
transaction. A request contains every connected output's mode, scale,
transform, position, and primary flag.
The service:
1. Validates every output, offered mode, clean scale, transform, integer
coordinate, and the single-primary invariant.
2. Captures the complete current connected layout.
3. Applies the complete requested layout through one generated, allow-listed
Hyprland Lua evaluation.
4. Reads all monitors back and enables **Keep** only when every field matches.
5. Reverts the complete captured layout after 15 seconds unless confirmed.
6. Persists only the verified requested layout when **Keep** is pressed.
7. Reads the reverted layout back and reports if restoration cannot be proven.
A disconnected output invalidates an in-flight transaction and triggers
rollback for every still-connected output. A newly connected output receives
automatic placement; the connected-screen event refreshes the page, but the
new geometry is not persisted until the user confirms a layout. Stored
overrides for disconnected outputs are retained for reconnect, but never
participate in a live transaction while absent.
`hypr/monitors.lua` validates and replays the extended entries at startup. The
shipped DP-2 bit depth and color-management policy remain authoritative and are
not moved into user preferences.
### Settings experience
Displays gains an **Arrangement** card above the selected-display controls when
more than one monitor is connected. The canvas scales the complete logical
desktop into its available area while preserving real aspect ratios. Monitor
tiles show the human display name and connector. The selected tile gets the
Prism outline; the primary tile also receives a restrained **Primary** label.
Dragging moves a tile and snaps nearby edges. The change remains a draft until
pointer release, when the normal 15-second confirmation transaction begins.
Focused tiles also support arrow-key movement, with Shift for larger steps, so
arrangement is not pointer-only. **Make primary** normalizes the draft around
that display and enters the same confirmation flow.
**Identify** briefly draws a static numbered overlay on every connected screen.
The overlay has no repeating animation and dismisses itself after three seconds.
The existing resolution, scale, rotation, DDC brightness, Night Light, and
gaming-policy cards remain. Display selection now follows selection in the
arrangement canvas but still has a compact textual selector for narrow layouts.
## Lock-screen appearance
### Generated configuration
The tracked `hyprlock.conf` remains the documented shipped fallback. A new
`scripts/panama-lock` helper generates the effective configuration at
`$XDG_STATE_HOME/panama/hyprlock.conf` and launches:
```text
hyprlock -c $XDG_STATE_HOME/panama/hyprlock.conf
```
The helper supports `generate`, `run`, and `status`. It reads only validated
schema values, writes through a temporary file followed by an atomic rename,
and never edits the repository symlink under `~/.config/hypr`.
Both the shipped and Panama-managed hypridle configurations use
`pidof hyprlock || panama-lock run` as `lock_cmd`. Existing `loginctl
lock-session` actions remain unchanged; hypridle receives the session lock
request and invokes the configured locker. If generation fails, `run` falls
back to the tracked `hyprlock.conf` rather than leaving the session unlocked.
Relevant preference changes coalesce into one regeneration. They never restart
or mutate a lock screen that is already active; the next lock uses the new
file.
### User-facing settings
Appearance owns a **Lock screen** card because these choices are visual. Power
continues to own blank, lock, and suspend timing, and Privacy continues to mirror
the two established security controls.
The card exposes:
- **Background**: blurred desktop, current wallpaper, or solid theme color.
- **Background blur**: Off through Strong, stored as a small integer level and
mapped by the generator to bounded blur passes and size.
- **Show clock**, **Show date**, and **Show user name**.
- **Hide password field until typing**, mapped to hyprlock's empty-field fade.
The clock follows the existing global 12/24-hour preference; there is no second
clock-format setting. Current-wallpaper mode resolves the effective wallpaper
for each monitor, with the shipped image as fallback. Solid mode uses the
current light/dark scheme's background role. User-authored markup, commands,
fonts, and arbitrary paths are not accepted.
An inline preview uses ordinary QML and the current Theme tokens. It shows the
chosen visibility and background treatment but does not start hyprlock or
capture the desktop. The preview is explicitly representative, not a second
renderer that promises pixel identity with hyprlock.
### Failure behavior
- Missing or malformed preferences use shipped defaults.
- An unavailable selected wallpaper falls back to the shipped image and reports
the fallback in Settings.
- Generation failure leaves the last valid generated file in place.
- `run` falling back to the tracked config is logged as a bounded diagnostic
status and remains visible in System Health.
- Lock authentication and PAM configuration are never made adjustable.
## Wallpaper modes
### Preference model
The existing `wallpaperPath` remains the single-image choice and migration
fallback. The schema adds:
- `wallpaperMode`: `single`, `slideshow`, or `per-monitor`.
- `wallpaperSlideshowPaths`: a validated JSON array of absolute image paths.
- `wallpaperIntervalMinutes`: an integer from 5 to 1,440.
- `wallpaperShuffle`: a boolean.
- `wallpaperPerMonitor`: a validated JSON object from connector name to image
path.
Path validation retains the existing absolute-path, no-comma, no-newline rule
because hyprpaper receives `output,path` as one IPC argument. The service also
requires a selected path to exist in its scanned image candidates before a UI
action stores it. Hand-edited missing paths are tolerated at load and skipped
with a visible warning.
### Runtime policy
`services/Wallpaper.qml` owns one effective path per connected output and
parses `hyprpaper listactive` into an output-to-path map.
- **Single** applies `wallpaperPath` to every output.
- **Per monitor** applies `wallpaperPerMonitor[output]`, falling back to
`wallpaperPath` when an output has no assignment.
- **Slideshow** rotates the selected collection on every output. Sequential
mode advances in collection order. Shuffle mode uses a shuffled in-memory bag
and does not repeat an image until every valid selected image has appeared.
The slideshow timer wakes only at the configured minute interval. It does not
rewrite `wallpaperPath` on every rotation. Its current item and shuffle bag are
runtime state; the policy and collection are the durable state.
Every manual policy change applies all connected outputs, reads `listactive`
back, and persists only after the expected output map matches. An automatic
slideshow transition keeps the previous policy on failure, reports the problem,
and retries at the next interval rather than entering a rapid retry loop.
When the connected-screen set changes, Wallpaper reapplies the current policy
to the new set after a short coalescing delay. There is no generic filesystem
watcher: Rescan remains explicit, and startup performs the existing bounded
scan.
### Settings experience
Appearance keeps the current thumbnail-first wallpaper card. A compact mode
control sits above the grid:
- In **Single**, tapping a tile immediately applies it everywhere.
- In **Slideshow**, tapping toggles membership in the collection. Interval and
shuffle controls appear beneath the mode row. The active image still receives
the Prism outline, while selected collection members receive a quieter
checkmark treatment.
- In **Per monitor**, a connected-display selector appears above the grid and
tapping assigns the tile to that output. Each output's current assignment is
named in the card summary.
Mode-specific controls disappear when irrelevant; the grid itself does not
change size or become a nested settings panel. Empty slideshow collections
explain how to select images and do not start a timer. A collection containing
one valid image behaves as a static background and says so.
## Search, reset, backup, and ownership
Search routes application volume to Sound, arrangement and primary display to
Displays, lock appearance to Appearance, and wallpaper automation to
Appearance. Lock timing continues to route to Power.
Schema-backed lock and wallpaper values participate automatically in reset and
snapshots. The extended `displays` value remains protected by the existing
display-restore transaction during snapshot restore. Restore order is:
1. Restore and validate preference files.
2. Apply the protected confirmed display layout.
3. Regenerate idle and lock configuration.
4. Apply wallpaper policy.
5. Reload the shell only after those operations settle or reach their bounded
failure state.
Reset returns to the shipped static wallpaper, blurred screenshot lock screen,
and shipped/automatic display layout. It never changes physical audio stream
volumes because those are not preferences.
The Settings ownership ledger is updated only for new search groups and links;
none of these controls creates a new cross-page mirror.
## Error presentation
Errors stay on the page and name the failed boundary: **PipeWire is
unavailable**, **The display layout could not be verified**, **The lock-screen
configuration could not be generated**, or **Hyprpaper did not apply that
background**. There is no generic "Something went wrong."
Previous valid state remains visible during refresh and apply. Busy controls
disable only the operation they conflict with. Display confirmation remains
pinned above the page because it is the only time-sensitive state in Phase 2.
## Verification strategy
Tests are written before each production slice and use real service behavior at
the narrowest safe boundary.
### Application audio
- Construct complete PipeWire-shaped stream fixtures and prove grouping,
labeling, average volume, mute normalization, and disappearing-node behavior.
- Render the real application mixer with multiple streams, missing metadata,
an empty stream list, and unavailable PipeWire state.
- Perform one final live read against current streams; it does not start audio
or alter any stream unless a disposable test stream is available.
### Displays
- Exercise layout normalization, edge snapping, validation, persistence, and
startup replay with literal multi-monitor fixtures.
- Prove confirmation remains disabled until every output matches.
- Prove timeout, explicit revert, apply failure, disconnect, and failed-revert
paths restore the complete original layout.
- Keep development tests static. Run the existing live single-monitor display
contract once at feature completion. Do not invent a second physical display
in the live compositor; multi-monitor behavior is tested through controlled
compositor fixtures.
### Lock screen
- Run `panama-lock generate` against isolated settings, state, and wallpaper
fixtures and compare parsed hyprlock values, not source-text fragments.
- Prove every background mode, visibility control, global clock format, invalid
input fallback, atomic replacement, last-good preservation, and run fallback.
- Validate generated configuration with hyprlock's config checker when the
installed version exposes one; otherwise run a parser-only disposable launch
that cannot acquire the live session lock.
- Never activate the live lock screen automatically during tests.
### Wallpaper
- Use a fake hyprpaper IPC boundary and real temporary image files to prove
output maps, verification-before-persist, fallback, collection validation,
sequential order, shuffle-without-repeat, and interval clamping.
- Prove automatic failures do not spin or rewrite policy.
- Perform one final live no-op readback against the already active wallpaper.
Do not cycle the user's desktop through test images.
### Consolidated completion gate
- Run all new contracts and the existing Settings, Sound, Displays, backup,
ownership, migration, schema, and Hyprland configuration contracts once.
- Construct every changed QML surface without warnings in isolated harnesses.
- Run `Hyprland --verify-config` once.
- Restart Quickshell once after merge, inspect the fresh log, and visually
review the four finished experiences. Avoid repeated windows, reloads, and
live display changes during development.
## Delivery order
The phase is implemented as four independently green slices on one feature
branch:
1. Application mixer — event-driven and lowest risk.
2. Generated lock appearance — establishes the safe state-file pattern.
3. Wallpaper policy — reuses that state discipline and extends existing IPC.
4. Display arrangement — highest-risk slice, implemented after the supporting
Settings patterns and contracts are settled.
Each slice receives a focused commit after its contracts pass. The branch is
merged only when the consolidated completion gate passes. The visual mock is a
design reference, not a production dependency and is not committed.