From c23f4313fb186f02871047cdafec338355a4234d Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 15 Sep 2022 12:20:13 -0700 Subject: [PATCH 01/28] Release v5, including addon version bumps --- addons/xterm-addon-attach/package.json | 2 +- addons/xterm-addon-canvas/package.json | 2 +- addons/xterm-addon-fit/package.json | 2 +- addons/xterm-addon-ligatures/package.json | 2 +- addons/xterm-addon-search/package.json | 2 +- addons/xterm-addon-serialize/package.json | 2 +- addons/xterm-addon-unicode11/package.json | 2 +- addons/xterm-addon-web-links/package.json | 2 +- addons/xterm-addon-webgl/package.json | 2 +- package.json | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/addons/xterm-addon-attach/package.json b/addons/xterm-addon-attach/package.json index c6225b46..43a80322 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.6.0", + "version": "0.7.0", "author": { "name": "The xterm.js authors", "url": "https://xtermjs.org/" diff --git a/addons/xterm-addon-canvas/package.json b/addons/xterm-addon-canvas/package.json index fdc2d93a..2555e75d 100644 --- a/addons/xterm-addon-canvas/package.json +++ b/addons/xterm-addon-canvas/package.json @@ -1,6 +1,6 @@ { "name": "xterm-addon-canvas", - "version": "0.1.0", + "version": "0.2.0", "author": { "name": "The xterm.js authors", "url": "https://xtermjs.org/" diff --git a/addons/xterm-addon-fit/package.json b/addons/xterm-addon-fit/package.json index 1d2f6dd5..ddff6e47 100644 --- a/addons/xterm-addon-fit/package.json +++ b/addons/xterm-addon-fit/package.json @@ -1,6 +1,6 @@ { "name": "xterm-addon-fit", - "version": "0.5.0", + "version": "0.6.0", "author": { "name": "The xterm.js authors", "url": "https://xtermjs.org/" diff --git a/addons/xterm-addon-ligatures/package.json b/addons/xterm-addon-ligatures/package.json index 525f377c..e62be196 100644 --- a/addons/xterm-addon-ligatures/package.json +++ b/addons/xterm-addon-ligatures/package.json @@ -1,6 +1,6 @@ { "name": "xterm-addon-ligatures", - "version": "0.5.3", + "version": "0.6.0", "description": "Add support for programming ligatures to xterm.js", "author": { "name": "The xterm.js authors", diff --git a/addons/xterm-addon-search/package.json b/addons/xterm-addon-search/package.json index 8a75c812..f8267f52 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.9.0", + "version": "0.10.0", "author": { "name": "The xterm.js authors", "url": "https://xtermjs.org/" diff --git a/addons/xterm-addon-serialize/package.json b/addons/xterm-addon-serialize/package.json index 94562688..1ba3a34f 100644 --- a/addons/xterm-addon-serialize/package.json +++ b/addons/xterm-addon-serialize/package.json @@ -1,6 +1,6 @@ { "name": "xterm-addon-serialize", - "version": "0.7.0", + "version": "0.8.0", "author": { "name": "The xterm.js authors", "url": "https://xtermjs.org/" diff --git a/addons/xterm-addon-unicode11/package.json b/addons/xterm-addon-unicode11/package.json index 275cca4a..488077e7 100644 --- a/addons/xterm-addon-unicode11/package.json +++ b/addons/xterm-addon-unicode11/package.json @@ -1,6 +1,6 @@ { "name": "xterm-addon-unicode11", - "version": "0.3.0", + "version": "0.4.0", "author": { "name": "The xterm.js authors", "url": "https://xtermjs.org/" diff --git a/addons/xterm-addon-web-links/package.json b/addons/xterm-addon-web-links/package.json index 2cfbae6c..d3e6f863 100644 --- a/addons/xterm-addon-web-links/package.json +++ b/addons/xterm-addon-web-links/package.json @@ -1,6 +1,6 @@ { "name": "xterm-addon-web-links", - "version": "0.6.0", + "version": "0.7.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 45b04897..47dcac23 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.12.0", + "version": "0.13.0", "author": { "name": "The xterm.js authors", "url": "https://xtermjs.org/" diff --git a/package.json b/package.json index a270a52f..a36ad5c9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "xterm", "description": "Full xterm terminal, in your browser", - "version": "4.19.0", + "version": "5.0.0", "main": "lib/xterm.js", "style": "css/xterm.css", "types": "typings/xterm.d.ts", From 73cb96ddec39e3531d9b55efde01cd7ba538dff7 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 15 Sep 2022 12:39:32 -0700 Subject: [PATCH 02/28] Remove special v5 beta publishing --- bin/publish.js | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/bin/publish.js b/bin/publish.js index 6b5abcd9..91c6d09a 100644 --- a/bin/publish.js +++ b/bin/publish.js @@ -98,13 +98,8 @@ function getNextBetaVersion(packageJson) { process.exit(1); } const tag = 'beta'; - let nextStableVersion; - if (packageJson.name === 'xterm') { - nextStableVersion = `5.0.0`; - } else { - const stableVersion = packageJson.version.split('.'); - nextStableVersion = `${stableVersion[0]}.${parseInt(stableVersion[1]) + 1}.0`; - } + const stableVersion = packageJson.version.split('.'); + const nextStableVersion = `${stableVersion[0]}.${parseInt(stableVersion[1]) + 1}.0`; const publishedVersions = getPublishedVersions(packageJson, nextStableVersion, tag); if (publishedVersions.length === 0) { return `${nextStableVersion}-${tag}.1`; From 9fb84cb89df6754d06c470748825aa0cdd71077a Mon Sep 17 00:00:00 2001 From: TatLead Date: Mon, 19 Sep 2022 05:53:50 +0800 Subject: [PATCH 03/28] Update xterm-headless.d.ts --- typings/xterm-headless.d.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/typings/xterm-headless.d.ts b/typings/xterm-headless.d.ts index 1c0a986a..7453b7e2 100644 --- a/typings/xterm-headless.d.ts +++ b/typings/xterm-headless.d.ts @@ -19,8 +19,7 @@ declare module 'xterm-headless' { export interface ITerminalOptions { /** * Whether to allow the use of proposed API. When false, any usage of APIs - * marked as experimental/proposed will throw an error. This defaults to - * true currently, but will change to false in v5.0. + * marked as experimental/proposed will throw an error. The default is false. */ allowProposedApi?: boolean; From 11c806a4d034a8247de9f40d1e74b24fe7fcef09 Mon Sep 17 00:00:00 2001 From: TatLead Date: Mon, 19 Sep 2022 05:54:11 +0800 Subject: [PATCH 04/28] Update xterm.d.ts --- typings/xterm.d.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index d66e3b03..0e2b0357 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -26,8 +26,7 @@ declare module 'xterm' { export interface ITerminalOptions { /** * Whether to allow the use of proposed API. When false, any usage of APIs - * marked as experimental/proposed will throw an error. This defaults to - * true currently, but will change to false in v5.0. + * marked as experimental/proposed will throw an error. The default is false. */ allowProposedApi?: boolean; From 78128762f921e7ea1dcb286a75586fae2e1d21d2 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 22 Sep 2022 13:06:08 -0700 Subject: [PATCH 05/28] Perform texture atlas warm up in an idle callback Part of #4103 --- addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index b4593d90..9ba83de6 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -121,7 +121,9 @@ export class WebglCharAtlas implements IDisposable { public warmUp(): void { if (!this._didWarmUp) { - this._doWarmUp(); + (typeof requestIdleCallback !== 'function' ? requestIdleCallback : setTimeout)(() => { + this._doWarmUp(); + }); this._didWarmUp = true; } } @@ -129,8 +131,10 @@ export class WebglCharAtlas implements IDisposable { private _doWarmUp(): void { // Pre-fill with ASCII 33-126 for (let i = 33; i < 126; i++) { - const rasterizedGlyph = this._drawToCache(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT); - this._cacheMap.set(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT, rasterizedGlyph); + if (!this._cacheMap.get(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT)) { + const rasterizedGlyph = this._drawToCache(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT); + this._cacheMap.set(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT, rasterizedGlyph); + } } } From 09a5da5d33b14f93d888736faa975a2fa14e34d1 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 23 Sep 2022 10:11:24 -0700 Subject: [PATCH 06/28] Fix width of upper 1/8 block char See microsoft/vscode#161323 --- src/browser/renderer/CustomGlyphs.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browser/renderer/CustomGlyphs.ts b/src/browser/renderer/CustomGlyphs.ts index 4c3874c7..32256cf4 100644 --- a/src/browser/renderer/CustomGlyphs.ts +++ b/src/browser/renderer/CustomGlyphs.ts @@ -33,7 +33,7 @@ export const blockElementDefinitions: { [index: string]: IBlockVector[] | undefi '▐': [{ x: 4, y: 0, w: 4, h: 8 }], // RIGHT HALF BLOCK // Block elements (0x2594-0x2595) - '▔': [{ x: 0, y: 0, w: 9, h: 1 }], // UPPER ONE EIGHTH BLOCK + '▔': [{ x: 0, y: 0, w: 8, h: 1 }], // UPPER ONE EIGHTH BLOCK '▕': [{ x: 7, y: 0, w: 1, h: 8 }], // RIGHT ONE EIGHTH BLOCK // Terminal graphic characters (0x2596-0x259F) From d62b4531c3a6e72b964c4a6c4e8706a2b55be9fa Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 24 Sep 2022 06:58:01 -0700 Subject: [PATCH 07/28] Optimize contexts for reading frequently This was a recommendation from Edge. Docs here: https://html.spec.whatwg.org/multipage/canvas.html#concept-canvas-will-read-frequently --- addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts | 5 ++++- src/browser/ColorManager.ts | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index 9ba83de6..092cf2e5 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -110,7 +110,10 @@ export class WebglCharAtlas implements IDisposable { this._tmpCanvas = document.createElement('canvas'); this._tmpCanvas.width = this._config.scaledCellWidth * 4 + TMP_CANVAS_GLYPH_PADDING * 2; this._tmpCanvas.height = this._config.scaledCellHeight + TMP_CANVAS_GLYPH_PADDING * 2; - this._tmpCtx = throwIfFalsy(this._tmpCanvas.getContext('2d', { alpha: this._config.allowTransparency })); + this._tmpCtx = throwIfFalsy(this._tmpCanvas.getContext('2d', { + alpha: this._config.allowTransparency, + willReadFrequently: true + })); } public dispose(): void { diff --git a/src/browser/ColorManager.ts b/src/browser/ColorManager.ts index d8dcde6f..4ec0053c 100644 --- a/src/browser/ColorManager.ts +++ b/src/browser/ColorManager.ts @@ -89,7 +89,9 @@ export class ColorManager implements IColorManager { const canvas = document.createElement('canvas'); canvas.width = 1; canvas.height = 1; - const ctx = canvas.getContext('2d'); + const ctx = canvas.getContext('2d', { + willReadFrequently: true + }); if (!ctx) { throw new Error('Could not get rendering context'); } From 81d68b115c8f86269c6a91e6641ad7cf458382b2 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 24 Sep 2022 10:00:22 -0700 Subject: [PATCH 08/28] Share rgba vars throughout Color.ts, fast setTheme parseColor --- src/browser/ColorManager.ts | 20 ++++++++---- src/common/Color.ts | 63 ++++++++++++++++++++----------------- 2 files changed, 48 insertions(+), 35 deletions(-) diff --git a/src/browser/ColorManager.ts b/src/browser/ColorManager.ts index d8dcde6f..5bd6319c 100644 --- a/src/browser/ColorManager.ts +++ b/src/browser/ColorManager.ts @@ -220,21 +220,29 @@ export class ColorManager implements IColorManager { } private _parseColor( - css: string | undefined, + cssString: string | undefined, fallback: IColor, allowTransparency: boolean = this.allowTransparency ): IColor { - if (css === undefined) { + if (cssString === undefined) { return fallback; } + // Fast path: avoid parsing via canvas if it looks like #RGB[A] or #RRGGBB[AA] + if (cssString.startsWith('#')) { + const c = css.toColor(cssString); + if (c) { + return c; + } + } + // If parsing the value results in failure, then it must be ignored, and the attribute must // retain its previous value. // -- https://html.spec.whatwg.org/multipage/canvas.html#fill-and-stroke-styles this._ctx.fillStyle = this._litmusColor; - this._ctx.fillStyle = css; + this._ctx.fillStyle = cssString; if (typeof this._ctx.fillStyle !== 'string') { - console.warn(`Color: ${css} is invalid using fallback ${fallback.css}`); + console.warn(`Color: ${cssString} is invalid using fallback ${fallback.css}`); return fallback; } @@ -261,7 +269,7 @@ export class ColorManager implements IColorManager { // // So let's just use the fallback color in this case instead. console.warn( - `Color: ${css} is using transparency, but allowTransparency is false. ` + + `Color: ${cssString} is using transparency, but allowTransparency is false. ` + `Using fallback ${fallback.css}.` ); return fallback; @@ -274,7 +282,7 @@ export class ColorManager implements IColorManager { const rgba: number = channels.toRgba(r, g, b, alpha); return { rgba, - css + css: cssString }; } diff --git a/src/common/Color.ts b/src/common/Color.ts index a66e39c1..a9f8cc02 100644 --- a/src/common/Color.ts +++ b/src/common/Color.ts @@ -5,6 +5,11 @@ import { IColor, IColorRGB } from 'common/Types'; +let $r = 0; +let $g = 0; +let $b = 0; +let $a = 0; + /** * Helper functions where the source type is "channels" (individual color channels as numbers). */ @@ -29,8 +34,8 @@ export namespace channels { */ export namespace color { export function blend(bg: IColor, fg: IColor): IColor { - const a = (fg.rgba & 0xFF) / 255; - if (a === 1) { + $a = (fg.rgba & 0xFF) / 255; + if ($a === 1) { return { css: fg.css, rgba: fg.rgba @@ -42,11 +47,11 @@ export namespace color { 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); + $r = bgR + Math.round((fgR - bgR) * $a); + $g = bgG + Math.round((fgG - bgG) * $a); + $b = bgB + Math.round((fgB - bgB) * $a); + const css = channels.toCss($r, $g, $b); + const rgba = channels.toRgba($r, $g, $b); return { css, rgba }; } @@ -68,25 +73,25 @@ export namespace color { export function opaque(color: IColor): IColor { const rgbaColor = (color.rgba | 0xFF) >>> 0; - const [r, g, b] = rgba.toChannels(rgbaColor); + [$r, $g, $b] = rgba.toChannels(rgbaColor); return { - css: channels.toCss(r, g, b), + css: channels.toCss($r, $g, $b), rgba: rgbaColor }; } export function opacity(color: IColor, opacity: number): IColor { - const a = Math.round(opacity * 0xFF); - const [r, g, b] = rgba.toChannels(color.rgba); + $a = Math.round(opacity * 0xFF); + [$r, $g, $b] = rgba.toChannels(color.rgba); return { - css: channels.toCss(r, g, b, a), - rgba: channels.toRgba(r, g, b, a) + css: channels.toCss($r, $g, $b, $a), + rgba: channels.toRgba($r, $g, $b, $a) }; } export function multiplyOpacity(color: IColor, factor: number): IColor { - const a = color.rgba & 0xFF; - return opacity(color, (a * factor) / 0xFF); + $a = color.rgba & 0xFF; + return opacity(color, ($a * factor) / 0xFF); } export function toColorRGB(color: IColor): IColorRGB { @@ -102,17 +107,17 @@ export namespace css { if (css.match(/#[0-9a-f]{3,8}/i)) { switch (css.length) { case 4: { // #rgb - const r = parseInt(css.slice(1, 2).repeat(2), 16); - const g = parseInt(css.slice(2, 3).repeat(2), 16); - const b = parseInt(css.slice(3, 4).repeat(2), 16); - return rgba.toColor(r, g, b); + $r = parseInt(css.slice(1, 2).repeat(2), 16); + $g = parseInt(css.slice(2, 3).repeat(2), 16); + $b = parseInt(css.slice(3, 4).repeat(2), 16); + return rgba.toColor($r, $g, $b); } case 5: { // #rgba - const r = parseInt(css.slice(1, 2).repeat(2), 16); - const g = parseInt(css.slice(2, 3).repeat(2), 16); - const b = parseInt(css.slice(3, 4).repeat(2), 16); - const a = parseInt(css.slice(4, 5).repeat(2), 16); - return rgba.toColor(r, g, b, a); + $r = parseInt(css.slice(1, 2).repeat(2), 16); + $g = parseInt(css.slice(2, 3).repeat(2), 16); + $b = parseInt(css.slice(3, 4).repeat(2), 16); + $a = parseInt(css.slice(4, 5).repeat(2), 16); + return rgba.toColor($r, $g, $b, $a); } case 7: // #rrggbb return { @@ -128,11 +133,11 @@ export namespace css { } const rgbaMatch = css.match(/rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(,\s*(0|1|\d?\.(\d+))\s*)?\)/); if (rgbaMatch) { // rgb() or rgba() - const r = parseInt(rgbaMatch[1]); - const g = parseInt(rgbaMatch[2]); - const b = parseInt(rgbaMatch[3]); - const a = Math.round((rgbaMatch[5] === undefined ? 1 : parseFloat(rgbaMatch[5])) * 0xFF); - return rgba.toColor(r, g, b, a); + $r = parseInt(rgbaMatch[1]); + $g = parseInt(rgbaMatch[2]); + $b = parseInt(rgbaMatch[3]); + $a = Math.round((rgbaMatch[5] === undefined ? 1 : parseFloat(rgbaMatch[5])) * 0xFF); + return rgba.toColor($r, $g, $b, $a); } throw new Error('css.toColor: Unsupported css format'); } From 6368cb4cff68d41a28db24e9d1af72973b7d9a64 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 24 Sep 2022 10:37:40 -0700 Subject: [PATCH 09/28] Move canvas-based parsing to Color.ts lib This removed the allowTransparency code, it didn't actually do much before, just whether the 'advanced parsing' would allow transparency. Since we already do the common formats, I'm not even sure what other format it could be. --- .../src/SerializeAddon.test.ts | 2 +- src/browser/ColorManager.test.ts | 2 +- src/browser/ColorManager.ts | 101 +++--------------- src/browser/Terminal.ts | 2 +- src/common/Color.ts | 57 +++++++++- src/common/Platform.ts | 2 +- 6 files changed, 72 insertions(+), 94 deletions(-) diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.test.ts b/addons/xterm-addon-serialize/src/SerializeAddon.test.ts index 05f2c61c..df4a72da 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.test.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.test.ts @@ -78,7 +78,7 @@ describe('xterm-addon-serialize', () => { terminal.loadAddon(serializeAddon); selectionService = new TestSelectionService((terminal as any)._core._bufferService); - cm = new ColorManager(document, false); + cm = new ColorManager(); (terminal as any)._core._colorManager = cm; (terminal as any)._core._selectionService = selectionService; }); diff --git a/src/browser/ColorManager.test.ts b/src/browser/ColorManager.test.ts index 019bf42a..cf60a1f5 100644 --- a/src/browser/ColorManager.test.ts +++ b/src/browser/ColorManager.test.ts @@ -28,7 +28,7 @@ describe('ColorManager', () => { return {data: [0, 0, 0, 0xFF]}; } }); - cm = new ColorManager(document, false); + cm = new ColorManager(); }); describe('constructor', () => { diff --git a/src/browser/ColorManager.ts b/src/browser/ColorManager.ts index 5bd6319c..a22a423f 100644 --- a/src/browser/ColorManager.ts +++ b/src/browser/ColorManager.ts @@ -80,22 +80,11 @@ export const DEFAULT_ANSI_COLORS = Object.freeze((() => { */ export class ColorManager implements IColorManager { public colors: IColorSet; - private _ctx: CanvasRenderingContext2D; - private _litmusColor: CanvasGradient; + private _contrastCache: IColorContrastCache; private _restoreColors!: IRestoreColorSet; - constructor(document: Document, public allowTransparency: boolean) { - const canvas = document.createElement('canvas'); - canvas.width = 1; - canvas.height = 1; - const ctx = canvas.getContext('2d'); - if (!ctx) { - throw new Error('Could not get rendering context'); - } - this._ctx = ctx; - this._ctx.globalCompositeOperation = 'copy'; - this._litmusColor = this._ctx.createLinearGradient(0, 0, 1, 1); + constructor() { this._contrastCache = new ColorContrastCache(); this.colors = { foreground: DEFAULT_FOREGROUND, @@ -118,9 +107,6 @@ export class ColorManager implements IColorManager { case 'minimumContrastRatio': this._contrastCache.clear(); break; - case 'allowTransparency': - this.allowTransparency = value; - break; } } @@ -132,11 +118,11 @@ export class ColorManager implements IColorManager { public setTheme(theme: ITheme = {}): void { this.colors.foreground = this._parseColor(theme.foreground, DEFAULT_FOREGROUND); this.colors.background = this._parseColor(theme.background, DEFAULT_BACKGROUND); - this.colors.cursor = this._parseColor(theme.cursor, DEFAULT_CURSOR, true); - this.colors.cursorAccent = this._parseColor(theme.cursorAccent, DEFAULT_CURSOR_ACCENT, true); - this.colors.selectionBackgroundTransparent = this._parseColor(theme.selectionBackground, DEFAULT_SELECTION, true); + this.colors.cursor = this._parseColor(theme.cursor, DEFAULT_CURSOR); + this.colors.cursorAccent = this._parseColor(theme.cursorAccent, DEFAULT_CURSOR_ACCENT); + this.colors.selectionBackgroundTransparent = this._parseColor(theme.selectionBackground, DEFAULT_SELECTION); this.colors.selectionBackgroundOpaque = color.blend(this.colors.background, this.colors.selectionBackgroundTransparent); - this.colors.selectionInactiveBackgroundTransparent = this._parseColor(theme.selectionInactiveBackground, this.colors.selectionBackgroundTransparent, true); + this.colors.selectionInactiveBackgroundTransparent = this._parseColor(theme.selectionInactiveBackground, this.colors.selectionBackgroundTransparent); this.colors.selectionInactiveBackgroundOpaque = color.blend(this.colors.background, this.colors.selectionInactiveBackgroundTransparent); const nullColor: IColor = { css: '', @@ -221,76 +207,15 @@ export class ColorManager implements IColorManager { private _parseColor( cssString: string | undefined, - fallback: IColor, - allowTransparency: boolean = this.allowTransparency + fallback: IColor ): IColor { - if (cssString === undefined) { - return fallback; - } - - // Fast path: avoid parsing via canvas if it looks like #RGB[A] or #RRGGBB[AA] - if (cssString.startsWith('#')) { - const c = css.toColor(cssString); - if (c) { - return c; + if (cssString !== undefined) { + try { + return css.toColor(cssString); + } catch { + // no-op } } - - // If parsing the value results in failure, then it must be ignored, and the attribute must - // retain its previous value. - // -- https://html.spec.whatwg.org/multipage/canvas.html#fill-and-stroke-styles - this._ctx.fillStyle = this._litmusColor; - this._ctx.fillStyle = cssString; - if (typeof this._ctx.fillStyle !== 'string') { - console.warn(`Color: ${cssString} is invalid using fallback ${fallback.css}`); - return fallback; - } - - this._ctx.fillRect(0, 0, 1, 1); - const data = this._ctx.getImageData(0, 0, 1, 1).data; - - // 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: ${cssString} is using transparency, but allowTransparency is false. ` + - `Using fallback ${fallback.css}.` - ); - return fallback; - } - - // 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 = channels.toRgba(r, g, b, alpha); - return { - rgba, - css: cssString - }; - } - - return { - // 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: channels.toRgba(data[0], data[1], data[2], data[3]) - }; + return fallback; } } diff --git a/src/browser/Terminal.ts b/src/browser/Terminal.ts index 9aba3818..afdac748 100644 --- a/src/browser/Terminal.ts +++ b/src/browser/Terminal.ts @@ -502,7 +502,7 @@ export class Terminal extends CoreTerminal implements ITerminal { this._instantiationService.setService(ICharSizeService, this._charSizeService); this._theme = this.options.theme || this._theme; - this._colorManager = new ColorManager(document, this.options.allowTransparency); + this._colorManager = new ColorManager(); this.register(this.optionsService.onOptionChange(e => this._colorManager!.onOptionsChange(e, this.optionsService.rawOptions[e]))); this._colorManager.setTheme(this._theme); diff --git a/src/common/Color.ts b/src/common/Color.ts index a9f8cc02..35bcdb88 100644 --- a/src/common/Color.ts +++ b/src/common/Color.ts @@ -3,6 +3,7 @@ * @license MIT */ +import { isNode } from 'common/Platform'; import { IColor, IColorRGB } from 'common/Types'; let $r = 0; @@ -103,7 +104,29 @@ export namespace color { * Helper functions where the source type is "css" (string: '#rgb', '#rgba', '#rrggbb', '#rrggbbaa'). */ export namespace css { + let $ctx: CanvasRenderingContext2D | undefined; + let $litmusColor: CanvasGradient | undefined; + if (!isNode) { + const canvas = document.createElement('canvas'); + canvas.width = 1; + canvas.height = 1; + const ctx = canvas.getContext('2d'); + if (ctx) { + $ctx = ctx; + $ctx.globalCompositeOperation = 'copy'; + $litmusColor = $ctx.createLinearGradient(0, 0, 1, 1); + } + } + + /** + * Converts a css string to an IColor, this should handle all valid CSS color strings and will + * throw if it's invalid. The ideal format to use is `#rrggbb[aa]` as it's the fastest to parse. + * + * Only `#rgb[a]`, `#rrggbb[aa]`, `rgb()` and `rgba()` formats are supported when run in a Node + * environment. + */ export function toColor(css: string): IColor { + // Formats: #rgb[a] and #rrggbb[aa] if (css.match(/#[0-9a-f]{3,8}/i)) { switch (css.length) { case 4: { // #rgb @@ -131,15 +154,45 @@ export namespace css { }; } } + + // Formats: rgb() or rgba() const rgbaMatch = css.match(/rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(,\s*(0|1|\d?\.(\d+))\s*)?\)/); - if (rgbaMatch) { // rgb() or rgba() + if (rgbaMatch) { $r = parseInt(rgbaMatch[1]); $g = parseInt(rgbaMatch[2]); $b = parseInt(rgbaMatch[3]); $a = Math.round((rgbaMatch[5] === undefined ? 1 : parseFloat(rgbaMatch[5])) * 0xFF); return rgba.toColor($r, $g, $b, $a); } - throw new Error('css.toColor: Unsupported css format'); + + // Validate the context is available for canvas-based color parsing + if (!$ctx || !$litmusColor) { + throw new Error('css.toColor: Unsupported css format'); + } + + // Validate the color using canvas fillStyle + // See https://html.spec.whatwg.org/multipage/canvas.html#fill-and-stroke-styles + $ctx.fillStyle = $litmusColor; + $ctx.fillStyle = css; + if (typeof $ctx.fillStyle !== 'string') { + throw new Error('css.toColor: Unsupported css format'); + } + + $ctx.fillRect(0, 0, 1, 1); + [$r, $g, $b, $a] = $ctx.getImageData(0, 0, 1, 1).data; + + // Validate the color is non-transparent as color hue gets lost when drawn to the canvas + if ($a !== 0xFF) { + throw new Error('css.toColor: Unsupported css format'); + } + + // Extract the color from the canvas' fillStyle property which exposes the color value in rgba() + // format + // See https://html.spec.whatwg.org/multipage/canvas.html#serialisation-of-a-color + return { + rgba: channels.toRgba($r, $g, $b, $a), + css + }; } } diff --git a/src/common/Platform.ts b/src/common/Platform.ts index 7b823b12..6be0584f 100644 --- a/src/common/Platform.ts +++ b/src/common/Platform.ts @@ -13,7 +13,7 @@ interface INavigator { // we want this module to live in common. declare const navigator: INavigator; -const isNode = (typeof navigator === 'undefined') ? true : false; +export const isNode = (typeof navigator === 'undefined') ? true : false; const userAgent = (isNode) ? 'node' : navigator.userAgent; const platform = (isNode) ? 'node' : navigator.platform; From c42f290eb8cadea56eff5b0f6dbfb995b4048c9e Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 24 Sep 2022 11:10:26 -0700 Subject: [PATCH 10/28] Do char atlas warmup via new IdleTaskQueue --- .../src/atlas/WebglCharAtlas.ts | 18 ++++---- src/common/IdleTaskQueue.ts | 41 +++++++++++++++++++ 2 files changed, 51 insertions(+), 8 deletions(-) create mode 100644 src/common/IdleTaskQueue.ts diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index 092cf2e5..d23caac0 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -16,6 +16,7 @@ import { tryDrawCustomChar } from 'browser/renderer/CustomGlyphs'; import { excludeFromContrastRatioDemands, isPowerlineGlyph, isRestrictedPowerlineGlyph } from 'browser/renderer/RendererUtils'; import { IUnicodeService } from 'common/services/Services'; import { FourKeyMap } from 'common/MultiKeyMap'; +import { IdleTaskQueue } from 'common/IdleTaskQueue'; // For debugging purposes, it can be useful to set this to a really tiny value, // to verify that LRU eviction works. @@ -124,20 +125,21 @@ export class WebglCharAtlas implements IDisposable { public warmUp(): void { if (!this._didWarmUp) { - (typeof requestIdleCallback !== 'function' ? requestIdleCallback : setTimeout)(() => { - this._doWarmUp(); - }); + this._doWarmUp(); this._didWarmUp = true; } } private _doWarmUp(): void { - // Pre-fill with ASCII 33-126 + // Pre-fill with ASCII 33-126, this is not urgent and done in idle callbacks + const queue = new IdleTaskQueue(); for (let i = 33; i < 126; i++) { - if (!this._cacheMap.get(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT)) { - const rasterizedGlyph = this._drawToCache(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT); - this._cacheMap.set(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT, rasterizedGlyph); - } + queue.enqueue(() => { + if (!this._cacheMap.get(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT)) { + const rasterizedGlyph = this._drawToCache(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT); + this._cacheMap.set(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT, rasterizedGlyph); + } + }); } } diff --git a/src/common/IdleTaskQueue.ts b/src/common/IdleTaskQueue.ts new file mode 100644 index 00000000..6d3b341b --- /dev/null +++ b/src/common/IdleTaskQueue.ts @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2022 The xterm.js authors. All rights reserved. + * @license MIT + */ + +export class IdleTaskQueue { + private _tasks: Function[] = []; + private _idleCallback?: number; + private _maxTaskDuration: number; + private _i = 0; + + constructor(targetFps: number = 240) { + this._maxTaskDuration = 1000 / targetFps; + } + + public enqueue(task: Function): void { + this._tasks.push(task); + this._start(); + } + + private _start(): void { + if (!this._idleCallback) { + this._idleCallback = requestIdleCallback(() => this._process()); + } + } + + private _process(): void { + const start = performance.now(); + this._idleCallback = undefined; + while (this._i < this._tasks.length) { + this._tasks[this._i++](); + if (performance.now() - start > this._maxTaskDuration) { + this._start(); + return; + } + } + // Clear the queue + this._i = 0; + this._tasks.length = 0; + } +} From 270ac1c7d9b6b3d23e99652f68e24d6c867aa1cc Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 24 Sep 2022 11:17:39 -0700 Subject: [PATCH 11/28] Docs --- src/common/IdleTaskQueue.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/common/IdleTaskQueue.ts b/src/common/IdleTaskQueue.ts index 6d3b341b..240a26ff 100644 --- a/src/common/IdleTaskQueue.ts +++ b/src/common/IdleTaskQueue.ts @@ -3,16 +3,27 @@ * @license MIT */ +/** + * A queue of that runs tasks over several idle callbacks, trying to maintain the specified + * frame rate. The tasks will run in the order they are enqueued, but they will run some time later, + * and care should be taken to ensure they're non-urgent and will not introduce race conditions. + */ export class IdleTaskQueue { private _tasks: Function[] = []; private _idleCallback?: number; private _maxTaskDuration: number; private _i = 0; + /** + * @param targetFps The target frame rate. + */ constructor(targetFps: number = 240) { this._maxTaskDuration = 1000 / targetFps; } + /** + * Adds a task to the queue which will run in a future idle callback. + */ public enqueue(task: Function): void { this._tasks.push(task); this._start(); From 4e8e6b978889c32fa4d8711c87dac2a1d5552637 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 24 Sep 2022 11:45:48 -0700 Subject: [PATCH 12/28] Defer paused renderer resize to idle callback --- src/browser/services/RenderService.ts | 10 +++++++++- src/common/IdleTaskQueue.ts | 26 +++++++++++++++++++++++--- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/browser/services/RenderService.ts b/src/browser/services/RenderService.ts index 7db0e5cd..880a0667 100644 --- a/src/browser/services/RenderService.ts +++ b/src/browser/services/RenderService.ts @@ -12,6 +12,7 @@ import { addDisposableDomListener } from 'browser/Lifecycle'; import { IColorSet, IRenderDebouncerWithCallback } from 'browser/Types'; import { IOptionsService, IBufferService, IDecorationService } from 'common/services/Services'; import { ICharSizeService, ICoreBrowserService, IRenderService } from 'browser/services/Services'; +import { IdleTaskQueue } from 'common/IdleTaskQueue'; interface ISelectionState { start: [number, number] | undefined; @@ -105,6 +106,7 @@ export class RenderService extends Disposable implements IRenderService { } if (!this._isPaused && this._needsFullRefresh) { + this._pausedResizeQueue.flush(); this.refreshRows(0, this._rowCount - 1); this._needsFullRefresh = false; } @@ -203,8 +205,14 @@ export class RenderService extends Disposable implements IRenderService { this.refreshRows(0, this._rowCount - 1); } + private _pausedResizeQueue = new IdleTaskQueue(); public onResize(cols: number, rows: number): void { - this._renderer.onResize(cols, rows); + if (this._isPaused) { + this._pausedResizeQueue.clear(); + this._pausedResizeQueue.enqueue(() => this._renderer.onResize(cols, rows)); + } else { + this._renderer.onResize(cols, rows); + } this._fullRefresh(); } diff --git a/src/common/IdleTaskQueue.ts b/src/common/IdleTaskQueue.ts index 240a26ff..167e79e9 100644 --- a/src/common/IdleTaskQueue.ts +++ b/src/common/IdleTaskQueue.ts @@ -29,6 +29,28 @@ export class IdleTaskQueue { this._start(); } + /** + * Flushes the queue, running all remaining tasks synchronously. + */ + public flush(): void { + while (this._i < this._tasks.length) { + this._tasks[this._i++](); + } + this.clear(); + } + + /** + * Clears any remaining tasks from the queue, these will not be run. + */ + public clear(): void { + if (this._idleCallback) { + cancelIdleCallback(this._idleCallback); + this._idleCallback = undefined; + } + this._i = 0; + this._tasks.length = 0; + } + private _start(): void { if (!this._idleCallback) { this._idleCallback = requestIdleCallback(() => this._process()); @@ -45,8 +67,6 @@ export class IdleTaskQueue { return; } } - // Clear the queue - this._i = 0; - this._tasks.length = 0; + this.clear(); } } From 4ac5a91c95aa877c457d476c80a389551a4a8b61 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 24 Sep 2022 11:56:46 -0700 Subject: [PATCH 13/28] Make DebouncedIdleTask helper --- src/browser/services/RenderService.ts | 9 ++++----- src/common/{IdleTaskQueue.ts => Idle.ts} | 24 ++++++++++++++++++++++-- 2 files changed, 26 insertions(+), 7 deletions(-) rename src/common/{IdleTaskQueue.ts => Idle.ts} (79%) diff --git a/src/browser/services/RenderService.ts b/src/browser/services/RenderService.ts index 880a0667..ca1321d6 100644 --- a/src/browser/services/RenderService.ts +++ b/src/browser/services/RenderService.ts @@ -12,7 +12,7 @@ import { addDisposableDomListener } from 'browser/Lifecycle'; import { IColorSet, IRenderDebouncerWithCallback } from 'browser/Types'; import { IOptionsService, IBufferService, IDecorationService } from 'common/services/Services'; import { ICharSizeService, ICoreBrowserService, IRenderService } from 'browser/services/Services'; -import { IdleTaskQueue } from 'common/IdleTaskQueue'; +import { DebouncedIdleTask } from 'common/Idle'; interface ISelectionState { start: [number, number] | undefined; @@ -106,7 +106,7 @@ export class RenderService extends Disposable implements IRenderService { } if (!this._isPaused && this._needsFullRefresh) { - this._pausedResizeQueue.flush(); + this._pausedResizeTask.flush(); this.refreshRows(0, this._rowCount - 1); this._needsFullRefresh = false; } @@ -205,11 +205,10 @@ export class RenderService extends Disposable implements IRenderService { this.refreshRows(0, this._rowCount - 1); } - private _pausedResizeQueue = new IdleTaskQueue(); + private _pausedResizeTask = new DebouncedIdleTask(); public onResize(cols: number, rows: number): void { if (this._isPaused) { - this._pausedResizeQueue.clear(); - this._pausedResizeQueue.enqueue(() => this._renderer.onResize(cols, rows)); + this._pausedResizeTask.set(() => this._renderer.onResize(cols, rows)); } else { this._renderer.onResize(cols, rows); } diff --git a/src/common/IdleTaskQueue.ts b/src/common/Idle.ts similarity index 79% rename from src/common/IdleTaskQueue.ts rename to src/common/Idle.ts index 167e79e9..529d38a4 100644 --- a/src/common/IdleTaskQueue.ts +++ b/src/common/Idle.ts @@ -9,7 +9,7 @@ * and care should be taken to ensure they're non-urgent and will not introduce race conditions. */ export class IdleTaskQueue { - private _tasks: Function[] = []; + private _tasks: (() => void)[] = []; private _idleCallback?: number; private _maxTaskDuration: number; private _i = 0; @@ -24,7 +24,7 @@ export class IdleTaskQueue { /** * Adds a task to the queue which will run in a future idle callback. */ - public enqueue(task: Function): void { + public enqueue(task: () => void): void { this._tasks.push(task); this._start(); } @@ -70,3 +70,23 @@ export class IdleTaskQueue { this.clear(); } } + +export class DebouncedIdleTask { + private _queue: IdleTaskQueue; + + /** + * @param targetFps The target frame rate. + */ + constructor(targetFps: number = 240) { + this._queue = new IdleTaskQueue(targetFps); + } + + public set(task: () => void): void { + this._queue.clear(); + this._queue.enqueue(task); + } + + public flush(): void { + this._queue.flush(); + } +} From 66efb0388abdf8c8dea9c3b6ed3bc51a6916b6bf Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 24 Sep 2022 11:57:12 -0700 Subject: [PATCH 14/28] Move prop to top --- src/browser/services/RenderService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browser/services/RenderService.ts b/src/browser/services/RenderService.ts index ca1321d6..3849bdd3 100644 --- a/src/browser/services/RenderService.ts +++ b/src/browser/services/RenderService.ts @@ -25,6 +25,7 @@ export class RenderService extends Disposable implements IRenderService { private _renderDebouncer: IRenderDebouncerWithCallback; private _screenDprMonitor: ScreenDprMonitor; + private _pausedResizeTask = new DebouncedIdleTask(); private _isPaused: boolean = false; private _needsFullRefresh: boolean = false; @@ -205,7 +206,6 @@ export class RenderService extends Disposable implements IRenderService { this.refreshRows(0, this._rowCount - 1); } - private _pausedResizeTask = new DebouncedIdleTask(); public onResize(cols: number, rows: number): void { if (this._isPaused) { this._pausedResizeTask.set(() => this._renderer.onResize(cols, rows)); From aec1f2b2be7687d333d1eedf4724a88e2bf642f1 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 24 Sep 2022 11:58:17 -0700 Subject: [PATCH 15/28] Fix import --- addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index d23caac0..dfcede38 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -16,7 +16,7 @@ import { tryDrawCustomChar } from 'browser/renderer/CustomGlyphs'; import { excludeFromContrastRatioDemands, isPowerlineGlyph, isRestrictedPowerlineGlyph } from 'browser/renderer/RendererUtils'; import { IUnicodeService } from 'common/services/Services'; import { FourKeyMap } from 'common/MultiKeyMap'; -import { IdleTaskQueue } from 'common/IdleTaskQueue'; +import { IdleTaskQueue } from 'common/Idle'; // For debugging purposes, it can be useful to set this to a really tiny value, // to verify that LRU eviction works. From 1f45d5344c7049d8997a011b5378b73dce0f8d7a Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 24 Sep 2022 14:10:29 -0700 Subject: [PATCH 16/28] Use the browser's IdleDeadline to determine max task time --- src/common/Idle.ts | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/src/common/Idle.ts b/src/common/Idle.ts index 529d38a4..2aebc599 100644 --- a/src/common/Idle.ts +++ b/src/common/Idle.ts @@ -11,16 +11,8 @@ export class IdleTaskQueue { private _tasks: (() => void)[] = []; private _idleCallback?: number; - private _maxTaskDuration: number; private _i = 0; - /** - * @param targetFps The target frame rate. - */ - constructor(targetFps: number = 240) { - this._maxTaskDuration = 1000 / targetFps; - } - /** * Adds a task to the queue which will run in a future idle callback. */ @@ -53,16 +45,22 @@ export class IdleTaskQueue { private _start(): void { if (!this._idleCallback) { - this._idleCallback = requestIdleCallback(() => this._process()); + this._idleCallback = requestIdleCallback(this._process.bind(this)); } } - private _process(): void { - const start = performance.now(); + private _process(deadline: IdleDeadline): void { this._idleCallback = undefined; + let taskDuration = 0; + let longestTask = 0; while (this._i < this._tasks.length) { + taskDuration = performance.now(); this._tasks[this._i++](); - if (performance.now() - start > this._maxTaskDuration) { + taskDuration = performance.now() - taskDuration; + longestTask = Math.max(taskDuration, longestTask); + // Guess the following task will take a similar time to task that just finished, allow + // additional room to try avoid exceeding the deadline + if (longestTask * 1.5 > deadline.timeRemaining()) { this._start(); return; } @@ -71,14 +69,15 @@ export class IdleTaskQueue { } } +/** + * An object that tracks a single debounced task that will run on the next idle frame. When called + * multiple times, only the last set task will run. + */ export class DebouncedIdleTask { private _queue: IdleTaskQueue; - /** - * @param targetFps The target frame rate. - */ - constructor(targetFps: number = 240) { - this._queue = new IdleTaskQueue(targetFps); + constructor() { + this._queue = new IdleTaskQueue(); } public set(task: () => void): void { From ab90dcd0dc2562a6d0952eeca5c5d531bdb9851b Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 24 Sep 2022 14:13:04 -0700 Subject: [PATCH 17/28] Clarify comment --- src/common/Idle.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/Idle.ts b/src/common/Idle.ts index 2aebc599..e05ad068 100644 --- a/src/common/Idle.ts +++ b/src/common/Idle.ts @@ -58,7 +58,7 @@ export class IdleTaskQueue { this._tasks[this._i++](); taskDuration = performance.now() - taskDuration; longestTask = Math.max(taskDuration, longestTask); - // Guess the following task will take a similar time to task that just finished, allow + // Guess the following task will take a similar time to the longest task in this batch, allow // additional room to try avoid exceeding the deadline if (longestTask * 1.5 > deadline.timeRemaining()) { this._start(); From 8f8813a19a52e20d242b5017e109da6805d4d4b3 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 24 Sep 2022 15:16:02 -0700 Subject: [PATCH 18/28] Add PriorityTaskQueue --- src/common/{Idle.ts => TaskQueue.ts} | 65 +++++++++++++++++++++++++--- 1 file changed, 58 insertions(+), 7 deletions(-) rename src/common/{Idle.ts => TaskQueue.ts} (54%) diff --git a/src/common/Idle.ts b/src/common/TaskQueue.ts similarity index 54% rename from src/common/Idle.ts rename to src/common/TaskQueue.ts index e05ad068..49e3f5ec 100644 --- a/src/common/Idle.ts +++ b/src/common/TaskQueue.ts @@ -3,16 +3,24 @@ * @license MIT */ -/** - * A queue of that runs tasks over several idle callbacks, trying to maintain the specified - * frame rate. The tasks will run in the order they are enqueued, but they will run some time later, - * and care should be taken to ensure they're non-urgent and will not introduce race conditions. - */ -export class IdleTaskQueue { +import { isNode } from 'common/Platform'; + +interface ITaskQueue { +} + +interface ITaskDeadline { + timeRemaining(): number; +} +type CallbackWithDeadline = (deadline: ITaskDeadline) => void; + +abstract class TaskQueue implements ITaskQueue { private _tasks: (() => void)[] = []; private _idleCallback?: number; private _i = 0; + protected abstract _requestCallback(callback: CallbackWithDeadline): number; + protected abstract _cancelCallback(identifier: number): void; + /** * Adds a task to the queue which will run in a future idle callback. */ @@ -69,12 +77,55 @@ export class IdleTaskQueue { } } +/** + * A queue of that runs tasks over several tasks via setTimeout, trying to maintain above 60 frames + * per second. The tasks will run in the order they are enqueued, but they will run some time later, + * and care should be taken to ensure they're non-urgent and will not introduce race conditions. + */ +export class PriorityTaskQueue extends TaskQueue { + protected _requestCallback(callback: CallbackWithDeadline): number { + return setTimeout(() => callback(this._createDeadline(16))); + } + + protected _cancelCallback(identifier: number): void { + clearTimeout(identifier); + } + + private _createDeadline(duration: number): ITaskDeadline { + const end = performance.now() + duration; + return { + timeRemaining: () => Math.max(0, end - performance.now()) + }; + } +} + +class IdleTaskQueueInternal extends TaskQueue { + protected _requestCallback(callback: IdleRequestCallback): number { + return requestIdleCallback(callback); + } + + protected _cancelCallback(identifier: number): void { + cancelIdleCallback(identifier); + } +} + +/** + * A queue of that runs tasks over several idle callbacks, trying to respect the idle callback's + * deadline given by the environment. The tasks will run in the order they are enqueued, but they + * will run some time later, and care should be taken to ensure they're non-urgent and will not + * introduce race conditions. + * + * This reverts to a {@link PriorityTaskQueue} if the environment does not support idle callbacks. + */ +// eslint-disable-next-line @typescript-eslint/naming-convention +export const IdleTaskQueue = (!isNode && 'requestIdleCallback' in window) ? IdleTaskQueueInternal : PriorityTaskQueue; + /** * An object that tracks a single debounced task that will run on the next idle frame. When called * multiple times, only the last set task will run. */ export class DebouncedIdleTask { - private _queue: IdleTaskQueue; + private _queue: IdleTaskQueueInternal | PriorityTaskQueue; constructor() { this._queue = new IdleTaskQueue(); From 494fe3a95d8bca2afaaf7af8c10485f2ec5f8167 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 24 Sep 2022 16:13:53 -0700 Subject: [PATCH 19/28] Fill in ITaskQueue --- src/common/TaskQueue.ts | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/common/TaskQueue.ts b/src/common/TaskQueue.ts index 49e3f5ec..27e3a492 100644 --- a/src/common/TaskQueue.ts +++ b/src/common/TaskQueue.ts @@ -6,6 +6,20 @@ import { isNode } from 'common/Platform'; interface ITaskQueue { + /** + * Adds a task to the queue which will run in a future idle callback. + */ + enqueue(task: () => void): void; + + /** + * Flushes the queue, running all remaining tasks synchronously. + */ + flush(): void; + + /** + * Clears any remaining tasks from the queue, these will not be run. + */ + clear(): void; } interface ITaskDeadline { @@ -21,17 +35,11 @@ abstract class TaskQueue implements ITaskQueue { protected abstract _requestCallback(callback: CallbackWithDeadline): number; protected abstract _cancelCallback(identifier: number): void; - /** - * Adds a task to the queue which will run in a future idle callback. - */ public enqueue(task: () => void): void { this._tasks.push(task); this._start(); } - /** - * Flushes the queue, running all remaining tasks synchronously. - */ public flush(): void { while (this._i < this._tasks.length) { this._tasks[this._i++](); @@ -39,9 +47,6 @@ abstract class TaskQueue implements ITaskQueue { this.clear(); } - /** - * Clears any remaining tasks from the queue, these will not be run. - */ public clear(): void { if (this._idleCallback) { cancelIdleCallback(this._idleCallback); From 04d196a32dd36657ad367ed115693b5d5f987981 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 24 Sep 2022 16:14:38 -0700 Subject: [PATCH 20/28] Use task queue interface --- src/common/TaskQueue.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/TaskQueue.ts b/src/common/TaskQueue.ts index 27e3a492..3b1acfa6 100644 --- a/src/common/TaskQueue.ts +++ b/src/common/TaskQueue.ts @@ -130,7 +130,7 @@ export const IdleTaskQueue = (!isNode && 'requestIdleCallback' in window) ? Idle * multiple times, only the last set task will run. */ export class DebouncedIdleTask { - private _queue: IdleTaskQueueInternal | PriorityTaskQueue; + private _queue: ITaskQueue; constructor() { this._queue = new IdleTaskQueue(); From c548977a3eb5b96f70d440900d66c0d5abf38d80 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 24 Sep 2022 16:16:14 -0700 Subject: [PATCH 21/28] Fix imports --- addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts | 2 +- src/browser/services/RenderService.ts | 2 +- 2 files 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 dfcede38..4764ede4 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -16,7 +16,7 @@ import { tryDrawCustomChar } from 'browser/renderer/CustomGlyphs'; import { excludeFromContrastRatioDemands, isPowerlineGlyph, isRestrictedPowerlineGlyph } from 'browser/renderer/RendererUtils'; import { IUnicodeService } from 'common/services/Services'; import { FourKeyMap } from 'common/MultiKeyMap'; -import { IdleTaskQueue } from 'common/Idle'; +import { IdleTaskQueue } from 'common/TaskQueue'; // For debugging purposes, it can be useful to set this to a really tiny value, // to verify that LRU eviction works. diff --git a/src/browser/services/RenderService.ts b/src/browser/services/RenderService.ts index 3849bdd3..97258609 100644 --- a/src/browser/services/RenderService.ts +++ b/src/browser/services/RenderService.ts @@ -12,7 +12,7 @@ import { addDisposableDomListener } from 'browser/Lifecycle'; import { IColorSet, IRenderDebouncerWithCallback } from 'browser/Types'; import { IOptionsService, IBufferService, IDecorationService } from 'common/services/Services'; import { ICharSizeService, ICoreBrowserService, IRenderService } from 'browser/services/Services'; -import { DebouncedIdleTask } from 'common/Idle'; +import { DebouncedIdleTask } from 'common/TaskQueue'; interface ISelectionState { start: [number, number] | undefined; From c6e4788344bf5f83596efed24a9634496d3df6ad Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 24 Sep 2022 16:23:14 -0700 Subject: [PATCH 22/28] Use microtasks on server This reduces demo input latency on my mac approx 10ms -> 4ms --- demo/server.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/demo/server.js b/demo/server.js index 8d295942..8bb684a2 100644 --- a/demo/server.js +++ b/demo/server.js @@ -86,7 +86,7 @@ function startServer() { return (data) => { s += data; if (!sender) { - sender = setTimeout(() => { + sender = queueMicrotask(() => { socket.send(s); s = ''; sender = null; @@ -103,7 +103,7 @@ function startServer() { buffer.push(data); length += data.length; if (!sender) { - sender = setTimeout(() => { + sender = queueMicrotask(() => { socket.send(Buffer.concat(buffer, length)); buffer = []; sender = null; From 0b8120808c539feee99965f8fbebc58d4a83ffde Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 24 Sep 2022 16:27:56 -0700 Subject: [PATCH 23/28] Prefer microtask to timeout in write queue This seems to make little difference but it's more correct as we want to write as fast as possible to minimize input latency --- src/common/input/WriteBuffer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/common/input/WriteBuffer.ts b/src/common/input/WriteBuffer.ts index 67fd751e..c9dfd039 100644 --- a/src/common/input/WriteBuffer.ts +++ b/src/common/input/WriteBuffer.ts @@ -105,7 +105,7 @@ export class WriteBuffer { // schedule chunk processing for next event loop run if (!this._writeBuffer.length) { this._bufferOffset = 0; - setTimeout(() => this._innerWrite()); + queueMicrotask(() => this._innerWrite()); } this._pendingData += data.length; @@ -217,7 +217,7 @@ export class WriteBuffer { this._callbacks = this._callbacks.slice(this._bufferOffset); this._bufferOffset = 0; } - setTimeout(() => this._innerWrite()); + queueMicrotask(() => this._innerWrite()); } else { this._writeBuffer.length = 0; this._callbacks.length = 0; From 2b597a82a119982dbffa1eea6c280c8054e30a61 Mon Sep 17 00:00:00 2001 From: Daniel Steinberg Date: Mon, 26 Sep 2022 11:19:44 -0400 Subject: [PATCH 24/28] Move real-world uses section down --- README.md | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 2c450e42..5c4d2084 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,26 @@ The full API for xterm.js is contained within the [TypeScript declaration file]( Note that some APIs are marked *experimental*, these are added to enable experimentation with new ideas without committing to support it like a normal [semver](https://semver.org/) API. Note that these APIs can change radically between versions, so be sure to read release notes if you plan on using experimental APIs. +## Releases + +Xterm.js follows a monthly release cycle roughly. + +All current and past releases are available on this repo's [Releases page](https://github.com/sourcelair/xterm.js/releases), you can view the [high-level roadmap on the wiki](https://github.com/xtermjs/xterm.js/wiki/Roadmap) and see what we're working on now by looking through [Milestones](https://github.com/sourcelair/xterm.js/milestones). + +### Beta builds + +Our CI releases beta builds to npm for every change that goes into master. Install the latest beta build with: + +```bash +npm install -S xterm@beta +``` + +These should generally be stable, but some bugs may slip in. We recommend using the beta build primarily to test out new features and to verify bug fixes. + +## Contributing + +You can read the [guide on the wiki](https://github.com/xtermjs/xterm.js/wiki/Contributing) to learn how to contribute and set up xterm.js for development. + ## Real-world uses Xterm.js is used in several world-class applications to provide great terminal experiences. @@ -197,26 +217,6 @@ Xterm.js is used in several world-class applications to provide great terminal e Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it on our list. Note: Please add any new contributions to the end of the list only. -## Releases - -Xterm.js follows a monthly release cycle roughly. - -All current and past releases are available on this repo's [Releases page](https://github.com/sourcelair/xterm.js/releases), you can view the [high-level roadmap on the wiki](https://github.com/xtermjs/xterm.js/wiki/Roadmap) and see what we're working on now by looking through [Milestones](https://github.com/sourcelair/xterm.js/milestones). - -### Beta builds - -Our CI releases beta builds to npm for every change that goes into master. Install the latest beta build with: - -```bash -npm install -S xterm@beta -``` - -These should generally be stable, but some bugs may slip in. We recommend using the beta build primarily to test out new features and to verify bug fixes. - -## Contributing - -You can read the [guide on the wiki](https://github.com/xtermjs/xterm.js/wiki/Contributing) to learn how to contribute and set up xterm.js for development. - ## License Agreement If you contribute code to this project, you implicitly allow your code to be distributed under the MIT license. You are also implicitly verifying that all code is your original work. From 25b69e5ed0939a6c7723421f8c3501c3ee4d0c6c Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 27 Sep 2022 06:43:07 -0700 Subject: [PATCH 25/28] Fix repetitive microtask problem See #4145 --- src/common/input/WriteBuffer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/input/WriteBuffer.ts b/src/common/input/WriteBuffer.ts index c9dfd039..3c7727bb 100644 --- a/src/common/input/WriteBuffer.ts +++ b/src/common/input/WriteBuffer.ts @@ -217,7 +217,7 @@ export class WriteBuffer { this._callbacks = this._callbacks.slice(this._bufferOffset); this._bufferOffset = 0; } - queueMicrotask(() => this._innerWrite()); + setTimeout(() => this._innerWrite()); } else { this._writeBuffer.length = 0; this._callbacks.length = 0; From d74e43a42a238d613a34f050f4301fc4a164b096 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 27 Sep 2022 06:44:18 -0700 Subject: [PATCH 26/28] Remove queuemicrotask polyfill Support seems good --- src/common/input/WriteBuffer.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/common/input/WriteBuffer.ts b/src/common/input/WriteBuffer.ts index c9dfd039..c324d434 100644 --- a/src/common/input/WriteBuffer.ts +++ b/src/common/input/WriteBuffer.ts @@ -33,12 +33,6 @@ const WRITE_TIMEOUT_MS = 12; */ const WRITE_BUFFER_LENGTH_THRESHOLD = 50; -// queueMicrotask polyfill for nodejs < v11 -const qmt: (cb: () => void) => void = (typeof queueMicrotask === 'undefined') - ? (cb: () => void) => { Promise.resolve().then(cb); } - : queueMicrotask; - - export class WriteBuffer { private _writeBuffer: (string | Uint8Array)[] = []; private _callbacks: ((() => void) | undefined)[] = []; @@ -194,7 +188,7 @@ export class WriteBuffer { // 2. spawn a promise immediately resolving to `true` // (executed on the same queue, thus properly aligned before continuation happens) result.catch(err => { - qmt(() => {throw err;}); + queueMicrotask(() => {throw err;}); return Promise.resolve(false); }).then(continuation); return; From 8201925fe886a61626b9201466e87db6e1a9bf83 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 27 Sep 2022 06:46:14 -0700 Subject: [PATCH 27/28] Fix priority task queue and idle callback fallback Mistake of not hooking up the abstract methods, this should throw on Safari for example. --- src/common/TaskQueue.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/common/TaskQueue.ts b/src/common/TaskQueue.ts index 3b1acfa6..94c5c53b 100644 --- a/src/common/TaskQueue.ts +++ b/src/common/TaskQueue.ts @@ -49,7 +49,7 @@ abstract class TaskQueue implements ITaskQueue { public clear(): void { if (this._idleCallback) { - cancelIdleCallback(this._idleCallback); + this._cancelCallback(this._idleCallback); this._idleCallback = undefined; } this._i = 0; @@ -58,11 +58,11 @@ abstract class TaskQueue implements ITaskQueue { private _start(): void { if (!this._idleCallback) { - this._idleCallback = requestIdleCallback(this._process.bind(this)); + this._idleCallback = this._requestCallback(this._process.bind(this)); } } - private _process(deadline: IdleDeadline): void { + private _process(deadline: ITaskDeadline): void { this._idleCallback = undefined; let taskDuration = 0; let longestTask = 0; From 23328a394bbb10790014d78f888d96174e1aae98 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 27 Sep 2022 13:01:36 -0700 Subject: [PATCH 28/28] Fix trusted types in dom renderer Fixes #4156 --- src/browser/renderer/dom/DomRendererRowFactory.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts index d3eb9e8e..b6ee7bf1 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.ts @@ -166,7 +166,7 @@ export class DomRendererRowFactory { if (cell.isUnderline()) { charElement.classList.add(`${UNDERLINE_CLASS}-${cell.extended.underlineStyle}`); if (charElement.textContent === ' ') { - charElement.innerHTML = ' '; + charElement.textContent = '\xa0'; // =   } if (!cell.isUnderlineColorDefault()) { if (cell.isUnderlineColorRGB()) {