mirror of
https://github.com/netbirdio/netbird.git
synced 2026-08-08 11:05:16 -04:00
Stamp modifiers on injected macOS key events so shifted input works
This commit is contained in:
@@ -47,12 +47,26 @@ const (
|
||||
// letter is treated as a menu accelerator.
|
||||
kCGEventFlagMaskSecondaryFn uint64 = 0x00800000
|
||||
|
||||
// The modifier bits macOS reads off an event to decide what a keystroke
|
||||
// means. A synthesised key event carries its own modifier state: pressing
|
||||
// the Shift keycode first does not shift the key that follows, which is why
|
||||
// every posted event has to spell its modifiers out.
|
||||
kCGEventFlagMaskAlphaShift uint64 = 0x00010000
|
||||
kCGEventFlagMaskShift uint64 = 0x00020000
|
||||
kCGEventFlagMaskControl uint64 = 0x00040000
|
||||
kCGEventFlagMaskAlternate uint64 = 0x00080000
|
||||
kCGEventFlagMaskCommand uint64 = 0x00100000
|
||||
|
||||
// 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
|
||||
// never see the dblclick (window-bar maximize, text word-select, ...).
|
||||
kCGMouseEventClickState int32 = 1
|
||||
|
||||
// kCGKeyboardEventKeycode (event field 9) names the physical key on a
|
||||
// flagsChanged event, which is how apps tell left Shift from right.
|
||||
kCGKeyboardEventKeycode int32 = 9
|
||||
|
||||
// doubleClickWindow is the upper bound on the gap between two
|
||||
// down events that still counts as a multi-click. macOS reads the
|
||||
// user's setting from CGEventSourceGetDoubleClickInterval; 500ms is
|
||||
@@ -324,6 +338,9 @@ type MacInputInjector struct {
|
||||
// rather than by event count.
|
||||
axNextTry atomic.Int64
|
||||
axAsks atomic.Int32
|
||||
// modifiers is the CGEventFlags state the remote client has built up with
|
||||
// its modifier key events, stamped onto everything posted afterwards.
|
||||
modifiers atomic.Uint64
|
||||
}
|
||||
|
||||
// NewMacInputInjector creates a macOS input injector.
|
||||
@@ -527,27 +544,78 @@ func (m *MacInputInjector) InjectKeyScancode(scancode, keysym uint32, down bool)
|
||||
// active" for the next key (e.g. the next letter activates a menu
|
||||
// accelerator).
|
||||
func (m *MacInputInjector) postMacKey(src uintptr, keycode uint16, down bool) {
|
||||
if bit, ok := modifierFlagFor(keycode); ok {
|
||||
m.postModifier(src, keycode, down, bit)
|
||||
return
|
||||
}
|
||||
|
||||
fnShifted := isFnShiftedKeycode(keycode)
|
||||
if fnShifted && down {
|
||||
postFnFlagsChanged(src, true)
|
||||
}
|
||||
event := cgEventCreateKeyboardEvent(src, keycode, down)
|
||||
if event == 0 {
|
||||
if fnShifted && !down {
|
||||
postFnFlagsChanged(src, false)
|
||||
}
|
||||
return
|
||||
flags := m.modifiers.Load()
|
||||
if fnShifted {
|
||||
flags |= kCGEventFlagMaskSecondaryFn
|
||||
}
|
||||
if fnShifted && cgEventSetFlags != nil {
|
||||
cgEventSetFlags(event, kCGEventFlagMaskSecondaryFn)
|
||||
}
|
||||
cgEventPost(kCGHIDEventTap, event)
|
||||
cfRelease(event)
|
||||
m.postKeyEvent(src, keycode, down, flags)
|
||||
if fnShifted && !down {
|
||||
postFnFlagsChanged(src, false)
|
||||
}
|
||||
}
|
||||
|
||||
// postModifier records the new modifier state and reports it the way hardware
|
||||
// does, as a flagsChanged event rather than a keystroke. Apps read the modifier
|
||||
// bits off each event they receive, so the state has to be attached to
|
||||
// everything posted afterwards, which is what m.modifiers is for.
|
||||
func (m *MacInputInjector) postModifier(src uintptr, keycode uint16, down bool, bit uint64) {
|
||||
var flags uint64
|
||||
for {
|
||||
old := m.modifiers.Load()
|
||||
flags = old | bit
|
||||
if !down {
|
||||
flags = old &^ bit
|
||||
}
|
||||
if m.modifiers.CompareAndSwap(old, flags) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if cgEventCreateForInput == nil || cgEventSetType == nil || cgEventSetFlags == nil {
|
||||
// No way to synthesise the transition, so fall back to the keystroke.
|
||||
m.postKeyEvent(src, keycode, down, flags)
|
||||
return
|
||||
}
|
||||
event := cgEventCreateForInput(src)
|
||||
if event == 0 {
|
||||
return
|
||||
}
|
||||
cgEventSetType(event, kCGEventFlagsChanged)
|
||||
// The keycode tells apps which physical key moved; several read it to tell
|
||||
// left from right.
|
||||
if cgEventSetIntegerValueField != nil {
|
||||
cgEventSetIntegerValueField(event, kCGKeyboardEventKeycode, int64(keycode))
|
||||
}
|
||||
cgEventSetFlags(event, flags)
|
||||
cgEventPost(kCGHIDEventTap, event)
|
||||
cfRelease(event)
|
||||
}
|
||||
|
||||
// postKeyEvent posts one key event carrying exactly the given modifiers. The
|
||||
// flags are always set, including when empty: the event source is the combined
|
||||
// session state, so an event left alone inherits whatever the local user happens
|
||||
// to be holding down.
|
||||
func (m *MacInputInjector) postKeyEvent(src uintptr, keycode uint16, down bool, flags uint64) {
|
||||
event := cgEventCreateKeyboardEvent(src, keycode, down)
|
||||
if event == 0 {
|
||||
return
|
||||
}
|
||||
if cgEventSetFlags != nil {
|
||||
cgEventSetFlags(event, flags)
|
||||
}
|
||||
cgEventPost(kCGHIDEventTap, event)
|
||||
cfRelease(event)
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -774,14 +842,13 @@ func (m *MacInputInjector) TypeText(text string) {
|
||||
break
|
||||
}
|
||||
count++
|
||||
typeRune(src, r)
|
||||
m.typeRune(src, r)
|
||||
}
|
||||
}
|
||||
|
||||
// typeRune emits the press/release events for a single ASCII rune, framing
|
||||
// the keystroke with Shift-down/up when required by the keysym.
|
||||
func typeRune(src uintptr, r rune) {
|
||||
const shiftKey = uint16(0x38) // kVK_Shift
|
||||
func (m *MacInputInjector) typeRune(src uintptr, r rune) {
|
||||
keysym, shift, ok := keysymForASCIIRune(r)
|
||||
if !ok {
|
||||
return
|
||||
@@ -790,23 +857,15 @@ func typeRune(src uintptr, r rune) {
|
||||
if keycode == 0xFFFF {
|
||||
return
|
||||
}
|
||||
// The shift travels on the key event itself. Framing the keystroke with
|
||||
// Shift key events instead leaves the letter unshifted, which is how pasted
|
||||
// text used to arrive in lower case.
|
||||
var flags uint64
|
||||
if shift {
|
||||
postKey(src, shiftKey, true)
|
||||
flags = kCGEventFlagMaskShift
|
||||
}
|
||||
postKey(src, keycode, true)
|
||||
postKey(src, keycode, false)
|
||||
if shift {
|
||||
postKey(src, shiftKey, false)
|
||||
}
|
||||
}
|
||||
|
||||
func postKey(src uintptr, keycode uint16, down bool) {
|
||||
e := cgEventCreateKeyboardEvent(src, keycode, down)
|
||||
if e == 0 {
|
||||
return
|
||||
}
|
||||
cgEventPost(kCGHIDEventTap, e)
|
||||
cfRelease(e)
|
||||
m.postKeyEvent(src, keycode, true, flags)
|
||||
m.postKeyEvent(src, keycode, false, flags)
|
||||
}
|
||||
|
||||
// GetClipboard reads the macOS clipboard using pbpaste.
|
||||
@@ -827,6 +886,23 @@ func (m *MacInputInjector) Close() {
|
||||
releasePreventIdleSleep()
|
||||
}
|
||||
|
||||
// modifierFlagFor maps a modifier keycode to the CGEventFlags bit it controls.
|
||||
func modifierFlagFor(keycode uint16) (uint64, bool) {
|
||||
switch keycode {
|
||||
case 0x38, 0x3C: // Shift, Right Shift
|
||||
return kCGEventFlagMaskShift, true
|
||||
case 0x3B, 0x3E: // Control, Right Control
|
||||
return kCGEventFlagMaskControl, true
|
||||
case 0x3A, 0x3D: // Option, Right Option
|
||||
return kCGEventFlagMaskAlternate, true
|
||||
case 0x37, 0x36: // Command, Right Command
|
||||
return kCGEventFlagMaskCommand, true
|
||||
case 0x39: // Caps Lock
|
||||
return kCGEventFlagMaskAlphaShift, true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func keysymToMacKeycode(keysym uint32) uint16 {
|
||||
if keysym >= 0x61 && keysym <= 0x7a {
|
||||
return asciiToMacKey[keysym-0x61]
|
||||
|
||||
Reference in New Issue
Block a user