diff --git a/.eslintrc.json b/.eslintrc.json index 2ad17123..491ee8f7 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -163,6 +163,16 @@ "jsdoc/check-param-names": 1, "jsdoc/no-multi-asterisks": 1, "keyword-spacing": "warn", + "max-len": [ + "warn", + { + "code": 1000, // Don't enforce for code + "comments": 100, + "ignoreTrailingComments": true, + "ignoreUrls": true, + "ignorePattern": "^ *((?(//|\\*) @vt)|(?\\* \\| )|(?// ))" + } + ], "new-parens": "warn", "no-duplicate-imports": "warn", "no-else-return": [ @@ -229,7 +239,8 @@ { "files": ["**/*.test.ts"], "rules": { - "object-curly-spacing": "off" + "object-curly-spacing": "off", + "max-len": "off" } } ] diff --git a/.eslintrc.json.typings b/.eslintrc.json.typings new file mode 100644 index 00000000..5af028d3 --- /dev/null +++ b/.eslintrc.json.typings @@ -0,0 +1,104 @@ +{ + "env": { + "browser": true, + "es6": true, + "node": true + }, + "parser": "@typescript-eslint/parser", + "plugins": [ + "@typescript-eslint", + "jsdoc" + ], + "rules": { + "no-extra-semi": "error", + "@typescript-eslint/array-type": [ + "warn", + { + "default": "array", + "readonly": "generic" + } + ], + "@typescript-eslint/explicit-function-return-type": [ + "warn", + { + "allowExpressions": true + } + ], + "@typescript-eslint/indent": [ + "warn", + 2 + ], + "@typescript-eslint/member-delimiter-style": [ + "warn", + { + "multiline": { + "delimiter": "semi", + "requireLast": true + }, + "singleline": { + "delimiter": "comma", + "requireLast": false + } + } + ], + "@typescript-eslint/naming-convention": [ + "warn", + { "selector": "typeLike", "format": ["PascalCase"] }, + { "selector": "interface", "format": ["PascalCase"], "prefix": ["I"] } + ], + "@typescript-eslint/prefer-namespace-keyword": "warn", + "@typescript-eslint/type-annotation-spacing": "warn", + "@typescript-eslint/quotes": [ + "warn", + "single", + { "allowTemplateLiterals": true } + ], + "@typescript-eslint/semi": [ + "warn", + "always" + ], + "comma-dangle": [ + "warn", + { + "objects": "never", + "arrays": "never", + "functions": "never" + } + ], + "curly": [ + "warn", + "multi-line" + ], + "eol-last": "warn", + "eqeqeq": [ + "warn", + "always" + ], + "jsdoc/check-alignment": 1, + "jsdoc/check-param-names": 1, + "keyword-spacing": "warn", + "max-len": [ + "warn", + { + "code": 1000, // Don't enforce for code + "comments": 80, + "ignoreUrls": true, + "ignorePattern": "^ *(?\\* Ps=)" + } + ], + "no-irregular-whitespace": "warn", + "no-trailing-spaces": "warn", + "object-curly-spacing": [ + "warn", + "always" + ], + "spaced-comment": [ + "warn", + "always", + { + "markers": ["/"], + "exceptions": ["-"] + } + ] + } +} diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000..b6a7d89c --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +16 diff --git a/addons/xterm-addon-canvas/src/CursorRenderLayer.ts b/addons/xterm-addon-canvas/src/CursorRenderLayer.ts index 19b07b5f..2ef1d072 100644 --- a/addons/xterm-addon-canvas/src/CursorRenderLayer.ts +++ b/addons/xterm-addon-canvas/src/CursorRenderLayer.ts @@ -58,7 +58,8 @@ export class CursorRenderLayer extends BaseRenderLayer { this._cursorRenderers = { 'bar': this._renderBarCursor.bind(this), 'block': this._renderBlockCursor.bind(this), - 'underline': this._renderUnderlineCursor.bind(this) + 'underline': this._renderUnderlineCursor.bind(this), + 'outline': this._renderOutlineCursor.bind(this) }; this.register(optionsService.onOptionChange(() => this._handleOptionsChanged())); this._handleOptionsChanged(); @@ -150,7 +151,10 @@ export class CursorRenderLayer extends BaseRenderLayer { this._ctx.save(); this._ctx.fillStyle = this._themeService.colors.cursor.css; const cursorStyle = this._optionsService.rawOptions.cursorStyle; - this._renderBlurCursor(cursorX, viewportRelativeCursorY, this._cell); + const cursorInactiveStyle = this._optionsService.rawOptions.cursorInactiveStyle; + if (cursorInactiveStyle && cursorInactiveStyle !== 'none') { + this._cursorRenderers[cursorInactiveStyle](cursorX, viewportRelativeCursorY, this._cell); + } this._ctx.restore(); this._state.x = cursorX; this._state.y = viewportRelativeCursorY; @@ -231,7 +235,7 @@ export class CursorRenderLayer extends BaseRenderLayer { this._ctx.restore(); } - private _renderBlurCursor(x: number, y: number, cell: ICellData): void { + private _renderOutlineCursor(x: number, y: number, cell: ICellData): void { this._ctx.save(); this._ctx.strokeStyle = this._themeService.colors.cursor.css; this._strokeRectAtCell(x, y, cell.getWidth(), 1); diff --git a/addons/xterm-addon-image/src/IIPHeaderParser.ts b/addons/xterm-addon-image/src/IIPHeaderParser.ts index 21dc1937..05a350c1 100644 --- a/addons/xterm-addon-image/src/IIPHeaderParser.ts +++ b/addons/xterm-addon-image/src/IIPHeaderParser.ts @@ -23,7 +23,8 @@ export interface IHeaderFields { height?: string; // Optional, defaults to 1 respecting aspect ratio (width takes precedence). preserveAspectRatio?: number; - // Optional, defaults to 0. If set to 1, the file will be displayed inline, else downloaded (download not supported). + // Optional, defaults to 0. If set to 1, the file will be displayed inline, else downloaded + // (download not supported). inline?: number; } diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index a029fc24..cbde1a1b 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -68,8 +68,6 @@ export class SearchAddon extends Disposable implements ITerminalAddon { private _highlightDecorations: IHighlight[] = []; private _selectedDecoration: IHighlight | undefined; private _highlightLimit: number; - private _onDataDisposable: IDisposable | undefined; - private _onResizeDisposable: IDisposable | undefined; private _lastSearchOptions: ISearchOptions | undefined; private _highlightTimeout: number | undefined; /** @@ -93,13 +91,9 @@ export class SearchAddon extends Disposable implements ITerminalAddon { public activate(terminal: Terminal): void { this._terminal = terminal; - this._onDataDisposable = this.register(this._terminal.onWriteParsed(() => this._updateMatches())); - this._onResizeDisposable = this.register(this._terminal.onResize(() => this._updateMatches())); - this.register(toDisposable(() => { - this.clearDecorations(); - this._onDataDisposable?.dispose(); - this._onResizeDisposable?.dispose(); - })); + this.register(this._terminal.onWriteParsed(() => this._updateMatches())); + this.register(this._terminal.onResize(() => this._updateMatches())); + this.register(toDisposable(() => this.clearDecorations())); } private _updateMatches(): void { @@ -440,7 +434,8 @@ export class SearchAddon extends Disposable implements ITerminalAddon { } /** - * A found substring is a whole word if it doesn't have an alphanumeric character directly adjacent to it. + * A found substring is a whole word if it doesn't have an alphanumeric character directly + * adjacent to it. * @param searchIndex starting indext of the potential whole word substring * @param line entire string in which the potential whole word was found * @param term the substring that starts at searchIndex @@ -451,14 +446,15 @@ export class SearchAddon extends Disposable implements ITerminalAddon { } /** - * Searches a line for a search term. Takes the provided terminal line and searches the text line, which may contain - * subsequent terminal lines if the text is wrapped. If the provided line number is part of a wrapped text line that - * started on an earlier line then it is skipped since it will be properly searched when the terminal line that the - * text starts on is searched. + * Searches a line for a search term. Takes the provided terminal line and searches the text line, + * which may contain subsequent terminal lines if the text is wrapped. If the provided line number + * is part of a wrapped text line that started on an earlier line then it is skipped since it will + * be properly searched when the terminal line that the text starts on is searched. * @param term The search term. * @param searchPosition The position to start the search. * @param searchOptions Search options. - * @param isReverseSearch Whether the search should start from the right side of the terminal and search to the left. + * @param isReverseSearch Whether the search should start from the right side of the terminal and + * search to the left. * @returns The search result if it was found. */ protected _findInLine(term: string, searchPosition: ISearchPosition, searchOptions: ISearchOptions = {}, isReverseSearch: boolean = false): ISearchResult | undefined { @@ -526,7 +522,8 @@ export class SearchAddon extends Disposable implements ITerminalAddon { return; } - // Adjust the row number and search index if needed since a "line" of text can span multiple rows + // Adjust the row number and search index if needed since a "line" of text can span multiple + // rows let startRowOffset = 0; while (startRowOffset < offsets.length - 1 && resultIndex >= offsets[startRowOffset + 1]) { startRowOffset++; diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts index ceee48bf..737d529a 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -131,7 +131,8 @@ class StringSerializeHandler extends BaseSerializeHandler { private _thisRowLastSecondChar: IBufferCell = this._buffer.getNullCell(); private _nextRowFirstChar: IBufferCell = this._buffer.getNullCell(); protected _rowEnd(row: number, isLastRow: boolean): void { - // if there is colorful empty cell at line end, whe must pad it back, or the the color block will missing + // if there is colorful empty cell at line end, whe must pad it back, or the the color block + // will missing if (this._nullCellCount > 0 && !equalBg(this._cursorStyle, this._backgroundCell)) { // use clear right to set background. this._currentRow += `\u001b[${this._nullCellCount}X`; @@ -292,7 +293,8 @@ class StringSerializeHandler extends BaseSerializeHandler { const sgrSeq = this._diffStyle(cell, this._cursorStyle); - // the empty cell style is only assumed to be changed when background changed, because foreground is always 0. + // the empty cell style is only assumed to be changed when background changed, because + // foreground is always 0. const styleChanged = isEmptyCell ? !equalBg(this._cursorStyle, cell) : sgrSeq.length > 0; /** diff --git a/addons/xterm-addon-webgl/src/RectangleRenderer.ts b/addons/xterm-addon-webgl/src/RectangleRenderer.ts index 4706b442..b2e5d0fa 100644 --- a/addons/xterm-addon-webgl/src/RectangleRenderer.ts +++ b/addons/xterm-addon-webgl/src/RectangleRenderer.ts @@ -255,7 +255,7 @@ export class RectangleRenderer extends Disposable { let offset: number; let rectangleCount = 0; - if (cursor.style === 'bar' || cursor.style === 'blur') { + if (cursor.style === 'bar' || cursor.style === 'outline') { // Left edge offset = rectangleCount++ * INDICES_PER_RECTANGLE; this._addRectangleFloat( @@ -268,7 +268,7 @@ export class RectangleRenderer extends Disposable { this._cursorFloat ); } - if (cursor.style === 'underline' || cursor.style === 'blur') { + if (cursor.style === 'underline' || cursor.style === 'outline') { // Bottom edge offset = rectangleCount++ * INDICES_PER_RECTANGLE; this._addRectangleFloat( @@ -281,7 +281,7 @@ export class RectangleRenderer extends Disposable { this._cursorFloat ); } - if (cursor.style === 'blur') { + if (cursor.style === 'outline') { // Top edge offset = rectangleCount++ * INDICES_PER_RECTANGLE; this._addRectangleFloat( diff --git a/addons/xterm-addon-webgl/src/Types.d.ts b/addons/xterm-addon-webgl/src/Types.d.ts index 73b15bc7..3eb3b300 100644 --- a/addons/xterm-addon-webgl/src/Types.d.ts +++ b/addons/xterm-addon-webgl/src/Types.d.ts @@ -16,11 +16,13 @@ export interface ICursorRenderModel { x: number; y: number; width: number; - style: string; + style: CursorStyle; cursorWidth: number; dpr: number; } +export type CursorStyle = 'outline' | 'block' | 'bar' | 'underline' | 'none'; + export interface IWebGL2RenderingContext extends WebGLRenderingContext { vertexAttribDivisor(index: number, divisor: number): void; createVertexArray(): IWebGLVertexArrayObject; diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index a4dc0b28..c21086e2 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -461,15 +461,17 @@ export class WebglRenderer extends Disposable implements IRenderer { y: this._terminal.buffer.active.cursorY, width: cell.getWidth(), style: this._coreBrowserService.isFocused ? - (terminal.options.cursorStyle || 'block') : 'blur', + (terminal.options.cursorStyle || 'block') : terminal.options.cursorInactiveStyle, cursorWidth: terminal.options.cursorWidth, dpr: this._devicePixelRatio }; lastCursorX = cursorX + cell.getWidth() - 1; } if (x >= cursorX && x <= lastCursorX && - this._coreBrowserService.isFocused && - (terminal.options.cursorStyle || 'block') === 'block') { + ((this._coreBrowserService.isFocused && + (terminal.options.cursorStyle || 'block') === 'block') || + (this._coreBrowserService.isFocused === false && + terminal.options.cursorInactiveStyle === 'block'))) { this._cellColorResolver.result.fg = Attributes.CM_RGB | (this._themeService.colors.cursorAccent.rgba >> 8 & Attributes.RGB_MASK); this._cellColorResolver.result.bg = diff --git a/addons/xterm-addon-webgl/test/WebglRenderer.api.ts b/addons/xterm-addon-webgl/test/WebglRenderer.api.ts index cca37e0a..8d84b200 100644 --- a/addons/xterm-addon-webgl/test/WebglRenderer.api.ts +++ b/addons/xterm-addon-webgl/test/WebglRenderer.api.ts @@ -789,7 +789,7 @@ describe('WebGL Renderer Integration Tests', async () => { 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 + // Setting and check for minimum contrast values, note that these are not // exact to the contrast ratio, if the increase luminance algorithm // changes then these will probably fail await page.evaluate(`window.term.options.minimumContrastRatio = 10;`); @@ -858,7 +858,7 @@ describe('WebGL Renderer Integration Tests', async () => { 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 + // Setting and check for minimum contrast values, note that these are not // exact to the contrast ratio, if the increase luminance algorithm // changes then these will probably fail await page.evaluate(`window.term.options.minimumContrastRatio = 10;`); @@ -879,6 +879,75 @@ describe('WebGL Renderer Integration Tests', async () => { await pollFor(page, () => getCellColor(7, 2), [13, 67, 67, 255]); await pollFor(page, () => getCellColor(8, 2), [64, 64, 64, 255]); }); + + itWebgl('should enforce half the contrast for dim cells', 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.options.theme = ${JSON.stringify(theme)}; + window.term.options.minimumContrastRatio = 1; + `); + // Block characters ignore block elements so a different char is used here + await writeSync(page, + '\\x1b[2m' + + `\\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), [Math.floor((255 + 0x2e) / 2), Math.floor((255 + 0x34) / 2), Math.floor((255 + 0x36) / 2), 255]); + await pollFor(page, () => getCellColor(2, 1), [Math.floor((255 + 0xcc) / 2), Math.floor((255 + 0x00) / 2), Math.floor((255 + 0x00) / 2), 255]); + await pollFor(page, () => getCellColor(3, 1), [Math.floor((255 + 0x4e) / 2), Math.floor((255 + 0x9a) / 2), Math.floor((255 + 0x06) / 2), 255]); + await pollFor(page, () => getCellColor(4, 1), [Math.floor((255 + 0xc4) / 2), Math.floor((255 + 0xa0) / 2), Math.floor((255 + 0x00) / 2), 255]); + await pollFor(page, () => getCellColor(5, 1), [Math.floor((255 + 0x34) / 2), Math.floor((255 + 0x65) / 2), Math.floor((255 + 0xa4) / 2), 255]); + await pollFor(page, () => getCellColor(6, 1), [Math.floor((255 + 0x75) / 2), Math.floor((255 + 0x50) / 2), Math.floor((255 + 0x7b) / 2), 255]); + await pollFor(page, () => getCellColor(7, 1), [Math.floor((255 + 0x06) / 2), Math.floor((255 + 0x98) / 2), Math.floor((255 + 0x9a) / 2), 255]); + await pollFor(page, () => getCellColor(8, 1), [Math.floor((255 + 0xd3) / 2), Math.floor((255 + 0xd7) / 2), Math.floor((255 + 0xcf) / 2), 255]); + await pollFor(page, () => getCellColor(1, 2), [Math.floor((255 + 0x55) / 2), Math.floor((255 + 0x57) / 2), Math.floor((255 + 0x53) / 2), 255]); + await pollFor(page, () => getCellColor(2, 2), [Math.floor((255 + 0xef) / 2), Math.floor((255 + 0x29) / 2), Math.floor((255 + 0x29) / 2), 255]); + await pollFor(page, () => getCellColor(3, 2), [Math.floor((255 + 0x8a) / 2), Math.floor((255 + 0xe2) / 2), Math.floor((255 + 0x34) / 2), 255]); + await pollFor(page, () => getCellColor(4, 2), [Math.floor((255 + 0xfc) / 2), Math.floor((255 + 0xe9) / 2), Math.floor((255 + 0x4f) / 2), 255]); + await pollFor(page, () => getCellColor(5, 2), [Math.floor((255 + 0x72) / 2), Math.floor((255 + 0x9f) / 2), Math.floor((255 + 0xcf) / 2), 255]); + await pollFor(page, () => getCellColor(6, 2), [Math.floor((255 + 0xad) / 2), Math.floor((255 + 0x7f) / 2), Math.floor((255 + 0xa8) / 2), 255]); + await pollFor(page, () => getCellColor(7, 2), [Math.floor((255 + 0x34) / 2), Math.floor((255 + 0xe2) / 2), Math.floor((255 + 0xe2) / 2), 255]); + await pollFor(page, () => getCellColor(8, 2), [Math.floor((255 + 0xee) / 2), Math.floor((255 + 0xee) / 2), Math.floor((255 + 0xec) / 2), 255]); + // Setting and check for minimum contrast values, note that these are not + // exact to the contrast ratio, if the increase luminance algorithm + // changes then these will probably fail + await page.evaluate(`window.term.options.minimumContrastRatio = 10;`); + await pollFor(page, () => getCellColor(1, 1), [150, 153, 154, 255]); + await pollFor(page, () => getCellColor(2, 1), [229, 127, 127, 255]); + await pollFor(page, () => getCellColor(3, 1), [63, 124, 4, 255]); + await pollFor(page, () => getCellColor(4, 1), [127, 104, 0, 255]); + await pollFor(page, () => getCellColor(5, 1), [153, 178, 209, 255]); + await pollFor(page, () => getCellColor(6, 1), [186, 167, 189, 255]); + await pollFor(page, () => getCellColor(7, 1), [4, 122, 124, 255]); + await pollFor(page, () => getCellColor(8, 1), [110, 112, 108, 255]); + await pollFor(page, () => getCellColor(1, 2), [170, 171, 169, 255]); + await pollFor(page, () => getCellColor(2, 2), [215, 36, 36, 255]); + await pollFor(page, () => getCellColor(3, 2), [72, 117, 25, 255]); + await pollFor(page, () => getCellColor(4, 2), [117, 109, 36, 255]); + await pollFor(page, () => getCellColor(5, 2), [72, 103, 135, 255]); + await pollFor(page, () => getCellColor(6, 2), [125, 91, 121, 255]); + await pollFor(page, () => getCellColor(7, 2), [25, 117, 117, 255]); + await pollFor(page, () => getCellColor(8, 2), [111, 111, 110, 255]); + }); }); describe('selectionBackground', async () => { diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 354f72c2..f96a0e8a 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -33,7 +33,9 @@ jobs: exit $EXIT_CODE displayName: 'Unit tests' - script: yarn lint - displayName: 'Lint' + displayName: 'Lint code' + - script: yarn lint-api + displayName: 'Lint API' - task: PublishCodeCoverageResults@1 inputs: codeCoverageTool: Cobertura @@ -58,7 +60,9 @@ jobs: - script: yarn test-unit --forbid-only displayName: 'Unit tests' - script: yarn lint - displayName: 'Lint' + displayName: 'Lint code' + - script: yarn lint-api + displayName: 'Lint API' - job: Windows pool: @@ -78,7 +82,9 @@ jobs: - script: yarn test-unit --forbid-only displayName: 'Unit tests' - script: yarn lint - displayName: 'Lint' + displayName: 'Lint code' + - script: yarn lint-api + displayName: 'Lint API' - job: Linux_IntegrationTests pool: diff --git a/demo/client.ts b/demo/client.ts index f8e5d4c4..ff38623c 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -430,6 +430,7 @@ function initOptions(term: TerminalType): void { ]; const stringOptions = { cursorStyle: ['block', 'underline', 'bar'], + cursorInactiveStyle: ['outline', 'block', 'bar', 'underline', 'none'], fastScrollModifier: ['none', 'alt', 'ctrl', 'shift'], fontFamily: null, fontWeight: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'], diff --git a/package.json b/package.json index 5ebc7762..5fc4e558 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "start": "node demo/start", "start-debug": "node --inspect-brk demo/start", "lint": "eslint -c .eslintrc.json --max-warnings 0 --ext .ts src/ addons/", + "lint-api": "eslint --no-eslintrc -c .eslintrc.json.typings --max-warnings 0 --no-ignore --ext .d.ts typings/", "test": "npm run test-unit", "posttest": "npm run lint", "test-api": "npm run test-api-chromium", @@ -43,9 +44,10 @@ "prepare": "npm run setup", "setup": "npm run build", "presetup": "node ./bin/install-addons.js", - "postsetup": "cd addons/xterm-addon-image && npm run inwasm -- -S", + "postsetup": "npm run inwasm", "prepublishOnly": "npm run package", "watch": "tsc -b -w ./tsconfig.all.json --preserveWatchOutput", + "inwasm": "cd addons/xterm-addon-image && npm run inwasm -- -S", "benchmark": "NODE_PATH=./out xterm-benchmark -r 5 -c test/benchmark/benchmark.json", "benchmark-baseline": "NODE_PATH=./out xterm-benchmark -r 5 -c test/benchmark/benchmark.json --baseline out-test/benchmark/test/benchmark/*benchmark.js", "benchmark-eval": "NODE_PATH=./out xterm-benchmark -r 5 -c test/benchmark/benchmark.json --eval out-test/benchmark/test/benchmark/*benchmark.js", diff --git a/src/browser/Linkifier2.ts b/src/browser/Linkifier2.ts index c1d1d282..64467426 100644 --- a/src/browser/Linkifier2.ts +++ b/src/browser/Linkifier2.ts @@ -109,7 +109,8 @@ export class Linkifier2 extends Disposable implements ILinkifier2 { } private _handleHover(position: IBufferCellPosition): void { - // TODO: This currently does not cache link provider results across wrapped lines, activeLine should be something like `activeRange: {startY, endY}` + // TODO: This currently does not cache link provider results across wrapped lines, activeLine + // should be something like `activeRange: {startY, endY}` // Check if we need to clear the link if (this._activeLine !== position.y || this._wasResized) { this._clearCurrentLink(); diff --git a/src/browser/OscLinkProvider.ts b/src/browser/OscLinkProvider.ts index 648ffa44..fee1ae7c 100644 --- a/src/browser/OscLinkProvider.ts +++ b/src/browser/OscLinkProvider.ts @@ -104,7 +104,8 @@ export class OscLinkProvider implements ILinkProvider { } } - // TODO: Handle fetching and returning other link ranges to underline other links with the same id + // TODO: Handle fetching and returning other link ranges to underline other links with the same + // id callback(result); } } diff --git a/src/browser/TestUtils.test.ts b/src/browser/TestUtils.test.ts index 2015d046..5981a6b5 100644 --- a/src/browser/TestUtils.test.ts +++ b/src/browser/TestUtils.test.ts @@ -529,6 +529,8 @@ export class MockThemeService implements IThemeService{ css.toColor('#ad7fa8'), css.toColor('#34e2e2'), css.toColor('#eeeeec') - ] + ], + selectionBackgroundOpaque: css.toColor('#ff0000'), + selectionInactiveBackgroundOpaque: css.toColor('#00ff00') } as any; } diff --git a/src/browser/Types.d.ts b/src/browser/Types.d.ts index 5337a169..d32b6099 100644 --- a/src/browser/Types.d.ts +++ b/src/browser/Types.d.ts @@ -136,7 +136,10 @@ export interface IColorSet { selectionInactiveBackgroundTransparent: IColor; selectionInactiveBackgroundOpaque: IColor; ansi: IColor[]; + /** Maps original colors to colors that respect minimum contrast ratio. */ contrastCache: IColorContrastCache; + /** Maps original colors to colors that respect _half_ of the minimum contrast ratio. */ + halfContrastCache: IColorContrastCache; } export type ReadonlyColorSet = Readonly> & { ansi: Readonly['ansi']> }; diff --git a/src/browser/Viewport.ts b/src/browser/Viewport.ts index 48e97341..7c1ae945 100644 --- a/src/browser/Viewport.ts +++ b/src/browser/Viewport.ts @@ -65,8 +65,8 @@ export class Viewport extends Disposable implements IViewport { super(); // Measure the width of the scrollbar. If it is 0 we can assume it's an OSX overlay scrollbar. - // Unfortunately the overlay scrollbar would be hidden underneath the screen element in that case, - // therefore we account for a standard amount to make it visible + // Unfortunately the overlay scrollbar would be hidden underneath the screen element in that + // case, therefore we account for a standard amount to make it visible this.scrollBarWidth = (this._viewportElement.offsetWidth - this._scrollArea.offsetWidth) || FALLBACK_SCROLL_BAR_WIDTH; this.register(addDisposableDomListener(this._viewportElement, 'scroll', this._handleScroll.bind(this))); diff --git a/src/browser/decorations/BufferDecorationRenderer.ts b/src/browser/decorations/BufferDecorationRenderer.ts index fb77ce9c..ba4f6ca8 100644 --- a/src/browser/decorations/BufferDecorationRenderer.ts +++ b/src/browser/decorations/BufferDecorationRenderer.ts @@ -103,6 +103,10 @@ export class BufferDecorationRenderer extends Disposable { decoration.element = element; this._decorationElements.set(decoration, element); this._container.appendChild(element); + decoration.onDispose(() => { + this._decorationElements.delete(decoration); + element!.remove(); + }); } element.style.top = `${line * this._renderService.dimensions.css.cell.height}px`; element.style.display = this._altBufferIsActive ? 'none' : 'block'; diff --git a/src/browser/renderer/dom/DomRenderer.ts b/src/browser/renderer/dom/DomRenderer.ts index cd854696..1449aee6 100644 --- a/src/browser/renderer/dom/DomRenderer.ts +++ b/src/browser/renderer/dom/DomRenderer.ts @@ -197,23 +197,20 @@ export class DomRenderer extends Disposable implements IRenderer { `}`; // Cursor styles += - `${this._terminalSelector} .${ROW_CONTAINER_CLASS}:not(.${FOCUS_CLASS}) .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_STYLE_BLOCK_CLASS} ,` + - `${this._terminalSelector} .${ROW_CONTAINER_CLASS}:not(.${FOCUS_CLASS}) .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_STYLE_BAR_CLASS} ,` + - `${this._terminalSelector} .${ROW_CONTAINER_CLASS}:not(.${FOCUS_CLASS}) .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_STYLE_UNDERLINE_CLASS} ` + - `{` + - ` outline: 1px solid ${colors.cursor.css};` + - ` outline-offset: -1px;` + - `}` + `${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_BLINK_CLASS}:not(.${RowCss.CURSOR_STYLE_BLOCK_CLASS}) {` + ` animation: blink_box_shadow` + `_` + this._terminalClass + ` 1s step-end infinite;` + `}` + `${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_BLINK_CLASS}.${RowCss.CURSOR_STYLE_BLOCK_CLASS} {` + ` animation: blink_block` + `_` + this._terminalClass + ` 1s step-end infinite;` + `}` + - `${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_STYLE_BLOCK_CLASS} {` + + `${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_STYLE_BLOCK_CLASS} {` + ` background-color: ${colors.cursor.css};` + ` color: ${colors.cursorAccent.css};` + `}` + + `${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_STYLE_OUTLINE_CLASS} {` + + ` outline: 1px solid ${colors.cursor.css};` + + ` outline-offset: -1px;` + + `}` + `${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_STYLE_BAR_CLASS} {` + ` box-shadow: ${this._optionsService.rawOptions.cursorWidth}px 0 0 ${colors.cursor.css} inset;` + `}` + @@ -392,7 +389,8 @@ export class DomRenderer extends Disposable implements IRenderer { public clear(): void { for (const e of this._rowElements) { /** - * NOTE: This used to be `e.innerText = '';` but that doesn't work when using `jsdom` and `@testing-library/react` + * NOTE: This used to be `e.innerText = '';` but that doesn't work when using `jsdom` and + * `@testing-library/react` * * references: * - https://github.com/testing-library/react-testing-library/issues/1146 @@ -408,6 +406,7 @@ export class DomRenderer extends Disposable implements IRenderer { const cursorX = Math.min(buffer.x, this._bufferService.cols - 1); const cursorBlink = this._optionsService.rawOptions.cursorBlink; const cursorStyle = this._optionsService.rawOptions.cursorStyle; + const cursorInactiveStyle = this._optionsService.rawOptions.cursorInactiveStyle; for (let y = start; y <= end; y++) { const row = y + buffer.ydisp; @@ -422,6 +421,7 @@ export class DomRenderer extends Disposable implements IRenderer { row, row === cursorAbsoluteY, cursorStyle, + cursorInactiveStyle, cursorX, cursorBlink, this.dimensions.css.cell.width, @@ -474,6 +474,7 @@ export class DomRenderer extends Disposable implements IRenderer { const cursorX = Math.min(buffer.x, cols - 1); const cursorBlink = this._optionsService.rawOptions.cursorBlink; const cursorStyle = this._optionsService.rawOptions.cursorStyle; + const cursorInactiveStyle = this._optionsService.rawOptions.cursorInactiveStyle; // refresh rows within link range for (let i = y; i <= y2; ++i) { @@ -489,6 +490,7 @@ export class DomRenderer extends Disposable implements IRenderer { row, row === cursorAbsoluteY, cursorStyle, + cursorInactiveStyle, cursorX, cursorBlink, this.dimensions.css.cell.width, diff --git a/src/browser/renderer/dom/DomRendererRowFactory.test.ts b/src/browser/renderer/dom/DomRendererRowFactory.test.ts index 455a401d..36923dbb 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.test.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.test.ts @@ -39,7 +39,7 @@ describe('DomRendererRowFactory', () => { describe('createRow', () => { it('should not create anything for an empty row', () => { - const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); assert.equal(extractHtml(spans), '' ); @@ -50,7 +50,7 @@ describe('DomRendererRowFactory', () => { lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, '語', 2, '語'.charCodeAt(0)])); // There should be no element for the following "empty" cell lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, '', 0, 0])); - const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); assert.equal(extractHtml(spans), '' ); @@ -58,7 +58,7 @@ describe('DomRendererRowFactory', () => { it('should add class for cursor and cursor style', () => { for (const style of ['block', 'bar', 'underline']) { - const spans = rowFactory.createRow(lineData, 0, true, style, 0, false, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, true, style, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); assert.equal(extractHtml(spans), ` ` ); @@ -66,18 +66,42 @@ describe('DomRendererRowFactory', () => { }); it('should add class for cursor blink', () => { - const spans = rowFactory.createRow(lineData, 0, true, 'block', 0, true, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, true, 'block', undefined, 0, true, 5, EMPTY_WIDTH, -1, -1); assert.equal(extractHtml(spans), ` ` ); }); + it('should add class for inactive cursor', () => { + const coreBrowserService = new MockCoreBrowserService(); + coreBrowserService.isFocused = false; + const rowFactory = new DomRendererRowFactory( + dom.window.document, + new MockCharacterJoinerService(), + new MockOptionsService({ drawBoldTextInBrightColors: true }), + coreBrowserService, + new MockCoreService(), + new MockDecorationService(), + new MockThemeService() + ); + for (const inactiveStyle of ['outline', 'block', 'bar', 'underline', 'none']){ + const spans = rowFactory.createRow(lineData, 0, true, 'block', inactiveStyle, 0, false, 5, EMPTY_WIDTH, -1, -1); + if (inactiveStyle === 'none') { + assert.equal(extractHtml(spans), + ` `); + } else { + assert.equal(extractHtml(spans), + ` `); + } + } + }); + describe('attributes', () => { it('should add class for bold', () => { const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]); cell.fg = DEFAULT_ATTR_DATA.fg | FgFlags.BOLD; lineData.setCell(0, cell); - const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); assert.equal(extractHtml(spans), 'a' ); @@ -87,7 +111,7 @@ describe('DomRendererRowFactory', () => { const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]); cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.ITALIC; lineData.setCell(0, cell); - const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); assert.equal(extractHtml(spans), 'a' ); @@ -97,7 +121,7 @@ describe('DomRendererRowFactory', () => { const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]); cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.DIM; lineData.setCell(0, cell); - const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); assert.equal(extractHtml(spans), 'a' ); @@ -110,7 +134,7 @@ describe('DomRendererRowFactory', () => { cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.HAS_EXTENDED; cell.extended.underlineStyle = UnderlineStyle.SINGLE; lineData.setCell(0, cell); - const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); assert.equal(extractHtml(spans), 'a' ); @@ -121,7 +145,7 @@ describe('DomRendererRowFactory', () => { cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.HAS_EXTENDED; cell.extended.underlineStyle = UnderlineStyle.DOUBLE; lineData.setCell(0, cell); - const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); assert.equal(extractHtml(spans), 'a' ); @@ -132,7 +156,7 @@ describe('DomRendererRowFactory', () => { cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.HAS_EXTENDED; cell.extended.underlineStyle = UnderlineStyle.CURLY; lineData.setCell(0, cell); - const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); assert.equal(extractHtml(spans), 'a' ); @@ -143,7 +167,7 @@ describe('DomRendererRowFactory', () => { cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.HAS_EXTENDED; cell.extended.underlineStyle = UnderlineStyle.DOTTED; lineData.setCell(0, cell); - const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); assert.equal(extractHtml(spans), 'a' ); @@ -154,7 +178,7 @@ describe('DomRendererRowFactory', () => { cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.HAS_EXTENDED; cell.extended.underlineStyle = UnderlineStyle.DASHED; lineData.setCell(0, cell); - const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); assert.equal(extractHtml(spans), 'a' ); @@ -165,7 +189,7 @@ describe('DomRendererRowFactory', () => { const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]); cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.OVERLINE; lineData.setCell(0, cell); - const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); assert.equal(extractHtml(spans), 'a' ); @@ -175,7 +199,7 @@ describe('DomRendererRowFactory', () => { const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]); cell.fg = DEFAULT_ATTR_DATA.fg | FgFlags.STRIKETHROUGH; lineData.setCell(0, cell); - const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); assert.equal(extractHtml(spans), 'a' ); @@ -188,7 +212,7 @@ describe('DomRendererRowFactory', () => { cell.fg &= ~Attributes.PCOLOR_MASK; cell.fg |= i; lineData.setCell(0, cell); - const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); assert.equal(extractHtml(spans), `a` ); @@ -202,7 +226,7 @@ describe('DomRendererRowFactory', () => { cell.bg &= ~Attributes.PCOLOR_MASK; cell.bg |= i; lineData.setCell(0, cell); - const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); assert.equal(extractHtml(spans), `a` ); @@ -214,7 +238,7 @@ describe('DomRendererRowFactory', () => { cell.fg |= Attributes.CM_P16 | 2 | FgFlags.INVERSE; cell.bg |= Attributes.CM_P16 | 1; lineData.setCell(0, cell); - const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); assert.equal(extractHtml(spans), 'a' ); @@ -225,7 +249,7 @@ describe('DomRendererRowFactory', () => { cell.fg |= FgFlags.INVERSE; cell.bg |= Attributes.CM_P16 | 1; lineData.setCell(0, cell); - const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); assert.equal(extractHtml(spans), 'a' ); @@ -235,7 +259,7 @@ describe('DomRendererRowFactory', () => { const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]); cell.fg |= Attributes.CM_P16 | 1 | FgFlags.INVERSE; lineData.setCell(0, cell); - const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); assert.equal(extractHtml(spans), 'a' ); @@ -248,7 +272,7 @@ describe('DomRendererRowFactory', () => { cell.fg &= ~Attributes.PCOLOR_MASK; cell.fg |= i; lineData.setCell(0, cell); - const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); assert.equal(extractHtml(spans), `a` ); @@ -260,7 +284,7 @@ describe('DomRendererRowFactory', () => { cell.fg |= Attributes.CM_RGB | 1 << 16 | 2 << 8 | 3; cell.bg |= Attributes.CM_RGB | 4 << 16 | 5 << 8 | 6; lineData.setCell(0, cell); - const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); assert.equal(extractHtml(spans), 'a' ); @@ -271,7 +295,7 @@ describe('DomRendererRowFactory', () => { cell.fg |= Attributes.CM_RGB | 1 << 16 | 2 << 8 | 3 | FgFlags.INVERSE; cell.bg |= Attributes.CM_RGB | 4 << 16 | 5 << 8 | 6; lineData.setCell(0, cell); - const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); assert.equal(extractHtml(spans), 'a' ); @@ -283,17 +307,17 @@ describe('DomRendererRowFactory', () => { lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)])); lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)])); rowFactory.handleSelectionChanged([1, 0], [2, 0], false); - const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); assert.equal(extractHtml(spans), - 'ab' + 'ab' ); }); it('should force whitespace cells to be rendered above the background', () => { lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)])); rowFactory.handleSelectionChanged([0, 0], [2, 0], false); - const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); assert.equal(extractHtml(spans), - ' a' + ' a' ); }); }); @@ -308,7 +332,7 @@ describe('DomRendererRowFactory', () => { }); it('should not create anything for an empty row', () => { - const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); assert.equal(extractHtml(spans), '' ); @@ -318,7 +342,7 @@ describe('DomRendererRowFactory', () => { lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)])); lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)])); lineData.setCell(2, CellData.fromCharData([DEFAULT_ATTR, 'c', 1, 'c'.charCodeAt(0)])); - const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); assert.equal(extractHtml(spans), 'abc' ); @@ -329,7 +353,7 @@ describe('DomRendererRowFactory', () => { lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)])); lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, '€', 1, '€'.charCodeAt(0)])); lineData.setCell(2, CellData.fromCharData([DEFAULT_ATTR, 'c', 1, 'c'.charCodeAt(0)])); - const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); assert.equal(extractHtml(spans), 'ac' ); @@ -344,7 +368,7 @@ describe('DomRendererRowFactory', () => { lineData.setCell(1, aColor1); lineData.setCell(2, bColor2); lineData.setCell(3, bColor2); - const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); assert.equal(extractHtml(spans), 'aabb' ); @@ -356,7 +380,7 @@ describe('DomRendererRowFactory', () => { lineData.setCell(2, CellData.fromCharData([DEFAULT_ATTR, 'X', 1, 'X'.charCodeAt(0)])); lineData.setCell(3, CellData.fromCharData([DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)])); lineData.setCell(4, CellData.fromCharData([DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)])); - const spans = rowFactory.createRow(lineData, 0, true, undefined, 2, false, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, true, undefined, undefined, 2, false, 5, EMPTY_WIDTH, -1, -1); assert.equal(extractHtml(spans), 'aaXbb' ); @@ -369,7 +393,7 @@ describe('DomRendererRowFactory', () => { nullCell.bg = Attributes.CM_P16 | 2; lineData.setCell(3, nullCell); lineData.setCell(4, nullCell); - const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); assert.equal(extractHtml(spans), ' ' ); @@ -379,23 +403,23 @@ describe('DomRendererRowFactory', () => { const nullCell = lineData.loadCell(0, new CellData()); nullCell.bg = Attributes.CM_P16 | 1; lineData.setCell(0, nullCell); - let spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + let spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); assert.equal(extractHtml(spans), ' ' ); lineData.setCell(1, nullCell); - spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); assert.equal(extractHtml(spans), ' ' ); lineData.setCell(2, nullCell); lineData.setCell(3, nullCell); - spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); assert.equal(extractHtml(spans), ' ' ); lineData.setCell(4, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)])); - spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); assert.equal(extractHtml(spans), ' a' ); @@ -410,7 +434,7 @@ describe('DomRendererRowFactory', () => { lineData.setCell(2, CellData.fromCharData([DEFAULT_ATTR, 'c', 1, 'c'.charCodeAt(0)])); lineData.setCell(3, CellData.fromCharData([DEFAULT_ATTR, '語', 2, 'c'.charCodeAt(0)])); lineData.setCell(4, CellData.fromCharData([DEFAULT_ATTR, '𝄞', 1, 'c'.charCodeAt(0)])); - const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); + const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); assert.equal(extractHtml(spans), 'ac語𝄞' ); @@ -424,7 +448,7 @@ describe('DomRendererRowFactory', () => { lineData.setCell(4, CellData.fromCharData([DEFAULT_ATTR, 'x', 1, 'x'.charCodeAt(0)])); lineData.setCell(5, CellData.fromCharData([DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)])); lineData.setCell(6, CellData.fromCharData([DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)])); - const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, 2, 4); + const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, 2, 4); assert.equal(extractHtml(spans), 'aaxxxbb' ); @@ -435,7 +459,7 @@ describe('DomRendererRowFactory', () => { lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)])); lineData.setCell(2, CellData.fromCharData([DEFAULT_ATTR, 'x', 1, 'x'.charCodeAt(0)])); lineData.setCell(4, CellData.fromCharData([DEFAULT_ATTR, 'x', 1, 'x'.charCodeAt(0)])); - const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, 2, 4); + const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, 2, 4); assert.equal(extractHtml(spans), 'aax x' ); @@ -445,7 +469,7 @@ describe('DomRendererRowFactory', () => { for (let i = 0; i < 10; ++i) { lineData.setCell(i, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)])); } - const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -100, 100); + const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -100, 100); assert.equal(extractHtml(spans), 'aaaaaaaaaa' ); diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts index 186a1eb3..dfb17a1d 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.ts @@ -14,6 +14,7 @@ import { JoinedCellData } from 'browser/services/CharacterJoinerService'; import { excludeFromContrastRatioDemands } from 'browser/renderer/shared/RendererUtils'; import { AttributeData } from 'common/buffer/AttributeData'; import { WidthCache } from 'browser/renderer/dom/WidthCache'; +import { IColorContrastCache } from 'browser/Types'; export const enum RowCss { @@ -26,6 +27,7 @@ export const enum RowCss { CURSOR_CLASS = 'xterm-cursor', CURSOR_BLINK_CLASS = 'xterm-cursor-blink', CURSOR_STYLE_BLOCK_CLASS = 'xterm-cursor-block', + CURSOR_STYLE_OUTLINE_CLASS = 'xterm-cursor-outline', CURSOR_STYLE_BAR_CLASS = 'xterm-cursor-bar', CURSOR_STYLE_UNDERLINE_CLASS = 'xterm-cursor-underline' } @@ -61,6 +63,7 @@ export class DomRendererRowFactory { row: number, isCursorRow: boolean, cursorStyle: string | undefined, + cursorInactiveStyle: string | undefined, cursorX: number, cursorBlink: boolean, cellWidth: number, @@ -86,6 +89,7 @@ export class DomRendererRowFactory { let oldExt = 0; let oldLinkHover: number | boolean = false; let oldSpacing = 0; + let oldIsInSelection: boolean = false; let spacing = 0; const classes: string[] = []; @@ -151,16 +155,24 @@ export class DomRendererRowFactory { /** * chars can only be merged on existing span if: * - existing span only contains mergeable chars (cellAmount != 0) - * - fg/bg/ul did not change - * - char not part of a selection + * - bg did not change (or both are in selection) + * - fg did not change (or both are in selection and selection fg is set) + * - ext did not change * - underline from hover state did not change * - cell content renders to same letter-spacing * - cell is not cursor */ if ( cellAmount - && cell.bg === oldBg && cell.fg === oldFg && cell.extended.ext === oldExt - && !isInSelection + && ( + (isInSelection && oldIsInSelection) + || (!isInSelection && cell.bg === oldBg) + ) + && ( + (isInSelection && oldIsInSelection && colors.selectionForeground) + || (!(isInSelection && oldIsInSelection && colors.selectionForeground) && cell.fg === oldFg) + ) + && cell.extended.ext === oldExt && isLinkHover === oldLinkHover && spacing === oldSpacing && !isCursorCell @@ -191,6 +203,7 @@ export class DomRendererRowFactory { oldExt = cell.extended.ext; oldLinkHover = isLinkHover; oldSpacing = spacing; + oldIsInSelection = isInSelection; if (isJoined) { // The DOM renderer colors the background of the cursor but for ligatures all cells are @@ -203,16 +216,37 @@ export class DomRendererRowFactory { if (!this._coreService.isCursorHidden && isCursorCell) { classes.push(RowCss.CURSOR_CLASS); - if (cursorBlink) { - classes.push(RowCss.CURSOR_BLINK_CLASS); + if (this._coreBrowserService.isFocused) { + if (cursorBlink) { + classes.push(RowCss.CURSOR_BLINK_CLASS); + } + classes.push( + cursorStyle === 'bar' + ? RowCss.CURSOR_STYLE_BAR_CLASS + : cursorStyle === 'underline' + ? RowCss.CURSOR_STYLE_UNDERLINE_CLASS + : RowCss.CURSOR_STYLE_BLOCK_CLASS + ); + } else { + if (cursorInactiveStyle) { + switch (cursorInactiveStyle) { + case 'outline': + classes.push(RowCss.CURSOR_STYLE_OUTLINE_CLASS); + break; + case 'block': + classes.push(RowCss.CURSOR_STYLE_BLOCK_CLASS); + break; + case 'bar': + classes.push(RowCss.CURSOR_STYLE_BAR_CLASS); + break; + case 'underline': + classes.push(RowCss.CURSOR_STYLE_UNDERLINE_CLASS); + break; + default: + break; + } + } } - classes.push( - cursorStyle === 'bar' - ? RowCss.CURSOR_STYLE_BAR_CLASS - : cursorStyle === 'underline' - ? RowCss.CURSOR_STYLE_UNDERLINE_CLASS - : RowCss.CURSOR_STYLE_BLOCK_CLASS - ); } if (cell.isBold()) { @@ -262,7 +296,8 @@ export class DomRendererRowFactory { classes.push(RowCss.STRIKETHROUGH_CLASS); } - // apply link hover underline late, effectively overrides any previous text-decoration settings + // apply link hover underline late, effectively overrides any previous text-decoration + // settings if (isLinkHover) { charElement.style.textDecoration = 'underline'; } @@ -303,22 +338,26 @@ export class DomRendererRowFactory { isTop = d.options.layer === 'top'; }); - // Apply selection foreground if applicable - if (!isTop) { - if (colors.selectionForeground && isInSelection) { + // Apply selection + if (!isTop && isInSelection) { + // If in the selection, force the element to be above the selection to improve contrast and + // support opaque selections. The applies background is not actually needed here as + // selection is drawn in a seperate container, the main purpose of this to ensuring minimum + // contrast ratio + bgOverride = this._coreBrowserService.isFocused ? colors.selectionBackgroundOpaque : colors.selectionInactiveBackgroundOpaque; + bg = bgOverride.rgba >> 8 & 0xFFFFFF; + bgColorMode = Attributes.CM_RGB; + // Since an opaque selection is being rendered, the selection pretends to be a decoration to + // ensure text is drawn above the selection. + isTop = true; + // Apply selection foreground if applicable + if (colors.selectionForeground) { fgColorMode = Attributes.CM_RGB; fg = colors.selectionForeground.rgba >> 8 & 0xFFFFFF; fgOverride = colors.selectionForeground; } } - // If in the selection, force the element to be above the selection to improve contrast and - // support opaque selections - if (isInSelection) { - bgOverride = this._coreBrowserService.isFocused ? colors.selectionBackgroundOpaque : colors.selectionInactiveBackgroundOpaque; - isTop = true; - } - // If it's a top decoration, render above the selection if (isTop) { classes.push('xterm-decoration-top'); @@ -392,7 +431,7 @@ export class DomRendererRowFactory { } // exclude conditions for cell merging - never merge these - if (!isCursorCell && !isInSelection && !isJoined && !isDecorated) { + if (!isCursorCell && !isJoined && !isDecorated) { cellAmount++; } else { charElement.textContent = text; @@ -420,15 +459,19 @@ export class DomRendererRowFactory { } // Try get from cache first, only use the cache when there are no decoration overrides + const cache = this._getContrastCache(cell); let adjustedColor: IColor | undefined | null = undefined; if (!bgOverride && !fgOverride) { - adjustedColor = this._themeService.colors.contrastCache.getColor(bg.rgba, fg.rgba); + adjustedColor = cache.getColor(bg.rgba, fg.rgba); } // Calculate and store in cache if (adjustedColor === undefined) { - adjustedColor = color.ensureContrastRatio(bgOverride || bg, fgOverride || fg, this._optionsService.rawOptions.minimumContrastRatio); - this._themeService.colors.contrastCache.setColor((bgOverride || bg).rgba, (fgOverride || fg).rgba, adjustedColor ?? null); + // Dim cells only require half the contrast, otherwise they wouldn't be distinguishable from + // non-dim cells + const ratio = this._optionsService.rawOptions.minimumContrastRatio / (cell.isDim() ? 2 : 1); + adjustedColor = color.ensureContrastRatio(bgOverride || bg, fgOverride || fg, ratio); + cache.setColor((bgOverride || bg).rgba, (fgOverride || fg).rgba, adjustedColor ?? null); } if (adjustedColor) { @@ -439,6 +482,13 @@ export class DomRendererRowFactory { return false; } + private _getContrastCache(cell: ICellData): IColorContrastCache { + if (cell.isDim()) { + return this._themeService.colors.halfContrastCache; + } + return this._themeService.colors.contrastCache; + } + private _addStyle(element: HTMLElement, style: string): void { element.setAttribute('style', `${element.getAttribute('style') || ''}${style};`); } diff --git a/src/browser/renderer/shared/CharAtlasUtils.ts b/src/browser/renderer/shared/CharAtlasUtils.ts index 89b21dbc..955bd4e6 100644 --- a/src/browser/renderer/shared/CharAtlasUtils.ts +++ b/src/browser/renderer/shared/CharAtlasUtils.ts @@ -24,7 +24,8 @@ export function generateConfig(deviceCellWidth: number, deviceCellHeight: 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: colors.contrastCache + contrastCache: colors.contrastCache, + halfContrastCache: colors.halfContrastCache }; return { customGlyphs: options.customGlyphs, diff --git a/src/browser/renderer/shared/Constants.ts b/src/browser/renderer/shared/Constants.ts index ac698b85..b5105ec7 100644 --- a/src/browser/renderer/shared/Constants.ts +++ b/src/browser/renderer/shared/Constants.ts @@ -8,7 +8,7 @@ import { isFirefox, isLegacyEdge } from 'common/Platform'; export const INVERTED_DEFAULT_COLOR = 257; export const DIM_OPACITY = 0.5; -// The text baseline is set conditionally by browser. Using 'ideographic' for Firefox or Legacy Edge would -// result in truncated text (Issue 3353). Using 'bottom' for Chrome would result in slightly +// The text baseline is set conditionally by browser. Using 'ideographic' for Firefox or Legacy Edge +// would result in truncated text (Issue 3353). Using 'bottom' for Chrome would result in slightly // unaligned Powerline fonts (PR 3356#issuecomment-850928179). export const TEXT_BASELINE: CanvasTextBaseline = isFirefox || isLegacyEdge ? 'bottom' : 'ideographic'; diff --git a/src/browser/renderer/shared/CustomGlyphs.ts b/src/browser/renderer/shared/CustomGlyphs.ts index fc36e96c..cface515 100644 --- a/src/browser/renderer/shared/CustomGlyphs.ts +++ b/src/browser/renderer/shared/CustomGlyphs.ts @@ -349,7 +349,8 @@ const enum VectorType { * not been patched with powerline characters and also to get pixel perfect rendering as rendering * issues can occur around AA/SPAA. * - * The line variants draw beyond the cell and get clipped to ensure the end of the line is not visible. + * The line variants draw beyond the cell and get clipped to ensure the end of the line is not + * visible. * * Original symbols defined in https://github.com/powerline/fontpatcher */ diff --git a/src/browser/renderer/shared/TextureAtlas.ts b/src/browser/renderer/shared/TextureAtlas.ts index e77e595d..eec560e0 100644 --- a/src/browser/renderer/shared/TextureAtlas.ts +++ b/src/browser/renderer/shared/TextureAtlas.ts @@ -15,6 +15,7 @@ import { FourKeyMap } from 'common/MultiKeyMap'; import { IdleTaskQueue } from 'common/TaskQueue'; import { IBoundingBox, ICharAtlasConfig, IRasterizedGlyph, IRequestRedrawEvent, ITextureAtlas } from 'browser/renderer/shared/Types'; import { EventEmitter } from 'common/EventEmitter'; +import { IColorContrastCache } from 'browser/Types'; /** * A shared object which is used to draw nothing for a particular cell. @@ -298,7 +299,7 @@ export class TextureAtlas implements ITextureAtlas { case Attributes.CM_DEFAULT: default: if (inverse) { - result = this._config.colors.foreground; + result = color.opaque(this._config.colors.foreground); } else { result = this._config.colors.background; } @@ -309,8 +310,7 @@ export class TextureAtlas implements ITextureAtlas { } private _getForegroundColor(bg: number, bgColorMode: number, bgColor: number, fg: number, fgColorMode: number, fgColor: number, inverse: boolean, dim: boolean, bold: boolean, excludeFromContrastRatioDemands: boolean): IColor { - // TODO: Pass dim along to get min contrast? - const minimumContrastColor = this._getMinimumContrastColor(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, false, bold, excludeFromContrastRatioDemands); + const minimumContrastColor = this._getMinimumContrastColor(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, false, bold, dim, excludeFromContrastRatioDemands); if (minimumContrastColor) { return minimumContrastColor; } @@ -385,23 +385,26 @@ export class TextureAtlas implements ITextureAtlas { } } - private _getMinimumContrastColor(bg: number, bgColorMode: number, bgColor: number, fg: number, fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean, excludeFromContrastRatioDemands: boolean): IColor | undefined { + private _getMinimumContrastColor(bg: number, bgColorMode: number, bgColor: number, fg: number, fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean, dim: boolean, excludeFromContrastRatioDemands: boolean): IColor | undefined { if (this._config.minimumContrastRatio === 1 || excludeFromContrastRatioDemands) { return undefined; } // Try get from cache first - const adjustedColor = this._config.colors.contrastCache.getColor(bg, fg); + const cache = this._getContrastCache(dim); + const adjustedColor = cache.getColor(bg, fg); if (adjustedColor !== undefined) { return adjustedColor || undefined; } const bgRgba = this._resolveBackgroundRgba(bgColorMode, bgColor, inverse); const fgRgba = this._resolveForegroundRgba(fgColorMode, fgColor, inverse, bold); - const result = rgba.ensureContrastRatio(bgRgba, fgRgba, this._config.minimumContrastRatio); + // Dim cells only require half the contrast, otherwise they wouldn't be distinguishable from + // non-dim cells + const result = rgba.ensureContrastRatio(bgRgba, fgRgba, this._config.minimumContrastRatio / (dim ? 2 : 1)); if (!result) { - this._config.colors.contrastCache.setColor(bg, fg, null); + cache.setColor(bg, fg, null); return undefined; } @@ -410,11 +413,18 @@ export class TextureAtlas implements ITextureAtlas { (result >> 16) & 0xFF, (result >> 8) & 0xFF ); - this._config.colors.contrastCache.setColor(bg, fg, color); + cache.setColor(bg, fg, color); return color; } + private _getContrastCache(dim: boolean): IColorContrastCache { + if (dim) { + return this._config.colors.halfContrastCache; + } + return this._config.colors.contrastCache; + } + private _drawToCache(codeOrChars: number | string, bg: number, fg: number, ext: number, restrictToCellHeight: boolean = false): IRasterizedGlyph { const chars = typeof codeOrChars === 'number' ? String.fromCharCode(codeOrChars) : codeOrChars; @@ -466,8 +476,8 @@ export class TextureAtlas implements ITextureAtlas { // draw the background const backgroundColor = this._getBackgroundColor(bgColorMode, bgColor, inverse, dim); - // Use a 'copy' composite operation to clear any existing glyph out of _tmpCtxWithAlpha, regardless of - // transparency in backgroundColor + // Use a 'copy' composite operation to clear any existing glyph out of _tmpCtxWithAlpha, + // regardless of transparency in backgroundColor this._tmpCtx.globalCompositeOperation = 'copy'; this._tmpCtx.fillStyle = backgroundColor.css; this._tmpCtx.fillRect(0, 0, this._tmpCanvas.width, this._tmpCanvas.height); @@ -532,10 +542,9 @@ export class TextureAtlas implements ITextureAtlas { // Underline style/stroke this._tmpCtx.beginPath(); const xLeft = padding; - const yTop = Math.ceil(padding + this._config.deviceCharHeight) - yOffset; - const yMid = padding + this._config.deviceCharHeight + lineWidth - yOffset; - const yBot = Math.ceil(padding + this._config.deviceCharHeight + lineWidth * 2) - yOffset; - const ySpace = lineWidth * 2; + const yTop = Math.ceil(padding + this._config.deviceCharHeight) - yOffset - (restrictToCellHeight ? lineWidth * 2 : 0); + const yMid = yTop + lineWidth; + const yBot = yTop + lineWidth * 2; for (let i = 0; i < chWidth; i++) { this._tmpCtx.save(); @@ -544,17 +553,10 @@ export class TextureAtlas implements ITextureAtlas { const xChMid = xChLeft + this._config.deviceCellWidth / 2; switch (this._workAttributeData.extended.underlineStyle) { case UnderlineStyle.DOUBLE: - if (restrictToCellHeight) { - this._tmpCtx.moveTo(xChLeft, yTop - ySpace); - this._tmpCtx.lineTo(xChRight, yTop - ySpace); - this._tmpCtx.moveTo(xChLeft, yTop); - this._tmpCtx.lineTo(xChRight, yTop); - } else { - this._tmpCtx.moveTo(xChLeft, yTop); - this._tmpCtx.lineTo(xChRight, yTop); - this._tmpCtx.moveTo(xChLeft, yBot); - this._tmpCtx.lineTo(xChRight, yBot); - } + this._tmpCtx.moveTo(xChLeft, yTop); + this._tmpCtx.lineTo(xChRight, yTop); + this._tmpCtx.moveTo(xChLeft, yBot); + this._tmpCtx.lineTo(xChRight, yBot); break; case UnderlineStyle.CURLY: // Choose the bezier top and bottom based on the device pixel ratio, the curly line is @@ -567,7 +569,8 @@ export class TextureAtlas implements ITextureAtlas { const clipRegion = new Path2D(); clipRegion.rect(xChLeft, yTop, this._config.deviceCellWidth, yBot - yTop); this._tmpCtx.clip(clipRegion); - // Start 1/2 cell before and end 1/2 cells after to ensure a smooth curve with other cells + // Start 1/2 cell before and end 1/2 cells after to ensure a smooth curve with other + // cells this._tmpCtx.moveTo(xChLeft - this._config.deviceCellWidth / 2, yMid); this._tmpCtx.bezierCurveTo( xChLeft - this._config.deviceCellWidth / 2, yCurlyTop, diff --git a/src/browser/services/CharSizeService.ts b/src/browser/services/CharSizeService.ts index 8e2a7019..614b9b30 100644 --- a/src/browser/services/CharSizeService.ts +++ b/src/browser/services/CharSizeService.ts @@ -60,7 +60,8 @@ interface IMeasureResult { height: number; } -// TODO: For supporting browsers we should also provide a CanvasCharDimensionsProvider that uses ctx.measureText +// TODO: For supporting browsers we should also provide a CanvasCharDimensionsProvider that uses +// ctx.measureText class DomMeasureStrategy implements IMeasureStrategy { private _result: IMeasureResult = { width: 0, height: 0 }; private _measureElement: HTMLElement; diff --git a/src/browser/services/ThemeService.test.ts b/src/browser/services/ThemeService.test.ts index f2b2def3..cdfa87df 100644 --- a/src/browser/services/ThemeService.test.ts +++ b/src/browser/services/ThemeService.test.ts @@ -35,7 +35,12 @@ describe('ThemeService', () => { describe('constructor', () => { it('should fill all colors with values', () => { for (const key of Object.keys(themeService.colors)) { - if (key !== 'ansi' && key !== 'contrastCache' && key !== 'selectionForeground') { + if (![ + 'ansi', + 'contrastCache', + 'halfContrastCache', + 'selectionForeground' + ].includes(key)) { // A #rrggbb or rgba(...) assert.ok((themeService.colors as any)[key].css.length >= 7); } diff --git a/src/browser/services/ThemeService.ts b/src/browser/services/ThemeService.ts index 2411ce85..58c9f354 100644 --- a/src/browser/services/ThemeService.ts +++ b/src/browser/services/ThemeService.ts @@ -81,7 +81,8 @@ export class ThemeService extends Disposable implements IThemeService { public serviceBrand: undefined; private _colors: IColorSet; - private _contrastCache: IColorContrastCache; + private _contrastCache: IColorContrastCache = new ColorContrastCache(); + private _halfContrastCache: IColorContrastCache = new ColorContrastCache(); private _restoreColors!: IRestoreColorSet; public get colors(): ReadonlyColorSet { return this._colors; } @@ -94,7 +95,6 @@ export class ThemeService extends Disposable implements IThemeService { ) { super(); - this._contrastCache = new ColorContrastCache(); this._colors = { foreground: DEFAULT_FOREGROUND, background: DEFAULT_BACKGROUND, @@ -106,7 +106,8 @@ export class ThemeService extends Disposable implements IThemeService { selectionInactiveBackgroundTransparent: DEFAULT_SELECTION, selectionInactiveBackgroundOpaque: color.blend(DEFAULT_BACKGROUND, DEFAULT_SELECTION), ansi: DEFAULT_ANSI_COLORS.slice(), - contrastCache: this._contrastCache + contrastCache: this._contrastCache, + halfContrastCache: this._halfContrastCache }; this._updateRestoreColors(); this._setTheme(this._optionsService.rawOptions.theme); @@ -172,6 +173,7 @@ export class ThemeService extends Disposable implements IThemeService { } // Clear our the cache this._contrastCache.clear(); + this._halfContrastCache.clear(); this._updateRestoreColors(); this._onChangeColors.fire(this.colors); } diff --git a/src/common/Color.ts b/src/common/Color.ts index 0d7bffe0..108d72f3 100644 --- a/src/common/Color.ts +++ b/src/common/Color.ts @@ -106,7 +106,8 @@ export namespace color { } /** - * Helper functions where the source type is "css" (string: '#rgb', '#rgba', '#rrggbb', '#rrggbbaa'). + * Helper functions where the source type is "css" (string: '#rgb', '#rgba', '#rrggbb', + * '#rrggbbaa'). */ export namespace css { let $ctx: CanvasRenderingContext2D | undefined; diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index 60a92266..8d36fb21 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -421,11 +421,11 @@ export class InputHandler extends Disposable implements IInputHandler { * - undefined (void): * all handlers were sync, no stack save, continue normally with next chunk * - Promise\: - * execution stopped at async handler, stack saved, continue with - * same chunk and the promise resolve value as `promiseResult` until the method returns `undefined` + * execution stopped at async handler, stack saved, continue with same chunk and the promise + * resolve value as `promiseResult` until the method returns `undefined` * - * Note: This method should only be called by `Terminal.write` to ensure correct execution order and - * proper continuation of async parser handlers. + * Note: This method should only be called by `Terminal.write` to ensure correct execution order + * and proper continuation of async parser handlers. */ public parse(data: string | Uint8Array, promiseResult?: boolean): void | Promise { let result: void | Promise; @@ -804,12 +804,13 @@ export class InputHandler extends Disposable implements IInputHandler { // find last taken cell - last cell can have 3 different states: // - hasContent(true) + hasWidth(1): narrow char - we are done // - hasWidth(0): second part of wide char - we are done - // - hasContent(false) + hasWidth(1): empty cell due to early wrapping wide char, go one cell further back + // - hasContent(false) + hasWidth(1): empty cell due to early wrapping wide char, go one + // cell further back const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!; if (line.hasWidth(this._activeBuffer.x) && !line.hasContent(this._activeBuffer.x)) { this._activeBuffer.x--; - // We do this only once, since width=1 + hasContent=false currently happens only once before - // early wrapping of a wide char. + // We do this only once, since width=1 + hasContent=false currently happens only once + // before early wrapping of a wide char. // This needs to be fixed once we support graphemes taking more than 2 cells. } } @@ -1164,8 +1165,8 @@ export class InputHandler extends Disposable implements IInputHandler { } /** - * Helper method to reset cells in a terminal row. - * The cell gets replaced with the eraseChar of the terminal and the isWrapped property is set to false. + * Helper method to reset cells in a terminal row. The cell gets replaced with the eraseChar of + * the terminal and the isWrapped property is set to false. * @param y row index */ private _resetBufferLine(y: number, respectProtect: boolean = false): void { @@ -1362,8 +1363,9 @@ export class InputHandler extends Disposable implements IInputHandler { * Insert Ps (Blank) Character(s) (default = 1) (ICH). * * @vt: #Y CSI ICH "Insert Characters" "CSI Ps @" "Insert `Ps` (blank) characters (default = 1)." - * The ICH sequence inserts `Ps` blank characters. The cursor remains at the beginning of the blank characters. - * Text between the cursor and right margin moves to the right. Characters moved past the right margin are lost. + * The ICH sequence inserts `Ps` blank characters. The cursor remains at the beginning of the + * blank characters. Text between the cursor and right margin moves to the right. Characters moved + * past the right margin are lost. * * * FIXME: check against xterm - should not work outside of scroll margins (see VT520 manual) @@ -1388,8 +1390,9 @@ export class InputHandler extends Disposable implements IInputHandler { * Delete Ps Character(s) (default = 1) (DCH). * * @vt: #Y CSI DCH "Delete Character" "CSI Ps P" "Delete `Ps` characters (default=1)." - * As characters are deleted, the remaining characters between the cursor and right margin move to the left. - * Character attributes move with the characters. The terminal adds blank characters at the right margin. + * As characters are deleted, the remaining characters between the cursor and right margin move to + * the left. Character attributes move with the characters. The terminal adds blank characters at + * the right margin. * * * FIXME: check against xterm - should not work outside of scroll margins (see VT520 manual) @@ -1514,9 +1517,9 @@ export class InputHandler extends Disposable implements IInputHandler { * Insert Ps Column(s) (default = 1) (DECIC), VT420 and up. * * @vt: #Y CSI DECIC "Insert Columns" "CSI Ps ' }" "Insert `Ps` columns at cursor position." - * DECIC inserts `Ps` times blank columns at the cursor position for all lines with the scroll margins, - * moving content to the right. Content at the right margin is lost. - * DECIC has no effect outside the scrolling margins. + * DECIC inserts `Ps` times blank columns at the cursor position for all lines with the scroll + * margins, moving content to the right. Content at the right margin is lost. DECIC has no effect + * outside the scrolling margins. */ public insertColumns(params: IParams): boolean { if (this._activeBuffer.y > this._activeBuffer.scrollBottom || this._activeBuffer.y < this._activeBuffer.scrollTop) { @@ -1597,12 +1600,12 @@ export class InputHandler extends Disposable implements IInputHandler { * - wrap around is respected * - any valid sequence resets the carried forward char * - * Note: To get reset on a valid sequence working correctly without much runtime penalty, - * the preceding codepoint is stored on the parser in `this.print` and reset during `parser.parse`. + * Note: To get reset on a valid sequence working correctly without much runtime penalty, the + * preceding codepoint is stored on the parser in `this.print` and reset during `parser.parse`. * * @vt: #Y CSI REP "Repeat Preceding Character" "CSI Ps b" "Repeat preceding character `Ps` times (default=1)." - * REP repeats the previous character `Ps` times advancing the cursor, also wrapping if DECAWM is set. - * REP has no effect if the sequence does not follow a printable ASCII character + * REP repeats the previous character `Ps` times advancing the cursor, also wrapping if DECAWM is + * set. REP has no effect if the sequence does not follow a printable ASCII character * (NOOP for any other sequence in between or NON ASCII characters). */ public repeatPrecedingCharacter(params: IParams): boolean { @@ -2463,7 +2466,8 @@ export class InputHandler extends Disposable implements IInputHandler { * | 5 | Dashed underline. | #Y | * | other | Single underline. Same as `SGR 4 m`. | #Y | * - * Extended colors are supported for foreground (Ps=38), background (Ps=48) and underline (Ps=58) as follows: + * Extended colors are supported for foreground (Ps=38), background (Ps=48) and underline (Ps=58) + * as follows: * * | Ps + 1 | Meaning | Support | * | ------ | ------------------------------------------------------------- | ------- | @@ -2673,8 +2677,9 @@ export class InputHandler extends Disposable implements IInputHandler { * http://vt100.net/docs/vt220-rm/table4-10.html * * @vt: #Y CSI DECSTR "Soft Terminal Reset" "CSI ! p" "Reset several terminal attributes to initial state." - * There are two terminal reset sequences - RIS and DECSTR. While RIS performs almost a full terminal bootstrap, - * DECSTR only resets certain attributes. For most needs DECSTR should be sufficient. + * There are two terminal reset sequences - RIS and DECSTR. While RIS performs almost a full + * terminal bootstrap, DECSTR only resets certain attributes. For most needs DECSTR should be + * sufficient. * * The following terminal attributes are reset to default values: * - IRM is reset (dafault = false) @@ -2899,7 +2904,8 @@ export class InputHandler extends Disposable implements IInputHandler { * Icon name is not supported. For Window Title see below. * * @vt: #Y OSC 2 "Set Windows Title" "OSC 2 ; Pt BEL" "Set window title." - * xterm.js does not manipulate the title directly, instead exposes changes via the event `Terminal.onTitleChange`. + * xterm.js does not manipulate the title directly, instead exposes changes via the event + * `Terminal.onTitleChange`. */ public setTitle(data: string): boolean { this._windowTitle = data; @@ -2920,9 +2926,10 @@ export class InputHandler extends Disposable implements IInputHandler { * OSC 4; ; ST (set ANSI color to ) * * @vt: #Y OSC 4 "Set ANSI color" "OSC 4 ; c ; spec BEL" "Change color number `c` to the color specified by `spec`." - * `c` is the color index between 0 and 255. The color format of `spec` is derived from `XParseColor` (see OSC 10 for supported formats). - * There may be multipe `c ; spec` pairs present in the same instruction. - * If `spec` contains `?` the terminal returns a sequence with the currently set color. + * `c` is the color index between 0 and 255. The color format of `spec` is derived from + * `XParseColor` (see OSC 10 for supported formats). There may be multipe `c ; spec` pairs present + * in the same instruction. If `spec` contains `?` the terminal returns a sequence with the + * currently set color. */ public setOrReportIndexedColor(data: string): boolean { const event: IColorEvent = []; @@ -2962,9 +2969,10 @@ export class InputHandler extends Disposable implements IInputHandler { * * @vt: #Y OSC 8 "Create hyperlink" "OSC 8 ; params ; uri BEL" "Create a hyperlink to `uri` using `params`." * `uri` is a hyperlink starting with `http://`, `https://`, `ftp://`, `file://` or `mailto://`. `params` is an - * optional list of key=value assignments, separated by the : character. Example: `id=xyz123:foo=bar:baz=quux`. - * Currently only the id key is defined. Cells that share the same ID and URI share hover feedback. - * Use `OSC 8 ; ; BEL` to finish the current hyperlink. + * optional list of key=value assignments, separated by the : character. + * Example: `id=xyz123:foo=bar:baz=quux`. + * Currently only the id key is defined. Cells that share the same ID and URI share hover + * feedback. Use `OSC 8 ; ; BEL` to finish the current hyperlink. */ public setHyperlink(data: string): boolean { const args = data.split(';'); @@ -3351,8 +3359,8 @@ export class InputHandler extends Disposable implements IInputHandler { * Response: DECRPSS (https://vt100.net/docs/vt510-rm/DECRPSS.html) * * @vt: #P[Limited support, see below.] DCS DECRQSS "Request Selection or Setting" "DCS $ q Pt ST" "Request several terminal settings." - * Response is in the form `ESC P 1 $ r Pt ST` for valid requests, where `Pt` contains the corresponding CSI string, - * `ESC P 0 ST` for invalid requests. + * Response is in the form `ESC P 1 $ r Pt ST` for valid requests, where `Pt` contains the + * corresponding CSI string, `ESC P 0 ST` for invalid requests. * * Supported requests and responses: * diff --git a/src/common/SortedList.test.ts b/src/common/SortedList.test.ts index ecafdb8f..d2e01ba8 100644 --- a/src/common/SortedList.test.ts +++ b/src/common/SortedList.test.ts @@ -104,4 +104,18 @@ describe('SortedList', () => { { key: 10 } ]); }); + describe('values', () => { + it('should iterate correctly when list items change during iteration', () => { + list.insert(1); + list.insert(2); + list.insert(3); + list.insert(4); + const visited: number[] = []; + for (const item of list.values()) { + visited.push(item); + list.delete(item); + } + deepStrictEqual(visited, [1, 2, 3, 4]); + }); + }); }); diff --git a/src/common/SortedList.ts b/src/common/SortedList.ts index c5e7bc36..c3250091 100644 --- a/src/common/SortedList.ts +++ b/src/common/SortedList.ts @@ -89,7 +89,8 @@ export class SortedList { } public values(): IterableIterator { - return this._array.values(); + // Duplicate the array to avoid issues when _array changes while iterating + return [...this._array].values(); } private _search(key: number): number { diff --git a/src/common/Types.d.ts b/src/common/Types.d.ts index 4d7df460..3870fa56 100644 --- a/src/common/Types.d.ts +++ b/src/common/Types.d.ts @@ -38,6 +38,8 @@ export interface ITerminalOptions extends IPublicTerminalOptions { export type CursorStyle = 'block' | 'underline' | 'bar'; +export type CursorInactiveStyle = 'outline' | 'block' | 'bar' | 'underline' | 'none'; + export type XtermListener = (...args: any[]) => void; /** diff --git a/src/common/buffer/Buffer.ts b/src/common/buffer/Buffer.ts index f32ce385..a82a4569 100644 --- a/src/common/buffer/Buffer.ts +++ b/src/common/buffer/Buffer.ts @@ -270,8 +270,8 @@ export class Buffer implements IBuffer { private _batchedMemoryCleanup(): boolean { let normalRun = true; if (this._memoryCleanupPosition >= this.lines.length) { - // cleanup made it once through all lines, thus rescan in loop below to also catch shifted lines, - // which should finish rather quick if there are no more cleanups pending + // cleanup made it once through all lines, thus rescan in loop below to also catch shifted + // lines, which should finish rather quick if there are no more cleanups pending this._memoryCleanupPosition = 0; normalRun = false; } diff --git a/src/common/buffer/Marker.ts b/src/common/buffer/Marker.ts index 0629e26a..96df6366 100644 --- a/src/common/buffer/Marker.ts +++ b/src/common/buffer/Marker.ts @@ -11,9 +11,9 @@ export class Marker implements IMarker { private static _nextId = 1; public isDisposed: boolean = false; - private _disposables: IDisposable[] = []; + private readonly _disposables: IDisposable[] = []; - private _id: number = Marker._nextId++; + private readonly _id: number = Marker._nextId++; public get id(): number { return this._id; } private readonly _onDispose = this.register(new EventEmitter()); diff --git a/src/common/input/Keyboard.ts b/src/common/input/Keyboard.ts index 225c914e..9420a974 100644 --- a/src/common/input/Keyboard.ts +++ b/src/common/input/Keyboard.ts @@ -380,7 +380,8 @@ export function evaluateKeyboardEvent( result.type = KeyboardResultType.SELECT_ALL; } } else if (ev.key && !ev.ctrlKey && !ev.altKey && !ev.metaKey && ev.keyCode >= 48 && ev.key.length === 1) { - // Include only keys that that result in a _single_ character; don't include num lock, volume up, etc. + // Include only keys that that result in a _single_ character; don't include num lock, + // volume up, etc. result.key = ev.key; } else if (ev.key && ev.ctrlKey) { if (ev.key === '_') { // ^_ diff --git a/src/common/input/TextDecoder.ts b/src/common/input/TextDecoder.ts index 715e9197..7ec9c7cd 100644 --- a/src/common/input/TextDecoder.ts +++ b/src/common/input/TextDecoder.ts @@ -28,8 +28,8 @@ export function utf32ToString(data: Uint32Array, start: number = 0, end: number for (let i = start; i < end; ++i) { let codepoint = data[i]; if (codepoint > 0xFFFF) { - // JS strings are encoded as UTF16, thus a non BMP codepoint gets converted into a surrogate pair - // conversion rules: + // JS strings are encoded as UTF16, thus a non BMP codepoint gets converted into a surrogate + // pair conversion rules: // - subtract 0x10000 from code point, leaving a 20 bit number // - add high 10 bits to 0xD800 --> first surrogate // - add low 10 bits to 0xDC00 --> second surrogate diff --git a/src/common/input/WriteBuffer.ts b/src/common/input/WriteBuffer.ts index 68dbc6f7..6c3dbf64 100644 --- a/src/common/input/WriteBuffer.ts +++ b/src/common/input/WriteBuffer.ts @@ -181,9 +181,10 @@ export class WriteBuffer extends Disposable { /** * If a promise takes long to resolve, we should schedule continuation behind setTimeout. - * This might already be too late, if our .then enters really late (executor + prev thens took very long). - * This cannot be solved here for the handler itself (it is the handlers responsibility to slice hard work), - * but we can at least schedule a screen update as we gain control. + * This might already be too late, if our .then enters really late (executor + prev thens + * took very long). This cannot be solved here for the handler itself (it is the handlers + * responsibility to slice hard work), but we can at least schedule a screen update as we + * gain control. */ const continuation: (r: boolean) => void = (r: boolean) => Date.now() - startTime >= WRITE_TIMEOUT_MS ? setTimeout(() => this._innerWrite(0, r)) diff --git a/src/common/parser/EscapeSequenceParser.ts b/src/common/parser/EscapeSequenceParser.ts index 2f3ddd92..de206322 100644 --- a/src/common/parser/EscapeSequenceParser.ts +++ b/src/common/parser/EscapeSequenceParser.ts @@ -532,9 +532,9 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP } else { if (promiseResult === undefined || this._parseStack.state === ParserStackType.FAIL) { /** - * Reject further parsing on improper continuation after pausing. - * This is a really bad condition with screwed up execution order and prolly messed up - * terminal state, therefore we exit hard with an exception and reject any further parsing. + * Reject further parsing on improper continuation after pausing. This is a really bad + * condition with screwed up execution order and prolly messed up terminal state, + * therefore we exit hard with an exception and reject any further parsing. * * Note: With `Terminal.write` usage this exception should never occur, as the top level * calls are guaranteed to handle async conditions properly. If you ever encounter this @@ -542,9 +542,9 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP * `InputHandler.parse` or `EscapeSequenceParser.parse` synchronously without waiting for * continuation of a running async handler. * - * It is possible to get rid of this error by calling `reset`. But dont rely on that, - * as the pending async handler still might mess up the terminal later. Instead fix the faulty - * async handling, so this error will not be thrown anymore. + * It is possible to get rid of this error by calling `reset`. But dont rely on that, as + * the pending async handler still might mess up the terminal later. Instead fix the + * faulty async handling, so this error will not be thrown anymore. */ this._parseStack.state = ParserStackType.FAIL; throw new Error('improper continuation due to previous async handler, giving up parsing'); diff --git a/src/common/services/BufferService.ts b/src/common/services/BufferService.ts index 7b02cb7d..7d8e2846 100644 --- a/src/common/services/BufferService.ts +++ b/src/common/services/BufferService.ts @@ -43,7 +43,8 @@ export class BufferService extends Disposable implements IBufferService { this.cols = cols; this.rows = rows; this.buffers.resize(cols, rows); - // TODO: This doesn't fire when scrollback changes - add a resize event to BufferSet and forward event + // TODO: This doesn't fire when scrollback changes - add a resize event to BufferSet and forward + // event this._onResize.fire({ cols, rows }); } diff --git a/src/common/services/DecorationService.ts b/src/common/services/DecorationService.ts index 1ccc712c..ed96fb35 100644 --- a/src/common/services/DecorationService.ts +++ b/src/common/services/DecorationService.ts @@ -35,12 +35,7 @@ export class DecorationService extends Disposable implements IDecorationService constructor() { super(); - this.register(toDisposable(() => { - for (const d of this._decorations.values()) { - this._onDecorationRemoved.fire(d); - } - this.reset(); - })); + this.register(toDisposable(() => this.reset())); } public registerDecoration(options: IDecorationOptions): IDecoration | undefined { @@ -92,13 +87,6 @@ export class DecorationService extends Disposable implements IDecorationService } }); } - - public dispose(): void { - for (const d of this._decorations.values()) { - this._onDecorationRemoved.fire(d); - } - this.reset(); - } } class Decoration extends Disposable implements IInternalDecoration { diff --git a/src/common/services/OptionsService.ts b/src/common/services/OptionsService.ts index cbeb6188..fb4251d9 100644 --- a/src/common/services/OptionsService.ts +++ b/src/common/services/OptionsService.ts @@ -15,6 +15,7 @@ export const DEFAULT_OPTIONS: Readonly> = { cursorBlink: false, cursorStyle: 'block', cursorWidth: 1, + cursorInactiveStyle: 'outline', customGlyphs: true, drawBoldTextInBrightColors: true, fastScrollModifier: 'alt', diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index 8f5ec7c6..625fc7de 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -5,7 +5,7 @@ import { IEvent, IEventEmitter } from 'common/EventEmitter'; import { IBuffer, IBufferSet } from 'common/buffer/Types'; -import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEncoding, ICoreMouseProtocol, CoreMouseEventType, ICharset, IWindowOptions, IModes, IAttributeData, ScrollSource, IDisposable, IColor, CursorStyle, IOscLinkData } from 'common/Types'; +import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEncoding, ICoreMouseProtocol, CoreMouseEventType, ICharset, IWindowOptions, IModes, IAttributeData, ScrollSource, IDisposable, IColor, CursorStyle, CursorInactiveStyle, IOscLinkData } from 'common/Types'; import { createDecorator } from 'common/services/ServiceRegistry'; import { IDecorationOptions, IDecoration, ILinkHandler, IWindowsPty, ILogger } from 'xterm'; @@ -212,6 +212,7 @@ export interface ITerminalOptions { cursorBlink?: boolean; cursorStyle?: CursorStyle; cursorWidth?: number; + cursorInactiveStyle?: CursorInactiveStyle; customGlyphs?: boolean; disableStdin?: boolean; drawBoldTextInBrightColors?: boolean; diff --git a/typings/xterm-headless.d.ts b/typings/xterm-headless.d.ts index 4633883b..118c5905 100644 --- a/typings/xterm-headless.d.ts +++ b/typings/xterm-headless.d.ts @@ -19,7 +19,8 @@ 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. The default is false. + * marked as experimental/proposed will throw an error. The default is + * false. */ allowProposedApi?: boolean; @@ -63,13 +64,13 @@ declare module 'xterm-headless' { cursorWidth?: number; /** - * Whether to draw custom glyphs for block element and box drawing characters instead of using - * the font. This should typically result in better rendering with continuous lines, even when - * line height and letter spacing is used. Note that this doesn't work with the DOM renderer - * which renders all characters using the font. The default is true. + * Whether to draw custom glyphs for block element and box drawing + * characters instead of using the font. This should typically result in + * better rendering with continuous lines, even when line height and letter + * spacing is used. Note that this doesn't work with the DOM renderer which + * renders all characters using the font. The default is true. */ customGlyphs?: boolean; - /** * Whether input should be disabled. */ @@ -217,9 +218,9 @@ declare module 'xterm-headless' { windowsPty?: IWindowsPty; /** - * A string containing all characters that are considered word separated by the - * double click to select work logic. - */ + * A string containing all characters that are considered word separated by + * the double click to select work logic. + */ wordSeparator?: string; /** @@ -314,23 +315,23 @@ declare module 'xterm-headless' { */ export interface ILogger { /** - * Log a debug message, this will only be called if {@link ITerminalOptions.logLevel} is set to - * debug. + * Log a debug message, this will only be called if + * {@link ITerminalOptions.logLevel} is set to debug. */ debug(message: string, ...args: any[]): void; /** - * Log a debug message, this will only be called if {@link ITerminalOptions.logLevel} is set to - * info or below. + * Log a debug message, this will only be called if + * {@link ITerminalOptions.logLevel} is set to info or below. */ info(message: string, ...args: any[]): void; /** - * Log a debug message, this will only be called if {@link ITerminalOptions.logLevel} is set to - * warn or below. + * Log a debug message, this will only be called if + * {@link ITerminalOptions.logLevel} is set to warn or below. */ warn(message: string, ...args: any[]): void; /** - * Log a debug message, this will only be called if {@link ITerminalOptions.logLevel} is set to - * error or below. + * Log a debug message, this will only be called if + * {@link ITerminalOptions.logLevel} is set to error or below. */ error(message: string | Error, ...args: any[]): void; } @@ -355,29 +356,32 @@ declare module 'xterm-headless' { * is trimmed and lines are added or removed. This is a single line that may * be part of a larger wrapped line. */ - export interface IMarker extends IDisposable { + export interface IMarker extends IDisposableWithEvent { /** * A unique identifier for this marker. */ readonly id: number; - /** - * Whether this marker is disposed. - */ - readonly isDisposed: boolean; - /** * The actual line index in the buffer at this point in time. This is set to * -1 if the marker has been disposed. */ readonly line: number; + } + /** + * Represents a disposable that tracks is disposed state. + */ + export interface IDisposableWithEvent extends IDisposable { /** - * Event listener to get notified when the marker gets disposed. Automatic disposal - * might happen for a marker, that got invalidated by scrolling out or removal of - * a line from the buffer. + * Event listener to get notified when this gets disposed. */ onDispose: IEvent; + + /** + * Whether this is disposed. + */ + readonly isDisposed: boolean; } /** @@ -397,7 +401,8 @@ declare module 'xterm-headless' { } /** - * Enable various window manipulation and report features (CSI Ps ; Ps ; Ps t). + * Enable various window manipulation and report features + * (`CSI Ps ; Ps ; Ps t`). * * Most settings have no default implementation, as they heavily rely on * the embedding environment. @@ -417,10 +422,10 @@ declare module 'xterm-headless' { * * Note on security: * Most features are meant to deal with some information of the host machine - * where the terminal runs on. This is seen as a security risk possibly leaking - * sensitive data of the host to the program in the terminal. Therefore all options - * (even those without a default implementation) are guarded by the boolean flag - * and disabled by default. + * where the terminal runs on. This is seen as a security risk possibly + * leaking sensitive data of the host to the program in the terminal. + * Therefore all options (even those without a default implementation) are + * guarded by the boolean flag and disabled by default. */ export interface IWindowOptions { /** @@ -601,23 +606,36 @@ declare module 'xterm-headless' { readonly modes: IModes; /** - * Gets or sets the terminal options. This supports setting multiple options. + * Gets or sets the terminal options. This supports setting multiple + * options. * * @example Get a single option - * ```typescript + * ```ts * console.log(terminal.options.fontSize); * ``` * - * @example Set a single option - * ```typescript + * @example Set a single option: + * ```ts * terminal.options.fontSize = 12; * ``` + * Note that for options that are object, a new object must be used in order + * to take effect as a reference comparison will be done: + * ```ts + * const newValue = terminal.options.theme; + * newValue.background = '#000000'; + * + * // This won't work + * terminal.options.theme = newValue; + * + * // This will work + * terminal.options.theme = { ...newValue }; + * ``` * * @example Set multiple options - * ```typescript + * ```ts * terminal.options = { * fontSize: 12, - * fontFamily: 'Courier New', + * fontFamily: 'Courier New' * }; * ``` */ @@ -1283,6 +1301,6 @@ declare module 'xterm-headless' { /** * Auto-Wrap Mode (DECAWM): `CSI ? 7 h` */ - readonly wraparoundMode: boolean + readonly wraparoundMode: boolean; } } diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 2598903a..d7c5e320 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -26,7 +26,8 @@ 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. The default is false. + * marked as experimental/proposed will throw an error. The default is + * false. */ allowProposedApi?: boolean; @@ -60,7 +61,7 @@ declare module 'xterm' { cursorBlink?: boolean; /** - * The style of the cursor. + * The style of the cursor when the terminal is focused. */ cursorStyle?: 'block' | 'underline' | 'bar'; @@ -70,10 +71,16 @@ declare module 'xterm' { cursorWidth?: number; /** - * Whether to draw custom glyphs for block element and box drawing characters instead of using - * the font. This should typically result in better rendering with continuous lines, even when - * line height and letter spacing is used. Note that this doesn't work with the DOM renderer - * which renders all characters using the font. The default is true. + * The style of the cursor when the terminal is not focused. + */ + cursorInactiveStyle?: 'outline' | 'block' | 'bar' | 'underline' | 'none'; + + /** + * Whether to draw custom glyphs for block element and box drawing + * characters instead of using the font. This should typically result in + * better rendering with continuous lines, even when line height and letter + * spacing is used. Note that this doesn't work with the DOM renderer which + * renders all characters using the font. The default is true. */ customGlyphs?: boolean; @@ -275,9 +282,9 @@ declare module 'xterm' { windowsPty?: IWindowsPty; /** - * A string containing all characters that are considered word separated by the - * double click to select work logic. - */ + * A string containing all characters that are considered word separated by + * the double click to select work logic. + */ wordSeparator?: string; /** @@ -325,7 +332,10 @@ declare module 'xterm' { selectionBackground?: string; /** The selection foreground color */ selectionForeground?: string; - /** The selection background color when the terminal does not have focus (can be transparent) */ + /** + * The selection background color when the terminal does not have focus (can + * be transparent) + */ selectionInactiveBackground?: string; /** ANSI black (eg. `\x1b[30m`) */ black?: string; @@ -382,23 +392,23 @@ declare module 'xterm' { */ export interface ILogger { /** - * Log a debug message, this will only be called if {@link ITerminalOptions.logLevel} is set to - * debug. + * Log a debug message, this will only be called if + * {@link ITerminalOptions.logLevel} is set to debug. */ debug(message: string, ...args: any[]): void; /** - * Log a debug message, this will only be called if {@link ITerminalOptions.logLevel} is set to - * info or below. + * Log a debug message, this will only be called if + * {@link ITerminalOptions.logLevel} is set to info or below. */ info(message: string, ...args: any[]): void; /** - * Log a debug message, this will only be called if {@link ITerminalOptions.logLevel} is set to - * warn or below. + * Log a debug message, this will only be called if + * {@link ITerminalOptions.logLevel} is set to warn or below. */ warn(message: string, ...args: any[]): void; /** - * Log a debug message, this will only be called if {@link ITerminalOptions.logLevel} is set to - * error or below. + * Log a debug message, this will only be called if + * {@link ITerminalOptions.logLevel} is set to error or below. */ error(message: string | Error, ...args: any[]): void; } @@ -438,8 +448,6 @@ declare module 'xterm' { /** * Represents a disposable that tracks is disposed state. - * @param onDispose event listener and - * @param isDisposed property. */ export interface IDisposableWithEvent extends IDisposable { /** @@ -454,7 +462,8 @@ declare module 'xterm' { } /** - * Represents a decoration in the terminal that is associated with a particular marker and DOM element. + * Represents a decoration in the terminal that is associated with a + * particular marker and DOM element. */ export interface IDecoration extends IDisposableWithEvent { /* @@ -477,9 +486,9 @@ declare module 'xterm' { element: HTMLElement | undefined; /** - * The options for the overview ruler that can be updated. - * This will only take effect when {@link IDecorationOptions.overviewRulerOptions} - * were provided initially. + * The options for the overview ruler that can be updated. This will only + * take effect when {@link IDecorationOptions.overviewRulerOptions} were + * provided initially. */ options: Pick; } @@ -526,24 +535,26 @@ declare module 'xterm' { readonly height?: number; /** - * The background color of the cell(s). When 2 decorations both set the foreground color the - * last registered decoration will be used. Only the `#RRGGBB` format is supported. + * The background color of the cell(s). When 2 decorations both set the + * foreground color the last registered decoration will be used. Only the + * `#RRGGBB` format is supported. */ readonly backgroundColor?: string; /** - * The foreground color of the cell(s). When 2 decorations both set the foreground color the - * last registered decoration will be used. Only the `#RRGGBB` format is supported. + * The foreground color of the cell(s). When 2 decorations both set the + * foreground color the last registered decoration will be used. Only the + * `#RRGGBB` format is supported. */ readonly foregroundColor?: string; /** * What layer to render the decoration at when {@link backgroundColor} or - * {@link foregroundColor} are used. `'bottom'` will render under the selection, `'top`' will - * render above the selection\*. + * {@link foregroundColor} are used. `'bottom'` will render under the + * selection, `'top`' will render above the selection\*. * - * *\* The selection will render on top regardless of layer on the canvas renderer due to how - * it renders selection separately.* + * *\* The selection will render on top regardless of layer on the canvas + * renderer due to how it renders selection separately.* */ readonly layer?: 'bottom' | 'top'; @@ -554,7 +565,7 @@ declare module 'xterm' { * @param color The color of the decoration. * @param position The position of the decoration. */ - overviewRulerOptions?: IDecorationOverviewRulerOptions + overviewRulerOptions?: IDecorationOverviewRulerOptions; } /** @@ -574,7 +585,8 @@ declare module 'xterm' { } /** - * Enable various window manipulation and report features (CSI Ps ; Ps ; Ps t). + * Enable various window manipulation and report features + * (`CSI Ps ; Ps ; Ps t`). * * Most settings have no default implementation, as they heavily rely on * the embedding environment. @@ -594,10 +606,10 @@ declare module 'xterm' { * * Note on security: * Most features are meant to deal with some information of the host machine - * where the terminal runs on. This is seen as a security risk possibly leaking - * sensitive data of the host to the program in the terminal. Therefore all options - * (even those without a default implementation) are guarded by the boolean flag - * and disabled by default. + * where the terminal runs on. This is seen as a security risk possibly + * leaking sensitive data of the host to the program in the terminal. + * Therefore all options (even those without a default implementation) are + * guarded by the boolean flag and disabled by default. */ export interface IWindowOptions { /** @@ -865,9 +877,9 @@ declare module 'xterm' { onData: IEvent; /** - * Adds an event listener for when a key is pressed. The event value contains the - * string that will be sent in the data event as well as the DOM event that - * triggered it. + * Adds an event listener for when a key is pressed. The event value + * contains the string that will be sent in the data event as well as the + * DOM event that triggered it. * @returns an `IDisposable` to stop listening. */ onKey: IEvent<{ key: string, domEvent: KeyboardEvent }>; @@ -1035,10 +1047,11 @@ declare module 'xterm' { /** * (EXPERIMENTAL) Adds a decoration to the terminal using - * @param decorationOptions, which takes a marker and an optional anchor, - * width, height, and x offset from the anchor. Returns the decoration or - * undefined if the alt buffer is active or the marker has already been disposed of. - * @throws when options include a negative x offset. + * @param decorationOptions, which takes a marker and an optional anchor, + * width, height, and x offset from the anchor. Returns the decoration or + * undefined if the alt buffer is active or the marker has already been + * disposed of. + * @throws when options include a negative x offset. */ registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined; @@ -1144,7 +1157,8 @@ declare module 'xterm' { writeln(data: string | Uint8Array, callback?: () => void): void; /** - * Writes text to the terminal, performing the necessary transformations for pasted text. + * Writes text to the terminal, performing the necessary transformations for + * pasted text. * @param data The text to write to the terminal. */ paste(data: string): void; @@ -1158,10 +1172,10 @@ declare module 'xterm' { refresh(start: number, end: number): void; /** - * Clears the texture atlas of the canvas renderer if it's active. Doing this will force a - * redraw of all glyphs which can workaround issues causing the texture to become corrupt, for - * example Chromium/Nvidia has an issue where the texture gets messed up when resuming the OS - * from sleep. + * Clears the texture atlas of the canvas renderer if it's active. Doing + * this will force a redraw of all glyphs which can workaround issues + * causing the texture to become corrupt, for example Chromium/Nvidia has an + * issue where the texture gets messed up when resuming the OS from sleep. */ clearTextureAtlas(): void; @@ -1232,32 +1246,34 @@ declare module 'xterm' { * @param text The text of the link. * @param range The buffer range of the link. */ - activate(event: MouseEvent, text: string, range: IBufferRange): void; + activate(event: MouseEvent, text: string, range: IBufferRange): void; - /** - * Called when the mouse hovers the link. To use this to create a DOM-based hover tooltip, - * create the hover element within `Terminal.element` and add the `xterm-hover` class to it, - * that will cause mouse events to not fall through and activate other links. - * @param event The mouse event triggering the callback. - * @param text The text of the link. - * @param range The buffer range of the link. - */ - hover?(event: MouseEvent, text: string, range: IBufferRange): void; + /** + * Called when the mouse hovers the link. To use this to create a DOM-based + * hover tooltip, create the hover element within `Terminal.element` and + * add the `xterm-hover` class to it, that will cause mouse events to not + * fall through and activate other links. + * @param event The mouse event triggering the callback. + * @param text The text of the link. + * @param range The buffer range of the link. + */ + hover?(event: MouseEvent, text: string, range: IBufferRange): void; - /** - * Called when the mouse leaves the link. - * @param event The mouse event triggering the callback. - * @param text The text of the link. - * @param range The buffer range of the link. - */ - leave?(event: MouseEvent, text: string, range: IBufferRange): void; + /** + * Called when the mouse leaves the link. + * @param event The mouse event triggering the callback. + * @param text The text of the link. + * @param range The buffer range of the link. + */ + leave?(event: MouseEvent, text: string, range: IBufferRange): void; - /** - * Whether to receive non-HTTP URLs from LinkProvider. When false, any usage of non-HTTP URLs - * will be ignored. Enabling this option without proper protection in `activate` function - * may cause security issues such as XSS. - */ - allowNonHttpProtocols?: boolean; + /** + * Whether to receive non-HTTP URLs from LinkProvider. When false, any + * usage of non-HTTP URLs will be ignored. Enabling this option without + * proper protection in `activate` function may cause security issues such + * as XSS. + */ + allowNonHttpProtocols?: boolean; } /** @@ -1289,9 +1305,9 @@ declare module 'xterm' { text: string; /** - * What link decorations to show when hovering the link, this property is tracked and changes - * made after the link is provided will trigger changes. If not set, all decroations will be - * enabled. + * What link decorations to show when hovering the link, this property is + * tracked and changes made after the link is provided will trigger changes. + * If not set, all decroations will be enabled. */ decorations?: ILinkDecorations; @@ -1303,9 +1319,10 @@ declare module 'xterm' { activate(event: MouseEvent, text: string): void; /** - * Called when the mouse hovers the link. To use this to create a DOM-based hover tooltip, - * create the hover element within `Terminal.element` and add the `xterm-hover` class to it, - * that will cause mouse events to not fall through and activate other links. + * Called when the mouse hovers the link. To use this to create a DOM-based + * hover tooltip, create the hover element within `Terminal.element` and add + * the `xterm-hover` class to it, that will cause mouse events to not fall + * through and activate other links. * @param event The mouse event triggering the callback. * @param text The text of the link. */ @@ -1429,7 +1446,8 @@ declare module 'xterm' { export interface IBufferElementProvider { /** - * Provides a document fragment or HTMLElement containing the buffer elements. + * Provides a document fragment or HTMLElement containing the buffer + * elements. */ provideBufferElements(): DocumentFragment | HTMLElement; } @@ -1676,43 +1694,43 @@ declare module 'xterm' { export interface IParser { /** * Adds a handler for CSI escape sequences. - * @param id Specifies the function identifier under which the callback - * gets registered, e.g. {final: 'm'} for SGR. + * @param id Specifies the function identifier under which the callback gets + * registered, e.g. {final: 'm'} for SGR. * @param callback The function to handle the sequence. The callback is - * called with the numerical params. If the sequence has subparams the - * array will contain subarrays with their numercial values. - * Return `true` if the sequence was handled, `false` if the parser should try - * a previous handler. The most recently added handler is tried first. + * called with the numerical params. If the sequence has subparams the array + * will contain subarrays with their numercial values. Return `true` if the + * sequence was handled, `false` if the parser should try a previous + * handler. The most recently added handler is tried first. * @returns An IDisposable you can call to remove this handler. */ registerCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean | Promise): IDisposable; /** * Adds a handler for DCS escape sequences. - * @param id Specifies the function identifier under which the callback - * gets registered, e.g. {intermediates: '$' final: 'q'} for DECRQSS. + * @param id Specifies the function identifier under which the callback gets + * registered, e.g. {intermediates: '$' final: 'q'} for DECRQSS. * @param callback The function to handle the sequence. Note that the * function will only be called once if the sequence finished sucessfully. * There is currently no way to intercept smaller data chunks, data chunks - * will be stored up until the sequence is finished. Since DCS sequences - * are not limited by the amount of data this might impose a problem for - * big payloads. Currently xterm.js limits DCS payload to 10 MB - * which should give enough room for most use cases. - * The function gets the payload and numerical parameters as arguments. - * Return `true` if the sequence was handled, `false` if the parser should try - * a previous handler. The most recently added handler is tried first. + * will be stored up until the sequence is finished. Since DCS sequences are + * not limited by the amount of data this might impose a problem for big + * payloads. Currently xterm.js limits DCS payload to 10 MB which should + * give enough room for most use cases. The function gets the payload and + * numerical parameters as arguments. Return `true` if the sequence was + * handled, `false` if the parser should try a previous handler. The most + * recently added handler is tried first. * @returns An IDisposable you can call to remove this handler. */ registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean | Promise): IDisposable; /** * Adds a handler for ESC escape sequences. - * @param id Specifies the function identifier under which the callback - * gets registered, e.g. {intermediates: '%' final: 'G'} for - * default charset selection. + * @param id Specifies the function identifier under which the callback gets + * registered, e.g. {intermediates: '%' final: 'G'} for default charset + * selection. * @param callback The function to handle the sequence. - * Return `true` if the sequence was handled, `false` if the parser should try - * a previous handler. The most recently added handler is tried first. + * Return `true` if the sequence was handled, `false` if the parser should + * try a previous handler. The most recently added handler is tried first. * @returns An IDisposable you can call to remove this handler. */ registerEscHandler(id: IFunctionIdentifier, handler: () => boolean | Promise): IDisposable; @@ -1723,13 +1741,13 @@ declare module 'xterm' { * @param callback The function to handle the sequence. Note that the * function will only be called once if the sequence finished sucessfully. * There is currently no way to intercept smaller data chunks, data chunks - * will be stored up until the sequence is finished. Since OSC sequences - * are not limited by the amount of data this might impose a problem for - * big payloads. Currently xterm.js limits OSC payload to 10 MB - * which should give enough room for most use cases. - * The callback is called with OSC data string. - * Return `true` if the sequence was handled, `false` if the parser should try - * a previous handler. The most recently added handler is tried first. + * will be stored up until the sequence is finished. Since OSC sequences are + * not limited by the amount of data this might impose a problem for big + * payloads. Currently xterm.js limits OSC payload to 10 MB which should + * give enough room for most use cases. The callback is called with OSC data + * string. Return `true` if the sequence was handled, `false` if the parser + * should try a previous handler. The most recently added handler is tried + * first. * @returns An IDisposable you can call to remove this handler. */ registerOscHandler(ident: number, callback: (data: string) => boolean | Promise): IDisposable; @@ -1816,6 +1834,6 @@ declare module 'xterm' { /** * Auto-Wrap Mode (DECAWM): `CSI ? 7 h` */ - readonly wraparoundMode: boolean + readonly wraparoundMode: boolean; } }