forked from lodestone/macpaste
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmacpaste.c
More file actions
1102 lines (1024 loc) · 42.1 KB
/
Copy pathmacpaste.c
File metadata and controls
1102 lines (1024 loc) · 42.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Public Domain License 2016
//
// Simulate right-handed unix/linux X11 middle-mouse-click copy and paste.
//
// References:
// http://stackoverflow.com/questions/3134901/mouse-tracking-daemon
// http://stackoverflow.com/questions/2379867/simulating-key-press-events-in-mac-os-x#2380280
//
// Compile with:
// gcc -O2 -Wall -Wextra -framework ApplicationServices -o macpaste macpaste.c
//
// Start with:
// ./macpaste
//
// Terminate with Ctrl+C
//
// Optional click-through (-t): a left click on a background window's content is
// re-posted after the window is raised, so the first click also acts on the
// content (links, buttons) instead of only focusing the window. -x "App" disables
// this per app. Off by default.
//
// Windows are brought forward with the Accessibility API (kAXRaiseAction), never
// by synthesising a click. A synthetic click can only ask the window server to
// deliver it and hope the target app treats it as a raise; raising asks the
// window itself, and names the exact window rather than whichever one its app
// considers main.
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <limits.h>
#include <time.h>
#include <search.h>
#include <libproc.h>
#include <ApplicationServices/ApplicationServices.h>
#include <CoreFoundation/CoreFoundation.h>
#include <CoreGraphics/CoreGraphics.h>
#define kVK_ANSI_C 0x08
#define kVK_ANSI_V 0x09
#define kVK_Command 0x37
#define kVK_Control 0x3B
#define DOUBLE_CLICK_MILLIS 500
#define DOUBLE_CLICK_DISTANCE_PX 10 // two clicks farther apart than this aren't a double-click
#define DRAG_THRESHOLD_PX 5
// Time for the focus click to land before the paste keystroke follows it. This
// used to be 1 ms, which was never actually exercised: the click was posted to
// the annotated tap and no application ever received it. Now that it is
// delivered, the target has to process it and move its caret first, which is a
// round trip to another process -- paste ahead of that and the text lands where
// the caret used to be. Blocking the tap callback this long is fine; it costs
// one middle click and stays far below the tap's own disable-by-timeout.
#define PASTE_DELAY_NS 15000000LL // 15 ms
#define MODIFIER_DELAY_NS 1000000LL // 1 ms, lets the modifier register before the key
#define MAX_WINDOW_NAME_SIZE 400
#define CLICK_THROUGH_DELAY_SECONDS 0.1 // first check for app activation
// Activation latency varies with how busy the target app is, so poll for it
// instead of assuming it has completed by a fixed deadline: a single check at
// CLICK_THROUGH_DELAY_SECONDS drops the re-post whenever activation runs long.
#define CLICK_THROUGH_POLL_SECONDS 0.025
#define CLICK_THROUGH_MAX_ATTEMPTS 16 // ~0.5s total before giving up
// Accessibility calls are synchronous IPC into the target app and several run
// inside the event tap callback, where blocking stalls the whole input stream.
// Bound them below the tap's own disable-by-timeout so an unresponsive app costs
// one failed lookup instead of a frozen cursor. Measured cold worst case for
// AXUIElementCopyElementAtPosition on this machine was ~52ms, so keep healthy
// margin over that; too tight a value silently disables window matching.
#define AX_MESSAGING_TIMEOUT_SECONDS 0.25f
// Depth cap for walking an element's parents up to its window, for apps that
// publish neither kAXWindowAttribute nor kAXTopLevelUIElementAttribute. Real
// hierarchies are nowhere near this deep; the cap is there so a chain that
// cycles can't spin inside the event tap callback.
#define AX_PARENT_WALK_MAX 32
// Stamped on every mouse event we post. The session tap is upstream of our own
// tap, so these come straight back to mouseCallback; unmarked, a re-posted click
// would look like a fresh user click and start a second click-through, or land
// close enough in time to count as a double-click and fire a spurious copy.
#define MACPASTE_SYNTHETIC 0x6D6370737465LL // "mcpste"
static bool gIsDragging = false;
static long long gPrevClickTime = 0;
static long long gCurClickTime = 0;
static CGPoint gPrevClickPoint;
static CGPoint gCurClickPoint;
static CGPoint gDragStartPoint;
// Keyboard posts go to the annotated tap, which routes them to the focused app.
static CGEventTapLocation gTapA = kCGAnnotatedSessionEventTap;
// Mouse posts must not. Measured on this machine: a click posted to the
// annotated tap is never delivered to any application, frontmost or not, while
// the same click posted to the session tap always is. Everything synthetic we
// aim at a window therefore goes here.
static CGEventTapLocation gTapMouse = kCGSessionEventTap;
static CFMachPortRef gEventTap;
static AXUIElementRef gSystemWide;
static CGEventFlags gCommandKey = kCGEventFlagMaskCommand;
static bool gVerbose = false;
static bool gClickThrough = false;
static bool gClickThroughExclusions = false;
static bool gClickThroughPending = false;
static CGPoint gClickThroughPoint;
static int gClickThroughClickState = 1;
static CGEventFlags gClickThroughFlags = 0;
static pid_t gClickThroughPid = -1;
static AXUIElementRef gClickThroughWindow = NULL; // retained while a click is pending
struct lookup {
bool skipWindow;
bool noFocus;
bool noClickThrough;
};
static long long now(void) {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
long long milliseconds = ts.tv_sec * 1000LL + ts.tv_nsec / 1000000; // calculate milliseconds
return milliseconds;
}
static char *asciiLowerDup(const char *s) {
size_t len = strlen(s);
char *out = malloc(len + 1);
if (NULL == out) {
return NULL;
}
for (size_t i = 0; i < len; i++) {
char c = s[i];
out[i] = (c >= 'A' && c <= 'Z') ? (char)(c + 'a' - 'A') : c;
}
out[len] = '\0';
return out;
}
// Case-fold for hash lookups. App display names are UTF-8, so fold via CFString
// (locale-independent) rather than ASCII-only, otherwise -s/-n/-x silently stay
// case-sensitive for any non-ASCII character in a name.
static char *lowerDup(const char *s) {
CFStringRef str = CFStringCreateWithCString(kCFAllocatorDefault, s, kCFStringEncodingUTF8);
if (NULL == str) {
return asciiLowerDup(s); // not valid UTF-8; fold what we can
}
CFMutableStringRef folded = CFStringCreateMutableCopy(kCFAllocatorDefault, 0, str);
CFRelease(str);
if (NULL == folded) {
return NULL;
}
CFStringLowercase(folded, NULL);
CFIndex bufLen = CFStringGetMaximumSizeForEncoding(CFStringGetLength(folded),
kCFStringEncodingUTF8) + 1;
char *out = malloc((size_t)bufLen);
if (NULL == out) {
CFRelease(folded);
return NULL;
}
if (!CFStringGetCString(folded, out, bufLen, kCFStringEncodingUTF8)) {
free(out);
CFRelease(folded);
return NULL;
}
CFRelease(folded);
return out;
}
static bool copyBasename(const char *path, char *buf, size_t buf_len) {
const char *base = strrchr(path, '/');
base = (base != NULL) ? base + 1 : path;
int n = snprintf(buf, buf_len, "%s", base);
return n >= 0 && (size_t)n < buf_len; // a truncated name would never match anyway
}
static bool findBundlePath(const char *execPath, char *out, size_t out_len) {
char dir[PROC_PIDPATHINFO_MAXSIZE]; // proc_pidpath() paths can exceed PATH_MAX
if (strlen(execPath) >= sizeof(dir)) {
return false;
}
snprintf(dir, sizeof(dir), "%s", execPath);
char *slash = strrchr(dir, '/');
while (slash != NULL && slash != dir) {
*slash = '\0';
size_t len = strlen(dir);
if (len >= 4 && strcmp(dir + len - 4, ".app") == 0) {
if (len >= out_len) {
return false; // rather than silently truncate to the wrong bundle
}
snprintf(out, out_len, "%s", dir);
return true;
}
slash = strrchr(dir, '/');
}
return false;
}
static bool displayNameForExecutable(const char *execPath, char *buf, size_t buf_len) {
bool ok = false;
CFURLRef url = NULL;
CFBundleRef bundle = NULL;
char bundlePath[PATH_MAX];
if (findBundlePath(execPath, bundlePath, sizeof(bundlePath))) {
url = CFURLCreateFromFileSystemRepresentation(kCFAllocatorDefault,
(const UInt8 *)bundlePath,
strlen(bundlePath), true);
}
if (url != NULL) {
bundle = CFBundleCreate(kCFAllocatorDefault, url);
CFRelease(url);
}
if (bundle != NULL) {
CFStringRef display = NULL;
CFTypeRef displayVal = CFBundleGetValueForInfoDictionaryKey(bundle, CFSTR("CFBundleDisplayName"));
if (displayVal != NULL && CFGetTypeID(displayVal) == CFStringGetTypeID()) {
display = (CFStringRef)CFRetain(displayVal);
}
if (display == NULL) {
CFTypeRef nameVal = CFBundleGetValueForInfoDictionaryKey(bundle, kCFBundleNameKey);
if (nameVal != NULL && CFGetTypeID(nameVal) == CFStringGetTypeID()) {
display = (CFStringRef)CFRetain(nameVal);
}
}
if (display != NULL) {
ok = CFStringGetCString(display, buf, buf_len, kCFStringEncodingUTF8);
CFRelease(display);
}
CFRelease(bundle);
}
if (!ok) {
return copyBasename(execPath, buf, buf_len);
}
return true;
}
static bool elementIsWindow(AXUIElementRef el) {
CFTypeRef role = NULL;
if (AXUIElementCopyAttributeValue(el, kAXRoleAttribute, &role) != kAXErrorSuccess ||
role == NULL) {
return false;
}
bool isWindow = CFGetTypeID(role) == CFStringGetTypeID() && CFEqual(role, kAXWindowRole);
CFRelease(role);
return isWindow;
}
// The window containing a hit-tested element. AXUIElementCopyElementAtPosition()
// answers with the deepest thing under the point -- a button, a table cell -- and
// it is the window above it that can be raised. Most apps publish a shortcut
// attribute straight to it; the rest have to be climbed. Returns a retained
// element, or NULL if the window can't be resolved.
static AXUIElementRef copyWindowForElement(AXUIElementRef el) {
if (elementIsWindow(el)) {
return (AXUIElementRef)CFRetain(el);
}
CFStringRef shortcuts[2];
shortcuts[0] = kAXWindowAttribute;
shortcuts[1] = kAXTopLevelUIElementAttribute;
for (size_t i = 0; i < sizeof(shortcuts) / sizeof(shortcuts[0]); i++) {
CFTypeRef win = NULL;
if (AXUIElementCopyAttributeValue(el, shortcuts[i], &win) != kAXErrorSuccess ||
win == NULL) {
continue;
}
if (CFGetTypeID(win) == AXUIElementGetTypeID() &&
elementIsWindow((AXUIElementRef)win)) {
return (AXUIElementRef)win;
}
CFRelease(win);
}
// Bounded climb: a malformed parent chain that cycles would otherwise spin
// here, and this runs inside the event tap callback.
AXUIElementRef cur = (AXUIElementRef)CFRetain(el);
for (int depth = 0; depth < AX_PARENT_WALK_MAX; depth++) {
CFTypeRef parent = NULL;
if (AXUIElementCopyAttributeValue(cur, kAXParentAttribute, &parent) != kAXErrorSuccess ||
parent == NULL) {
break;
}
CFRelease(cur);
if (CFGetTypeID(parent) != AXUIElementGetTypeID()) {
CFRelease(parent);
return NULL;
}
cur = (AXUIElementRef)parent;
if (elementIsWindow(cur)) {
return cur;
}
}
CFRelease(cur);
return NULL;
}
// One hit test answers everything a caller can want about the window under the
// pointer: the owning process, its display name, and a handle on the window
// itself. AXUIElementCopyElementAtPosition() is synchronous IPC into the target
// app and the slowest thing we do, so callers that need more than one of those
// must not pay for it twice. On failure nothing is returned and *win_out is
// left NULL; on success *win_out may still be NULL if the window didn't resolve.
static bool windowContextAt(CGPoint *mouse, char *buf, size_t buf_len,
pid_t *pid_out, AXUIElementRef *win_out) {
if (win_out != NULL) {
*win_out = NULL;
}
AXUIElementRef el = NULL;
AXError err = AXUIElementCopyElementAtPosition(gSystemWide,
(float)mouse->x, (float)mouse->y, &el);
if (err != kAXErrorSuccess || el == NULL) {
return false;
}
pid_t pid = 0;
err = AXUIElementGetPid(el, &pid);
if (err != kAXErrorSuccess || pid <= 0) {
CFRelease(el);
return false;
}
// Name before window: it needs no further AX round trip, so a failed name
// lookup costs nothing and cannot strand a retained window element.
if (buf != NULL) {
char path[PROC_PIDPATHINFO_MAXSIZE];
if (proc_pidpath(pid, path, sizeof(path)) <= 0 ||
!displayNameForExecutable(path, buf, buf_len)) {
CFRelease(el);
return false;
}
}
if (win_out != NULL) {
*win_out = copyWindowForElement(el);
}
CFRelease(el);
if (pid_out != NULL) {
*pid_out = pid;
}
return true;
}
static bool windowNameAt(CGPoint *mouse, char *buf, size_t buf_len) {
return windowContextAt(mouse, buf, buf_len, NULL, NULL);
}
static struct lookup *lookupByName(const char *name) {
char *key = lowerDup(name);
if (NULL == key) {
return NULL;
}
ENTRY e = {0}; // hsearch() takes ENTRY by value; leave no field uninitialized
e.key = key;
ENTRY *ep = hsearch(e, FIND);
free(key);
struct lookup *le = (ep != NULL) ? (struct lookup *)ep->data : NULL;
if (gVerbose) {
printf("%s: skipWindow %d noFocus %d noClickThrough %d\n", name,
le ? le->skipWindow : 0, le ? le->noFocus : 0, le ? le->noClickThrough : 0);
}
return le;
}
static struct lookup *windowLookup(CGPoint *mouse) {
char name[MAX_WINDOW_NAME_SIZE];
if (!windowNameAt(mouse, name, sizeof(name))) {
if (gVerbose) {
printf("no window\n");
}
return NULL;
}
return lookupByName(name);
}
static bool isSkipWindow(CGPoint *mouse) {
struct lookup *le = windowLookup(mouse);
return (le != NULL) && le->skipWindow;
}
static bool isDockPid(pid_t pid) {
char path[PROC_PIDPATHINFO_MAXSIZE];
if (proc_pidpath(pid, path, sizeof(path)) <= 0) {
return false;
}
const char *base = strrchr(path, '/');
base = (base != NULL) ? base + 1 : path;
return strcmp(base, "Dock") == 0;
}
static void releaseWindow(AXUIElementRef *win) {
if (*win != NULL) {
CFRelease(*win);
*win = NULL;
}
}
// Drop a click-through that never made it to the re-post: the window element is
// retained for as long as the click is pending, so every path that clears the
// pending flag has to go through here or leak one element per click.
static void clearPendingClickThrough(void) {
gClickThroughPending = false;
releaseWindow(&gClickThroughWindow);
}
static void activateApp(pid_t pid) {
AXUIElementRef app = AXUIElementCreateApplication(pid);
if (NULL == app) {
return;
}
AXUIElementSetAttributeValue(app, kAXFrontmostAttribute, kCFBooleanTrue);
CFRelease(app);
}
// Bring a specific window forward without synthesising a click. kAXRaiseAction
// is the window's own "come to the front" verb, so it moves the exact window
// under the pointer; kAXFrontmostAttribute on the app only says which app is in
// front, and the app answers that by raising whichever window it considers main.
// For a background window of a background app those are different windows, and
// activating alone can leave the clicked window behind a sibling that just came
// forward over it. Do both, window first: right window within the app, right app
// on screen. Both calls are synchronous IPC, so by the time this returns the app
// has at least processed the request.
static void raiseWindow(AXUIElementRef win, pid_t pid) {
if (win != NULL) {
AXUIElementPerformAction(win, kAXRaiseAction);
// Raising orders the window on screen; main is what makes it the window
// the app treats as its active one, which is the other half of what a
// click on the title bar would have done.
AXUIElementSetAttributeValue(win, kAXMainAttribute, kCFBooleanTrue);
}
if (pid > 0) {
activateApp(pid);
}
}
static CGEventRef createSyntheticClick(CGEventType type, CGPoint point,
int clickState, CGEventFlags flags) {
CGEventRef event = CGEventCreateMouseEvent(NULL, type, point, kCGMouseButtonLeft);
if (NULL == event) {
return NULL;
}
CGEventSetIntegerValueField(event, kCGMouseEventClickState, clickState);
CGEventSetFlags(event, flags);
CGEventSetIntegerValueField(event, kCGEventSourceUserData, MACPASTE_SYNTHETIC);
return event;
}
// A lone down, for a swallowed click that turned out to be the start of a drag:
// the hardware up that ends the drag supplies the other half.
static void postMouseDown(CGPoint point, int clickState, CGEventFlags flags) {
CGEventRef mouseClickDown = createSyntheticClick(kCGEventLeftMouseDown, point,
clickState, flags);
if (NULL == mouseClickDown) {
return;
}
CGEventPost(gTapMouse, mouseClickDown);
CFRelease(mouseClickDown);
}
// Post both halves or neither: a down with no up leaves the app in a tracking
// loop that reads every later mouse move as a drag, selecting text as it goes.
static void postClick(CGPoint point, int clickState, CGEventFlags flags) {
CGEventRef mouseClickDown = createSyntheticClick(kCGEventLeftMouseDown, point,
clickState, flags);
CGEventRef mouseClickUp = createSyntheticClick(kCGEventLeftMouseUp, point,
clickState, flags);
if (mouseClickDown != NULL && mouseClickUp != NULL) {
CGEventPost(gTapMouse, mouseClickDown);
CGEventPost(gTapMouse, mouseClickUp);
}
if (mouseClickDown != NULL) {
CFRelease(mouseClickDown);
}
if (mouseClickUp != NULL) {
CFRelease(mouseClickUp);
}
}
struct clickThroughInfo {
CGPoint point;
int clickState;
CGEventFlags flags;
pid_t pid;
AXUIElementRef window; // the window the click was aimed at; may be NULL
int attemptsLeft;
};
// Ask an app directly whether it is frontmost, reading back the same attribute
// activateApp() sets. This replaces querying kAXFocusedApplicationAttribute on
// the system-wide element, which fails with kAXErrorCannotComplete whenever the
// current frontmost app declines to answer it (VMware Fusion, for one) and so
// silently disabled click-through for as long as such an app was in front.
//
// Returns false if the app does not answer at all, which is NOT the same as
// answering "not frontmost": callers must never swallow a click for an app whose
// state they cannot read, or the click would be lost entirely.
static bool readAppFrontmost(pid_t pid, bool *frontmost) {
AXUIElementRef app = AXUIElementCreateApplication(pid);
if (NULL == app) {
return false;
}
bool ok = false;
CFTypeRef val = NULL;
if (AXUIElementCopyAttributeValue(app, kAXFrontmostAttribute, &val) == kAXErrorSuccess &&
val != NULL) {
if (CFGetTypeID(val) == CFBooleanGetTypeID()) {
*frontmost = CFBooleanGetValue((CFBooleanRef)val);
ok = true;
}
CFRelease(val);
}
CFRelease(app);
return ok;
}
static void endClickThrough(CFRunLoopTimerRef timer, struct clickThroughInfo *ci) {
releaseWindow(&ci->window);
free(ci);
CFRunLoopTimerInvalidate(timer);
CFRelease(timer);
}
// Does the window we aimed at still own the click point? Comparing pids alone
// isn't enough: activating an app can bring one of its other windows forward
// over the point, and the click would then land on a window the user never
// aimed at -- the same wrong-target problem as a dialog appearing, but from a
// sibling of the intended window, so the pid check waves it through. Falls back
// to the pid comparison whenever either window is unknown, because refusing to
// post here would lose a click that was already swallowed on the way in.
static bool stillOwnsPoint(struct clickThroughInfo *ci, pid_t *under_out) {
pid_t under = -1;
AXUIElementRef underWin = NULL;
if (!windowContextAt(&ci->point, NULL, 0, &under, &underWin)) {
*under_out = under;
return false;
}
*under_out = under;
bool same = (under == ci->pid);
if (same && ci->window != NULL && underWin != NULL) {
same = CFEqual(ci->window, underWin);
}
releaseWindow(&underWin);
return same;
}
// Re-post the swallowed click once the target app is actually frontmost, and
// only if it still owns whatever is under the click point. Without the ownership
// check a dialog that appeared while we waited, or a window that moved or closed,
// would receive a click the user never aimed at it. This runs on the run loop
// rather than in the tap callback, so these AX calls cannot stall input.
static void clickThroughTimerCallback(CFRunLoopTimerRef timer, void *info) {
struct clickThroughInfo *ci = (struct clickThroughInfo *)info;
bool frontmost = false;
if (!readAppFrontmost(ci->pid, &frontmost) || !frontmost) {
if (--ci->attemptsLeft > 0) {
return; // activation still in flight; the repeating timer retries
}
// Out of attempts. The down was swallowed on the way in, so dropping the
// re-post now would lose the click outright; send it regardless. A click
// on a window that never came forward is just the ordinary
// activate-on-first-click behaviour, so this is safe to fall through to.
if (gVerbose) {
printf("click-through: pid %d never became frontmost, posting anyway\n", ci->pid);
}
}
pid_t under = -1;
if (stillOwnsPoint(ci, &under)) {
postClick(ci->point, ci->clickState, ci->flags);
} else if (gVerbose) {
printf("click-through: pid %d no longer owns the click point (now %d), "
"dropping re-post\n", ci->pid, under);
}
endClickThrough(timer, ci);
}
// Consumes `win` either way: on success the timer owns it, on failure it is
// released here, so the caller must not touch it again.
static bool scheduleClickThrough(CGPoint point, int clickState, CGEventFlags flags,
pid_t pid, AXUIElementRef win) {
struct clickThroughInfo *ci = malloc(sizeof(*ci));
if (NULL == ci) {
releaseWindow(&win);
return false;
}
ci->point = point;
ci->clickState = clickState;
ci->flags = flags;
ci->pid = pid;
ci->window = win;
ci->attemptsLeft = CLICK_THROUGH_MAX_ATTEMPTS;
CFRunLoopTimerContext context;
context.version = 0;
context.info = ci;
context.retain = NULL;
context.release = NULL;
context.copyDescription = NULL;
// Repeating: the callback polls for activation and stops the timer itself,
// either after re-posting the click or once it runs out of attempts.
CFRunLoopTimerRef timer = CFRunLoopTimerCreate(
kCFAllocatorDefault,
CFAbsoluteTimeGetCurrent() + CLICK_THROUGH_DELAY_SECONDS,
CLICK_THROUGH_POLL_SECONDS, 0, 0,
clickThroughTimerCallback,
&context);
if (NULL == timer) {
releaseWindow(&ci->window);
free(ci);
return false;
}
CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer, kCFRunLoopDefaultMode);
return true;
}
// Returns true if we are taking over this click: the caller must then swallow
// the down, because both halves are re-posted together once the app is
// frontmost. Swallowing one half and passing the other is what leaves an app
// selecting text under a button the user already released.
static bool maybeStartClickThrough(CGEventRef event) {
CGPoint point = CGEventGetLocation(event);
char name[MAX_WINDOW_NAME_SIZE];
pid_t pid = -1;
AXUIElementRef win = NULL;
if (!windowContextAt(&point, name, sizeof(name), &pid, &win)) {
return false;
}
// Every path below this point either hands `win` to the pending state or
// drops the click-through, so each early return has to let it go.
if (isDockPid(pid)) {
releaseWindow(&win);
return false;
}
bool frontmost = false;
if (!readAppFrontmost(pid, &frontmost)) {
// Can't tell, so don't swallow the up: passing the click through beats
// holding one back for an app that may never report itself frontmost.
if (gVerbose) {
printf("click-through: can't read frontmost state of %s, passing click through\n",
name);
}
releaseWindow(&win);
return false;
}
if (frontmost) {
if (gVerbose) {
printf("click-through: %s already frontmost\n", name);
}
releaseWindow(&win);
return false;
}
struct lookup *le = lookupByName(name);
if (le != NULL && le->noClickThrough) {
if (gVerbose) {
printf("click-through: %s excluded\n", name);
}
releaseWindow(&win);
return false;
}
int clickState = (int)CGEventGetIntegerValueField(event, kCGMouseEventClickState);
if (clickState < 1) {
clickState = 1;
}
if (gVerbose) {
printf("click-through: raising pid %d (%s)%s\n", pid, name,
win != NULL ? "" : " [no window element; app-level activate only]");
}
raiseWindow(win, pid);
gClickThroughPoint = point;
gClickThroughClickState = clickState;
gClickThroughFlags = CGEventGetFlags(event);
gClickThroughPid = pid;
gClickThroughWindow = win; // handed to scheduleClickThrough on mouse up
gClickThroughPending = true;
return true;
}
static void nsleep(long long nanos) {
struct timespec delay;
delay.tv_sec = 0;
delay.tv_nsec = nanos;
nanosleep(&delay, NULL);
}
// The virtual keycode of the physical modifier key that produces `flags`, so we
// can press and release it as hardware would.
static CGKeyCode modifierKeyCode(CGEventFlags flags) {
return (flags & kCGEventFlagMaskControl) ? kVK_Control : kVK_Command;
}
// Press or release the modifier key itself. Given a modifier keycode,
// CGEventCreateKeyboardEvent() returns a flagsChanged event already carrying the
// modifier mask plus the device-dependent left/right bit that real hardware sets
// (0x...108 for left command down, cleared on release), so take those flags as
// given and only add `extra`. Returns the posted flags so the key event between
// the two can carry the same state.
static CGEventFlags postModifier(CGKeyCode keycode, bool down, CGEventFlags extra) {
CGEventRef event = CGEventCreateKeyboardEvent(NULL, keycode, down);
if (NULL == event) {
return extra;
}
CGEventFlags flags = CGEventGetFlags(event) | extra;
CGEventSetFlags(event, flags);
CGEventPost(gTapA, event);
CFRelease(event);
return flags;
}
// Bracket the keystroke with real modifier press/release events instead of only
// stamping the flags onto the key event. Native Cocoa apps read the flags field
// and so accept the bare key event, but VM and remote-desktop clients (VMware
// Fusion, VirtualBox, RDP/VNC) track modifier state from flagsChanged and ignore
// that field, so without this the guest receives a plain "c"/"v" keypress and
// types the letter instead of copying or pasting.
static void postKeyDownUp(CGKeyCode keycode, CGEventFlags flags) {
CGEventRef kbdEventDown = CGEventCreateKeyboardEvent(NULL, keycode, 1);
CGEventRef kbdEventUp = CGEventCreateKeyboardEvent(NULL, keycode, 0);
if (NULL == kbdEventDown || NULL == kbdEventUp) {
if (kbdEventDown != NULL) {
CFRelease(kbdEventDown);
}
if (kbdEventUp != NULL) {
CFRelease(kbdEventUp);
}
return;
}
// Whatever the user is physically holding right now. It is deliberately kept
// out of the keystroke itself: shift+drag to extend a selection and alt+drag
// for column selection are ordinary gestures, and folding those in would send
// cmd+shift+c or cmd+alt+c, which mean something else entirely. It goes only
// on the release, so we hand their real modifier state back afterwards
// instead of leaving the system believing they let go.
CGEventFlags held = CGEventSourceFlagsState(kCGEventSourceStateCombinedSessionState) &
(kCGEventFlagMaskCommand | kCGEventFlagMaskControl |
kCGEventFlagMaskAlternate | kCGEventFlagMaskShift);
CGKeyCode modKey = modifierKeyCode(flags);
CGEventFlags down = postModifier(modKey, true, 0);
nsleep(MODIFIER_DELAY_NS);
CGEventSetFlags(kbdEventDown, down);
CGEventSetFlags(kbdEventUp, down);
CGEventPost(gTapA, kbdEventDown);
CGEventPost(gTapA, kbdEventUp);
nsleep(MODIFIER_DELAY_NS);
postModifier(modKey, false, held & ~flags);
CFRelease(kbdEventDown);
CFRelease(kbdEventUp);
}
static void paste(CGEventRef event) {
CGPoint mouseLocation = CGEventGetLocation(event);
// One hit test for both -n and -s and for the window to raise. Each check
// used to run its own, which meant two rounds of synchronous IPC into the
// target app on every middle click.
char name[MAX_WINDOW_NAME_SIZE];
pid_t pid = -1;
AXUIElementRef win = NULL;
struct lookup *le = NULL;
if (windowContextAt(&mouseLocation, name, sizeof(name), &pid, &win)) {
le = lookupByName(name);
} else if (gVerbose) {
printf("no window\n");
}
if (le == NULL || !le->noFocus) {
// Raise first, click second. The click used to do both jobs -- bring the
// window forward and put the caret where you clicked -- and an app that
// declines a click aimed at its background window does the first and
// skips the second, so the paste landed wherever the caret already was.
// Raising needs no click, which leaves the click free to do only the one
// thing no API can do for it: move the caret. The click carries the
// synthetic marker, so coming back through our own tap costs nothing.
raiseWindow(win, pid);
postClick(mouseLocation, 1, 0);
}
releaseWindow(&win);
if (le != NULL && le->skipWindow) {
return;
}
// Allow click events time to position cursor before pasting.
nsleep(PASTE_DELAY_NS);
// Paste.
postKeyDownUp(kVK_ANSI_V, gCommandKey);
}
static void copy(CGEventRef event) {
CGPoint mouseLocation = CGEventGetLocation(event);
if (isSkipWindow(&mouseLocation)) {
return;
}
postKeyDownUp(kVK_ANSI_C, gCommandKey);
}
static void recordClick(CGPoint point) {
gPrevClickTime = gCurClickTime;
gCurClickTime = now();
gPrevClickPoint = gCurClickPoint;
gCurClickPoint = point;
}
static bool isDoubleClickSpeed(void) {
return (gCurClickTime - gPrevClickTime) < DOUBLE_CLICK_MILLIS;
}
static bool isDoubleClickDistance(void) {
double dx = gCurClickPoint.x - gPrevClickPoint.x;
double dy = gCurClickPoint.y - gPrevClickPoint.y;
return (dx * dx + dy * dy) <=
(double)DOUBLE_CLICK_DISTANCE_PX * DOUBLE_CLICK_DISTANCE_PX;
}
// Both tests matter: time alone treats two unrelated clicks at opposite corners
// of the screen as a double-click and fires a copy, overwriting the clipboard.
static bool isDoubleClick(void) {
return isDoubleClickSpeed() && isDoubleClickDistance();
}
static CGEventRef mouseCallback (
CGEventTapProxy proxy,
CGEventType type,
CGEventRef event,
void * refcon
) {
(void)proxy;
(void)refcon;
// Our own posted clicks, echoed back because the session tap sits upstream
// of this one. They have already been through this logic once; re-entering
// it would start a nested click-through or fake a double-click.
if (CGEventGetIntegerValueField(event, kCGEventSourceUserData) == MACPASTE_SYNTHETIC) {
return event;
}
switch (type) {
case kCGEventOtherMouseDown:
if (CGEventGetIntegerValueField(event, kCGMouseEventButtonNumber) == 2) {
paste(event);
}
break;
case kCGEventLeftMouseDown:
gDragStartPoint = CGEventGetLocation(event);
recordClick(gDragStartPoint);
if (gClickThrough) {
clearPendingClickThrough(); // stale pending from a lost up; start fresh
if (maybeStartClickThrough(event)) {
return NULL; // re-posted with its up once the app is frontmost
}
}
break;
case kCGEventLeftMouseUp:
// Still pending means the click stayed a click: a drag would have
// released the swallowed down already, below.
if (gClickThrough && gClickThroughPending) {
gClickThroughPending = false;
AXUIElementRef win = gClickThroughWindow; // ownership moves to the timer
gClickThroughWindow = NULL;
if (gVerbose) {
printf("click-through: swallowing click, re-posting\n");
}
if (!scheduleClickThrough(gClickThroughPoint, gClickThroughClickState,
gClickThroughFlags, gClickThroughPid, win)) {
// No re-post is coming, and the down is already swallowed, so
// deliver the click now rather than lose it. It goes to a window
// that isn't frontmost yet, which is just the ordinary
// activate-on-first-click behaviour.
if (gVerbose) {
printf("click-through: couldn't schedule re-post, posting click now\n");
}
postClick(gClickThroughPoint, gClickThroughClickState, gClickThroughFlags);
}
// Either way this up's down is gone, so it must not go through alone.
gIsDragging = false;
return NULL;
}
if (isDoubleClick() || gIsDragging) {
copy(event);
}
gIsDragging = false;
break;
case kCGEventLeftMouseDragged:
if (!gIsDragging) {
CGPoint p = CGEventGetLocation(event);
if (p.x - gDragStartPoint.x > DRAG_THRESHOLD_PX ||
gDragStartPoint.x - p.x > DRAG_THRESHOLD_PX ||
p.y - gDragStartPoint.y > DRAG_THRESHOLD_PX ||
gDragStartPoint.y - p.y > DRAG_THRESHOLD_PX) {
gIsDragging = true;
if (gClickThrough && gClickThroughPending) {
// The user is dragging, not clicking, so there is nothing to
// defer: hand the app the down we swallowed, at the point it
// was pressed, and let the rest of the drag and its up run
// normally. Waiting for activation here would eat the drag.
clearPendingClickThrough();
if (gVerbose) {
printf("click-through: click became a drag, releasing the down\n");
}
postMouseDown(gClickThroughPoint, gClickThroughClickState,
gClickThroughFlags);
}
}
}
break;
case kCGEventTapDisabledByTimeout:
case kCGEventTapDisabledByUserInput:
if (gVerbose) {
printf("event tap disabled, re-enabling\n");
}
CGEventTapEnable(gEventTap, true);
break;
default:
break;
}
// Pass on the event. The only events swallowed are both halves of a
// click-through click, re-posted together (see scheduleClickThrough) once
// the app is frontmost.
return event;
}
static bool addLookupEntry(const char *name, bool skipWindow, bool noFocus, bool noClickThrough) {
char *key = lowerDup(name);
if (NULL == key) {
printf("Couldn't allocate lookup key\n");
return false;
}
ENTRY e = {0}; // hsearch() takes ENTRY by value; leave no field uninitialized
e.key = key;
ENTRY *ep = hsearch(e, FIND);
if (NULL == ep) {
struct lookup *le = malloc(sizeof(*le));
if (NULL == le) {
printf("Couldn't allocate lookup entry\n");
free(key);
return false;
}
le->skipWindow = skipWindow;
le->noFocus = noFocus;
le->noClickThrough = noClickThrough;
e.data = le;
ep = hsearch(e, ENTER);
if (NULL == ep) {
printf("Failed to insert lookup entry for '%s'\n", name);
free(key);
free(le);
return false;
}
} else {
struct lookup *le = ep->data;
le->skipWindow |= skipWindow;
le->noFocus |= noFocus;
le->noClickThrough |= noClickThrough;
free(key); // only ENTER hands the key to the table (hdestroy() frees those)
}
return true;
}
static void usage(const char *prog) {
fprintf(stderr,
"Usage: %s [-v] [-c] [-t] [-n name] [-s name] [-x name]\n"
" -v verbose logging\n"
" -c use ctrl instead of cmd for the synthesized copy/paste\n"
" -t enable click-through: the first click on a background\n"
" window is re-posted after its window is raised\n"
" -n name don't raise or click windows of this app before pasting\n"
" -s name skip this app entirely (no copy, no paste)\n"
" -x name exclude this app from click-through (requires -t)\n"
" -h show this help\n"
"Names match the app's display name, case-insensitively.\n"
"Terminate with Ctrl+C.\n",
prog);
}
int main (int argc, char **argv) {
CGEventMask emask;
CFMachPortRef myEventTap;
CFRunLoopSourceRef eventTapRLSrc;
// Line-buffer stdout: it is block-buffered when piped, so -v output would
// otherwise not appear until the buffer fills (this runs until Ctrl+C).
setvbuf(stdout, NULL, _IOLBF, 0);
// Always create the lookup table: windowLookup() runs on every paste/copy
// even with no args, and hsearch() on an uncreated table is a crash.
if (0 == hcreate((size_t)argc + 10)) {
fprintf(stderr, "Couldn't create hash table\n");
return 1;
}
int opt;
while ((opt = getopt(argc, argv, "hvctn:s:x:")) != -1) {
switch (opt) {
case 'v':
gVerbose = true;
break;
case 'c':
gCommandKey = kCGEventFlagMaskControl;