From 9b4babc225c0f01f1f99d1b6162c6368d5408475 Mon Sep 17 00:00:00 2001
From: Tim Sheridan <tghs@tghs.net>
Date: Fri, 15 Apr 2016 23:01:47 +0100
Subject: [PATCH] Terminal: Add tabs support

Mostly matches OS X Terminal.app behaviour (except tab rearranging).

* Create new tabs with #t
* Close tabs with #w
* Show tab bar for singleton tab with #T
* Switch between tabs with #{ and #}
* Move tabs around with #( and #) (until we can drag and drop!)
---
 system-apps/Terminal/Services.m       |   2 +-
 system-apps/Terminal/TerminalWindow.h |  18 ++-
 system-apps/Terminal/TerminalWindow.m | 272 ++++++++++++++++++++++++++++++++--
 system-apps/Terminal/main.m           |  83 ++++++++++-
 4 files changed, 355 insertions(+), 20 deletions(-)

diff --git a/system-apps/Terminal/Services.m b/system-apps/Terminal/Services.m
index efab7c6..38a58e0 100644
--- a/system-apps/Terminal/Services.m
+++ b/system-apps/Terminal/Services.m
@@ -264,7 +264,7 @@ of the License. See COPYING or main.m for more information.
 
 		NSDebugLLog(@"service",@"got window %@",twc);
 
-		[[twc terminalView] runProgram: @"/bin/sh"
+		[[twc frontTerminalView] runProgram: @"/bin/sh"
 			withArguments: [NSArray arrayWithObjects: @"-c",cmdline,nil]
 			initialInput: input==INPUT_STDIN?data:0];
 	}
diff --git a/system-apps/Terminal/TerminalWindow.h b/system-apps/Terminal/TerminalWindow.h
index 35ec233..cae26de 100644
--- a/system-apps/Terminal/TerminalWindow.h
+++ b/system-apps/Terminal/TerminalWindow.h
@@ -13,12 +13,14 @@ of the License. See COPYING or main.m for more information.
 @class TerminalView;
 
 #include <AppKit/NSWindowController.h>
+#include <AppKit/NSTabView.h>
 
 NSString *TerminalWindowNoMoreActiveWindowsNotification;
 
 @interface TerminalWindowController : NSWindowController
 {
-	TerminalView *tv;
+	NSMutableArray *terminal_views;
+	NSTabView *tab_view;
 	BOOL close_on_idle;
 }
 
@@ -30,10 +32,22 @@ NSString *TerminalWindowNoMoreActiveWindowsNotification;
 
 - init;
 
--(TerminalView *) terminalView;
+-(TerminalView *) frontTerminalView;
 
 -(void) setShouldCloseWhenIdle: (BOOL)should_close;
 
+-(BOOL) showTabBar;
+-(void) setShowTabBar:(BOOL)visible inWindow:(NSWindow *)window;
+
+-(void) newTerminalTabInWindow:(NSWindow *)window;
+-(void) closeTerminalTab:(TerminalView *)tv inWindow:(NSWindow *)window;
+
+-(void) showPreviousTab;
+-(void) showNextTab;
+
+-(void) moveTabLeft;
+-(void) moveTabRight;
+
 @end
 
 #endif
diff --git a/system-apps/Terminal/TerminalWindow.m b/system-apps/Terminal/TerminalWindow.m
index c63d252..1174f7a 100644
--- a/system-apps/Terminal/TerminalWindow.m
+++ b/system-apps/Terminal/TerminalWindow.m
@@ -17,6 +17,7 @@ of the License. See COPYING or main.m for more information.
 #include <Foundation/NSUserDefaults.h>
 #include <AppKit/NSApplication.h>
 #include <AppKit/NSScroller.h>
+#include <AppKit/NSTabViewItem.h>
 #include <AppKit/NSWindow.h>
 #include <GNUstepGUI/GSHbox.h>
 
@@ -114,7 +115,14 @@ NSString *TerminalWindowNoMoreActiveWindowsNotification=
 	[hb addView: scroller  enablingXResizing: NO];
 	[scroller release];
 
-	tv=[[TerminalView alloc] init];
+	tab_view = [[NSTabView alloc] init];
+	[tab_view setDelegate:self];
+	NSTabViewItem *item = [[NSTabViewItem alloc] init];
+	[item setLabel:@"Terminal"];
+	[item setView:hb];
+	[tab_view addTabViewItem:item];
+
+	TerminalView *tv = [[TerminalView alloc] init];
 	[tv setIgnoreResize: YES];
 	[tv setAutoresizingMask: NSViewHeightSizable|NSViewWidthSizable];
 	[tv setScroller: scroller];
@@ -123,6 +131,9 @@ NSString *TerminalWindowNoMoreActiveWindowsNotification=
 	[win makeFirstResponder: tv];
 	[tv setIgnoreResize: NO];
 
+	terminal_views = [[NSMutableArray alloc] init];
+	[terminal_views addObject:tv];
+
 	if ([TerminalWindowPrefs addYBorders])
 		[tv setBorder: 4 : 4];
 	else
@@ -131,16 +142,19 @@ NSString *TerminalWindowNoMoreActiveWindowsNotification=
 	[win setContentView: hb];
 	DESTROY(hb);
 
+	// Show tab bar if needed
+	[self setShowTabBar:[self showTabBar] inWindow:win];
+
 	[win release];
 
 	[[NSNotificationCenter defaultCenter]
 		addObserver: self
-		selector: @selector(_becameIdle)
+		selector: @selector(_becameIdle:)
 		name: TerminalViewBecameIdleNotification
 		object: tv];
 	[[NSNotificationCenter defaultCenter]
 		addObserver: self
-		selector: @selector(_becameNonIdle)
+		selector: @selector(_becameNonIdle:)
 		name: TerminalViewBecameNonIdleNotification
 		object: tv];
 	[[NSNotificationCenter defaultCenter]
@@ -153,11 +167,23 @@ NSString *TerminalWindowNoMoreActiveWindowsNotification=
 }
 
 
+-(void) _updateTitleFromTerminalView: (TerminalView *)tv
+{
+	NSInteger index = [terminal_views indexOfObjectIdenticalTo:tv];
+	[[tab_view tabViewItemAtIndex:index] setLabel:[tv windowTitle]];
+	[tab_view display];
+
+	if (tv == [self frontTerminalView]) {
+		[[self window] setTitle: [tv windowTitle]];
+		[[self window] setMiniwindowTitle: [tv miniwindowTitle]];
+		[[self window] setRepresentedFilename: [tv representedFilename]];
+	}
+}
+
 -(void) _updateTitle: (NSNotification *)n
 {
-	[[self window] setTitle: [tv windowTitle]];
-	[[self window] setMiniwindowTitle: [tv miniwindowTitle]];
-	[[self window] setRepresentedFilename: [tv representedFilename]];
+	TerminalView *tv = [n object];
+	[self _updateTitleFromTerminalView:tv];
 }
 
 
@@ -180,13 +206,14 @@ static NSMutableArray *idle_list;
 	[self autorelease];
 }
 
--(void) _becameIdle
+-(void) _becameIdle: (NSNotification *)n
 {
 	NSDebugLLog(@"idle",@"%@ _becameIdle",self);
 
 	if (close_on_idle)
 	{
-		[self close];
+		TerminalView *tv = [n object];
+		[self closeTerminalTab:tv inWindow:[self window]];
 		return;
 	}
 
@@ -208,7 +235,7 @@ static NSMutableArray *idle_list;
 	[isa checkActiveWindows];
 }
 
--(void) _becameNonIdle
+-(void) _becameNonIdle: (NSNotification *)n
 {
 	NSDebugLLog(@"idle",@"%@ _becameNonIdle",self);
 	[idle_list removeObject: self];
@@ -216,9 +243,11 @@ static NSMutableArray *idle_list;
 }
 
 
--(TerminalView *) terminalView
+-(TerminalView *) frontTerminalView
 {
-	return tv;
+	NSTabViewItem *item = [tab_view selectedTabViewItem];
+	NSInteger index = [tab_view indexOfTabViewItem:item];
+	return [terminal_views objectAtIndex:index];
 }
 
 -(void) setShouldCloseWhenIdle: (BOOL)should
@@ -272,5 +301,226 @@ static NSMutableArray *idle_list;
 	}
 }
 
+-(BOOL) showTabBar
+{
+	NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
+	return [defaults boolForKey:@"ShowTabBar"];
+}
+
+-(void) setShowTabBar:(BOOL)visible inWindow:(NSWindow *)window
+{
+	// TODO more explicit default value
+	NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
+	[defaults setBool:visible forKey:@"ShowTabBar"];
+	[defaults synchronize];
+
+	if ([tab_view numberOfTabViewItems] == 1) {
+		if (visible) {
+			[window setContentView:tab_view];
+			[tab_view selectFirstTabViewItem:nil];
+			[tab_view display];
+		} else {
+			NSView *view = [[tab_view selectedTabViewItem] view];
+			[window setContentView:view];
+		}
+	}
+}
+
+-(void) newTerminalTabInWindow:(NSWindow *)window
+{
+	if ([tab_view numberOfTabViewItems] == 1) {
+		[window setContentView: tab_view];
+	}
+
+	float fy;
+
+	{
+		NSSize size=[TerminalView characterCellSize];
+		fy=size.height;
+	}
+
+	NSTabViewItem *item = [[NSTabViewItem alloc] init];
+	[item setLabel:@"Terminal"];
+	[tab_view addTabViewItem:item];
+
+	GSHbox *hb=[[GSHbox alloc] init];
+
+	NSScroller *scroller=[[NSScroller alloc] initWithFrame: NSMakeRect(0,0,[NSScroller scrollerWidth],fy)];
+	[scroller setArrowsPosition: NSScrollerArrowsMaxEnd];
+	[scroller setEnabled: YES];
+	[scroller setAutoresizingMask: NSViewHeightSizable];
+	[hb addView: scroller  enablingXResizing: NO];
+	[scroller release];
+
+	TerminalView *tv = [[TerminalView alloc] init];
+	[tv setIgnoreResize: YES];
+	[tv setAutoresizingMask: NSViewHeightSizable|NSViewWidthSizable];
+	[tv setScroller: scroller];
+	[hb addView: tv];
+	[tv release];
+	[tv setIgnoreResize: NO];
+
+	[terminal_views addObject:tv];
+
+	if ([TerminalWindowPrefs addYBorders])
+		[tv setBorder: 4 : 4];
+	else
+		[tv setBorder: 4 : 0];
+
+	[item setView:hb];
+
+	[tab_view selectLastTabViewItem:nil];
+
+	[[NSNotificationCenter defaultCenter]
+		addObserver: self
+		selector: @selector(_becameIdle:)
+		name: TerminalViewBecameIdleNotification
+		object: tv];
+	[[NSNotificationCenter defaultCenter]
+		addObserver: self
+		selector: @selector(_becameNonIdle:)
+		name: TerminalViewBecameNonIdleNotification
+		object: tv];
+	[[NSNotificationCenter defaultCenter]
+		addObserver: self
+		selector: @selector(_updateTitle:)
+		name: TerminalViewTitleDidChangeNotification
+		object: tv];
+}
+
+-(void) closeTerminalTab:(TerminalView *)tv inWindow:(NSWindow *)window
+{
+	if ([tab_view numberOfTabViewItems] == 1) {
+		[window performClose:nil];
+		return;
+	}
+
+	NSTabViewItem *item = [tab_view selectedTabViewItem];
+	NSInteger index = [tab_view indexOfTabViewItem:item];
+
+	// Select new tab before removing old one
+	NSInteger first_tab_index = 0;
+	NSInteger last_tab_index = [tab_view numberOfTabViewItems] - 1;
+	// TODO A better tab selection heuristic (e.g. most recently used)
+	if (index == first_tab_index) {
+		[tab_view selectNextTabViewItem:nil];
+	} else if (index == last_tab_index) {
+		[tab_view selectPreviousTabViewItem:nil];
+	} else {
+		[tab_view selectNextTabViewItem:nil];
+	}
+
+	[tab_view removeTabViewItem:item];
+	[terminal_views removeObjectAtIndex:index];
+
+	if ([tab_view numberOfTabViewItems] == 1) {
+		NSTabViewItem *item = [tab_view tabViewItemAtIndex:0];
+		NSView *view = [item view];
+		[window setContentView: view];
+		[window makeFirstResponder:[terminal_views objectAtIndex:0]];
+
+		// Follow tab bar visible setting
+		[self setShowTabBar:[self showTabBar] inWindow:window];
+	}
+
+	[tab_view display];
+}
+
+-(void) showPreviousTab
+{
+	// Clamp
+	NSInteger first_tab_index = 0;
+	NSTabViewItem *item = [tab_view selectedTabViewItem];
+	NSInteger index = [tab_view indexOfTabViewItem:item];
+	if (index == first_tab_index) {
+		return;
+	}
+
+	[tab_view selectPreviousTabViewItem:nil];
+}
+
+-(void) showNextTab
+{
+	// Clamp
+	NSInteger last_tab_index = [tab_view numberOfTabViewItems] - 1;
+	NSTabViewItem *item = [tab_view selectedTabViewItem];
+	NSInteger index = [tab_view indexOfTabViewItem:item];
+	if (index == last_tab_index) {
+		return;
+	}
+
+	[tab_view selectNextTabViewItem:nil];
+}
+
+-(void) moveTabLeft
+{
+	// Clamp
+	NSInteger first_tab_index = 0;
+	NSTabViewItem *item = [tab_view selectedTabViewItem];
+	NSInteger index = [tab_view indexOfTabViewItem:item];
+	if (index == first_tab_index) {
+		return;
+	}
+
+	NSInteger destination_index = index - 1;
+
+	TerminalView *old_tv = [terminal_views objectAtIndex:destination_index];
+	TerminalView *new_tv = [terminal_views objectAtIndex:index];
+	[terminal_views replaceObjectAtIndex:destination_index withObject:new_tv];
+	[terminal_views replaceObjectAtIndex:index             withObject:old_tv];
+
+	NSTabViewItem *old_item = [tab_view tabViewItemAtIndex:destination_index];
+
+	[tab_view removeTabViewItem:old_item];
+	[tab_view insertTabViewItem:old_item atIndex:index];
+
+	[self _updateTitleFromTerminalView:old_tv];
+	[self _updateTitleFromTerminalView:new_tv];
+
+	[tab_view selectTabViewItemAtIndex:destination_index];
+
+	[tab_view display];
+}
+
+-(void) moveTabRight
+{
+	// Clamp
+	NSInteger last_tab_index = [tab_view numberOfTabViewItems] - 1;
+	NSTabViewItem *item = [tab_view selectedTabViewItem];
+	NSInteger index = [tab_view indexOfTabViewItem:item];
+	if (index == last_tab_index) {
+		return;
+	}
+
+	NSInteger destination_index = index + 1;
+
+	TerminalView *old_tv = [terminal_views objectAtIndex:destination_index];
+	TerminalView *new_tv = [terminal_views objectAtIndex:index];
+	[terminal_views replaceObjectAtIndex:destination_index withObject:new_tv];
+	[terminal_views replaceObjectAtIndex:index             withObject:old_tv];
+
+	NSTabViewItem *old_item = [tab_view tabViewItemAtIndex:destination_index];
+
+	[tab_view removeTabViewItem:old_item];
+	[tab_view insertTabViewItem:old_item atIndex:index];
+
+	[self _updateTitleFromTerminalView:old_tv];
+	[self _updateTitleFromTerminalView:new_tv];
+
+	[tab_view selectTabViewItemAtIndex:destination_index];
+
+	[tab_view display];
+}
+
+- (void) tabView: (NSTabView*)tabView didSelectTabViewItem: (NSTabViewItem*)tabViewItem
+{
+	NSInteger tab_number = [tabView indexOfTabViewItem:tabViewItem];
+
+	TerminalView *tv = [terminal_views objectAtIndex:tab_number];
+	[self _updateTitleFromTerminalView:tv];
+
+	[[self window] makeFirstResponder:[terminal_views objectAtIndex:tab_number]];
+}
+
 @end
 
diff --git a/system-apps/Terminal/main.m b/system-apps/Terminal/main.m
index fec4a45..a151ac1 100644
--- a/system-apps/Terminal/main.m
+++ b/system-apps/Terminal/main.m
@@ -28,6 +28,7 @@ copyright 2002, 2003 Alexander Malmberg <alexander@malmberg.org>
 #import <Foundation/NSDictionary.h>
 #import <AppKit/NSApplication.h>
 #import <AppKit/NSControl.h>
+#import <AppKit/NSEvent.h>
 #import <AppKit/NSMenu.h>
 #import <AppKit/NSMenuItem.h>
 #import <AppKit/NSPanel.h>
@@ -167,9 +168,12 @@ copyright 2002, 2003 Alexander Malmberg <alexander@malmberg.org>
 		keyEquivalent: @"n"];
 	[menu setSubmenu: m forItem: [menu addItemWithTitle: _(@"Terminal")]];
 	[m release];*/
-	[menu addItemWithTitle: _(@"New terminal")
+	[menu addItemWithTitle: _(@"New window")
 		action: @selector(openWindow:)
 		keyEquivalent: @"n"];
+	[menu addItemWithTitle: _(@"New tab")
+		action: @selector(openTab:)
+		keyEquivalent: @"t"];
 
 	/* 'Edit' menu */
 	m=[[NSMenu alloc] init];
@@ -185,11 +189,34 @@ copyright 2002, 2003 Alexander Malmberg <alexander@malmberg.org>
 	[menu setSubmenu: m forItem: [menu addItemWithTitle: _(@"Edit")]];
 	[m release];
 
+	/* 'View' menu */
+	m=[[NSMenu alloc] init];
+	[m addItemWithTitle: _(@"Show tab bar")
+		action: @selector(showTabBarToggle:)
+		keyEquivalent: @"T"];
+	[menu setSubmenu: m forItem: [menu addItemWithTitle: _(@"View")]];
+	[m release];
+
 	/* 'Windows' menu */
 	m=[[NSMenu alloc] init];
-	[m addItemWithTitle: _(@"Close")
+	[m addItemWithTitle: _(@"Close window")
 		action: @selector(performClose:)
+		keyEquivalent: @"W"];
+	[m addItemWithTitle: _(@"Close tab")
+		action: @selector(closeTab:)
 		keyEquivalent: @"w"];
+	[m addItemWithTitle: _(@"Show previous tab")
+		action: @selector(showPreviousTab:)
+		keyEquivalent: @"{"];
+	[m addItemWithTitle: _(@"Show next tab")
+		action: @selector(showNextTab:)
+		keyEquivalent: @"}"];
+	[m addItemWithTitle: _(@"Move tab left")
+		action: @selector(moveTabLeft:)
+		keyEquivalent: @"("];
+	[m addItemWithTitle: _(@"Move tab right")
+		action: @selector(moveTabRight:)
+		keyEquivalent: @")"];
 	[m addItemWithTitle: _(@"Miniaturize all")
 		action: @selector(miniaturizeAll:)
 		keyEquivalent: @"m"];
@@ -224,7 +251,51 @@ copyright 2002, 2003 Alexander Malmberg <alexander@malmberg.org>
 {
 	TerminalWindowController *twc;
 	twc=[TerminalWindowController newTerminalWindow];
-	[[twc terminalView] runShell];
+	[[twc frontTerminalView] runShell];
+}
+
+-(void) openTab: (id)sender
+{
+	TerminalWindowController *twc = [[NSApp keyWindow] windowController];
+	[twc newTerminalTabInWindow:[NSApp keyWindow]];
+	[[twc frontTerminalView] runShell];
+}
+
+-(void) closeTab: (id)sender
+{
+	TerminalWindowController *twc = [[NSApp keyWindow] windowController];
+	TerminalView *tv = [twc frontTerminalView];
+	[twc closeTerminalTab:tv inWindow:[NSApp keyWindow]];
+}
+
+-(void) showTabBarToggle: (id)sender
+{
+	TerminalWindowController *twc = [[NSApp keyWindow] windowController];
+	[twc setShowTabBar:(! [twc showTabBar]) inWindow:[NSApp keyWindow]];
+}
+
+-(void) showPreviousTab: (id)sender
+{
+	TerminalWindowController *twc = [[NSApp keyWindow] windowController];
+	[twc showPreviousTab];
+}
+
+-(void) showNextTab: (id)sender
+{
+	TerminalWindowController *twc = [[NSApp keyWindow] windowController];
+	[twc showNextTab];
+}
+
+-(void) moveTabLeft: (id)sender
+{
+	TerminalWindowController *twc = [[NSApp keyWindow] windowController];
+	[twc moveTabLeft];
+}
+
+-(void) moveTabRight: (id)sender
+{
+	TerminalWindowController *twc = [[NSApp keyWindow] windowController];
+	[twc moveTabRight];
 }
 
 
@@ -245,7 +316,7 @@ copyright 2002, 2003 Alexander Malmberg <alexander@malmberg.org>
 		cmdline=[args componentsJoinedByString: @" "];
 
 		twc=[TerminalWindowController newTerminalWindow];
-		[[twc terminalView] runProgram: @"/bin/sh"
+		[[twc frontTerminalView] runProgram: @"/bin/sh"
 			withArguments: [NSArray arrayWithObjects: @"-c",cmdline,nil]
 			initialInput: nil];
                 [twc release];
@@ -405,7 +476,7 @@ copyright 2002, 2003 Alexander Malmberg <alexander@malmberg.org>
 	[NSApp activateIgnoringOtherApps: YES];
 
 	twc=[TerminalWindowController newTerminalWindow];
-	[[twc terminalView] runProgram: filename
+	[[twc frontTerminalView] runProgram: filename
 		withArguments: nil
 		initialInput: nil];
 
@@ -442,7 +513,7 @@ copyright 2002, 2003 Alexander Malmberg <alexander@malmberg.org>
 		}
 	}
 
-	[[twc terminalView] runProgram: path
+	[[twc frontTerminalView] runProgram: path
 		withArguments: args
 		inDirectory: directory
 		initialInput: nil
-- 
2.1.4

