forked from gnachman/iTerm2
-
Notifications
You must be signed in to change notification settings - Fork 0
/
TmuxWindowOpener.m
366 lines (333 loc) · 12.9 KB
/
TmuxWindowOpener.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
//
// TmuxWindowOpener.m
// iTerm
//
// Created by George Nachman on 11/29/11.
//
#import "TmuxWindowOpener.h"
#import "iTermController.h"
#import "TmuxLayoutParser.h"
#import "ScreenChar.h"
#import "PseudoTerminal.h"
#import "TmuxHistoryParser.h"
#import "TmuxStateParser.h"
#import "PTYTab.h"
NSString * const kTmuxWindowOpenerStatePendingOutput = @"pending_output";
@interface TmuxWindowOpener (Private)
- (id)appendRequestsForNode:(NSMutableDictionary *)node
toArray:(NSMutableArray *)cmdList;
- (void)decorateParseTree:(NSMutableDictionary *)parseTree;
- (id)decorateWindowPane:(NSMutableDictionary *)parseTree;
- (void)requestDidComplete;
- (void)dumpHistoryResponse:(NSString *)response
paneAndAlternate:(NSArray *)info;
- (NSDictionary *)dictForDumpStateForWindowPane:(NSNumber *)wp;
- (NSDictionary *)dictForRequestHistoryForWindowPane:(NSNumber *)wp
alt:(BOOL)alternate;
- (NSDictionary *)dictForGetPendingOutputForWindowPane:(NSNumber *)wp;
- (void)appendRequestsForWindowPane:(NSNumber *)wp
toArray:(NSMutableArray *)cmdList;
@end
@implementation TmuxWindowOpener
@synthesize windowIndex = windowIndex_;
@synthesize name = name_;
@synthesize size = size_;
@synthesize layout = layout_;
@synthesize maxHistory = maxHistory_;
@synthesize gateway = gateway_;
@synthesize parseTree = parseTree_;
@synthesize controller = controller_;
@synthesize target = target_;
@synthesize selector = selector_;
@synthesize ambiguousIsDoubleWidth = ambiguousIsDoubleWidth_;
+ (TmuxWindowOpener *)windowOpener
{
return [[[TmuxWindowOpener alloc] init] autorelease];
}
- (id)init
{
self = [super init];
if (self) {
histories_ = [[NSMutableDictionary alloc] init];
altHistories_ = [[NSMutableDictionary alloc] init];
states_ = [[NSMutableDictionary alloc] init];
}
return self;
}
- (void)dealloc
{
[name_ release];
[layout_ release];
[gateway_ release];
[parseTree_ release];
[target_ release];
[histories_ release];
[altHistories_ release];
[states_ release];
[tabToUpdate_ release];
[super dealloc];
}
- (void)openWindows:(BOOL)initial
{
if (!self.layout) {
NSLog(@"Bad layout");
return;
}
self.parseTree = [[TmuxLayoutParser sharedInstance] parsedLayoutFromString:self.layout];
if (!self.parseTree) {
[gateway_ abortWithErrorMessage:[NSString stringWithFormat:@"Error parsing layout %@", self.layout]];
return;
}
NSMutableArray *cmdList = [NSMutableArray array];
[[TmuxLayoutParser sharedInstance] depthFirstSearchParseTree:self.parseTree
callingSelector:@selector(appendRequestsForNode:toArray:)
onTarget:self
withObject:cmdList];
[gateway_ sendCommandList:cmdList initial:initial];
}
- (void)updateLayoutInTab:(PTYTab *)tab;
{
if (!self.layout) {
NSLog(@"Bad layout");
return;
}
if (!self.controller) {
NSLog(@"No controller");
return;
}
if (!self.gateway) {
NSLog(@"No gateway");
return;
}
TmuxLayoutParser *parser = [TmuxLayoutParser sharedInstance];
self.parseTree = [parser parsedLayoutFromString:self.layout];
if (!self.parseTree) {
[gateway_ abortWithErrorMessage:[NSString stringWithFormat:@"Error parsing layout %@", self.layout]];
return;
}
NSSet *oldPanes = [NSSet setWithArray:[tab windowPanes]];
NSMutableArray *cmdList = [NSMutableArray array];
for (NSNumber *addedPane in [parser windowPanesInParseTree:self.parseTree]) {
if (![oldPanes containsObject:addedPane]) {
[self appendRequestsForWindowPane:addedPane
toArray:cmdList];
}
}
if (cmdList.count) {
tabToUpdate_ = [tab retain];
[gateway_ sendCommandList:cmdList];
} else {
[tab setTmuxLayout:self.parseTree
tmuxController:controller_];
if ([tab layoutIsTooLarge]) {
// The tab's root splitter is larger than the window's tabview.
// If there are no outstanding window resizes then setTmuxLayout:tmuxController:
// has called fitWindowToTabs:, and it's still too big, so shrink
// the layout.
for (TmuxController *controller in [[tab realParentWindow] uniqueTmuxControllers]) {
if ([controller hasOutstandingWindowResize]) {
return;
}
}
[controller_ fitLayoutToWindows];
}
}
}
@end
@implementation TmuxWindowOpener (Private)
// This is called for each window pane via a DFS. It sends all commands needed
// to open a window.
- (id)appendRequestsForNode:(NSMutableDictionary *)node
toArray:(NSMutableArray *)cmdList
{
NSNumber *wp = [node objectForKey:kLayoutDictWindowPaneKey];
[self appendRequestsForWindowPane:wp toArray:cmdList];
return nil; // returning nil means keep going with the DFS
}
- (void)appendRequestsForWindowPane:(NSNumber *)wp
toArray:(NSMutableArray *)cmdList
{
[cmdList addObject:[self dictForRequestHistoryForWindowPane:wp alt:NO]];
[cmdList addObject:[self dictForRequestHistoryForWindowPane:wp alt:YES]];
[cmdList addObject:[self dictForDumpStateForWindowPane:wp]];
[cmdList addObject:[self dictForGetPendingOutputForWindowPane:wp]];
}
- (NSDictionary *)dictForGetPendingOutputForWindowPane:(NSNumber *)wp
{
++pendingRequests_;
NSString *command = [NSString stringWithFormat:@"capture-pane -p -P -C -t %%%d", [wp intValue]];
return [gateway_ dictionaryForCommand:command
responseTarget:self
responseSelector:@selector(getPendingOutputResponse:pane:)
responseObject:wp
flags:kTmuxGatewayCommandWantsData];
}
- (NSDictionary *)dictForDumpStateForWindowPane:(NSNumber *)wp
{
++pendingRequests_;
NSString *command = [NSString stringWithFormat:@"list-panes -t %%%d -F \"%@\"", [wp intValue],
[TmuxStateParser format]];
return [gateway_ dictionaryForCommand:command
responseTarget:self
responseSelector:@selector(dumpStateResponse:pane:)
responseObject:wp
flags:0];
}
- (NSDictionary *)dictForRequestHistoryForWindowPane:(NSNumber *)wp
alt:(BOOL)alternate
{
++pendingRequests_;
NSString *command = [NSString stringWithFormat:@"capture-pane -peqJ %@-t %%%d -S -%d",
(alternate ? @"-a " : @""), [wp intValue], self.maxHistory];
return [gateway_ dictionaryForCommand:command
responseTarget:self
responseSelector:@selector(dumpHistoryResponse:paneAndAlternate:)
responseObject:[NSArray arrayWithObjects:
wp,
[NSNumber numberWithBool:alternate],
nil]
flags:0];
}
// Command response handler for dump-history
// info is an array: [window pane number, isAlternate flag]
- (void)dumpHistoryResponse:(NSString *)response
paneAndAlternate:(NSArray *)info
{
NSNumber *wp = [info objectAtIndex:0];
NSNumber *alt = [info objectAtIndex:1];
NSArray *history = [[TmuxHistoryParser sharedInstance] parseDumpHistoryResponse:response
ambiguousIsDoubleWidth:ambiguousIsDoubleWidth_];
if (history) {
if ([alt boolValue]) {
[altHistories_ setObject:history forKey:wp];
} else {
[histories_ setObject:history forKey:wp];
}
} else {
[[NSAlert alertWithMessageText:@"Error: malformed history line from tmux."
defaultButton:@"Ok"
alternateButton:@""
otherButton:@""
informativeTextWithFormat:@"See Console.app for details"] runModal];
}
[self requestDidComplete];
}
- (void)getPendingOutputResponse:(NSData *)response pane:(NSNumber *)wp
{
const char *bytes = response.bytes;
NSMutableData *pending = [NSMutableData data];
for (int i = 0; i < response.length; i++) {
char c = bytes[i];
if (c == '\\') {
if (i + 3 >= response.length) {
NSLog(@"Bogus pending output (truncated): %@", response);
return;
}
i++;
int value = 0;
for (int j = 0; j < 3; j++, i++) {
c = bytes[i];
if (c < '0' || c > '7') {
NSLog(@"Bogus pending output (non-octal): %@", response);
return;
}
value *= 8;
value += (c - '0');
}
i--;
c = value;
}
[pending appendBytes:&c length:1];
}
NSMutableDictionary *state = [[[states_ objectForKey:wp] mutableCopy] autorelease];
[state setObject:pending forKey:kTmuxWindowOpenerStatePendingOutput];
[states_ setObject:state forKey:wp];
[self requestDidComplete];
}
- (void)dumpStateResponse:(NSString *)response pane:(NSNumber *)wp
{
NSDictionary *state = [[TmuxStateParser sharedInstance] parsedStateFromString:response
forPaneId:[wp intValue]];
[states_ setObject:state forKey:wp];
[self requestDidComplete];
}
- (void)requestDidComplete
{
--pendingRequests_;
if (pendingRequests_ == 0) {
PseudoTerminal *term = nil;
if (!tabToUpdate_) {
term = [self.controller windowWithAffinityForWindowId:self.windowIndex];
} else {
term = [tabToUpdate_ realParentWindow];
}
if (!term) {
term = [[iTermController sharedInstance] openWindow];
}
NSMutableDictionary *parseTree = [[TmuxLayoutParser sharedInstance] parsedLayoutFromString:self.layout];
if (!parseTree) {
[gateway_ abortWithErrorMessage:[NSString stringWithFormat:@"Error parsing layout %@", self.layout]];
return;
}
[self decorateParseTree:parseTree];
if (tabToUpdate_) {
[tabToUpdate_ setTmuxLayout:parseTree
tmuxController:controller_];
if ([tabToUpdate_ layoutIsTooLarge]) {
[controller_ fitLayoutToWindows];
}
} else {
if (![self.controller window:windowIndex_]) {
// Safety valve: don't open an existing tmux window.
[term loadTmuxLayout:parseTree
window:windowIndex_
tmuxController:controller_
name:name_];
// Check if we know the position for the window
NSArray *panes = [[TmuxLayoutParser sharedInstance] windowPanesInParseTree:parseTree];
NSValue *windowPos = [self.controller positionForWindowWithPanes:panes];
if (windowPos) {
[[term window] setFrameOrigin:[windowPos pointValue]];
}
// This is to handle the case where we couldn't create a window as
// large as we were asked to (for instance, if the gateway is full-
// screen).
[controller_ windowDidResize:term];
}
}
if (self.target) {
[self.target performSelector:self.selector
withObject:[NSNumber numberWithInt:windowIndex_]];
}
}
}
// Add info from command responses to leaf nodes of parse tree.
- (void)decorateParseTree:(NSMutableDictionary *)parseTree
{
[[TmuxLayoutParser sharedInstance] depthFirstSearchParseTree:parseTree
callingSelector:@selector(decorateWindowPane:)
onTarget:self
withObject:nil];
}
// Callback for DFS of parse tree from decorateParseTree:
- (id)decorateWindowPane:(NSMutableDictionary *)parseTree
{
NSNumber *n = [parseTree objectForKey:kLayoutDictWindowPaneKey];
if (!n) {
return nil;
}
NSArray *history = [histories_ objectForKey:n];
if (history) {
[parseTree setObject:history forKey:kLayoutDictHistoryKey];
}
history = [altHistories_ objectForKey:n];
if (history) {
[parseTree setObject:history forKey:kLayoutDictAltHistoryKey];
}
NSDictionary *state = [states_ objectForKey:n];
if (state) {
[parseTree setObject:state forKey:kLayoutDictStateKey];
}
return nil;
}
@end