From 7f49bd99c216216fbeb0af67cb7ca1fcdc59d7a8 Mon Sep 17 00:00:00 2001 From: dcylabs Date: Mon, 21 Aug 2017 20:27:17 +0200 Subject: [PATCH 01/30] Fixed selection issue when terminal is in nested scrolled elements --- src/utils/Mouse.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/utils/Mouse.ts b/src/utils/Mouse.ts index c61624e9..e1901f2d 100644 --- a/src/utils/Mouse.ts +++ b/src/utils/Mouse.ts @@ -12,6 +12,7 @@ export function getCoordsRelativeToElement(event: MouseEvent, element: HTMLEleme let x = event.pageX; let y = event.pageY; + const originalElement = element; // Converts the coordinates from being relative to the document to being // relative to the terminal. @@ -20,6 +21,12 @@ export function getCoordsRelativeToElement(event: MouseEvent, element: HTMLEleme y -= element.offsetTop; element = 'offsetParent' in element ? element.offsetParent : element.parentElement; } + element = originalElement; + while (element && element !== self.document.documentElement) { + x += element.scrollLeft; + y += element.scrollTop; + element = element.parentElement; + } return [x, y]; } From 725ec80983638af28b4fcf29a3b96167dbbd180f Mon Sep 17 00:00:00 2001 From: dcylabs Date: Mon, 21 Aug 2017 20:27:17 +0200 Subject: [PATCH 02/30] Fixed selection issue when terminal is in nested scrolled elements --- src/utils/Mouse.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/utils/Mouse.ts b/src/utils/Mouse.ts index c61624e9..6852452a 100644 --- a/src/utils/Mouse.ts +++ b/src/utils/Mouse.ts @@ -10,6 +10,7 @@ export function getCoordsRelativeToElement(event: MouseEvent, element: HTMLEleme return null; } + const originalElement = element; let x = event.pageX; let y = event.pageY; @@ -20,6 +21,12 @@ export function getCoordsRelativeToElement(event: MouseEvent, element: HTMLEleme y -= element.offsetTop; element = 'offsetParent' in element ? element.offsetParent : element.parentElement; } + element = originalElement; + while (element && element !== self.document.documentElement) { + x += element.scrollLeft; + y += element.scrollTop; + element = element.parentElement; + } return [x, y]; } From 91d22e3d84fa6f8ed0c25006d7447f3306b9820d Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 28 Sep 2017 21:23:27 -0400 Subject: [PATCH 03/30] Add null check before cursor is drawn --- src/renderer/CursorRenderLayer.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/renderer/CursorRenderLayer.ts b/src/renderer/CursorRenderLayer.ts index e57c326d..689d6914 100644 --- a/src/renderer/CursorRenderLayer.ts +++ b/src/renderer/CursorRenderLayer.ts @@ -131,6 +131,9 @@ export class CursorRenderLayer extends BaseRenderLayer { } const charData = terminal.buffer.lines.get(cursorY)[terminal.buffer.x]; + if (!charData) { + return; + } if (!terminal.isFocused) { this._clearCursor(); From feb2b958a44622f5e6a1bdc75b4b292f7ace99c7 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 30 Sep 2017 06:13:44 -0400 Subject: [PATCH 04/30] Mostly emoji selection Selection is not aware of characters that have a size greater than 1. When the emoji is at the start of the selection it still doesn't work. Fixes #1015 --- src/Buffer.ts | 38 ++++++++++++++++++++++++++------- src/SelectionManager.ts | 47 ++++++++++++++++++++++++++++++++++++----- 2 files changed, 72 insertions(+), 13 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index 2d23980d..585855db 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -208,9 +208,19 @@ export class Buffer implements IBuffer { public translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol: number = 0, endCol: number = null): string { // Get full line let lineString = ''; - let widthAdjustedStartCol = startCol; - let widthAdjustedEndCol = endCol; const line = this.lines.get(lineIndex); + if (!line) { + return ''; + } + + // Initialize column and index values. Column values represent the actual + // cell column, indexes represent the index in the string. Indexes are + // needed here because some chars are 0 characters long (eg. after wide + // chars) and some chars are longer than 1 characters long (eg. emojis). + let startIndex = startCol; + endCol = endCol || line.length; + let endIndex = endCol; + for (let i = 0; i < line.length; i++) { const char = line[i]; lineString += char[CHAR_DATA_CHAR_INDEX]; @@ -218,29 +228,41 @@ export class Buffer implements IBuffer { // column indexes if (char[CHAR_DATA_WIDTH_INDEX] === 0) { if (startCol >= i) { - widthAdjustedStartCol--; + startIndex -= char[CHAR_DATA_CHAR_INDEX].length; } if (endCol >= i) { - widthAdjustedEndCol--; + endIndex -= char[CHAR_DATA_CHAR_INDEX].length; + } + } else { + // Adjust the columns to take glyphs that are represented by multiple + // code points into account. + if (char[CHAR_DATA_CHAR_INDEX].length > 1) { + if (startCol >= i) { + startIndex += char[CHAR_DATA_CHAR_INDEX].length - 1; + } + if (endCol >= i) { + endIndex += char[CHAR_DATA_CHAR_INDEX].length - 1; + } } } + // TODO: startCol needs to be emoji-aware, currently each emoji code point is + // consuming addition space in the selection text } // Calculate the final end col by trimming whitespace on the right of the // line if needed. - let finalEndCol = widthAdjustedEndCol || line.length; if (trimRight) { const rightWhitespaceIndex = lineString.search(/\s+$/); if (rightWhitespaceIndex !== -1) { - finalEndCol = Math.min(finalEndCol, rightWhitespaceIndex); + endIndex = Math.min(endIndex, rightWhitespaceIndex); } // Return the empty string if only trimmed whitespace is selected - if (finalEndCol <= widthAdjustedStartCol) { + if (endIndex <= startIndex) { return ''; } } - return lineString.substring(widthAdjustedStartCol, finalEndCol); + return lineString.substring(startIndex, endIndex); } /** diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index 28ed1522..65f6a57e 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -11,7 +11,7 @@ import { EventEmitter } from './EventEmitter'; import { ITerminal, ICircularList, ISelectionManager, IBuffer } from './Interfaces'; import { SelectionModel } from './SelectionModel'; import { LineData } from './Types'; -import { CHAR_DATA_WIDTH_INDEX } from './Buffer'; +import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from './Buffer'; /** * The number of pixels the mouse needs to be above or below the viewport in @@ -281,6 +281,9 @@ export class SelectionManager extends EventEmitter implements ISelectionManager // Convert to 0-based coords[0]--; coords[1]--; + + console.log('coords', coords); + // Convert viewport coords to buffer coords coords[1] += this._terminal.buffer.ydisp; return coords; @@ -545,9 +548,16 @@ export class SelectionManager extends EventEmitter implements ISelectionManager for (let i = 0; coords[0] >= i; i++) { const char = bufferLine[i]; if (char[CHAR_DATA_WIDTH_INDEX] === 0) { + // Wide characters aren't included in the line string so decrement the index charIndex--; + // } + } else if (char[CHAR_DATA_CHAR_INDEX].length > 1) { + console.log('char length > 1', char[CHAR_DATA_CHAR_INDEX], char[CHAR_DATA_CHAR_INDEX].length); + // Emojis take up multiple characters, so adjust accordingly + charIndex += char[CHAR_DATA_CHAR_INDEX].length - 1; } } + console.log('character index: ', charIndex); return charIndex; } @@ -606,20 +616,47 @@ export class SelectionManager extends EventEmitter implements ISelectionManager endCol++; } // Expand the string in both directions until a space is hit - while (startIndex > 0 && !this._isCharWordSeparator(line.charAt(startIndex - 1))) { - if (bufferLine[startCol - 1][CHAR_DATA_WIDTH_INDEX] === 0) { + let nextCharData = startIndex > 0 ? bufferLine[startCol - 1] : null; + + + +// TODO: Need to make sure that characters whose strings are longer than 1 get compensated for +// Double click words should expand to the spaces. + + + // while (startIndex > 0 && !this._isCharWordSeparator(line.charAt(startIndex - 1))) { + console.log('start char: ' + bufferLine[startCol]); + console.log('scan backwards'); + console.log(' startIndex:',startIndex); + while (startIndex > 0 && !this._isCharWordSeparator(bufferLine[startCol - 1][CHAR_DATA_CHAR_INDEX])) { + const char = bufferLine[startCol - 1]; + console.log(' char: ' + char); + if (char[CHAR_DATA_WIDTH_INDEX] === 0) { // If the next character is a wide char, record it and skip the column leftWideCharCount++; startCol--; + } else if (char[CHAR_DATA_CHAR_INDEX].length > 1) { + startIndex -= char[CHAR_DATA_CHAR_INDEX].length - 1; +console.log('x', char[CHAR_DATA_CHAR_INDEX], char[CHAR_DATA_CHAR_INDEX].length); } startIndex--; startCol--; } - while (endIndex + 1 < line.length && !this._isCharWordSeparator(line.charAt(endIndex + 1))) { - if (bufferLine[endCol + 1][CHAR_DATA_WIDTH_INDEX] === 2) { + console.log('scan forwards'); + // while (endIndex + 1 < line.length && !this._isCharWordSeparator(line.charAt(endIndex + 1))) { + console.log(' first checking: ',bufferLine[endCol + 1]); + console.log(' endIndex:',endIndex); + console.log(' line:',line); + console.log(' line.length:',line.length); + while (endIndex + 1 < line.length && !this._isCharWordSeparator(bufferLine[endCol + 1][CHAR_DATA_CHAR_INDEX])) { + const char = bufferLine[endCol + 1]; + console.log(' char: ' + char); + if (char[CHAR_DATA_WIDTH_INDEX] === 2) { // If the next character is a wide char, record it and skip the column rightWideCharCount++; endCol++; + } else if (char[CHAR_DATA_CHAR_INDEX].length > 1) { + startIndex += char[CHAR_DATA_CHAR_INDEX].length - 1; } endIndex++; endCol++; From fa86f31f36fbef234d4dffa987c0331e38062258 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 30 Sep 2017 06:30:00 -0400 Subject: [PATCH 05/30] Fix copying emoji characters on the edge of selection Fixes #1015 --- src/Buffer.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index 585855db..9c5c717f 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -237,16 +237,14 @@ export class Buffer implements IBuffer { // Adjust the columns to take glyphs that are represented by multiple // code points into account. if (char[CHAR_DATA_CHAR_INDEX].length > 1) { - if (startCol >= i) { + if (startCol > i) { startIndex += char[CHAR_DATA_CHAR_INDEX].length - 1; } - if (endCol >= i) { + if (endCol > i) { endIndex += char[CHAR_DATA_CHAR_INDEX].length - 1; } } } - // TODO: startCol needs to be emoji-aware, currently each emoji code point is - // consuming addition space in the selection text } // Calculate the final end col by trimming whitespace on the right of the From 3e29a03f874202d74e464e4083565ccefe9977a1 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 30 Sep 2017 07:54:52 -0400 Subject: [PATCH 06/30] Fix rendering issues for emojis that come out as wide chars The fix is to disallow non-single cell width chars to be overlapping --- src/renderer/TextRenderLayer.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/renderer/TextRenderLayer.ts b/src/renderer/TextRenderLayer.ts index e319efe7..eb163c1c 100644 --- a/src/renderer/TextRenderLayer.ts +++ b/src/renderer/TextRenderLayer.ts @@ -183,10 +183,15 @@ export class TextRenderLayer extends BaseRenderLayer { } /** - * Whether a character is overlapping to the - * next cell. + * Whether a character is overlapping to the next cell. */ private _isOverlapping(charData: CharData): boolean { + // Only single cell characters can be overlapping, rendering issues can + // occur without this check + if (charData[CHAR_DATA_WIDTH_INDEX] !== 1) { + return false; + } + // We assume that any ascii character will not overlap const code = charData[CHAR_DATA_CODE_INDEX]; if (code < 256) { From 4828c4a6faa15d5d4609b4921d28254bdd3fa6eb Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 30 Sep 2017 07:59:06 -0400 Subject: [PATCH 07/30] Clean up logs/comments --- src/SelectionManager.ts | 32 +++++--------------------------- 1 file changed, 5 insertions(+), 27 deletions(-) diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index 65f6a57e..9ba66efd 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -282,8 +282,6 @@ export class SelectionManager extends EventEmitter implements ISelectionManager coords[0]--; coords[1]--; - console.log('coords', coords); - // Convert viewport coords to buffer coords coords[1] += this._terminal.buffer.ydisp; return coords; @@ -550,14 +548,11 @@ export class SelectionManager extends EventEmitter implements ISelectionManager if (char[CHAR_DATA_WIDTH_INDEX] === 0) { // Wide characters aren't included in the line string so decrement the index charIndex--; - // } } else if (char[CHAR_DATA_CHAR_INDEX].length > 1) { - console.log('char length > 1', char[CHAR_DATA_CHAR_INDEX], char[CHAR_DATA_CHAR_INDEX].length); // Emojis take up multiple characters, so adjust accordingly charIndex += char[CHAR_DATA_CHAR_INDEX].length - 1; } } - console.log('character index: ', charIndex); return charIndex; } @@ -616,47 +611,30 @@ export class SelectionManager extends EventEmitter implements ISelectionManager endCol++; } // Expand the string in both directions until a space is hit - let nextCharData = startIndex > 0 ? bufferLine[startCol - 1] : null; - - - -// TODO: Need to make sure that characters whose strings are longer than 1 get compensated for -// Double click words should expand to the spaces. - - - // while (startIndex > 0 && !this._isCharWordSeparator(line.charAt(startIndex - 1))) { - console.log('start char: ' + bufferLine[startCol]); - console.log('scan backwards'); - console.log(' startIndex:',startIndex); while (startIndex > 0 && !this._isCharWordSeparator(bufferLine[startCol - 1][CHAR_DATA_CHAR_INDEX])) { const char = bufferLine[startCol - 1]; - console.log(' char: ' + char); if (char[CHAR_DATA_WIDTH_INDEX] === 0) { // If the next character is a wide char, record it and skip the column leftWideCharCount++; startCol--; } else if (char[CHAR_DATA_CHAR_INDEX].length > 1) { + // If the next character's string is longer than 1 char (eg. emoji), + // adjust the index startIndex -= char[CHAR_DATA_CHAR_INDEX].length - 1; -console.log('x', char[CHAR_DATA_CHAR_INDEX], char[CHAR_DATA_CHAR_INDEX].length); } startIndex--; startCol--; } - console.log('scan forwards'); - // while (endIndex + 1 < line.length && !this._isCharWordSeparator(line.charAt(endIndex + 1))) { - console.log(' first checking: ',bufferLine[endCol + 1]); - console.log(' endIndex:',endIndex); - console.log(' line:',line); - console.log(' line.length:',line.length); while (endIndex + 1 < line.length && !this._isCharWordSeparator(bufferLine[endCol + 1][CHAR_DATA_CHAR_INDEX])) { const char = bufferLine[endCol + 1]; - console.log(' char: ' + char); if (char[CHAR_DATA_WIDTH_INDEX] === 2) { // If the next character is a wide char, record it and skip the column rightWideCharCount++; endCol++; } else if (char[CHAR_DATA_CHAR_INDEX].length > 1) { - startIndex += char[CHAR_DATA_CHAR_INDEX].length - 1; + // If the next character's string is longer than 1 char (eg. emoji), + // adjust the index + endIndex += char[CHAR_DATA_CHAR_INDEX].length - 1; } endIndex++; endCol++; From 58259f8bd965ec23fc8e07a588bb8a03c48ed895 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 30 Sep 2017 08:30:19 -0400 Subject: [PATCH 08/30] Fix select word on wide characters --- src/Buffer.ts | 4 ++-- src/SelectionManager.ts | 15 ++++++++++----- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index 9c5c717f..8922c3d6 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -228,10 +228,10 @@ export class Buffer implements IBuffer { // column indexes if (char[CHAR_DATA_WIDTH_INDEX] === 0) { if (startCol >= i) { - startIndex -= char[CHAR_DATA_CHAR_INDEX].length; + startIndex--; } if (endCol >= i) { - endIndex -= char[CHAR_DATA_CHAR_INDEX].length; + endIndex--; } } else { // Adjust the columns to take glyphs that are represented by multiple diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index 9ba66efd..f8464d09 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -10,7 +10,7 @@ import { CircularList } from './utils/CircularList'; import { EventEmitter } from './EventEmitter'; import { ITerminal, ICircularList, ISelectionManager, IBuffer } from './Interfaces'; import { SelectionModel } from './SelectionModel'; -import { LineData } from './Types'; +import { LineData, CharData } from './Types'; import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from './Buffer'; /** @@ -611,7 +611,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager endCol++; } // Expand the string in both directions until a space is hit - while (startIndex > 0 && !this._isCharWordSeparator(bufferLine[startCol - 1][CHAR_DATA_CHAR_INDEX])) { + while (startIndex > 0 && !this._isCharWordSeparator(bufferLine[startCol - 1])) { const char = bufferLine[startCol - 1]; if (char[CHAR_DATA_WIDTH_INDEX] === 0) { // If the next character is a wide char, record it and skip the column @@ -625,7 +625,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager startIndex--; startCol--; } - while (endIndex + 1 < line.length && !this._isCharWordSeparator(bufferLine[endCol + 1][CHAR_DATA_CHAR_INDEX])) { + while (endIndex + 1 < line.length && !this._isCharWordSeparator(bufferLine[endCol + 1])) { const char = bufferLine[endCol + 1]; if (char[CHAR_DATA_WIDTH_INDEX] === 2) { // If the next character is a wide char, record it and skip the column @@ -674,8 +674,13 @@ export class SelectionManager extends EventEmitter implements ISelectionManager * word logic. * @param char The character to check. */ - private _isCharWordSeparator(char: string): boolean { - return WORD_SEPARATORS.indexOf(char) >= 0; + private _isCharWordSeparator(charData: CharData): boolean { + // Zero width characters are never separators as they are always to the + // right of wide characters + if (charData[CHAR_DATA_WIDTH_INDEX] === 0) { + return false; + } + return WORD_SEPARATORS.indexOf(charData[CHAR_DATA_CHAR_INDEX]) >= 0; } /** From e0c28bc7d2fffd1019c82e865b343c665cb084b7 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 30 Sep 2017 09:32:33 -0400 Subject: [PATCH 09/30] Fix selecting words containing ZWJ emojis --- src/SelectionManager.test.ts | 53 +++++++++++++++++++++++++++++++++++- src/SelectionManager.ts | 33 ++++++++++++++++------ 2 files changed, 76 insertions(+), 10 deletions(-) diff --git a/src/SelectionManager.test.ts b/src/SelectionManager.test.ts index e5bdc117..77312467 100644 --- a/src/SelectionManager.test.ts +++ b/src/SelectionManager.test.ts @@ -12,7 +12,7 @@ import { SelectionManager } from './SelectionManager'; import { SelectionModel } from './SelectionModel'; import { BufferSet } from './BufferSet'; import { MockTerminal } from './utils/TestUtils.test'; -import { LineData } from './Types'; +import { LineData, CharData } from './Types'; class TestSelectionManager extends SelectionManager { constructor( @@ -66,6 +66,10 @@ describe('SelectionManager', () => { return result; } + function stringArrayToRow(chars: string[]): LineData { + return chars.map(c => [0, c, 1, c.charCodeAt(0)]); + } + describe('_selectWordAt', () => { it('should expand selection for normal width chars', () => { buffer.lines.set(0, stringToRow('foo bar')); @@ -185,6 +189,53 @@ describe('SelectionManager', () => { selectionManager.selectWordAt([15, 0]); assert.equal(selectionManager.selectionText, 'ij"'); }); + describe('emoji', () => { + it('should treat a single emoji as a word when wrapped in spaces', () => { + buffer.lines.set(0, stringToRow(' ⚽ a')); // The a is here to prevent the space being trimmed in selectionText + selectionManager.selectWordAt([0, 0]); + assert.equal(selectionManager.selectionText, ' '); + selectionManager.selectWordAt([1, 0]); + assert.equal(selectionManager.selectionText, '⚽'); + selectionManager.selectWordAt([2, 0]); + assert.equal(selectionManager.selectionText, ' '); + }); + it('should treat multiple emojis as a word when wrapped in spaces', () => { + buffer.lines.set(0, stringToRow(' ⚽⚽ a')); // The a is here to prevent the space being trimmed in selectionText + selectionManager.selectWordAt([0, 0]); + assert.equal(selectionManager.selectionText, ' '); + selectionManager.selectWordAt([1, 0]); + assert.equal(selectionManager.selectionText, '⚽⚽'); + selectionManager.selectWordAt([2, 0]); + assert.equal(selectionManager.selectionText, '⚽⚽'); + selectionManager.selectWordAt([3, 0]); + assert.equal(selectionManager.selectionText, ' '); + }); + it('should treat emojis using the zero-width-joiner as a single word', () => { + buffer.lines.set(0, stringArrayToRow([ + ' ', + '👨‍', // Note that the first 3 emojis include the invisible ZWJ char + '👩‍', + '👧‍', + '👦', + ' ', + 'a' + ])); // The a is here to prevent the space being trimmed in selectionText + selectionManager.selectWordAt([0, 0]); + assert.equal(selectionManager.selectionText, ' '); + // ZWJ emojis do not combine in the terminal so the family emoji used here consumed 4 cells + // The selection text should retain ZWJ chars despite not combining on the terminal + selectionManager.selectWordAt([1, 0]); + assert.equal(selectionManager.selectionText, '👨‍👩‍👧‍👦'); + selectionManager.selectWordAt([2, 0]); + assert.equal(selectionManager.selectionText, '👨‍👩‍👧‍👦'); + selectionManager.selectWordAt([3, 0]); + assert.equal(selectionManager.selectionText, '👨‍👩‍👧‍👦'); + selectionManager.selectWordAt([4, 0]); + assert.equal(selectionManager.selectionText, '👨‍👩‍👧‍👦'); + selectionManager.selectWordAt([5, 0]); + assert.equal(selectionManager.selectionText, ' '); + }); + }); }); describe('_selectLineAt', () => { diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index f8464d09..27aeafff 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -546,10 +546,13 @@ export class SelectionManager extends EventEmitter implements ISelectionManager for (let i = 0; coords[0] >= i; i++) { const char = bufferLine[i]; if (char[CHAR_DATA_WIDTH_INDEX] === 0) { - // Wide characters aren't included in the line string so decrement the index + // Wide characters aren't included in the line string so decrement the + // index so the index is back on the wide character. charIndex--; - } else if (char[CHAR_DATA_CHAR_INDEX].length > 1) { - // Emojis take up multiple characters, so adjust accordingly + } else if (char[CHAR_DATA_CHAR_INDEX].length > 1 && coords[0] !== i) { + // Emojis take up multiple characters, so adjust accordingly. For these + // we don't want ot include the character at the column as we're + // returning the start index in the string, not the end index. charIndex += char[CHAR_DATA_CHAR_INDEX].length - 1; } } @@ -577,13 +580,15 @@ export class SelectionManager extends EventEmitter implements ISelectionManager const line = this._buffer.translateBufferLineToString(coords[1], false); // Get actual index, taking into consideration wide characters - let endIndex = this._convertViewportColToCharacterIndex(bufferLine, coords); - let startIndex = endIndex; + let startIndex = this._convertViewportColToCharacterIndex(bufferLine, coords); + let endIndex = startIndex; // Record offset to be used later const charOffset = coords[0] - startIndex; let leftWideCharCount = 0; let rightWideCharCount = 0; + let leftLongCharOffset = 0; + let rightLongCharOffset = 0; if (line.charAt(startIndex) === ' ') { // Expand until non-whitespace is hit @@ -600,6 +605,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager // character is hit, it is recorded and the column index is adjusted. let startCol = coords[0]; let endCol = coords[0]; + // Consider the initial position, skip it and increment the wide char // variable if (bufferLine[startCol][CHAR_DATA_WIDTH_INDEX] === 0) { @@ -610,8 +616,15 @@ export class SelectionManager extends EventEmitter implements ISelectionManager rightWideCharCount++; endCol++; } + + // Adjust the end index for characters whose length are > 1 (emojis) + if (bufferLine[endCol][CHAR_DATA_CHAR_INDEX].length > 1) { + rightLongCharOffset += bufferLine[endCol][CHAR_DATA_CHAR_INDEX].length - 1; + endIndex += bufferLine[endCol][CHAR_DATA_CHAR_INDEX].length - 1; + } + // Expand the string in both directions until a space is hit - while (startIndex > 0 && !this._isCharWordSeparator(bufferLine[startCol - 1])) { + while (startCol > 0 && startIndex > 0 && !this._isCharWordSeparator(bufferLine[startCol - 1])) { const char = bufferLine[startCol - 1]; if (char[CHAR_DATA_WIDTH_INDEX] === 0) { // If the next character is a wide char, record it and skip the column @@ -620,12 +633,13 @@ export class SelectionManager extends EventEmitter implements ISelectionManager } else if (char[CHAR_DATA_CHAR_INDEX].length > 1) { // If the next character's string is longer than 1 char (eg. emoji), // adjust the index + leftLongCharOffset += char[CHAR_DATA_CHAR_INDEX].length - 1; startIndex -= char[CHAR_DATA_CHAR_INDEX].length - 1; } startIndex--; startCol--; } - while (endIndex + 1 < line.length && !this._isCharWordSeparator(bufferLine[endCol + 1])) { + while (endCol < bufferLine.length && endIndex + 1 < line.length && !this._isCharWordSeparator(bufferLine[endCol + 1])) { const char = bufferLine[endCol + 1]; if (char[CHAR_DATA_WIDTH_INDEX] === 2) { // If the next character is a wide char, record it and skip the column @@ -634,6 +648,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager } else if (char[CHAR_DATA_CHAR_INDEX].length > 1) { // If the next character's string is longer than 1 char (eg. emoji), // adjust the index + rightLongCharOffset += char[CHAR_DATA_CHAR_INDEX].length - 1; endIndex += char[CHAR_DATA_CHAR_INDEX].length - 1; } endIndex++; @@ -641,8 +656,8 @@ export class SelectionManager extends EventEmitter implements ISelectionManager } } - const start = startIndex + charOffset - leftWideCharCount; - const length = Math.min(endIndex - startIndex + leftWideCharCount + rightWideCharCount + 1/*include endIndex char*/, this._terminal.cols); + const start = startIndex + charOffset - leftWideCharCount + leftLongCharOffset; + const length = Math.min(endIndex - startIndex + leftWideCharCount + rightWideCharCount - leftLongCharOffset - rightLongCharOffset + 1/*include endIndex char*/, this._terminal.cols); return { start, length }; } From da4e3e6e9ffce221c5cb0e67a6fe413de159a218 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 30 Sep 2017 09:45:11 -0400 Subject: [PATCH 10/30] Improve documentation on word selection --- src/SelectionManager.ts | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index 27aeafff..da484b85 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -656,8 +656,27 @@ export class SelectionManager extends EventEmitter implements ISelectionManager } } - const start = startIndex + charOffset - leftWideCharCount + leftLongCharOffset; - const length = Math.min(endIndex - startIndex + leftWideCharCount + rightWideCharCount - leftLongCharOffset - rightLongCharOffset + 1/*include endIndex char*/, this._terminal.cols); + // Incremenet the end index so it is at the start of the next character + endIndex++; + + // Calculate the start _column_, converting the the string indexes back to + // column coordinates. + const start = + startIndex // The index of the selection's start char in the line string + + charOffset // The difference between the initial char's column and index + - leftWideCharCount // The number of wide chars left of the initial char + + leftLongCharOffset; // The number of additional chars left of the initial char added by columns with strings longer than 1 (emojis) + + // Calculate the length in _columns_, converting the the string indexes back + // to column coordinates. + const length = Math.min(this._terminal.cols, // Disallow lengths larger than the terminal cols + endIndex // The index of the selection's end char in the line string + - startIndex // The index of the selection's start char in the line string + + leftWideCharCount // The number of wide chars left of the initial char + + rightWideCharCount // The number of wide chars right of the initial char (inclusive) + - leftLongCharOffset // The number of additional chars left of the initial char added by columns with strings longer than 1 (emojis) + - rightLongCharOffset); // The number of additional chars right of the initial char (inclusive) added by columns with strings longer than 1 (emojis) + return { start, length }; } From b1214063d23491522a421c3b7b5b4ea119fb296c Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 30 Sep 2017 10:00:18 -0400 Subject: [PATCH 11/30] Add more complex emoji selection tests --- src/SelectionManager.test.ts | 74 ++++++++++++++++++++++++++++++++---- 1 file changed, 67 insertions(+), 7 deletions(-) diff --git a/src/SelectionManager.test.ts b/src/SelectionManager.test.ts index 77312467..8c6ea3e1 100644 --- a/src/SelectionManager.test.ts +++ b/src/SelectionManager.test.ts @@ -211,14 +211,9 @@ describe('SelectionManager', () => { assert.equal(selectionManager.selectionText, ' '); }); it('should treat emojis using the zero-width-joiner as a single word', () => { + // Note that the first 3 emojis include the invisible ZWJ char buffer.lines.set(0, stringArrayToRow([ - ' ', - '👨‍', // Note that the first 3 emojis include the invisible ZWJ char - '👩‍', - '👧‍', - '👦', - ' ', - 'a' + ' ', '👨‍', '👩‍', '👧‍', '👦', ' ', 'a' ])); // The a is here to prevent the space being trimmed in selectionText selectionManager.selectWordAt([0, 0]); assert.equal(selectionManager.selectionText, ' '); @@ -235,6 +230,71 @@ describe('SelectionManager', () => { selectionManager.selectWordAt([5, 0]); assert.equal(selectionManager.selectionText, ' '); }); + it('should treat emojis and characters joined together as a word', () => { + buffer.lines.set(0, stringToRow(' ⚽ab cd⚽ ef⚽gh')); // The a is here to prevent the space being trimmed in selectionText + selectionManager.selectWordAt([0, 0]); + assert.equal(selectionManager.selectionText, ' '); + selectionManager.selectWordAt([1, 0]); + assert.equal(selectionManager.selectionText, '⚽ab'); + selectionManager.selectWordAt([2, 0]); + assert.equal(selectionManager.selectionText, '⚽ab'); + selectionManager.selectWordAt([3, 0]); + assert.equal(selectionManager.selectionText, '⚽ab'); + selectionManager.selectWordAt([4, 0]); + assert.equal(selectionManager.selectionText, ' '); + selectionManager.selectWordAt([5, 0]); + assert.equal(selectionManager.selectionText, 'cd⚽'); + selectionManager.selectWordAt([6, 0]); + assert.equal(selectionManager.selectionText, 'cd⚽'); + selectionManager.selectWordAt([7, 0]); + assert.equal(selectionManager.selectionText, 'cd⚽'); + selectionManager.selectWordAt([8, 0]); + assert.equal(selectionManager.selectionText, ' '); + selectionManager.selectWordAt([9, 0]); + assert.equal(selectionManager.selectionText, 'ef⚽gh'); + selectionManager.selectWordAt([10, 0]); + assert.equal(selectionManager.selectionText, 'ef⚽gh'); + selectionManager.selectWordAt([11, 0]); + assert.equal(selectionManager.selectionText, 'ef⚽gh'); + selectionManager.selectWordAt([12, 0]); + assert.equal(selectionManager.selectionText, 'ef⚽gh'); + selectionManager.selectWordAt([13, 0]); + assert.equal(selectionManager.selectionText, 'ef⚽gh'); + }); + it('should treat complex emojis and characters joined together as a word', () => { + // This emoji is the flag for England and is made up of: 1F3F4 E0067 E0062 E0065 E006E E0067 E007F + buffer.lines.set(0, stringArrayToRow([ + ' ', '🏴󠁧󠁢󠁥󠁮󠁧󠁿', 'a', 'b', ' ', 'c', 'd', '🏴󠁧󠁢󠁥󠁮󠁧󠁿', ' ', 'e', 'f', '🏴󠁧󠁢󠁥󠁮󠁧󠁿', 'g', 'h', ' ', 'a' + ])); // The a is here to prevent the space being trimmed in selectionText + selectionManager.selectWordAt([0, 0]); + assert.equal(selectionManager.selectionText, ' '); + selectionManager.selectWordAt([1, 0]); + assert.equal(selectionManager.selectionText, '🏴󠁧󠁢󠁥󠁮󠁧󠁿ab'); + selectionManager.selectWordAt([2, 0]); + assert.equal(selectionManager.selectionText, '🏴󠁧󠁢󠁥󠁮󠁧󠁿ab'); + selectionManager.selectWordAt([3, 0]); + assert.equal(selectionManager.selectionText, '🏴󠁧󠁢󠁥󠁮󠁧󠁿ab'); + selectionManager.selectWordAt([4, 0]); + assert.equal(selectionManager.selectionText, ' '); + selectionManager.selectWordAt([5, 0]); + assert.equal(selectionManager.selectionText, 'cd🏴󠁧󠁢󠁥󠁮󠁧󠁿'); + selectionManager.selectWordAt([6, 0]); + assert.equal(selectionManager.selectionText, 'cd🏴󠁧󠁢󠁥󠁮󠁧󠁿'); + selectionManager.selectWordAt([7, 0]); + assert.equal(selectionManager.selectionText, 'cd🏴󠁧󠁢󠁥󠁮󠁧󠁿'); + selectionManager.selectWordAt([8, 0]); + assert.equal(selectionManager.selectionText, ' '); + selectionManager.selectWordAt([9, 0]); + assert.equal(selectionManager.selectionText, 'ef🏴󠁧󠁢󠁥󠁮󠁧󠁿gh'); + selectionManager.selectWordAt([10, 0]); + assert.equal(selectionManager.selectionText, 'ef🏴󠁧󠁢󠁥󠁮󠁧󠁿gh'); + selectionManager.selectWordAt([11, 0]); + assert.equal(selectionManager.selectionText, 'ef🏴󠁧󠁢󠁥󠁮󠁧󠁿gh'); + selectionManager.selectWordAt([12, 0]); + assert.equal(selectionManager.selectionText, 'ef🏴󠁧󠁢󠁥󠁮󠁧󠁿gh'); + selectionManager.selectWordAt([13, 0]); + assert.equal(selectionManager.selectionText, 'ef🏴󠁧󠁢󠁥󠁮󠁧󠁿gh'); + }); }); }); From 95f68a41d7e625155561edfa0e19f7d9fe65d71b Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 2 Oct 2017 10:42:14 -0700 Subject: [PATCH 12/30] Ensure linkifier cancels mouse event when being handled The terminal mouse events handling code was being run before links were being considered. This caused clicking link to have unexpected behavior in mouse mode term apps. Fixes #1020 --- src/input/MouseZoneManager.ts | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/input/MouseZoneManager.ts b/src/input/MouseZoneManager.ts index 8329d0b6..ebb91699 100644 --- a/src/input/MouseZoneManager.ts +++ b/src/input/MouseZoneManager.ts @@ -21,7 +21,6 @@ export class MouseZoneManager implements IMouseZoneManager { private _areZonesActive: boolean = false; private _mouseMoveListener: (e: MouseEvent) => any; - private _mouseDownListener: (e: MouseEvent) => any; private _clickListener: (e: MouseEvent) => any; private _tooltipTimeout: number = null; @@ -31,6 +30,8 @@ export class MouseZoneManager implements IMouseZoneManager { constructor( private _terminal: ITerminal ) { + this._terminal.element.addEventListener('mousedown', e => this._onMouseDown(e)); + // These events are expensive, only listen to it when mouse zones are active this._mouseMoveListener = e => this._onMouseMove(e); this._clickListener = e => this._onClick(e); @@ -140,11 +141,30 @@ export class MouseZoneManager implements IMouseZoneManager { } } + private _onMouseDown(e: MouseEvent): void { + // Ignore the event if there are no zones active + if (!this._areZonesActive) { + return; + } + + // Find the active zone, prevent event propagation if found to prevent other + // components from handling the mouse event. + const zone = this._findZoneEventAt(e); + if (zone) { + // TODO: When link modifier support is added, the event should only be + // cancelled when the modifier is held (see #1021) + e.preventDefault(); + e.stopImmediatePropagation(); + } + } + private _onClick(e: MouseEvent): void { + // Find the active zone and click it if found const zone = this._findZoneEventAt(e); if (zone) { zone.clickCallback(e); e.preventDefault(); + e.stopImmediatePropagation(); } } From 56da538ca01d16deed42a002af5a7e159dffbdfc Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 2 Oct 2017 11:12:33 -0700 Subject: [PATCH 13/30] Fix reference to document --- src/utils/MouseHelper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/MouseHelper.ts b/src/utils/MouseHelper.ts index 7b4a3611..b7ef09ad 100644 --- a/src/utils/MouseHelper.ts +++ b/src/utils/MouseHelper.ts @@ -27,7 +27,7 @@ export class MouseHelper { element = 'offsetParent' in element ? element.offsetParent : element.parentElement; } element = originalElement; - while (element && element !== self.document.body) { + while (element && element !== element.ownerDocument.body) { x += element.scrollLeft; y += element.scrollTop; element = element.parentElement; From 966f5c1f32d161f055d0abdae204d958d54302dd Mon Sep 17 00:00:00 2001 From: Rick Baker Date: Mon, 2 Oct 2017 13:34:30 -0500 Subject: [PATCH 14/30] Update README.md Who would have thought that a name could have so much controversy. I've renamed due to a bunch of flack about devops not logging into servers :) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1551c864..a4e70ae5 100644 --- a/README.md +++ b/README.md @@ -129,7 +129,7 @@ Xterm.js is used in several world-class applications to provide great terminal e - [**JupyterLab**](https://github.com/jupyterlab/jupyterlab): An extensible computational environment for Jupyter, supporting interactive data science and scientific computing across all programming languages. - [**Theia**](https://github.com/theia-ide/theia): Theia is a cloud & desktop IDE framework implemented in TypeScript. -- [**DevOps Helper**](https://github.com/ricktbaker/devops_helper) DevOps Helper tool to make life easier working with AWS instances across multiple organizations. +- [**Opshell**](https://github.com/ricktbaker/opshell) Ops Helper tool to make life easier working with AWS instances across multiple organizations. Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it in our list. From 2a126cf1dc38712a0967415818d1d4287d862c3e Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 4 Oct 2017 11:45:13 -0700 Subject: [PATCH 15/30] Fix links sometimes not getting applied There was an incorrect comparison which occasionally caused links to not be picked up. Fixes #1025 --- src/Linkifier.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Linkifier.ts b/src/Linkifier.ts index 42d20f9c..e6a63b5c 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -87,7 +87,7 @@ export class Linkifier extends EventEmitter implements ILinkifier { this._rowsToLinkify.end = end; } else { this._rowsToLinkify.start = this._rowsToLinkify.start < start ? this._rowsToLinkify.start : start; - this._rowsToLinkify.end = this._rowsToLinkify.end < end ? this._rowsToLinkify.end : end; + this._rowsToLinkify.end = this._rowsToLinkify.end > end ? this._rowsToLinkify.end : end; } // Clear out any existing links on this row range From 7cfc108fb457b765d3b28c8416a7614b9b9ad838 Mon Sep 17 00:00:00 2001 From: Jakob Gillich Date: Thu, 5 Oct 2017 01:35:34 +0200 Subject: [PATCH 16/30] remove import file extension --- src/utils/CharMeasure.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/CharMeasure.ts b/src/utils/CharMeasure.ts index e5b03f87..779ecbe8 100644 --- a/src/utils/CharMeasure.ts +++ b/src/utils/CharMeasure.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { EventEmitter } from '../EventEmitter.js'; +import { EventEmitter } from '../EventEmitter'; import { ICharMeasure, ITerminal, ITerminalOptions } from '../Interfaces'; /** From e693c9e86b60c67a7a139e33726416085ca830d9 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 5 Oct 2017 07:52:34 -0700 Subject: [PATCH 17/30] Normalize tsconfig outDir It was coming out with the wrap path separators on Windows Fixes #1024 --- gulpfile.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gulpfile.js b/gulpfile.js index 6a20c729..cf001da3 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -25,8 +25,8 @@ let outDir = tsProject.config.compilerOptions.outDir; // Under some environments like TravisCI, this comes out at absolute which can // break the build. This ensures that the outDir is absolute. -if (outDir.indexOf(__dirname) !== 0) { - outDir = `${__dirname}/${outDir}`; +if (path.normalize(outDir).indexOf(__dirname) !== 0) { + outDir = `${__dirname}/${path.normalize(outDir)}`; } /** From f36ba01460c568ffa73c03354b9dd7334eb3ee7a Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 5 Oct 2017 11:15:46 -0700 Subject: [PATCH 18/30] Add a null check to areSelectionValuesReversed See Microsoft/vscode#35601 --- src/SelectionModel.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/SelectionModel.ts b/src/SelectionModel.ts index 1fbf40fe..5982b1e9 100644 --- a/src/SelectionModel.ts +++ b/src/SelectionModel.ts @@ -97,6 +97,9 @@ export class SelectionModel { public areSelectionValuesReversed(): boolean { const start = this.selectionStart; const end = this.selectionEnd; + if (!start || !end) { + return false; + } return start[1] > end[1] || (start[1] === end[1] && start[0] > end[0]); } From 0a08d6389a59ba2af1566fe2422f0b109262edef Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 5 Oct 2017 19:31:07 -0700 Subject: [PATCH 19/30] Fix sub-pixel anti-aliasing for uncached text Fixes #1032 --- src/renderer/BaseRenderLayer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index d2a704a3..8ff47503 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -31,7 +31,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._canvas = document.createElement('canvas'); this._canvas.id = `xterm-${id}-layer`; this._canvas.style.zIndex = zIndex.toString(); - this._ctx = this._canvas.getContext('2d', {_alpha}); + this._ctx = this._canvas.getContext('2d', {alpha: _alpha}); this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio); // Draw the background if this is an opaque layer if (!_alpha) { From 108534a4fe42a767761293970e41e73d2c9e1e51 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 5 Oct 2017 20:09:17 -0700 Subject: [PATCH 20/30] Fix null pointer exception --- src/renderer/TextRenderLayer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/TextRenderLayer.ts b/src/renderer/TextRenderLayer.ts index e319efe7..88930165 100644 --- a/src/renderer/TextRenderLayer.ts +++ b/src/renderer/TextRenderLayer.ts @@ -121,7 +121,7 @@ export class TextRenderLayer extends BaseRenderLayer { // get removed, and `a` would not re-render because it thinks it's // already in the correct state. this._state.cache[x][y] = OVERLAP_OWNED_CHAR_DATA; - if (x < line.length && line[x + 1][CHAR_DATA_CODE_INDEX] === 32 /*' '*/) { + if (x < line.length - 1 && line[x + 1][CHAR_DATA_CODE_INDEX] === 32 /*' '*/) { width = 2; this._clearChar(x + 1, y); // The overlapping char's char data will force a clear and render when the From be8ddb11dbcbc855f0695c79b17b3e32546d4449 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 5 Oct 2017 20:21:34 -0700 Subject: [PATCH 21/30] Add support back for IE11 Fixes #1033 --- README.md | 1 + demo/main.js | 2 +- src/addons/fit/fit.js | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a4e70ae5..e7a80bfb 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,7 @@ Since xterm.js is typically implemented as a developer tool, only modern browser - Edge latest - Firefox latest - Safari latest +- IE11 Xterm.js works seamlessly in Electron apps and may even work on earlier versions of the browsers but these are the browsers we strive to keep working. diff --git a/demo/main.js b/demo/main.js index f4fb0a2a..2ed2b33c 100644 --- a/demo/main.js +++ b/demo/main.js @@ -93,7 +93,7 @@ function createTerminal() { term.fit(); // fit is called within a setTimeout, cols and rows need this. - setTimeout(() => { + setTimeout(function () { colsElement.value = term.cols; rowsElement.value = term.rows; diff --git a/src/addons/fit/fit.js b/src/addons/fit/fit.js index 3e61a7ef..c82039ab 100644 --- a/src/addons/fit/fit.js +++ b/src/addons/fit/fit.js @@ -56,7 +56,7 @@ exports.fit = function (term) { // Wrap fit in a setTimeout as charMeasure needs time to get initialized // after calling Terminal.open - setTimeout(() => { + setTimeout(function () { var geometry = exports.proposeGeometry(term); if (geometry) { From 356a14bb161589cac72bbc3ccc7fa975ae9f7a3c Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 5 Oct 2017 20:30:09 -0700 Subject: [PATCH 22/30] Fix non-default backgrounds on left cell of wide/overlapping chars Fixes #1031 --- src/renderer/BaseRenderLayer.ts | 5 ----- src/renderer/TextRenderLayer.ts | 5 +++++ 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 8ff47503..aa6c7e92 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -224,11 +224,6 @@ export abstract class BaseRenderLayer implements IRenderLayer { * @param bold Whether the text is bold. */ protected drawChar(terminal: ITerminal, char: string, code: number, width: number, x: number, y: number, fg: number, bg: number, bold: boolean): void { - // Clear the cell next to this character if it's wide - if (width === 2) { - this.clearCells(x + 1, y, 1, 1); - } - let colorIndex = 0; if (fg < 256) { colorIndex = fg + 2; diff --git a/src/renderer/TextRenderLayer.ts b/src/renderer/TextRenderLayer.ts index 88930165..86159af3 100644 --- a/src/renderer/TextRenderLayer.ts +++ b/src/renderer/TextRenderLayer.ts @@ -146,6 +146,11 @@ export class TextRenderLayer extends BaseRenderLayer { } } + // Clear the cell next to this character if it's wide + if (width === 2) { + this.clearCells(x + 1, y, 1, 1); + } + // Draw background if (bg < 256) { this._ctx.save(); From 06b9f1db873068b940502e1a88b30f2baa76cbb7 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 5 Oct 2017 23:24:44 -0700 Subject: [PATCH 23/30] Always draw entire line and allow overlapping State/diffing code is kept in comments as it may be reinstated soon. Fixes #1035 Fixes #1036 Fixes #1032 Fixes #1037 --- src/renderer/BaseRenderLayer.ts | 4 ++-- src/renderer/CharAtlas.ts | 35 +++++++++++++++++++++++++++++-- src/renderer/ColorManager.ts | 15 ++++++++++++- src/renderer/TextRenderLayer.ts | 37 +++++++++++++++++++-------------- 4 files changed, 70 insertions(+), 21 deletions(-) diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index aa6c7e92..d15f1ee7 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -242,7 +242,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { const charAtlasCellWidth = this._scaledCharWidth + CHAR_ATLAS_CELL_SPACING; const charAtlasCellHeight = this._scaledCharHeight + CHAR_ATLAS_CELL_SPACING; this._ctx.drawImage(this._charAtlas, - code * charAtlasCellWidth, colorIndex * charAtlasCellHeight, this._scaledCharWidth, this._scaledCharHeight, + code * charAtlasCellWidth, colorIndex * charAtlasCellHeight, charAtlasCellWidth, this._scaledCharHeight, x * this._scaledCharWidth, y * this._scaledLineHeight + this._scaledLineDrawY, this._scaledCharWidth, this._scaledCharHeight); } else { this._drawUncachedChar(terminal, char, width, fg, x, y, bold); @@ -285,7 +285,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { // can bleed into other cells. This code will clip the following fillText, // ensuring that its contents don't go beyond the cell bounds. this._ctx.beginPath(); - this._ctx.rect(x * this._scaledCharWidth, y * this._scaledLineHeight + this._scaledLineDrawY, width * this._scaledCharWidth, this._scaledCharHeight); + this._ctx.rect(0, y * this._scaledLineHeight + this._scaledLineDrawY, terminal.cols * this._scaledCharWidth, this._scaledCharHeight); this._ctx.clip(); // Draw the character diff --git a/src/renderer/CharAtlas.ts b/src/renderer/CharAtlas.ts index 6808f85e..9ac1e161 100644 --- a/src/renderer/CharAtlas.ts +++ b/src/renderer/CharAtlas.ts @@ -142,13 +142,23 @@ class CharAtlasGenerator { // Default color for (let i = 0; i < 256; i++) { + this._ctx.save(); + this._ctx.beginPath(); + this._ctx.rect(i * cellWidth, 0, cellWidth, cellHeight); + this._ctx.clip(); this._ctx.fillText(String.fromCharCode(i), i * cellWidth, 0); + this._ctx.restore(); } // Default color bold this._ctx.save(); this._ctx.font = `bold ${this._ctx.font}`; for (let i = 0; i < 256; i++) { + this._ctx.save(); + this._ctx.beginPath(); + this._ctx.rect(i * cellWidth, cellHeight, cellWidth, cellHeight); + this._ctx.clip(); this._ctx.fillText(String.fromCharCode(i), i * cellWidth, cellHeight); + this._ctx.restore(); } this._ctx.restore(); @@ -162,14 +172,17 @@ class CharAtlasGenerator { const y = (colorIndex + 2) * cellHeight; // Draw ascii characters for (let i = 0; i < 256; i++) { + this._ctx.save(); + this._ctx.beginPath(); + this._ctx.rect(i * cellWidth, y, cellWidth, cellHeight); + this._ctx.clip(); this._ctx.fillStyle = ansiColors[colorIndex]; this._ctx.fillText(String.fromCharCode(i), i * cellWidth, y); + this._ctx.restore(); } } this._ctx.restore(); - const charAtlasImageData = this._ctx.getImageData(0, 0, this._canvas.width, this._canvas.height); - // Support is patchy for createImageBitmap at the moment, pass a canvas back // if support is lacking as drawImage works there too. Firefox is also // included here as ImageBitmap appears both buggy and has horrible @@ -183,9 +196,27 @@ class CharAtlasGenerator { return result; } + const charAtlasImageData = this._ctx.getImageData(0, 0, this._canvas.width, this._canvas.height); + + // Remove the background color from the image so characters may overlap + const r = parseInt(background.substr(1, 2), 16); + const g = parseInt(background.substr(3, 2), 16); + const b = parseInt(background.substr(5, 2), 16); + this._clearColor(charAtlasImageData, r, g, b); + const promise = window.createImageBitmap(charAtlasImageData); // Clear the rect while the promise is in progress this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); return promise; } + + private _clearColor(imageData: ImageData, r: number, g: number, b: number): void { + for (let offset = 0; offset < imageData.data.length; offset += 4) { + if (imageData.data[offset] === r && + imageData.data[offset + 1] === g && + imageData.data[offset + 2] === b) { + imageData.data[offset + 3] = 0; + } + } + } } diff --git a/src/renderer/ColorManager.ts b/src/renderer/ColorManager.ts index ea5f1cb0..80daf31f 100644 --- a/src/renderer/ColorManager.ts +++ b/src/renderer/ColorManager.ts @@ -86,7 +86,7 @@ export class ColorManager implements IColorManager { */ public setTheme(theme: ITheme): void { this.colors.foreground = theme.foreground || DEFAULT_FOREGROUND; - this.colors.background = theme.background || DEFAULT_BACKGROUND; + this.colors.background = this._validateColor(theme.background, DEFAULT_BACKGROUND); this.colors.cursor = theme.cursor || DEFAULT_CURSOR; this.colors.cursorAccent = theme.cursorAccent || DEFAULT_CURSOR_ACCENT; this.colors.selection = theme.selection || DEFAULT_SELECTION; @@ -107,4 +107,17 @@ export class ColorManager implements IColorManager { this.colors.ansi[14] = theme.brightCyan || DEFAULT_ANSI_COLORS[14]; this.colors.ansi[15] = theme.brightWhite || DEFAULT_ANSI_COLORS[15]; } + + private _validateColor(color: string, fallback: string): string { + if (color.length === 7 && color.charAt(0) === '#') { + return color; + } + if (color.length === 4 && color.charAt(0) === '#') { + const r = color.charAt(1); + const g = color.charAt(2); + const b = color.charAt(3); + return `#${r}${r}${g}${g}${b}${b}`; + } + return fallback; + } } diff --git a/src/renderer/TextRenderLayer.ts b/src/renderer/TextRenderLayer.ts index 86159af3..75c1dae8 100644 --- a/src/renderer/TextRenderLayer.ts +++ b/src/renderer/TextRenderLayer.ts @@ -59,6 +59,11 @@ export class TextRenderLayer extends BaseRenderLayer { const row = y + terminal.buffer.ydisp; const line = terminal.buffer.lines.get(row); + this.clearCells(0, y, terminal.cols, 1); + // for (let x = 0; x < terminal.cols; x++) { + // this._state.cache[x][y] = null; + // } + for (let x = 0; x < terminal.cols; x++) { const charData = line[x]; const code: number = charData[CHAR_DATA_CODE_INDEX]; @@ -69,7 +74,7 @@ export class TextRenderLayer extends BaseRenderLayer { // The character to the left is a wide character, drawing is owned by // the char at x-1 if (width === 0) { - this._state.cache[x][y] = null; + // this._state.cache[x][y] = null; continue; } @@ -86,19 +91,19 @@ export class TextRenderLayer extends BaseRenderLayer { } // Skip rendering if the character is identical - const state = this._state.cache[x][y]; - if (state && state[CHAR_DATA_CHAR_INDEX] === char && state[CHAR_DATA_ATTR_INDEX] === attr) { - // Skip render, contents are identical - this._state.cache[x][y] = charData; - continue; - } + // const state = this._state.cache[x][y]; + // if (state && state[CHAR_DATA_CHAR_INDEX] === char && state[CHAR_DATA_ATTR_INDEX] === attr) { + // // Skip render, contents are identical + // this._state.cache[x][y] = charData; + // continue; + // } // Clear the old character was not a space with the default background - const wasInverted = !!(state && state[CHAR_DATA_ATTR_INDEX] && state[CHAR_DATA_ATTR_INDEX] >> 18 & FLAGS.INVERSE); - if (state && !(state[CHAR_DATA_CODE_INDEX] === 32 /*' '*/ && (state[CHAR_DATA_ATTR_INDEX] & 0x1ff) >= 256 && !wasInverted)) { - this._clearChar(x, y); - } - this._state.cache[x][y] = charData; + // const wasInverted = !!(state && state[CHAR_DATA_ATTR_INDEX] && state[CHAR_DATA_ATTR_INDEX] >> 18 & FLAGS.INVERSE); + // if (state && !(state[CHAR_DATA_CODE_INDEX] === 32 /*' '*/ && (state[CHAR_DATA_ATTR_INDEX] & 0x1ff) >= 256 && !wasInverted)) { + // this._clearChar(x, y); + // } + // this._state.cache[x][y] = charData; const flags = attr >> 18; let bg = attr & 0x1ff; @@ -120,14 +125,14 @@ export class TextRenderLayer extends BaseRenderLayer { // space is added. Without this, the first half of `b` would never // get removed, and `a` would not re-render because it thinks it's // already in the correct state. - this._state.cache[x][y] = OVERLAP_OWNED_CHAR_DATA; + // this._state.cache[x][y] = OVERLAP_OWNED_CHAR_DATA; if (x < line.length - 1 && line[x + 1][CHAR_DATA_CODE_INDEX] === 32 /*' '*/) { width = 2; - this._clearChar(x + 1, y); + // this._clearChar(x + 1, y); // The overlapping char's char data will force a clear and render when the // overlapping char is no longer to the left of the character and also when // the space changes to another character. - this._state.cache[x + 1][y] = OVERLAP_OWNED_CHAR_DATA; + // this._state.cache[x + 1][y] = OVERLAP_OWNED_CHAR_DATA; } } @@ -148,7 +153,7 @@ export class TextRenderLayer extends BaseRenderLayer { // Clear the cell next to this character if it's wide if (width === 2) { - this.clearCells(x + 1, y, 1, 1); + // this.clearCells(x + 1, y, 1, 1); } // Draw background From 1b2bac7aef2ba2d492b57aead9af238d32821dfa Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 9 Oct 2017 20:07:28 -0700 Subject: [PATCH 24/30] Add letterSpacing option This provides an option for fonts to be more spaced out. This is particularly useful for fonts that can seem crammed due to not allowing floating point numbers when drawing, such as Monoid or Operator Mono. --- src/Interfaces.ts | 1 + src/Terminal.ts | 2 + src/Viewport.ts | 4 +- src/renderer/BaseRenderLayer.ts | 90 +++++++++++++++++++++------------ src/renderer/Interfaces.ts | 6 ++- src/renderer/Renderer.ts | 21 +++++--- 6 files changed, 81 insertions(+), 43 deletions(-) diff --git a/src/Interfaces.ts b/src/Interfaces.ts index 2c77e8d3..0ab0af5a 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -139,6 +139,7 @@ export interface ITerminalOptions { fontFamily?: string; geometry?: [number, number]; handler?: (data: string) => void; + letterSpacing?: number; lineHeight?: number; rows?: number; screenKeys?: boolean; diff --git a/src/Terminal.ts b/src/Terminal.ts index 8486f265..5ea01092 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -80,6 +80,7 @@ const DEFAULT_OPTIONS: ITerminalOptions = { fontFamily: 'courier-new, courier, monospace', fontSize: 15, lineHeight: 1.0, + letterSpacing: 0, scrollback: 1000, screenKeys: false, debug: false, @@ -410,6 +411,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.renderer.clear(); this.charMeasure.measure(this.options); break; + case 'letterSpacing': case 'lineHeight': // When the font changes the size of the cells may change which requires a renderer clear this.renderer.clear(); diff --git a/src/Viewport.ts b/src/Viewport.ts index 064076a2..d20749c3 100644 --- a/src/Viewport.ts +++ b/src/Viewport.ts @@ -47,7 +47,7 @@ export class Viewport implements IViewport { */ private refresh(): void { if (this.charMeasure.height > 0) { - this.currentRowHeight = this.terminal.renderer.dimensions.scaledLineHeight / window.devicePixelRatio; + this.currentRowHeight = this.terminal.renderer.dimensions.scaledCellHeight / window.devicePixelRatio; if (this.lastRecordedViewportHeight !== this.terminal.renderer.dimensions.canvasHeight) { this.lastRecordedViewportHeight = this.terminal.renderer.dimensions.canvasHeight; @@ -75,7 +75,7 @@ export class Viewport implements IViewport { this.refresh(); } else { // If size has changed, refresh viewport - if (this.terminal.renderer.dimensions.scaledLineHeight / window.devicePixelRatio !== this.currentRowHeight) { + if (this.terminal.renderer.dimensions.scaledCellHeight / window.devicePixelRatio !== this.currentRowHeight) { this.refresh(); } } diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index d15f1ee7..6b264e4b 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -16,8 +16,10 @@ export abstract class BaseRenderLayer implements IRenderLayer { protected _ctx: CanvasRenderingContext2D; private _scaledCharWidth: number; private _scaledCharHeight: number; - private _scaledLineHeight: number; - private _scaledLineDrawY: number; + private _scaledCellWidth: number; + private _scaledCellHeight: number; + private _scaledCharLeft: number; + private _scaledCharTop: number; private _charAtlas: HTMLCanvasElement | ImageBitmap; @@ -70,10 +72,12 @@ export abstract class BaseRenderLayer implements IRenderLayer { } public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void { + this._scaledCellWidth = dim.scaledCellWidth; + this._scaledCellHeight = dim.scaledCellHeight; this._scaledCharWidth = dim.scaledCharWidth; this._scaledCharHeight = dim.scaledCharHeight; - this._scaledLineHeight = dim.scaledLineHeight; - this._scaledLineDrawY = dim.scaledLineDrawY; + this._scaledCharLeft = dim.scaledCharLeft; + this._scaledCharTop = dim.scaledCharTop; this._canvas.width = dim.scaledCanvasWidth; this._canvas.height = dim.scaledCanvasHeight; this._canvas.style.width = `${dim.canvasWidth}px`; @@ -100,10 +104,10 @@ export abstract class BaseRenderLayer implements IRenderLayer { */ protected fillCells(x: number, y: number, width: number, height: number): void { this._ctx.fillRect( - x * this._scaledCharWidth, - y * this._scaledLineHeight, - width * this._scaledCharWidth, - height * this._scaledLineHeight); + x * this._scaledCellWidth, + y * this._scaledCellHeight, + width * this._scaledCellWidth, + height * this._scaledCellHeight); } /** @@ -114,9 +118,9 @@ export abstract class BaseRenderLayer implements IRenderLayer { */ protected fillBottomLineAtCells(x: number, y: number, width: number = 1): void { this._ctx.fillRect( - x * this._scaledCharWidth, - (y + 1) * this._scaledLineHeight - window.devicePixelRatio - 1 /* Ensure it's drawn within the cell */, - width * this._scaledCharWidth, + x * this._scaledCellWidth, + (y + 1) * this._scaledCellHeight - window.devicePixelRatio - 1 /* Ensure it's drawn within the cell */, + width * this._scaledCellWidth, window.devicePixelRatio); } @@ -128,10 +132,10 @@ export abstract class BaseRenderLayer implements IRenderLayer { */ protected fillLeftLineAtCell(x: number, y: number): void { this._ctx.fillRect( - x * this._scaledCharWidth, - y * this._scaledLineHeight, + x * this._scaledCellWidth, + y * this._scaledCellHeight, window.devicePixelRatio, - this._scaledLineHeight); + this._scaledCellHeight); } /** @@ -143,10 +147,10 @@ export abstract class BaseRenderLayer implements IRenderLayer { protected strokeRectAtCell(x: number, y: number, width: number, height: number): void { this._ctx.lineWidth = window.devicePixelRatio; this._ctx.strokeRect( - x * this._scaledCharWidth + window.devicePixelRatio / 2, - y * this._scaledLineHeight + (window.devicePixelRatio / 2), - width * this._scaledCharWidth - window.devicePixelRatio, - (height * this._scaledLineHeight) - window.devicePixelRatio); + x * this._scaledCellWidth + window.devicePixelRatio / 2, + y * this._scaledCellHeight + (window.devicePixelRatio / 2), + width * this._scaledCellWidth - window.devicePixelRatio, + (height * this._scaledCellHeight) - window.devicePixelRatio); } /** @@ -171,17 +175,17 @@ export abstract class BaseRenderLayer implements IRenderLayer { protected clearCells(x: number, y: number, width: number, height: number): void { if (this._alpha) { this._ctx.clearRect( - x * this._scaledCharWidth, - y * this._scaledLineHeight, - width * this._scaledCharWidth, - height * this._scaledLineHeight); + x * this._scaledCellWidth, + y * this._scaledCellHeight, + width * this._scaledCellWidth, + height * this._scaledCellHeight); } else { this._ctx.fillStyle = this._colors.background; this._ctx.fillRect( - x * this._scaledCharWidth, - y * this._scaledLineHeight, - width * this._scaledCharWidth, - height * this._scaledLineHeight); + x * this._scaledCellWidth, + y * this._scaledCellHeight, + width * this._scaledCellWidth, + height * this._scaledCellHeight); } } @@ -204,9 +208,17 @@ export abstract class BaseRenderLayer implements IRenderLayer { // can bleed into other cells. This code will clip the following fillText, // ensuring that its contents don't go beyond the cell bounds. this._ctx.beginPath(); - this._ctx.rect(x * this._scaledCharWidth, y * this._scaledLineHeight + this._scaledLineDrawY, charData[CHAR_DATA_WIDTH_INDEX] * this._scaledCharWidth, this._scaledCharHeight); + // TODO: Make clip rect use cell size? + this._ctx.rect( + x * this._scaledCellWidth + this._scaledCharLeft, + y * this._scaledCellHeight + this._scaledCharTop, + charData[CHAR_DATA_WIDTH_INDEX] * this._scaledCharWidth, + this._scaledCharHeight); this._ctx.clip(); - this._ctx.fillText(charData[CHAR_DATA_CHAR_INDEX], x * this._scaledCharWidth, y * this._scaledCharHeight); + this._ctx.fillText( + charData[CHAR_DATA_CHAR_INDEX], + x * this._scaledCellWidth + this._scaledCharLeft, + y * this._scaledCellHeight + this._scaledCharTop); } /** @@ -242,8 +254,14 @@ export abstract class BaseRenderLayer implements IRenderLayer { const charAtlasCellWidth = this._scaledCharWidth + CHAR_ATLAS_CELL_SPACING; const charAtlasCellHeight = this._scaledCharHeight + CHAR_ATLAS_CELL_SPACING; this._ctx.drawImage(this._charAtlas, - code * charAtlasCellWidth, colorIndex * charAtlasCellHeight, charAtlasCellWidth, this._scaledCharHeight, - x * this._scaledCharWidth, y * this._scaledLineHeight + this._scaledLineDrawY, this._scaledCharWidth, this._scaledCharHeight); + code * charAtlasCellWidth, + colorIndex * charAtlasCellHeight, + charAtlasCellWidth, + this._scaledCharHeight, + x * this._scaledCellWidth + this._scaledCharLeft, + y * this._scaledCellHeight + this._scaledCharTop, + this._scaledCharWidth, + this._scaledCharHeight); } else { this._drawUncachedChar(terminal, char, width, fg, x, y, bold); } @@ -285,11 +303,19 @@ export abstract class BaseRenderLayer implements IRenderLayer { // can bleed into other cells. This code will clip the following fillText, // ensuring that its contents don't go beyond the cell bounds. this._ctx.beginPath(); - this._ctx.rect(0, y * this._scaledLineHeight + this._scaledLineDrawY, terminal.cols * this._scaledCharWidth, this._scaledCharHeight); + // TODO: Why is this be clipped at char top? + this._ctx.rect( + 0, + y * this._scaledCellHeight + this._scaledCharTop, + terminal.cols * this._scaledCharWidth, + this._scaledCharHeight); this._ctx.clip(); // Draw the character - this._ctx.fillText(char, x * this._scaledCharWidth, y * this._scaledLineHeight + this._scaledLineDrawY); + this._ctx.fillText( + char, + x * this._scaledCellWidth + this._scaledCharLeft, + y * this._scaledCellHeight + this._scaledCharTop); this._ctx.restore(); } } diff --git a/src/renderer/Interfaces.ts b/src/renderer/Interfaces.ts index 74400b8c..be1b39dd 100644 --- a/src/renderer/Interfaces.ts +++ b/src/renderer/Interfaces.ts @@ -86,8 +86,10 @@ export interface IColorSet { export interface IRenderDimensions { scaledCharWidth: number; scaledCharHeight: number; - scaledLineHeight: number; - scaledLineDrawY: number; + scaledCellWidth: number; + scaledCellHeight: number; + scaledCharLeft: number; + scaledCharTop: number; scaledCanvasWidth: number; scaledCanvasHeight: number; canvasWidth: number; diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index 168d6dc8..52cad50e 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -40,8 +40,8 @@ export class Renderer extends EventEmitter implements IRenderer { this.dimensions = { scaledCharWidth: null, scaledCharHeight: null, - scaledLineHeight: null, - scaledLineDrawY: null, + scaledCellHeight: null, + scaledCharTop: null, scaledCanvasWidth: null, scaledCanvasHeight: null, canvasWidth: null, @@ -91,20 +91,27 @@ export class Renderer extends EventEmitter implements IRenderer { // enough space to draw the character to the cell. this.dimensions.scaledCharHeight = Math.ceil(this._terminal.charMeasure.height * window.devicePixelRatio); - // Calculate the scaled line height, if lineHeight is not 1 then the value + // Calculate the scaled cell height, if lineHeight is not 1 then the value // will be floored because since lineHeight can never be lower then 1, there // is a guarentee that the scaled line height will always be larger than // scaled char height. - this.dimensions.scaledLineHeight = Math.floor(this.dimensions.scaledCharHeight * this._terminal.options.lineHeight); + this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._terminal.options.lineHeight); // Calculate the y coordinate within a cell that text should draw from in // order to draw in the center of a cell. - this.dimensions.scaledLineDrawY = this._terminal.options.lineHeight === 1 ? 0 : Math.round((this.dimensions.scaledLineHeight - this.dimensions.scaledCharHeight) / 2); + this.dimensions.scaledCharTop = this._terminal.options.lineHeight === 1 ? 0 : Math.round((this.dimensions.scaledCellHeight - this.dimensions.scaledCharHeight) / 2); + + // Calculate the scaled cell width, taking the letterSpacing into account. + this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._terminal.options.letterSpacing); + + // Calculate the x coordinate with a cell that text should draw from in + // order to draw in the center of a cell. + this.dimensions.scaledCharLeft = Math.floor(this._terminal.options.letterSpacing / 2); // Recalculate the canvas dimensions; scaled* define the actual number of // pixel in the canvas - this.dimensions.scaledCanvasHeight = this._terminal.rows * this.dimensions.scaledLineHeight; - this.dimensions.scaledCanvasWidth = this._terminal.cols * this.dimensions.scaledCharWidth; + this.dimensions.scaledCanvasHeight = this._terminal.rows * this.dimensions.scaledCellHeight; + this.dimensions.scaledCanvasWidth = this._terminal.cols * this.dimensions.scaledCellWidth; // The the size of the canvas on the page. It's very important that this // rounds to nearest integer and not ceils as browsers often set From eb57cc3e9eb69e73572e6ce0fa302664e9861344 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 9 Oct 2017 20:10:13 -0700 Subject: [PATCH 25/30] Clip rows using the row's cell height, not char height --- src/renderer/BaseRenderLayer.ts | 42 +++++++++++++-------------------- src/renderer/Renderer.ts | 2 ++ 2 files changed, 19 insertions(+), 25 deletions(-) diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 6b264e4b..b62c3a6b 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -202,19 +202,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { protected fillCharTrueColor(terminal: ITerminal, charData: CharData, x: number, y: number): void { this._ctx.font = `${terminal.options.fontSize * window.devicePixelRatio}px ${terminal.options.fontFamily}`; this._ctx.textBaseline = 'top'; - - // Since uncached characters are not coming off the char atlas with source - // coordinates, it means that text drawn to the canvas (particularly '_') - // can bleed into other cells. This code will clip the following fillText, - // ensuring that its contents don't go beyond the cell bounds. - this._ctx.beginPath(); - // TODO: Make clip rect use cell size? - this._ctx.rect( - x * this._scaledCellWidth + this._scaledCharLeft, - y * this._scaledCellHeight + this._scaledCharTop, - charData[CHAR_DATA_WIDTH_INDEX] * this._scaledCharWidth, - this._scaledCharHeight); - this._ctx.clip(); + this._clipRow(terminal, y); this._ctx.fillText( charData[CHAR_DATA_CHAR_INDEX], x * this._scaledCellWidth + this._scaledCharLeft, @@ -298,18 +286,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._ctx.fillStyle = this._colors.foreground; } - // Since uncached characters are not coming off the char atlas with source - // coordinates, it means that text drawn to the canvas (particularly '_') - // can bleed into other cells. This code will clip the following fillText, - // ensuring that its contents don't go beyond the cell bounds. - this._ctx.beginPath(); - // TODO: Why is this be clipped at char top? - this._ctx.rect( - 0, - y * this._scaledCellHeight + this._scaledCharTop, - terminal.cols * this._scaledCharWidth, - this._scaledCharHeight); - this._ctx.clip(); + this._clipRow(terminal, y); // Draw the character this._ctx.fillText( @@ -318,5 +295,20 @@ export abstract class BaseRenderLayer implements IRenderLayer { y * this._scaledCellHeight + this._scaledCharTop); this._ctx.restore(); } + + /** + * Clips a row to ensure no pixels will be drawn outside the cells in the row. + * @param terminal The terminal. + * @param y The row to clip. + */ + private _clipRow(terminal: ITerminal, y: number): void { + this._ctx.beginPath(); + this._ctx.rect( + 0, + y * this._scaledCellHeight, + terminal.cols * this._scaledCellWidth, + this._scaledCellHeight); + this._ctx.clip(); + } } diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index 52cad50e..ea373e5e 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -40,7 +40,9 @@ export class Renderer extends EventEmitter implements IRenderer { this.dimensions = { scaledCharWidth: null, scaledCharHeight: null, + scaledCellWidth: null, scaledCellHeight: null, + scaledCharLeft: null, scaledCharTop: null, scaledCanvasWidth: null, scaledCanvasHeight: null, From 09c9cb4508bc13a8b5960e06b5eb34f2c93e66f3 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 10 Oct 2017 22:19:48 -0700 Subject: [PATCH 26/30] Draw from charatlas without changing scale Fixes #1050 --- src/renderer/BaseRenderLayer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index d15f1ee7..a167b3b1 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -243,7 +243,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { const charAtlasCellHeight = this._scaledCharHeight + CHAR_ATLAS_CELL_SPACING; this._ctx.drawImage(this._charAtlas, code * charAtlasCellWidth, colorIndex * charAtlasCellHeight, charAtlasCellWidth, this._scaledCharHeight, - x * this._scaledCharWidth, y * this._scaledLineHeight + this._scaledLineDrawY, this._scaledCharWidth, this._scaledCharHeight); + x * this._scaledCharWidth, y * this._scaledLineHeight + this._scaledLineDrawY, charAtlasCellWidth, this._scaledCharHeight); } else { this._drawUncachedChar(terminal, char, width, fg, x, y, bold); } From a56edd65a7d02340fe2c225639c5793ab156896d Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 10 Oct 2017 22:37:13 -0700 Subject: [PATCH 27/30] Fix ansi bright text being bold without bold flag Fixes #1047 --- src/renderer/BaseRenderLayer.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index d15f1ee7..614286c8 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -234,7 +234,10 @@ export abstract class BaseRenderLayer implements IRenderLayer { } } const isAscii = code < 256; - const isBasicColor = (colorIndex > 1 && fg < 16); + // A color is basic if it is one of the standard normal or bold weight + // colors of the characters held in the char atlas. Note that this excludes + // the normal weight light color characters + const isBasicColor = (colorIndex > 1 && fg < 16) && (fg < 8 || bold); const isDefaultColor = fg >= 256; const isDefaultBackground = bg >= 256; if (this._charAtlas && isAscii && (isBasicColor || isDefaultColor) && isDefaultBackground) { From b6c886837f7fddd75de2a0b320ec448c34193810 Mon Sep 17 00:00:00 2001 From: Thomas Zilz Date: Wed, 11 Oct 2017 20:24:26 +0200 Subject: [PATCH 28/30] Add support for dimmed characters (faint) (#1043) * Add support for dimmed character style --- src/InputHandler.ts | 28 +++++++++++++++++----------- src/renderer/BaseRenderLayer.ts | 15 ++++++++++++--- src/renderer/TextRenderLayer.ts | 2 +- src/renderer/Types.ts | 3 ++- 4 files changed, 32 insertions(+), 16 deletions(-) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 000e58ea..c953a250 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -9,6 +9,7 @@ import { C0 } from './EscapeSequences'; import { DEFAULT_CHARSET } from './Charsets'; import { CharData } from './Types'; import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX } from './Buffer'; +import { FLAGS } from './renderer/Types'; /** * The terminal's standard implementation of IInputHandler, this handles all @@ -1107,6 +1108,7 @@ export class InputHandler implements IInputHandler { * CSI Pm m Character Attributes (SGR). * Ps = 0 -> Normal (default). * Ps = 1 -> Bold. + * Ps = 2 -> Faint, decreased intensity (ISO 6429). * Ps = 4 -> Underlined. * Ps = 5 -> Blink (appears as Bold). * Ps = 7 -> Inverse. @@ -1206,35 +1208,39 @@ export class InputHandler implements IInputHandler { // bg = 0x1ff; } else if (p === 1) { // bold text - flags |= 1; + flags |= FLAGS.BOLD; } else if (p === 4) { // underlined text - flags |= 2; + flags |= FLAGS.UNDERLINE; } else if (p === 5) { // blink - flags |= 4; + flags |= FLAGS.BLINK; } else if (p === 7) { // inverse and positive // test with: echo -e '\e[31m\e[42mhello\e[7mworld\e[27mhi\e[m' - flags |= 8; + flags |= FLAGS.INVERSE; } else if (p === 8) { // invisible - flags |= 16; + flags |= FLAGS.INVISIBLE; + } else if (p === 2) { + // dimmed text + flags |= FLAGS.DIM; } else if (p === 22) { - // not bold - flags &= ~1; + // not bold nor faint + flags &= ~FLAGS.BOLD; + flags &= ~FLAGS.DIM; } else if (p === 24) { // not underlined - flags &= ~2; + flags &= ~FLAGS.UNDERLINE; } else if (p === 25) { // not blink - flags &= ~4; + flags &= ~FLAGS.BLINK; } else if (p === 27) { // not inverse - flags &= ~8; + flags &= ~FLAGS.INVERSE; } else if (p === 28) { // not invisible - flags &= ~16; + flags &= ~FLAGS.INVISIBLE; } else if (p === 39) { // reset fg fg = (this._terminal.defAttr >> 9) & 0x1ff; diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index a167b3b1..67c704d0 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -10,6 +10,7 @@ import { CharData } from '../Types'; import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer'; export const INVERTED_DEFAULT_COLOR = -1; +const DIM_OPACITY = 0.5; export abstract class BaseRenderLayer implements IRenderLayer { private _canvas: HTMLCanvasElement; @@ -223,7 +224,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { * This is used to validate whether a cached image can be used. * @param bold Whether the text is bold. */ - protected drawChar(terminal: ITerminal, char: string, code: number, width: number, x: number, y: number, fg: number, bg: number, bold: boolean): void { + protected drawChar(terminal: ITerminal, char: string, code: number, width: number, x: number, y: number, fg: number, bg: number, bold: boolean, dim: boolean): void { let colorIndex = 0; if (fg < 256) { colorIndex = fg + 2; @@ -241,11 +242,15 @@ export abstract class BaseRenderLayer implements IRenderLayer { // ImageBitmap's draw about twice as fast as from a canvas const charAtlasCellWidth = this._scaledCharWidth + CHAR_ATLAS_CELL_SPACING; const charAtlasCellHeight = this._scaledCharHeight + CHAR_ATLAS_CELL_SPACING; + // Apply alpha to dim the character + if (dim) { + this._ctx.globalAlpha = DIM_OPACITY; + } this._ctx.drawImage(this._charAtlas, code * charAtlasCellWidth, colorIndex * charAtlasCellHeight, charAtlasCellWidth, this._scaledCharHeight, x * this._scaledCharWidth, y * this._scaledLineHeight + this._scaledLineDrawY, charAtlasCellWidth, this._scaledCharHeight); } else { - this._drawUncachedChar(terminal, char, width, fg, x, y, bold); + this._drawUncachedChar(terminal, char, width, fg, x, y, bold, dim); } // This draws the atlas (for debugging purposes) // this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); @@ -263,7 +268,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { * @param x The column to draw at. * @param y The row to draw at. */ - private _drawUncachedChar(terminal: ITerminal, char: string, width: number, fg: number, x: number, y: number, bold: boolean): void { + private _drawUncachedChar(terminal: ITerminal, char: string, width: number, fg: number, x: number, y: number, bold: boolean, dim: boolean): void { this._ctx.save(); this._ctx.font = `${terminal.options.fontSize * window.devicePixelRatio}px ${terminal.options.fontFamily}`; if (bold) { @@ -288,6 +293,10 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._ctx.rect(0, y * this._scaledLineHeight + this._scaledLineDrawY, terminal.cols * this._scaledCharWidth, this._scaledCharHeight); this._ctx.clip(); + // Apply alpha to dim the character + if (dim) { + this._ctx.globalAlpha = DIM_OPACITY; + } // Draw the character this._ctx.fillText(char, x * this._scaledCharWidth, y * this._scaledLineHeight + this._scaledLineDrawY); this._ctx.restore(); diff --git a/src/renderer/TextRenderLayer.ts b/src/renderer/TextRenderLayer.ts index 996e1728..2ecb6ec6 100644 --- a/src/renderer/TextRenderLayer.ts +++ b/src/renderer/TextRenderLayer.ts @@ -185,7 +185,7 @@ export class TextRenderLayer extends BaseRenderLayer { this.fillBottomLineAtCells(x, y); } - this.drawChar(terminal, char, code, width, x, y, fg, bg, !!(flags & FLAGS.BOLD)); + this.drawChar(terminal, char, code, width, x, y, fg, bg, !!(flags & FLAGS.BOLD), !!(flags & FLAGS.DIM)); this._ctx.restore(); } diff --git a/src/renderer/Types.ts b/src/renderer/Types.ts index b1a930f9..31705a3f 100644 --- a/src/renderer/Types.ts +++ b/src/renderer/Types.ts @@ -11,5 +11,6 @@ export enum FLAGS { UNDERLINE = 2, BLINK = 4, INVERSE = 8, - INVISIBLE = 16 + INVISIBLE = 16, + DIM = 32 }; From c1ad9dba2a6a70e176cb7cb54daf0e05cff3453a Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 11 Oct 2017 22:46:08 -0700 Subject: [PATCH 29/30] Remove long wcwidth test Fixes #1057 --- src/InputHandler.test.ts | 139 --------------------------------------- 1 file changed, 139 deletions(-) diff --git a/src/InputHandler.test.ts b/src/InputHandler.test.ts index a8b2ff12..caf74624 100644 --- a/src/InputHandler.test.ts +++ b/src/InputHandler.test.ts @@ -67,142 +67,3 @@ describe('InputHandler', () => { }); }); }); - -const old_wcwidth = (function(opts: {nul: number, control: number}): (ucs: number) => number { - // extracted from https://www.cl.cam.ac.uk/%7Emgk25/ucs/wcwidth.c - // combining characters - const COMBINING = [ - [0x0300, 0x036F], [0x0483, 0x0486], [0x0488, 0x0489], - [0x0591, 0x05BD], [0x05BF, 0x05BF], [0x05C1, 0x05C2], - [0x05C4, 0x05C5], [0x05C7, 0x05C7], [0x0600, 0x0603], - [0x0610, 0x0615], [0x064B, 0x065E], [0x0670, 0x0670], - [0x06D6, 0x06E4], [0x06E7, 0x06E8], [0x06EA, 0x06ED], - [0x070F, 0x070F], [0x0711, 0x0711], [0x0730, 0x074A], - [0x07A6, 0x07B0], [0x07EB, 0x07F3], [0x0901, 0x0902], - [0x093C, 0x093C], [0x0941, 0x0948], [0x094D, 0x094D], - [0x0951, 0x0954], [0x0962, 0x0963], [0x0981, 0x0981], - [0x09BC, 0x09BC], [0x09C1, 0x09C4], [0x09CD, 0x09CD], - [0x09E2, 0x09E3], [0x0A01, 0x0A02], [0x0A3C, 0x0A3C], - [0x0A41, 0x0A42], [0x0A47, 0x0A48], [0x0A4B, 0x0A4D], - [0x0A70, 0x0A71], [0x0A81, 0x0A82], [0x0ABC, 0x0ABC], - [0x0AC1, 0x0AC5], [0x0AC7, 0x0AC8], [0x0ACD, 0x0ACD], - [0x0AE2, 0x0AE3], [0x0B01, 0x0B01], [0x0B3C, 0x0B3C], - [0x0B3F, 0x0B3F], [0x0B41, 0x0B43], [0x0B4D, 0x0B4D], - [0x0B56, 0x0B56], [0x0B82, 0x0B82], [0x0BC0, 0x0BC0], - [0x0BCD, 0x0BCD], [0x0C3E, 0x0C40], [0x0C46, 0x0C48], - [0x0C4A, 0x0C4D], [0x0C55, 0x0C56], [0x0CBC, 0x0CBC], - [0x0CBF, 0x0CBF], [0x0CC6, 0x0CC6], [0x0CCC, 0x0CCD], - [0x0CE2, 0x0CE3], [0x0D41, 0x0D43], [0x0D4D, 0x0D4D], - [0x0DCA, 0x0DCA], [0x0DD2, 0x0DD4], [0x0DD6, 0x0DD6], - [0x0E31, 0x0E31], [0x0E34, 0x0E3A], [0x0E47, 0x0E4E], - [0x0EB1, 0x0EB1], [0x0EB4, 0x0EB9], [0x0EBB, 0x0EBC], - [0x0EC8, 0x0ECD], [0x0F18, 0x0F19], [0x0F35, 0x0F35], - [0x0F37, 0x0F37], [0x0F39, 0x0F39], [0x0F71, 0x0F7E], - [0x0F80, 0x0F84], [0x0F86, 0x0F87], [0x0F90, 0x0F97], - [0x0F99, 0x0FBC], [0x0FC6, 0x0FC6], [0x102D, 0x1030], - [0x1032, 0x1032], [0x1036, 0x1037], [0x1039, 0x1039], - [0x1058, 0x1059], [0x1160, 0x11FF], [0x135F, 0x135F], - [0x1712, 0x1714], [0x1732, 0x1734], [0x1752, 0x1753], - [0x1772, 0x1773], [0x17B4, 0x17B5], [0x17B7, 0x17BD], - [0x17C6, 0x17C6], [0x17C9, 0x17D3], [0x17DD, 0x17DD], - [0x180B, 0x180D], [0x18A9, 0x18A9], [0x1920, 0x1922], - [0x1927, 0x1928], [0x1932, 0x1932], [0x1939, 0x193B], - [0x1A17, 0x1A18], [0x1B00, 0x1B03], [0x1B34, 0x1B34], - [0x1B36, 0x1B3A], [0x1B3C, 0x1B3C], [0x1B42, 0x1B42], - [0x1B6B, 0x1B73], [0x1DC0, 0x1DCA], [0x1DFE, 0x1DFF], - [0x200B, 0x200F], [0x202A, 0x202E], [0x2060, 0x2063], - [0x206A, 0x206F], [0x20D0, 0x20EF], [0x302A, 0x302F], - [0x3099, 0x309A], [0xA806, 0xA806], [0xA80B, 0xA80B], - [0xA825, 0xA826], [0xFB1E, 0xFB1E], [0xFE00, 0xFE0F], - [0xFE20, 0xFE23], [0xFEFF, 0xFEFF], [0xFFF9, 0xFFFB], - [0x10A01, 0x10A03], [0x10A05, 0x10A06], [0x10A0C, 0x10A0F], - [0x10A38, 0x10A3A], [0x10A3F, 0x10A3F], [0x1D167, 0x1D169], - [0x1D173, 0x1D182], [0x1D185, 0x1D18B], [0x1D1AA, 0x1D1AD], - [0x1D242, 0x1D244], [0xE0001, 0xE0001], [0xE0020, 0xE007F], - [0xE0100, 0xE01EF] - ]; - // binary search - function bisearch(ucs: number): boolean { - let min = 0; - let max = COMBINING.length - 1; - let mid; - if (ucs < COMBINING[0][0] || ucs > COMBINING[max][1]) - return false; - while (max >= min) { - mid = Math.floor((min + max) / 2); - if (ucs > COMBINING[mid][1]) - min = mid + 1; - else if (ucs < COMBINING[mid][0]) - max = mid - 1; - else - return true; - } - return false; - } - function wcwidth(ucs: number): number { - // test for 8-bit control characters - if (ucs === 0) { - return opts.nul; - } - if (ucs < 32 || (ucs >= 0x7f && ucs < 0xa0)) { - return opts.control; - } - // binary search in table of non-spacing characters - if (bisearch(ucs)) { - return 0; - } - // if we arrive here, ucs is not a combining or C0/C1 control character - if (isWide(ucs)) { - return 2; - } - return 1; - } - function isWide(ucs: number): boolean { - return ( - ucs >= 0x1100 && ( - ucs <= 0x115f || // Hangul Jamo init. consonants - ucs === 0x2329 || - ucs === 0x232a || - (ucs >= 0x2e80 && ucs <= 0xa4cf && ucs !== 0x303f) || // CJK..Yi - (ucs >= 0xac00 && ucs <= 0xd7a3) || // Hangul Syllables - (ucs >= 0xf900 && ucs <= 0xfaff) || // CJK Compat Ideographs - (ucs >= 0xfe10 && ucs <= 0xfe19) || // Vertical forms - (ucs >= 0xfe30 && ucs <= 0xfe6f) || // CJK Compat Forms - (ucs >= 0xff00 && ucs <= 0xff60) || // Fullwidth Forms - (ucs >= 0xffe0 && ucs <= 0xffe6) || - (ucs >= 0x20000 && ucs <= 0x2fffd) || - (ucs >= 0x30000 && ucs <= 0x3fffd))); - } - return wcwidth; -})({nul: 0, control: 0}); // configurable options - -describe('wcwidth', () => { - it('same as old implementation for BMP and individual higher', (done) => { - for (let i = 0; i < 65536; ++i) - assert.equal(wcwidth(i), old_wcwidth(i)); - // test some individual higher to fullfill branching - assert.equal(wcwidth(0x10A01), old_wcwidth(0x10A01)); - assert.equal(wcwidth(0x30000), old_wcwidth(0x30000)); - assert.equal(wcwidth(0x3fffe), old_wcwidth(0x3fffe)); - done(); - }).timeout(3000); - /* - it('new is at least 5 times faster', () => { - let start_new = new Date().getTime(); - let x = 0; - for (let runs = 0; runs < 1; ++runs) - for (let i = 0; i < 65536; ++i) - x = wcwidth(i); - let end_new = new Date().getTime(); - let start_old = new Date().getTime(); - let y = 0; - for (let runs = 0; runs < 1; ++runs) - for (let i = 0; i < 65536; ++i) - y = old_wcwidth(i); - let end_old = new Date().getTime(); - // console.log((end_new - start_new)); - // console.log((end_old - start_old)); - assert.equal(((end_new - start_new) * 5 < (end_old - start_old)), true); - }); - */ -}); From db6695435d2034bcd869b31619d9d2bf6075c323 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 13 Oct 2017 13:38:28 -0700 Subject: [PATCH 30/30] Add letterSpacing typings and test --- fixtures/typings-test/typings-test.ts | 2 ++ typings/xterm.d.ts | 9 +++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/fixtures/typings-test/typings-test.ts b/fixtures/typings-test/typings-test.ts index 928ca238..bc8f1bd6 100644 --- a/fixtures/typings-test/typings-test.ts +++ b/fixtures/typings-test/typings-test.ts @@ -140,6 +140,7 @@ namespace methods_core { const r18: (data: string) => void = t.getOption('handler'); const r19: string = t.getOption('bellSound'); const r20: string = t.getOption('bellStyle'); + const r22: number = t.getOption('letterSpacing'); } { const t: Terminal = new Terminal(); @@ -157,6 +158,7 @@ namespace methods_core { t.setOption('useFlowControl', true); t.setOption('visualBell', true); t.setOption('colors', ['a', 'b']); + t.setOption('letterSpacing', 1); t.setOption('cols', 1); t.setOption('rows', 1); t.setOption('tabStopWidth', 1); diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index de2a261b..36176f8b 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -51,6 +51,11 @@ interface ITerminalOptions { */ fontFamily?: string; + /** + * The spacing in whole pixels between characters.. + */ + letterSpacing?: number; + /** * The line height used to render text. */ @@ -407,7 +412,7 @@ declare module 'xterm' { * Retrieves an option's value from the terminal. * @param key The option key. */ - getOption(key: 'cols' | 'fontSize' | 'lineHeight' | 'rows' | 'tabStopWidth' | 'scrollback'): number; + getOption(key: 'cols' | 'fontSize' | 'letterSpacing' | 'lineHeight' | 'rows' | 'tabStopWidth' | 'scrollback'): number; /** * Retrieves an option's value from the terminal. * @param key The option key. @@ -459,7 +464,7 @@ declare module 'xterm' { * @param key The option key. * @param value The option value. */ - setOption(key: 'cols' | 'fontSize' | 'lineHeight' | 'rows' | 'tabStopWidth' | 'scrollback', value: number): void; + setOption(key: 'cols' | 'fontSize' | 'letterSpacing' | 'lineHeight' | 'rows' | 'tabStopWidth' | 'scrollback', value: number): void; /** * Sets an option on the terminal. * @param key The option key.