Emit explicit Fn flagsChanged transitions around macOS navigation keys

This commit is contained in:
Viktor Liu
2026-05-21 12:30:14 +02:00
parent 98d533c8e8
commit 2f4ddf0796
+47 -6
View File
@@ -27,6 +27,7 @@ const (
kCGEventRightMouseDragged int32 = 7 kCGEventRightMouseDragged int32 = 7
kCGEventKeyDown int32 = 10 kCGEventKeyDown int32 = 10
kCGEventKeyUp int32 = 11 kCGEventKeyUp int32 = 11
kCGEventFlagsChanged int32 = 12
kCGEventOtherMouseDown int32 = 25 kCGEventOtherMouseDown int32 = 25
kCGEventOtherMouseUp int32 = 26 kCGEventOtherMouseUp int32 = 26
@@ -74,6 +75,8 @@ var (
cgEventPost func(int32, uintptr) cgEventPost func(int32, uintptr)
cgEventSetIntegerValueField func(uintptr, int32, int64) cgEventSetIntegerValueField func(uintptr, int32, int64)
cgEventSetFlags func(uintptr, uint64) cgEventSetFlags func(uintptr, uint64)
cgEventSetType func(uintptr, int32)
cgEventCreateForInput func(uintptr) uintptr
// CGEventCreateScrollWheelEvent is variadic, call via SyscallN. // CGEventCreateScrollWheelEvent is variadic, call via SyscallN.
cgEventCreateScrollWheelEventAddr uintptr cgEventCreateScrollWheelEventAddr uintptr
@@ -136,6 +139,8 @@ func initDarwinInput() {
purego.RegisterLibFunc(&cgEventPost, cg, "CGEventPost") purego.RegisterLibFunc(&cgEventPost, cg, "CGEventPost")
purego.RegisterLibFunc(&cgEventSetIntegerValueField, cg, "CGEventSetIntegerValueField") purego.RegisterLibFunc(&cgEventSetIntegerValueField, cg, "CGEventSetIntegerValueField")
purego.RegisterLibFunc(&cgEventSetFlags, cg, "CGEventSetFlags") purego.RegisterLibFunc(&cgEventSetFlags, cg, "CGEventSetFlags")
purego.RegisterLibFunc(&cgEventSetType, cg, "CGEventSetType")
purego.RegisterLibFunc(&cgEventCreateForInput, cg, "CGEventCreate")
sym, err := purego.Dlsym(cg, "CGEventCreateScrollWheelEvent") sym, err := purego.Dlsym(cg, "CGEventCreateScrollWheelEvent")
if err == nil { if err == nil {
@@ -408,18 +413,54 @@ func (m *MacInputInjector) InjectKeyScancode(scancode, keysym uint32, down bool)
m.postMacKey(src, vk, down) m.postMacKey(src, vk, down)
} }
// postMacKey emits a single key down/up event via Core Graphics. The // postMacKey emits a single key down/up event via Core Graphics. For
// Fn flag is attached for keycodes that live in the Fn-shifted region of // keycodes that live in the Fn-shifted region of an Apple keyboard we
// an Apple keyboard so the system doesn't treat the next plain key as // also emit explicit flagsChanged events around the keypress: posting
// Fn-modified. // the Fn flag on the key event alone leaves macOS's modifier state
// machine without a matching transition, which manifests as "Fn stays
// active" for the next key (e.g. the next letter activates a menu
// accelerator).
func (m *MacInputInjector) postMacKey(src uintptr, keycode uint16, down bool) { func (m *MacInputInjector) postMacKey(src uintptr, keycode uint16, down bool) {
fnShifted := isFnShiftedKeycode(keycode)
if fnShifted && down {
postFnFlagsChanged(src, true)
}
event := cgEventCreateKeyboardEvent(src, keycode, down) event := cgEventCreateKeyboardEvent(src, keycode, down)
if event == 0 {
if fnShifted && !down {
postFnFlagsChanged(src, false)
}
return
}
if fnShifted && cgEventSetFlags != nil {
cgEventSetFlags(event, kCGEventFlagMaskSecondaryFn)
}
cgEventPost(kCGHIDEventTap, event)
cfRelease(event)
if fnShifted && !down {
postFnFlagsChanged(src, false)
}
}
// postFnFlagsChanged emits a synthetic Fn modifier transition so the
// system updates its global modifier state to match the key events we
// post for the navigation cluster. Without this, posting a Fn-flagged
// key event leaves macOS thinking Fn is still held after the key is
// released.
func postFnFlagsChanged(src uintptr, fnOn bool) {
if cgEventCreateForInput == nil || cgEventSetType == nil || cgEventSetFlags == nil {
return
}
event := cgEventCreateForInput(src)
if event == 0 { if event == 0 {
return return
} }
if isFnShiftedKeycode(keycode) && cgEventSetFlags != nil { cgEventSetType(event, kCGEventFlagsChanged)
cgEventSetFlags(event, kCGEventFlagMaskSecondaryFn) var flags uint64
if fnOn {
flags = kCGEventFlagMaskSecondaryFn
} }
cgEventSetFlags(event, flags)
cgEventPost(kCGHIDEventTap, event) cgEventPost(kCGHIDEventTap, event)
cfRelease(event) cfRelease(event)
} }