Ignore "Recent Files" when no "File" menu is found
[MacVim.git] / src / MacVim / MMAppController.m
blob88b8f79bc1ce818474efaebc6a0742e8e0a614ee
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 "MMVimController.h"
42 #import "MMWindowController.h"
43 #import "MMPreferenceController.h"
44 #import <unistd.h>
47 #define MM_HANDLE_XCODE_MOD_EVENT 0
51 // Default timeout intervals on all connections.
52 static NSTimeInterval MMRequestTimeout = 5;
53 static NSTimeInterval MMReplyTimeout = 5;
55 static NSString *MMWebsiteString = @"http://code.google.com/p/macvim/";
58 #pragma options align=mac68k
59 typedef struct
61     short unused1;      // 0 (not used)
62     short lineNum;      // line to select (< 0 to specify range)
63     long  startRange;   // start of selection range (if line < 0)
64     long  endRange;     // end of selection range (if line < 0)
65     long  unused2;      // 0 (not used)
66     long  theDate;      // modification date/time
67 } MMSelectionRange;
68 #pragma options align=reset
71 static int executeInLoginShell(NSString *path, NSArray *args);
74 @interface MMAppController (MMServices)
75 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
76                 error:(NSString **)error;
77 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
78            error:(NSString **)error;
79 @end
82 @interface MMAppController (Private)
83 - (MMVimController *)keyVimController;
84 - (MMVimController *)topmostVimController;
85 - (int)launchVimProcessWithArguments:(NSArray *)args;
86 - (NSArray *)filterFilesAndNotify:(NSArray *)files;
87 - (NSArray *)filterOpenFiles:(NSArray *)filenames
88                    arguments:(NSDictionary *)args;
89 #if MM_HANDLE_XCODE_MOD_EVENT
90 - (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
91                  replyEvent:(NSAppleEventDescriptor *)reply;
92 #endif
93 - (int)findLaunchingProcessWithoutArguments;
94 - (MMVimController *)findUntitledWindow;
95 - (NSMutableDictionary *)extractArgumentsFromOdocEvent:
96     (NSAppleEventDescriptor *)desc;
97 - (void)passArguments:(NSDictionary *)args toVimController:(MMVimController*)vc;
98 @end
101 @interface NSNumber (MMExtras)
102 - (int)tag;
103 @end
106 @interface NSMenu (MMExtras)
107 - (int)indexOfItemWithAction:(SEL)action;
108 - (NSMenuItem *)itemWithAction:(SEL)action;
109 - (NSMenu *)findMenuContainingItemWithAction:(SEL)action;
110 - (NSMenu *)findWindowsMenu;
111 - (NSMenu *)findApplicationMenu;
112 - (NSMenu *)findServicesMenu;
113 - (NSMenu *)findFileMenu;
114 @end
119 @implementation MMAppController
121 + (void)initialize
123     NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
124         [NSNumber numberWithBool:NO],   MMNoWindowKey,
125         [NSNumber numberWithInt:64],    MMTabMinWidthKey,
126         [NSNumber numberWithInt:6*64],  MMTabMaxWidthKey,
127         [NSNumber numberWithInt:132],   MMTabOptimumWidthKey,
128         [NSNumber numberWithInt:2],     MMTextInsetLeftKey,
129         [NSNumber numberWithInt:1],     MMTextInsetRightKey,
130         [NSNumber numberWithInt:1],     MMTextInsetTopKey,
131         [NSNumber numberWithInt:1],     MMTextInsetBottomKey,
132         [NSNumber numberWithBool:NO],   MMTerminateAfterLastWindowClosedKey,
133         @"MMTypesetter",                MMTypesetterKey,
134         [NSNumber numberWithFloat:1],   MMCellWidthMultiplierKey,
135         [NSNumber numberWithFloat:-1],  MMBaselineOffsetKey,
136         [NSNumber numberWithBool:YES],  MMTranslateCtrlClickKey,
137         [NSNumber numberWithBool:NO],   MMOpenFilesInTabsKey,
138         [NSNumber numberWithBool:NO],   MMNoFontSubstitutionKey,
139         [NSNumber numberWithBool:NO],   MMLoginShellKey,
140         [NSNumber numberWithBool:NO],   MMAtsuiRendererKey,
141         [NSNumber numberWithInt:MMUntitledWindowAlways],
142                                         MMUntitledWindowKey,
143         [NSNumber numberWithBool:NO],   MMTexturedWindowKey,
144         [NSNumber numberWithBool:NO],   MMZoomBothKey,
145         @"",                            MMLoginShellCommandKey,
146         @"",                            MMLoginShellArgumentKey,
147         [NSNumber numberWithBool:YES],  MMDialogsTrackPwdKey,
148         nil];
150     [[NSUserDefaults standardUserDefaults] registerDefaults:dict];
152     NSArray *types = [NSArray arrayWithObject:NSStringPboardType];
153     [NSApp registerServicesMenuSendTypes:types returnTypes:types];
156 - (id)init
158     if ((self = [super init])) {
159         fontContainerRef = loadFonts();
161         vimControllers = [NSMutableArray new];
162         pidArguments = [NSMutableDictionary new];
164         // NOTE: Do not use the default connection since the Logitech Control
165         // Center (LCC) input manager steals and this would cause MacVim to
166         // never open any windows.  (This is a bug in LCC but since they are
167         // unlikely to fix it, we graciously give them the default connection.)
168         connection = [[NSConnection alloc] initWithReceivePort:[NSPort port]
169                                                       sendPort:nil];
170         [connection setRootObject:self];
171         [connection setRequestTimeout:MMRequestTimeout];
172         [connection setReplyTimeout:MMReplyTimeout];
174         // NOTE: When the user is resizing the window the AppKit puts the run
175         // loop in event tracking mode.  Unless the connection listens to
176         // request in this mode, live resizing won't work.
177         [connection addRequestMode:NSEventTrackingRunLoopMode];
179         // NOTE!  If the name of the connection changes here it must also be
180         // updated in MMBackend.m.
181         NSString *name = [NSString stringWithFormat:@"%@-connection",
182                  [[NSBundle mainBundle] bundleIdentifier]];
183         //NSLog(@"Registering connection with name '%@'", name);
184         if (![connection registerName:name]) {
185             NSLog(@"FATAL ERROR: Failed to register connection with name '%@'",
186                     name);
187             [connection release];  connection = nil;
188         }
189     }
191     return self;
194 - (void)dealloc
196     //NSLog(@"MMAppController dealloc");
198     [connection release];  connection = nil;
199     [pidArguments release];  pidArguments = nil;
200     [vimControllers release];  vimControllers = nil;
201     [openSelectionString release];  openSelectionString = nil;
202     [recentFilesMenuItem release];  recentFilesMenuItem = nil;
203     [defaultMainMenu release];  defaultMainMenu = nil;
205     [super dealloc];
208 - (void)applicationWillFinishLaunching:(NSNotification *)notification
210     // Remember the default menu so that it can be restored if the user closes
211     // all editor windows.
212     defaultMainMenu = [[NSApp mainMenu] retain];
214     // Set up the "Open Recent" menu. See
215     //   http://lapcatsoftware.com/blog/2007/07/10/
216     //     working-without-a-nib-part-5-open-recent-menu/
217     // and
218     //   http://www.cocoabuilder.com/archive/message/cocoa/2007/8/15/187793
219     // for more information.
220     //
221     // The menu itself is created in MainMenu.nib but we still seem to have to
222     // hack around a bit to get it to work.  (This has to be done in
223     // applicationWillFinishLaunching at the latest, otherwise it doesn't
224     // work.)
225     NSMenu *fileMenu = [defaultMainMenu findFileMenu];
226     if (fileMenu) {
227         int idx = [fileMenu indexOfItemWithAction:@selector(fileOpen:)];
228         if (idx >= 0 && idx+1 < [fileMenu numberOfItems])
230         recentFilesMenuItem = [fileMenu itemWithTitle:@"Open Recent"];
231         [[recentFilesMenuItem submenu] performSelector:@selector(_setMenuName:)
232                                         withObject:@"NSRecentDocumentsMenu"];
234         // Note: The "Recent Files" menu must be moved around since there is no
235         // -[NSApp setRecentFilesMenu:] method.  We keep a reference to it to
236         // facilitate this move (see setMainMenu: below).
237         [recentFilesMenuItem retain];
238     }
240 #if MM_HANDLE_XCODE_MOD_EVENT
241     [[NSAppleEventManager sharedAppleEventManager]
242             setEventHandler:self
243                 andSelector:@selector(handleXcodeModEvent:replyEvent:)
244               forEventClass:'KAHL'
245                  andEventID:'MOD '];
246 #endif
249 - (void)applicationDidFinishLaunching:(NSNotification *)notification
251     [NSApp setServicesProvider:self];
254 - (BOOL)applicationShouldOpenUntitledFile:(NSApplication *)sender
256     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
257     NSAppleEventManager *aem = [NSAppleEventManager sharedAppleEventManager];
258     NSAppleEventDescriptor *desc = [aem currentAppleEvent];
260     // The user default MMUntitledWindow can be set to control whether an
261     // untitled window should open on 'Open' and 'Reopen' events.
262     int untitledWindowFlag = [ud integerForKey:MMUntitledWindowKey];
263     if ([desc eventID] == kAEOpenApplication
264             && (untitledWindowFlag & MMUntitledWindowOnOpen) == 0)
265         return NO;
266     else if ([desc eventID] == kAEReopenApplication
267             && (untitledWindowFlag & MMUntitledWindowOnReopen) == 0)
268         return NO;
270     // When a process is started from the command line, the 'Open' event will
271     // contain a parameter to surpress the opening of an untitled window.
272     desc = [desc paramDescriptorForKeyword:keyAEPropData];
273     desc = [desc paramDescriptorForKeyword:keyMMUntitledWindow];
274     if (desc && ![desc booleanValue])
275         return NO;
277     // Never open an untitled window if there is at least one open window or if
278     // there are processes that are currently launching.
279     if ([vimControllers count] > 0 || [pidArguments count] > 0)
280         return NO;
282     // NOTE!  This way it possible to start the app with the command-line
283     // argument '-nowindow yes' and no window will be opened by default.
284     return ![ud boolForKey:MMNoWindowKey];
287 - (BOOL)applicationOpenUntitledFile:(NSApplication *)sender
289     [self newWindow:self];
290     return YES;
293 - (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames
295     // Opening files works like this:
296     //  a) extract ODB/Xcode/Spotlight parameters from the current Apple event
297     //  b) filter out any already open files (see filterOpenFiles::)
298     //  c) open any remaining files
299     //
300     // A file is opened in an untitled window if there is one (it may be
301     // currently launching, or it may already be visible), otherwise a new
302     // window is opened.
303     //
304     // Each launching Vim process has a dictionary of arguments that are passed
305     // to the process when in checks in (via connectBackend:pid:).  The
306     // arguments for each launching process can be looked up by its PID (in the
307     // pidArguments dictionary).
309     NSMutableDictionary *arguments = [self extractArgumentsFromOdocEvent:
310             [[NSAppleEventManager sharedAppleEventManager] currentAppleEvent]];
312     // Filter out files that are already open
313     filenames = [self filterOpenFiles:filenames arguments:arguments];
315     // Open any files that remain
316     if ([filenames count]) {
317         MMVimController *vc;
318         BOOL openInTabs = [[NSUserDefaults standardUserDefaults]
319             boolForKey:MMOpenFilesInTabsKey];
321         [arguments setObject:filenames forKey:@"filenames"];
322         [arguments setObject:[NSNumber numberWithBool:YES] forKey:@"openFiles"];
324         // Add file names to "Recent Files" menu.
325         int i, count = [filenames count];
326         for (i = 0; i < count; ++i) {
327             // Don't add files that are being edited remotely (using ODB).
328             if ([arguments objectForKey:@"remoteID"]) continue;
330             [[NSDocumentController sharedDocumentController]
331                     noteNewRecentFilePath:[filenames objectAtIndex:i]];
332         }
334         if ((openInTabs && (vc = [self topmostVimController]))
335                || (vc = [self findUntitledWindow])) {
336             // Open files in an already open window.
337             [[[vc windowController] window] makeKeyAndOrderFront:self];
338             [self passArguments:arguments toVimController:vc];
339         } else {
340             // Open files in a launching Vim process or start a new process.
341             int pid = [self findLaunchingProcessWithoutArguments];
342             if (!pid) {
343                 // Pass the filenames to the process straight away.
344                 //
345                 // TODO: It would be nicer if all arguments were passed to the
346                 // Vim process in connectBackend::, but if we don't pass the
347                 // filename arguments here, the window 'flashes' once when it
348                 // opens.  This is due to the 'welcome' screen first being
349                 // displayed, then quickly thereafter the files are opened.
350                 NSArray *fileArgs = [NSArray arrayWithObject:@"-p"];
351                 fileArgs = [fileArgs arrayByAddingObjectsFromArray:filenames];
353                 pid = [self launchVimProcessWithArguments:fileArgs];
355                 if (-1 == pid) {
356                     // TODO: Notify user of failure?
357                     [NSApp replyToOpenOrPrint:
358                         NSApplicationDelegateReplyFailure];
359                     return;
360                 }
362                 // Make sure these files aren't opened again when
363                 // connectBackend:pid: is called.
364                 [arguments setObject:[NSNumber numberWithBool:NO]
365                               forKey:@"openFiles"];
366             }
368             // TODO: If the Vim process fails to start, or if it changes PID,
369             // then the memory allocated for these parameters will leak.
370             // Ensure that this cannot happen or somehow detect it.
372             if ([arguments count] > 0)
373                 [pidArguments setObject:arguments
374                                  forKey:[NSNumber numberWithInt:pid]];
375         }
376     }
378     [NSApp replyToOpenOrPrint:NSApplicationDelegateReplySuccess];
379     // NSApplicationDelegateReplySuccess = 0,
380     // NSApplicationDelegateReplyCancel = 1,
381     // NSApplicationDelegateReplyFailure = 2
384 - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender
386     return [[NSUserDefaults standardUserDefaults]
387             boolForKey:MMTerminateAfterLastWindowClosedKey];
390 - (NSApplicationTerminateReply)applicationShouldTerminate:
391     (NSApplication *)sender
393     // TODO: Follow Apple's guidelines for 'Graceful Application Termination'
394     // (in particular, allow user to review changes and save).
395     int reply = NSTerminateNow;
396     BOOL modifiedBuffers = NO;
398     // Go through windows, checking for modified buffers.  (Each Vim process
399     // tells MacVim when any buffer has been modified and MacVim sets the
400     // 'documentEdited' flag of the window correspondingly.)
401     NSEnumerator *e = [[NSApp windows] objectEnumerator];
402     id window;
403     while ((window = [e nextObject])) {
404         if ([window isDocumentEdited]) {
405             modifiedBuffers = YES;
406             break;
407         }
408     }
410     if (modifiedBuffers) {
411         NSAlert *alert = [[NSAlert alloc] init];
412         [alert setAlertStyle:NSWarningAlertStyle];
413         [alert addButtonWithTitle:NSLocalizedString(@"Quit",
414                 @"Dialog button")];
415         [alert addButtonWithTitle:NSLocalizedString(@"Cancel",
416                 @"Dialog button")];
417         [alert setMessageText:NSLocalizedString(@"Quit without saving?",
418                 @"Quit dialog with changed buffers, title")];
419         [alert setInformativeText:NSLocalizedString(
420                 @"There are modified buffers, "
421                 "if you quit now all changes will be lost.  Quit anyway?",
422                 @"Quit dialog with changed buffers, text")];
424         if ([alert runModal] != NSAlertFirstButtonReturn)
425             reply = NSTerminateCancel;
427         [alert release];
428     } else {
429         // No unmodified buffers, but give a warning if there are multiple
430         // windows and/or tabs open.
431         int numWindows = [vimControllers count];
432         int numTabs = 0;
434         // Count the number of open tabs
435         e = [vimControllers objectEnumerator];
436         id vc;
437         while ((vc = [e nextObject])) {
438             NSString *eval = [vc evaluateVimExpression:@"tabpagenr('$')"];
439             if (eval) {
440                 int count = [eval intValue];
441                 if (count > 0 && count < INT_MAX)
442                     numTabs += count;
443             }
444         }
446         if (numWindows > 1 || numTabs > 1) {
447             NSAlert *alert = [[NSAlert alloc] init];
448             [alert setAlertStyle:NSWarningAlertStyle];
449             [alert addButtonWithTitle:NSLocalizedString(@"Quit",
450                     @"Dialog button")];
451             [alert addButtonWithTitle:NSLocalizedString(@"Cancel",
452                     @"Dialog button")];
453             [alert setMessageText:NSLocalizedString(
454                     @"Are you sure you want to quit MacVim?",
455                     @"Quit dialog with no changed buffers, title")];
457             NSString *info = nil;
458             if (numWindows > 1) {
459                 if (numTabs > numWindows)
460                     info = [NSString stringWithFormat:NSLocalizedString(
461                             @"There are %d windows open in MacVim, with a "
462                             "total of %d tabs. Do you want to quit anyway?",
463                             @"Quit dialog with no changed buffers, text"),
464                          numWindows, numTabs];
465                 else
466                     info = [NSString stringWithFormat:NSLocalizedString(
467                             @"There are %d windows open in MacVim. "
468                             "Do you want to quit anyway?",
469                             @"Quit dialog with no changed buffers, text"),
470                         numWindows];
472             } else {
473                 info = [NSString stringWithFormat:NSLocalizedString(
474                         @"There are %d tabs open in MacVim. "
475                         "Do you want to quit anyway?",
476                         @"Quit dialog with no changed buffers, text"), 
477                      numTabs];
478             }
480             [alert setInformativeText:info];
482             if ([alert runModal] != NSAlertFirstButtonReturn)
483                 reply = NSTerminateCancel;
485             [alert release];
486         }
487     }
490     // Tell all Vim processes to terminate now (otherwise they'll leave swap
491     // files behind).
492     if (NSTerminateNow == reply) {
493         e = [vimControllers objectEnumerator];
494         id vc;
495         while ((vc = [e nextObject]))
496             [vc sendMessage:TerminateNowMsgID data:nil];
497     }
499     return reply;
502 - (void)applicationWillTerminate:(NSNotification *)notification
504 #if MM_HANDLE_XCODE_MOD_EVENT
505     [[NSAppleEventManager sharedAppleEventManager]
506             removeEventHandlerForEventClass:'KAHL'
507                                  andEventID:'MOD '];
508 #endif
510     // This will invalidate all connections (since they were spawned from this
511     // connection).
512     [connection invalidate];
514     // Send a SIGINT to all running Vim processes, so that they are sure to
515     // receive the connectionDidDie: notification (a process has to be checking
516     // the run-loop for this to happen).
517     unsigned i, count = [vimControllers count];
518     for (i = 0; i < count; ++i) {
519         MMVimController *controller = [vimControllers objectAtIndex:i];
520         int pid = [controller pid];
521         if (pid > 0)
522             kill(pid, SIGINT);
523     }
525     if (fontContainerRef) {
526         ATSFontDeactivate(fontContainerRef, NULL, kATSOptionFlagsDefault);
527         fontContainerRef = 0;
528     }
530     [NSApp setDelegate:nil];
533 + (MMAppController *)sharedInstance
535     // Note: The app controller is a singleton which is instantiated in
536     // MainMenu.nib where it is also connected as the delegate of NSApp.
537     id delegate = [NSApp delegate];
538     return [delegate isKindOfClass:self] ? (MMAppController*)delegate : nil;
541 - (NSMenu *)defaultMainMenu
543     return defaultMainMenu;
546 - (void)removeVimController:(id)controller
548     //NSLog(@"%s%@", _cmd, controller);
550     [[controller windowController] close];
552     [vimControllers removeObject:controller];
554     if (![vimControllers count]) {
555         // The last editor window just closed so restore the main menu back to
556         // its default state (which is defined in MainMenu.nib).
557         [self setMainMenu:defaultMainMenu];
558     }
561 - (void)windowControllerWillOpen:(MMWindowController *)windowController
563     NSPoint topLeft = NSZeroPoint;
564     NSWindow *topWin = [[[self topmostVimController] windowController] window];
565     NSWindow *win = [windowController window];
567     if (!win) return;
569     // If there is a window belonging to a Vim process, cascade from it,
570     // otherwise use the autosaved window position (if any).
571     if (topWin) {
572         NSRect frame = [topWin frame];
573         topLeft = NSMakePoint(frame.origin.x, NSMaxY(frame));
574     } else {
575         NSString *topLeftString = [[NSUserDefaults standardUserDefaults]
576             stringForKey:MMTopLeftPointKey];
577         if (topLeftString)
578             topLeft = NSPointFromString(topLeftString);
579     }
581     if (!NSEqualPoints(topLeft, NSZeroPoint)) {
582         if (topWin)
583             topLeft = [win cascadeTopLeftFromPoint:topLeft];
585         [win setFrameTopLeftPoint:topLeft];
586     }
588     if (openSelectionString) {
589         // TODO: Pass this as a parameter instead!  Get rid of
590         // 'openSelectionString' etc.
591         //
592         // There is some text to paste into this window as a result of the
593         // services menu "Open selection ..." being used.
594         [[windowController vimController] dropString:openSelectionString];
595         [openSelectionString release];
596         openSelectionString = nil;
597     }
600 - (void)setMainMenu:(NSMenu *)mainMenu
602     if ([NSApp mainMenu] == mainMenu) return;
604     // If the new menu has a "Recent Files" dummy item, then swap the real item
605     // for the dummy.  We are forced to do this since Cocoa initializes the
606     // "Recent Files" menu and there is no way to simply point Cocoa to a new
607     // item each time the menus are swapped.
608     NSMenu *fileMenu = [mainMenu findFileMenu];
609     if (recentFilesMenuItem && fileMenu) {
610         int dummyIdx =
611                 [fileMenu indexOfItemWithAction:@selector(recentFilesDummy:)];
612         if (dummyIdx >= 0) {
613             NSMenuItem *dummyItem = [[fileMenu itemAtIndex:dummyIdx] retain];
614             [fileMenu removeItemAtIndex:dummyIdx];
616             NSMenu *recentFilesParentMenu = [recentFilesMenuItem menu];
617             int idx = [recentFilesParentMenu indexOfItem:recentFilesMenuItem];
618             if (idx >= 0) {
619                 [[recentFilesMenuItem retain] autorelease];
620                 [recentFilesParentMenu removeItemAtIndex:idx];
621                 [recentFilesParentMenu insertItem:dummyItem atIndex:idx];
622             }
624             [fileMenu insertItem:recentFilesMenuItem atIndex:dummyIdx];
625             [dummyItem release];
626         }
627     }
629     // Now set the new menu.  Notice that we keep one menu for each editor
630     // window since each editor can have its own set of menus.  When swapping
631     // menus we have to tell Cocoa where the new "MacVim", "Windows", and
632     // "Services" menu are.
633     [NSApp setMainMenu:mainMenu];
635     // Setting the "MacVim" (or "Application") menu ensures that it is typeset
636     // in boldface.  (The setAppleMenu: method used to be public but is now
637     // private so this will have to be considered a bit of a hack!)
638     NSMenu *appMenu = [mainMenu findApplicationMenu];
639     [NSApp performSelector:@selector(setAppleMenu:) withObject:appMenu];
641     NSMenu *servicesMenu = [mainMenu findServicesMenu];
642     [NSApp setServicesMenu:servicesMenu];
644     NSMenu *windowsMenu = [mainMenu findWindowsMenu];
645     if (windowsMenu) {
646         // Cocoa isn't clever enough to get rid of items it has added to the
647         // "Windows" menu so we have to do it ourselves otherwise there will be
648         // multiple menu items for each window in the "Windows" menu.
649         //   This code assumes that the only items Cocoa add are ones which
650         // send off the action makeKeyAndOrderFront:.  (Cocoa will not add
651         // another separator item if the last item on the "Windows" menu
652         // already is a separator, so we needen't worry about separators.)
653         int i, count = [windowsMenu numberOfItems];
654         for (i = count-1; i >= 0; --i) {
655             NSMenuItem *item = [windowsMenu itemAtIndex:i];
656             if ([item action] == @selector(makeKeyAndOrderFront:))
657                 [windowsMenu removeItem:item];
658         }
659     }
660     [NSApp setWindowsMenu:windowsMenu];
663 - (IBAction)newWindow:(id)sender
665     [self launchVimProcessWithArguments:nil];
668 - (IBAction)fileOpen:(id)sender
670     NSString *dir = nil;
671     BOOL trackPwd = [[NSUserDefaults standardUserDefaults]
672             boolForKey:MMDialogsTrackPwdKey];
673     if (trackPwd) {
674         MMVimController *vc = [self keyVimController];
675         if (vc) dir = [[vc vimState] objectForKey:@"pwd"];
676     }
678     NSOpenPanel *panel = [NSOpenPanel openPanel];
679     [panel setAllowsMultipleSelection:YES];
680     int result = [panel runModalForDirectory:dir file:nil types:nil];
681     if (NSOKButton == result)
682         [self application:NSApp openFiles:[panel filenames]];
685 - (IBAction)selectNextWindow:(id)sender
687     unsigned i, count = [vimControllers count];
688     if (!count) return;
690     NSWindow *keyWindow = [NSApp keyWindow];
691     for (i = 0; i < count; ++i) {
692         MMVimController *vc = [vimControllers objectAtIndex:i];
693         if ([[[vc windowController] window] isEqual:keyWindow])
694             break;
695     }
697     if (i < count) {
698         if (++i >= count)
699             i = 0;
700         MMVimController *vc = [vimControllers objectAtIndex:i];
701         [[vc windowController] showWindow:self];
702     }
705 - (IBAction)selectPreviousWindow:(id)sender
707     unsigned i, count = [vimControllers count];
708     if (!count) return;
710     NSWindow *keyWindow = [NSApp keyWindow];
711     for (i = 0; i < count; ++i) {
712         MMVimController *vc = [vimControllers objectAtIndex:i];
713         if ([[[vc windowController] window] isEqual:keyWindow])
714             break;
715     }
717     if (i < count) {
718         if (i > 0) {
719             --i;
720         } else {
721             i = count - 1;
722         }
723         MMVimController *vc = [vimControllers objectAtIndex:i];
724         [[vc windowController] showWindow:self];
725     }
728 - (IBAction)fontSizeUp:(id)sender
730     [[NSFontManager sharedFontManager] modifyFont:
731             [NSNumber numberWithInt:NSSizeUpFontAction]];
734 - (IBAction)fontSizeDown:(id)sender
736     [[NSFontManager sharedFontManager] modifyFont:
737             [NSNumber numberWithInt:NSSizeDownFontAction]];
740 - (IBAction)orderFrontPreferencePanel:(id)sender
742     [[MMPreferenceController sharedPrefsWindowController] showWindow:self];
745 - (IBAction)openWebsite:(id)sender
747     [[NSWorkspace sharedWorkspace] openURL:
748             [NSURL URLWithString:MMWebsiteString]];
751 - (IBAction)showVimHelp:(id)sender
753     // Open a new window with the help window maximized.
754     [self launchVimProcessWithArguments:[NSArray arrayWithObjects:
755             @"-c", @":h gui_mac", @"-c", @":res", nil]];
758 - (IBAction)zoomAll:(id)sender
760     [NSApp makeWindowsPerform:@selector(performZoom:) inOrder:YES];
763 - (byref id <MMFrontendProtocol>)
764     connectBackend:(byref in id <MMBackendProtocol>)backend
765                pid:(int)pid
767     //NSLog(@"Connect backend (pid=%d)", pid);
768     NSNumber *pidKey = [NSNumber numberWithInt:pid];
769     MMVimController *vc = nil;
771     @try {
772         [(NSDistantObject*)backend
773                 setProtocolForProxy:@protocol(MMBackendProtocol)];
775         vc = [[[MMVimController alloc]
776             initWithBackend:backend pid:pid]
777             autorelease];
779         if (![vimControllers count]) {
780             // The first window autosaves its position.  (The autosaving
781             // features of Cocoa are not used because we need more control over
782             // what is autosaved and when it is restored.)
783             [[vc windowController] setWindowAutosaveKey:MMTopLeftPointKey];
784         }
786         [vimControllers addObject:vc];
788         id args = [pidArguments objectForKey:pidKey];
789         if (args && [NSNull null] != args)
790             [self passArguments:args toVimController:vc];
792         // HACK!  MacVim does not get activated if it is launched from the
793         // terminal, so we forcibly activate here unless it is an untitled
794         // window opening.  Untitled windows are treated differently, else
795         // MacVim would steal the focus if another app was activated while the
796         // untitled window was loading.
797         if (!args || args != [NSNull null])
798             [NSApp activateIgnoringOtherApps:YES];
800         if (args)
801             [pidArguments removeObjectForKey:pidKey];
803         return vc;
804     }
806     @catch (NSException *e) {
807         NSLog(@"Exception caught in %s: \"%@\"", _cmd, e);
809         if (vc)
810             [vimControllers removeObject:vc];
812         [pidArguments removeObjectForKey:pidKey];
813     }
815     return nil;
818 - (NSArray *)serverList
820     NSMutableArray *array = [NSMutableArray array];
822     unsigned i, count = [vimControllers count];
823     for (i = 0; i < count; ++i) {
824         MMVimController *controller = [vimControllers objectAtIndex:i];
825         if ([controller serverName])
826             [array addObject:[controller serverName]];
827     }
829     return array;
832 @end // MMAppController
837 @implementation MMAppController (MMServices)
839 - (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)userData
840                 error:(NSString **)error
842     if (![[pboard types] containsObject:NSStringPboardType]) {
843         NSLog(@"WARNING: Pasteboard contains no object of type "
844                 "NSStringPboardType");
845         return;
846     }
848     MMVimController *vc = [self topmostVimController];
849     if (vc) {
850         // Open a new tab first, since dropString: does not do this.
851         [vc sendMessage:AddNewTabMsgID data:nil];
852         [vc dropString:[pboard stringForType:NSStringPboardType]];
853     } else {
854         // NOTE: There is no window to paste the selection into, so save the
855         // text, open a new window, and paste the text when the next window
856         // opens.  (If this is called several times in a row, then all but the
857         // last call might be ignored.)
858         if (openSelectionString) [openSelectionString release];
859         openSelectionString = [[pboard stringForType:NSStringPboardType] copy];
861         [self newWindow:self];
862     }
865 - (void)openFile:(NSPasteboard *)pboard userData:(NSString *)userData
866            error:(NSString **)error
868     if (![[pboard types] containsObject:NSStringPboardType]) {
869         NSLog(@"WARNING: Pasteboard contains no object of type "
870                 "NSStringPboardType");
871         return;
872     }
874     // TODO: Parse multiple filenames and create array with names.
875     NSString *string = [pboard stringForType:NSStringPboardType];
876     string = [string stringByTrimmingCharactersInSet:
877             [NSCharacterSet whitespaceAndNewlineCharacterSet]];
878     string = [string stringByStandardizingPath];
880     NSArray *filenames = [self filterFilesAndNotify:
881             [NSArray arrayWithObject:string]];
882     if ([filenames count] > 0) {
883         MMVimController *vc = nil;
884         if (userData && [userData isEqual:@"Tab"])
885             vc = [self topmostVimController];
887         if (vc) {
888             [vc dropFiles:filenames forceOpen:YES];
889         } else {
890             [self application:NSApp openFiles:filenames];
891         }
892     }
895 @end // MMAppController (MMServices)
900 @implementation MMAppController (Private)
902 - (MMVimController *)keyVimController
904     NSWindow *keyWindow = [NSApp keyWindow];
905     if (keyWindow) {
906         unsigned i, count = [vimControllers count];
907         for (i = 0; i < count; ++i) {
908             MMVimController *vc = [vimControllers objectAtIndex:i];
909             if ([[[vc windowController] window] isEqual:keyWindow])
910                 return vc;
911         }
912     }
914     return nil;
917 - (MMVimController *)topmostVimController
919     // Find the topmost visible window which has an associated vim controller.
920     NSEnumerator *e = [[NSApp orderedWindows] objectEnumerator];
921     id window;
922     while ((window = [e nextObject]) && [window isVisible]) {
923         unsigned i, count = [vimControllers count];
924         for (i = 0; i < count; ++i) {
925             MMVimController *vc = [vimControllers objectAtIndex:i];
926             if ([[[vc windowController] window] isEqual:window])
927                 return vc;
928         }
929     }
931     return nil;
934 - (int)launchVimProcessWithArguments:(NSArray *)args
936     int pid = -1;
937     NSString *path = [[NSBundle mainBundle] pathForAuxiliaryExecutable:@"Vim"];
939     if (!path) {
940         NSLog(@"ERROR: Vim executable could not be found inside app bundle!");
941         return -1;
942     }
944     NSArray *taskArgs = [NSArray arrayWithObjects:@"-g", @"-f", nil];
945     if (args)
946         taskArgs = [taskArgs arrayByAddingObjectsFromArray:args];
948     BOOL useLoginShell = [[NSUserDefaults standardUserDefaults]
949             boolForKey:MMLoginShellKey];
950     if (useLoginShell) {
951         // Run process with a login shell, roughly:
952         //   echo "exec Vim -g -f args" | ARGV0=-`basename $SHELL` $SHELL [-l]
953         pid = executeInLoginShell(path, taskArgs);
954     } else {
955         // Run process directly:
956         //   Vim -g -f args
957         NSTask *task = [NSTask launchedTaskWithLaunchPath:path
958                                                 arguments:taskArgs];
959         pid = task ? [task processIdentifier] : -1;
960     }
962     if (-1 != pid) {
963         // NOTE: If the process has no arguments, then add a null argument to
964         // the pidArguments dictionary.  This is later used to detect that a
965         // process without arguments is being launched.
966         if (!args)
967             [pidArguments setObject:[NSNull null]
968                              forKey:[NSNumber numberWithInt:pid]];
969     } else {
970         NSLog(@"WARNING: %s%@ failed (useLoginShell=%d)", _cmd, args,
971                 useLoginShell);
972     }
974     return pid;
977 - (NSArray *)filterFilesAndNotify:(NSArray *)filenames
979     // Go trough 'filenames' array and make sure each file exists.  Present
980     // warning dialog if some file was missing.
982     NSString *firstMissingFile = nil;
983     NSMutableArray *files = [NSMutableArray array];
984     unsigned i, count = [filenames count];
986     for (i = 0; i < count; ++i) {
987         NSString *name = [filenames objectAtIndex:i];
988         if ([[NSFileManager defaultManager] fileExistsAtPath:name]) {
989             [files addObject:name];
990         } else if (!firstMissingFile) {
991             firstMissingFile = name;
992         }
993     }
995     if (firstMissingFile) {
996         NSAlert *alert = [[NSAlert alloc] init];
997         [alert addButtonWithTitle:NSLocalizedString(@"OK",
998                 @"Dialog button")];
1000         NSString *text;
1001         if ([files count] >= count-1) {
1002             [alert setMessageText:NSLocalizedString(@"File not found",
1003                     @"File not found dialog, title")];
1004             text = [NSString stringWithFormat:NSLocalizedString(
1005                     @"Could not open file with name %@.",
1006                     @"File not found dialog, text"), firstMissingFile];
1007         } else {
1008             [alert setMessageText:NSLocalizedString(@"Multiple files not found",
1009                     @"File not found dialog, title")];
1010             text = [NSString stringWithFormat:NSLocalizedString(
1011                     @"Could not open file with name %@, and %d other files.",
1012                     @"File not found dialog, text"),
1013                 firstMissingFile, count-[files count]-1];
1014         }
1016         [alert setInformativeText:text];
1017         [alert setAlertStyle:NSWarningAlertStyle];
1019         [alert runModal];
1020         [alert release];
1022         [NSApp replyToOpenOrPrint:NSApplicationDelegateReplyFailure];
1023     }
1025     return files;
1028 - (NSArray *)filterOpenFiles:(NSArray *)filenames
1029                    arguments:(NSDictionary *)args
1031     // Check if any of the files in the 'filenames' array are open in any Vim
1032     // process.  Remove the files that are open from the 'filenames' array and
1033     // return it.  If all files were filtered out, then raise the first file in
1034     // the Vim process it is open.  Files that are filtered are sent an odb
1035     // open event in case theID is not zero.
1037     NSMutableDictionary *localArgs =
1038             [NSMutableDictionary dictionaryWithDictionary:args];
1039     MMVimController *raiseController = nil;
1040     NSString *raiseFile = nil;
1041     NSMutableArray *files = [filenames mutableCopy];
1042     NSString *expr = [NSString stringWithFormat:
1043             @"map([\"%@\"],\"bufloaded(v:val)\")",
1044             [files componentsJoinedByString:@"\",\""]];
1045     unsigned i, count = [vimControllers count];
1047     // Ensure that the files aren't opened when passing arguments.
1048     [localArgs setObject:[NSNumber numberWithBool:NO] forKey:@"openFiles"];
1050     for (i = 0; i < count && [files count]; ++i) {
1051         MMVimController *controller = [vimControllers objectAtIndex:i];
1053         // Query Vim for which files in the 'files' array are open.
1054         NSString *eval = [controller evaluateVimExpression:expr];
1055         if (!eval) continue;
1057         NSIndexSet *idxSet = [NSIndexSet indexSetWithVimList:eval];
1058         if ([idxSet count]) {
1059             if (!raiseFile) {
1060                 // Remember the file and which Vim that has it open so that
1061                 // we can raise it later on.
1062                 raiseController = controller;
1063                 raiseFile = [files objectAtIndex:[idxSet firstIndex]];
1064                 [[raiseFile retain] autorelease];
1065             }
1067             // Pass (ODB/Xcode/Spotlight) arguments to this process.
1068             [localArgs setObject:[files objectsAtIndexes:idxSet]
1069                           forKey:@"filenames"];
1070             [self passArguments:localArgs toVimController:controller];
1072             // Remove all the files that were open in this Vim process and
1073             // create a new expression to evaluate.
1074             [files removeObjectsAtIndexes:idxSet];
1075             expr = [NSString stringWithFormat:
1076                     @"map([\"%@\"],\"bufloaded(v:val)\")",
1077                     [files componentsJoinedByString:@"\",\""]];
1078         }
1079     }
1081     if (![files count] && raiseFile) {
1082         // Raise the window containing the first file that was already open,
1083         // and make sure that the tab containing that file is selected.  Only
1084         // do this if there are no more files to open, otherwise sometimes the
1085         // window with 'raiseFile' will be raised, other times it might be the
1086         // window that will open with the files in the 'files' array.
1087         raiseFile = [raiseFile stringByEscapingSpecialFilenameCharacters];
1088         NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>"
1089             ":let oldswb=&swb|let &swb=\"useopen,usetab\"|"
1090             "tab sb %@|let &swb=oldswb|unl oldswb|"
1091             "cal foreground()|redr|f<CR>", raiseFile];
1093         [raiseController addVimInput:input];
1094     }
1096     return files;
1099 #if MM_HANDLE_XCODE_MOD_EVENT
1100 - (void)handleXcodeModEvent:(NSAppleEventDescriptor *)event
1101                  replyEvent:(NSAppleEventDescriptor *)reply
1103 #if 0
1104     // Xcode sends this event to query MacVim which open files have been
1105     // modified.
1106     NSLog(@"reply:%@", reply);
1107     NSLog(@"event:%@", event);
1109     NSEnumerator *e = [vimControllers objectEnumerator];
1110     id vc;
1111     while ((vc = [e nextObject])) {
1112         DescType type = [reply descriptorType];
1113         unsigned len = [[type data] length];
1114         NSMutableData *data = [NSMutableData data];
1116         [data appendBytes:&type length:sizeof(DescType)];
1117         [data appendBytes:&len length:sizeof(unsigned)];
1118         [data appendBytes:[reply data] length:len];
1120         [vc sendMessage:XcodeModMsgID data:data];
1121     }
1122 #endif
1124 #endif
1126 - (int)findLaunchingProcessWithoutArguments
1128     NSArray *keys = [pidArguments allKeysForObject:[NSNull null]];
1129     if ([keys count] > 0) {
1130         //NSLog(@"found launching process without arguments");
1131         return [[keys objectAtIndex:0] intValue];
1132     }
1134     return 0;
1137 - (MMVimController *)findUntitledWindow
1139     NSEnumerator *e = [vimControllers objectEnumerator];
1140     id vc;
1141     while ((vc = [e nextObject])) {
1142         // TODO: This is a moronic test...should query the Vim process if there
1143         // are any open buffers or something like that instead.
1144         NSString *title = [[[vc windowController] window] title];
1146         // TODO: this will not work in a localized MacVim
1147         if ([title hasPrefix:@"[No Name] - VIM"]) {
1148             //NSLog(@"found untitled window");
1149             return vc;
1150         }
1151     }
1153     return nil;
1156 - (NSMutableDictionary *)extractArgumentsFromOdocEvent:
1157     (NSAppleEventDescriptor *)desc
1159     NSMutableDictionary *dict = [NSMutableDictionary dictionary];
1161     // 1. Extract ODB parameters (if any)
1162     NSAppleEventDescriptor *odbdesc = desc;
1163     if (![odbdesc paramDescriptorForKeyword:keyFileSender]) {
1164         // The ODB paramaters may hide inside the 'keyAEPropData' descriptor.
1165         odbdesc = [odbdesc paramDescriptorForKeyword:keyAEPropData];
1166         if (![odbdesc paramDescriptorForKeyword:keyFileSender])
1167             odbdesc = nil;
1168     }
1170     if (odbdesc) {
1171         NSAppleEventDescriptor *p =
1172                 [odbdesc paramDescriptorForKeyword:keyFileSender];
1173         if (p)
1174             [dict setObject:[NSNumber numberWithUnsignedInt:[p typeCodeValue]]
1175                      forKey:@"remoteID"];
1177         p = [odbdesc paramDescriptorForKeyword:keyFileCustomPath];
1178         if (p)
1179             [dict setObject:[p stringValue] forKey:@"remotePath"];
1181         p = [odbdesc paramDescriptorForKeyword:keyFileSenderToken];
1182         if (p)
1183             [dict setObject:p forKey:@"remotePath"];
1184     }
1186     // 2. Extract Xcode parameters (if any)
1187     NSAppleEventDescriptor *xcodedesc =
1188             [desc paramDescriptorForKeyword:keyAEPosition];
1189     if (xcodedesc) {
1190         NSRange range;
1191         MMSelectionRange *sr = (MMSelectionRange*)[[xcodedesc data] bytes];
1193         if (sr->lineNum < 0) {
1194             // Should select a range of lines.
1195             range.location = sr->startRange + 1;
1196             range.length = sr->endRange - sr->startRange + 1;
1197         } else {
1198             // Should only move cursor to a line.
1199             range.location = sr->lineNum + 1;
1200             range.length = 0;
1201         }
1203         [dict setObject:NSStringFromRange(range) forKey:@"selectionRange"];
1204     }
1206     // 3. Extract Spotlight search text (if any)
1207     NSAppleEventDescriptor *spotlightdesc = 
1208             [desc paramDescriptorForKeyword:keyAESearchText];
1209     if (spotlightdesc)
1210         [dict setObject:[spotlightdesc stringValue] forKey:@"searchText"];
1212     return dict;
1215 - (void)passArguments:(NSDictionary *)args toVimController:(MMVimController*)vc
1217     if (!args) return;
1219     // Pass filenames to open if required (the 'openFiles' argument can be used
1220     // to disallow opening of the files).
1221     NSArray *filenames = [args objectForKey:@"filenames"];
1222     if (filenames && [[args objectForKey:@"openFiles"] boolValue]) {
1223         NSString *tabDrop = buildTabDropCommand(filenames);
1224         [vc addVimInput:tabDrop];
1225     }
1227     // Pass ODB data
1228     if (filenames && [args objectForKey:@"remoteID"]) {
1229         [vc odbEdit:filenames
1230              server:[[args objectForKey:@"remoteID"] unsignedIntValue]
1231                path:[args objectForKey:@"remotePath"]
1232               token:[args objectForKey:@"remoteToken"]];
1233     }
1235     // Pass range of lines to select
1236     if ([args objectForKey:@"selectionRange"]) {
1237         NSRange selectionRange = NSRangeFromString(
1238                 [args objectForKey:@"selectionRange"]);
1239         [vc addVimInput:buildSelectRangeCommand(selectionRange)];
1240     }
1242     // Pass search text
1243     NSString *searchText = [args objectForKey:@"searchText"];
1244     if (searchText)
1245         [vc addVimInput:buildSearchTextCommand(searchText)];
1248 @end // MMAppController (Private)
1253 @implementation NSNumber (MMExtras)
1254 - (int)tag
1256     return [self intValue];
1258 @end // NSNumber (MMExtras)
1263 @implementation NSMenu (MMExtras)
1265 - (int)indexOfItemWithAction:(SEL)action
1267     int i, count = [self numberOfItems];
1268     for (i = 0; i < count; ++i) {
1269         NSMenuItem *item = [self itemAtIndex:i];
1270         if ([item action] == action)
1271             return i;
1272     }
1274     return -1;
1277 - (NSMenuItem *)itemWithAction:(SEL)action
1279     int idx = [self indexOfItemWithAction:action];
1280     return idx >= 0 ? [self itemAtIndex:idx] : nil;
1283 - (NSMenu *)findMenuContainingItemWithAction:(SEL)action
1285     // NOTE: We only look for the action in the submenus of 'self'
1286     int i, count = [self numberOfItems];
1287     for (i = 0; i < count; ++i) {
1288         NSMenu *menu = [[self itemAtIndex:i] submenu];
1289         NSMenuItem *item = [menu itemWithAction:action];
1290         if (item) return menu;
1291     }
1293     return nil;
1296 - (NSMenu *)findWindowsMenu
1298     return [self findMenuContainingItemWithAction:
1299         @selector(performMiniaturize:)];
1302 - (NSMenu *)findApplicationMenu
1304     // TODO: Just return [self itemAtIndex:0]?
1305     return [self findMenuContainingItemWithAction:@selector(terminate:)];
1308 - (NSMenu *)findServicesMenu
1310     // NOTE!  Our heuristic for finding the "Services" menu is to look for the
1311     // second item before the "Hide MacVim" menu item on the "MacVim" menu.
1312     // (The item before "Hide MacVim" should be a separator, but this is not
1313     // important as long as the item before that is the "Services" menu.)
1315     NSMenu *appMenu = [self findApplicationMenu];
1316     if (!appMenu) return nil;
1318     int idx = [appMenu indexOfItemWithAction: @selector(hide:)];
1319     if (idx-2 < 0) return nil;  // idx == -1, if selector not found
1321     return [[appMenu itemAtIndex:idx-2] submenu];
1324 - (NSMenu *)findFileMenu
1326     return [self findMenuContainingItemWithAction:@selector(performClose:)];
1329 @end // NSMenu (MMExtras)
1334     static int
1335 executeInLoginShell(NSString *path, NSArray *args)
1337     // Start a login shell and execute the command 'path' with arguments 'args'
1338     // in the shell.  This ensures that user environment variables are set even
1339     // when MacVim was started from the Finder.
1341     int pid = -1;
1342     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
1344     // Determine which shell to use to execute the command.  The user
1345     // may decide which shell to use by setting a user default or the
1346     // $SHELL environment variable.
1347     NSString *shell = [ud stringForKey:MMLoginShellCommandKey];
1348     if (!shell || [shell length] == 0)
1349         shell = [[[NSProcessInfo processInfo] environment]
1350             objectForKey:@"SHELL"];
1351     if (!shell)
1352         shell = @"/bin/bash";
1354     //NSLog(@"shell = %@", shell);
1356     // Bash needs the '-l' flag to launch a login shell.  The user may add
1357     // flags by setting a user default.
1358     NSString *shellArgument = [ud stringForKey:MMLoginShellArgumentKey];
1359     if (!shellArgument || [shellArgument length] == 0) {
1360         if ([[shell lastPathComponent] isEqual:@"bash"])
1361             shellArgument = @"-l";
1362         else
1363             shellArgument = nil;
1364     }
1366     //NSLog(@"shellArgument = %@", shellArgument);
1368     // Build input string to pipe to the login shell.
1369     NSMutableString *input = [NSMutableString stringWithFormat:
1370             @"exec \"%@\"", path];
1371     if (args) {
1372         // Append all arguments, making sure they are properly quoted, even
1373         // when they contain single quotes.
1374         NSEnumerator *e = [args objectEnumerator];
1375         id obj;
1377         while ((obj = [e nextObject])) {
1378             NSMutableString *arg = [NSMutableString stringWithString:obj];
1379             [arg replaceOccurrencesOfString:@"'" withString:@"'\"'\"'"
1380                                     options:NSLiteralSearch
1381                                       range:NSMakeRange(0, [arg length])];
1382             [input appendFormat:@" '%@'", arg];
1383         }
1384     }
1386     // Build the argument vector used to start the login shell.
1387     NSString *shellArg0 = [NSString stringWithFormat:@"-%@",
1388              [shell lastPathComponent]];
1389     char *shellArgv[3] = { (char *)[shellArg0 UTF8String], NULL, NULL };
1390     if (shellArgument)
1391         shellArgv[1] = (char *)[shellArgument UTF8String];
1393     // Get the C string representation of the shell path before the fork since
1394     // we must not call Foundation functions after a fork.
1395     const char *shellPath = [shell fileSystemRepresentation];
1397     // Fork and execute the process.
1398     int ds[2];
1399     if (pipe(ds)) return -1;
1401     pid = fork();
1402     if (pid == -1) {
1403         return -1;
1404     } else if (pid == 0) {
1405         // Child process
1406         if (close(ds[1]) == -1) exit(255);
1407         if (dup2(ds[0], 0) == -1) exit(255);
1409         execv(shellPath, shellArgv);
1411         // Never reached unless execv fails
1412         exit(255);
1413     } else {
1414         // Parent process
1415         if (close(ds[0]) == -1) return -1;
1417         // Send input to execute to the child process
1418         [input appendString:@"\n"];
1419         int bytes = [input lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
1421         if (write(ds[1], [input UTF8String], bytes) != bytes) return -1;
1422         if (close(ds[1]) == -1) return -1;
1423     }
1425     return pid;