diff --git a/aqua/iTerm2/Portfile b/aqua/iTerm2/Portfile index 4848771073241..d8e9aa2c6f776 100644 --- a/aqua/iTerm2/Portfile +++ b/aqua/iTerm2/Portfile @@ -10,6 +10,7 @@ if {[info exists use_xcode]} { if {[vercmp ${os.version} 17.0.0] < 0} { version 3.2.0 + revision 0 checksums \ rmd160 07915ff5db0545c0c059f47e7f71761e023a26e1 \ sha256 017aff348352369abcc994caaca0f6112e1f17c4d65041acdb9f19830b2b96bd \ @@ -17,11 +18,13 @@ if {[vercmp ${os.version} 17.0.0] < 0} { patchfiles patch-Makefile.diff } else { version 3.3.5 + revision 1 checksums \ rmd160 612cb41be30fe2940bc300055afbb77938d4a19d \ sha256 6ce9e5650fa4245fc5b702c58cdea43b6d3fe995abc7b52520377f840fd8a492 \ size 18852672 patchfiles patch-Makefile-XC10.diff + patchfiles-append patch-CVE-2019-9535.diff } github.setup gnachman iTerm2 ${version} v diff --git a/aqua/iTerm2/files/patch-CVE-2019-9535.diff b/aqua/iTerm2/files/patch-CVE-2019-9535.diff new file mode 100644 index 0000000000000..0fd7e45590a5e --- /dev/null +++ b/aqua/iTerm2/files/patch-CVE-2019-9535.diff @@ -0,0 +1,1840 @@ +From 538d570ea54614d3a2b5724f820953d717fbeb0c Mon Sep 17 00:00:00 2001 +From: George Nachman +Date: Wed, 25 Sep 2019 23:13:00 -0700 +Subject: [PATCH] Do not send server-controlled values in tmux integration + mode. + +CVE-2019-9535 + +- Use session number everywhere rather than session name +- Do not poll tmux for the set-titles-string, status-left, and status-right and + then request the values of the returned format strings. Use ${T:} eval + instead. These features are now only available for tmux 2.9 and later. +- Hex-encode options saved in the tmux server to make them unexploitable (e.g., + hotkeys, window affinities, window origins, etc.). The old values are + accepted as inputs but will never be produced as output. +--- + iTerm2.xcodeproj/project.pbxproj | 8 + + sources/PTYSession.m | 28 ++- + sources/PTYTab.m | 13 +- + sources/PseudoTerminal.m | 9 +- + sources/TmuxController.h | 47 ++-- + sources/TmuxController.m | 305 +++++++++++++++----------- + sources/TmuxDashboardController.m | 163 +++++++------- + sources/TmuxSessionsTable.h | 30 +-- + sources/TmuxSessionsTable.m | 106 ++++----- + sources/TmuxWindowsTable.h | 2 +- + sources/TmuxWindowsTable.m | 21 +- + sources/iTermInitialDirectory+Tmux.h | 2 +- + sources/iTermInitialDirectory+Tmux.m | 20 +- + sources/iTermTmuxOptionMonitor.h | 4 + + sources/iTermTmuxOptionMonitor.m | 16 +- + sources/iTermTmuxSessionObject.h | 17 ++ + sources/iTermTmuxSessionObject.m | 12 + + sources/iTermTmuxStatusBarMonitor.h | 1 + + sources/iTermTmuxStatusBarMonitor.m | 28 +-- + sources/iTermWorkingDirectoryPoller.m | 1 + + 20 files changed, 471 insertions(+), 362 deletions(-) + create mode 100644 sources/iTermTmuxSessionObject.h + create mode 100644 sources/iTermTmuxSessionObject.m + +diff --git iTerm2.xcodeproj/project.pbxproj iTerm2.xcodeproj/project.pbxproj +index 7e2f86a..32e1df2 100644 +--- iTerm2.xcodeproj/project.pbxproj ++++ iTerm2.xcodeproj/project.pbxproj +@@ -1376,6 +1376,8 @@ + 53E184F21FE32F2800DB78F3 /* iTermMetalBufferPool.m in Sources */ = {isa = PBXBuildFile; fileRef = 53E184F01FE32F2800DB78F3 /* iTermMetalBufferPool.m */; }; + 53E8F36F2244A58800F3770F /* iTermActionsModel.h in Headers */ = {isa = PBXBuildFile; fileRef = 53E8F36D2244A58800F3770F /* iTermActionsModel.h */; }; + 53E8F3702244A58800F3770F /* iTermActionsModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 53E8F36E2244A58800F3770F /* iTermActionsModel.m */; }; ++ 53E98E7B233C6B760094D8A9 /* iTermTmuxSessionObject.h in Headers */ = {isa = PBXBuildFile; fileRef = 53E98E79233C6B760094D8A9 /* iTermTmuxSessionObject.h */; }; ++ 53E98E7C233C6B760094D8A9 /* iTermTmuxSessionObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 53E98E7A233C6B760094D8A9 /* iTermTmuxSessionObject.m */; }; + 53E9DFE0220D518E0070C9C0 /* AlertTrigger.m in Sources */ = {isa = PBXBuildFile; fileRef = 1D9DCC15142D7FC10016228A /* AlertTrigger.m */; }; + 53E9DFE1220D51D50070C9C0 /* CoprocessTrigger.m in Sources */ = {isa = PBXBuildFile; fileRef = 1D9DDD91142E5AC600275650 /* CoprocessTrigger.m */; }; + 53E9DFE2220D52730070C9C0 /* PasswordTrigger.m in Sources */ = {isa = PBXBuildFile; fileRef = 1DABA03219253FEA00A228D8 /* PasswordTrigger.m */; }; +@@ -4429,6 +4431,8 @@ + 53E184F01FE32F2800DB78F3 /* iTermMetalBufferPool.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = iTermMetalBufferPool.m; sourceTree = ""; }; + 53E8F36D2244A58800F3770F /* iTermActionsModel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = iTermActionsModel.h; sourceTree = ""; }; + 53E8F36E2244A58800F3770F /* iTermActionsModel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = iTermActionsModel.m; sourceTree = ""; }; ++ 53E98E79233C6B760094D8A9 /* iTermTmuxSessionObject.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = iTermTmuxSessionObject.h; sourceTree = ""; }; ++ 53E98E7A233C6B760094D8A9 /* iTermTmuxSessionObject.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = iTermTmuxSessionObject.m; sourceTree = ""; }; + 53EBF29B1DCBFF7C00766613 /* iTermAPIServer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = iTermAPIServer.h; sourceTree = ""; }; + 53EBF29C1DCBFF7C00766613 /* iTermAPIServer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = iTermAPIServer.m; sourceTree = ""; }; + 53EBF29F1DCCF4AC00766613 /* iTermIPV4Address.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = iTermIPV4Address.h; sourceTree = ""; }; +@@ -7010,6 +7014,8 @@ + A6F718B4226438FC0053488E /* iTermInitialDirectory+Tmux.m */, + 53D68F7E2283EE7C0018710D /* iTermTmuxLayoutBuilder.h */, + 53D68F7F2283EE7C0018710D /* iTermTmuxLayoutBuilder.m */, ++ 53E98E79233C6B760094D8A9 /* iTermTmuxSessionObject.h */, ++ 53E98E7A233C6B760094D8A9 /* iTermTmuxSessionObject.m */, + ); + name = tmux; + sourceTree = ""; +@@ -9987,6 +9993,7 @@ + A6153D4C21F30A9C002976FC /* iTermJobTreeViewController.h in Headers */, + 5370678F21C9D2780088D0F3 /* SIGSHA2VerificationAlgorithm.h in Headers */, + A66719161DCE36C3000CE608 /* NSURL+iTerm.h in Headers */, ++ 53E98E7B233C6B760094D8A9 /* iTermTmuxSessionObject.h in Headers */, + A66719171DCE36C3000CE608 /* NSDate+iTerm.h in Headers */, + A6BCAAD821F6F53E0000CD29 /* iTermTextPopoverViewController.h in Headers */, + A66719181DCE36C3000CE608 /* iTermBaseHotKey.h in Headers */, +@@ -12429,6 +12436,7 @@ + A6AC5D241E9036D70097C0A7 /* iTermURLMark.m in Sources */, + 535EA51420D8C1EB00FC81E0 /* iTermProfilesWindowController.m in Sources */, + A6A4866F20B67A1600493302 /* iTermSemanticHistoryPrefsController.m in Sources */, ++ 53E98E7C233C6B760094D8A9 /* iTermTmuxSessionObject.m in Sources */, + A66719621DCE3772000CE608 /* iTermSocketAddress.m in Sources */, + A6A4867E20B67FD100493302 /* SmartSelectionController.m in Sources */, + 53AFFC8E1DD2A04100E6CEC6 /* iTermLSOF.m in Sources */, +diff --git sources/PTYSession.m sources/PTYSession.m +index 9b64e03..0332d8c 100644 +--- sources/PTYSession.m ++++ sources/PTYSession.m +@@ -4726,7 +4726,7 @@ ITERM_WEAKLY_REFERENCEABLE + } else { + // Legacy code path for pre tmux 2.6 + [_tmuxController renameWindowWithId:_delegate.tmuxWindow +- inSession:nil ++ inSessionNumber:nil + toName:profile[KEY_NAME]]; + } + _tmuxTitleOutOfSync = NO; +@@ -5910,14 +5910,18 @@ scrollToFirstResult:(BOOL)scrollToFirstResult { + + - (void)installTmuxStatusBarMonitor { + assert(!_tmuxStatusBarMonitor); +- _tmuxStatusBarMonitor = [[iTermTmuxStatusBarMonitor alloc] initWithGateway:_tmuxController.gateway +- scope:self.variablesScope]; +- _tmuxStatusBarMonitor.active = [iTermProfilePreferences boolForKey:KEY_SHOW_STATUS_BAR inProfile:self.profile]; +- if ([iTermPreferences boolForKey:kPreferenceKeyUseTmuxStatusBar] || +- [iTermStatusBarLayout shouldOverrideLayout:self.profile[KEY_STATUS_BAR_LAYOUT]]) { +- [self setSessionSpecificProfileValues:@{ KEY_STATUS_BAR_LAYOUT: [[iTermStatusBarLayout tmuxLayoutWithController:_tmuxController +- scope:nil +- window:self.delegate.tmuxWindow] dictionaryValue] }]; ++ ++ if (_tmuxController.gateway.minimumServerVersion.doubleValue >= 2.9) { ++ // Just use the built-in status bar for older versions of tmux because they don't support ${T:xxx} or ${E:xxx} ++ _tmuxStatusBarMonitor = [[iTermTmuxStatusBarMonitor alloc] initWithGateway:_tmuxController.gateway ++ scope:self.variablesScope]; ++ _tmuxStatusBarMonitor.active = [iTermProfilePreferences boolForKey:KEY_SHOW_STATUS_BAR inProfile:self.profile]; ++ if ([iTermPreferences boolForKey:kPreferenceKeyUseTmuxStatusBar] || ++ [iTermStatusBarLayout shouldOverrideLayout:self.profile[KEY_STATUS_BAR_LAYOUT]]) { ++ [self setSessionSpecificProfileValues:@{ KEY_STATUS_BAR_LAYOUT: [[iTermStatusBarLayout tmuxLayoutWithController:_tmuxController ++ scope:nil ++ window:self.delegate.tmuxWindow] dictionaryValue] }]; ++ } + } + } + +@@ -5929,7 +5933,8 @@ scrollToFirstResult:(BOOL)scrollToFirstResult { + } + __weak __typeof(self) weakSelf = self; + _tmuxTitleMonitor = [[iTermTmuxOptionMonitor alloc] initWithGateway:_tmuxController.gateway +- scope:self.variablesScope ++ scope:self.variablesScope ++ fallbackVariableName:nil + format:@"#{pane_title}" + target:[NSString stringWithFormat:@"%%%@", @(self.tmuxPane)] + variableName:iTermVariableKeySessionTmuxPaneTitle +@@ -6308,8 +6313,7 @@ scrollToFirstResult:(BOOL)scrollToFirstResult { + [_tmuxController ping]; + [_tmuxController validateOptions]; + [_tmuxController checkForUTF8]; +- [_tmuxController guessVersion]; +- [_tmuxController loadTitleFormat]; ++ [_tmuxController guessVersion]; // NOTE: This kicks off more stuff that depends on knowing the version number. + } + + - (void)tmuxInitialCommandDidFailWithError:(NSString *)error { +diff --git sources/PTYTab.m sources/PTYTab.m +index a8f8b48..6c4766e 100644 +--- sources/PTYTab.m ++++ sources/PTYTab.m +@@ -3690,11 +3690,12 @@ static void SetAgainstGrainDim(BOOL isVertical, NSSize *dest, CGFloat value) { + return; + } + _tmuxTitleMonitor = [[iTermTmuxOptionMonitor alloc] initWithGateway:tmuxController_.gateway +- scope:self.variablesScope +- format:tmuxController_.setTitlesString +- target:[NSString stringWithFormat:@"@%@", @(self.tmuxWindow)] +- variableName:iTermVariableKeyTabTmuxWindowTitle +- block:nil]; ++ scope:self.variablesScope ++ fallbackVariableName:iTermVariableKeySessionWindowName ++ format:@"#{T:set-titles-string}" ++ target:[NSString stringWithFormat:@"@%@", @(self.tmuxWindow)] ++ variableName:iTermVariableKeyTabTmuxWindowTitle ++ block:nil]; + [_tmuxTitleMonitor updateOnce]; + if (self.titleOverride.length == 0) { + // Show the tmux window title if both the tmux option set-titles is on and the user hasn't +@@ -4312,7 +4313,7 @@ static void SetAgainstGrainDim(BOOL isVertical, NSSize *dest, CGFloat value) { + if (self.tmuxTab) { + if (titleOverride) { + [self.tmuxController renameWindowWithId:self.tmuxWindow +- inSession:nil ++ inSessionNumber:nil + toName:titleOverride]; + } + return; +diff --git sources/PseudoTerminal.m sources/PseudoTerminal.m +index b90e733..24f041a 100644 +--- sources/PseudoTerminal.m ++++ sources/PseudoTerminal.m +@@ -3018,8 +3018,8 @@ ITERM_WEAKLY_REFERENCEABLE + if ([arrangement objectForKey:TERMINAL_GUID] && + [[arrangement objectForKey:TERMINAL_GUID] isKindOfClass:[NSString class]]) { + NSString *savedGUID = [arrangement objectForKey:TERMINAL_GUID]; +- if ([[iTermController sharedInstance] terminalWithGuid:savedGUID]) { +- // Refuse to create a window with an already-used guid. ++ if ([[iTermController sharedInstance] terminalWithGuid:savedGUID] || ![self stringIsValidTerminalGuid:savedGUID]) { ++ // Refuse to create a window with an already-used or invalid guid. + self.terminalGuid = [NSString stringWithFormat:@"pty-%@", [NSString uuid]]; + } else { + self.terminalGuid = savedGUID; +@@ -3039,6 +3039,11 @@ ITERM_WEAKLY_REFERENCEABLE + return YES; + } + ++- (BOOL)stringIsValidTerminalGuid:(NSString *)string { ++ NSCharacterSet *characterSet = [NSCharacterSet characterSetWithCharactersInString:@"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-"]; ++ return [string rangeOfCharacterFromSet:characterSet.invertedSet].location == NSNotFound; ++} ++ + - (BOOL)restoreTabsFromArrangement:(NSDictionary *)arrangement sessions:(NSArray *)sessions { + for (NSDictionary *tabArrangement in arrangement[TERMINAL_ARRANGEMENT_TABS]) { + NSDictionary *sessionMap = nil; +diff --git sources/TmuxController.h sources/TmuxController.h +index f27bacc..f1e5ad3 100644 +--- sources/TmuxController.h ++++ sources/TmuxController.h +@@ -8,6 +8,7 @@ + #import + #import "ProfileModel.h" + #import "iTermInitialDirectory.h" ++#import "iTermTmuxSessionObject.h" + #import "TmuxGateway.h" + #import "WindowControllerInterface.h" + +@@ -33,7 +34,7 @@ extern NSString *const kTmuxControllerWindowDidClose; + extern NSString *const kTmuxControllerAttachedSessionDidChange; + // Posted when a session changes name + extern NSString *const kTmuxControllerSessionWasRenamed; +-// Posted when set-titles-string option changes. Object is tmux controller. ++// Posted when set-titles option changes. Object is tmux controller. + extern NSString *const kTmuxControllerDidFetchSetTitlesStringOption; + + @interface TmuxController : NSObject +@@ -41,7 +42,7 @@ extern NSString *const kTmuxControllerDidFetchSetTitlesStringOption; + @property(nonatomic, readonly) TmuxGateway *gateway; + @property(nonatomic, retain) NSMutableDictionary *windowPositions; + @property(nonatomic, copy) NSString *sessionName; +-@property(nonatomic, retain) NSArray *sessions; ++@property(nonatomic, copy) NSArray *sessionObjects; + @property(nonatomic, assign) BOOL ambiguousIsDoubleWidth; + @property(nonatomic, assign) NSInteger unicodeVersion; + @property(nonatomic, readonly) NSString *clientName; +@@ -53,7 +54,6 @@ extern NSString *const kTmuxControllerDidFetchSetTitlesStringOption; + @property(nonatomic, readonly) NSDictionary *sharedFontOverrides; + @property(nonatomic, readonly) NSString *sessionGuid; + @property(nonatomic, readonly) BOOL variableWindowSize; +-@property(nonatomic, readonly) NSString *setTitlesString; + @property(nonatomic, readonly) BOOL shouldSetTitles; + @property(nonatomic, readonly) BOOL serverIsLocal; + +@@ -120,9 +120,9 @@ extern NSString *const kTmuxControllerDidFetchSetTitlesStringOption; + scope:(iTermVariableScope *)scope + initialDirectory:(iTermInitialDirectory *)initialDirectory; + +-- (void)newWindowInSession:(NSString *)targetSession +- scope:(iTermVariableScope *)scope +- initialDirectory:(iTermInitialDirectory *)initialDirectory; ++- (void)newWindowInSessionNumber:(NSNumber *)sessionNumber ++ scope:(iTermVariableScope *)scope ++ initialDirectory:(iTermInitialDirectory *)initialDirectory; + + - (void)selectPane:(int)windowPane; + +@@ -150,9 +150,11 @@ extern NSString *const kTmuxControllerDidFetchSetTitlesStringOption; + + - (void)killWindowPane:(int)windowPane; + - (void)killWindow:(int)window; +-- (void)unlinkWindowWithId:(int)windowId inSession:(NSString *)sessionName; ++- (void)unlinkWindowWithId:(int)windowId; + - (void)requestDetach; +-- (void)renameWindowWithId:(int)windowId inSession:(NSString *)sessionName toName:(NSString *)newName; ++- (void)renameWindowWithId:(int)windowId ++ inSessionNumber:(NSNumber *)sessionNumber ++ toName:(NSString *)newName; + - (BOOL)canRenamePane; + - (void)renamePane:(int)windowPane toTitle:(NSString *)newTitle; + - (void)setHotkeyForWindowPane:(int)windowPane to:(NSDictionary *)hotkey; +@@ -162,21 +164,25 @@ extern NSString *const kTmuxControllerDidFetchSetTitlesStringOption; + - (NSString *)tabColorStringForWindowPane:(int)windowPane; + + - (void)linkWindowId:(int)windowId +- inSession:(NSString *)sessionName +- toSession:(NSString *)targetSession; ++ inSessionNumber:(int)sessionNumber ++ toSessionNumber:(int)targetSession; ++ + - (void)moveWindowId:(int)windowId +- inSession:(NSString *)sessionName +- toSession:(NSString *)targetSession; ++ inSessionNumber:(int)sessionNumber ++ toSessionNumber:(int)targetSessionNumber; ++ ++- (void)renameSessionNumber:(int)sessionNumber ++ to:(NSString *)newName; + +-- (void)renameSession:(NSString *)oldName to:(NSString *)newName; +-- (void)killSession:(NSString *)sessionName; +-- (void)attachToSession:(NSString *)sessionName; ++- (void)killSessionNumber:(int)sessionNumber; ++- (void)attachToSessionWithNumber:(int)sessionNumber; + - (void)addSessionWithName:(NSString *)sessionName; +-// NOTE: If the session name is bogus (or any other error occurs) the selector will not be called. +-- (void)listWindowsInSession:(NSString *)sessionName +- target:(id)target +- selector:(SEL)selector +- object:(id)object; ++// NOTE: If anything goes wrong the selector will not be called. ++- (void)listWindowsInSessionNumber:(int)sessionNumber ++ target:(id)target ++ selector:(SEL)selector ++ object:(id)object; ++ + - (void)listSessions; + - (void)saveAffinities; + - (void)saveWindowOrigins; +@@ -200,7 +206,6 @@ extern NSString *const kTmuxControllerDidFetchSetTitlesStringOption; + - (void)setLayoutInWindow:(int)window toLayout:(NSString *)layout; + - (NSArray *)clientSessions; + +-- (void)setSize:(NSSize)size windows:(NSArray *)windows; + - (void)setSize:(NSSize)size window:(int)window; + + @end +diff --git sources/TmuxController.m sources/TmuxController.m +index 08cbd91..77b0424 100644 +--- sources/TmuxController.m ++++ sources/TmuxController.m +@@ -20,6 +20,7 @@ + #import "iTermShortcut.h" + #import "iTermTuple.h" + #import "NSArray+iTerm.h" ++#import "NSData+iTerm.h" + #import "NSFont+iTerm.h" + #import "NSStringITerm.h" + #import "PreferencePanel.h" +@@ -42,6 +43,12 @@ NSString *const kTmuxControllerWindowDidClose = @"kTmuxControllerWindowDidClose" + NSString *const kTmuxControllerSessionWasRenamed = @"kTmuxControllerSessionWasRenamed"; + NSString *const kTmuxControllerDidFetchSetTitlesStringOption = @"kTmuxControllerDidFetchSetTitlesStringOption"; + ++static NSString *const iTermTmuxControllerEncodingPrefixHotkeys = @"h_"; ++static NSString *const iTermTmuxControllerEncodingPrefixTabColors = @"t_"; ++static NSString *const iTermTmuxControllerEncodingPrefixAffinities = @"a_"; ++static NSString *const iTermTmuxControllerEncodingPrefixOrigins = @"o_"; ++static NSString *const iTermTmuxControllerEncodingPrefixHidden = @"i_"; ++ + // Unsupported global options: + static NSString *const kAggressiveResize = @"aggressive-resize"; + +@@ -81,7 +88,7 @@ static NSString *kListWindowsFormat = @"\"#{session_name}\t#{window_id}\t" + TmuxGateway *gateway_; + NSMutableDictionary *windowPanes_; // paneId -> PTYSession * + NSMutableDictionary *_windowStates; // Key is window number +- NSArray *sessions_; ++ NSArray *sessionObjects_; + int numOutstandingWindowResizes_; + NSMutableDictionary *windowPositions_; + NSSize lastSize_; // last size for windowDidChange: +@@ -97,7 +104,7 @@ static NSString *kListWindowsFormat = @"\"#{session_name}\t#{window_id}\t" + BOOL windowOriginsDirty_; + BOOL haveOutstandingSaveWindowOrigins_; + NSMutableDictionary *origins_; // window id -> NSValue(Point) window origin +- NSMutableSet *hiddenWindows_; ++ NSMutableSet *hiddenWindows_; + NSTimer *listSessionsTimer_; // Used to do a cancelable delayed perform of listSessions. + NSTimer *listWindowsTimer_; // Used to do a cancelable delayed perform of listWindows. + BOOL ambiguousIsDoubleWidth_; +@@ -119,7 +126,7 @@ static NSString *kListWindowsFormat = @"\"#{session_name}\t#{window_id}\t" + @synthesize gateway = gateway_; + @synthesize windowPositions = windowPositions_; + @synthesize sessionName = sessionName_; +-@synthesize sessions = sessions_; ++@synthesize sessionObjects = sessionObjects_; + @synthesize ambiguousIsDoubleWidth = ambiguousIsDoubleWidth_; + @synthesize sessionId = sessionId_; + +@@ -180,8 +187,7 @@ static NSDictionary *iTermTmuxControllerDefaultFontOverridesFromProfile(Profile + [_sharedFontOverrides release]; + [_pendingWindows release]; + [sessionName_ release]; +- [sessions_ release]; +- [_setTitlesString release]; ++ [sessionObjects_ release]; + + [super dealloc]; + } +@@ -392,16 +398,22 @@ static NSDictionary *iTermTmuxControllerDefaultFontOverridesFromProfile(Profile + } + [windowsToOpen addObject:record]; + } ++ BOOL tooMany = NO; + if (windowsToOpen.count > [iTermPreferences intForKey:kPreferenceKeyTmuxDashboardLimit]) { + DLog(@"There are too many windows to open so just show the dashboard"); + haveHidden = YES; ++ tooMany = YES; + [windowsToOpen removeAllObjects]; + } + if (haveHidden) { + DLog(@"Hidden windows existing, showing dashboard"); + [[TmuxDashboardController sharedInstance] showWindow:nil]; + [[[TmuxDashboardController sharedInstance] window] makeKeyAndOrderFront:nil]; +- [[iTermNotificationController sharedInstance] notify:@"Too many tmux windows!" withDescription:@"Use the tmux dashboard to select which to open."]; ++ if (tooMany) { ++ [[iTermNotificationController sharedInstance] notify:@"Too many tmux windows!" withDescription:@"Use the tmux dashboard to select which to open."]; ++ } else { ++ [[iTermNotificationController sharedInstance] notify:@"Some tmux windows were hidden." withDescription:@"Use the tmux dashboard to select which to open."]; ++ } + } + for (NSArray *record in windowsToOpen) { + DLog(@"Open window %@", record); +@@ -459,7 +471,7 @@ static NSDictionary *iTermTmuxControllerDefaultFontOverridesFromProfile(Profile + NSString *setSizeCommand = [NSString stringWithFormat:@"refresh-client -C %d,%d", + size.width, [self adjustHeightForStatusBar:size.height]]; + NSString *listWindowsCommand = [NSString stringWithFormat:@"list-windows -F %@", kListWindowsFormat]; +- NSString *listSessionsCommand = @"list-sessions -F \"#{session_name}\""; ++ NSString *listSessionsCommand = @"list-sessions -F \"#{session_id} #{session_name}\""; + NSString *getAffinitiesCommand = [NSString stringWithFormat:@"show -v -q -t $%d @affinities", sessionId_]; + NSString *getOriginsCommand = [NSString stringWithFormat:@"show -v -q -t $%d @origins", sessionId_]; + NSString *getHotkeysCommand = [NSString stringWithFormat:@"show -v -q -t $%d @hotkeys", sessionId_]; +@@ -715,10 +727,6 @@ static NSDictionary *iTermTmuxControllerDefaultFontOverridesFromProfile(Profile + } + } + +-- (void)setSize:(NSSize)size windows:(NSArray *)windows { +- [gateway_ sendCommandList:[self commandListToSetSize:size ofWindows:windows]]; +-} +- + - (void)setWindowSizes:(NSArray *> *)windowSizes { + [gateway_ sendCommandList:[self commandListToSetWindowSizes:windowSizes]]; + } +@@ -727,14 +735,14 @@ static NSDictionary *iTermTmuxControllerDefaultFontOverridesFromProfile(Profile + return [windowSizes mapWithBlock:^NSDictionary *(iTermTuple *tuple) { + NSString *window = tuple.firstObject; + NSSize size = tuple.secondObject.sizeValue; +- if ([window hasPrefix:@"pty"]) { ++ if ([window hasPrefix:@"pty"] || [window hasSuffix:@"_ph"]) { + return nil; + } + // 10000 comes from WINDOW_MAXIMUM in tmux.h + if (size.width < 1 || size.height < 1 || size.width >= 10000 || size.height >= 10000) { + return nil; + } +- NSString *command = [NSString stringWithFormat:@"resize-window -x %@ -y %@ -t @%@", @((int)size.width), @((int)size.height), window]; ++ NSString *command = [NSString stringWithFormat:@"resize-window -x %@ -y %@ -t @%d", @((int)size.width), @((int)size.height), window.intValue]; + NSDictionary *dict = [gateway_ dictionaryForCommand:command + responseTarget:self + responseSelector:@selector(handleResizeWindowResponse:) +@@ -857,11 +865,6 @@ static NSDictionary *iTermTmuxControllerDefaultFontOverridesFromProfile(Profile + [gateway_ sendCommandList:@[ [gateway_ dictionaryForCommand:@"show-options -v -g set-titles" + responseTarget:self + responseSelector:@selector(handleShowSetTitles:) +- responseObject:nil +- flags:0], +- [gateway_ dictionaryForCommand:@"show-options -v -g set-titles-string" +- responseTarget:self +- responseSelector:@selector(handleShowSetTitlesString:) + responseObject:nil + flags:0] ]]; + } +@@ -872,10 +875,6 @@ static NSDictionary *iTermTmuxControllerDefaultFontOverridesFromProfile(Profile + object:self]; + } + +-- (void)handleShowSetTitlesString:(NSString *)setTitlesString { +- _setTitlesString = [setTitlesString copy]; +-} +- + - (void)guessVersion { + // Run commands that will fail in successively older versions. + // show-window-options pane-border-format will succeed in 2.3 and later (presumably. 2.3 isn't out yet) +@@ -1013,17 +1012,25 @@ static NSDictionary *iTermTmuxControllerDefaultFontOverridesFromProfile(Profile + } + + // This is the oldest version supported. By the time you get here you know the version. ++ [self didGuessVersion]; ++} ++ ++// Actions to perform after the version number is known. ++- (void)didGuessVersion { + [self loadServerPID]; ++ [self loadTitleFormat]; + } + +-- (BOOL)recyclingSupported { +- NSDecimalNumber *version1_9 = [NSDecimalNumber decimalNumberWithString:@"1.9"]; +- if (gateway_.minimumServerVersion != nil) { +- return ([gateway_.minimumServerVersion compare:version1_9] != NSOrderedAscending); +- } else { +- // Assume 1.8 ++- (BOOL)versionAtLeastDecimalNumberWithString:(NSString *)string { ++ NSDecimalNumber *version = [NSDecimalNumber decimalNumberWithString:string]; ++ if (gateway_.minimumServerVersion == nil) { + return NO; + } ++ return ([gateway_.minimumServerVersion compare:version] != NSOrderedAscending); ++} ++ ++- (BOOL)recyclingSupported { ++ return [self versionAtLeastDecimalNumberWithString:@"1.9"]; + } + + // Show an error and terminate the connection because tmux has an unsupported option turned on. +@@ -1143,10 +1150,10 @@ static NSDictionary *iTermTmuxControllerDefaultFontOverridesFromProfile(Profile + responseSelector:nil]; + } + +-- (void)newWindowInSession:(NSString *)targetSession +- scope:(iTermVariableScope *)scope +- initialDirectory:(iTermInitialDirectory *)initialDirectory { +- [initialDirectory tmuxNewWindowCommandInSession:targetSession ++- (void)newWindowInSessionNumber:(NSNumber *)sessionNumber ++ scope:(iTermVariableScope *)scope ++ initialDirectory:(iTermInitialDirectory *)initialDirectory { ++ [initialDirectory tmuxNewWindowCommandInSessionNumber:sessionNumber + recyclingSupported:self.recyclingSupported + scope:scope + completion: +@@ -1164,7 +1171,11 @@ static NSDictionary *iTermTmuxControllerDefaultFontOverridesFromProfile(Profile + responseObject:nil + flags:0]]; + } +- [commands addObject:[gateway_ dictionaryForCommand:command responseTarget:nil responseSelector:nil responseObject:nil flags:0]]; ++ [commands addObject:[gateway_ dictionaryForCommand:command ++ responseTarget:nil ++ responseSelector:nil ++ responseObject:nil ++ flags:0]]; + [gateway_ sendCommandList:commands]; + }]; + } +@@ -1218,8 +1229,7 @@ static NSDictionary *iTermTmuxControllerDefaultFontOverridesFromProfile(Profile + responseSelector:nil]; + } + +-- (void)unlinkWindowWithId:(int)windowId inSession:(NSString *)sessionName +-{ ++- (void)unlinkWindowWithId:(int)windowId { + [gateway_ sendCommand:[NSString stringWithFormat:@"unlink-window -k -t @%d", windowId] + responseTarget:nil + responseSelector:nil +@@ -1232,11 +1242,14 @@ static NSDictionary *iTermTmuxControllerDefaultFontOverridesFromProfile(Profile + } + + - (void)renameWindowWithId:(int)windowId +- inSession:(NSString *)sessionName ++ inSessionNumber:(NSNumber *)sessionNumber + toName:(NSString *)newName { + NSString *theCommand; +- if (sessionName) { +- theCommand = [NSString stringWithFormat:@"rename-window -t \"%@:@%d\" \"%@\"", sessionName, windowId, [self stringByEscapingBackslashesAndRemovingNewlines:newName]]; ++ if (sessionNumber) { ++ theCommand = [NSString stringWithFormat:@"rename-window -t \"$%d:@%d\" \"%@\"", ++ sessionNumber.intValue, ++ windowId, ++ [self stringByEscapingBackslashesAndRemovingNewlines:newName]]; + } else { + theCommand = [NSString stringWithFormat:@"rename-window -t @%d \"%@\"", windowId, [self stringByEscapingBackslashesAndRemovingNewlines:newName]]; + } +@@ -1312,9 +1325,23 @@ static NSDictionary *iTermTmuxControllerDefaultFontOverridesFromProfile(Profile + [self sendCommandToSetTabColors]; + } + ++- (NSString *)encodedString:(NSString *)string prefix:(NSString *)prefix { ++ return [prefix stringByAppendingString:[[string dataUsingEncoding:NSUTF8StringEncoding] it_hexEncoded]]; ++} ++ ++- (NSString *)decodedString:(NSString *)string optionalPrefix:(NSString *)prefix { ++ if (![string hasPrefix:prefix]) { ++ return string; ++ } ++ return [[[NSString alloc] initWithData:[[string substringFromIndex:prefix.length] dataFromHexValues] ++ encoding:NSUTF8StringEncoding] autorelease]; ++} ++ + - (void)sendCommandToSetHotkeys { ++ NSString *hexEncoded = [self encodedString:[self.hotkeysString stringByEscapingQuotes] ++ prefix:iTermTmuxControllerEncodingPrefixHotkeys]; + NSString *command = [NSString stringWithFormat:@"set -t $%d @hotkeys \"%@\"", +- sessionId_, [self.hotkeysString stringByEscapingQuotes]]; ++ sessionId_, hexEncoded]; + [gateway_ sendCommand:command + responseTarget:nil + responseSelector:nil +@@ -1323,8 +1350,10 @@ static NSDictionary *iTermTmuxControllerDefaultFontOverridesFromProfile(Profile + } + + - (void)sendCommandToSetTabColors { ++ + NSString *command = [NSString stringWithFormat:@"set -t $%d @tab_colors \"%@\"", +- sessionId_, [self.tabColorsString stringByEscapingQuotes]]; ++ sessionId_, [self encodedString:[self.tabColorsString stringByEscapingQuotes] ++ prefix:iTermTmuxControllerEncodingPrefixTabColors]]; + [gateway_ sendCommand:command + responseTarget:nil + responseSelector:nil +@@ -1376,7 +1405,8 @@ static NSDictionary *iTermTmuxControllerDefaultFontOverridesFromProfile(Profile + + - (void)breakOutWindowPane:(int)windowPane toTabAside:(NSString *)sibling + { +- [gateway_ sendCommand:[NSString stringWithFormat:@"break-pane -P -F \"#{window_id}\" %@ \"%%%d\"", [self breakPaneWindowPaneFlag], windowPane] ++ [gateway_ sendCommand:[NSString stringWithFormat:@"break-pane -P -F \"#{window_id}\" %@ \"%%%d\"", ++ [self breakPaneWindowPaneFlag], windowPane] + responseTarget:self + responseSelector:@selector(windowPaneBrokeOutWithWindowId:setAffinityTo:) + responseObject:sibling +@@ -1435,19 +1465,19 @@ static NSDictionary *iTermTmuxControllerDefaultFontOverridesFromProfile(Profile + } + + - (void)linkWindowId:(int)windowId +- inSession:(NSString *)sessionName +- toSession:(NSString *)targetSession { +- [gateway_ sendCommand:[NSString stringWithFormat:@"link-window -s \"%@:@%d\" -t \"%@:+\"", +- sessionName, windowId, targetSession] ++ inSessionNumber:(int)sessionNumber ++ toSessionNumber:(int)targetSessionNumber { ++ [gateway_ sendCommand:[NSString stringWithFormat:@"link-window -s \"$%d:@%d\" -t \"$%d:+\"", ++ sessionNumber, windowId, targetSessionNumber] + responseTarget:nil + responseSelector:nil]; + } + + - (void)moveWindowId:(int)windowId +- inSession:(NSString *)sessionName +- toSession:(NSString *)targetSession { +- [gateway_ sendCommand:[NSString stringWithFormat:@"move-window -s \"%@:@%d\" -t \"%@:+\"", +- sessionName, windowId, targetSession] ++ inSessionNumber:(int)sessionNumber ++ toSessionNumber:(int)targetSessionNumber { ++ [gateway_ sendCommand:[NSString stringWithFormat:@"move-window -s \"$%d:@%d\" -t \"$%d:+\"", ++ sessionNumber, windowId, targetSessionNumber] + responseTarget:nil + responseSelector:nil]; + } +@@ -1467,19 +1497,19 @@ static NSDictionary *iTermTmuxControllerDefaultFontOverridesFromProfile(Profile + return pos; + } + +-- (void)renameSession:(NSString *)oldName to:(NSString *)newName +-{ +- NSString *renameCommand = [NSString stringWithFormat:@"rename-session -t \"%@\" \"%@\"", +- [oldName stringByEscapingQuotes], ++- (void)renameSessionNumber:(int)sessionNumber ++ to:(NSString *)newName { ++ NSString *renameCommand = [NSString stringWithFormat:@"rename-session -t \"$%d\" \"%@\"", ++ sessionNumber, + [newName stringByEscapingQuotes]]; + [gateway_ sendCommand:renameCommand responseTarget:nil responseSelector:nil]; + } + +-- (void)killSession:(NSString *)sessionName +-{ +- NSString *killCommand = [NSString stringWithFormat:@"kill-session -t \"%@\"", +- [sessionName stringByEscapingQuotes]]; +- [gateway_ sendCommand:killCommand responseTarget:nil responseSelector:nil]; ++- (void)killSessionNumber:(int)sessionNumber { ++ NSString *killCommand = [NSString stringWithFormat:@"kill-session -t \"$%d\"", sessionNumber]; ++ [gateway_ sendCommand:killCommand ++ responseTarget:nil ++ responseSelector:nil]; + [self listSessions]; + } + +@@ -1493,44 +1523,46 @@ static NSDictionary *iTermTmuxControllerDefaultFontOverridesFromProfile(Profile + [self listSessions]; + } + +-- (void)attachToSession:(NSString *)sessionName +-{ +- NSString *attachCommand = [NSString stringWithFormat:@"attach-session -t \"%@\"", +- [sessionName stringByEscapingQuotes]]; ++- (void)attachToSessionWithNumber:(int)sessionNumber { ++ NSString *attachCommand = [NSString stringWithFormat:@"attach-session -t \"$%d\"", sessionNumber]; + [gateway_ sendCommand:attachCommand + responseTarget:nil + responseSelector:nil]; + } + +-- (void)listWindowsInSession:(NSString *)sessionName +- target:(id)target +- selector:(SEL)selector +- object:(id)object { ++- (void)listWindowsInSessionNumber:(int)sessionNumber ++ target:(id)target ++ selector:(SEL)selector ++ object:(id)object { + if (detached_ || !object) { + // This can happen if you're not attached to a session. + return; + } +- NSString *listWindowsCommand = [NSString stringWithFormat:@"list-windows -F %@ -t \"%@\"", +- kListWindowsFormat, sessionName]; ++ NSString *listWindowsCommand = [NSString stringWithFormat:@"list-windows -F %@ -t \"$%d\"", ++ kListWindowsFormat, sessionNumber]; + // Wait a few seconds. We always get a windows-close notification when the last window in + // a window closes. To avoid spamming the command line with list-windows, we wait a bit to see + // if there is an exit notification coming down the pipe. + const CGFloat kListWindowsDelay = 1.5; + [listWindowsTimer_ invalidate]; +- listWindowsTimer_ = [NSTimer scheduledTimerWithTimeInterval:kListWindowsDelay +- target:self +- selector:@selector(listWindowsTimerFired:) +- userInfo:[NSArray arrayWithObjects:listWindowsCommand, object, target, NSStringFromSelector(selector), nil] +- repeats:NO]; ++ listWindowsTimer_ = ++ [NSTimer scheduledTimerWithTimeInterval:kListWindowsDelay ++ target:self ++ selector:@selector(listWindowsTimerFired:) ++ userInfo:@[listWindowsCommand, ++ object, ++ target, ++ NSStringFromSelector(selector) ] ++ repeats:NO]; + } + + - (void)listWindowsTimerFired:(NSTimer *)timer + { + NSArray *array = [timer userInfo]; +- NSString *command = [array objectAtIndex:0]; +- id object = [array objectAtIndex:1]; +- id target = [array objectAtIndex:2]; +- NSString *selector = [array objectAtIndex:3]; ++ NSString *command = array[0]; ++ id object = array[1]; ++ id target = array[2]; ++ NSString *selector = array[3]; + + [listWindowsTimer_ invalidate]; + listWindowsTimer_ = nil; +@@ -1538,10 +1570,7 @@ static NSDictionary *iTermTmuxControllerDefaultFontOverridesFromProfile(Profile + [gateway_ sendCommand:command + responseTarget:self + responseSelector:@selector(didListWindows:userData:) +- responseObject:[NSArray arrayWithObjects:object, +- selector, +- target, +- nil] ++ responseObject:@[object, selector, target] + flags:kTmuxGatewayCommandShouldTolerateErrors]; // Tolerates errors because the session may have been detached by the time we get the notification or the timer fires. + } + +@@ -1549,8 +1578,10 @@ static NSDictionary *iTermTmuxControllerDefaultFontOverridesFromProfile(Profile + { + NSString *hidden = [[hiddenWindows_ allObjects] componentsJoinedByString:@","]; + NSString *command = [NSString stringWithFormat: +- @"set -t $%d @hidden \"%@\"", +- sessionId_, hidden]; ++ @"set -t $%d @hidden \"%@\"", ++ sessionId_, ++ [self encodedString:hidden ++ prefix:iTermTmuxControllerEncodingPrefixHidden]]; + [gateway_ sendCommand:command + responseTarget:nil + responseSelector:nil +@@ -1594,7 +1625,9 @@ static NSDictionary *iTermTmuxControllerDefaultFontOverridesFromProfile(Profile + NSString *enc = [maps componentsJoinedByString:@" "]; + DLog(@"Save window origins to %@ called from %@", enc, [NSThread callStackSymbols]); + NSString *command = [NSString stringWithFormat:@"set -t $%d @origins \"%@\"", +- sessionId_, [enc stringByEscapingQuotes]]; ++ sessionId_, ++ [self encodedString:[enc stringByEscapingQuotes] ++ prefix:iTermTmuxControllerEncodingPrefixOrigins]]; + if (!lastOrigins_ || ![command isEqualToString:lastOrigins_]) { + [lastOrigins_ release]; + lastOrigins_ = [command copy]; +@@ -1603,7 +1636,8 @@ static NSDictionary *iTermTmuxControllerDefaultFontOverridesFromProfile(Profile + responseTarget:self + responseSelector:@selector(saveWindowOriginsResponse:)]; + } +- [self getOriginsResponse:enc]; ++ [self getOriginsResponse:[self encodedString:[enc stringByEscapingQuotes] ++ prefix:iTermTmuxControllerEncodingPrefixOrigins]]; + } + + - (void)saveWindowOriginsResponse:(NSString *)response +@@ -1651,7 +1685,8 @@ static NSDictionary *iTermTmuxControllerDefaultFontOverridesFromProfile(Profile + // Update affinities if any have changed. + NSString *arg = [affinities componentsJoinedByString:@" "]; + NSString *command = [NSString stringWithFormat:@"set -t $%d @affinities \"%@\"", +- sessionId_, [arg stringByEscapingQuotes]]; ++ sessionId_, [self encodedString:[arg stringByEscapingQuotes] ++ prefix:iTermTmuxControllerEncodingPrefixAffinities]]; + if ([command isEqualToString:lastSaveAffinityCommand_]) { + return; + } +@@ -1763,7 +1798,7 @@ static NSDictionary *iTermTmuxControllerDefaultFontOverridesFromProfile(Profile + responseSelector:@selector(didSetLayout:) + responseObject:nil + flags:0], +- [gateway_ dictionaryForCommand:[NSString stringWithFormat:@"list-windows -F \"#{window_id} #{window_layout} #{window_flags}\" -t \"%@\"", sessionName_] ++ [gateway_ dictionaryForCommand:[NSString stringWithFormat:@"list-windows -F \"#{window_id} #{window_layout} #{window_flags}\" -t \"$%d\"", sessionId_] + responseTarget:self + responseSelector:@selector(didListWindowsSubsequentToSettingLayout:) + responseObject:nil +@@ -1777,7 +1812,7 @@ static NSDictionary *iTermTmuxControllerDefaultFontOverridesFromProfile(Profile + responseSelector:@selector(didSetLayout:) + responseObject:nil + flags:0], +- [gateway_ dictionaryForCommand:[NSString stringWithFormat:@"list-windows -F \"#{window_id} #{window_layout} #{window_flags}\" -t \"%@\"", sessionName_] ++ [gateway_ dictionaryForCommand:[NSString stringWithFormat:@"list-windows -F \"#{window_id} #{window_layout} #{window_flags}\" -t \"$%d\"", sessionId_] + responseTarget:self + responseSelector:@selector(didListWindowsSubsequentToSettingLayout:) + responseObject:nil +@@ -1798,31 +1833,32 @@ static NSDictionary *iTermTmuxControllerDefaultFontOverridesFromProfile(Profile + + #pragma mark - Private + +-- (void)getOriginsResponse:(NSString *)result +-{ +- [origins_ removeAllObjects]; +- if ([result length] > 0) { +- NSArray *windows = [result componentsSeparatedByString:@" "]; +- for (NSString *wstr in windows) { +- NSArray *tuple = [wstr componentsSeparatedByString:@":"]; +- if (tuple.count != 2) { +- continue; +- } +- NSString *windowsStr = [tuple objectAtIndex:0]; +- NSString *coords = [tuple objectAtIndex:1]; +- NSArray *windowIds = [windowsStr componentsSeparatedByString:@","]; +- NSArray *xy = [coords componentsSeparatedByString:@","]; +- if (xy.count != 2) { +- continue; +- } +- NSPoint origin = NSMakePoint([[xy objectAtIndex:0] intValue], +- [[xy objectAtIndex:1] intValue]); +- for (NSString *wid in windowIds) { +- [origins_ setObject:[NSValue valueWithPoint:origin] +- forKey:[NSNumber numberWithInt:[wid intValue]]]; +- } +- } +- } ++- (void)getOriginsResponse:(NSString *)encodedResult { ++ NSString *result = [self decodedString:encodedResult ++ optionalPrefix:iTermTmuxControllerEncodingPrefixOrigins]; ++ [origins_ removeAllObjects]; ++ if ([result length] > 0) { ++ NSArray *windows = [result componentsSeparatedByString:@" "]; ++ for (NSString *wstr in windows) { ++ NSArray *tuple = [wstr componentsSeparatedByString:@":"]; ++ if (tuple.count != 2) { ++ continue; ++ } ++ NSString *windowsStr = [tuple objectAtIndex:0]; ++ NSString *coords = [tuple objectAtIndex:1]; ++ NSArray *windowIds = [windowsStr componentsSeparatedByString:@","]; ++ NSArray *xy = [coords componentsSeparatedByString:@","]; ++ if (xy.count != 2) { ++ continue; ++ } ++ NSPoint origin = NSMakePoint([[xy objectAtIndex:0] intValue], ++ [[xy objectAtIndex:1] intValue]); ++ for (NSString *wid in windowIds) { ++ [origins_ setObject:[NSValue valueWithPoint:origin] ++ forKey:[NSNumber numberWithInt:[wid intValue]]]; ++ } ++ } ++ } + } + + - (NSString *)shortStringForHotkeyDictionary:(NSDictionary *)dict paneID:(int)wp { +@@ -1851,7 +1887,8 @@ static NSDictionary *iTermTmuxControllerDefaultFontOverridesFromProfile(Profile + return [parts componentsJoinedByString:@" "]; + } + +-- (void)getHotkeysResponse:(NSString *)result { ++- (void)getHotkeysResponse:(NSString *)encodedResult { ++ NSString *result = [self decodedString:encodedResult optionalPrefix:iTermTmuxControllerEncodingPrefixHotkeys]; + [_hotkeys removeAllObjects]; + if (result.length > 0) { + [_hotkeys removeAllObjects]; +@@ -1870,7 +1907,9 @@ static NSDictionary *iTermTmuxControllerDefaultFontOverridesFromProfile(Profile + } + } + +-- (void)getTabColorsResponse:(NSString *)result { ++- (void)getTabColorsResponse:(NSString *)encodedResult { ++ NSString *result = [self decodedString:encodedResult ++ optionalPrefix:iTermTmuxControllerEncodingPrefixTabColors]; + [_tabColors removeAllObjects]; + if (result.length > 0) { + [_tabColors removeAllObjects]; +@@ -1903,14 +1942,15 @@ static NSDictionary *iTermTmuxControllerDefaultFontOverridesFromProfile(Profile + response = @""; + } + TSVDocument *doc = [response tsvDocumentWithFields:[self listWindowFields]]; +- id object = [userData objectAtIndex:0]; +- SEL selector = NSSelectorFromString([userData objectAtIndex:1]); +- id target = [userData objectAtIndex:2]; ++ id object = userData[0]; ++ SEL selector = NSSelectorFromString(userData[1]); ++ id target = userData[2]; + [target performSelector:selector withObject:doc withObject:object]; + } + +-- (void)getHiddenWindowsResponse:(NSString *)response +-{ ++- (void)getHiddenWindowsResponse:(NSString *)encodedResponse { ++ NSString *response = [self decodedString:encodedResponse ++ optionalPrefix:iTermTmuxControllerEncodingPrefixHidden]; + [hiddenWindows_ removeAllObjects]; + if ([response length] > 0) { + NSArray *windowIds = [response componentsSeparatedByString:@","]; +@@ -1922,7 +1962,7 @@ static NSDictionary *iTermTmuxControllerDefaultFontOverridesFromProfile(Profile + } + + - (void)getAffinitiesResponse:(NSString *)result { +- [self setAffinitiesFromString:result]; ++ [self setAffinitiesFromString:[self decodedString:result optionalPrefix:iTermTmuxControllerEncodingPrefixAffinities]]; + } + + - (NSArray *)componentsOfAffinities:(NSString *)affinities { +@@ -1991,9 +2031,28 @@ static NSDictionary *iTermTmuxControllerDefaultFontOverridesFromProfile(Profile + + - (void)listSessionsResponse:(NSString *)result + { +- self.sessions = [result componentsSeparatedByRegex:@"\n"]; ++ self.sessionObjects = [[result componentsSeparatedByRegex:@"\n"] mapWithBlock:^iTermTmuxSessionObject *(NSString *line) { ++ const NSInteger space = [line rangeOfString:@" "].location; ++ if (space == NSNotFound) { ++ return nil; ++ } ++ NSString *sessionID = [line substringToIndex:space]; ++ NSString *sessionName = [line substringFromIndex:space + 1]; ++ if (![sessionID hasPrefix:@"$"]) { ++ return nil; ++ } ++ NSScanner *scanner = [NSScanner scannerWithString:[sessionID substringFromIndex:1]]; ++ int sessionNumber = -1; ++ if (![scanner scanInt:&sessionNumber] || sessionNumber < 0) { ++ return nil; ++ } ++ iTermTmuxSessionObject *obj = [[[iTermTmuxSessionObject alloc] init] autorelease]; ++ obj.name = sessionName; ++ obj.number = sessionNumber; ++ return obj; ++ }]; + [[NSNotificationCenter defaultCenter] postNotificationName:kTmuxControllerSessionsDidChange +- object:self.sessions]; ++ object:nil]; + } + + - (void)listedWindowsToOpenOne:(NSString *)response +diff --git sources/TmuxDashboardController.m sources/TmuxDashboardController.m +index b83bfe6..b2ed7b7 100644 +--- sources/TmuxDashboardController.m ++++ sources/TmuxDashboardController.m +@@ -49,7 +49,7 @@ + if (self) { + [self window]; + +- [sessionsTable_ selectSessionWithName:[[self tmuxController] sessionName]]; ++ [sessionsTable_ selectSessionNumber:[[self tmuxController] sessionId]]; + [self reloadWindows]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(tmuxControllerDetached:) +@@ -140,78 +140,79 @@ + + #pragma mark TmuxSessionsTableProtocol + +-- (void)renameSessionWithName:(NSString *)oldName toName:(NSString *)newName +-{ +- [[self tmuxController] renameSession:oldName to:newName]; ++- (void)renameSessionWithNumber:(int)sessionNumber toName:(NSString *)newName { ++ [[self tmuxController] renameSessionNumber:sessionNumber ++ to:newName]; + } + +-- (void)removeSessionWithName:(NSString *)sessionName +-{ +- [[self tmuxController] killSession:sessionName]; ++- (void)removeSessionWithNumber:(int)sessionNumber { ++ [[self tmuxController] killSessionNumber:sessionNumber]; + } + +-- (void)addSessionWithName:(NSString *)sessionName +-{ ++- (void)addSessionWithName:(NSString *)sessionName { + [[self tmuxController] addSessionWithName:sessionName]; + } + +-- (void)attachToSessionWithName:(NSString *)sessionName +-{ +- [[self tmuxController] attachToSession:sessionName]; ++- (void)attachToSessionWithNumber:(int)sessionNumber { ++ [[self tmuxController] attachToSessionWithNumber:sessionNumber]; + } + +-- (void)detach +-{ ++- (void)detach { + [[self tmuxController] requestDetach]; + } + +-- (NSString *)nameOfAttachedSession +-{ +- return [[self tmuxController] sessionName]; ++- (NSNumber *)numberOfAttachedSession { ++ TmuxController *controller = [self tmuxController]; ++ if (!controller) { ++ return nil; ++ } ++ return @([controller sessionId]); + } + +-- (NSArray *)sessions +-{ +- return [[self tmuxController] sessions]; ++- (NSArray *)sessionsTableModelValues:(id)sender { ++ return [self.tmuxController sessionObjects]; + } + +-- (void)selectedSessionChangedTo:(NSString *)newSessionName +-{ ++- (NSArray *)sessionsTableObjects:(TmuxSessionsTable *)sender { ++ return [[self tmuxController] sessionObjects]; ++} ++ ++- (void)selectedSessionDidChange { + [windowsTable_ setWindows:[NSArray array]]; + [self reloadWindows]; + } + + - (void)linkWindowId:(int)windowId +- inSession:(NSString *)sessionName +- toSession:(NSString *)targetSession +-{ ++ inSessionNumber:(int)sourceSessionNumber ++ toSessionNumber:(int)targetSessionNumber { + [[self tmuxController] linkWindowId:windowId +- inSession:sessionName +- toSession:targetSession]; ++ inSessionNumber:sourceSessionNumber ++ toSessionNumber:targetSessionNumber]; + } + + - (void)moveWindowId:(int)windowId +- inSession:(NSString *)sessionName +- toSession:(NSString *)targetSession +-{ ++ inSessionNumber:(int)sessionNumber ++ toSessionNumber:(int)targetSessionNumber { + [[self tmuxController] moveWindowId:windowId +- inSession:sessionName +- toSession:targetSession]; ++ inSessionNumber:sessionNumber ++ toSessionNumber:targetSessionNumber]; + } + + #pragma mark TmuxWindowsTableProtocol + +-- (void)reloadWindows +-{ +- [[self tmuxController] listWindowsInSession:[sessionsTable_ selectedSessionName] +- target:self +- selector:@selector(setWindows:forSession:) +- object:[sessionsTable_ selectedSessionName]]; ++- (void)reloadWindows { ++ NSNumber *sessionNumber = [sessionsTable_ selectedSessionNumber]; ++ if (!sessionNumber) { ++ return; ++ } ++ [[self tmuxController] listWindowsInSessionNumber:sessionNumber.intValue ++ target:self ++ selector:@selector(setWindows:forSession:) ++ object:[sessionsTable_ selectedSessionNumber]]; + } + +-- (void)setWindows:(TSVDocument *)doc forSession:(NSString *)sessionName +-{ +- if ([sessionName isEqualToString:[sessionsTable_ selectedSessionName]]) { ++- (void)setWindows:(TSVDocument *)doc forSession:(NSNumber *)sessionNumber { ++ if ([sessionNumber isEqual:[sessionsTable_ selectedSessionNumber]]) { + NSMutableArray *windows = [NSMutableArray array]; + for (NSArray *record in doc.records) { + [windows addObject:[NSMutableArray arrayWithObjects: +@@ -223,18 +224,19 @@ + } + } + +-- (void)renameWindowWithId:(int)windowId toName:(NSString *)newName +-{ ++- (void)renameWindowWithId:(int)windowId toName:(NSString *)newName { ++ NSNumber *sessionNumber = [sessionsTable_ selectedSessionNumber]; ++ if (!sessionNumber) { ++ return; ++ } + [[self tmuxController] renameWindowWithId:windowId +- inSession:[sessionsTable_ selectedSessionName] ++ inSessionNumber:sessionNumber + toName:newName]; + [self reloadWindows]; + } + +-- (void)unlinkWindowWithId:(int)windowId +-{ +- [[self tmuxController] unlinkWindowWithId:windowId +- inSession:[sessionsTable_ selectedSessionName]]; ++- (void)unlinkWindowWithId:(int)windowId { ++ [[self tmuxController] unlinkWindowWithId:windowId]; + [self reloadWindows]; + } + +@@ -242,15 +244,14 @@ + NSString *lastName = [[windowsTable_ names] lastObject]; + if (lastName) { + TmuxController *tmuxController = self.tmuxController; +- [tmuxController newWindowInSession:[sessionsTable_ selectedSessionName] +- scope:[iTermVariableScope globalsScope] +- initialDirectory:[iTermInitialDirectory initialDirectoryFromProfile:tmuxController.sharedProfile +- objectType:iTermWindowObject]]; ++ [tmuxController newWindowInSessionNumber:[sessionsTable_ selectedSessionNumber] ++ scope:[iTermVariableScope globalsScope] ++ initialDirectory:[iTermInitialDirectory initialDirectoryFromProfile:tmuxController.sharedProfile ++ objectType:iTermWindowObject]]; + } + } + +-- (void)showWindowsWithIds:(NSArray *)windowIds inTabs:(BOOL)inTabs +-{ ++- (void)showWindowsWithIds:(NSArray *)windowIds inTabs:(BOOL)inTabs { + if (inTabs) { + for (NSNumber *wid in windowIds) { + [[self tmuxController] openWindowWithId:[wid intValue] +@@ -265,27 +266,24 @@ + profile:self.tmuxController.sharedProfile]; + } + } +- [[self tmuxController] saveHiddenWindows]; ++ [[self tmuxController] saveHiddenWindows]; + } + + - (void)hideWindowWithId:(int)windowId + { +- [[self tmuxController] hideWindow:windowId]; ++ [[self tmuxController] hideWindow:windowId]; + [windowsTable_ updateEnabledStateOfButtons]; + } + +-- (BOOL)haveSelectedSession +-{ +- return [sessionsTable_ selectedSessionName] != nil; ++- (BOOL)haveSelectedSession { ++ return [sessionsTable_ selectedSessionNumber] != nil; + } + +-- (BOOL)currentSessionSelected +-{ +- return [[sessionsTable_ selectedSessionName] isEqualToString:[[self tmuxController] sessionName]]; ++- (BOOL)currentSessionSelected { ++ return [[sessionsTable_ selectedSessionNumber] isEqual:@([[self tmuxController] sessionId])]; + } + +-- (BOOL)haveOpenWindowWithId:(int)windowId +-{ ++- (BOOL)haveOpenWindowWithId:(int)windowId { + return [[self tmuxController] window:windowId] != nil; + } + +@@ -294,48 +292,41 @@ + [tab.activeSession reveal]; + } + +-- (NSString *)selectedSessionName +-{ +- return [sessionsTable_ selectedSessionName]; ++- (NSNumber *)selectedSessionNumber { ++ return [sessionsTable_ selectedSessionNumber]; + } + + #pragma mark - Private + +-- (void)tmuxControllerDetached:(NSNotification *)notification +-{ +- [sessionsTable_ setSessions:[NSArray array]]; ++- (void)tmuxControllerDetached:(NSNotification *)notification { ++ [sessionsTable_ setSessionObjects:@[]]; + } + +-- (void)tmuxControllerSessionsDidChange:(NSNotification *)notification +-{ +- [sessionsTable_ setSessions:[[self tmuxController] sessions]]; ++- (void)tmuxControllerSessionsDidChange:(NSNotification *)notification { ++ [sessionsTable_ setSessionObjects:[[self tmuxController] sessionObjects]]; + } + +-- (void)tmuxControllerWindowsDidChange:(NSNotification *)notification +-{ ++- (void)tmuxControllerWindowsDidChange:(NSNotification *)notification { + if ([[self window] isVisible]) { + [self reloadWindows]; + } + } + +-- (void)tmuxControllerAttachedSessionChanged:(NSNotification *)notification +-{ ++- (void)tmuxControllerAttachedSessionChanged:(NSNotification *)notification { + if ([[self window] isVisible]) { +- [sessionsTable_ selectSessionWithName:[[self tmuxController] sessionName]]; ++ [sessionsTable_ selectSessionNumber:[[self tmuxController] sessionId]]; + [windowsTable_ updateEnabledStateOfButtons]; + } + } + +-- (void)tmuxControllerWindowOpenedOrClosed:(NSNotification *)notification +-{ ++- (void)tmuxControllerWindowOpenedOrClosed:(NSNotification *)notification { + if ([[self window] isVisible]) { + [windowsTable_ updateEnabledStateOfButtons]; + [windowsTable_ reloadData]; + } + } + +-- (void)tmuxControllerWindowWasRenamed:(NSNotification *)notification +-{ ++- (void)tmuxControllerWindowWasRenamed:(NSNotification *)notification { + if ([[self window] isVisible]) { + NSArray *objects = [notification object]; + int wid = [[objects objectAtIndex:0] intValue]; +@@ -344,8 +335,7 @@ + } + } + +-- (void)tmuxControllerSessionWasRenamed:(NSNotification *)notification +-{ ++- (void)tmuxControllerSessionWasRenamed:(NSNotification *)notification { + // This is a bit of extra work but the sessions table wasn't built knowing about session IDs. + [[self tmuxController] listSessions]; + } +@@ -364,8 +354,7 @@ + [self connectionSelectionDidChange:nil]; + } + +-- (TmuxController *)tmuxController +-{ ++- (TmuxController *)tmuxController { + return [[TmuxControllerRegistry sharedInstance] controllerForClient:[self currentClient]]; // TODO: track the current client when multiples are supported + } + +@@ -375,7 +364,7 @@ + + + - (IBAction)connectionSelectionDidChange:(id)sender { +- [sessionsTable_ setSessions:[[self tmuxController] sessions]]; ++ [sessionsTable_ setSessionObjects:[[self tmuxController] sessionObjects]]; + [self reloadWindows]; + } + +diff --git sources/TmuxSessionsTable.h sources/TmuxSessionsTable.h +index 856f0f5..a3e5823 100644 +--- sources/TmuxSessionsTable.h ++++ sources/TmuxSessionsTable.h +@@ -9,21 +9,25 @@ + #import + #import "FutureMethods.h" + ++@class TmuxSessionsTable; ++@class iTermTmuxSessionObject; ++ + @protocol TmuxSessionsTableProtocol + +-- (NSArray *)sessions; +-- (void)renameSessionWithName:(NSString *)oldName toName:(NSString *)newName; +-- (void)removeSessionWithName:(NSString *)sessionName; ++- (NSArray *)sessionsTableObjects:(TmuxSessionsTable *)sender; ++- (void)renameSessionWithNumber:(int)sessionNumber ++ toName:(NSString *)newName; ++- (void)removeSessionWithNumber:(int)sessionNumber; + - (void)addSessionWithName:(NSString *)sessionName; +-- (void)attachToSessionWithName:(NSString *)sessionName; +-- (NSString *)nameOfAttachedSession; +-- (void)selectedSessionChangedTo:(NSString *)newName; ++- (void)attachToSessionWithNumber:(int)sessionNumber; ++- (NSNumber *)numberOfAttachedSession; ++- (void)selectedSessionDidChange; + - (void)linkWindowId:(int)windowId +- inSession:(NSString *)sessionName +- toSession:(NSString *)targetSession; ++ inSessionNumber:(int)sourceSessionNumber ++ toSessionNumber:(int)targetSessionNumber; + - (void)moveWindowId:(int)windowId +- inSession:(NSString *)sessionName +- toSession:(NSString *)targetSession; ++ inSessionNumber:(int)sessionNumber ++ toSessionNumber:(int)targetSessionNumber; + - (void)detach; + + @end +@@ -31,9 +35,9 @@ + @interface TmuxSessionsTable : NSObject + + @property(nonatomic, assign) id delegate; +-@property(nonatomic, readonly) NSString *selectedSessionName; ++@property(nonatomic, readonly) NSNumber *selectedSessionNumber; + +-- (void)setSessions:(NSArray *)names; +-- (void)selectSessionWithName:(NSString *)name; ++- (void)setSessionObjects:(NSArray *)names; ++- (void)selectSessionNumber:(int)number; + + @end +diff --git sources/TmuxSessionsTable.m sources/TmuxSessionsTable.m +index f9a086f..88e2f72 100644 +--- sources/TmuxSessionsTable.m ++++ sources/TmuxSessionsTable.m +@@ -7,12 +7,15 @@ + // + + #import "TmuxSessionsTable.h" ++ + #import "FutureMethods.h" ++#import "iTermTmuxSessionObject.h" ++#import "NSArray+iTerm.h" + + extern NSString *kWindowPasteboardType; + + @implementation TmuxSessionsTable { +- NSMutableArray *model_; ++ NSMutableArray *_model; + BOOL canAttachToSelectedSession_; + + IBOutlet NSTableColumn *checkColumn_; +@@ -28,7 +31,7 @@ extern NSString *kWindowPasteboardType; + - (instancetype)init { + self = [super init]; + if (self) { +- model_ = [[NSMutableArray alloc] init]; ++ _model = [[NSMutableArray alloc] init]; + } + return self; + } +@@ -39,27 +42,27 @@ extern NSString *kWindowPasteboardType; + [tableView_ setDraggingDestinationFeedbackStyle:NSTableViewDraggingDestinationFeedbackStyleRegular]; + } + +-- (void)dealloc +-{ +- [model_ release]; ++- (void)dealloc { ++ [_model release]; + [super dealloc]; + } + + - (void)setDelegate:(id)delegate { + delegate_ = delegate; +- [self setSessions:[delegate_ sessions]]; ++ [self setSessionObjects:[delegate_ sessionsTableObjects:self]]; + } + +-- (void)setSessions:(NSArray *)names ++- (void)setSessionObjects:(NSArray *)sessions + { +- [model_ removeAllObjects]; +- [model_ addObjectsFromArray:names]; ++ [_model removeAllObjects]; ++ [_model addObjectsFromArray:sessions]; + [tableView_ reloadData]; + } + +-- (void)selectSessionWithName:(NSString *)name +-{ +- NSUInteger i = [model_ indexOfObject:name]; ++- (void)selectSessionNumber:(int)number { ++ NSInteger i = [_model indexOfObjectPassingTest:^BOOL(iTermTmuxSessionObject * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { ++ return obj.number == number; ++ }]; + if (i != NSNotFound) { + [tableView_ selectRowIndexes:[NSIndexSet indexSetWithIndex:i] + byExtendingSelection:NO]; +@@ -74,47 +77,45 @@ extern NSString *kWindowPasteboardType; + + - (IBAction)removeSession:(id)sender + { +- NSString *name = [self selectedSessionName]; +- if (name) { +- [delegate_ removeSessionWithName:name]; ++ NSNumber *number = [self selectedSessionNumber]; ++ if (number) { ++ [delegate_ removeSessionWithNumber:number.intValue]; + } + } + + - (IBAction)attach:(id)sender { +- NSString *name = [self selectedSessionName]; +- if (name) { +- [delegate_ attachToSessionWithName:name]; ++ NSNumber *number = [self selectedSessionNumber]; ++ if (number) { ++ [delegate_ attachToSessionWithNumber:number.intValue]; + } + } + + - (IBAction)detach:(id)sender { +- NSString *name = [self selectedSessionName]; +- if (name) { ++ NSNumber *number = [self selectedSessionNumber]; ++ if (number) { + [delegate_ detach]; + } + } + + #pragma mark NSTableViewDataSource + +-- (NSInteger)numberOfRowsInTableView:(NSTableView *)aTableView +-{ +- return model_.count; ++- (NSInteger)numberOfRowsInTableView:(NSTableView *)aTableView { ++ return _model.count; + } + + - (id)tableView:(NSTableView *)aTableView + objectValueForTableColumn:(NSTableColumn *)aTableColumn +- row:(NSInteger)rowIndex +-{ +- NSString *name = [model_ objectAtIndex:rowIndex]; ++ row:(NSInteger)rowIndex { ++ iTermTmuxSessionObject *sessionObject = _model[rowIndex]; + if (aTableColumn == checkColumn_) { +- if ([[delegate_ nameOfAttachedSession] isEqualToString:name]) { ++ if ([[delegate_ numberOfAttachedSession] isEqual:@(sessionObject.number)]) { + return @"✓"; + } else { + return @""; + } + } else { +- if (rowIndex < model_.count) { +- return name; ++ if (rowIndex < _model.count) { ++ return sessionObject.name; + } else { + return nil; + } +@@ -124,10 +125,9 @@ extern NSString *kWindowPasteboardType; + - (void)tableView:(NSTableView *)aTableView + setObjectValue:(id)anObject + forTableColumn:(NSTableColumn *)aTableColumn +- row:(NSInteger)rowIndex +-{ +- [delegate_ renameSessionWithName:[model_ objectAtIndex:rowIndex] +- toName:(NSString *)anObject]; ++ row:(NSInteger)rowIndex { ++ [delegate_ renameSessionWithNumber:_model[rowIndex].number ++ toName:(NSString *)anObject]; + } + + #pragma mark NSTableViewDataSource +@@ -138,16 +138,15 @@ extern NSString *kWindowPasteboardType; + return YES; + } + +-- (void)tableViewSelectionDidChange:(NSNotification *)aNotification +-{ ++- (void)tableViewSelectionDidChange:(NSNotification *)aNotification { + [self updateEnabledStateOfButtons]; +- [delegate_ selectedSessionChangedTo:[self selectedSessionName]]; ++ [delegate_ selectedSessionDidChange]; + } + +-- (NSString *)selectedSessionName { ++- (NSNumber *)selectedSessionNumber { + int i = [tableView_ selectedRow]; +- if (i >= 0 && i < model_.count) { +- return [model_ objectAtIndex:i]; ++ if (i >= 0 && i < _model.count) { ++ return @(_model[i].number); + } else { + return nil; + } +@@ -159,19 +158,19 @@ extern NSString *kWindowPasteboardType; + dropOperation:(NSTableViewDropOperation)operation { + NSPasteboard *pb = [info draggingPasteboard]; + NSArray* pair = [pb propertyListForType:kWindowPasteboardType]; +- NSString *sessionName = [pair objectAtIndex:0]; +- NSArray *draggedItems = [pair objectAtIndex:1]; +- NSString *targetSession = [model_ objectAtIndex:row]; ++ NSNumber *sessionNumber = pair[0]; ++ NSArray *draggedItems = pair[1]; ++ iTermTmuxSessionObject *targetSessionObject = _model[row]; + for (NSArray *tuple in draggedItems) { + NSNumber *windowId = [tuple objectAtIndex:1]; + if (info.draggingSourceOperationMask & NSDragOperationLink) { + [delegate_ linkWindowId:[windowId intValue] +- inSession:sessionName +- toSession:targetSession]; ++ inSessionNumber:sessionNumber.intValue ++ toSessionNumber:targetSessionObject.number]; + } else { + [delegate_ moveWindowId:[windowId intValue] +- inSession:sessionName +- toSession:targetSession]; ++ inSessionNumber:sessionNumber.intValue ++ toSessionNumber:targetSessionObject.number]; + } + } + return YES; +@@ -205,11 +204,16 @@ extern NSString *kWindowPasteboardType; + } + } + +-- (NSString *)nameForNewSession +-{ ++- (BOOL)haveSessionWithName:(NSString *)name { ++ return [_model anyWithBlock:^BOOL(iTermTmuxSessionObject *anObject) { ++ return [anObject.name isEqualToString:name]; ++ }]; ++} ++ ++- (NSString *)nameForNewSession { + int n = 0; + NSString *candidate = [self nameForNewSessionWithNumber:n]; +- while ([model_ indexOfObject:candidate] != NSNotFound) { ++ while ([self haveSessionWithName:candidate]) { + n++; + candidate = [self nameForNewSessionWithNumber:n]; + } +@@ -223,7 +227,9 @@ extern NSString *kWindowPasteboardType; + [detachButton_ setEnabled:NO]; + [removeButton_ setEnabled:NO]; + } else { +- BOOL isAttachedSession = [[delegate_ nameOfAttachedSession] isEqualToString:[self selectedSessionName]]; ++ NSNumber *selected = [self selectedSessionNumber]; ++ BOOL isAttachedSession = (selected != nil && ++ [[delegate_ numberOfAttachedSession] isEqual:@(selected.intValue)]); + [attachButton_ setEnabled:!isAttachedSession]; + [detachButton_ setEnabled:isAttachedSession]; + [removeButton_ setEnabled:YES]; +diff --git sources/TmuxWindowsTable.h sources/TmuxWindowsTable.h +index 7c3e16c..6e9604e 100644 +--- sources/TmuxWindowsTable.h ++++ sources/TmuxWindowsTable.h +@@ -22,7 +22,7 @@ extern NSString *kWindowPasteboardType; + - (BOOL)haveSelectedSession; + - (BOOL)currentSessionSelected; + - (BOOL)haveOpenWindowWithId:(int)windowId; +-- (NSString *)selectedSessionName; ++- (NSNumber *)selectedSessionNumber; + - (void)tmuxWindowsTableDidSelectWindowWithId:(int)windowId; + + @end +diff --git sources/TmuxWindowsTable.m sources/TmuxWindowsTable.m +index 56f4954..8f92993 100644 +--- sources/TmuxWindowsTable.m ++++ sources/TmuxWindowsTable.m +@@ -173,9 +173,9 @@ NSString *kWindowPasteboardType = @"kWindowPasteboardType"; + - (void)tableView:(NSTableView *)aTableView + setObjectValue:(id)anObject + forTableColumn:(NSTableColumn *)aTableColumn +- row:(NSInteger)rowIndex +-{ +- [delegate_ renameWindowWithId:[[[[self filteredModel] objectAtIndex:rowIndex] objectAtIndex:1] intValue] ++ row:(NSInteger)rowIndex { ++ const int windowID = [[[[self filteredModel] objectAtIndex:rowIndex] objectAtIndex:1] intValue]; ++ [delegate_ renameWindowWithId:windowID + toName:anObject]; + } + +@@ -200,7 +200,7 @@ NSString *kWindowPasteboardType = @"kWindowPasteboardType"; + NSArray* selectedItems = [[self filteredModel] objectsAtIndexes:rowIndexes]; + [pboard declareTypes:[NSArray arrayWithObject:kWindowPasteboardType] owner:self]; + [pboard setPropertyList:[NSArray arrayWithObjects: +- [delegate_ selectedSessionName], ++ [delegate_ selectedSessionNumber], + selectedItems, + nil] + forType:kWindowPasteboardType]; +@@ -219,14 +219,13 @@ NSString *kWindowPasteboardType = @"kWindowPasteboardType"; + + #pragma mark - Private + +-- (NSArray *)selectedWindowIdsAsStrings +-{ +- NSArray *ids = [self selectedWindowIds]; ++- (NSArray *)selectedWindowIdsAsStrings { ++ NSArray *ids = [self selectedWindowIds]; + NSMutableArray *result = [NSMutableArray array]; +- for (NSString *n in ids) { +- [result addObject:n]; +- } +- return result; ++ for (NSString *n in ids) { ++ [result addObject:n]; ++ } ++ return result; + } + + - (NSArray *)selectedWindowIds +diff --git sources/iTermInitialDirectory+Tmux.h sources/iTermInitialDirectory+Tmux.h +index a470a56..586ade6 100644 +--- sources/iTermInitialDirectory+Tmux.h ++++ sources/iTermInitialDirectory+Tmux.h +@@ -13,7 +13,7 @@ NS_ASSUME_NONNULL_BEGIN + + @interface iTermInitialDirectory (Tmux) + +-- (void)tmuxNewWindowCommandInSession:(nullable NSString *)session ++- (void)tmuxNewWindowCommandInSessionNumber:(nullable NSNumber *)sessionNumber + recyclingSupported:(BOOL)recyclingSupported + scope:(iTermVariableScope *)scope + completion:(void (^)(NSString *))completion; +diff --git sources/iTermInitialDirectory+Tmux.m sources/iTermInitialDirectory+Tmux.m +index a67bea4..3259eda 100644 +--- sources/iTermInitialDirectory+Tmux.m ++++ sources/iTermInitialDirectory+Tmux.m +@@ -11,14 +11,14 @@ + + @implementation iTermInitialDirectory(Tmux) + +-- (void)tmuxNewWindowCommandInSession:(NSString *)session +- recyclingSupported:(BOOL)recyclingSupported +- scope:(iTermVariableScope *)scope +- completion:(void (^)(NSString *))completion { ++- (void)tmuxNewWindowCommandInSessionNumber:(nullable NSNumber *)sessionNumber ++ recyclingSupported:(BOOL)recyclingSupported ++ scope:(iTermVariableScope *)scope ++ completion:(void (^)(NSString *))completion { + NSArray *args = @[ @"new-window", @"-PF '#{window_id}'" ]; + +- if (session) { +- NSString *targetSessionArg = [NSString stringWithFormat:@"\"%@:+\"", [session stringByEscapingQuotes]]; ++ if (sessionNumber) { ++ NSString *targetSessionArg = [NSString stringWithFormat:@"\"$%d:+\"", sessionNumber.intValue]; + NSArray *insertionArguments = @[ @"-a", + @"-t", + targetSessionArg ]; +@@ -33,10 +33,10 @@ + - (void)tmuxNewWindowCommandRecyclingSupported:(BOOL)recyclingSupported + scope:(iTermVariableScope *)scope + completion:(void (^)(NSString *))completion { +- [self tmuxNewWindowCommandInSession:nil +- recyclingSupported:recyclingSupported +- scope:scope +- completion:completion]; ++ [self tmuxNewWindowCommandInSessionNumber:nil ++ recyclingSupported:recyclingSupported ++ scope:scope ++ completion:completion]; + } + + - (void)tmuxSplitWindowCommand:(int)wp +diff --git sources/iTermTmuxOptionMonitor.h sources/iTermTmuxOptionMonitor.h +index 147fcd6..8dffe52 100644 +--- sources/iTermTmuxOptionMonitor.h ++++ sources/iTermTmuxOptionMonitor.h +@@ -18,8 +18,12 @@ NS_ASSUME_NONNULL_BEGIN + @property (nullable, nonatomic, strong) iTermVariableScope *scope; + + - (instancetype)init NS_UNAVAILABLE; ++ ++// If `fallbackVariableName` is nonnil, the value of the variable named ++// `fallbackVariableName` will be used for tmux 2.8 and earlier. + - (instancetype)initWithGateway:(TmuxGateway *)gateway + scope:(iTermVariableScope *)scope ++ fallbackVariableName:(nullable NSString *)fallbackVariableName + format:(NSString *)format + target:(NSString *)tmuxTarget + variableName:(nullable NSString *)variableName +diff --git sources/iTermTmuxOptionMonitor.m sources/iTermTmuxOptionMonitor.m +index 4f72d02..fd6120f 100644 +--- sources/iTermTmuxOptionMonitor.m ++++ sources/iTermTmuxOptionMonitor.m +@@ -10,6 +10,7 @@ + #import "DebugLogging.h" + #import "iTermVariables.h" + #import "iTermVariableScope+Session.h" ++#import "NSStringITerm.h" + #import "NSTimer+iTerm.h" + #import "TmuxGateway.h" + +@@ -19,11 +20,13 @@ + BOOL _haveOutstandingRequest; + NSString *_target; + NSString *_variableName; ++ NSString *_fallbackVariableName; + void (^_block)(NSString *); + } + + - (instancetype)initWithGateway:(TmuxGateway *)gateway + scope:(iTermVariableScope *)scope ++ fallbackVariableName:(NSString *)fallbackVariableName + format:(NSString *)format + target:(NSString *)target + variableName:(NSString *)variableName +@@ -36,6 +39,7 @@ + _target = [target copy]; + _variableName = [variableName copy]; + _block = [block copy]; ++ _fallbackVariableName = [fallbackVariableName copy]; + } + return self; + } +@@ -64,13 +68,21 @@ + [self updateOnce]; + } + ++- (NSString *)command { ++ return [NSString stringWithFormat:@"display-message -t '%@' -p '%@'", _target, self.escapedFormat]; ++} ++ + - (void)updateOnce { + if (_haveOutstandingRequest) { + DLog(@"Not making a request because one is outstanding"); + return; + } ++ if (_fallbackVariableName && self.gateway.minimumServerVersion.doubleValue <= 2.9) { ++ [self didFetch:[self.scope valueForVariableName:_fallbackVariableName]]; ++ return; ++ } + _haveOutstandingRequest = YES; +- NSString *command = [NSString stringWithFormat:@"display-message -t '%@' -p '%@'", _target, self.escapedFormat]; ++ NSString *command = [self command]; + DLog(@"Request option with command %@", command); + [self.gateway sendCommand:command + responseTarget:self +@@ -80,7 +92,7 @@ + } + + - (void)didFetch:(NSString *)value { +- DLog(@"Did fetch %@", value); ++ DLog(@"%@ -> %@", self.command, value); + if (!value) { + // Probably the pane went away and we'll be dealloced soon. + return; +diff --git sources/iTermTmuxSessionObject.h sources/iTermTmuxSessionObject.h +new file mode 100644 +index 0000000..6407d5f +--- /dev/null ++++ sources/iTermTmuxSessionObject.h +@@ -0,0 +1,17 @@ ++// ++// iTermTmuxSessionObject.h ++// iTerm2SharedARC ++// ++// Created by George Nachman on 9/25/19. ++// ++ ++#import ++ ++NS_ASSUME_NONNULL_BEGIN ++ ++@interface iTermTmuxSessionObject : NSObject ++@property (nonatomic, copy) NSString *name; ++@property (nonatomic) int number; ++@end ++ ++NS_ASSUME_NONNULL_END +diff --git sources/iTermTmuxSessionObject.m sources/iTermTmuxSessionObject.m +new file mode 100644 +index 0000000..5ac9130 +--- /dev/null ++++ sources/iTermTmuxSessionObject.m +@@ -0,0 +1,12 @@ ++// ++// iTermTmuxSessionObject.m ++// iTerm2SharedARC ++// ++// Created by George Nachman on 9/25/19. ++// ++ ++#import "iTermTmuxSessionObject.h" ++ ++@implementation iTermTmuxSessionObject ++ ++@end +diff --git sources/iTermTmuxStatusBarMonitor.h sources/iTermTmuxStatusBarMonitor.h +index 8172779..96338ec 100644 +--- sources/iTermTmuxStatusBarMonitor.h ++++ sources/iTermTmuxStatusBarMonitor.h +@@ -14,6 +14,7 @@ + + NS_ASSUME_NONNULL_BEGIN + ++// NOTE: Requires tmux 2.9 + @interface iTermTmuxStatusBarMonitor : NSObject + + @property (nonatomic) BOOL active; +diff --git sources/iTermTmuxStatusBarMonitor.m sources/iTermTmuxStatusBarMonitor.m +index 50504da..0d3daf3 100644 +--- sources/iTermTmuxStatusBarMonitor.m ++++ sources/iTermTmuxStatusBarMonitor.m +@@ -9,6 +9,7 @@ + + #import "DebugLogging.h" + #import "iTermVariableScope.h" ++#import "NSStringITerm.h" + #import "NSTimer+iTerm.h" + #import "TmuxGateway.h" + #import "RegexKitLite.h" +@@ -49,13 +50,8 @@ + + - (void)requestUpdates { + _accelerated = NO; +- [_gateway sendCommand:@"display-message -p \"#{status-left}\"" responseTarget:self responseSelector:@selector(handleStatusLeftResponse:)]; +- [_gateway sendCommand:@"display-message -p \"#{status-right}\"" responseTarget:self responseSelector:@selector(handleStatusRightResponse:)]; +-} +- +-- (NSString *)escapedString:(NSString *)string { +- return [[string stringByReplacingOccurrencesOfString:@"\\" withString:@"\\\\"] +- stringByReplacingOccurrencesOfString:@"\"" withString:@"\\\""]; ++ [_gateway sendCommand:@"display-message -p \"#{T:status-left}\"" responseTarget:self responseSelector:@selector(handleStatusLeftValueExpansionResponse:)]; ++ [_gateway sendCommand:@"display-message -p \"#{T:status-right}\"" responseTarget:self responseSelector:@selector(handleStatusRightValueExpansionResponse:)]; + } + + - (void)handleStatusIntervalResponse:(NSString *)response { +@@ -66,28 +62,14 @@ + _timer = [NSTimer scheduledWeakTimerWithTimeInterval:interval target:self selector:@selector(timerDidFire:) userInfo:nil repeats:YES]; + } + +-- (void)handleStatusLeftResponse:(NSString *)response { +- if (!response) { +- return; +- } +- NSString *command = [NSString stringWithFormat:@"display-message -p \"%@\"", [self escapedString:response]]; +- [_gateway sendCommand:command responseTarget:self responseSelector:@selector(handleStatusLeftValueExpansionResponse:)]; +-} +- +-- (void)handleStatusRightResponse:(NSString *)response { +- if (!response) { +- return; +- } +- NSString *command = [NSString stringWithFormat:@"display-message -p \"%@\"", [self escapedString:response]]; +- [_gateway sendCommand:command responseTarget:self responseSelector:@selector(handleStatusRightValueExpansionResponse:)]; +-} +- + - (void)handleStatusLeftValueExpansionResponse:(NSString *)string { ++ DLog(@"Left status bar is: %@", string); + [self.scope setValue:[self sanitizedString:string] ?: @"" forVariableNamed:iTermVariableKeySessionTmuxStatusLeft]; + [self accelerateUpdateIfStringContainsNotReady:string]; + } + + - (void)handleStatusRightValueExpansionResponse:(NSString *)string { ++ DLog(@"Right status bar is: %@", string); + [self.scope setValue:[self sanitizedString:string] ?: @"" forVariableNamed:iTermVariableKeySessionTmuxStatusRight]; + [self accelerateUpdateIfStringContainsNotReady:string]; + } +diff --git sources/iTermWorkingDirectoryPoller.m sources/iTermWorkingDirectoryPoller.m +index 5271227..f4d8602 100644 +--- sources/iTermWorkingDirectoryPoller.m ++++ sources/iTermWorkingDirectoryPoller.m +@@ -37,6 +37,7 @@ + __weak __typeof(self) weakSelf = self; + _tmuxOptionMonitor = [[iTermTmuxOptionMonitor alloc] initWithGateway:gateway + scope:scope ++ fallbackVariableName:nil + format:@"#{pane_current_path}" + target:[NSString stringWithFormat:@"%%%@", @(windowPane)] + variableName:nil +-- +2.23.0 +