diff --git a/NSStringITerm.h b/NSStringITerm.h index 6970aeff5b..5b62a00e17 100644 --- a/NSStringITerm.h +++ b/NSStringITerm.h @@ -34,12 +34,27 @@ #import +// This is the standard unicode replacement character for when input couldn't +// be parsed properly but we need to render something there. +#define UNICODE_REPLACEMENT_CHAR 0xfffd + +// Examine the leading UTF-8 sequence in a char array and check that it +// is properly encoded. Computes the number of bytes to use for the +// first code point. Returns the first code point, if it exists, in *result. +// +// Return value: +// positive: This many bytes compose a legal Unicode character. +// negative: abs(this many) bytes are illegal, should be replaced by one +// single replacement symbol. +// zero: Unfinished sequence, input needs to grow. +int decode_utf8_char(const unsigned char * restrict datap, + int datalen, + int * restrict result); @interface NSString (iTerm) + (NSString *)stringWithInt:(int)num; + (BOOL)isDoubleWidthCharacter:(int)unicode - encoding:(NSStringEncoding)e ambiguousIsDoubleWidth:(BOOL)ambiguousIsDoubleWidth; - (NSMutableString *)stringReplaceSubstringFrom:(NSString *)oldSubstring to:(NSString *)newSubstring; @@ -60,4 +75,8 @@ // Convert a string of hex values (an even number of [0-9A-Fa-f]) into data. - (NSData *)dataFromHexValues; +// Always returns a non-null vaule, but it may contain replacement chars for +// malformed utf-8 sequences. +- (NSString *)initWithUTF8DataIgnoringErrors:(NSData *)data; + @end diff --git a/NSStringITerm.m b/NSStringITerm.m index a9af4f73f0..c9b4276d16 100644 --- a/NSStringITerm.m +++ b/NSStringITerm.m @@ -71,7 +71,6 @@ + (NSString *)stringWithInt:(int)num } + (BOOL)isDoubleWidthCharacter:(int)unicode - encoding:(NSStringEncoding)e ambiguousIsDoubleWidth:(BOOL)ambiguousIsDoubleWidth { if (unicode <= 0xa0 || @@ -458,4 +457,98 @@ - (NSString *)stringByEscapingQuotes { stringByReplacingOccurrencesOfString:@"\"" withString:@"\\\""]; } +int decode_utf8_char(const unsigned char *datap, + int datalen, + int * restrict result) +{ + unsigned int theChar; + int utf8Length; + unsigned char c; + // This maps a utf-8 sequence length to the smallest code point it should + // encode (e.g., using 5 bytes to encode an ascii character would be + // considered an error). + unsigned int smallest[7] = { 0, 0, 0x80UL, 0x800UL, 0x10000UL, 0x200000UL, 0x4000000UL }; + + if (datalen == 0) { + return 0; + } + + c = *datap; + if ((c & 0x80) == 0x00) { + *result = c; + return 1; + } else if ((c & 0xE0) == 0xC0) { + theChar = c & 0x1F; + utf8Length = 2; + } else if ((c & 0xF0) == 0xE0) { + theChar = c & 0x0F; + utf8Length = 3; + } else if ((c & 0xF8) == 0xF0) { + theChar = c & 0x07; + utf8Length = 4; + } else if ((c & 0xFC) == 0xF8) { + theChar = c & 0x03; + utf8Length = 5; + } else if ((c & 0xFE) == 0xFC) { + theChar = c & 0x01; + utf8Length = 6; + } else { + return -1; + } + for (int i = 1; i < utf8Length; i++) { + if (datalen <= i) { + return 0; + } + c = datap[i]; + if ((c & 0xc0) != 0x80) { + // Expected a continuation character but did not get one. + return -i; + } + theChar = (theChar << 6) | (c & 0x3F); + } + + if (theChar < smallest[utf8Length]) { + // Reject overlong sequences. + return -utf8Length; + } + + *result = (int)theChar; + return utf8Length; +} + +- (NSString *)initWithUTF8DataIgnoringErrors:(NSData *)data { + const unsigned char *p = data.bytes; + int len = data.length; + int utf8DecodeResult; + int theChar = 0; + NSMutableData *utf16Data = [NSMutableData data]; + + while (len > 0) { + utf8DecodeResult = decode_utf8_char(p, len, &theChar); + if (utf8DecodeResult == 0) { + // Stop on end of stream. + break; + } else if (utf8DecodeResult < 0) { + theChar = UNICODE_REPLACEMENT_CHAR; + utf8DecodeResult = -utf8DecodeResult; + } else if (theChar > 0xFFFF) { + // Convert to surrogate pair. + UniChar high, low; + high = ((theChar - 0x10000) >> 10) + 0xd800; + low = (theChar & 0x3ff) + 0xdc00; + + [utf16Data appendBytes:&high length:sizeof(high)]; + theChar = low; + } + + UniChar c = theChar; + [utf16Data appendBytes:&c length:sizeof(c)]; + + p += utf8DecodeResult; + len -= utf8DecodeResult; + } + + return [self initWithData:utf16Data encoding:NSUTF16LittleEndianStringEncoding]; +} + @end diff --git a/PTYSession.m b/PTYSession.m index b41efe401e..bfd53ff085 100644 --- a/PTYSession.m +++ b/PTYSession.m @@ -2339,7 +2339,7 @@ - (void)setUniqueID:(NSString*)uniqueID - (NSString*)formattedName:(NSString*)base { - NSString *prefix = tmuxController_ ? @"↣ " : @""; + NSString *prefix = tmuxController_ ? [NSString stringWithFormat:@"↣ %@: ", [[self tab] tmuxWindowName]] : @""; BOOL baseIsBookmarkName = [base isEqualToString:bookmarkName]; PreferencePanel* panel = [PreferencePanel sharedInstance]; @@ -2880,6 +2880,7 @@ - (BOOL)doubleWidth - (void)setDoubleWidth:(BOOL)set { doubleWidth = set; + tmuxController_.ambiguousIsDoubleWidth = set; } - (BOOL)xtermMouseReporting @@ -3242,7 +3243,11 @@ - (void)setFont:(NSFont*)font } // If the window isn't able to adjust, or adjust enough, make the session // work with whatever size we ended up having. - [[self tab] fitSessionToCurrentViewSize:self]; + if ([self isTmuxClient]) { + [tmuxController_ windowDidResize:[[self tab] realParentWindow]]; + } else { + [[self tab] fitSessionToCurrentViewSize:self]; + } } - (void)synchronizeTmuxFonts:(NSNotification *)notification @@ -3630,6 +3635,7 @@ - (void)startTmuxMode tmuxMode_ = TMUX_GATEWAY; tmuxGateway_ = [[TmuxGateway alloc] initWithDelegate:self]; tmuxController_ = [[TmuxController alloc] initWithGateway:tmuxGateway_]; + tmuxController_.ambiguousIsDoubleWidth = doubleWidth; NSSize theSize; Profile *tmuxBookmark = [PTYTab tmuxBookmark]; theSize.width = MAX(1, [[tmuxBookmark objectForKey:KEY_COLUMNS] intValue]); @@ -3746,6 +3752,10 @@ - (void)tmuxWindowClosedWithId:(int)windowId - (void)tmuxWindowRenamedWithId:(int)windowId to:(NSString *)newName { + PTYTab *tab = [tmuxController_ window:windowId]; + if (tab) { + [tab setTmuxWindowName:newName]; + } [tmuxController_ windowWasRenamedWithId:windowId to:newName]; } diff --git a/PTYTab.h b/PTYTab.h index 4ab11ec738..3ea02eb712 100644 --- a/PTYTab.h +++ b/PTYTab.h @@ -118,6 +118,8 @@ static const int MIN_SESSION_COLUMNS = 2; // Temporarily hidden live views (this is needed to hold a reference count). NSMutableArray *hiddenLiveViews_; // SessionView objects + + NSString *tmuxWindowName_; } // init/dealloc @@ -255,6 +257,8 @@ static const int MIN_SESSION_COLUMNS = 2; - (NSSize)tmuxSize; // Size we are given the current layout - (NSSize)maxTmuxSize; +- (NSString *)tmuxWindowName; +- (void)setTmuxWindowName:(NSString *)tmuxWindowName; - (int)tmuxWindow; - (BOOL)isTmuxTab; diff --git a/PTYTab.m b/PTYTab.m index 5c37b488a7..843a6f5129 100644 --- a/PTYTab.m +++ b/PTYTab.m @@ -2113,8 +2113,8 @@ - (void)notifyWindowChanged } + (PTYTab *)tabWithArrangement:(NSDictionary*)arrangement - inTerminal:(PseudoTerminal*)term - hasFlexibleView:(BOOL)hasFlexible + inTerminal:(PseudoTerminal*)term + hasFlexibleView:(BOOL)hasFlexible { PTYTab* theTab; // Build a tree with splitters and SessionViews but no PTYSessions. @@ -2335,6 +2335,18 @@ - (int)tmuxWindow return tmuxWindow_; } +- (NSString *)tmuxWindowName +{ + return tmuxWindowName_ ? tmuxWindowName_ : @"tmux"; +} + +- (void)setTmuxWindowName:(NSString *)tmuxWindowName +{ + [tmuxWindowName_ autorelease]; + tmuxWindowName_ = [tmuxWindowName retain]; + [[self realParentWindow] setWindowTitle]; +} + + (Profile *)tmuxBookmark { Profile *bookmark = [[ProfileModel sharedInstance] bookmarkWithName:@"tmux"]; @@ -2970,9 +2982,11 @@ - (void)splitView:(PTYSplitView *)splitView draggingDidEndOfSplit:(int)splitterI // Ask the tmux server to perform the move and we'll update our layout when // it finishes. - [tmuxController_ windowPane:[session tmuxPane] - resizedBy:amount - horizontally:[splitView isVertical]]; + if (amount > 0) { + [tmuxController_ windowPane:[session tmuxPane] + resizedBy:amount + horizontally:[splitView isVertical]]; + } } // Prevent any session from becoming smaller than its minimum size because of diff --git a/PTYTextView.m b/PTYTextView.m index a358a1a52d..88936f55b1 100644 --- a/PTYTextView.m +++ b/PTYTextView.m @@ -6732,7 +6732,6 @@ - (int)inputMethodEditorLength fg, bg, &len, - 0, [[dataSource session] doubleWidth], NULL); @@ -6778,7 +6777,6 @@ - (BOOL)drawInputMethodEditorTextAt:(int)xStart fg, bg, &len, - 0, [[dataSource session] doubleWidth], &cursorIndex); int cursorX = 0; diff --git a/PseudoTerminal.m b/PseudoTerminal.m index edd31f4dbf..6199cf4d05 100644 --- a/PseudoTerminal.m +++ b/PseudoTerminal.m @@ -1382,7 +1382,7 @@ - (void)loadTmuxLayout:(NSMutableDictionary *)parseTree inTerminal:self tmuxWindow:window tmuxController:tmuxController]; - [self setWindowTitle:name]; + [tab setTmuxWindowName:name]; [tab setReportIdealSizeAsCurrent:YES]; [self fitWindowToTabs]; [tab setReportIdealSizeAsCurrent:NO]; @@ -2167,7 +2167,7 @@ - (void)windowDidToggleToolbarVisibility:(id)sender if ([term lionFullScreen]) { [term restoreFrameAfterToolbarToggle]; } else { - PtyLog(@"zeroing preToolbarToggleFrame_"); + PtyLog(@"zeroing preToolbarToggleFrame_"); preToolbarToggleFrame_ = NSZeroRect; } if ([[[term window] toolbar] isVisible] != [itad toolbarShouldBeVisible]) { diff --git a/README.md b/README.md index a329e4940e..60c08a2e8a 100644 --- a/README.md +++ b/README.md @@ -1 +1,2 @@ This site hosts code for iTerm2. Issues are still on Google Code becaue Github doesn't support attachments. + diff --git a/ScreenChar.h b/ScreenChar.h index 3c483af983..0ca3537c49 100644 --- a/ScreenChar.h +++ b/ScreenChar.h @@ -30,6 +30,7 @@ */ #import +#import "NSStringITerm.h" // This is used in the rightmost column when a double-width character would // have been split in half and was wrapped to the next line. It is nonprintable @@ -62,10 +63,7 @@ #define ITERM2_PRIVATE_BEGIN 0xf000 #define ITERM2_PRIVATE_END 0xf003 -// This is the standard unicode replacement character for when input couldn't -// be parsed properly but we need to render something there. -#define UNICODE_REPLACEMENT_CHAR 0xfffd -#define ONECHAR_UNKNOWN ('?') // Used for encodings other than utf-8. +#define ONECHAR_UNKNOWN ('?') // Relacement character for encodings other than utf-8. // Alternate semantics definitions // Default background color @@ -235,3 +233,5 @@ NSString* ScreenCharArrayToStringDebug(screen_char_t* screenChars, // Convert an array of chars to a string, quickly. NSString* CharArrayToString(unichar* charHaystack, int o); + +void DumpScreenCharArray(screen_char_t* screenChars, int lineLength); diff --git a/ScreenChar.m b/ScreenChar.m index 0a100f30c4..6b6ec3e072 100644 --- a/ScreenChar.m +++ b/ScreenChar.m @@ -335,6 +335,10 @@ BOOL IsHighSurrogate(unichar c) freeWhenDone:NO] autorelease]; } +void DumpScreenCharArray(screen_char_t* screenChars, int lineLength) { + NSLog(@"%@", ScreenCharArrayToStringDebug(screenChars, lineLength)); +} + NSString* ScreenCharArrayToStringDebug(screen_char_t* screenChars, int lineLength) { NSMutableString* result = [NSMutableString stringWithCapacity:lineLength]; diff --git a/TmuxController.h b/TmuxController.h index ebe5725b47..a445704893 100644 --- a/TmuxController.h +++ b/TmuxController.h @@ -55,6 +55,7 @@ extern NSString *kTmuxControllerAttachedSessionDidChange; @property (nonatomic, retain) NSMutableDictionary *windowPositions; @property (nonatomic, copy) NSString *sessionName; @property (nonatomic, retain) NSArray *sessions; +@property (nonatomic, assign) BOOL ambiguousIsDoubleWidth; - (id)initWithGateway:(TmuxGateway *)gateway; - (void)openWindowsInitial; @@ -117,6 +118,7 @@ extern NSString *kTmuxControllerAttachedSessionDidChange; - (void)killSession:(NSString *)sessionName; - (void)attachToSession:(NSString *)sessionName; - (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 diff --git a/TmuxController.m b/TmuxController.m index 56a5a248b6..ddf026b864 100644 --- a/TmuxController.m +++ b/TmuxController.m @@ -52,6 +52,7 @@ @implementation TmuxController @synthesize windowPositions = windowPositions_; @synthesize sessionName = sessionName_; @synthesize sessions = sessions_; +@synthesize ambiguousIsDoubleWidth = ambiguousIsDoubleWidth_; - (id)initWithGateway:(TmuxGateway *)gateway { @@ -103,6 +104,7 @@ - (void)openWindowWithIndex:(int)windowIndex } [pendingWindowOpens_ addObject:n]; TmuxWindowOpener *windowOpener = [TmuxWindowOpener windowOpener]; + windowOpener.ambiguousIsDoubleWidth = ambiguousIsDoubleWidth_; windowOpener.windowIndex = windowIndex; windowOpener.name = name; windowOpener.size = size; @@ -127,6 +129,7 @@ - (void)setLayoutInTab:(PTYTab *)tab toLayout:(NSString *)layout { TmuxWindowOpener *windowOpener = [TmuxWindowOpener windowOpener]; + windowOpener.ambiguousIsDoubleWidth = ambiguousIsDoubleWidth_; windowOpener.layout = layout; windowOpener.maxHistory = MAX([[gateway_ delegate] tmuxBookmarkSize].height, @@ -240,39 +243,44 @@ - (void)initialListWindowsResponse:(NSString *)response - (void)openWindowsInitial { NSSize size = [[gateway_ delegate] tmuxBookmarkSize]; - NSString *setSizeCommand = [NSString stringWithFormat:@"control set-client-size %d,%d", + NSString *setSizeCommand = [NSString stringWithFormat:@"refresh-client -C %d,%d", (int)size.width, (int)size.height]; NSString *listWindowsCommand = [NSString stringWithFormat:@"list-windows -F %@", kListWindowsFormat]; NSString *listSessionsCommand = @"list-sessions -F \"#{session_name}\""; - NSString *getAffinitiesCommand = [NSString stringWithFormat:@"control get-value affinities%d", sessionId_]; - NSString *getOriginsCommand = [NSString stringWithFormat:@"control get-value origins%d", sessionId_]; - NSString *getHiddenWindowsCommand = - [NSString stringWithFormat:@"control get-value hidden%d", sessionId_]; + NSString *getAffinitiesCommand = [NSString stringWithFormat:@"show -v -q -t $%d @affinities", sessionId_]; + NSString *getOriginsCommand = [NSString stringWithFormat:@"show -v -q -t $%d @origins", sessionId_]; + NSString *getHiddenWindowsCommand = [NSString stringWithFormat:@"show -v -q -t $%d @hidden", sessionId_]; NSArray *commands = [NSArray arrayWithObjects: [gateway_ dictionaryForCommand:setSizeCommand responseTarget:nil responseSelector:nil - responseObject:nil], + responseObject:nil + toleratesErrors:NO], [gateway_ dictionaryForCommand:getHiddenWindowsCommand responseTarget:self responseSelector:@selector(getHiddenWindowsResponse:) - responseObject:nil], + responseObject:nil + toleratesErrors:YES], [gateway_ dictionaryForCommand:getAffinitiesCommand responseTarget:self responseSelector:@selector(getAffinitiesResponse:) - responseObject:nil], + responseObject:nil + toleratesErrors:YES], [gateway_ dictionaryForCommand:getOriginsCommand responseTarget:self responseSelector:@selector(getOriginsResponse:) - responseObject:nil], + responseObject:nil + toleratesErrors:YES], [gateway_ dictionaryForCommand:listSessionsCommand responseTarget:self responseSelector:@selector(listSessionsResponse:) - responseObject:nil], + responseObject:nil + toleratesErrors:NO], [gateway_ dictionaryForCommand:listWindowsCommand responseTarget:self responseSelector:@selector(initialListWindowsResponse:) - responseObject:nil], + responseObject:nil + toleratesErrors:NO], nil]; [gateway_ sendCommandList:commands]; } @@ -366,12 +374,22 @@ - (void)setClientSize:(NSSize)size { assert(size.width > 0 && size.height > 0); lastSize_ = size; + NSString *listStr = [NSString stringWithFormat:@"list-windows -F \"#{window_id} #{window_layout}\""]; + NSArray *commands = [NSArray arrayWithObjects: + [gateway_ dictionaryForCommand:[NSString stringWithFormat:@"refresh-client -C %d,%d", + (int)size.width, (int)size.height] + responseTarget:nil + responseSelector:nil + responseObject:nil + toleratesErrors:NO], + [gateway_ dictionaryForCommand:listStr + responseTarget:self + responseSelector:@selector(listWindowsResponse:) + responseObject:nil + toleratesErrors:NO], + nil]; ++numOutstandingWindowResizes_; - [gateway_ sendCommand:[NSString stringWithFormat:@"control set-client-size %d,%d", - (int)size.width, (int)size.height] - responseTarget:self - responseSelector:@selector(clientSizeChangeResponse:) - responseObject:nil]; + [gateway_ sendCommandList:commands]; } - (BOOL)hasOutstandingWindowResize @@ -404,11 +422,13 @@ - (void)windowPane:(int)wp [gateway_ dictionaryForCommand:resizeStr responseTarget:nil responseSelector:nil - responseObject:nil], + responseObject:nil + toleratesErrors:NO], [gateway_ dictionaryForCommand:listStr responseTarget:self - responseSelector:@selector(clientSizeChangeResponse:) - responseObject:nil], + responseSelector:@selector(listWindowsResponse:) + responseObject:nil + toleratesErrors:NO], nil]; ++numOutstandingWindowResizes_; [gateway_ sendCommandList:commands]; @@ -438,12 +458,14 @@ - (void)newWindowWithAffinity:(int)windowId [gateway_ sendCommand:@"new-window -PF '#{window_id}'" responseTarget:self responseSelector:@selector(newWindowWithAffinityCreated:affinityWindow:) - responseObject:[NSString stringWithInt:windowId]]; + responseObject:[NSString stringWithInt:windowId] + toleratesErrors:NO]; } else { [gateway_ sendCommand:@"new-window -PF '#{window_id}'" responseTarget:self responseSelector:@selector(newWindowWithoutAffinityCreated:) - responseObject:nil]; + responseObject:nil + toleratesErrors:NO]; } } @@ -503,7 +525,8 @@ - (void)breakOutWindowPane:(int)windowPane toTabAside:(NSString *)sibling [gateway_ sendCommand:[NSString stringWithFormat:@"break-pane -P -F \"#{window_id}\" -t %%%d", windowPane] responseTarget:self responseSelector:@selector(windowPaneBrokeOutWithWindowId:setAffinityTo:) - responseObject:sibling]; + responseObject:sibling + toleratesErrors:NO]; } - (void)windowPaneBrokeOutWithWindowId:(NSString *)windowId @@ -542,7 +565,8 @@ - (void)openWindowWithId:(int)windowId responseSelector:@selector(listedWindowsToOpenOne:forWindowIdAndAffinities:) responseObject:[NSArray arrayWithObjects:[NSNumber numberWithInt:windowId], affinities, - nil]]; + nil] + toleratesErrors:NO]; } - (void)openWindowWithId:(int)windowId @@ -622,19 +646,21 @@ - (void)listWindowsInSession:(NSString *)sessionName responseObject:[NSArray arrayWithObjects:object, NSStringFromSelector(selector), target, - nil]]; + nil] + toleratesErrors:YES]; // Tolerates errors because the session may have been detached while the command was on the wire, and this command is called automatically. } - (void)saveHiddenWindows { NSString *hidden = [[hiddenWindows_ allObjects] componentsJoinedByString:@","]; NSString *command = [NSString stringWithFormat: - @"control set-value hidden%d=%@", + @"set -t $%d @hidden \"%@\"", sessionId_, hidden]; [gateway_ sendCommand:command responseTarget:nil responseSelector:nil - responseObject:nil]; + responseObject:nil + toleratesErrors:NO]; } - (void)saveWindowOrigins @@ -671,7 +697,7 @@ - (void)saveWindowOrigins } } NSString *enc = [maps componentsJoinedByString:@" "]; - NSString *command = [NSString stringWithFormat:@"control set-value \"origins%d=%@\"", + NSString *command = [NSString stringWithFormat:@"set -t $%d @origins \"%@\"", sessionId_, [enc stringByEscapingQuotes]]; if (!lastOrigins_ || ![command isEqualToString:lastOrigins_]) { [lastOrigins_ release]; @@ -716,7 +742,7 @@ - (void)saveAffinities } } NSString *arg = [affinities componentsJoinedByString:@" "]; - NSString *command = [NSString stringWithFormat:@"control set-value \"affinities%d=%@\"", + NSString *command = [NSString stringWithFormat:@"set -t $%d @affinities \"%@\"", sessionId_, [arg stringByEscapingQuotes]]; if ([command isEqualToString:lastSaveAffinityCommand_]) { return; @@ -778,6 +804,10 @@ - (int)windowIdFromString:(NSString *)s - (void)didListWindows:(NSString *)response userData:(NSArray *)userData { + if (!response) { + // In case of error. + response = @""; + } TSVDocument *doc = [response tsvDocumentWithFields:[self listWindowFields]]; id object = [userData objectAtIndex:0]; SEL selector = NSSelectorFromString([userData objectAtIndex:1]); @@ -787,38 +817,41 @@ - (void)didListWindows:(NSString *)response userData:(NSArray *)userData - (void)getHiddenWindowsResponse:(NSString *)response { - if (response.length == 0) { - return; - } - NSArray *windowIds = [response componentsSeparatedByString:@","]; [hiddenWindows_ removeAllObjects]; - NSLog(@"getHiddneWindowsResponse: Add these window IDS to hidden: %@", windowIds); - for (NSString *wid in windowIds) { - [hiddenWindows_ addObject:[NSNumber numberWithInt:[wid intValue]]]; - } + if ([response length] > 0) { + NSArray *windowIds = [response componentsSeparatedByString:@","]; + NSLog(@"getHiddneWindowsResponse: Add these window IDS to hidden: %@", windowIds); + for (NSString *wid in windowIds) { + [hiddenWindows_ addObject:[NSNumber numberWithInt:[wid intValue]]]; + } + } } - (void)getAffinitiesResponse:(NSString *)result { - // Replace the existing equivalence classes with those defined by the - // affinity response. - // For example "1,2,3 4,5,6" has two equivalence classes. - // 1=2=3 and 4=5=6. + // Replace the existing equivalence classes with those defined by the + // affinity response. + // For example "1,2,3 4,5,6" has two equivalence classes. + // 1=2=3 and 4=5=6. NSArray *affinities = [result componentsSeparatedByString:@" "]; [affinities_ release]; affinities_ = [[EquivalenceClassSet alloc] init]; + if (![result length]) { + return; + } + for (NSString *affset in affinities) { NSArray *siblings = [affset componentsSeparatedByString:@","]; NSString *exemplar = [siblings lastObject]; - if (siblings.count == 1) { - // This is a wee hack. If a tmux Window is in a native window with one tab - // then create an equivalence class containing only (wid, wid+"_ph"). ph=placeholder - // We'll never see a window id that's negative, but the equivalence - // class's existance signals not to apply the default mode for - // unrecognized windows. - exemplar = [exemplar stringByAppendingString:@"_ph"]; - } + if (siblings.count == 1) { + // This is a wee hack. If a tmux Window is in a native window with one tab + // then create an equivalence class containing only (wid, wid+"_ph"). ph=placeholder + // We'll never see a window id that's negative, but the equivalence + // class's existance signals not to apply the default mode for + // unrecognized windows. + exemplar = [exemplar stringByAppendingString:@"_ph"]; + } for (NSString *widString in siblings) { if (![widString isEqualToString:exemplar]) { [affinities_ setValue:widString @@ -830,27 +863,29 @@ - (void)getAffinitiesResponse:(NSString *)result - (void)getOriginsResponse:(NSString *)result { - NSArray *windows = [result componentsSeparatedByString:@" "]; [origins_ removeAllObjects]; - 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]]]; - } - } + 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)listSessionsResponse:(NSString *)result @@ -886,7 +921,7 @@ - (void)listedWindowsToOpenOne:(NSString *)response forWindowIdAndAffinities:(NS // When an iTerm2 window is resized, a control -s client-size w,h // command is sent. It responds with new layouts for all the windows in the // client's session. Update the layouts for the affected tabs. -- (void)clientSizeChangeResponse:(NSString *)response +- (void)listWindowsResponse:(NSString *)response { --numOutstandingWindowResizes_; if (numOutstandingWindowResizes_ > 0) { @@ -989,7 +1024,7 @@ - (void)windowDidOpen:(NSNumber *)windowIndex PTYTab *tab = [self window:[windowIndex intValue]]; PseudoTerminal *term = [tab realParentWindow]; NSValue *p = [origins_ objectForKey:windowIndex]; - if (term && p) { + if (term && p && ![term anyFullScreen]) { [[term window] setFrameOrigin:[p pointValue]]; } [self saveAffinities]; diff --git a/TmuxGateway.h b/TmuxGateway.h index db9e2220af..32949f0e38 100644 --- a/TmuxGateway.h +++ b/TmuxGateway.h @@ -9,6 +9,8 @@ @class TmuxController; +extern NSString * const kTmuxGatewayErrorDomain; + @protocol TmuxGatewayDelegate - (TmuxController *)tmuxController; @@ -51,7 +53,7 @@ typedef enum { // Data from parsing an incoming command ControlCommand command_; - NSMutableArray *commandQueue_; // Dictionaries + NSMutableArray *commandQueue_; // NSMutableDictionary objects NSMutableString *currentCommandResponse_; NSMutableDictionary *currentCommand_; // Set between %begin and %end @@ -63,8 +65,16 @@ typedef enum { // Returns any unconsumed data if tmux mode is exited. - (NSData *)readTask:(NSData *)data; -- (void)sendCommand:(NSString *)command responseTarget:(id)target responseSelector:(SEL)selector; -- (void)sendCommand:(NSString *)command responseTarget:(id)target responseSelector:(SEL)selector responseObject:(id)obj; +- (void)sendCommand:(NSString *)command + responseTarget:(id)target + responseSelector:(SEL)selector; + +- (void)sendCommand:(NSString *)command + responseTarget:(id)target + responseSelector:(SEL)selector + responseObject:(id)obj + toleratesErrors:(BOOL)toleratesErrors; + - (void)sendCommandList:(NSArray *)commandDicts; // Set initial to YES when notifications should be accepted after the last // command gets a response. @@ -75,7 +85,8 @@ typedef enum { - (NSDictionary *)dictionaryForCommand:(NSString *)command responseTarget:(id)target responseSelector:(SEL)selector - responseObject:(id)obj; + responseObject:(id)obj + toleratesErrors:(BOOL)toleratesErrors; - (void)sendKeys:(NSData *)data toWindowPane:(int)windowPane; - (void)detach; diff --git a/TmuxGateway.m b/TmuxGateway.m index 98254ecfb9..59251e0eac 100644 --- a/TmuxGateway.m +++ b/TmuxGateway.m @@ -9,6 +9,9 @@ #import "RegexKitLite.h" #import "TmuxController.h" #import "iTermApplicationDelegate.h" +#import "NSStringITerm.h" + +NSString * const kTmuxGatewayErrorDomain = @"kTmuxGatewayErrorDomain";; #define NEWLINE @"\r" @@ -29,6 +32,10 @@ static NSString *kCommandString = @"string"; static NSString *kCommandObject = @"object"; static NSString *kCommandIsInitial = @"isInitial"; +static NSString *kCommandToleratesErrors = @"toleratesErrors"; +static NSString *kCommandId = @"id"; +static NSString *kCommandIsInList = @"inList"; +static NSString *kCommandIsLastInList = @"lastInList"; @implementation TmuxGateway @@ -57,7 +64,6 @@ - (void)dealloc - (void)abortWithErrorMessage:(NSString *)message { // TODO: be more forgiving of errors. - NSLog(@"TmuxGateway parse errror: %@", message); [[NSAlert alertWithMessageText:@"A tmux protocol error occurred." defaultButton:@"Ok" alternateButton:@"" @@ -199,15 +205,26 @@ - (void)hostDisconnected state_ = CONTROL_STATE_DETACHED; } -- (void)currentCommandResponseFinished +- (void)currentCommandResponseFinishedWithError:(BOOL)withError { id target = [currentCommand_ objectForKey:kCommandTarget]; if (target) { SEL selector = NSSelectorFromString([currentCommand_ objectForKey:kCommandSelector]); id obj = [currentCommand_ objectForKey:kCommandObject]; - [target performSelector:selector - withObject:currentCommandResponse_ - withObject:obj]; + if (withError) { + if ([[currentCommand_ objectForKey:kCommandToleratesErrors] boolValue]) { + [target performSelector:selector + withObject:nil + withObject:obj]; + } else { + [self abortWithErrorMessage:[NSString stringWithFormat:@"Error: %@", currentCommand_]]; + return; + } + } else { + [target performSelector:selector + withObject:currentCommandResponse_ + withObject:obj]; + } } if ([[currentCommand_ objectForKey:kCommandIsInitial] boolValue]) { acceptNotifications_ = YES; @@ -218,6 +235,32 @@ - (void)currentCommandResponseFinished currentCommandResponse_ = nil; } +- (void)parseBegin:(NSString *)command +{ + NSArray *components = [command captureComponentsMatchedByRegex:@"^%begin ([0-9]+)$"]; + if (components.count != 2) { + [self abortWithErrorMessage:[NSString stringWithFormat:@"Malformed command (expected %%begin command_id): \"%@\"", command]]; + return; + } + NSString *commandId = [components objectAtIndex:1]; + + currentCommand_ = [[commandQueue_ objectAtIndex:0] retain]; + [currentCommand_ setObject:commandId forKey:kCommandId]; + TmuxLog(@"Begin response to %@", [currentCommand_ objectForKey:kCommandString]); + [currentCommandResponse_ release]; + currentCommandResponse_ = [[NSMutableString alloc] init]; + [commandQueue_ removeObjectAtIndex:0]; +} + +- (void)stripLastNewline { + if ([currentCommandResponse_ hasSuffix:@"\n"]) { + // Strip the last newline. + NSRange theRange = NSMakeRange(currentCommandResponse_.length - 1, 1); + [currentCommandResponse_ replaceCharactersInRange:theRange + withString:@""]; + } +} + - (BOOL)parseCommand { NSRange newlineRange = NSMakeRange(NSNotFound, 0); @@ -239,9 +282,9 @@ - (BOOL)parseCommand NSString *command = [[[NSString alloc] initWithData:[stream_ subdataWithRange:commandRange] encoding:NSUTF8StringEncoding] autorelease]; if (!command) { - NSLog(@"Non-UTF-8 command in stream %@", [stream_ subdataWithRange:commandRange]); - [self abortWithErrorMessage:@"Non-UTF-8 command in stream (please copy hex data from Console.app into a bug report)"]; - return NO; + // The command was not UTF-8. Unfortunately, this can happen. If tmux has a non-UTF-8 + // character in a pane, it will just output it in capture-pane. + command = [[[NSString alloc] initWithUTF8DataIgnoringErrors:[stream_ subdataWithRange:commandRange]] autorelease]; } // At least on osx, the terminal driver adds \r at random places, sometimes adding two of them in a row! // We split on \n, which is safe, and just throw out any \r's that we see. @@ -254,15 +297,23 @@ - (BOOL)parseCommand } // Advance range to include newline so we can chop it off commandRange.length += newlineRange.length; + if (!currentCommand_ && !acceptNotifications_) { + TmuxLog(@"Ignoring notification %@", command); + } - if ([command isEqualToString:@"%end"]) { + NSString *endCommand = [NSString stringWithFormat:@"%%end %@", [currentCommand_ objectForKey:kCommandId]]; + NSString *errorCommand = [NSString stringWithFormat:@"%%error %@", [currentCommand_ objectForKey:kCommandId]]; + if (currentCommand_ && [command isEqualToString:endCommand]) { TmuxLog(@"End for command %@", currentCommand_); - [self currentCommandResponseFinished]; + [self stripLastNewline]; + [self currentCommandResponseFinishedWithError:NO]; + } else if (currentCommand_ && [command isEqualToString:errorCommand]) { + [self stripLastNewline]; + [self currentCommandResponseFinishedWithError:YES]; } else if (currentCommand_) { - if (currentCommandResponse_.length) { - [currentCommandResponse_ appendString:@"\n"]; - } [currentCommandResponse_ appendString:command]; + // Always append a newline; then at the end, remove the last one. + [currentCommandResponse_ appendString:@"\n"]; } else if ([command hasPrefix:@"%output "]) { if (acceptNotifications_) [self parseOutputCommand:command]; } else if ([command hasPrefix:@"%layout-change "]) { @@ -287,19 +338,13 @@ - (BOOL)parseCommand [command isEqualToString:@"%exit"]) { TmuxLog(@"tmux exit message: %@", command); [self hostDisconnected]; - } else if ([command hasPrefix:@"%error"]) { - [self abortWithErrorMessage:[NSString stringWithFormat:@"Error: %@", command]]; - } else if ([command isEqualToString:@"%begin"]) { + } else if ([command hasPrefix:@"%begin "]) { if (currentCommand_) { [self abortWithErrorMessage:@"%begin without %end"]; } else if (!commandQueue_.count) { [self abortWithErrorMessage:@"%begin with empty command queue"]; } else { - currentCommand_ = [[commandQueue_ objectAtIndex:0] retain]; - TmuxLog(@"Begin response to %@", currentCommand_); - [currentCommandResponse_ release]; - currentCommandResponse_ = [[NSMutableString alloc] init]; - [commandQueue_ removeObjectAtIndex:0]; + [self parseBegin:command]; } } else { // We'll be tolerant of unrecognized commands. @@ -307,7 +352,9 @@ - (BOOL)parseCommand } // Erase the just-handled command from the stream. - [stream_ replaceBytesInRange:commandRange withBytes:"" length:0]; + if (stream_.length > 0) { // length could be 0 if abortWtihErrorMessage: was called. + [stream_ replaceBytesInRange:commandRange withBytes:"" length:0]; + } return YES; } @@ -385,18 +432,20 @@ - (NSDictionary *)dictionaryForCommand:(NSString *)command responseTarget:(id)target responseSelector:(SEL)selector responseObject:(id)obj + toleratesErrors:(BOOL)toleratesErrors { return [NSDictionary dictionaryWithObjectsAndKeys: command, kCommandString, target, kCommandTarget, NSStringFromSelector(selector), kCommandSelector, obj, kCommandObject, + [NSNumber numberWithBool:toleratesErrors], kCommandToleratesErrors, nil]; } - (void)enqueueCommandDict:(NSDictionary *)dict { - [commandQueue_ addObject:dict]; + [commandQueue_ addObject:[NSMutableDictionary dictionaryWithDictionary:dict]]; } - (void)sendCommand:(NSString *)command responseTarget:(id)target responseSelector:(SEL)selector @@ -404,10 +453,15 @@ - (void)sendCommand:(NSString *)command responseTarget:(id)target responseSelect [self sendCommand:command responseTarget:target responseSelector:selector - responseObject:nil]; + responseObject:nil + toleratesErrors:NO]; } -- (void)sendCommand:(NSString *)command responseTarget:(id)target responseSelector:(SEL)selector responseObject:(id)obj +- (void)sendCommand:(NSString *)command + responseTarget:(id)target + responseSelector:(SEL)selector + responseObject:(id)obj + toleratesErrors:(BOOL)toleratesErrors { if (detachSent_ || state_ == CONTROL_STATE_DETACHED) { return; @@ -416,9 +470,12 @@ - (void)sendCommand:(NSString *)command responseTarget:(id)target responseSelect NSDictionary *dict = [self dictionaryForCommand:commandWithNewline responseTarget:target responseSelector:selector - responseObject:obj]; + responseObject:obj + toleratesErrors:toleratesErrors]; [self enqueueCommandDict:dict]; + TmuxLog(@"Send command: %@", commandWithNewline); [delegate_ tmuxWriteData:[commandWithNewline dataUsingEncoding:NSUTF8StringEncoding]]; + TmuxLog(@"Send command: %@", [dict objectForKey:kCommandString]); } - (void)sendCommandList:(NSArray *)commandDicts { @@ -432,19 +489,25 @@ - (void)sendCommandList:(NSArray *)commandDicts initial:(BOOL)initial } NSMutableString *cmd = [NSMutableString string]; NSString *sep = @""; + TmuxLog(@"-- Begin command list --"); for (NSDictionary *dict in commandDicts) { [cmd appendString:sep]; [cmd appendString:[dict objectForKey:kCommandString]]; + NSMutableDictionary *amended = [NSMutableDictionary dictionaryWithDictionary:dict]; + if (dict == [commandDicts lastObject]) { + [amended setObject:[NSNumber numberWithBool:YES] forKey:kCommandIsLastInList]; + } + [amended setObject:[NSNumber numberWithBool:YES] forKey:kCommandIsInList]; if (initial && dict == [commandDicts lastObject]) { - NSMutableDictionary *amended = [NSMutableDictionary dictionaryWithDictionary:dict]; [amended setObject:[NSNumber numberWithBool:YES] forKey:kCommandIsInitial]; - [self enqueueCommandDict:amended]; - } else { - [self enqueueCommandDict:dict]; } + [self enqueueCommandDict:amended]; sep = @"; "; + TmuxLog(@"Send command: %@", [dict objectForKey:kCommandString]); } + TmuxLog(@"-- End command list --"); [cmd appendString:NEWLINE]; + TmuxLog(@"Send command: %@", cmd); [delegate_ tmuxWriteData:[cmd dataUsingEncoding:NSUTF8StringEncoding]]; } diff --git a/TmuxHistoryParser.h b/TmuxHistoryParser.h index fceac2e7cd..0442bae501 100644 --- a/TmuxHistoryParser.h +++ b/TmuxHistoryParser.h @@ -10,6 +10,7 @@ @interface TmuxHistoryParser : NSObject + (TmuxHistoryParser *)sharedInstance; -- (NSArray *)parseDumpHistoryResponse:(NSString *)response; +- (NSArray *)parseDumpHistoryResponse:(NSString *)response + ambiguousIsDoubleWidth:(BOOL)ambiguousIsDoubleWidth; @end diff --git a/TmuxHistoryParser.m b/TmuxHistoryParser.m index fb1200ae74..2d7f7da367 100644 --- a/TmuxHistoryParser.m +++ b/TmuxHistoryParser.m @@ -7,37 +7,8 @@ #import "TmuxHistoryParser.h" #import "ScreenChar.h" - -typedef struct { - int attr; - int flags; - int fg; - int bg; - screen_char_t prototype; - BOOL isDwcPadding; -} HistoryParseContext; - -// -- begin section copied from tmux.h -- -/* Grid attributes. */ -#define GRID_ATTR_BRIGHT 0x1 -#define GRID_ATTR_DIM 0x2 -#define GRID_ATTR_UNDERSCORE 0x4 -#define GRID_ATTR_BLINK 0x8 -#define GRID_ATTR_REVERSE 0x10 -#define GRID_ATTR_HIDDEN 0x20 -#define GRID_ATTR_ITALICS 0x40 -#define GRID_ATTR_CHARSET 0x80 /* alternative character set */ - -/* Grid flags. */ -#define GRID_FLAG_FG256 0x1 -#define GRID_FLAG_BG256 0x2 -#define GRID_FLAG_PADDING 0x4 -#define GRID_FLAG_UTF8 0x8 - -/* Grid line flags. */ -#define GRID_LINE_WRAPPED 0x1 -// -- end section copied from tmux.h -- - +#import "VT100Screen.h" +#import "VT100Terminal.h" @implementation TmuxHistoryParser @@ -50,225 +21,50 @@ + (TmuxHistoryParser *)sharedInstance return instance; } -- (screen_char_t)prototypeScreenCharWithAttributes:(int)attributes - flags:(int)flags - fg:(int)fg - bg:(int)bg -{ - screen_char_t temp; - memset(&temp, 0, sizeof(temp)); - - if (fg == 8) { - // Default fg - temp.foregroundColor = ALTSEM_FG_DEFAULT; - temp.alternateForegroundSemantics = YES; - } else { - temp.foregroundColor = fg; - temp.alternateForegroundSemantics = NO; - // TODO: GRID_ATTR_DIM not supported - } - if (bg == 8) { - temp.backgroundColor = ALTSEM_BG_DEFAULT; - temp.alternateBackgroundSemantics = YES; - } else { - temp.backgroundColor = bg; - temp.alternateBackgroundSemantics = NO; - } - - if (attributes & GRID_ATTR_BRIGHT) { - temp.bold = YES; - } - if (attributes & GRID_ATTR_UNDERSCORE) { - temp.underline = YES; - } - if (attributes & GRID_ATTR_BLINK) { - temp.blink = YES; - } - if (attributes & GRID_ATTR_REVERSE) { - int x = temp.foregroundColor; - temp.foregroundColor = temp.backgroundColor; - temp.backgroundColor = x; - - x = temp.alternateForegroundSemantics; - temp.alternateForegroundSemantics = temp.alternateBackgroundSemantics; - temp.alternateBackgroundSemantics = x; - } - - if (attributes & GRID_ATTR_HIDDEN) { - // TODO not supported (SGR 8) - } - if (attributes & GRID_ATTR_ITALICS) { - // TODO not supported - } - if (attributes & GRID_ATTR_CHARSET) { - // TODO not supported - } - return temp; -} - -// Convert a hex digit at s into an int placing it in *out and returning the number -// of characters used in the conversion. -static int consume_hex(const char *s, int *out) -{ - char const *endptr = s; - *out = strtol(s, (char**) &endptr, 16); - return endptr - s; -} - // Returns nil on error - (NSData *)dataForHistoryLine:(NSString *)hist - withContext:(HistoryParseContext *)ctx + withTerminal:(VT100Terminal *)terminal + ambiguousIsDoubleWidth:(BOOL)ambiguousIsDoubleWidth { + screen_char_t *screenChars; NSMutableData *result = [NSMutableData data]; - screen_char_t lastChar; - BOOL softEol = NO; - if ([hist hasSuffix:@"+"]) { - softEol = YES; - hist = [hist substringWithRange:NSMakeRange(0, hist.length - 1)]; - } - - const char *s = [hist UTF8String]; - for (int i = 0; s[i]; ) { - int i_prev = i; - if (s[i] == ':' || s[i] == '<') { // : is deprecated, new tmuxen use < - // Old style: - // :attr,flags,fg,bg,char,char,char,... - // New style: - // char,char,char,... - BOOL isOldStyle = (s[i] == ':'); - // Context update follows - i++; - int values[4]; - for (int j = 0; j < 4; j++) { - int n = consume_hex(s + i, &values[j]); - i += n; - char expected = ','; - if (!isOldStyle && j == 3) { - expected = '>'; - } - if (s[i] == expected) { - i++; - } else { - NSLog(@"Malformed history line: invalid prefix: expected '%c' but got '%c' at %d: <<%@>>", expected, s[i], i, hist); - return nil; - } - } - ctx->prototype = [self prototypeScreenCharWithAttributes:values[0] - flags:values[1] - fg:values[2] - bg:values[3]]; - // attr, flags, fg, bg - if (values[1] & GRID_FLAG_PADDING) { - ctx->isDwcPadding = YES; - } else { - ctx->isDwcPadding = NO; - } - } else if (s[i] == '*') { -// NSLog(@"found a * at %d", i); - i++; - NSInteger repeats; - // We have a "* " sequence. Scan the number. - if ([[NSScanner scannerWithString:[NSString stringWithUTF8String:s + i]] scanInteger:&repeats]) { - // Append the last character repeats-1 times. - for (int j = 0; j < repeats - 1; j++) { - [result appendBytes:&lastChar length:sizeof(screen_char_t)]; - } - // Advance up to and then past the terminal space, if present. - while (s[i] && s[i] != ' ') { - i++; - } - if (s[i] == ' ') { - i++; - } else { - NSLog(@"Malformed history line: lacks a space after '*n' at %d: <<%@>>", i, hist); - return nil; - } - } else { - NSLog(@"Malformed history line: lacks a number after '*' at %d: <<%@>>", i, hist); - return nil; - } - } - - // array of 2-digit hex values interspersed with [ 2 digit hex values ]. - BOOL utf8 = NO; - NSMutableData *utf8Buffer = [NSMutableData data]; - while (s[i] == '[' || - s[i] == ']' || - (ishexnumber(s[i]) && ishexnumber(s[i + 1]))) { -// NSLog(@"top of while loop: i=%d", i); - if (s[i] == '[') { -// NSLog(@"-- begin utf 8 --"); - if (s[i+1] && s[i+2] && s[i+3]) { - utf8 = YES; - [utf8Buffer setLength:0]; - } else { - NSLog(@"Malformed history line: malformed text after '[': <<%@>>", hist); - return nil; - } - i++; - } else if (s[i] == ']') { -// NSLog(@"-- end utf 8 --"); - if (utf8) { - utf8 = NO; - NSString *stringValue = [[[NSString alloc] initWithData:utf8Buffer encoding:NSUTF8StringEncoding] autorelease]; - ctx->prototype.code = GetOrSetComplexChar(stringValue); - ctx->prototype.complexChar = 1; - lastChar = ctx->prototype; - [result appendBytes:&ctx->prototype length:sizeof(screen_char_t)]; - } else { - NSLog(@"Malformed history line: ']' without '[': <<%@>>", hist); - return nil; - } - i++; - continue; - } else { - // Read a hex digit - unsigned scanned; - if ([[NSScanner scannerWithString:[NSString stringWithFormat:@"%c%c", s[i], s[i+1]]] scanHexInt:&scanned]) { -// NSLog(@"scanned %@", [NSString stringWithFormat:@"%c%c", s[i], s[i+1]]); - if (utf8) { - char c = scanned; - [utf8Buffer appendBytes:&c length:1]; - } else { - if (ctx->isDwcPadding) { - ctx->prototype.code = DWC_RIGHT; - } else { - ctx->prototype.code = scanned; - } - ctx->prototype.complexChar = 0; - // Skip DWC_RIGHT if it's the first thing in a line. It would - // be better to set the last char of the previous line to DWC_SKIP - // and the eol to EOF_DWC, but I think tmux prevents this from - // happening anyway. - if (result.length > 0 || ctx->prototype.code != DWC_RIGHT) { - lastChar = ctx->prototype; - [result appendBytes:&ctx->prototype length:sizeof(screen_char_t)]; - } + [terminal putStreamData:[hist dataUsingEncoding:NSUTF8StringEncoding]]; + VT100TCC token; + token = [terminal getNextToken]; + while (token.type != VT100_WAIT && + token.type != VT100CC_NULL) { + if (token.type != VT100_NOTSUPPORT) { + int len = 0; + switch (token.type) { + case VT100_STRING: + case VT100_ASCIISTRING: + // Allocate double space in case they're all double-width characters. + screenChars = malloc(sizeof(screen_char_t) * 2 * token.u.string.length); + StringToScreenChars(token.u.string, + screenChars, + [terminal foregroundColorCode], + [terminal backgroundColorCode], + &len, + ambiguousIsDoubleWidth, + NULL); + if ([terminal charset]) { + TranslateCharacterSet(screenChars, len); } - i += 2; - } else { - NSLog(@"Malformed history line: malformed hex array at %d: \"%c%c\" (%d %d): <<%@>>", i, s[i], s[i+1], (int) s[i], (int) s[i+1], hist); - return nil; - } + [result appendBytes:screenChars + length:sizeof(screen_char_t) * len]; + free(screenChars); + break; + + case VT100CSI_SGR: + break; + case VT100CC_SO: + break; + case VT100CC_SI: + break; } } - if (utf8) { - NSLog(@"Malformed history line: unclosed utf8 at %d: <<%@>>", i, hist); - return nil; - } - if (i == i_prev) { - NSLog(@"Malformed history line: bad hex digit stream at %d: <<%@>>", i, hist); - return nil; - } - } - - screen_char_t eolSct; - if (softEol) { - eolSct.code = EOL_SOFT; - } else { - eolSct.code = EOL_HARD; + token = [terminal getNextToken]; } - [result appendBytes:&eolSct length:sizeof(eolSct)]; return result; } @@ -276,14 +72,19 @@ - (NSData *)dataForHistoryLine:(NSString *)hist // Return an NSArray of NSData's. Each NSData is an array of screen_char_t's, // with the last element in each being the newline. Returns nil on error. - (NSArray *)parseDumpHistoryResponse:(NSString *)response + ambiguousIsDoubleWidth:(BOOL)ambiguousIsDoubleWidth { + if (![response length]) { + return [NSArray array]; + } NSArray *lines = [response componentsSeparatedByString:@"\n"]; NSMutableArray *screenLines = [NSMutableArray array]; - HistoryParseContext ctx; - memset(&ctx, 0, sizeof(ctx)); + VT100Terminal *terminal = [[[VT100Terminal alloc] init] autorelease]; + [terminal setEncoding:NSUTF8StringEncoding]; for (NSString *line in lines) { NSData *data = [self dataForHistoryLine:line - withContext:&ctx]; + withTerminal:terminal + ambiguousIsDoubleWidth:ambiguousIsDoubleWidth]; if (!data) { return nil; } diff --git a/TmuxStateParser.h b/TmuxStateParser.h index 8ce7102f0b..7c02490c00 100644 --- a/TmuxStateParser.h +++ b/TmuxStateParser.h @@ -33,7 +33,9 @@ extern NSString *kStateDictMouseUTF8Mode; @interface TmuxStateParser : NSObject ++ (NSString *)format; + (TmuxStateParser *)sharedInstance; -- (NSMutableDictionary *)parsedStateFromString:(NSString *)layout; +- (NSMutableDictionary *)parsedStateFromString:(NSString *)layout + forPaneId:(int)paneId; @end diff --git a/TmuxStateParser.m b/TmuxStateParser.m index 76332119b3..05b22bb0b0 100644 --- a/TmuxStateParser.m +++ b/TmuxStateParser.m @@ -9,35 +9,47 @@ #import "TmuxStateParser.h" NSString *kStateDictInAlternateScreen = @"in_alternate_screen"; // Deprecated: same as kStateDictSavedGrid below. -NSString *kStateDictSavedGrid = @"saved_grid"; -NSString *kStateDictBaseCursorX = @"base_cursor_x"; -NSString *kStateDictBaseCursorY = @"base_cursor_y"; -NSString *kStateDictSavedCX = @"saved_cx"; -NSString *kStateDictSavedCY = @"saved_cy"; +NSString *kStateDictSavedGrid = @"alternate_on"; +NSString *kStateDictBaseCursorX = @"base_cursor_x"; // Deprecated: use saved_cx +NSString *kStateDictBaseCursorY = @"base_cursor_y"; // Deprecated: use saved_cy +NSString *kStateDictSavedCX = @"alternate_saved_x"; +NSString *kStateDictSavedCY = @"alternate_saved_y"; NSString *kStateDictCursorX = @"cursor_x"; NSString *kStateDictCursorY = @"cursor_y"; NSString *kStateDictScrollRegionUpper = @"scroll_region_upper"; NSString *kStateDictScrollRegionLower = @"scroll_region_lower"; -NSString *kStateDictTabstops = @"tabstops"; -NSString *kStateDictDECSCCursorX = @"decsc_cursor_x"; -NSString *kStateDictDECSCCursorY = @"decsc_cursor_y"; -NSString *kStateDictCursorMode = @"cursor_mode"; -NSString *kStateDictInsertMode = @"insert_mode"; -NSString *kStateDictKCursorMode = @"kcursor_mode"; -NSString *kStateDictKKeypadMode = @"kkeypad_mode"; -NSString *kStateDictWrapMode = @"wrap_mode"; -NSString *kStateDictMouseStandardMode = @"mouse_standard_mode"; -NSString *kStateDictMouseButtonMode = @"mouse_button_mode"; -NSString *kStateDictMouseAnyMode = @"mouse_any_mode"; -NSString *kStateDictMouseUTF8Mode = @"mouse_utf8_mode"; +NSString *kStateDictPaneId = @"pane_id"; +NSString *kStateDictTabstops = @"pane_tabs"; +NSString *kStateDictDECSCCursorX = @"saved_cursor_x"; +NSString *kStateDictDECSCCursorY = @"saved_cursor_y"; +NSString *kStateDictCursorMode = @"cursor_flag"; +NSString *kStateDictInsertMode = @"insert_flag"; +NSString *kStateDictKCursorMode = @"keypad_cursor_flag"; +NSString *kStateDictKKeypadMode = @"keypad_flag"; +NSString *kStateDictWrapMode = @"wrap_flag"; +NSString *kStateDictMouseStandardMode = @"mouse_standard_flag"; +NSString *kStateDictMouseButtonMode = @"mouse_button_flag"; +NSString *kStateDictMouseAnyMode = @"mouse_any_flag"; +NSString *kStateDictMouseUTF8Mode = @"mouse_utf8_flag"; @interface NSString (TmuxStateParser) - (NSArray *)intlistValue; - (NSNumber *)numberValue; +- (NSNumber *)paneIdNumberValue; @end @implementation NSString (TmuxStateParser) +- (NSNumber *)paneIdNumberValue +{ + if ([self hasPrefix:@"%"] && [self length] > 1) { + return [NSNumber numberWithInt:[[self substringFromIndex:1] intValue]]; + } else { + NSLog(@"WARNING: Bogus pane id %@", self); + return [NSNumber numberWithInt:-1]; + } +} + - (NSNumber *)numberValue { return [NSNumber numberWithInt:[self intValue]]; @@ -57,6 +69,25 @@ - (NSArray *)intlistValue @implementation TmuxStateParser ++ (NSString *)format { + NSMutableString *format = [NSMutableString string]; + NSArray *theModes = [NSArray arrayWithObjects: + kStateDictPaneId, kStateDictSavedGrid, kStateDictSavedCX, kStateDictSavedCY, + kStateDictCursorX, kStateDictCursorY, kStateDictScrollRegionUpper, + kStateDictScrollRegionLower, kStateDictTabstops, kStateDictDECSCCursorX, + kStateDictDECSCCursorY, kStateDictCursorMode, kStateDictInsertMode, + kStateDictKCursorMode, kStateDictKKeypadMode, kStateDictWrapMode, + kStateDictMouseStandardMode, kStateDictMouseButtonMode, + kStateDictMouseAnyMode, kStateDictMouseUTF8Mode, nil]; + for (NSString *value in theModes) { + [format appendFormat:@"%@=#{%@}", value, value]; + if (value != [theModes lastObject]) { + [format appendString:@"\t"]; + } + } + return format; +} + + (TmuxStateParser *)sharedInstance { static TmuxStateParser *instance; @@ -66,7 +97,7 @@ + (TmuxStateParser *)sharedInstance return instance; } -- (NSMutableDictionary *)parsedStateFromString:(NSString *)layout ++ (NSMutableDictionary *)dictionaryForState:(NSString *)state { // State is a collection of key-value pairs. Each KVP is delimited by // newlines. The key is to the left of the first =, the value is to the @@ -74,6 +105,7 @@ - (NSMutableDictionary *)parsedStateFromString:(NSString *)layout NSString *intType = @"numberValue"; NSString *uintType = @"numberValue"; NSString *intlistType = @"intlistValue"; + NSString *paneIdNumberType = @"paneIdNumberValue"; NSDictionary *fieldTypes = [NSDictionary dictionaryWithObjectsAndKeys: intType, kStateDictInAlternateScreen, @@ -86,12 +118,13 @@ - (NSMutableDictionary *)parsedStateFromString:(NSString *)layout uintType, kStateDictCursorY, uintType, kStateDictScrollRegionUpper, uintType, kStateDictScrollRegionLower, + paneIdNumberType, kStateDictPaneId, intlistType, kStateDictTabstops, intType, kStateDictDECSCCursorX, intType, kStateDictDECSCCursorY, nil]; - NSArray *fields = [layout componentsSeparatedByString:@"\n"]; + NSArray *fields = [state componentsSeparatedByString:@"\t"]; NSMutableDictionary *result = [NSMutableDictionary dictionary]; for (NSString *kvp in fields) { NSRange eq = [kvp rangeOfString:@"="]; @@ -106,12 +139,25 @@ - (NSMutableDictionary *)parsedStateFromString:(NSString *)layout } else { [result setObject:value forKey:key]; } - } else { + } else if ([kvp length] > 0) { NSLog(@"Bogus result in control command: \"%@\"", kvp); } } return result; } +- (NSMutableDictionary *)parsedStateFromString:(NSString *)stateLines + forPaneId:(int)paneId +{ + NSArray *states = [stateLines componentsSeparatedByString:@"\n"]; + for (NSString *state in states) { + NSMutableDictionary *dict = [[self class] dictionaryForState:state]; + NSNumber *paneIdNumber = [dict objectForKey:kStateDictPaneId]; + if (paneIdNumber && [paneIdNumber intValue] == paneId) { + return dict; + } + } + return [NSMutableDictionary dictionary]; +} @end diff --git a/TmuxWindowOpener.h b/TmuxWindowOpener.h index 45225515f6..07edfec421 100644 --- a/TmuxWindowOpener.h +++ b/TmuxWindowOpener.h @@ -42,6 +42,7 @@ @property (nonatomic, retain) id target; // Selector is called even if the window is already open and nothing is done. @property (nonatomic, assign) SEL selector; +@property (nonatomic, assign) BOOL ambiguousIsDoubleWidth; + (TmuxWindowOpener *)windowOpener; - (void)openWindows:(BOOL)initial; diff --git a/TmuxWindowOpener.m b/TmuxWindowOpener.m index 27e6d93cae..c49ba0092e 100644 --- a/TmuxWindowOpener.m +++ b/TmuxWindowOpener.m @@ -43,6 +43,7 @@ @implementation TmuxWindowOpener @synthesize controller = controller_; @synthesize target = target_; @synthesize selector = selector_; +@synthesize ambiguousIsDoubleWidth = ambiguousIsDoubleWidth_; + (TmuxWindowOpener *)windowOpener { @@ -167,18 +168,20 @@ - (void)appendRequestsForWindowPane:(NSNumber *)wp - (NSDictionary *)dictForDumpStateForWindowPane:(NSNumber *)wp { ++pendingRequests_; - NSString *command = [NSString stringWithFormat:@"control -t %%%d get-emulatorstate", [wp intValue]]; + 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]; + responseObject:wp + toleratesErrors:NO]; } - (NSDictionary *)dictForRequestHistoryForWindowPane:(NSNumber *)wp alt:(BOOL)alternate { ++pendingRequests_; - NSString *command = [NSString stringWithFormat:@"control %@-t %%%d -l %d get-history", + NSString *command = [NSString stringWithFormat:@"capture-pane -peqJ %@-t %%%d -S -%d", (alternate ? @"-a " : @""), [wp intValue], self.maxHistory]; return [gateway_ dictionaryForCommand:command responseTarget:self @@ -186,7 +189,8 @@ - (NSDictionary *)dictForRequestHistoryForWindowPane:(NSNumber *)wp responseObject:[NSArray arrayWithObjects: wp, [NSNumber numberWithBool:alternate], - nil]]; + nil] + toleratesErrors:NO]; } // Command response handler for dump-history @@ -196,7 +200,8 @@ - (void)dumpHistoryResponse:(NSString *)response { NSNumber *wp = [info objectAtIndex:0]; NSNumber *alt = [info objectAtIndex:1]; - NSArray *history = [[TmuxHistoryParser sharedInstance] parseDumpHistoryResponse:response]; + NSArray *history = [[TmuxHistoryParser sharedInstance] parseDumpHistoryResponse:response + ambiguousIsDoubleWidth:ambiguousIsDoubleWidth_]; if (history) { if ([alt boolValue]) { [altHistories_ setObject:history forKey:wp]; @@ -215,7 +220,8 @@ - (void)dumpHistoryResponse:(NSString *)response - (void)dumpStateResponse:(NSString *)response pane:(NSNumber *)wp { - NSDictionary *state = [[TmuxStateParser sharedInstance] parsedStateFromString:response]; + NSDictionary *state = [[TmuxStateParser sharedInstance] parsedStateFromString:response + forPaneId:[wp intValue]]; [states_ setObject:state forKey:wp]; [self requestDidComplete]; } diff --git a/VT100Screen.h b/VT100Screen.h index 06ebe39efa..4bbbb2e9ad 100644 --- a/VT100Screen.h +++ b/VT100Screen.h @@ -67,9 +67,9 @@ void StringToScreenChars(NSString *s, screen_char_t fg, screen_char_t bg, int *len, - NSStringEncoding encoding, BOOL ambiguousIsDoubleWidth, int* cursorIndex); +void TranslateCharacterSet(screen_char_t *s, int len); @interface VT100Screen : NSObject { diff --git a/VT100Screen.m b/VT100Screen.m index fa21764f46..96421f4cf1 100644 --- a/VT100Screen.m +++ b/VT100Screen.m @@ -81,7 +81,7 @@ @implementation SearchResult @end /* translates normal char into graphics char */ -static void translate(screen_char_t *s, int len) +void TranslateCharacterSet(screen_char_t *s, int len) { int i; @@ -98,7 +98,6 @@ void StringToScreenChars(NSString *s, screen_char_t fg, screen_char_t bg, int *len, - NSStringEncoding encoding, BOOL ambiguousIsDoubleWidth, int* cursorIndex) { unichar *sc; @@ -151,7 +150,6 @@ void StringToScreenChars(NSString *s, // render what you get than to try to be clever and break such edge cases. buf[j].code = '?'; } else if (sc[i] > 0xa0 && [NSString isDoubleWidthCharacter:sc[i] - encoding:encoding ambiguousIsDoubleWidth:ambiguousIsDoubleWidth]) { // This code path is for double-width characters in BMP only. j++; @@ -187,7 +185,6 @@ void StringToScreenChars(NSString *s, if (IsLowSurrogate(sc[i])) { NSString* str = ComplexCharToStr(buf[j].code); if ([NSString isDoubleWidthCharacter:DecodeSurrogatePair([str characterAtIndex:0], [str characterAtIndex:1]) - encoding:encoding ambiguousIsDoubleWidth:ambiguousIsDoubleWidth]) { j++; buf[j].code = DWC_RIGHT; @@ -2401,16 +2398,7 @@ - (void)putToken:(VT100TCC)token } break; - case UNDERSCORE_TMUX_UNSUPPORTED: - [self crlf]; - [self setString:@"You have run an unsupported version of tmux. Please " - @"install a version that is compatible with this build of iTerm2." - ascii:YES]; - [self crlf]; - [SESSION writeTask:[@"detach\n\n" dataUsingEncoding:NSUTF8StringEncoding]]; - break; - - case UNDERSCORE_TMUX1: + case DCS_TMUX: [SESSION startTmuxMode]; break; @@ -2620,7 +2608,7 @@ - (void)setString:(NSString *)string ascii:(BOOL)ascii // If a graphics character set was selected then translate buffer // characters into graphics charaters. if (charset[[TERMINAL charset]]) { - translate(buffer, len); + TranslateCharacterSet(buffer, len); } if (dynamicTemp) { free(dynamicTemp); @@ -2678,7 +2666,6 @@ - (void)setString:(NSString *)string ascii:(BOOL)ascii [TERMINAL foregroundColorCode], [TERMINAL backgroundColorCode], &len, - [TERMINAL encoding], [SESSION doubleWidth], NULL); } @@ -4033,7 +4020,6 @@ - (void)doPrint - (BOOL)isDoubleWidthCharacter:(unichar)c { return [NSString isDoubleWidthCharacter:c - encoding:[TERMINAL encoding] ambiguousIsDoubleWidth:[SESSION doubleWidth]]; } @@ -4057,11 +4043,11 @@ - (void)setHistory:(NSArray *)history [self clearBuffer]; for (NSData *chars in history) { screen_char_t *line = (screen_char_t *) [chars bytes]; - int length = [chars length] / sizeof(screen_char_t); - length--; // Last position is wrap flag - - BOOL isPartial = (line[length].code == EOL_SOFT); - [linebuffer appendLine:line length:length partial:isPartial width:WIDTH]; + const int len = [chars length] / sizeof(screen_char_t); + [linebuffer appendLine:line + length:len + partial:NO + width:WIDTH]; } if (!unlimitedScrollback_) { [linebuffer dropExcessLinesWithWidth:WIDTH]; @@ -4089,16 +4075,23 @@ - (void)setAltScreen:(NSArray *)lines temp_default_char = [self defaultChar]; // Copy the lines back over it - for (int i = 0; i < MIN(lines.count, HEIGHT); i++) { + int o = 0; + for (int i = 0; o < HEIGHT && i < MIN(lines.count, HEIGHT); i++) { NSData *chars = [lines objectAtIndex:i]; screen_char_t *line = (screen_char_t *) [chars bytes]; int length = [chars length] / sizeof(screen_char_t); - length--; // Last position is wrap flag - BOOL isPartial = (line[length].code == EOL_SOFT); - memmove(temp_buffer + i * REAL_WIDTH, - line, - length * sizeof(screen_char_t)); - temp_buffer[i * REAL_WIDTH + WIDTH].code = (isPartial ? EOL_SOFT : EOL_HARD); + + do { + // Add up to WIDTH characters at a time until they're all used. + memmove(temp_buffer + o * REAL_WIDTH, + line, + MIN(WIDTH, length) * sizeof(screen_char_t)); + const BOOL isPartial = (length > WIDTH); + temp_buffer[o * REAL_WIDTH + WIDTH].code = (isPartial ? EOL_SOFT : EOL_HARD); + length -= WIDTH; + line += WIDTH; + o++; + } while (o < HEIGHT && length > 0); } } diff --git a/VT100Terminal.h b/VT100Terminal.h index 6530b1d068..c251eb2464 100644 --- a/VT100Terminal.h +++ b/VT100Terminal.h @@ -151,8 +151,7 @@ // iTerm extension #define ITERM_GROWL 5000 -#define UNDERSCORE_TMUX1 5001 -#define UNDERSCORE_TMUX_UNSUPPORTED 5002 +#define DCS_TMUX 5001 #define VT100CSIPARAM_MAX 16 diff --git a/VT100Terminal.m b/VT100Terminal.m index 0c59fe2578..b5dc2d2e6c 100644 --- a/VT100Terminal.m +++ b/VT100Terminal.m @@ -155,7 +155,6 @@ Simplifed Chinese (EUC_CN) static VT100TCC decode_ansi(unsigned char *,int, int *,VT100Screen *); static VT100TCC decode_other(unsigned char *, int, int *, NSStringEncoding); static VT100TCC decode_control(unsigned char *, int, int *,NSStringEncoding,VT100Screen *); -static int decode_utf8_char(unsigned char *, int, int *); static VT100TCC decode_utf8(unsigned char *, int, int *); static VT100TCC decode_euccn(unsigned char *, int, int *); static VT100TCC decode_big5(unsigned char *,int, int *); @@ -186,9 +185,9 @@ static BOOL isANSI(unsigned char *code, int len) return NO; } -static BOOL isUNDERSCORE(unsigned char *code, int len) +static BOOL isDCS(unsigned char *code, int len) { - if (len >= 2 && code[0] == ESC && code[1] == '_') { + if (len >= 2 && code[0] == ESC && code[1] == 'P') { return YES; } return NO; @@ -746,49 +745,23 @@ case MAKE_CSI_COMMAND('!', 'p'): return result; } -static VT100TCC decode_underscore(unsigned char *datap, - int datalen, - int *rmlen, - NSStringEncoding enc) +static VT100TCC decode_dcs(unsigned char *datap, + int datalen, + int *rmlen, + NSStringEncoding enc) { + // DCS is kind of messy to parse, but we only support one code, so we just check if it's that. VT100TCC result; result.type = VT100_WAIT; - // Can assume we have "ESC _" so skip past that. + // Can assume we have "ESC P" so skip past that. datap += 2; datalen -= 2; *rmlen=2; - if (datalen > 0) { - int i; - BOOL found = NO; - // Search for esc \ terminator. - for (i = 0; i < datalen; i++) { - if (i > 0 && datap[i - 1] == ESC && datap[i] == '\\') { - // Found esc \. Grab text from datap to char before esc and - // save in result.u.string. - NSData *data = [NSData dataWithBytes:datap length:i - 1]; - result.u.string = [[[NSString alloc] initWithData:data - encoding:enc] autorelease]; - // Consume everything up to the backslash - (*rmlen) += i + 1; - found = YES; - break; - } else if (i > 0 && datap[i - 1] == ESC) { - // Stop on ESC to avoid getting stuck after a broken escape code - result.type = VT100_NOTSUPPORT; - return result; - } - } - - if (found && [result.u.string hasPrefix:@"tmux"]) { - if ([result.u.string isEqualToString:@"tmux0.5"] || - [result.u.string hasPrefix:@"tmux0.5;"]) { - result.type = UNDERSCORE_TMUX1; - } else if ([result.u.string hasPrefix:@"tmux"]) { - result.type = UNDERSCORE_TMUX_UNSUPPORTED; - } else { - result.type = VT100_NOTSUPPORT; - } - } else if (found) { + if (datalen >= 5) { + if (!strncmp((char *)datap, "1000p", 5)) { + result.type = DCS_TMUX; + *rmlen += 5; + } else { result.type = VT100_NOTSUPPORT; } } @@ -1187,8 +1160,8 @@ static VT100TCC decode_control(unsigned char *datap, result = decode_xterm(datap, datalen, rmlen, enc); } else if (isANSI(datap, datalen)) { result = decode_ansi(datap, datalen, rmlen, SCREEN); - } else if (isUNDERSCORE(datap, datalen)) { - result = decode_underscore(datap, datalen, rmlen, enc); + } else if (isDCS(datap, datalen)) { + result = decode_dcs(datap, datalen, rmlen, enc); } else { NSCParameterAssert(datalen > 0); @@ -1220,74 +1193,6 @@ static VT100TCC decode_control(unsigned char *datap, return result; } -// Examine the leading UTF-8 sequence in a char array and check that it -// is properly encoded. Computes the number of bytes to use for the -// first code point. -// -// Return value: -// positive: This many bytes compose a legal Unicode character. -// negative: abs(this many) bytes are illegal, should be replaced by one -// single replacement symbol. -// zero: Unfinished sequence, input needs to grow. -static int decode_utf8_char(unsigned char *datap, - int datalen, - int *result) -{ - unsigned int theChar; - int utf8Length; - unsigned char c; - // This maps a utf-8 sequence length to the smallest code point it should - // encode (e.g., using 5 bytes to encode an ascii character would be - // considered an error). - unsigned int smallest[7] = { 0, 0, 0x80UL, 0x800UL, 0x10000UL, 0x200000UL, 0x4000000UL }; - - if (datalen == 0) { - return 0; - } - - c = *datap; - if ((c & 0x80) == 0x00) { - *result = c; - return 1; - } else if ((c & 0xE0) == 0xC0) { - theChar = c & 0x1F; - utf8Length = 2; - } else if ((c & 0xF0) == 0xE0) { - theChar = c & 0x0F; - utf8Length = 3; - } else if ((c & 0xF8) == 0xF0) { - theChar = c & 0x07; - utf8Length = 4; - } else if ((c & 0xFC) == 0xF8) { - theChar = c & 0x03; - utf8Length = 5; - } else if ((c & 0xFE) == 0xFC) { - theChar = c & 0x01; - utf8Length = 6; - } else { - return -1; - } - for (int i = 1; i < utf8Length; i++) { - if (datalen <= i) { - return 0; - } - c = datap[i]; - if ((c & 0xc0) != 0x80) { - // Expected a continuation character but did not get one. - return -i; - } - theChar = (theChar << 6) | (c & 0x3F); - } - - if (theChar < smallest[utf8Length]) { - // Reject overlong sequences. - return -utf8Length; - } - - *result = (int)theChar; - return utf8Length; -} - static VT100TCC decode_utf8(unsigned char *datap, int datalen, int *rmlen) diff --git a/tests/dwc-test.txt b/tests/dwc-test.txt new file mode 100644 index 0000000000..7773bbb3be --- /dev/null +++ b/tests/dwc-test.txt @@ -0,0 +1,11 @@ +This test should be run on an 80-column wide screen. +0 1 2 3 4 5 6 7 +01234567890123456789012345678901234567890123456789012345678901234567890123456789 +Here are some DWCs: 0123456789 Follwed by regular text. +Here is a long line with a DWC that doesn't span followed by text: xxxxxxxxxxx0abc +Here is a long line with a DWC that wants to span lines: xxxxxxxxxxxxxxxxxxxxxx0abc +Here are two lines of DWCs, both with a DWC spanning a line: +0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 +0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789 +.Loremipsumdolorstametconsecteturadipisicingelitse.ddoeiusmodtemporincididuntutlaboreet +This is the end.