fix(host/linux,clients/android): honor the host/device keyboard layout in keymaps
apple / swift (push) Successful in 1m5s
ci / web (push) Successful in 46s
ci / docs-site (push) Successful in 56s
ci / rust (push) Successful in 7m4s
audit / cargo-audit (push) Successful in 1m19s
android / android (push) Successful in 4m16s
windows-host / package (push) Successful in 7m53s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 1m22s
release / apple (push) Successful in 8m22s
windows-msix / package (x64, C:\Users\Public\ffmpeg, x86_64-pc-windows-msvc, C:\t) (push) Successful in 1m20s
ci / bench (push) Successful in 4m43s
decky / build-publish (push) Successful in 13s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 5s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 5s
deb / build-publish (push) Successful in 3m21s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 1m9s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 1m11s
apple / screenshots (push) Successful in 5m34s
flatpak / build-publish (push) Successful in 4m33s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 9m33s
docker / deploy-docs (push) Successful in 18s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 9m12s
apple / swift (push) Successful in 1m5s
ci / web (push) Successful in 46s
ci / docs-site (push) Successful in 56s
ci / rust (push) Successful in 7m4s
audit / cargo-audit (push) Successful in 1m19s
android / android (push) Successful in 4m16s
windows-host / package (push) Successful in 7m53s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 1m22s
release / apple (push) Successful in 8m22s
windows-msix / package (x64, C:\Users\Public\ffmpeg, x86_64-pc-windows-msvc, C:\t) (push) Successful in 1m20s
ci / bench (push) Successful in 4m43s
decky / build-publish (push) Successful in 13s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 5s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 5s
deb / build-publish (push) Successful in 3m21s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 1m9s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 1m11s
apple / screenshots (push) Successful in 5m34s
flatpak / build-publish (push) Successful in 4m33s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 9m33s
docker / deploy-docs (push) Successful in 18s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 9m12s
wlroots injector: the virtual keyboard keymap now defers to the standard XKB_DEFAULT_RULES/MODEL/LAYOUT/VARIANT/OPTIONS env vars (libxkbcommon built-ins as fallback) instead of hardcoding evdev/pc105/us, matching the libei path where the session compositor's own keymap applies. Android: Keymap gains the same positional-key coverage for non-US layouts (+ tests). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -72,7 +72,9 @@ class MainActivity : ComponentActivity() {
|
||||
KeyEvent.ACTION_UP -> false
|
||||
else -> return super.dispatchKeyEvent(event)
|
||||
}
|
||||
val vk = Keymap.toVk(event.keyCode)
|
||||
// Full-event overload: evdev scancode first (positional under ANY selected
|
||||
// physical-keyboard layout), keycode fallback — see Keymap docs.
|
||||
val vk = Keymap.toVk(event)
|
||||
if (vk != 0) {
|
||||
NativeBridge.nativeSendKey(handle, vk, down, 0)
|
||||
return true // consumed — don't let the system also act on it
|
||||
|
||||
@@ -3,13 +3,79 @@ package io.unom.punktfunk.kit
|
||||
import android.view.KeyEvent
|
||||
|
||||
/**
|
||||
* Android `KEYCODE_*` → Windows Virtual-Key code (the punktfunk wire contract; the host maps VK →
|
||||
* evdev via `inject::vk_to_evdev`). The Android analogue of the Linux client's evdev→VK table
|
||||
* (`punktfunk-client-linux/src/keymap.rs`) and the Apple client's `hidToVK`. Positional/US-layout —
|
||||
* we forward the physical key, not the typed character. Unmapped keys → 0 (the Rust side drops them).
|
||||
* Extend this alongside `punktfunk-host/src/inject.rs::vk_to_evdev` (emit only VKs the host knows).
|
||||
* Hardware key → Windows Virtual-Key code (the punktfunk wire contract: **US-positional** — we
|
||||
* forward the physical key, not the typed character; the host maps VK → evdev via
|
||||
* `inject::vk_to_evdev`). The Android analogue of the Linux client's evdev→VK table
|
||||
* (`punktfunk-client-linux/src/keymap.rs`) and the Apple client's `hidToVK`.
|
||||
*
|
||||
* Prefer [toVk] with the full [KeyEvent]: it reads the raw evdev scancode first, because
|
||||
* `KeyEvent.keyCode` is only positional under the stock US key layout — a user-selected physical
|
||||
* keyboard layout (Settings → Physical keyboard) remaps keycodes semantically (AOSP's German .kcm
|
||||
* carries `map key 21 Z` / `map key 44 Y`), which would apply the layout twice: once here, once on
|
||||
* the host (the y↔z / ü-on-ö scramble). Unmapped keys → 0 (the Rust side drops them). Extend this
|
||||
* alongside `punktfunk-host/src/inject.rs::vk_to_evdev` (emit only VKs the host knows).
|
||||
*/
|
||||
object Keymap {
|
||||
/**
|
||||
* Positional wire VK for a hardware key event: the evdev scancode table first (immune to the
|
||||
* selected physical-keyboard layout), falling back to the keycode table for events without a
|
||||
* scancode (soft keyboards, synthetic events) and for everything outside the typing area
|
||||
* (layout-invariant there, incl. gamepad buttons whose scancodes lie outside the table).
|
||||
*/
|
||||
fun toVk(event: KeyEvent): Int {
|
||||
val positional = evdevToVk(event.scanCode)
|
||||
return if (positional != 0) positional else toVk(event.keyCode)
|
||||
}
|
||||
|
||||
/**
|
||||
* Linux evdev keycode (`KeyEvent.scanCode`) → US-positional VK for the layout-**variant**
|
||||
* typing area — the same 48-key table as the Linux client's `evdev_to_vk` and the hosts'
|
||||
* fixed tables. Everything else → 0 (the keycode path is already positional for those).
|
||||
*/
|
||||
fun evdevToVk(scan: Int): Int = when (scan) {
|
||||
in 2..10 -> 0x31 + (scan - 2) // KEY_1..KEY_9
|
||||
11 -> 0x30 // KEY_0
|
||||
12 -> 0xBD // KEY_MINUS -_ VK_OEM_MINUS (DE: ß)
|
||||
13 -> 0xBB // KEY_EQUAL =+ VK_OEM_PLUS
|
||||
16 -> 0x51 // Q
|
||||
17 -> 0x57 // W
|
||||
18 -> 0x45 // E
|
||||
19 -> 0x52 // R
|
||||
20 -> 0x54 // T
|
||||
21 -> 0x59 // KEY_Y — US-Y position (QWERTZ: the Z key)
|
||||
22 -> 0x55 // U
|
||||
23 -> 0x49 // I
|
||||
24 -> 0x4F // O
|
||||
25 -> 0x50 // P
|
||||
26 -> 0xDB // KEY_LEFTBRACE [{ VK_OEM_4 (DE: ü)
|
||||
27 -> 0xDD // KEY_RIGHTBRACE ]} VK_OEM_6
|
||||
30 -> 0x41 // A
|
||||
31 -> 0x53 // S
|
||||
32 -> 0x44 // D
|
||||
33 -> 0x46 // F
|
||||
34 -> 0x47 // G
|
||||
35 -> 0x48 // H
|
||||
36 -> 0x4A // J
|
||||
37 -> 0x4B // K
|
||||
38 -> 0x4C // L
|
||||
39 -> 0xBA // KEY_SEMICOLON ;: VK_OEM_1 (DE: ö)
|
||||
40 -> 0xDE // KEY_APOSTROPHE '" VK_OEM_7 (DE: ä)
|
||||
41 -> 0xC0 // KEY_GRAVE `~ VK_OEM_3 (DE: ^)
|
||||
43 -> 0xDC // KEY_BACKSLASH \| VK_OEM_5
|
||||
44 -> 0x5A // KEY_Z — US-Z position (QWERTZ: the Y key)
|
||||
45 -> 0x58 // X
|
||||
46 -> 0x43 // C
|
||||
47 -> 0x56 // V
|
||||
48 -> 0x42 // B
|
||||
49 -> 0x4E // N
|
||||
50 -> 0x4D // M
|
||||
51 -> 0xBC // KEY_COMMA ,< VK_OEM_COMMA
|
||||
52 -> 0xBE // KEY_DOT .> VK_OEM_PERIOD
|
||||
53 -> 0xBF // KEY_SLASH /? VK_OEM_2
|
||||
86 -> 0xE2 // KEY_102ND <>| VK_OEM_102 (ISO)
|
||||
else -> 0
|
||||
}
|
||||
|
||||
fun toVk(keyCode: Int): Int = when (keyCode) {
|
||||
in KeyEvent.KEYCODE_A..KeyEvent.KEYCODE_Z -> 0x41 + (keyCode - KeyEvent.KEYCODE_A) // A–Z
|
||||
in KeyEvent.KEYCODE_0..KeyEvent.KEYCODE_9 -> 0x30 + (keyCode - KeyEvent.KEYCODE_0) // 0–9 row
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package io.unom.punktfunk.kit
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* Pure JVM test of the positional scancode table (`Keymap.evdevToVk`) — no Android runtime types
|
||||
* (the `KeyEvent` constants in the keycode table are compile-time-inlined ints). Run:
|
||||
* `./gradlew :kit:testDebugUnitTest`.
|
||||
*/
|
||||
class KeymapTest {
|
||||
/**
|
||||
* The German-scramble regression pins: the physical keys a QWERTZ board labels Z/Y/ö/ü/ä/ß
|
||||
* must leave this client as their US-position VKs, regardless of the user-selected physical
|
||||
* keyboard layout (which remaps `keyCode`, not `scanCode`).
|
||||
*/
|
||||
@Test
|
||||
fun positionalPinsForTheQwertzScramble() {
|
||||
assertEquals(0x59, Keymap.evdevToVk(21)) // KEY_Y (QWERTZ: Z key) → VK_Y
|
||||
assertEquals(0x5A, Keymap.evdevToVk(44)) // KEY_Z (QWERTZ: Y key) → VK_Z
|
||||
assertEquals(0xBA, Keymap.evdevToVk(39)) // KEY_SEMICOLON (QWERTZ: ö) → VK_OEM_1
|
||||
assertEquals(0xDB, Keymap.evdevToVk(26)) // KEY_LEFTBRACE (QWERTZ: ü) → VK_OEM_4
|
||||
assertEquals(0xDE, Keymap.evdevToVk(40)) // KEY_APOSTROPHE (QWERTZ: ä) → VK_OEM_7
|
||||
assertEquals(0xBD, Keymap.evdevToVk(12)) // KEY_MINUS (QWERTZ: ß) → VK_OEM_MINUS
|
||||
}
|
||||
|
||||
/**
|
||||
* Exactly the 48 typing-area keys are covered (10 digits + 26 letters + 12 OEM) with unique
|
||||
* VKs; everything else (nav, F-row, modifiers, gamepad buttons at 0x100+) falls through to
|
||||
* the keycode table.
|
||||
*/
|
||||
@Test
|
||||
fun tableCoversTheTypingAreaBijectively() {
|
||||
val mapped = (0..0x200).mapNotNull { sc ->
|
||||
Keymap.evdevToVk(sc).takeIf { it != 0 }?.let { sc to it }
|
||||
}
|
||||
assertEquals(48, mapped.size)
|
||||
assertEquals(48, mapped.map { it.second }.toSet().size)
|
||||
assertEquals(0, Keymap.evdevToVk(1)) // KEY_ESC — layout-invariant, keycode path
|
||||
assertEquals(0, Keymap.evdevToVk(59)) // KEY_F1
|
||||
assertEquals(0, Keymap.evdevToVk(304)) // BTN_SOUTH — gamepad, never a typing key
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user