feat(vdisplay): mirror a pinned physical monitor on KWin

P2 of design/per-monitor-portal-capture.md. zkde_screencast's
stream_output records an output KWin already has — the connector name IS
the selection, so there is no dialog, no portal and no chooser for a
background service to answer.

It arrives as a VirtualDisplay reporting DisplayOwnership::External, which
already means "someone else's display, merely mirrored: no keep-alive, no
topology, no reuse". So the rule that we must never disable, move or
"restore" a monitor the user is sitting in front of holds by construction
rather than by everyone remembering it — and create() ignores the client's
requested mode for the same reason: a panel runs at the mode its owner set.

Routing lives in vdisplay::open, the one place every session opens a
display, so the pin cannot be honored on one plane and ignored on another.
The host sets the libei anchor from the same pin (pf-vdisplay must not
depend on pf-inject), which is what makes absolute input land on the
mirrored head instead of a same-sized neighbour.

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 2eeee650b9
commit 93c2765db7
4 changed files with 449 additions and 24 deletions
+29 -13
View File
@@ -231,26 +231,42 @@ fn real_main() -> Result<()> {
);
}
// Report a `PUNKTFUNK_CAPTURE_MONITOR` pin at startupthe operator sets it in a host.env and
// then has no session to watch, so this is the only place a typo can surface. It is deliberately
// loud about not being enforced yet: a knob that is parsed but inert must SAY so, or a silent
// "it didn't work" reads as a bug.
// Resolve a `PUNKTFUNK_CAPTURE_MONITOR` pin at startup: report it (the operator sets it in a
// host.env and then has no session to watch, so this is where a typo has to surface), and aim
// absolute input at that head.
//
// The anchor is set HERE rather than inside the mirror backend for two reasons: pf-vdisplay must
// not depend on pf-inject (its crate doc), and the anchor is a host-level pin anyway — the
// injector is host-lifetime and shared by every concurrent session, so there is nothing
// per-session to track (`design/per-monitor-portal-capture.md` §7.2).
#[cfg(target_os = "linux")]
if !management_cli {
if let Some(want) = pf_host_config::config().capture_monitor.as_deref() {
match pf_vdisplay::detect().and_then(pf_vdisplay::monitors::list) {
Ok(ms) => match pf_vdisplay::monitors::resolve(&ms, want) {
Ok(m) => tracing::info!(
connector = %m.connector,
description = %m.description,
mode = %m.mode_label(),
at = %format!("+{}+{}", m.x, m.y),
"PUNKTFUNK_CAPTURE_MONITOR names this monitor — NOTE: per-monitor capture \
is not implemented yet, so sessions still use the normal display path"
),
Ok(m) => {
// Match the libei region by the head's ORIGIN: two monitors can share a
// size — and a mirrored head's region is not the client's size at all — so
// size matching would put the pointer on the wrong screen.
pf_inject::set_absolute_anchor(Some(pf_inject::AbsoluteAnchor {
origin: Some((m.x, m.y)),
mapping_id: None,
}));
tracing::info!(
connector = %m.connector,
description = %m.description,
mode = %m.mode_label(),
at = %format!("+{}+{}", m.x, m.y),
"PUNKTFUNK_CAPTURE_MONITOR: sessions will mirror this monitor (no \
virtual display) and absolute input is anchored to it"
);
}
// Left unanchored on purpose: a pin that resolves to nothing must not aim input
// at a guess. The session's own `create` fails with the same reason.
Err(e) => tracing::warn!(
error = %e,
"PUNKTFUNK_CAPTURE_MONITOR names no monitor on this host"
"PUNKTFUNK_CAPTURE_MONITOR names no monitor on this host — sessions will \
fail to start until it is corrected or unset"
),
},
Err(e) => tracing::warn!(