fix(vdisplay/mutter): make the virtual output the SOLE display, not primary + secondary
ci / web (push) Failing after 38s
ci / rust (push) Successful in 55s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 3s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 3s
ci / docs-site (push) Failing after 43s
docker / deploy-docs (push) Successful in 16s
apple / swift (push) Successful in 1m13s

Keeping the physical monitor enabled as a secondary let the cursor, windows, and keyboard
focus land on it — relative pointer motion wandered off the streamed surface, so on the
client the cursor "disappeared" and clicks/keys went nowhere visible. Omit the physical
outputs from ApplyMonitorsConfig so Mutter disables them for the session; everything is
confined to the streamed virtual output. Restored on teardown.

Validated on-box: mid-session DisplayConfig shows only the virtual output (Meta-0) as the
sole primary; the physical (HDMI-1) is restored after the session ends.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-12 13:05:02 +00:00
parent 1293b7e001
commit 2ed755f0c3
+12 -36
View File
@@ -382,7 +382,9 @@ fn current_mode(state: &CurrentState, connector: &str) -> Option<(String, i32, i
} }
/// Wait for the virtual output to appear in DisplayConfig (its size follows PipeWire negotiation, /// Wait for the virtual output to appear in DisplayConfig (its size follows PipeWire negotiation,
/// which lands shortly after the node id), then promote it to primary with the physicals kept on. /// which lands shortly after the node id), then make it the SOLE primary output (physicals
/// disabled for the session) so the cursor, windows, and keyboard focus stay on the streamed
/// surface. Restored on teardown.
async fn make_virtual_primary(dc: &zbus::Proxy<'_>, mode: Mode, pre: &CurrentState) -> Result<()> { async fn make_virtual_primary(dc: &zbus::Proxy<'_>, mode: Mode, pre: &CurrentState) -> Result<()> {
let pre_conns = connectors(pre); let pre_conns = connectors(pre);
let deadline = Instant::now() + Duration::from_secs(6); let deadline = Instant::now() + Duration::from_secs(6);
@@ -409,7 +411,7 @@ async fn make_virtual_primary(dc: &zbus::Proxy<'_>, mode: Mode, pre: &CurrentSta
let Some(vmode) = vmode else { let Some(vmode) = vmode else {
bail!("virtual monitor {vconn} has no usable mode yet"); bail!("virtual monitor {vconn} has no usable mode yet");
}; };
let config = build_primary_config(&state, &vconn, &vmode, mode.width as i32); let config = build_primary_config(&vconn, &vmode);
let _: () = dc let _: () = dc
.call( .call(
"ApplyMonitorsConfig", "ApplyMonitorsConfig",
@@ -431,46 +433,20 @@ async fn make_virtual_primary(dc: &zbus::Proxy<'_>, mode: Mode, pre: &CurrentSta
} }
} }
/// Virtual output = primary at the top-left; physical monitors kept enabled but secondary, laid /// The virtual output as the SOLE, primary monitor — physical outputs are omitted, so Mutter
/// out adjacently to its right (so the local screen never blanks). /// disables them for the session. This confines the cursor, windows, and keyboard focus to the
fn build_primary_config( /// streamed surface; keeping the physical enabled as a *secondary* monitor instead lets relative
state: &CurrentState, /// pointer motion and window focus wander onto it (invisible to the client — the cursor seems to
vconn: &str, /// vanish). The physical layout is restored on teardown.
vmode: &str, fn build_primary_config(vconn: &str, vmode: &str) -> Vec<ApplyLogical> {
virt_width: i32, vec![(
) -> Vec<ApplyLogical> {
let mut logicals: Vec<ApplyLogical> = Vec::new();
logicals.push((
0, 0,
0, 0,
1.0, 1.0,
0, 0,
true, true,
vec![(vconn.to_string(), vmode.to_string(), HashMap::new())], vec![(vconn.to_string(), vmode.to_string(), HashMap::new())],
)); )]
let mut x = virt_width;
for lm in &state.2 {
if lm.5.iter().any(|s| s.0 == vconn) {
continue; // skip the virtual output's own logical monitor
}
let mons: Vec<ApplyMon> =
lm.5.iter()
.filter_map(|s| {
current_mode(state, &s.0).map(|(id, _, _)| (s.0.clone(), id, HashMap::new()))
})
.collect();
if mons.is_empty() {
continue;
}
let width =
lm.5.first()
.and_then(|s| current_mode(state, &s.0))
.map(|(_, w, _)| w)
.unwrap_or(1920);
logicals.push((x, 0, lm.2, lm.3, false, mons));
x += width;
}
logicals
} }
/// Convert a captured `GetCurrentState` layout back into an `ApplyMonitorsConfig` argument (used /// Convert a captured `GetCurrentState` layout back into an `ApplyMonitorsConfig` argument (used