b6f59f5000
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>
88 lines
4.0 KiB
Swift
88 lines
4.0 KiB
Swift
#if os(macOS)
|
|
import XCTest
|
|
|
|
@testable import PunktfunkKit
|
|
|
|
/// Pins the macOS flagsChanged → modifier-VK resolution (InputCapture.resolveModifier).
|
|
/// Modifier keys arrive only as flagsChanged, which carries no down-vs-up: the changed key
|
|
/// is the event's keyCode, and the direction is recovered from the flag bits — with a
|
|
/// held-state fallback for keyboards that omit the device-dependent L/R bits (the gap that
|
|
/// used to silently drop Control when the transition was diffed from those bits alone).
|
|
final class ModifierResolveTests: XCTestCase {
|
|
/// Resolve with a fixed already-held answer for the fallback path.
|
|
private func resolve(
|
|
keyCode: UInt16, rawFlags: UInt, held: Bool = false
|
|
) -> (vk: UInt32, down: Bool)? {
|
|
InputCapture.resolveModifier(keyCode: keyCode, rawFlags: rawFlags) { _ in held }
|
|
}
|
|
|
|
// MARK: Keyboards that report the device-dependent L/R bits (the common case)
|
|
|
|
func testControlPressAndReleaseWithDeviceBits() {
|
|
// Real left-Control down: NX_CONTROLMASK | NX_DEVICELCTLKEYMASK (+ misc low bits).
|
|
let down = resolve(keyCode: 59, rawFlags: 0x4_0101)
|
|
XCTAssertEqual(down?.vk, 0xA2) // VK_LCONTROL
|
|
XCTAssertEqual(down?.down, true)
|
|
// Release: the class mask is gone entirely.
|
|
let up = resolve(keyCode: 59, rawFlags: 0x100)
|
|
XCTAssertEqual(up?.vk, 0xA2)
|
|
XCTAssertEqual(up?.down, false)
|
|
}
|
|
|
|
func testRightControlUsesItsOwnDeviceBit() {
|
|
let down = resolve(keyCode: 62, rawFlags: 0x4_2000)
|
|
XCTAssertEqual(down?.vk, 0xA3) // VK_RCONTROL
|
|
XCTAssertEqual(down?.down, true)
|
|
}
|
|
|
|
func testReleasingOneOfTwoHeldControls() {
|
|
// Left goes up while right stays held: class mask still set, right device bit
|
|
// still set, LEFT device bit cleared — the left key must resolve as UP.
|
|
let leftUp = resolve(keyCode: 59, rawFlags: 0x4_2000, held: true)
|
|
XCTAssertEqual(leftUp?.vk, 0xA2)
|
|
XCTAssertEqual(leftUp?.down, false)
|
|
}
|
|
|
|
func testEverySideMapsToItsOwnVK() {
|
|
XCTAssertEqual(resolve(keyCode: 56, rawFlags: 0x2_0002)?.vk, 0xA0) // VK_LSHIFT
|
|
XCTAssertEqual(resolve(keyCode: 60, rawFlags: 0x2_0004)?.vk, 0xA1) // VK_RSHIFT
|
|
XCTAssertEqual(resolve(keyCode: 58, rawFlags: 0x8_0020)?.vk, 0xA4) // VK_LMENU
|
|
XCTAssertEqual(resolve(keyCode: 61, rawFlags: 0x8_0040)?.vk, 0xA5) // VK_RMENU
|
|
XCTAssertEqual(resolve(keyCode: 55, rawFlags: 0x10_0008)?.vk, 0x5B) // VK_LWIN
|
|
XCTAssertEqual(resolve(keyCode: 54, rawFlags: 0x10_0010)?.vk, 0x5C) // VK_RWIN
|
|
for (_, down) in [56, 60, 58, 61, 55, 54].compactMap({
|
|
self.resolve(keyCode: UInt16($0), rawFlags: 0xFF_FFFF)
|
|
}) {
|
|
XCTAssertTrue(down)
|
|
}
|
|
}
|
|
|
|
// MARK: Keyboards that DON'T report the device bits (the bug this resolver fixes)
|
|
|
|
func testControlPressWithoutDeviceBitsFallsBackToHeldState() {
|
|
// Only NX_CONTROLMASK, no low bits at all: a flag diff of the device bits sees no
|
|
// transition and drops the key — the fallback must infer DOWN from "not held yet".
|
|
let down = resolve(keyCode: 59, rawFlags: 0x4_0000, held: false)
|
|
XCTAssertEqual(down?.vk, 0xA2)
|
|
XCTAssertEqual(down?.down, true)
|
|
// And the mirror release (class cleared) still resolves as UP.
|
|
let up = resolve(keyCode: 59, rawFlags: 0, held: true)
|
|
XCTAssertEqual(up?.down, false)
|
|
}
|
|
|
|
func testClassBitStillSetButKeyAlreadyHeldResolvesUp() {
|
|
// Device-bit-less keyboard, second same-class key still holding the class bit:
|
|
// the best available answer for the key that changed is to flip its held state.
|
|
let up = resolve(keyCode: 59, rawFlags: 0x4_0000, held: true)
|
|
XCTAssertEqual(up?.down, false)
|
|
}
|
|
|
|
// MARK: Modifiers the host doesn't consume on this path
|
|
|
|
func testFnAndCapsLockResolveToNothing() {
|
|
XCTAssertNil(resolve(keyCode: 63, rawFlags: 0x80_0000)) // Fn / Globe
|
|
XCTAssertNil(resolve(keyCode: 57, rawFlags: 0x1_0000)) // Caps Lock
|
|
}
|
|
}
|
|
#endif
|