mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-24 07:39:07 +02: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.
|
// letter is treated as a menu accelerator.
|
||||||
kCGEventFlagMaskSecondaryFn uint64 = 0x00800000
|
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
|
// kCGMouseEventClickState (event field 1) tells macOS how many
|
||||||
// consecutive clicks of this button have happened. Without it, a
|
// consecutive clicks of this button have happened. Without it, a
|
||||||
// double click looks like two independent single clicks and apps
|
// double click looks like two independent single clicks and apps
|
||||||
// never see the dblclick (window-bar maximize, text word-select, ...).
|
// never see the dblclick (window-bar maximize, text word-select, ...).
|
||||||
kCGMouseEventClickState int32 = 1
|
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
|
// doubleClickWindow is the upper bound on the gap between two
|
||||||
// down events that still counts as a multi-click. macOS reads the
|
// down events that still counts as a multi-click. macOS reads the
|
||||||
// user's setting from CGEventSourceGetDoubleClickInterval; 500ms is
|
// user's setting from CGEventSourceGetDoubleClickInterval; 500ms is
|
||||||
@@ -324,6 +338,9 @@ type MacInputInjector struct {
|
|||||||
// rather than by event count.
|
// rather than by event count.
|
||||||
axNextTry atomic.Int64
|
axNextTry atomic.Int64
|
||||||
axAsks atomic.Int32
|
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.
|
// 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
|
// active" for the next key (e.g. the next letter activates a menu
|
||||||
// accelerator).
|
// accelerator).
|
||||||
func (m *MacInputInjector) postMacKey(src uintptr, keycode uint16, down bool) {
|
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)
|
fnShifted := isFnShiftedKeycode(keycode)
|
||||||
if fnShifted && down {
|
if fnShifted && down {
|
||||||
postFnFlagsChanged(src, true)
|
postFnFlagsChanged(src, true)
|
||||||
}
|
}
|
||||||
event := cgEventCreateKeyboardEvent(src, keycode, down)
|
flags := m.modifiers.Load()
|
||||||
if event == 0 {
|
if fnShifted {
|
||||||
if fnShifted && !down {
|
flags |= kCGEventFlagMaskSecondaryFn
|
||||||
postFnFlagsChanged(src, false)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
if fnShifted && cgEventSetFlags != nil {
|
m.postKeyEvent(src, keycode, down, flags)
|
||||||
cgEventSetFlags(event, kCGEventFlagMaskSecondaryFn)
|
|
||||||
}
|
|
||||||
cgEventPost(kCGHIDEventTap, event)
|
|
||||||
cfRelease(event)
|
|
||||||
if fnShifted && !down {
|
if fnShifted && !down {
|
||||||
postFnFlagsChanged(src, false)
|
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
|
// postFnFlagsChanged emits a synthetic Fn modifier transition so the
|
||||||
// system updates its global modifier state to match the key events we
|
// system updates its global modifier state to match the key events we
|
||||||
// post for the navigation cluster. Without this, posting a Fn-flagged
|
// post for the navigation cluster. Without this, posting a Fn-flagged
|
||||||
@@ -774,14 +842,13 @@ func (m *MacInputInjector) TypeText(text string) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
count++
|
count++
|
||||||
typeRune(src, r)
|
m.typeRune(src, r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// typeRune emits the press/release events for a single ASCII rune, framing
|
// typeRune emits the press/release events for a single ASCII rune, framing
|
||||||
// the keystroke with Shift-down/up when required by the keysym.
|
// the keystroke with Shift-down/up when required by the keysym.
|
||||||
func typeRune(src uintptr, r rune) {
|
func (m *MacInputInjector) typeRune(src uintptr, r rune) {
|
||||||
const shiftKey = uint16(0x38) // kVK_Shift
|
|
||||||
keysym, shift, ok := keysymForASCIIRune(r)
|
keysym, shift, ok := keysymForASCIIRune(r)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
@@ -790,23 +857,15 @@ func typeRune(src uintptr, r rune) {
|
|||||||
if keycode == 0xFFFF {
|
if keycode == 0xFFFF {
|
||||||
return
|
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 {
|
if shift {
|
||||||
postKey(src, shiftKey, true)
|
flags = kCGEventFlagMaskShift
|
||||||
}
|
}
|
||||||
postKey(src, keycode, true)
|
m.postKeyEvent(src, keycode, true, flags)
|
||||||
postKey(src, keycode, false)
|
m.postKeyEvent(src, keycode, false, flags)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetClipboard reads the macOS clipboard using pbpaste.
|
// GetClipboard reads the macOS clipboard using pbpaste.
|
||||||
@@ -827,6 +886,23 @@ func (m *MacInputInjector) Close() {
|
|||||||
releasePreventIdleSleep()
|
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 {
|
func keysymToMacKeycode(keysym uint32) uint16 {
|
||||||
if keysym >= 0x61 && keysym <= 0x7a {
|
if keysym >= 0x61 && keysym <= 0x7a {
|
||||||
return asciiToMacKey[keysym-0x61]
|
return asciiToMacKey[keysym-0x61]
|
||||||
|
|||||||
Reference in New Issue
Block a user