20 KiB
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.displaysowns 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:
application.idapplication.process.binaryapplication.name- 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:
{
"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:
- Validates every output, offered mode, clean scale, transform, integer coordinate, and the single-primary invariant.
- Captures the complete current connected layout.
- Applies the complete requested layout through one generated, allow-listed Hyprland Lua evaluation.
- Reads all monitors back and enables Keep only when every field matches.
- Reverts the complete captured layout after 15 seconds unless confirmed.
- Persists only the verified requested layout when Keep is pressed.
- 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:
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.
runfalling 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, orper-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
wallpaperPathto every output. - Per monitor applies
wallpaperPerMonitor[output], falling back towallpaperPathwhen 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:
- Restore and validate preference files.
- Apply the protected confirmed display layout.
- Regenerate idle and lock configuration.
- Apply wallpaper policy.
- 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 generateagainst 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-configonce. - 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:
- Application mixer — event-driven and lowest risk.
- Generated lock appearance — establishes the safe state-file pattern.
- Wallpaper policy — reuses that state discipline and extends existing IPC.
- 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.