A German keyboard typed US characters — nothing carried the box's layout into the session #257

Merged
enricobuehler merged 1 commits from worktree-ipad-keyboard-layout into main 2026-08-15 21:58:29 +00:00
Owner

Field report (iPad → Bazzite host, Game Mode): # arrived as \, - as /, ä as '.

That set is not random — they are the US ANSI keys that sit exactly where the German ISO ones do. So the client was right and the host session was resolving key positions with a US keymap.

Why

The key wire is US-positional by design: a client sends the physical key, vk_to_evdev turns it into an evdev code, and the session's keymap picks the character. The standing contract is "host layout == the layout printed on the client's keyboard". Nothing was upholding it, for two independent reasons.

1. Nothing exports XKB_DEFAULT_*. localectl set-x11-keymap de writes /etc/X11/xorg.conf.d/00-keyboard.conf — a file only Xorg opens — and libxkbcommon's fallback chain stops at the env vars, which no session manager sets. Compiling a keymap from empty names on a properly-configured German box therefore yielded evdev/pc105/us, silently.

2. gamescope reads those env vars and then publishes the keymap to nobody. It builds the keymap onto wlserver.keyboard_group, but the seat carries wlserver.wlr.virtual_keyboard_device — a stub whose own comment says it exists "only used to set the keymap" and which never gets one. wlserver_keyboardfocus() rebinds that stub on every focus change, and the real group only reaches the seat from a libinput key event, which a --backend headless session never has.

Verified on the box, not inferred: both gamescope Xwayland servers map evdev 53/40/43 to slash/apostrophe/backslash on a de/nodeadkeys machine, and setting XKB_DEFAULT_LAYOUT=de on gamescope changed nothing.

⚠️ setxkbmap -query on an Xwayland server lies — it prints a static _XKB_RULES_NAMES property that always reads evdev/pc105/us, not the live keymap. Probe with DISPLAY=:N xmodmap -pke instead.

What this does

pf_host_config::layout resolves what the box actually recorded — XKB_DEFAULT_*, then xorg.conf.d/*keyboard*.conf, then vconsole's XKBLAYOUT. It deliberately never reads vconsole's KEYMAP=: those are console keymap names whose namespace only coincides with xkb's by accident (uk is xkb gb), and a wrong guess would mistype every key instead of falling back to a visible default.

From there:

  • the wlroots injector compiles its uploaded keymap from that instead of from empty names — this fixes Sway/Hyprland hosts outright;
  • all four gamescope launch paths (bare spawn, systemd-run --setenv, both drop-ins) hand the session XKB_DEFAULT_*, behind a +pfhdr8 probe that warns when the binary predates the fix rather than leaving it unexplained;
  • sync_session_keyboard_layout() covers the case none of that reaches — the autologin session punktfunk attaches to, where no launch-time decision applies — by pointing each gamescope Xwayland at the box's layout on adoption. Everything in Game Mode is an X11 client of those servers, so this is the leg that actually fixes the report. Kill switch: PUNKTFUNK_SESSION_LAYOUT=0.

gamescope patch 0010 sets the keymap on the stub as well, which is what makes the env legs mean anything for Wayland-native clients. Bumps the fork stamp +pfhdr7+pfhdr8.

Nothing here changes which physical key does what in a game — WASD stays under the same fingers on every layout.

Verification

  • 10 new unit tests over the resolver (precedence, both file formats, the KEYMAP= exclusion).
  • pf-inject + pf-host-config: clippy --all-targets -D warnings and those tests on real linux-gnu in the CI image.
  • pf-vdisplay: check + clippy clean on linux-gnu via scripts/xcheck.sh, confirmed non-vacuous with a planted type error in the new function.
  • The character mapping itself confirmed on the box with xmodmap -pke before and after applying the layout: slashminus, apostropheadiaeresis, backslashnumbersign.

Caveat for review

The gamescope patch is not build-verified — no gamescope build environment was available, so it is reviewed and git am-clean against the pinned 5fb8dce4 (whole 10-patch series re-applied from a clean checkout), but CI is its first real compile. The Rust side is independent of it: the env legs are inert against an older gamescope, and the Xwayland alignment needs no gamescope change at all.

Field report (iPad → Bazzite host, Game Mode): `#` arrived as `\`, `-` as `/`, `ä` as `'`. That set is not random — they are the US ANSI keys that sit exactly where the German ISO ones do. So the client was right and the **host session** was resolving key positions with a US keymap. ## Why The key wire is **US-positional** by design: a client sends the physical key, `vk_to_evdev` turns it into an evdev code, and the *session's* keymap picks the character. The standing contract is "host layout == the layout printed on the client's keyboard". Nothing was upholding it, for two independent reasons. **1. Nothing exports `XKB_DEFAULT_*`.** `localectl set-x11-keymap de` writes `/etc/X11/xorg.conf.d/00-keyboard.conf` — a file only Xorg opens — and libxkbcommon's fallback chain stops at the env vars, which no session manager sets. Compiling a keymap from empty names on a properly-configured German box therefore yielded `evdev/pc105/us`, silently. **2. gamescope reads those env vars and then publishes the keymap to nobody.** It builds the keymap onto `wlserver.keyboard_group`, but the seat carries `wlserver.wlr.virtual_keyboard_device` — a stub whose own comment says it exists *"only used to set the keymap"* and which never gets one. `wlserver_keyboardfocus()` rebinds that stub on every focus change, and the real group only reaches the seat from a **libinput** key event, which a `--backend headless` session never has. Verified on the box, not inferred: both gamescope Xwayland servers map evdev 53/40/43 to `slash`/`apostrophe`/`backslash` on a `de`/`nodeadkeys` machine, and setting `XKB_DEFAULT_LAYOUT=de` on gamescope changed nothing. > ⚠️ `setxkbmap -query` on an Xwayland server **lies** — it prints a static `_XKB_RULES_NAMES` property that always reads `evdev/pc105/us`, not the live keymap. Probe with `DISPLAY=:N xmodmap -pke` instead. ## What this does `pf_host_config::layout` resolves what the box actually recorded — `XKB_DEFAULT_*`, then `xorg.conf.d/*keyboard*.conf`, then vconsole's `XKBLAYOUT`. It deliberately never reads vconsole's `KEYMAP=`: those are console keymap names whose namespace only coincides with xkb's by accident (`uk` is xkb `gb`), and a wrong guess would mistype every key instead of falling back to a visible default. From there: - the **wlroots injector** compiles its uploaded keymap from that instead of from empty names — this fixes Sway/Hyprland hosts outright; - all four **gamescope launch paths** (bare spawn, `systemd-run --setenv`, both drop-ins) hand the session `XKB_DEFAULT_*`, behind a `+pfhdr8` probe that warns when the binary predates the fix rather than leaving it unexplained; - **`sync_session_keyboard_layout()`** covers the case none of that reaches — the autologin session punktfunk *attaches* to, where no launch-time decision applies — by pointing each gamescope Xwayland at the box's layout on adoption. Everything in Game Mode is an X11 client of those servers, so this is the leg that actually fixes the report. Kill switch: `PUNKTFUNK_SESSION_LAYOUT=0`. **gamescope patch 0010** sets the keymap on the stub as well, which is what makes the env legs mean anything for Wayland-native clients. Bumps the fork stamp `+pfhdr7` → `+pfhdr8`. Nothing here changes which physical key does what in a game — `WASD` stays under the same fingers on every layout. ## Verification - 10 new unit tests over the resolver (precedence, both file formats, the `KEYMAP=` exclusion). - `pf-inject` + `pf-host-config`: `clippy --all-targets -D warnings` **and** those tests on real `linux-gnu` in the CI image. - `pf-vdisplay`: check + clippy clean on `linux-gnu` via `scripts/xcheck.sh`, confirmed **non-vacuous** with a planted type error in the new function. - The character mapping itself confirmed on the box with `xmodmap -pke` before and after applying the layout: `slash`→`minus`, `apostrophe`→`adiaeresis`, `backslash`→`numbersign`. ## Caveat for review The gamescope patch is **not build-verified** — no gamescope build environment was available, so it is reviewed and `git am`-clean against the pinned `5fb8dce4` (whole 10-patch series re-applied from a clean checkout), but CI is its first real compile. The Rust side is independent of it: the env legs are inert against an older gamescope, and the Xwayland alignment needs no gamescope change at all.
enricobuehler added 1 commit 2026-08-15 19:48:46 +00:00
fix(input): a German keyboard typed US characters — nothing carried the box's layout into the session
ci / bun-nix (pull_request) Successful in 21s
ci / web (pull_request) Successful in 1m5s
ci / docs-site (pull_request) Successful in 9m7s
ci / rust-arm64 (pull_request) Successful in 9m47s
android / android (pull_request) Successful in 15m41s
ci / rust (pull_request) Successful in 16m49s
94be547da0
Reported from an iPad on a Bazzite host in Game Mode: `#` arrived as `\`, `-` as `/`, `ä` as `'`.
That set is not random — they are the US ANSI keys that sit where the German ISO ones do, which
says the client was right and the host session was resolving positions with a US keymap.

The key wire is US-POSITIONAL by design: a client sends the physical key, `vk_to_evdev` turns it
into an evdev code, and the session's keymap picks the character. So the contract is "host layout
== the layout on the client's keyboard", and nothing was upholding it:

- Nothing exports `XKB_DEFAULT_*`. `localectl set-x11-keymap de` writes
  /etc/X11/xorg.conf.d/00-keyboard.conf — a file only Xorg reads — and libxkbcommon's fallback
  chain stops at the env vars, which no session manager sets. Compiling from empty names on a
  properly-configured German box therefore yielded evdev/pc105/us, silently.
- gamescope reads those env vars and then publishes the keymap to nobody. It builds the keymap
  onto `keyboard_group`, but the seat carries `virtual_keyboard_device` — a stub whose own comment
  says it exists "only to set the keymap" and which never gets one. `wlserver_keyboardfocus()`
  rebinds that stub on every focus change, and the real group only reaches the seat from a libinput
  key event, which a `--backend headless` session never has. Verified on the box: both Xwayland
  servers map evdev 53/40/43 to slash/apostrophe/backslash on a de/nodeadkeys machine.

`pf_host_config::layout` resolves what the box actually recorded (`XKB_DEFAULT_*`, then
xorg.conf.d, then vconsole's XKBLAYOUT — never vconsole's KEYMAP, whose names are console names and
do not map onto xkb's). From there:

- the wlroots injector compiles its uploaded keymap from it instead of from empty names, which
  fixes Sway/Hyprland hosts outright;
- all four gamescope launch paths hand the session `XKB_DEFAULT_*`, gated behind a `+pfhdr8` probe
  that warns when the binary predates the fix rather than leaving it unexplained;
- `sync_session_keyboard_layout()` covers the case none of that reaches — the autologin session
  punktfunk ATTACHES to, where no launch-time decision applies — by pointing each gamescope
  Xwayland at the box's layout on adoption. Everything in Game Mode is an X11 client of those
  servers, so this is the leg that fixes the report. `PUNKTFUNK_SESSION_LAYOUT=0` turns it off.

gamescope patch 0010 sets the keymap on the stub as well, which is what makes the env legs mean
anything for Wayland-native clients. It is NOT build-verified (no gamescope build environment
here); the series does apply `git am`-clean at the pinned 5fb8dce4.

Verified: 10 new unit tests over the resolver; pf-inject + pf-host-config clippy `-D warnings` and
those tests on real linux-gnu in the CI image; pf-vdisplay checks and clippies clean on linux-gnu
via scripts/xcheck.sh, confirmed non-vacuous with a planted error. The character mapping itself was
confirmed on the box with `xmodmap -pke` before and after applying the layout.
enricobuehler merged commit 9140a2e6e1 into main 2026-08-15 21:58:29 +00:00
enricobuehler deleted branch worktree-ipad-keyboard-layout 2026-08-15 21:58:34 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: unom/punktfunk#257