From 5bc80fdf67d3852fa94dab3ec97e76ebfdf247cb Mon Sep 17 00:00:00 2001 From: ivanwonder Date: Mon, 11 Nov 2019 11:01:32 +0800 Subject: [PATCH 001/103] force alpha to 1 when using background color as inverted foreground color. --- src/browser/Color.test.ts | 24 ++++++++++++++++++- src/browser/Color.ts | 4 ++++ .../renderer/atlas/DynamicCharAtlas.ts | 9 ++++++- src/browser/renderer/dom/DomRenderer.ts | 4 +++- 4 files changed, 38 insertions(+), 3 deletions(-) diff --git a/src/browser/Color.test.ts b/src/browser/Color.test.ts index 44cd52f3..005e7376 100644 --- a/src/browser/Color.test.ts +++ b/src/browser/Color.test.ts @@ -4,7 +4,7 @@ */ import { assert } from 'chai'; -import { blend, fromCss, toPaddedHex, toCss, toRgba } from 'browser/Color'; +import { blend, fromCss, toPaddedHex, toCss, toRgba, fromRgba } from 'browser/Color'; describe('Color', () => { describe('blend', () => { @@ -135,4 +135,26 @@ describe('Color', () => { 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]); + }); + }); }); diff --git a/src/browser/Color.ts b/src/browser/Color.ts index a8ce2d16..a9db2215 100644 --- a/src/browser/Color.ts +++ b/src/browser/Color.ts @@ -47,3 +47,7 @@ export function toRgba(r: number, g: number, b: number, a: number = 0xFF): numbe // >>> 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]; +} diff --git a/src/browser/renderer/atlas/DynamicCharAtlas.ts b/src/browser/renderer/atlas/DynamicCharAtlas.ts index 40103bc7..4ddaf2bd 100644 --- a/src/browser/renderer/atlas/DynamicCharAtlas.ts +++ b/src/browser/renderer/atlas/DynamicCharAtlas.ts @@ -11,6 +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 { fromRgba, toCss } 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. @@ -253,7 +254,13 @@ export class DynamicCharAtlas extends BaseCharAtlas { `${fontStyle} ${fontWeight} ${this._config.fontSize * this._config.devicePixelRatio}px ${this._config.fontFamily}`; this._tmpCtx.textBaseline = 'middle'; - this._tmpCtx.fillStyle = this._getForegroundColor(glyph).css; + const fgColor = this._getForegroundColor(glyph); + this._tmpCtx.fillStyle = fgColor.css; + + if (glyph.fg === INVERTED_DEFAULT_COLOR) { + const rgba = fromRgba(fgColor.rgba); + this._tmpCtx.fillStyle = toCss(rgba[0], rgba[1], rgba[2]); + } // Apply alpha to dim the character if (glyph.dim) { diff --git a/src/browser/renderer/dom/DomRenderer.ts b/src/browser/renderer/dom/DomRenderer.ts index e8fc85f6..e11a413d 100644 --- a/src/browser/renderer/dom/DomRenderer.ts +++ b/src/browser/renderer/dom/DomRenderer.ts @@ -11,6 +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 { fromRgba, toCss } from 'browser/Color'; const TERMINAL_CLASS_PREFIX = 'xterm-dom-renderer-owner-'; const ROW_CONTAINER_CLASS = 'xterm-rows'; @@ -229,8 +230,9 @@ export class DomRenderer extends Disposable implements IRenderer { `${this._terminalSelector} .${FG_CLASS_PREFIX}${i} { color: ${c.css}; }` + `${this._terminalSelector} .${BG_CLASS_PREFIX}${i} { background-color: ${c.css}; }`; }); + const rgba = fromRgba(this._colors.background.rgba); styles += - `${this._terminalSelector} .${FG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { color: ${this._colors.background.css}; }` + + `${this._terminalSelector} .${FG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { color: ${toCss(rgba[0], rgba[1], rgba[2])}; }` + `${this._terminalSelector} .${BG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { background-color: ${this._colors.foreground.css}; }`; this._themeStyleElement.innerHTML = styles; From dfc1664d6e29d2ff7ed7a1bab6a8560e673b837c Mon Sep 17 00:00:00 2001 From: Andrew Sverdrup Date: Mon, 11 Nov 2019 13:23:59 -0800 Subject: [PATCH 002/103] Update Codevolve to Next Tech We changed our name several months back. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5cf2848f..d2944003 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ Xterm.js is used in several world-class applications to provide great terminal e - [**WebSSH2**](https://github.com/billchurch/WebSSH2): A web based SSH2 client using xterm.js, socket.io, and ssh2. - [**Spyder Terminal**](https://github.com/spyder-ide/spyder-terminal): A full fledged system terminal embedded on Spyder IDE. - [**Cloud Commander**](https://cloudcmd.io "Cloud Commander"): Orthodox web file manager with console and editor. -- [**Codevolve**](https://www.codevolve.com "Codevolve"): Online platform for interactive coding and web development courses. Live container-backed terminal uses xterm.js. +- [**Next Tech**](https://next.tech "Next Tech"): Online platform for interactive coding and web development courses. Live container-backed terminal uses xterm.js. - [**RStudio**](https://www.rstudio.com/products/RStudio "RStudio"): RStudio is an integrated development environment (IDE) for R. - [**Terminal for Atom**](https://github.com/jsmecham/atom-terminal-tab): A simple terminal for the Atom text editor. - [**Eclipse Orion**](https://orionhub.org): A modern, open source software development environment that runs in the cloud. Code, deploy and run in the cloud. From fd868044fd4c6eb819a3dbdf429bd61b2aa59b59 Mon Sep 17 00:00:00 2001 From: ivanwonder Date: Tue, 12 Nov 2019 10:06:51 +0800 Subject: [PATCH 003/103] add a helper to make color opaque --- src/browser/Color.test.ts | 24 ++++++++++++++++++- src/browser/Color.ts | 9 +++++++ .../renderer/atlas/DynamicCharAtlas.ts | 12 +++------- src/browser/renderer/dom/DomRenderer.ts | 5 ++-- 4 files changed, 37 insertions(+), 13 deletions(-) diff --git a/src/browser/Color.test.ts b/src/browser/Color.test.ts index 005e7376..7dea98e2 100644 --- a/src/browser/Color.test.ts +++ b/src/browser/Color.test.ts @@ -4,7 +4,7 @@ */ import { assert } from 'chai'; -import { blend, fromCss, toPaddedHex, toCss, toRgba, fromRgba } from 'browser/Color'; +import { blend, fromCss, toPaddedHex, toCss, toRgba, fromRgba, opaque } from 'browser/Color'; describe('Color', () => { describe('blend', () => { @@ -157,4 +157,26 @@ describe('Color', () => { 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 }); + }); + }); }); diff --git a/src/browser/Color.ts b/src/browser/Color.ts index a9db2215..bc4f7d13 100644 --- a/src/browser/Color.ts +++ b/src/browser/Color.ts @@ -51,3 +51,12 @@ export function toRgba(r: number, g: number, b: number, a: number = 0xFF): numbe 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 + }; +} diff --git a/src/browser/renderer/atlas/DynamicCharAtlas.ts b/src/browser/renderer/atlas/DynamicCharAtlas.ts index 4ddaf2bd..84a50160 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 { fromRgba, toCss } from 'browser/Color'; +import { opaque } 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 this._config.colors.background; + return opaque(this._config.colors.background); } else if (glyph.fg < 256) { // 256 color support return this._getColorFromAnsiIndex(glyph.fg); @@ -254,13 +254,7 @@ export class DynamicCharAtlas extends BaseCharAtlas { `${fontStyle} ${fontWeight} ${this._config.fontSize * this._config.devicePixelRatio}px ${this._config.fontFamily}`; this._tmpCtx.textBaseline = 'middle'; - const fgColor = this._getForegroundColor(glyph); - this._tmpCtx.fillStyle = fgColor.css; - - if (glyph.fg === INVERTED_DEFAULT_COLOR) { - const rgba = fromRgba(fgColor.rgba); - this._tmpCtx.fillStyle = toCss(rgba[0], rgba[1], rgba[2]); - } + this._tmpCtx.fillStyle = this._getForegroundColor(glyph).css; // Apply alpha to dim the character if (glyph.dim) { diff --git a/src/browser/renderer/dom/DomRenderer.ts b/src/browser/renderer/dom/DomRenderer.ts index e11a413d..d776f608 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 { fromRgba, toCss } from 'browser/Color'; +import { opaque } from 'browser/Color'; const TERMINAL_CLASS_PREFIX = 'xterm-dom-renderer-owner-'; const ROW_CONTAINER_CLASS = 'xterm-rows'; @@ -230,9 +230,8 @@ export class DomRenderer extends Disposable implements IRenderer { `${this._terminalSelector} .${FG_CLASS_PREFIX}${i} { color: ${c.css}; }` + `${this._terminalSelector} .${BG_CLASS_PREFIX}${i} { background-color: ${c.css}; }`; }); - const rgba = fromRgba(this._colors.background.rgba); styles += - `${this._terminalSelector} .${FG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { color: ${toCss(rgba[0], rgba[1], rgba[2])}; }` + + `${this._terminalSelector} .${FG_CLASS_PREFIX}${INVERTED_DEFAULT_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; From e46ff9ec852f1ee4f8c076950e445b9c4442c868 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 11 Nov 2019 19:03:12 -0800 Subject: [PATCH 004/103] Add minimumContrastRatio to DOM renderer Part of #322 --- src/browser/Color.test.ts | 24 ++++- src/browser/Color.ts | 99 +++++++++++++++++ src/browser/renderer/dom/DomRenderer.ts | 2 +- .../dom/DomRendererRowFactory.test.ts | 2 +- .../renderer/dom/DomRendererRowFactory.ts | 100 +++++++++++++----- src/common/services/OptionsService.ts | 3 + src/common/services/Services.ts | 1 + typings/xterm.d.ts | 8 ++ 8 files changed, 209 insertions(+), 30 deletions(-) diff --git a/src/browser/Color.test.ts b/src/browser/Color.test.ts index 44cd52f3..a8e4fa03 100644 --- a/src/browser/Color.test.ts +++ b/src/browser/Color.test.ts @@ -4,7 +4,7 @@ */ import { assert } from 'chai'; -import { blend, fromCss, toPaddedHex, toCss, toRgba } from 'browser/Color'; +import { blend, fromCss, toPaddedHex, toCss, toRgba, rgbRelativeLuminance, contrastRatio } from 'browser/Color'; describe('Color', () => { describe('blend', () => { @@ -135,4 +135,26 @@ describe('Color', () => { assert.equal(toRgba(0xff, 0xff, 0xff, 0xff), 0xffffffff); }); }); + describe('rgbRelativeLuminance', () => { + it('should calculate the relative luminance of the color', () => { + assert.equal(rgbRelativeLuminance(0x000000), 0); + + // TODO: Fill in tests + + assert.equal(rgbRelativeLuminance(0xFFFFFF), 1); + }); + }); + describe('contrastRatio', () => { + it('should calculate the relative luminance of the color', () => { + assert.equal(contrastRatio(0, 0), 1); + + // TODO: Fill in tests + + assert.equal(contrastRatio(0, 1), 21); + }); + it('should work regardless of the parameter order', () => { + assert.equal(contrastRatio(0, 1), 21); + assert.equal(contrastRatio(1, 0), 21); + }); + }); }); diff --git a/src/browser/Color.ts b/src/browser/Color.ts index a8ce2d16..4063e4c1 100644 --- a/src/browser/Color.ts +++ b/src/browser/Color.ts @@ -47,3 +47,102 @@ export function toRgba(r: number, g: number, b: number, a: number = 0xFF): numbe // >>> 0 forces an unsigned int return (r << 24 | g << 16 | b << 8 | a) >>> 0; } + +/** + * 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); +} + +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. + * @param l2 The first relative luminance. + * + * // TODO: Is this link right? + * @see https://www.w3.org/TR/WCAG20/#contrastratio + */ +export function contrastRatio(l1: number, l2: number): number { + if (l1 < l2) { + return (l2 + 0.05) / (l1 + 0.05); + } + return (l1 + 0.05) / (l2 + 0.05); +} + +// TODO: Cache [bg][fg]: result, should probably be owned by ColorManager? + +export function ensureContrastRatio(bg: IColor, fg: IColor, ratio: number): IColor | undefined { + const bgL = rgbRelativeLuminance(bg.rgba >> 8); + const fgL = rgbRelativeLuminance(fg.rgba >> 8); + const cr = contrastRatio(bgL, fgL); + if (cr < ratio) { + if (fgL < bgL) { + return reduceLuminance(bg, fg, ratio); + } + return increaseLuminance(bg, fg, ratio); + } + return undefined; +} + +export function reduceLuminance(bg: IColor, fg: IColor, ratio: number): IColor { + // This is a naive but fast approach to reducing luminance as converting to + // HSL and back is expensive + const bgR = (bg.rgba >> 24) & 0xFF; + const bgG = (bg.rgba >> 16) & 0xFF; + const bgB = (bg.rgba >> 8) & 0xFF; + let fgR = (fg.rgba >> 24) & 0xFF; + let fgG = (fg.rgba >> 16) & 0xFF; + let fgB = (fg.rgba >> 8) & 0xFF; + let cr = contrastRatio(rgbRelativeLuminance2(fgR, fgB, fgG), rgbRelativeLuminance2(bgR, bgG, bgB)); + while (cr < ratio && (fgR > 0 || fgG > 0 || fgB > 0)) { + // Increase by 10% (ceil) 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 { + css: toCss(fgR, fgG, fgB), + rgba: toRgba(fgR, fgG, fgB) + }; +} + +export function increaseLuminance(bg: IColor, fg: IColor, ratio: number): IColor { + // This is a naive but fast approach to increasing luminance as converting to + // HSL and back is expensive + const bgR = (bg.rgba >> 24) & 0xFF; + const bgG = (bg.rgba >> 16) & 0xFF; + const bgB = (bg.rgba >> 8) & 0xFF; + let fgR = (fg.rgba >> 24) & 0xFF; + let fgG = (fg.rgba >> 16) & 0xFF; + let fgB = (fg.rgba >> 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.floor((255 - fgR) * 0.1)); + fgG = Math.min(0xFF, fgG + Math.floor((255 - fgG) * 0.1)); + fgB = Math.min(0xFF, fgB + Math.floor((255 - fgB) * 0.1)); + cr = contrastRatio(rgbRelativeLuminance2(fgR, fgB, fgG), rgbRelativeLuminance2(bgR, bgG, bgB)); + } + return { + css: toCss(fgR, fgG, fgB), + rgba: toRgba(fgR, fgG, fgB) + }; +} diff --git a/src/browser/renderer/dom/DomRenderer.ts b/src/browser/renderer/dom/DomRenderer.ts index e8fc85f6..23c5a3a1 100644 --- a/src/browser/renderer/dom/DomRenderer.ts +++ b/src/browser/renderer/dom/DomRenderer.ts @@ -79,7 +79,7 @@ export class DomRenderer extends Disposable implements IRenderer { this._updateDimensions(); this._injectCss(); - this._rowFactory = new DomRendererRowFactory(document, this._optionsService); + this._rowFactory = new DomRendererRowFactory(document, this._optionsService, this._colors); this._element.classList.add(TERMINAL_CLASS_PREFIX + this._terminalClass); this._screenElement.appendChild(this._rowContainer); diff --git a/src/browser/renderer/dom/DomRendererRowFactory.test.ts b/src/browser/renderer/dom/DomRendererRowFactory.test.ts index b76d644d..bb9d97b6 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.test.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.test.ts @@ -19,7 +19,7 @@ describe('DomRendererRowFactory', () => { beforeEach(() => { dom = new jsdom.JSDOM(''); - rowFactory = new DomRendererRowFactory(dom.window.document, new MockOptionsService({ drawBoldTextInBrightColors: true })); + rowFactory = new DomRendererRowFactory(dom.window.document, new MockOptionsService({ drawBoldTextInBrightColors: true }), {} as any); lineData = createEmptyLineData(2); }); diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts index 3e3b5df0..2c206f33 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.ts @@ -6,9 +6,11 @@ import { IBufferLine } from 'common/Types'; import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants'; import { AttributeData } from 'common/buffer/AttributeData'; -import { NULL_CELL_CODE, WHITESPACE_CELL_CHAR } from 'common/buffer/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 { rgbRelativeLuminance, fromCss, contrastRatio, ensureContrastRatio } from 'browser/Color'; +import { IColorSet, IColor } from 'browser/Types'; export const BOLD_CLASS = 'xterm-bold'; export const DIM_CLASS = 'xterm-dim'; @@ -24,11 +26,16 @@ export class DomRendererRowFactory { private _workCell: CellData = new CellData(); constructor( - private _document: Document, - private _optionsService: IOptionsService + private readonly _document: Document, + private readonly _optionsService: IOptionsService, + private _colors: IColorSet ) { } + public setColors(colors: IColorSet): void { + this._colors = colors; + } + public createRow(lineData: IBufferLine, isCursorRow: boolean, cursorStyle: string | undefined, cursorX: number, cursorBlink: boolean, cellWidth: number, cols: number): DocumentFragment { const fragment = this._document.createDocumentFragment(); @@ -97,36 +104,75 @@ export class DomRendererRowFactory { charElement.textContent = this._workCell.getChars() || WHITESPACE_CELL_CHAR; - const swapColor = this._workCell.isInverse(); - - // fg - if (this._workCell.isFgRGB()) { - let style = charElement.getAttribute('style') || ''; - style += `${swapColor ? 'background-' : ''}color:rgb(${(AttributeData.toColorRGB(this._workCell.getFgColor())).join(',')});`; - charElement.setAttribute('style', style); - } else if (this._workCell.isFgPalette()) { - let fg = this._workCell.getFgColor(); - if (this._workCell.isBold() && fg < 8 && !swapColor && this._optionsService.options.drawBoldTextInBrightColors) { - fg += 8; - } - charElement.classList.add(`xterm-${swapColor ? 'b' : 'f'}g-${fg}`); - } else if (swapColor) { - charElement.classList.add(`xterm-bg-${INVERTED_DEFAULT_COLOR}`); + let fg = this._workCell.getFgColor(); + let fgColorMode = this._workCell.getFgColorMode(); + let bg = this._workCell.getBgColor(); + let bgColorMode = this._workCell.getBgColorMode(); + const isInverse = !!this._workCell.isInverse(); + if (isInverse) { + const temp = fg; + fg = bg; + bg = temp; + const temp2 = fgColorMode; + fgColorMode = bgColorMode; + bgColorMode = temp2; } - // bg - if (this._workCell.isBgRGB()) { - let style = charElement.getAttribute('style') || ''; - style += `${swapColor ? '' : 'background-'}color:rgb(${(AttributeData.toColorRGB(this._workCell.getBgColor())).join(',')});`; - charElement.setAttribute('style', style); - } else if (this._workCell.isBgPalette()) { - charElement.classList.add(`xterm-${swapColor ? 'f' : 'b'}g-${this._workCell.getBgColor()}`); - } else if (swapColor) { - charElement.classList.add(`xterm-fg-${INVERTED_DEFAULT_COLOR}`); + // Foreground + switch (fgColorMode) { + case Attributes.CM_P16: + case Attributes.CM_P256: + if (this._workCell.isBold() && fg < 8 && this._optionsService.options.drawBoldTextInBrightColors) { + fg += 8; + } + if (!this._applyMinimumContrast(charElement, this._colors.background, this._colors.ansi[fg])) { + charElement.classList.add(`xterm-fg-${fg}`); + } + break; + case Attributes.CM_RGB: + charElement.setAttribute('style', `${charElement.getAttribute('style') || ''}color:rgb(${(AttributeData.toColorRGB(fg)).join(',')});`); + break; + case Attributes.CM_DEFAULT: + default: + if (!this._applyMinimumContrast(charElement, this._colors.background, this._colors.foreground)) { + if (isInverse) { + charElement.classList.add(`xterm-fg-${INVERTED_DEFAULT_COLOR}`); + } + } + } + + // Background + switch (bgColorMode) { + case Attributes.CM_P16: + case Attributes.CM_P256: + charElement.classList.add(`xterm-bg-${bg}`); + break; + case Attributes.CM_RGB: + charElement.setAttribute('style', `${charElement.getAttribute('style') || ''}background-color:rgb(${(AttributeData.toColorRGB(bg)).join(',')});`); + break; + case Attributes.CM_DEFAULT: + default: + if (isInverse) { + charElement.classList.add(`xterm-bg-${INVERTED_DEFAULT_COLOR}`); + } } fragment.appendChild(charElement); } return fragment; } + + private _applyMinimumContrast(element: HTMLElement, bg: IColor, fg: IColor): boolean { + if (this._optionsService.options.minimumContrastRatio === 1) { + return false; + } + + const adustedColor = ensureContrastRatio(bg, fg, this._optionsService.options.minimumContrastRatio); + if (adustedColor) { + element.setAttribute('style', `${element.getAttribute('style') || ''}color:${adustedColor.css}`); + return true; + } + + return false; + } } diff --git a/src/common/services/OptionsService.ts b/src/common/services/OptionsService.ts index d9ea60d5..9174398e 100644 --- a/src/common/services/OptionsService.ts +++ b/src/common/services/OptionsService.ts @@ -37,6 +37,7 @@ export const DEFAULT_OPTIONS: ITerminalOptions = Object.freeze({ screenReaderMode: false, macOptionIsMeta: false, macOptionClickForcesSelection: false, + minimumContrastRatio: 6, disableStdin: false, allowTransparency: false, tabStopWidth: 8, @@ -116,6 +117,8 @@ export class OptionsService implements IOptionsService { throw new Error(`${key} cannot be less than 1, value: ${value}`); } break; + case 'minimumContrastRatio': + value = Math.max(1, Math.min(21, Math.round(value * 10) / 10)); case 'scrollback': value = Math.min(value, 4294967295); if (value < 0) { diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index e4ff90d0..1a39cced 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -229,6 +229,7 @@ export interface ITerminalOptions { logLevel: LogLevel; macOptionIsMeta: boolean; macOptionClickForcesSelection: boolean; + minimumContrastRatio: number; rendererType: RendererType; rightClickSelectsWord: boolean; rows: number; diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 9728a72c..b49ed04b 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -148,6 +148,14 @@ declare module 'xterm' { */ macOptionClickForcesSelection?: boolean; + /** + * The minimum contrast ratio for text in the terminal, setting this will + * change the foreground color dynamically depending on whether the contrast + * ratio is met. This can be to set 0.1 increments between 1 (default, do + * nothing) and 21 (foreground will be black or white). + */ + minimumContrastRatio?: number; + /** * The type of renderer to use, this allows using the fallback DOM renderer * when canvas is too slow for the environment. The following features do From 9cd66e72bbfac8f06c752fe8c88bda307d49fa9a Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 11 Nov 2019 19:14:35 -0800 Subject: [PATCH 005/103] Add tests for relative luminance --- src/browser/Color.test.ts | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/browser/Color.test.ts b/src/browser/Color.test.ts index a8e4fa03..975315c6 100644 --- a/src/browser/Color.test.ts +++ b/src/browser/Color.test.ts @@ -138,18 +138,28 @@ describe('Color', () => { describe('rgbRelativeLuminance', () => { it('should calculate the relative luminance of the color', () => { assert.equal(rgbRelativeLuminance(0x000000), 0); - - // TODO: Fill in tests - + 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); - - // TODO: Fill in tests - + assert.equal(contrastRatio(0, 0.5), 11); assert.equal(contrastRatio(0, 1), 21); }); it('should work regardless of the parameter order', () => { From a6dcb21f1ca8c0cd70437d92a3f2da12fca3ca46 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 11 Nov 2019 19:30:36 -0800 Subject: [PATCH 006/103] Fix tests --- src/browser/Color.ts | 4 +-- .../dom/DomRendererRowFactory.test.ts | 34 ++++++++++++++++--- .../renderer/dom/DomRendererRowFactory.ts | 2 +- src/common/services/OptionsService.ts | 2 +- 4 files changed, 32 insertions(+), 10 deletions(-) diff --git a/src/browser/Color.ts b/src/browser/Color.ts index 4063e4c1..c74e8745 100644 --- a/src/browser/Color.ts +++ b/src/browser/Color.ts @@ -75,9 +75,7 @@ export function rgbRelativeLuminance2(r: number, g: number, b: number): number { * Gets the contrast ratio between two relative luminance values. * @param l1 The first relative luminance. * @param l2 The first relative luminance. - * - * // TODO: Is this link right? - * @see https://www.w3.org/TR/WCAG20/#contrastratio + * @see https://www.w3.org/TR/WCAG20/#contrast-ratiodef */ export function contrastRatio(l1: number, l2: number): number { if (l1 < l2) { diff --git a/src/browser/renderer/dom/DomRendererRowFactory.test.ts b/src/browser/renderer/dom/DomRendererRowFactory.test.ts index bb9d97b6..180db0fc 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.test.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.test.ts @@ -11,6 +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'; describe('DomRendererRowFactory', () => { let dom: jsdom.JSDOM; @@ -19,7 +20,30 @@ describe('DomRendererRowFactory', () => { beforeEach(() => { dom = new jsdom.JSDOM(''); - rowFactory = new DomRendererRowFactory(dom.window.document, new MockOptionsService({ drawBoldTextInBrightColors: true }), {} as any); + rowFactory = new DomRendererRowFactory(dom.window.document, new MockOptionsService({ drawBoldTextInBrightColors: true }), { + background: fromCss('#010101'), + foreground: fromCss('#020202'), + ansi: [ + // dark: + fromCss('#2e3436'), + fromCss('#cc0000'), + fromCss('#4e9a06'), + fromCss('#c4a000'), + fromCss('#3465a4'), + fromCss('#75507b'), + fromCss('#06989a'), + fromCss('#d3d7cf'), + // bright: + fromCss('#555753'), + fromCss('#ef2929'), + fromCss('#8ae234'), + fromCss('#fce94f'), + fromCss('#729fcf'), + fromCss('#ad7fa8'), + fromCss('#34e2e2'), + fromCss('#eeeeec') + ] + } as any); lineData = createEmptyLineData(2); }); @@ -142,7 +166,7 @@ describe('DomRendererRowFactory', () => { lineData.setCell(0, cell); const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), - 'a' + 'a' ); }); @@ -153,7 +177,7 @@ describe('DomRendererRowFactory', () => { lineData.setCell(0, cell); const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), - 'a' + 'a' ); }); @@ -163,7 +187,7 @@ describe('DomRendererRowFactory', () => { lineData.setCell(0, cell); const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), - 'a' + 'a' ); }); @@ -199,7 +223,7 @@ describe('DomRendererRowFactory', () => { lineData.setCell(0, cell); const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), - 'a' + 'a' ); }); }); diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts index 2c206f33..b5caccaf 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.ts @@ -9,7 +9,7 @@ import { AttributeData } from 'common/buffer/AttributeData'; 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 { rgbRelativeLuminance, fromCss, contrastRatio, ensureContrastRatio } from 'browser/Color'; +import { ensureContrastRatio } from 'browser/Color'; import { IColorSet, IColor } from 'browser/Types'; export const BOLD_CLASS = 'xterm-bold'; diff --git a/src/common/services/OptionsService.ts b/src/common/services/OptionsService.ts index 9174398e..b0b57694 100644 --- a/src/common/services/OptionsService.ts +++ b/src/common/services/OptionsService.ts @@ -37,7 +37,7 @@ export const DEFAULT_OPTIONS: ITerminalOptions = Object.freeze({ screenReaderMode: false, macOptionIsMeta: false, macOptionClickForcesSelection: false, - minimumContrastRatio: 6, + minimumContrastRatio: 1, disableStdin: false, allowTransparency: false, tabStopWidth: 8, From b1bcb899e0aa478ee49023ede4c346bbc6f20c66 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 11 Nov 2019 21:29:02 -0800 Subject: [PATCH 007/103] Fix inverse in webgl, add tests --- .../src/RectangleRenderer.ts | 26 +- .../src/WebglRenderer.api.ts | 233 +++++++++++++++++- addons/xterm-addon-webgl/src/WebglRenderer.ts | 32 +-- .../src/atlas/WebglCharAtlas.ts | 74 ++++-- 4 files changed, 309 insertions(+), 56 deletions(-) diff --git a/addons/xterm-addon-webgl/src/RectangleRenderer.ts b/addons/xterm-addon-webgl/src/RectangleRenderer.ts index 8dcc5c08..356e1747 100644 --- a/addons/xterm-addon-webgl/src/RectangleRenderer.ts +++ b/addons/xterm-addon-webgl/src/RectangleRenderer.ts @@ -248,23 +248,26 @@ export class RectangleRenderer { let currentStartX = -1; let currentBg = 0; let currentFg = 0; + let currentInverse = false; for (let x = 0; x < terminal.cols; x++) { const modelIndex = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL; const bg = model.cells[modelIndex + RENDER_MODEL_BG_OFFSET]; const fg = model.cells[modelIndex + RENDER_MODEL_FG_OFFSET]; - if (bg !== currentBg) { + const inverse = !!(fg & FgFlags.INVERSE); + if (bg !== currentBg || ((inverse || currentInverse) && fg !== currentFg)) { // A rectangle needs to be drawn if going from non-default to another color - if (currentBg !== 0) { + if (currentBg !== 0 || (currentInverse && currentFg !== 0)) { const offset = rectangleCount++ * INDICES_PER_RECTANGLE; this._updateRectangle(vertices, offset, currentFg, currentBg, currentStartX, x, y); } currentStartX = x; currentBg = bg; currentFg = fg; + currentInverse = inverse; } } // Finish rectangle if it's still going - if (currentBg !== 0) { + if (currentBg !== 0 || (currentInverse && currentFg !== 0)) { const offset = rectangleCount++ * INDICES_PER_RECTANGLE; this._updateRectangle(vertices, offset, currentFg, currentBg, currentStartX, terminal.cols, y); } @@ -274,12 +277,21 @@ export class RectangleRenderer { private _updateRectangle(vertices: IVertices, offset: number, fg: number, bg: number, startX: number, endX: number, y: number): void { let rgba: number | undefined; - const colorMode = bg & Attributes.CM_MASK; if (fg & FgFlags.INVERSE) { - // Inverted color - rgba = this._colors.foreground.rgba; + switch (fg & Attributes.CM_MASK) { + case Attributes.CM_P16: + case Attributes.CM_P256: + rgba = this._colors.ansi[fg & Attributes.PCOLOR_MASK].rgba; + break; + case Attributes.CM_RGB: + rgba = (fg & Attributes.RGB_MASK) << 8; + break; + case Attributes.CM_DEFAULT: + default: + rgba = this._colors.foreground.rgba; + } } else { - switch (colorMode) { + switch (bg & Attributes.CM_MASK) { case Attributes.CM_P16: case Attributes.CM_P256: rgba = this._colors.ansi[bg & Attributes.PCOLOR_MASK].rgba; diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.api.ts b/addons/xterm-addon-webgl/src/WebglRenderer.api.ts index 304973fe..556061e7 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.api.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.api.ts @@ -16,7 +16,7 @@ let page: puppeteer.Page; const width = 800; const height = 600; -describe('WebGL Renderer Integration Tests', function(): void { +describe.only('WebGL Renderer Integration Tests', function(): void { it('dispose removes renderer canvases', async () => { await setupBrowser(); assert.equal(await page.evaluate(`document.querySelectorAll('.xterm canvas').length`), 3); @@ -76,6 +76,52 @@ describe('WebGL Renderer Integration Tests', function(): void { await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]); }); + it('foreground 0-15 inverse', 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[7;30m \\x1b[7;31m \\x1b[7;32m \\x1b[7;33m \\x1b[7;34m \\x1b[7;35m \\x1b[7;36m \\x1b[7;37m `); + 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('background 0-15 inverse', 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[7;40m█\\x1b[7;41m█\\x1b[7;42m█\\x1b[7;43m█\\x1b[7;44m█\\x1b[7;45m█\\x1b[7;46m█\\x1b[7;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', @@ -162,6 +208,46 @@ describe('WebGL Renderer Integration Tests', function(): void { } }); + it('foreground 16-255 inverse', async () => { + let data = ''; + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + data += `\\x1b[7;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), [r, g, b, 255]); + } + } + }); + + it('background 16-255 inverse', async () => { + let data = ''; + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + data += `\\x1b[7;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++) { @@ -305,6 +391,151 @@ describe('WebGL Renderer Integration Tests', function(): void { } } }); + + it('foreground true color red inverse', async function(): Promise { + this.timeout(60000); + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\x1b[7;38;2;${i};0;0m \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, 0, 0, 255]); + } + } + }); + + it('background true color red inverse', async function(): Promise { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\\x1b[7;48;2;${i};0;0m█\\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, 0, 0, 255]); + } + } + }); + + it('foreground true color green inverse', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\\x1b[7;38;2;0;${i};0m \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, i, 0, 255]); + } + } + }); + + it('background true color green inverse', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\\x1b[7;48;2;0;${i};0m█\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, i, 0, 255]); + } + } + }); + + it('foreground true color blue inverse', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\\x1b[7;38;2;0;0;${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, i, 255]); + } + } + }); + + it('background true color blue inverse', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\\x1b[7;48;2;0;0;${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, i, 255]); + } + } + }); + + it('foreground true color grey inverse', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\\x1b[7;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), [i, i, i, 255]); + } + } + }); + + it('background true color grey inverse', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\\x1b[7;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]); + } + } + }); }); }); diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index 5eae61db..3a71dcd8 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -252,35 +252,13 @@ export class WebglRenderer extends Disposable implements IRenderer { this._model.lineLengths[y] = x + 1; } - // Resolve bg and fg - let bg = this._workCell.bg; - let fg = this._workCell.fg; - // Nothing has changed, no updates needed if (this._model.cells[i] === code && - this._model.cells[i + RENDER_MODEL_BG_OFFSET] === bg && - this._model.cells[i + RENDER_MODEL_FG_OFFSET] === fg) { + this._model.cells[i + RENDER_MODEL_BG_OFFSET] === this._workCell.bg && + this._model.cells[i + RENDER_MODEL_FG_OFFSET] === this._workCell.fg) { continue; } - // If inverse flag is on, the foreground should become the background. - if (this._workCell.isInverse()) { - const temp = bg; - bg = fg; - fg = temp; - if (fg === DEFAULT_COLOR) { - fg = INVERTED_DEFAULT_COLOR; - } - if (bg === DEFAULT_COLOR) { - bg = INVERTED_DEFAULT_COLOR; - } - } - - // Apply drawBoldTextInBrightColors - if (terminal.options.drawBoldTextInBrightColors && this._workCell.isBold() && fg & FgFlags.BOLD && this._workCell.getFgColor() < 8) { - fg += 8; - } - // Flag combined chars with a bit mask so they're easily identifiable if (chars.length > 1) { code = code | COMBINED_CHAR_BIT_MASK; @@ -288,10 +266,10 @@ export class WebglRenderer extends Disposable implements IRenderer { // Cache the results in the model this._model.cells[i] = code; - this._model.cells[i + RENDER_MODEL_BG_OFFSET] = bg; - this._model.cells[i + RENDER_MODEL_FG_OFFSET] = fg; + this._model.cells[i + RENDER_MODEL_BG_OFFSET] = this._workCell.bg; + this._model.cells[i + RENDER_MODEL_FG_OFFSET] = this._workCell.fg; - this._glyphRenderer.updateCell(x, y, code, bg, fg, chars); + this._glyphRenderer.updateCell(x, y, code, this._workCell.bg, this._workCell.fg, chars); } } this._rectangleRenderer.updateBackgrounds(this._model); diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index 16b7ba07..b4d3f822 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -176,51 +176,47 @@ export class WebglCharAtlas implements IDisposable { return this._config.colors.ansi[idx]; } - private _getBackgroundColor(bg: number, fg: number): IColor { + private _getBackgroundColor(bgColorMode: number, bgColor: number, inverse: boolean): IColor { if (this._config.allowTransparency) { // The background color might have some transparency, so we need to render it as fully // transparent in the atlas. Otherwise we'd end up drawing the transparent background twice // around the anti-aliased edges of the glyph, and it would look too dark. return TRANSPARENT_COLOR; - } else if (fg & FgFlags.INVERSE) { - return this._config.colors.foreground; } - const colorMode = bg & Attributes.CM_MASK; - switch (colorMode) { + switch (bgColorMode) { case Attributes.CM_P16: case Attributes.CM_P256: - return this._getColorFromAnsiIndex(bg & Attributes.PCOLOR_MASK); + return this._getColorFromAnsiIndex(bgColor); case Attributes.CM_RGB: - const rgb = bg & Attributes.RGB_MASK; - const arr = AttributeData.toColorRGB(rgb); + const arr = AttributeData.toColorRGB(bgColor); // TODO: This object creation is slow return { - rgba: rgb << 8, + rgba: bgColor << 8, css: `#${toPaddedHex(arr[0])}${toPaddedHex(arr[1])}${toPaddedHex(arr[2])}` }; case Attributes.CM_DEFAULT: default: + if (inverse) { + return this._config.colors.foreground; + } return this._config.colors.background; } } - private _getForegroundCss(fg: number): string { - if (fg & FgFlags.INVERSE) { - return this._config.colors.background.css; - } - - const colorMode = fg & Attributes.CM_MASK; - switch (colorMode) { + private _getForegroundCss(fgColorMode: number, fgColor: number, inverse: boolean): string { + switch (fgColorMode) { case Attributes.CM_P16: case Attributes.CM_P256: - return this._getColorFromAnsiIndex(fg & Attributes.PCOLOR_MASK).css; + return this._getColorFromAnsiIndex(fgColor).css; case Attributes.CM_RGB: - const rgb = fg & Attributes.RGB_MASK; - const arr = AttributeData.toColorRGB(rgb); + const arr = AttributeData.toColorRGB(fgColor); return `#${toPaddedHex(arr[0])}${toPaddedHex(arr[1])}${toPaddedHex(arr[2])}`; case Attributes.CM_DEFAULT: default: + if (inverse) { + return this._config.colors.background.css; + } return this._config.colors.foreground.css; } } @@ -233,13 +229,33 @@ export class WebglCharAtlas implements IDisposable { this.hasCanvasChanged = true; const bold = !!(fg & FgFlags.BOLD); + const inverse = !!(fg & FgFlags.INVERSE); const dim = !!(bg & BgFlags.DIM); const italic = !!(bg & BgFlags.ITALIC); this._tmpCtx.save(); + let fgColor = getFgColor(fg); + let fgColorMode = fg & Attributes.CM_MASK; + let bgColor = getBgColor(bg); + let bgColorMode = bg & Attributes.CM_MASK; + if (inverse) { + const temp = fgColor; + fgColor = bgColor; + bgColor = temp; + const temp2 = fgColorMode; + fgColorMode = bgColorMode; + bgColorMode = temp2; + } + + // TODO: Pass drawBoldTextInBrightColors through + // Apply drawBoldTextInBrightColors + // if (terminal.options.drawBoldTextInBrightColors && this._workCell.isBold() && fg & FgFlags.BOLD && this._workCell.getFgColor() < 8) { + // fg += 8; + // } + // draw the background - const backgroundColor = this._getBackgroundColor(bg, fg); + const backgroundColor = this._getBackgroundColor(bgColorMode, bgColor, inverse); // Use a 'copy' composite operation to clear any existing glyph out of _tmpCtxWithAlpha, regardless of // transparency in backgroundColor this._tmpCtx.globalCompositeOperation = 'copy'; @@ -254,7 +270,7 @@ export class WebglCharAtlas implements IDisposable { `${fontStyle} ${fontWeight} ${this._config.fontSize * this._config.devicePixelRatio}px ${this._config.fontFamily}`; this._tmpCtx.textBaseline = 'top'; - this._tmpCtx.fillStyle = this._getForegroundCss(fg); + this._tmpCtx.fillStyle = this._getForegroundCss(fgColorMode, fgColor, inverse); // Apply alpha to dim the character if (dim) { @@ -441,3 +457,19 @@ function toPaddedHex(c: number): string { return s.length < 2 ? '0' + s : s; } +function getFgColor(fg: number): number { + switch (fg & Attributes.CM_MASK) { + case Attributes.CM_P16: + case Attributes.CM_P256: return fg & Attributes.PCOLOR_MASK; + case Attributes.CM_RGB: return fg & Attributes.RGB_MASK; + default: return -1; // CM_DEFAULT defaults to -1 + } +} +function getBgColor(bg: number): number { + switch (bg & Attributes.CM_MASK) { + case Attributes.CM_P16: + case Attributes.CM_P256: return bg & Attributes.PCOLOR_MASK; + case Attributes.CM_RGB: return bg & Attributes.RGB_MASK; + default: return -1; // CM_DEFAULT defaults to -1 + } +} From db775afd70086be49fe2df9aa429523900ac0d42 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 11 Nov 2019 21:35:49 -0800 Subject: [PATCH 008/103] Support drawBoldTextInBrightColors in webgl again This broke as part of this PR --- .../src/WebglRenderer.api.ts | 27 ++++++++++++++++++- .../src/atlas/CharAtlasUtils.ts | 1 + addons/xterm-addon-webgl/src/atlas/Types.d.ts | 1 + .../src/atlas/WebglCharAtlas.ts | 18 ++++++------- 4 files changed, 37 insertions(+), 10 deletions(-) diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.api.ts b/addons/xterm-addon-webgl/src/WebglRenderer.api.ts index 556061e7..18449e8a 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.api.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.api.ts @@ -53,6 +53,32 @@ describe.only('WebGL Renderer Integration Tests', function(): void { await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]); }); + it('foreground 0-7 drawBoldTextInBrightColors', async () => { + const theme: ITheme = { + brightBlack: '#010203', + brightRed: '#040506', + brightGreen: '#070809', + brightYellow: '#0a0b0c', + brightBlue: '#0d0e0f', + brightMagenta: '#101112', + brightCyan: '#131415', + brightWhite: '#161718' + }; + await page.evaluate(` + window.term.setOption('theme', ${JSON.stringify(theme)}); + window.term.setOption('drawBoldTextInBrightColors', true); + `); + await writeSync(`\\x1b[1;30m█\\x1b[1;31m█\\x1b[1;32m█\\x1b[1;33m█\\x1b[1;34m█\\x1b[1;35m█\\x1b[1;36m█\\x1b[1;37m█`); + 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('background 0-15', async () => { const theme: ITheme = { black: '#010203', @@ -393,7 +419,6 @@ describe.only('WebGL Renderer Integration Tests', function(): void { }); it('foreground true color red inverse', async function(): Promise { - this.timeout(60000); let data = ''; for (let y = 0; y < 16; y++) { for (let x = 0; x < 16; x++) { diff --git a/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts b/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts index 03867b41..ec7d9a7e 100644 --- a/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts +++ b/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts @@ -35,6 +35,7 @@ export function generateConfig(scaledCharWidth: number, scaledCharHeight: number fontWeight: terminal.getOption('fontWeight') as FontWeight, fontWeightBold: terminal.getOption('fontWeightBold') as FontWeight, allowTransparency: terminal.getOption('allowTransparency'), + drawBoldTextInBrightColors: terminal.getOption('drawBoldTextInBrightColors'), colors: clonedColors }; } diff --git a/addons/xterm-addon-webgl/src/atlas/Types.d.ts b/addons/xterm-addon-webgl/src/atlas/Types.d.ts index 1de843e0..5b96adb0 100644 --- a/addons/xterm-addon-webgl/src/atlas/Types.d.ts +++ b/addons/xterm-addon-webgl/src/atlas/Types.d.ts @@ -25,5 +25,6 @@ export interface ICharAtlasConfig { scaledCharWidth: number; scaledCharHeight: number; allowTransparency: boolean; + drawBoldTextInBrightColors: boolean; colors: IColorSet; } diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index b4d3f822..6325ca92 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -68,7 +68,10 @@ export class WebglCharAtlas implements IDisposable { private _workBoundingBox: IBoundingBox = { top: 0, left: 0, bottom: 0, right: 0 }; - constructor(document: Document, private _config: ICharAtlasConfig) { + constructor( + document: Document, + private _config: ICharAtlasConfig + ) { this.cacheCanvas = document.createElement('canvas'); this.cacheCanvas.width = TEXTURE_WIDTH; this.cacheCanvas.height = TEXTURE_HEIGHT; @@ -204,10 +207,13 @@ export class WebglCharAtlas implements IDisposable { } } - private _getForegroundCss(fgColorMode: number, fgColor: number, inverse: boolean): string { + private _getForegroundCss(fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean): string { switch (fgColorMode) { case Attributes.CM_P16: case Attributes.CM_P256: + if (this._config.drawBoldTextInBrightColors && bold && fgColor < 8) { + fgColor += 8; + } return this._getColorFromAnsiIndex(fgColor).css; case Attributes.CM_RGB: const arr = AttributeData.toColorRGB(fgColor); @@ -248,12 +254,6 @@ export class WebglCharAtlas implements IDisposable { bgColorMode = temp2; } - // TODO: Pass drawBoldTextInBrightColors through - // Apply drawBoldTextInBrightColors - // if (terminal.options.drawBoldTextInBrightColors && this._workCell.isBold() && fg & FgFlags.BOLD && this._workCell.getFgColor() < 8) { - // fg += 8; - // } - // draw the background const backgroundColor = this._getBackgroundColor(bgColorMode, bgColor, inverse); // Use a 'copy' composite operation to clear any existing glyph out of _tmpCtxWithAlpha, regardless of @@ -270,7 +270,7 @@ export class WebglCharAtlas implements IDisposable { `${fontStyle} ${fontWeight} ${this._config.fontSize * this._config.devicePixelRatio}px ${this._config.fontFamily}`; this._tmpCtx.textBaseline = 'top'; - this._tmpCtx.fillStyle = this._getForegroundCss(fgColorMode, fgColor, inverse); + this._tmpCtx.fillStyle = this._getForegroundCss(fgColorMode, fgColor, inverse, bold); // Apply alpha to dim the character if (dim) { From 2283ea45a6de0dddacd7a112cdca2b5bd5d694e7 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 11 Nov 2019 22:18:39 -0800 Subject: [PATCH 009/103] Refresh rows after ratio changes --- src/Terminal.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Terminal.ts b/src/Terminal.ts index 3c589ffa..b08270fd 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -329,6 +329,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp case 'lineHeight': case 'fontWeight': case 'fontWeightBold': + case 'minimumContrastRatio': // When the font changes the size of the cells may change which requires a renderer clear if (this._renderService) { this._renderService.clear(); From 531409c2c7c3dea4b05394e0ec1952bfe443fa2a Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 11 Nov 2019 22:47:26 -0800 Subject: [PATCH 010/103] Support minimum contrast ratio in webgl --- .../src/WebglRenderer.api.ts | 72 ++++++- .../src/atlas/CharAtlasUtils.ts | 3 + addons/xterm-addon-webgl/src/atlas/Types.d.ts | 1 + .../src/atlas/WebglCharAtlas.ts | 179 +++++++++++++++++- src/browser/Color.ts | 67 ++++--- 5 files changed, 291 insertions(+), 31 deletions(-) diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.api.ts b/addons/xterm-addon-webgl/src/WebglRenderer.api.ts index 18449e8a..25719faa 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.api.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.api.ts @@ -16,7 +16,7 @@ let page: puppeteer.Page; const width = 800; const height = 600; -describe.only('WebGL Renderer Integration Tests', function(): void { +describe('WebGL Renderer Integration Tests', function(): void { it('dispose removes renderer canvases', async () => { await setupBrowser(); assert.equal(await page.evaluate(`document.querySelectorAll('.xterm canvas').length`), 3); @@ -562,6 +562,76 @@ describe.only('WebGL Renderer Integration Tests', function(): void { } }); }); + + describe('minimumContrastRatio', async () => { + before(async () => setupBrowser()); + after(async () => browser.close()); + beforeEach(async () => page.evaluate(`window.term.reset()`)); + + it('should adjust 0-15 colors on black background', async () => { + const theme: ITheme = { + black: '#2e3436', + red: '#cc0000', + green: '#4e9a06', + yellow: '#c4a000', + blue: '#3465a4', + magenta: '#75507b', + cyan: '#06989a', + white: '#d3d7cf', + brightBlack: '#555753', + brightRed: '#ef2929', + brightGreen: '#8ae234', + brightYellow: '#fce94f', + brightBlue: '#729fcf', + brightMagenta: '#ad7fa8', + brightCyan: '#34e2e2', + brightWhite: '#eeeeec' + }; + await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`); + await writeSync( + `\\x1b[30m█\\x1b[31m█\\x1b[32m█\\x1b[33m█\\x1b[34m█\\x1b[35m█\\x1b[36m█\\x1b[37m█\\r\\n` + + `\\x1b[90m█\\x1b[91m█\\x1b[92m█\\x1b[93m█\\x1b[94m█\\x1b[95m█\\x1b[96m█\\x1b[97m█` + ); + // Validate before minimumContrastRatio is applied + await pollFor(page, () => getCellColor(1, 1), [0x2e, 0x34, 0x36, 255]); + await pollFor(page, () => getCellColor(2, 1), [0xcc, 0x00, 0x00, 255]); + await pollFor(page, () => getCellColor(3, 1), [0x4e, 0x9a, 0x06, 255]); + await pollFor(page, () => getCellColor(4, 1), [0xc4, 0xa0, 0x00, 255]); + await pollFor(page, () => getCellColor(5, 1), [0x34, 0x65, 0xa4, 255]); + await pollFor(page, () => getCellColor(6, 1), [0x75, 0x50, 0x7b, 255]); + await pollFor(page, () => getCellColor(7, 1), [0x06, 0x98, 0x9a, 255]); + await pollFor(page, () => getCellColor(8, 1), [0xd3, 0xd7, 0xcf, 255]); + await pollFor(page, () => getCellColor(1, 2), [0x55, 0x57, 0x53, 255]); + await pollFor(page, () => getCellColor(2, 2), [0xef, 0x29, 0x29, 255]); + await pollFor(page, () => getCellColor(3, 2), [0x8a, 0xe2, 0x34, 255]); + await pollFor(page, () => getCellColor(4, 2), [0xfc, 0xe9, 0x4f, 255]); + await pollFor(page, () => getCellColor(5, 2), [0x72, 0x9f, 0xcf, 255]); + await pollFor(page, () => getCellColor(6, 2), [0xad, 0x7f, 0xa8, 255]); + await pollFor(page, () => getCellColor(7, 2), [0x34, 0xe2, 0xe2, 255]); + await pollFor(page, () => getCellColor(8, 2), [0xee, 0xee, 0xec, 255]); + // Setting and check for minimum contrast values, note that these are note + // exact to the contrast ratio, if the increase luminance algorithm + // changes then these will probably fail + await page.evaluate(`window.term.setOption('minimumContrastRatio', 10);`); + await pollFor(page, () => getCellColor(1, 1), [179, 182, 182, 255]); + await pollFor(page, () => getCellColor(2, 1), [234, 163, 163, 255]); + await pollFor(page, () => getCellColor(3, 1), [196, 221, 174, 255]); + await pollFor(page, () => getCellColor(4, 1), [231, 219, 163, 255]); + await pollFor(page, () => getCellColor(5, 1), [133, 162, 200, 255]); + await pollFor(page, () => getCellColor(6, 1), [180, 159, 182, 255]); + await pollFor(page, () => getCellColor(7, 1), [106, 192, 194, 255]); + await pollFor(page, () => getCellColor(8, 1), [211, 215, 207, 255]); + await pollFor(page, () => getCellColor(1, 2), [180, 181, 179, 255]); + await pollFor(page, () => getCellColor(2, 2), [246, 160, 160, 255]); + await pollFor(page, () => getCellColor(3, 2), [138, 226, 52, 255]); + await pollFor(page, () => getCellColor(4, 2), [252, 233, 79, 255]); + await pollFor(page, () => getCellColor(5, 2), [114, 159, 207, 255]); + await pollFor(page, () => getCellColor(6, 2), [188, 150, 183, 255]); + // Unchanged + await pollFor(page, () => getCellColor(7, 2), [0x34, 0xe2, 0xe2, 255]); + await pollFor(page, () => getCellColor(8, 2), [0xee, 0xee, 0xec, 255]); + }); + }); }); async function openTerminal(options: ITerminalOptions = {}): Promise { diff --git a/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts b/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts index ec7d9a7e..e8e19095 100644 --- a/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts +++ b/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts @@ -36,6 +36,7 @@ export function generateConfig(scaledCharWidth: number, scaledCharHeight: number fontWeightBold: terminal.getOption('fontWeightBold') as FontWeight, allowTransparency: terminal.getOption('allowTransparency'), drawBoldTextInBrightColors: terminal.getOption('drawBoldTextInBrightColors'), + minimumContrastRatio: terminal.getOption('minimumContrastRatio'), colors: clonedColors }; } @@ -54,6 +55,8 @@ export function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean a.allowTransparency === b.allowTransparency && a.scaledCharWidth === b.scaledCharWidth && a.scaledCharHeight === b.scaledCharHeight && + a.drawBoldTextInBrightColors === b.drawBoldTextInBrightColors && + a.minimumContrastRatio === b.minimumContrastRatio && a.colors.foreground === b.colors.foreground && a.colors.background === b.colors.background; } diff --git a/addons/xterm-addon-webgl/src/atlas/Types.d.ts b/addons/xterm-addon-webgl/src/atlas/Types.d.ts index 5b96adb0..cd73393c 100644 --- a/addons/xterm-addon-webgl/src/atlas/Types.d.ts +++ b/addons/xterm-addon-webgl/src/atlas/Types.d.ts @@ -26,5 +26,6 @@ export interface ICharAtlasConfig { scaledCharHeight: number; allowTransparency: boolean; drawBoldTextInBrightColors: boolean; + minimumContrastRatio: number; colors: IColorSet; } diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index 6325ca92..f6691067 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -207,7 +207,12 @@ export class WebglCharAtlas implements IDisposable { } } - private _getForegroundCss(fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean): string { + private _getForegroundCss(bgColorMode: number, bgColor: number, fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean): string { + const minimumContrastCss = this._getMinimumContrastCss(bgColorMode, bgColor, fgColorMode, fgColor, inverse); + if (minimumContrastCss) { + return minimumContrastCss; + } + switch (fgColorMode) { case Attributes.CM_P16: case Attributes.CM_P256: @@ -217,7 +222,7 @@ export class WebglCharAtlas implements IDisposable { return this._getColorFromAnsiIndex(fgColor).css; case Attributes.CM_RGB: const arr = AttributeData.toColorRGB(fgColor); - return `#${toPaddedHex(arr[0])}${toPaddedHex(arr[1])}${toPaddedHex(arr[2])}`; + return toCss(arr[0], arr[1], arr[2]); case Attributes.CM_DEFAULT: default: if (inverse) { @@ -227,6 +232,57 @@ export class WebglCharAtlas implements IDisposable { } } + private _resolveBackgroundRgba(bgColorMode: number, bgColor: number, fgColorMode: number, fgColor: number, inverse: boolean): number { + switch (bgColorMode) { + case Attributes.CM_P16: + case Attributes.CM_P256: + return this._getColorFromAnsiIndex(bgColor).rgba; + case Attributes.CM_RGB: + return bgColor << 8; + case Attributes.CM_DEFAULT: + default: + if (inverse) { + this._config.colors.foreground.rgba; + } + return this._config.colors.background.rgba; + } + } + + private _resolveForegroundRgba(bgColorMode: number, bgColor: number, fgColorMode: number, fgColor: number, inverse: boolean): number { + switch (fgColorMode) { + case Attributes.CM_P16: + case Attributes.CM_P256: + return this._getColorFromAnsiIndex(fgColor).rgba; + case Attributes.CM_RGB: + return fgColor << 8; + case Attributes.CM_DEFAULT: + default: + if (inverse) { + this._config.colors.background.rgba; + } + return this._config.colors.foreground.rgba; + } + } + + private _getMinimumContrastCss(bgColorMode: number, bgColor: number, fgColorMode: number, fgColor: number, inverse: boolean): string | undefined { + const bgRgba = this._resolveBackgroundRgba(bgColorMode, bgColor, fgColorMode, fgColor, inverse); + const fgRgba = this._resolveForegroundRgba(bgColorMode, bgColor, fgColorMode, fgColor, inverse); + console.log('ratio', this._config.minimumContrastRatio); + if (this._config.minimumContrastRatio === 1) { + return undefined; + } + const result = ensureContrastRatioRgba(bgRgba, fgRgba, this._config.minimumContrastRatio); + console.log('get min', result, bgRgba, fgRgba); + if (!result) { + return undefined; + } + return toCss( + (result >> 24) & 0xFF, + (result >> 16) & 0xFF, + (result >> 8) & 0xFF + ); + } + private _drawToCache(code: number, bg: number, fg: number): IRasterizedGlyph; private _drawToCache(chars: string, bg: number, fg: number): IRasterizedGlyph; private _drawToCache(codeOrChars: number | string, bg: number, fg: number): IRasterizedGlyph { @@ -270,7 +326,7 @@ export class WebglCharAtlas implements IDisposable { `${fontStyle} ${fontWeight} ${this._config.fontSize * this._config.devicePixelRatio}px ${this._config.fontFamily}`; this._tmpCtx.textBaseline = 'top'; - this._tmpCtx.fillStyle = this._getForegroundCss(fgColorMode, fgColor, inverse, bold); + this._tmpCtx.fillStyle = this._getForegroundCss(bgColorMode, bgColor, fgColorMode, fgColor, inverse, bold); // Apply alpha to dim the character if (dim) { @@ -473,3 +529,120 @@ function getBgColor(bg: number): number { default: return -1; // CM_DEFAULT defaults to -1 } } + +export function toCss(r: number, g: number, b: number): string { + 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; +} + +/** + * 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); +} + +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. + * @param l2 The first relative luminance. + * @see https://www.w3.org/TR/WCAG20/#contrast-ratiodef + */ +export function contrastRatio(l1: number, l2: number): number { + if (l1 < l2) { + return (l2 + 0.05) / (l1 + 0.05); + } + return (l1 + 0.05) / (l2 + 0.05); +} + +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)) { + // Increase by 10% (ceil) 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; +} + +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.floor((255 - fgR) * 0.1)); + fgG = Math.min(0xFF, fgG + Math.floor((255 - fgG) * 0.1)); + fgB = Math.min(0xFF, fgB + Math.floor((255 - fgB) * 0.1)); + cr = contrastRatio(rgbRelativeLuminance2(fgR, fgB, fgG), rgbRelativeLuminance2(bgR, bgG, bgB)); + } + return fgR << 24 | fgG << 16 | fgB << 8 | 0xFF; +} diff --git a/src/browser/Color.ts b/src/browser/Color.ts index c74e8745..c63ccee3 100644 --- a/src/browser/Color.ts +++ b/src/browser/Color.ts @@ -86,28 +86,47 @@ export function contrastRatio(l1: number, l2: number): number { // TODO: Cache [bg][fg]: result, should probably be owned by ColorManager? -export function ensureContrastRatio(bg: IColor, fg: IColor, ratio: number): IColor | undefined { - const bgL = rgbRelativeLuminance(bg.rgba >> 8); - const fgL = rgbRelativeLuminance(fg.rgba >> 8); +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(bg, fg, ratio); + return reduceLuminance(bgRgba, fgRgba, ratio); } - return increaseLuminance(bg, fg, ratio); + return increaseLuminance(bgRgba, fgRgba, ratio); } return undefined; } -export function reduceLuminance(bg: IColor, fg: IColor, ratio: number): IColor { +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 = (bg.rgba >> 24) & 0xFF; - const bgG = (bg.rgba >> 16) & 0xFF; - const bgB = (bg.rgba >> 8) & 0xFF; - let fgR = (fg.rgba >> 24) & 0xFF; - let fgG = (fg.rgba >> 16) & 0xFF; - let fgB = (fg.rgba >> 8) & 0xFF; + 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)) { // Increase by 10% (ceil) until the ratio is hit @@ -116,21 +135,18 @@ export function reduceLuminance(bg: IColor, fg: IColor, ratio: number): IColor { fgB -= Math.max(0, Math.ceil(fgB * 0.1)); cr = contrastRatio(rgbRelativeLuminance2(fgR, fgB, fgG), rgbRelativeLuminance2(bgR, bgG, bgB)); } - return { - css: toCss(fgR, fgG, fgB), - rgba: toRgba(fgR, fgG, fgB) - }; + return fgR << 24 | fgG << 16 | fgB << 8 | 0xFF; } -export function increaseLuminance(bg: IColor, fg: IColor, ratio: number): IColor { +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 = (bg.rgba >> 24) & 0xFF; - const bgG = (bg.rgba >> 16) & 0xFF; - const bgB = (bg.rgba >> 8) & 0xFF; - let fgR = (fg.rgba >> 24) & 0xFF; - let fgG = (fg.rgba >> 16) & 0xFF; - let fgB = (fg.rgba >> 8) & 0xFF; + 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 @@ -139,8 +155,5 @@ export function increaseLuminance(bg: IColor, fg: IColor, ratio: number): IColor fgB = Math.min(0xFF, fgB + Math.floor((255 - fgB) * 0.1)); cr = contrastRatio(rgbRelativeLuminance2(fgR, fgB, fgG), rgbRelativeLuminance2(bgR, bgG, bgB)); } - return { - css: toCss(fgR, fgG, fgB), - rgba: toRgba(fgR, fgG, fgB) - }; + return fgR << 24 | fgG << 16 | fgB << 8 | 0xFF; } From 9b139a744b4d4862dda4e66b924210ed7ec4d0c8 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 11 Nov 2019 22:55:08 -0800 Subject: [PATCH 011/103] Add test for min contrast on white bg --- .../src/WebglRenderer.api.ts | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.api.ts b/addons/xterm-addon-webgl/src/WebglRenderer.api.ts index 25719faa..998d5536 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.api.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.api.ts @@ -631,6 +631,70 @@ describe('WebGL Renderer Integration Tests', function(): void { await pollFor(page, () => getCellColor(7, 2), [0x34, 0xe2, 0xe2, 255]); await pollFor(page, () => getCellColor(8, 2), [0xee, 0xee, 0xec, 255]); }); + + it.only('should adjust 0-15 colors on white background', async () => { + const theme: ITheme = { + background: '#ffffff', + black: '#2e3436', + red: '#cc0000', + green: '#4e9a06', + yellow: '#c4a000', + blue: '#3465a4', + magenta: '#75507b', + cyan: '#06989a', + white: '#d3d7cf', + brightBlack: '#555753', + brightRed: '#ef2929', + brightGreen: '#8ae234', + brightYellow: '#fce94f', + brightBlue: '#729fcf', + brightMagenta: '#ad7fa8', + brightCyan: '#34e2e2', + brightWhite: '#eeeeec' + }; + await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`); + await writeSync( + `\\x1b[30m█\\x1b[31m█\\x1b[32m█\\x1b[33m█\\x1b[34m█\\x1b[35m█\\x1b[36m█\\x1b[37m█\\r\\n` + + `\\x1b[90m█\\x1b[91m█\\x1b[92m█\\x1b[93m█\\x1b[94m█\\x1b[95m█\\x1b[96m█\\x1b[97m█` + ); + // Validate before minimumContrastRatio is applied + await pollFor(page, () => getCellColor(1, 1), [0x2e, 0x34, 0x36, 255]); + await pollFor(page, () => getCellColor(2, 1), [0xcc, 0x00, 0x00, 255]); + await pollFor(page, () => getCellColor(3, 1), [0x4e, 0x9a, 0x06, 255]); + await pollFor(page, () => getCellColor(4, 1), [0xc4, 0xa0, 0x00, 255]); + await pollFor(page, () => getCellColor(5, 1), [0x34, 0x65, 0xa4, 255]); + await pollFor(page, () => getCellColor(6, 1), [0x75, 0x50, 0x7b, 255]); + await pollFor(page, () => getCellColor(7, 1), [0x06, 0x98, 0x9a, 255]); + await pollFor(page, () => getCellColor(8, 1), [0xd3, 0xd7, 0xcf, 255]); + await pollFor(page, () => getCellColor(1, 2), [0x55, 0x57, 0x53, 255]); + await pollFor(page, () => getCellColor(2, 2), [0xef, 0x29, 0x29, 255]); + await pollFor(page, () => getCellColor(3, 2), [0x8a, 0xe2, 0x34, 255]); + await pollFor(page, () => getCellColor(4, 2), [0xfc, 0xe9, 0x4f, 255]); + await pollFor(page, () => getCellColor(5, 2), [0x72, 0x9f, 0xcf, 255]); + await pollFor(page, () => getCellColor(6, 2), [0xad, 0x7f, 0xa8, 255]); + await pollFor(page, () => getCellColor(7, 2), [0x34, 0xe2, 0xe2, 255]); + await pollFor(page, () => getCellColor(8, 2), [0xee, 0xee, 0xec, 255]); + // Setting and check for minimum contrast values, note that these are note + // exact to the contrast ratio, if the increase luminance algorithm + // changes then these will probably fail + await page.evaluate(`window.term.setOption('minimumContrastRatio', 10);`); + await pollFor(page, () => getCellColor(1, 1), [46, 52, 54, 255]); + await pollFor(page, () => getCellColor(2, 1), [132, 0, 0, 255]); + await pollFor(page, () => getCellColor(3, 1), [78, 154, 6, 255]); + await pollFor(page, () => getCellColor(4, 1), [114, 93, 0, 255]); + await pollFor(page, () => getCellColor(5, 1), [19, 40, 68, 255]); + await pollFor(page, () => getCellColor(6, 1), [60, 40, 64, 255]); + await pollFor(page, () => getCellColor(7, 1), [0, 71, 72, 255]); + await pollFor(page, () => getCellColor(8, 1), [64, 64, 63, 255]); + await pollFor(page, () => getCellColor(1, 2), [61, 63, 59, 255]); + await pollFor(page, () => getCellColor(2, 2), [125, 19, 19, 255]); + await pollFor(page, () => getCellColor(3, 2), [89, 146, 32, 255]); + await pollFor(page, () => getCellColor(4, 2), [105, 98, 32, 255]); + await pollFor(page, () => getCellColor(5, 2), [36, 52, 70, 255]); + await pollFor(page, () => getCellColor(6, 2), [64, 45, 63, 255]); + await pollFor(page, () => getCellColor(7, 2), [13, 67, 67, 255]); + await pollFor(page, () => getCellColor(8, 2), [64, 64, 64, 255]); + }); }); }); From 2fc1791dde2bc26e23ada463c45319410016b831 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 11 Nov 2019 23:11:36 -0800 Subject: [PATCH 012/103] Fix floor/ceil in luminance methods --- .../src/atlas/WebglCharAtlas.ts | 27 ++++++++++--------- src/browser/Color.ts | 14 +++++----- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index f6691067..60068de8 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -208,7 +208,7 @@ export class WebglCharAtlas implements IDisposable { } private _getForegroundCss(bgColorMode: number, bgColor: number, fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean): string { - const minimumContrastCss = this._getMinimumContrastCss(bgColorMode, bgColor, fgColorMode, fgColor, inverse); + const minimumContrastCss = this._getMinimumContrastCss(bgColorMode, bgColor, fgColorMode, fgColor, inverse, bold); if (minimumContrastCss) { return minimumContrastCss; } @@ -248,10 +248,13 @@ export class WebglCharAtlas implements IDisposable { } } - private _resolveForegroundRgba(bgColorMode: number, bgColor: number, fgColorMode: number, fgColor: number, inverse: boolean): number { + private _resolveForegroundRgba(bgColorMode: number, bgColor: number, fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean): number { switch (fgColorMode) { case Attributes.CM_P16: case Attributes.CM_P256: + if (this._config.drawBoldTextInBrightColors && bold && fgColor < 8) { + fgColor += 8; + } return this._getColorFromAnsiIndex(fgColor).rgba; case Attributes.CM_RGB: return fgColor << 8; @@ -264,15 +267,13 @@ export class WebglCharAtlas implements IDisposable { } } - private _getMinimumContrastCss(bgColorMode: number, bgColor: number, fgColorMode: number, fgColor: number, inverse: boolean): string | undefined { + private _getMinimumContrastCss(bgColorMode: number, bgColor: number, fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean): string | undefined { const bgRgba = this._resolveBackgroundRgba(bgColorMode, bgColor, fgColorMode, fgColor, inverse); - const fgRgba = this._resolveForegroundRgba(bgColorMode, bgColor, fgColorMode, fgColor, inverse); - console.log('ratio', this._config.minimumContrastRatio); + const fgRgba = this._resolveForegroundRgba(bgColorMode, bgColor, fgColorMode, fgColor, inverse, bold); if (this._config.minimumContrastRatio === 1) { return undefined; } const result = ensureContrastRatioRgba(bgRgba, fgRgba, this._config.minimumContrastRatio); - console.log('get min', result, bgRgba, fgRgba); if (!result) { return undefined; } @@ -618,10 +619,10 @@ export function reduceLuminance(bgRgba: number, fgRgba: number, ratio: number): 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)) { - // Increase by 10% (ceil) 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)); + // Reduce by 10% until the ratio is hit + fgR -= Math.max(0, Math.floor(fgR * 0.1)); + fgG -= Math.max(0, Math.floor(fgG * 0.1)); + fgB -= Math.max(0, Math.floor(fgB * 0.1)); cr = contrastRatio(rgbRelativeLuminance2(fgR, fgB, fgG), rgbRelativeLuminance2(bgR, bgG, bgB)); } return fgR << 24 | fgG << 16 | fgB << 8 | 0xFF; @@ -639,9 +640,9 @@ export function increaseLuminance(bgRgba: number, fgRgba: number, ratio: number) 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.floor((255 - fgR) * 0.1)); - fgG = Math.min(0xFF, fgG + Math.floor((255 - fgG) * 0.1)); - fgB = Math.min(0xFF, fgB + Math.floor((255 - fgB) * 0.1)); + 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; diff --git a/src/browser/Color.ts b/src/browser/Color.ts index c63ccee3..258ea45a 100644 --- a/src/browser/Color.ts +++ b/src/browser/Color.ts @@ -129,10 +129,10 @@ export function reduceLuminance(bgRgba: number, fgRgba: number, ratio: number): 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)) { - // Increase by 10% (ceil) 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)); + // Reduce by 10% until the ratio is hit + fgR -= Math.max(0, Math.floor(fgR * 0.1)); + fgG -= Math.max(0, Math.floor(fgG * 0.1)); + fgB -= Math.max(0, Math.floor(fgB * 0.1)); cr = contrastRatio(rgbRelativeLuminance2(fgR, fgB, fgG), rgbRelativeLuminance2(bgR, bgG, bgB)); } return fgR << 24 | fgG << 16 | fgB << 8 | 0xFF; @@ -150,9 +150,9 @@ export function increaseLuminance(bgRgba: number, fgRgba: number, ratio: number) 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.floor((255 - fgR) * 0.1)); - fgG = Math.min(0xFF, fgG + Math.floor((255 - fgG) * 0.1)); - fgB = Math.min(0xFF, fgB + Math.floor((255 - fgB) * 0.1)); + 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; From 6626f9628d22ba9e41efc033ac34eff7e7c77dee Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 11 Nov 2019 23:13:21 -0800 Subject: [PATCH 013/103] Use ceil in both --- addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts | 6 +++--- src/browser/Color.ts | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index 60068de8..1642ce08 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -620,9 +620,9 @@ export function reduceLuminance(bgRgba: number, fgRgba: number, ratio: number): 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.floor(fgR * 0.1)); - fgG -= Math.max(0, Math.floor(fgG * 0.1)); - fgB -= Math.max(0, Math.floor(fgB * 0.1)); + 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; diff --git a/src/browser/Color.ts b/src/browser/Color.ts index 258ea45a..3a2b5032 100644 --- a/src/browser/Color.ts +++ b/src/browser/Color.ts @@ -130,9 +130,9 @@ export function reduceLuminance(bgRgba: number, fgRgba: number, ratio: number): 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.floor(fgR * 0.1)); - fgG -= Math.max(0, Math.floor(fgG * 0.1)); - fgB -= Math.max(0, Math.floor(fgB * 0.1)); + 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; From 5ee9bcf2b58e2d21291471c31c83cd76adfa9187 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 11 Nov 2019 23:23:33 -0800 Subject: [PATCH 014/103] 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 998d5536..88aa3b45 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.api.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.api.ts @@ -632,7 +632,7 @@ describe('WebGL Renderer Integration Tests', function(): void { await pollFor(page, () => getCellColor(8, 2), [0xee, 0xee, 0xec, 255]); }); - it.only('should adjust 0-15 colors on white background', async () => { + it('should adjust 0-15 colors on white background', async () => { const theme: ITheme = { background: '#ffffff', black: '#2e3436', From ca5cee7e8625ec7ed0dfd0bdf642513d32372f6d Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 12 Nov 2019 08:53:06 -0800 Subject: [PATCH 015/103] Fix contrast tests --- .../src/WebglRenderer.api.ts | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.api.ts b/addons/xterm-addon-webgl/src/WebglRenderer.api.ts index 88aa3b45..72f65c97 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.api.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.api.ts @@ -8,6 +8,7 @@ import { ITerminalOptions } from '../../../src/Types'; import { ITheme } from 'xterm'; import { assert } from 'chai'; import deepEqual = require('deep-equal'); +import { contrastRatio, reduceLuminance, toRgba } from 'atlas/WebglCharAtlas'; const APP = 'http://127.0.0.1:3000/test'; @@ -613,20 +614,20 @@ describe('WebGL Renderer Integration Tests', function(): void { // exact to the contrast ratio, if the increase luminance algorithm // changes then these will probably fail await page.evaluate(`window.term.setOption('minimumContrastRatio', 10);`); - await pollFor(page, () => getCellColor(1, 1), [179, 182, 182, 255]); - await pollFor(page, () => getCellColor(2, 1), [234, 163, 163, 255]); - await pollFor(page, () => getCellColor(3, 1), [196, 221, 174, 255]); - await pollFor(page, () => getCellColor(4, 1), [231, 219, 163, 255]); - await pollFor(page, () => getCellColor(5, 1), [133, 162, 200, 255]); - await pollFor(page, () => getCellColor(6, 1), [180, 159, 182, 255]); - await pollFor(page, () => getCellColor(7, 1), [106, 192, 194, 255]); + await pollFor(page, () => getCellColor(1, 1), [176, 180, 180, 255]); + await pollFor(page, () => getCellColor(2, 1), [238, 158, 158, 255]); + await pollFor(page, () => getCellColor(3, 1), [197, 223, 171, 255]); + await pollFor(page, () => getCellColor(4, 1), [235, 221, 158, 255]); + await pollFor(page, () => getCellColor(5, 1), [124, 156, 198, 255]); + await pollFor(page, () => getCellColor(6, 1), [183, 165, 187, 255]); + await pollFor(page, () => getCellColor(7, 1), [110, 197, 198, 255]); await pollFor(page, () => getCellColor(8, 1), [211, 215, 207, 255]); - await pollFor(page, () => getCellColor(1, 2), [180, 181, 179, 255]); - await pollFor(page, () => getCellColor(2, 2), [246, 160, 160, 255]); + await pollFor(page, () => getCellColor(1, 2), [183, 185, 183, 255]); + await pollFor(page, () => getCellColor(2, 2), [249, 156, 156, 255]); await pollFor(page, () => getCellColor(3, 2), [138, 226, 52, 255]); await pollFor(page, () => getCellColor(4, 2), [252, 233, 79, 255]); await pollFor(page, () => getCellColor(5, 2), [114, 159, 207, 255]); - await pollFor(page, () => getCellColor(6, 2), [188, 150, 183, 255]); + await pollFor(page, () => getCellColor(6, 2), [190, 152, 185, 255]); // Unchanged await pollFor(page, () => getCellColor(7, 2), [0x34, 0xe2, 0xe2, 255]); await pollFor(page, () => getCellColor(8, 2), [0xee, 0xee, 0xec, 255]); @@ -748,6 +749,7 @@ export async function pollFor(page: puppeteer.Page, evalOrFn: string | (() => await preFn(); } const result = typeof evalOrFn === 'string' ? await page.evaluate(evalOrFn) : await evalOrFn(); + console.log(result); if (!deepEqual(result, val)) { return new Promise(r => { setTimeout(() => r(pollFor(page, evalOrFn, val, preFn)), 1); From c2474626f0678402e9e572ada2a5921af19a06ce Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 12 Nov 2019 09:07:08 -0800 Subject: [PATCH 016/103] Clear setting as part of run --- addons/xterm-addon-webgl/src/WebglRenderer.api.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.api.ts b/addons/xterm-addon-webgl/src/WebglRenderer.api.ts index 72f65c97..26e966a0 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.api.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.api.ts @@ -8,7 +8,6 @@ import { ITerminalOptions } from '../../../src/Types'; import { ITheme } from 'xterm'; import { assert } from 'chai'; import deepEqual = require('deep-equal'); -import { contrastRatio, reduceLuminance, toRgba } from 'atlas/WebglCharAtlas'; const APP = 'http://127.0.0.1:3000/test'; @@ -588,7 +587,10 @@ describe('WebGL Renderer Integration Tests', function(): void { brightCyan: '#34e2e2', brightWhite: '#eeeeec' }; - await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`); + await page.evaluate(` + window.term.setOption('theme', ${JSON.stringify(theme)}); + window.term.setOption('minimumContrastRatio', 1); + `); await writeSync( `\\x1b[30m█\\x1b[31m█\\x1b[32m█\\x1b[33m█\\x1b[34m█\\x1b[35m█\\x1b[36m█\\x1b[37m█\\r\\n` + `\\x1b[90m█\\x1b[91m█\\x1b[92m█\\x1b[93m█\\x1b[94m█\\x1b[95m█\\x1b[96m█\\x1b[97m█` @@ -653,7 +655,10 @@ describe('WebGL Renderer Integration Tests', function(): void { brightCyan: '#34e2e2', brightWhite: '#eeeeec' }; - await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`); + await page.evaluate(` + window.term.setOption('theme', ${JSON.stringify(theme)}); + window.term.setOption('minimumContrastRatio', 1); + `); await writeSync( `\\x1b[30m█\\x1b[31m█\\x1b[32m█\\x1b[33m█\\x1b[34m█\\x1b[35m█\\x1b[36m█\\x1b[37m█\\r\\n` + `\\x1b[90m█\\x1b[91m█\\x1b[92m█\\x1b[93m█\\x1b[94m█\\x1b[95m█\\x1b[96m█\\x1b[97m█` From faaa07be68849e0a85dc17181900f904d15a1ba9 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 12 Nov 2019 09:33:50 -0800 Subject: [PATCH 017/103] Add contrast caching for DOM renderer --- .../src/atlas/CharAtlasUtils.ts | 3 +- src/Terminal.ts | 1 + src/browser/ColorContrastCache.ts | 38 +++++++++++++++++++ src/browser/ColorManager.ts | 16 +++++++- src/browser/Types.d.ts | 10 +++++ .../renderer/dom/DomRendererRowFactory.ts | 14 +++++-- 6 files changed, 76 insertions(+), 6 deletions(-) create mode 100644 src/browser/ColorContrastCache.ts diff --git a/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts b/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts index e8e19095..ad25bf87 100644 --- a/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts +++ b/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts @@ -24,7 +24,8 @@ export function generateConfig(scaledCharWidth: number, scaledCharHeight: number selectionOpaque: NULL_COLOR, // For the static char atlas, we only use the first 16 colors, but we need all 256 for the // dynamic character atlas. - ansi: colors.ansi.slice() + ansi: colors.ansi.slice(), + contrastCache: {} as any }; return { devicePixelRatio: window.devicePixelRatio, diff --git a/src/Terminal.ts b/src/Terminal.ts index b08270fd..927ff47b 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -546,6 +546,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this._theme = this.options.theme || this._theme; this.options.theme = undefined; this._colorManager = new ColorManager(document, this.options.allowTransparency); + this.optionsService.onOptionChange(e => this._colorManager.onOptionsChange(e)); this._colorManager.setTheme(this._theme); const renderer = this._createRenderer(); diff --git a/src/browser/ColorContrastCache.ts b/src/browser/ColorContrastCache.ts new file mode 100644 index 00000000..3198ad83 --- /dev/null +++ b/src/browser/ColorContrastCache.ts @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IColor, IColorContrastCache } from 'browser/Types'; + +export class ColorContrastCache implements IColorContrastCache { + private _color: { [bg: number]: { [fg: number]: IColor | null | undefined } | undefined } = {}; + private _rgba: { [bg: number]: { [fg: number]: number | null | undefined } | undefined } = {}; + + public clear(): void { + this._color = {}; + this._rgba = {}; + } + + public setRgba(bg: number, fg: number, value: number | null): void { + if (!this._rgba[bg]) { + this._rgba[bg] = {}; + } + this._rgba[bg]![fg] = value; + } + + public getRgba(bg: number, fg: number): number | null | undefined { + return this._rgba[bg] ? this._rgba[bg]![fg] : undefined; + } + + public setColor(bg: number, fg: number, value: IColor | null): void { + if (!this._color[bg]) { + this._color[bg] = {}; + } + this._color[bg]![fg] = value; + } + + public getColor(bg: number, fg: number): IColor | null | undefined { + return this._color[bg] ? this._color[bg]![fg] : undefined; + } +} diff --git a/src/browser/ColorManager.ts b/src/browser/ColorManager.ts index c6354f71..92994cfc 100644 --- a/src/browser/ColorManager.ts +++ b/src/browser/ColorManager.ts @@ -3,9 +3,10 @@ * @license MIT */ -import { IColorManager, IColor, IColorSet } from 'browser/Types'; +import { IColorManager, IColor, IColorSet, IColorContrastCache } from 'browser/Types'; import { ITheme } from 'common/services/Services'; import { fromCss, toCss, blend, toRgba } from 'browser/Color'; +import { ColorContrastCache } from 'browser/ColorContrastCache'; const DEFAULT_FOREGROUND = fromCss('#ffffff'); const DEFAULT_BACKGROUND = fromCss('#000000'); @@ -72,6 +73,7 @@ export class ColorManager implements IColorManager { public colors: IColorSet; private _ctx: CanvasRenderingContext2D; private _litmusColor: CanvasGradient; + private _contrastCache: IColorContrastCache; constructor(document: Document, public allowTransparency: boolean) { const canvas = document.createElement('canvas'); @@ -84,6 +86,7 @@ export class ColorManager implements IColorManager { this._ctx = ctx; this._ctx.globalCompositeOperation = 'copy'; this._litmusColor = this._ctx.createLinearGradient(0, 0, 1, 1); + this._contrastCache = new ColorContrastCache(); this.colors = { foreground: DEFAULT_FOREGROUND, background: DEFAULT_BACKGROUND, @@ -91,10 +94,17 @@ export class ColorManager implements IColorManager { cursorAccent: DEFAULT_CURSOR_ACCENT, selection: DEFAULT_SELECTION, selectionOpaque: blend(DEFAULT_BACKGROUND, DEFAULT_SELECTION), - ansi: DEFAULT_ANSI_COLORS.slice() + ansi: DEFAULT_ANSI_COLORS.slice(), + contrastCache: this._contrastCache }; } + public onOptionsChange(key: string): void { + if (key === 'minimumContrastRatio') { + this._contrastCache.clear(); + } + } + /** * Sets the terminal's theme. * @param theme The theme to use. If a partial theme is provided then default @@ -123,6 +133,8 @@ export class ColorManager implements IColorManager { this.colors.ansi[13] = this._parseColor(theme.brightMagenta, DEFAULT_ANSI_COLORS[13]); this.colors.ansi[14] = this._parseColor(theme.brightCyan, DEFAULT_ANSI_COLORS[14]); this.colors.ansi[15] = this._parseColor(theme.brightWhite, DEFAULT_ANSI_COLORS[15]); + // Clear our the cache + this._contrastCache.clear(); } private _parseColor( diff --git a/src/browser/Types.d.ts b/src/browser/Types.d.ts index ca852b6a..06c3fe38 100644 --- a/src/browser/Types.d.ts +++ b/src/browser/Types.d.ts @@ -8,6 +8,7 @@ import { IDisposable } from 'common/Types'; export interface IColorManager { colors: IColorSet; + onOptionsChange(key: string): void; } export interface IColor { @@ -24,6 +25,15 @@ export interface IColorSet { /** The selection blended on top of background. */ selectionOpaque: IColor; ansi: IColor[]; + contrastCache: IColorContrastCache +} + +export interface IColorContrastCache { + clear(): void; + setRgba(bg: number, fg: number, value: number | null): void; + getRgba(bg: number, fg: number): number | null | undefined; + setColor(bg: number, fg: number, value: IColor | null): void; + getColor(bg: number, fg: number): IColor | null | undefined; } export interface IPartialColorSet { diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts index b5caccaf..bb96fb72 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.ts @@ -167,9 +167,17 @@ export class DomRendererRowFactory { return false; } - const adustedColor = ensureContrastRatio(bg, fg, this._optionsService.options.minimumContrastRatio); - if (adustedColor) { - element.setAttribute('style', `${element.getAttribute('style') || ''}color:${adustedColor.css}`); + // Try get from cache first + let adjustedColor = this._colors.contrastCache.getColor(this._workCell.bg, this._workCell.fg); + + // Calculate and store in cache + if (adjustedColor === undefined) { + adjustedColor = ensureContrastRatio(bg, fg, this._optionsService.options.minimumContrastRatio); + this._colors.contrastCache.setColor(this._workCell.bg, this._workCell.fg, adjustedColor ?? null); + } + + if (adjustedColor) { + element.setAttribute('style', `${element.getAttribute('style') || ''}color:${adjustedColor.css}`); return true; } From 8a0122f482a2c9a63dcb535be8d9f29e596e1e5f Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 12 Nov 2019 09:42:55 -0800 Subject: [PATCH 018/103] Add contrast caching to webgl --- .../src/atlas/CharAtlasUtils.ts | 2 +- .../src/atlas/WebglCharAtlas.ts | 27 ++++++++++++++----- src/browser/ColorContrastCache.ts | 6 ++--- src/browser/Types.d.ts | 4 +-- 4 files changed, 26 insertions(+), 13 deletions(-) diff --git a/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts b/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts index ad25bf87..8e26b1d6 100644 --- a/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts +++ b/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts @@ -25,7 +25,7 @@ export function generateConfig(scaledCharWidth: number, scaledCharHeight: number // For the static char atlas, we only use the first 16 colors, but we need all 256 for the // dynamic character atlas. ansi: colors.ansi.slice(), - contrastCache: {} as any + contrastCache: colors.contrastCache }; return { devicePixelRatio: window.devicePixelRatio, diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index 1642ce08..b40c27cc 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -207,8 +207,8 @@ export class WebglCharAtlas implements IDisposable { } } - private _getForegroundCss(bgColorMode: number, bgColor: number, fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean): string { - const minimumContrastCss = this._getMinimumContrastCss(bgColorMode, bgColor, fgColorMode, fgColor, inverse, bold); + private _getForegroundCss(bg: number, bgColorMode: number, bgColor: number, fg: number, fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean): string { + const minimumContrastCss = this._getMinimumContrastCss(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, bold); if (minimumContrastCss) { return minimumContrastCss; } @@ -267,21 +267,34 @@ export class WebglCharAtlas implements IDisposable { } } - private _getMinimumContrastCss(bgColorMode: number, bgColor: number, fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean): string | undefined { - const bgRgba = this._resolveBackgroundRgba(bgColorMode, bgColor, fgColorMode, fgColor, inverse); - const fgRgba = this._resolveForegroundRgba(bgColorMode, bgColor, fgColorMode, fgColor, inverse, bold); + private _getMinimumContrastCss(bg: number, bgColorMode: number, bgColor: number, fg: number, fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean): string | undefined { if (this._config.minimumContrastRatio === 1) { return undefined; } + + // Try get from cache first + const adjustedColor = this._config.colors.contrastCache.getCss(bg, fg); + if (adjustedColor !== undefined) { + return adjustedColor || undefined; + } + + const bgRgba = this._resolveBackgroundRgba(bgColorMode, bgColor, fgColorMode, fgColor, inverse); + const fgRgba = this._resolveForegroundRgba(bgColorMode, bgColor, fgColorMode, fgColor, inverse, bold); const result = ensureContrastRatioRgba(bgRgba, fgRgba, this._config.minimumContrastRatio); + if (!result) { + this._config.colors.contrastCache.setCss(bg, fg, null); return undefined; } - return toCss( + + const css = toCss( (result >> 24) & 0xFF, (result >> 16) & 0xFF, (result >> 8) & 0xFF ); + this._config.colors.contrastCache.setCss(bg, fg, css); + + return css; } private _drawToCache(code: number, bg: number, fg: number): IRasterizedGlyph; @@ -327,7 +340,7 @@ export class WebglCharAtlas implements IDisposable { `${fontStyle} ${fontWeight} ${this._config.fontSize * this._config.devicePixelRatio}px ${this._config.fontFamily}`; this._tmpCtx.textBaseline = 'top'; - this._tmpCtx.fillStyle = this._getForegroundCss(bgColorMode, bgColor, fgColorMode, fgColor, inverse, bold); + this._tmpCtx.fillStyle = this._getForegroundCss(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, bold); // Apply alpha to dim the character if (dim) { diff --git a/src/browser/ColorContrastCache.ts b/src/browser/ColorContrastCache.ts index 3198ad83..b96b66cc 100644 --- a/src/browser/ColorContrastCache.ts +++ b/src/browser/ColorContrastCache.ts @@ -7,21 +7,21 @@ import { IColor, IColorContrastCache } from 'browser/Types'; export class ColorContrastCache implements IColorContrastCache { private _color: { [bg: number]: { [fg: number]: IColor | null | undefined } | undefined } = {}; - private _rgba: { [bg: number]: { [fg: number]: number | null | undefined } | undefined } = {}; + private _rgba: { [bg: number]: { [fg: number]: string | null | undefined } | undefined } = {}; public clear(): void { this._color = {}; this._rgba = {}; } - public setRgba(bg: number, fg: number, value: number | null): void { + public setCss(bg: number, fg: number, value: string | null): void { if (!this._rgba[bg]) { this._rgba[bg] = {}; } this._rgba[bg]![fg] = value; } - public getRgba(bg: number, fg: number): number | null | undefined { + public getCss(bg: number, fg: number): string | null | undefined { return this._rgba[bg] ? this._rgba[bg]![fg] : undefined; } diff --git a/src/browser/Types.d.ts b/src/browser/Types.d.ts index 06c3fe38..89c72fa3 100644 --- a/src/browser/Types.d.ts +++ b/src/browser/Types.d.ts @@ -30,8 +30,8 @@ export interface IColorSet { export interface IColorContrastCache { clear(): void; - setRgba(bg: number, fg: number, value: number | null): void; - getRgba(bg: number, fg: number): number | null | undefined; + setCss(bg: number, fg: number, value: string | null): void; + getCss(bg: number, fg: number): string | null | undefined; setColor(bg: number, fg: number, value: IColor | null): void; getColor(bg: number, fg: number): IColor | null | undefined; } From 3b0042fd46df81b44f4fe16a8068034502b9e934 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 12 Nov 2019 09:46:07 -0800 Subject: [PATCH 019/103] Remove log --- addons/xterm-addon-webgl/src/WebglRenderer.api.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.api.ts b/addons/xterm-addon-webgl/src/WebglRenderer.api.ts index 26e966a0..e26e054a 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.api.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.api.ts @@ -754,7 +754,6 @@ export async function pollFor(page: puppeteer.Page, evalOrFn: string | (() => await preFn(); } const result = typeof evalOrFn === 'string' ? await page.evaluate(evalOrFn) : await evalOrFn(); - console.log(result); if (!deepEqual(result, val)) { return new Promise(r => { setTimeout(() => r(pollFor(page, evalOrFn, val, preFn)), 1); From 647cff53dbd9c4f964b87b763b5ea97a4158751d Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 12 Nov 2019 09:53:42 -0800 Subject: [PATCH 020/103] Lint --- src/browser/Types.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browser/Types.d.ts b/src/browser/Types.d.ts index 89c72fa3..31d00af8 100644 --- a/src/browser/Types.d.ts +++ b/src/browser/Types.d.ts @@ -25,7 +25,7 @@ export interface IColorSet { /** The selection blended on top of background. */ selectionOpaque: IColor; ansi: IColor[]; - contrastCache: IColorContrastCache + contrastCache: IColorContrastCache; } export interface IColorContrastCache { From b3f439af092c1adaf4a1695be44f7a5b8e5753dd Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 12 Nov 2019 09:55:09 -0800 Subject: [PATCH 021/103] Remove unused imports --- addons/xterm-addon-webgl/src/WebglRenderer.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index 3a71dcd8..46fd2730 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -11,10 +11,9 @@ import { acquireCharAtlas } from './atlas/CharAtlasCache'; import { WebglCharAtlas } from './atlas/WebglCharAtlas'; import { RectangleRenderer } from './RectangleRenderer'; import { IWebGL2RenderingContext } from './Types'; -import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants'; import { RenderModel, COMBINED_CHAR_BIT_MASK, RENDER_MODEL_BG_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel'; import { Disposable } from 'common/Lifecycle'; -import { DEFAULT_COLOR, NULL_CELL_CODE, FgFlags } from 'common/buffer/Constants'; +import { NULL_CELL_CODE } from 'common/buffer/Constants'; import { Terminal, IEvent } from 'xterm'; import { IRenderLayer } from './renderLayer/Types'; import { IRenderDimensions, IRenderer, IRequestRefreshRowsEvent } from 'browser/renderer/Types'; From 032185f8b06a6289375e4e899a99638b29d97e9b Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 12 Nov 2019 10:00:40 -0800 Subject: [PATCH 022/103] Link webgl directly to browser/Color --- .../src/atlas/WebglCharAtlas.ts | 118 +----------------- 1 file changed, 1 insertion(+), 117 deletions(-) diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index b40c27cc..e1b0474f 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -11,6 +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'; // 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. @@ -543,120 +544,3 @@ function getBgColor(bg: number): number { default: return -1; // CM_DEFAULT defaults to -1 } } - -export function toCss(r: number, g: number, b: number): string { - 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; -} - -/** - * 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); -} - -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. - * @param l2 The first relative luminance. - * @see https://www.w3.org/TR/WCAG20/#contrast-ratiodef - */ -export function contrastRatio(l1: number, l2: number): number { - if (l1 < l2) { - return (l2 + 0.05) / (l1 + 0.05); - } - return (l1 + 0.05) / (l2 + 0.05); -} - -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; -} - -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; -} From b0c3606207f043f234de51860d5a94fbee5b9481 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 12 Nov 2019 10:03:38 -0800 Subject: [PATCH 023/103] Add example values to .d.ts --- typings/xterm.d.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index b49ed04b..03f89e0c 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -151,8 +151,12 @@ declare module 'xterm' { /** * The minimum contrast ratio for text in the terminal, setting this will * change the foreground color dynamically depending on whether the contrast - * ratio is met. This can be to set 0.1 increments between 1 (default, do - * nothing) and 21 (foreground will be black or white). + * ratio is met. Example values: + * + * - 1: The default, do nothing. + * - 4.5: Minimum for WCAG AA compliance. + * - 7: Minimum for WCAG AAA compliance. + * - 21: White on black or black on white. */ minimumContrastRatio?: number; From 56d12fe84651a9a59b28b444c0debfb23a7c3010 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 12 Nov 2019 10:09:32 -0800 Subject: [PATCH 024/103] Fix color manager unit test --- src/browser/ColorManager.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browser/ColorManager.test.ts b/src/browser/ColorManager.test.ts index a213c616..39a8f8c2 100644 --- a/src/browser/ColorManager.test.ts +++ b/src/browser/ColorManager.test.ts @@ -34,7 +34,7 @@ describe('ColorManager', () => { describe('constructor', () => { it('should fill all colors with values', () => { for (const key of Object.keys(cm.colors)) { - if (key !== 'ansi') { + if (key !== 'ansi' && key !== 'contrastCache') { // A #rrggbb or rgba(...) assert.ok((cm.colors)[key].css.length >= 7); } From d1f7d2ef6acf9577ca4e2ca9556eaba9a55a8829 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 12 Nov 2019 10:10:55 -0800 Subject: [PATCH 025/103] Tweak readme to call out min contrast ratio --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d2944003..70ff7891 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Xterm.js is a front-end component written in TypeScript that lets applications b - **Performant**: Xterm.js is *really* fast, it even includes a GPU-accelerated renderer. - **Rich unicode support**: Supports CJK, emojis and IMEs. - **Self-contained**: Requires zero dependencies to work. -- **Accessible**: Screen reader support can be turned on using the `screenReaderMode` option. +- **Accessible**: Screen reader and minimum contrast ratio support can be turned on - **And much more**: Links, theming, addons, well documented API, etc. ## What xterm.js is not From db090c70de99e8623e38d01e44f5132a67a58dd4 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 12 Nov 2019 10:21:23 -0800 Subject: [PATCH 026/103] Add unit tests for ColorContrastCache --- src/browser/Color.ts | 2 -- src/browser/ColorContrastCache.test.ts | 43 ++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 src/browser/ColorContrastCache.test.ts diff --git a/src/browser/Color.ts b/src/browser/Color.ts index 3a2b5032..09c6a4f1 100644 --- a/src/browser/Color.ts +++ b/src/browser/Color.ts @@ -84,8 +84,6 @@ export function contrastRatio(l1: number, l2: number): number { return (l1 + 0.05) / (l2 + 0.05); } -// TODO: Cache [bg][fg]: result, should probably be owned by ColorManager? - function rgbaToColor(r: number, g: number, b: number): IColor { return { css: toCss(r, g, b), diff --git a/src/browser/ColorContrastCache.test.ts b/src/browser/ColorContrastCache.test.ts new file mode 100644 index 00000000..17e5df38 --- /dev/null +++ b/src/browser/ColorContrastCache.test.ts @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { assert } from 'chai'; +import { ColorContrastCache } from 'browser/ColorContrastCache'; + +describe('ColorContrastCache', () => { + let cache: ColorContrastCache; + + beforeEach(() => { + cache = new ColorContrastCache(); + }); + + it('should save and get color values', () => { + assert.strictEqual(cache.getColor(0x01, 0x00), undefined); + cache.setColor(0x01, 0x01, null); + assert.strictEqual(cache.getColor(0x01, 0x01), null); + cache.setColor(0x01, 0x02, { css: '#030303', rgba: 0x030303ff}); + assert.deepEqual(cache.getColor(0x01, 0x02), { css: '#030303', rgba: 0x030303ff}); + }); + + it('should save and get css values', () => { + assert.strictEqual(cache.getCss(0x01, 0x00), undefined); + cache.setCss(0x01, 0x01, null); + assert.strictEqual(cache.getCss(0x01, 0x01), null); + cache.setCss(0x01, 0x02, '#030303'); + assert.deepEqual(cache.getCss(0x01, 0x02), '#030303'); + }); + + it('should clear all values on clear', () => { + cache.setColor(0x01, 0x01, null); + cache.setColor(0x01, 0x02, { css: '#030303', rgba: 0x030303ff}); + cache.setCss(0x01, 0x01, null); + cache.setCss(0x01, 0x02, '#030303'); + cache.clear(); + assert.strictEqual(cache.getColor(0x01, 0x01), undefined); + assert.strictEqual(cache.getColor(0x01, 0x02), undefined); + assert.strictEqual(cache.getCss(0x01, 0x01), undefined); + assert.strictEqual(cache.getCss(0x01, 0x02), undefined); + }); +}); From 20b0a95bb0ef8decb76720f6187d1e8c8609a523 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 12 Nov 2019 10:40:35 -0800 Subject: [PATCH 027/103] Add tests for ensureContrastRatioRgba --- src/browser/Color.test.ts | 54 ++++++++++++++++++++++++++++++++++++++- src/browser/Color.ts | 4 +-- 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/src/browser/Color.test.ts b/src/browser/Color.test.ts index 975315c6..673fffc8 100644 --- a/src/browser/Color.test.ts +++ b/src/browser/Color.test.ts @@ -4,7 +4,7 @@ */ import { assert } from 'chai'; -import { blend, fromCss, toPaddedHex, toCss, toRgba, rgbRelativeLuminance, contrastRatio } from 'browser/Color'; +import { blend, fromCss, toPaddedHex, toCss, toRgba, rgbRelativeLuminance, contrastRatio, ensureContrastRatioRgba } from 'browser/Color'; describe('Color', () => { describe('blend', () => { @@ -167,4 +167,56 @@ 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 09c6a4f1..aaf5ea2e 100644 --- a/src/browser/Color.ts +++ b/src/browser/Color.ts @@ -133,7 +133,7 @@ export function reduceLuminance(bgRgba: number, fgRgba: number, ratio: number): 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; + return (fgR << 24 | fgG << 16 | fgB << 8 | 0xFF) >>> 0; } export function increaseLuminance(bgRgba: number, fgRgba: number, ratio: number): number { @@ -153,5 +153,5 @@ export function increaseLuminance(bgRgba: number, fgRgba: number, ratio: number) 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; + return (fgR << 24 | fgG << 16 | fgB << 8 | 0xFF) >>> 0; } From 2c959dfb2e3df7b6ecefa20d69af019702b774f2 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 13 Nov 2019 06:52:17 -0800 Subject: [PATCH 028/103] Keep a working AttributeData in WebglCharAtlas --- .../src/atlas/WebglCharAtlas.ts | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index e1b0474f..9910b009 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -68,6 +68,7 @@ export class WebglCharAtlas implements IDisposable { public hasCanvasChanged = false; private _workBoundingBox: IBoundingBox = { top: 0, left: 0, bottom: 0, right: 0 }; + private _workAttributeData: AttributeData = new AttributeData(); constructor( document: Document, @@ -305,17 +306,19 @@ export class WebglCharAtlas implements IDisposable { this.hasCanvasChanged = true; - const bold = !!(fg & FgFlags.BOLD); - const inverse = !!(fg & FgFlags.INVERSE); - const dim = !!(bg & BgFlags.DIM); - const italic = !!(bg & BgFlags.ITALIC); - this._tmpCtx.save(); - let fgColor = getFgColor(fg); - let fgColorMode = fg & Attributes.CM_MASK; - let bgColor = getBgColor(bg); - let bgColorMode = bg & Attributes.CM_MASK; + this._workAttributeData.fg = fg; + this._workAttributeData.bg = bg; + + const bold = !!this._workAttributeData.isBold(); + const inverse = !!this._workAttributeData.isInverse(); + const dim = !!this._workAttributeData.isDim(); + const italic = !!this._workAttributeData.isItalic(); + let fgColor = this._workAttributeData.getFgColor(); + let fgColorMode = this._workAttributeData.getFgColorMode(); + let bgColor = this._workAttributeData.getBgColor(); + let bgColorMode = this._workAttributeData.getBgColorMode(); if (inverse) { const temp = fgColor; fgColor = bgColor; From 579d74ee33b54c1db62d568ef71c7f0028f93ccd Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 13 Nov 2019 06:53:29 -0800 Subject: [PATCH 029/103] jsdoc rgbRelativeLuminance2 --- src/browser/Color.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/browser/Color.ts b/src/browser/Color.ts index aaf5ea2e..d4cbfe0e 100644 --- a/src/browser/Color.ts +++ b/src/browser/Color.ts @@ -61,6 +61,14 @@ export function rgbRelativeLuminance(rgb: number): number { (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; From 7652cd8af3a20d5466634fd417126db9f81e049c Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 13 Nov 2019 06:59:46 -0800 Subject: [PATCH 030/103] Use hash notation over rgb() for dom colors --- .../renderer/dom/DomRendererRowFactory.test.ts | 4 ++-- src/browser/renderer/dom/DomRendererRowFactory.ts | 11 +++++++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/browser/renderer/dom/DomRendererRowFactory.test.ts b/src/browser/renderer/dom/DomRendererRowFactory.test.ts index 180db0fc..c71945cf 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.test.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.test.ts @@ -212,7 +212,7 @@ describe('DomRendererRowFactory', () => { lineData.setCell(0, cell); const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), - 'a' + 'a' ); }); @@ -223,7 +223,7 @@ describe('DomRendererRowFactory', () => { lineData.setCell(0, cell); const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), - 'a' + 'a' ); }); }); diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts index bb96fb72..5e53cf60 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.ts @@ -130,7 +130,7 @@ export class DomRendererRowFactory { } break; case Attributes.CM_RGB: - charElement.setAttribute('style', `${charElement.getAttribute('style') || ''}color:rgb(${(AttributeData.toColorRGB(fg)).join(',')});`); + charElement.setAttribute('style', `${charElement.getAttribute('style') || ''}color:#${padStart(fg.toString(16), '0', 6)};`); break; case Attributes.CM_DEFAULT: default: @@ -148,7 +148,7 @@ export class DomRendererRowFactory { charElement.classList.add(`xterm-bg-${bg}`); break; case Attributes.CM_RGB: - charElement.setAttribute('style', `${charElement.getAttribute('style') || ''}background-color:rgb(${(AttributeData.toColorRGB(bg)).join(',')});`); + charElement.setAttribute('style', `${charElement.getAttribute('style') || ''}background-color:#${padStart(bg.toString(16), '0', 6)};`); break; case Attributes.CM_DEFAULT: default: @@ -184,3 +184,10 @@ export class DomRendererRowFactory { return false; } } + +function padStart(text: string, padChar: string, length: number): string { + while (text.length < length) { + text = padChar + text; + } + return text; +} From b780abaf3b934dcae1c3cadd5391538b0fdbb42a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Wed, 13 Nov 2019 23:46:01 +0100 Subject: [PATCH 031/103] add onBinary on CoreService, use for DEFAULT mouse reports --- src/common/TestUtils.test.ts | 2 ++ src/common/services/CoreMouseService.test.ts | 11 +++++----- src/common/services/CoreMouseService.ts | 21 ++++++++++++-------- src/common/services/CoreService.ts | 10 ++++++++++ src/common/services/Services.ts | 9 ++++++++- 5 files changed, 39 insertions(+), 14 deletions(-) diff --git a/src/common/TestUtils.test.ts b/src/common/TestUtils.test.ts index 7d9c3aa6..7e47d4b0 100644 --- a/src/common/TestUtils.test.ts +++ b/src/common/TestUtils.test.ts @@ -50,8 +50,10 @@ export class MockCoreService implements ICoreService { decPrivateModes: IDecPrivateModes = {} as any; onData: IEvent = new EventEmitter().event; onUserInput: IEvent = new EventEmitter().event; + onBinary: IEvent = new EventEmitter().event; reset(): void {} triggerDataEvent(data: string, wasUserInput?: boolean): void {} + triggerBinaryEvent(data: string): void {} } export class MockDirtyRowService implements IDirtyRowService { diff --git a/src/common/services/CoreMouseService.test.ts b/src/common/services/CoreMouseService.test.ts index f7d3bf83..d2a470e5 100644 --- a/src/common/services/CoreMouseService.test.ts +++ b/src/common/services/CoreMouseService.test.ts @@ -6,7 +6,7 @@ import { CoreMouseService } from 'common/services/CoreMouseService'; import { MockCoreService, MockBufferService } from 'common/TestUtils.test'; import { assert } from 'chai'; import { ICoreMouseEvent, CoreMouseEventType, CoreMouseButton, CoreMouseAction } from 'common/Types'; - +declare const console: any; // needed mock services const bufferService = new MockBufferService(300, 100); const coreService = new MockCoreService(); @@ -79,6 +79,7 @@ describe('CoreMouseService', () => { cms = new CoreMouseService(bufferService, coreService); reports = []; coreService.triggerDataEvent = (data: string, userInput?: boolean) => reports.push(data); + coreService.triggerBinaryEvent = (data: string) => reports.push(data); }); it('NONE', () => { assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false); @@ -143,11 +144,11 @@ describe('CoreMouseService', () => { cms.activeProtocol = 'ANY'; for (let i = 0; i < bufferService.cols; ++i) { assert.equal(cms.triggerMouseEvent({ col: i, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true); - // capped at 95 - if (i < 95) { - assert.deepEqual(toBytes(reports.pop()), [0x1b, 0x5b, 0x4d, 0x20, i + 33, 0x21]); + if (i > 222) { + // supress mouse reports if we are out of addressible range (max. 222) + assert.deepEqual(toBytes(reports.pop()), []); } else { - assert.deepEqual(toBytes(reports.pop()), [0x1b, 0x5b, 0x4d, 0x20, 0x7f, 0x21]); + assert.deepEqual(toBytes(reports.pop()), [0x1b, 0x5b, 0x4d, 0x20, i + 33, 0x21]); } } }); diff --git a/src/common/services/CoreMouseService.ts b/src/common/services/CoreMouseService.ts index 500655b9..57aa2581 100644 --- a/src/common/services/CoreMouseService.ts +++ b/src/common/services/CoreMouseService.ts @@ -121,15 +121,13 @@ const DEFAULT_ENCODINGS: {[key: string]: CoreMouseEncoding} = { /** * DEFAULT - CSI M Pb Px Py * Single byte encoding for coords and event code. - * Can encode values up to 223. The Encoding of higher - * values is not UTF-8 compatible (and currently limited - * to 95 in xterm.js). + * Can encode values up to 223 (1-based). */ DEFAULT: (e: ICoreMouseEvent) => { - let params = [eventCode(e, false) + 32, e.col + 32, e.row + 32]; - // FIXME: we are currently limited to ASCII range - params = params.map(v => (v > 127) ? 127 : v); - // FIXED: params = params.map(v => (v > 255) ? 0 : value); + const params = [eventCode(e, false) + 32, e.col + 32, e.row + 32]; + if (params[0] > 255 || params[1] > 255 || params[2] > 255) { + return ''; + } return `\x1b[M${S(params[0])}${S(params[1])}${S(params[2])}`; }, /** @@ -266,7 +264,14 @@ export class CoreMouseService implements ICoreMouseService { // encode report and send const report = this._encodings[this._activeEncoding](e); - this._coreService.triggerDataEvent(report, true); + if (this._activeProtocol === 'DEFAULT') { + // always send DEFAULT as binary data + if (report) { + this._coreService.triggerBinaryEvent(report); + } + } else { + this._coreService.triggerDataEvent(report, true); + } this._lastEvent = e; diff --git a/src/common/services/CoreService.ts b/src/common/services/CoreService.ts index 11c3f305..35b61e84 100644 --- a/src/common/services/CoreService.ts +++ b/src/common/services/CoreService.ts @@ -23,6 +23,8 @@ export class CoreService implements ICoreService { public get onData(): IEvent { return this._onData.event; } private _onUserInput = new EventEmitter(); public get onUserInput(): IEvent { return this._onUserInput.event; } + private _onBinary = new EventEmitter(); + public get onBinary(): IEvent { return this._onBinary.event; } constructor( // TODO: Move this into a service @@ -59,4 +61,12 @@ export class CoreService implements ICoreService { this._logService.debug(`sending data "${data}"`, () => data.split('').map(e => e.charCodeAt(0))); this._onData.fire(data); } + + public triggerBinaryEvent(data: string): void { + if (this._optionsService.options.disableStdin) { + return; + } + this._logService.debug(`sending binary "${data}"`, () => data.split('').map(e => e.charCodeAt(0))); + this._onBinary.fire(data); + } } diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index e4ff90d0..45d39a6a 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -68,6 +68,7 @@ export interface ICoreService { readonly onData: IEvent; readonly onUserInput: IEvent; + readonly onBinary: IEvent; reset(): void; @@ -78,8 +79,14 @@ export interface ICoreService { * resulting from parsing incoming data). When true this will also: * - Scroll to the bottom of the buffer.s * - Fire the `onUserInput` event (so selection can be cleared). - */ + */ triggerDataEvent(data: string, wasUserInput?: boolean): void; + + /** + * Triggers the onBinary event in the public API. + * @param data The data that is being emitted. + */ + triggerBinaryEvent(data: string): void; } export const IDirtyRowService = createDecorator('DirtyRowService'); From 00d626b6ccd449698de12a5132fd8c95058b7827 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Wed, 13 Nov 2019 23:57:51 +0100 Subject: [PATCH 032/103] add onBinary to interfaces --- src/Terminal.ts | 2 ++ src/TestUtils.test.ts | 1 + src/Types.d.ts | 1 + src/common/services/CoreMouseService.test.ts | 2 +- src/public/Terminal.ts | 1 + typings/xterm.d.ts | 11 +++++++++++ 6 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index 3c589ffa..d19f34e3 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -169,6 +169,8 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp public get onCursorMove(): IEvent { return this._onCursorMove.event; } private _onData = new EventEmitter(); public get onData(): IEvent { return this._onData.event; } + private _onBinary = new EventEmitter(); + public get onBinary(): IEvent { return this._onBinary.event; } private _onKey = new EventEmitter<{ key: string, domEvent: KeyboardEvent }>(); public get onKey(): IEvent<{ key: string, domEvent: KeyboardEvent }> { return this._onKey.event; } private _onLineFeed = new EventEmitter(); diff --git a/src/TestUtils.test.ts b/src/TestUtils.test.ts index e7c48381..84c0d0b7 100644 --- a/src/TestUtils.test.ts +++ b/src/TestUtils.test.ts @@ -32,6 +32,7 @@ export class MockTerminal implements ITerminal { onLineFeed: IEvent; onSelectionChange: IEvent; onData: IEvent; + onBinary: IEvent; onTitleChange: IEvent; onScroll: IEvent; onKey: IEvent<{ key: string; domEvent: KeyboardEvent; }>; diff --git a/src/Types.d.ts b/src/Types.d.ts index 2a922ae1..f4d3a556 100644 --- a/src/Types.d.ts +++ b/src/Types.d.ts @@ -180,6 +180,7 @@ export interface IPublicTerminal extends IDisposable { markers: IMarker[]; onCursorMove: IEvent; onData: IEvent; + onBinary: IEvent; onKey: IEvent<{ key: string, domEvent: KeyboardEvent }>; onLineFeed: IEvent; onScroll: IEvent; diff --git a/src/common/services/CoreMouseService.test.ts b/src/common/services/CoreMouseService.test.ts index d2a470e5..2bf011b0 100644 --- a/src/common/services/CoreMouseService.test.ts +++ b/src/common/services/CoreMouseService.test.ts @@ -6,7 +6,7 @@ import { CoreMouseService } from 'common/services/CoreMouseService'; import { MockCoreService, MockBufferService } from 'common/TestUtils.test'; import { assert } from 'chai'; import { ICoreMouseEvent, CoreMouseEventType, CoreMouseButton, CoreMouseAction } from 'common/Types'; -declare const console: any; + // needed mock services const bufferService = new MockBufferService(300, 100); const coreService = new MockCoreService(); diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index c167bed8..397898c7 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -27,6 +27,7 @@ export class Terminal implements ITerminalApi { public get onLineFeed(): IEvent { return this._core.onLineFeed; } public get onSelectionChange(): IEvent { return this._core.onSelectionChange; } public get onData(): IEvent { return this._core.onData; } + public get onBinary(): IEvent { return this._core.onBinary; } public get onTitleChange(): IEvent { return this._core.onTitleChange; } public get onScroll(): IEvent { return this._core.onScroll; } public get onKey(): IEvent<{ key: string, domEvent: KeyboardEvent }> { return this._core.onKey; } diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 9728a72c..a086d030 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -421,6 +421,17 @@ declare module 'xterm' { */ constructor(options?: ITerminalOptions); + /** + * Adds an event listener for when a binary event fires. This is used to + * enable non UTF-8 conformant binary messages to be sent to the backend. + * 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'))`. + * @returns an `IDisposable` to stop listening. + */ + onBinary: IEvent; + /** * Adds an event listener for the cursor moves. * @returns an `IDisposable` to stop listening. From f1f103850ce2bcf43d43a269e48d74fa8980f542 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 14 Nov 2019 00:48:16 +0100 Subject: [PATCH 033/103] fix tests; fix attach addon --- addons/xterm-addon-attach/src/AttachAddon.ts | 12 ++++++ src/Terminal.ts | 1 + src/common/services/CoreMouseService.ts | 2 +- test/api/MouseTracking.api.ts | 43 +++++++++++--------- 4 files changed, 37 insertions(+), 21 deletions(-) diff --git a/addons/xterm-addon-attach/src/AttachAddon.ts b/addons/xterm-addon-attach/src/AttachAddon.ts index 117b2b58..279d1b2e 100644 --- a/addons/xterm-addon-attach/src/AttachAddon.ts +++ b/addons/xterm-addon-attach/src/AttachAddon.ts @@ -33,6 +33,7 @@ export class AttachAddon implements ITerminalAddon { if (this._bidirectional) { this._disposables.push(terminal.onData(data => this._sendData(data))); + this._disposables.push(terminal.onBinary(data => this._sendBinary(data))); } this._disposables.push(addSocketListener(this._socket, 'close', () => this.dispose())); @@ -51,6 +52,17 @@ export class AttachAddon implements ITerminalAddon { } this._socket.send(data); } + + private _sendBinary(data: string): void { + if (this._socket.readyState !== 1) { + return; + } + const buffer = new Uint8Array(data.length); + for (let i = 0; i < data.length; ++i) { + buffer[i] = data.charCodeAt(i) & 255; + } + this._socket.send(buffer); + } } function addSocketListener(socket: WebSocket, type: K, handler: (this: WebSocket, ev: WebSocketEventMap[K]) => any): IDisposable { diff --git a/src/Terminal.ts b/src/Terminal.ts index d19f34e3..6a039a0e 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -223,6 +223,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this._coreService = this._instantiationService.createInstance(CoreService, () => this.scrollToBottom()); this._instantiationService.setService(ICoreService, this._coreService); this._coreService.onData(e => this._onData.fire(e)); + this._coreService.onBinary(e => this._onBinary.fire(e)); this._coreMouseService = this._instantiationService.createInstance(CoreMouseService); this._instantiationService.setService(ICoreMouseService, this._coreMouseService); this._dirtyRowService = this._instantiationService.createInstance(DirtyRowService); diff --git a/src/common/services/CoreMouseService.ts b/src/common/services/CoreMouseService.ts index 57aa2581..5c9d3944 100644 --- a/src/common/services/CoreMouseService.ts +++ b/src/common/services/CoreMouseService.ts @@ -264,7 +264,7 @@ export class CoreMouseService implements ICoreMouseService { // encode report and send const report = this._encodings[this._activeEncoding](e); - if (this._activeProtocol === 'DEFAULT') { + if (this._activeEncoding === 'DEFAULT') { // always send DEFAULT as binary data if (report) { this._coreService.triggerBinaryEvent(report); diff --git a/test/api/MouseTracking.api.ts b/test/api/MouseTracking.api.ts index f8124350..7cae660f 100644 --- a/test/api/MouseTracking.api.ts +++ b/test/api/MouseTracking.api.ts @@ -220,6 +220,7 @@ describe('Mouse Tracking Tests', () => { await page.evaluate(` window.calls = []; window.term.onData(e => calls.push( Array.from(e).map(el => el.charCodeAt(0)) )); + window.term.onBinary(e => calls.push( Array.from(e).map(el => el.charCodeAt(0)) )); window.term.setOption('fontSize', ${fontSize}); window.term.resize(${cols}, ${rows}); `); @@ -255,12 +256,17 @@ describe('Mouse Tracking Tests', () => { await pollFor(page, () => getReports(encoding), [{col: 51, row: 11, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); // test at max rows/cols - // bug: we are capped at col 95 currently - // fix: allow values up to 223, any bigger should drop to 0 - await mouseMove(cols - 1, rows - 1); + // capped at 223 (1-based) + await mouseMove(223 - 1, rows - 1); await mouseDown('left'); await mouseUp('left'); - await pollFor(page, () => getReports(encoding), [{col: 95, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); + await pollFor(page, () => getReports(encoding), [{col: 223, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); + + // higher than 223 should not report at all + await mouseMove(257, rows - 1); + await mouseDown('left'); + await mouseUp('left'); + await pollFor(page, () => getReports(encoding), []); // button press/move/release tests // left button @@ -511,14 +517,13 @@ describe('Mouse Tracking Tests', () => { ]); // test at max rows/cols - // bug: we are capped at col 95 currently - // fix: allow values up to 223, any bigger should drop to 0 - await mouseMove(cols - 1, rows - 1); + // capped at 223 (1-based) + await mouseMove(223 - 1, rows - 1); await mouseDown('left'); await mouseUp('left'); await pollFor(page, () => getReports(encoding), [ - {col: 95, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 95, row: rows, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} + {col: 223, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + {col: 223, row: rows, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} ]); // button press/move/release tests @@ -821,14 +826,13 @@ describe('Mouse Tracking Tests', () => { ]); // test at max rows/cols - // bug: we are capped at col 95 currently - // fix: allow values up to 223, any bigger should drop to 0 - await mouseMove(cols - 1, rows - 1); + // capped at 223 (1-based) + await mouseMove(223 - 1, rows - 1); await mouseDown('left'); await mouseUp('left'); await pollFor(page, () => getReports(encoding), [ - {col: 95, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 95, row: rows, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} + {col: 223, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + {col: 223, row: rows, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} ]); // button press/move/release tests @@ -1142,15 +1146,14 @@ describe('Mouse Tracking Tests', () => { ]); // test at max rows/cols - // bug: we are capped at col 95 currently - // fix: allow values up to 223, any bigger should drop to 0 - await mouseMove(cols - 1, rows - 1); + // capped at 223 (1-based) + await mouseMove(223 - 1, rows - 1); await mouseDown('left'); await mouseUp('left'); await pollFor(page, () => getReports(encoding), [ - {col: 95, row: rows, state: {action: 'move', button: '', modifier: {control: false, shift: false, meta: false}}}, - {col: 95, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, - {col: 95, row: rows, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} + {col: 223, row: rows, state: {action: 'move', button: '', modifier: {control: false, shift: false, meta: false}}}, + {col: 223, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + {col: 223, row: rows, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} ]); // button press/move/release tests From 57fe84508cb43291d508e546ed5130174ee5a141 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 14 Nov 2019 22:29:46 +0100 Subject: [PATCH 034/103] apply empty report rule to all encodings; comments added --- src/common/Types.d.ts | 3 +++ src/common/services/CoreMouseService.ts | 12 ++++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/common/Types.d.ts b/src/common/Types.d.ts index 2f742038..2dcc704b 100644 --- a/src/common/Types.d.ts +++ b/src/common/Types.d.ts @@ -249,5 +249,8 @@ export interface ICoreMouseProtocol { * The tracking encoding can be registered and activated at the CoreMouseService. * If a ICoreMouseEvent passes all procotol restrictions it will be encoded * with the active encoding and sent out. + * Note: Returning an empty string will supress sending a mouse report, + * which can be used to skip creating falsey reports in limited encodings + * (DEFAULT only supports up to 223 1-based as coord value). */ export type CoreMouseEncoding = (event: ICoreMouseEvent) => string; diff --git a/src/common/services/CoreMouseService.ts b/src/common/services/CoreMouseService.ts index 5c9d3944..0bd25dd0 100644 --- a/src/common/services/CoreMouseService.ts +++ b/src/common/services/CoreMouseService.ts @@ -125,6 +125,10 @@ const DEFAULT_ENCODINGS: {[key: string]: CoreMouseEncoding} = { */ DEFAULT: (e: ICoreMouseEvent) => { const params = [eventCode(e, false) + 32, e.col + 32, e.row + 32]; + // supress mouse report if we exceed addressible range + // Note this is handled differently by emulators + // - xterm: sends 0;0 coords instead + // - vte, konsole: no report if (params[0] > 255 || params[1] > 255 || params[2] > 255) { return ''; } @@ -264,13 +268,13 @@ export class CoreMouseService implements ICoreMouseService { // encode report and send const report = this._encodings[this._activeEncoding](e); - if (this._activeEncoding === 'DEFAULT') { + if (report) { // always send DEFAULT as binary data - if (report) { + if (this._activeEncoding === 'DEFAULT') { this._coreService.triggerBinaryEvent(report); + } else { + this._coreService.triggerDataEvent(report, true); } - } else { - this._coreService.triggerDataEvent(report, true); } this._lastEvent = e; From 24dc87af2c6165216d4fb4b70c84f474f72155ed Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 14 Nov 2019 19:46:24 -0800 Subject: [PATCH 035/103] Only enter updateBackgounds condition if cell changed We want to check when the background changed or when the foreground changed (and either the old or new value was inverse) --- addons/xterm-addon-webgl/src/RectangleRenderer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/xterm-addon-webgl/src/RectangleRenderer.ts b/addons/xterm-addon-webgl/src/RectangleRenderer.ts index 356e1747..b52a506e 100644 --- a/addons/xterm-addon-webgl/src/RectangleRenderer.ts +++ b/addons/xterm-addon-webgl/src/RectangleRenderer.ts @@ -254,7 +254,7 @@ export class RectangleRenderer { const bg = model.cells[modelIndex + RENDER_MODEL_BG_OFFSET]; const fg = model.cells[modelIndex + RENDER_MODEL_FG_OFFSET]; const inverse = !!(fg & FgFlags.INVERSE); - if (bg !== currentBg || ((inverse || currentInverse) && fg !== currentFg)) { + if (bg !== currentBg || (fg !== currentFg && (currentInverse || inverse))) { // A rectangle needs to be drawn if going from non-default to another color if (currentBg !== 0 || (currentInverse && currentFg !== 0)) { const offset = rectangleCount++ * INDICES_PER_RECTANGLE; From be9700878b7604dfec6133c73c20926fc72af8ae Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 14 Nov 2019 20:02:40 -0800 Subject: [PATCH 036/103] Have inverse bold respect drawBoldTextInBrightColors --- src/browser/renderer/BaseRenderLayer.ts | 8 ++++++-- src/browser/renderer/TextRenderLayer.ts | 6 +++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/browser/renderer/BaseRenderLayer.ts b/src/browser/renderer/BaseRenderLayer.ts index a33bef28..2c748c50 100644 --- a/src/browser/renderer/BaseRenderLayer.ts +++ b/src/browser/renderer/BaseRenderLayer.ts @@ -282,7 +282,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { fg = (cell.isFgDefault()) ? DEFAULT_COLOR : cell.getFgColor(); } - const drawInBrightColor = this._optionsService.options.drawBoldTextInBrightColors && cell.isBold() && fg < 8 && fg !== INVERTED_DEFAULT_COLOR; + const drawInBrightColor = this._optionsService.options.drawBoldTextInBrightColors && cell.isBold() && fg < 8; fg += drawInBrightColor ? 8 : 0; this._currentGlyphIdentifier.chars = cell.getChars() || WHITESPACE_CELL_CHAR; @@ -325,7 +325,11 @@ export abstract class BaseRenderLayer implements IRenderLayer { } else if (cell.isBgRGB()) { this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`; } else { - this._ctx.fillStyle = this._colors.ansi[cell.getBgColor()].css; + let bg = cell.getBgColor(); + if (this._optionsService.options.drawBoldTextInBrightColors && cell.isBold() && bg < 8) { + bg += 8; + } + this._ctx.fillStyle = this._colors.ansi[bg].css; } } else { if (cell.isFgDefault()) { diff --git a/src/browser/renderer/TextRenderLayer.ts b/src/browser/renderer/TextRenderLayer.ts index 330400ca..fee261f6 100644 --- a/src/browser/renderer/TextRenderLayer.ts +++ b/src/browser/renderer/TextRenderLayer.ts @@ -227,7 +227,11 @@ export class TextRenderLayer extends BaseRenderLayer { } else if (cell.isBgRGB()) { this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`; } else { - this._ctx.fillStyle = this._colors.ansi[cell.getBgColor()].css; + let bg = cell.getBgColor(); + if (this._optionsService.options.drawBoldTextInBrightColors && cell.isBold() && bg < 8) { + bg += 8; + } + this._ctx.fillStyle = this._colors.ansi[bg].css; } } else { if (cell.isFgDefault()) { From 023fab9095c63ff3e549dbc171225da232b7d2ea Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 14 Nov 2019 20:34:01 -0800 Subject: [PATCH 037/103] Remove unused import --- src/browser/renderer/dom/DomRendererRowFactory.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts index 5e53cf60..cffc5624 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.ts @@ -5,7 +5,6 @@ import { IBufferLine } from 'common/Types'; import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants'; -import { AttributeData } from 'common/buffer/AttributeData'; import { NULL_CELL_CODE, WHITESPACE_CELL_CHAR, Attributes } from 'common/buffer/Constants'; import { CellData } from 'common/buffer/CellData'; import { IOptionsService } from 'common/services/Services'; From c95337a742c6334df50c82121416b97e63dbae55 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 14 Nov 2019 21:57:15 -0800 Subject: [PATCH 038/103] Remove unneeded params from webgl funcs --- addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index 9910b009..686dcb10 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -234,7 +234,7 @@ export class WebglCharAtlas implements IDisposable { } } - private _resolveBackgroundRgba(bgColorMode: number, bgColor: number, fgColorMode: number, fgColor: number, inverse: boolean): number { + private _resolveBackgroundRgba(bgColorMode: number, bgColor: number, inverse: boolean): number { switch (bgColorMode) { case Attributes.CM_P16: case Attributes.CM_P256: @@ -250,7 +250,7 @@ export class WebglCharAtlas implements IDisposable { } } - private _resolveForegroundRgba(bgColorMode: number, bgColor: number, fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean): number { + private _resolveForegroundRgba(fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean): number { switch (fgColorMode) { case Attributes.CM_P16: case Attributes.CM_P256: @@ -280,8 +280,8 @@ export class WebglCharAtlas implements IDisposable { return adjustedColor || undefined; } - const bgRgba = this._resolveBackgroundRgba(bgColorMode, bgColor, fgColorMode, fgColor, inverse); - const fgRgba = this._resolveForegroundRgba(bgColorMode, bgColor, fgColorMode, fgColor, inverse, bold); + const bgRgba = this._resolveBackgroundRgba(bgColorMode, bgColor, inverse); + const fgRgba = this._resolveForegroundRgba(fgColorMode, fgColor, inverse, bold); const result = ensureContrastRatioRgba(bgRgba, fgRgba, this._config.minimumContrastRatio); if (!result) { From e5f4e2ff8f2ac034764744fb4d411c0ac1b62952 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 14 Nov 2019 22:07:50 -0800 Subject: [PATCH 039/103] Support min contrast ratio in canvas renderer too: --- src/browser/renderer/BaseRenderLayer.ts | 103 ++++++++++++++++++++++-- 1 file changed, 96 insertions(+), 7 deletions(-) diff --git a/src/browser/renderer/BaseRenderLayer.ts b/src/browser/renderer/BaseRenderLayer.ts index 2c748c50..599ab114 100644 --- a/src/browser/renderer/BaseRenderLayer.ts +++ b/src/browser/renderer/BaseRenderLayer.ts @@ -5,16 +5,17 @@ import { IRenderDimensions, IRenderLayer } from 'browser/renderer/Types'; import { ICellData } from 'common/Types'; -import { DEFAULT_COLOR, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE } from 'common/buffer/Constants'; +import { DEFAULT_COLOR, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE, Attributes } from 'common/buffer/Constants'; import { IGlyphIdentifier } from 'browser/renderer/atlas/Types'; import { DIM_OPACITY, INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants'; import { BaseCharAtlas } from 'browser/renderer/atlas/BaseCharAtlas'; import { acquireCharAtlas } from 'browser/renderer/atlas/CharAtlasCache'; import { AttributeData } from 'common/buffer/AttributeData'; -import { IColorSet } from 'browser/Types'; +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 } from 'browser/Color'; export abstract class BaseRenderLayer implements IRenderLayer { private _canvas: HTMLCanvasElement; @@ -262,13 +263,14 @@ export abstract class BaseRenderLayer implements IRenderLayer { * @param bold Whether the text is bold. */ protected _drawChars(cell: ICellData, x: number, y: number): void { + const contrastColor = this._getContrastColor(cell); // skip cache right away if we draw in RGB // Note: to avoid bad runtime JoinedCellData will be skipped // in the cache handler itself (atlasDidDraw == false) and // fall through to uncached later down below - if (cell.isFgRGB() || cell.isBgRGB()) { - this._drawUncachedChars(cell, x, y); + if (contrastColor || cell.isFgRGB() || cell.isBgRGB()) { + this._drawUncachedChars(cell, x, y, contrastColor); return; } @@ -314,13 +316,15 @@ export abstract class BaseRenderLayer implements IRenderLayer { * @param x The column to draw at. * @param y The row to draw at. */ - private _drawUncachedChars(cell: ICellData, x: number, y: number): void { + private _drawUncachedChars(cell: ICellData, x: number, y: number, fgOverride?: IColor): void { this._ctx.save(); this._ctx.font = this._getFont(!!cell.isBold(), !!cell.isItalic()); this._ctx.textBaseline = 'middle'; if (cell.isInverse()) { - if (cell.isBgDefault()) { + if (fgOverride) { + this._ctx.fillStyle = fgOverride.css; + } else if (cell.isBgDefault()) { this._ctx.fillStyle = this._colors.background.css; } else if (cell.isBgRGB()) { this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`; @@ -332,7 +336,9 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._ctx.fillStyle = this._colors.ansi[bg].css; } } else { - if (cell.isFgDefault()) { + if (fgOverride) { + this._ctx.fillStyle = fgOverride.css; + } else if (cell.isFgDefault()) { this._ctx.fillStyle = this._colors.foreground.css; } else if (cell.isFgRGB()) { this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`; @@ -383,5 +389,88 @@ export abstract class BaseRenderLayer implements IRenderLayer { return `${fontStyle} ${fontWeight} ${this._optionsService.options.fontSize * window.devicePixelRatio}px ${this._optionsService.options.fontFamily}`; } + + private _getContrastColor(cell: CellData): IColor | undefined { + if (this._optionsService.options.minimumContrastRatio === 1) { + return undefined; + } + + // Try get from cache first + const adjustedColor = this._colors.contrastCache.getColor(cell.bg, cell.fg); + if (adjustedColor !== undefined) { + return adjustedColor || undefined; + } + + let fgColor = cell.getFgColor(); + let fgColorMode = cell.getFgColorMode(); + let bgColor = cell.getBgColor(); + let bgColorMode = cell.getBgColorMode(); + const isInverse = !!cell.isInverse(); + const isBold = !!cell.isInverse(); + if (isInverse) { + const temp = fgColor; + fgColor = bgColor; + bgColor = temp; + const temp2 = fgColorMode; + fgColorMode = bgColorMode; + bgColorMode = temp2; + } + + const bgRgba = this._resolveBackgroundRgba(bgColorMode, bgColor, isInverse); + const fgRgba = this._resolveForegroundRgba(fgColorMode, fgColor, isInverse, isBold); + const result = ensureContrastRatioRgba(bgRgba, fgRgba, this._optionsService.options.minimumContrastRatio); + + if (!result) { + this._colors.contrastCache.setColor(cell.bg, cell.fg, null); + return undefined; + } + + const color: IColor = { + css: toCss( + (result >> 24) & 0xFF, + (result >> 16) & 0xFF, + (result >> 8) & 0xFF + ), + rgba: result + }; + this._colors.contrastCache.setColor(cell.bg, cell.fg, color); + + return color; + } + + private _resolveBackgroundRgba(bgColorMode: number, bgColor: number, inverse: boolean): number { + switch (bgColorMode) { + case Attributes.CM_P16: + case Attributes.CM_P256: + return this._colors.ansi[bgColor].rgba; + case Attributes.CM_RGB: + return bgColor << 8; + case Attributes.CM_DEFAULT: + default: + if (inverse) { + return this._colors.foreground.rgba; + } + return this._colors.background.rgba; + } + } + + private _resolveForegroundRgba(fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean): number { + switch (fgColorMode) { + case Attributes.CM_P16: + case Attributes.CM_P256: + if (this._optionsService.options.drawBoldTextInBrightColors && bold && fgColor < 8) { + fgColor += 8; + } + return this._colors.ansi[fgColor].rgba; + case Attributes.CM_RGB: + return fgColor << 8; + case Attributes.CM_DEFAULT: + default: + if (inverse) { + return this._colors.background.rgba; + } + return this._colors.foreground.rgba; + } + } } From 5aa31576722f02213411be860b678d12281b58d0 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 14 Nov 2019 22:12:36 -0800 Subject: [PATCH 040/103] Return inverse colors correctly This probably wasn't causing a bug since the opposite values were being returned, it was wrong and made the code confusing though. --- addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index 686dcb10..cda20225 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -244,7 +244,7 @@ export class WebglCharAtlas implements IDisposable { case Attributes.CM_DEFAULT: default: if (inverse) { - this._config.colors.foreground.rgba; + return this._config.colors.foreground.rgba; } return this._config.colors.background.rgba; } @@ -263,7 +263,7 @@ export class WebglCharAtlas implements IDisposable { case Attributes.CM_DEFAULT: default: if (inverse) { - this._config.colors.background.rgba; + return this._config.colors.background.rgba; } return this._config.colors.foreground.rgba; } From 943928c4d1a6d4f46fb33fd7969fff71aeed7786 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 14 Nov 2019 22:26:02 -0800 Subject: [PATCH 041/103] Move WindowsMode to common/ Part of #1507 --- src/Terminal.ts | 14 +++++++++----- src/{ => common}/WindowsMode.ts | 15 +++++++-------- 2 files changed, 16 insertions(+), 13 deletions(-) rename src/{ => common}/WindowsMode.ts (67%) diff --git a/src/Terminal.ts b/src/Terminal.ts index 3c589ffa..fc354966 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -43,7 +43,7 @@ import { IKeyboardEvent, KeyboardResultType, ICharset, IBufferLine, IAttributeDa import { evaluateKeyboardEvent } from 'common/input/Keyboard'; import { EventEmitter, IEvent } from 'common/EventEmitter'; import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; -import { applyWindowsMode } from './WindowsMode'; +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'; @@ -281,7 +281,13 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this.linkifier = this.linkifier || new Linkifier(this._bufferService, this._logService); if (this.options.windowsMode) { - this._windowsMode = applyWindowsMode(this); + this._enableWindowsMode(); + } + } + + private _enableWindowsMode(): void { + if (!this._windowsMode) { + this._windowsMode = this.onLineFeed(handleWindowsModeLineFeed.bind(null, this._bufferService)); } } @@ -362,9 +368,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp break; case 'windowsMode': if (this.optionsService.options.windowsMode) { - if (!this._windowsMode) { - this._windowsMode = applyWindowsMode(this); - } + this._enableWindowsMode(); } else { this._windowsMode?.dispose(); this._windowsMode = undefined; diff --git a/src/WindowsMode.ts b/src/common/WindowsMode.ts similarity index 67% rename from src/WindowsMode.ts rename to src/common/WindowsMode.ts index 0838cae2..ff0e7591 100644 --- a/src/WindowsMode.ts +++ b/src/common/WindowsMode.ts @@ -3,11 +3,10 @@ * @license MIT */ -import { IDisposable } from 'xterm'; -import { ITerminal } from './Types'; import { CHAR_DATA_CODE_INDEX, NULL_CELL_CODE, WHITESPACE_CELL_CODE } from 'common/buffer/Constants'; +import { IBufferService } from 'common/services/Services'; -export function applyWindowsMode(terminal: ITerminal): IDisposable { +export function handleWindowsModeLineFeed(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 @@ -18,11 +17,11 @@ export function applyWindowsMode(terminal: ITerminal): IDisposable { // space. This is certainly not without its problems, but generally on // Windows when text reaches the end of the terminal it's likely going to be // wrapped. - return terminal.onLineFeed(() => { - const line = terminal.buffer.lines.get(terminal.buffer.ybase + terminal.buffer.y - 1); - const lastChar = line.get(terminal.cols - 1); + const line = bufferService.buffer.lines.get(bufferService.buffer.ybase + bufferService.buffer.y - 1); + const lastChar = line?.get(bufferService.cols - 1); - const nextLine = terminal.buffer.lines.get(terminal.buffer.ybase + terminal.buffer.y); + const nextLine = bufferService.buffer.lines.get(bufferService.buffer.ybase + bufferService.buffer.y); + if (nextLine && lastChar) { nextLine.isWrapped = (lastChar[CHAR_DATA_CODE_INDEX] !== NULL_CELL_CODE && lastChar[CHAR_DATA_CODE_INDEX] !== WHITESPACE_CELL_CODE); - }); + } } From 93487d4fb373602686ab0e874af1824287eafbae Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 22 Nov 2019 09:34:06 -0800 Subject: [PATCH 042/103] Direct question issues to Stack Overflow --- .github/ISSUE_TEMPLATE/config.yml | 5 +++++ .github/ISSUE_TEMPLATE/question.md | 8 -------- 2 files changed, 5 insertions(+), 8 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/config.yml delete mode 100644 .github/ISSUE_TEMPLATE/question.md diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..dccd0c5d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Question + url: https://stackoverflow.com/questions/tagged/xtermjs + about: Please ask and answer questions here. diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md deleted file mode 100644 index 1fe5c763..00000000 --- a/.github/ISSUE_TEMPLATE/question.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: Question -about: The issue tracker is not for questions. Please ask questions on https://stackoverflow.com/questions/tagged/xtermjs ---- - -🛑 The issue tracker is not for questions 🛑 - -If you have a question, please ask it on https://stackoverflow.com/questions/tagged/xtermjs. From 0bd0f3a364c8f403d7a49c2d434d614e6f1a7911 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 22 Nov 2019 16:00:25 -0800 Subject: [PATCH 043/103] Prevent atlas missing exception when opened before attach See microsoft/vscode#85048 --- addons/xterm-addon-webgl/src/WebglRenderer.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index 46fd2730..6f98d2c1 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -37,6 +37,7 @@ export class WebglRenderer extends Disposable implements IRenderer { public dimensions: IRenderDimensions; private _core: ITerminal; + private _isAttached: boolean; private _onRequestRefreshRows = new EventEmitter(); public get onRequestRefreshRows(): IEvent { return this._onRequestRefreshRows.event; } @@ -89,6 +90,8 @@ export class WebglRenderer extends Disposable implements IRenderer { // Update dimensions and acquire char atlas this.onCharSizeChanged(); + + this._isAttached = document.body.contains(this._core.screenElement); } public dispose(): void { @@ -99,7 +102,6 @@ export class WebglRenderer extends Disposable implements IRenderer { public setColors(colors: IColorSet): void { this._colors = colors; - // Clear layers and force a full render this._renderLayers.forEach(l => { l.setColors(this._terminal, this._colors); @@ -192,6 +194,8 @@ export class WebglRenderer extends Disposable implements IRenderer { */ private _refreshCharAtlas(): void { if (this.dimensions.scaledCharWidth <= 0 && this.dimensions.scaledCharHeight <= 0) { + // Mark as not attached so char atlas gets refreshed on next render + this._isAttached = false; return; } @@ -217,6 +221,16 @@ export class WebglRenderer extends Disposable implements IRenderer { } public renderRows(start: number, end: number): void { + if (!this._isAttached) { + if (document.body.contains(this._core.screenElement) && (this._core)._charSizeService.width && (this._core)._charSizeService.height) { + this._updateDimensions(); + this._refreshCharAtlas(); + this._isAttached = true; + } else { + return; + } + } + // Update render layers this._renderLayers.forEach(l => l.onGridChanged(this._terminal, start, end)); From 2f1f2f87855b2d78af6f6e021f0d97127fdb7740 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 22 Nov 2019 19:37:53 -0800 Subject: [PATCH 044/103] Add a . before beta and beta version Fixes #2576 --- bin/publish.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/publish.js b/bin/publish.js index 9d58ed15..4a7536cb 100644 --- a/bin/publish.js +++ b/bin/publish.js @@ -105,7 +105,7 @@ function getNextBetaVersion(packageJson) { return aVersion > bVersion ? -1 : 1; })[0]; const latestTagVersion = parseInt(latestPublishedVersion.substr(latestPublishedVersion.search(/[0-9]+$/)), 10); - return `${nextStableVersion}-${tag}${latestTagVersion + 1}`; + return `${nextStableVersion}-${tag}.${latestTagVersion + 1}`; } function getPublishedVersions(packageJson, version, tag) { From 53fb1ba54df402d24f0e48ba36e918ca46d05653 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 24 Nov 2019 09:22:07 -0800 Subject: [PATCH 045/103] Don't throw when rendering before atlas is set This fixes a problem when loading the webgl addon before the terminal is attached. See microsoft/vscode#85048 --- addons/xterm-addon-webgl/src/GlyphRenderer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/xterm-addon-webgl/src/GlyphRenderer.ts b/addons/xterm-addon-webgl/src/GlyphRenderer.ts index 3d4d43d1..e72346dc 100644 --- a/addons/xterm-addon-webgl/src/GlyphRenderer.ts +++ b/addons/xterm-addon-webgl/src/GlyphRenderer.ts @@ -184,7 +184,7 @@ export class GlyphRenderer { let rasterizedGlyph: IRasterizedGlyph; if (!this._atlas) { - throw new Error('atlas must be set before updating cell'); + return; } if (chars && chars.length > 1) { rasterizedGlyph = this._atlas.getRasterizedGlyphCombinedChar(chars, bg, fg); From c60b822f4b3dd8422253c41795e726c57eb5d13d Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 24 Nov 2019 09:34:29 -0800 Subject: [PATCH 046/103] Fix publish script for dot separated beta --- addons/xterm-addon-webgl/src/GlyphRenderer.ts | 1 - bin/publish.js | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/addons/xterm-addon-webgl/src/GlyphRenderer.ts b/addons/xterm-addon-webgl/src/GlyphRenderer.ts index e72346dc..7b35949d 100644 --- a/addons/xterm-addon-webgl/src/GlyphRenderer.ts +++ b/addons/xterm-addon-webgl/src/GlyphRenderer.ts @@ -101,7 +101,6 @@ export class GlyphRenderer { private _dimensions: IRenderDimensions ) { const gl = this._gl; - const program = throwIfFalsy(createProgram(gl, vertexShaderSource, fragmentShaderSource)); this._program = program; diff --git a/bin/publish.js b/bin/publish.js index 4a7536cb..9050c344 100644 --- a/bin/publish.js +++ b/bin/publish.js @@ -97,7 +97,7 @@ function getNextBetaVersion(packageJson) { const nextStableVersion = `${stableVersion[0]}.${parseInt(stableVersion[1]) + 1}.0`; const publishedVersions = getPublishedVersions(packageJson, nextStableVersion, tag); if (publishedVersions.length === 0) { - return `${nextStableVersion}-${tag}1`; + return `${nextStableVersion}-${tag}.1`; } const latestPublishedVersion = publishedVersions.sort((a, b) => { const aVersion = parseInt(a.substr(a.search(/[0-9]+$/))); @@ -112,7 +112,7 @@ function getPublishedVersions(packageJson, version, tag) { const versionsProcess = cp.spawnSync('npm', ['view', packageJson.name, 'versions', '--json']); const versionsJson = JSON.parse(versionsProcess.stdout); if (tag) { - return versionsJson.filter(v => !v.search(new RegExp(`${version}-${tag}[0-9]+`))); + return versionsJson.filter(v => !v.search(new RegExp(`${version}-${tag}.[0-9]+`))); } return versionsJson; } From 2da5a7a26b44dc66f4ac9b772cd6b9bb4a4271b8 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 24 Nov 2019 09:42:33 -0800 Subject: [PATCH 047/103] Skip puppeteer download during release job --- azure-pipelines.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 0f61dfe1..f6f892c7 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -127,6 +127,8 @@ jobs: condition: and(succeeded(), or(eq(variables['Build.SourceBranch'], 'refs/heads/master'), eq(variables['FORCE_RELEASE'], 'true'))) pool: vmImage: 'ubuntu-16.04' + variables: + PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: 1 steps: - task: NodeTool@0 inputs: @@ -140,6 +142,7 @@ jobs: inputs: key: yarn2 | $(Agent.OS) | yarn.lock path: node_modules + displayName: Cache node modules - script: yarn --frozen-lockfile displayName: 'Install dependencies and build' - script: NPM_AUTH_TOKEN="$(NPM_AUTH_TOKEN)" node ./bin/publish.js From 217bc242355dd261f9fb9bf2e4a3e4b726329809 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 24 Nov 2019 11:15:28 -0800 Subject: [PATCH 048/103] Mark before and after cursor rows dirty when changed The DOM renderer renders the cursor as part of the content, not on a separate layer, so it needs to be marked dirty to draw. Fixes #2581 --- src/InputHandler.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 3994d532..7a0a6ac3 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -546,6 +546,7 @@ export class InputHandler extends Disposable implements IInputHandler { // make buffer local for faster access const buffer = this._bufferService.buffer; + this._dirtyRowService.markDirty(buffer.y); if (this._optionsService.options.convertEol) { buffer.x = 0; } @@ -560,6 +561,7 @@ export class InputHandler extends Disposable implements IInputHandler { if (buffer.x >= this._bufferService.cols) { buffer.x--; } + this._dirtyRowService.markDirty(buffer.y); this._onLineFeed.fire(); } @@ -624,12 +626,14 @@ export class InputHandler extends Disposable implements IInputHandler { this._bufferService.buffer.y = this._terminal.originMode ? 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); } /** * Set absolute cursor position. */ private _setCursor(x: number, y: number): void { + this._dirtyRowService.markDirty(this._bufferService.buffer.y); if (this._terminal.originMode) { this._bufferService.buffer.x = x; this._bufferService.buffer.y = this._bufferService.buffer.scrollTop + y; @@ -638,6 +642,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._bufferService.buffer.y = y; } this._restrictCursor(); + this._dirtyRowService.markDirty(this._bufferService.buffer.y); } /** From 25f2e66222fd1557ddc0131d62fc64e0565a9f1f Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 25 Nov 2019 07:36:06 -0800 Subject: [PATCH 049/103] Include ` in word separators See microsoft/vscode#84483 --- src/common/services/OptionsService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/services/OptionsService.ts b/src/common/services/OptionsService.ts index b0b57694..9eaad94f 100644 --- a/src/common/services/OptionsService.ts +++ b/src/common/services/OptionsService.ts @@ -51,7 +51,7 @@ export const DEFAULT_OPTIONS: ITerminalOptions = Object.freeze({ screenKeys: false, cancelEvents: false, useFlowControl: false, - wordSeparator: ' ()[]{}\',:;"' + wordSeparator: ' ()[]{}\',:;"`' }); /** From afe6cb6b865a0f805e3a71996cd176325c3d5f89 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 25 Nov 2019 09:12:39 -0800 Subject: [PATCH 050/103] Remove unused imports/functions --- .../src/atlas/WebglCharAtlas.ts | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index cda20225..097c420d 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -6,7 +6,7 @@ import { ICharAtlasConfig } from './Types'; import { DIM_OPACITY } from 'browser/renderer/atlas/Constants'; import { IRasterizedGlyph, IBoundingBox, IRasterizedGlyphSet } from '../Types'; -import { DEFAULT_COLOR, FgFlags, Attributes, BgFlags } from 'common/buffer/Constants'; +import { DEFAULT_COLOR, Attributes } from 'common/buffer/Constants'; import { throwIfFalsy } from '../WebglUtils'; import { IColor } from 'browser/Types'; import { IDisposable } from 'xterm'; @@ -530,20 +530,3 @@ function toPaddedHex(c: number): string { const s = c.toString(16); return s.length < 2 ? '0' + s : s; } - -function getFgColor(fg: number): number { - switch (fg & Attributes.CM_MASK) { - case Attributes.CM_P16: - case Attributes.CM_P256: return fg & Attributes.PCOLOR_MASK; - case Attributes.CM_RGB: return fg & Attributes.RGB_MASK; - default: return -1; // CM_DEFAULT defaults to -1 - } -} -function getBgColor(bg: number): number { - switch (bg & Attributes.CM_MASK) { - case Attributes.CM_P16: - case Attributes.CM_P256: return bg & Attributes.PCOLOR_MASK; - case Attributes.CM_RGB: return bg & Attributes.RGB_MASK; - default: return -1; // CM_DEFAULT defaults to -1 - } -} From 1a5c3f936460f6e8b9705f2322d1352d29dca4d0 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 25 Nov 2019 11:57:52 -0800 Subject: [PATCH 051/103] Properly set non-black transparency backgrounds Before it was always set to 0 as the resolved color was pulled from an ImageData --- src/browser/Color.ts | 5 ++- src/browser/ColorManager.ts | 73 +++++++++++++++++++++++++------------ 2 files changed, 54 insertions(+), 24 deletions(-) diff --git a/src/browser/Color.ts b/src/browser/Color.ts index 2fc97f0a..e20f62e6 100644 --- a/src/browser/Color.ts +++ b/src/browser/Color.ts @@ -39,7 +39,10 @@ export function toPaddedHex(c: number): string { return s.length < 2 ? '0' + s : s; } -export function toCss(r: number, g: number, b: number): string { +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)}`; } diff --git a/src/browser/ColorManager.ts b/src/browser/ColorManager.ts index 92994cfc..46a7710e 100644 --- a/src/browser/ColorManager.ts +++ b/src/browser/ColorManager.ts @@ -5,7 +5,7 @@ import { IColorManager, IColor, IColorSet, IColorContrastCache } from 'browser/Types'; import { ITheme } from 'common/services/Services'; -import { fromCss, toCss, blend, toRgba } from 'browser/Color'; +import { fromCss, toCss, blend, toRgba, toPaddedHex } from 'browser/Color'; import { ColorContrastCache } from 'browser/ColorContrastCache'; const DEFAULT_FOREGROUND = fromCss('#ffffff'); @@ -159,28 +159,55 @@ export class ColorManager implements IColorManager { this._ctx.fillRect(0, 0, 1, 1); const data = this._ctx.getImageData(0, 0, 1, 1).data; - if (!allowTransparency && data[3] !== 0xFF) { - // Ideally we'd just ignore the alpha channel, but... - // - // Browsers may not give back exactly the same RGB values we put in, because most/all - // convert the color to a pre-multiplied representation. getImageData converts that back to - // a un-premultipled representation, but the precision loss may make the RGB channels unuable - // on their own. - // - // E.g. In Chrome #12345610 turns into #10305010, and in the extreme case, 0xFFFFFF00 turns - // into 0x00000000. - // - // "Note: Due to the lossy nature of converting to and from premultiplied alpha color values, - // pixels that have just been set using putImageData() might be returned to an equivalent - // getImageData() as different values." - // -- https://html.spec.whatwg.org/multipage/canvas.html#pixel-manipulation - // - // So let's just use the fallback color in this case instead. - console.warn( - `Color: ${css} is using transparency, but allowTransparency is false. ` + - `Using fallback ${fallback.css}.` - ); - return fallback; + // Check if the printed color was transparent + if (data[3] !== 0xFF) { + if (!allowTransparency) { + // Ideally we'd just ignore the alpha channel, but... + // + // Browsers may not give back exactly the same RGB values we put in, because most/all + // convert the color to a pre-multiplied representation. getImageData converts that back to + // a un-premultipled representation, but the precision loss may make the RGB channels unuable + // on their own. + // + // E.g. In Chrome #12345610 turns into #10305010, and in the extreme case, 0xFFFFFF00 turns + // into 0x00000000. + // + // "Note: Due to the lossy nature of converting to and from premultiplied alpha color values, + // pixels that have just been set using putImageData() might be returned to an equivalent + // getImageData() as different values." + // -- https://html.spec.whatwg.org/multipage/canvas.html#pixel-manipulation + // + // So let's just use the fallback color in this case instead. + console.warn( + `Color: ${css} is using transparency, but allowTransparency is false. ` + + `Using fallback ${fallback.css}.` + ); + 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 = (r << 24) | (g << 16) | (b << 8) | a; + } else { + rgba = parseInt(css.substr(1), 16); + r = (rgba >> 24) & 0xFF; + g = (rgba >> 16) & 0xFF; + b = (rgba >> 8) & 0xFF; + a = (rgba ) & 0xFF; + } + + return { + rgba, + css: toCss(r, g, b, a) + }; } return { From 4cd8b22a5935657b35c6d123b7cd6c597fe200a7 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 25 Nov 2019 11:58:26 -0800 Subject: [PATCH 052/103] Use toRgba helper --- src/browser/ColorManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browser/ColorManager.ts b/src/browser/ColorManager.ts index 46a7710e..b4bcdde0 100644 --- a/src/browser/ColorManager.ts +++ b/src/browser/ColorManager.ts @@ -195,7 +195,7 @@ export class ColorManager implements IColorManager { g = ((num >> 8) & 0xF) * 16; b = ((num >> 4) & 0xF) * 16; a = (num & 0xF) * 16; - rgba = (r << 24) | (g << 16) | (b << 8) | a; + rgba = toRgba(r, g, b, a); } else { rgba = parseInt(css.substr(1), 16); r = (rgba >> 24) & 0xFF; From bceddfbc28f968d89ff84eb3ce922a01d29f8fd9 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 25 Nov 2019 14:22:03 -0800 Subject: [PATCH 053/103] Cover inverse non-black uncached case in canvas renderer --- src/browser/renderer/BaseRenderLayer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/browser/renderer/BaseRenderLayer.ts b/src/browser/renderer/BaseRenderLayer.ts index 599ab114..f55a130d 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 } from 'browser/Color'; +import { toCss, ensureContrastRatioRgba, opaque } 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 = this._colors.background.css; + this._ctx.fillStyle = opaque(this._colors.background).css; } else if (cell.isBgRGB()) { this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`; } else { From ef05a6a05354901eef52177d03858a4e05111697 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 25 Nov 2019 14:33:12 -0800 Subject: [PATCH 054/103] Draw transparent background in WebGL renderer Part of #2252 --- addons/xterm-addon-webgl/src/RectangleRenderer.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/addons/xterm-addon-webgl/src/RectangleRenderer.ts b/addons/xterm-addon-webgl/src/RectangleRenderer.ts index b52a506e..337ceace 100644 --- a/addons/xterm-addon-webgl/src/RectangleRenderer.ts +++ b/addons/xterm-addon-webgl/src/RectangleRenderer.ts @@ -22,13 +22,13 @@ const enum VertexAttribLocations { const vertexShaderSource = `#version 300 es layout (location = ${VertexAttribLocations.POSITION}) in vec2 a_position; layout (location = ${VertexAttribLocations.SIZE}) in vec2 a_size; -layout (location = ${VertexAttribLocations.COLOR}) in vec3 a_color; +layout (location = ${VertexAttribLocations.COLOR}) in vec4 a_color; layout (location = ${VertexAttribLocations.UNIT_QUAD}) in vec2 a_unitquad; uniform mat4 u_projection; uniform vec2 u_resolution; -out vec3 v_color; +out vec4 v_color; void main() { vec2 zeroToOne = (a_position + (a_unitquad * a_size)) / u_resolution; @@ -39,12 +39,12 @@ void main() { const fragmentShaderSource = `#version 300 es precision lowp float; -in vec3 v_color; +in vec4 v_color; out vec4 outColor; void main() { - outColor = vec4(v_color, 1); + outColor = v_color; }`; interface IVertices { @@ -155,6 +155,7 @@ export class RectangleRenderer { private _updateCachedColors(): void { this._bgFloat = this._colorToFloat32Array(this._colors.background); + console.log('bgFloat', this._colors.background, this._bgFloat); this._selectionFloat = this._colorToFloat32Array(this._colors.selectionOpaque); } From 1ff1809ab75be9a9aeee134e6dd5c040f68efda0 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 25 Nov 2019 15:04:07 -0800 Subject: [PATCH 055/103] Support opaque inverse background in webgl, add test --- .../src/RectangleRenderer.ts | 1 - .../src/WebglRenderer.api.ts | 22 +++++++++++++++---- .../src/atlas/WebglCharAtlas.ts | 7 +++++- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/addons/xterm-addon-webgl/src/RectangleRenderer.ts b/addons/xterm-addon-webgl/src/RectangleRenderer.ts index 337ceace..2246cddd 100644 --- a/addons/xterm-addon-webgl/src/RectangleRenderer.ts +++ b/addons/xterm-addon-webgl/src/RectangleRenderer.ts @@ -155,7 +155,6 @@ export class RectangleRenderer { private _updateCachedColors(): void { this._bgFloat = this._colorToFloat32Array(this._colors.background); - console.log('bgFloat', this._colors.background, this._bgFloat); this._selectionFloat = this._colorToFloat32Array(this._colors.selectionOpaque); } diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.api.ts b/addons/xterm-addon-webgl/src/WebglRenderer.api.ts index e26e054a..bf877dd8 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.api.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.api.ts @@ -702,6 +702,22 @@ describe('WebGL Renderer Integration Tests', function(): void { await pollFor(page, () => getCellColor(8, 2), [64, 64, 64, 255]); }); }); + + describe('allowTransparency', async () => { + before(async () => setupBrowser({ rendererType: 'dom', allowTransparency: true})); + after(async () => browser.close()); + beforeEach(async () => page.evaluate(`window.term.reset()`)); + it('transparent background inverse', async () => { + const theme: ITheme = { + background: '#ff000080' + }; + await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`); + const data = `\\x1b[7m█\x1b[0m`; + await writeSync(data); + // Inverse background should be opaque + await pollFor(page, () => getCellColor(1, 1), [255, 0, 0, 255]); + }); + }); }); async function openTerminal(options: ITerminalOptions = {}): Promise { @@ -732,7 +748,7 @@ async function getCellColor(col: number, row: number): Promise { return await page.evaluate(`Array.from(window.result)`); } -async function setupBrowser(): Promise { +async function setupBrowser(options: ITerminalOptions = { rendererType: 'dom' }): Promise { browser = await puppeteer.launch({ headless: process.argv.indexOf('--headless') !== -1, args: [`--window-size=${width},${height}`, `--no-sandbox`] @@ -740,9 +756,7 @@ async function setupBrowser(): Promise { page = (await browser.pages())[0]; await page.setViewport({ width, height }); await page.goto(APP); - await openTerminal({ - rendererType: 'dom' - }); + await openTerminal(options); await page.evaluate(` window.addon = new WebglAddon(true); window.term.loadAddon(window.addon); diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index 097c420d..c96c2a69 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -228,7 +228,12 @@ export class WebglCharAtlas implements IDisposable { case Attributes.CM_DEFAULT: default: if (inverse) { - return this._config.colors.background.css; + const bg = this._config.colors.background.css; + if (bg.length === 9) { + // Remove bg alpha channel if present + return bg.substr(0, 7); + } + return bg; } return this._config.colors.foreground.css; } From c630b92772fcc9f4a0b544be12cb33b1a9e4a926 Mon Sep 17 00:00:00 2001 From: Nick Pezza Date: Sat, 30 Nov 2019 14:37:07 -0500 Subject: [PATCH 056/103] 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 543b3d6c9f45b2b2b5141e02c20cca0b76a1cfc8 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 4 Dec 2019 10:54:17 -0800 Subject: [PATCH 057/103] Fix minimumContrastRatio on dom/truecolor Fixes #2593 --- src/browser/Color.ts | 2 +- .../renderer/dom/DomRendererRowFactory.ts | 19 +++++++++++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/browser/Color.ts b/src/browser/Color.ts index e20f62e6..e40ff9e1 100644 --- a/src/browser/Color.ts +++ b/src/browser/Color.ts @@ -108,7 +108,7 @@ export function contrastRatio(l1: number, l2: number): number { return (l1 + 0.05) / (l2 + 0.05); } -function rgbaToColor(r: number, g: number, b: number): IColor { +export function rgbaToColor(r: number, g: number, b: number): IColor { return { css: toCss(r, g, b), rgba: toRgba(r, g, b) diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts index cffc5624..bd922f57 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 } from 'browser/Color'; +import { ensureContrastRatio, rgbaToColor } from 'browser/Color'; import { IColorSet, IColor } from 'browser/Types'; export const BOLD_CLASS = 'xterm-bold'; @@ -129,7 +129,14 @@ export class DomRendererRowFactory { } break; case Attributes.CM_RGB: - charElement.setAttribute('style', `${charElement.getAttribute('style') || ''}color:#${padStart(fg.toString(16), '0', 6)};`); + const color = rgbaToColor( + (fg >> 16) & 0xFF, + (fg >> 8) & 0xFF, + (fg ) & 0xFF + ); + if (!this._applyMinimumContrast(charElement, this._colors.background, color)) { + this._addStyle(charElement, `color:#${padStart(fg.toString(16), '0', 6)}`); + } break; case Attributes.CM_DEFAULT: default: @@ -147,7 +154,7 @@ export class DomRendererRowFactory { charElement.classList.add(`xterm-bg-${bg}`); break; case Attributes.CM_RGB: - charElement.setAttribute('style', `${charElement.getAttribute('style') || ''}background-color:#${padStart(bg.toString(16), '0', 6)};`); + this._addStyle(charElement, `background-color:#${padStart(bg.toString(16), '0', 6)}`); break; case Attributes.CM_DEFAULT: default: @@ -176,12 +183,16 @@ export class DomRendererRowFactory { } if (adjustedColor) { - element.setAttribute('style', `${element.getAttribute('style') || ''}color:${adjustedColor.css}`); + this._addStyle(element, `color:${adjustedColor.css}`); return true; } return false; } + + private _addStyle(element: HTMLElement, style: string): void { + element.setAttribute('style', `${element.getAttribute('style') || ''}${style};`); + } } function padStart(text: string, padChar: string, length: number): string { From c9babb9477324e49d5ce7a0a3f087b8f29755741 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 5 Dec 2019 12:34:36 -0800 Subject: [PATCH 058/103] v4.3.0 --- addons/xterm-addon-attach/package.json | 2 +- addons/xterm-addon-search/package.json | 2 +- addons/xterm-addon-webgl/package.json | 2 +- package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/addons/xterm-addon-attach/package.json b/addons/xterm-addon-attach/package.json index 5718f356..dd134308 100644 --- a/addons/xterm-addon-attach/package.json +++ b/addons/xterm-addon-attach/package.json @@ -1,6 +1,6 @@ { "name": "xterm-addon-attach", - "version": "0.3.0", + "version": "0.4.0", "author": { "name": "The xterm.js authors", "url": "https://xtermjs.org/" diff --git a/addons/xterm-addon-search/package.json b/addons/xterm-addon-search/package.json index c426ee3b..d66e8a68 100644 --- a/addons/xterm-addon-search/package.json +++ b/addons/xterm-addon-search/package.json @@ -1,6 +1,6 @@ { "name": "xterm-addon-search", - "version": "0.3.0", + "version": "0.4.0", "author": { "name": "The xterm.js authors", "url": "https://xtermjs.org/" diff --git a/addons/xterm-addon-webgl/package.json b/addons/xterm-addon-webgl/package.json index a6befe08..21e394d1 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.3.0", + "version": "0.4.0", "author": { "name": "The xterm.js authors", "url": "https://xtermjs.org/" diff --git a/package.json b/package.json index c37594af..e04a1ff8 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "xterm", "description": "Full xterm terminal, in your browser", - "version": "4.2.0", + "version": "4.3.0", "main": "lib/xterm.js", "style": "css/xterm.css", "types": "typings/xterm.d.ts", From c5e144f327e7990853f2623f05562d877e133058 Mon Sep 17 00:00:00 2001 From: Steven Degutis Date: Fri, 6 Dec 2019 15:57:26 -0600 Subject: [PATCH 059/103] Avoid roundtrip to browser when double-disposing. --- src/browser/Lifecycle.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/browser/Lifecycle.ts b/src/browser/Lifecycle.ts index 1344ea9b..a8172db5 100644 --- a/src/browser/Lifecycle.ts +++ b/src/browser/Lifecycle.ts @@ -17,12 +17,13 @@ export function addDisposableDomListener( useCapture?: boolean ): IDisposable { node.addEventListener(type, handler, useCapture); + let disposed = false; return { dispose: () => { - if (!handler) { - // Already disposed + if (!disposed) { return; } + disposed = true; node.removeEventListener(type, handler, useCapture); } }; From 58078459f874fd535a6fc59c711c66a6f8242f23 Mon Sep 17 00:00:00 2001 From: Nick Pezza Date: Sat, 7 Dec 2019 15:38:12 -0500 Subject: [PATCH 060/103] 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 061/103] 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 062/103] 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 063/103] 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 064/103] 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 065/103] 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 066/103] 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 067/103] 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 068/103] 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 069/103] 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 070/103] 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 071/103] 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 072/103] 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 073/103] 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 074/103] 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 075/103] 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 076/103] 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 077/103] 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 078/103] 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 079/103] 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 080/103] 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 081/103] 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 082/103] 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 083/103] 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 084/103] 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 085/103] 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 086/103] 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 087/103] 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 088/103] 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 089/103] 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 090/103] 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 091/103] 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 092/103] 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 093/103] 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 094/103] 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 095/103] 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 096/103] 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 097/103] 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 098/103] 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 099/103] 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 100/103] 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 101/103] 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 102/103] 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 103/103] 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"