StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.
| 1 | #import <AppKit/AppKit.h> |
| 2 | #import <Carbon/Carbon.h> |
| 3 | #import <ServiceManagement/ServiceManagement.h> |
| 4 | #import <UserNotifications/UserNotifications.h> |
| 5 | |
| 6 | #import "alert.h" |
| 7 | #import "app.h" |
| 8 | #import "host_view.h" |
| 9 | #import "hotkey.h" |
| 10 | #import "menus.h" |
| 11 | |
| 12 | #import "reachability.h" |
| 13 | |
| 14 | static void *NSAppThemeChangeContext = &NSAppThemeChangeContext; |
| 15 | |
| 16 | NSMutableDictionary<NSNumber *, WarpHotKey *> *_hotKeys; |
| 17 | UInt32 _nextHotKeyID; |
| 18 | |
| 19 | OSStatus HotkeyPressedHandler(EventHandlerCallRef _inCaller __unused, EventRef inEvent, |
| 20 | void *inUserData); |
| 21 | OSStatus HotkeyPressedHandler(EventHandlerCallRef _inCaller __unused, EventRef inEvent, |
| 22 | void *inUserData) { |
| 23 | EventHotKeyID hotKeyID; |
| 24 | |
| 25 | // Get the hotKeyID corresponding to the pressed hot key. |
| 26 | if (GetEventParameter(inEvent, kEventParamDirectObject, typeEventHotKeyID, nil, |
| 27 | sizeof(EventHotKeyID), nil, &hotKeyID)) { |
| 28 | return eventNotHandledErr; |
| 29 | } |
| 30 | |
| 31 | WarpHotKey *hotkey = _hotKeys[@(hotKeyID.id)]; |
| 32 | if (hotkey) { |
| 33 | warp_app_send_global_keybinding((NSApplication *)inUserData, hotkey->_modifierKeys, |
| 34 | hotkey->_keyCode); |
| 35 | return noErr; |
| 36 | } |
| 37 | |
| 38 | return eventNotHandledErr; |
| 39 | } |
| 40 | |
| 41 | BOOL isDarkMode() { |
| 42 | NSAppearanceName name = [NSApp.effectiveAppearance |
| 43 | bestMatchFromAppearancesWithNames:@[ NSAppearanceNameAqua, NSAppearanceNameDarkAqua ]]; |
| 44 | return name == NSAppearanceNameDarkAqua; |
| 45 | } |
| 46 | |
| 47 | NSArray *getFilePathsFromPasteboard() { |
| 48 | NSPasteboard *pb = [NSPasteboard generalPasteboard]; |
| 49 | NSArray *types = [pb types]; |
| 50 | |
| 51 | if ([types containsObject:NSPasteboardTypeFileURL]) { |
| 52 | return [pb getFilePaths]; |
| 53 | } |
| 54 | |
| 55 | return [NSArray array]; |
| 56 | } |
| 57 | |
| 58 | void *registerGlobalHotkey(NSUInteger key, NSUInteger modifiers) { |
| 59 | EventHotKeyRef hotKeyRef = NULL; |
| 60 | EventHotKeyID hotKeyID = {0, _nextHotKeyID}; |
| 61 | if (RegisterEventHotKey((UInt32)key, (UInt32)modifiers, hotKeyID, GetEventDispatcherTarget(), 0, |
| 62 | &hotKeyRef)) { |
| 63 | return nil; |
| 64 | }; |
| 65 | [_hotKeys setObject:[[[WarpHotKey alloc] initWithEventHotKey:hotKeyRef |
| 66 | keyCode:key |
| 67 | modifierKeys:modifiers] autorelease] |
| 68 | forKey:@(hotKeyID.id)]; |
| 69 | _nextHotKeyID++; |
| 70 | return nil; |
| 71 | } |
| 72 | |
| 73 | void *unregisterGlobalHotkey(NSUInteger key, NSUInteger modifiers) { |
| 74 | NSNumber *keyIdx; |
| 75 | BOOL found = NO; |
| 76 | |
| 77 | for (NSNumber *hotKeyID in _hotKeys) { |
| 78 | if ([[_hotKeys objectForKey:hotKeyID] hotKeyKeyAndModifierEquals:key |
| 79 | modifierKeys:modifiers]) { |
| 80 | keyIdx = hotKeyID; |
| 81 | found = YES; |
| 82 | break; |
| 83 | } |
| 84 | } |
| 85 | |
| 86 | if (found) { |
| 87 | UnregisterEventHotKey([_hotKeys objectForKey:keyIdx]->_eventHotKey); |
| 88 | [_hotKeys removeObjectForKey:keyIdx]; |
| 89 | } |
| 90 | return nil; |
| 91 | } |
| 92 | |
| 93 | NSRect screenFrame() { return [[NSScreen mainScreen] frame]; } |
| 94 | |
| 95 | NSUInteger activeScreenId() { |
| 96 | return [[[[NSScreen mainScreen] deviceDescription] objectForKey:@"NSScreenNumber"] |
| 97 | unsignedIntegerValue]; |
| 98 | } |
| 99 | |
| 100 | @interface WarpMenuItemDelegate : NSObject <NSMenuDelegate> { |
| 101 | // Rust expects an ivar with this name. |
| 102 | void *rustWrapper; |
| 103 | } |
| 104 | @end |
| 105 | |
| 106 | @implementation WarpDelegate { |
| 107 | // Rust expects an ivar with this name. |
| 108 | void *rustWrapper; |
| 109 | |
| 110 | // Whether we have a pending active window change notification. |
| 111 | BOOL hasPendingActiveWindowChange; |
| 112 | |
| 113 | // Internet reachability. |
| 114 | Reachability *internetReachable; |
| 115 | |
| 116 | // Track the current reachable state so we don't double fire reachability state |
| 117 | // changed events. |
| 118 | NSNumber *isReachable; |
| 119 | |
| 120 | // Whether we should force termination. |
| 121 | BOOL forceTermination; |
| 122 | |
| 123 | // Whether we should terminate the application upon the app |
| 124 | // being hidden. This allows us to hide the app before running any |
| 125 | // slower termination logic. |
| 126 | BOOL terminateOnHide; |
| 127 | } |
| 128 | |
| 129 | - (id)init { |
| 130 | [super init]; |
| 131 | NSNotificationCenter *defaultCenter = [NSNotificationCenter defaultCenter]; |
| 132 | [defaultCenter addObserver:self |
| 133 | selector:@selector(keyWindowChanged:) |
| 134 | name:NSWindowDidBecomeKeyNotification |
| 135 | object:nil]; |
| 136 | [defaultCenter addObserver:self |
| 137 | selector:@selector(keyWindowChanged:) |
| 138 | name:NSWindowDidResignKeyNotification |
| 139 | object:nil]; |
| 140 | |
| 141 | [defaultCenter addObserver:self |
| 142 | selector:@selector(windowMoved:) |
| 143 | name:NSWindowDidMoveNotification |
| 144 | object:nil]; |
| 145 | [defaultCenter addObserver:self |
| 146 | selector:@selector(windowResized:) |
| 147 | name:NSWindowDidResizeNotification |
| 148 | object:nil]; |
| 149 | [defaultCenter addObserver:self |
| 150 | selector:@selector(screenChanged:) |
| 151 | name:NSApplicationDidChangeScreenParametersNotification |
| 152 | object:nil]; |
| 153 | |
| 154 | // For the following notifications, we need to register them on the workspace |
| 155 | // notification center, which is different from the default NSNotificationCenter. |
| 156 | // See here for more details: https://developer.apple.com/library/archive/qa/qa1340/_index.html. |
| 157 | NSNotificationCenter *workspaceCenter = [[NSWorkspace sharedWorkspace] notificationCenter]; |
| 158 | [workspaceCenter addObserver:self |
| 159 | selector:@selector(cpuAwakened:) |
| 160 | name:NSWorkspaceDidWakeNotification |
| 161 | object:nil]; |
| 162 | [workspaceCenter addObserver:self |
| 163 | selector:@selector(cpuWillSleep:) |
| 164 | name:NSWorkspaceWillSleepNotification |
| 165 | object:nil]; |
| 166 | |
| 167 | // Tell the shared notification center to use the current view as the |
| 168 | // `UNUserNotificationCenterDelegate` delegate. We only do this if the application |
| 169 | // is bundled, otherwise the app will crash when trying to set the delegate. This allows |
| 170 | // strato_ui to still be run via `cargo run` since the app is not bundled in this case. Note this |
| 171 | // has no functional change in the non-bundled case since the app must be bundled for |
| 172 | // notifications to actually be sent/received. |
| 173 | NSString *bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier]; |
| 174 | if (bundleIdentifier != nil && ![bundleIdentifier isEqualToString:(@"")]) { |
| 175 | UNUserNotificationCenter *user_notification_center = |
| 176 | [UNUserNotificationCenter currentNotificationCenter]; |
| 177 | user_notification_center.delegate = self; |
| 178 | |
| 179 | // Create and register the notification category. |
| 180 | UNNotificationCategory *CustomizedNotification = [UNNotificationCategory |
| 181 | categoryWithIdentifier:@"CUSTOMIZED_NOTIFICATION" |
| 182 | actions:@[] |
| 183 | intentIdentifiers:@[] |
| 184 | options:UNNotificationCategoryOptionCustomDismissAction]; |
| 185 | |
| 186 | [user_notification_center |
| 187 | setNotificationCategories:[NSSet setWithObjects:CustomizedNotification, nil]]; |
| 188 | } |
| 189 | |
| 190 | // Initiate the global hotkey handlers first so we could register them at the rust |
| 191 | // side callback. |
| 192 | EventTypeSpec eventType = {kEventClassKeyboard, kEventHotKeyPressed}; |
| 193 | InstallApplicationEventHandler(HotkeyPressedHandler, 1, &eventType, self, NULL); |
| 194 | _hotKeys = [[NSMutableDictionary alloc] init]; |
| 195 | |
| 196 | return self; |
| 197 | } |
| 198 | |
| 199 | - (void)dealloc { |
| 200 | [NSApp removeObserver:self forKeyPath:@"effectiveAppearance" context:NSAppThemeChangeContext]; |
| 201 | [[NSNotificationCenter defaultCenter] removeObserver:self]; |
| 202 | [internetReachable stopNotifier]; |
| 203 | [internetReachable release]; |
| 204 | [super dealloc]; |
| 205 | } |
| 206 | |
| 207 | - (void)applicationWillFinishLaunching:(NSNotification *)note { |
| 208 | // On macOS 26, the autofill heuristic controller causes significant slowdowns. |
| 209 | // It's not clear why, but other apps which use custom text inputs have reported |
| 210 | // the same issue. See: |
| 211 | // * Ghostty: https://github.com/ghostty-org/ghostty/pull/8625 |
| 212 | // * Zed: https://github.com/zed-industries/zed/issues/33182 |
| 213 | // * Twitter thread discussing the issue: https://x.com/mitchellh/status/1967324131801915875 |
| 214 | NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; |
| 215 | [defaults setBool:NO forKey:@"NSAutoFillHeuristicControllerEnabled"]; |
| 216 | |
| 217 | if (rustWrapper) warp_app_will_finish_launching(note.object); |
| 218 | } |
| 219 | |
| 220 | - (void)applicationDidFinishLaunching:(NSNotification *)note { |
| 221 | [NSApp addObserver:self |
| 222 | forKeyPath:@"effectiveAppearance" |
| 223 | options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) |
| 224 | context:NSAppThemeChangeContext]; |
| 225 | } |
| 226 | |
| 227 | - (void)observeValueForKeyPath:(NSString *)keyPath |
| 228 | ofObject:(id)object |
| 229 | change:(NSDictionary *)change |
| 230 | context:(void *)context { |
| 231 | if (context == NSAppThemeChangeContext) { |
| 232 | if (rustWrapper) warp_app_os_appearance_changed(self); |
| 233 | } else { |
| 234 | // Any unrecognized context must belong to super |
| 235 | [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; |
| 236 | } |
| 237 | } |
| 238 | |
| 239 | - (void)applicationDidBecomeActive:(NSNotification *)note { |
| 240 | if (rustWrapper) warp_app_did_become_active(note.object); |
| 241 | } |
| 242 | |
| 243 | - (void)setForceTermination { |
| 244 | forceTermination = YES; |
| 245 | } |
| 246 | |
| 247 | // Unfullscreens any windows that are currently fullscreen. |
| 248 | - (void)unfullscreenAllWindows:(NSApplication *)application { |
| 249 | for (NSWindow *window in [application windows]) { |
| 250 | if ((window.styleMask & NSWindowStyleMaskFullScreen) == NSWindowStyleMaskFullScreen) { |
| 251 | [window toggleFullScreen:nil]; |
| 252 | return; |
| 253 | } |
| 254 | } |
| 255 | } |
| 256 | |
| 257 | - (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)application { |
| 258 | BOOL okToTerminate = YES; |
| 259 | |
| 260 | // If this is the second termination attempt after we've already hidden the app, we can go ahead |
| 261 | // and terminate. |
| 262 | if (terminateOnHide) { |
| 263 | return NSTerminateNow; |
| 264 | } |
| 265 | |
| 266 | if (!forceTermination) { |
| 267 | // Make sure the rust app doesn't have any reasons to interrupt quit, e.g. needs to relaunch |
| 268 | // for autoupdate, but launching the new process failed. |
| 269 | okToTerminate = warp_app_should_terminate_app(application); |
| 270 | } |
| 271 | |
| 272 | if (okToTerminate) { |
| 273 | // We want to hide the application before we start the teardown |
| 274 | // process, to ensure the user isn't affected by any slow teardown |
| 275 | // steps. The tricky part here is that a call to `[NSApp hide]` isn't |
| 276 | // handled synchronously, it is processed on the event loop. |
| 277 | // |
| 278 | // To work around this, we enqueue the hide on the event loop and set |
| 279 | // some state so we know to resume termination of the application when |
| 280 | // the hide takes effect. As a fallback, if we never get notified that |
| 281 | // the application was hidden, we always resume termination after 5s. |
| 282 | // We deliberately do _not_ return `NSTerminateLater` here because it enables a special mode |
| 283 | // of the event loop that is meant specifically for handling modals. We also make sure to |
| 284 | // first exit any fullscreen windows before we hide--`NSApplication#hide` is a NOOP if there |
| 285 | // are any full screen windows. |
| 286 | |
| 287 | [self unfullscreenAllWindows:application]; |
| 288 | [application hide:nil]; |
| 289 | terminateOnHide = YES; |
| 290 | |
| 291 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC), |
| 292 | dispatch_get_main_queue(), ^{ |
| 293 | [application terminate:nil]; |
| 294 | }); |
| 295 | } |
| 296 | return NSTerminateCancel; |
| 297 | } |
| 298 | |
| 299 | - (void)applicationDidHide:(NSNotification *)note { |
| 300 | if (terminateOnHide) { |
| 301 | NSApplication *app = note.object; |
| 302 | [app terminate:nil]; |
| 303 | } |
| 304 | } |
| 305 | |
| 306 | - (void)applicationDidResignActive:(NSNotification *)note { |
| 307 | if (rustWrapper) warp_app_did_resign_active(note.object); |
| 308 | } |
| 309 | |
| 310 | - (void)applicationWillTerminate:(NSNotification *)note { |
| 311 | if (rustWrapper) warp_app_will_terminate(note.object); |
| 312 | } |
| 313 | |
| 314 | - (void)application:(NSApplication *)sender openFiles:(NSArray<NSString *> *)filenames { |
| 315 | if (rustWrapper) warp_app_open_files(sender, filenames); |
| 316 | } |
| 317 | |
| 318 | - (void)application:(NSApplication *)application openURLs:(NSArray<NSURL *> *)urls { |
| 319 | if (rustWrapper) warp_app_open_urls(application, urls); |
| 320 | } |
| 321 | |
| 322 | // This is called when clicking on the app in the Dock or from Finder. |
| 323 | // If there's no visible windows, we will open one. |
| 324 | - (BOOL)applicationShouldHandleReopen:(NSApplication *)app hasVisibleWindows:(BOOL)flag { |
| 325 | if (rustWrapper && !flag) { |
| 326 | warp_app_new_window(app); |
| 327 | return NO; // do nothing |
| 328 | } |
| 329 | return YES; |
| 330 | } |
| 331 | |
| 332 | - (void)keyWindowChanged:(NSNotification *)note { |
| 333 | // We use an async dispatch here for two reasons: |
| 334 | // 1. When the active window changes, this will be called twice (once for resign, once for |
| 335 | // activated). We can coalesce these calls. |
| 336 | // 2. When a new window is created, warp will activate it; if we recursively call back into |
| 337 | // warp then we will cause the app to be mutably borrowed while already borrowed. |
| 338 | if (!hasPendingActiveWindowChange) { |
| 339 | hasPendingActiveWindowChange = YES; |
| 340 | dispatch_async(dispatch_get_main_queue(), ^{ |
| 341 | self->hasPendingActiveWindowChange = NO; |
| 342 | if (self->rustWrapper) warp_app_active_window_changed(self); |
| 343 | }); |
| 344 | } |
| 345 | } |
| 346 | |
| 347 | - (void)windowMoved:(NSNotification *)note { |
| 348 | // We need to use async dispatch here because the event loop in appkit calls the |
| 349 | // app notification first before calling the window notification. Since we are updating |
| 350 | // the window properties within the window notification, we need to make sure this |
| 351 | // callback gets triggered after the window notification. Thus using the async dispatch |
| 352 | // here ensures we always save the most up-to-date value within the database. |
| 353 | dispatch_async(dispatch_get_main_queue(), ^{ |
| 354 | if (self->rustWrapper) warp_app_window_did_move(self); |
| 355 | }); |
| 356 | } |
| 357 | |
| 358 | - (void)windowResized:(NSNotification *)note { |
| 359 | dispatch_async(dispatch_get_main_queue(), ^{ |
| 360 | if (self->rustWrapper) warp_app_window_did_resize(self); |
| 361 | }); |
| 362 | } |
| 363 | |
| 364 | - (void)screenChanged:(NSNotification *)note { |
| 365 | dispatch_async(dispatch_get_main_queue(), ^{ |
| 366 | if (self->rustWrapper) warp_app_screen_did_change(self); |
| 367 | }); |
| 368 | } |
| 369 | |
| 370 | - (void)cpuAwakened:(NSNotification *)note { |
| 371 | dispatch_async(dispatch_get_main_queue(), ^{ |
| 372 | if (self->rustWrapper) cpu_awakened(self); |
| 373 | }); |
| 374 | } |
| 375 | |
| 376 | - (void)cpuWillSleep:(NSNotification *)note { |
| 377 | dispatch_async(dispatch_get_main_queue(), ^{ |
| 378 | if (self->rustWrapper) cpu_will_sleep(self); |
| 379 | }); |
| 380 | } |
| 381 | |
| 382 | - (void)menuNeedsUpdate:(NSMenu *)menu { |
| 383 | // Trigger warp_menu_item_needs_update for every item with our class set as its represented |
| 384 | // object. |
| 385 | Class warpHandlerClass = [WarpCustomMenuItemHandler class]; |
| 386 | for (NSMenuItem *item in menu.itemArray) { |
| 387 | id obj = item.representedObject; |
| 388 | if ([obj isKindOfClass:warpHandlerClass]) { |
| 389 | [obj itemNeedsUpdate:item]; |
| 390 | } |
| 391 | } |
| 392 | } |
| 393 | |
| 394 | - (void)setReachabilityListener { |
| 395 | internetReachable = [[Reachability reachabilityWithHostname:@"0.0.0.0"] retain]; |
| 396 | |
| 397 | // Internet is reachable. |
| 398 | internetReachable.reachableBlock = ^(Reachability *reach __unused) { |
| 399 | // Update the UI on the main thread. |
| 400 | dispatch_async(dispatch_get_main_queue(), ^{ |
| 401 | if (self->isReachable == nil || [self->isReachable intValue] == 0) { |
| 402 | self->isReachable = [NSNumber numberWithBool:YES]; |
| 403 | if (self->rustWrapper) warp_app_internet_reachability_changed(self, YES); |
| 404 | } |
| 405 | }); |
| 406 | }; |
| 407 | |
| 408 | // Internet is not reachable. |
| 409 | internetReachable.unreachableBlock = ^(Reachability *reach __unused) { |
| 410 | // Update the UI on the main thread. |
| 411 | dispatch_async(dispatch_get_main_queue(), ^{ |
| 412 | if (self->isReachable == nil || [self->isReachable intValue] > 0) { |
| 413 | self->isReachable = [NSNumber numberWithBool:NO]; |
| 414 | if (self->rustWrapper) warp_app_internet_reachability_changed(self, NO); |
| 415 | } |
| 416 | }); |
| 417 | }; |
| 418 | |
| 419 | // Dispatch an initial call to check internet reachability so app could get notified |
| 420 | // of the reachability status it starts in. |
| 421 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) { |
| 422 | BOOL internetIsReachable = [internetReachable isReachable]; |
| 423 | dispatch_async(dispatch_get_main_queue(), ^{ |
| 424 | if (self->isReachable == nil) { |
| 425 | self->isReachable = [NSNumber numberWithBool:internetIsReachable]; |
| 426 | if (self->rustWrapper) |
| 427 | warp_app_internet_reachability_changed(self, internetIsReachable); |
| 428 | } |
| 429 | }); |
| 430 | }); |
| 431 | |
| 432 | [internetReachable startNotifier]; |
| 433 | } |
| 434 | |
| 435 | // Returns a new NSMenu in the mac dock. Gets called every time we pull up the dock menu |
| 436 | - (NSMenu *)applicationDockMenu:(NSApplication *)sender { |
| 437 | return self.dockMenu; |
| 438 | } |
| 439 | |
| 440 | - (void)userNotificationCenter:(UNUserNotificationCenter *)center |
| 441 | didReceiveNotificationResponse:(UNNotificationResponse *)response |
| 442 | withCompletionHandler:(void (^)(void))completionHandler { |
| 443 | // Handle what happens when the user clicks the notification. Warp doesn't support any actions |
| 444 | // other than the default action currently. |
| 445 | if ([response.actionIdentifier isEqualToString:UNNotificationDefaultActionIdentifier]) { |
| 446 | NSDictionary *userInfo = response.notification.request.content.userInfo; |
| 447 | NSString *data = userInfo[@"DATA"]; |
| 448 | |
| 449 | if (rustWrapper) { |
| 450 | warp_app_notification_clicked(self, response.notification.date.timeIntervalSince1970, |
| 451 | data); |
| 452 | } |
| 453 | } |
| 454 | } |
| 455 | |
| 456 | @end |
| 457 | |
| 458 | @implementation WarpApplication { |
| 459 | // Rust expects an ivar with this name. |
| 460 | void *rustWrapper; |
| 461 | } |
| 462 | |
| 463 | - (void)setForceTermination { |
| 464 | WarpDelegate *delegate = (WarpDelegate *)self.delegate; |
| 465 | [delegate setForceTermination]; |
| 466 | } |
| 467 | |
| 468 | - (void)showModal:(NSAlert *)alert modalId:(NSUInteger)modalId { |
| 469 | dispatch_async(dispatch_get_main_queue(), ^{ |
| 470 | NSModalResponse response = configureAndRunModal(alert, self); |
| 471 | |
| 472 | BOOL disable_modal = alert.suppressionButton.state == NSControlStateValueOn; |
| 473 | // Subtracting `NSAlertFirstButtonReturn` from `response` yields the 0-based index of the |
| 474 | // button that was actually clicked. |
| 475 | warp_app_process_modal_response(self, modalId, response - NSAlertFirstButtonReturn, |
| 476 | disable_modal); |
| 477 | }); |
| 478 | } |
| 479 | |
| 480 | @end |
| 481 | |
| 482 | WarpApplication *get_warp_app() { |
| 483 | // Set up the delegate (once). |
| 484 | // The delegate is deliberately leaked. |
| 485 | WarpApplication *app = [WarpApplication sharedApplication]; |
| 486 | static dispatch_once_t once; |
| 487 | static id sharedDelegate; |
| 488 | dispatch_once(&once, ^{ |
| 489 | sharedDelegate = [[WarpDelegate alloc] init]; |
| 490 | [app setDelegate:sharedDelegate]; |
| 491 | |
| 492 | // Hack to work around the fact that warp is frequently tested as a |
| 493 | // standalone (unbundled) binary. |
| 494 | app.activationPolicy = NSApplicationActivationPolicyRegular; |
| 495 | }); |
| 496 | return app; |
| 497 | } |
| 498 | |
| 499 | // \return an empty NSMenu with the given title, setting up the delegate appropriately. |
| 500 | // The result is autoreleased. |
| 501 | NSMenu *make_delegated_menu(NSString *title) { |
| 502 | NSMenu *result = [[[NSMenu alloc] initWithTitle:title] autorelease]; |
| 503 | result.delegate = (WarpDelegate *)[[WarpApplication sharedApplication] delegate]; |
| 504 | return result; |
| 505 | } |
| 506 | |
| 507 | // Create Services, a system-defined standard menu on macOS |
| 508 | // The result is autoreleased. |
| 509 | NSMenuItem *make_services_menu_item() { |
| 510 | // Create the services menu. `servicesMenu` retains, so autorelease our +1 ownership. |
| 511 | NSApp.servicesMenu = [[[NSMenu alloc] init] autorelease]; |
| 512 | |
| 513 | // Create menu item for it |
| 514 | NSMenuItem *servicesItem = [[[NSMenuItem alloc] init] autorelease]; |
| 515 | servicesItem.title = @"Services"; |
| 516 | servicesItem.submenu = NSApp.servicesMenu; |
| 517 | |
| 518 | return servicesItem; |
| 519 | } |
| 520 | |
| 521 | // \return a new menu item that wraps the given context pointer. |
| 522 | // The pointer will be provided back to Warp in the callbacks (see menus.h). |
| 523 | // The result is autoreleased. |
| 524 | NSMenuItem *make_warp_custom_menu_item(void *context) { |
| 525 | WarpCustomMenuItemHandler *handler = |
| 526 | [[[WarpCustomMenuItemHandler alloc] initWithContext:context] autorelease]; |
| 527 | |
| 528 | // Sets action to NULL if menu item has submenu, so the menu doesn't close when item is clicked |
| 529 | NSMenuItem *item = [[[NSMenuItem alloc] initWithTitle:@"" |
| 530 | action:@selector(itemWasTriggered:) |
| 531 | keyEquivalent:@""] autorelease]; |
| 532 | item.representedObject = handler; |
| 533 | item.target = handler; |
| 534 | return item; |
| 535 | } |
| 536 | |
| 537 | NSString *executableInApplicationBundleWithIdentifier(NSString *bundle_path) { |
| 538 | NSBundle *bundle = [NSBundle bundleWithPath:bundle_path]; |
| 539 | NSString *executable = [bundle.bundlePath stringByAppendingPathComponent:@"Contents/MacOS"]; |
| 540 | executable = [executable |
| 541 | stringByAppendingPathComponent:[bundle |
| 542 | objectForInfoDictionaryKey:(id)kCFBundleExecutableKey]]; |
| 543 | return executable; |
| 544 | } |
| 545 | |
| 546 | NSString *absolutePathForApplicationBundleWithIdentifier(NSString *bundle_identifier) { |
| 547 | NSURL *url = |
| 548 | [[NSWorkspace sharedWorkspace] URLForApplicationWithBundleIdentifier:bundle_identifier]; |
| 549 | return url.path; |
| 550 | } |
| 551 | |
| 552 | BOOL isVoiceOverEnabled() { return [[NSWorkspace sharedWorkspace] isVoiceOverEnabled]; } |
| 553 |