Add file for misc code in frontend
[MacVim.git] / src / MacVim / MMAppController.m
blob445532f584d56a60a4e95c2a1f99dde634bfc26e
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 and takes care of the main menu.
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  *
28  * The main menu is handled as follows.  Each Vim controller keeps its own main
29  * menu.  All menus except the "MacVim" menu are controlled by the Vim process.
30  * The app controller also keeps a reference to the "default main menu" which
31  * is set up in MainMenu.nib.  When no editor window is open the default main
32  * menu is used.  When a new editor window becomes main its main menu becomes
33  * the new main menu, this is done in -[MMAppController setMainMenu:].
34  *   NOTE: Certain heuristics are used to find the "MacVim", "Windows", "File",
35  * and "Services" menu.  If MainMenu.nib changes these heuristics may have to
36  * change as well.  For specifics see the find... methods defined in the NSMenu
37  * category "MMExtras".
38  */
40 #import "MMAppController.h"
41 #import "MMPreferenceController.h"
42 #import "MMVimController.h"
43 #import "MMWindowController.h"
44 #import "Miscellaneous.h"
45 #import <unistd.h>
48 #define MM_HANDLE_XCODE_MOD_EVENT 0
52 // Default timeout intervals on all connections.
53 static NSTimeInterval MMRequestTimeout = 5;
54 static NSTimeInterval MMReplyTimeout = 5;
56 static NSString *MMWebsiteString = @"http://code.google.com/p/macvim/";
58 // When terminating, notify Vim processes then sleep for these many
59 // microseconds.
60 static useconds_t MMTerminationSleepPeriod = 10000;
63 #pragma options align=mac68k
64 typedef struct
66     short unused1;      // 0 (not used)
67     short lineNum;      // line to select (< 0 to specify range)
68     long  startRange;   // start of selection range (if line < 0)
69     long  endRange;     // end of selection range (if line < 0)
70     long  unused2;      // 0 (not used)
71     long  theDate;      // modification date/time
72 } MMSelectionRange;
73 #pragma options align=reset
76 static int executeInLoginShell(NSString *path, NSArray *args);
79 @interface MMAppController (MMServices)
80 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
81                 error:(NSString **)error;
82 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
83            error:(NSString **)error;
84 @end
87 @interface MMAppController (Private)
88 - (MMVimController *)keyVimController;
89 - (MMVimController *)topmostVimController;
90 - (int)launchVimProcessWithArguments:(NSArray *)args;
91 - (NSArray *)filterFilesAndNotify:(NSArray *)files;
92 - (NSArray *)filterOpenFiles:(NSArray *)filenames
93                    arguments:(NSDictionary *)args;
94 #if MM_HANDLE_XCODE_MOD_EVENT
95 - (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
96                  replyEvent:(NSAppleEventDescriptor *)reply;
97 #endif
98 - (int)findLaunchingProcessWithoutArguments;
99 - (MMVimController *)findUntitledWindow;
100 - (NSMutableDictionary *)extractArgumentsFromOdocEvent:
101     (NSAppleEventDescriptor *)desc;
102 - (void)passArguments:(NSDictionary *)args toVimController:(MMVimController*)vc;
103 @end
107 @implementation MMAppController
109 + (void)initialize
111     NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
112         [NSNumber numberWithBool:NO],   MMNoWindowKey,
113         [NSNumber numberWithInt:64],    MMTabMinWidthKey,
114         [NSNumber numberWithInt:6*64],  MMTabMaxWidthKey,
115         [NSNumber numberWithInt:132],   MMTabOptimumWidthKey,
116         [NSNumber numberWithInt:2],     MMTextInsetLeftKey,
117         [NSNumber numberWithInt:1],     MMTextInsetRightKey,
118         [NSNumber numberWithInt:1],     MMTextInsetTopKey,
119         [NSNumber numberWithInt:1],     MMTextInsetBottomKey,
120         [NSNumber numberWithBool:NO],   MMTerminateAfterLastWindowClosedKey,
121         @"MMTypesetter",                MMTypesetterKey,
122         [NSNumber numberWithFloat:1],   MMCellWidthMultiplierKey,
123         [NSNumber numberWithFloat:-1],  MMBaselineOffsetKey,
124         [NSNumber numberWithBool:YES],  MMTranslateCtrlClickKey,
125         [NSNumber numberWithBool:NO],   MMOpenFilesInTabsKey,
126         [NSNumber numberWithBool:NO],   MMNoFontSubstitutionKey,
127         [NSNumber numberWithBool:NO],   MMLoginShellKey,
128         [NSNumber numberWithBool:NO],   MMAtsuiRendererKey,
129         [NSNumber numberWithInt:MMUntitledWindowAlways],
130                                         MMUntitledWindowKey,
131         [NSNumber numberWithBool:NO],   MMTexturedWindowKey,
132         [NSNumber numberWithBool:NO],   MMZoomBothKey,
133         @"",                            MMLoginShellCommandKey,
134         @"",                            MMLoginShellArgumentKey,
135         [NSNumber numberWithBool:YES],  MMDialogsTrackPwdKey,
136         nil];
138     [[NSUserDefaults standardUserDefaults] registerDefaults:dict];
140     NSArray *types = [NSArray arrayWithObject:NSStringPboardType];
141     [NSApp registerServicesMenuSendTypes:types returnTypes:types];
144 - (id)init
146     if ((self = [super init])) {
147         fontContainerRef = loadFonts();
149         vimControllers = [NSMutableArray new];
150         pidArguments = [NSMutableDictionary new];
152         // NOTE: Do not use the default connection since the Logitech Control
153         // Center (LCC) input manager steals and this would cause MacVim to
154         // never open any windows.  (This is a bug in LCC but since they are
155         // unlikely to fix it, we graciously give them the default connection.)
156         connection = [[NSConnection alloc] initWithReceivePort:[NSPort port]
157                                                       sendPort:nil];
158         [connection setRootObject:self];
159         [connection setRequestTimeout:MMRequestTimeout];
160         [connection setReplyTimeout:MMReplyTimeout];
162         // NOTE: When the user is resizing the window the AppKit puts the run
163         // loop in event tracking mode.  Unless the connection listens to
164         // request in this mode, live resizing won't work.
165         [connection addRequestMode:NSEventTrackingRunLoopMode];
167         // NOTE!  If the name of the connection changes here it must also be
168         // updated in MMBackend.m.
169         NSString *name = [NSString stringWithFormat:@"%@-connection",
170                  [[NSBundle mainBundle] bundleIdentifier]];
171         //NSLog(@"Registering connection with name '%@'", name);
172         if (![connection registerName:name]) {
173             NSLog(@"FATAL ERROR: Failed to register connection with name '%@'",
174                     name);
175             [connection release];  connection = nil;
176         }
177     }
179     return self;
182 - (void)dealloc
184     //NSLog(@"MMAppController dealloc");
186     [connection release];  connection = nil;
187     [pidArguments release];  pidArguments = nil;
188     [vimControllers release];  vimControllers = nil;
189     [openSelectionString release];  openSelectionString = nil;
190     [recentFilesMenuItem release];  recentFilesMenuItem = nil;
191     [defaultMainMenu release];  defaultMainMenu = nil;
192     [appMenuItemTemplate release];  appMenuItemTemplate = 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     // Store a copy of the default app menu so we can use this as a template
204     // for all other menus.  We make a copy here because the "Services" menu
205     // will not yet have been populated at this time.  If we don't we get
206     // problems trying to set key equivalents later on because they might clash
207     // with items on the "Services" menu.
208     appMenuItemTemplate = [defaultMainMenu itemAtIndex:0];
209     appMenuItemTemplate = [appMenuItemTemplate copy];
211     // Set up the "Open Recent" menu. See
212     //   http://lapcatsoftware.com/blog/2007/07/10/
213     //     working-without-a-nib-part-5-open-recent-menu/
214     // and
215     //   http://www.cocoabuilder.com/archive/message/cocoa/2007/8/15/187793
216     // for more information.
217     //
218     // The menu itself is created in MainMenu.nib but we still seem to have to
219     // hack around a bit to get it to work.  (This has to be done in
220     // applicationWillFinishLaunching at the latest, otherwise it doesn't
221     // work.)
222     NSMenu *fileMenu = [defaultMainMenu findFileMenu];
223     if (fileMenu) {
224         int idx = [fileMenu indexOfItemWithAction:@selector(fileOpen:)];
225         if (idx >= 0 && idx+1 < [fileMenu numberOfItems])
227         recentFilesMenuItem = [fileMenu itemWithTitle:@"Open Recent"];
228         [[recentFilesMenuItem submenu] performSelector:@selector(_setMenuName:)
229                                         withObject:@"NSRecentDocumentsMenu"];
231         // Note: The "Recent Files" menu must be moved around since there is no
232         // -[NSApp setRecentFilesMenu:] method.  We keep a reference to it to
233         // facilitate this move (see setMainMenu: below).
234         [recentFilesMenuItem retain];
235     }
237 #if MM_HANDLE_XCODE_MOD_EVENT
238     [[NSAppleEventManager sharedAppleEventManager]
239             setEventHandler:self
240                 andSelector:@selector(handleXcodeModEvent:replyEvent:)
241               forEventClass:'KAHL'
242                  andEventID:'MOD '];
243 #endif
246 - (void)applicationDidFinishLaunching:(NSNotification *)notification
248     [NSApp setServicesProvider:self];
251 - (BOOL)applicationShouldOpenUntitledFile:(NSApplication *)sender
253     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
254     NSAppleEventManager *aem = [NSAppleEventManager sharedAppleEventManager];
255     NSAppleEventDescriptor *desc = [aem currentAppleEvent];
257     // The user default MMUntitledWindow can be set to control whether an
258     // untitled window should open on 'Open' and 'Reopen' events.
259     int untitledWindowFlag = [ud integerForKey:MMUntitledWindowKey];
260     if ([desc eventID] == kAEOpenApplication
261             && (untitledWindowFlag & MMUntitledWindowOnOpen) == 0)
262         return NO;
263     else if ([desc eventID] == kAEReopenApplication
264             && (untitledWindowFlag & MMUntitledWindowOnReopen) == 0)
265         return NO;
267     // When a process is started from the command line, the 'Open' event will
268     // contain a parameter to surpress the opening of an untitled window.
269     desc = [desc paramDescriptorForKeyword:keyAEPropData];
270     desc = [desc paramDescriptorForKeyword:keyMMUntitledWindow];
271     if (desc && ![desc booleanValue])
272         return NO;
274     // Never open an untitled window if there is at least one open window or if
275     // there are processes that are currently launching.
276     if ([vimControllers count] > 0 || [pidArguments count] > 0)
277         return NO;
279     // NOTE!  This way it possible to start the app with the command-line
280     // argument '-nowindow yes' and no window will be opened by default.
281     return ![ud boolForKey:MMNoWindowKey];
284 - (BOOL)applicationOpenUntitledFile:(NSApplication *)sender
286     [self newWindow:self];
287     return YES;
290 - (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames
292     // Opening files works like this:
293     //  a) extract ODB/Xcode/Spotlight parameters from the current Apple event
294     //  b) filter out any already open files (see filterOpenFiles::)
295     //  c) open any remaining files
296     //
297     // A file is opened in an untitled window if there is one (it may be
298     // currently launching, or it may already be visible), otherwise a new
299     // window is opened.
300     //
301     // Each launching Vim process has a dictionary of arguments that are passed
302     // to the process when in checks in (via connectBackend:pid:).  The
303     // arguments for each launching process can be looked up by its PID (in the
304     // pidArguments dictionary).
306     NSMutableDictionary *arguments = [self extractArgumentsFromOdocEvent:
307             [[NSAppleEventManager sharedAppleEventManager] currentAppleEvent]];
309     // Filter out files that are already open
310     filenames = [self filterOpenFiles:filenames arguments:arguments];
312     // Open any files that remain
313     if ([filenames count]) {
314         MMVimController *vc;
315         BOOL openInTabs = [[NSUserDefaults standardUserDefaults]
316             boolForKey:MMOpenFilesInTabsKey];
318         [arguments setObject:filenames forKey:@"filenames"];
319         [arguments setObject:[NSNumber numberWithBool:YES] forKey:@"openFiles"];
321         // Add file names to "Recent Files" menu.
322         int i, count = [filenames count];
323         for (i = 0; i < count; ++i) {
324             // Don't add files that are being edited remotely (using ODB).
325             if ([arguments objectForKey:@"remoteID"]) continue;
327             [[NSDocumentController sharedDocumentController]
328                     noteNewRecentFilePath:[filenames objectAtIndex:i]];
329         }
331         if ((openInTabs && (vc = [self topmostVimController]))
332                || (vc = [self findUntitledWindow])) {
333             // Open files in an already open window.
334             [[[vc windowController] window] makeKeyAndOrderFront:self];
335             [self passArguments:arguments toVimController:vc];
336         } else {
337             // Open files in a launching Vim process or start a new process.
338             int pid = [self findLaunchingProcessWithoutArguments];
339             if (!pid) {
340                 // Pass the filenames to the process straight away.
341                 //
342                 // TODO: It would be nicer if all arguments were passed to the
343                 // Vim process in connectBackend::, but if we don't pass the
344                 // filename arguments here, the window 'flashes' once when it
345                 // opens.  This is due to the 'welcome' screen first being
346                 // displayed, then quickly thereafter the files are opened.
347                 NSArray *fileArgs = [NSArray arrayWithObject:@"-p"];
348                 fileArgs = [fileArgs arrayByAddingObjectsFromArray:filenames];
350                 pid = [self launchVimProcessWithArguments:fileArgs];
352                 if (-1 == pid) {
353                     // TODO: Notify user of failure?
354                     [NSApp replyToOpenOrPrint:
355                         NSApplicationDelegateReplyFailure];
356                     return;
357                 }
359                 // Make sure these files aren't opened again when
360                 // connectBackend:pid: is called.
361                 [arguments setObject:[NSNumber numberWithBool:NO]
362                               forKey:@"openFiles"];
363             }
365             // TODO: If the Vim process fails to start, or if it changes PID,
366             // then the memory allocated for these parameters will leak.
367             // Ensure that this cannot happen or somehow detect it.
369             if ([arguments count] > 0)
370                 [pidArguments setObject:arguments
371                                  forKey:[NSNumber numberWithInt:pid]];
372         }
373     }
375     [NSApp replyToOpenOrPrint:NSApplicationDelegateReplySuccess];
376     // NSApplicationDelegateReplySuccess = 0,
377     // NSApplicationDelegateReplyCancel = 1,
378     // NSApplicationDelegateReplyFailure = 2
381 - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender
383     return [[NSUserDefaults standardUserDefaults]
384             boolForKey:MMTerminateAfterLastWindowClosedKey];
387 - (NSApplicationTerminateReply)applicationShouldTerminate:
388     (NSApplication *)sender
390     // TODO: Follow Apple's guidelines for 'Graceful Application Termination'
391     // (in particular, allow user to review changes and save).
392     int reply = NSTerminateNow;
393     BOOL modifiedBuffers = NO;
395     // Go through windows, checking for modified buffers.  (Each Vim process
396     // tells MacVim when any buffer has been modified and MacVim sets the
397     // 'documentEdited' flag of the window correspondingly.)
398     NSEnumerator *e = [[NSApp windows] objectEnumerator];
399     id window;
400     while ((window = [e nextObject])) {
401         if ([window isDocumentEdited]) {
402             modifiedBuffers = YES;
403             break;
404         }
405     }
407     if (modifiedBuffers) {
408         NSAlert *alert = [[NSAlert alloc] init];
409         [alert setAlertStyle:NSWarningAlertStyle];
410         [alert addButtonWithTitle:NSLocalizedString(@"Quit",
411                 @"Dialog button")];
412         [alert addButtonWithTitle:NSLocalizedString(@"Cancel",
413                 @"Dialog button")];
414         [alert setMessageText:NSLocalizedString(@"Quit without saving?",
415                 @"Quit dialog with changed buffers, title")];
416         [alert setInformativeText:NSLocalizedString(
417                 @"There are modified buffers, "
418                 "if you quit now all changes will be lost.  Quit anyway?",
419                 @"Quit dialog with changed buffers, text")];
421         if ([alert runModal] != NSAlertFirstButtonReturn)
422             reply = NSTerminateCancel;
424         [alert release];
425     } else {
426         // No unmodified buffers, but give a warning if there are multiple
427         // windows and/or tabs open.
428         int numWindows = [vimControllers count];
429         int numTabs = 0;
431         // Count the number of open tabs
432         e = [vimControllers objectEnumerator];
433         id vc;
434         while ((vc = [e nextObject])) {
435             NSString *eval = [vc evaluateVimExpression:@"tabpagenr('$')"];
436             if (eval) {
437                 int count = [eval intValue];
438                 if (count > 0 && count < INT_MAX)
439                     numTabs += count;
440             }
441         }
443         if (numWindows > 1 || numTabs > 1) {
444             NSAlert *alert = [[NSAlert alloc] init];
445             [alert setAlertStyle:NSWarningAlertStyle];
446             [alert addButtonWithTitle:NSLocalizedString(@"Quit",
447                     @"Dialog button")];
448             [alert addButtonWithTitle:NSLocalizedString(@"Cancel",
449                     @"Dialog button")];
450             [alert setMessageText:NSLocalizedString(
451                     @"Are you sure you want to quit MacVim?",
452                     @"Quit dialog with no changed buffers, title")];
454             NSString *info = nil;
455             if (numWindows > 1) {
456                 if (numTabs > numWindows)
457                     info = [NSString stringWithFormat:NSLocalizedString(
458                             @"There are %d windows open in MacVim, with a "
459                             "total of %d tabs. Do you want to quit anyway?",
460                             @"Quit dialog with no changed buffers, text"),
461                          numWindows, numTabs];
462                 else
463                     info = [NSString stringWithFormat:NSLocalizedString(
464                             @"There are %d windows open in MacVim. "
465                             "Do you want to quit anyway?",
466                             @"Quit dialog with no changed buffers, text"),
467                         numWindows];
469             } else {
470                 info = [NSString stringWithFormat:NSLocalizedString(
471                         @"There are %d tabs open in MacVim. "
472                         "Do you want to quit anyway?",
473                         @"Quit dialog with no changed buffers, text"), 
474                      numTabs];
475             }
477             [alert setInformativeText:info];
479             if ([alert runModal] != NSAlertFirstButtonReturn)
480                 reply = NSTerminateCancel;
482             [alert release];
483         }
484     }
487     // Tell all Vim processes to terminate now (otherwise they'll leave swap
488     // files behind).
489     if (NSTerminateNow == reply) {
490         e = [vimControllers objectEnumerator];
491         id vc;
492         while ((vc = [e nextObject]))
493             [vc sendMessage:TerminateNowMsgID data:nil];
495         // Give Vim processes a chance to terminate before MacVim.  If they
496         // haven't terminated by the time applicationWillTerminate: is sent,
497         // they may be forced to quit (see below).
498         usleep(MMTerminationSleepPeriod);
499     }
501     return reply;
504 - (void)applicationWillTerminate:(NSNotification *)notification
506 #if MM_HANDLE_XCODE_MOD_EVENT
507     [[NSAppleEventManager sharedAppleEventManager]
508             removeEventHandlerForEventClass:'KAHL'
509                                  andEventID:'MOD '];
510 #endif
512     // This will invalidate all connections (since they were spawned from this
513     // connection).
514     [connection invalidate];
516     // Send a SIGINT to all running Vim processes, so that they are sure to
517     // receive the connectionDidDie: notification (a process has to be checking
518     // the run-loop for this to happen).
519     unsigned i, count = [vimControllers count];
520     for (i = 0; i < count; ++i) {
521         MMVimController *controller = [vimControllers objectAtIndex:i];
522         int pid = [controller pid];
523         if (pid > 0)
524             kill(pid, SIGINT);
525     }
527     if (fontContainerRef) {
528         ATSFontDeactivate(fontContainerRef, NULL, kATSOptionFlagsDefault);
529         fontContainerRef = 0;
530     }
532     [NSApp setDelegate:nil];
535 + (MMAppController *)sharedInstance
537     // Note: The app controller is a singleton which is instantiated in
538     // MainMenu.nib where it is also connected as the delegate of NSApp.
539     id delegate = [NSApp delegate];
540     return [delegate isKindOfClass:self] ? (MMAppController*)delegate : nil;
543 - (NSMenu *)defaultMainMenu
545     return defaultMainMenu;
548 - (NSMenuItem *)appMenuItemTemplate
550     return appMenuItemTemplate;
553 - (void)removeVimController:(id)controller
555     //NSLog(@"%s%@", _cmd, controller);
557     [controller cleanup];
558     [[controller windowController] close];
560     [vimControllers removeObject:controller];
562     if (![vimControllers count]) {
563         // The last editor window just closed so restore the main menu back to
564         // its default state (which is defined in MainMenu.nib).
565         [self setMainMenu:defaultMainMenu];
566     }
569 - (void)windowControllerWillOpen:(MMWindowController *)windowController
571     NSPoint topLeft = NSZeroPoint;
572     NSWindow *topWin = [[[self topmostVimController] windowController] window];
573     NSWindow *win = [windowController window];
575     if (!win) return;
577     // If there is a window belonging to a Vim process, cascade from it,
578     // otherwise use the autosaved window position (if any).
579     if (topWin) {
580         NSRect frame = [topWin frame];
581         topLeft = NSMakePoint(frame.origin.x, NSMaxY(frame));
582     } else {
583         NSString *topLeftString = [[NSUserDefaults standardUserDefaults]
584             stringForKey:MMTopLeftPointKey];
585         if (topLeftString)
586             topLeft = NSPointFromString(topLeftString);
587     }
589     if (!NSEqualPoints(topLeft, NSZeroPoint)) {
590         if (topWin)
591             topLeft = [win cascadeTopLeftFromPoint:topLeft];
593         [win setFrameTopLeftPoint:topLeft];
594     }
596     if (openSelectionString) {
597         // TODO: Pass this as a parameter instead!  Get rid of
598         // 'openSelectionString' etc.
599         //
600         // There is some text to paste into this window as a result of the
601         // services menu "Open selection ..." being used.
602         [[windowController vimController] dropString:openSelectionString];
603         [openSelectionString release];
604         openSelectionString = nil;
605     }
608 - (void)setMainMenu:(NSMenu *)mainMenu
610     if ([NSApp mainMenu] == mainMenu) return;
612     // If the new menu has a "Recent Files" dummy item, then swap the real item
613     // for the dummy.  We are forced to do this since Cocoa initializes the
614     // "Recent Files" menu and there is no way to simply point Cocoa to a new
615     // item each time the menus are swapped.
616     NSMenu *fileMenu = [mainMenu findFileMenu];
617     if (recentFilesMenuItem && fileMenu) {
618         int dummyIdx =
619                 [fileMenu indexOfItemWithAction:@selector(recentFilesDummy:)];
620         if (dummyIdx >= 0) {
621             NSMenuItem *dummyItem = [[fileMenu itemAtIndex:dummyIdx] retain];
622             [fileMenu removeItemAtIndex:dummyIdx];
624             NSMenu *recentFilesParentMenu = [recentFilesMenuItem menu];
625             int idx = [recentFilesParentMenu indexOfItem:recentFilesMenuItem];
626             if (idx >= 0) {
627                 [[recentFilesMenuItem retain] autorelease];
628                 [recentFilesParentMenu removeItemAtIndex:idx];
629                 [recentFilesParentMenu insertItem:dummyItem atIndex:idx];
630             }
632             [fileMenu insertItem:recentFilesMenuItem atIndex:dummyIdx];
633             [dummyItem release];
634         }
635     }
637     // Now set the new menu.  Notice that we keep one menu for each editor
638     // window since each editor can have its own set of menus.  When swapping
639     // menus we have to tell Cocoa where the new "MacVim", "Windows", and
640     // "Services" menu are.
641     [NSApp setMainMenu:mainMenu];
643     // Setting the "MacVim" (or "Application") menu ensures that it is typeset
644     // in boldface.  (The setAppleMenu: method used to be public but is now
645     // private so this will have to be considered a bit of a hack!)
646     NSMenu *appMenu = [mainMenu findApplicationMenu];
647     [NSApp performSelector:@selector(setAppleMenu:) withObject:appMenu];
649     NSMenu *servicesMenu = [mainMenu findServicesMenu];
650     [NSApp setServicesMenu:servicesMenu];
652     NSMenu *windowsMenu = [mainMenu findWindowsMenu];
653     if (windowsMenu) {
654         // Cocoa isn't clever enough to get rid of items it has added to the
655         // "Windows" menu so we have to do it ourselves otherwise there will be
656         // multiple menu items for each window in the "Windows" menu.
657         //   This code assumes that the only items Cocoa add are ones which
658         // send off the action makeKeyAndOrderFront:.  (Cocoa will not add
659         // another separator item if the last item on the "Windows" menu
660         // already is a separator, so we needen't worry about separators.)
661         int i, count = [windowsMenu numberOfItems];
662         for (i = count-1; i >= 0; --i) {
663             NSMenuItem *item = [windowsMenu itemAtIndex:i];
664             if ([item action] == @selector(makeKeyAndOrderFront:))
665                 [windowsMenu removeItem:item];
666         }
667     }
668     [NSApp setWindowsMenu:windowsMenu];
671 - (IBAction)newWindow:(id)sender
673     [self launchVimProcessWithArguments:nil];
676 - (IBAction)fileOpen:(id)sender
678     NSString *dir = nil;
679     BOOL trackPwd = [[NSUserDefaults standardUserDefaults]
680             boolForKey:MMDialogsTrackPwdKey];
681     if (trackPwd) {
682         MMVimController *vc = [self keyVimController];
683         if (vc) dir = [[vc vimState] objectForKey:@"pwd"];
684     }
686     NSOpenPanel *panel = [NSOpenPanel openPanel];
687     [panel setAllowsMultipleSelection:YES];
688     [panel setAccessoryView:openPanelAccessoryView()];
690     int result = [panel runModalForDirectory:dir file:nil types:nil];
691     if (NSOKButton == result)
692         [self application:NSApp openFiles:[panel filenames]];
695 - (IBAction)selectNextWindow:(id)sender
697     unsigned i, count = [vimControllers count];
698     if (!count) return;
700     NSWindow *keyWindow = [NSApp keyWindow];
701     for (i = 0; i < count; ++i) {
702         MMVimController *vc = [vimControllers objectAtIndex:i];
703         if ([[[vc windowController] window] isEqual:keyWindow])
704             break;
705     }
707     if (i < count) {
708         if (++i >= count)
709             i = 0;
710         MMVimController *vc = [vimControllers objectAtIndex:i];
711         [[vc windowController] showWindow:self];
712     }
715 - (IBAction)selectPreviousWindow:(id)sender
717     unsigned i, count = [vimControllers count];
718     if (!count) return;
720     NSWindow *keyWindow = [NSApp keyWindow];
721     for (i = 0; i < count; ++i) {
722         MMVimController *vc = [vimControllers objectAtIndex:i];
723         if ([[[vc windowController] window] isEqual:keyWindow])
724             break;
725     }
727     if (i < count) {
728         if (i > 0) {
729             --i;
730         } else {
731             i = count - 1;
732         }
733         MMVimController *vc = [vimControllers objectAtIndex:i];
734         [[vc windowController] showWindow:self];
735     }
738 - (IBAction)orderFrontPreferencePanel:(id)sender
740     [[MMPreferenceController sharedPrefsWindowController] showWindow:self];
743 - (IBAction)openWebsite:(id)sender
745     [[NSWorkspace sharedWorkspace] openURL:
746             [NSURL URLWithString:MMWebsiteString]];
749 - (IBAction)showVimHelp:(id)sender
751     // Open a new window with the help window maximized.
752     [self launchVimProcessWithArguments:[NSArray arrayWithObjects:
753             @"-c", @":h gui_mac", @"-c", @":res", nil]];
756 - (IBAction)zoomAll:(id)sender
758     [NSApp makeWindowsPerform:@selector(performZoom:) inOrder:YES];
761 - (byref id <MMFrontendProtocol>)
762     connectBackend:(byref in id <MMBackendProtocol>)backend
763                pid:(int)pid
765     //NSLog(@"Connect backend (pid=%d)", pid);
766     NSNumber *pidKey = [NSNumber numberWithInt:pid];
767     MMVimController *vc = nil;
769     @try {
770         [(NSDistantObject*)backend
771                 setProtocolForProxy:@protocol(MMBackendProtocol)];
773         vc = [[[MMVimController alloc]
774             initWithBackend:backend pid:pid]
775             autorelease];
777         if (![vimControllers count]) {
778             // The first window autosaves its position.  (The autosaving
779             // features of Cocoa are not used because we need more control over
780             // what is autosaved and when it is restored.)
781             [[vc windowController] setWindowAutosaveKey:MMTopLeftPointKey];
782         }
784         [vimControllers addObject:vc];
786         id args = [pidArguments objectForKey:pidKey];
787         if (args && [NSNull null] != args)
788             [self passArguments:args toVimController:vc];
790         // HACK!  MacVim does not get activated if it is launched from the
791         // terminal, so we forcibly activate here unless it is an untitled
792         // window opening.  Untitled windows are treated differently, else
793         // MacVim would steal the focus if another app was activated while the
794         // untitled window was loading.
795         if (!args || args != [NSNull null])
796             [NSApp activateIgnoringOtherApps:YES];
798         if (args)
799             [pidArguments removeObjectForKey:pidKey];
801         return vc;
802     }
804     @catch (NSException *e) {
805         NSLog(@"Exception caught in %s: \"%@\"", _cmd, e);
807         if (vc)
808             [vimControllers removeObject:vc];
810         [pidArguments removeObjectForKey:pidKey];
811     }
813     return nil;
816 - (NSArray *)serverList
818     NSMutableArray *array = [NSMutableArray array];
820     unsigned i, count = [vimControllers count];
821     for (i = 0; i < count; ++i) {
822         MMVimController *controller = [vimControllers objectAtIndex:i];
823         if ([controller serverName])
824             [array addObject:[controller serverName]];
825     }
827     return array;
830 @end // MMAppController
835 @implementation MMAppController (MMServices)
837 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
838                 error:(NSString **)error
840     if (![[pboard types] containsObject:NSStringPboardType]) {
841         NSLog(@"WARNING: Pasteboard contains no object of type "
842                 "NSStringPboardType");
843         return;
844     }
846     MMVimController *vc = [self topmostVimController];
847     if (vc) {
848         // Open a new tab first, since dropString: does not do this.
849         [vc sendMessage:AddNewTabMsgID data:nil];
850         [vc dropString:[pboard stringForType:NSStringPboardType]];
851     } else {
852         // NOTE: There is no window to paste the selection into, so save the
853         // text, open a new window, and paste the text when the next window
854         // opens.  (If this is called several times in a row, then all but the
855         // last call might be ignored.)
856         if (openSelectionString) [openSelectionString release];
857         openSelectionString = [[pboard stringForType:NSStringPboardType] copy];
859         [self newWindow:self];
860     }
863 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
864            error:(NSString **)error
866     if (![[pboard types] containsObject:NSStringPboardType]) {
867         NSLog(@"WARNING: Pasteboard contains no object of type "
868                 "NSStringPboardType");
869         return;
870     }
872     // TODO: Parse multiple filenames and create array with names.
873     NSString *string = [pboard stringForType:NSStringPboardType];
874     string = [string stringByTrimmingCharactersInSet:
875             [NSCharacterSet whitespaceAndNewlineCharacterSet]];
876     string = [string stringByStandardizingPath];
878     NSArray *filenames = [self filterFilesAndNotify:
879             [NSArray arrayWithObject:string]];
880     if ([filenames count] > 0) {
881         MMVimController *vc = nil;
882         if (userData && [userData isEqual:@"Tab"])
883             vc = [self topmostVimController];
885         if (vc) {
886             [vc dropFiles:filenames forceOpen:YES];
887         } else {
888             [self application:NSApp openFiles:filenames];
889         }
890     }
893 @end // MMAppController (MMServices)
898 @implementation MMAppController (Private)
900 - (MMVimController *)keyVimController
902     NSWindow *keyWindow = [NSApp keyWindow];
903     if (keyWindow) {
904         unsigned i, count = [vimControllers count];
905         for (i = 0; i < count; ++i) {
906             MMVimController *vc = [vimControllers objectAtIndex:i];
907             if ([[[vc windowController] window] isEqual:keyWindow])
908                 return vc;
909         }
910     }
912     return nil;
915 - (MMVimController *)topmostVimController
917     // Find the topmost visible window which has an associated vim controller.
918     NSEnumerator *e = [[NSApp orderedWindows] objectEnumerator];
919     id window;
920     while ((window = [e nextObject]) && [window isVisible]) {
921         unsigned i, count = [vimControllers count];
922         for (i = 0; i < count; ++i) {
923             MMVimController *vc = [vimControllers objectAtIndex:i];
924             if ([[[vc windowController] window] isEqual:window])
925                 return vc;
926         }
927     }
929     return nil;
932 - (int)launchVimProcessWithArguments:(NSArray *)args
934     int pid = -1;
935     NSString *path = [[NSBundle mainBundle] pathForAuxiliaryExecutable:@"Vim"];
937     if (!path) {
938         NSLog(@"ERROR: Vim executable could not be found inside app bundle!");
939         return -1;
940     }
942     NSArray *taskArgs = [NSArray arrayWithObjects:@"-g", @"-f", nil];
943     if (args)
944         taskArgs = [taskArgs arrayByAddingObjectsFromArray:args];
946     BOOL useLoginShell = [[NSUserDefaults standardUserDefaults]
947             boolForKey:MMLoginShellKey];
948     if (useLoginShell) {
949         // Run process with a login shell, roughly:
950         //   echo "exec Vim -g -f args" | ARGV0=-`basename $SHELL` $SHELL [-l]
951         pid = executeInLoginShell(path, taskArgs);
952     } else {
953         // Run process directly:
954         //   Vim -g -f args
955         NSTask *task = [NSTask launchedTaskWithLaunchPath:path
956                                                 arguments:taskArgs];
957         pid = task ? [task processIdentifier] : -1;
958     }
960     if (-1 != pid) {
961         // NOTE: If the process has no arguments, then add a null argument to
962         // the pidArguments dictionary.  This is later used to detect that a
963         // process without arguments is being launched.
964         if (!args)
965             [pidArguments setObject:[NSNull null]
966                              forKey:[NSNumber numberWithInt:pid]];
967     } else {
968         NSLog(@"WARNING: %s%@ failed (useLoginShell=%d)", _cmd, args,
969                 useLoginShell);
970     }
972     return pid;
975 - (NSArray *)filterFilesAndNotify:(NSArray *)filenames
977     // Go trough 'filenames' array and make sure each file exists.  Present
978     // warning dialog if some file was missing.
980     NSString *firstMissingFile = nil;
981     NSMutableArray *files = [NSMutableArray array];
982     unsigned i, count = [filenames count];
984     for (i = 0; i < count; ++i) {
985         NSString *name = [filenames objectAtIndex:i];
986         if ([[NSFileManager defaultManager] fileExistsAtPath:name]) {
987             [files addObject:name];
988         } else if (!firstMissingFile) {
989             firstMissingFile = name;
990         }
991     }
993     if (firstMissingFile) {
994         NSAlert *alert = [[NSAlert alloc] init];
995         [alert addButtonWithTitle:NSLocalizedString(@"OK",
996                 @"Dialog button")];
998         NSString *text;
999         if ([files count] >= count-1) {
1000             [alert setMessageText:NSLocalizedString(@"File not found",
1001                     @"File not found dialog, title")];
1002             text = [NSString stringWithFormat:NSLocalizedString(
1003                     @"Could not open file with name %@.",
1004                     @"File not found dialog, text"), firstMissingFile];
1005         } else {
1006             [alert setMessageText:NSLocalizedString(@"Multiple files not found",
1007                     @"File not found dialog, title")];
1008             text = [NSString stringWithFormat:NSLocalizedString(
1009                     @"Could not open file with name %@, and %d other files.",
1010                     @"File not found dialog, text"),
1011                 firstMissingFile, count-[files count]-1];
1012         }
1014         [alert setInformativeText:text];
1015         [alert setAlertStyle:NSWarningAlertStyle];
1017         [alert runModal];
1018         [alert release];
1020         [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
1021     }
1023     return files;
1026 - (NSArray *)filterOpenFiles:(NSArray *)filenames
1027                    arguments:(NSDictionary *)args
1029     // Check if any of the files in the 'filenames' array are open in any Vim
1030     // process.  Remove the files that are open from the 'filenames' array and
1031     // return it.  If all files were filtered out, then raise the first file in
1032     // the Vim process it is open.  Files that are filtered are sent an odb
1033     // open event in case theID is not zero.
1035     NSMutableDictionary *localArgs =
1036             [NSMutableDictionary dictionaryWithDictionary:args];
1037     MMVimController *raiseController = nil;
1038     NSString *raiseFile = nil;
1039     NSMutableArray *files = [filenames mutableCopy];
1040     NSString *expr = [NSString stringWithFormat:
1041             @"map([\"%@\"],\"bufloaded(v:val)\")",
1042             [files componentsJoinedByString:@"\",\""]];
1043     unsigned i, count = [vimControllers count];
1045     // Ensure that the files aren't opened when passing arguments.
1046     [localArgs setObject:[NSNumber numberWithBool:NO] forKey:@"openFiles"];
1048     for (i = 0; i < count && [files count]; ++i) {
1049         MMVimController *controller = [vimControllers objectAtIndex:i];
1051         // Query Vim for which files in the 'files' array are open.
1052         NSString *eval = [controller evaluateVimExpression:expr];
1053         if (!eval) continue;
1055         NSIndexSet *idxSet = [NSIndexSet indexSetWithVimList:eval];
1056         if ([idxSet count]) {
1057             if (!raiseFile) {
1058                 // Remember the file and which Vim that has it open so that
1059                 // we can raise it later on.
1060                 raiseController = controller;
1061                 raiseFile = [files objectAtIndex:[idxSet firstIndex]];
1062                 [[raiseFile retain] autorelease];
1063             }
1065             // Pass (ODB/Xcode/Spotlight) arguments to this process.
1066             [localArgs setObject:[files objectsAtIndexes:idxSet]
1067                           forKey:@"filenames"];
1068             [self passArguments:localArgs toVimController:controller];
1070             // Remove all the files that were open in this Vim process and
1071             // create a new expression to evaluate.
1072             [files removeObjectsAtIndexes:idxSet];
1073             expr = [NSString stringWithFormat:
1074                     @"map([\"%@\"],\"bufloaded(v:val)\")",
1075                     [files componentsJoinedByString:@"\",\""]];
1076         }
1077     }
1079     if (![files count] && raiseFile) {
1080         // Raise the window containing the first file that was already open,
1081         // and make sure that the tab containing that file is selected.  Only
1082         // do this if there are no more files to open, otherwise sometimes the
1083         // window with 'raiseFile' will be raised, other times it might be the
1084         // window that will open with the files in the 'files' array.
1085         raiseFile = [raiseFile stringByEscapingSpecialFilenameCharacters];
1086         NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>"
1087             ":let oldswb=&swb|let &swb=\"useopen,usetab\"|"
1088             "tab sb %@|let &swb=oldswb|unl oldswb|"
1089             "cal foreground()|redr|f<CR>", raiseFile];
1091         [raiseController addVimInput:input];
1092     }
1094     return files;
1097 #if MM_HANDLE_XCODE_MOD_EVENT
1098 - (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
1099                  replyEvent:(NSAppleEventDescriptor *)reply
1101 #if 0
1102     // Xcode sends this event to query MacVim which open files have been
1103     // modified.
1104     NSLog(@"reply:%@", reply);
1105     NSLog(@"event:%@", event);
1107     NSEnumerator *e = [vimControllers objectEnumerator];
1108     id vc;
1109     while ((vc = [e nextObject])) {
1110         DescType type = [reply descriptorType];
1111         unsigned len = [[type data] length];
1112         NSMutableData *data = [NSMutableData data];
1114         [data appendBytes:&type length:sizeof(DescType)];
1115         [data appendBytes:&len length:sizeof(unsigned)];
1116         [data appendBytes:[reply data] length:len];
1118         [vc sendMessage:XcodeModMsgID data:data];
1119     }
1120 #endif
1122 #endif
1124 - (int)findLaunchingProcessWithoutArguments
1126     NSArray *keys = [pidArguments allKeysForObject:[NSNull null]];
1127     if ([keys count] > 0) {
1128         //NSLog(@"found launching process without arguments");
1129         return [[keys objectAtIndex:0] intValue];
1130     }
1132     return 0;
1135 - (MMVimController *)findUntitledWindow
1137     NSEnumerator *e = [vimControllers objectEnumerator];
1138     id vc;
1139     while ((vc = [e nextObject])) {
1140         // TODO: This is a moronic test...should query the Vim process if there
1141         // are any open buffers or something like that instead.
1142         NSString *title = [[[vc windowController] window] title];
1144         // TODO: this will not work in a localized MacVim
1145         if ([title hasPrefix:@"[No Name] - VIM"]) {
1146             //NSLog(@"found untitled window");
1147             return vc;
1148         }
1149     }
1151     return nil;
1154 - (NSMutableDictionary *)extractArgumentsFromOdocEvent:
1155     (NSAppleEventDescriptor *)desc
1157     NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1159     // 1. Extract ODB parameters (if any)
1160     NSAppleEventDescriptor *odbdesc = desc;
1161     if (![odbdesc paramDescriptorForKeyword:keyFileSender]) {
1162         // The ODB paramaters may hide inside the 'keyAEPropData' descriptor.
1163         odbdesc = [odbdesc paramDescriptorForKeyword:keyAEPropData];
1164         if (![odbdesc paramDescriptorForKeyword:keyFileSender])
1165             odbdesc = nil;
1166     }
1168     if (odbdesc) {
1169         NSAppleEventDescriptor *p =
1170                 [odbdesc paramDescriptorForKeyword:keyFileSender];
1171         if (p)
1172             [dict setObject:[NSNumber numberWithUnsignedInt:[p typeCodeValue]]
1173                      forKey:@"remoteID"];
1175         p = [odbdesc paramDescriptorForKeyword:keyFileCustomPath];
1176         if (p)
1177             [dict setObject:[p stringValue] forKey:@"remotePath"];
1179         p = [odbdesc paramDescriptorForKeyword:keyFileSenderToken];
1180         if (p)
1181             [dict setObject:p forKey:@"remotePath"];
1182     }
1184     // 2. Extract Xcode parameters (if any)
1185     NSAppleEventDescriptor *xcodedesc =
1186             [desc paramDescriptorForKeyword:keyAEPosition];
1187     if (xcodedesc) {
1188         NSRange range;
1189         MMSelectionRange *sr = (MMSelectionRange*)[[xcodedesc data] bytes];
1191         if (sr->lineNum < 0) {
1192             // Should select a range of lines.
1193             range.location = sr->startRange + 1;
1194             range.length = sr->endRange - sr->startRange + 1;
1195         } else {
1196             // Should only move cursor to a line.
1197             range.location = sr->lineNum + 1;
1198             range.length = 0;
1199         }
1201         [dict setObject:NSStringFromRange(range) forKey:@"selectionRange"];
1202     }
1204     // 3. Extract Spotlight search text (if any)
1205     NSAppleEventDescriptor *spotlightdesc = 
1206             [desc paramDescriptorForKeyword:keyAESearchText];
1207     if (spotlightdesc)
1208         [dict setObject:[spotlightdesc stringValue] forKey:@"searchText"];
1210     return dict;
1213 - (void)passArguments:(NSDictionary *)args toVimController:(MMVimController*)vc
1215     if (!args) return;
1217     // Pass filenames to open if required (the 'openFiles' argument can be used
1218     // to disallow opening of the files).
1219     NSArray *filenames = [args objectForKey:@"filenames"];
1220     if (filenames && [[args objectForKey:@"openFiles"] boolValue]) {
1221         NSString *tabDrop = buildTabDropCommand(filenames);
1222         [vc addVimInput:tabDrop];
1223     }
1225     // Pass ODB data
1226     if (filenames && [args objectForKey:@"remoteID"]) {
1227         [vc odbEdit:filenames
1228              server:[[args objectForKey:@"remoteID"] unsignedIntValue]
1229                path:[args objectForKey:@"remotePath"]
1230               token:[args objectForKey:@"remoteToken"]];
1231     }
1233     // Pass range of lines to select
1234     if ([args objectForKey:@"selectionRange"]) {
1235         NSRange selectionRange = NSRangeFromString(
1236                 [args objectForKey:@"selectionRange"]);
1237         [vc addVimInput:buildSelectRangeCommand(selectionRange)];
1238     }
1240     // Pass search text
1241     NSString *searchText = [args objectForKey:@"searchText"];
1242     if (searchText)
1243         [vc addVimInput:buildSearchTextCommand(searchText)];
1246 @end // MMAppController (Private)
1251     static int
1252 executeInLoginShell(NSString *path, NSArray *args)
1254     // Start a login shell and execute the command 'path' with arguments 'args'
1255     // in the shell.  This ensures that user environment variables are set even
1256     // when MacVim was started from the Finder.
1258     int pid = -1;
1259     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1261     // Determine which shell to use to execute the command.  The user
1262     // may decide which shell to use by setting a user default or the
1263     // $SHELL environment variable.
1264     NSString *shell = [ud stringForKey:MMLoginShellCommandKey];
1265     if (!shell || [shell length] == 0)
1266         shell = [[[NSProcessInfo processInfo] environment]
1267             objectForKey:@"SHELL"];
1268     if (!shell)
1269         shell = @"/bin/bash";
1271     //NSLog(@"shell = %@", shell);
1273     // Bash needs the '-l' flag to launch a login shell.  The user may add
1274     // flags by setting a user default.
1275     NSString *shellArgument = [ud stringForKey:MMLoginShellArgumentKey];
1276     if (!shellArgument || [shellArgument length] == 0) {
1277         if ([[shell lastPathComponent] isEqual:@"bash"])
1278             shellArgument = @"-l";
1279         else
1280             shellArgument = nil;
1281     }
1283     //NSLog(@"shellArgument = %@", shellArgument);
1285     // Build input string to pipe to the login shell.
1286     NSMutableString *input = [NSMutableString stringWithFormat:
1287             @"exec \"%@\"", path];
1288     if (args) {
1289         // Append all arguments, making sure they are properly quoted, even
1290         // when they contain single quotes.
1291         NSEnumerator *e = [args objectEnumerator];
1292         id obj;
1294         while ((obj = [e nextObject])) {
1295             NSMutableString *arg = [NSMutableString stringWithString:obj];
1296             [arg replaceOccurrencesOfString:@"'" withString:@"'\"'\"'"
1297                                     options:NSLiteralSearch
1298                                       range:NSMakeRange(0, [arg length])];
1299             [input appendFormat:@" '%@'", arg];
1300         }
1301     }
1303     // Build the argument vector used to start the login shell.
1304     NSString *shellArg0 = [NSString stringWithFormat:@"-%@",
1305              [shell lastPathComponent]];
1306     char *shellArgv[3] = { (char *)[shellArg0 UTF8String], NULL, NULL };
1307     if (shellArgument)
1308         shellArgv[1] = (char *)[shellArgument UTF8String];
1310     // Get the C string representation of the shell path before the fork since
1311     // we must not call Foundation functions after a fork.
1312     const char *shellPath = [shell fileSystemRepresentation];
1314     // Fork and execute the process.
1315     int ds[2];
1316     if (pipe(ds)) return -1;
1318     pid = fork();
1319     if (pid == -1) {
1320         return -1;
1321     } else if (pid == 0) {
1322         // Child process
1323         if (close(ds[1]) == -1) exit(255);
1324         if (dup2(ds[0], 0) == -1) exit(255);
1326         execv(shellPath, shellArgv);
1328         // Never reached unless execv fails
1329         exit(255);
1330     } else {
1331         // Parent process
1332         if (close(ds[0]) == -1) return -1;
1334         // Send input to execute to the child process
1335         [input appendString:@"\n"];
1336         int bytes = [input lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
1338         if (write(ds[1], [input UTF8String], bytes) != bytes) return -1;
1339         if (close(ds[1]) == -1) return -1;
1340     }
1342     return pid;