feat: the streamed screen is a persisted setting, pickable from the console

P4 of design/per-monitor-portal-capture.md. The pin was an env var read
once at startup, which a console picker can never write — so it becomes a
field of the display policy (orthogonal to presets, like game_session), and
`vdisplay::capture_monitor()` resolves env-over-policy: an appliance that
pinned in its unit's host.env stays pinned there, and a console click
cannot re-aim a machine whose operator already declared the answer.

Read per `open` rather than cached, so a picker change takes effect on the
next session instead of the next host restart. The host re-resolves the
input anchor on every policy write — including clearing it when the pin is
cleared, or a later virtual-display session inherits an anchor aimed at a
monitor it is not showing.

`with_manual_layout` carries the pin through like the other orthogonal
axes: without that, saving a display ARRANGEMENT would silently swap the
streamed screen back to virtual.

Console: a "Streamed screen" card listing the host's real monitors beside
"Virtual screen (default)", saving on selection. A disabled head is listed
but not selectable (so "why isn't it here?" has an answer), and an
env-pinned host renders read-only with the reason rather than offering
controls that silently lose to the environment.

Verified on GNOME/Mutter: pinning via the policy file alone (no env) routes
sessions to the mirror and anchors input; setting the env to a different
connector overrides it, exactly as documented.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-27 23:51:41 +02:00
co-authored by Claude Opus 5
parent 358cfa4be4
commit d461d889c3
10 changed files with 1291 additions and 893 deletions
+14 -1
View File
@@ -88,6 +88,9 @@ pub(crate) fn display_settings_state() -> DisplaySettingsState {
// (`vdisplay/windows/manager.rs`); stored-but-inert elsewhere.
"ddc_power_off".into(),
"pnp_disable_monitors".into(),
// Linux-only in effect: routes every session to the mirror backend
// (design/per-monitor-portal-capture.md).
"capture_monitor".into(),
],
}
}
@@ -141,6 +144,10 @@ pub(crate) async fn set_display_settings(
);
}
tracing::info!("management API: display policy updated");
// The policy carries the capture-monitor pin, so a picker change must re-aim absolute input now
// rather than at the next host restart — and must clear the anchor when the pin is cleared.
#[cfg(target_os = "linux")]
crate::refresh_capture_monitor_anchor("display policy updated");
Json(display_settings_state()).into_response()
}
@@ -237,7 +244,12 @@ pub(crate) struct MonitorsResponse {
)
)]
pub(crate) async fn get_display_monitors() -> Json<MonitorsResponse> {
let pinned = pf_host_config::config().capture_monitor.clone();
// The EFFECTIVE pin (env override, else the stored policy) — so the picker highlights what
// sessions will actually mirror, not just what the console last wrote.
#[cfg(target_os = "linux")]
let pinned = crate::vdisplay::capture_monitor();
#[cfg(not(target_os = "linux"))]
let pinned: Option<String> = None;
// Enumeration shells out / round-trips D-Bus + Wayland, so keep it off the async worker.
let (compositor, listed) = tokio::task::spawn_blocking(|| match crate::vdisplay::detect() {
Ok(c) => (Some(c.id().to_string()), crate::vdisplay::monitors::list(c)),
@@ -392,6 +404,7 @@ pub(crate) async fn set_display_layout(ApiJson(req): ApiJson<DisplayLayoutReques
store.game_session(),
store.ddc_power_off(),
store.pnp_disable_monitors(),
store.get().capture_monitor,
);
if let Err(e) = store.set(policy) {
return api_error(