platform.rs

  1use super::{BoolExt as _, Dispatcher, FontSystem, Window};
  2use crate::{
  3    executor,
  4    keymap::Keystroke,
  5    platform::{self, CursorStyle},
  6    Action, ClipboardItem, Event, Menu, MenuItem,
  7};
  8use anyhow::{anyhow, Result};
  9use block::ConcreteBlock;
 10use cocoa::{
 11    appkit::{
 12        NSApplication, NSApplicationActivationPolicy::NSApplicationActivationPolicyRegular,
 13        NSEventModifierFlags, NSMenu, NSMenuItem, NSModalResponse, NSOpenPanel, NSPasteboard,
 14        NSPasteboardTypeString, NSSavePanel, NSWindow,
 15    },
 16    base::{id, nil, selector, YES},
 17    foundation::{NSArray, NSAutoreleasePool, NSBundle, NSData, NSInteger, NSString, NSURL},
 18};
 19use core_foundation::{
 20    base::{CFType, CFTypeRef, OSStatus, TCFType as _},
 21    boolean::CFBoolean,
 22    data::CFData,
 23    dictionary::{CFDictionary, CFDictionaryRef, CFMutableDictionary},
 24    string::{CFString, CFStringRef},
 25};
 26use ctor::ctor;
 27use objc::{
 28    class,
 29    declare::ClassDecl,
 30    msg_send,
 31    runtime::{Class, Object, Sel},
 32    sel, sel_impl,
 33};
 34use postage::oneshot;
 35use ptr::null_mut;
 36use std::{
 37    cell::{Cell, RefCell},
 38    convert::TryInto,
 39    ffi::{c_void, CStr, OsStr},
 40    os::{raw::c_char, unix::ffi::OsStrExt},
 41    path::{Path, PathBuf},
 42    ptr,
 43    rc::Rc,
 44    slice, str,
 45    sync::Arc,
 46};
 47use time::UtcOffset;
 48
 49const MAC_PLATFORM_IVAR: &'static str = "platform";
 50static mut APP_CLASS: *const Class = ptr::null();
 51static mut APP_DELEGATE_CLASS: *const Class = ptr::null();
 52
 53#[ctor]
 54unsafe fn build_classes() {
 55    APP_CLASS = {
 56        let mut decl = ClassDecl::new("GPUIApplication", class!(NSApplication)).unwrap();
 57        decl.add_ivar::<*mut c_void>(MAC_PLATFORM_IVAR);
 58        decl.add_method(
 59            sel!(sendEvent:),
 60            send_event as extern "C" fn(&mut Object, Sel, id),
 61        );
 62        decl.register()
 63    };
 64
 65    APP_DELEGATE_CLASS = {
 66        let mut decl = ClassDecl::new("GPUIApplicationDelegate", class!(NSResponder)).unwrap();
 67        decl.add_ivar::<*mut c_void>(MAC_PLATFORM_IVAR);
 68        decl.add_method(
 69            sel!(applicationDidFinishLaunching:),
 70            did_finish_launching as extern "C" fn(&mut Object, Sel, id),
 71        );
 72        decl.add_method(
 73            sel!(applicationDidBecomeActive:),
 74            did_become_active as extern "C" fn(&mut Object, Sel, id),
 75        );
 76        decl.add_method(
 77            sel!(applicationDidResignActive:),
 78            did_resign_active as extern "C" fn(&mut Object, Sel, id),
 79        );
 80        decl.add_method(
 81            sel!(applicationWillTerminate:),
 82            will_terminate as extern "C" fn(&mut Object, Sel, id),
 83        );
 84        decl.add_method(
 85            sel!(handleGPUIMenuItem:),
 86            handle_menu_item as extern "C" fn(&mut Object, Sel, id),
 87        );
 88        decl.add_method(
 89            sel!(application:openFiles:),
 90            open_files as extern "C" fn(&mut Object, Sel, id, id),
 91        );
 92        decl.add_method(
 93            sel!(application:openURLs:),
 94            open_urls as extern "C" fn(&mut Object, Sel, id, id),
 95        );
 96        decl.register()
 97    }
 98}
 99
100#[derive(Default)]
101pub struct MacForegroundPlatform(RefCell<MacForegroundPlatformState>);
102
103#[derive(Default)]
104pub struct MacForegroundPlatformState {
105    become_active: Option<Box<dyn FnMut()>>,
106    resign_active: Option<Box<dyn FnMut()>>,
107    quit: Option<Box<dyn FnMut()>>,
108    event: Option<Box<dyn FnMut(crate::Event) -> bool>>,
109    menu_command: Option<Box<dyn FnMut(&dyn Action)>>,
110    open_files: Option<Box<dyn FnMut(Vec<PathBuf>)>>,
111    open_urls: Option<Box<dyn FnMut(Vec<String>)>>,
112    finish_launching: Option<Box<dyn FnOnce() -> ()>>,
113    menu_actions: Vec<Box<dyn Action>>,
114}
115
116impl MacForegroundPlatform {
117    unsafe fn create_menu_bar(&self, menus: Vec<Menu>) -> id {
118        let menu_bar = NSMenu::new(nil).autorelease();
119        let mut state = self.0.borrow_mut();
120
121        state.menu_actions.clear();
122
123        for menu_config in menus {
124            let menu_bar_item = NSMenuItem::new(nil).autorelease();
125            let menu = NSMenu::new(nil).autorelease();
126            let menu_name = menu_config.name;
127
128            menu.setTitle_(ns_string(menu_name));
129
130            for item_config in menu_config.items {
131                let item;
132
133                match item_config {
134                    MenuItem::Separator => {
135                        item = NSMenuItem::separatorItem(nil);
136                    }
137                    MenuItem::Action {
138                        name,
139                        keystroke,
140                        action,
141                    } => {
142                        if let Some(keystroke) = keystroke {
143                            let keystroke = Keystroke::parse(keystroke).unwrap_or_else(|err| {
144                                panic!(
145                                    "Invalid keystroke for menu item {}:{} - {:?}",
146                                    menu_name, name, err
147                                )
148                            });
149
150                            let mut mask = NSEventModifierFlags::empty();
151                            for (modifier, flag) in &[
152                                (keystroke.cmd, NSEventModifierFlags::NSCommandKeyMask),
153                                (keystroke.ctrl, NSEventModifierFlags::NSControlKeyMask),
154                                (keystroke.alt, NSEventModifierFlags::NSAlternateKeyMask),
155                            ] {
156                                if *modifier {
157                                    mask |= *flag;
158                                }
159                            }
160
161                            item = NSMenuItem::alloc(nil)
162                                .initWithTitle_action_keyEquivalent_(
163                                    ns_string(name),
164                                    selector("handleGPUIMenuItem:"),
165                                    ns_string(&keystroke.key),
166                                )
167                                .autorelease();
168                            item.setKeyEquivalentModifierMask_(mask);
169                        } else {
170                            item = NSMenuItem::alloc(nil)
171                                .initWithTitle_action_keyEquivalent_(
172                                    ns_string(name),
173                                    selector("handleGPUIMenuItem:"),
174                                    ns_string(""),
175                                )
176                                .autorelease();
177                        }
178
179                        let tag = state.menu_actions.len() as NSInteger;
180                        let _: () = msg_send![item, setTag: tag];
181                        state.menu_actions.push(action);
182                    }
183                }
184
185                menu.addItem_(item);
186            }
187
188            menu_bar_item.setSubmenu_(menu);
189            menu_bar.addItem_(menu_bar_item);
190        }
191
192        menu_bar
193    }
194}
195
196impl platform::ForegroundPlatform for MacForegroundPlatform {
197    fn on_become_active(&self, callback: Box<dyn FnMut()>) {
198        self.0.borrow_mut().become_active = Some(callback);
199    }
200
201    fn on_resign_active(&self, callback: Box<dyn FnMut()>) {
202        self.0.borrow_mut().resign_active = Some(callback);
203    }
204
205    fn on_quit(&self, callback: Box<dyn FnMut()>) {
206        self.0.borrow_mut().quit = Some(callback);
207    }
208
209    fn on_event(&self, callback: Box<dyn FnMut(crate::Event) -> bool>) {
210        self.0.borrow_mut().event = Some(callback);
211    }
212
213    fn on_open_files(&self, callback: Box<dyn FnMut(Vec<PathBuf>)>) {
214        self.0.borrow_mut().open_files = Some(callback);
215    }
216
217    fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>) {
218        self.0.borrow_mut().open_urls = Some(callback);
219    }
220
221    fn run(&self, on_finish_launching: Box<dyn FnOnce() -> ()>) {
222        self.0.borrow_mut().finish_launching = Some(on_finish_launching);
223
224        unsafe {
225            let app: id = msg_send![APP_CLASS, sharedApplication];
226            let app_delegate: id = msg_send![APP_DELEGATE_CLASS, new];
227            app.setDelegate_(app_delegate);
228
229            let self_ptr = self as *const Self as *const c_void;
230            (*app).set_ivar(MAC_PLATFORM_IVAR, self_ptr);
231            (*app_delegate).set_ivar(MAC_PLATFORM_IVAR, self_ptr);
232
233            let pool = NSAutoreleasePool::new(nil);
234            app.run();
235            pool.drain();
236
237            (*app).set_ivar(MAC_PLATFORM_IVAR, null_mut::<c_void>());
238            (*app.delegate()).set_ivar(MAC_PLATFORM_IVAR, null_mut::<c_void>());
239        }
240    }
241
242    fn on_menu_command(&self, callback: Box<dyn FnMut(&dyn Action)>) {
243        self.0.borrow_mut().menu_command = Some(callback);
244    }
245
246    fn set_menus(&self, menus: Vec<Menu>) {
247        unsafe {
248            let app: id = msg_send![APP_CLASS, sharedApplication];
249            app.setMainMenu_(self.create_menu_bar(menus));
250        }
251    }
252
253    fn prompt_for_paths(
254        &self,
255        options: platform::PathPromptOptions,
256    ) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
257        unsafe {
258            let panel = NSOpenPanel::openPanel(nil);
259            panel.setCanChooseDirectories_(options.directories.to_objc());
260            panel.setCanChooseFiles_(options.files.to_objc());
261            panel.setAllowsMultipleSelection_(options.multiple.to_objc());
262            panel.setResolvesAliases_(false.to_objc());
263            let (done_tx, done_rx) = oneshot::channel();
264            let done_tx = Cell::new(Some(done_tx));
265            let block = ConcreteBlock::new(move |response: NSModalResponse| {
266                let result = if response == NSModalResponse::NSModalResponseOk {
267                    let mut result = Vec::new();
268                    let urls = panel.URLs();
269                    for i in 0..urls.count() {
270                        let url = urls.objectAtIndex(i);
271                        if url.isFileURL() == YES {
272                            if let Ok(path) = ns_url_to_path(url) {
273                                result.push(path)
274                            }
275                        }
276                    }
277                    Some(result)
278                } else {
279                    None
280                };
281
282                if let Some(mut done_tx) = done_tx.take() {
283                    let _ = postage::sink::Sink::try_send(&mut done_tx, result);
284                }
285            });
286            let block = block.copy();
287            let _: () = msg_send![panel, beginWithCompletionHandler: block];
288            done_rx
289        }
290    }
291
292    fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Option<PathBuf>> {
293        unsafe {
294            let panel = NSSavePanel::savePanel(nil);
295            let path = ns_string(directory.to_string_lossy().as_ref());
296            let url = NSURL::fileURLWithPath_isDirectory_(nil, path, true.to_objc());
297            panel.setDirectoryURL(url);
298
299            let (done_tx, done_rx) = oneshot::channel();
300            let done_tx = Cell::new(Some(done_tx));
301            let block = ConcreteBlock::new(move |response: NSModalResponse| {
302                let mut result = None;
303                if response == NSModalResponse::NSModalResponseOk {
304                    let url = panel.URL();
305                    if url.isFileURL() == YES {
306                        result = ns_url_to_path(panel.URL()).ok()
307                    }
308                }
309
310                if let Some(mut done_tx) = done_tx.take() {
311                    let _ = postage::sink::Sink::try_send(&mut done_tx, result);
312                }
313            });
314            let block = block.copy();
315            let _: () = msg_send![panel, beginWithCompletionHandler: block];
316            done_rx
317        }
318    }
319}
320
321pub struct MacPlatform {
322    dispatcher: Arc<Dispatcher>,
323    fonts: Arc<FontSystem>,
324    pasteboard: id,
325    text_hash_pasteboard_type: id,
326    metadata_pasteboard_type: id,
327}
328
329impl MacPlatform {
330    pub fn new() -> Self {
331        Self {
332            dispatcher: Arc::new(Dispatcher),
333            fonts: Arc::new(FontSystem::new()),
334            pasteboard: unsafe { NSPasteboard::generalPasteboard(nil) },
335            text_hash_pasteboard_type: unsafe { ns_string("zed-text-hash") },
336            metadata_pasteboard_type: unsafe { ns_string("zed-metadata") },
337        }
338    }
339
340    unsafe fn read_from_pasteboard(&self, kind: id) -> Option<&[u8]> {
341        let data = self.pasteboard.dataForType(kind);
342        if data == nil {
343            None
344        } else {
345            Some(slice::from_raw_parts(
346                data.bytes() as *mut u8,
347                data.length() as usize,
348            ))
349        }
350    }
351}
352
353unsafe impl Send for MacPlatform {}
354unsafe impl Sync for MacPlatform {}
355
356impl platform::Platform for MacPlatform {
357    fn dispatcher(&self) -> Arc<dyn platform::Dispatcher> {
358        self.dispatcher.clone()
359    }
360
361    fn activate(&self, ignoring_other_apps: bool) {
362        unsafe {
363            let app = NSApplication::sharedApplication(nil);
364            app.activateIgnoringOtherApps_(ignoring_other_apps.to_objc());
365        }
366    }
367
368    fn open_window(
369        &self,
370        id: usize,
371        options: platform::WindowOptions,
372        executor: Rc<executor::Foreground>,
373    ) -> Box<dyn platform::Window> {
374        Box::new(Window::open(id, options, executor, self.fonts()))
375    }
376
377    fn key_window_id(&self) -> Option<usize> {
378        Window::key_window_id()
379    }
380
381    fn fonts(&self) -> Arc<dyn platform::FontSystem> {
382        self.fonts.clone()
383    }
384
385    fn quit(&self) {
386        // Quitting the app causes us to close windows, which invokes `Window::on_close` callbacks
387        // synchronously before this method terminates. If we call `Platform::quit` while holding a
388        // borrow of the app state (which most of the time we will do), we will end up
389        // double-borrowing the app state in the `on_close` callbacks for our open windows. To solve
390        // this, we make quitting the application asynchronous so that we aren't holding borrows to
391        // the app state on the stack when we actually terminate the app.
392
393        use super::dispatcher::{dispatch_async_f, dispatch_get_main_queue};
394
395        unsafe {
396            dispatch_async_f(dispatch_get_main_queue(), ptr::null_mut(), Some(quit));
397        }
398
399        unsafe extern "C" fn quit(_: *mut c_void) {
400            let app = NSApplication::sharedApplication(nil);
401            let _: () = msg_send![app, terminate: nil];
402        }
403    }
404
405    fn write_to_clipboard(&self, item: ClipboardItem) {
406        unsafe {
407            self.pasteboard.clearContents();
408
409            let text_bytes = NSData::dataWithBytes_length_(
410                nil,
411                item.text.as_ptr() as *const c_void,
412                item.text.len() as u64,
413            );
414            self.pasteboard
415                .setData_forType(text_bytes, NSPasteboardTypeString);
416
417            if let Some(metadata) = item.metadata.as_ref() {
418                let hash_bytes = ClipboardItem::text_hash(&item.text).to_be_bytes();
419                let hash_bytes = NSData::dataWithBytes_length_(
420                    nil,
421                    hash_bytes.as_ptr() as *const c_void,
422                    hash_bytes.len() as u64,
423                );
424                self.pasteboard
425                    .setData_forType(hash_bytes, self.text_hash_pasteboard_type);
426
427                let metadata_bytes = NSData::dataWithBytes_length_(
428                    nil,
429                    metadata.as_ptr() as *const c_void,
430                    metadata.len() as u64,
431                );
432                self.pasteboard
433                    .setData_forType(metadata_bytes, self.metadata_pasteboard_type);
434            }
435        }
436    }
437
438    fn read_from_clipboard(&self) -> Option<ClipboardItem> {
439        unsafe {
440            if let Some(text_bytes) = self.read_from_pasteboard(NSPasteboardTypeString) {
441                let text = String::from_utf8_lossy(&text_bytes).to_string();
442                let hash_bytes = self
443                    .read_from_pasteboard(self.text_hash_pasteboard_type)
444                    .and_then(|bytes| bytes.try_into().ok())
445                    .map(u64::from_be_bytes);
446                let metadata_bytes = self
447                    .read_from_pasteboard(self.metadata_pasteboard_type)
448                    .and_then(|bytes| String::from_utf8(bytes.to_vec()).ok());
449
450                if let Some((hash, metadata)) = hash_bytes.zip(metadata_bytes) {
451                    if hash == ClipboardItem::text_hash(&text) {
452                        Some(ClipboardItem {
453                            text,
454                            metadata: Some(metadata),
455                        })
456                    } else {
457                        Some(ClipboardItem {
458                            text,
459                            metadata: None,
460                        })
461                    }
462                } else {
463                    Some(ClipboardItem {
464                        text,
465                        metadata: None,
466                    })
467                }
468            } else {
469                None
470            }
471        }
472    }
473
474    fn open_url(&self, url: &str) {
475        unsafe {
476            let url = NSURL::alloc(nil)
477                .initWithString_(ns_string(url))
478                .autorelease();
479            let workspace: id = msg_send![class!(NSWorkspace), sharedWorkspace];
480            msg_send![workspace, openURL: url]
481        }
482    }
483
484    fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Result<()> {
485        let url = CFString::from(url);
486        let username = CFString::from(username);
487        let password = CFData::from_buffer(password);
488
489        unsafe {
490            use security::*;
491
492            // First, check if there are already credentials for the given server. If so, then
493            // update the username and password.
494            let mut verb = "updating";
495            let mut query_attrs = CFMutableDictionary::with_capacity(2);
496            query_attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _);
497            query_attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef());
498
499            let mut attrs = CFMutableDictionary::with_capacity(4);
500            attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _);
501            attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef());
502            attrs.set(kSecAttrAccount as *const _, username.as_CFTypeRef());
503            attrs.set(kSecValueData as *const _, password.as_CFTypeRef());
504
505            let mut status = SecItemUpdate(
506                query_attrs.as_concrete_TypeRef(),
507                attrs.as_concrete_TypeRef(),
508            );
509
510            // If there were no existing credentials for the given server, then create them.
511            if status == errSecItemNotFound {
512                verb = "creating";
513                status = SecItemAdd(attrs.as_concrete_TypeRef(), ptr::null_mut());
514            }
515
516            if status != errSecSuccess {
517                return Err(anyhow!("{} password failed: {}", verb, status));
518            }
519        }
520        Ok(())
521    }
522
523    fn read_credentials(&self, url: &str) -> Result<Option<(String, Vec<u8>)>> {
524        let url = CFString::from(url);
525        let cf_true = CFBoolean::true_value().as_CFTypeRef();
526
527        unsafe {
528            use security::*;
529
530            // Find any credentials for the given server URL.
531            let mut attrs = CFMutableDictionary::with_capacity(5);
532            attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _);
533            attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef());
534            attrs.set(kSecReturnAttributes as *const _, cf_true);
535            attrs.set(kSecReturnData as *const _, cf_true);
536
537            let mut result = CFTypeRef::from(ptr::null_mut());
538            let status = SecItemCopyMatching(attrs.as_concrete_TypeRef(), &mut result);
539            match status {
540                security::errSecSuccess => {}
541                security::errSecItemNotFound | security::errSecUserCanceled => return Ok(None),
542                _ => return Err(anyhow!("reading password failed: {}", status)),
543            }
544
545            let result = CFType::wrap_under_create_rule(result)
546                .downcast::<CFDictionary>()
547                .ok_or_else(|| anyhow!("keychain item was not a dictionary"))?;
548            let username = result
549                .find(kSecAttrAccount as *const _)
550                .ok_or_else(|| anyhow!("account was missing from keychain item"))?;
551            let username = CFType::wrap_under_get_rule(*username)
552                .downcast::<CFString>()
553                .ok_or_else(|| anyhow!("account was not a string"))?;
554            let password = result
555                .find(kSecValueData as *const _)
556                .ok_or_else(|| anyhow!("password was missing from keychain item"))?;
557            let password = CFType::wrap_under_get_rule(*password)
558                .downcast::<CFData>()
559                .ok_or_else(|| anyhow!("password was not a string"))?;
560
561            Ok(Some((username.to_string(), password.bytes().to_vec())))
562        }
563    }
564
565    fn delete_credentials(&self, url: &str) -> Result<()> {
566        let url = CFString::from(url);
567
568        unsafe {
569            use security::*;
570
571            let mut query_attrs = CFMutableDictionary::with_capacity(2);
572            query_attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _);
573            query_attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef());
574
575            let status = SecItemDelete(query_attrs.as_concrete_TypeRef());
576
577            if status != errSecSuccess {
578                return Err(anyhow!("delete password failed: {}", status));
579            }
580        }
581        Ok(())
582    }
583
584    fn set_cursor_style(&self, style: CursorStyle) {
585        unsafe {
586            let cursor: id = match style {
587                CursorStyle::Arrow => msg_send![class!(NSCursor), arrowCursor],
588                CursorStyle::ResizeLeftRight => msg_send![class!(NSCursor), resizeLeftRightCursor],
589                CursorStyle::PointingHand => msg_send![class!(NSCursor), pointingHandCursor],
590            };
591            let _: () = msg_send![cursor, set];
592        }
593    }
594
595    fn local_timezone(&self) -> UtcOffset {
596        unsafe {
597            let local_timezone: id = msg_send![class!(NSTimeZone), localTimeZone];
598            let seconds_from_gmt: NSInteger = msg_send![local_timezone, secondsFromGMT];
599            UtcOffset::from_whole_seconds(seconds_from_gmt.try_into().unwrap()).unwrap()
600        }
601    }
602
603    fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf> {
604        unsafe {
605            let bundle: id = NSBundle::mainBundle();
606            if bundle.is_null() {
607                Err(anyhow!("app is not running inside a bundle"))
608            } else {
609                let name = ns_string(name);
610                let url: id = msg_send![bundle, URLForAuxiliaryExecutable: name];
611                if url.is_null() {
612                    Err(anyhow!("resource not found"))
613                } else {
614                    ns_url_to_path(url)
615                }
616            }
617        }
618    }
619}
620
621unsafe fn get_foreground_platform(object: &mut Object) -> &MacForegroundPlatform {
622    let platform_ptr: *mut c_void = *object.get_ivar(MAC_PLATFORM_IVAR);
623    assert!(!platform_ptr.is_null());
624    &*(platform_ptr as *const MacForegroundPlatform)
625}
626
627extern "C" fn send_event(this: &mut Object, _sel: Sel, native_event: id) {
628    unsafe {
629        if let Some(event) = Event::from_native(native_event, None) {
630            let platform = get_foreground_platform(this);
631            if let Some(callback) = platform.0.borrow_mut().event.as_mut() {
632                if callback(event) {
633                    return;
634                }
635            }
636        }
637
638        msg_send![super(this, class!(NSApplication)), sendEvent: native_event]
639    }
640}
641
642extern "C" fn did_finish_launching(this: &mut Object, _: Sel, _: id) {
643    unsafe {
644        let app: id = msg_send![APP_CLASS, sharedApplication];
645        app.setActivationPolicy_(NSApplicationActivationPolicyRegular);
646
647        let platform = get_foreground_platform(this);
648        let callback = platform.0.borrow_mut().finish_launching.take();
649        if let Some(callback) = callback {
650            callback();
651        }
652    }
653}
654
655extern "C" fn did_become_active(this: &mut Object, _: Sel, _: id) {
656    let platform = unsafe { get_foreground_platform(this) };
657    if let Some(callback) = platform.0.borrow_mut().become_active.as_mut() {
658        callback();
659    }
660}
661
662extern "C" fn did_resign_active(this: &mut Object, _: Sel, _: id) {
663    let platform = unsafe { get_foreground_platform(this) };
664    if let Some(callback) = platform.0.borrow_mut().resign_active.as_mut() {
665        callback();
666    }
667}
668
669extern "C" fn will_terminate(this: &mut Object, _: Sel, _: id) {
670    let platform = unsafe { get_foreground_platform(this) };
671    if let Some(callback) = platform.0.borrow_mut().quit.as_mut() {
672        callback();
673    }
674}
675
676extern "C" fn open_files(this: &mut Object, _: Sel, _: id, paths: id) {
677    let paths = unsafe {
678        (0..paths.count())
679            .into_iter()
680            .filter_map(|i| {
681                let path = paths.objectAtIndex(i);
682                match CStr::from_ptr(path.UTF8String() as *mut c_char).to_str() {
683                    Ok(string) => Some(PathBuf::from(string)),
684                    Err(err) => {
685                        log::error!("error converting path to string: {}", err);
686                        None
687                    }
688                }
689            })
690            .collect::<Vec<_>>()
691    };
692    let platform = unsafe { get_foreground_platform(this) };
693    if let Some(callback) = platform.0.borrow_mut().open_files.as_mut() {
694        callback(paths);
695    }
696}
697
698extern "C" fn open_urls(this: &mut Object, _: Sel, _: id, urls: id) {
699    let urls = unsafe {
700        (0..urls.count())
701            .into_iter()
702            .filter_map(|i| {
703                let path = urls.objectAtIndex(i);
704                match CStr::from_ptr(path.absoluteString().UTF8String() as *mut c_char).to_str() {
705                    Ok(string) => Some(string.to_string()),
706                    Err(err) => {
707                        log::error!("error converting path to string: {}", err);
708                        None
709                    }
710                }
711            })
712            .collect::<Vec<_>>()
713    };
714    let platform = unsafe { get_foreground_platform(this) };
715    if let Some(callback) = platform.0.borrow_mut().open_urls.as_mut() {
716        callback(urls);
717    }
718}
719
720extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) {
721    unsafe {
722        let platform = get_foreground_platform(this);
723        let mut platform = platform.0.borrow_mut();
724        if let Some(mut callback) = platform.menu_command.take() {
725            let tag: NSInteger = msg_send![item, tag];
726            let index = tag as usize;
727            if let Some(action) = platform.menu_actions.get(index) {
728                callback(action.as_ref());
729            }
730            platform.menu_command = Some(callback);
731        }
732    }
733}
734
735unsafe fn ns_string(string: &str) -> id {
736    NSString::alloc(nil).init_str(string).autorelease()
737}
738
739unsafe fn ns_url_to_path(url: id) -> Result<PathBuf> {
740    let path: *mut c_char = msg_send![url, fileSystemRepresentation];
741    if path.is_null() {
742        Err(anyhow!(
743            "url is not a file path: {}",
744            CStr::from_ptr(url.absoluteString().UTF8String()).to_string_lossy()
745        ))
746    } else {
747        Ok(PathBuf::from(OsStr::from_bytes(
748            CStr::from_ptr(path).to_bytes(),
749        )))
750    }
751}
752
753mod security {
754    #![allow(non_upper_case_globals)]
755    use super::*;
756
757    #[link(name = "Security", kind = "framework")]
758    extern "C" {
759        pub static kSecClass: CFStringRef;
760        pub static kSecClassInternetPassword: CFStringRef;
761        pub static kSecAttrServer: CFStringRef;
762        pub static kSecAttrAccount: CFStringRef;
763        pub static kSecValueData: CFStringRef;
764        pub static kSecReturnAttributes: CFStringRef;
765        pub static kSecReturnData: CFStringRef;
766
767        pub fn SecItemAdd(attributes: CFDictionaryRef, result: *mut CFTypeRef) -> OSStatus;
768        pub fn SecItemUpdate(query: CFDictionaryRef, attributes: CFDictionaryRef) -> OSStatus;
769        pub fn SecItemDelete(query: CFDictionaryRef) -> OSStatus;
770        pub fn SecItemCopyMatching(query: CFDictionaryRef, result: *mut CFTypeRef) -> OSStatus;
771    }
772
773    pub const errSecSuccess: OSStatus = 0;
774    pub const errSecUserCanceled: OSStatus = -128;
775    pub const errSecItemNotFound: OSStatus = -25300;
776}
777
778#[cfg(test)]
779mod tests {
780    use crate::platform::Platform;
781
782    use super::*;
783
784    #[test]
785    fn test_clipboard() {
786        let platform = build_platform();
787        assert_eq!(platform.read_from_clipboard(), None);
788
789        let item = ClipboardItem::new("1".to_string());
790        platform.write_to_clipboard(item.clone());
791        assert_eq!(platform.read_from_clipboard(), Some(item));
792
793        let item = ClipboardItem::new("2".to_string()).with_metadata(vec![3, 4]);
794        platform.write_to_clipboard(item.clone());
795        assert_eq!(platform.read_from_clipboard(), Some(item));
796
797        let text_from_other_app = "text from other app";
798        unsafe {
799            let bytes = NSData::dataWithBytes_length_(
800                nil,
801                text_from_other_app.as_ptr() as *const c_void,
802                text_from_other_app.len() as u64,
803            );
804            platform
805                .pasteboard
806                .setData_forType(bytes, NSPasteboardTypeString);
807        }
808        assert_eq!(
809            platform.read_from_clipboard(),
810            Some(ClipboardItem::new(text_from_other_app.to_string()))
811        );
812    }
813
814    fn build_platform() -> MacPlatform {
815        let mut platform = MacPlatform::new();
816        platform.pasteboard = unsafe { NSPasteboard::pasteboardWithUniqueName(nil) };
817        platform
818    }
819}