fix(apple): resolve macOS modifiers by keyCode — Control was silently dropped
apple / swift (push) Successful in 1m40s
apple / screenshots (push) Has been cancelled
android / android (push) Has been cancelled
arch / build-publish (push) Has been cancelled
ci / rust (push) Has been cancelled
ci / web (push) Has been cancelled
ci / docs-site (push) Has been cancelled
ci / bench (push) Has been cancelled
deb / build-publish (push) Has been cancelled
decky / build-publish (push) Has been cancelled
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Has been cancelled
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Has been cancelled
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Has been cancelled
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Has been cancelled
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Has been cancelled
docker / deploy-docs (push) Has been cancelled
release / apple (push) Has been cancelled
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled

Modifier keys arrive only as flagsChanged, and the direction was recovered
by diffing the device-dependent L/R bits (NX_DEVICE*KEYMASK) alone. Those
bits are undocumented and some keyboards omit them (only the class bit,
e.g. NX_CONTROLMASK, is set), so the diff saw no transition and the key
never reached the host — no Ctrl shortcuts. SDL/Moonlight key off the
event's keyCode for exactly this reason; do the same: keyCode names the
changed key, the class bit says up, the device bits (when present) pick
the side, and a tracked-held-state flip covers keyboards without them.

PUNKTFUNK_INPUT_DEBUG=1 now also logs every flagsChanged (keyCode + raw
flags) so a field report is diagnosable from client logs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-06 18:29:33 +02:00
parent 7649ccb66b
commit b6f59f5000
4 changed files with 154 additions and 48 deletions
@@ -98,5 +98,23 @@ extension InputCapture {
m[0x47] = 0x90 // KP clear sits where NumLock is VK_NUMLOCK. (KP equals 0x51 dropped.)
return m
}()
/// NSEvent.keyCode of each modifier key (kVK_Shift & co. modifiers arrive only as
/// flagsChanged) its Windows VK plus the `NSEvent.modifierFlags` bits that describe
/// it: `classMask` is the device-INDEPENDENT NX_*MASK for the modifier class,
/// `deviceBit`/`siblingBit` the device-dependent bits (LOW 16 bits, NX_DEVICE*KEYMASK
/// in IOLLEvent.h) for this key and its opposite-side twin. Consumed by
/// `resolveModifier`, which explains why both kinds of bit are needed.
static let modifierBits:
[UInt16: (vk: UInt32, classMask: UInt, deviceBit: UInt, siblingBit: UInt)] = [
56: (0xA0, 0x2_0000, 0x2, 0x4), // left shift VK_LSHIFT
60: (0xA1, 0x2_0000, 0x4, 0x2), // right shift VK_RSHIFT
59: (0xA2, 0x4_0000, 0x1, 0x2000), // left control VK_LCONTROL
62: (0xA3, 0x4_0000, 0x2000, 0x1), // right control VK_RCONTROL
58: (0xA4, 0x8_0000, 0x20, 0x40), // left option VK_LMENU
61: (0xA5, 0x8_0000, 0x40, 0x20), // right option VK_RMENU
55: (0x5B, 0x10_0000, 0x8, 0x10), // left command VK_LWIN
54: (0x5C, 0x10_0000, 0x10, 0x8), // right command VK_RWIN
]
#endif
}