feat(host/windows): SendInput input-injection backend

Windows InputInjector via SendInput (Win32 KeyboardAndMouse), mirroring the wlroots backend: absolute mouse (MOUSEEVENTF_VIRTUALDESK normalized to the virtual desktop), relative mouse, scancode keyboard (MapVirtualKeyExW + extended-key flagging), scroll (no sign flip — Windows wheel matches GameStream), buttons. Client already sends Windows VK codes (no keycode table). Reattaches the thread to the input desktop (OpenInputDesktop/SetThreadDesktop) to survive UAC/lock switches. New Backend::SendInput, the Windows auto-default in default_backend(), open() arm, windows-crate features. Compiles clean on Windows + Linux. Live injection validates with the in-session host run (SendInput is desktop-isolated from an SSH network logon).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-15 00:34:40 +00:00
parent 5c5b85eed6
commit 87ecd7e7ac
3 changed files with 264 additions and 9 deletions
+31 -9
View File
@@ -31,6 +31,8 @@ pub enum Backend {
GamescopeEi,
/// `/dev/uinput` — universal fallback (but invisible to `WLR_LIBINPUT_NO_DEVICES=1`).
Uinput,
/// Windows `SendInput` (Win32 KeyboardAndMouse) — the Windows host path.
SendInput,
}
pub fn open(backend: Backend) -> Result<Box<dyn InputInjector>> {
@@ -71,6 +73,16 @@ pub fn open(backend: Backend) -> Result<Box<dyn InputInjector>> {
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"),
}
}
@@ -87,23 +99,31 @@ 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"
),
}
}
if std::env::var("PUNKTFUNK_COMPOSITOR")
.is_ok_and(|v| v.trim().eq_ignore_ascii_case("gamescope"))
#[cfg(target_os = "windows")]
{
return Backend::GamescopeEi;
Backend::SendInput
}
let desktop = std::env::var("XDG_CURRENT_DESKTOP").unwrap_or_default();
let d = desktop.to_ascii_uppercase();
if d.contains("KDE") || d.contains("GNOME") {
Backend::Libei
} else {
Backend::WlrVirtual
#[cfg(not(target_os = "windows"))]
{
if std::env::var("PUNKTFUNK_COMPOSITOR")
.is_ok_and(|v| v.trim().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") || d.contains("GNOME") {
Backend::Libei
} else {
Backend::WlrVirtual
}
}
}
@@ -295,3 +315,5 @@ pub mod gamepad {
mod libei;
#[cfg(target_os = "linux")]
mod wlr;
#[cfg(target_os = "windows")]
mod sendinput;