Bug 15438 - Improvements to Quartz/Cocoa Device when Used from Terminal
Summary: Improvements to Quartz/Cocoa Device when Used from Terminal
Status: NEW
Alias: None
Product: R
Classification: Unclassified
Component: Mac GUI / Mac specific (show other bugs)
Version: R-devel (trunk)
Hardware: x86_64/x64/amd64 (64-bit) Mac OS X v10.8
: P5 enhancement
Assignee: Simon Urbanek
URL:
Depends on:
Blocks:
 
Reported: 2013-08-31 12:48 UTC by Colin A. Smith
Modified: 2013-09-04 14:08 UTC (History)
0 users

See Also:


Attachments
Changed files in grDevices (32.35 KB, application/x-gzip)
2013-08-31 12:48 UTC, Colin A. Smith
Details
Changed/Added files in grDevices (38.11 KB, application/x-gzip)
2013-09-04 14:08 UTC, Colin A. Smith
Details

Note You need to log in before you can comment on or make changes to this bug.
Description Colin A. Smith 2013-08-31 12:48:47 UTC
Created attachment 1479 [details]
Changed files in grDevices

Here are a few changes in grDevices to improve the Cocoa/Quartz device appearance/functionality:

- Altered how the menus were created so that the "..." and "R" menus appear as "R" and "File", respectively.

Getting a real application menu with About, Hide, Quit, etc. requires loading from an application package, which command line R obviously doesn't do. There are workarounds that call functions that were removed around the time of Mac OS X 10.4. (Google setAppleMenu for more details.) They can still be called in an unsupported fashion, but I don't think it's a good idea to be calling APIs that are now private.

This patch simply adds an empty menu item to the menu bar at index 0. (The File menu used to be at index 0, it is now index 1.) Under OS X 10.8, this results in correct menu names, although it is not guaranteed to work in the future. However, if the behavior changes it will hopefully not be worse than it was before this patch.

- Changed the dock icon to use the R logo instead of the default command line program icon.

The R logo was taken from the R.app icon file. It is a 128x128 pixel PNG file located in a new directory, grDevices/inst/aqua. Makefile.in is set up to only install this directory if aqua is enabled. When a Cocoa/Quartz device is initially created, it finds this directory using R_HomeDir().

- Added a menu item to the Window menu that will show the Terminal window/tab the R process is running in.

This is done using embedded AppleScripts in two class methods. Class (as opposed to instance) methods were used so the menu item is functional even if all Quartz device windows are closed. The menu item is only added if R was run from Terminal.app (as opposed to xterm, ssh, etc. Compiling/running the AppleScript that tests for this causes a small delay in opening the Quartz device. On my laptop this is only noticeable if you compare the old and new code side by side. If this very slight lag is unacceptable, the check could be removed, or potentially delayed until after the run loop starts (but that would increase complexity).

The attached archive has the files modified from the latest version of trunk.

The diff is below:

Index: grDevices/Makefile.in
===================================================================
--- grDevices/Makefile.in	(revision 63776)
+++ grDevices/Makefile.in	(working copy)
@@ -17,7 +17,7 @@
 R_EXE = $(top_builddir)/bin/R --vanilla --slave
 
 RSRC = `LC_COLLATE=C ls $(srcdir)/R/*.R $(srcdir)/R/$(R_OSTYPE)/*.R`
-INSTDIRS = afm enc icc
+INSTDIRS = afm enc icc @BUILD_AQUA_TRUE@ aqua
 DEFPKGS = NULL
 
 all: Makefile DESCRIPTION
Index: grDevices/src/qdCocoa.m
===================================================================
--- grDevices/src/qdCocoa.m	(revision 63776)
+++ grDevices/src/qdCocoa.m	(working copy)
@@ -30,6 +30,7 @@
 
 #include <R.h>
 #include <Rinternals.h>
+#include <Rinterface.h>
 #include <R_ext/QuartzDevice.h>
 #include <R_ext/eventloop.h>
 
@@ -113,14 +114,25 @@
 	/* File menu is tricky - it may have a different name in different localizations. Hence we use a trick - the File menu should be first and have the <Cmd><W> shortcut for "Close Window" by convenience */
 	BOOL hasFileMenu = NO;
 	if (!soleMenu) { /* in the case of a soleMenu we already know that we don't have it. Otherwise look for it. */
-	    if (!hasFileMenu && [mainMenu indexOfItemWithTitle:@"File"]) hasFileMenu = YES; /* first shot is cheap - it will succeed if we added the menu ourself */
-	    if (!hasFileMenu && [mainMenu numberOfItems] > 0 && (menuItem = [mainMenu itemAtIndex:0]) && (menu = [menuItem submenu])) { /* potentially a File menu */
+	    if (!hasFileMenu && [mainMenu indexOfItemWithTitle:@"File"] >= 0) hasFileMenu = YES; /* first shot is cheap - it will succeed if we added the menu ourself */
+	    if (!hasFileMenu && [mainMenu numberOfItems] > 0 && (menuItem = [mainMenu itemAtIndex:1]) && (menu = [menuItem submenu])) { /* potentially a File menu */
 		int i = 0, n = [menu numberOfItems];
 		while (i < n) {
 		    NSString *ke = [[menu itemAtIndex: i++] keyEquivalent];
 		    if (ke && [ke isEqualToString:@"w"]) { hasFileMenu = YES; break; }
 		}
 	    }
+	} else {
+		/* Create an empty menu item that becomes the application menu */
+		menuItem = [[NSMenuItem alloc] initWithTitle:@"R" action:nil keyEquivalent:@""];
+		[mainMenu addItem:menuItem];
+		[menuItem release];
+		/* Set the dock icon to be the R logo */
+		NSString *iconPath = [[NSString alloc] initWithFormat:@"%s/library/grDevices/aqua/RLogo.png", R_HomeDir()];
+		NSImage *iconImage = [[NSImage alloc] initWithContentsOfFile:iconPath];
+		if (iconImage) [NSApp setApplicationIconImage:iconImage];
+		[iconImage release];
+		[iconPath release];
 	}
 	if (!hasFileMenu) { /* No file menu? Add it. */
             menu = [[NSMenu alloc] initWithTitle:@"File"];
@@ -132,14 +144,14 @@
 	    
             menuItem = [[NSMenuItem alloc] initWithTitle:[menu title] action:nil keyEquivalent:@""]; /* the "Quartz" item in the main menu */
             [menuItem setSubmenu:menu];
-	    [mainMenu insertItem: menuItem atIndex:0];
+	    [mainMenu insertItem: menuItem atIndex:1];
 	}
 
 	/* same trick for Edit */
 	BOOL hasEditMenu = NO;
 	if (!soleMenu) { /* in the case of a soleMenu we already know that we don't have it. Otherwise look for it. */
-	    if (!hasEditMenu && [mainMenu indexOfItemWithTitle:@"Edit"]) hasEditMenu = YES; /* first shot is cheap - it will succeed if we added the menu ourself */
-	    if (!hasEditMenu && [mainMenu numberOfItems] > 1 && (menuItem = [mainMenu itemAtIndex:1]) && (menu = [menuItem submenu])) { /* potentially a Edit menu */
+	    if (!hasEditMenu && [mainMenu indexOfItemWithTitle:@"Edit"] >= 0) hasEditMenu = YES; /* first shot is cheap - it will succeed if we added the menu ourself */
+	    if (!hasEditMenu && [mainMenu numberOfItems] > 1 && (menuItem = [mainMenu itemAtIndex:2]) && (menu = [menuItem submenu])) { /* potentially a Edit menu */
 		int i = 0, n = [menu numberOfItems];
 		while (i < n) {
 		    NSString *ke = [[menu itemAtIndex: i++] keyEquivalent];
@@ -161,7 +173,7 @@
             menuItem = [[NSMenuItem alloc] initWithTitle:[menu title] action:nil keyEquivalent:@""]; /* the "Quartz" item in the main menu */
             [menuItem setSubmenu:menu];
 	    if ([mainMenu numberOfItems] > 0)
-		[mainMenu insertItem: menuItem atIndex:1];
+		[mainMenu insertItem: menuItem atIndex:2];
 	    else /* this should never be the case because we have added "File" menu, but just in case something goes wrong ... */
 		[mainMenu addItem: menuItem];
 	}
@@ -191,6 +203,11 @@
             
             menuItem = [[NSMenuItem alloc] initWithTitle:@"Minimize" action:@selector(performMiniaturize:) keyEquivalent:@"m"]; [menu addItem:menuItem];
             menuItem = [[NSMenuItem alloc] initWithTitle:@"Zoom" action:@selector(performZoom:) keyEquivalent:@""]; [menu addItem:menuItem];
+            /* If R is running within a Terminal window tab, add a menu item to show it */
+            if ([self canShowTerminal]) {
+                [menu addItem:[NSMenuItem separatorItem]];
+                menuItem = [[NSMenuItem alloc] initWithTitle:@"Show Terminal" action:@selector(showTerminal:) keyEquivalent:@"t"]; [menuItem setTarget:[QuartzCocoaView class]]; [menu addItem:menuItem]; [menuItem release];
+            }
             
             /* Add to menubar */
             menuItem = [[NSMenuItem alloc] initWithTitle:@"Window" action:nil keyEquivalent:@""];
@@ -508,6 +525,72 @@
         [self addCursorRect:[self bounds] cursor:[NSCursor crosshairCursor]];
 }
 
++ (BOOL)canShowTerminal
+{
+    if (!isatty(STDOUT_FILENO)) return NO;
+    char *tty = ttyname(STDOUT_FILENO);
+    
+    NSString *appleScriptSource = [[NSString alloc] initWithFormat:
+        @"tell application \"Terminal\"\n"
+        "	if it is running then\n"
+        "		repeat with theWindow in windows\n"
+        "			repeat with theTab in (tabs of theWindow)\n"
+        "				if tty of theTab is \"%s\" then\n"
+        "					return true\n"
+        "				end if\n"
+        "			end repeat\n"
+        "		end repeat\n"
+        "	end if\n"
+        "end tell\n"
+        "return false", tty];
+    
+    NSAppleScript *appleScript = [[NSAppleScript alloc] initWithSource:appleScriptSource];
+    NSDictionary *errorInfo = nil;
+    NSAppleEventDescriptor *appleEventDescriptor = [appleScript executeAndReturnError:&errorInfo];
+    
+    [appleScriptSource release];
+    [appleScript release];
+    
+    if (errorInfo) return NO;
+    return [appleEventDescriptor booleanValue];
+}
+
++ (void)showTerminal: (id) sender
+{
+    if (!isatty(STDOUT_FILENO)) return;
+    char *tty = ttyname(STDOUT_FILENO);
+    
+    NSString *appleScriptSource = [[NSString alloc] initWithFormat:
+        @"tell application \"Terminal\"\n"
+        "	repeat with theWindow in windows\n"
+        "		repeat with theTab in (tabs of theWindow)\n"
+        "			if tty of theTab is \"%s\" then\n"
+        "				if selected tab of theWindow is not theTab then\n"
+        "					set selected tab of theWindow to theTab\n"
+        "				end if\n"
+        "				if index of theWindow is not 1 then\n"
+        "					set frontmost of theWindow to true\n"
+        "				end if\n"
+        "				activate\n"
+        "				return true\n"
+        "			end if\n"
+        "		end repeat\n"
+        "	end repeat\n"
+        "end tell\n"
+        "return false", tty];
+    
+    NSAppleScript *appleScript = [[NSAppleScript alloc] initWithSource:appleScriptSource];
+    NSDictionary *errorInfo = nil;
+    if ([appleScript compileAndReturnError:&errorInfo]) {
+        [appleScript executeAndReturnError:&errorInfo];
+    } else {
+    	NSLog(@"Error Compiling AppleScript: %@", errorInfo);
+    }
+    
+    [appleScriptSource release];
+    [appleScript release];
+}
+
 @end
 
 #pragma mark --- Cocoa event loop ---
Comment 1 Colin A. Smith 2013-09-04 14:08:02 UTC
Created attachment 1480 [details]
Changed/Added files in grDevices

Here is a revised set of new files that addresses several drawbacks with the previous approach.

Instead of programmatically creating the main menu, it is now loaded from a nib file. This is an officially supported way of creating a main menu and should be much more future proof. The nib file is stored in a bundle and could be localized if desired. The makefiles were updated to generate the nib file a xib file. If used the use of the nib obsoletes quite a bit of C code that could be removed. The largest blocks that are candidates for removal are marked with comments.

In addition, this makes the application menu more functional by including both an "About R" menu item and a working "Quit R" menu item. The "About R" menu item could be enhanced by including version/copyright information. I don't know how to get this information through C code. The "Quit R" currently switches to the Terminal (if being used) and displays a message about how to quit R. This could also be enhanced to actually quit and/or show the save workspace image prompt, but I don't know how to do that through C code.

The loading the mani menu also instantiates a new application delegate class, QuartzAppDelegate, which handles the quit and about actions. In addition, it handles the AppleScript code. Checking whether R is run from the terminal is now delayed from Cocoa initialization to subsequent run loop iterations, which removes the startup delay. The compiled show terminal AppleScript is cached so that subsequent uses are cached.

The diff against the current trunk is below:

Index: grDevices/Makefile.in
===================================================================
--- grDevices/Makefile.in	(revision 63830)
+++ grDevices/Makefile.in	(working copy)
@@ -25,6 +25,8 @@
 	@$(MKINSTALLDIRS) $(top_builddir)/library/$(pkg)
 	@$(MAKE) mkR1 mkdesc mkdemos instdirs
 	@$(MAKE) mksrc
+@BUILD_AQUA_TRUE@	@cp -R $(srcdir)/inst/aqua.bundle $(top_builddir)/library/$(pkg)
+@BUILD_AQUA_TRUE@	@cp -R $(srcdir)/src/MainMenu.nib $(top_builddir)/library/$(pkg)/aqua.bundle/Contents/Resources
 @BYTE_COMPILE_PACKAGES_FALSE@	@$(MAKE) mklazy
 @BYTE_COMPILE_PACKAGES_TRUE@	@$(MAKE) mklazycomp
 	@$(R_GZIPCMD) -9f $(top_builddir)/library/grDevices/afm/*.afm
Index: grDevices/src/Makefile.in
===================================================================
--- grDevices/src/Makefile.in	(revision 63830)
+++ grDevices/src/Makefile.in	(working copy)
@@ -35,6 +35,7 @@
 all: Makefile Makedeps
 	@$(MAKE) Makedeps
 	@$(MAKE) shlib @BUILD_DEVCAIRO_TRUE@ cairodevice
+@BUILD_AQUA_TRUE@	@$(MAKE) MainMenu.nib
 
 Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
 	@cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@
@@ -52,6 +53,9 @@
 cairodevice:
 	@(cd cairo && $(MAKE))
 
+MainMenu.nib: MainMenu.xib
+	ibtool --compile MainMenu.nib MainMenu.xib
+
 include $(R_HOME)/etc${R_ARCH}/Makeconf
 include $(top_srcdir)/share/make/shlib.mk
 LTO = @LTO@
@@ -59,7 +63,7 @@
 mostlyclean: clean
 clean:
 	@-rm -rf .libs _libs
-	@-rm -f Makedeps *.d *.o *$(SHLIB_EXT)
+	@-rm -f Makedeps *.d *.o *$(SHLIB_EXT) *.nib
 	@(cd cairo && $(MAKE) clean)
 distclean: clean
 	@-rm -f Makefile
Index: grDevices/src/qdCocoa.h
===================================================================
--- grDevices/src/qdCocoa.h	(revision 63830)
+++ grDevices/src/qdCocoa.h	(working copy)
@@ -40,7 +40,7 @@
 
 typedef struct sQuartzCocoaDevice QuartzCocoaDevice;
 
-@interface QuartzCocoaView : NSView
+@interface QuartzCocoaView : NSView <NSWindowDelegate>
 {
 	QuartzCocoaDevice *ci;
 }
@@ -51,4 +51,19 @@
 
 @end
 
+@interface QuartzAppDelegate : NSObject <NSApplicationDelegate> {
+
+	BOOL _isRunningInTerminal;
+	NSAppleScript *_showTerminalAppleScript;
+}
+
+@property(readonly) BOOL isRunningInTerminal;
+@property(readonly) NSAppleScript *showTerminalAppleScript;
+
+- (IBAction)showAboutPanel:(id)sender;
+- (void)checkIsRunningInTerminal;
+- (IBAction)showTerminal:(id)sender;
+
+@end
+
 #endif
Index: grDevices/src/qdCocoa.m
===================================================================
--- grDevices/src/qdCocoa.m	(revision 63830)
+++ grDevices/src/qdCocoa.m	(working copy)
@@ -30,6 +30,7 @@
 
 #include <R.h>
 #include <Rinternals.h>
+#include <Rinterface.h>
 #include <R_ext/QuartzDevice.h>
 #include <R_ext/eventloop.h>
 
@@ -94,6 +95,7 @@
 	NSColor *canvasColor = [view canvasColor];
 	[window setBackgroundColor:canvasColor ? canvasColor : [NSColor colorWithCalibratedRed:1.0 green:1.0 blue:1.0 alpha:0.5]];
 	[window setOpaque:NO];
+	[window setReleasedWhenClosed:NO];
 	ci->window = window;
 	
 	[window setDelegate: view];
@@ -110,6 +112,7 @@
         if (soleMenu) [NSApp setMainMenu:[[NSMenu alloc] init]];
 	mainMenu = [NSApp mainMenu];
 
+	/* This code is replaced by the MainMenu nib and can be removed */
 	/* File menu is tricky - it may have a different name in different localizations. Hence we use a trick - the File menu should be first and have the <Cmd><W> shortcut for "Close Window" by convenience */
 	BOOL hasFileMenu = NO;
 	if (!soleMenu) { /* in the case of a soleMenu we already know that we don't have it. Otherwise look for it. */
@@ -165,6 +168,7 @@
 	    else /* this should never be the case because we have added "File" menu, but just in case something goes wrong ... */
 		[mainMenu addItem: menuItem];
 	}
+    /* End code that can be removed */
 	
         if ([mainMenu indexOfItemWithTitle:@"Quartz"] < 0) { /* Quartz menu - if it doesn't exist, add it */
             unichar leftArrow = NSLeftArrowFunctionKey, rightArrow = NSRightArrowFunctionKey;
@@ -186,6 +190,7 @@
                     [[NSApp mainMenu] addItem:menuItem];
             }
         }
+        /* This code is replaced by the MainMenu nib and can be removed */
         if (soleMenu) { /* those should be standard if we have some menu */
             menu = [[NSMenu alloc] initWithTitle:@"Window"];
             
@@ -199,6 +204,7 @@
             [NSApp setWindowsMenu:menu];
             [menu release];
             [menuItem release];
+        /* End code that can be removed */
         }        
     } @catch (NSException *ex) {
 	/* on error release what we know about, issue a warning and return nil */
@@ -512,6 +518,8 @@
 
 #pragma mark --- Cocoa event loop ---
 
+static NSAutoreleasePool *global_pool = 0;
+
 /* --- Cocoa event loop
    This EL is enabled upon the first use of Quartz or alternatively using
    the QuartzCocoa_SetupEventLoop function */
@@ -548,7 +556,12 @@
                                           untilDate:nil
                                              inMode:NSDefaultRunLoopMode 
                                             dequeue:YES]))
+        {
             [NSApp sendEvent:event];
+            [NSApp updateWindows];
+            [global_pool release];
+            global_pool = [[NSAutoreleasePool alloc] init];
+        }
         el_pe_serial = el_serial;
     }
 }
@@ -638,7 +651,7 @@
 /*----- R Quartz interface ------*/
 
 static int cocoa_initialized = 0;
-static NSAutoreleasePool *global_pool = 0;
+static NSBundle *aquaBundle = nil;
 
 static void initialize_cocoa() {
     /* check embedding parameters to see if Rapp (or other Cocoa app) didn't do the work for us */
@@ -652,7 +665,14 @@
 	return;
     }
 
-    NSApplicationLoad();
+    /* Initialize a global NSBundle instance for retrieving Mac OS X resources */
+    NSString *bundlePath = [[NSString alloc] initWithFormat:@"%s/library/grDevices/aqua.bundle", R_HomeDir()];
+    aquaBundle = [[NSBundle alloc] initWithPath:bundlePath];
+    [bundlePath release];
+    /* Instantiate the main NSMenu and QuartzAppDelegate objects from the MainMenu nib */
+    NSApplication *application = [NSApplication sharedApplication];
+    NSNib *mainNib = [[NSNib alloc] initWithNibNamed:@"MainMenu" bundle:aquaBundle];
+    [mainNib instantiateNibWithOwner:application topLevelObjects:nil];
     global_pool = [[NSAutoreleasePool alloc] init];
     if (eflags & QP_Flags_CFLoop) {
 	cocoa_initialized = 1;
@@ -663,9 +683,19 @@
         QuartzCocoa_SetupEventLoop(QCF_SET_PEPTR|QCF_SET_FRONT, 100);
 
     
-    [NSApplication sharedApplication];
+    /* Close All seems to be added automatically, remove it because it currently crashes */
+    NSMenu *fileMenu = [[[NSApp mainMenu] itemWithTitle:@"File"] submenu];
+    NSMenuItem *closeAllMenuItem = [fileMenu itemWithTitle:@"Close All"];
+    if (closeAllMenuItem) [fileMenu removeItem:closeAllMenuItem];
+    /* Set the dock icon to be the R logo */
+    NSImage *iconImage = [[NSImage alloc] initWithContentsOfFile:[aquaBundle pathForResource:@"RLogo" ofType:@"png"]];
+    if (iconImage) [NSApp setApplicationIconImage:iconImage];
+    [iconImage release];
+    [application finishLaunching];
     cocoa_process_events();
     cocoa_initialized = 1;
+    /* During the next call to the run loop, check if we are running in the Terminal */
+    [[NSApp delegate] performSelector:@selector(checkIsRunningInTerminal) withObject:nil afterDelay:0.0];
 }
 
 static CGContextRef QuartzCocoa_GetCGContext(QuartzDesc_t dev, void *userInfo) {
@@ -937,3 +967,132 @@
         [[dev->view window] makeKeyAndOrderFront: dev->view];
     return qd;
 }
+
+#pragma mark --- Application Delegate ---
+
+@implementation QuartzAppDelegate
+
+- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender {
+
+    printf("\nType 'q()' to quit R.\n> ");
+    fflush(stdout);
+    [self showTerminal:self];
+    
+    return NSTerminateCancel;
+}
+
+- (IBAction)showAboutPanel:(id)sender {
+
+    NSImage *iconImage = [[NSImage alloc] initWithContentsOfFile:[aquaBundle pathForResource:@"RLogo" ofType:@"png"]];
+    
+    NSDictionary *optionsDictionary = [NSDictionary dictionaryWithObjectsAndKeys:
+        @"R", @"ApplicationName",
+        iconImage, @"ApplicationIcon",
+        nil
+    ];
+    
+    [NSApp orderFrontStandardAboutPanelWithOptions:optionsDictionary];
+    
+    [iconImage release];
+}
+
+- (void)checkIsRunningInTerminal
+{
+    if (!isatty(STDOUT_FILENO)) return;
+    char *tty = ttyname(STDOUT_FILENO);
+    
+    NSString *appleScriptSource = [[NSString alloc] initWithFormat:
+        @"tell application \"Terminal\"\n"
+        "	if it is running then\n"
+        "		repeat with theWindow in windows\n"
+        "			repeat with theTab in (tabs of theWindow)\n"
+        "				if tty of theTab is \"%s\" then\n"
+        "					return true\n"
+        "				end if\n"
+        "			end repeat\n"
+        "		end repeat\n"
+        "	end if\n"
+        "end tell\n"
+        "return false", tty];
+    
+    NSAppleScript *appleScript = [[NSAppleScript alloc] initWithSource:appleScriptSource];
+    NSDictionary *errorInfo = nil;
+    NSAppleEventDescriptor *appleEventDescriptor = [appleScript executeAndReturnError:&errorInfo];
+    
+    [appleScriptSource release];
+    [appleScript release];
+    
+    if (errorInfo) return;
+    
+    if ([appleEventDescriptor booleanValue])
+    {
+        _isRunningInTerminal = YES;
+        NSMenu *windowMenu = [[[NSApp mainMenu] itemWithTitle:@"Window"] submenu];
+        NSMenuItem *showTerminalMenuItem = [windowMenu itemWithTitle:@"Show Terminal"];
+        [showTerminalMenuItem setTarget:self];
+        [showTerminalMenuItem setAction:@selector(showTerminal:)];
+    }
+}
+
+- (BOOL)isRunningInTerminal
+{
+    return _isRunningInTerminal;
+}
+
+- (NSAppleScript *)showTerminalAppleScript
+{
+    if (_showTerminalAppleScript) return _showTerminalAppleScript;
+    
+    if (!self.isRunningInTerminal) return nil;
+    
+    if (!isatty(STDOUT_FILENO)) return nil;
+    char *tty = ttyname(STDOUT_FILENO);
+    
+    NSString *appleScriptSource = [[NSString alloc] initWithFormat:
+        @"tell application \"Terminal\"\n"
+        "	repeat with theWindow in windows\n"
+        "		repeat with theTab in (tabs of theWindow)\n"
+        "			if tty of theTab is \"%s\" then\n"
+        "				if selected tab of theWindow is not theTab then\n"
+        "					set selected tab of theWindow to theTab\n"
+        "				end if\n"
+        "				if index of theWindow is not 1 then\n"
+        "					set frontmost of theWindow to true\n"
+        "				end if\n"
+        "				activate\n"
+        "				return true\n"
+        "			end if\n"
+        "		end repeat\n"
+        "	end repeat\n"
+        "end tell\n"
+        "return false", tty];
+    
+    _showTerminalAppleScript = [[NSAppleScript alloc] initWithSource:appleScriptSource];
+    NSDictionary *errorInfo = nil;
+    if (![_showTerminalAppleScript compileAndReturnError:&errorInfo])
+    {
+        _showTerminalAppleScript = nil;
+        NSLog(@"Error Compiling Show Terminal AppleScript: %@", errorInfo);
+    }
+    
+    [appleScriptSource release];
+    
+    return _showTerminalAppleScript;
+}
+
+- (IBAction)showTerminal:(id)sender
+{
+    if (self.isRunningInTerminal)
+    {
+        NSDictionary *errorInfo = nil;
+        [self.showTerminalAppleScript executeAndReturnError:&errorInfo];
+    }
+}
+
+- (void)dealloc
+{
+    [_showTerminalAppleScript release];
+    [super dealloc];
+}
+
+@end