fix(host/inject): make the injector factory OS-representable + drop vestigial Uinput
apple / swift (push) Successful in 1m19s
apple / screenshots (push) Successful in 4m35s
windows-host / package (push) Failing after 15m33s
ci / web (push) Successful in 52s
ci / docs-site (push) Successful in 1m1s
ci / bench (push) Successful in 6m42s
decky / build-publish (push) Successful in 21s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 11s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 12s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 10s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 12s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 14s
arch / build-publish (push) Successful in 15m0s
android / android (push) Successful in 15m23s
deb / build-publish (push) Successful in 12m26s
ci / rust (push) Successful in 18m35s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 13m22s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 17m57s
docker / deploy-docs (push) Successful in 23s
apple / swift (push) Successful in 1m19s
apple / screenshots (push) Successful in 4m35s
windows-host / package (push) Failing after 15m33s
ci / web (push) Successful in 52s
ci / docs-site (push) Successful in 1m1s
ci / bench (push) Successful in 6m42s
decky / build-publish (push) Successful in 21s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 11s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 12s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 10s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 12s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 14s
arch / build-publish (push) Successful in 15m0s
android / android (push) Successful in 15m23s
deb / build-publish (push) Successful in 12m26s
ci / rust (push) Successful in 18m35s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 13m22s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 17m57s
docker / deploy-docs (push) Successful in 23s
Restructure open()/Backend/default_backend so an impossible OS/backend pairing
is a compile error instead of a runtime bail! (plan §2.3). Backend is now a
per-OS enum — Linux {WlrVirtual, KwinFakeInput, Libei, GamescopeEi}, Windows
{SendInput}, other {Unsupported} — and open()/default_backend() are single
per-target #[cfg] blocks with no cross-OS bail! arms.
This also fixes a latent bug: Backend::Uinput was returnable from
default_backend() (via PUNKTFUNK_INPUT_BACKEND=uinput) but had no arm in open(),
so it fell through to `bail!("not implemented")` — a runtime failure. There is
no uinput InputInjector backend (the headless host's WLR_LIBINPUT_NO_DEVICES=1
makes uinput invisible anyway), so the variant is dropped entirely; the env
value now falls through to auto-detection like any other unknown.
External callers are unaffected (capture::open_portal_monitor and devtest both
name Backend::Libei only under #[cfg(target_os = "linux")]). Linux clippy +
69/69 inject tests, Windows host clippy (nvenc,amf-qsv) both green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -35,7 +35,10 @@ pub trait InputInjector {
|
||||
fn inject(&mut self, event: &InputEvent) -> Result<()>;
|
||||
}
|
||||
|
||||
/// Preferred injection backend.
|
||||
/// Preferred injection backend. Which variants exist is **per-OS**: the factory ([`open`]) is a
|
||||
/// single per-target block, so it can only be handed a backend that exists on the target — an
|
||||
/// impossible OS/backend pairing is a compile error, not a runtime `bail!` (plan §2.3).
|
||||
#[cfg(target_os = "linux")]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Backend {
|
||||
/// wlroots virtual pointer + keyboard Wayland protocols — the headless-Sway path.
|
||||
@@ -43,77 +46,59 @@ pub enum Backend {
|
||||
/// KWin `org_kde_kwin_fake_input` — direct injection, no RemoteDesktop portal / approval dialog
|
||||
/// (authorized by the host's `.desktop`). The headless KDE-Desktop path; what krdpserver uses.
|
||||
KwinFakeInput,
|
||||
/// libei via `reis` — Wayland-native (RemoteDesktop portal). Not yet implemented.
|
||||
/// libei via `reis` — Wayland-native (RemoteDesktop portal).
|
||||
Libei,
|
||||
/// libei directly against gamescope's own EIS socket (no portal): input lands in the
|
||||
/// nested game — the SteamOS-like session.
|
||||
GamescopeEi,
|
||||
/// `/dev/uinput` — universal fallback (but invisible to `WLR_LIBINPUT_NO_DEVICES=1`).
|
||||
Uinput,
|
||||
}
|
||||
|
||||
/// Preferred injection backend. Windows has exactly one path (`SendInput`).
|
||||
#[cfg(target_os = "windows")]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Backend {
|
||||
/// Windows `SendInput` (Win32 KeyboardAndMouse) — the Windows host path.
|
||||
SendInput,
|
||||
}
|
||||
|
||||
/// Preferred injection backend. No injector exists on this platform; [`open`] rejects it.
|
||||
#[cfg(not(any(target_os = "linux", target_os = "windows")))]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Backend {
|
||||
/// Placeholder so the host still builds; the platform has no input injection.
|
||||
Unsupported,
|
||||
}
|
||||
|
||||
/// Open the injector for `backend`. The body is one per-OS block: on each target `backend` can only
|
||||
/// name a backend that platform has, so there are no cross-OS `bail!` arms (plan §2.3).
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn open(backend: Backend) -> Result<Box<dyn InputInjector>> {
|
||||
match backend {
|
||||
Backend::WlrVirtual => {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
Ok(Box::new(wlr::WlrootsInjector::open()?))
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
anyhow::bail!("wlroots virtual input requires Linux + a Wayland compositor")
|
||||
}
|
||||
}
|
||||
Backend::KwinFakeInput => {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
Ok(Box::new(kwin_fake_input::KwinFakeInjector::open()?))
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
anyhow::bail!("KWin fake_input requires Linux + a KWin Wayland session")
|
||||
}
|
||||
}
|
||||
Backend::Libei => {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
Ok(Box::new(
|
||||
libei::LibeiInjector::open_with(libei_ei_source())?,
|
||||
))
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
anyhow::bail!("libei input requires Linux + a RemoteDesktop portal")
|
||||
}
|
||||
}
|
||||
Backend::GamescopeEi => {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
Ok(Box::new(libei::LibeiInjector::open_with(
|
||||
libei::EiSource::SocketPathFile(crate::vdisplay::gamescope_ei_socket_file()),
|
||||
)?))
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
anyhow::bail!("gamescope EIS input requires Linux")
|
||||
}
|
||||
}
|
||||
Backend::SendInput => {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
Ok(Box::new(sendinput::SendInputInjector::open()?))
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
anyhow::bail!("SendInput injection requires Windows")
|
||||
}
|
||||
}
|
||||
other => anyhow::bail!("injection backend {other:?} not implemented"),
|
||||
Backend::WlrVirtual => Ok(Box::new(wlr::WlrootsInjector::open()?)),
|
||||
Backend::KwinFakeInput => Ok(Box::new(kwin_fake_input::KwinFakeInjector::open()?)),
|
||||
Backend::Libei => Ok(Box::new(
|
||||
libei::LibeiInjector::open_with(libei_ei_source())?,
|
||||
)),
|
||||
Backend::GamescopeEi => Ok(Box::new(libei::LibeiInjector::open_with(
|
||||
libei::EiSource::SocketPathFile(crate::vdisplay::gamescope_ei_socket_file()),
|
||||
)?)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Open the injector for `backend` (Windows: always `SendInput`).
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn open(backend: Backend) -> Result<Box<dyn InputInjector>> {
|
||||
match backend {
|
||||
Backend::SendInput => Ok(Box::new(sendinput::SendInputInjector::open()?)),
|
||||
}
|
||||
}
|
||||
|
||||
/// No input-injection backend exists on this platform.
|
||||
#[cfg(not(any(target_os = "linux", target_os = "windows")))]
|
||||
pub fn open(_backend: Backend) -> Result<Box<dyn InputInjector>> {
|
||||
anyhow::bail!("no input-injection backend on this platform")
|
||||
}
|
||||
|
||||
/// Pick the injection backend for the current session. gamescope hosts its own EIS server (no
|
||||
/// portal), so a gamescope session injects directly into it. wlroots/Sway only implements the
|
||||
/// ScreenCast portal (no RemoteDesktop), so libei can't run there — use the wlr virtual-input
|
||||
@@ -121,7 +106,8 @@ pub fn open(backend: Backend) -> Result<Box<dyn InputInjector>> {
|
||||
/// dialog — the only headless-capable path; what krdpserver uses), so prefer it there. **GNOME**
|
||||
/// has neither fake_input nor the wlr protocols, so it uses libei via the RemoteDesktop portal
|
||||
/// (which needs a user to approve, or a pre-seeded grant — not truly headless).
|
||||
/// `PUNKTFUNK_INPUT_BACKEND=wlr|kwin|libei|gamescope|uinput` overrides the auto-detection.
|
||||
/// `PUNKTFUNK_INPUT_BACKEND=wlr|kwin|libei|gamescope` overrides the auto-detection.
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn default_backend() -> Backend {
|
||||
if let Ok(v) = std::env::var("PUNKTFUNK_INPUT_BACKEND") {
|
||||
match v.trim().to_ascii_lowercase().as_str() {
|
||||
@@ -131,50 +117,53 @@ pub fn default_backend() -> Backend {
|
||||
}
|
||||
"libei" | "ei" | "portal" => return Backend::Libei,
|
||||
"gamescope" | "gamescope-ei" => return Backend::GamescopeEi,
|
||||
"uinput" => return Backend::Uinput,
|
||||
"sendinput" | "win" | "windows" => return Backend::SendInput,
|
||||
other => tracing::warn!(
|
||||
value = other,
|
||||
"unknown PUNKTFUNK_INPUT_BACKEND — auto-detecting"
|
||||
),
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
Backend::SendInput
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
// An explicit compositor pick (set per connect / mid-stream) is the strongest signal.
|
||||
let compositor = crate::config::config().compositor.clone();
|
||||
if let Some(c) = compositor.as_deref() {
|
||||
let c = c.trim();
|
||||
if c.eq_ignore_ascii_case("gamescope") {
|
||||
return Backend::GamescopeEi;
|
||||
}
|
||||
if c.eq_ignore_ascii_case("kwin") {
|
||||
return Backend::KwinFakeInput;
|
||||
}
|
||||
if c.eq_ignore_ascii_case("wlroots")
|
||||
|| c.eq_ignore_ascii_case("sway")
|
||||
// Hyprland kept the wlr virtual-input protocols, so it injects through the same
|
||||
// backend as sway/river (design/hyprland-support.md D4).
|
||||
|| c.eq_ignore_ascii_case("hyprland")
|
||||
{
|
||||
return Backend::WlrVirtual;
|
||||
}
|
||||
// mutter (GNOME) falls through to the XDG_CURRENT_DESKTOP check below.
|
||||
// An explicit compositor pick (set per connect / mid-stream) is the strongest signal.
|
||||
let compositor = crate::config::config().compositor.clone();
|
||||
if let Some(c) = compositor.as_deref() {
|
||||
let c = c.trim();
|
||||
if c.eq_ignore_ascii_case("gamescope") {
|
||||
return Backend::GamescopeEi;
|
||||
}
|
||||
let desktop = std::env::var("XDG_CURRENT_DESKTOP").unwrap_or_default();
|
||||
let d = desktop.to_ascii_uppercase();
|
||||
if d.contains("KDE") {
|
||||
Backend::KwinFakeInput
|
||||
} else if d.contains("GNOME") {
|
||||
Backend::Libei
|
||||
} else {
|
||||
Backend::WlrVirtual
|
||||
if c.eq_ignore_ascii_case("kwin") {
|
||||
return Backend::KwinFakeInput;
|
||||
}
|
||||
if c.eq_ignore_ascii_case("wlroots")
|
||||
|| c.eq_ignore_ascii_case("sway")
|
||||
// Hyprland kept the wlr virtual-input protocols, so it injects through the same
|
||||
// backend as sway/river (design/hyprland-support.md D4).
|
||||
|| c.eq_ignore_ascii_case("hyprland")
|
||||
{
|
||||
return Backend::WlrVirtual;
|
||||
}
|
||||
// mutter (GNOME) falls through to the XDG_CURRENT_DESKTOP check below.
|
||||
}
|
||||
let desktop = std::env::var("XDG_CURRENT_DESKTOP").unwrap_or_default();
|
||||
let d = desktop.to_ascii_uppercase();
|
||||
if d.contains("KDE") {
|
||||
Backend::KwinFakeInput
|
||||
} else if d.contains("GNOME") {
|
||||
Backend::Libei
|
||||
} else {
|
||||
Backend::WlrVirtual
|
||||
}
|
||||
}
|
||||
|
||||
/// The Windows host has a single injection backend.
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn default_backend() -> Backend {
|
||||
Backend::SendInput
|
||||
}
|
||||
|
||||
/// No injector on this platform.
|
||||
#[cfg(not(any(target_os = "linux", target_os = "windows")))]
|
||||
pub fn default_backend() -> Backend {
|
||||
Backend::Unsupported
|
||||
}
|
||||
|
||||
/// Host-lifetime pointer/keyboard injector running on its OWN thread, fed over a clonable `Send`
|
||||
|
||||
Reference in New Issue
Block a user