fix(vdisplay): compositor availability follows the live session, not the env

GNOME/Mutter reported "Unavailable" on a host sitting in a live Mutter
session — and, on the same request, "Default", because the two columns had
different sources. available() asked each backend, and those probes read
the process env (XDG_CURRENT_DESKTOP for Mutter, WAYLAND_DISPLAY for
KWin's registry handshake, SWAYSOCK for sway) — env a host started outside
the session (systemd --user, a TTY, ssh) never inherited. It is only
retargeted at the live session on the connect path, so the answer also
flipped depending on whether anyone had connected yet.

Both columns now come from the same /proc scan detect() already used: the
live session's compositor is usable by definition, as is an explicit
operator pin, and the per-backend probe stays as the fallback for backends
that are not the live session (gamescope, which spawns its own). A live
KWin without the zkde_screencast grant now surfaces as available and fails
at create with that probe's precise message, which beats "no usable
compositor" on a box visibly running KDE.

Mutter's env sniff stays deliberately narrow — one var, not three.
XDG_CURRENT_DESKTOP is the one apply_session_env owns end to end (written
per connect, scrubbed when nothing is live); sniffing DESKTOP_SESSION
alongside would resurrect the bug that scrub exists to prevent, where a
stale value after a gnome-shell crash routes the next client into a dead
session.

Non-Linux hosts now report no compositors at all rather than five Linux
backends flagged unavailable with no default — on Windows the pf-vdisplay
driver is the only backend and vdisplay::open ignores the argument, so the
old list read as broken detection instead of "not applicable here". The
console says so explicitly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit eb7ba3d6177552f5c3ed6a15439404084869c636)
This commit is contained in:
2026-07-28 17:01:59 +02:00
parent 3b11288c97
commit 0868b6a364
8 changed files with 137 additions and 68 deletions
+16 -5
View File
@@ -319,9 +319,19 @@ pub(crate) struct AvailableCompositor {
)
)]
pub(crate) async fn list_compositors() -> Json<Vec<AvailableCompositor>> {
let available = crate::vdisplay::available();
let default = crate::vdisplay::detect().ok();
Json(
// Compositor backends are a Linux concept. On Windows the pf-vdisplay IddCx driver is the sole
// virtual-display backend and `vdisplay::open` ignores the compositor argument entirely, so the
// list could only ever be five Linux backends flagged unavailable with no default — which reads
// as broken detection rather than "not applicable here". Report none and let the console say so.
#[cfg(not(target_os = "linux"))]
let list = Vec::new();
#[cfg(target_os = "linux")]
// One `/proc` scan backs BOTH columns (see `vdisplay::available`), so a backend can no longer
// be the auto-detect default and "unavailable" on the same row — the contradiction that made
// this list look broken on a box plainly running the compositor it called unavailable.
let list = {
let available = crate::vdisplay::available();
let default = crate::vdisplay::detect().ok();
crate::vdisplay::Compositor::all()
.into_iter()
.map(|c| AvailableCompositor {
@@ -330,8 +340,9 @@ pub(crate) async fn list_compositors() -> Json<Vec<AvailableCompositor>> {
available: available.contains(&c),
default: default == Some(c),
})
.collect(),
)
.collect()
};
Json(list)
}
/// Live host status
+9 -2
View File
@@ -652,9 +652,16 @@ async fn compositors_lists_all_backends_with_flags() {
let (status, body) = send(&app, get_req("/api/v1/compositors")).await;
assert_eq!(status, StatusCode::OK);
let arr = body.as_array().expect("array");
// Compositor backends are Linux-only; elsewhere the list is empty on purpose (the console
// renders "not applicable on this host" instead of five greyed-out rows).
#[cfg(not(target_os = "linux"))]
assert!(arr.is_empty(), "non-Linux hosts advertise no compositors");
// Every backend the host knows, in stable order.
let ids: Vec<&str> = arr.iter().map(|c| c["id"].as_str().unwrap()).collect();
assert_eq!(ids, ["kwin", "gamescope", "mutter", "wlroots", "hyprland"]);
#[cfg(target_os = "linux")]
{
let ids: Vec<&str> = arr.iter().map(|c| c["id"].as_str().unwrap()).collect();
assert_eq!(ids, ["kwin", "gamescope", "mutter", "wlroots", "hyprland"]);
}
for c in arr {
assert!(c["available"].is_boolean());
assert!(c["default"].is_boolean());