From ef4ea2e311c9fb1644dc9df6b5221f9ea4af925e Mon Sep 17 00:00:00 2001 From: Viktor Liu Date: Wed, 20 May 2026 18:03:38 +0200 Subject: [PATCH] Set Fn flag on macOS navigation keycodes so the next key isn't treated as Fn-modified --- client/vnc/server/input_darwin.go | 38 ++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/client/vnc/server/input_darwin.go b/client/vnc/server/input_darwin.go index 2e8542aa8..dd5ebe4c2 100644 --- a/client/vnc/server/input_darwin.go +++ b/client/vnc/server/input_darwin.go @@ -36,6 +36,15 @@ const ( kCGHIDEventTap int32 = 0 + // kCGEventFlagMaskSecondaryFn is the CGEventFlags bit Apple sets when + // a key was activated via the Fn modifier on internal keyboards. The + // navigation cluster (ForwardDelete, Home, End, PageUp, PageDown, + // Help/Insert, arrows) lives in the Fn-shifted region of an Apple + // keyboard, so synthesising those keycodes without this bit leaves the + // system in a confused "Fn implied" state where the next plain + // letter is treated as a menu accelerator. + kCGEventFlagMaskSecondaryFn uint64 = 0x00800000 + // kCGMouseEventClickState (event field 1) tells macOS how many // consecutive clicks of this button have happened. Without it, a // double click looks like two independent single clicks and apps @@ -64,6 +73,7 @@ var ( cgEventCreateMouseEvent func(uintptr, int32, float64, float64, int32) uintptr cgEventPost func(int32, uintptr) cgEventSetIntegerValueField func(uintptr, int32, int64) + cgEventSetFlags func(uintptr, uint64) // CGEventCreateScrollWheelEvent is variadic, call via SyscallN. cgEventCreateScrollWheelEventAddr uintptr @@ -125,6 +135,7 @@ func initDarwinInput() { purego.RegisterLibFunc(&cgEventCreateMouseEvent, cg, "CGEventCreateMouseEvent") purego.RegisterLibFunc(&cgEventPost, cg, "CGEventPost") purego.RegisterLibFunc(&cgEventSetIntegerValueField, cg, "CGEventSetIntegerValueField") + purego.RegisterLibFunc(&cgEventSetFlags, cg, "CGEventSetFlags") sym, err := purego.Dlsym(cg, "CGEventCreateScrollWheelEvent") if err == nil { @@ -397,16 +408,41 @@ func (m *MacInputInjector) InjectKeyScancode(scancode, keysym uint32, down bool) m.postMacKey(src, vk, down) } -// postMacKey emits a single key down/up event via Core Graphics. +// postMacKey emits a single key down/up event via Core Graphics. The +// Fn flag is attached for keycodes that live in the Fn-shifted region of +// an Apple keyboard so the system doesn't treat the next plain key as +// Fn-modified. func (m *MacInputInjector) postMacKey(src uintptr, keycode uint16, down bool) { event := cgEventCreateKeyboardEvent(src, keycode, down) if event == 0 { return } + if isFnShiftedKeycode(keycode) && cgEventSetFlags != nil { + cgEventSetFlags(event, kCGEventFlagMaskSecondaryFn) + } cgEventPost(kCGHIDEventTap, event) cfRelease(event) } +// isFnShiftedKeycode reports whether keycode is one of the Apple +// navigation/edit keys that hardware produces with the Fn modifier held. +func isFnShiftedKeycode(keycode uint16) bool { + switch keycode { + case 0x72, // Help / Insert + 0x73, // Home + 0x74, // PageUp + 0x75, // ForwardDelete + 0x77, // End + 0x79, // PageDown + 0x7B, // Left + 0x7C, // Right + 0x7D, // Down + 0x7E: // Up + return true + } + return false +} + // InjectPointer simulates mouse movement and button events. func (m *MacInputInjector) InjectPointer(buttonMask uint16, px, py, serverW, serverH int) { wakeDisplay()