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<()>;
|
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)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
pub enum Backend {
|
pub enum Backend {
|
||||||
/// wlroots virtual pointer + keyboard Wayland protocols — the headless-Sway path.
|
/// wlroots virtual pointer + keyboard Wayland protocols — the headless-Sway path.
|
||||||
@@ -43,75 +46,57 @@ pub enum Backend {
|
|||||||
/// KWin `org_kde_kwin_fake_input` — direct injection, no RemoteDesktop portal / approval dialog
|
/// 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.
|
/// (authorized by the host's `.desktop`). The headless KDE-Desktop path; what krdpserver uses.
|
||||||
KwinFakeInput,
|
KwinFakeInput,
|
||||||
/// libei via `reis` — Wayland-native (RemoteDesktop portal). Not yet implemented.
|
/// libei via `reis` — Wayland-native (RemoteDesktop portal).
|
||||||
Libei,
|
Libei,
|
||||||
/// libei directly against gamescope's own EIS socket (no portal): input lands in the
|
/// libei directly against gamescope's own EIS socket (no portal): input lands in the
|
||||||
/// nested game — the SteamOS-like session.
|
/// nested game — the SteamOS-like session.
|
||||||
GamescopeEi,
|
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.
|
/// Windows `SendInput` (Win32 KeyboardAndMouse) — the Windows host path.
|
||||||
SendInput,
|
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>> {
|
pub fn open(backend: Backend) -> Result<Box<dyn InputInjector>> {
|
||||||
match backend {
|
match backend {
|
||||||
Backend::WlrVirtual => {
|
Backend::WlrVirtual => Ok(Box::new(wlr::WlrootsInjector::open()?)),
|
||||||
#[cfg(target_os = "linux")]
|
Backend::KwinFakeInput => Ok(Box::new(kwin_fake_input::KwinFakeInjector::open()?)),
|
||||||
{
|
Backend::Libei => Ok(Box::new(
|
||||||
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())?,
|
libei::LibeiInjector::open_with(libei_ei_source())?,
|
||||||
))
|
)),
|
||||||
}
|
Backend::GamescopeEi => Ok(Box::new(libei::LibeiInjector::open_with(
|
||||||
#[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()),
|
libei::EiSource::SocketPathFile(crate::vdisplay::gamescope_ei_socket_file()),
|
||||||
)?))
|
)?)),
|
||||||
}
|
|
||||||
#[cfg(not(target_os = "linux"))]
|
|
||||||
{
|
|
||||||
anyhow::bail!("gamescope EIS input requires Linux")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Backend::SendInput => {
|
|
||||||
|
/// Open the injector for `backend` (Windows: always `SendInput`).
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
{
|
pub fn open(backend: Backend) -> Result<Box<dyn InputInjector>> {
|
||||||
Ok(Box::new(sendinput::SendInputInjector::open()?))
|
match backend {
|
||||||
}
|
Backend::SendInput => 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"),
|
|
||||||
}
|
/// 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
|
/// Pick the injection backend for the current session. gamescope hosts its own EIS server (no
|
||||||
@@ -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**
|
/// 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
|
/// 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).
|
/// (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 {
|
pub fn default_backend() -> Backend {
|
||||||
if let Ok(v) = std::env::var("PUNKTFUNK_INPUT_BACKEND") {
|
if let Ok(v) = std::env::var("PUNKTFUNK_INPUT_BACKEND") {
|
||||||
match v.trim().to_ascii_lowercase().as_str() {
|
match v.trim().to_ascii_lowercase().as_str() {
|
||||||
@@ -131,20 +117,12 @@ pub fn default_backend() -> Backend {
|
|||||||
}
|
}
|
||||||
"libei" | "ei" | "portal" => return Backend::Libei,
|
"libei" | "ei" | "portal" => return Backend::Libei,
|
||||||
"gamescope" | "gamescope-ei" => return Backend::GamescopeEi,
|
"gamescope" | "gamescope-ei" => return Backend::GamescopeEi,
|
||||||
"uinput" => return Backend::Uinput,
|
|
||||||
"sendinput" | "win" | "windows" => return Backend::SendInput,
|
|
||||||
other => tracing::warn!(
|
other => tracing::warn!(
|
||||||
value = other,
|
value = other,
|
||||||
"unknown PUNKTFUNK_INPUT_BACKEND — auto-detecting"
|
"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.
|
// An explicit compositor pick (set per connect / mid-stream) is the strongest signal.
|
||||||
let compositor = crate::config::config().compositor.clone();
|
let compositor = crate::config::config().compositor.clone();
|
||||||
if let Some(c) = compositor.as_deref() {
|
if let Some(c) = compositor.as_deref() {
|
||||||
@@ -175,6 +153,17 @@ pub fn default_backend() -> Backend {
|
|||||||
Backend::WlrVirtual
|
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`
|
/// Host-lifetime pointer/keyboard injector running on its OWN thread, fed over a clonable `Send`
|
||||||
|
|||||||
Reference in New Issue
Block a user