Design Phase 2 expectation gaps
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user