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