fix(host/windows): layout-correct keyboard injection - semantic vs positional VKs

First-party punktfunk clients send US-positional VKs (the physical key's
US-layout VK), GameStream/Moonlight clients send layout-semantic VKs
(Sunshine's model). The SendInput injector previously resolved everything
through the SYSTEM service's layout - on a German host that is the y/z swap
and u-umlaut-on-o-umlaut scramble. GameStream ingest now tags its key events
KEY_FLAG_SEMANTIC_VK (stripped from punktfunk/1 wire events so a network
client can't flip the convention); the injector maps semantic VKs under the
foreground app's layout and positional VKs through a fixed scancode table.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-02 16:24:04 +02:00
parent dacc3b5209
commit 03ad2406b6
4 changed files with 203 additions and 28 deletions
+13 -3
View File
@@ -5,13 +5,23 @@
//! protocols — `zwlr_virtual_pointer_manager_v1` + `zwp_virtual_keyboard_manager_v1` — which
//! Sway always advertises. We connect as an ordinary Wayland client (the host process
//! inherits Sway's `WAYLAND_DISPLAY`/`XDG_RUNTIME_DIR`), bind the two managers, and translate
//! events into virtual pointer/keyboard requests. Keyboard codes are Linux evdev; we upload a
//! standard evdev/US xkb keymap and track modifier state so the compositor resolves shifted
//! keysyms correctly.
//! events into virtual pointer/keyboard requests. Keyboard codes are Linux evdev; we upload an
//! xkb keymap (the host's layout via `XKB_DEFAULT_LAYOUT` et al., defaulting to evdev/US) and
//! track modifier state so the compositor resolves shifted keysyms correctly.
use anyhow::Result;
use punktfunk_core::input::{InputEvent, InputKind};
/// In-process tag on a key event's `flags`: the VK in `code` is **layout-semantic** (already
/// resolved under the sending client's keyboard layout — the GameStream/Moonlight convention)
/// rather than the punktfunk-native **US-positional** convention (the physical key's US-layout VK,
/// which every first-party client sends — the client's local layout never touches the wire).
/// The Windows injector maps semantic VKs through the foreground app's layout and positional VKs
/// through a fixed table; conflating the two is exactly the German y↔z / ö→ü scramble.
/// Set ONLY by `gamestream::input::decode`; the punktfunk/1 ingest strips it from wire events, so
/// a network client can never flip the host's key-decoding convention.
pub const KEY_FLAG_SEMANTIC_VK: u32 = 0x8000_0000;
/// Injects input events into the host session. Not `Send`: an injector owns compositor
/// resources (a Wayland connection, an xkb state) and lives entirely on the control thread
/// that creates it.