Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T02:35:19.854Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

platform.rs

1538 lines · 59.3 KB · rust
1use crate::{
2    BoolExt, MacDispatcher, MacDisplay, MacKeyboardLayout, MacKeyboardMapper, MacWindow,
3    events::key_to_native, ns_string, pasteboard::Pasteboard, renderer,
4    set_active_window_cursor_style,
5};
6use anyhow::{Context as _, anyhow};
7use block::ConcreteBlock;
8use cocoa::{
9    appkit::{
10        NSApplication, NSApplicationActivationPolicy::NSApplicationActivationPolicyRegular,
11        NSControl as _, NSEventModifierFlags, NSMenu, NSMenuItem, NSModalResponse, NSOpenPanel,
12        NSSavePanel, NSVisualEffectState, NSVisualEffectView, NSWindow,
13    },
14    base::{BOOL, NO, YES, id, nil, selector},
15    foundation::{
16        NSArray, NSAutoreleasePool, NSBundle, NSInteger, NSProcessInfo, NSString, NSUInteger, NSURL,
17    },
18};
19use core_foundation::{
20    base::{CFRelease, CFType, CFTypeRef, OSStatus, TCFType},
21    boolean::CFBoolean,
22    data::CFData,
23    dictionary::{CFDictionary, CFDictionaryRef, CFMutableDictionary},
24    runloop::CFRunLoopRun,
25    string::{CFString, CFStringRef},
26};
27use ctor::ctor;
28use dispatch2::DispatchQueue;
29use futures::channel::oneshot;
30use gpui::{
31    Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, ForegroundExecutor,
32    KeyContext, Keymap, Menu, MenuItem, OsMenu, OwnedMenu, PathPromptOptions, Platform,
33    PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem,
34    PlatformWindow, Result, SystemMenuType, Task, ThermalState, WindowAppearance, WindowKind,
35    WindowParams, popup::PopupNotSupportedError,
36};
37use gpui_util::{ResultExt, new_std_command};
38use itertools::Itertools;
39use objc::{
40    class,
41    declare::ClassDecl,
42    msg_send,
43    runtime::{Class, Object, Sel},
44    sel, sel_impl,
45};
46use parking_lot::Mutex;
47use ptr::null_mut;
48use semver::Version;
49use std::{
50    cell::Cell,
51    ffi::{CStr, OsStr, c_void},
52    os::{raw::c_char, unix::ffi::OsStrExt},
53    path::{Path, PathBuf},
54    ptr,
55    rc::Rc,
56    slice, str,
57    sync::{
58        Arc, OnceLock,
59        atomic::{AtomicBool, Ordering},
60    },
61};
62
63#[allow(non_upper_case_globals)]
64const NSUTF8StringEncoding: NSUInteger = 4;
65
66const MAC_PLATFORM_IVAR: &str = "platform";
67static mut APP_CLASS: *const Class = ptr::null();
68static mut APP_DELEGATE_CLASS: *const Class = ptr::null();
69
70#[ctor(unsafe)]
71unsafe fn build_classes() {
72    unsafe {
73        APP_CLASS = {
74            let mut decl = ClassDecl::new("GPUIApplication", class!(NSApplication)).unwrap();
75            decl.add_ivar::<*mut c_void>(MAC_PLATFORM_IVAR);
76            decl.register()
77        }
78    };
79    unsafe {
80        APP_DELEGATE_CLASS = {
81            let mut decl = ClassDecl::new("GPUIApplicationDelegate", class!(NSResponder)).unwrap();
82            decl.add_ivar::<*mut c_void>(MAC_PLATFORM_IVAR);
83            decl.add_method(
84                sel!(applicationWillFinishLaunching:),
85                will_finish_launching as extern "C" fn(&mut Object, Sel, id),
86            );
87            decl.add_method(
88                sel!(applicationDidFinishLaunching:),
89                did_finish_launching as extern "C" fn(&mut Object, Sel, id),
90            );
91            decl.add_method(
92                sel!(applicationShouldHandleReopen:hasVisibleWindows:),
93                should_handle_reopen as extern "C" fn(&mut Object, Sel, id, bool),
94            );
95            decl.add_method(
96                sel!(applicationWillTerminate:),
97                will_terminate as extern "C" fn(&mut Object, Sel, id),
98            );
99            decl.add_method(
100                sel!(handleGPUIMenuItem:),
101                handle_menu_item as extern "C" fn(&mut Object, Sel, id),
102            );
103            // Add menu item handlers so that OS save panels have the correct key commands
104            decl.add_method(
105                sel!(cut:),
106                handle_menu_item as extern "C" fn(&mut Object, Sel, id),
107            );
108            decl.add_method(
109                sel!(copy:),
110                handle_menu_item as extern "C" fn(&mut Object, Sel, id),
111            );
112            decl.add_method(
113                sel!(paste:),
114                handle_menu_item as extern "C" fn(&mut Object, Sel, id),
115            );
116            decl.add_method(
117                sel!(selectAll:),
118                handle_menu_item as extern "C" fn(&mut Object, Sel, id),
119            );
120            decl.add_method(
121                sel!(undo:),
122                handle_menu_item as extern "C" fn(&mut Object, Sel, id),
123            );
124            decl.add_method(
125                sel!(redo:),
126                handle_menu_item as extern "C" fn(&mut Object, Sel, id),
127            );
128            decl.add_method(
129                sel!(validateMenuItem:),
130                validate_menu_item as extern "C" fn(&mut Object, Sel, id) -> bool,
131            );
132            decl.add_method(
133                sel!(menuWillOpen:),
134                menu_will_open as extern "C" fn(&mut Object, Sel, id),
135            );
136            decl.add_method(
137                sel!(applicationDockMenu:),
138                handle_dock_menu as extern "C" fn(&mut Object, Sel, id) -> id,
139            );
140            decl.add_method(
141                sel!(application:openURLs:),
142                open_urls as extern "C" fn(&mut Object, Sel, id, id),
143            );
144
145            decl.add_method(
146                sel!(onKeyboardLayoutChange:),
147                on_keyboard_layout_change as extern "C" fn(&mut Object, Sel, id),
148            );
149
150            decl.add_method(
151                sel!(onThermalStateChange:),
152                on_thermal_state_change as extern "C" fn(&mut Object, Sel, id),
153            );
154
155            decl.add_method(
156                sel!(onSystemWake:),
157                on_system_wake as extern "C" fn(&mut Object, Sel, id),
158            );
159
160            decl.register()
161        }
162    }
163}
164
165pub struct MacPlatform(Mutex<MacPlatformState>);
166
167pub(crate) struct MacPlatformState {
168    background_executor: BackgroundExecutor,
169    foreground_executor: ForegroundExecutor,
170    text_system: Arc<dyn PlatformTextSystem>,
171    renderer_context: renderer::Context,
172    headless: bool,
173    general_pasteboard: Pasteboard,
174    find_pasteboard: Pasteboard,
175    reopen: Option<Box<dyn FnMut()>>,
176    on_keyboard_layout_change: Option<Box<dyn FnMut()>>,
177    on_thermal_state_change: Option<Box<dyn FnMut()>>,
178    on_system_wake: Option<Box<dyn FnMut()>>,
179    system_wake_observer_registered: bool,
180    quit: Option<Box<dyn FnMut()>>,
181    menu_command: Option<Box<dyn FnMut(&dyn Action)>>,
182    validate_menu_command: Option<Box<dyn FnMut(&dyn Action) -> bool>>,
183    will_open_menu: Option<Box<dyn FnMut()>>,
184    menu_actions: Vec<Box<dyn Action>>,
185    open_urls: Option<Box<dyn FnMut(Vec<String>)>>,
186    finish_launching: Option<Box<dyn FnOnce()>>,
187    dock_menu: Option<id>,
188    menus: Option<Vec<OwnedMenu>>,
189    keyboard_mapper: Rc<MacKeyboardMapper>,
190    /// Mirrors `[NSCursor setHiddenUntilMouseMoves:]` state, which AppKit doesn't expose.
191    cursor_visible: Arc<AtomicBool>,
192    system_notifications: crate::system_notifications::SystemNotificationState,
193}
194
195impl MacPlatform {
196    pub fn new(headless: bool) -> Self {
197        let dispatcher = Arc::new(MacDispatcher::new());
198
199        #[cfg(feature = "font-kit")]
200        let text_system = Arc::new(crate::MacTextSystem::new());
201
202        #[cfg(not(feature = "font-kit"))]
203        let text_system = {
204            if !headless {
205                log::warn!(
206                    "gpui_macos was compiled without the `font-kit` feature, so no text will be rendered."
207                );
208            }
209            Arc::new(gpui::NoopTextSystem::new())
210        };
211
212        let keyboard_layout = MacKeyboardLayout::new();
213        let keyboard_mapper = Rc::new(MacKeyboardMapper::new(keyboard_layout.id()));
214
215        Self(Mutex::new(MacPlatformState {
216            headless,
217            text_system,
218            background_executor: BackgroundExecutor::new(dispatcher.clone()),
219            foreground_executor: ForegroundExecutor::new(dispatcher),
220            renderer_context: renderer::Context::default(),
221            general_pasteboard: Pasteboard::general(),
222            find_pasteboard: Pasteboard::find(),
223            reopen: None,
224            quit: None,
225            menu_command: None,
226            validate_menu_command: None,
227            will_open_menu: None,
228            menu_actions: Default::default(),
229            open_urls: None,
230            finish_launching: None,
231            dock_menu: None,
232            on_keyboard_layout_change: None,
233            on_thermal_state_change: None,
234            on_system_wake: None,
235            system_wake_observer_registered: false,
236            menus: None,
237            keyboard_mapper,
238            cursor_visible: Arc::new(AtomicBool::new(true)),
239            system_notifications: crate::system_notifications::SystemNotificationState::new(),
240        }))
241    }
242
243    unsafe fn create_menu_bar(
244        &self,
245        menus: &Vec<Menu>,
246        delegate: id,
247        actions: &mut Vec<Box<dyn Action>>,
248        keymap: &Keymap,
249    ) -> id {
250        unsafe {
251            let application_menu = NSMenu::new(nil).autorelease();
252            application_menu.setDelegate_(delegate);
253
254            for menu_config in menus {
255                let menu = NSMenu::new(nil).autorelease();
256                let menu_title = ns_string(&menu_config.name);
257                menu.setTitle_(menu_title);
258                menu.setDelegate_(delegate);
259
260                for item_config in &menu_config.items {
261                    menu.addItem_(Self::create_menu_item(
262                        item_config,
263                        delegate,
264                        actions,
265                        keymap,
266                    ));
267                }
268
269                let menu_item = NSMenuItem::new(nil).autorelease();
270                menu_item.setTitle_(menu_title);
271                menu_item.setSubmenu_(menu);
272                application_menu.addItem_(menu_item);
273
274                if menu_config.name == "Window" {
275                    let app: id = msg_send![APP_CLASS, sharedApplication];
276                    app.setWindowsMenu_(menu);
277                }
278            }
279
280            application_menu
281        }
282    }
283
284    unsafe fn create_dock_menu(
285        &self,
286        menu_items: Vec<MenuItem>,
287        delegate: id,
288        actions: &mut Vec<Box<dyn Action>>,
289        keymap: &Keymap,
290    ) -> id {
291        unsafe {
292            let dock_menu = NSMenu::new(nil);
293            dock_menu.setDelegate_(delegate);
294            for item_config in menu_items {
295                dock_menu.addItem_(Self::create_menu_item(
296                    &item_config,
297                    delegate,
298                    actions,
299                    keymap,
300                ));
301            }
302
303            dock_menu
304        }
305    }
306
307    unsafe fn create_menu_item(
308        item: &MenuItem,
309        delegate: id,
310        actions: &mut Vec<Box<dyn Action>>,
311        keymap: &Keymap,
312    ) -> id {
313        static DEFAULT_CONTEXT: OnceLock<Vec<KeyContext>> = OnceLock::new();
314
315        unsafe {
316            match item {
317                MenuItem::Separator => NSMenuItem::separatorItem(nil),
318                MenuItem::Action {
319                    name,
320                    action,
321                    os_action,
322                    checked,
323                    disabled,
324                } => {
325                    // Note that this is intentionally using earlier bindings, whereas typically
326                    // later ones take display precedence. See the discussion on
327                    // https://github.com/zed-industries/zed/issues/23621
328                    let keystrokes = keymap
329                        .bindings_for_action(action.as_ref())
330                        .find_or_first(|binding| {
331                            binding.predicate().is_none_or(|predicate| {
332                                predicate.eval(DEFAULT_CONTEXT.get_or_init(|| {
333                                    let mut workspace_context = KeyContext::new_with_defaults();
334                                    workspace_context.add("Workspace");
335                                    let mut pane_context = KeyContext::new_with_defaults();
336                                    pane_context.add("Pane");
337                                    let mut editor_context = KeyContext::new_with_defaults();
338                                    editor_context.add("Editor");
339
340                                    pane_context.extend(&editor_context);
341                                    workspace_context.extend(&pane_context);
342                                    vec![workspace_context]
343                                }))
344                            })
345                        })
346                        .map(|binding| binding.keystrokes());
347
348                    let selector = match os_action {
349                        Some(gpui::OsAction::Cut) => selector("cut:"),
350                        Some(gpui::OsAction::Copy) => selector("copy:"),
351                        Some(gpui::OsAction::Paste) => selector("paste:"),
352                        Some(gpui::OsAction::SelectAll) => selector("selectAll:"),
353                        // "undo:" and "redo:" are always disabled in our case, as
354                        // we don't have a NSTextView/NSTextField to enable them on.
355                        Some(gpui::OsAction::Undo) => selector("handleGPUIMenuItem:"),
356                        Some(gpui::OsAction::Redo) => selector("handleGPUIMenuItem:"),
357                        None => selector("handleGPUIMenuItem:"),
358                    };
359
360                    let item;
361                    if let Some(keystrokes) = keystrokes {
362                        if keystrokes.len() == 1 {
363                            let keystroke = &keystrokes[0];
364                            let mut mask = NSEventModifierFlags::empty();
365                            for (modifier, flag) in &[
366                                (
367                                    keystroke.modifiers().platform,
368                                    NSEventModifierFlags::NSCommandKeyMask,
369                                ),
370                                (
371                                    keystroke.modifiers().control,
372                                    NSEventModifierFlags::NSControlKeyMask,
373                                ),
374                                (
375                                    keystroke.modifiers().alt,
376                                    NSEventModifierFlags::NSAlternateKeyMask,
377                                ),
378                                (
379                                    keystroke.modifiers().shift,
380                                    NSEventModifierFlags::NSShiftKeyMask,
381                                ),
382                            ] {
383                                if *modifier {
384                                    mask |= *flag;
385                                }
386                            }
387
388                            item = NSMenuItem::alloc(nil)
389                                .initWithTitle_action_keyEquivalent_(
390                                    ns_string(name),
391                                    selector,
392                                    ns_string(key_to_native(keystroke.key()).as_ref()),
393                                )
394                                .autorelease();
395                            if Self::os_version() >= Version::new(12, 0, 0) {
396                                let _: () = msg_send![item, setAllowsAutomaticKeyEquivalentLocalization: NO];
397                            }
398                            item.setKeyEquivalentModifierMask_(mask);
399                        } else {
400                            item = NSMenuItem::alloc(nil)
401                                .initWithTitle_action_keyEquivalent_(
402                                    ns_string(name),
403                                    selector,
404                                    ns_string(""),
405                                )
406                                .autorelease();
407                        }
408                    } else {
409                        item = NSMenuItem::alloc(nil)
410                            .initWithTitle_action_keyEquivalent_(
411                                ns_string(name),
412                                selector,
413                                ns_string(""),
414                            )
415                            .autorelease();
416                    }
417
418                    if *checked {
419                        item.setState_(NSVisualEffectState::Active);
420                    }
421                    item.setEnabled_(if *disabled { NO } else { YES });
422
423                    let tag = actions.len() as NSInteger;
424                    let _: () = msg_send![item, setTag: tag];
425                    actions.push(action.boxed_clone());
426                    item
427                }
428                MenuItem::Submenu(Menu {
429                    name,
430                    items,
431                    disabled,
432                }) => {
433                    let item = NSMenuItem::new(nil).autorelease();
434                    let submenu = NSMenu::new(nil).autorelease();
435                    submenu.setDelegate_(delegate);
436                    for item in items {
437                        submenu.addItem_(Self::create_menu_item(item, delegate, actions, keymap));
438                    }
439                    item.setSubmenu_(submenu);
440                    item.setEnabled_(if *disabled { NO } else { YES });
441                    item.setTitle_(ns_string(name));
442                    item
443                }
444                MenuItem::SystemMenu(OsMenu { name, menu_type }) => {
445                    let item = NSMenuItem::new(nil).autorelease();
446                    let submenu = NSMenu::new(nil).autorelease();
447                    submenu.setDelegate_(delegate);
448                    item.setSubmenu_(submenu);
449                    item.setTitle_(ns_string(name));
450
451                    match menu_type {
452                        SystemMenuType::Services => {
453                            let app: id = msg_send![APP_CLASS, sharedApplication];
454                            app.setServicesMenu_(item);
455                        }
456                    }
457
458                    item
459                }
460            }
461        }
462    }
463
464    fn os_version() -> Version {
465        let version = unsafe {
466            let process_info = NSProcessInfo::processInfo(nil);
467            process_info.operatingSystemVersion()
468        };
469        Version::new(
470            version.majorVersion,
471            version.minorVersion,
472            version.patchVersion,
473        )
474    }
475}
476
477impl Platform for MacPlatform {
478    fn background_executor(&self) -> BackgroundExecutor {
479        self.0.lock().background_executor.clone()
480    }
481
482    fn foreground_executor(&self) -> gpui::ForegroundExecutor {
483        self.0.lock().foreground_executor.clone()
484    }
485
486    fn text_system(&self) -> Arc<dyn PlatformTextSystem> {
487        self.0.lock().text_system.clone()
488    }
489
490    fn run(&self, on_finish_launching: Box<dyn FnOnce()>) {
491        let mut state = self.0.lock();
492        if state.headless {
493            drop(state);
494            on_finish_launching();
495            unsafe { CFRunLoopRun() };
496        } else {
497            state.finish_launching = Some(on_finish_launching);
498            drop(state);
499        }
500
501        unsafe {
502            let app: id = msg_send![APP_CLASS, sharedApplication];
503            let app_delegate: id = msg_send![APP_DELEGATE_CLASS, new];
504            app.setDelegate_(app_delegate);
505
506            let self_ptr = self as *const Self as *const c_void;
507            (*app).set_ivar(MAC_PLATFORM_IVAR, self_ptr);
508            (*app_delegate).set_ivar(MAC_PLATFORM_IVAR, self_ptr);
509
510            let pool = NSAutoreleasePool::new(nil);
511            app.run();
512            pool.drain();
513
514            (*app).set_ivar(MAC_PLATFORM_IVAR, null_mut::<c_void>());
515            (*NSWindow::delegate(app)).set_ivar(MAC_PLATFORM_IVAR, null_mut::<c_void>());
516        }
517    }
518
519    fn quit(&self) {
520        // Quitting the app causes us to close windows, which invokes `Window::on_close` callbacks
521        // synchronously before this method terminates. If we call `Platform::quit` while holding a
522        // borrow of the app state (which most of the time we will do), we will end up
523        // double-borrowing the app state in the `on_close` callbacks for our open windows. To solve
524        // this, we make quitting the application asynchronous so that we aren't holding borrows to
525        // the app state on the stack when we actually terminate the app.
526
527        unsafe {
528            DispatchQueue::main().exec_async_f(ptr::null_mut(), quit);
529        }
530
531        extern "C" fn quit(_: *mut c_void) {
532            unsafe {
533                let app = NSApplication::sharedApplication(nil);
534                let _: () = msg_send![app, terminate: nil];
535            }
536        }
537    }
538
539    fn restart(&self, binary_path: Option<PathBuf>) {
540        use std::os::unix::process::CommandExt as _;
541
542        let app_pid = std::process::id().to_string();
543        let app_path = binary_path
544            .or_else(|| {
545                self.app_path()
546                    .ok()
547                    // When the app is not bundled, `app_path` returns the
548                    // directory containing the executable. Disregard this
549                    // and get the path to the executable itself.
550                    .and_then(|path| (path.extension()?.to_str()? == "app").then_some(path))
551            })
552            .unwrap_or_else(|| std::env::current_exe().unwrap());
553
554        // Wait until this process has exited and then re-open this path.
555        let script = r#"
556            while kill -0 $0 2> /dev/null; do
557                sleep 0.1
558            done
559            open "$1"
560        "#;
561
562        #[allow(
563            clippy::disallowed_methods,
564            reason = "We are restarting ourselves, using std command thus is fine"
565        )]
566        let restart_process = new_std_command("/bin/bash")
567            .arg("-c")
568            .arg(script)
569            .arg(app_pid)
570            .arg(app_path)
571            .process_group(0)
572            .spawn();
573
574        match restart_process {
575            Ok(_) => self.quit(),
576            Err(e) => log::error!("failed to spawn restart script: {:?}", e),
577        }
578    }
579
580    fn activate(&self, ignoring_other_apps: bool) {
581        unsafe {
582            let app = NSApplication::sharedApplication(nil);
583            app.activateIgnoringOtherApps_(ignoring_other_apps.to_objc());
584        }
585    }
586
587    fn hide(&self) {
588        unsafe {
589            let app = NSApplication::sharedApplication(nil);
590            let _: () = msg_send![app, hide: nil];
591        }
592    }
593
594    fn hide_other_apps(&self) {
595        unsafe {
596            let app = NSApplication::sharedApplication(nil);
597            let _: () = msg_send![app, hideOtherApplications: nil];
598        }
599    }
600
601    fn unhide_other_apps(&self) {
602        unsafe {
603            let app = NSApplication::sharedApplication(nil);
604            let _: () = msg_send![app, unhideAllApplications: nil];
605        }
606    }
607
608    fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>> {
609        Some(Rc::new(MacDisplay::primary()))
610    }
611
612    fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
613        MacDisplay::all()
614            .map(|screen| Rc::new(screen) as Rc<_>)
615            .collect()
616    }
617
618    #[cfg(feature = "screen-capture")]
619    fn is_screen_capture_supported(&self) -> bool {
620        let min_version = cocoa::foundation::NSOperatingSystemVersion::new(12, 3, 0);
621        crate::is_macos_version_at_least(min_version)
622    }
623
624    #[cfg(feature = "screen-capture")]
625    fn screen_capture_sources(
626        &self,
627    ) -> oneshot::Receiver<Result<Vec<Rc<dyn gpui::ScreenCaptureSource>>>> {
628        crate::screen_capture::get_sources()
629    }
630
631    fn active_window(&self) -> Option<AnyWindowHandle> {
632        MacWindow::active_window()
633    }
634
635    // Returns the windows ordered front-to-back, meaning that the active
636    // window is the first one in the returned vec.
637    fn window_stack(&self) -> Option<Vec<AnyWindowHandle>> {
638        Some(MacWindow::ordered_windows())
639    }
640
641    fn open_window(
642        &self,
643        handle: AnyWindowHandle,
644        options: WindowParams,
645    ) -> Result<Box<dyn PlatformWindow>> {
646        // Native popups are not implemented on macOS yet. Rejecting lets callers fall back to
647        // gpui's in-window popovers.
648        if let WindowKind::AnchoredPopup(_) = options.kind {
649            return Err(PopupNotSupportedError.into());
650        }
651
652        let (cursor_visible, foreground_executor, background_executor, renderer_context) = {
653            let guard = self.0.lock();
654            (
655                guard.cursor_visible.clone(),
656                guard.foreground_executor.clone(),
657                guard.background_executor.clone(),
658                guard.renderer_context.clone(),
659            )
660        };
661
662        Ok(Box::new(MacWindow::open(
663            handle,
664            options,
665            cursor_visible,
666            foreground_executor,
667            background_executor,
668            renderer_context,
669        )))
670    }
671
672    fn window_appearance(&self) -> WindowAppearance {
673        unsafe {
674            let app = NSApplication::sharedApplication(nil);
675            let appearance: id = msg_send![app, effectiveAppearance];
676            crate::window_appearance::window_appearance_from_native(appearance)
677        }
678    }
679
680    fn open_url(&self, url: &str) {
681        unsafe {
682            let ns_url = NSURL::alloc(nil).initWithString_(ns_string(url));
683            if ns_url.is_null() {
684                log::error!("Failed to create NSURL from string: {}", url);
685                return;
686            }
687            let url = ns_url.autorelease();
688            let workspace: id = msg_send![class!(NSWorkspace), sharedWorkspace];
689            msg_send![workspace, openURL: url]
690        }
691    }
692
693    fn register_url_scheme(&self, scheme: &str) -> Task<anyhow::Result<()>> {
694        // API only available post Monterey
695        // https://developer.apple.com/documentation/appkit/nsworkspace/3753004-setdefaultapplicationaturl
696        let (done_tx, done_rx) = oneshot::channel();
697        if Self::os_version() < Version::new(12, 0, 0) {
698            return Task::ready(Err(anyhow!(
699                "macOS 12.0 or later is required to register URL schemes"
700            )));
701        }
702
703        let bundle_id = unsafe {
704            let bundle: id = msg_send![class!(NSBundle), mainBundle];
705            let bundle_id: id = msg_send![bundle, bundleIdentifier];
706            if bundle_id == nil {
707                return Task::ready(Err(anyhow!("Can only register URL scheme in bundled apps")));
708            }
709            bundle_id
710        };
711
712        unsafe {
713            let workspace: id = msg_send![class!(NSWorkspace), sharedWorkspace];
714            let scheme: id = ns_string(scheme);
715            let app: id = msg_send![workspace, URLForApplicationWithBundleIdentifier: bundle_id];
716            if app == nil {
717                return Task::ready(Err(anyhow!(
718                    "Cannot register URL scheme until app is installed"
719                )));
720            }
721            let done_tx = Cell::new(Some(done_tx));
722            let block = ConcreteBlock::new(move |error: id| {
723                let result = if error == nil {
724                    Ok(())
725                } else {
726                    let msg: id = msg_send![error, localizedDescription];
727                    Err(anyhow!("Failed to register: {msg:?}"))
728                };
729
730                if let Some(done_tx) = done_tx.take() {
731                    let _ = done_tx.send(result);
732                }
733            });
734            let block = block.copy();
735            let _: () = msg_send![workspace, setDefaultApplicationAtURL: app toOpenURLsWithScheme: scheme completionHandler: block];
736        }
737
738        self.background_executor()
739            .spawn(async { done_rx.await.map_err(|e| anyhow!(e))? })
740    }
741
742    fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>) {
743        self.0.lock().open_urls = Some(callback);
744    }
745
746    fn prompt_for_paths(
747        &self,
748        options: PathPromptOptions,
749    ) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>> {
750        let (done_tx, done_rx) = oneshot::channel();
751        self.foreground_executor()
752            .spawn(async move {
753                unsafe {
754                    let panel = NSOpenPanel::openPanel(nil);
755                    panel.setCanChooseDirectories_(options.directories.to_objc());
756                    panel.setCanChooseFiles_(options.files.to_objc());
757                    panel.setAllowsMultipleSelection_(options.multiple.to_objc());
758
759                    panel.setCanCreateDirectories(true.to_objc());
760                    panel.setResolvesAliases_(false.to_objc());
761                    let done_tx = Cell::new(Some(done_tx));
762                    let block = ConcreteBlock::new(move |response: NSModalResponse| {
763                        let result = if response == NSModalResponse::NSModalResponseOk {
764                            let mut result = Vec::new();
765                            let urls = panel.URLs();
766                            for i in 0..urls.count() {
767                                let url = urls.objectAtIndex(i);
768                                if url.isFileURL() == YES
769                                    && let Ok(path) = ns_url_to_path(url)
770                                {
771                                    result.push(path)
772                                }
773                            }
774                            Some(result)
775                        } else {
776                            None
777                        };
778
779                        if let Some(done_tx) = done_tx.take() {
780                            let _ = done_tx.send(Ok(result));
781                        }
782                    });
783                    let block = block.copy();
784
785                    if let Some(prompt) = options.prompt {
786                        let _: () = msg_send![panel, setPrompt: ns_string(&prompt)];
787                    }
788
789                    let _: () = msg_send![panel, beginWithCompletionHandler: block];
790                }
791            })
792            .detach();
793        done_rx
794    }
795
796    fn prompt_for_new_path(
797        &self,
798        directory: &Path,
799        suggested_name: Option<&str>,
800    ) -> oneshot::Receiver<Result<Option<PathBuf>>> {
801        let directory = directory.to_owned();
802        let suggested_name = suggested_name.map(|s| s.to_owned());
803        let (done_tx, done_rx) = oneshot::channel();
804        self.foreground_executor()
805            .spawn(async move {
806                unsafe {
807                    let panel = NSSavePanel::savePanel(nil);
808                    let path = ns_string(directory.to_string_lossy().as_ref());
809                    let url = NSURL::fileURLWithPath_isDirectory_(nil, path, true.to_objc());
810                    panel.setDirectoryURL(url);
811
812                    if let Some(suggested_name) = suggested_name {
813                        let name_string = ns_string(&suggested_name);
814                        let _: () = msg_send![panel, setNameFieldStringValue: name_string];
815                    }
816
817                    let done_tx = Cell::new(Some(done_tx));
818                    let block = ConcreteBlock::new(move |response: NSModalResponse| {
819                        let mut result = None;
820                        if response == NSModalResponse::NSModalResponseOk {
821                            let url = panel.URL();
822                            if url.isFileURL() == YES {
823                                result = ns_url_to_path(panel.URL()).ok().map(|mut result| {
824                                    let Some(filename) = result.file_name() else {
825                                        return result;
826                                    };
827                                    let chunks = filename
828                                        .as_bytes()
829                                        .split(|&b| b == b'.')
830                                        .collect::<Vec<_>>();
831
832                                    // https://github.com/zed-industries/zed/issues/16969
833                                    // Workaround a bug in macOS Sequoia that adds an extra file-extension
834                                    // sometimes. e.g. `a.sql` becomes `a.sql.s` or `a.txtx` becomes `a.txtx.txt`
835                                    //
836                                    // This is conditional on OS version because I'd like to get rid of it, so that
837                                    // you can manually create a file called `a.sql.s`. That said it seems better
838                                    // to break that use-case than breaking `a.sql`.
839                                    if chunks.len() == 3
840                                        && chunks[1].starts_with(chunks[2])
841                                        && Self::os_version() >= Version::new(15, 0, 0)
842                                    {
843                                        let new_filename = OsStr::from_bytes(
844                                            &filename.as_bytes()
845                                                [..chunks[0].len() + 1 + chunks[1].len()],
846                                        )
847                                        .to_owned();
848                                        result.set_file_name(&new_filename);
849                                    }
850                                    result
851                                })
852                            }
853                        }
854
855                        if let Some(done_tx) = done_tx.take() {
856                            let _ = done_tx.send(Ok(result));
857                        }
858                    });
859                    let block = block.copy();
860                    let _: () = msg_send![panel, beginWithCompletionHandler: block];
861                }
862            })
863            .detach();
864
865        done_rx
866    }
867
868    fn can_select_mixed_files_and_dirs(&self) -> bool {
869        true
870    }
871
872    fn reveal_path(&self, path: &Path) {
873        unsafe {
874            let path = path.to_path_buf();
875            self.0
876                .lock()
877                .background_executor
878                .spawn(async move {
879                    let full_path = ns_string(path.to_str().unwrap_or(""));
880                    let root_full_path = ns_string("");
881                    let workspace: id = msg_send![class!(NSWorkspace), sharedWorkspace];
882                    let _: BOOL = msg_send![
883                        workspace,
884                        selectFile: full_path
885                        inFileViewerRootedAtPath: root_full_path
886                    ];
887                })
888                .detach();
889        }
890    }
891
892    fn open_with_system(&self, path: &Path) {
893        let path = path.to_owned();
894        self.0
895            .lock()
896            .background_executor
897            .spawn(async move {
898                #[allow(
899                    clippy::disallowed_methods,
900                    reason = "running on a background thread, so blocking is fine"
901                )]
902                new_std_command("open")
903                    .arg("--")
904                    .arg(path)
905                    .status()
906                    .context("invoking open command")
907                    .log_err();
908            })
909            .detach();
910    }
911
912    fn on_quit(&self, callback: Box<dyn FnMut()>) {
913        self.0.lock().quit = Some(callback);
914    }
915
916    fn on_reopen(&self, callback: Box<dyn FnMut()>) {
917        self.0.lock().reopen = Some(callback);
918    }
919
920    fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>) {
921        self.0.lock().on_keyboard_layout_change = Some(callback);
922    }
923
924    fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>) {
925        self.0.lock().menu_command = Some(callback);
926    }
927
928    fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>) {
929        self.0.lock().will_open_menu = Some(callback);
930    }
931
932    fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>) {
933        self.0.lock().validate_menu_command = Some(callback);
934    }
935
936    fn on_thermal_state_change(&self, callback: Box<dyn FnMut()>) {
937        self.0.lock().on_thermal_state_change = Some(callback);
938    }
939
940    fn on_system_wake(&self, callback: Box<dyn FnMut()>) {
941        let mut state = self.0.lock();
942        state.on_system_wake = Some(callback);
943        if state.system_wake_observer_registered {
944            return;
945        }
946        drop(state);
947
948        // SAFETY: APP_CLASS is registered during startup and returns the shared NSApplication.
949        unsafe {
950            let app: id = msg_send![APP_CLASS, sharedApplication];
951            let delegate: id = msg_send![app, delegate];
952            if delegate != nil {
953                register_system_wake_observer(delegate);
954                self.0.lock().system_wake_observer_registered = true;
955            }
956        }
957    }
958
959    fn thermal_state(&self) -> ThermalState {
960        unsafe {
961            let process_info: id = msg_send![class!(NSProcessInfo), processInfo];
962            let state: NSInteger = msg_send![process_info, thermalState];
963            match state {
964                0 => ThermalState::Nominal,
965                1 => ThermalState::Fair,
966                2 => ThermalState::Serious,
967                3 => ThermalState::Critical,
968                _ => ThermalState::Nominal,
969            }
970        }
971    }
972
973    fn show_system_notification(&self, notification: gpui::SystemNotification) {
974        let mut state = self.0.lock();
975        let executor = state.foreground_executor.clone();
976        state.system_notifications.show(&executor, notification);
977    }
978
979    fn dismiss_system_notification(&self, tag: &str) {
980        let mut state = self.0.lock();
981        let executor = state.foreground_executor.clone();
982        state.system_notifications.dismiss(&executor, tag);
983    }
984
985    fn on_system_notification_response(
986        &self,
987        callback: Box<dyn FnMut(gpui::SystemNotificationResponse)>,
988    ) {
989        let mut state = self.0.lock();
990        let executor = state.foreground_executor.clone();
991        state.system_notifications.on_response(&executor, callback);
992    }
993
994    fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
995        Box::new(MacKeyboardLayout::new())
996    }
997
998    fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
999        self.0.lock().keyboard_mapper.clone()
1000    }
1001
1002    fn app_path(&self) -> Result<PathBuf> {
1003        unsafe {
1004            let bundle: id = NSBundle::mainBundle();
1005            anyhow::ensure!(!bundle.is_null(), "app is not running inside a bundle");
1006            Ok(path_from_objc(msg_send![bundle, bundlePath]))
1007        }
1008    }
1009
1010    fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap) {
1011        unsafe {
1012            let app: id = msg_send![APP_CLASS, sharedApplication];
1013            let mut state = self.0.lock();
1014            let actions = &mut state.menu_actions;
1015            let menu = self.create_menu_bar(&menus, NSWindow::delegate(app), actions, keymap);
1016            drop(state);
1017            app.setMainMenu_(menu);
1018        }
1019        self.0.lock().menus = Some(menus.into_iter().map(|menu| menu.owned()).collect());
1020    }
1021
1022    fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
1023        self.0.lock().menus.clone()
1024    }
1025
1026    fn set_dock_menu(&self, menu: Vec<MenuItem>, keymap: &Keymap) {
1027        unsafe {
1028            let app: id = msg_send![APP_CLASS, sharedApplication];
1029            let mut state = self.0.lock();
1030            let actions = &mut state.menu_actions;
1031            let new = self.create_dock_menu(menu, NSWindow::delegate(app), actions, keymap);
1032            if let Some(old) = state.dock_menu.replace(new) {
1033                CFRelease(old as _)
1034            }
1035        }
1036    }
1037
1038    fn add_recent_document(&self, path: &Path) {
1039        if let Some(path_str) = path.to_str() {
1040            unsafe {
1041                let document_controller: id =
1042                    msg_send![class!(NSDocumentController), sharedDocumentController];
1043                let url: id = NSURL::fileURLWithPath_(nil, ns_string(path_str));
1044                let _: () = msg_send![document_controller, noteNewRecentDocumentURL:url];
1045            }
1046        }
1047    }
1048
1049    fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf> {
1050        unsafe {
1051            let bundle: id = NSBundle::mainBundle();
1052            anyhow::ensure!(!bundle.is_null(), "app is not running inside a bundle");
1053            let name = ns_string(name);
1054            let url: id = msg_send![bundle, URLForAuxiliaryExecutable: name];
1055            anyhow::ensure!(!url.is_null(), "resource not found");
1056            ns_url_to_path(url)
1057        }
1058    }
1059
1060    /// Match cursor style to one of the styles available
1061    /// in macOS's [NSCursor](https://developer.apple.com/documentation/appkit/nscursor).
1062    fn set_cursor_style(&self, style: CursorStyle) {
1063        unsafe {
1064            set_active_window_cursor_style(style);
1065        }
1066    }
1067
1068    fn hide_cursor_until_mouse_moves(&self) {
1069        let cursor_visible = self.0.lock().cursor_visible.clone();
1070        if !cursor_visible.swap(false, Ordering::Relaxed) {
1071            return;
1072        }
1073        unsafe {
1074            let _: () = msg_send![class!(NSCursor), setHiddenUntilMouseMoves: YES];
1075        }
1076    }
1077
1078    fn is_cursor_visible(&self) -> bool {
1079        self.0.lock().cursor_visible.load(Ordering::Relaxed)
1080    }
1081
1082    fn should_auto_hide_scrollbars(&self) -> bool {
1083        #[allow(non_upper_case_globals)]
1084        const NSScrollerStyleOverlay: NSInteger = 1;
1085
1086        unsafe {
1087            let style: NSInteger = msg_send![class!(NSScroller), preferredScrollerStyle];
1088            style == NSScrollerStyleOverlay
1089        }
1090    }
1091
1092    fn read_from_clipboard(&self) -> Option<ClipboardItem> {
1093        let state = self.0.lock();
1094        state.general_pasteboard.read()
1095    }
1096
1097    fn write_to_clipboard(&self, item: ClipboardItem) {
1098        let state = self.0.lock();
1099        state.general_pasteboard.write(item);
1100    }
1101
1102    fn read_from_find_pasteboard(&self) -> Option<ClipboardItem> {
1103        let state = self.0.lock();
1104        state.find_pasteboard.read()
1105    }
1106
1107    fn write_to_find_pasteboard(&self, item: ClipboardItem) {
1108        let state = self.0.lock();
1109        state.find_pasteboard.write(item);
1110    }
1111
1112    fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>> {
1113        let url = url.to_string();
1114        let username = username.to_string();
1115        let password = password.to_vec();
1116        self.background_executor().spawn(async move {
1117            unsafe {
1118                use security::*;
1119
1120                let url = CFString::from(url.as_str());
1121                let username = CFString::from(username.as_str());
1122                let password = CFData::from_buffer(&password);
1123
1124                // First, check if there are already credentials for the given server. If so, then
1125                // update the username and password.
1126                let mut verb = "updating";
1127                let mut query_attrs = CFMutableDictionary::with_capacity(2);
1128                query_attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _);
1129                query_attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef());
1130
1131                let mut attrs = CFMutableDictionary::with_capacity(4);
1132                attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _);
1133                attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef());
1134                attrs.set(kSecAttrAccount as *const _, username.as_CFTypeRef());
1135                attrs.set(kSecValueData as *const _, password.as_CFTypeRef());
1136
1137                let mut status = SecItemUpdate(
1138                    query_attrs.as_concrete_TypeRef(),
1139                    attrs.as_concrete_TypeRef(),
1140                );
1141
1142                // If there were no existing credentials for the given server, then create them.
1143                if status == errSecItemNotFound {
1144                    verb = "creating";
1145                    status = SecItemAdd(attrs.as_concrete_TypeRef(), ptr::null_mut());
1146                }
1147                anyhow::ensure!(status == errSecSuccess, "{verb} password failed: {status}");
1148            }
1149            Ok(())
1150        })
1151    }
1152
1153    fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>> {
1154        let url = url.to_string();
1155        self.background_executor().spawn(async move {
1156            let url = CFString::from(url.as_str());
1157            let cf_true = CFBoolean::true_value().as_CFTypeRef();
1158
1159            unsafe {
1160                use security::*;
1161
1162                // Find any credentials for the given server URL.
1163                let mut attrs = CFMutableDictionary::with_capacity(5);
1164                attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _);
1165                attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef());
1166                attrs.set(kSecReturnAttributes as *const _, cf_true);
1167                attrs.set(kSecReturnData as *const _, cf_true);
1168
1169                let mut result = CFTypeRef::from(ptr::null());
1170                let status = SecItemCopyMatching(attrs.as_concrete_TypeRef(), &mut result);
1171                match status {
1172                    security::errSecSuccess => {}
1173                    security::errSecItemNotFound | security::errSecUserCanceled => return Ok(None),
1174                    _ => anyhow::bail!("reading password failed: {status}"),
1175                }
1176
1177                let result = CFType::wrap_under_create_rule(result)
1178                    .downcast::<CFDictionary>()
1179                    .context("keychain item was not a dictionary")?;
1180                let username = result
1181                    .find(kSecAttrAccount as *const _)
1182                    .context("account was missing from keychain item")?;
1183                let username = CFType::wrap_under_get_rule(*username)
1184                    .downcast::<CFString>()
1185                    .context("account was not a string")?;
1186                let password = result
1187                    .find(kSecValueData as *const _)
1188                    .context("password was missing from keychain item")?;
1189                let password = CFType::wrap_under_get_rule(*password)
1190                    .downcast::<CFData>()
1191                    .context("password was not a string")?;
1192
1193                Ok(Some((username.to_string(), password.bytes().to_vec())))
1194            }
1195        })
1196    }
1197
1198    fn delete_credentials(&self, url: &str) -> Task<Result<()>> {
1199        let url = url.to_string();
1200
1201        self.background_executor().spawn(async move {
1202            unsafe {
1203                use security::*;
1204
1205                let url = CFString::from(url.as_str());
1206                let mut query_attrs = CFMutableDictionary::with_capacity(2);
1207                query_attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _);
1208                query_attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef());
1209
1210                let status = SecItemDelete(query_attrs.as_concrete_TypeRef());
1211                anyhow::ensure!(status == errSecSuccess, "delete password failed: {status}");
1212            }
1213            Ok(())
1214        })
1215    }
1216}
1217
1218unsafe fn path_from_objc(path: id) -> PathBuf {
1219    let len = msg_send![path, lengthOfBytesUsingEncoding: NSUTF8StringEncoding];
1220    let bytes = unsafe { path.UTF8String() as *const u8 };
1221    let path = str::from_utf8(unsafe { slice::from_raw_parts(bytes, len) }).unwrap();
1222    PathBuf::from(path)
1223}
1224
1225unsafe fn get_mac_platform(object: &mut Object) -> &MacPlatform {
1226    unsafe {
1227        let platform_ptr: *mut c_void = *object.get_ivar(MAC_PLATFORM_IVAR);
1228        assert!(!platform_ptr.is_null());
1229        &*(platform_ptr as *const MacPlatform)
1230    }
1231}
1232
1233extern "C" fn will_finish_launching(_this: &mut Object, _: Sel, _: id) {
1234    unsafe {
1235        let user_defaults: id = msg_send![class!(NSUserDefaults), standardUserDefaults];
1236
1237        // The autofill heuristic controller causes slowdown and high CPU usage.
1238        // We don't know exactly why. This disables the full heuristic controller.
1239        //
1240        // Adapted from: https://github.com/ghostty-org/ghostty/pull/8625
1241        let name = ns_string("NSAutoFillHeuristicControllerEnabled");
1242        let existing_value: id = msg_send![user_defaults, objectForKey: name];
1243        if existing_value == nil {
1244            let false_value: id = msg_send![class!(NSNumber), numberWithBool:false];
1245            let _: () = msg_send![user_defaults, setObject: false_value forKey: name];
1246        }
1247    }
1248}
1249
1250extern "C" fn did_finish_launching(this: &mut Object, _: Sel, _: id) {
1251    unsafe {
1252        let app: id = msg_send![APP_CLASS, sharedApplication];
1253        app.setActivationPolicy_(NSApplicationActivationPolicyRegular);
1254
1255        let notification_center: *mut Object =
1256            msg_send![class!(NSNotificationCenter), defaultCenter];
1257        let name = ns_string("NSTextInputContextKeyboardSelectionDidChangeNotification");
1258        let _: () = msg_send![notification_center, addObserver: this as id
1259            selector: sel!(onKeyboardLayoutChange:)
1260            name: name
1261            object: nil
1262        ];
1263
1264        let thermal_name = ns_string("NSProcessInfoThermalStateDidChangeNotification");
1265        let process_info: id = msg_send![class!(NSProcessInfo), processInfo];
1266        let _: () = msg_send![notification_center, addObserver: this as id
1267            selector: sel!(onThermalStateChange:)
1268            name: thermal_name
1269            object: process_info
1270        ];
1271
1272        let observer = this as *mut Object as id;
1273        let platform = get_mac_platform(this);
1274        let callback = {
1275            let mut state = platform.0.lock();
1276            if state.on_system_wake.is_some() && !state.system_wake_observer_registered {
1277                register_system_wake_observer(observer);
1278                state.system_wake_observer_registered = true;
1279            }
1280            state.finish_launching.take()
1281        };
1282        if let Some(callback) = callback {
1283            callback();
1284        }
1285    }
1286}
1287
1288unsafe fn register_system_wake_observer(observer: id) {
1289    // SAFETY: observer is an Objective-C object implementing onSystemWake:.
1290    unsafe {
1291        let workspace: id = msg_send![class!(NSWorkspace), sharedWorkspace];
1292        let workspace_center: *mut Object = msg_send![workspace, notificationCenter];
1293        let wake_name = ns_string("NSWorkspaceDidWakeNotification");
1294        let _: () = msg_send![workspace_center, addObserver: observer
1295            selector: sel!(onSystemWake:)
1296            name: wake_name
1297            object: nil
1298        ];
1299    }
1300}
1301
1302extern "C" fn should_handle_reopen(this: &mut Object, _: Sel, _: id, has_open_windows: bool) {
1303    if !has_open_windows {
1304        let platform = unsafe { get_mac_platform(this) };
1305        let mut lock = platform.0.lock();
1306        if let Some(mut callback) = lock.reopen.take() {
1307            drop(lock);
1308            callback();
1309            platform.0.lock().reopen.get_or_insert(callback);
1310        }
1311    }
1312}
1313
1314extern "C" fn will_terminate(this: &mut Object, _: Sel, _: id) {
1315    let platform = unsafe { get_mac_platform(this) };
1316    let mut lock = platform.0.lock();
1317    if let Some(mut callback) = lock.quit.take() {
1318        drop(lock);
1319        callback();
1320        platform.0.lock().quit.get_or_insert(callback);
1321    }
1322}
1323
1324extern "C" fn on_keyboard_layout_change(this: &mut Object, _: Sel, _: id) {
1325    let platform = unsafe { get_mac_platform(this) };
1326    let mut lock = platform.0.lock();
1327    let keyboard_layout = MacKeyboardLayout::new();
1328    lock.keyboard_mapper = Rc::new(MacKeyboardMapper::new(keyboard_layout.id()));
1329    if let Some(mut callback) = lock.on_keyboard_layout_change.take() {
1330        drop(lock);
1331        callback();
1332        platform
1333            .0
1334            .lock()
1335            .on_keyboard_layout_change
1336            .get_or_insert(callback);
1337    }
1338}
1339
1340extern "C" fn on_thermal_state_change(this: &mut Object, _: Sel, _: id) {
1341    // Defer to the next run loop iteration to avoid re-entrant borrows of the App RefCell,
1342    // as NSNotificationCenter delivers this notification synchronously and it may fire while
1343    // the App is already borrowed (same pattern as quit() above).
1344    let platform = unsafe { get_mac_platform(this) };
1345    let platform_ptr = platform as *const MacPlatform as *mut c_void;
1346    unsafe {
1347        DispatchQueue::main().exec_async_f(platform_ptr, on_thermal_state_change);
1348    }
1349
1350    extern "C" fn on_thermal_state_change(context: *mut c_void) {
1351        let platform = unsafe { &*(context as *const MacPlatform) };
1352        let mut lock = platform.0.lock();
1353        if let Some(mut callback) = lock.on_thermal_state_change.take() {
1354            drop(lock);
1355            callback();
1356            platform
1357                .0
1358                .lock()
1359                .on_thermal_state_change
1360                .get_or_insert(callback);
1361        }
1362    }
1363}
1364
1365extern "C" fn on_system_wake(this: &mut Object, _: Sel, _: id) {
1366    // SAFETY: this is the registered app delegate carrying MAC_PLATFORM_IVAR.
1367    let platform = unsafe { get_mac_platform(this) };
1368    let platform_ptr = platform as *const MacPlatform as *mut c_void;
1369    // SAFETY: platform lives for the process lifetime while callbacks are registered.
1370    unsafe {
1371        DispatchQueue::main().exec_async_f(platform_ptr, on_system_wake);
1372    }
1373
1374    extern "C" fn on_system_wake(context: *mut c_void) {
1375        // SAFETY: context is the MacPlatform pointer queued above.
1376        let platform = unsafe { &*(context as *const MacPlatform) };
1377        let mut lock = platform.0.lock();
1378        if let Some(mut callback) = lock.on_system_wake.take() {
1379            drop(lock);
1380            callback();
1381            platform.0.lock().on_system_wake.get_or_insert(callback);
1382        }
1383    }
1384}
1385
1386extern "C" fn open_urls(this: &mut Object, _: Sel, _: id, urls: id) {
1387    let urls = unsafe {
1388        (0..urls.count())
1389            .filter_map(|i| {
1390                let url = urls.objectAtIndex(i);
1391                match CStr::from_ptr(url.absoluteString().UTF8String() as *mut c_char).to_str() {
1392                    Ok(string) => Some(string.to_string()),
1393                    Err(err) => {
1394                        log::error!("error converting path to string: {}", err);
1395                        None
1396                    }
1397                }
1398            })
1399            .collect::<Vec<_>>()
1400    };
1401    let platform = unsafe { get_mac_platform(this) };
1402    let mut lock = platform.0.lock();
1403    if let Some(mut callback) = lock.open_urls.take() {
1404        drop(lock);
1405        callback(urls);
1406        platform.0.lock().open_urls.get_or_insert(callback);
1407    }
1408}
1409
1410extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) {
1411    unsafe {
1412        let platform = get_mac_platform(this);
1413        let mut lock = platform.0.lock();
1414        if let Some(mut callback) = lock.menu_command.take() {
1415            let tag: NSInteger = msg_send![item, tag];
1416            let index = tag as usize;
1417            if let Some(action) = lock.menu_actions.get(index) {
1418                let action = action.boxed_clone();
1419                drop(lock);
1420                callback(&*action);
1421            }
1422            platform.0.lock().menu_command.get_or_insert(callback);
1423        }
1424    }
1425}
1426
1427extern "C" fn validate_menu_item(this: &mut Object, _: Sel, item: id) -> bool {
1428    unsafe {
1429        let mut result = false;
1430        let platform = get_mac_platform(this);
1431        let mut lock = platform.0.lock();
1432        if let Some(mut callback) = lock.validate_menu_command.take() {
1433            let tag: NSInteger = msg_send![item, tag];
1434            let index = tag as usize;
1435            if let Some(action) = lock.menu_actions.get(index) {
1436                let action = action.boxed_clone();
1437                drop(lock);
1438                result = callback(action.as_ref());
1439            }
1440            platform
1441                .0
1442                .lock()
1443                .validate_menu_command
1444                .get_or_insert(callback);
1445        }
1446        result
1447    }
1448}
1449
1450extern "C" fn menu_will_open(this: &mut Object, _: Sel, _: id) {
1451    unsafe {
1452        let platform = get_mac_platform(this);
1453        let mut lock = platform.0.lock();
1454        if let Some(mut callback) = lock.will_open_menu.take() {
1455            drop(lock);
1456            callback();
1457            platform.0.lock().will_open_menu.get_or_insert(callback);
1458        }
1459    }
1460}
1461
1462extern "C" fn handle_dock_menu(this: &mut Object, _: Sel, _: id) -> id {
1463    unsafe {
1464        let platform = get_mac_platform(this);
1465        let state = platform.0.lock();
1466        if let Some(id) = state.dock_menu {
1467            id
1468        } else {
1469            nil
1470        }
1471    }
1472}
1473
1474unsafe fn ns_url_to_path(url: id) -> Result<PathBuf> {
1475    let path: *mut c_char = msg_send![url, fileSystemRepresentation];
1476    anyhow::ensure!(!path.is_null(), "url is not a file path: {}", unsafe {
1477        CStr::from_ptr(url.absoluteString().UTF8String()).to_string_lossy()
1478    });
1479    Ok(PathBuf::from(OsStr::from_bytes(unsafe {
1480        CStr::from_ptr(path).to_bytes()
1481    })))
1482}
1483
1484#[link(name = "Carbon", kind = "framework")]
1485unsafe extern "C" {
1486    pub(super) fn TISCopyCurrentKeyboardLayoutInputSource() -> *mut Object;
1487    pub(super) fn TISCopyCurrentKeyboardInputSource() -> *mut Object;
1488    pub(super) fn TISGetInputSourceProperty(
1489        inputSource: *mut Object,
1490        propertyKey: *const c_void,
1491    ) -> *mut Object;
1492
1493    pub(super) fn UCKeyTranslate(
1494        keyLayoutPtr: *const ::std::os::raw::c_void,
1495        virtualKeyCode: u16,
1496        keyAction: u16,
1497        modifierKeyState: u32,
1498        keyboardType: u32,
1499        keyTranslateOptions: u32,
1500        deadKeyState: *mut u32,
1501        maxStringLength: usize,
1502        actualStringLength: *mut usize,
1503        unicodeString: *mut u16,
1504    ) -> u32;
1505    pub(super) fn LMGetKbdType() -> u16;
1506    pub(super) static kTISPropertyUnicodeKeyLayoutData: CFStringRef;
1507    pub(super) static kTISPropertyInputSourceID: CFStringRef;
1508    pub(super) static kTISPropertyLocalizedName: CFStringRef;
1509    pub(super) static kTISPropertyInputSourceIsASCIICapable: CFStringRef;
1510    pub(super) static kTISPropertyInputSourceType: CFStringRef;
1511    pub(super) static kTISTypeKeyboardInputMode: CFStringRef;
1512}
1513
1514mod security {
1515    #![allow(non_upper_case_globals)]
1516    use super::*;
1517
1518    #[link(name = "Security", kind = "framework")]
1519    unsafe extern "C" {
1520        pub static kSecClass: CFStringRef;
1521        pub static kSecClassInternetPassword: CFStringRef;
1522        pub static kSecAttrServer: CFStringRef;
1523        pub static kSecAttrAccount: CFStringRef;
1524        pub static kSecValueData: CFStringRef;
1525        pub static kSecReturnAttributes: CFStringRef;
1526        pub static kSecReturnData: CFStringRef;
1527
1528        pub fn SecItemAdd(attributes: CFDictionaryRef, result: *mut CFTypeRef) -> OSStatus;
1529        pub fn SecItemUpdate(query: CFDictionaryRef, attributes: CFDictionaryRef) -> OSStatus;
1530        pub fn SecItemDelete(query: CFDictionaryRef) -> OSStatus;
1531        pub fn SecItemCopyMatching(query: CFDictionaryRef, result: *mut CFTypeRef) -> OSStatus;
1532    }
1533
1534    pub const errSecSuccess: OSStatus = 0;
1535    pub const errSecUserCanceled: OSStatus = -128;
1536    pub const errSecItemNotFound: OSStatus = -25300;
1537}
1538
Served at tenant.openagents/omega Member data and write actions are omitted.