platform.rs

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