1#import <AppKit/AppKit.h>
2#import <ApplicationServices/ApplicationServices.h>
3#import <Foundation/Foundation.h>
4
5static CFStringRef const kCloverAXWindowNumberAttribute = CFSTR("AXWindowNumber");
6
7static NSArray<id> *CopyWindowElements(AXUIElementRef applicationElement) {
8 if (!applicationElement) {
9 return @[];
10 }
11
12 CFTypeRef value = NULL;
13 AXError error = AXUIElementCopyAttributeValue(
14 applicationElement,
15 kAXWindowsAttribute,
16 &value
17 );
18 if (error != kAXErrorSuccess || !value) {
19 if (value) {
20 CFRelease(value);
21 }
22 return @[];
23 }
24 if (CFGetTypeID(value) != CFArrayGetTypeID()) {
25 CFRelease(value);
26 return @[];
27 }
28
29 return CFBridgingRelease(value);
30}
31
32static NSString *CopyStringAttribute(AXUIElementRef element, CFStringRef attribute) {
33 if (!element) {
34 return nil;
35 }
36
37 CFTypeRef value = NULL;
38 AXError error = AXUIElementCopyAttributeValue(element, attribute, &value);
39 if (error != kAXErrorSuccess || !value) {
40 if (value) {
41 CFRelease(value);
42 }
43 return nil;
44 }
45 if (CFGetTypeID(value) != CFStringGetTypeID()) {
46 CFRelease(value);
47 return nil;
48 }
49
50 return CFBridgingRelease(value);
51}
52
53static NSNumber *CopyNumberAttribute(AXUIElementRef element, CFStringRef attribute) {
54 if (!element) {
55 return nil;
56 }
57
58 CFTypeRef value = NULL;
59 AXError error = AXUIElementCopyAttributeValue(element, attribute, &value);
60 if (error != kAXErrorSuccess || !value) {
61 if (value) {
62 CFRelease(value);
63 }
64 return nil;
65 }
66 if (CFGetTypeID(value) != CFNumberGetTypeID()) {
67 CFRelease(value);
68 return nil;
69 }
70
71 return CFBridgingRelease(value);
72}
73
74static BOOL CopyBoolAttribute(
75 AXUIElementRef element,
76 CFStringRef attribute,
77 BOOL fallback
78) {
79 if (!element) {
80 return fallback;
81 }
82
83 CFTypeRef value = NULL;
84 AXError error = AXUIElementCopyAttributeValue(element, attribute, &value);
85 if (error != kAXErrorSuccess || !value) {
86 if (value) {
87 CFRelease(value);
88 }
89 return fallback;
90 }
91
92 BOOL result = fallback;
93 CFTypeID typeId = CFGetTypeID(value);
94 if (typeId == CFBooleanGetTypeID()) {
95 result = CFBooleanGetValue((CFBooleanRef)value);
96 } else if (typeId == CFNumberGetTypeID()) {
97 int numericValue = 0;
98 if (CFNumberGetValue((CFNumberRef)value, kCFNumberIntType, &numericValue)) {
99 result = numericValue != 0;
100 }
101 }
102
103 CFRelease(value);
104 return result;
105}
106
107static NSNumber *WindowIdentifierForElement(AXUIElementRef windowElement, NSInteger index) {
108 NSNumber *windowNumber = CopyNumberAttribute(windowElement, kCloverAXWindowNumberAttribute);
109 return windowNumber ?: @(-(index + 1));
110}
111
112static NSDictionary<NSString *, id> *SnapshotWindow(
113 AXUIElementRef windowElement,
114 NSInteger index
115) {
116 if (!windowElement) {
117 return nil;
118 }
119
120 return @{
121 @"id": WindowIdentifierForElement(windowElement, index),
122 @"title": CopyStringAttribute(windowElement, kAXTitleAttribute) ?: @"",
123 @"main": @(CopyBoolAttribute(windowElement, kAXMainAttribute, NO)),
124 @"focused": @(CopyBoolAttribute(windowElement, kAXFocusedAttribute, NO)),
125 };
126}
127
128static NSArray<NSDictionary<NSString *, id> *> *SnapshotWindowsForApplicationElement(
129 AXUIElementRef applicationElement
130) {
131 NSArray<id> *windowElements = CopyWindowElements(applicationElement);
132 NSMutableArray<NSDictionary<NSString *, id> *> *snapshots = [NSMutableArray arrayWithCapacity:windowElements.count];
133
134 for (NSUInteger index = 0; index < windowElements.count; index++) {
135 AXUIElementRef windowElement = (__bridge AXUIElementRef)windowElements[index];
136 NSDictionary<NSString *, id> *snapshot = SnapshotWindow(windowElement, index);
137 if (snapshot) {
138 [snapshots addObject:snapshot];
139 }
140 }
141
142 return snapshots;
143}
144
145static void PrintState(
146 NSRunningApplication *application,
147 NSArray<NSDictionary<NSString *, id> *> *windows
148) {
149 NSDictionary<NSString *, id> *payload = @{
150 @"bundleId": application.bundleIdentifier ?: [NSNull null],
151 @"windows": windows ?: @[],
152 };
153
154 NSError *error = nil;
155 NSData *json = [NSJSONSerialization dataWithJSONObject:payload options:0 error:&error];
156 if (!json || error) {
157 const char *message = error.localizedDescription.UTF8String ?: "Failed to encode state";
158 fprintf(stderr, "%s\n", message);
159 return;
160 }
161
162 fwrite(json.bytes, 1, json.length, stdout);
163 fputc('\n', stdout);
164 fflush(stdout);
165}
166
167static void PrintCurrentState(void) {
168 NSRunningApplication *application = NSWorkspace.sharedWorkspace.frontmostApplication;
169 id applicationElement = application
170 ? CFBridgingRelease(AXUIElementCreateApplication(application.processIdentifier))
171 : nil;
172 PrintState(
173 application,
174 SnapshotWindowsForApplicationElement((__bridge AXUIElementRef)applicationElement)
175 );
176}
177
178static AXUIElementRef CopyWindowElementForIdentifier(
179 AXUIElementRef applicationElement,
180 long long targetIdentifier
181) {
182 NSArray<id> *windowElements = CopyWindowElements(applicationElement);
183 for (NSUInteger index = 0; index < windowElements.count; index++) {
184 AXUIElementRef windowElement = (__bridge AXUIElementRef)windowElements[index];
185 if (WindowIdentifierForElement(windowElement, index).longLongValue == targetIdentifier) {
186 return (AXUIElementRef)CFRetain(windowElement);
187 }
188 }
189 return NULL;
190}
191
192static AXUIElementRef CopyMainWindowElement(AXUIElementRef applicationElement) {
193 if (!applicationElement) {
194 return NULL;
195 }
196
197 CFTypeRef value = NULL;
198 AXError error = AXUIElementCopyAttributeValue(
199 applicationElement,
200 kAXMainWindowAttribute,
201 &value
202 );
203 if (error == kAXErrorSuccess && value) {
204 if (CFGetTypeID(value) == AXUIElementGetTypeID()) {
205 return (AXUIElementRef)value;
206 }
207 CFRelease(value);
208 }
209
210 NSArray<id> *windowElements = CopyWindowElements(applicationElement);
211 for (NSUInteger index = 0; index < windowElements.count; index++) {
212 AXUIElementRef windowElement = (__bridge AXUIElementRef)windowElements[index];
213 if (CopyBoolAttribute(windowElement, kAXMainAttribute, NO)) {
214 return (AXUIElementRef)CFRetain(windowElement);
215 }
216 }
217
218 return NULL;
219}
220
221static BOOL FocusWindowElement(
222 NSRunningApplication *application,
223 AXUIElementRef windowElement,
224 NSString **failure
225) {
226 if (!application || !windowElement) {
227 if (failure) {
228 *failure = @"No window is available to focus.";
229 }
230 return NO;
231 }
232
233 [application activateWithOptions:NSApplicationActivateAllWindows];
234
235 AXError unminimizeError = AXUIElementSetAttributeValue(
236 windowElement,
237 kAXMinimizedAttribute,
238 kCFBooleanFalse
239 );
240 AXError raiseError = AXUIElementPerformAction(windowElement, kAXRaiseAction);
241 AXError mainError = AXUIElementSetAttributeValue(
242 windowElement,
243 kAXMainAttribute,
244 kCFBooleanTrue
245 );
246 AXError focusedError = AXUIElementSetAttributeValue(
247 windowElement,
248 kAXFocusedAttribute,
249 kCFBooleanTrue
250 );
251
252 BOOL succeeded =
253 unminimizeError == kAXErrorSuccess ||
254 raiseError == kAXErrorSuccess ||
255 mainError == kAXErrorSuccess ||
256 focusedError == kAXErrorSuccess;
257 if (succeeded) {
258 return YES;
259 }
260
261 if (failure) {
262 *failure = [NSString stringWithFormat:
263 @"Could not focus the requested window (unminimize=%d raise=%d main=%d focused=%d).",
264 (int)unminimizeError,
265 (int)raiseError,
266 (int)mainError,
267 (int)focusedError
268 ];
269 }
270 return NO;
271}
272
273static BOOL FocusWindowWithIdentifier(long long targetIdentifier) {
274 if (!AXIsProcessTrusted()) {
275 fprintf(stderr, "%s\n", "Accessibility access is required to focus windows.");
276 return NO;
277 }
278
279 NSRunningApplication *application = NSWorkspace.sharedWorkspace.frontmostApplication;
280 if (!application) {
281 fprintf(stderr, "%s\n", "No frontmost application is available.");
282 return NO;
283 }
284
285 id applicationElement = CFBridgingRelease(AXUIElementCreateApplication(application.processIdentifier));
286 AXUIElementRef windowElement = CopyWindowElementForIdentifier(
287 (__bridge AXUIElementRef)applicationElement,
288 targetIdentifier
289 );
290 if (!windowElement) {
291 fprintf(stderr, "Window %lld was not found.\n", targetIdentifier);
292 return NO;
293 }
294
295 NSString *failure = nil;
296 BOOL focused = FocusWindowElement(application, windowElement, &failure);
297 CFRelease(windowElement);
298 if (!focused) {
299 fprintf(stderr, "%s\n", failure.UTF8String ?: "Failed to focus the requested window.");
300 }
301 return focused;
302}
303
304static BOOL FocusMainWindow(void) {
305 if (!AXIsProcessTrusted()) {
306 fprintf(stderr, "%s\n", "Accessibility access is required to focus windows.");
307 return NO;
308 }
309
310 NSRunningApplication *application = NSWorkspace.sharedWorkspace.frontmostApplication;
311 if (!application) {
312 fprintf(stderr, "%s\n", "No frontmost application is available.");
313 return NO;
314 }
315
316 id applicationElement = CFBridgingRelease(AXUIElementCreateApplication(application.processIdentifier));
317 AXUIElementRef windowElement = CopyMainWindowElement((__bridge AXUIElementRef)applicationElement);
318 if (!windowElement) {
319 fprintf(stderr, "%s\n", "The frontmost application does not report a main window.");
320 return NO;
321 }
322
323 NSString *failure = nil;
324 BOOL focused = FocusWindowElement(application, windowElement, &failure);
325 CFRelease(windowElement);
326 if (!focused) {
327 fprintf(stderr, "%s\n", failure.UTF8String ?: "Failed to focus the main window.");
328 }
329 return focused;
330}
331
332@interface FrontmostAppObserver : NSObject {
333 @private
334 id _accessibilityObserver;
335 id _applicationElement;
336 NSMutableArray<id> *_windowElements;
337}
338- (void)start;
339- (void)handleActivation:(NSNotification *)notification;
340- (void)handleAccessibilityNotification:(NSString *)notification;
341@end
342
343static void FrontmostAccessibilityCallback(
344 AXObserverRef observer,
345 AXUIElementRef element,
346 CFStringRef notification,
347 void *context
348) {
349 @autoreleasepool {
350 FrontmostAppObserver *frontmostObserver = (__bridge FrontmostAppObserver *)context;
351 [frontmostObserver handleAccessibilityNotification:(__bridge NSString *)notification];
352 }
353}
354
355@implementation FrontmostAppObserver
356
357- (instancetype)init {
358 self = [super init];
359 if (self) {
360 _windowElements = [NSMutableArray array];
361 }
362 return self;
363}
364
365- (void)dealloc {
366 [NSWorkspace.sharedWorkspace.notificationCenter removeObserver:self];
367 [self clearObservedApplication];
368}
369
370- (AXObserverRef)observerRef {
371 return (__bridge AXObserverRef)_accessibilityObserver;
372}
373
374- (AXUIElementRef)applicationElementRef {
375 return (__bridge AXUIElementRef)_applicationElement;
376}
377
378- (void)start {
379 NSWorkspace *workspace = NSWorkspace.sharedWorkspace;
380 [workspace.notificationCenter addObserver:self
381 selector:@selector(handleActivation:)
382 name:NSWorkspaceDidActivateApplicationNotification
383 object:nil];
384 [self observeApplication:workspace.frontmostApplication];
385}
386
387- (void)handleActivation:(NSNotification *)notification {
388 NSRunningApplication *application = notification.userInfo[NSWorkspaceApplicationKey];
389 [self observeApplication:application];
390}
391
392- (void)handleAccessibilityNotification:(NSString *)notification {
393 (void)notification;
394 [self refreshWindowsAndEmit];
395}
396
397- (void)observeApplication:(NSRunningApplication *)application {
398 [self clearObservedApplication];
399 if (!application) {
400 PrintState(nil, @[]);
401 return;
402 }
403
404 _applicationElement = CFBridgingRelease(AXUIElementCreateApplication(application.processIdentifier));
405
406 AXObserverRef observer = NULL;
407 AXError observerError = AXObserverCreate(
408 application.processIdentifier,
409 FrontmostAccessibilityCallback,
410 &observer
411 );
412 if (observerError == kAXErrorSuccess && observer) {
413 _accessibilityObserver = CFBridgingRelease(observer);
414 CFRunLoopAddSource(
415 CFRunLoopGetCurrent(),
416 AXObserverGetRunLoopSource(self.observerRef),
417 kCFRunLoopDefaultMode
418 );
419
420 [self addApplicationNotification:kAXFocusedWindowChangedNotification];
421 [self addApplicationNotification:kAXMainWindowChangedNotification];
422 [self addApplicationNotification:kAXWindowCreatedNotification];
423 }
424
425 [self refreshWindowsAndEmit];
426}
427
428- (void)clearObservedApplication {
429 [self clearWindowNotifications];
430
431 AXObserverRef observer = self.observerRef;
432 AXUIElementRef applicationElement = self.applicationElementRef;
433 if (observer && applicationElement) {
434 AXObserverRemoveNotification(observer, applicationElement, kAXFocusedWindowChangedNotification);
435 AXObserverRemoveNotification(observer, applicationElement, kAXMainWindowChangedNotification);
436 AXObserverRemoveNotification(observer, applicationElement, kAXWindowCreatedNotification);
437 }
438 if (observer) {
439 CFRunLoopRemoveSource(
440 CFRunLoopGetCurrent(),
441 AXObserverGetRunLoopSource(observer),
442 kCFRunLoopDefaultMode
443 );
444 }
445
446 _accessibilityObserver = nil;
447 _applicationElement = nil;
448}
449
450- (void)clearWindowNotifications {
451 AXObserverRef observer = self.observerRef;
452 if (observer) {
453 for (id windowObject in _windowElements) {
454 AXUIElementRef windowElement = (__bridge AXUIElementRef)windowObject;
455 AXObserverRemoveNotification(observer, windowElement, kAXTitleChangedNotification);
456 AXObserverRemoveNotification(observer, windowElement, kAXUIElementDestroyedNotification);
457 }
458 }
459
460 [_windowElements removeAllObjects];
461}
462
463- (void)addApplicationNotification:(CFStringRef)notification {
464 AXObserverRef observer = self.observerRef;
465 AXUIElementRef applicationElement = self.applicationElementRef;
466 if (!observer || !applicationElement) {
467 return;
468 }
469
470 AXObserverAddNotification(
471 observer,
472 applicationElement,
473 notification,
474 (__bridge void *)self
475 );
476}
477
478- (void)addWindowNotification:(CFStringRef)notification element:(AXUIElementRef)windowElement {
479 AXObserverRef observer = self.observerRef;
480 if (!observer || !windowElement) {
481 return;
482 }
483
484 AXObserverAddNotification(
485 observer,
486 windowElement,
487 notification,
488 (__bridge void *)self
489 );
490}
491
492- (NSArray<NSDictionary<NSString *, id> *> *)refreshObservedWindows {
493 [self clearWindowNotifications];
494
495 NSArray<id> *windowElements = CopyWindowElements(self.applicationElementRef);
496 NSMutableArray<NSDictionary<NSString *, id> *> *windows = [NSMutableArray arrayWithCapacity:windowElements.count];
497
498 for (NSUInteger index = 0; index < windowElements.count; index++) {
499 id windowObject = windowElements[index];
500 AXUIElementRef windowElement = (__bridge AXUIElementRef)windowObject;
501 NSDictionary<NSString *, id> *snapshot = SnapshotWindow(windowElement, index);
502 if (!snapshot) {
503 continue;
504 }
505
506 [_windowElements addObject:windowObject];
507 [self addWindowNotification:kAXTitleChangedNotification element:windowElement];
508 [self addWindowNotification:kAXUIElementDestroyedNotification element:windowElement];
509 [windows addObject:snapshot];
510 }
511
512 return windows;
513}
514
515- (void)refreshWindowsAndEmit {
516 PrintState(
517 NSWorkspace.sharedWorkspace.frontmostApplication,
518 [self refreshObservedWindows]
519 );
520}
521
522@end
523
524int main(int argc, const char *argv[]) {
525 @autoreleasepool {
526 [NSApplication sharedApplication];
527 [NSApp setActivationPolicy:NSApplicationActivationPolicyProhibited];
528
529 if (argc > 1) {
530 NSString *command = [NSString stringWithUTF8String:argv[1]];
531 if ([command isEqualToString:@"--once"]) {
532 PrintCurrentState();
533 return 0;
534 }
535 if ([command isEqualToString:@"--focus-window"]) {
536 if (argc < 3) {
537 fprintf(stderr, "%s\n", "Missing window identifier.");
538 return 1;
539 }
540 char *end = NULL;
541 long long windowIdentifier = strtoll(argv[2], &end, 10);
542 if (end == argv[2] || (end && *end != '\0')) {
543 fprintf(stderr, "%s\n", "Window identifier must be an integer.");
544 return 1;
545 }
546 return FocusWindowWithIdentifier(windowIdentifier) ? 0 : 1;
547 }
548 if ([command isEqualToString:@"--focus-main-window"]) {
549 return FocusMainWindow() ? 0 : 1;
550 }
551
552 fprintf(stderr, "Unknown argument: %s\n", argv[1]);
553 return 1;
554 }
555
556 FrontmostAppObserver *observer = [FrontmostAppObserver new];
557 [observer start];
558 [[NSRunLoop currentRunLoop] run];
559 }
560
561 return 0;
562}