diff --git a/docs/superpowers/specs/2026-08-18-phase2-expectation-gaps-design.md b/docs/superpowers/specs/2026-08-18-phase2-expectation-gaps-design.md new file mode 100644 index 0000000..057214b --- /dev/null +++ b/docs/superpowers/specs/2026-08-18-phase2-expectation-gaps-design.md @@ -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": "4500x3000@60.00", + "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.