Default menu "MacVim Help" item opens help window maximized
[MacVim.git] / src / MacVim / MMAppController.m
bloba8af0884135889fd90a1b2c0a2fc70ddac673726
1 /* vi:set ts=8 sts=4 sw=4 ft=objc:
2  *
3  * VIM - Vi IMproved            by Bram Moolenaar
4  *                              MacVim GUI port by Bjorn Winckler
5  *
6  * Do ":help uganda"  in Vim to read copying and usage conditions.
7  * Do ":help credits" in Vim to see a list of people who contributed.
8  * See README.txt for an overview of the Vim source code.
9  */
11  * MMAppController
12  *
13  * MMAppController is the delegate of NSApp and as such handles file open
14  * requests, application termination, etc.  It sets up a named NSConnection on
15  * which it listens to incoming connections from Vim processes.  It also
16  * coordinates all MMVimControllers.
17  *
18  * A new Vim process is started by calling launchVimProcessWithArguments:.
19  * When the Vim process is initialized it notifies the app controller by
20  * sending a connectBackend:pid: message.  At this point a new MMVimController
21  * is allocated.  Afterwards, the Vim process communicates directly with its
22  * MMVimController.
23  *
24  * A Vim process started from the command line connects directly by sending the
25  * connectBackend:pid: message (launchVimProcessWithArguments: is never called
26  * in this case).
27  */
29 #import "MMAppController.h"
30 #import "MMVimController.h"
31 #import "MMWindowController.h"
32 #import "MMPreferenceController.h"
33 #import <unistd.h>
36 #define MM_HANDLE_XCODE_MOD_EVENT 0
40 // Default timeout intervals on all connections.
41 static NSTimeInterval MMRequestTimeout = 5;
42 static NSTimeInterval MMReplyTimeout = 5;
44 static NSString *MMWebsiteString = @"http://code.google.com/p/macvim/";
47 #pragma options align=mac68k
48 typedef struct
50     short unused1;      // 0 (not used)
51     short lineNum;      // line to select (< 0 to specify range)
52     long  startRange;   // start of selection range (if line < 0)
53     long  endRange;     // end of selection range (if line < 0)
54     long  unused2;      // 0 (not used)
55     long  theDate;      // modification date/time
56 } MMSelectionRange;
57 #pragma options align=reset
60 static int executeInLoginShell(NSString *path, NSArray *args);
63 @interface MMAppController (MMServices)
64 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
65                 error:(NSString **)error;
66 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
67            error:(NSString **)error;
68 @end
71 @interface MMAppController (Private)
72 - (MMVimController *)keyVimController;
73 - (MMVimController *)topmostVimController;
74 - (int)launchVimProcessWithArguments:(NSArray *)args;
75 - (NSArray *)filterFilesAndNotify:(NSArray *)files;
76 - (NSArray *)filterOpenFiles:(NSArray *)filenames
77                    arguments:(NSDictionary *)args;
78 #if MM_HANDLE_XCODE_MOD_EVENT
79 - (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
80                  replyEvent:(NSAppleEventDescriptor *)reply;
81 #endif
82 - (int)findLaunchingProcessWithoutArguments;
83 - (MMVimController *)findUntitledWindow;
84 - (NSMutableDictionary *)extractArgumentsFromOdocEvent:
85     (NSAppleEventDescriptor *)desc;
86 - (void)passArguments:(NSDictionary *)args toVimController:(MMVimController*)vc;
87 @end
90 @interface NSNumber (MMExtras)
91 - (int)tag;
92 @end
95 @interface NSMenu (MMExtras)
96 - (int)indexOfItemWithAction:(SEL)action;
97 - (NSMenuItem *)itemWithAction:(SEL)action;
98 - (NSMenu *)findMenuContainingItemWithAction:(SEL)action;
99 - (NSMenu *)findWindowsMenu;
100 - (NSMenu *)findApplicationMenu;
101 - (NSMenu *)findServicesMenu;
102 - (NSMenu *)findFileMenu;
103 @end
108 @implementation MMAppController
110 + (void)initialize
112     NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
113         [NSNumber numberWithBool:NO],   MMNoWindowKey,
114         [NSNumber numberWithInt:64],    MMTabMinWidthKey,
115         [NSNumber numberWithInt:6*64],  MMTabMaxWidthKey,
116         [NSNumber numberWithInt:132],   MMTabOptimumWidthKey,
117         [NSNumber numberWithInt:2],     MMTextInsetLeftKey,
118         [NSNumber numberWithInt:1],     MMTextInsetRightKey,
119         [NSNumber numberWithInt:1],     MMTextInsetTopKey,
120         [NSNumber numberWithInt:1],     MMTextInsetBottomKey,
121         [NSNumber numberWithBool:NO],   MMTerminateAfterLastWindowClosedKey,
122         @"MMTypesetter",                MMTypesetterKey,
123         [NSNumber numberWithFloat:1],   MMCellWidthMultiplierKey,
124         [NSNumber numberWithFloat:-1],  MMBaselineOffsetKey,
125         [NSNumber numberWithBool:YES],  MMTranslateCtrlClickKey,
126         [NSNumber numberWithBool:NO],   MMOpenFilesInTabsKey,
127         [NSNumber numberWithBool:NO],   MMNoFontSubstitutionKey,
128         [NSNumber numberWithBool:NO],   MMLoginShellKey,
129         [NSNumber numberWithBool:NO],   MMAtsuiRendererKey,
130         [NSNumber numberWithInt:MMUntitledWindowAlways],
131                                         MMUntitledWindowKey,
132         [NSNumber numberWithBool:NO],   MMTexturedWindowKey,
133         [NSNumber numberWithBool:NO],   MMZoomBothKey,
134         @"",                            MMLoginShellCommandKey,
135         @"",                            MMLoginShellArgumentKey,
136         [NSNumber numberWithBool:YES],  MMDialogsTrackPwdKey,
137         nil];
139     [[NSUserDefaults standardUserDefaults] registerDefaults:dict];
141     NSArray *types = [NSArray arrayWithObject:NSStringPboardType];
142     [NSApp registerServicesMenuSendTypes:types returnTypes:types];
145 - (id)init
147     if ((self = [super init])) {
148         fontContainerRef = loadFonts();
150         vimControllers = [NSMutableArray new];
151         pidArguments = [NSMutableDictionary new];
153         // NOTE: Do not use the default connection since the Logitech Control
154         // Center (LCC) input manager steals and this would cause MacVim to
155         // never open any windows.  (This is a bug in LCC but since they are
156         // unlikely to fix it, we graciously give them the default connection.)
157         connection = [[NSConnection alloc] initWithReceivePort:[NSPort port]
158                                                       sendPort:nil];
159         [connection setRootObject:self];
160         [connection setRequestTimeout:MMRequestTimeout];
161         [connection setReplyTimeout:MMReplyTimeout];
163         // NOTE: When the user is resizing the window the AppKit puts the run
164         // loop in event tracking mode.  Unless the connection listens to
165         // request in this mode, live resizing won't work.
166         [connection addRequestMode:NSEventTrackingRunLoopMode];
168         // NOTE!  If the name of the connection changes here it must also be
169         // updated in MMBackend.m.
170         NSString *name = [NSString stringWithFormat:@"%@-connection",
171                  [[NSBundle mainBundle] bundleIdentifier]];
172         //NSLog(@"Registering connection with name '%@'", name);
173         if (![connection registerName:name]) {
174             NSLog(@"FATAL ERROR: Failed to register connection with name '%@'",
175                     name);
176             [connection release];  connection = nil;
177         }
178     }
180     return self;
183 - (void)dealloc
185     //NSLog(@"MMAppController dealloc");
187     [connection release];  connection = nil;
188     [pidArguments release];  pidArguments = nil;
189     [vimControllers release];  vimControllers = nil;
190     [openSelectionString release];  openSelectionString = nil;
191     [recentFilesMenuItem release];  recentFilesMenuItem = nil;
192     [defaultMainMenu release];  defaultMainMenu = nil;
194     [super dealloc];
197 - (void)applicationWillFinishLaunching:(NSNotification *)notification
199     // Remember the default menu so that it can be restored if the user closes
200     // all editor windows.
201     defaultMainMenu = [[NSApp mainMenu] retain];
203     // Set up the "Open Recent" menu. See
204     //   http://lapcatsoftware.com/blog/2007/07/10/
205     //     working-without-a-nib-part-5-open-recent-menu/
206     // and
207     //   http://www.cocoabuilder.com/archive/message/cocoa/2007/8/15/187793
208     // for more information.
209     //
210     // The menu itself is created in MainMenu.nib but we still seem to have to
211     // hack around a bit to get it to work.  (This has to be done in
212     // applicationWillFinishLaunching at the latest, otherwise it doesn't
213     // work.)
214     NSMenu *fileMenu = [defaultMainMenu findFileMenu];
215     if (fileMenu) {
216         int idx = [fileMenu indexOfItemWithAction:@selector(fileOpen:)];
217         if (idx >= 0 && idx+1 < [fileMenu numberOfItems])
219         recentFilesMenuItem = [fileMenu itemWithTitle:@"Open Recent"];
220         [[recentFilesMenuItem submenu] performSelector:@selector(_setMenuName:)
221                                         withObject:@"NSRecentDocumentsMenu"];
223         // Note: The "Recent Files" menu must be moved around since there is no
224         // -[NSApp setRecentFilesMenu:] method.  We keep a reference to it to
225         // facilitate this move (see setMainMenu: below).
226         [recentFilesMenuItem retain];
227     }
229 #if MM_HANDLE_XCODE_MOD_EVENT
230     [[NSAppleEventManager sharedAppleEventManager]
231             setEventHandler:self
232                 andSelector:@selector(handleXcodeModEvent:replyEvent:)
233               forEventClass:'KAHL'
234                  andEventID:'MOD '];
235 #endif
238 - (void)applicationDidFinishLaunching:(NSNotification *)notification
240     [NSApp setServicesProvider:self];
243 - (BOOL)applicationShouldOpenUntitledFile:(NSApplication *)sender
245     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
246     NSAppleEventManager *aem = [NSAppleEventManager sharedAppleEventManager];
247     NSAppleEventDescriptor *desc = [aem currentAppleEvent];
249     // The user default MMUntitledWindow can be set to control whether an
250     // untitled window should open on 'Open' and 'Reopen' events.
251     int untitledWindowFlag = [ud integerForKey:MMUntitledWindowKey];
252     if ([desc eventID] == kAEOpenApplication
253             && (untitledWindowFlag & MMUntitledWindowOnOpen) == 0)
254         return NO;
255     else if ([desc eventID] == kAEReopenApplication
256             && (untitledWindowFlag & MMUntitledWindowOnReopen) == 0)
257         return NO;
259     // When a process is started from the command line, the 'Open' event will
260     // contain a parameter to surpress the opening of an untitled window.
261     desc = [desc paramDescriptorForKeyword:keyAEPropData];
262     desc = [desc paramDescriptorForKeyword:keyMMUntitledWindow];
263     if (desc && ![desc booleanValue])
264         return NO;
266     // Never open an untitled window if there is at least one open window or if
267     // there are processes that are currently launching.
268     if ([vimControllers count] > 0 || [pidArguments count] > 0)
269         return NO;
271     // NOTE!  This way it possible to start the app with the command-line
272     // argument '-nowindow yes' and no window will be opened by default.
273     return ![ud boolForKey:MMNoWindowKey];
276 - (BOOL)applicationOpenUntitledFile:(NSApplication *)sender
278     [self newWindow:self];
279     return YES;
282 - (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames
284     // Opening files works like this:
285     //  a) extract ODB/Xcode/Spotlight parameters from the current Apple event
286     //  b) filter out any already open files (see filterOpenFiles::)
287     //  c) open any remaining files
288     //
289     // A file is opened in an untitled window if there is one (it may be
290     // currently launching, or it may already be visible), otherwise a new
291     // window is opened.
292     //
293     // Each launching Vim process has a dictionary of arguments that are passed
294     // to the process when in checks in (via connectBackend:pid:).  The
295     // arguments for each launching process can be looked up by its PID (in the
296     // pidArguments dictionary).
298     NSMutableDictionary *arguments = [self extractArgumentsFromOdocEvent:
299             [[NSAppleEventManager sharedAppleEventManager] currentAppleEvent]];
301     // Filter out files that are already open
302     filenames = [self filterOpenFiles:filenames arguments:arguments];
304     // Open any files that remain
305     if ([filenames count]) {
306         MMVimController *vc;
307         BOOL openInTabs = [[NSUserDefaults standardUserDefaults]
308             boolForKey:MMOpenFilesInTabsKey];
310         [arguments setObject:filenames forKey:@"filenames"];
311         [arguments setObject:[NSNumber numberWithBool:YES] forKey:@"openFiles"];
313         // Add file names to "Recent Files" menu.
314         int i, count = [filenames count];
315         for (i = 0; i < count; ++i) {
316             // Don't add files that are being edited remotely (using ODB).
317             if ([arguments objectForKey:@"remoteID"]) continue;
319             [[NSDocumentController sharedDocumentController]
320                     noteNewRecentFilePath:[filenames objectAtIndex:i]];
321         }
323         if ((openInTabs && (vc = [self topmostVimController]))
324                || (vc = [self findUntitledWindow])) {
325             // Open files in an already open window.
326             [[[vc windowController] window] makeKeyAndOrderFront:self];
327             [self passArguments:arguments toVimController:vc];
328         } else {
329             // Open files in a launching Vim process or start a new process.
330             int pid = [self findLaunchingProcessWithoutArguments];
331             if (!pid) {
332                 // Pass the filenames to the process straight away.
333                 //
334                 // TODO: It would be nicer if all arguments were passed to the
335                 // Vim process in connectBackend::, but if we don't pass the
336                 // filename arguments here, the window 'flashes' once when it
337                 // opens.  This is due to the 'welcome' screen first being
338                 // displayed, then quickly thereafter the files are opened.
339                 NSArray *fileArgs = [NSArray arrayWithObject:@"-p"];
340                 fileArgs = [fileArgs arrayByAddingObjectsFromArray:filenames];
342                 pid = [self launchVimProcessWithArguments:fileArgs];
344                 if (-1 == pid) {
345                     // TODO: Notify user of failure?
346                     [NSApp replyToOpenOrPrint:
347                         NSApplicationDelegateReplyFailure];
348                     return;
349                 }
351                 // Make sure these files aren't opened again when
352                 // connectBackend:pid: is called.
353                 [arguments setObject:[NSNumber numberWithBool:NO]
354                               forKey:@"openFiles"];
355             }
357             // TODO: If the Vim process fails to start, or if it changes PID,
358             // then the memory allocated for these parameters will leak.
359             // Ensure that this cannot happen or somehow detect it.
361             if ([arguments count] > 0)
362                 [pidArguments setObject:arguments
363                                  forKey:[NSNumber numberWithInt:pid]];
364         }
365     }
367     [NSApp replyToOpenOrPrint:NSApplicationDelegateReplySuccess];
368     // NSApplicationDelegateReplySuccess = 0,
369     // NSApplicationDelegateReplyCancel = 1,
370     // NSApplicationDelegateReplyFailure = 2
373 - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender
375     return [[NSUserDefaults standardUserDefaults]
376             boolForKey:MMTerminateAfterLastWindowClosedKey];
379 - (NSApplicationTerminateReply)applicationShouldTerminate:
380     (NSApplication *)sender
382     // TODO: Follow Apple's guidelines for 'Graceful Application Termination'
383     // (in particular, allow user to review changes and save).
384     int reply = NSTerminateNow;
385     BOOL modifiedBuffers = NO;
387     // Go through windows, checking for modified buffers.  (Each Vim process
388     // tells MacVim when any buffer has been modified and MacVim sets the
389     // 'documentEdited' flag of the window correspondingly.)
390     NSEnumerator *e = [[NSApp windows] objectEnumerator];
391     id window;
392     while ((window = [e nextObject])) {
393         if ([window isDocumentEdited]) {
394             modifiedBuffers = YES;
395             break;
396         }
397     }
399     if (modifiedBuffers) {
400         NSAlert *alert = [[NSAlert alloc] init];
401         [alert setAlertStyle:NSWarningAlertStyle];
402         [alert addButtonWithTitle:NSLocalizedString(@"Quit",
403                 @"Dialog button")];
404         [alert addButtonWithTitle:NSLocalizedString(@"Cancel",
405                 @"Dialog button")];
406         [alert setMessageText:NSLocalizedString(@"Quit without saving?",
407                 @"Quit dialog with changed buffers, title")];
408         [alert setInformativeText:NSLocalizedString(
409                 @"There are modified buffers, "
410                 "if you quit now all changes will be lost.  Quit anyway?",
411                 @"Quit dialog with changed buffers, text")];
413         if ([alert runModal] != NSAlertFirstButtonReturn)
414             reply = NSTerminateCancel;
416         [alert release];
417     } else {
418         // No unmodified buffers, but give a warning if there are multiple
419         // windows and/or tabs open.
420         int numWindows = [vimControllers count];
421         int numTabs = 0;
423         // Count the number of open tabs
424         e = [vimControllers objectEnumerator];
425         id vc;
426         while ((vc = [e nextObject])) {
427             NSString *eval = [vc evaluateVimExpression:@"tabpagenr('$')"];
428             if (eval) {
429                 int count = [eval intValue];
430                 if (count > 0 && count < INT_MAX)
431                     numTabs += count;
432             }
433         }
435         if (numWindows > 1 || numTabs > 1) {
436             NSAlert *alert = [[NSAlert alloc] init];
437             [alert setAlertStyle:NSWarningAlertStyle];
438             [alert addButtonWithTitle:NSLocalizedString(@"Quit",
439                     @"Dialog button")];
440             [alert addButtonWithTitle:NSLocalizedString(@"Cancel",
441                     @"Dialog button")];
442             [alert setMessageText:NSLocalizedString(
443                     @"Are you sure you want to quit MacVim?",
444                     @"Quit dialog with no changed buffers, title")];
446             NSString *info = nil;
447             if (numWindows > 1) {
448                 if (numTabs > numWindows)
449                     info = [NSString stringWithFormat:NSLocalizedString(
450                             @"There are %d windows open in MacVim, with a "
451                             "total of %d tabs. Do you want to quit anyway?",
452                             @"Quit dialog with no changed buffers, text"),
453                          numWindows, numTabs];
454                 else
455                     info = [NSString stringWithFormat:NSLocalizedString(
456                             @"There are %d windows open in MacVim. "
457                             "Do you want to quit anyway?",
458                             @"Quit dialog with no changed buffers, text"),
459                         numWindows];
461             } else {
462                 info = [NSString stringWithFormat:NSLocalizedString(
463                         @"There are %d tabs open in MacVim. "
464                         "Do you want to quit anyway?",
465                         @"Quit dialog with no changed buffers, text"), 
466                      numTabs];
467             }
469             [alert setInformativeText:info];
471             if ([alert runModal] != NSAlertFirstButtonReturn)
472                 reply = NSTerminateCancel;
474             [alert release];
475         }
476     }
479     // Tell all Vim processes to terminate now (otherwise they'll leave swap
480     // files behind).
481     if (NSTerminateNow == reply) {
482         e = [vimControllers objectEnumerator];
483         id vc;
484         while ((vc = [e nextObject]))
485             [vc sendMessage:TerminateNowMsgID data:nil];
486     }
488     return reply;
491 - (void)applicationWillTerminate:(NSNotification *)notification
493 #if MM_HANDLE_XCODE_MOD_EVENT
494     [[NSAppleEventManager sharedAppleEventManager]
495             removeEventHandlerForEventClass:'KAHL'
496                                  andEventID:'MOD '];
497 #endif
499     // This will invalidate all connections (since they were spawned from this
500     // connection).
501     [connection invalidate];
503     // Send a SIGINT to all running Vim processes, so that they are sure to
504     // receive the connectionDidDie: notification (a process has to be checking
505     // the run-loop for this to happen).
506     unsigned i, count = [vimControllers count];
507     for (i = 0; i < count; ++i) {
508         MMVimController *controller = [vimControllers objectAtIndex:i];
509         int pid = [controller pid];
510         if (pid > 0)
511             kill(pid, SIGINT);
512     }
514     if (fontContainerRef) {
515         ATSFontDeactivate(fontContainerRef, NULL, kATSOptionFlagsDefault);
516         fontContainerRef = 0;
517     }
519     [NSApp setDelegate:nil];
522 + (MMAppController *)sharedInstance
524     // Note: The app controller is a singleton which is instantiated in
525     // MainMenu.nib where it is also connected as the delegate of NSApp.
526     id delegate = [NSApp delegate];
527     return [delegate isKindOfClass:self] ? (MMAppController*)delegate : nil;
530 - (NSMenu *)defaultMainMenu
532     return defaultMainMenu;
535 - (void)removeVimController:(id)controller
537     //NSLog(@"%s%@", _cmd, controller);
539     [[controller windowController] close];
541     [vimControllers removeObject:controller];
543     if (![vimControllers count]) {
544         // The last editor window just closed so restore the main menu back to
545         // its default state (which is defined in MainMenu.nib).
546         [self setMainMenu:defaultMainMenu];
547     }
550 - (void)windowControllerWillOpen:(MMWindowController *)windowController
552     NSPoint topLeft = NSZeroPoint;
553     NSWindow *topWin = [[[self topmostVimController] windowController] window];
554     NSWindow *win = [windowController window];
556     if (!win) return;
558     // If there is a window belonging to a Vim process, cascade from it,
559     // otherwise use the autosaved window position (if any).
560     if (topWin) {
561         NSRect frame = [topWin frame];
562         topLeft = NSMakePoint(frame.origin.x, NSMaxY(frame));
563     } else {
564         NSString *topLeftString = [[NSUserDefaults standardUserDefaults]
565             stringForKey:MMTopLeftPointKey];
566         if (topLeftString)
567             topLeft = NSPointFromString(topLeftString);
568     }
570     if (!NSEqualPoints(topLeft, NSZeroPoint)) {
571         if (topWin)
572             topLeft = [win cascadeTopLeftFromPoint:topLeft];
574         [win setFrameTopLeftPoint:topLeft];
575     }
577     if (openSelectionString) {
578         // TODO: Pass this as a parameter instead!  Get rid of
579         // 'openSelectionString' etc.
580         //
581         // There is some text to paste into this window as a result of the
582         // services menu "Open selection ..." being used.
583         [[windowController vimController] dropString:openSelectionString];
584         [openSelectionString release];
585         openSelectionString = nil;
586     }
589 - (void)setMainMenu:(NSMenu *)mainMenu
591     if ([NSApp mainMenu] == mainMenu) return;
593     // If the new menu has a "Recent Files" dummy item, then swap the real item
594     // for the dummy.  We are forced to do this since Cocoa initializes the
595     // "Recent Files" menu and there is no way to simply point Cocoa to a new
596     // item each time the menus are swapped.
597     NSMenu *fileMenu = [mainMenu findFileMenu];
598     int dummyIdx =
599             [fileMenu indexOfItemWithAction:@selector(recentFilesDummy:)];
600     if (dummyIdx >= 0 && recentFilesMenuItem) {
601         NSMenuItem *dummyItem = [[fileMenu itemAtIndex:dummyIdx] retain];
602         [fileMenu removeItemAtIndex:dummyIdx];
604         NSMenu *recentFilesParentMenu = [recentFilesMenuItem menu];
605         int idx = [recentFilesParentMenu indexOfItem:recentFilesMenuItem];
606         if (idx >= 0) {
607             [[recentFilesMenuItem retain] autorelease];
608             [recentFilesParentMenu removeItemAtIndex:idx];
609             [recentFilesParentMenu insertItem:dummyItem atIndex:idx];
610         }
612         [fileMenu insertItem:recentFilesMenuItem atIndex:dummyIdx];
613         [dummyItem release];
614     }
616     // Now set the new menu.  Notice that we keep one menu for each editor
617     // window since each editor can have its own set of menus.  When swapping
618     // menus we have to tell Cocoa where the new "MacVim", "Windows", and
619     // "Services" menu are.
620     [NSApp setMainMenu:mainMenu];
622     // Setting the "MacVim" (or "Application") menu ensures that it is typeset
623     // in boldface.  (The setAppleMenu: method used to be public but is now
624     // private so this will have to be considered a bit of a hack!)
625     NSMenu *appMenu = [mainMenu findApplicationMenu];
626     [NSApp performSelector:@selector(setAppleMenu:) withObject:appMenu];
628     NSMenu *servicesMenu = [mainMenu findServicesMenu];
629     [NSApp setServicesMenu:servicesMenu];
631     NSMenu *windowsMenu = [mainMenu findWindowsMenu];
632     if (windowsMenu) {
633         // Cocoa isn't clever enough to get rid of items it has added to the
634         // "Windows" menu so we have to do it ourselves otherwise there will be
635         // multiple menu items for each window in the "Windows" menu.
636         //   This code assumes that the only items Cocoa add are ones which
637         // send off the action makeKeyAndOrderFront:.  (Cocoa will not add
638         // another separator item if the last item on the "Windows" menu
639         // already is a separator, so we needen't worry about separators.)
640         int i, count = [windowsMenu numberOfItems];
641         for (i = count-1; i >= 0; --i) {
642             NSMenuItem *item = [windowsMenu itemAtIndex:i];
643             if ([item action] == @selector(makeKeyAndOrderFront:))
644                 [windowsMenu removeItem:item];
645         }
647         [NSApp setWindowsMenu:windowsMenu];
648     }
651 - (IBAction)newWindow:(id)sender
653     [self launchVimProcessWithArguments:nil];
656 - (IBAction)fileOpen:(id)sender
658     NSString *dir = nil;
659     BOOL trackPwd = [[NSUserDefaults standardUserDefaults]
660             boolForKey:MMDialogsTrackPwdKey];
661     if (trackPwd) {
662         MMVimController *vc = [self keyVimController];
663         if (vc) dir = [[vc vimState] objectForKey:@"pwd"];
664     }
666     NSOpenPanel *panel = [NSOpenPanel openPanel];
667     [panel setAllowsMultipleSelection:YES];
668     int result = [panel runModalForDirectory:dir file:nil types:nil];
669     if (NSOKButton == result)
670         [self application:NSApp openFiles:[panel filenames]];
673 - (IBAction)selectNextWindow:(id)sender
675     unsigned i, count = [vimControllers count];
676     if (!count) return;
678     NSWindow *keyWindow = [NSApp keyWindow];
679     for (i = 0; i < count; ++i) {
680         MMVimController *vc = [vimControllers objectAtIndex:i];
681         if ([[[vc windowController] window] isEqual:keyWindow])
682             break;
683     }
685     if (i < count) {
686         if (++i >= count)
687             i = 0;
688         MMVimController *vc = [vimControllers objectAtIndex:i];
689         [[vc windowController] showWindow:self];
690     }
693 - (IBAction)selectPreviousWindow:(id)sender
695     unsigned i, count = [vimControllers count];
696     if (!count) return;
698     NSWindow *keyWindow = [NSApp keyWindow];
699     for (i = 0; i < count; ++i) {
700         MMVimController *vc = [vimControllers objectAtIndex:i];
701         if ([[[vc windowController] window] isEqual:keyWindow])
702             break;
703     }
705     if (i < count) {
706         if (i > 0) {
707             --i;
708         } else {
709             i = count - 1;
710         }
711         MMVimController *vc = [vimControllers objectAtIndex:i];
712         [[vc windowController] showWindow:self];
713     }
716 - (IBAction)fontSizeUp:(id)sender
718     [[NSFontManager sharedFontManager] modifyFont:
719             [NSNumber numberWithInt:NSSizeUpFontAction]];
722 - (IBAction)fontSizeDown:(id)sender
724     [[NSFontManager sharedFontManager] modifyFont:
725             [NSNumber numberWithInt:NSSizeDownFontAction]];
728 - (IBAction)orderFrontPreferencePanel:(id)sender
730     [[MMPreferenceController sharedPrefsWindowController] showWindow:self];
733 - (IBAction)openWebsite:(id)sender
735     [[NSWorkspace sharedWorkspace] openURL:
736             [NSURL URLWithString:MMWebsiteString]];
739 - (IBAction)showVimHelp:(id)sender
741     // Open a new window with the help window maximized.
742     [self launchVimProcessWithArguments:[NSArray arrayWithObjects:
743             @"-c", @":h gui_mac", @"-c", @":res", nil]];
746 - (IBAction)zoomAll:(id)sender
748     [NSApp makeWindowsPerform:@selector(performZoom:) inOrder:YES];
751 - (byref id <MMFrontendProtocol>)
752     connectBackend:(byref in id <MMBackendProtocol>)backend
753                pid:(int)pid
755     //NSLog(@"Connect backend (pid=%d)", pid);
756     NSNumber *pidKey = [NSNumber numberWithInt:pid];
757     MMVimController *vc = nil;
759     @try {
760         [(NSDistantObject*)backend
761                 setProtocolForProxy:@protocol(MMBackendProtocol)];
763         vc = [[[MMVimController alloc]
764             initWithBackend:backend pid:pid]
765             autorelease];
767         if (![vimControllers count]) {
768             // The first window autosaves its position.  (The autosaving
769             // features of Cocoa are not used because we need more control over
770             // what is autosaved and when it is restored.)
771             [[vc windowController] setWindowAutosaveKey:MMTopLeftPointKey];
772         }
774         [vimControllers addObject:vc];
776         id args = [pidArguments objectForKey:pidKey];
777         if (args && [NSNull null] != args)
778             [self passArguments:args toVimController:vc];
780         // HACK!  MacVim does not get activated if it is launched from the
781         // terminal, so we forcibly activate here unless it is an untitled
782         // window opening.  Untitled windows are treated differently, else
783         // MacVim would steal the focus if another app was activated while the
784         // untitled window was loading.
785         if (!args || args != [NSNull null])
786             [NSApp activateIgnoringOtherApps:YES];
788         if (args)
789             [pidArguments removeObjectForKey:pidKey];
791         return vc;
792     }
794     @catch (NSException *e) {
795         NSLog(@"Exception caught in %s: \"%@\"", _cmd, e);
797         if (vc)
798             [vimControllers removeObject:vc];
800         [pidArguments removeObjectForKey:pidKey];
801     }
803     return nil;
806 - (NSArray *)serverList
808     NSMutableArray *array = [NSMutableArray array];
810     unsigned i, count = [vimControllers count];
811     for (i = 0; i < count; ++i) {
812         MMVimController *controller = [vimControllers objectAtIndex:i];
813         if ([controller serverName])
814             [array addObject:[controller serverName]];
815     }
817     return array;
820 @end // MMAppController
825 @implementation MMAppController (MMServices)
827 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
828                 error:(NSString **)error
830     if (![[pboard types] containsObject:NSStringPboardType]) {
831         NSLog(@"WARNING: Pasteboard contains no object of type "
832                 "NSStringPboardType");
833         return;
834     }
836     MMVimController *vc = [self topmostVimController];
837     if (vc) {
838         // Open a new tab first, since dropString: does not do this.
839         [vc sendMessage:AddNewTabMsgID data:nil];
840         [vc dropString:[pboard stringForType:NSStringPboardType]];
841     } else {
842         // NOTE: There is no window to paste the selection into, so save the
843         // text, open a new window, and paste the text when the next window
844         // opens.  (If this is called several times in a row, then all but the
845         // last call might be ignored.)
846         if (openSelectionString) [openSelectionString release];
847         openSelectionString = [[pboard stringForType:NSStringPboardType] copy];
849         [self newWindow:self];
850     }
853 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
854            error:(NSString **)error
856     if (![[pboard types] containsObject:NSStringPboardType]) {
857         NSLog(@"WARNING: Pasteboard contains no object of type "
858                 "NSStringPboardType");
859         return;
860     }
862     // TODO: Parse multiple filenames and create array with names.
863     NSString *string = [pboard stringForType:NSStringPboardType];
864     string = [string stringByTrimmingCharactersInSet:
865             [NSCharacterSet whitespaceAndNewlineCharacterSet]];
866     string = [string stringByStandardizingPath];
868     NSArray *filenames = [self filterFilesAndNotify:
869             [NSArray arrayWithObject:string]];
870     if ([filenames count] > 0) {
871         MMVimController *vc = nil;
872         if (userData && [userData isEqual:@"Tab"])
873             vc = [self topmostVimController];
875         if (vc) {
876             [vc dropFiles:filenames forceOpen:YES];
877         } else {
878             [self application:NSApp openFiles:filenames];
879         }
880     }
883 @end // MMAppController (MMServices)
888 @implementation MMAppController (Private)
890 - (MMVimController *)keyVimController
892     NSWindow *keyWindow = [NSApp keyWindow];
893     if (keyWindow) {
894         unsigned i, count = [vimControllers count];
895         for (i = 0; i < count; ++i) {
896             MMVimController *vc = [vimControllers objectAtIndex:i];
897             if ([[[vc windowController] window] isEqual:keyWindow])
898                 return vc;
899         }
900     }
902     return nil;
905 - (MMVimController *)topmostVimController
907     // Find the topmost visible window which has an associated vim controller.
908     NSEnumerator *e = [[NSApp orderedWindows] objectEnumerator];
909     id window;
910     while ((window = [e nextObject]) && [window isVisible]) {
911         unsigned i, count = [vimControllers count];
912         for (i = 0; i < count; ++i) {
913             MMVimController *vc = [vimControllers objectAtIndex:i];
914             if ([[[vc windowController] window] isEqual:window])
915                 return vc;
916         }
917     }
919     return nil;
922 - (int)launchVimProcessWithArguments:(NSArray *)args
924     int pid = -1;
925     NSString *path = [[NSBundle mainBundle] pathForAuxiliaryExecutable:@"Vim"];
927     if (!path) {
928         NSLog(@"ERROR: Vim executable could not be found inside app bundle!");
929         return -1;
930     }
932     NSArray *taskArgs = [NSArray arrayWithObjects:@"-g", @"-f", nil];
933     if (args)
934         taskArgs = [taskArgs arrayByAddingObjectsFromArray:args];
936     BOOL useLoginShell = [[NSUserDefaults standardUserDefaults]
937             boolForKey:MMLoginShellKey];
938     if (useLoginShell) {
939         // Run process with a login shell, roughly:
940         //   echo "exec Vim -g -f args" | ARGV0=-`basename $SHELL` $SHELL [-l]
941         pid = executeInLoginShell(path, taskArgs);
942     } else {
943         // Run process directly:
944         //   Vim -g -f args
945         NSTask *task = [NSTask launchedTaskWithLaunchPath:path
946                                                 arguments:taskArgs];
947         pid = task ? [task processIdentifier] : -1;
948     }
950     if (-1 != pid) {
951         // NOTE: If the process has no arguments, then add a null argument to
952         // the pidArguments dictionary.  This is later used to detect that a
953         // process without arguments is being launched.
954         if (!args)
955             [pidArguments setObject:[NSNull null]
956                              forKey:[NSNumber numberWithInt:pid]];
957     } else {
958         NSLog(@"WARNING: %s%@ failed (useLoginShell=%d)", _cmd, args,
959                 useLoginShell);
960     }
962     return pid;
965 - (NSArray *)filterFilesAndNotify:(NSArray *)filenames
967     // Go trough 'filenames' array and make sure each file exists.  Present
968     // warning dialog if some file was missing.
970     NSString *firstMissingFile = nil;
971     NSMutableArray *files = [NSMutableArray array];
972     unsigned i, count = [filenames count];
974     for (i = 0; i < count; ++i) {
975         NSString *name = [filenames objectAtIndex:i];
976         if ([[NSFileManager defaultManager] fileExistsAtPath:name]) {
977             [files addObject:name];
978         } else if (!firstMissingFile) {
979             firstMissingFile = name;
980         }
981     }
983     if (firstMissingFile) {
984         NSAlert *alert = [[NSAlert alloc] init];
985         [alert addButtonWithTitle:NSLocalizedString(@"OK",
986                 @"Dialog button")];
988         NSString *text;
989         if ([files count] >= count-1) {
990             [alert setMessageText:NSLocalizedString(@"File not found",
991                     @"File not found dialog, title")];
992             text = [NSString stringWithFormat:NSLocalizedString(
993                     @"Could not open file with name %@.",
994                     @"File not found dialog, text"), firstMissingFile];
995         } else {
996             [alert setMessageText:NSLocalizedString(@"Multiple files not found",
997                     @"File not found dialog, title")];
998             text = [NSString stringWithFormat:NSLocalizedString(
999                     @"Could not open file with name %@, and %d other files.",
1000                     @"File not found dialog, text"),
1001                 firstMissingFile, count-[files count]-1];
1002         }
1004         [alert setInformativeText:text];
1005         [alert setAlertStyle:NSWarningAlertStyle];
1007         [alert runModal];
1008         [alert release];
1010         [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
1011     }
1013     return files;
1016 - (NSArray *)filterOpenFiles:(NSArray *)filenames
1017                    arguments:(NSDictionary *)args
1019     // Check if any of the files in the 'filenames' array are open in any Vim
1020     // process.  Remove the files that are open from the 'filenames' array and
1021     // return it.  If all files were filtered out, then raise the first file in
1022     // the Vim process it is open.  Files that are filtered are sent an odb
1023     // open event in case theID is not zero.
1025     NSMutableDictionary *localArgs =
1026             [NSMutableDictionary dictionaryWithDictionary:args];
1027     MMVimController *raiseController = nil;
1028     NSString *raiseFile = nil;
1029     NSMutableArray *files = [filenames mutableCopy];
1030     NSString *expr = [NSString stringWithFormat:
1031             @"map([\"%@\"],\"bufloaded(v:val)\")",
1032             [files componentsJoinedByString:@"\",\""]];
1033     unsigned i, count = [vimControllers count];
1035     // Ensure that the files aren't opened when passing arguments.
1036     [localArgs setObject:[NSNumber numberWithBool:NO] forKey:@"openFiles"];
1038     for (i = 0; i < count && [files count]; ++i) {
1039         MMVimController *controller = [vimControllers objectAtIndex:i];
1041         // Query Vim for which files in the 'files' array are open.
1042         NSString *eval = [controller evaluateVimExpression:expr];
1043         if (!eval) continue;
1045         NSIndexSet *idxSet = [NSIndexSet indexSetWithVimList:eval];
1046         if ([idxSet count]) {
1047             if (!raiseFile) {
1048                 // Remember the file and which Vim that has it open so that
1049                 // we can raise it later on.
1050                 raiseController = controller;
1051                 raiseFile = [files objectAtIndex:[idxSet firstIndex]];
1052                 [[raiseFile retain] autorelease];
1053             }
1055             // Pass (ODB/Xcode/Spotlight) arguments to this process.
1056             [localArgs setObject:[files objectsAtIndexes:idxSet]
1057                           forKey:@"filenames"];
1058             [self passArguments:localArgs toVimController:controller];
1060             // Remove all the files that were open in this Vim process and
1061             // create a new expression to evaluate.
1062             [files removeObjectsAtIndexes:idxSet];
1063             expr = [NSString stringWithFormat:
1064                     @"map([\"%@\"],\"bufloaded(v:val)\")",
1065                     [files componentsJoinedByString:@"\",\""]];
1066         }
1067     }
1069     if (![files count] && raiseFile) {
1070         // Raise the window containing the first file that was already open,
1071         // and make sure that the tab containing that file is selected.  Only
1072         // do this if there are no more files to open, otherwise sometimes the
1073         // window with 'raiseFile' will be raised, other times it might be the
1074         // window that will open with the files in the 'files' array.
1075         raiseFile = [raiseFile stringByEscapingSpecialFilenameCharacters];
1076         NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>"
1077             ":let oldswb=&swb|let &swb=\"useopen,usetab\"|"
1078             "tab sb %@|let &swb=oldswb|unl oldswb|"
1079             "cal foreground()|redr|f<CR>", raiseFile];
1081         [raiseController addVimInput:input];
1082     }
1084     return files;
1087 #if MM_HANDLE_XCODE_MOD_EVENT
1088 - (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
1089                  replyEvent:(NSAppleEventDescriptor *)reply
1091 #if 0
1092     // Xcode sends this event to query MacVim which open files have been
1093     // modified.
1094     NSLog(@"reply:%@", reply);
1095     NSLog(@"event:%@", event);
1097     NSEnumerator *e = [vimControllers objectEnumerator];
1098     id vc;
1099     while ((vc = [e nextObject])) {
1100         DescType type = [reply descriptorType];
1101         unsigned len = [[type data] length];
1102         NSMutableData *data = [NSMutableData data];
1104         [data appendBytes:&type length:sizeof(DescType)];
1105         [data appendBytes:&len length:sizeof(unsigned)];
1106         [data appendBytes:[reply data] length:len];
1108         [vc sendMessage:XcodeModMsgID data:data];
1109     }
1110 #endif
1112 #endif
1114 - (int)findLaunchingProcessWithoutArguments
1116     NSArray *keys = [pidArguments allKeysForObject:[NSNull null]];
1117     if ([keys count] > 0) {
1118         //NSLog(@"found launching process without arguments");
1119         return [[keys objectAtIndex:0] intValue];
1120     }
1122     return 0;
1125 - (MMVimController *)findUntitledWindow
1127     NSEnumerator *e = [vimControllers objectEnumerator];
1128     id vc;
1129     while ((vc = [e nextObject])) {
1130         // TODO: This is a moronic test...should query the Vim process if there
1131         // are any open buffers or something like that instead.
1132         NSString *title = [[[vc windowController] window] title];
1134         // TODO: this will not work in a localized MacVim
1135         if ([title hasPrefix:@"[No Name] - VIM"]) {
1136             //NSLog(@"found untitled window");
1137             return vc;
1138         }
1139     }
1141     return nil;
1144 - (NSMutableDictionary *)extractArgumentsFromOdocEvent:
1145     (NSAppleEventDescriptor *)desc
1147     NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1149     // 1. Extract ODB parameters (if any)
1150     NSAppleEventDescriptor *odbdesc = desc;
1151     if (![odbdesc paramDescriptorForKeyword:keyFileSender]) {
1152         // The ODB paramaters may hide inside the 'keyAEPropData' descriptor.
1153         odbdesc = [odbdesc paramDescriptorForKeyword:keyAEPropData];
1154         if (![odbdesc paramDescriptorForKeyword:keyFileSender])
1155             odbdesc = nil;
1156     }
1158     if (odbdesc) {
1159         NSAppleEventDescriptor *p =
1160                 [odbdesc paramDescriptorForKeyword:keyFileSender];
1161         if (p)
1162             [dict setObject:[NSNumber numberWithUnsignedInt:[p typeCodeValue]]
1163                      forKey:@"remoteID"];
1165         p = [odbdesc paramDescriptorForKeyword:keyFileCustomPath];
1166         if (p)
1167             [dict setObject:[p stringValue] forKey:@"remotePath"];
1169         p = [odbdesc paramDescriptorForKeyword:keyFileSenderToken];
1170         if (p)
1171             [dict setObject:p forKey:@"remotePath"];
1172     }
1174     // 2. Extract Xcode parameters (if any)
1175     NSAppleEventDescriptor *xcodedesc =
1176             [desc paramDescriptorForKeyword:keyAEPosition];
1177     if (xcodedesc) {
1178         NSRange range;
1179         MMSelectionRange *sr = (MMSelectionRange*)[[xcodedesc data] bytes];
1181         if (sr->lineNum < 0) {
1182             // Should select a range of lines.
1183             range.location = sr->startRange + 1;
1184             range.length = sr->endRange - sr->startRange + 1;
1185         } else {
1186             // Should only move cursor to a line.
1187             range.location = sr->lineNum + 1;
1188             range.length = 0;
1189         }
1191         [dict setObject:NSStringFromRange(range) forKey:@"selectionRange"];
1192     }
1194     // 3. Extract Spotlight search text (if any)
1195     NSAppleEventDescriptor *spotlightdesc = 
1196             [desc paramDescriptorForKeyword:keyAESearchText];
1197     if (spotlightdesc)
1198         [dict setObject:[spotlightdesc stringValue] forKey:@"searchText"];
1200     return dict;
1203 - (void)passArguments:(NSDictionary *)args toVimController:(MMVimController*)vc
1205     if (!args) return;
1207     // Pass filenames to open if required (the 'openFiles' argument can be used
1208     // to disallow opening of the files).
1209     NSArray *filenames = [args objectForKey:@"filenames"];
1210     if (filenames && [[args objectForKey:@"openFiles"] boolValue]) {
1211         NSString *tabDrop = buildTabDropCommand(filenames);
1212         [vc addVimInput:tabDrop];
1213     }
1215     // Pass ODB data
1216     if (filenames && [args objectForKey:@"remoteID"]) {
1217         [vc odbEdit:filenames
1218              server:[[args objectForKey:@"remoteID"] unsignedIntValue]
1219                path:[args objectForKey:@"remotePath"]
1220               token:[args objectForKey:@"remoteToken"]];
1221     }
1223     // Pass range of lines to select
1224     if ([args objectForKey:@"selectionRange"]) {
1225         NSRange selectionRange = NSRangeFromString(
1226                 [args objectForKey:@"selectionRange"]);
1227         [vc addVimInput:buildSelectRangeCommand(selectionRange)];
1228     }
1230     // Pass search text
1231     NSString *searchText = [args objectForKey:@"searchText"];
1232     if (searchText)
1233         [vc addVimInput:buildSearchTextCommand(searchText)];
1236 @end // MMAppController (Private)
1241 @implementation NSNumber (MMExtras)
1242 - (int)tag
1244     return [self intValue];
1246 @end // NSNumber (MMExtras)
1251 @implementation NSMenu (MMExtras)
1253 - (int)indexOfItemWithAction:(SEL)action
1255     int i, count = [self numberOfItems];
1256     for (i = 0; i < count; ++i) {
1257         NSMenuItem *item = [self itemAtIndex:i];
1258         if ([item action] == action)
1259             return i;
1260     }
1262     return -1;
1265 - (NSMenuItem *)itemWithAction:(SEL)action
1267     int idx = [self indexOfItemWithAction:action];
1268     return idx >= 0 ? [self itemAtIndex:idx] : nil;
1271 - (NSMenu *)findMenuContainingItemWithAction:(SEL)action
1273     // NOTE: We only look for the action in the submenus of 'self'
1274     int i, count = [self numberOfItems];
1275     for (i = 0; i < count; ++i) {
1276         NSMenu *menu = [[self itemAtIndex:i] submenu];
1277         NSMenuItem *item = [menu itemWithAction:action];
1278         if (item) return menu;
1279     }
1281     return nil;
1284 - (NSMenu *)findWindowsMenu
1286     return [self findMenuContainingItemWithAction:
1287         @selector(performMiniaturize:)];
1290 - (NSMenu *)findApplicationMenu
1292     // TODO: Just return [self itemAtIndex:0]?
1293     return [self findMenuContainingItemWithAction:@selector(terminate:)];
1296 - (NSMenu *)findServicesMenu
1298     // NOTE!  Our heuristic for finding the "Services" menu is to look for the
1299     // second item before the "Hide MacVim" menu item on the "MacVim" menu.
1300     // (The item before "Hide MacVim" should be a separator, but this is not
1301     // important as long as the item before that is the "Services" menu.)
1303     NSMenu *appMenu = [self findApplicationMenu];
1304     if (!appMenu) return nil;
1306     int idx = [appMenu indexOfItemWithAction: @selector(hide:)];
1307     if (idx-2 < 0) return nil;  // idx == -1, if selector not found
1309     return [[appMenu itemAtIndex:idx-2] submenu];
1312 - (NSMenu *)findFileMenu
1314     return [self findMenuContainingItemWithAction:@selector(performClose:)];
1317 @end // NSMenu (MMExtras)
1322     static int
1323 executeInLoginShell(NSString *path, NSArray *args)
1325     // Start a login shell and execute the command 'path' with arguments 'args'
1326     // in the shell.  This ensures that user environment variables are set even
1327     // when MacVim was started from the Finder.
1329     int pid = -1;
1330     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1332     // Determine which shell to use to execute the command.  The user
1333     // may decide which shell to use by setting a user default or the
1334     // $SHELL environment variable.
1335     NSString *shell = [ud stringForKey:MMLoginShellCommandKey];
1336     if (!shell || [shell length] == 0)
1337         shell = [[[NSProcessInfo processInfo] environment]
1338             objectForKey:@"SHELL"];
1339     if (!shell)
1340         shell = @"/bin/bash";
1342     //NSLog(@"shell = %@", shell);
1344     // Bash needs the '-l' flag to launch a login shell.  The user may add
1345     // flags by setting a user default.
1346     NSString *shellArgument = [ud stringForKey:MMLoginShellArgumentKey];
1347     if (!shellArgument || [shellArgument length] == 0) {
1348         if ([[shell lastPathComponent] isEqual:@"bash"])
1349             shellArgument = @"-l";
1350         else
1351             shellArgument = nil;
1352     }
1354     //NSLog(@"shellArgument = %@", shellArgument);
1356     // Build input string to pipe to the login shell.
1357     NSMutableString *input = [NSMutableString stringWithFormat:
1358             @"exec \"%@\"", path];
1359     if (args) {
1360         // Append all arguments, making sure they are properly quoted, even
1361         // when they contain single quotes.
1362         NSEnumerator *e = [args objectEnumerator];
1363         id obj;
1365         while ((obj = [e nextObject])) {
1366             NSMutableString *arg = [NSMutableString stringWithString:obj];
1367             [arg replaceOccurrencesOfString:@"'" withString:@"'\"'\"'"
1368                                     options:NSLiteralSearch
1369                                       range:NSMakeRange(0, [arg length])];
1370             [input appendFormat:@" '%@'", arg];
1371         }
1372     }
1374     // Build the argument vector used to start the login shell.
1375     NSString *shellArg0 = [NSString stringWithFormat:@"-%@",
1376              [shell lastPathComponent]];
1377     char *shellArgv[3] = { (char *)[shellArg0 UTF8String], NULL, NULL };
1378     if (shellArgument)
1379         shellArgv[1] = (char *)[shellArgument UTF8String];
1381     // Get the C string representation of the shell path before the fork since
1382     // we must not call Foundation functions after a fork.
1383     const char *shellPath = [shell fileSystemRepresentation];
1385     // Fork and execute the process.
1386     int ds[2];
1387     if (pipe(ds)) return -1;
1389     pid = fork();
1390     if (pid == -1) {
1391         return -1;
1392     } else if (pid == 0) {
1393         // Child process
1394         if (close(ds[1]) == -1) exit(255);
1395         if (dup2(ds[0], 0) == -1) exit(255);
1397         execv(shellPath, shellArgv);
1399         // Never reached unless execv fails
1400         exit(255);
1401     } else {
1402         // Parent process
1403         if (close(ds[0]) == -1) return -1;
1405         // Send input to execute to the child process
1406         [input appendString:@"\n"];
1407         int bytes = [input lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
1409         if (write(ds[1], [input UTF8String], bytes) != bytes) return -1;
1410         if (close(ds[1]) == -1) return -1;
1411     }
1413     return pid;