From c630b92772fcc9f4a0b544be12cb33b1a9e4a926 Mon Sep 17 00:00:00 2001 From: Nick Pezza Date: Sat, 30 Nov 2019 14:37:07 -0500 Subject: [PATCH 01/48] Allow the thickness of the bar cursor to be configured --- addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts | 4 ++-- .../xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts | 2 +- src/browser/renderer/BaseRenderLayer.ts | 4 ++-- src/browser/renderer/CursorRenderLayer.ts | 2 +- src/common/services/OptionsService.ts | 1 + src/common/services/Services.ts | 2 ++ typings/xterm.d.ts | 5 +++++ 7 files changed, 14 insertions(+), 6 deletions(-) diff --git a/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts b/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts index 29487ad0..2b181847 100644 --- a/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts +++ b/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts @@ -153,11 +153,11 @@ export abstract class BaseRenderLayer implements IRenderLayer { * @param x The column to fill. * @param y The row to fill. */ - protected _fillLeftLineAtCell(x: number, y: number): void { + protected _fillLeftLineAtCell(x: number, y: number, width: number = 1): void { this._ctx.fillRect( x * this._scaledCellWidth, y * this._scaledCellHeight, - window.devicePixelRatio, + window.devicePixelRatio * width, this._scaledCellHeight); } diff --git a/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts b/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts index 413f4fdd..29c380fb 100644 --- a/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts +++ b/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts @@ -204,7 +204,7 @@ export class CursorRenderLayer extends BaseRenderLayer { private _renderBarCursor(terminal: Terminal, x: number, y: number, cell: ICellData): void { this._ctx.save(); this._ctx.fillStyle = this._colors.cursor.css; - this._fillLeftLineAtCell(x, y); + this._fillLeftLineAtCell(x, y, terminal.getOption('cursorBarWidth')); this._ctx.restore(); } diff --git a/src/browser/renderer/BaseRenderLayer.ts b/src/browser/renderer/BaseRenderLayer.ts index f55a130d..2dfe5bd3 100644 --- a/src/browser/renderer/BaseRenderLayer.ts +++ b/src/browser/renderer/BaseRenderLayer.ts @@ -171,11 +171,11 @@ export abstract class BaseRenderLayer implements IRenderLayer { * @param x The column to fill. * @param y The row to fill. */ - protected _fillLeftLineAtCell(x: number, y: number): void { + protected _fillLeftLineAtCell(x: number, y: number, width: number = 1): void { this._ctx.fillRect( x * this._scaledCellWidth, y * this._scaledCellHeight, - window.devicePixelRatio, + window.devicePixelRatio * width, this._scaledCellHeight); } diff --git a/src/browser/renderer/CursorRenderLayer.ts b/src/browser/renderer/CursorRenderLayer.ts index 0ca1b97a..5927d8df 100644 --- a/src/browser/renderer/CursorRenderLayer.ts +++ b/src/browser/renderer/CursorRenderLayer.ts @@ -209,7 +209,7 @@ export class CursorRenderLayer extends BaseRenderLayer { private _renderBarCursor(x: number, y: number, cell: ICellData): void { this._ctx.save(); this._ctx.fillStyle = this._colors.cursor.css; - this._fillLeftLineAtCell(x, y); + this._fillLeftLineAtCell(x, y, this._optionsService.options.cursorBarWidth); this._ctx.restore(); } diff --git a/src/common/services/OptionsService.ts b/src/common/services/OptionsService.ts index 9eaad94f..987dde67 100644 --- a/src/common/services/OptionsService.ts +++ b/src/common/services/OptionsService.ts @@ -20,6 +20,7 @@ export const DEFAULT_OPTIONS: ITerminalOptions = Object.freeze({ rows: 24, cursorBlink: false, cursorStyle: 'block', + cursorBarWidth: 1, bellSound: DEFAULT_BELL_SOUND, bellStyle: 'none', drawBoldTextInBrightColors: true, diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index b90db299..4f90c0e9 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -191,6 +191,7 @@ export interface IPartialTerminalOptions { cols?: number; cursorBlink?: boolean; cursorStyle?: 'block' | 'underline' | 'bar'; + cursorBarWidth?: number; disableStdin?: boolean; drawBoldTextInBrightColors?: boolean; fastScrollModifier?: 'alt' | 'ctrl' | 'shift'; @@ -223,6 +224,7 @@ export interface ITerminalOptions { cols: number; cursorBlink: boolean; cursorStyle: 'block' | 'underline' | 'bar'; + cursorBarWidth: number; disableStdin: boolean; drawBoldTextInBrightColors: boolean; fastScrollModifier: 'alt' | 'ctrl' | 'shift' | undefined; diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 401f7abc..a0e91c36 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -72,6 +72,11 @@ declare module 'xterm' { */ cursorStyle?: 'block' | 'underline' | 'bar'; + /** + * The width of the bar cursor. + */ + cursorBarWidth?: number; + /** * Whether input should be disabled. */ From 58078459f874fd535a6fc59c711c66a6f8242f23 Mon Sep 17 00:00:00 2001 From: Nick Pezza Date: Sat, 7 Dec 2019 15:38:12 -0500 Subject: [PATCH 02/48] Update cursor width doc Co-Authored-By: Daniel Imms --- typings/xterm.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index a0e91c36..c492330c 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -73,7 +73,7 @@ declare module 'xterm' { cursorStyle?: 'block' | 'underline' | 'bar'; /** - * The width of the bar cursor. + * The width of the cursor in CSS pixels when `cursorStyle` is set to 'bar'. */ cursorBarWidth?: number; From 48aebec01fee1c5cc10368aa3f4d2629506dc56d Mon Sep 17 00:00:00 2001 From: Nick Pezza Date: Sat, 7 Dec 2019 15:39:34 -0500 Subject: [PATCH 03/48] Remove default width value --- addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts | 2 +- src/browser/renderer/BaseRenderLayer.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts b/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts index 2b181847..b897ed44 100644 --- a/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts +++ b/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts @@ -153,7 +153,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { * @param x The column to fill. * @param y The row to fill. */ - protected _fillLeftLineAtCell(x: number, y: number, width: number = 1): void { + protected _fillLeftLineAtCell(x: number, y: number, width: number): void { this._ctx.fillRect( x * this._scaledCellWidth, y * this._scaledCellHeight, diff --git a/src/browser/renderer/BaseRenderLayer.ts b/src/browser/renderer/BaseRenderLayer.ts index 2dfe5bd3..4ea8bd52 100644 --- a/src/browser/renderer/BaseRenderLayer.ts +++ b/src/browser/renderer/BaseRenderLayer.ts @@ -171,7 +171,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { * @param x The column to fill. * @param y The row to fill. */ - protected _fillLeftLineAtCell(x: number, y: number, width: number = 1): void { + protected _fillLeftLineAtCell(x: number, y: number, width: number): void { this._ctx.fillRect( x * this._scaledCellWidth, y * this._scaledCellHeight, From 97dc61e65f6acd496803929847d7809040c80f54 Mon Sep 17 00:00:00 2001 From: Nick Pezza Date: Sat, 7 Dec 2019 15:40:02 -0500 Subject: [PATCH 04/48] s/cursorBarWidth/cursorWidth --- addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts | 2 +- src/browser/renderer/CursorRenderLayer.ts | 2 +- src/common/services/OptionsService.ts | 2 +- src/common/services/Services.ts | 4 ++-- typings/xterm.d.ts | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts b/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts index 29c380fb..1072a171 100644 --- a/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts +++ b/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts @@ -204,7 +204,7 @@ export class CursorRenderLayer extends BaseRenderLayer { private _renderBarCursor(terminal: Terminal, x: number, y: number, cell: ICellData): void { this._ctx.save(); this._ctx.fillStyle = this._colors.cursor.css; - this._fillLeftLineAtCell(x, y, terminal.getOption('cursorBarWidth')); + this._fillLeftLineAtCell(x, y, terminal.getOption('cursorWidth')); this._ctx.restore(); } diff --git a/src/browser/renderer/CursorRenderLayer.ts b/src/browser/renderer/CursorRenderLayer.ts index 5927d8df..64ecf33f 100644 --- a/src/browser/renderer/CursorRenderLayer.ts +++ b/src/browser/renderer/CursorRenderLayer.ts @@ -209,7 +209,7 @@ export class CursorRenderLayer extends BaseRenderLayer { private _renderBarCursor(x: number, y: number, cell: ICellData): void { this._ctx.save(); this._ctx.fillStyle = this._colors.cursor.css; - this._fillLeftLineAtCell(x, y, this._optionsService.options.cursorBarWidth); + this._fillLeftLineAtCell(x, y, this._optionsService.options.cursorWidth); this._ctx.restore(); } diff --git a/src/common/services/OptionsService.ts b/src/common/services/OptionsService.ts index 987dde67..b92fa58f 100644 --- a/src/common/services/OptionsService.ts +++ b/src/common/services/OptionsService.ts @@ -20,7 +20,7 @@ export const DEFAULT_OPTIONS: ITerminalOptions = Object.freeze({ rows: 24, cursorBlink: false, cursorStyle: 'block', - cursorBarWidth: 1, + cursorWidth: 1, bellSound: DEFAULT_BELL_SOUND, bellStyle: 'none', drawBoldTextInBrightColors: true, diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index 4f90c0e9..2b284cd2 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -191,7 +191,7 @@ export interface IPartialTerminalOptions { cols?: number; cursorBlink?: boolean; cursorStyle?: 'block' | 'underline' | 'bar'; - cursorBarWidth?: number; + cursorWidth?: number; disableStdin?: boolean; drawBoldTextInBrightColors?: boolean; fastScrollModifier?: 'alt' | 'ctrl' | 'shift'; @@ -224,7 +224,7 @@ export interface ITerminalOptions { cols: number; cursorBlink: boolean; cursorStyle: 'block' | 'underline' | 'bar'; - cursorBarWidth: number; + cursorWidth: number; disableStdin: boolean; drawBoldTextInBrightColors: boolean; fastScrollModifier: 'alt' | 'ctrl' | 'shift' | undefined; diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index c492330c..22314fb6 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -75,7 +75,7 @@ declare module 'xterm' { /** * The width of the cursor in CSS pixels when `cursorStyle` is set to 'bar'. */ - cursorBarWidth?: number; + cursorWidth?: number; /** * Whether input should be disabled. From 70c8f8733a98c84dfdba8d955ab2ad66a5f2f1dc Mon Sep 17 00:00:00 2001 From: Nick Pezza Date: Sat, 7 Dec 2019 15:42:33 -0500 Subject: [PATCH 05/48] Sanitize cursorWidth option --- src/common/services/OptionsService.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/common/services/OptionsService.ts b/src/common/services/OptionsService.ts index b92fa58f..885f8ab5 100644 --- a/src/common/services/OptionsService.ts +++ b/src/common/services/OptionsService.ts @@ -127,6 +127,7 @@ export class OptionsService implements IOptionsService { } break; case 'fastScrollSensitivity': + case 'cursorWidth': case 'scrollSensitivity': if (value <= 0) { throw new Error(`${key} cannot be less than or equal to 0, value: ${value}`); From fa4de08934c6ceb769dfe7e34031629ba01e3427 Mon Sep 17 00:00:00 2001 From: Nick Pezza Date: Sat, 7 Dec 2019 15:54:17 -0500 Subject: [PATCH 06/48] Apply changes to the dom renderer --- src/browser/renderer/dom/DomRenderer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browser/renderer/dom/DomRenderer.ts b/src/browser/renderer/dom/DomRenderer.ts index 018e63e5..942a2792 100644 --- a/src/browser/renderer/dom/DomRenderer.ts +++ b/src/browser/renderer/dom/DomRenderer.ts @@ -206,7 +206,7 @@ export class DomRenderer extends Disposable implements IRenderer { ` color: ${this._colors.cursorAccent.css};` + `}` + `${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_BAR_CLASS} {` + - ` box-shadow: 1px 0 0 ${this._colors.cursor.css} inset;` + + ` box-shadow: ${this._optionsService.options.cursorWidth}px 0 0 ${this._colors.cursor.css} inset;` + `}` + `${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_UNDERLINE_CLASS} {` + ` box-shadow: 0 -1px 0 ${this._colors.cursor.css} inset;` + From 79ea49cafd73da44c684c54587dffcaf19894d30 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 7 Dec 2019 13:24:59 -0800 Subject: [PATCH 07/48] Floor cursorWidth to ensure it's an integer --- src/common/services/OptionsService.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/common/services/OptionsService.ts b/src/common/services/OptionsService.ts index 885f8ab5..7cd111ac 100644 --- a/src/common/services/OptionsService.ts +++ b/src/common/services/OptionsService.ts @@ -112,6 +112,9 @@ export class OptionsService implements IOptionsService { value = DEFAULT_OPTIONS[key]; } break; + case 'cursorWidth': + value = Math.floor(value); + // Fall through for bounds check case 'lineHeight': case 'tabStopWidth': if (value < 1) { @@ -120,6 +123,7 @@ export class OptionsService implements IOptionsService { break; case 'minimumContrastRatio': value = Math.max(1, Math.min(21, Math.round(value * 10) / 10)); + break; case 'scrollback': value = Math.min(value, 4294967295); if (value < 0) { @@ -127,7 +131,6 @@ export class OptionsService implements IOptionsService { } break; case 'fastScrollSensitivity': - case 'cursorWidth': case 'scrollSensitivity': if (value <= 0) { throw new Error(`${key} cannot be less than or equal to 0, value: ${value}`); From a628e427de0dbc01f6ce9289eae81a47dedb25cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sun, 8 Dec 2019 18:46:40 +0100 Subject: [PATCH 08/48] update version of node-pty --- package.json | 2 +- yarn.lock | 17 ++++++----------- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index e04a1ff8..6918e11f 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "glob": "^7.0.5", "jsdom": "^11.11.0", "mocha": "^6.1.4", - "node-pty": "0.7.6", + "node-pty": "^0.9.0", "nyc": "13", "puppeteer": "^1.15.0", "source-map-loader": "^0.2.4", diff --git a/yarn.lock b/yarn.lock index 171b66e4..e3932408 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3426,12 +3426,7 @@ mute-stream@0.0.7: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s= -nan@2.10.0: - version "2.10.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f" - integrity sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA== - -nan@^2.12.1: +nan@^2.12.1, nan@^2.14.0: version "2.14.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== @@ -3530,12 +3525,12 @@ node-pre-gyp@^0.12.0: semver "^5.3.0" tar "^4" -node-pty@0.7.6: - version "0.7.6" - resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-0.7.6.tgz#bff6148c9c5836ca7e73c7aaaec067dcbdac2f7b" - integrity sha512-ECzKUB7KkAFZ0cjyjMXp5WLJ+7YIZ1xnNmiiegOI6WdDaKABUNV5NbB1Dw9MXD4KrZipWII0wQ7RGZ6StU/7jA== +node-pty@^0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-0.9.0.tgz#8f9bcc0d1c5b970a3184ffd533d862c7eb6590a6" + integrity sha512-MBnCQl83FTYOu7B4xWw10AW77AAh7ThCE1VXEv+JeWj8mSpGo+0bwgsV+b23ljBFwEM9OmsOv3kM27iUPPm84g== dependencies: - nan "2.10.0" + nan "^2.14.0" nopt@^4.0.1: version "4.0.1" From ed7d8e7c3b9e56998da1eb810dc16a6f19cfe73f Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 9 Dec 2019 19:34:45 -0800 Subject: [PATCH 09/48] Support hidden attr in DOM renderer --- src/browser/renderer/dom/DomRendererRowFactory.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts index bd922f57..fea03b58 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.ts @@ -101,7 +101,11 @@ export class DomRendererRowFactory { charElement.classList.add(UNDERLINE_CLASS); } - charElement.textContent = this._workCell.getChars() || WHITESPACE_CELL_CHAR; + if (this._workCell.isInvisible()) { + charElement.textContent = WHITESPACE_CELL_CHAR; + } else { + charElement.textContent = this._workCell.getChars() || WHITESPACE_CELL_CHAR; + } let fg = this._workCell.getFgColor(); let fgColorMode = this._workCell.getFgColorMode(); From 93cdee3bab29db06f4a31e2ee72e9284026edcba Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 9 Dec 2019 19:39:59 -0800 Subject: [PATCH 10/48] Support hidden in WebGL renderer Fixes #2596 --- addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index c96c2a69..76acf2b2 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -316,6 +316,11 @@ export class WebglCharAtlas implements IDisposable { this._workAttributeData.fg = fg; this._workAttributeData.bg = bg; + const invisible = !!this._workAttributeData.isInvisible(); + if (invisible) { + return NULL_RASTERIZED_GLYPH; + } + const bold = !!this._workAttributeData.isBold(); const inverse = !!this._workAttributeData.isInverse(); const dim = !!this._workAttributeData.isDim(); From 31da88ad89d99a02810218525b076dcb2c8eb610 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 9 Dec 2019 19:47:47 -0800 Subject: [PATCH 11/48] Add webgl invisible tests --- .../src/WebglRenderer.api.ts | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.api.ts b/addons/xterm-addon-webgl/src/WebglRenderer.api.ts index bf877dd8..1cd198d9 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.api.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.api.ts @@ -148,6 +148,52 @@ describe('WebGL Renderer Integration Tests', function(): void { await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]); }); + it('foreground 0-15 inivisible', async () => { + const theme: ITheme = { + black: '#010203', + red: '#040506', + green: '#070809', + yellow: '#0a0b0c', + blue: '#0d0e0f', + magenta: '#101112', + cyan: '#131415', + white: '#161718' + }; + await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`); + await writeSync(`\\x1b[8;30m \\x1b[8;31m \\x1b[8;32m \\x1b[8;33m \\x1b[8;34m \\x1b[8;35m \\x1b[8;36m \\x1b[8;37m `); + await pollFor(page, () => getCellColor(1, 1), [0, 0, 0, 255]); + await pollFor(page, () => getCellColor(2, 1), [0, 0, 0, 255]); + await pollFor(page, () => getCellColor(3, 1), [0, 0, 0, 255]); + await pollFor(page, () => getCellColor(4, 1), [0, 0, 0, 255]); + await pollFor(page, () => getCellColor(5, 1), [0, 0, 0, 255]); + await pollFor(page, () => getCellColor(6, 1), [0, 0, 0, 255]); + await pollFor(page, () => getCellColor(7, 1), [0, 0, 0, 255]); + await pollFor(page, () => getCellColor(8, 1), [0, 0, 0, 255]); + }); + + it('background 0-15 inivisible', async () => { + const theme: ITheme = { + black: '#010203', + red: '#040506', + green: '#070809', + yellow: '#0a0b0c', + blue: '#0d0e0f', + magenta: '#101112', + cyan: '#131415', + white: '#161718' + }; + await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`); + await writeSync(`\\x1b[8;40m█\\x1b[8;41m█\\x1b[8;42m█\\x1b[8;43m█\\x1b[8;44m█\\x1b[8;45m█\\x1b[8;46m█\\x1b[8;47m█`); + await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]); + await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]); + await pollFor(page, () => getCellColor(3, 1), [7, 8, 9, 255]); + await pollFor(page, () => getCellColor(4, 1), [10, 11, 12, 255]); + await pollFor(page, () => getCellColor(5, 1), [13, 14, 15, 255]); + await pollFor(page, () => getCellColor(6, 1), [16, 17, 18, 255]); + await pollFor(page, () => getCellColor(7, 1), [19, 20, 21, 255]); + await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]); + }); + it('foreground 0-15 bright', async () => { const theme: ITheme = { brightBlack: '#010203', @@ -274,6 +320,46 @@ describe('WebGL Renderer Integration Tests', function(): void { } }); + it('foreground 16-255 invisible', async () => { + let data = ''; + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + data += `\\x1b[8;38;5;${16 + y * 16 + x}m \x1b[0m`; + } + data += '\\r\\n'; + } + await writeSync(data); + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + const cssColor = COLORS_16_TO_255[y * 16 + x]; + const r = parseInt(cssColor.substr(1, 2), 16); + const g = parseInt(cssColor.substr(3, 2), 16); + const b = parseInt(cssColor.substr(5, 2), 16); + await pollFor(page, () => getCellColor(x + 1, y + 1), [0, 0, 0, 255]); + } + } + }); + + it('background 16-255 invisible', async () => { + let data = ''; + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + data += `\\x1b[8;48;5;${16 + y * 16 + x}m█\x1b[0m`; + } + data += '\\r\\n'; + } + await writeSync(data); + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + const cssColor = COLORS_16_TO_255[y * 16 + x]; + const r = parseInt(cssColor.substr(1, 2), 16); + const g = parseInt(cssColor.substr(3, 2), 16); + const b = parseInt(cssColor.substr(5, 2), 16); + await pollFor(page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]); + } + } + }); + it('foreground true color red', async () => { let data = ''; for (let y = 0; y < 16; y++) { @@ -561,6 +647,42 @@ describe('WebGL Renderer Integration Tests', function(): void { } } }); + + it('foreground true color grey invisible', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\\x1b[8;38;2;${i};${i};${i}m \x1b[0m`; + } + data += '\\r\\n'; + } + await writeSync(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(page, () => getCellColor(x + 1, y + 1), [0, 0, 0, 255]); + } + } + }); + + it('background true color grey invisible', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\\x1b[8;48;2;${i};${i};${i}m█\x1b[0m`; + } + data += '\\r\\n'; + } + await writeSync(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(page, () => getCellColor(x + 1, y + 1), [i, i, i, 255]); + } + } + }); }); describe('minimumContrastRatio', async () => { From 31669cbad654e7e9f1ae3c1c98b4f758b762873c Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 11 Dec 2019 14:21:09 -0800 Subject: [PATCH 12/48] Expose texture atlas as API and use in demo Part of #2623 --- addons/xterm-addon-webgl/src/WebglAddon.ts | 9 ++++++++- addons/xterm-addon-webgl/src/WebglRenderer.ts | 4 ++++ addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts | 3 --- addons/xterm-addon-webgl/typings/xterm-addon-webgl.d.ts | 2 ++ demo/client.ts | 8 ++++++++ 5 files changed, 22 insertions(+), 4 deletions(-) diff --git a/addons/xterm-addon-webgl/src/WebglAddon.ts b/addons/xterm-addon-webgl/src/WebglAddon.ts index be867d75..3cf45f35 100644 --- a/addons/xterm-addon-webgl/src/WebglAddon.ts +++ b/addons/xterm-addon-webgl/src/WebglAddon.ts @@ -10,6 +10,7 @@ import { IColorSet } from 'browser/Types'; export class WebglAddon implements ITerminalAddon { private _terminal?: Terminal; + private _renderer?: WebglRenderer; constructor( private _preserveDrawingBuffer?: boolean @@ -22,7 +23,8 @@ export class WebglAddon implements ITerminalAddon { this._terminal = terminal; const renderService: IRenderService = (terminal)._core._renderService; const colors: IColorSet = (terminal)._core._colorManager.colors; - renderService.setRenderer(new WebglRenderer(terminal, colors, this._preserveDrawingBuffer)); + this._renderer = new WebglRenderer(terminal, colors, this._preserveDrawingBuffer); + renderService.setRenderer(this._renderer); } public dispose(): void { @@ -32,5 +34,10 @@ export class WebglAddon implements ITerminalAddon { const renderService: IRenderService = (this._terminal)._core._renderService; renderService.setRenderer((this._terminal)._core._createRenderer()); renderService.onResize(this._terminal.cols, this._terminal.rows); + this._renderer = undefined; + } + + public get textureAtlas(): HTMLCanvasElement | undefined { + return this._renderer?.textureAtlas; } } diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index 6f98d2c1..ae85d9a0 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -100,6 +100,10 @@ export class WebglRenderer extends Disposable implements IRenderer { super.dispose(); } + public get textureAtlas(): HTMLCanvasElement | undefined { + return this._charAtlas?.cacheCanvas; + } + public setColors(colors: IColorSet): void { this._colors = colors; // Clear layers and force a full render diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index c96c2a69..5c3fb239 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -86,9 +86,6 @@ export class WebglCharAtlas implements IDisposable { this._tmpCanvas.width = this._config.scaledCharWidth * 2 + TMP_CANVAS_GLYPH_PADDING * 2; this._tmpCanvas.height = this._config.scaledCharHeight + TMP_CANVAS_GLYPH_PADDING * 2; this._tmpCtx = throwIfFalsy(this._tmpCanvas.getContext('2d', {alpha: this._config.allowTransparency})); - - // This is useful for debugging - document.body.appendChild(this.cacheCanvas); } public dispose(): void { diff --git a/addons/xterm-addon-webgl/typings/xterm-addon-webgl.d.ts b/addons/xterm-addon-webgl/typings/xterm-addon-webgl.d.ts index 5199a260..9586433a 100644 --- a/addons/xterm-addon-webgl/typings/xterm-addon-webgl.d.ts +++ b/addons/xterm-addon-webgl/typings/xterm-addon-webgl.d.ts @@ -10,6 +10,8 @@ declare module 'xterm-addon-webgl' { * An xterm.js addon that provides search functionality. */ export class WebglAddon implements ITerminalAddon { + public textureAtlas?: HTMLCanvasElement; + constructor(preserveDrawingBuffer?: boolean); /** diff --git a/demo/client.ts b/demo/client.ts index 5b76700e..6d146264 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -343,7 +343,15 @@ function initAddons(term: TerminalType): void { if (checkbox.checked) { addon.instance = new addon.ctor(); term.loadAddon(addon.instance); + if (name === 'webgl') { + setTimeout(() => { + document.body.appendChild((addon.instance as WebglAddon).textureAtlas); + }, 0); + } } else { + if (name === 'webgl') { + document.body.removeChild((addon.instance as WebglAddon).textureAtlas); + } addon.instance!.dispose(); addon.instance = undefined; } From 89995fe7ca2b65d97b230a6b4ce80ba07b17447c Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 11 Dec 2019 14:47:26 -0800 Subject: [PATCH 13/48] Webgl v0.4.1 --- addons/xterm-addon-webgl/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/xterm-addon-webgl/package.json b/addons/xterm-addon-webgl/package.json index 21e394d1..55063525 100644 --- a/addons/xterm-addon-webgl/package.json +++ b/addons/xterm-addon-webgl/package.json @@ -1,6 +1,6 @@ { "name": "xterm-addon-webgl", - "version": "0.4.0", + "version": "0.4.1", "author": { "name": "The xterm.js authors", "url": "https://xtermjs.org/" From efb206950081b950d926d07e0d8d4c017c2cd29a Mon Sep 17 00:00:00 2001 From: ivanwonder Date: Fri, 13 Dec 2019 10:57:42 +0800 Subject: [PATCH 14/48] format color value to style '#rrggbbaa' --- src/browser/ColorManager.ts | 31 +++++++++---------------------- 1 file changed, 9 insertions(+), 22 deletions(-) diff --git a/src/browser/ColorManager.ts b/src/browser/ColorManager.ts index b4bcdde0..4ba49936 100644 --- a/src/browser/ColorManager.ts +++ b/src/browser/ColorManager.ts @@ -184,34 +184,21 @@ export class ColorManager implements IColorManager { ); return fallback; } - let r: number; - let g: number; - let b: number; - let a: number; - let rgba: number; - if (css.length === 5) { - const num = parseInt(css.substr(1), 16); - r = ((num >> 12) & 0xF) * 16; - g = ((num >> 8) & 0xF) * 16; - b = ((num >> 4) & 0xF) * 16; - a = (num & 0xF) * 16; - rgba = toRgba(r, g, b, a); - } else { - rgba = parseInt(css.substr(1), 16); - r = (rgba >> 24) & 0xFF; - g = (rgba >> 16) & 0xFF; - b = (rgba >> 8) & 0xFF; - a = (rgba ) & 0xFF; - } - + // https://html.spec.whatwg.org/multipage/canvas.html#serialisation-of-a-color + // the color value has alpha less than 1.0, and the string is the color value in the CSS rgba() + const [r, g, b, a] = this._ctx.fillStyle.substring(5, this._ctx.fillStyle.length - 1).split(',').map(component => Number(component)); + const alpha = Math.round(a * 255); + const rgba: number = toRgba(r, g, b, alpha); return { rgba, - css: toCss(r, g, b, a) + css: toCss(r, g, b, alpha) }; } return { - css, + // https://html.spec.whatwg.org/multipage/canvas.html#serialisation-of-a-color + // if it has alpha equal to 1.0, then the string is a lowercase six-digit hex value, prefixed with a "#" character + css: this._ctx.fillStyle, rgba: toRgba(data[0], data[1], data[2], data[3]) }; } From ed334fd7905c92f05a50e7acfafa65b73ffdc907 Mon Sep 17 00:00:00 2001 From: Clark Meyer Date: Sat, 14 Dec 2019 21:59:43 -0800 Subject: [PATCH 15/48] Added Gus to list of xterm real-world users --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 70ff7891..cd0d1d07 100644 --- a/README.md +++ b/README.md @@ -156,6 +156,7 @@ Xterm.js is used in several world-class applications to provide great terminal e - [**tty-share**](https://tty-share.com): Extremely simple terminal sharing over the Internet. - [**Ten Hands**](https://github.com/saisandeepvaddi/ten-hands): One place to run your command-line tasks. - [**WebAssembly.sh**](https://webassembly.sh): A WebAssembly WASI browser terminal +- [**Gus**](https://gus.jp): A shared coding pad where you can run Python with xterm.js [And much more...](https://github.com/xtermjs/xterm.js/network/dependents) From 94f5c378846d7d3cc80b21bf1fdc75184d65cbcd Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 18 Dec 2019 09:59:33 +1100 Subject: [PATCH 16/48] Support events with 2 args This will allow higher perf events by avoiding object creation --- src/common/EventEmitter.ts | 30 +++++++++++++++--------------- typings/xterm.d.ts | 6 +++--- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/common/EventEmitter.ts b/src/common/EventEmitter.ts index 5991e338..301574e4 100644 --- a/src/common/EventEmitter.ts +++ b/src/common/EventEmitter.ts @@ -5,28 +5,28 @@ import { IDisposable } from 'common/Types'; -interface IListener { - (e: T): void; +interface IListener { + (arg1: T, arg2: U): void; } -export interface IEvent { - (listener: (e: T) => any): IDisposable; +export interface IEvent { + (listener: (arg1: T, arg2: U) => any): IDisposable; } -export interface IEventEmitter { - event: IEvent; - fire(data: T): void; +export interface IEventEmitter { + event: IEvent; + fire(arg1: T, arg2: U): void; dispose(): void; } -export class EventEmitter implements IEventEmitter { - private _listeners: IListener[] = []; - private _event?: IEvent; +export class EventEmitter implements IEventEmitter { + private _listeners: IListener[] = []; + private _event?: IEvent; private _disposed: boolean = false; - public get event(): IEvent { + public get event(): IEvent { if (!this._event) { - this._event = (listener: (e: T) => any) => { + this._event = (listener: (arg1: T, arg2: U) => any) => { this._listeners.push(listener); const disposable = { dispose: () => { @@ -46,13 +46,13 @@ export class EventEmitter implements IEventEmitter { return this._event; } - public fire(data: T): void { - const queue: IListener[] = []; + public fire(arg1: T, arg2: U): void { + const queue: IListener[] = []; for (let i = 0; i < this._listeners.length; i++) { queue.push(this._listeners[i]); } for (let i = 0; i < queue.length; i++) { - queue[i].call(undefined, data); + queue[i].call(undefined, arg1, arg2); } } diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 22314fb6..ac33d087 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -336,8 +336,8 @@ declare module 'xterm' { * An event that can be listened to. * @returns an `IDisposable` to stop listening. */ - export interface IEvent { - (listener: (e: T) => any): IDisposable; + export interface IEvent { + (listener: (arg1: T, arg2: U) => any): IDisposable; } /** @@ -444,7 +444,7 @@ declare module 'xterm' { * Currently this is only used for a certain type of mouse reports that * happen to be not UTF-8 compatible. * The event value is a JS string, pass it to the underlying pty as - * binary data, e.g. `pty.write(Buffer.from(data, 'binary'))`. + * binary data, e.g. `pty.write(Buffer.from(data, 'binary'))`. * @returns an `IDisposable` to stop listening. */ onBinary: IEvent; From a2297a5fa6ce04600d0ded4f5fd0ce1ca43fb902 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 18 Dec 2019 10:01:33 +1100 Subject: [PATCH 17/48] Remove Terminal.refresh usage in InputHandler --- src/InputHandler.ts | 8 +++++--- src/Terminal.ts | 1 + src/Types.d.ts | 1 - 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 7a0a6ac3..cfd2e1ec 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -128,6 +128,8 @@ export class InputHandler extends Disposable implements IInputHandler { private _utf8Decoder: Utf8ToUtf32 = new Utf8ToUtf32(); private _workCell: CellData = new CellData(); + private _onRequestRefreshRows = new EventEmitter(); + public get onRequestRefreshRows(): IEvent { return this._onRequestRefreshRows.event; } private _onCursorMove = new EventEmitter(); public get onCursorMove(): IEvent { return this._onCursorMove.event; } private _onLineFeed = new EventEmitter(); @@ -372,7 +374,7 @@ export class InputHandler extends Disposable implements IInputHandler { } // Refresh any dirty rows accumulated as part of parsing - this._terminal.refresh(this._dirtyRowService.start, this._dirtyRowService.end); + this._onRequestRefreshRows.fire(this._dirtyRowService.start, this._dirtyRowService.end); } public print(data: Uint32Array, start: number, end: number): void { @@ -1465,7 +1467,7 @@ export class InputHandler extends Disposable implements IInputHandler { case 47: // alt screen buffer case 1047: // alt screen buffer this._bufferService.buffers.activateAltBuffer(this._terminal.eraseAttrData()); - this._terminal.refresh(0, this._bufferService.rows - 1); + this._onRequestRefreshRows.fire(0, this._bufferService.rows - 1); if (this._terminal.viewport) { this._terminal.viewport.syncScrollArea(); } @@ -1639,7 +1641,7 @@ export class InputHandler extends Disposable implements IInputHandler { if (params.params[i] === 1049) { this.restoreCursor(); } - this._terminal.refresh(0, this._bufferService.rows - 1); + this._onRequestRefreshRows.fire(0, this._bufferService.rows - 1); if (this._terminal.viewport) { this._terminal.viewport.syncScrollArea(); } diff --git a/src/Terminal.ts b/src/Terminal.ts index 5c08a709..bb67ac1f 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -277,6 +277,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp // Register input handler and refire/handle events this._inputHandler = new InputHandler(this, this._bufferService, this._coreService, this._dirtyRowService, this._logService, this.optionsService, this._coreMouseService); + this._inputHandler.onRequestRefreshRows((start, end) => this.refresh(start, end)); this._inputHandler.onCursorMove(() => this._onCursorMove.fire()); this._inputHandler.onLineFeed(() => this._onLineFeed.fire()); this.register(this._inputHandler); diff --git a/src/Types.d.ts b/src/Types.d.ts index f4d3a556..063331dd 100644 --- a/src/Types.d.ts +++ b/src/Types.d.ts @@ -56,7 +56,6 @@ export interface IInputHandlingTerminal { resize(x: number, y: number): void; reset(): void; showCursor(): void; - refresh(start: number, end: number): void; handleTitle(title: string): void; } From 05e1cd84787dd90e68781de4a050d21cd3e5a7cf Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 18 Dec 2019 10:32:38 +1100 Subject: [PATCH 18/48] Move charset logic into core service --- src/InputHandler.ts | 32 +++++++++++++------------- src/Terminal.ts | 36 ++---------------------------- src/TestUtils.test.ts | 13 ----------- src/Types.d.ts | 6 ----- src/common/TestUtils.test.ts | 10 ++++++++- src/common/Types.d.ts | 6 +++++ src/common/services/CoreService.ts | 25 ++++++++++++++++++++- src/common/services/Services.ts | 19 +++++++++++++++- 8 files changed, 74 insertions(+), 73 deletions(-) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index cfd2e1ec..1c9e6574 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -381,7 +381,7 @@ export class InputHandler extends Disposable implements IInputHandler { let code: number; let chWidth: number; const buffer = this._bufferService.buffer; - const charset = this._terminal.charset; + const charset = this._coreService.charsetModes.charset; const screenReaderMode = this._optionsService.options.screenReaderMode; const cols = this._bufferService.cols; const wraparoundMode = this._terminal.wraparoundMode; @@ -608,7 +608,7 @@ export class InputHandler extends Disposable implements IInputHandler { * G1 character set. */ public shiftOut(): void { - this._terminal.setgLevel(1); + this._coreService.setgLevel(1); } /** @@ -617,7 +617,7 @@ export class InputHandler extends Disposable implements IInputHandler { * character set (the default). */ public shiftIn(): void { - this._terminal.setgLevel(0); + this._coreService.setgLevel(0); } /** @@ -1396,10 +1396,10 @@ export class InputHandler extends Disposable implements IInputHandler { this._coreService.decPrivateModes.applicationCursorKeys = true; break; case 2: - this._terminal.setgCharset(0, DEFAULT_CHARSET); - this._terminal.setgCharset(1, DEFAULT_CHARSET); - this._terminal.setgCharset(2, DEFAULT_CHARSET); - this._terminal.setgCharset(3, DEFAULT_CHARSET); + this._coreService.setgCharset(0, DEFAULT_CHARSET); + this._coreService.setgCharset(1, DEFAULT_CHARSET); + this._coreService.setgCharset(2, DEFAULT_CHARSET); + this._coreService.setgCharset(3, DEFAULT_CHARSET); // set VT100 mode here break; case 3: // 132 col mode @@ -1974,9 +1974,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._bufferService.buffer.scrollBottom = this._bufferService.rows - 1; this._terminal.curAttrData = DEFAULT_ATTR_DATA.clone(); this._bufferService.buffer.x = this._bufferService.buffer.y = 0; // ? - this._terminal.charset = null; - this._terminal.glevel = 0; // ?? - this._terminal.charsets = [null]; // ?? + this._coreService.softReset(); } /** @@ -2040,7 +2038,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._bufferService.buffer.savedY = this._bufferService.buffer.ybase + this._bufferService.buffer.y; this._bufferService.buffer.savedCurAttrData.fg = this._terminal.curAttrData.fg; this._bufferService.buffer.savedCurAttrData.bg = this._terminal.curAttrData.bg; - this._bufferService.buffer.savedCharset = this._terminal.charset; + this._bufferService.buffer.savedCharset = this._coreService.charsetModes.charset; } @@ -2054,9 +2052,9 @@ export class InputHandler extends Disposable implements IInputHandler { this._bufferService.buffer.y = Math.max(this._bufferService.buffer.savedY - this._bufferService.buffer.ybase, 0); this._terminal.curAttrData.fg = this._bufferService.buffer.savedCurAttrData.fg; this._terminal.curAttrData.bg = this._bufferService.buffer.savedCurAttrData.bg; - this._terminal.charset = (this as any)._savedCharset; + this._coreService.charsetModes.charset = (this as any)._savedCharset; if (this._bufferService.buffer.savedCharset) { - this._terminal.charset = this._bufferService.buffer.savedCharset; + this._coreService.charsetModes.charset = this._bufferService.buffer.savedCharset; } this._restrictCursor(); } @@ -2115,8 +2113,8 @@ export class InputHandler extends Disposable implements IInputHandler { * therefore ESC % G does the same. */ public selectDefaultCharset(): void { - this._terminal.setgLevel(0); - this._terminal.setgCharset(0, DEFAULT_CHARSET); // US (default) + this._coreService.setgLevel(0); + this._coreService.setgCharset(0, DEFAULT_CHARSET); // US (default) } /** @@ -2143,7 +2141,7 @@ export class InputHandler extends Disposable implements IInputHandler { if (collectAndFlag[0] === '/') { return; // TODO: Is this supported? } - this._terminal.setgCharset(GLEVEL[collectAndFlag[0]], CHARSETS[collectAndFlag[1]] || DEFAULT_CHARSET); + this._coreService.setgCharset(GLEVEL[collectAndFlag[0]], CHARSETS[collectAndFlag[1]] || DEFAULT_CHARSET); return; } @@ -2222,7 +2220,7 @@ export class InputHandler extends Disposable implements IInputHandler { * you use another locking shift. (partly supported) */ public setgLevel(level: number): void { - this._terminal.setgLevel(level); // TODO: save to move from terminal? + this._coreService.setgLevel(level); } /** diff --git a/src/Terminal.ts b/src/Terminal.ts index bb67ac1f..eb199ea1 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -39,7 +39,7 @@ import { MouseZoneManager } from 'browser/MouseZoneManager'; import { AccessibilityManager } from './AccessibilityManager'; import { ITheme, IMarker, IDisposable, ISelectionPosition } from 'xterm'; import { DomRenderer } from 'browser/renderer/dom/DomRenderer'; -import { IKeyboardEvent, KeyboardResultType, ICharset, IBufferLine, IAttributeData, CoreMouseEventType, CoreMouseButton, CoreMouseAction } from 'common/Types'; +import { IKeyboardEvent, KeyboardResultType, IBufferLine, IAttributeData, CoreMouseEventType, CoreMouseButton, CoreMouseAction } from 'common/Types'; import { evaluateKeyboardEvent } from 'common/input/Keyboard'; import { EventEmitter, IEvent } from 'common/EventEmitter'; import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; @@ -116,13 +116,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp public wraparoundMode: boolean; // defaults: xterm - true, vt100 - false public bracketedPasteMode: boolean; - // charset - // The current charset - public charset: ICharset; - public gcharset: number; - public glevel: number; - public charsets: ICharset[]; - // mouse properties public mouseEvents: CoreMouseEventType = CoreMouseEventType.NONE; public sendFocus: boolean; @@ -261,11 +254,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this.bracketedPasteMode = false; // charset - this.charset = null; - this.gcharset = null; - this.glevel = 0; - // TODO: Can this be just []? - this.charsets = [null]; + this._coreService.softReset(); this.curAttrData = DEFAULT_ATTR_DATA.clone(); this._eraseAttrData = DEFAULT_ATTR_DATA.clone(); @@ -1307,27 +1296,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp return thirdLevelKey && (!ev.keyCode || ev.keyCode > 47); } - /** - * Set the G level of the terminal - * @param g - */ - public setgLevel(g: number): void { - this.glevel = g; - this.charset = this.charsets[g]; - } - - /** - * Set the charset for the given G level of the terminal - * @param g - * @param charset - */ - public setgCharset(g: number, charset: ICharset): void { - this.charsets[g] = charset; - if (this.glevel === g) { - this.charset = charset; - } - } - protected _keyUp(ev: KeyboardEvent): void { if (this._customKeyEventHandler && this._customKeyEventHandler(ev) === false) { return; diff --git a/src/TestUtils.test.ts b/src/TestUtils.test.ts index 84c0d0b7..8954ae83 100644 --- a/src/TestUtils.test.ts +++ b/src/TestUtils.test.ts @@ -201,10 +201,6 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal { options: ITerminalOptions = {}; cols: number; rows: number; - charset: { [key: string]: string; }; - gcharset: number; - glevel: number; - charsets: { [key: string]: string; }[]; applicationKeypad: boolean; applicationCursor: boolean; originMode: boolean; @@ -243,9 +239,6 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal { nextStop(x?: number): number { throw new Error('Method not implemented.'); } - setgLevel(g: number): void { - throw new Error('Method not implemented.'); - } eraseAttrData(): IAttributeData { throw new Error('Method not implemented.'); } @@ -264,9 +257,6 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal { is(term: string): boolean { throw new Error('Method not implemented.'); } - setgCharset(g: number, charset: { [key: string]: string; }): void { - throw new Error('Method not implemented.'); - } resize(x: number, y: number): void { throw new Error('Method not implemented.'); } @@ -279,9 +269,6 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal { showCursor(): void { throw new Error('Method not implemented.'); } - refresh(start: number, end: number): void { - throw new Error('Method not implemented.'); - } matchColor(r1: number, g1: number, b1: number): number { throw new Error('Method not implemented.'); } diff --git a/src/Types.d.ts b/src/Types.d.ts index 063331dd..3a3aef62 100644 --- a/src/Types.d.ts +++ b/src/Types.d.ts @@ -25,10 +25,6 @@ export interface IInputHandlingTerminal { options: ITerminalOptions; cols: number; rows: number; - charset: ICharset; - gcharset: number; - glevel: number; - charsets: ICharset[]; applicationKeypad: boolean; originMode: boolean; insertMode: boolean; @@ -49,10 +45,8 @@ export interface IInputHandlingTerminal { bell(): void; focus(): void; scroll(isWrapped?: boolean): void; - setgLevel(g: number): void; eraseAttrData(): IAttributeData; is(term: string): boolean; - setgCharset(g: number, charset: ICharset): void; resize(x: number, y: number): void; reset(): void; showCursor(): void; diff --git a/src/common/TestUtils.test.ts b/src/common/TestUtils.test.ts index 7e47d4b0..96027804 100644 --- a/src/common/TestUtils.test.ts +++ b/src/common/TestUtils.test.ts @@ -9,7 +9,7 @@ import { clone } from 'common/Clone'; import { DEFAULT_OPTIONS } from 'common/services/OptionsService'; import { IBufferSet, IBuffer } from 'common/buffer/Types'; import { BufferSet } from 'common/buffer/BufferSet'; -import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEventType } from 'common/Types'; +import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEventType, ICharsetModes, ICharset } from 'common/Types'; export class MockBufferService implements IBufferService { serviceBrand: any; @@ -47,11 +47,19 @@ export class MockCoreService implements ICoreService { isCursorHidden: boolean = false; isFocused: boolean = false; serviceBrand: any; + charsetModes: ICharsetModes = { + charset: undefined, + charsets: [], + glevel: 0 + }; decPrivateModes: IDecPrivateModes = {} as any; onData: IEvent = new EventEmitter().event; onUserInput: IEvent = new EventEmitter().event; onBinary: IEvent = new EventEmitter().event; reset(): void {} + softReset(): void {} + setgLevel(g: number): void {} + setgCharset(g: number, charset: ICharset): void {} triggerDataEvent(data: string, wasUserInput?: boolean): void {} triggerBinaryEvent(data: string): void {} } diff --git a/src/common/Types.d.ts b/src/common/Types.d.ts index 2dcc704b..b8cb9b80 100644 --- a/src/common/Types.d.ts +++ b/src/common/Types.d.ts @@ -65,6 +65,12 @@ export interface ICharset { [key: string]: string; } +export interface ICharsetModes { + charset: ICharset | undefined; + glevel: number; + charsets: ICharset[]; +} + export type CharData = [number, string, number, number]; export type IColorRGB = [number, number, number]; diff --git a/src/common/services/CoreService.ts b/src/common/services/CoreService.ts index 35b61e84..af0d5f63 100644 --- a/src/common/services/CoreService.ts +++ b/src/common/services/CoreService.ts @@ -5,7 +5,7 @@ import { ICoreService, ILogService, IOptionsService, IBufferService } from 'common/services/Services'; import { EventEmitter, IEvent } from 'common/EventEmitter'; -import { IDecPrivateModes } from 'common/Types'; +import { IDecPrivateModes, ICharset, ICharsetModes } from 'common/Types'; import { clone } from 'common/Clone'; const DEFAULT_DEC_PRIVATE_MODES: IDecPrivateModes = Object.freeze({ @@ -18,6 +18,11 @@ export class CoreService implements ICoreService { public isCursorInitialized: boolean = false; public isCursorHidden: boolean = false; public decPrivateModes: IDecPrivateModes; + public charsetModes: ICharsetModes = { + charset: undefined, + charsets: [], + glevel: 0 + }; private _onData = new EventEmitter(); public get onData(): IEvent { return this._onData.event; } @@ -40,6 +45,12 @@ export class CoreService implements ICoreService { this.decPrivateModes = clone(DEFAULT_DEC_PRIVATE_MODES); } + public softReset(): void { + this.charsetModes.charset = undefined; + this.charsetModes.charsets = []; + this.charsetModes.glevel = 0; + } + public triggerDataEvent(data: string, wasUserInput: boolean = false): void { // Prevents all events to pty process if stdin is disabled if (this._optionsService.options.disableStdin) { @@ -69,4 +80,16 @@ export class CoreService implements ICoreService { this._logService.debug(`sending binary "${data}"`, () => data.split('').map(e => e.charCodeAt(0))); this._onBinary.fire(data); } + + public setgLevel(g: number): void { + this.charsetModes.glevel = g; + this.charsetModes.charset = this.charsetModes.charsets[g]; + } + + public setgCharset(g: number, charset: ICharset): void { + this.charsetModes.charsets[g] = charset; + if (this.charsetModes.glevel === g) { + this.charsetModes.charset = charset; + } + } } diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index 2b284cd2..9e47be26 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -5,7 +5,7 @@ import { IEvent } from 'common/EventEmitter'; import { IBuffer, IBufferSet } from 'common/buffer/Types'; -import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEncoding, ICoreMouseProtocol, CoreMouseEventType } from 'common/Types'; +import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEncoding, ICoreMouseProtocol, CoreMouseEventType, ICharsetModes, ICharset } from 'common/Types'; import { createDecorator } from 'common/services/ServiceRegistry'; export const IBufferService = createDecorator('BufferService'); @@ -64,6 +64,9 @@ export interface ICoreService { */ isCursorInitialized: boolean; isCursorHidden: boolean; + + charsetModes: ICharsetModes; + readonly decPrivateModes: IDecPrivateModes; readonly onData: IEvent; @@ -71,6 +74,7 @@ export interface ICoreService { readonly onBinary: IEvent; reset(): void; + softReset(): void; /** * Triggers the onData event in the public API. @@ -87,6 +91,19 @@ export interface ICoreService { * @param data The data that is being emitted. */ triggerBinaryEvent(data: string): void; + + /** + * Set the G level of the terminal. + * @param g + */ + setgLevel(g: number): void; + + /** + * Set the charset for the given G level of the terminal. + * @param g + * @param charset + */ + setgCharset(g: number, charset: ICharset): void; } export const IDirtyRowService = createDecorator('DirtyRowService'); From 224fb5c6a2bfd9667e35db6e9c3d4a0aea79a0f1 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 18 Dec 2019 10:53:05 +1100 Subject: [PATCH 19/48] Move charset into own service --- src/InputHandler.test.ts | 20 ++++++++-------- src/InputHandler.ts | 33 ++++++++++++++------------- src/Terminal.ts | 10 +++++--- src/common/TestUtils.test.ts | 24 ++++++++++--------- src/common/Types.d.ts | 8 +------ src/common/services/CharsetService.ts | 33 +++++++++++++++++++++++++++ src/common/services/CoreService.ts | 25 +------------------- src/common/services/Services.ts | 16 +++++++++---- 8 files changed, 94 insertions(+), 75 deletions(-) create mode 100644 src/common/services/CharsetService.ts diff --git a/src/InputHandler.test.ts b/src/InputHandler.test.ts index d8defa1c..fa9ef67c 100644 --- a/src/InputHandler.test.ts +++ b/src/InputHandler.test.ts @@ -13,7 +13,7 @@ import { CellData } from 'common/buffer/CellData'; import { Attributes } from 'common/buffer/Constants'; import { AttributeData } from 'common/buffer/AttributeData'; import { Params } from 'common/parser/Params'; -import { MockCoreService, MockBufferService, MockDirtyRowService, MockOptionsService, MockLogService, MockCoreMouseService } from 'common/TestUtils.test'; +import { MockCoreService, MockBufferService, MockDirtyRowService, MockOptionsService, MockLogService, MockCoreMouseService, MockCharsetService } from 'common/TestUtils.test'; import { IBufferService } from 'common/services/Services'; import { DEFAULT_OPTIONS } from 'common/services/OptionsService'; import { clone } from 'common/Clone'; @@ -41,7 +41,7 @@ describe('InputHandler', () => { bufferService.buffer.x = 1; bufferService.buffer.y = 2; bufferService.buffer.ybase = 0; - const inputHandler = new InputHandler(terminal, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService()); + const inputHandler = new InputHandler(terminal, bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService()); // Save cursor position inputHandler.saveCursor(); assert.equal(bufferService.buffer.x, 1); @@ -60,7 +60,7 @@ describe('InputHandler', () => { describe('setCursorStyle', () => { it('should call Terminal.setOption with correct params', () => { const optionsService = new MockOptionsService(); - const inputHandler = new InputHandler(new MockInputHandlingTerminal(), new MockBufferService(80, 30), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), optionsService, new MockCoreMouseService()); + const inputHandler = new InputHandler(new MockInputHandlingTerminal(), new MockBufferService(80, 30), new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), optionsService, new MockCoreMouseService()); inputHandler.setCursorStyle(Params.fromArray([0])); assert.equal(optionsService.options['cursorStyle'], 'block'); @@ -101,7 +101,7 @@ describe('InputHandler', () => { it('should toggle Terminal.bracketedPasteMode', () => { const terminal = new MockInputHandlingTerminal(); terminal.bracketedPasteMode = false; - const inputHandler = new InputHandler(terminal, new MockBufferService(80, 30), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService()); + const inputHandler = new InputHandler(terminal, new MockBufferService(80, 30), new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService()); // Set bracketed paste mode inputHandler.setModePrivate(Params.fromArray([2004])); assert.equal(terminal.bracketedPasteMode, true); @@ -120,7 +120,7 @@ describe('InputHandler', () => { it('insertChars', function(): void { const term = new Terminal(); const bufferService = new MockBufferService(80, 30); - const inputHandler = new InputHandler(term, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService()); + const inputHandler = new InputHandler(term, bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService()); // insert some data in first and second line inputHandler.parse(Array(bufferService.cols - 9).join('a')); @@ -158,7 +158,7 @@ describe('InputHandler', () => { it('deleteChars', function(): void { const term = new Terminal(); const bufferService = new MockBufferService(80, 30); - const inputHandler = new InputHandler(term, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService()); + const inputHandler = new InputHandler(term, bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService()); // insert some data in first and second line inputHandler.parse(Array(bufferService.cols - 9).join('a')); @@ -199,7 +199,7 @@ describe('InputHandler', () => { it('eraseInLine', function(): void { const term = new Terminal(); const bufferService = new MockBufferService(80, 30); - const inputHandler = new InputHandler(term, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService()); + const inputHandler = new InputHandler(term, bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService()); // fill 6 lines to test 3 different states inputHandler.parse(Array(bufferService.cols + 1).join('a')); @@ -228,7 +228,7 @@ describe('InputHandler', () => { it('eraseInDisplay', function(): void { const term = new Terminal({cols: 80, rows: 7}); const bufferService = new MockBufferService(80, 7); - const inputHandler = new InputHandler(term, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService()); + const inputHandler = new InputHandler(term, bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService()); // fill display with a's for (let i = 0; i < bufferService.rows; ++i) inputHandler.parse(Array(bufferService.cols + 1).join('a')); @@ -363,7 +363,7 @@ describe('InputHandler', () => { describe('print', () => { it('should not cause an infinite loop (regression test)', () => { const term = new Terminal(); - const inputHandler = new InputHandler(term, new MockBufferService(80, 30), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService()); + const inputHandler = new InputHandler(term, new MockBufferService(80, 30), new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService()); const container = new Uint32Array(10); container[0] = 0x200B; inputHandler.print(container, 0, 1); @@ -378,7 +378,7 @@ describe('InputHandler', () => { beforeEach(() => { term = new Terminal(); bufferService = new MockBufferService(80, 30); - handler = new InputHandler(term, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService()); + handler = new InputHandler(term, bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService()); }); it('should handle DECSET/DECRST 47 (alt screen buffer)', () => { handler.parse('\x1b[?47h\r\n\x1b[31mJUNK\x1b[?47lTEST'); diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 1c9e6574..79c769c7 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -19,7 +19,7 @@ import { NULL_CELL_CODE, NULL_CELL_WIDTH, Attributes, FgFlags, BgFlags, Content import { CellData } from 'common/buffer/CellData'; import { AttributeData } from 'common/buffer/AttributeData'; import { IAttributeData, IDisposable } from 'common/Types'; -import { ICoreService, IBufferService, IOptionsService, ILogService, IDirtyRowService, ICoreMouseService } from 'common/services/Services'; +import { ICoreService, IBufferService, IOptionsService, ILogService, IDirtyRowService, ICoreMouseService, ICharsetService } from 'common/services/Services'; import { OscHandler } from 'common/parser/OscParser'; import { DcsHandler } from 'common/parser/DcsParser'; @@ -140,6 +140,7 @@ export class InputHandler extends Disposable implements IInputHandler { constructor( protected _terminal: IInputHandlingTerminal, private readonly _bufferService: IBufferService, + private readonly _charsetService: ICharsetService, private readonly _coreService: ICoreService, private readonly _dirtyRowService: IDirtyRowService, private readonly _logService: ILogService, @@ -381,7 +382,7 @@ export class InputHandler extends Disposable implements IInputHandler { let code: number; let chWidth: number; const buffer = this._bufferService.buffer; - const charset = this._coreService.charsetModes.charset; + const charset = this._charsetService.charset; const screenReaderMode = this._optionsService.options.screenReaderMode; const cols = this._bufferService.cols; const wraparoundMode = this._terminal.wraparoundMode; @@ -608,7 +609,7 @@ export class InputHandler extends Disposable implements IInputHandler { * G1 character set. */ public shiftOut(): void { - this._coreService.setgLevel(1); + this._charsetService.setgLevel(1); } /** @@ -617,7 +618,7 @@ export class InputHandler extends Disposable implements IInputHandler { * character set (the default). */ public shiftIn(): void { - this._coreService.setgLevel(0); + this._charsetService.setgLevel(0); } /** @@ -1396,10 +1397,10 @@ export class InputHandler extends Disposable implements IInputHandler { this._coreService.decPrivateModes.applicationCursorKeys = true; break; case 2: - this._coreService.setgCharset(0, DEFAULT_CHARSET); - this._coreService.setgCharset(1, DEFAULT_CHARSET); - this._coreService.setgCharset(2, DEFAULT_CHARSET); - this._coreService.setgCharset(3, DEFAULT_CHARSET); + this._charsetService.setgCharset(0, DEFAULT_CHARSET); + this._charsetService.setgCharset(1, DEFAULT_CHARSET); + this._charsetService.setgCharset(2, DEFAULT_CHARSET); + this._charsetService.setgCharset(3, DEFAULT_CHARSET); // set VT100 mode here break; case 3: // 132 col mode @@ -1974,7 +1975,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._bufferService.buffer.scrollBottom = this._bufferService.rows - 1; this._terminal.curAttrData = DEFAULT_ATTR_DATA.clone(); this._bufferService.buffer.x = this._bufferService.buffer.y = 0; // ? - this._coreService.softReset(); + this._charsetService.reset(); } /** @@ -2038,7 +2039,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._bufferService.buffer.savedY = this._bufferService.buffer.ybase + this._bufferService.buffer.y; this._bufferService.buffer.savedCurAttrData.fg = this._terminal.curAttrData.fg; this._bufferService.buffer.savedCurAttrData.bg = this._terminal.curAttrData.bg; - this._bufferService.buffer.savedCharset = this._coreService.charsetModes.charset; + this._bufferService.buffer.savedCharset = this._charsetService.charset; } @@ -2052,9 +2053,9 @@ export class InputHandler extends Disposable implements IInputHandler { this._bufferService.buffer.y = Math.max(this._bufferService.buffer.savedY - this._bufferService.buffer.ybase, 0); this._terminal.curAttrData.fg = this._bufferService.buffer.savedCurAttrData.fg; this._terminal.curAttrData.bg = this._bufferService.buffer.savedCurAttrData.bg; - this._coreService.charsetModes.charset = (this as any)._savedCharset; + this._charsetService.charset = (this as any)._savedCharset; if (this._bufferService.buffer.savedCharset) { - this._coreService.charsetModes.charset = this._bufferService.buffer.savedCharset; + this._charsetService.charset = this._bufferService.buffer.savedCharset; } this._restrictCursor(); } @@ -2113,8 +2114,8 @@ export class InputHandler extends Disposable implements IInputHandler { * therefore ESC % G does the same. */ public selectDefaultCharset(): void { - this._coreService.setgLevel(0); - this._coreService.setgCharset(0, DEFAULT_CHARSET); // US (default) + this._charsetService.setgLevel(0); + this._charsetService.setgCharset(0, DEFAULT_CHARSET); // US (default) } /** @@ -2141,7 +2142,7 @@ export class InputHandler extends Disposable implements IInputHandler { if (collectAndFlag[0] === '/') { return; // TODO: Is this supported? } - this._coreService.setgCharset(GLEVEL[collectAndFlag[0]], CHARSETS[collectAndFlag[1]] || DEFAULT_CHARSET); + this._charsetService.setgCharset(GLEVEL[collectAndFlag[0]], CHARSETS[collectAndFlag[1]] || DEFAULT_CHARSET); return; } @@ -2220,7 +2221,7 @@ export class InputHandler extends Disposable implements IInputHandler { * you use another locking shift. (partly supported) */ public setgLevel(level: number): void { - this._coreService.setgLevel(level); + this._charsetService.setgLevel(level); } /** diff --git a/src/Terminal.ts b/src/Terminal.ts index eb199ea1..2de11f8d 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -46,7 +46,7 @@ import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; import { handleWindowsModeLineFeed } from 'common/WindowsMode'; import { ColorManager } from 'browser/ColorManager'; import { RenderService } from 'browser/services/RenderService'; -import { IOptionsService, IBufferService, ICoreMouseService, ICoreService, ILogService, IDirtyRowService, IInstantiationService } from 'common/services/Services'; +import { IOptionsService, IBufferService, ICoreMouseService, ICoreService, ILogService, IDirtyRowService, IInstantiationService, ICharsetService } from 'common/services/Services'; import { OptionsService } from 'common/services/OptionsService'; import { ICharSizeService, IRenderService, IMouseService, ISelectionService, ISoundService, ICoreBrowserService } from 'browser/services/Services'; import { CharSizeService } from 'browser/services/CharSizeService'; @@ -64,6 +64,7 @@ import { InstantiationService } from 'common/services/InstantiationService'; import { CoreMouseService } from 'common/services/CoreMouseService'; import { WriteBuffer } from 'common/input/WriteBuffer'; import { CoreBrowserService } from 'browser/services/CoreBrowserService'; +import { CharsetService } from 'common/services/CharsetService'; // Let it work inside Node.js for automated testing purposes. const document = (typeof window !== 'undefined') ? window.document : null; @@ -96,6 +97,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp // common services private _bufferService: IBufferService; private _coreService: ICoreService; + private _charsetService: ICharsetService; private _coreMouseService: ICoreMouseService; private _dirtyRowService: IDirtyRowService; private _instantiationService: IInstantiationService; @@ -221,6 +223,8 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this._instantiationService.setService(ICoreMouseService, this._coreMouseService); this._dirtyRowService = this._instantiationService.createInstance(DirtyRowService); this._instantiationService.setService(IDirtyRowService, this._dirtyRowService); + this._charsetService = this._instantiationService.createInstance(CharsetService); + this._instantiationService.setService(ICharsetService, this._charsetService); this._setupOptionsListeners(); this._setup(); @@ -254,7 +258,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this.bracketedPasteMode = false; // charset - this._coreService.softReset(); + this._charsetService.reset(); this.curAttrData = DEFAULT_ATTR_DATA.clone(); this._eraseAttrData = DEFAULT_ATTR_DATA.clone(); @@ -265,7 +269,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this._userScrolling = false; // Register input handler and refire/handle events - this._inputHandler = new InputHandler(this, this._bufferService, this._coreService, this._dirtyRowService, this._logService, this.optionsService, this._coreMouseService); + this._inputHandler = new InputHandler(this, this._bufferService, this._charsetService, this._coreService, this._dirtyRowService, this._logService, this.optionsService, this._coreMouseService); this._inputHandler.onRequestRefreshRows((start, end) => this.refresh(start, end)); this._inputHandler.onCursorMove(() => this._onCursorMove.fire()); this._inputHandler.onLineFeed(() => this._onLineFeed.fire()); diff --git a/src/common/TestUtils.test.ts b/src/common/TestUtils.test.ts index 96027804..11bb84bc 100644 --- a/src/common/TestUtils.test.ts +++ b/src/common/TestUtils.test.ts @@ -3,13 +3,13 @@ * @license MIT */ -import { IBufferService, ICoreService, ILogService, IOptionsService, ITerminalOptions, IPartialTerminalOptions, IDirtyRowService, ICoreMouseService } from 'common/services/Services'; +import { IBufferService, ICoreService, ILogService, IOptionsService, ITerminalOptions, IPartialTerminalOptions, IDirtyRowService, ICoreMouseService, ICharsetService } from 'common/services/Services'; import { IEvent, EventEmitter } from 'common/EventEmitter'; import { clone } from 'common/Clone'; import { DEFAULT_OPTIONS } from 'common/services/OptionsService'; import { IBufferSet, IBuffer } from 'common/buffer/Types'; import { BufferSet } from 'common/buffer/BufferSet'; -import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEventType, ICharsetModes, ICharset } from 'common/Types'; +import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEventType, ICharset } from 'common/Types'; export class MockBufferService implements IBufferService { serviceBrand: any; @@ -42,24 +42,26 @@ export class MockCoreMouseService implements ICoreMouseService { } } +export class MockCharsetService implements ICharsetService { + serviceBrand: any; + charset: ICharset | undefined; + glevel: number = 0; + charsets: readonly ICharset[] = []; + reset(): void {} + setgLevel(g: number): void {} + setgCharset(g: number, charset: ICharset): void {} +} + export class MockCoreService implements ICoreService { + serviceBrand: any; isCursorInitialized: boolean = false; isCursorHidden: boolean = false; isFocused: boolean = false; - serviceBrand: any; - charsetModes: ICharsetModes = { - charset: undefined, - charsets: [], - glevel: 0 - }; decPrivateModes: IDecPrivateModes = {} as any; onData: IEvent = new EventEmitter().event; onUserInput: IEvent = new EventEmitter().event; onBinary: IEvent = new EventEmitter().event; reset(): void {} - softReset(): void {} - setgLevel(g: number): void {} - setgCharset(g: number, charset: ICharset): void {} triggerDataEvent(data: string, wasUserInput?: boolean): void {} triggerBinaryEvent(data: string): void {} } diff --git a/src/common/Types.d.ts b/src/common/Types.d.ts index b8cb9b80..35567dee 100644 --- a/src/common/Types.d.ts +++ b/src/common/Types.d.ts @@ -62,13 +62,7 @@ export interface IKeyboardResult { } export interface ICharset { - [key: string]: string; -} - -export interface ICharsetModes { - charset: ICharset | undefined; - glevel: number; - charsets: ICharset[]; + [key: string]: string | undefined; } export type CharData = [number, string, number, number]; diff --git a/src/common/services/CharsetService.ts b/src/common/services/CharsetService.ts new file mode 100644 index 00000000..5d20628f --- /dev/null +++ b/src/common/services/CharsetService.ts @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { ICharsetService } from 'common/services/Services'; +import { ICharset } from 'common/Types'; + +export class CharsetService implements ICharsetService { + serviceBrand: any; + + public charset: ICharset | undefined; + public charsets: ICharset[] = []; + public glevel: number = 0; + + public reset(): void { + this.charset = undefined; + this.charsets = []; + this.glevel = 0; + } + + public setgLevel(g: number): void { + this.glevel = g; + this.charset = this.charsets[g]; + } + + public setgCharset(g: number, charset: ICharset): void { + this.charsets[g] = charset; + if (this.glevel === g) { + this.charset = charset; + } + } +} diff --git a/src/common/services/CoreService.ts b/src/common/services/CoreService.ts index af0d5f63..58552f3f 100644 --- a/src/common/services/CoreService.ts +++ b/src/common/services/CoreService.ts @@ -5,7 +5,7 @@ import { ICoreService, ILogService, IOptionsService, IBufferService } from 'common/services/Services'; import { EventEmitter, IEvent } from 'common/EventEmitter'; -import { IDecPrivateModes, ICharset, ICharsetModes } from 'common/Types'; +import { IDecPrivateModes, ICharset } from 'common/Types'; import { clone } from 'common/Clone'; const DEFAULT_DEC_PRIVATE_MODES: IDecPrivateModes = Object.freeze({ @@ -18,11 +18,6 @@ export class CoreService implements ICoreService { public isCursorInitialized: boolean = false; public isCursorHidden: boolean = false; public decPrivateModes: IDecPrivateModes; - public charsetModes: ICharsetModes = { - charset: undefined, - charsets: [], - glevel: 0 - }; private _onData = new EventEmitter(); public get onData(): IEvent { return this._onData.event; } @@ -45,12 +40,6 @@ export class CoreService implements ICoreService { this.decPrivateModes = clone(DEFAULT_DEC_PRIVATE_MODES); } - public softReset(): void { - this.charsetModes.charset = undefined; - this.charsetModes.charsets = []; - this.charsetModes.glevel = 0; - } - public triggerDataEvent(data: string, wasUserInput: boolean = false): void { // Prevents all events to pty process if stdin is disabled if (this._optionsService.options.disableStdin) { @@ -80,16 +69,4 @@ export class CoreService implements ICoreService { this._logService.debug(`sending binary "${data}"`, () => data.split('').map(e => e.charCodeAt(0))); this._onBinary.fire(data); } - - public setgLevel(g: number): void { - this.charsetModes.glevel = g; - this.charsetModes.charset = this.charsetModes.charsets[g]; - } - - public setgCharset(g: number, charset: ICharset): void { - this.charsetModes.charsets[g] = charset; - if (this.charsetModes.glevel === g) { - this.charsetModes.charset = charset; - } - } } diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index 9e47be26..32274f5e 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -5,7 +5,7 @@ import { IEvent } from 'common/EventEmitter'; import { IBuffer, IBufferSet } from 'common/buffer/Types'; -import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEncoding, ICoreMouseProtocol, CoreMouseEventType, ICharsetModes, ICharset } from 'common/Types'; +import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEncoding, ICoreMouseProtocol, CoreMouseEventType, ICharset } from 'common/Types'; import { createDecorator } from 'common/services/ServiceRegistry'; export const IBufferService = createDecorator('BufferService'); @@ -65,8 +65,6 @@ export interface ICoreService { isCursorInitialized: boolean; isCursorHidden: boolean; - charsetModes: ICharsetModes; - readonly decPrivateModes: IDecPrivateModes; readonly onData: IEvent; @@ -74,7 +72,6 @@ export interface ICoreService { readonly onBinary: IEvent; reset(): void; - softReset(): void; /** * Triggers the onData event in the public API. @@ -91,6 +88,17 @@ export interface ICoreService { * @param data The data that is being emitted. */ triggerBinaryEvent(data: string): void; +} + +export const ICharsetService = createDecorator('CharsetService'); +export interface ICharsetService { + serviceBrand: any; + + charset: ICharset | undefined; + readonly glevel: number; + readonly charsets: ReadonlyArray; + + reset(): void; /** * Set the G level of the terminal. From bff74b1d7d58ed0dac245bee17bd46d24c416390 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 18 Dec 2019 11:16:23 +1100 Subject: [PATCH 20/48] Move wraparound mode into core service modes --- src/InputHandler.ts | 9 ++++----- src/Terminal.test.ts | 12 ++---------- src/Terminal.ts | 3 +-- src/TestUtils.test.ts | 1 - src/Types.d.ts | 1 - src/common/TestUtils.test.ts | 5 ++++- src/common/Types.d.ts | 1 + src/common/services/CoreService.ts | 3 ++- 8 files changed, 14 insertions(+), 21 deletions(-) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 79c769c7..750d03e9 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -385,7 +385,7 @@ export class InputHandler extends Disposable implements IInputHandler { const charset = this._charsetService.charset; const screenReaderMode = this._optionsService.options.screenReaderMode; const cols = this._bufferService.cols; - const wraparoundMode = this._terminal.wraparoundMode; + const wraparoundMode = this._coreService.decPrivateModes.wraparound; const insertMode = this._terminal.insertMode; const curAttr = this._terminal.curAttrData; let bufferRow = buffer.lines.get(buffer.y + buffer.ybase); @@ -1414,7 +1414,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._setCursor(0, 0); break; case 7: - this._terminal.wraparoundMode = true; + this._coreService.decPrivateModes.wraparound = true; break; case 12: // this.cursorBlink = true; @@ -1597,7 +1597,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._setCursor(0, 0); break; case 7: - this._terminal.wraparoundMode = false; + this._coreService.decPrivateModes.wraparound = false; break; case 12: // this.cursorBlink = false; @@ -1965,16 +1965,15 @@ export class InputHandler extends Disposable implements IInputHandler { this._coreService.isCursorHidden = false; this._terminal.insertMode = false; this._terminal.originMode = false; - this._terminal.wraparoundMode = true; // defaults: xterm - true, vt100 - false this._terminal.applicationKeypad = false; // ? if (this._terminal.viewport) { this._terminal.viewport.syncScrollArea(); } - this._coreService.decPrivateModes.applicationCursorKeys = false; this._bufferService.buffer.scrollTop = 0; this._bufferService.buffer.scrollBottom = this._bufferService.rows - 1; this._terminal.curAttrData = DEFAULT_ATTR_DATA.clone(); this._bufferService.buffer.x = this._bufferService.buffer.y = 0; // ? + this._coreService.reset(); this._charsetService.reset(); } diff --git a/src/Terminal.test.ts b/src/Terminal.test.ts index b55a516b..062bddf4 100644 --- a/src/Terminal.test.ts +++ b/src/Terminal.test.ts @@ -747,7 +747,7 @@ describe('Terminal', () => { const cell = new CellData(); for (let i = 0xDC00; i <= 0xDCFF; ++i) { term.buffer.x = term.cols - 1; - term.wraparoundMode = true; + term.writeSync('a' + high + String.fromCharCode(i)); expect(term.buffer.lines.get(0).loadCell(term.cols - 1, cell).getChars()).eql('a'); expect(term.buffer.lines.get(1).loadCell(0, cell).getChars()).eql(high + String.fromCharCode(i)); @@ -761,7 +761,7 @@ describe('Terminal', () => { const cell = new CellData(); for (let i = 0xDC00; i <= 0xDCFF; ++i) { term.buffer.x = term.cols - 1; - term.wraparoundMode = false; + term.writeSync('\x1b[?7l'); // Disable wraparound mode const width = wcwidth((0xD800 - 0xD800) * 0x400 + i - 0xDC00 + 0x10000); if (width !== 1) { continue; @@ -812,7 +812,6 @@ describe('Terminal', () => { expect(cell.getWidth()).eql(1); }); it('multiple combined é', () => { - term.wraparoundMode = true; term.writeSync(Array(100).join('e\u0301')); for (let i = 0; i < term.cols; ++i) { term.buffer.lines.get(0).loadCell(i, cell); @@ -826,7 +825,6 @@ describe('Terminal', () => { expect(cell.getWidth()).eql(1); }); it('multiple surrogate with combined', () => { - term.wraparoundMode = true; term.writeSync(Array(100).join('\uD800\uDC00\u0301')); for (let i = 0; i < term.cols; ++i) { term.buffer.lines.get(0).loadCell(i, cell); @@ -855,7 +853,6 @@ describe('Terminal', () => { expect(term.buffer.x).eql(3); }); it('line of ¥ even', () => { - term.wraparoundMode = true; term.writeSync(Array(50).join('¥')); for (let i = 0; i < term.cols; ++i) { term.buffer.lines.get(0).loadCell(i, cell); @@ -875,7 +872,6 @@ describe('Terminal', () => { expect(cell.getWidth()).eql(2); }); it('line of ¥ odd', () => { - term.wraparoundMode = true; term.buffer.x = 1; term.writeSync(Array(50).join('¥')); for (let i = 1; i < term.cols - 1; ++i) { @@ -900,7 +896,6 @@ describe('Terminal', () => { expect(cell.getWidth()).eql(2); }); it('line of ¥ with combining odd', () => { - term.wraparoundMode = true; term.buffer.x = 1; term.writeSync(Array(50).join('¥\u0301')); for (let i = 1; i < term.cols - 1; ++i) { @@ -925,7 +920,6 @@ describe('Terminal', () => { expect(cell.getWidth()).eql(2); }); it('line of ¥ with combining even', () => { - term.wraparoundMode = true; term.writeSync(Array(50).join('¥\u0301')); for (let i = 0; i < term.cols; ++i) { term.buffer.lines.get(0).loadCell(i, cell); @@ -945,7 +939,6 @@ describe('Terminal', () => { expect(cell.getWidth()).eql(2); }); it('line of surrogate fullwidth with combining odd', () => { - term.wraparoundMode = true; term.buffer.x = 1; term.writeSync(Array(50).join('\ud843\ude6d\u0301')); for (let i = 1; i < term.cols - 1; ++i) { @@ -970,7 +963,6 @@ describe('Terminal', () => { expect(cell.getWidth()).eql(2); }); it('line of surrogate fullwidth with combining even', () => { - term.wraparoundMode = true; term.writeSync(Array(50).join('\ud843\ude6d\u0301')); for (let i = 0; i < term.cols; ++i) { term.buffer.lines.get(0).loadCell(i, cell); diff --git a/src/Terminal.ts b/src/Terminal.ts index 2de11f8d..0a76aa62 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -115,7 +115,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp public applicationKeypad: boolean; public originMode: boolean; public insertMode: boolean; - public wraparoundMode: boolean; // defaults: xterm - true, vt100 - false public bracketedPasteMode: boolean; // mouse properties @@ -254,7 +253,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this.applicationKeypad = false; this.originMode = false; this.insertMode = false; - this.wraparoundMode = true; // defaults: xterm - true, vt100 - false + // this._coreService.decPrivateModes.wraparound = true; this.bracketedPasteMode = false; // charset diff --git a/src/TestUtils.test.ts b/src/TestUtils.test.ts index 8954ae83..b3aa7358 100644 --- a/src/TestUtils.test.ts +++ b/src/TestUtils.test.ts @@ -205,7 +205,6 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal { applicationCursor: boolean; originMode: boolean; insertMode: boolean; - wraparoundMode: boolean; bracketedPasteMode: boolean; curAttrData = new AttributeData(); savedCols: number; diff --git a/src/Types.d.ts b/src/Types.d.ts index 3a3aef62..98982133 100644 --- a/src/Types.d.ts +++ b/src/Types.d.ts @@ -28,7 +28,6 @@ export interface IInputHandlingTerminal { applicationKeypad: boolean; originMode: boolean; insertMode: boolean; - wraparoundMode: boolean; bracketedPasteMode: boolean; curAttrData: IAttributeData; savedCols: number; diff --git a/src/common/TestUtils.test.ts b/src/common/TestUtils.test.ts index 11bb84bc..65a68cbd 100644 --- a/src/common/TestUtils.test.ts +++ b/src/common/TestUtils.test.ts @@ -57,7 +57,10 @@ export class MockCoreService implements ICoreService { isCursorInitialized: boolean = false; isCursorHidden: boolean = false; isFocused: boolean = false; - decPrivateModes: IDecPrivateModes = {} as any; + decPrivateModes: IDecPrivateModes = { + applicationCursorKeys: false, + wraparound: true + }; onData: IEvent = new EventEmitter().event; onUserInput: IEvent = new EventEmitter().event; onBinary: IEvent = new EventEmitter().event; diff --git a/src/common/Types.d.ts b/src/common/Types.d.ts index 35567dee..67434425 100644 --- a/src/common/Types.d.ts +++ b/src/common/Types.d.ts @@ -152,6 +152,7 @@ export interface IMarker extends IDisposable { export interface IDecPrivateModes { applicationCursorKeys: boolean; + wraparound: boolean; // defaults: xterm - true, vt100 - false } export interface IRowRange { diff --git a/src/common/services/CoreService.ts b/src/common/services/CoreService.ts index 58552f3f..f5f1c438 100644 --- a/src/common/services/CoreService.ts +++ b/src/common/services/CoreService.ts @@ -9,7 +9,8 @@ import { IDecPrivateModes, ICharset } from 'common/Types'; import { clone } from 'common/Clone'; const DEFAULT_DEC_PRIVATE_MODES: IDecPrivateModes = Object.freeze({ - applicationCursorKeys: false + applicationCursorKeys: false, + wraparound: true // defaults: xterm - true, vt100 - false }); export class CoreService implements ICoreService { From dada7fe728798503707ebaca295cf025fd0415e6 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 18 Dec 2019 11:20:36 +1100 Subject: [PATCH 21/48] Move origin mode into core service --- src/InputHandler.ts | 9 ++++----- src/Types.d.ts | 1 - src/common/TestUtils.test.ts | 1 + src/common/Types.d.ts | 1 + src/common/services/CoreService.ts | 1 + 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 750d03e9..681fc7a4 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -626,7 +626,7 @@ export class InputHandler extends Disposable implements IInputHandler { */ private _restrictCursor(): void { this._bufferService.buffer.x = Math.min(this._bufferService.cols - 1, Math.max(0, this._bufferService.buffer.x)); - this._bufferService.buffer.y = this._terminal.originMode + this._bufferService.buffer.y = this._coreService.decPrivateModes.origin ? Math.min(this._bufferService.buffer.scrollBottom, Math.max(this._bufferService.buffer.scrollTop, this._bufferService.buffer.y)) : Math.min(this._bufferService.rows - 1, Math.max(0, this._bufferService.buffer.y)); this._dirtyRowService.markDirty(this._bufferService.buffer.y); @@ -637,7 +637,7 @@ export class InputHandler extends Disposable implements IInputHandler { */ private _setCursor(x: number, y: number): void { this._dirtyRowService.markDirty(this._bufferService.buffer.y); - if (this._terminal.originMode) { + if (this._coreService.decPrivateModes.origin) { this._bufferService.buffer.x = x; this._bufferService.buffer.y = this._bufferService.buffer.scrollTop + y; } else { @@ -1410,7 +1410,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._terminal.reset(); break; case 6: - this._terminal.originMode = true; + this._coreService.decPrivateModes.origin = true; this._setCursor(0, 0); break; case 7: @@ -1593,7 +1593,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._terminal.reset(); break; case 6: - this._terminal.originMode = false; + this._coreService.decPrivateModes.origin = false; this._setCursor(0, 0); break; case 7: @@ -1964,7 +1964,6 @@ export class InputHandler extends Disposable implements IInputHandler { public softReset(params: IParams): void { this._coreService.isCursorHidden = false; this._terminal.insertMode = false; - this._terminal.originMode = false; this._terminal.applicationKeypad = false; // ? if (this._terminal.viewport) { this._terminal.viewport.syncScrollArea(); diff --git a/src/Types.d.ts b/src/Types.d.ts index 98982133..095fdf35 100644 --- a/src/Types.d.ts +++ b/src/Types.d.ts @@ -26,7 +26,6 @@ export interface IInputHandlingTerminal { cols: number; rows: number; applicationKeypad: boolean; - originMode: boolean; insertMode: boolean; bracketedPasteMode: boolean; curAttrData: IAttributeData; diff --git a/src/common/TestUtils.test.ts b/src/common/TestUtils.test.ts index 65a68cbd..e556b371 100644 --- a/src/common/TestUtils.test.ts +++ b/src/common/TestUtils.test.ts @@ -59,6 +59,7 @@ export class MockCoreService implements ICoreService { isFocused: boolean = false; decPrivateModes: IDecPrivateModes = { applicationCursorKeys: false, + origin: false, wraparound: true }; onData: IEvent = new EventEmitter().event; diff --git a/src/common/Types.d.ts b/src/common/Types.d.ts index 67434425..a3887e04 100644 --- a/src/common/Types.d.ts +++ b/src/common/Types.d.ts @@ -152,6 +152,7 @@ export interface IMarker extends IDisposable { export interface IDecPrivateModes { applicationCursorKeys: boolean; + origin: boolean; wraparound: boolean; // defaults: xterm - true, vt100 - false } diff --git a/src/common/services/CoreService.ts b/src/common/services/CoreService.ts index f5f1c438..4a94b0bb 100644 --- a/src/common/services/CoreService.ts +++ b/src/common/services/CoreService.ts @@ -10,6 +10,7 @@ import { clone } from 'common/Clone'; const DEFAULT_DEC_PRIVATE_MODES: IDecPrivateModes = Object.freeze({ applicationCursorKeys: false, + origin: false, wraparound: true // defaults: xterm - true, vt100 - false }); From dc61fae12a7a6a7aeb4b8fd162c36869d4934dc3 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 18 Dec 2019 11:23:57 +1100 Subject: [PATCH 22/48] Move applicationKeypad into core service --- src/InputHandler.ts | 10 +++++----- src/Terminal.ts | 5 ----- src/TestUtils.test.ts | 3 --- src/Types.d.ts | 1 - src/common/TestUtils.test.ts | 1 + src/common/Types.d.ts | 1 + src/common/services/CoreService.ts | 1 + 7 files changed, 8 insertions(+), 14 deletions(-) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 681fc7a4..a154379a 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -1421,7 +1421,7 @@ export class InputHandler extends Disposable implements IInputHandler { break; case 66: this._logService.debug('Serial port requested application keypad.'); - this._terminal.applicationKeypad = true; + this._coreService.decPrivateModes.applicationKeypad = true; if (this._terminal.viewport) { this._terminal.viewport.syncScrollArea(); } @@ -1604,7 +1604,7 @@ export class InputHandler extends Disposable implements IInputHandler { break; case 66: this._logService.debug('Switching back to normal keypad.'); - this._terminal.applicationKeypad = false; + this._coreService.decPrivateModes.applicationKeypad = false; if (this._terminal.viewport) { this._terminal.viewport.syncScrollArea(); } @@ -1964,7 +1964,7 @@ export class InputHandler extends Disposable implements IInputHandler { public softReset(params: IParams): void { this._coreService.isCursorHidden = false; this._terminal.insertMode = false; - this._terminal.applicationKeypad = false; // ? + this._coreService.decPrivateModes.applicationKeypad = false; // ? if (this._terminal.viewport) { this._terminal.viewport.syncScrollArea(); } @@ -2086,7 +2086,7 @@ export class InputHandler extends Disposable implements IInputHandler { */ public keypadApplicationMode(): void { this._logService.debug('Serial port requested application keypad.'); - this._terminal.applicationKeypad = true; + this._coreService.decPrivateModes.applicationKeypad = true; if (this._terminal.viewport) { this._terminal.viewport.syncScrollArea(); } @@ -2099,7 +2099,7 @@ export class InputHandler extends Disposable implements IInputHandler { */ public keypadNumericMode(): void { this._logService.debug('Switching back to normal keypad.'); - this._terminal.applicationKeypad = false; + this._coreService.decPrivateModes.applicationKeypad = false; if (this._terminal.viewport) { this._terminal.viewport.syncScrollArea(); } diff --git a/src/Terminal.ts b/src/Terminal.ts index 0a76aa62..6d0bbea2 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -112,8 +112,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp private _soundService: ISoundService; // modes - public applicationKeypad: boolean; - public originMode: boolean; public insertMode: boolean; public bracketedPasteMode: boolean; @@ -250,10 +248,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this._customKeyEventHandler = null; // modes - this.applicationKeypad = false; - this.originMode = false; this.insertMode = false; - // this._coreService.decPrivateModes.wraparound = true; this.bracketedPasteMode = false; // charset diff --git a/src/TestUtils.test.ts b/src/TestUtils.test.ts index b3aa7358..cf23b8eb 100644 --- a/src/TestUtils.test.ts +++ b/src/TestUtils.test.ts @@ -201,9 +201,6 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal { options: ITerminalOptions = {}; cols: number; rows: number; - applicationKeypad: boolean; - applicationCursor: boolean; - originMode: boolean; insertMode: boolean; bracketedPasteMode: boolean; curAttrData = new AttributeData(); diff --git a/src/Types.d.ts b/src/Types.d.ts index 095fdf35..7742b69c 100644 --- a/src/Types.d.ts +++ b/src/Types.d.ts @@ -25,7 +25,6 @@ export interface IInputHandlingTerminal { options: ITerminalOptions; cols: number; rows: number; - applicationKeypad: boolean; insertMode: boolean; bracketedPasteMode: boolean; curAttrData: IAttributeData; diff --git a/src/common/TestUtils.test.ts b/src/common/TestUtils.test.ts index e556b371..58147748 100644 --- a/src/common/TestUtils.test.ts +++ b/src/common/TestUtils.test.ts @@ -59,6 +59,7 @@ export class MockCoreService implements ICoreService { isFocused: boolean = false; decPrivateModes: IDecPrivateModes = { applicationCursorKeys: false, + applicationKeypad: false, origin: false, wraparound: true }; diff --git a/src/common/Types.d.ts b/src/common/Types.d.ts index a3887e04..9b8a18b5 100644 --- a/src/common/Types.d.ts +++ b/src/common/Types.d.ts @@ -152,6 +152,7 @@ export interface IMarker extends IDisposable { export interface IDecPrivateModes { applicationCursorKeys: boolean; + applicationKeypad: boolean; origin: boolean; wraparound: boolean; // defaults: xterm - true, vt100 - false } diff --git a/src/common/services/CoreService.ts b/src/common/services/CoreService.ts index 4a94b0bb..df9161e9 100644 --- a/src/common/services/CoreService.ts +++ b/src/common/services/CoreService.ts @@ -10,6 +10,7 @@ import { clone } from 'common/Clone'; const DEFAULT_DEC_PRIVATE_MODES: IDecPrivateModes = Object.freeze({ applicationCursorKeys: false, + applicationKeypad: false, origin: false, wraparound: true // defaults: xterm - true, vt100 - false }); From e3e59c35fc492026d2cf1149b815c40dd072077b Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 18 Dec 2019 11:28:15 +1100 Subject: [PATCH 23/48] Use optional chaining in InputHandler --- src/InputHandler.ts | 28 +++++++--------------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index a154379a..acf149ed 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -1422,9 +1422,7 @@ export class InputHandler extends Disposable implements IInputHandler { case 66: this._logService.debug('Serial port requested application keypad.'); this._coreService.decPrivateModes.applicationKeypad = true; - if (this._terminal.viewport) { - this._terminal.viewport.syncScrollArea(); - } + this._terminal.viewport?.syncScrollArea(); break; case 9: // X10 Mouse // no release, no motion, no wheel, no modifiers. @@ -1469,9 +1467,7 @@ export class InputHandler extends Disposable implements IInputHandler { case 1047: // alt screen buffer this._bufferService.buffers.activateAltBuffer(this._terminal.eraseAttrData()); this._onRequestRefreshRows.fire(0, this._bufferService.rows - 1); - if (this._terminal.viewport) { - this._terminal.viewport.syncScrollArea(); - } + this._terminal.viewport?.syncScrollArea(); this._terminal.showCursor(); break; case 2004: // bracketed paste mode (https://cirw.in/blog/bracketed-paste) @@ -1605,9 +1601,7 @@ export class InputHandler extends Disposable implements IInputHandler { case 66: this._logService.debug('Switching back to normal keypad.'); this._coreService.decPrivateModes.applicationKeypad = false; - if (this._terminal.viewport) { - this._terminal.viewport.syncScrollArea(); - } + this._terminal.viewport?.syncScrollArea(); break; case 9: // X10 Mouse case 1000: // vt200 mouse @@ -1643,9 +1637,7 @@ export class InputHandler extends Disposable implements IInputHandler { this.restoreCursor(); } this._onRequestRefreshRows.fire(0, this._bufferService.rows - 1); - if (this._terminal.viewport) { - this._terminal.viewport.syncScrollArea(); - } + this._terminal.viewport?.syncScrollArea(); this._terminal.showCursor(); break; case 2004: // bracketed paste mode (https://cirw.in/blog/bracketed-paste) @@ -1965,9 +1957,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._coreService.isCursorHidden = false; this._terminal.insertMode = false; this._coreService.decPrivateModes.applicationKeypad = false; // ? - if (this._terminal.viewport) { - this._terminal.viewport.syncScrollArea(); - } + this._terminal.viewport?.syncScrollArea(); this._bufferService.buffer.scrollTop = 0; this._bufferService.buffer.scrollBottom = this._bufferService.rows - 1; this._terminal.curAttrData = DEFAULT_ATTR_DATA.clone(); @@ -2087,9 +2077,7 @@ export class InputHandler extends Disposable implements IInputHandler { public keypadApplicationMode(): void { this._logService.debug('Serial port requested application keypad.'); this._coreService.decPrivateModes.applicationKeypad = true; - if (this._terminal.viewport) { - this._terminal.viewport.syncScrollArea(); - } + this._terminal.viewport?.syncScrollArea(); } /** @@ -2100,9 +2088,7 @@ export class InputHandler extends Disposable implements IInputHandler { public keypadNumericMode(): void { this._logService.debug('Switching back to normal keypad.'); this._coreService.decPrivateModes.applicationKeypad = false; - if (this._terminal.viewport) { - this._terminal.viewport.syncScrollArea(); - } + this._terminal.viewport?.syncScrollArea(); } /** From 2bbda38488e4b246977d04850a48f77ae0b51128 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 18 Dec 2019 11:31:08 +1100 Subject: [PATCH 24/48] Remove redundant value reset Done in core service reset --- src/InputHandler.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index acf149ed..21b19475 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -1956,7 +1956,6 @@ export class InputHandler extends Disposable implements IInputHandler { public softReset(params: IParams): void { this._coreService.isCursorHidden = false; this._terminal.insertMode = false; - this._coreService.decPrivateModes.applicationKeypad = false; // ? this._terminal.viewport?.syncScrollArea(); this._bufferService.buffer.scrollTop = 0; this._bufferService.buffer.scrollBottom = this._bufferService.rows - 1; From 7a1bef7daf2e9f74529a607aa968eda1595ce649 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 18 Dec 2019 11:47:45 +1100 Subject: [PATCH 25/48] More cur/erase attr data to input handler --- src/InputHandler.ts | 68 +++++++++++++++++++++++++------------------ src/Terminal.test.ts | 18 ++++++------ src/Terminal.ts | 18 +----------- src/TestUtils.test.ts | 1 + src/Types.d.ts | 4 +-- 5 files changed, 52 insertions(+), 57 deletions(-) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 21b19475..d01ebc5e 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -128,6 +128,9 @@ export class InputHandler extends Disposable implements IInputHandler { private _utf8Decoder: Utf8ToUtf32 = new Utf8ToUtf32(); private _workCell: CellData = new CellData(); + private _curAttrData: IAttributeData = DEFAULT_ATTR_DATA.clone(); + private _eraseAttrDataInternal: IAttributeData = DEFAULT_ATTR_DATA.clone(); + private _onRequestRefreshRows = new EventEmitter(); public get onRequestRefreshRows(): IEvent { return this._onRequestRefreshRows.event; } private _onCursorMove = new EventEmitter(); @@ -387,7 +390,7 @@ export class InputHandler extends Disposable implements IInputHandler { const cols = this._bufferService.cols; const wraparoundMode = this._coreService.decPrivateModes.wraparound; const insertMode = this._terminal.insertMode; - const curAttr = this._terminal.curAttrData; + const curAttr = this._curAttrData; let bufferRow = buffer.lines.get(buffer.y + buffer.ybase); this._dirtyRowService.markDirty(buffer.y); @@ -442,7 +445,7 @@ export class InputHandler extends Disposable implements IInputHandler { buffer.y++; if (buffer.y === buffer.scrollBottom + 1) { buffer.y--; - this._terminal.scroll(true); + this._terminal.scroll(this._eraseAttrData(), true); } else { if (buffer.y >= this._bufferService.rows) { buffer.y = this._bufferService.rows - 1; @@ -556,7 +559,7 @@ export class InputHandler extends Disposable implements IInputHandler { buffer.y++; if (buffer.y === buffer.scrollBottom + 1) { buffer.y--; - this._terminal.scroll(); + this._terminal.scroll(this._eraseAttrData()); } else if (buffer.y >= this._bufferService.rows) { buffer.y = this._bufferService.rows - 1; } @@ -848,7 +851,7 @@ export class InputHandler extends Disposable implements IInputHandler { line.replaceCells( start, end, - this._bufferService.buffer.getNullCell(this._terminal.eraseAttrData()) + this._bufferService.buffer.getNullCell(this._eraseAttrData()) ); if (clearWrap) { line.isWrapped = false; @@ -862,7 +865,7 @@ export class InputHandler extends Disposable implements IInputHandler { */ private _resetBufferLine(y: number): void { const line = this._bufferService.buffer.lines.get(this._bufferService.buffer.ybase + y); - line.fill(this._bufferService.buffer.getNullCell(this._terminal.eraseAttrData())); + line.fill(this._bufferService.buffer.getNullCell(this._eraseAttrData())); line.isWrapped = false; } @@ -977,7 +980,7 @@ export class InputHandler extends Disposable implements IInputHandler { // test: echo -e '\e[44m\e[1L\e[0m' // blankLine(true) - xterm/linux behavior buffer.lines.splice(scrollBottomAbsolute - 1, 1); - buffer.lines.splice(row, 0, buffer.getBlankLine(this._terminal.eraseAttrData())); + buffer.lines.splice(row, 0, buffer.getBlankLine(this._eraseAttrData())); } this._dirtyRowService.markRangeDirty(buffer.y, buffer.scrollBottom); @@ -1008,7 +1011,7 @@ export class InputHandler extends Disposable implements IInputHandler { // test: echo -e '\e[44m\e[1M\e[0m' // blankLine(true) - xterm/linux behavior buffer.lines.splice(row, 1); - buffer.lines.splice(j, 0, buffer.getBlankLine(this._terminal.eraseAttrData())); + buffer.lines.splice(j, 0, buffer.getBlankLine(this._eraseAttrData())); } this._dirtyRowService.markRangeDirty(buffer.y, buffer.scrollBottom); @@ -1026,7 +1029,7 @@ export class InputHandler extends Disposable implements IInputHandler { line.insertCells( this._bufferService.buffer.x, params.params[0] || 1, - this._bufferService.buffer.getNullCell(this._terminal.eraseAttrData()) + this._bufferService.buffer.getNullCell(this._eraseAttrData()) ); this._dirtyRowService.markDirty(this._bufferService.buffer.y); } @@ -1043,7 +1046,7 @@ export class InputHandler extends Disposable implements IInputHandler { line.deleteCells( this._bufferService.buffer.x, params.params[0] || 1, - this._bufferService.buffer.getNullCell(this._terminal.eraseAttrData()) + this._bufferService.buffer.getNullCell(this._eraseAttrData()) ); this._dirtyRowService.markDirty(this._bufferService.buffer.y); } @@ -1060,7 +1063,7 @@ export class InputHandler extends Disposable implements IInputHandler { while (param--) { buffer.lines.splice(buffer.ybase + buffer.scrollTop, 1); - buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 0, buffer.getBlankLine(this._terminal.eraseAttrData())); + buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 0, buffer.getBlankLine(this._eraseAttrData())); } this._dirtyRowService.markRangeDirty(buffer.scrollTop, buffer.scrollBottom); } @@ -1103,7 +1106,7 @@ export class InputHandler extends Disposable implements IInputHandler { const param = params.params[0] || 1; for (let y = buffer.scrollTop; y <= buffer.scrollBottom; ++y) { const line = buffer.lines.get(buffer.ybase + y); - line.deleteCells(0, param, buffer.getNullCell(this._terminal.eraseAttrData())); + line.deleteCells(0, param, buffer.getNullCell(this._eraseAttrData())); line.isWrapped = false; } this._dirtyRowService.markRangeDirty(buffer.scrollTop, buffer.scrollBottom); @@ -1131,7 +1134,7 @@ export class InputHandler extends Disposable implements IInputHandler { const param = params.params[0] || 1; for (let y = buffer.scrollTop; y <= buffer.scrollBottom; ++y) { const line = buffer.lines.get(buffer.ybase + y); - line.insertCells(0, param, buffer.getNullCell(this._terminal.eraseAttrData())); + line.insertCells(0, param, buffer.getNullCell(this._eraseAttrData())); line.isWrapped = false; } this._dirtyRowService.markRangeDirty(buffer.scrollTop, buffer.scrollBottom); @@ -1149,7 +1152,7 @@ export class InputHandler extends Disposable implements IInputHandler { const param = params.params[0] || 1; for (let y = buffer.scrollTop; y <= buffer.scrollBottom; ++y) { const line = this._bufferService.buffer.lines.get(buffer.ybase + y); - line.insertCells(buffer.x, param, buffer.getNullCell(this._terminal.eraseAttrData())); + line.insertCells(buffer.x, param, buffer.getNullCell(this._eraseAttrData())); line.isWrapped = false; } this._dirtyRowService.markRangeDirty(buffer.scrollTop, buffer.scrollBottom); @@ -1167,7 +1170,7 @@ export class InputHandler extends Disposable implements IInputHandler { const param = params.params[0] || 1; for (let y = buffer.scrollTop; y <= buffer.scrollBottom; ++y) { const line = buffer.lines.get(buffer.ybase + y); - line.deleteCells(buffer.x, param, buffer.getNullCell(this._terminal.eraseAttrData())); + line.deleteCells(buffer.x, param, buffer.getNullCell(this._eraseAttrData())); line.isWrapped = false; } this._dirtyRowService.markRangeDirty(buffer.scrollTop, buffer.scrollBottom); @@ -1184,7 +1187,7 @@ export class InputHandler extends Disposable implements IInputHandler { line.replaceCells( this._bufferService.buffer.x, this._bufferService.buffer.x + (params.params[0] || 1), - this._bufferService.buffer.getNullCell(this._terminal.eraseAttrData()) + this._bufferService.buffer.getNullCell(this._eraseAttrData()) ); this._dirtyRowService.markDirty(this._bufferService.buffer.y); } @@ -1465,7 +1468,7 @@ export class InputHandler extends Disposable implements IInputHandler { // FALL-THROUGH case 47: // alt screen buffer case 1047: // alt screen buffer - this._bufferService.buffers.activateAltBuffer(this._terminal.eraseAttrData()); + this._bufferService.buffers.activateAltBuffer(this._eraseAttrData()); this._onRequestRefreshRows.fire(0, this._bufferService.rows - 1); this._terminal.viewport?.syncScrollArea(); this._terminal.showCursor(); @@ -1787,14 +1790,14 @@ export class InputHandler extends Disposable implements IInputHandler { public charAttributes(params: IParams): void { // Optimize a single SGR0. if (params.length === 1 && params.params[0] === 0) { - this._terminal.curAttrData.fg = DEFAULT_ATTR_DATA.fg; - this._terminal.curAttrData.bg = DEFAULT_ATTR_DATA.bg; + this._curAttrData.fg = DEFAULT_ATTR_DATA.fg; + this._curAttrData.bg = DEFAULT_ATTR_DATA.bg; return; } const l = params.length; let p; - const attr = this._terminal.curAttrData; + const attr = this._curAttrData; for (let i = 0; i < l; i++) { p = params.params[i]; @@ -1959,7 +1962,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._terminal.viewport?.syncScrollArea(); this._bufferService.buffer.scrollTop = 0; this._bufferService.buffer.scrollBottom = this._bufferService.rows - 1; - this._terminal.curAttrData = DEFAULT_ATTR_DATA.clone(); + this._curAttrData = DEFAULT_ATTR_DATA.clone(); this._bufferService.buffer.x = this._bufferService.buffer.y = 0; // ? this._coreService.reset(); this._charsetService.reset(); @@ -2024,8 +2027,8 @@ export class InputHandler extends Disposable implements IInputHandler { public saveCursor(params?: IParams): void { this._bufferService.buffer.savedX = this._bufferService.buffer.x; this._bufferService.buffer.savedY = this._bufferService.buffer.ybase + this._bufferService.buffer.y; - this._bufferService.buffer.savedCurAttrData.fg = this._terminal.curAttrData.fg; - this._bufferService.buffer.savedCurAttrData.bg = this._terminal.curAttrData.bg; + this._bufferService.buffer.savedCurAttrData.fg = this._curAttrData.fg; + this._bufferService.buffer.savedCurAttrData.bg = this._curAttrData.bg; this._bufferService.buffer.savedCharset = this._charsetService.charset; } @@ -2038,8 +2041,8 @@ export class InputHandler extends Disposable implements IInputHandler { public restoreCursor(params?: IParams): void { this._bufferService.buffer.x = this._bufferService.buffer.savedX || 0; this._bufferService.buffer.y = Math.max(this._bufferService.buffer.savedY - this._bufferService.buffer.ybase, 0); - this._terminal.curAttrData.fg = this._bufferService.buffer.savedCurAttrData.fg; - this._terminal.curAttrData.bg = this._bufferService.buffer.savedCurAttrData.bg; + this._curAttrData.fg = this._bufferService.buffer.savedCurAttrData.fg; + this._curAttrData.bg = this._bufferService.buffer.savedCurAttrData.bg; this._charsetService.charset = (this as any)._savedCharset; if (this._bufferService.buffer.savedCharset) { this._charsetService.charset = this._bufferService.buffer.savedCharset; @@ -2141,7 +2144,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._bufferService.buffer.y++; if (buffer.y === buffer.scrollBottom + 1) { buffer.y--; - this._terminal.scroll(); + this._terminal.scroll(this._eraseAttrData()); } else if (buffer.y >= this._bufferService.rows) { buffer.y = this._bufferService.rows - 1; } @@ -2175,7 +2178,7 @@ export class InputHandler extends Disposable implements IInputHandler { // blankLine(true) is xterm/linux behavior const scrollRegionHeight = buffer.scrollBottom - buffer.scrollTop; buffer.lines.shiftElements(buffer.y + buffer.ybase, scrollRegionHeight, 1); - buffer.lines.set(buffer.y + buffer.ybase, buffer.getBlankLine(this._terminal.eraseAttrData())); + buffer.lines.set(buffer.y + buffer.ybase, buffer.getBlankLine(this._eraseAttrData())); this._dirtyRowService.markRangeDirty(buffer.scrollTop, buffer.scrollBottom); } else { buffer.y--; @@ -2193,6 +2196,15 @@ export class InputHandler extends Disposable implements IInputHandler { this._terminal.reset(); // TODO: save to move from terminal? } + /** + * back_color_erase feature for xterm. + */ + private _eraseAttrData(): IAttributeData { + this._eraseAttrDataInternal.bg &= ~(Attributes.CM_MASK | 0xFFFFFF); + this._eraseAttrDataInternal.bg |= this._curAttrData.bg & ~0xFC000000; + return this._eraseAttrDataInternal; + } + /** * ESC n * ESC o @@ -2219,8 +2231,8 @@ export class InputHandler extends Disposable implements IInputHandler { // prepare cell data const cell = new CellData(); cell.content = 1 << Content.WIDTH_SHIFT | 'E'.charCodeAt(0); - cell.fg = this._terminal.curAttrData.fg; - cell.bg = this._terminal.curAttrData.bg; + cell.fg = this._curAttrData.fg; + cell.bg = this._curAttrData.bg; const buffer = this._bufferService.buffer; diff --git a/src/Terminal.test.ts b/src/Terminal.test.ts index 062bddf4..90522bf4 100644 --- a/src/Terminal.test.ts +++ b/src/Terminal.test.ts @@ -112,7 +112,7 @@ describe('Terminal', () => { assert.equal(typeof e, 'number'); done(); }); - term.scroll(); + term.scroll(DEFAULT_ATTR_DATA.clone()); }); it('should fire the onTitleChange event', (done) => { term.onTitleChange(e => { @@ -397,7 +397,7 @@ describe('Terminal', () => { term.buffer.lines.get(0).setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)])); term.buffer.lines.get(INIT_ROWS - 1).setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)])); term.buffer.y = INIT_ROWS - 1; // Move cursor to last line - term.scroll(); + term.scroll(DEFAULT_ATTR_DATA.clone()); assert.equal(term.buffer.lines.length, INIT_ROWS + 1); assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'a'); assert.equal(term.buffer.lines.get(INIT_ROWS - 1).loadCell(0, new CellData()).getChars(), 'b'); @@ -410,7 +410,7 @@ describe('Terminal', () => { term.buffer.lines.get(2).setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)])); term.buffer.y = INIT_ROWS - 1; // Move cursor to last line term.buffer.scrollTop = 1; - term.scroll(); + term.scroll(DEFAULT_ATTR_DATA.clone()); assert.equal(term.buffer.lines.length, INIT_ROWS); assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'a'); assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), 'c'); @@ -424,7 +424,7 @@ describe('Terminal', () => { term.buffer.lines.get(4).setCell(0, CellData.fromCharData([0, 'e', 0, 'e'.charCodeAt(0)])); term.buffer.y = 3; term.buffer.scrollBottom = 3; - term.scroll(); + term.scroll(DEFAULT_ATTR_DATA.clone()); assert.equal(term.buffer.lines.length, INIT_ROWS + 1); assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'a', '\'a\' should be pushed to the scrollback'); assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), 'b'); @@ -443,7 +443,7 @@ describe('Terminal', () => { term.buffer.y = INIT_ROWS - 1; // Move cursor to last line term.buffer.scrollTop = 1; term.buffer.scrollBottom = 3; - term.scroll(); + term.scroll(DEFAULT_ATTR_DATA.clone()); assert.equal(term.buffer.lines.length, INIT_ROWS); assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'a'); assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), 'c', '\'b\' should be removed from the buffer'); @@ -465,7 +465,7 @@ describe('Terminal', () => { term.buffer.lines.get(INIT_ROWS - 1).setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)])); term.buffer.y = INIT_ROWS - 1; // Move cursor to last line assert.equal(term.buffer.lines.length, INIT_ROWS); - term.scroll(); + term.scroll(DEFAULT_ATTR_DATA.clone()); assert.equal(term.buffer.lines.length, INIT_ROWS); // 'a' gets pushed out of buffer assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'b'); @@ -480,7 +480,7 @@ describe('Terminal', () => { term.buffer.lines.get(2).setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)])); term.buffer.y = INIT_ROWS - 1; // Move cursor to last line term.buffer.scrollTop = 1; - term.scroll(); + term.scroll(DEFAULT_ATTR_DATA.clone()); assert.equal(term.buffer.lines.length, INIT_ROWS); assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'a'); assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), 'c'); @@ -494,7 +494,7 @@ describe('Terminal', () => { term.buffer.lines.get(4).setCell(0, CellData.fromCharData([0, 'e', 0, 'e'.charCodeAt(0)])); term.buffer.y = 3; term.buffer.scrollBottom = 3; - term.scroll(); + term.scroll(DEFAULT_ATTR_DATA.clone()); assert.equal(term.buffer.lines.length, INIT_ROWS); assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'b'); assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), 'c'); @@ -512,7 +512,7 @@ describe('Terminal', () => { term.buffer.y = INIT_ROWS - 1; // Move cursor to last line term.buffer.scrollTop = 1; term.buffer.scrollBottom = 3; - term.scroll(); + term.scroll(DEFAULT_ATTR_DATA.clone()); assert.equal(term.buffer.lines.length, INIT_ROWS); assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'a'); assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), 'c', '\'b\' should be removed from the buffer'); diff --git a/src/Terminal.ts b/src/Terminal.ts index 6d0bbea2..21d8ee60 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -122,9 +122,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp // misc public savedCols: number; - public curAttrData: IAttributeData; - private _eraseAttrData: IAttributeData; - public params: (string | number)[]; public currentParam: string | number; @@ -254,9 +251,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp // charset this._charsetService.reset(); - this.curAttrData = DEFAULT_ATTR_DATA.clone(); - this._eraseAttrData = DEFAULT_ATTR_DATA.clone(); - this.params = []; this.currentParam = 0; @@ -293,15 +287,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp return this._bufferService.buffers; } - /** - * back_color_erase feature for xterm. - */ - public eraseAttrData(): IAttributeData { - this._eraseAttrData.bg &= ~(Attributes.CM_MASK | 0xFFFFFF); - this._eraseAttrData.bg |= this.curAttrData.bg & ~0xFC000000; - return this._eraseAttrData; - } - /** * Focus the terminal. Delegates focus handling to the terminal's DOM element. */ @@ -946,10 +931,9 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp * Scroll the terminal down 1 row, creating a blank line. * @param isWrapped Whether the new line is wrapped from the previous line. */ - public scroll(isWrapped: boolean = false): void { + public scroll(eraseAttr: IAttributeData, isWrapped: boolean = false): void { let newLine: IBufferLine; newLine = this._blankLine; - const eraseAttr = this.eraseAttrData(); if (!newLine || newLine.length !== this.cols || newLine.getFg(0) !== eraseAttr.fg || newLine.getBg(0) !== eraseAttr.bg) { newLine = this.buffer.getBlankLine(eraseAttr, isWrapped); this._blankLine = newLine; diff --git a/src/TestUtils.test.ts b/src/TestUtils.test.ts index cf23b8eb..9338fbc0 100644 --- a/src/TestUtils.test.ts +++ b/src/TestUtils.test.ts @@ -19,6 +19,7 @@ import { IParams, IFunctionIdentifier } from 'common/parser/Types'; import { ISelectionService } from 'browser/services/Services'; export class TestTerminal extends Terminal { + get curAttrData(): IAttributeData { return (this as any)._inputHandler._curAttrData; } keyDown(ev: any): boolean { return this._keyDown(ev); } keyPress(ev: any): boolean { return this._keyPress(ev); } } diff --git a/src/Types.d.ts b/src/Types.d.ts index 7742b69c..e5021f88 100644 --- a/src/Types.d.ts +++ b/src/Types.d.ts @@ -27,7 +27,6 @@ export interface IInputHandlingTerminal { rows: number; insertMode: boolean; bracketedPasteMode: boolean; - curAttrData: IAttributeData; savedCols: number; mouseEvents: CoreMouseEventType; sendFocus: boolean; @@ -41,8 +40,7 @@ export interface IInputHandlingTerminal { bell(): void; focus(): void; - scroll(isWrapped?: boolean): void; - eraseAttrData(): IAttributeData; + scroll(eraseAttr: IAttributeData, isWrapped?: boolean): void; is(term: string): boolean; resize(x: number, y: number): void; reset(): void; From 33db0203ddadd8e37fc7aaa6fce02f261bb21de5 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 18 Dec 2019 11:55:25 +1100 Subject: [PATCH 26/48] Remove terminal.reset usage from input handler --- src/InputHandler.test.ts | 8 ++++---- src/InputHandler.ts | 8 +++++--- src/Terminal.ts | 2 +- src/TestUtils.test.ts | 4 +--- src/Types.d.ts | 1 - 5 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/InputHandler.test.ts b/src/InputHandler.test.ts index fa9ef67c..fa42312c 100644 --- a/src/InputHandler.test.ts +++ b/src/InputHandler.test.ts @@ -36,26 +36,26 @@ function getLines(term: TestTerminal, limit: number = term.rows): string[] { describe('InputHandler', () => { describe('save and restore cursor', () => { const terminal = new MockInputHandlingTerminal(); - terminal.curAttrData.fg = 3; const bufferService = new MockBufferService(80, 30); bufferService.buffer.x = 1; bufferService.buffer.y = 2; bufferService.buffer.ybase = 0; const inputHandler = new InputHandler(terminal, bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService()); + (inputHandler as any)._curAttrData.fg = 3; // Save cursor position inputHandler.saveCursor(); assert.equal(bufferService.buffer.x, 1); assert.equal(bufferService.buffer.y, 2); - assert.equal(terminal.curAttrData.fg, 3); + assert.equal((inputHandler as any)._curAttrData.fg, 3); // Change cursor position bufferService.buffer.x = 10; bufferService.buffer.y = 20; - terminal.curAttrData.fg = 30; + (inputHandler as any)._curAttrData.fg = 30; // Restore cursor position inputHandler.restoreCursor(); assert.equal(bufferService.buffer.x, 1); assert.equal(bufferService.buffer.y, 2); - assert.equal(terminal.curAttrData.fg, 3); + assert.equal((inputHandler as any)._curAttrData.fg, 3); }); describe('setCursorStyle', () => { it('should call Terminal.setOption with correct params', () => { diff --git a/src/InputHandler.ts b/src/InputHandler.ts index d01ebc5e..ecc09a7d 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -133,6 +133,8 @@ export class InputHandler extends Disposable implements IInputHandler { private _onRequestRefreshRows = new EventEmitter(); public get onRequestRefreshRows(): IEvent { return this._onRequestRefreshRows.event; } + private _onRequestReset = new EventEmitter(); + public get onRequestReset(): IEvent { return this._onRequestReset.event; } private _onCursorMove = new EventEmitter(); public get onCursorMove(): IEvent { return this._onCursorMove.event; } private _onLineFeed = new EventEmitter(); @@ -1410,7 +1412,7 @@ export class InputHandler extends Disposable implements IInputHandler { // TODO: move DECCOLM into compat addon this._terminal.savedCols = this._bufferService.cols; this._terminal.resize(132, this._bufferService.rows); - this._terminal.reset(); + this._onRequestReset.fire(); break; case 6: this._coreService.decPrivateModes.origin = true; @@ -1589,7 +1591,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._terminal.resize(this._terminal.savedCols, this._bufferService.rows); } delete this._terminal.savedCols; - this._terminal.reset(); + this._onRequestReset.fire(); break; case 6: this._coreService.decPrivateModes.origin = false; @@ -2193,7 +2195,7 @@ export class InputHandler extends Disposable implements IInputHandler { */ public reset(): void { this._parser.reset(); - this._terminal.reset(); // TODO: save to move from terminal? + this._onRequestReset.fire(); } /** diff --git a/src/Terminal.ts b/src/Terminal.ts index 21d8ee60..91684cfd 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -53,7 +53,6 @@ import { CharSizeService } from 'browser/services/CharSizeService'; import { BufferService, MINIMUM_COLS, MINIMUM_ROWS } from 'common/services/BufferService'; import { Disposable } from 'common/Lifecycle'; import { IBufferSet, IBuffer } from 'common/buffer/Types'; -import { Attributes } from 'common/buffer/Constants'; import { MouseService } from 'browser/services/MouseService'; import { IParams, IFunctionIdentifier } from 'common/parser/Types'; import { CoreService } from 'common/services/CoreService'; @@ -259,6 +258,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp // Register input handler and refire/handle events this._inputHandler = new InputHandler(this, this._bufferService, this._charsetService, this._coreService, this._dirtyRowService, this._logService, this.optionsService, this._coreMouseService); this._inputHandler.onRequestRefreshRows((start, end) => this.refresh(start, end)); + this._inputHandler.onRequestReset(() => this.reset()); this._inputHandler.onCursorMove(() => this._onCursorMove.fire()); this._inputHandler.onLineFeed(() => this._onLineFeed.fire()); this.register(this._inputHandler); diff --git a/src/TestUtils.test.ts b/src/TestUtils.test.ts index 9338fbc0..d425a358 100644 --- a/src/TestUtils.test.ts +++ b/src/TestUtils.test.ts @@ -204,7 +204,6 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal { rows: number; insertMode: boolean; bracketedPasteMode: boolean; - curAttrData = new AttributeData(); savedCols: number; x10Mouse: boolean; vt200Mouse: boolean; @@ -226,11 +225,10 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal { bell(): void { throw new Error('Method not implemented.'); } - updateRange(y: number): void { throw new Error('Method not implemented.'); } - scroll(isWrapped?: boolean): void { + scroll(eraseAttr: IAttributeData, isWrapped?: boolean): void { throw new Error('Method not implemented.'); } nextStop(x?: number): number { diff --git a/src/Types.d.ts b/src/Types.d.ts index e5021f88..bf3f3221 100644 --- a/src/Types.d.ts +++ b/src/Types.d.ts @@ -43,7 +43,6 @@ export interface IInputHandlingTerminal { scroll(eraseAttr: IAttributeData, isWrapped?: boolean): void; is(term: string): boolean; resize(x: number, y: number): void; - reset(): void; showCursor(): void; handleTitle(title: string): void; } From 5d351ec8c0705fbec62ffb8981d4c139b427efb5 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 18 Dec 2019 11:57:16 +1100 Subject: [PATCH 27/48] Isolate cast in TestInputHandler --- src/InputHandler.test.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/InputHandler.test.ts b/src/InputHandler.test.ts index fa42312c..ec3699ce 100644 --- a/src/InputHandler.test.ts +++ b/src/InputHandler.test.ts @@ -7,7 +7,7 @@ import { assert, expect } from 'chai'; import { InputHandler } from './InputHandler'; import { MockInputHandlingTerminal, TestTerminal } from './TestUtils.test'; import { Terminal } from './Terminal'; -import { IBufferLine } from 'common/Types'; +import { IBufferLine, IAttributeData } from 'common/Types'; import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; import { CellData } from 'common/buffer/CellData'; import { Attributes } from 'common/buffer/Constants'; @@ -33,6 +33,10 @@ function getLines(term: TestTerminal, limit: number = term.rows): string[] { return res; } +class TestInputHandler extends InputHandler { + get curAttrData(): IAttributeData { return (this as any)._curAttrData; } +} + describe('InputHandler', () => { describe('save and restore cursor', () => { const terminal = new MockInputHandlingTerminal(); @@ -40,22 +44,22 @@ describe('InputHandler', () => { bufferService.buffer.x = 1; bufferService.buffer.y = 2; bufferService.buffer.ybase = 0; - const inputHandler = new InputHandler(terminal, bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService()); - (inputHandler as any)._curAttrData.fg = 3; + const inputHandler = new TestInputHandler(terminal, bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService()); + inputHandler.curAttrData.fg = 3; // Save cursor position inputHandler.saveCursor(); assert.equal(bufferService.buffer.x, 1); assert.equal(bufferService.buffer.y, 2); - assert.equal((inputHandler as any)._curAttrData.fg, 3); + assert.equal(inputHandler.curAttrData.fg, 3); // Change cursor position bufferService.buffer.x = 10; bufferService.buffer.y = 20; - (inputHandler as any)._curAttrData.fg = 30; + inputHandler.curAttrData.fg = 30; // Restore cursor position inputHandler.restoreCursor(); assert.equal(bufferService.buffer.x, 1); assert.equal(bufferService.buffer.y, 2); - assert.equal((inputHandler as any)._curAttrData.fg, 3); + assert.equal(inputHandler.curAttrData.fg, 3); }); describe('setCursorStyle', () => { it('should call Terminal.setOption with correct params', () => { From 8e996ae571b9750eb04733ac46f4cfb8d3d26362 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 18 Dec 2019 12:00:38 +1100 Subject: [PATCH 28/48] Remove terminal.bell usage --- src/InputHandler.ts | 6 ++++-- src/Terminal.ts | 1 + src/Types.d.ts | 1 - 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index ecc09a7d..c7eaeb26 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -135,6 +135,8 @@ export class InputHandler extends Disposable implements IInputHandler { public get onRequestRefreshRows(): IEvent { return this._onRequestRefreshRows.event; } private _onRequestReset = new EventEmitter(); public get onRequestReset(): IEvent { return this._onRequestReset.event; } + private _onRequestBell = new EventEmitter(); + public get onRequestBell(): IEvent { return this._onRequestBell.event; } private _onCursorMove = new EventEmitter(); public get onCursorMove(): IEvent { return this._onCursorMove.event; } private _onLineFeed = new EventEmitter(); @@ -143,7 +145,7 @@ export class InputHandler extends Disposable implements IInputHandler { public get onScroll(): IEvent { return this._onScroll.event; } constructor( - protected _terminal: IInputHandlingTerminal, + private _terminal: IInputHandlingTerminal, private readonly _bufferService: IBufferService, private readonly _charsetService: ICharsetService, private readonly _coreService: ICoreService, @@ -543,7 +545,7 @@ export class InputHandler extends Disposable implements IInputHandler { * Bell (Ctrl-G). */ public bell(): void { - this._terminal.bell(); + this._onRequestBell.fire(); } /** diff --git a/src/Terminal.ts b/src/Terminal.ts index 91684cfd..ffecc6f2 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -257,6 +257,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp // Register input handler and refire/handle events this._inputHandler = new InputHandler(this, this._bufferService, this._charsetService, this._coreService, this._dirtyRowService, this._logService, this.optionsService, this._coreMouseService); + this._inputHandler.onRequestBell(() => this.bell()); this._inputHandler.onRequestRefreshRows((start, end) => this.refresh(start, end)); this._inputHandler.onRequestReset(() => this.reset()); this._inputHandler.onCursorMove(() => this._onCursorMove.fire()); diff --git a/src/Types.d.ts b/src/Types.d.ts index bf3f3221..e19a7d53 100644 --- a/src/Types.d.ts +++ b/src/Types.d.ts @@ -38,7 +38,6 @@ export interface IInputHandlingTerminal { onA11yCharEmitter: IEventEmitter; onA11yTabEmitter: IEventEmitter; - bell(): void; focus(): void; scroll(eraseAttr: IAttributeData, isWrapped?: boolean): void; is(term: string): boolean; From 914861503bbca114f0d4e424a4c40d58acaf6a70 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 18 Dec 2019 12:06:41 +1100 Subject: [PATCH 29/48] Remove a bunch of unused interfaces --- src/TestUtils.test.ts | 70 +------------------------------------------ src/Types.d.ts | 6 ---- 2 files changed, 1 insertion(+), 75 deletions(-) diff --git a/src/TestUtils.test.ts b/src/TestUtils.test.ts index d425a358..57a9fe78 100644 --- a/src/TestUtils.test.ts +++ b/src/TestUtils.test.ts @@ -6,7 +6,7 @@ import { IRenderer, IRenderDimensions, CharacterJoinerHandler, IRequestRefreshRowsEvent } from 'browser/renderer/Types'; import { IInputHandlingTerminal, ICompositionHelper, ITerminal, IBrowser, ITerminalOptions } from './Types'; import { IBuffer, IBufferStringIterator, IBufferSet } from 'common/buffer/Types'; -import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener, ICharset, CoreMouseEventType } from 'common/Types'; +import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener, ICharset } from 'common/Types'; import { Buffer } from 'common/buffer/Buffer'; import * as Browser from 'common/Platform'; import { IDisposable, IMarker, IEvent, ISelectionPosition } from 'xterm'; @@ -198,93 +198,25 @@ export class MockTerminal implements ITerminal { export class MockInputHandlingTerminal implements IInputHandlingTerminal { onA11yCharEmitter: EventEmitter; onA11yTabEmitter: EventEmitter; - element: HTMLElement; - options: ITerminalOptions = {}; - cols: number; - rows: number; insertMode: boolean; bracketedPasteMode: boolean; savedCols: number; - x10Mouse: boolean; - vt200Mouse: boolean; - normalMouse: boolean; - mouseEvents: CoreMouseEventType; sendFocus: boolean; - utfMouse: boolean; - sgrMouse: boolean; - urxvtMouse: boolean; - cursorHidden: boolean; buffers: IBufferSet; buffer: IBuffer = new MockBuffer(); viewport: IViewport; - selectionService: ISelectionService; - focus(): void { - throw new Error('Method not implemented.'); - } - convertEol: boolean; - bell(): void { - throw new Error('Method not implemented.'); - } - updateRange(y: number): void { - throw new Error('Method not implemented.'); - } scroll(eraseAttr: IAttributeData, isWrapped?: boolean): void { throw new Error('Method not implemented.'); } - nextStop(x?: number): number { - throw new Error('Method not implemented.'); - } - eraseAttrData(): IAttributeData { - throw new Error('Method not implemented.'); - } - eraseRight(x: number, y: number): void { - throw new Error('Method not implemented.'); - } - eraseLine(y: number): void { - throw new Error('Method not implemented.'); - } - eraseLeft(x: number, y: number): void { - throw new Error('Method not implemented.'); - } - prevStop(x?: number): number { - throw new Error('Method not implemented.'); - } is(term: string): boolean { throw new Error('Method not implemented.'); } resize(x: number, y: number): void { throw new Error('Method not implemented.'); } - log(text: string, data?: any): void { - throw new Error('Method not implemented.'); - } - reset(): void { - throw new Error('Method not implemented.'); - } showCursor(): void { throw new Error('Method not implemented.'); } - matchColor(r1: number, g1: number, b1: number): number { - throw new Error('Method not implemented.'); - } - error(text: string, data?: any): void { - throw new Error('Method not implemented.'); - } - setOption(key: string, value: any): void { - (this.options)[key] = value; - } - on(type: string, listener: XtermListener): void { - throw new Error('Method not implemented.'); - } - off(type: string, listener: XtermListener): void { - throw new Error('Method not implemented.'); - } - emit(type: string, data?: any): void { - throw new Error('Method not implemented.'); - } - addDisposableListener(type: string, handler: XtermListener): IDisposable { - throw new Error('Method not implemented.'); - } handler(data: string): void { throw new Error('Method not implemented.'); } diff --git a/src/Types.d.ts b/src/Types.d.ts index e19a7d53..761f556f 100644 --- a/src/Types.d.ts +++ b/src/Types.d.ts @@ -21,14 +21,9 @@ export type LineData = CharData[]; * InputHandler cleanly from the ITerminal interface. */ export interface IInputHandlingTerminal { - element: HTMLElement; - options: ITerminalOptions; - cols: number; - rows: number; insertMode: boolean; bracketedPasteMode: boolean; savedCols: number; - mouseEvents: CoreMouseEventType; sendFocus: boolean; buffers: IBufferSet; @@ -38,7 +33,6 @@ export interface IInputHandlingTerminal { onA11yCharEmitter: IEventEmitter; onA11yTabEmitter: IEventEmitter; - focus(): void; scroll(eraseAttr: IAttributeData, isWrapped?: boolean): void; is(term: string): boolean; resize(x: number, y: number): void; From 072779725407e7f105cad34bb5ad5892436a20ba Mon Sep 17 00:00:00 2001 From: Phillip Campbell <15082+phillc@users.noreply.github.com> Date: Wed, 18 Dec 2019 09:05:29 -0500 Subject: [PATCH 30/48] Add Linode to real world uses --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 70ff7891..f4d562f5 100644 --- a/README.md +++ b/README.md @@ -156,6 +156,7 @@ Xterm.js is used in several world-class applications to provide great terminal e - [**tty-share**](https://tty-share.com): Extremely simple terminal sharing over the Internet. - [**Ten Hands**](https://github.com/saisandeepvaddi/ten-hands): One place to run your command-line tasks. - [**WebAssembly.sh**](https://webassembly.sh): A WebAssembly WASI browser terminal +- [**Linode**](https://linode.com): Linode uses xterm.js to provide users a web console for their Linode instances. [And much more...](https://github.com/xtermjs/xterm.js/network/dependents) From 34168bd22393a1df09bd1a9097059f963f17e76c Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 19 Dec 2019 15:42:23 +1100 Subject: [PATCH 31/48] Fix InputHandler resetting --- src/InputHandler.ts | 9 +++++++-- src/Terminal.ts | 22 ++++++++++++---------- src/Types.d.ts | 2 +- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index c7eaeb26..f512ac1c 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -306,7 +306,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._parser.setEscHandler({final: 'M'}, () => this.reverseIndex()); this._parser.setEscHandler({final: '='}, () => this.keypadApplicationMode()); this._parser.setEscHandler({final: '>'}, () => this.keypadNumericMode()); - this._parser.setEscHandler({final: 'c'}, () => this.reset()); + this._parser.setEscHandler({final: 'c'}, () => this.fullReset()); this._parser.setEscHandler({final: 'n'}, () => this.setgLevel(2)); this._parser.setEscHandler({final: 'o'}, () => this.setgLevel(3)); this._parser.setEscHandler({final: '|'}, () => this.setgLevel(3)); @@ -2195,9 +2195,14 @@ export class InputHandler extends Disposable implements IInputHandler { * DEC mnemonic: RIS (https://vt100.net/docs/vt510-rm/RIS.html) * Reset to initial state. */ + public fullReset(): void { + this._onRequestReset.fire(); + } + public reset(): void { this._parser.reset(); - this._onRequestReset.fire(); + this._curAttrData = DEFAULT_ATTR_DATA.clone(); + this._eraseAttrDataInternal = DEFAULT_ATTR_DATA.clone(); } /** diff --git a/src/Terminal.ts b/src/Terminal.ts index ffecc6f2..2364ffd1 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -255,14 +255,18 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this._userScrolling = false; - // Register input handler and refire/handle events - this._inputHandler = new InputHandler(this, this._bufferService, this._charsetService, this._coreService, this._dirtyRowService, this._logService, this.optionsService, this._coreMouseService); - this._inputHandler.onRequestBell(() => this.bell()); - this._inputHandler.onRequestRefreshRows((start, end) => this.refresh(start, end)); - this._inputHandler.onRequestReset(() => this.reset()); - this._inputHandler.onCursorMove(() => this._onCursorMove.fire()); - this._inputHandler.onLineFeed(() => this._onLineFeed.fire()); - this.register(this._inputHandler); + if (this._inputHandler) { + this._inputHandler.reset(); + } else { + // Register input handler and refire/handle events + this._inputHandler = new InputHandler(this, this._bufferService, this._charsetService, this._coreService, this._dirtyRowService, this._logService, this.optionsService, this._coreMouseService); + this._inputHandler.onRequestBell(() => this.bell()); + this._inputHandler.onRequestRefreshRows((start, end) => this.refresh(start, end)); + this._inputHandler.onRequestReset(() => this.reset()); + this._inputHandler.onCursorMove(() => this._onCursorMove.fire()); + this._inputHandler.onLineFeed(() => this._onLineFeed.fire()); + this.register(this._inputHandler); + } this.linkifier = this.linkifier || new Linkifier(this._bufferService, this._logService); @@ -1464,7 +1468,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this.options.rows = this.rows; this.options.cols = this.cols; const customKeyEventHandler = this._customKeyEventHandler; - const inputHandler = this._inputHandler; const userScrolling = this._userScrolling; this._setup(); @@ -1475,7 +1478,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp // reattach this._customKeyEventHandler = customKeyEventHandler; - this._inputHandler = inputHandler; this._userScrolling = userScrolling; // do a full screen refresh diff --git a/src/Types.d.ts b/src/Types.d.ts index 761f556f..ebc5d338 100644 --- a/src/Types.d.ts +++ b/src/Types.d.ts @@ -121,7 +121,7 @@ export interface IInputHandler { /** ESC D */ index(): void; /** ESC H */ tabSet(): void; /** ESC M */ reverseIndex(): void; - /** ESC c */ reset(): void; + /** ESC c */ fullReset(): void; /** ESC n ESC o ESC | From d045d39f8833ac16005d4b7dd95ea1795ee9f0c1 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 19 Dec 2019 15:58:15 +1100 Subject: [PATCH 32/48] Move charset service reset out of _setup --- src/Terminal.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index 2364ffd1..b89efdb6 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -247,9 +247,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this.insertMode = false; this.bracketedPasteMode = false; - // charset - this._charsetService.reset(); - this.params = []; this.currentParam = 0; @@ -268,7 +265,9 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this.register(this._inputHandler); } - this.linkifier = this.linkifier || new Linkifier(this._bufferService, this._logService); + if (!this.linkifier) { + this.linkifier = new Linkifier(this._bufferService, this._logService); + } if (this.options.windowsMode) { this._enableWindowsMode(); @@ -1472,6 +1471,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this._setup(); this._bufferService.reset(); + this._charsetService.reset(); this._coreService.reset(); this._coreMouseService.reset(); this._selectionService?.reset(); From e6c095754fcdbe5af307de29d3b2c1a39088798e Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 19 Dec 2019 15:59:59 +1100 Subject: [PATCH 33/48] Remove old params props from Terminal --- src/Terminal.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index b89efdb6..77e7677e 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -121,9 +121,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp // misc public savedCols: number; - public params: (string | number)[]; - public currentParam: string | number; - // write buffer private _writeBuffer: WriteBuffer; @@ -247,9 +244,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this.insertMode = false; this.bracketedPasteMode = false; - this.params = []; - this.currentParam = 0; - this._userScrolling = false; if (this._inputHandler) { From 03fc6c3f972241c060398f80fe96a0fb3836ea2a Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 19 Dec 2019 16:04:15 +1100 Subject: [PATCH 34/48] Don't keep a reference to parent anymore --- src/Terminal.ts | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index 77e7677e..74c2d00e 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -74,10 +74,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp public element: HTMLElement; public screenElement: HTMLElement; - /** - * The HTMLElement that the terminal is created in, set by Terminal.open. - */ - private _parent: HTMLElement | null; private _document: Document; private _viewportScrollArea: HTMLElement; private _viewportElement: HTMLElement; @@ -236,8 +232,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp } private _setup(): void { - this._parent = document ? document.body : null; - this._customKeyEventHandler = null; // modes @@ -456,9 +450,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp * @param parent The element to create the terminal within. */ public open(parent: HTMLElement): void { - this._parent = parent || this._parent; - - if (!this._parent) { + if (!parent) { throw new Error('Terminal requires a parent element.'); } @@ -466,7 +458,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this._logService.warn('Terminal.open was called on an element that was not attached to the DOM'); } - this._document = this._parent.ownerDocument; + this._document = parent.ownerDocument; // Create main element container this.element = this._document.createElement('div'); @@ -474,7 +466,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this.element.classList.add('terminal'); this.element.classList.add('xterm'); this.element.setAttribute('tabindex', '0'); - this._parent.appendChild(this.element); + parent.appendChild(this.element); // Performance: Use a document fragment to build the terminal // viewport and helper elements detached from the DOM From 15e3173177cb23fe4e7576339fd5130e52b6bf80 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 20 Dec 2019 06:14:26 +1100 Subject: [PATCH 35/48] Move back to reseting parser only on RIS Related #2637 --- src/InputHandler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index f512ac1c..ae44c31c 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -2196,11 +2196,11 @@ export class InputHandler extends Disposable implements IInputHandler { * Reset to initial state. */ public fullReset(): void { + this._parser.reset(); this._onRequestReset.fire(); } public reset(): void { - this._parser.reset(); this._curAttrData = DEFAULT_ATTR_DATA.clone(); this._eraseAttrDataInternal = DEFAULT_ATTR_DATA.clone(); } From 2b4da1f88e0660a7542c9727bc9f32bf62c7b395 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sat, 21 Dec 2019 21:40:01 +0100 Subject: [PATCH 36/48] add wide char handling to bufferline primitives --- src/common/buffer/BufferLine.test.ts | 90 ++++++++++++++++++++++++++++ src/common/buffer/BufferLine.ts | 38 ++++++++++-- 2 files changed, 124 insertions(+), 4 deletions(-) diff --git a/src/common/buffer/BufferLine.test.ts b/src/common/buffer/BufferLine.test.ts index ae80aa16..686371f2 100644 --- a/src/common/buffer/BufferLine.test.ts +++ b/src/common/buffer/BufferLine.test.ts @@ -366,4 +366,94 @@ describe('BufferLine', function(): void { chai.assert.equal(cell.isCombined(), Content.IS_COMBINED_MASK); }); }); + describe('correct fullwidth handling', () => { + function populate(line: BufferLine): void { + const cell = CellData.fromCharData([1, '¥', 2, '¥'.charCodeAt(0)]); + for (let i = 0; i < line.length; i += 2) { + line.setCell(i, cell); + } + } + it('insert - wide char at pos', () => { + const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); + populate(line); + line.insertCells(9, 1, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + chai.assert.equal(line.translateToString(), '¥¥¥¥ a'); + line.insertCells(8, 1, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + chai.assert.equal(line.translateToString(), '¥¥¥¥a '); + line.insertCells(1, 1, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + chai.assert.equal(line.translateToString(), ' a ¥¥¥a'); + }); + it('insert - wide char at end', () => { + const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); + populate(line); + line.insertCells(0, 3, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + chai.assert.equal(line.translateToString(), 'aaa¥¥¥ '); + line.insertCells(4, 1, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + chai.assert.equal(line.translateToString(), 'aaa a ¥¥'); + line.insertCells(4, 1, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + chai.assert.equal(line.translateToString(), 'aaa aa ¥ '); + }); + it('delete', () => { + const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); + populate(line); + line.deleteCells(0, 1, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + chai.assert.equal(line.translateToString(), ' ¥¥¥¥a'); + line.deleteCells(5, 2, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + chai.assert.equal(line.translateToString(), ' ¥¥¥aaa'); + line.deleteCells(0, 2, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + chai.assert.equal(line.translateToString(), ' ¥¥aaaaa'); + }); + it('replace - start at 0', () => { + let line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); + populate(line); + line.replaceCells(0, 1, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + chai.assert.equal(line.translateToString(), 'a ¥¥¥¥'); + line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); + populate(line); + line.replaceCells(0, 2, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + chai.assert.equal(line.translateToString(), 'aa¥¥¥¥'); + line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); + populate(line); + line.replaceCells(0, 3, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + chai.assert.equal(line.translateToString(), 'aaa ¥¥¥'); + line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); + populate(line); + line.replaceCells(0, 8, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + chai.assert.equal(line.translateToString(), 'aaaaaaaa¥'); + line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); + populate(line); + line.replaceCells(0, 9, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + chai.assert.equal(line.translateToString(), 'aaaaaaaaa '); + line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); + populate(line); + line.replaceCells(0, 10, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + chai.assert.equal(line.translateToString(), 'aaaaaaaaaa'); + }); + it('replace - start at 1', () => { + let line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); + populate(line); + line.replaceCells(1, 2, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + chai.assert.equal(line.translateToString(), ' a¥¥¥¥'); + line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); + populate(line); + line.replaceCells(1, 3, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + chai.assert.equal(line.translateToString(), ' aa ¥¥¥'); + line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); + populate(line); + line.replaceCells(1, 4, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + chai.assert.equal(line.translateToString(), ' aaa¥¥¥'); + line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); + populate(line); + line.replaceCells(1, 8, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + chai.assert.equal(line.translateToString(), ' aaaaaaa¥'); + line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); + populate(line); + line.replaceCells(1, 9, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + chai.assert.equal(line.translateToString(), ' aaaaaaaa '); + line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); + populate(line); + line.replaceCells(1, 10, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + chai.assert.equal(line.translateToString(), ' aaaaaaaaa'); + }); + }); }); diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index 1e95e004..d54b59aa 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { CharData, IBufferLine, ICellData } from 'common/Types'; +import { CharData, IBufferLine, ICellData, IAttributeData } from 'common/Types'; import { stringFromCodePoint } from 'common/input/TextDecoder'; import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_ATTR_INDEX, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, WHITESPACE_CELL_CHAR, Content } from 'common/buffer/Constants'; import { CellData } from 'common/buffer/CellData'; @@ -228,8 +228,14 @@ export class BufferLine implements IBufferLine { } } - public insertCells(pos: number, n: number, fillCellData: ICellData): void { + public insertCells(pos: number, n: number, fillCellData: ICellData, eraseAttr?: IAttributeData): void { pos %= this.length; + + // handle fullwidth at pos: reset cell one to the left if pos is second cell of a wide char + if (pos && this.getWidth(pos - 1) === 2) { + this.setCellFromCodePoint(pos - 1, 0, 1, eraseAttr?.fg || 0, eraseAttr?.bg || 0); + } + if (n < this.length - pos) { const cell = new CellData(); for (let i = this.length - pos - n - 1; i >= 0; --i) { @@ -243,9 +249,14 @@ export class BufferLine implements IBufferLine { this.setCell(i, fillCellData); } } + + // handle fullwidth at line end: reset last cell if it is first cell of a wide char + if (this.getWidth(this.length - 1) === 2) { + this.setCellFromCodePoint(this.length - 1, 0, 1, eraseAttr?.fg || 0, eraseAttr?.bg || 0); + } } - public deleteCells(pos: number, n: number, fillCellData: ICellData): void { + public deleteCells(pos: number, n: number, fillCellData: ICellData, eraseAttr?: IAttributeData): void { pos %= this.length; if (n < this.length - pos) { const cell = new CellData(); @@ -260,9 +271,28 @@ export class BufferLine implements IBufferLine { this.setCell(i, fillCellData); } } + + // handle fullwidth at pos: + // - reset pos-1 if wide char + // - reset pos if width==0 (previous second cell of a wide char) + if (pos && this.getWidth(pos - 1) === 2) { + this.setCellFromCodePoint(pos - 1, 0, 1, eraseAttr?.fg || 0, eraseAttr?.bg || 0); + } + if (this.getWidth(pos) === 0 && !this.hasContent(pos)) { + this.setCellFromCodePoint(pos, 0, 1, eraseAttr?.fg || 0, eraseAttr?.bg || 0); + } } - public replaceCells(start: number, end: number, fillCellData: ICellData): void { + public replaceCells(start: number, end: number, fillCellData: ICellData, eraseAttr?: IAttributeData): void { + // handle fullwidth at start: reset cell one to the left if start is second cell of a wide char + if (start && this.getWidth(start - 1) === 2) { + this.setCellFromCodePoint(start - 1, 0, 1, eraseAttr?.fg || 0, eraseAttr?.bg || 0); + } + // handle fullwidth at last cell + 1: reset to empty cell if it is second part of a wide char + if (end < this.length && this.getWidth(end - 1) === 2) { + this.setCellFromCodePoint(end, 0, 1, eraseAttr?.fg || 0, eraseAttr?.bg || 0); + } + while (start < end && start < this.length) { this.setCell(start++, fillCellData); } From 7ec658a3f806db268d478e4e5c23bab3f7c559bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sat, 21 Dec 2019 21:57:39 +0100 Subject: [PATCH 37/48] apply erase attrs in handler methods --- src/InputHandler.ts | 22 +++++++++++++--------- src/common/Types.d.ts | 6 +++--- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index ae44c31c..963ce145 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -473,7 +473,7 @@ export class InputHandler extends Disposable implements IInputHandler { // insert mode: move characters to right if (insertMode) { // right shift cells according to the width - bufferRow.insertCells(buffer.x, chWidth, buffer.getNullCell(curAttr)); + bufferRow.insertCells(buffer.x, chWidth, buffer.getNullCell(curAttr), curAttr); // test last cell - since the last cell has only room for // a halfwidth char any fullwidth shifted there is lost // and will be set to empty cell @@ -855,7 +855,8 @@ export class InputHandler extends Disposable implements IInputHandler { line.replaceCells( start, end, - this._bufferService.buffer.getNullCell(this._eraseAttrData()) + this._bufferService.buffer.getNullCell(this._eraseAttrData()), + this._eraseAttrData() ); if (clearWrap) { line.isWrapped = false; @@ -1033,7 +1034,8 @@ export class InputHandler extends Disposable implements IInputHandler { line.insertCells( this._bufferService.buffer.x, params.params[0] || 1, - this._bufferService.buffer.getNullCell(this._eraseAttrData()) + this._bufferService.buffer.getNullCell(this._eraseAttrData()), + this._eraseAttrData() ); this._dirtyRowService.markDirty(this._bufferService.buffer.y); } @@ -1050,7 +1052,8 @@ export class InputHandler extends Disposable implements IInputHandler { line.deleteCells( this._bufferService.buffer.x, params.params[0] || 1, - this._bufferService.buffer.getNullCell(this._eraseAttrData()) + this._bufferService.buffer.getNullCell(this._eraseAttrData()), + this._eraseAttrData() ); this._dirtyRowService.markDirty(this._bufferService.buffer.y); } @@ -1110,7 +1113,7 @@ export class InputHandler extends Disposable implements IInputHandler { const param = params.params[0] || 1; for (let y = buffer.scrollTop; y <= buffer.scrollBottom; ++y) { const line = buffer.lines.get(buffer.ybase + y); - line.deleteCells(0, param, buffer.getNullCell(this._eraseAttrData())); + line.deleteCells(0, param, buffer.getNullCell(this._eraseAttrData()), this._eraseAttrData()); line.isWrapped = false; } this._dirtyRowService.markRangeDirty(buffer.scrollTop, buffer.scrollBottom); @@ -1138,7 +1141,7 @@ export class InputHandler extends Disposable implements IInputHandler { const param = params.params[0] || 1; for (let y = buffer.scrollTop; y <= buffer.scrollBottom; ++y) { const line = buffer.lines.get(buffer.ybase + y); - line.insertCells(0, param, buffer.getNullCell(this._eraseAttrData())); + line.insertCells(0, param, buffer.getNullCell(this._eraseAttrData()), this._eraseAttrData()); line.isWrapped = false; } this._dirtyRowService.markRangeDirty(buffer.scrollTop, buffer.scrollBottom); @@ -1156,7 +1159,7 @@ export class InputHandler extends Disposable implements IInputHandler { const param = params.params[0] || 1; for (let y = buffer.scrollTop; y <= buffer.scrollBottom; ++y) { const line = this._bufferService.buffer.lines.get(buffer.ybase + y); - line.insertCells(buffer.x, param, buffer.getNullCell(this._eraseAttrData())); + line.insertCells(buffer.x, param, buffer.getNullCell(this._eraseAttrData()), this._eraseAttrData()); line.isWrapped = false; } this._dirtyRowService.markRangeDirty(buffer.scrollTop, buffer.scrollBottom); @@ -1174,7 +1177,7 @@ export class InputHandler extends Disposable implements IInputHandler { const param = params.params[0] || 1; for (let y = buffer.scrollTop; y <= buffer.scrollBottom; ++y) { const line = buffer.lines.get(buffer.ybase + y); - line.deleteCells(buffer.x, param, buffer.getNullCell(this._eraseAttrData())); + line.deleteCells(buffer.x, param, buffer.getNullCell(this._eraseAttrData()), this._eraseAttrData()); line.isWrapped = false; } this._dirtyRowService.markRangeDirty(buffer.scrollTop, buffer.scrollBottom); @@ -1191,7 +1194,8 @@ export class InputHandler extends Disposable implements IInputHandler { line.replaceCells( this._bufferService.buffer.x, this._bufferService.buffer.x + (params.params[0] || 1), - this._bufferService.buffer.getNullCell(this._eraseAttrData()) + this._bufferService.buffer.getNullCell(this._eraseAttrData()), + this._eraseAttrData() ); this._dirtyRowService.markDirty(this._bufferService.buffer.y); } diff --git a/src/common/Types.d.ts b/src/common/Types.d.ts index 9b8a18b5..fdb5eb73 100644 --- a/src/common/Types.d.ts +++ b/src/common/Types.d.ts @@ -123,9 +123,9 @@ export interface IBufferLine { setCell(index: number, cell: ICellData): void; setCellFromCodePoint(index: number, codePoint: number, width: number, fg: number, bg: number): void; addCodepointToCell(index: number, codePoint: number): void; - insertCells(pos: number, n: number, ch: ICellData): void; - deleteCells(pos: number, n: number, fill: ICellData): void; - replaceCells(start: number, end: number, fill: ICellData): void; + insertCells(pos: number, n: number, ch: ICellData, eraseAttr?: IAttributeData): void; + deleteCells(pos: number, n: number, fill: ICellData, eraseAttr?: IAttributeData): void; + replaceCells(start: number, end: number, fill: ICellData, eraseAttr?: IAttributeData): void; resize(cols: number, fill: ICellData): void; fill(fillCellData: ICellData): void; copyFrom(line: IBufferLine): void; From 0edbbf9fe0c4a331ae8a9fc39d0f3dbb27856d76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sat, 21 Dec 2019 23:07:12 +0100 Subject: [PATCH 38/48] fix print handler; tests --- src/InputHandler.test.ts | 59 ++++++++++++++++++++++++++++++++++++++++ src/InputHandler.ts | 12 ++++++++ 2 files changed, 71 insertions(+) diff --git a/src/InputHandler.test.ts b/src/InputHandler.test.ts index ec3699ce..b7eeb352 100644 --- a/src/InputHandler.test.ts +++ b/src/InputHandler.test.ts @@ -1270,4 +1270,63 @@ describe('InputHandler', () => { [131072, 131072], [131072, 131072], [131072, 300000 - 131072 - 131072] ]); }); + describe('should correctly reset cells taken by wide chars', () => { + let term: TestTerminal; + beforeEach(() => { + term = new TestTerminal({cols: 10, rows: 5, scrollback: 1}); + term.writeSync('¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥'); + }); + it('print', () => { + term.writeSync('\x1b[H#'); + assert.deepEqual(getLines(term), ['# ¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); + term.writeSync('\x1b[1;6H######'); + assert.deepEqual(getLines(term), ['# ¥ #####', '# ¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); + term.writeSync('#'); + assert.deepEqual(getLines(term), ['# ¥ #####', '##¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); + term.writeSync('#'); + assert.deepEqual(getLines(term), ['# ¥ #####', '### ¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); + term.writeSync('\x1b[3;9H#'); + assert.deepEqual(getLines(term), ['# ¥ #####', '### ¥¥¥', '¥¥¥¥#', '¥¥¥¥¥', '']); + term.writeSync('#'); + assert.deepEqual(getLines(term), ['# ¥ #####', '### ¥¥¥', '¥¥¥¥##', '¥¥¥¥¥', '']); + term.writeSync('#'); + assert.deepEqual(getLines(term), ['# ¥ #####', '### ¥¥¥', '¥¥¥¥##', '# ¥¥¥¥', '']); + term.writeSync('\x1b[4;10H#'); + assert.deepEqual(getLines(term), ['# ¥ #####', '### ¥¥¥', '¥¥¥¥##', '# ¥¥¥ #', '']); + }); + it('EL', () => { + term.writeSync('\x1b[1;6H\x1b[K#'); + assert.deepEqual(getLines(term), ['¥¥ #', '¥¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); + term.writeSync('\x1b[2;5H\x1b[1K'); + assert.deepEqual(getLines(term), ['¥¥ #', ' ¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); + term.writeSync('\x1b[3;6H\x1b[1K'); + assert.deepEqual(getLines(term), ['¥¥ #', ' ¥¥', ' ¥¥', '¥¥¥¥¥', '']); + }); + it('ICH', () => { + term.writeSync('\x1b[1;6H\x1b[@'); + assert.deepEqual(getLines(term), ['¥¥ ¥', '¥¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); + term.writeSync('\x1b[2;4H\x1b[2@'); + assert.deepEqual(getLines(term), ['¥¥ ¥', '¥ ¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); + term.writeSync('\x1b[3;4H\x1b[3@'); + assert.deepEqual(getLines(term), ['¥¥ ¥', '¥ ¥¥', '¥ ¥', '¥¥¥¥¥', '']); + term.writeSync('\x1b[4;4H\x1b[4@'); + assert.deepEqual(getLines(term), ['¥¥ ¥', '¥ ¥¥', '¥ ¥', '¥ ¥', '']); + }); + it('DCH', () => { + term.writeSync('\x1b[1;6H\x1b[P'); + assert.deepEqual(getLines(term), ['¥¥ ¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); + term.writeSync('\x1b[2;6H\x1b[2P'); + assert.deepEqual(getLines(term), ['¥¥ ¥¥', '¥¥ ¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); + term.writeSync('\x1b[3;6H\x1b[3P'); + assert.deepEqual(getLines(term), ['¥¥ ¥¥', '¥¥ ¥', '¥¥ ¥', '¥¥¥¥¥', '']); + }); + it('ECH', () => { + term.writeSync('\x1b[1;6H\x1b[X'); + assert.deepEqual(getLines(term), ['¥¥ ¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); + term.writeSync('\x1b[2;6H\x1b[2X'); + assert.deepEqual(getLines(term), ['¥¥ ¥¥', '¥¥ ¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); + term.writeSync('\x1b[3;6H\x1b[3X'); + assert.deepEqual(getLines(term), ['¥¥ ¥¥', '¥¥ ¥', '¥¥ ¥', '¥¥¥¥¥', '']); + }); + }); }); diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 963ce145..0124f499 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -398,6 +398,12 @@ export class InputHandler extends Disposable implements IInputHandler { let bufferRow = buffer.lines.get(buffer.y + buffer.ybase); this._dirtyRowService.markDirty(buffer.y); + + // handle wide chars: reset start_cell-1 if we would overwrite the second cell of a wide char + if (buffer.x && bufferRow.getWidth(buffer.x - 1) === 2) { + bufferRow.setCellFromCodePoint(buffer.x - 1, 0, 1, curAttr.fg, curAttr.bg); + } + for (let pos = start; pos < end; ++pos) { code = data[pos]; @@ -509,6 +515,12 @@ export class InputHandler extends Disposable implements IInputHandler { this._parser.precedingCodepoint = this._workCell.content; } } + + // handle wide chars: reset cell to the right if is second cell of a wide char + if (buffer.x < cols && bufferRow.getWidth(buffer.x) === 0 && !bufferRow.hasContent(buffer.x)) { + bufferRow.setCellFromCodePoint(buffer.x, 0, 1, curAttr.fg, curAttr.bg); + } + this._dirtyRowService.markDirty(buffer.y); } From ee741308069ae6401691829e31c55e7105412c92 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 26 Dec 2019 12:12:08 +1100 Subject: [PATCH 39/48] Standardize how colors helper lib is structured Fixes #2604 --- .../src/atlas/WebglCharAtlas.ts | 8 +- src/browser/Color.test.ts | 451 +++++++++--------- src/browser/Color.ts | 329 +++++++------ src/browser/ColorManager.ts | 60 +-- src/browser/renderer/BaseRenderLayer.ts | 8 +- .../renderer/atlas/DynamicCharAtlas.ts | 4 +- src/browser/renderer/dom/DomRenderer.ts | 4 +- .../dom/DomRendererRowFactory.test.ts | 38 +- .../renderer/dom/DomRendererRowFactory.ts | 6 +- 9 files changed, 473 insertions(+), 435 deletions(-) diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index c96c2a69..429669ee 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -11,7 +11,7 @@ import { throwIfFalsy } from '../WebglUtils'; import { IColor } from 'browser/Types'; import { IDisposable } from 'xterm'; import { AttributeData } from 'common/buffer/AttributeData'; -import { toCss, ensureContrastRatioRgba } from 'browser/Color'; +import { channels, rgba } from 'browser/Color'; // In practice we're probably never going to exhaust a texture this large. For debugging purposes, // however, it can be useful to set this to a really tiny value, to verify that LRU eviction works. @@ -224,7 +224,7 @@ export class WebglCharAtlas implements IDisposable { return this._getColorFromAnsiIndex(fgColor).css; case Attributes.CM_RGB: const arr = AttributeData.toColorRGB(fgColor); - return toCss(arr[0], arr[1], arr[2]); + return channels.toCss(arr[0], arr[1], arr[2]); case Attributes.CM_DEFAULT: default: if (inverse) { @@ -287,14 +287,14 @@ export class WebglCharAtlas implements IDisposable { const bgRgba = this._resolveBackgroundRgba(bgColorMode, bgColor, inverse); const fgRgba = this._resolveForegroundRgba(fgColorMode, fgColor, inverse, bold); - const result = ensureContrastRatioRgba(bgRgba, fgRgba, this._config.minimumContrastRatio); + const result = rgba.ensureContrastRatio(bgRgba, fgRgba, this._config.minimumContrastRatio); if (!result) { this._config.colors.contrastCache.setCss(bg, fg, null); return undefined; } - const css = toCss( + const css = channels.toCss( (result >> 24) & 0xFF, (result >> 16) & 0xFF, (result >> 8) & 0xFF diff --git a/src/browser/Color.test.ts b/src/browser/Color.test.ts index cbeeaba9..ff8da246 100644 --- a/src/browser/Color.test.ts +++ b/src/browser/Color.test.ts @@ -4,50 +4,243 @@ */ import { assert } from 'chai'; -import { blend, fromCss, toPaddedHex, toCss, toRgba, fromRgba, opaque, rgbRelativeLuminance, contrastRatio, ensureContrastRatioRgba } from 'browser/Color'; +import { channels, color, css, rgb, rgba, toPaddedHex, contrastRatio } from 'browser/Color'; describe('Color', () => { - describe('blend', () => { - it('should blend colors based on the alpha channel', () => { - assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF00', rgba: 0xFFFFFF00 }), { css: '#000000', rgba: 0x000000FF }); - assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF10', rgba: 0xFFFFFF10 }), { css: '#101010', rgba: 0x101010FF }); - assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF20', rgba: 0xFFFFFF20 }), { css: '#202020', rgba: 0x202020FF }); - assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF30', rgba: 0xFFFFFF30 }), { css: '#303030', rgba: 0x303030FF }); - assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF40', rgba: 0xFFFFFF40 }), { css: '#404040', rgba: 0x404040FF }); - assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF50', rgba: 0xFFFFFF50 }), { css: '#505050', rgba: 0x505050FF }); - assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF60', rgba: 0xFFFFFF60 }), { css: '#606060', rgba: 0x606060FF }); - assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF70', rgba: 0xFFFFFF70 }), { css: '#707070', rgba: 0x707070FF }); - assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF80', rgba: 0xFFFFFF80 }), { css: '#808080', rgba: 0x808080FF }); - assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF90', rgba: 0xFFFFFF90 }), { css: '#909090', rgba: 0x909090FF }); - assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFFA0', rgba: 0xFFFFFFA0 }), { css: '#a0a0a0', rgba: 0xA0A0A0FF }); - assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFFB0', rgba: 0xFFFFFFB0 }), { css: '#b0b0b0', rgba: 0xB0B0B0FF }); - assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFFC0', rgba: 0xFFFFFFC0 }), { css: '#c0c0c0', rgba: 0xC0C0C0FF }); - assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFFD0', rgba: 0xFFFFFFD0 }), { css: '#d0d0d0', rgba: 0xD0D0D0FF }); - assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFFE0', rgba: 0xFFFFFFE0 }), { css: '#e0e0e0', rgba: 0xE0E0E0FF }); - assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFFF0', rgba: 0xFFFFFFF0 }), { css: '#f0f0f0', rgba: 0xF0F0F0FF }); - assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFFFF', rgba: 0xFFFFFFFF }), { css: '#FFFFFFFF', rgba: 0xFFFFFFFF }); + + describe('channels', () => { + describe('toCss', () => { + it('should convert an rgb array to css hex string', () => { + assert.equal(channels.toCss(0x00, 0x00, 0x00), '#000000'); + assert.equal(channels.toCss(0x10, 0x10, 0x10), '#101010'); + assert.equal(channels.toCss(0x20, 0x20, 0x20), '#202020'); + assert.equal(channels.toCss(0x30, 0x30, 0x30), '#303030'); + assert.equal(channels.toCss(0x40, 0x40, 0x40), '#404040'); + assert.equal(channels.toCss(0x50, 0x50, 0x50), '#505050'); + assert.equal(channels.toCss(0x60, 0x60, 0x60), '#606060'); + assert.equal(channels.toCss(0x70, 0x70, 0x70), '#707070'); + assert.equal(channels.toCss(0x80, 0x80, 0x80), '#808080'); + assert.equal(channels.toCss(0x90, 0x90, 0x90), '#909090'); + assert.equal(channels.toCss(0xa0, 0xa0, 0xa0), '#a0a0a0'); + assert.equal(channels.toCss(0xb0, 0xb0, 0xb0), '#b0b0b0'); + assert.equal(channels.toCss(0xc0, 0xc0, 0xc0), '#c0c0c0'); + assert.equal(channels.toCss(0xd0, 0xd0, 0xd0), '#d0d0d0'); + assert.equal(channels.toCss(0xe0, 0xe0, 0xe0), '#e0e0e0'); + assert.equal(channels.toCss(0xf0, 0xf0, 0xf0), '#f0f0f0'); + assert.equal(channels.toCss(0xff, 0xff, 0xff), '#ffffff'); + }); + }); + + describe('toRgba', () => { + it('should convert an rgb array to an rgba number', () => { + assert.equal(channels.toRgba(0x00, 0x00, 0x00), 0x000000FF); + assert.equal(channels.toRgba(0x10, 0x10, 0x10), 0x101010FF); + assert.equal(channels.toRgba(0x20, 0x20, 0x20), 0x202020FF); + assert.equal(channels.toRgba(0x30, 0x30, 0x30), 0x303030FF); + assert.equal(channels.toRgba(0x40, 0x40, 0x40), 0x404040FF); + assert.equal(channels.toRgba(0x50, 0x50, 0x50), 0x505050FF); + assert.equal(channels.toRgba(0x60, 0x60, 0x60), 0x606060FF); + assert.equal(channels.toRgba(0x70, 0x70, 0x70), 0x707070FF); + assert.equal(channels.toRgba(0x80, 0x80, 0x80), 0x808080FF); + assert.equal(channels.toRgba(0x90, 0x90, 0x90), 0x909090FF); + assert.equal(channels.toRgba(0xa0, 0xa0, 0xa0), 0xa0a0a0FF); + assert.equal(channels.toRgba(0xb0, 0xb0, 0xb0), 0xb0b0b0FF); + assert.equal(channels.toRgba(0xc0, 0xc0, 0xc0), 0xc0c0c0FF); + assert.equal(channels.toRgba(0xd0, 0xd0, 0xd0), 0xd0d0d0FF); + assert.equal(channels.toRgba(0xe0, 0xe0, 0xe0), 0xe0e0e0FF); + assert.equal(channels.toRgba(0xf0, 0xf0, 0xf0), 0xf0f0f0FF); + assert.equal(channels.toRgba(0xff, 0xff, 0xff), 0xffffffFF); + }); + it('should convert an rgba array to an rgba number', () => { + assert.equal(channels.toRgba(0x00, 0x00, 0x00, 0x00), 0x00000000); + assert.equal(channels.toRgba(0x10, 0x10, 0x10, 0x10), 0x10101010); + assert.equal(channels.toRgba(0x20, 0x20, 0x20, 0x20), 0x20202020); + assert.equal(channels.toRgba(0x30, 0x30, 0x30, 0x30), 0x30303030); + assert.equal(channels.toRgba(0x40, 0x40, 0x40, 0x40), 0x40404040); + assert.equal(channels.toRgba(0x50, 0x50, 0x50, 0x50), 0x50505050); + assert.equal(channels.toRgba(0x60, 0x60, 0x60, 0x60), 0x60606060); + assert.equal(channels.toRgba(0x70, 0x70, 0x70, 0x70), 0x70707070); + assert.equal(channels.toRgba(0x80, 0x80, 0x80, 0x80), 0x80808080); + assert.equal(channels.toRgba(0x90, 0x90, 0x90, 0x90), 0x90909090); + assert.equal(channels.toRgba(0xa0, 0xa0, 0xa0, 0xa0), 0xa0a0a0a0); + assert.equal(channels.toRgba(0xb0, 0xb0, 0xb0, 0xb0), 0xb0b0b0b0); + assert.equal(channels.toRgba(0xc0, 0xc0, 0xc0, 0xc0), 0xc0c0c0c0); + assert.equal(channels.toRgba(0xd0, 0xd0, 0xd0, 0xd0), 0xd0d0d0d0); + assert.equal(channels.toRgba(0xe0, 0xe0, 0xe0, 0xe0), 0xe0e0e0e0); + assert.equal(channels.toRgba(0xf0, 0xf0, 0xf0, 0xf0), 0xf0f0f0f0); + assert.equal(channels.toRgba(0xff, 0xff, 0xff, 0xff), 0xffffffff); + }); }); }); - describe('fromCss', () => { - it('should covert a CSS string to an IColor', () => { - assert.deepEqual(fromCss('#000000'), { css: '#000000', rgba: 0x000000FF }); - assert.deepEqual(fromCss('#101010'), { css: '#101010', rgba: 0x101010FF }); - assert.deepEqual(fromCss('#202020'), { css: '#202020', rgba: 0x202020FF }); - assert.deepEqual(fromCss('#303030'), { css: '#303030', rgba: 0x303030FF }); - assert.deepEqual(fromCss('#404040'), { css: '#404040', rgba: 0x404040FF }); - assert.deepEqual(fromCss('#505050'), { css: '#505050', rgba: 0x505050FF }); - assert.deepEqual(fromCss('#606060'), { css: '#606060', rgba: 0x606060FF }); - assert.deepEqual(fromCss('#707070'), { css: '#707070', rgba: 0x707070FF }); - assert.deepEqual(fromCss('#808080'), { css: '#808080', rgba: 0x808080FF }); - assert.deepEqual(fromCss('#909090'), { css: '#909090', rgba: 0x909090FF }); - assert.deepEqual(fromCss('#a0a0a0'), { css: '#a0a0a0', rgba: 0xa0a0a0FF }); - assert.deepEqual(fromCss('#b0b0b0'), { css: '#b0b0b0', rgba: 0xb0b0b0FF }); - assert.deepEqual(fromCss('#c0c0c0'), { css: '#c0c0c0', rgba: 0xc0c0c0FF }); - assert.deepEqual(fromCss('#d0d0d0'), { css: '#d0d0d0', rgba: 0xd0d0d0FF }); - assert.deepEqual(fromCss('#e0e0e0'), { css: '#e0e0e0', rgba: 0xe0e0e0FF }); - assert.deepEqual(fromCss('#f0f0f0'), { css: '#f0f0f0', rgba: 0xf0f0f0FF }); - assert.deepEqual(fromCss('#ffffff'), { css: '#ffffff', rgba: 0xffffffFF }); + describe('color', () => { + describe('blend', () => { + it('should blend colors based on the alpha channel', () => { + assert.deepEqual(color.blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF00', rgba: 0xFFFFFF00 }), { css: '#000000', rgba: 0x000000FF }); + assert.deepEqual(color.blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF10', rgba: 0xFFFFFF10 }), { css: '#101010', rgba: 0x101010FF }); + assert.deepEqual(color.blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF20', rgba: 0xFFFFFF20 }), { css: '#202020', rgba: 0x202020FF }); + assert.deepEqual(color.blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF30', rgba: 0xFFFFFF30 }), { css: '#303030', rgba: 0x303030FF }); + assert.deepEqual(color.blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF40', rgba: 0xFFFFFF40 }), { css: '#404040', rgba: 0x404040FF }); + assert.deepEqual(color.blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF50', rgba: 0xFFFFFF50 }), { css: '#505050', rgba: 0x505050FF }); + assert.deepEqual(color.blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF60', rgba: 0xFFFFFF60 }), { css: '#606060', rgba: 0x606060FF }); + assert.deepEqual(color.blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF70', rgba: 0xFFFFFF70 }), { css: '#707070', rgba: 0x707070FF }); + assert.deepEqual(color.blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF80', rgba: 0xFFFFFF80 }), { css: '#808080', rgba: 0x808080FF }); + assert.deepEqual(color.blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF90', rgba: 0xFFFFFF90 }), { css: '#909090', rgba: 0x909090FF }); + assert.deepEqual(color.blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFFA0', rgba: 0xFFFFFFA0 }), { css: '#a0a0a0', rgba: 0xA0A0A0FF }); + assert.deepEqual(color.blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFFB0', rgba: 0xFFFFFFB0 }), { css: '#b0b0b0', rgba: 0xB0B0B0FF }); + assert.deepEqual(color.blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFFC0', rgba: 0xFFFFFFC0 }), { css: '#c0c0c0', rgba: 0xC0C0C0FF }); + assert.deepEqual(color.blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFFD0', rgba: 0xFFFFFFD0 }), { css: '#d0d0d0', rgba: 0xD0D0D0FF }); + assert.deepEqual(color.blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFFE0', rgba: 0xFFFFFFE0 }), { css: '#e0e0e0', rgba: 0xE0E0E0FF }); + assert.deepEqual(color.blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFFF0', rgba: 0xFFFFFFF0 }), { css: '#f0f0f0', rgba: 0xF0F0F0FF }); + assert.deepEqual(color.blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFFFF', rgba: 0xFFFFFFFF }), { css: '#FFFFFFFF', rgba: 0xFFFFFFFF }); + }); + }); + + describe('opaque', () => { + it('should make the color opaque', () => { + assert.deepEqual(color.opaque({ css: '#00000000', rgba: 0x00000000 }), { css: '#000000', rgba: 0x000000FF }); + assert.deepEqual(color.opaque({ css: '#10101010', rgba: 0x10101010 }), { css: '#101010', rgba: 0x101010FF }); + assert.deepEqual(color.opaque({ css: '#20202020', rgba: 0x20202020 }), { css: '#202020', rgba: 0x202020FF }); + assert.deepEqual(color.opaque({ css: '#30303030', rgba: 0x30303030 }), { css: '#303030', rgba: 0x303030FF }); + assert.deepEqual(color.opaque({ css: '#40404040', rgba: 0x40404040 }), { css: '#404040', rgba: 0x404040FF }); + assert.deepEqual(color.opaque({ css: '#50505050', rgba: 0x50505050 }), { css: '#505050', rgba: 0x505050FF }); + assert.deepEqual(color.opaque({ css: '#60606060', rgba: 0x60606060 }), { css: '#606060', rgba: 0x606060FF }); + assert.deepEqual(color.opaque({ css: '#70707070', rgba: 0x70707070 }), { css: '#707070', rgba: 0x707070FF }); + assert.deepEqual(color.opaque({ css: '#80808080', rgba: 0x80808080 }), { css: '#808080', rgba: 0x808080FF }); + assert.deepEqual(color.opaque({ css: '#90909090', rgba: 0x90909090 }), { css: '#909090', rgba: 0x909090FF }); + assert.deepEqual(color.opaque({ css: '#a0a0a0a0', rgba: 0xa0a0a0a0 }), { css: '#a0a0a0', rgba: 0xa0a0a0FF }); + assert.deepEqual(color.opaque({ css: '#b0b0b0b0', rgba: 0xb0b0b0b0 }), { css: '#b0b0b0', rgba: 0xb0b0b0FF }); + assert.deepEqual(color.opaque({ css: '#c0c0c0c0', rgba: 0xc0c0c0c0 }), { css: '#c0c0c0', rgba: 0xc0c0c0FF }); + assert.deepEqual(color.opaque({ css: '#d0d0d0d0', rgba: 0xd0d0d0d0 }), { css: '#d0d0d0', rgba: 0xd0d0d0FF }); + assert.deepEqual(color.opaque({ css: '#e0e0e0e0', rgba: 0xe0e0e0e0 }), { css: '#e0e0e0', rgba: 0xe0e0e0FF }); + assert.deepEqual(color.opaque({ css: '#f0f0f0f0', rgba: 0xf0f0f0f0 }), { css: '#f0f0f0', rgba: 0xf0f0f0FF }); + assert.deepEqual(color.opaque({ css: '#ffffffff', rgba: 0xffffffff }), { css: '#ffffff', rgba: 0xffffffFF }); + }); + }); + }); + + describe('css', () => { + describe('toColor', () => { + it('should covert a CSS string to an IColor', () => { + assert.deepEqual(css.toColor('#000000'), { css: '#000000', rgba: 0x000000FF }); + assert.deepEqual(css.toColor('#101010'), { css: '#101010', rgba: 0x101010FF }); + assert.deepEqual(css.toColor('#202020'), { css: '#202020', rgba: 0x202020FF }); + assert.deepEqual(css.toColor('#303030'), { css: '#303030', rgba: 0x303030FF }); + assert.deepEqual(css.toColor('#404040'), { css: '#404040', rgba: 0x404040FF }); + assert.deepEqual(css.toColor('#505050'), { css: '#505050', rgba: 0x505050FF }); + assert.deepEqual(css.toColor('#606060'), { css: '#606060', rgba: 0x606060FF }); + assert.deepEqual(css.toColor('#707070'), { css: '#707070', rgba: 0x707070FF }); + assert.deepEqual(css.toColor('#808080'), { css: '#808080', rgba: 0x808080FF }); + assert.deepEqual(css.toColor('#909090'), { css: '#909090', rgba: 0x909090FF }); + assert.deepEqual(css.toColor('#a0a0a0'), { css: '#a0a0a0', rgba: 0xa0a0a0FF }); + assert.deepEqual(css.toColor('#b0b0b0'), { css: '#b0b0b0', rgba: 0xb0b0b0FF }); + assert.deepEqual(css.toColor('#c0c0c0'), { css: '#c0c0c0', rgba: 0xc0c0c0FF }); + assert.deepEqual(css.toColor('#d0d0d0'), { css: '#d0d0d0', rgba: 0xd0d0d0FF }); + assert.deepEqual(css.toColor('#e0e0e0'), { css: '#e0e0e0', rgba: 0xe0e0e0FF }); + assert.deepEqual(css.toColor('#f0f0f0'), { css: '#f0f0f0', rgba: 0xf0f0f0FF }); + assert.deepEqual(css.toColor('#ffffff'), { css: '#ffffff', rgba: 0xffffffFF }); + }); + }); + }); + + describe('rgb', () => { + describe('relativeLuminance', () => { + it('should calculate the relative luminance of the color', () => { + assert.equal(rgb.relativeLuminance(0x000000), 0); + assert.equal(rgb.relativeLuminance(0x101010).toFixed(4), '0.0052'); + assert.equal(rgb.relativeLuminance(0x202020).toFixed(4), '0.0144'); + assert.equal(rgb.relativeLuminance(0x303030).toFixed(4), '0.0296'); + assert.equal(rgb.relativeLuminance(0x404040).toFixed(4), '0.0513'); + assert.equal(rgb.relativeLuminance(0x505050).toFixed(4), '0.0802'); + assert.equal(rgb.relativeLuminance(0x606060).toFixed(4), '0.1170'); + assert.equal(rgb.relativeLuminance(0x707070).toFixed(4), '0.1620'); + assert.equal(rgb.relativeLuminance(0x808080).toFixed(4), '0.2159'); + assert.equal(rgb.relativeLuminance(0x909090).toFixed(4), '0.2789'); + assert.equal(rgb.relativeLuminance(0xA0A0A0).toFixed(4), '0.3515'); + assert.equal(rgb.relativeLuminance(0xB0B0B0).toFixed(4), '0.4342'); + assert.equal(rgb.relativeLuminance(0xC0C0C0).toFixed(4), '0.5271'); + assert.equal(rgb.relativeLuminance(0xD0D0D0).toFixed(4), '0.6308'); + assert.equal(rgb.relativeLuminance(0xE0E0E0).toFixed(4), '0.7454'); + assert.equal(rgb.relativeLuminance(0xF0F0F0).toFixed(4), '0.8714'); + assert.equal(rgb.relativeLuminance(0xFFFFFF), 1); + }); + }); + }); + + describe('rgba', () => { + describe('ensureContrastRatio', () => { + it('should return undefined if the color already meets the contrast ratio (black bg)', () => { + assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 1), undefined); + assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 2), undefined); + assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 3), undefined); + }); + it('should return a color that meets the contrast ratio (black bg)', () => { + assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 4), 0x707070ff); + assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 5), 0x7f7f7fff); + assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 6), 0x8c8c8cff); + assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 7), 0x989898ff); + assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 8), 0xa3a3a3ff); + assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 9), 0xadadadff); + assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 10), 0xb6b6b6ff); + assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 11), 0xbebebeff); + assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 12), 0xc5c5c5ff); + assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 13), 0xd1d1d1ff); + assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 14), 0xd6d6d6ff); + assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 15), 0xdbdbdbff); + assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 16), 0xe3e3e3ff); + assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 17), 0xe9e9e9ff); + assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 18), 0xeeeeeeff); + assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 19), 0xf4f4f4ff); + assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 20), 0xfafafaff); + assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 21), 0xffffffff); + }); + it('should return undefined if the color already meets the contrast ratio (white bg)', () => { + assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 1), undefined); + assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 2), undefined); + assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 3), undefined); + assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 4), undefined); + assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 5), undefined); + assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 6), undefined); + }); + it('should return a color that meets the contrast ratio (white bg)', () => { + assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 7), 0x565656ff); + assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 8), 0x4d4d4dff); + assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 9), 0x454545ff); + assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 10), 0x3e3e3eff); + assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 11), 0x373737ff); + assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 12), 0x313131ff); + assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 13), 0x313131ff); + assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 14), 0x272727ff); + assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 15), 0x232323ff); + assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 16), 0x1f1f1fff); + assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 17), 0x1b1b1bff); + assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 18), 0x151515ff); + assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 19), 0x101010ff); + assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 20), 0x080808ff); + assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 21), 0x000000ff); + }); + }); + + describe('toChannels', () => { + it('should convert an rgba number to an rgba array', () => { + assert.deepEqual(rgba.toChannels(0x00000000), [0x00, 0x00, 0x00, 0x00]); + assert.deepEqual(rgba.toChannels(0x10101010), [0x10, 0x10, 0x10, 0x10]); + assert.deepEqual(rgba.toChannels(0x20202020), [0x20, 0x20, 0x20, 0x20]); + assert.deepEqual(rgba.toChannels(0x30303030), [0x30, 0x30, 0x30, 0x30]); + assert.deepEqual(rgba.toChannels(0x40404040), [0x40, 0x40, 0x40, 0x40]); + assert.deepEqual(rgba.toChannels(0x50505050), [0x50, 0x50, 0x50, 0x50]); + assert.deepEqual(rgba.toChannels(0x60606060), [0x60, 0x60, 0x60, 0x60]); + assert.deepEqual(rgba.toChannels(0x70707070), [0x70, 0x70, 0x70, 0x70]); + assert.deepEqual(rgba.toChannels(0x80808080), [0x80, 0x80, 0x80, 0x80]); + assert.deepEqual(rgba.toChannels(0x90909090), [0x90, 0x90, 0x90, 0x90]); + assert.deepEqual(rgba.toChannels(0xa0a0a0a0), [0xa0, 0xa0, 0xa0, 0xa0]); + assert.deepEqual(rgba.toChannels(0xb0b0b0b0), [0xb0, 0xb0, 0xb0, 0xb0]); + assert.deepEqual(rgba.toChannels(0xc0c0c0c0), [0xc0, 0xc0, 0xc0, 0xc0]); + assert.deepEqual(rgba.toChannels(0xd0d0d0d0), [0xd0, 0xd0, 0xd0, 0xd0]); + assert.deepEqual(rgba.toChannels(0xe0e0e0e0), [0xe0, 0xe0, 0xe0, 0xe0]); + assert.deepEqual(rgba.toChannels(0xf0f0f0f0), [0xf0, 0xf0, 0xf0, 0xf0]); + assert.deepEqual(rgba.toChannels(0xffffffff), [0xff, 0xff, 0xff, 0xff]); + }); }); }); @@ -73,134 +266,6 @@ describe('Color', () => { }); }); - describe('toCss', () => { - it('should convert an rgb array to css hex string', () => { - assert.equal(toCss(0x00, 0x00, 0x00), '#000000'); - assert.equal(toCss(0x10, 0x10, 0x10), '#101010'); - assert.equal(toCss(0x20, 0x20, 0x20), '#202020'); - assert.equal(toCss(0x30, 0x30, 0x30), '#303030'); - assert.equal(toCss(0x40, 0x40, 0x40), '#404040'); - assert.equal(toCss(0x50, 0x50, 0x50), '#505050'); - assert.equal(toCss(0x60, 0x60, 0x60), '#606060'); - assert.equal(toCss(0x70, 0x70, 0x70), '#707070'); - assert.equal(toCss(0x80, 0x80, 0x80), '#808080'); - assert.equal(toCss(0x90, 0x90, 0x90), '#909090'); - assert.equal(toCss(0xa0, 0xa0, 0xa0), '#a0a0a0'); - assert.equal(toCss(0xb0, 0xb0, 0xb0), '#b0b0b0'); - assert.equal(toCss(0xc0, 0xc0, 0xc0), '#c0c0c0'); - assert.equal(toCss(0xd0, 0xd0, 0xd0), '#d0d0d0'); - assert.equal(toCss(0xe0, 0xe0, 0xe0), '#e0e0e0'); - assert.equal(toCss(0xf0, 0xf0, 0xf0), '#f0f0f0'); - assert.equal(toCss(0xff, 0xff, 0xff), '#ffffff'); - }); - }); - - describe('toRgba', () => { - it('should convert an rgb array to an rgba number', () => { - assert.equal(toRgba(0x00, 0x00, 0x00), 0x000000FF); - assert.equal(toRgba(0x10, 0x10, 0x10), 0x101010FF); - assert.equal(toRgba(0x20, 0x20, 0x20), 0x202020FF); - assert.equal(toRgba(0x30, 0x30, 0x30), 0x303030FF); - assert.equal(toRgba(0x40, 0x40, 0x40), 0x404040FF); - assert.equal(toRgba(0x50, 0x50, 0x50), 0x505050FF); - assert.equal(toRgba(0x60, 0x60, 0x60), 0x606060FF); - assert.equal(toRgba(0x70, 0x70, 0x70), 0x707070FF); - assert.equal(toRgba(0x80, 0x80, 0x80), 0x808080FF); - assert.equal(toRgba(0x90, 0x90, 0x90), 0x909090FF); - assert.equal(toRgba(0xa0, 0xa0, 0xa0), 0xa0a0a0FF); - assert.equal(toRgba(0xb0, 0xb0, 0xb0), 0xb0b0b0FF); - assert.equal(toRgba(0xc0, 0xc0, 0xc0), 0xc0c0c0FF); - assert.equal(toRgba(0xd0, 0xd0, 0xd0), 0xd0d0d0FF); - assert.equal(toRgba(0xe0, 0xe0, 0xe0), 0xe0e0e0FF); - assert.equal(toRgba(0xf0, 0xf0, 0xf0), 0xf0f0f0FF); - assert.equal(toRgba(0xff, 0xff, 0xff), 0xffffffFF); - }); - it('should convert an rgba array to an rgba number', () => { - assert.equal(toRgba(0x00, 0x00, 0x00, 0x00), 0x00000000); - assert.equal(toRgba(0x10, 0x10, 0x10, 0x10), 0x10101010); - assert.equal(toRgba(0x20, 0x20, 0x20, 0x20), 0x20202020); - assert.equal(toRgba(0x30, 0x30, 0x30, 0x30), 0x30303030); - assert.equal(toRgba(0x40, 0x40, 0x40, 0x40), 0x40404040); - assert.equal(toRgba(0x50, 0x50, 0x50, 0x50), 0x50505050); - assert.equal(toRgba(0x60, 0x60, 0x60, 0x60), 0x60606060); - assert.equal(toRgba(0x70, 0x70, 0x70, 0x70), 0x70707070); - assert.equal(toRgba(0x80, 0x80, 0x80, 0x80), 0x80808080); - assert.equal(toRgba(0x90, 0x90, 0x90, 0x90), 0x90909090); - assert.equal(toRgba(0xa0, 0xa0, 0xa0, 0xa0), 0xa0a0a0a0); - assert.equal(toRgba(0xb0, 0xb0, 0xb0, 0xb0), 0xb0b0b0b0); - assert.equal(toRgba(0xc0, 0xc0, 0xc0, 0xc0), 0xc0c0c0c0); - assert.equal(toRgba(0xd0, 0xd0, 0xd0, 0xd0), 0xd0d0d0d0); - assert.equal(toRgba(0xe0, 0xe0, 0xe0, 0xe0), 0xe0e0e0e0); - assert.equal(toRgba(0xf0, 0xf0, 0xf0, 0xf0), 0xf0f0f0f0); - assert.equal(toRgba(0xff, 0xff, 0xff, 0xff), 0xffffffff); - }); - }); - - describe('fromRgba', () => { - it('should convert an rgba number to an rgba array', () => { - assert.deepEqual(fromRgba(0x00000000), [0x00, 0x00, 0x00, 0x00]); - assert.deepEqual(fromRgba(0x10101010), [0x10, 0x10, 0x10, 0x10]); - assert.deepEqual(fromRgba(0x20202020), [0x20, 0x20, 0x20, 0x20]); - assert.deepEqual(fromRgba(0x30303030), [0x30, 0x30, 0x30, 0x30]); - assert.deepEqual(fromRgba(0x40404040), [0x40, 0x40, 0x40, 0x40]); - assert.deepEqual(fromRgba(0x50505050), [0x50, 0x50, 0x50, 0x50]); - assert.deepEqual(fromRgba(0x60606060), [0x60, 0x60, 0x60, 0x60]); - assert.deepEqual(fromRgba(0x70707070), [0x70, 0x70, 0x70, 0x70]); - assert.deepEqual(fromRgba(0x80808080), [0x80, 0x80, 0x80, 0x80]); - assert.deepEqual(fromRgba(0x90909090), [0x90, 0x90, 0x90, 0x90]); - assert.deepEqual(fromRgba(0xa0a0a0a0), [0xa0, 0xa0, 0xa0, 0xa0]); - assert.deepEqual(fromRgba(0xb0b0b0b0), [0xb0, 0xb0, 0xb0, 0xb0]); - assert.deepEqual(fromRgba(0xc0c0c0c0), [0xc0, 0xc0, 0xc0, 0xc0]); - assert.deepEqual(fromRgba(0xd0d0d0d0), [0xd0, 0xd0, 0xd0, 0xd0]); - assert.deepEqual(fromRgba(0xe0e0e0e0), [0xe0, 0xe0, 0xe0, 0xe0]); - assert.deepEqual(fromRgba(0xf0f0f0f0), [0xf0, 0xf0, 0xf0, 0xf0]); - assert.deepEqual(fromRgba(0xffffffff), [0xff, 0xff, 0xff, 0xff]); - }); - }); - - describe('opaque', () => { - it('should make the color opaque', () => { - assert.deepEqual(opaque({ css: '#00000000', rgba: 0x00000000 }), { css: '#000000', rgba: 0x000000FF }); - assert.deepEqual(opaque({ css: '#10101010', rgba: 0x10101010 }), { css: '#101010', rgba: 0x101010FF }); - assert.deepEqual(opaque({ css: '#20202020', rgba: 0x20202020 }), { css: '#202020', rgba: 0x202020FF }); - assert.deepEqual(opaque({ css: '#30303030', rgba: 0x30303030 }), { css: '#303030', rgba: 0x303030FF }); - assert.deepEqual(opaque({ css: '#40404040', rgba: 0x40404040 }), { css: '#404040', rgba: 0x404040FF }); - assert.deepEqual(opaque({ css: '#50505050', rgba: 0x50505050 }), { css: '#505050', rgba: 0x505050FF }); - assert.deepEqual(opaque({ css: '#60606060', rgba: 0x60606060 }), { css: '#606060', rgba: 0x606060FF }); - assert.deepEqual(opaque({ css: '#70707070', rgba: 0x70707070 }), { css: '#707070', rgba: 0x707070FF }); - assert.deepEqual(opaque({ css: '#80808080', rgba: 0x80808080 }), { css: '#808080', rgba: 0x808080FF }); - assert.deepEqual(opaque({ css: '#90909090', rgba: 0x90909090 }), { css: '#909090', rgba: 0x909090FF }); - assert.deepEqual(opaque({ css: '#a0a0a0a0', rgba: 0xa0a0a0a0 }), { css: '#a0a0a0', rgba: 0xa0a0a0FF }); - assert.deepEqual(opaque({ css: '#b0b0b0b0', rgba: 0xb0b0b0b0 }), { css: '#b0b0b0', rgba: 0xb0b0b0FF }); - assert.deepEqual(opaque({ css: '#c0c0c0c0', rgba: 0xc0c0c0c0 }), { css: '#c0c0c0', rgba: 0xc0c0c0FF }); - assert.deepEqual(opaque({ css: '#d0d0d0d0', rgba: 0xd0d0d0d0 }), { css: '#d0d0d0', rgba: 0xd0d0d0FF }); - assert.deepEqual(opaque({ css: '#e0e0e0e0', rgba: 0xe0e0e0e0 }), { css: '#e0e0e0', rgba: 0xe0e0e0FF }); - assert.deepEqual(opaque({ css: '#f0f0f0f0', rgba: 0xf0f0f0f0 }), { css: '#f0f0f0', rgba: 0xf0f0f0FF }); - assert.deepEqual(opaque({ css: '#ffffffff', rgba: 0xffffffff }), { css: '#ffffff', rgba: 0xffffffFF }); - }); - }); - - describe('rgbRelativeLuminance', () => { - it('should calculate the relative luminance of the color', () => { - assert.equal(rgbRelativeLuminance(0x000000), 0); - assert.equal(rgbRelativeLuminance(0x101010).toFixed(4), '0.0052'); - assert.equal(rgbRelativeLuminance(0x202020).toFixed(4), '0.0144'); - assert.equal(rgbRelativeLuminance(0x303030).toFixed(4), '0.0296'); - assert.equal(rgbRelativeLuminance(0x404040).toFixed(4), '0.0513'); - assert.equal(rgbRelativeLuminance(0x505050).toFixed(4), '0.0802'); - assert.equal(rgbRelativeLuminance(0x606060).toFixed(4), '0.1170'); - assert.equal(rgbRelativeLuminance(0x707070).toFixed(4), '0.1620'); - assert.equal(rgbRelativeLuminance(0x808080).toFixed(4), '0.2159'); - assert.equal(rgbRelativeLuminance(0x909090).toFixed(4), '0.2789'); - assert.equal(rgbRelativeLuminance(0xA0A0A0).toFixed(4), '0.3515'); - assert.equal(rgbRelativeLuminance(0xB0B0B0).toFixed(4), '0.4342'); - assert.equal(rgbRelativeLuminance(0xC0C0C0).toFixed(4), '0.5271'); - assert.equal(rgbRelativeLuminance(0xD0D0D0).toFixed(4), '0.6308'); - assert.equal(rgbRelativeLuminance(0xE0E0E0).toFixed(4), '0.7454'); - assert.equal(rgbRelativeLuminance(0xF0F0F0).toFixed(4), '0.8714'); - assert.equal(rgbRelativeLuminance(0xFFFFFF), 1); - }); - }); describe('contrastRatio', () => { it('should calculate the relative luminance of the color', () => { assert.equal(contrastRatio(0, 0), 1); @@ -212,56 +277,4 @@ describe('Color', () => { assert.equal(contrastRatio(1, 0), 21); }); }); - describe('ensureContrastRatioRgba', () => { - it('should return undefined if the color already meets the contrast ratio (black bg)', () => { - assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 1), undefined); - assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 2), undefined); - assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 3), undefined); - }); - it('should return a color that meets the contrast ratio (black bg)', () => { - assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 4), 0x707070ff); - assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 5), 0x7f7f7fff); - assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 6), 0x8c8c8cff); - assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 7), 0x989898ff); - assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 8), 0xa3a3a3ff); - assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 9), 0xadadadff); - assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 10), 0xb6b6b6ff); - assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 11), 0xbebebeff); - assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 12), 0xc5c5c5ff); - assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 13), 0xd1d1d1ff); - assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 14), 0xd6d6d6ff); - assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 15), 0xdbdbdbff); - assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 16), 0xe3e3e3ff); - assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 17), 0xe9e9e9ff); - assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 18), 0xeeeeeeff); - assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 19), 0xf4f4f4ff); - assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 20), 0xfafafaff); - assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 21), 0xffffffff); - }); - it('should return undefined if the color already meets the contrast ratio (white bg)', () => { - assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 1), undefined); - assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 2), undefined); - assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 3), undefined); - assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 4), undefined); - assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 5), undefined); - assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 6), undefined); - }); - it('should return a color that meets the contrast ratio (white bg)', () => { - assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 7), 0x565656ff); - assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 8), 0x4d4d4dff); - assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 9), 0x454545ff); - assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 10), 0x3e3e3eff); - assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 11), 0x373737ff); - assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 12), 0x313131ff); - assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 13), 0x313131ff); - assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 14), 0x272727ff); - assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 15), 0x232323ff); - assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 16), 0x1f1f1fff); - assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 17), 0x1b1b1bff); - assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 18), 0x151515ff); - assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 19), 0x101010ff); - assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 20), 0x080808ff); - assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 21), 0x000000ff); - }); - }); }); diff --git a/src/browser/Color.ts b/src/browser/Color.ts index e40ff9e1..9649227e 100644 --- a/src/browser/Color.ts +++ b/src/browser/Color.ts @@ -5,33 +5,186 @@ import { IColor } from 'browser/Types'; -export function blend(bg: IColor, fg: IColor): IColor { - const a = (fg.rgba & 0xFF) / 255; - if (a === 1) { - return { - css: fg.css, - rgba: fg.rgba - }; +/** + * Helper functions where the source type is "channels" (individual color channels as numbers). + */ +export namespace channels { + export function toCss(r: number, g: number, b: number, a?: number): string { + if (a !== undefined) { + return `#${toPaddedHex(r)}${toPaddedHex(g)}${toPaddedHex(b)}${toPaddedHex(a)}`; + } + return `#${toPaddedHex(r)}${toPaddedHex(g)}${toPaddedHex(b)}`; + } + + export function toRgba(r: number, g: number, b: number, a: number = 0xFF): number { + // >>> 0 forces an unsigned int + return (r << 24 | g << 16 | b << 8 | a) >>> 0; } - const fgR = (fg.rgba >> 24) & 0xFF; - const fgG = (fg.rgba >> 16) & 0xFF; - const fgB = (fg.rgba >> 8) & 0xFF; - const bgR = (bg.rgba >> 24) & 0xFF; - const bgG = (bg.rgba >> 16) & 0xFF; - const bgB = (bg.rgba >> 8) & 0xFF; - const r = bgR + Math.round((fgR - bgR) * a); - const g = bgG + Math.round((fgG - bgG) * a); - const b = bgB + Math.round((fgB - bgB) * a); - const css = toCss(r, g, b); - const rgba = toRgba(r, g, b); - return { css, rgba }; } -export function fromCss(css: string): IColor { - return { - css, - rgba: (parseInt(css.slice(1), 16) << 8 | 0xFF) >>> 0 - }; +/** + * Helper functions where the source type is `IColor`. + */ +export namespace color { + export function blend(bg: IColor, fg: IColor): IColor { + const a = (fg.rgba & 0xFF) / 255; + if (a === 1) { + return { + css: fg.css, + rgba: fg.rgba + }; + } + const fgR = (fg.rgba >> 24) & 0xFF; + const fgG = (fg.rgba >> 16) & 0xFF; + const fgB = (fg.rgba >> 8) & 0xFF; + const bgR = (bg.rgba >> 24) & 0xFF; + const bgG = (bg.rgba >> 16) & 0xFF; + const bgB = (bg.rgba >> 8) & 0xFF; + const r = bgR + Math.round((fgR - bgR) * a); + const g = bgG + Math.round((fgG - bgG) * a); + const b = bgB + Math.round((fgB - bgB) * a); + const css = channels.toCss(r, g, b); + const rgba = channels.toRgba(r, g, b); + return { css, rgba }; + } + + export function ensureContrastRatio(bg: IColor, fg: IColor, ratio: number): IColor | undefined { + const result = rgba.ensureContrastRatio(bg.rgba, fg.rgba, ratio); + if (!result) { + return undefined; + } + return rgba.toColor( + (result >> 24 & 0xFF), + (result >> 16 & 0xFF), + (result >> 8 & 0xFF) + ); + } + + export function opaque(color: IColor): IColor { + const rgbaColor = (color.rgba | 0xFF) >>> 0; + const [r, g, b] = rgba.toChannels(rgbaColor); + return { + css: channels.toCss(r, g, b), + rgba: rgbaColor + }; + } +} + +/** + * Helper functions where the source type is "css" (string: '#rgb', '#rgba', '#rrggbb', '#rrggbbaa'). + */ +export namespace css { + export function toColor(css: string): IColor { + return { + css, + rgba: (parseInt(css.slice(1), 16) << 8 | 0xFF) >>> 0 + }; + } +} + +/** + * Helper functions where the source type is "rgb" (number: 0xrrggbb). + */ +export namespace rgb { + /** + * Gets the relative luminance of an RGB color, this is useful in determining the contrast ratio + * between two colors. + * @param rgb The color to use. + * @see https://www.w3.org/TR/WCAG20/#relativeluminancedef + */ + export function relativeLuminance(rgb: number): number { + return relativeLuminance2( + (rgb >> 16) & 0xFF, + (rgb >> 8 ) & 0xFF, + (rgb ) & 0xFF); + } + + /** + * Gets the relative luminance of an RGB color, this is useful in determining the contrast ratio + * between two colors. + * @param r The red channel (0x00 to 0xFF). + * @param g The green channel (0x00 to 0xFF). + * @param b The blue channel (0x00 to 0xFF). + * @see https://www.w3.org/TR/WCAG20/#relativeluminancedef + */ + export function relativeLuminance2(r: number, g: number, b: number): number { + const rs = r / 255; + const gs = g / 255; + const bs = b / 255; + const rr = rs <= 0.03928 ? rs / 12.92 : Math.pow((rs + 0.055) / 1.055, 2.4); + const rg = gs <= 0.03928 ? gs / 12.92 : Math.pow((gs + 0.055) / 1.055, 2.4); + const rb = bs <= 0.03928 ? bs / 12.92 : Math.pow((bs + 0.055) / 1.055, 2.4); + return rr * 0.2126 + rg * 0.7152 + rb * 0.0722; + } +} + +/** + * Helper functions where the source type is "rgba" (number: 0xrrggbbaa). + */ +export namespace rgba { + export function ensureContrastRatio(bgRgba: number, fgRgba: number, ratio: number): number | undefined { + const bgL = rgb.relativeLuminance(bgRgba >> 8); + const fgL = rgb.relativeLuminance(fgRgba >> 8); + const cr = contrastRatio(bgL, fgL); + if (cr < ratio) { + if (fgL < bgL) { + return reduceLuminance(bgRgba, fgRgba, ratio); + } + return increaseLuminance(bgRgba, fgRgba, ratio); + } + return undefined; + } + + export function reduceLuminance(bgRgba: number, fgRgba: number, ratio: number): number { + // This is a naive but fast approach to reducing luminance as converting to + // HSL and back is expensive + const bgR = (bgRgba >> 24) & 0xFF; + const bgG = (bgRgba >> 16) & 0xFF; + const bgB = (bgRgba >> 8) & 0xFF; + let fgR = (fgRgba >> 24) & 0xFF; + let fgG = (fgRgba >> 16) & 0xFF; + let fgB = (fgRgba >> 8) & 0xFF; + let cr = contrastRatio(rgb.relativeLuminance2(fgR, fgB, fgG), rgb.relativeLuminance2(bgR, bgG, bgB)); + while (cr < ratio && (fgR > 0 || fgG > 0 || fgB > 0)) { + // Reduce by 10% until the ratio is hit + fgR -= Math.max(0, Math.ceil(fgR * 0.1)); + fgG -= Math.max(0, Math.ceil(fgG * 0.1)); + fgB -= Math.max(0, Math.ceil(fgB * 0.1)); + cr = contrastRatio(rgb.relativeLuminance2(fgR, fgB, fgG), rgb.relativeLuminance2(bgR, bgG, bgB)); + } + return (fgR << 24 | fgG << 16 | fgB << 8 | 0xFF) >>> 0; + } + + export function increaseLuminance(bgRgba: number, fgRgba: number, ratio: number): number { + // This is a naive but fast approach to increasing luminance as converting to + // HSL and back is expensive + const bgR = (bgRgba >> 24) & 0xFF; + const bgG = (bgRgba >> 16) & 0xFF; + const bgB = (bgRgba >> 8) & 0xFF; + let fgR = (fgRgba >> 24) & 0xFF; + let fgG = (fgRgba >> 16) & 0xFF; + let fgB = (fgRgba >> 8) & 0xFF; + let cr = contrastRatio(rgb.relativeLuminance2(fgR, fgB, fgG), rgb.relativeLuminance2(bgR, bgG, bgB)); + while (cr < ratio && (fgR < 0xFF || fgG < 0xFF || fgB < 0xFF)) { + // Increase by 10% until the ratio is hit + fgR = Math.min(0xFF, fgR + Math.ceil((255 - fgR) * 0.1)); + fgG = Math.min(0xFF, fgG + Math.ceil((255 - fgG) * 0.1)); + fgB = Math.min(0xFF, fgB + Math.ceil((255 - fgB) * 0.1)); + cr = contrastRatio(rgb.relativeLuminance2(fgR, fgB, fgG), rgb.relativeLuminance2(bgR, bgG, bgB)); + } + return (fgR << 24 | fgG << 16 | fgB << 8 | 0xFF) >>> 0; + } + + export function toChannels(value: number): [number, number, number, number] { + return [(value >> 24) & 0xFF, (value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF]; + } + + export function toColor(r: number, g: number, b: number): IColor { + return { + css: channels.toCss(r, g, b), + rgba: channels.toRgba(r, g, b) + }; + } } export function toPaddedHex(c: number): string { @@ -39,62 +192,6 @@ export function toPaddedHex(c: number): string { return s.length < 2 ? '0' + s : s; } -export function toCss(r: number, g: number, b: number, a?: number): string { - if (a !== undefined) { - return `#${toPaddedHex(r)}${toPaddedHex(g)}${toPaddedHex(b)}${toPaddedHex(a)}`; - } - return `#${toPaddedHex(r)}${toPaddedHex(g)}${toPaddedHex(b)}`; -} - -export function toRgba(r: number, g: number, b: number, a: number = 0xFF): number { - // >>> 0 forces an unsigned int - return (r << 24 | g << 16 | b << 8 | a) >>> 0; -} - -export function fromRgba(value: number): [number, number, number, number] { - return [(value >> 24) & 0xFF, (value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF]; -} - -export function opaque(color: IColor): IColor { - const rgba = (color.rgba | 0xFF) >>> 0; - const [r, g, b] = fromRgba(rgba); - return { - css: toCss(r, g, b), - rgba - }; -} - -/** - * Gets the relative luminance of an RGB color, this is useful in determining the contrast ratio - * between two colors. - * @param rgb The color to use. - * @see https://www.w3.org/TR/WCAG20/#relativeluminancedef - */ -export function rgbRelativeLuminance(rgb: number): number { - return rgbRelativeLuminance2( - (rgb >> 16) & 0xFF, - (rgb >> 8 ) & 0xFF, - (rgb ) & 0xFF); -} - -/** - * Gets the relative luminance of an RGB color, this is useful in determining the contrast ratio - * between two colors. - * @param r The red channel (0x00 to 0xFF). - * @param g The green channel (0x00 to 0xFF). - * @param b The blue channel (0x00 to 0xFF). - * @see https://www.w3.org/TR/WCAG20/#relativeluminancedef - */ -export function rgbRelativeLuminance2(r: number, g: number, b: number): number { - const rs = r / 255; - const gs = g / 255; - const bs = b / 255; - const rr = rs <= 0.03928 ? rs / 12.92 : Math.pow((rs + 0.055) / 1.055, 2.4); - const rg = gs <= 0.03928 ? gs / 12.92 : Math.pow((gs + 0.055) / 1.055, 2.4); - const rb = bs <= 0.03928 ? bs / 12.92 : Math.pow((bs + 0.055) / 1.055, 2.4); - return rr * 0.2126 + rg * 0.7152 + rb * 0.0722; -} - /** * Gets the contrast ratio between two relative luminance values. * @param l1 The first relative luminance. @@ -107,75 +204,3 @@ export function contrastRatio(l1: number, l2: number): number { } return (l1 + 0.05) / (l2 + 0.05); } - -export function rgbaToColor(r: number, g: number, b: number): IColor { - return { - css: toCss(r, g, b), - rgba: toRgba(r, g, b) - }; -} - -export function ensureContrastRatioRgba(bgRgba: number, fgRgba: number, ratio: number): number | undefined { - const bgL = rgbRelativeLuminance(bgRgba >> 8); - const fgL = rgbRelativeLuminance(fgRgba >> 8); - const cr = contrastRatio(bgL, fgL); - if (cr < ratio) { - if (fgL < bgL) { - return reduceLuminance(bgRgba, fgRgba, ratio); - } - return increaseLuminance(bgRgba, fgRgba, ratio); - } - return undefined; -} - -export function ensureContrastRatio(bg: IColor, fg: IColor, ratio: number): IColor | undefined { - const result = ensureContrastRatioRgba(bg.rgba, fg.rgba, ratio); - if (!result) { - return undefined; - } - return rgbaToColor( - (result >> 24 & 0xFF), - (result >> 16 & 0xFF), - (result >> 8 & 0xFF) - ); -} - -export function reduceLuminance(bgRgba: number, fgRgba: number, ratio: number): number { - // This is a naive but fast approach to reducing luminance as converting to - // HSL and back is expensive - const bgR = (bgRgba >> 24) & 0xFF; - const bgG = (bgRgba >> 16) & 0xFF; - const bgB = (bgRgba >> 8) & 0xFF; - let fgR = (fgRgba >> 24) & 0xFF; - let fgG = (fgRgba >> 16) & 0xFF; - let fgB = (fgRgba >> 8) & 0xFF; - let cr = contrastRatio(rgbRelativeLuminance2(fgR, fgB, fgG), rgbRelativeLuminance2(bgR, bgG, bgB)); - while (cr < ratio && (fgR > 0 || fgG > 0 || fgB > 0)) { - // Reduce by 10% until the ratio is hit - fgR -= Math.max(0, Math.ceil(fgR * 0.1)); - fgG -= Math.max(0, Math.ceil(fgG * 0.1)); - fgB -= Math.max(0, Math.ceil(fgB * 0.1)); - cr = contrastRatio(rgbRelativeLuminance2(fgR, fgB, fgG), rgbRelativeLuminance2(bgR, bgG, bgB)); - } - return (fgR << 24 | fgG << 16 | fgB << 8 | 0xFF) >>> 0; -} - -export function increaseLuminance(bgRgba: number, fgRgba: number, ratio: number): number { - // This is a naive but fast approach to increasing luminance as converting to - // HSL and back is expensive - const bgR = (bgRgba >> 24) & 0xFF; - const bgG = (bgRgba >> 16) & 0xFF; - const bgB = (bgRgba >> 8) & 0xFF; - let fgR = (fgRgba >> 24) & 0xFF; - let fgG = (fgRgba >> 16) & 0xFF; - let fgB = (fgRgba >> 8) & 0xFF; - let cr = contrastRatio(rgbRelativeLuminance2(fgR, fgB, fgG), rgbRelativeLuminance2(bgR, bgG, bgB)); - while (cr < ratio && (fgR < 0xFF || fgG < 0xFF || fgB < 0xFF)) { - // Increase by 10% until the ratio is hit - fgR = Math.min(0xFF, fgR + Math.ceil((255 - fgR) * 0.1)); - fgG = Math.min(0xFF, fgG + Math.ceil((255 - fgG) * 0.1)); - fgB = Math.min(0xFF, fgB + Math.ceil((255 - fgB) * 0.1)); - cr = contrastRatio(rgbRelativeLuminance2(fgR, fgB, fgG), rgbRelativeLuminance2(bgR, bgG, bgB)); - } - return (fgR << 24 | fgG << 16 | fgB << 8 | 0xFF) >>> 0; -} diff --git a/src/browser/ColorManager.ts b/src/browser/ColorManager.ts index b4bcdde0..ee0dd4c7 100644 --- a/src/browser/ColorManager.ts +++ b/src/browser/ColorManager.ts @@ -5,13 +5,13 @@ import { IColorManager, IColor, IColorSet, IColorContrastCache } from 'browser/Types'; import { ITheme } from 'common/services/Services'; -import { fromCss, toCss, blend, toRgba, toPaddedHex } from 'browser/Color'; +import { channels, color, css } from 'browser/Color'; import { ColorContrastCache } from 'browser/ColorContrastCache'; -const DEFAULT_FOREGROUND = fromCss('#ffffff'); -const DEFAULT_BACKGROUND = fromCss('#000000'); -const DEFAULT_CURSOR = fromCss('#ffffff'); -const DEFAULT_CURSOR_ACCENT = fromCss('#000000'); +const DEFAULT_FOREGROUND = css.toColor('#ffffff'); +const DEFAULT_BACKGROUND = css.toColor('#000000'); +const DEFAULT_CURSOR = css.toColor('#ffffff'); +const DEFAULT_CURSOR_ACCENT = css.toColor('#000000'); const DEFAULT_SELECTION = { css: 'rgba(255, 255, 255, 0.3)', rgba: 0xFFFFFF4D @@ -22,23 +22,23 @@ const DEFAULT_SELECTION = { export const DEFAULT_ANSI_COLORS = (() => { const colors = [ // dark: - fromCss('#2e3436'), - fromCss('#cc0000'), - fromCss('#4e9a06'), - fromCss('#c4a000'), - fromCss('#3465a4'), - fromCss('#75507b'), - fromCss('#06989a'), - fromCss('#d3d7cf'), + css.toColor('#2e3436'), + css.toColor('#cc0000'), + css.toColor('#4e9a06'), + css.toColor('#c4a000'), + css.toColor('#3465a4'), + css.toColor('#75507b'), + css.toColor('#06989a'), + css.toColor('#d3d7cf'), // bright: - fromCss('#555753'), - fromCss('#ef2929'), - fromCss('#8ae234'), - fromCss('#fce94f'), - fromCss('#729fcf'), - fromCss('#ad7fa8'), - fromCss('#34e2e2'), - fromCss('#eeeeec') + css.toColor('#555753'), + css.toColor('#ef2929'), + css.toColor('#8ae234'), + css.toColor('#fce94f'), + css.toColor('#729fcf'), + css.toColor('#ad7fa8'), + css.toColor('#34e2e2'), + css.toColor('#eeeeec') ]; // Fill in the remaining 240 ANSI colors. @@ -49,8 +49,8 @@ export const DEFAULT_ANSI_COLORS = (() => { const g = v[(i / 6) % 6 | 0]; const b = v[i % 6]; colors.push({ - css: toCss(r, g, b), - rgba: toRgba(r, g, b) + css: channels.toCss(r, g, b), + rgba: channels.toRgba(r, g, b) }); } @@ -58,8 +58,8 @@ export const DEFAULT_ANSI_COLORS = (() => { for (let i = 0; i < 24; i++) { const c = 8 + i * 10; colors.push({ - css: toCss(c, c, c), - rgba: toRgba(c, c, c) + css: channels.toCss(c, c, c), + rgba: channels.toRgba(c, c, c) }); } @@ -93,7 +93,7 @@ export class ColorManager implements IColorManager { cursor: DEFAULT_CURSOR, cursorAccent: DEFAULT_CURSOR_ACCENT, selection: DEFAULT_SELECTION, - selectionOpaque: blend(DEFAULT_BACKGROUND, DEFAULT_SELECTION), + selectionOpaque: color.blend(DEFAULT_BACKGROUND, DEFAULT_SELECTION), ansi: DEFAULT_ANSI_COLORS.slice(), contrastCache: this._contrastCache }; @@ -116,7 +116,7 @@ export class ColorManager implements IColorManager { this.colors.cursor = this._parseColor(theme.cursor, DEFAULT_CURSOR, true); this.colors.cursorAccent = this._parseColor(theme.cursorAccent, DEFAULT_CURSOR_ACCENT, true); this.colors.selection = this._parseColor(theme.selection, DEFAULT_SELECTION, true); - this.colors.selectionOpaque = blend(this.colors.background, this.colors.selection); + this.colors.selectionOpaque = color.blend(this.colors.background, this.colors.selection); this.colors.ansi[0] = this._parseColor(theme.black, DEFAULT_ANSI_COLORS[0]); this.colors.ansi[1] = this._parseColor(theme.red, DEFAULT_ANSI_COLORS[1]); this.colors.ansi[2] = this._parseColor(theme.green, DEFAULT_ANSI_COLORS[2]); @@ -195,7 +195,7 @@ export class ColorManager implements IColorManager { g = ((num >> 8) & 0xF) * 16; b = ((num >> 4) & 0xF) * 16; a = (num & 0xF) * 16; - rgba = toRgba(r, g, b, a); + rgba = channels.toRgba(r, g, b, a); } else { rgba = parseInt(css.substr(1), 16); r = (rgba >> 24) & 0xFF; @@ -206,13 +206,13 @@ export class ColorManager implements IColorManager { return { rgba, - css: toCss(r, g, b, a) + css: channels.toCss(r, g, b, a) }; } return { css, - rgba: toRgba(data[0], data[1], data[2], data[3]) + rgba: channels.toRgba(data[0], data[1], data[2], data[3]) }; } } diff --git a/src/browser/renderer/BaseRenderLayer.ts b/src/browser/renderer/BaseRenderLayer.ts index 4ea8bd52..f109f42e 100644 --- a/src/browser/renderer/BaseRenderLayer.ts +++ b/src/browser/renderer/BaseRenderLayer.ts @@ -15,7 +15,7 @@ import { IColorSet, IColor } from 'browser/Types'; import { CellData } from 'common/buffer/CellData'; import { IBufferService, IOptionsService } from 'common/services/Services'; import { throwIfFalsy } from 'browser/renderer/RendererUtils'; -import { toCss, ensureContrastRatioRgba, opaque } from 'browser/Color'; +import { channels, color, rgba } from 'browser/Color'; export abstract class BaseRenderLayer implements IRenderLayer { private _canvas: HTMLCanvasElement; @@ -325,7 +325,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { if (fgOverride) { this._ctx.fillStyle = fgOverride.css; } else if (cell.isBgDefault()) { - this._ctx.fillStyle = opaque(this._colors.background).css; + this._ctx.fillStyle = color.opaque(this._colors.background).css; } else if (cell.isBgRGB()) { this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`; } else { @@ -418,7 +418,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { const bgRgba = this._resolveBackgroundRgba(bgColorMode, bgColor, isInverse); const fgRgba = this._resolveForegroundRgba(fgColorMode, fgColor, isInverse, isBold); - const result = ensureContrastRatioRgba(bgRgba, fgRgba, this._optionsService.options.minimumContrastRatio); + const result = rgba.ensureContrastRatio(bgRgba, fgRgba, this._optionsService.options.minimumContrastRatio); if (!result) { this._colors.contrastCache.setColor(cell.bg, cell.fg, null); @@ -426,7 +426,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { } const color: IColor = { - css: toCss( + css: channels.toCss( (result >> 24) & 0xFF, (result >> 16) & 0xFF, (result >> 8) & 0xFF diff --git a/src/browser/renderer/atlas/DynamicCharAtlas.ts b/src/browser/renderer/atlas/DynamicCharAtlas.ts index 84a50160..f2443a52 100644 --- a/src/browser/renderer/atlas/DynamicCharAtlas.ts +++ b/src/browser/renderer/atlas/DynamicCharAtlas.ts @@ -11,7 +11,7 @@ import { LRUMap } from 'browser/renderer/atlas/LRUMap'; import { isFirefox, isSafari } from 'common/Platform'; import { IColor } from 'browser/Types'; import { throwIfFalsy } from 'browser/renderer/RendererUtils'; -import { opaque } from 'browser/Color'; +import { color } from 'browser/Color'; // In practice we're probably never going to exhaust a texture this large. For debugging purposes, // however, it can be useful to set this to a really tiny value, to verify that LRU eviction works. @@ -223,7 +223,7 @@ export class DynamicCharAtlas extends BaseCharAtlas { private _getForegroundColor(glyph: IGlyphIdentifier): IColor { if (glyph.fg === INVERTED_DEFAULT_COLOR) { - return opaque(this._config.colors.background); + return color.opaque(this._config.colors.background); } else if (glyph.fg < 256) { // 256 color support return this._getColorFromAnsiIndex(glyph.fg); diff --git a/src/browser/renderer/dom/DomRenderer.ts b/src/browser/renderer/dom/DomRenderer.ts index 942a2792..5fbcfdc9 100644 --- a/src/browser/renderer/dom/DomRenderer.ts +++ b/src/browser/renderer/dom/DomRenderer.ts @@ -11,7 +11,7 @@ import { IColorSet, ILinkifierEvent, ILinkifier } from 'browser/Types'; import { ICharSizeService } from 'browser/services/Services'; import { IOptionsService, IBufferService } from 'common/services/Services'; import { EventEmitter, IEvent } from 'common/EventEmitter'; -import { opaque } from 'browser/Color'; +import { color } from 'browser/Color'; const TERMINAL_CLASS_PREFIX = 'xterm-dom-renderer-owner-'; const ROW_CONTAINER_CLASS = 'xterm-rows'; @@ -231,7 +231,7 @@ export class DomRenderer extends Disposable implements IRenderer { `${this._terminalSelector} .${BG_CLASS_PREFIX}${i} { background-color: ${c.css}; }`; }); styles += - `${this._terminalSelector} .${FG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { color: ${opaque(this._colors.background).css}; }` + + `${this._terminalSelector} .${FG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { color: ${color.opaque(this._colors.background).css}; }` + `${this._terminalSelector} .${BG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { background-color: ${this._colors.foreground.css}; }`; this._themeStyleElement.innerHTML = styles; diff --git a/src/browser/renderer/dom/DomRendererRowFactory.test.ts b/src/browser/renderer/dom/DomRendererRowFactory.test.ts index c71945cf..b6604b14 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.test.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.test.ts @@ -11,7 +11,7 @@ import { BufferLine, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; import { IBufferLine } from 'common/Types'; import { CellData } from 'common/buffer/CellData'; import { MockOptionsService } from 'common/TestUtils.test'; -import { fromCss } from 'browser/Color'; +import { css } from 'browser/Color'; describe('DomRendererRowFactory', () => { let dom: jsdom.JSDOM; @@ -21,27 +21,27 @@ describe('DomRendererRowFactory', () => { beforeEach(() => { dom = new jsdom.JSDOM(''); rowFactory = new DomRendererRowFactory(dom.window.document, new MockOptionsService({ drawBoldTextInBrightColors: true }), { - background: fromCss('#010101'), - foreground: fromCss('#020202'), + background: css.toColor('#010101'), + foreground: css.toColor('#020202'), ansi: [ // dark: - fromCss('#2e3436'), - fromCss('#cc0000'), - fromCss('#4e9a06'), - fromCss('#c4a000'), - fromCss('#3465a4'), - fromCss('#75507b'), - fromCss('#06989a'), - fromCss('#d3d7cf'), + css.toColor('#2e3436'), + css.toColor('#cc0000'), + css.toColor('#4e9a06'), + css.toColor('#c4a000'), + css.toColor('#3465a4'), + css.toColor('#75507b'), + css.toColor('#06989a'), + css.toColor('#d3d7cf'), // bright: - fromCss('#555753'), - fromCss('#ef2929'), - fromCss('#8ae234'), - fromCss('#fce94f'), - fromCss('#729fcf'), - fromCss('#ad7fa8'), - fromCss('#34e2e2'), - fromCss('#eeeeec') + css.toColor('#555753'), + css.toColor('#ef2929'), + css.toColor('#8ae234'), + css.toColor('#fce94f'), + css.toColor('#729fcf'), + css.toColor('#ad7fa8'), + css.toColor('#34e2e2'), + css.toColor('#eeeeec') ] } as any); lineData = createEmptyLineData(2); diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts index bd922f57..9ffe6701 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.ts @@ -8,7 +8,7 @@ import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants'; import { NULL_CELL_CODE, WHITESPACE_CELL_CHAR, Attributes } from 'common/buffer/Constants'; import { CellData } from 'common/buffer/CellData'; import { IOptionsService } from 'common/services/Services'; -import { ensureContrastRatio, rgbaToColor } from 'browser/Color'; +import { color, rgba } from 'browser/Color'; import { IColorSet, IColor } from 'browser/Types'; export const BOLD_CLASS = 'xterm-bold'; @@ -129,7 +129,7 @@ export class DomRendererRowFactory { } break; case Attributes.CM_RGB: - const color = rgbaToColor( + const color = rgba.toColor( (fg >> 16) & 0xFF, (fg >> 8) & 0xFF, (fg ) & 0xFF @@ -178,7 +178,7 @@ export class DomRendererRowFactory { // Calculate and store in cache if (adjustedColor === undefined) { - adjustedColor = ensureContrastRatio(bg, fg, this._optionsService.options.minimumContrastRatio); + adjustedColor = color.ensureContrastRatio(bg, fg, this._optionsService.options.minimumContrastRatio); this._colors.contrastCache.setColor(this._workCell.bg, this._workCell.fg, adjustedColor ?? null); } From 509327b4c787457a68c4815772a85a19277c5a21 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 26 Dec 2019 12:19:03 +1100 Subject: [PATCH 40/48] Use register over add for APIs returning disposables Fixes #2610 --- src/public/Terminal.ts | 25 ++++++++++++++++++++----- typings/xterm.d.ts | 25 +++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index 397898c7..96e13f63 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -74,10 +74,13 @@ export class Terminal implements ITerminalApi { public deregisterCharacterJoiner(joinerId: number): void { this._core.deregisterCharacterJoiner(joinerId); } - public addMarker(cursorYOffset: number): IMarker { + public registerMarker(cursorYOffset: number): IMarker { this._verifyIntegers(cursorYOffset); return this._core.addMarker(cursorYOffset); } + public addMarker(cursorYOffset: number): IMarker { + return this.registerMarker(cursorYOffset); + } public hasSelection(): boolean { return this._core.hasSelection(); } @@ -227,16 +230,28 @@ class BufferCellApiView implements IBufferCellApi { class ParserApi implements IParser { constructor(private _core: ITerminal) {} - public addCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean): IDisposable { + public registerCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean): IDisposable { return this._core.addCsiHandler(id, (params: IParams) => callback(params.toArray())); } - public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean): IDisposable { + public addCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean): IDisposable { + return this.registerCsiHandler(id, callback); + } + public registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean): IDisposable { return this._core.addDcsHandler(id, (data: string, params: IParams) => callback(data, params.toArray())); } - public addEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable { + public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean): IDisposable { + return this.registerDcsHandler(id, callback); + } + public registerEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable { return this._core.addEscHandler(id, handler); } - public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { + public addEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable { + return this.registerEscHandler(id, handler); + } + public registerOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { return this._core.addOscHandler(ident, callback); } + public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { + return this.registerOscHandler(ident, callback); + } } diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index ac33d087..e848c547 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -612,6 +612,11 @@ declare module 'xterm' { * alt buffer is active, undefined is returned. * @param cursorYOffset The y position offset of the marker from the cursor. */ + registerMarker(cursorYOffset: number): IMarker; + + /** + * @deprecated use `registerMarker` instead. + */ addMarker(cursorYOffset: number): IMarker; /** @@ -1078,6 +1083,11 @@ declare module 'xterm' { * The most recently added handler is tried first. * @return An IDisposable you can call to remove this handler. */ + registerCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean): IDisposable; + + /** + * @deprecated use `registerMarker` instead. + */ addCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean): IDisposable; /** @@ -1097,6 +1107,11 @@ declare module 'xterm' { * The most recently added handler is tried first. * @return An IDisposable you can call to remove this handler. */ + registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean): IDisposable; + + /** + * @deprecated use `registerMarker` instead. + */ addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean): IDisposable; /** @@ -1110,6 +1125,11 @@ declare module 'xterm' { * The most recently added handler is tried first. * @return An IDisposable you can call to remove this handler. */ + registerEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable; + + /** + * @deprecated use `registerMarker` instead. + */ addEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable; /** @@ -1128,6 +1148,11 @@ declare module 'xterm' { * The most recently added handler is tried first. * @return An IDisposable you can call to remove this handler. */ + registerOscHandler(ident: number, callback: (data: string) => boolean): IDisposable; + + /** + * @deprecated use `registerMarker` instead. + */ addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable; } } From 28d7eddcb4fed5a2d0ef30eb1ee11c5b7d24ee08 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 26 Dec 2019 14:04:47 +1100 Subject: [PATCH 41/48] Set glyph fg color based on original bg, not selection This involves resolving the rgb channels of the original background color and encoding them using the RGB color mode. Fixes #2599 --- addons/xterm-addon-webgl/src/GlyphRenderer.ts | 42 ++++++++++++++++--- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/addons/xterm-addon-webgl/src/GlyphRenderer.ts b/addons/xterm-addon-webgl/src/GlyphRenderer.ts index 7b35949d..332abeb8 100644 --- a/addons/xterm-addon-webgl/src/GlyphRenderer.ts +++ b/addons/xterm-addon-webgl/src/GlyphRenderer.ts @@ -6,13 +6,14 @@ import { createProgram, PROJECTION_MATRIX, throwIfFalsy } from './WebglUtils'; import { WebglCharAtlas } from './atlas/WebglCharAtlas'; import { IWebGL2RenderingContext, IWebGLVertexArrayObject, IRenderModel, IRasterizedGlyph } from './Types'; -import { COMBINED_CHAR_BIT_MASK, RENDER_MODEL_INDICIES_PER_CELL, RENDER_MODEL_FG_OFFSET } from './RenderModel'; +import { COMBINED_CHAR_BIT_MASK, RENDER_MODEL_INDICIES_PER_CELL, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_BG_OFFSET } from './RenderModel'; import { fill } from 'common/TypedArrayUtils'; import { slice } from './TypedArray'; -import { NULL_CELL_CODE, WHITESPACE_CELL_CODE, Attributes } from 'common/buffer/Constants'; +import { NULL_CELL_CODE, WHITESPACE_CELL_CODE, Attributes, FgFlags } from 'common/buffer/Constants'; import { Terminal, IBufferLine } from 'xterm'; -import { IColorSet } from 'browser/Types'; +import { IColorSet, IColor } from 'browser/Types'; import { IRenderDimensions } from 'browser/renderer/Types'; +import { AttributeData } from 'common/buffer/AttributeData'; interface IVertices { attributes: Float32Array; @@ -254,18 +255,49 @@ export class GlyphRenderer { for (let x = startCol; x < endCol; x++) { const offset = (y * this._terminal.cols + x) * RENDER_MODEL_INDICIES_PER_CELL; const code = model.cells[offset]; + let fg = model.cells[offset + RENDER_MODEL_FG_OFFSET]; + if (fg & FgFlags.INVERSE) { + const workCell = new AttributeData(); + workCell.fg = fg; + workCell.bg = model.cells[offset + RENDER_MODEL_BG_OFFSET]; + // Get attributes from fg (excluding inverse) and resolve inverse by pullibng rgb colors + // from bg. This is needed since the inverse fg color should be based on the original bg + // color, not on the selection color + fg = (fg & ~(Attributes.CM_MASK | Attributes.RGB_MASK | FgFlags.INVERSE)); + switch (workCell.getBgColorMode()) { + case Attributes.CM_P16: + case Attributes.CM_P256: + const c = this._getColorFromAnsiIndex(workCell.getBgColor()).rgba; + fg |= (c >> 8) & Attributes.RED_MASK | (c >> 8) & Attributes.GREEN_MASK | (c >> 8) & Attributes.BLUE_MASK; + case Attributes.CM_RGB: + const arr = AttributeData.toColorRGB(workCell.getBgColor()); + fg |= arr[0] << Attributes.RED_SHIFT | arr[1] << Attributes.GREEN_SHIFT | arr[2] << Attributes.BLUE_SHIFT; + case Attributes.CM_DEFAULT: + default: + const c2 = this._colors.background.rgba; + fg |= (c2 >> 8) & Attributes.RED_MASK | (c2 >> 8) & Attributes.GREEN_MASK | (c2 >> 8) & Attributes.BLUE_MASK; + } + fg |= Attributes.CM_RGB; + } if (code & COMBINED_CHAR_BIT_MASK) { if (!line) { line = terminal.buffer.getLine(row); } const chars = line!.getCell(x)!.char; - this._updateCell(this._vertices.selectionAttributes, x, y, model.cells[offset], bg, model.cells[offset + RENDER_MODEL_FG_OFFSET], chars); + this._updateCell(this._vertices.selectionAttributes, x, y, model.cells[offset], bg, fg, chars); } else { - this._updateCell(this._vertices.selectionAttributes, x, y, model.cells[offset], bg, model.cells[offset + RENDER_MODEL_FG_OFFSET]); + this._updateCell(this._vertices.selectionAttributes, x, y, model.cells[offset], bg, fg); } } } + private _getColorFromAnsiIndex(idx: number): IColor { + if (idx >= this._colors.ansi.length) { + throw new Error('No color found for idx ' + idx); + } + return this._colors.ansi[idx]; + } + public onResize(): void { const terminal = this._terminal; const gl = this._gl; From 3e3c51ae5342b207bab8f548eeaf931f66a1e4d9 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 26 Dec 2019 14:26:53 +1100 Subject: [PATCH 42/48] Add a test for selection Part of #2600 --- .../src/WebglRenderer.api.ts | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.api.ts b/addons/xterm-addon-webgl/src/WebglRenderer.api.ts index 1cd198d9..40000fd1 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.api.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.api.ts @@ -825,6 +825,30 @@ describe('WebGL Renderer Integration Tests', function(): void { }); }); + describe('selection', async () => { + before(async () => setupBrowser()); + after(async () => browser.close()); + beforeEach(async () => page.evaluate(`window.term.reset()`)); + + it.only('should resolve the inverse foreground color based on the original background color, not the selection', async () => { + const theme: ITheme = { + foreground: '#FF0000', + background: '#00FF00', + selection: '#0000FF' + }; + await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`); + await writeSync(` █\\x1b[7m█\\x1b[0m`); + await pollFor(page, () => getCellColor(1, 1), [0, 255, 0, 255]); + await pollFor(page, () => getCellColor(2, 1), [255, 0, 0, 255]); + await pollFor(page, () => getCellColor(3, 1), [0, 255, 0, 255]); + await page.evaluate(`window.term.selectAll()`); + // Selection only cell needs to be first to ensure renderer has kicked in + await pollFor(page, () => getCellColor(1, 1), [0, 0, 255, 255]); + await pollFor(page, () => getCellColor(2, 1), [255, 0, 0, 255]); + await pollFor(page, () => getCellColor(3, 1), [0, 255, 0, 255]); + }); + }); + describe('allowTransparency', async () => { before(async () => setupBrowser({ rendererType: 'dom', allowTransparency: true})); after(async () => browser.close()); From 79860e7b1568e14abd84d4e3002bafc3411fe101 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 27 Dec 2019 02:59:27 +1100 Subject: [PATCH 43/48] Remove .only --- addons/xterm-addon-webgl/src/WebglRenderer.api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.api.ts b/addons/xterm-addon-webgl/src/WebglRenderer.api.ts index 40000fd1..8b9c84bf 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.api.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.api.ts @@ -830,7 +830,7 @@ describe('WebGL Renderer Integration Tests', function(): void { after(async () => browser.close()); beforeEach(async () => page.evaluate(`window.term.reset()`)); - it.only('should resolve the inverse foreground color based on the original background color, not the selection', async () => { + it('should resolve the inverse foreground color based on the original background color, not the selection', async () => { const theme: ITheme = { foreground: '#FF0000', background: '#00FF00', From 9f8ea28db405f781380631f869bcfe32c464b778 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 26 Dec 2019 20:04:11 +0100 Subject: [PATCH 44/48] sanity checks in print wide char handling --- src/InputHandler.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 0124f499..5c454184 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -400,7 +400,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._dirtyRowService.markDirty(buffer.y); // handle wide chars: reset start_cell-1 if we would overwrite the second cell of a wide char - if (buffer.x && bufferRow.getWidth(buffer.x - 1) === 2) { + if (buffer.x && end - start > 0 && bufferRow.getWidth(buffer.x - 1) === 2) { bufferRow.setCellFromCodePoint(buffer.x - 1, 0, 1, curAttr.fg, curAttr.bg); } @@ -505,7 +505,7 @@ export class InputHandler extends Disposable implements IInputHandler { // This needs to check whether: // - fullwidth + surrogates: reset // - combining: only base char gets carried on (bug in xterm?) - if (end) { + if (end - start > 0) { bufferRow.loadCell(buffer.x - 1, this._workCell); if (this._workCell.getWidth() === 2 || this._workCell.getCode() > 0xFFFF) { this._parser.precedingCodepoint = 0; @@ -516,8 +516,8 @@ export class InputHandler extends Disposable implements IInputHandler { } } - // handle wide chars: reset cell to the right if is second cell of a wide char - if (buffer.x < cols && bufferRow.getWidth(buffer.x) === 0 && !bufferRow.hasContent(buffer.x)) { + // handle wide chars: reset cell to the right if it is second cell of a wide char + if (buffer.x < cols && end - start > 0 && bufferRow.getWidth(buffer.x) === 0 && !bufferRow.hasContent(buffer.x)) { bufferRow.setCellFromCodePoint(buffer.x, 0, 1, curAttr.fg, curAttr.bg); } From c512485514ba0651ddc8594cb060dd0bb758179d Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 27 Dec 2019 08:33:42 -0800 Subject: [PATCH 45/48] Target es5 in attach addon Fixes #2646 --- addons/xterm-addon-attach/src/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/xterm-addon-attach/src/tsconfig.json b/addons/xterm-addon-attach/src/tsconfig.json index d875aa53..5539aa56 100644 --- a/addons/xterm-addon-attach/src/tsconfig.json +++ b/addons/xterm-addon-attach/src/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "module": "commonjs", - "target": "es2015", + "target": "es5", "lib": [ "dom", "es2015" From 5f25f40db936dd85489ee766737d14bcbc6566e4 Mon Sep 17 00:00:00 2001 From: Yucong Sun Date: Thu, 2 Jan 2020 11:57:45 -0800 Subject: [PATCH 46/48] Adding FluffOS to xtermjs user list --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 5cf2848f..21109efe 100644 --- a/README.md +++ b/README.md @@ -156,6 +156,7 @@ Xterm.js is used in several world-class applications to provide great terminal e - [**tty-share**](https://tty-share.com): Extremely simple terminal sharing over the Internet. - [**Ten Hands**](https://github.com/saisandeepvaddi/ten-hands): One place to run your command-line tasks. - [**WebAssembly.sh**](https://webassembly.sh): A WebAssembly WASI browser terminal +- [**FluffOS**](https://www.fluffos.info): Active maintained LPMUD driver with websocket support. [And much more...](https://github.com/xtermjs/xterm.js/network/dependents) From c50cd4cc3ef603a90d9303cd5816c6a3e97b38b4 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 7 Jan 2020 09:15:04 -0800 Subject: [PATCH 47/48] Flag lines as wrapped after CUP in windows mode Fixes #2666 --- src/Terminal.test.ts | 42 ++++++++++++++++++++++++++++++++++++++++++ src/Terminal.ts | 12 +++++++++++- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/src/Terminal.test.ts b/src/Terminal.test.ts index 90522bf4..9ea3a184 100644 --- a/src/Terminal.test.ts +++ b/src/Terminal.test.ts @@ -1359,6 +1359,48 @@ describe('Terminal', () => { }).to.not.throw(); }); }); + + describe('Windows Mode', () => { + it('should mark lines as wrapped when the line ends in a non-null character after a LF', () => { + const data = [ + 'aaaaaaaaaa\n\r', // cannot wrap as it's the first + 'aaaaaaaaa\n\r', // wrapped (windows mode only) + 'aaaaaaaaa' // not wrapped + ]; + + const normalTerminal = new TestTerminal({rows: 5, cols: 10, windowsMode: false}); + normalTerminal.writeSync(data.join('')); + assert.equal(normalTerminal.buffer.lines.get(0).isWrapped, false); + assert.equal(normalTerminal.buffer.lines.get(1).isWrapped, false); + assert.equal(normalTerminal.buffer.lines.get(2).isWrapped, false); + + const windowsModeTerminal = new TestTerminal({rows: 5, cols: 10, windowsMode: true}); + windowsModeTerminal.writeSync(data.join('')); + assert.equal(windowsModeTerminal.buffer.lines.get(0).isWrapped, false); + assert.equal(windowsModeTerminal.buffer.lines.get(1).isWrapped, true, 'This line should wrap in Windows mode as the previous line ends in a non-null character'); + assert.equal(windowsModeTerminal.buffer.lines.get(2).isWrapped, false); + }); + + it('should mark lines as wrapped when the line ends in a non-null character after a CUP', () => { + const data = [ + 'aaaaaaaaaa\x1b[2;1H', // cannot wrap as it's the first + 'aaaaaaaaa\x1b[3;1H', // wrapped (windows mode only) + 'aaaaaaaaa' // not wrapped + ]; + + const normalTerminal = new TestTerminal({rows: 5, cols: 10, windowsMode: false}); + normalTerminal.writeSync(data.join('')); + assert.equal(normalTerminal.buffer.lines.get(0).isWrapped, false); + assert.equal(normalTerminal.buffer.lines.get(1).isWrapped, false); + assert.equal(normalTerminal.buffer.lines.get(2).isWrapped, false); + + const windowsModeTerminal = new TestTerminal({rows: 5, cols: 10, windowsMode: true}); + windowsModeTerminal.writeSync(data.join('')); + assert.equal(windowsModeTerminal.buffer.lines.get(0).isWrapped, false); + assert.equal(windowsModeTerminal.buffer.lines.get(1).isWrapped, true, 'This line should wrap in Windows mode as the previous line ends in a non-null character'); + assert.equal(windowsModeTerminal.buffer.lines.get(2).isWrapped, false); + }); + }); }); class TestLinkifier extends Linkifier { diff --git a/src/Terminal.ts b/src/Terminal.ts index 74c2d00e..85cfbe67 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -264,7 +264,17 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp private _enableWindowsMode(): void { if (!this._windowsMode) { - this._windowsMode = this.onLineFeed(handleWindowsModeLineFeed.bind(null, this._bufferService)); + const disposables: IDisposable[] = []; + disposables.push(this.onLineFeed(handleWindowsModeLineFeed.bind(null, this._bufferService))); + disposables.push(this.addCsiHandler({ final: 'H' }, () => { + handleWindowsModeLineFeed(this._bufferService); + return false; + })); + this._windowsMode = { + dispose: () => { + disposables.forEach(d => d.dispose()); + } + }; } } From 685d14c4d52dc2fe750437499a63169c68dc3f38 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 7 Jan 2020 09:16:16 -0800 Subject: [PATCH 48/48] Improve windows mode method name --- src/Terminal.ts | 6 +++--- src/common/WindowsMode.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index 85cfbe67..7d0def9b 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -43,7 +43,7 @@ import { IKeyboardEvent, KeyboardResultType, IBufferLine, IAttributeData, CoreMo import { evaluateKeyboardEvent } from 'common/input/Keyboard'; import { EventEmitter, IEvent } from 'common/EventEmitter'; import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; -import { handleWindowsModeLineFeed } from 'common/WindowsMode'; +import { updateWindowsModeWrappedState } from 'common/WindowsMode'; import { ColorManager } from 'browser/ColorManager'; import { RenderService } from 'browser/services/RenderService'; import { IOptionsService, IBufferService, ICoreMouseService, ICoreService, ILogService, IDirtyRowService, IInstantiationService, ICharsetService } from 'common/services/Services'; @@ -265,9 +265,9 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp private _enableWindowsMode(): void { if (!this._windowsMode) { const disposables: IDisposable[] = []; - disposables.push(this.onLineFeed(handleWindowsModeLineFeed.bind(null, this._bufferService))); + disposables.push(this.onLineFeed(updateWindowsModeWrappedState.bind(null, this._bufferService))); disposables.push(this.addCsiHandler({ final: 'H' }, () => { - handleWindowsModeLineFeed(this._bufferService); + updateWindowsModeWrappedState(this._bufferService); return false; })); this._windowsMode = { diff --git a/src/common/WindowsMode.ts b/src/common/WindowsMode.ts index ff0e7591..7cff094b 100644 --- a/src/common/WindowsMode.ts +++ b/src/common/WindowsMode.ts @@ -6,7 +6,7 @@ import { CHAR_DATA_CODE_INDEX, NULL_CELL_CODE, WHITESPACE_CELL_CODE } from 'common/buffer/Constants'; import { IBufferService } from 'common/services/Services'; -export function handleWindowsModeLineFeed(bufferService: IBufferService): void { +export function updateWindowsModeWrappedState(bufferService: IBufferService): void { // Winpty does not support wraparound mode which means that lines will never // be marked as wrapped. This causes issues for things like copying a line // retaining the wrapped new line characters or if consumers are listening