From a32e3958d8a560a9db714dac9f3c6e87d6a0cbf2 Mon Sep 17 00:00:00 2001 From: Eugene Pankov Date: Wed, 22 Dec 2021 20:53:38 +0100 Subject: [PATCH 01/26] fixed macOptionIsMeta on macOS and added Alt-Shift-Letter key handling --- src/browser/Terminal.ts | 7 +++++-- src/common/Types.d.ts | 1 + src/common/input/Keyboard.test.ts | 2 ++ src/common/input/Keyboard.ts | 18 +++++++++++++++++- 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/browser/Terminal.ts b/src/browser/Terminal.ts index f08d8581..206b8bcf 100644 --- a/src/browser/Terminal.ts +++ b/src/browser/Terminal.ts @@ -1096,14 +1096,17 @@ export class Terminal extends CoreTerminal implements ITerminal { return false; } - if (!this._compositionHelper!.keydown(event)) { + // Ignore composing with Alt key on Mac when macOptionIsMeta is enabled + const shouldIgnoreComposition = this.browser.isMac && this.options.macOptionIsMeta && event.altKey + + if (!shouldIgnoreComposition && !this._compositionHelper!.keydown(event)) { if (this.buffer.ybase !== this.buffer.ydisp) { this._bufferService.scrollToBottom(); } return false; } - if (event.key === 'Dead' || event.key === 'AltGraph') { + if (!shouldIgnoreComposition && (event.key === 'Dead' || event.key === 'AltGraph')) { this._unprocessedDeadKey = true; } diff --git a/src/common/Types.d.ts b/src/common/Types.d.ts index fee426e1..bd6132c2 100644 --- a/src/common/Types.d.ts +++ b/src/common/Types.d.ts @@ -50,6 +50,7 @@ export interface IKeyboardEvent { keyCode: number; key: string; type: string; + code: string; } export interface IScrollEvent { diff --git a/src/common/input/Keyboard.test.ts b/src/common/input/Keyboard.test.ts index ac78ce3c..fffe99b9 100644 --- a/src/common/input/Keyboard.test.ts +++ b/src/common/input/Keyboard.test.ts @@ -13,6 +13,7 @@ function testEvaluateKeyboardEvent(partialEvent: { shiftKey?: boolean; metaKey?: boolean; keyCode?: number; + code?: string; key?: string; type?: string; }, partialOptions: { @@ -26,6 +27,7 @@ function testEvaluateKeyboardEvent(partialEvent: { shiftKey: partialEvent.shiftKey || false, metaKey: partialEvent.metaKey || false, keyCode: partialEvent.keyCode !== undefined ? partialEvent.keyCode : 0, + code: partialEvent.code || '', key: partialEvent.key || '', type: partialEvent.type || '' }; diff --git a/src/common/input/Keyboard.ts b/src/common/input/Keyboard.ts index b4b3dce4..c8408337 100644 --- a/src/common/input/Keyboard.ts +++ b/src/common/input/Keyboard.ts @@ -354,7 +354,23 @@ export function evaluateKeyboardEvent( result.key = C0.ESC + key; } else if (ev.keyCode >= 65 && ev.keyCode <= 90) { const keyCode = ev.ctrlKey ? ev.keyCode - 64 : ev.keyCode + 32; - result.key = C0.ESC + String.fromCharCode(keyCode); + let keyString = String.fromCharCode(keyCode); + if (ev.shiftKey) { + keyString = keyString.toUpperCase(); + } + result.key = C0.ESC + keyString; + } else if (ev.key === 'Dead' && ev.code.startsWith('Key')) { + // Reference: https://github.com/xtermjs/xterm.js/issues/3725 + // Alt will produce a "dead key" (initate composition) with some + // of the letters in US layout (e.g. N/E/U). + // It's safe to match against Key* since no other `code` values begin with "Key". + // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code/code_values#code_values_on_mac + let keyString = ev.code.slice(3, 4); + if (!ev.shiftKey) { + keyString = keyString.toLowerCase(); + } + result.key = C0.ESC + keyString; + result.cancel = true; } } else if (isMac && !ev.altKey && !ev.ctrlKey && !ev.shiftKey && ev.metaKey) { if (ev.keyCode === 65) { // cmd + a From b3fd9016996f6fccc77d73422e06aa38138f3f0d Mon Sep 17 00:00:00 2001 From: Eugene Pankov Date: Sun, 3 Apr 2022 17:44:15 +0200 Subject: [PATCH 02/26] lint --- src/browser/Terminal.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browser/Terminal.ts b/src/browser/Terminal.ts index 206b8bcf..e9aa6873 100644 --- a/src/browser/Terminal.ts +++ b/src/browser/Terminal.ts @@ -1097,7 +1097,7 @@ export class Terminal extends CoreTerminal implements ITerminal { } // Ignore composing with Alt key on Mac when macOptionIsMeta is enabled - const shouldIgnoreComposition = this.browser.isMac && this.options.macOptionIsMeta && event.altKey + const shouldIgnoreComposition = this.browser.isMac && this.options.macOptionIsMeta && event.altKey; if (!shouldIgnoreComposition && !this._compositionHelper!.keydown(event)) { if (this.buffer.ybase !== this.buffer.ydisp) { From 5019e14753f17c99bfb248f251672de889cb6f7c Mon Sep 17 00:00:00 2001 From: binghuiluo Date: Sat, 7 May 2022 17:19:04 +0800 Subject: [PATCH 03/26] Send the record separator for C-@, like some other keyboards --- src/common/input/Keyboard.test.ts | 4 ++++ src/common/input/Keyboard.ts | 3 +++ 2 files changed, 7 insertions(+) diff --git a/src/common/input/Keyboard.test.ts b/src/common/input/Keyboard.test.ts index ac78ce3c..64886e38 100644 --- a/src/common/input/Keyboard.test.ts +++ b/src/common/input/Keyboard.test.ts @@ -313,5 +313,9 @@ describe('Keyboard', () => { assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 49, key: '!' }).key, '!'); }); + it('should return proper sequence for ctrl+@', () => { + assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, shiftKey: true, keyCode: 50, key: '@' }).key, '\x00'); + }); + }); }); diff --git a/src/common/input/Keyboard.ts b/src/common/input/Keyboard.ts index b4b3dce4..59d4fe69 100644 --- a/src/common/input/Keyboard.ts +++ b/src/common/input/Keyboard.ts @@ -367,6 +367,9 @@ export function evaluateKeyboardEvent( if (ev.key === '_') { // ^_ result.key = C0.US; } + if (ev.key === '@') { // ^ + shift + 2 = ^ + @ + result.key = C0.NUL; + } } break; } From 1c8b08d55c68296e81478b0da0615635ca9c8c71 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 23 May 2022 10:42:31 -0700 Subject: [PATCH 04/26] Add test button to print all powerline symbols Part of #3807 --- demo/client.ts | 19 +++++++++++++++++++ demo/index.html | 1 + 2 files changed, 20 insertions(+) diff --git a/demo/client.ts b/demo/client.ts index b9e52d7b..8e477d77 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -159,6 +159,7 @@ if (document.location.pathname === '/test') { document.getElementById('htmlserialize').addEventListener('click', htmlSerializeButtonHandler); document.getElementById('custom-glyph').addEventListener('click', writeCustomGlyphHandler); document.getElementById('load-test').addEventListener('click', loadTest); + document.getElementById('powerline-symbol-test').addEventListener('click', powerlineSymbolTest); document.getElementById('add-decoration').addEventListener('click', addDecoration); document.getElementById('add-overview-ruler').addEventListener('click', addOverviewRuler); } @@ -558,6 +559,24 @@ function loadTest() { }); } +function powerlineSymbolTest() { + function s(char: string): string { + return `${char} \x1b[7m${char}\x1b[0m `; + } + term.write('\n\n\r'); + term.writeln('Standard powerline symbols:'); + term.writeln(' 0 1 2 3 4 5 6 7 8 9 A B C D E F'); + term.writeln(`0xA_ ${s('\ue0a0')}${s('\ue0a1')}${s('\ue0a2')}`); + term.writeln(`0xB_ ${s('\ue0b0')}${s('\ue0b1')}${s('\ue0b2')}${s('\ue0b3')}`); + term.writeln(''); + term.writeln('Powerline extra symbols:'); + term.writeln(' 0 1 2 3 4 5 6 7 8 9 A B C D E F'); + term.writeln(`0xA_ ${s('\ue0a3')}`); + term.writeln(`0xB_ ${s('\ue0b4')}${s('\ue0b5')}${s('\ue0b6')}${s('\ue0b7')}${s('\ue0b8')}${s('\ue0b9')}${s('\ue0ba')}${s('\ue0bb')}${s('\ue0bc')}${s('\ue0bd')}${s('\ue0be')}${s('\ue0bf')}`); + term.writeln(`0xC_ ${s('\ue0c0')}${s('\ue0c1')}${s('\ue0c2')}${s('\ue0c3')}${s('\ue0c4')}${s('\ue0c5')}${s('\ue0c6')}${s('\ue0c7')}${s('\ue0c8')}${s('\ue0c9')}${s('\ue0ca')}${s('\ue0cb')}${s('\ue0cc')}${s('\ue0cd')}${s('\ue0be')}${s('\ue0bf')}`); + term.writeln(`0xD_ ${s('\ue0d0')}${s('\ue0d1')}${s('\ue0d2')} ${s('\ue0d4')}`); +} + function addDecoration() { term.options['overviewRulerWidth'] = 15; const marker = term.addMarker(1); diff --git a/demo/index.html b/demo/index.html index f36fc629..f05b4e64 100644 --- a/demo/index.html +++ b/demo/index.html @@ -69,6 +69,7 @@ + From 1e17ba7a806cacbeb66faafdd002c6231a95cf26 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 23 May 2022 12:08:28 -0700 Subject: [PATCH 05/26] Add some NF icons --- demo/client.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/demo/client.ts b/demo/client.ts index 8e477d77..d812ca67 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -575,6 +575,10 @@ function powerlineSymbolTest() { term.writeln(`0xB_ ${s('\ue0b4')}${s('\ue0b5')}${s('\ue0b6')}${s('\ue0b7')}${s('\ue0b8')}${s('\ue0b9')}${s('\ue0ba')}${s('\ue0bb')}${s('\ue0bc')}${s('\ue0bd')}${s('\ue0be')}${s('\ue0bf')}`); term.writeln(`0xC_ ${s('\ue0c0')}${s('\ue0c1')}${s('\ue0c2')}${s('\ue0c3')}${s('\ue0c4')}${s('\ue0c5')}${s('\ue0c6')}${s('\ue0c7')}${s('\ue0c8')}${s('\ue0c9')}${s('\ue0ca')}${s('\ue0cb')}${s('\ue0cc')}${s('\ue0cd')}${s('\ue0be')}${s('\ue0bf')}`); term.writeln(`0xD_ ${s('\ue0d0')}${s('\ue0d1')}${s('\ue0d2')} ${s('\ue0d4')}`); + term.writeln(''); + term.writeln('Sample of nerd fonts icons:'); + term.writeln(' nf-linux-apple (\\uF302) \uf302'); + term.writeln('nf-mdi-github_face (\\uFbd9) \ufbd9'); } function addDecoration() { From 221db55680ce07a6357d5b503075036b09efe42a Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 26 May 2022 10:32:53 -0700 Subject: [PATCH 06/26] Add find result count/index to demo Part of #148109 --- demo/client.ts | 20 ++++++++++++++++++-- demo/index.html | 1 + 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/demo/client.ts b/demo/client.ts index d812ca67..b67a84a5 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -94,7 +94,8 @@ const terminalContainer = document.getElementById('terminal-container'); const actionElements = { find: document.querySelector('#find'), findNext: document.querySelector('#find-next'), - findPrevious: document.querySelector('#find-previous') + findPrevious: document.querySelector('#find-previous'), + findResults: document.querySelector('#find-results') }; const paddingElement = document.getElementById('padding'); @@ -398,9 +399,12 @@ function initAddons(term: TerminalType): void { if (!addon.canChange) { checkbox.disabled = true; } - if(name === 'unicode11' && checkbox.checked) { + if (name === 'unicode11' && checkbox.checked) { term.unicode.activeVersion = '11'; } + if (name === 'search' && checkbox.checked) { + addon.instance.onDidChangeResults(e => updateFindResults(e)); + } addDomListener(checkbox, 'change', () => { if (checkbox.checked) { addon.instance = new addon.ctor(); @@ -411,6 +415,8 @@ function initAddons(term: TerminalType): void { }, 0); } else if (name === 'unicode11') { term.unicode.activeVersion = '11'; + } else if (name === 'search') { + addon.instance.onDidChangeResults(e => updateFindResults(e)); } } else { if (name === 'webgl') { @@ -439,6 +445,16 @@ function initAddons(term: TerminalType): void { container.appendChild(fragment); } +function updateFindResults(e: { resultIndex: number, resultCount: number } | undefined) { + let content: string; + if (e === undefined) { + content = 'undefined'; + } else { + content = `index: ${e.resultIndex}, count: ${e.resultCount}`; + } + actionElements.findResults.textContent = content; +} + function addDomListener(element: HTMLElement, type: string, handler: (...args: any[]) => any): void { element.addEventListener(type, handler); term._core.register({ dispose: () => element.removeEventListener(type, handler) }); diff --git a/demo/index.html b/demo/index.html index f05b4e64..a460fea1 100644 --- a/demo/index.html +++ b/demo/index.html @@ -40,6 +40,7 @@
+
Results:
From b5f53b341dbbf2032ca4e903768008b83a1a07e5 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 26 May 2022 11:37:54 -0700 Subject: [PATCH 07/26] Test search event, fix returning -1 for no results Fixes #3830 --- addons/xterm-addon-search/src/SearchAddon.ts | 20 +- .../test/SearchAddon.api.ts | 181 +++++++++++++++++- 2 files changed, 190 insertions(+), 11 deletions(-) diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index 0f356e14..1c816939 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -134,7 +134,7 @@ export class SearchAddon implements ITerminalAddon { } this._lastSearchOptions = searchOptions; if (searchOptions?.decorations) { - if (this._resultIndex !== undefined || this._cachedSearchTerm && term !== this._cachedSearchTerm) { + if (this._resultIndex !== undefined || this._cachedSearchTerm === undefined || term !== this._cachedSearchTerm) { this._highlightAllMatches(term, searchOptions); } } @@ -288,7 +288,9 @@ export class SearchAddon implements ITerminalAddon { } if (this._searchResults) { - if (this._resultIndex === undefined) { + if (this._searchResults.size === 0) { + this._resultIndex = -1; + } else if (this._resultIndex === undefined) { this._resultIndex = 0; } else { this._resultIndex++; @@ -312,8 +314,10 @@ export class SearchAddon implements ITerminalAddon { throw new Error('Cannot use addon until it has been loaded'); } this._lastSearchOptions = searchOptions; - if (searchOptions?.decorations && (this._resultIndex !== undefined || term !== this._cachedSearchTerm)) { - this._highlightAllMatches(term, searchOptions); + if (searchOptions?.decorations) { + if (this._resultIndex !== undefined || this._cachedSearchTerm === undefined || term !== this._cachedSearchTerm) { + this._highlightAllMatches(term, searchOptions); + } } return this._fireResults(term, this._findPreviousAndSelect(term, searchOptions), searchOptions); } @@ -408,12 +412,14 @@ export class SearchAddon implements ITerminalAddon { } if (this._searchResults) { - if (this._resultIndex === undefined || this._resultIndex < 0) { - this._resultIndex = this._searchResults?.size - 1; + if (this._searchResults.size === 0) { + this._resultIndex = -1; + } else if (this._resultIndex === undefined || this._resultIndex < 0) { + this._resultIndex = this._searchResults.size - 1; } else { this._resultIndex--; if (this._resultIndex === -1) { - this._resultIndex = this._searchResults?.size - 1; + this._resultIndex = this._searchResults.size - 1; } } } diff --git a/addons/xterm-addon-search/test/SearchAddon.api.ts b/addons/xterm-addon-search/test/SearchAddon.api.ts index 7cb15c5d..e665f54e 100644 --- a/addons/xterm-addon-search/test/SearchAddon.api.ts +++ b/addons/xterm-addon-search/test/SearchAddon.api.ts @@ -6,7 +6,7 @@ import { assert } from 'chai'; import { readFile } from 'fs'; import { resolve } from 'path'; -import { openTerminal, writeSync, launchBrowser } from '../../../out-test/api/TestUtils'; +import { openTerminal, writeSync, launchBrowser, timeout } from '../../../out-test/api/TestUtils'; import { Browser, Page } from 'playwright'; const APP = 'http://127.0.0.1:3001/test'; @@ -23,8 +23,6 @@ describe('Search Tests', function(): void { await page.setViewportSize({ width, height }); await page.goto(APP); await openTerminal(page); - await page.evaluate(`window.search = new SearchAddon();`); - await page.evaluate(`window.term.loadAddon(window.search);`); }); after(() => { @@ -32,7 +30,12 @@ describe('Search Tests', function(): void { }); beforeEach(async () => { - await page.evaluate(`window.term.reset()`); + await page.evaluate(` + window.term.reset() + window.search?.dispose(); + window.search = new SearchAddon(); + window.term.loadAddon(window.search); + `); }); it('Simple Search', async () => { @@ -120,6 +123,176 @@ describe('Search Tests', function(): void { }); }); + describe('onDidChangeResults', async () => { + describe('findNext', () => { + it('should not fire unless the decorations option is set', async () => { + await page.evaluate(` + window.calls = []; + window.search.onDidChangeResults(e => window.calls.push(e)); + `); + await writeSync(page, 'abc'); + assert.strictEqual(await page.evaluate(`window.search.findNext('a')`), true); + assert.strictEqual(await page.evaluate('window.calls.length'), 0); + assert.strictEqual(await page.evaluate(`window.search.findNext('b', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true); + assert.strictEqual(await page.evaluate('window.calls.length'), 1); + }); + it('should fire with correct event values', async () => { + await page.evaluate(` + window.calls = []; + window.search.onDidChangeResults(e => window.calls.push(e)); + `); + await writeSync(page, 'abc bc c'); + assert.strictEqual(await page.evaluate(`window.search.findNext('a', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true); + assert.deepStrictEqual(await page.evaluate('window.calls'), [ + { resultCount: 1, resultIndex: 0 } + ]); + assert.strictEqual(await page.evaluate(`window.search.findNext('b', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true); + assert.deepStrictEqual(await page.evaluate('window.calls'), [ + { resultCount: 1, resultIndex: 0 }, + { resultCount: 2, resultIndex: 0 } + ]); + assert.strictEqual(await page.evaluate(`window.search.findNext('d', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), false); + assert.deepStrictEqual(await page.evaluate('window.calls'), [ + { resultCount: 1, resultIndex: 0 }, + { resultCount: 2, resultIndex: 0 }, + { resultCount: -1, resultIndex: -1 } + ]); + assert.strictEqual(await page.evaluate(`window.search.findNext('c', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true); + assert.strictEqual(await page.evaluate(`window.search.findNext('c', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true); + assert.strictEqual(await page.evaluate(`window.search.findNext('c', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true); + assert.deepStrictEqual(await page.evaluate('window.calls'), [ + { resultCount: 1, resultIndex: 0 }, + { resultCount: 2, resultIndex: 0 }, + { resultCount: -1, resultIndex: -1 }, + { resultCount: 3, resultIndex: 0 }, + { resultCount: 3, resultIndex: 1 }, + { resultCount: 3, resultIndex: 2 } + ]); + }); + it('should fire with correct event values (incremental)', async () => { + await page.evaluate(` + window.calls = []; + window.search.onDidChangeResults(e => window.calls.push(e)); + `); + await writeSync(page, 'abc aabc'); + assert.deepStrictEqual(await page.evaluate(`window.search.findNext('a', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true); + assert.deepStrictEqual(await page.evaluate('window.calls'), [ + { resultCount: 3, resultIndex: 0 } + ]); + assert.deepStrictEqual(await page.evaluate(`window.search.findNext('ab', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true); + assert.deepStrictEqual(await page.evaluate('window.calls'), [ + { resultCount: 3, resultIndex: 0 }, + { resultCount: 2, resultIndex: 0 } + ]); + assert.deepStrictEqual(await page.evaluate(`window.search.findNext('abc', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true); + assert.deepStrictEqual(await page.evaluate('window.calls'), [ + { resultCount: 3, resultIndex: 0 }, + { resultCount: 2, resultIndex: 0 }, + { resultCount: 2, resultIndex: 0 } + ]); + assert.deepStrictEqual(await page.evaluate(`window.search.findNext('abc', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true); + assert.deepStrictEqual(await page.evaluate('window.calls'), [ + { resultCount: 3, resultIndex: 0 }, + { resultCount: 2, resultIndex: 0 }, + { resultCount: 2, resultIndex: 0 }, + { resultCount: 2, resultIndex: 1 } + ]); + assert.deepStrictEqual(await page.evaluate(`window.search.findNext('abcd', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), false); + assert.deepStrictEqual(await page.evaluate('window.calls'), [ + { resultCount: 3, resultIndex: 0 }, + { resultCount: 2, resultIndex: 0 }, + { resultCount: 2, resultIndex: 0 }, + { resultCount: 2, resultIndex: 1 }, + { resultCount: -1, resultIndex: -1 } + ]); + }); + }); + describe('findPrevious', () => { + it('should not fire unless the decorations option is set', async () => { + await page.evaluate(` + window.calls = []; + window.search.onDidChangeResults(e => window.calls.push(e)); + `); + await writeSync(page, 'abc'); + assert.strictEqual(await page.evaluate(`window.search.findPrevious('a')`), true); + assert.strictEqual(await page.evaluate('window.calls.length'), 0); + assert.strictEqual(await page.evaluate(`window.search.findPrevious('b', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true); + assert.strictEqual(await page.evaluate('window.calls.length'), 1); + }); + it('should fire with correct event values', async () => { + await page.evaluate(` + window.calls = []; + window.search.onDidChangeResults(e => window.calls.push(e)); + `); + await writeSync(page, 'abc bc c'); + assert.strictEqual(await page.evaluate(`window.search.findPrevious('a', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true); + assert.deepStrictEqual(await page.evaluate('window.calls'), [ + { resultCount: 1, resultIndex: 0 } + ]); + assert.strictEqual(await page.evaluate(`window.search.findPrevious('b', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true); + assert.deepStrictEqual(await page.evaluate('window.calls'), [ + { resultCount: 1, resultIndex: 0 }, + { resultCount: 2, resultIndex: 1 } + ]); + await timeout(2000); + assert.strictEqual(await page.evaluate(`debugger; window.search.findPrevious('d', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), false); + assert.deepStrictEqual(await page.evaluate('window.calls'), [ + { resultCount: 1, resultIndex: 0 }, + { resultCount: 2, resultIndex: 1 }, + { resultCount: -1, resultIndex: -1 } + ]); + assert.strictEqual(await page.evaluate(`window.search.findPrevious('c', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true); + assert.strictEqual(await page.evaluate(`window.search.findPrevious('c', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true); + assert.strictEqual(await page.evaluate(`window.search.findPrevious('c', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true); + assert.deepStrictEqual(await page.evaluate('window.calls'), [ + { resultCount: 1, resultIndex: 0 }, + { resultCount: 2, resultIndex: 1 }, + { resultCount: -1, resultIndex: -1 }, + { resultCount: 3, resultIndex: 2 }, + { resultCount: 3, resultIndex: 1 }, + { resultCount: 3, resultIndex: 0 } + ]); + }); + it('should fire with correct event values (incremental)', async () => { + await page.evaluate(` + window.calls = []; + window.search.onDidChangeResults(e => window.calls.push(e)); + `); + await writeSync(page, 'abc aabc'); + assert.deepStrictEqual(await page.evaluate(`window.search.findPrevious('a', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true); + assert.deepStrictEqual(await page.evaluate('window.calls'), [ + { resultCount: 3, resultIndex: 2 } + ]); + assert.deepStrictEqual(await page.evaluate(`window.search.findPrevious('ab', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true); + assert.deepStrictEqual(await page.evaluate('window.calls'), [ + { resultCount: 3, resultIndex: 2 }, + { resultCount: 2, resultIndex: 1 } + ]); + assert.deepStrictEqual(await page.evaluate(`window.search.findPrevious('abc', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true); + assert.deepStrictEqual(await page.evaluate('window.calls'), [ + { resultCount: 3, resultIndex: 2 }, + { resultCount: 2, resultIndex: 1 }, + { resultCount: 2, resultIndex: 1 } + ]); + assert.deepStrictEqual(await page.evaluate(`window.search.findPrevious('abc', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true); + assert.deepStrictEqual(await page.evaluate('window.calls'), [ + { resultCount: 3, resultIndex: 2 }, + { resultCount: 2, resultIndex: 1 }, + { resultCount: 2, resultIndex: 1 }, + { resultCount: 2, resultIndex: 0 } + ]); + assert.deepStrictEqual(await page.evaluate(`window.search.findPrevious('abcd', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), false); + assert.deepStrictEqual(await page.evaluate('window.calls'), [ + { resultCount: 3, resultIndex: 2 }, + { resultCount: 2, resultIndex: 1 }, + { resultCount: 2, resultIndex: 1 }, + { resultCount: 2, resultIndex: 0 }, + { resultCount: -1, resultIndex: -1 } + ]); + }); + }); + }); + describe('Regression tests', () => { describe('#2444 wrapped line content not being found', () => { let fixture: string; From 0c0a34d88882d56dd0e419ceb13dbdcdfacbd8d0 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 26 May 2022 15:09:01 -0700 Subject: [PATCH 08/26] Fix search when the search line contains a null character Fixes #3834 --- addons/xterm-addon-search/src/SearchAddon.ts | 3 ++- addons/xterm-addon-search/test/SearchAddon.api.ts | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index 1c816939..170a7a2a 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -610,7 +610,8 @@ export class SearchAddon implements ITerminalAddon { break; } if (cell.getWidth()) { - offset += cell.getChars().length; + // Treat null characters as whitespace to align with the translateToString API + offset += cell.getCode() === 0 ? 1 : cell.getChars().length; } } lineIndex++; diff --git a/addons/xterm-addon-search/test/SearchAddon.api.ts b/addons/xterm-addon-search/test/SearchAddon.api.ts index e665f54e..c1305345 100644 --- a/addons/xterm-addon-search/test/SearchAddon.api.ts +++ b/addons/xterm-addon-search/test/SearchAddon.api.ts @@ -379,6 +379,21 @@ describe('Search Tests', function(): void { }); }); }); + describe('#3834 lines with null characters before search terms', () => { + // This case can be triggered by the prompt when using starship under conpty + it('should find all matches on a line containing null characters', async () => { + await page.evaluate(` + window.calls = []; + window.search.onDidChangeResults(e => window.calls.push(e)); + `); + // Move cursor forward 1 time to create a null character, as opposed to regular whitespace + await writeSync(page, '\\x1b[CHi Hi'); + assert.strictEqual(await page.evaluate(`window.search.findPrevious('h', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true); + assert.deepStrictEqual(await page.evaluate('window.calls'), [ + { resultCount: 2, resultIndex: 1 } + ]); + }); + }); }); function makeData(length: number): string { From e3c008ba38d67bbc59ef9c7ecaeab0187b454a90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie?= Date: Fri, 27 May 2022 15:52:40 +0200 Subject: [PATCH 09/26] Update of README to add hack.courses Update of README to add hack.courses as a project using xterm.js (https://hack.courses) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 94494c71..7d98de01 100644 --- a/README.md +++ b/README.md @@ -189,6 +189,7 @@ Xterm.js is used in several world-class applications to provide great terminal e - [**KubeSail**](https://kubesail.com): The Self-Hosting Company - uses xterm to allow users to exec into kubernetes pods and build github apps - [**WiTTY**](https://github.com/syssecfsu/witty): Web-based interactive terminal emulator that allows users to easily record, share, and replay console sessions. - [**libv86 Terminal Forwarding**](https://github.com/hello-smile6/libv86-terminal-forwarding): Peer-to-peer SSH for the web, using WebRTC via [Bugout](https://github.com/chr15m/bugout) for data transfer and [v86](https://github.com/copy/v86) for web-based virtualization. +- [**hack.courses**](https://hack.courses): Interactive Linux and command-line classes using xterm.js to expose a real terminal available for everyone. - [And much more...](https://github.com/xtermjs/xterm.js/network/dependents?package_id=UGFja2FnZS0xNjYzMjc4OQ%3D%3D) Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it on our list. Note: Please add any new contributions to the end of the list only. From 6ced3014cf2989acb22877aa755c68c3c479bb04 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 27 May 2022 10:37:04 -0700 Subject: [PATCH 10/26] Support opaque selections in DOM renderer Brings the DOM renderer closer to the webgl renderer which draws the text on top of the selection. This is done by leveraging the override system similar to in webgl and as well as forcing the element above the selection. This also improves contrast color caching for the DOM renderer. This should result in a slight performance hit but only when a selection is being made. The improved contrast seems worth it. Fixes #3838 --- src/browser/renderer/dom/DomRenderer.ts | 2 +- .../renderer/dom/DomRendererRowFactory.ts | 22 ++++++++++++------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/browser/renderer/dom/DomRenderer.ts b/src/browser/renderer/dom/DomRenderer.ts index fdeef91c..a966e6f9 100644 --- a/src/browser/renderer/dom/DomRenderer.ts +++ b/src/browser/renderer/dom/DomRenderer.ts @@ -226,7 +226,7 @@ export class DomRenderer extends Disposable implements IRenderer { `}` + `${this._terminalSelector} .${SELECTION_CLASS} div {` + ` position: absolute;` + - ` background-color: ${this._colors.selectionTransparent.css};` + + ` background-color: ${this._colors.selectionOpaque.css};` + `}`; // Colors this._colors.ansi.forEach((c, i) => { diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts index 266d7587..39fd4fdd 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.ts @@ -206,14 +206,22 @@ export class DomRendererRowFactory { } // Apply selection foreground if applicable + const isInSelection = this._isCellInSelection(x, row); if (!isTop) { - if (this._colors.selectionForeground && this._isCellInSelection(x, row)) { + if (this._colors.selectionForeground && isInSelection) { fgColorMode = Attributes.CM_RGB; fg = this._colors.selectionForeground.rgba >> 8 & 0xFFFFFF; fgOverride = this._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._colors.selectionOpaque; + isTop = true; + } + // If it's a top decoration, render above the selection if (isTop) { charElement.classList.add(`xterm-decoration-top`); @@ -226,7 +234,7 @@ export class DomRendererRowFactory { if (cell.isBold() && fg < 8 && this._optionsService.rawOptions.drawBoldTextInBrightColors) { fg += 8; } - if (!this._applyMinimumContrast(charElement, this._colors.background, this._colors.ansi[fg], cell, undefined, undefined)) { + if (!this._applyMinimumContrast(charElement, this._colors.background, this._colors.ansi[fg], cell, bgOverride, undefined)) { charElement.classList.add(`xterm-fg-${fg}`); } break; @@ -242,7 +250,7 @@ export class DomRendererRowFactory { break; case Attributes.CM_DEFAULT: default: - if (!this._applyMinimumContrast(charElement, this._colors.background, this._colors.foreground, cell, undefined, undefined)) { + if (!this._applyMinimumContrast(charElement, this._colors.background, this._colors.foreground, cell, bgOverride, undefined)) { if (isInverse) { charElement.classList.add(`xterm-fg-${INVERTED_DEFAULT_COLOR}`); } @@ -279,16 +287,14 @@ export class DomRendererRowFactory { // Try get from cache first, only use the cache when there are no decoration overrides let adjustedColor: IColor | undefined | null = undefined; - if (!bgOverride || !fgOverride) { - adjustedColor = this._colors.contrastCache.getColor(this._workCell.bg, this._workCell.fg); + if (!bgOverride && !fgOverride) { + adjustedColor = this._colors.contrastCache.getColor(bg.rgba, fg.rgba); } // Calculate and store in cache if (adjustedColor === undefined) { adjustedColor = color.ensureContrastRatio(bgOverride || bg, fgOverride || fg, this._optionsService.rawOptions.minimumContrastRatio); - if (!bgOverride || !fgOverride) { - this._colors.contrastCache.setColor(this._workCell.bg, this._workCell.fg, adjustedColor ?? null); - } + this._colors.contrastCache.setColor((bgOverride || bg).rgba, (fgOverride || fg).rgba, adjustedColor ?? null); } if (adjustedColor) { From 98c40e3f8e85437d7a9987b0a286a4b76d3746da Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 27 May 2022 10:46:23 -0700 Subject: [PATCH 11/26] Add opaque selection dom renderer tests --- .../dom/DomRendererRowFactory.test.ts | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/browser/renderer/dom/DomRendererRowFactory.test.ts b/src/browser/renderer/dom/DomRendererRowFactory.test.ts index 5a374b14..7c48cce8 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.test.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.test.ts @@ -12,7 +12,7 @@ import { IBufferLine } from 'common/Types'; import { CellData } from 'common/buffer/CellData'; import { MockCoreService, MockDecorationService, MockOptionsService } from 'common/TestUtils.test'; import { css } from 'common/Color'; -import { MockCharacterJoinerService, MockSelectionService } from 'browser/TestUtils.test'; +import { MockCharacterJoinerService } from 'browser/TestUtils.test'; describe('DomRendererRowFactory', () => { let dom: jsdom.JSDOM; @@ -245,6 +245,26 @@ describe('DomRendererRowFactory', () => { ); }); }); + + describe('selectionForeground', () => { + it('should force selected cells with content to be rendered above the background', () => { + 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.onSelectionChanged([1, 0], [2, 0], false); + const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20); + assert.equal(getFragmentHtml(fragment), + '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.onSelectionChanged([0, 0], [2, 0], false); + const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20); + assert.equal(getFragmentHtml(fragment), + ' a' + ); + }); + }); }); function getFragmentHtml(fragment: DocumentFragment): string { From 7a652c4169b1865962c988deaee20111f5cf55bf Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 27 May 2022 11:56:54 -0700 Subject: [PATCH 12/26] Allow up to double the size for overview ruler decorations Fixes #3840 --- .../decorations/OverviewRulerRenderer.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/browser/decorations/OverviewRulerRenderer.ts b/src/browser/decorations/OverviewRulerRenderer.ts index f31409ca..22f3ddf5 100644 --- a/src/browser/decorations/OverviewRulerRenderer.ts +++ b/src/browser/decorations/OverviewRulerRenderer.ts @@ -88,6 +88,7 @@ export class OverviewRulerRenderer extends Disposable { })); this.register(this._bufferService.onScroll(() => { if (this._lastKnownBufferLength !== this._bufferService.buffers.normal.lines.length) { + this._refreshDrawHeightConstants(); this._refreshColorZonePadding(); } })); @@ -132,10 +133,7 @@ export class OverviewRulerRenderer extends Disposable { drawWidth.center = innerWidth; drawWidth.right = outerWidth; // height - drawHeight.full = Math.round(2 * window.devicePixelRatio); - drawHeight.left = Math.round(6 * window.devicePixelRatio); - drawHeight.center = Math.round(6 * window.devicePixelRatio); - drawHeight.right = Math.round(6 * window.devicePixelRatio); + this._refreshDrawHeightConstants(); // x drawX.full = 0; drawX.left = 0; @@ -143,6 +141,17 @@ export class OverviewRulerRenderer extends Disposable { drawX.right = drawWidth.left + drawWidth.center; } + private _refreshDrawHeightConstants(): void { + drawHeight.full = Math.round(2 * window.devicePixelRatio); + // Calculate actual pixels per line + const pixelsPerLine = this._canvas.height / this._bufferService.buffer.lines.length; + // Clamp actual pixels within a range + const nonFullHeight = Math.round(Math.max(Math.min(pixelsPerLine, 12), 6) * window.devicePixelRatio); + drawHeight.left = nonFullHeight; + drawHeight.center = nonFullHeight; + drawHeight.right = nonFullHeight; + } + private _refreshColorZonePadding(): void { this._colorZoneStore.setPadding({ full: Math.floor(this._bufferService.buffers.active.lines.length / (this._canvas.height - 1) * drawHeight.full), From 6b167321924eccf37899ee19feb7696987b0faa3 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Wed, 1 Jun 2022 17:05:27 -0700 Subject: [PATCH 13/26] exclude box and block glyphs from contrast ratio demands (#3844) --- addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts | 12 ++++++------ addons/xterm-addon-webgl/test/WebglRenderer.api.ts | 2 +- src/browser/renderer/BaseRenderLayer.ts | 4 ++-- src/browser/renderer/RendererUtils.ts | 10 +++++++++- src/browser/renderer/dom/DomRendererRowFactory.ts | 4 ++-- 5 files changed, 20 insertions(+), 12 deletions(-) diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index 34107fc5..f0099d02 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -13,7 +13,7 @@ import { IDisposable } from 'xterm'; import { AttributeData } from 'common/buffer/AttributeData'; import { channels, rgba } from 'common/Color'; import { tryDrawCustomChar } from 'browser/renderer/CustomGlyphs'; -import { isPowerlineGlyph } from 'browser/renderer/RendererUtils'; +import { excludeFromContrastRatioDemands, isPowerlineGlyph } from 'browser/renderer/RendererUtils'; // For debugging purposes, it can be useful to set this to a really tiny value, // to verify that LRU eviction works. @@ -217,8 +217,8 @@ export class WebglCharAtlas implements IDisposable { } } - private _getForegroundCss(bg: number, bgColorMode: number, bgColor: number, fg: number, fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean, isPowerLineGlyph: boolean): string { - const minimumContrastCss = this._getMinimumContrastCss(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, bold, isPowerLineGlyph); + private _getForegroundCss(bg: number, bgColorMode: number, bgColor: number, fg: number, fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean, excludeFromContrastRatioDemands: boolean): string { + const minimumContrastCss = this._getMinimumContrastCss(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, bold, excludeFromContrastRatioDemands); if (minimumContrastCss) { return minimumContrastCss; } @@ -282,8 +282,8 @@ export class WebglCharAtlas implements IDisposable { } } - private _getMinimumContrastCss(bg: number, bgColorMode: number, bgColor: number, fg: number, fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean, isPowerLineGlyph: boolean): string | undefined { - if (this._config.minimumContrastRatio === 1 || isPowerLineGlyph) { + private _getMinimumContrastCss(bg: number, bgColorMode: number, bgColor: number, fg: number, fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean, excludeFromContrastRatioDemands: boolean): string | undefined { + if (this._config.minimumContrastRatio === 1 || excludeFromContrastRatioDemands) { return undefined; } @@ -372,7 +372,7 @@ export class WebglCharAtlas implements IDisposable { this._tmpCtx.textBaseline = TEXT_BASELINE; const powerLineGlyph = chars.length === 1 && isPowerlineGlyph(chars.charCodeAt(0)); - this._tmpCtx.fillStyle = this._getForegroundCss(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, bold, powerLineGlyph); + this._tmpCtx.fillStyle = this._getForegroundCss(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, bold, excludeFromContrastRatioDemands(chars.charCodeAt(0))); // Apply alpha to dim the character if (dim) { diff --git a/addons/xterm-addon-webgl/test/WebglRenderer.api.ts b/addons/xterm-addon-webgl/test/WebglRenderer.api.ts index d51990a7..bf8cf80c 100644 --- a/addons/xterm-addon-webgl/test/WebglRenderer.api.ts +++ b/addons/xterm-addon-webgl/test/WebglRenderer.api.ts @@ -688,7 +688,7 @@ describe('WebGL Renderer Integration Tests', async () => { }); }); - describe('minimumContrastRatio', async () => { + describe.skip('minimumContrastRatio', async () => { if (areTestsEnabled) { before(async () => setupBrowser()); after(async () => browser.close()); diff --git a/src/browser/renderer/BaseRenderLayer.ts b/src/browser/renderer/BaseRenderLayer.ts index ead0844f..0a9b8057 100644 --- a/src/browser/renderer/BaseRenderLayer.ts +++ b/src/browser/renderer/BaseRenderLayer.ts @@ -14,7 +14,7 @@ import { AttributeData } from 'common/buffer/AttributeData'; import { IColorSet } from 'browser/Types'; import { CellData } from 'common/buffer/CellData'; import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services'; -import { isPowerlineGlyph, throwIfFalsy } from 'browser/renderer/RendererUtils'; +import { excludeFromContrastRatioDemands, throwIfFalsy } from 'browser/renderer/RendererUtils'; import { channels, color, rgba } from 'common/Color'; import { removeElementFromParent } from 'browser/Dom'; import { tryDrawCustomChar } from 'browser/renderer/CustomGlyphs'; @@ -473,7 +473,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { } } - if (!bgOverride && !fgOverride && (this._optionsService.rawOptions.minimumContrastRatio === 1 || isPowerlineGlyph(cell.getCode()))) { + if (!bgOverride && !fgOverride && (this._optionsService.rawOptions.minimumContrastRatio === 1 || excludeFromContrastRatioDemands(cell.getCode()))) { return undefined; } diff --git a/src/browser/renderer/RendererUtils.ts b/src/browser/renderer/RendererUtils.ts index 3fc2bb3b..0a4a77e8 100644 --- a/src/browser/renderer/RendererUtils.ts +++ b/src/browser/renderer/RendererUtils.ts @@ -14,5 +14,13 @@ export function isPowerlineGlyph(codepoint: number): boolean { // Only return true for Powerline symbols which require // different padding and should be excluded from minimum contrast // ratio standards - return 0xE0A0 <= codepoint && codepoint <= 0xE0D6; + return 0xE0A4 <= codepoint && codepoint <= 0xE0D6; +} + +function isBoxOrBlockGlyph(codepoint: number): boolean { + return (0x2500 <= codepoint && codepoint <= 0x259F); +} + +export function excludeFromContrastRatioDemands(codepoint: number): boolean { + return isPowerlineGlyph(codepoint) || isBoxOrBlockGlyph(codepoint); } diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts index 39fd4fdd..61c3d72d 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.ts @@ -12,7 +12,7 @@ import { color, rgba } from 'common/Color'; import { IColorSet } from 'browser/Types'; import { ICharacterJoinerService, ISelectionService } from 'browser/services/Services'; import { JoinedCellData } from 'browser/services/CharacterJoinerService'; -import { isPowerlineGlyph } from 'browser/renderer/RendererUtils'; +import { excludeFromContrastRatioDemands } from 'browser/renderer/RendererUtils'; export const BOLD_CLASS = 'xterm-bold'; export const DIM_CLASS = 'xterm-dim'; @@ -281,7 +281,7 @@ export class DomRendererRowFactory { } private _applyMinimumContrast(element: HTMLElement, bg: IColor, fg: IColor, cell: ICellData, bgOverride: IColor | undefined, fgOverride: IColor | undefined): boolean { - if (this._optionsService.rawOptions.minimumContrastRatio === 1 || isPowerlineGlyph(cell.getCode())) { + if (this._optionsService.rawOptions.minimumContrastRatio === 1 || excludeFromContrastRatioDemands(cell.getCode())) { return false; } From bc9c9dd6fdea3d2c9e6920dbc0aef6210babba36 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Thu, 2 Jun 2022 11:02:36 -0700 Subject: [PATCH 14/26] fix #3846 (#3849) Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com> --- .../dom/DomRendererRowFactory.test.ts | 10 ++--- .../renderer/dom/DomRendererRowFactory.ts | 44 +++++++++++-------- 2 files changed, 30 insertions(+), 24 deletions(-) diff --git a/src/browser/renderer/dom/DomRendererRowFactory.test.ts b/src/browser/renderer/dom/DomRendererRowFactory.test.ts index 7c48cce8..db5d258e 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.test.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.test.ts @@ -184,7 +184,7 @@ describe('DomRendererRowFactory', () => { lineData.setCell(0, cell); const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), - 'a' + 'a' ); }); @@ -195,7 +195,7 @@ describe('DomRendererRowFactory', () => { lineData.setCell(0, cell); const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), - 'a' + 'a' ); }); @@ -205,7 +205,7 @@ describe('DomRendererRowFactory', () => { lineData.setCell(0, cell); const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), - 'a' + 'a' ); }); @@ -230,7 +230,7 @@ describe('DomRendererRowFactory', () => { lineData.setCell(0, cell); const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), - 'a' + 'a' ); }); @@ -241,7 +241,7 @@ describe('DomRendererRowFactory', () => { lineData.setCell(0, cell); const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), - 'a' + 'a' ); }); }); diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts index 61c3d72d..c37fd485 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.ts @@ -227,6 +227,28 @@ export class DomRendererRowFactory { charElement.classList.add(`xterm-decoration-top`); } + // Background + let resolvedBg: IColor; + switch (bgColorMode) { + case Attributes.CM_P16: + case Attributes.CM_P256: + resolvedBg = this._colors.ansi[bg]; + charElement.classList.add(`xterm-bg-${bg}`); + break; + case Attributes.CM_RGB: + resolvedBg = rgba.toColor(bg >> 16, bg >> 8 & 0xFF, bg & 0xFF); + this._addStyle(charElement, `background-color:#${padStart((bg >>> 0).toString(16), '0', 6)}`); + break; + case Attributes.CM_DEFAULT: + default: + if (isInverse) { + resolvedBg = this._colors.foreground; + charElement.classList.add(`xterm-bg-${INVERTED_DEFAULT_COLOR}`); + } else { + resolvedBg = this._colors.background; + } + } + // Foreground switch (fgColorMode) { case Attributes.CM_P16: @@ -234,7 +256,7 @@ export class DomRendererRowFactory { if (cell.isBold() && fg < 8 && this._optionsService.rawOptions.drawBoldTextInBrightColors) { fg += 8; } - if (!this._applyMinimumContrast(charElement, this._colors.background, this._colors.ansi[fg], cell, bgOverride, undefined)) { + if (!this._applyMinimumContrast(charElement, resolvedBg, this._colors.ansi[fg], cell, bgOverride, undefined)) { charElement.classList.add(`xterm-fg-${fg}`); } break; @@ -244,35 +266,19 @@ export class DomRendererRowFactory { (fg >> 8) & 0xFF, (fg ) & 0xFF ); - if (!this._applyMinimumContrast(charElement, this._colors.background, color, cell, bgOverride, fgOverride)) { + if (!this._applyMinimumContrast(charElement, resolvedBg, color, cell, bgOverride, fgOverride)) { this._addStyle(charElement, `color:#${padStart(fg.toString(16), '0', 6)}`); } break; case Attributes.CM_DEFAULT: default: - if (!this._applyMinimumContrast(charElement, this._colors.background, this._colors.foreground, cell, bgOverride, undefined)) { + if (!this._applyMinimumContrast(charElement, resolvedBg, this._colors.foreground, cell, bgOverride, undefined)) { if (isInverse) { charElement.classList.add(`xterm-fg-${INVERTED_DEFAULT_COLOR}`); } } } - // Background - switch (bgColorMode) { - case Attributes.CM_P16: - case Attributes.CM_P256: - charElement.classList.add(`xterm-bg-${bg}`); - break; - case Attributes.CM_RGB: - this._addStyle(charElement, `background-color:#${padStart((bg >>> 0).toString(16), '0', 6)}`); - break; - case Attributes.CM_DEFAULT: - default: - if (isInverse) { - charElement.classList.add(`xterm-bg-${INVERTED_DEFAULT_COLOR}`); - } - } - fragment.appendChild(charElement); x = lastCharX; From 58b657bc3b9c932b0383fd525dd585c85a7b5987 Mon Sep 17 00:00:00 2001 From: Umaxyon Date: Sat, 4 Jun 2022 12:52:43 +0900 Subject: [PATCH 15/26] organaize demo test buttons --- demo/index.html | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/demo/index.html b/demo/index.html index a460fea1..23f78aa6 100644 --- a/demo/index.html +++ b/demo/index.html @@ -67,12 +67,21 @@

Test

- - - - - - +
+
Lifecycle
+
+ +
Performance
+
+ +
Styles
+
+
+ +
Decorations
+
+
+
From f8e9f1813a42374463b14767a0bdec709907b3a8 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 11 Jun 2022 16:15:04 -0700 Subject: [PATCH 16/26] Custom common powerline glyph rendering Fixes #3827 --- demo/client.ts | 26 +++++++++++ src/browser/renderer/CustomGlyphs.ts | 67 ++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+) diff --git a/demo/client.ts b/demo/client.ts index b67a84a5..1c38af0f 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -585,6 +585,32 @@ function powerlineSymbolTest() { term.writeln(`0xA_ ${s('\ue0a0')}${s('\ue0a1')}${s('\ue0a2')}`); term.writeln(`0xB_ ${s('\ue0b0')}${s('\ue0b1')}${s('\ue0b2')}${s('\ue0b3')}`); term.writeln(''); + term.writeln( + `\x1b[7m` + + ` inverse \ue0b1 \x1b[0;40m\ue0b0` + + ` 0 \ue0b1 \x1b[30;41m\ue0b0\x1b[39m` + + ` 1 \ue0b1 \x1b[31;42m\ue0b0\x1b[39m` + + ` 2 \ue0b1 \x1b[32;43m\ue0b0\x1b[39m` + + ` 3 \ue0b1 \x1b[33;44m\ue0b0\x1b[39m` + + ` 4 \ue0b1 \x1b[34;45m\ue0b0\x1b[39m` + + ` 5 \ue0b1 \x1b[35;46m\ue0b0\x1b[39m` + + ` 6 \ue0b1 \x1b[36;47m\ue0b0\x1b[39m` + + ` 7 \ue0b1 \x1b[37;49m\ue0b0\x1b[0m` + ); + term.writeln(''); + term.writeln( + `\x1b[7m` + + ` inverse \ue0b3 \x1b[0;7;40m\ue0b2\x1b[27m` + + ` 0 \ue0b3 \x1b[7;30;41m\ue0b2\x1b[27;39m` + + ` 1 \ue0b3 \x1b[7;31;42m\ue0b2\x1b[27;39m` + + ` 2 \ue0b3 \x1b[7;32;43m\ue0b2\x1b[27;39m` + + ` 3 \ue0b3 \x1b[7;33;44m\ue0b2\x1b[27;39m` + + ` 4 \ue0b3 \x1b[7;34;45m\ue0b2\x1b[27;39m` + + ` 5 \ue0b3 \x1b[7;35;46m\ue0b2\x1b[27;39m` + + ` 6 \ue0b3 \x1b[7;36;47m\ue0b2\x1b[27;39m` + + ` 7 \ue0b3 \x1b[7;37;49m\ue0b2\x1b[0m` + ); + term.writeln(''); term.writeln('Powerline extra symbols:'); term.writeln(' 0 1 2 3 4 5 6 7 8 9 A B C D E F'); term.writeln(`0xA_ ${s('\ue0a3')}`); diff --git a/src/browser/renderer/CustomGlyphs.ts b/src/browser/renderer/CustomGlyphs.ts index c2bfc210..dc9450d6 100644 --- a/src/browser/renderer/CustomGlyphs.ts +++ b/src/browser/renderer/CustomGlyphs.ts @@ -325,6 +325,35 @@ export const boxDrawingDefinitions: { [character: string]: { [fontWeight: number '╰': { [Style.NORMAL]: 'C.5,0,.5,.5,1,.5' } }; +interface IVectorShape { + d: string; + type: VectorType; +} + +const enum VectorType { + FILL, + STROKE +} + +/** + * This contains the definitions of the primarily used box drawing characters as vector shapes. The + * reason these characters are defined specially is to avoid common problems if a user's font has + * not been patched with powerline characters and also to get pixel perfect rendering as rendering + * issues can occur around AA/SPAA. + * + * Original symbols defined in https://github.com/powerline/fontpatcher + */ +export const powerlineDefinitions: { [index: string]: IVectorShape } = { + // Right triangle solid + '\u{E0B0}': { d: 'M0,0 L1,.5 L0,1', type: VectorType.FILL }, + // Right triangle line + '\u{E0B1}': { d: 'M0,0 L1,.5 L0,1', type: VectorType.STROKE }, + // Left triangle solid + '\u{E0B2}': { d: 'M1,0 L0,.5 L1,1', type: VectorType.FILL }, + // Left triangle line + '\u{E0B3}': { d: 'M1,0 L0,.5 L1,1', type: VectorType.STROKE } +}; + /** * Try drawing a custom block element or box drawing character, returning whether it was * successfully drawn. @@ -355,6 +384,12 @@ export function tryDrawCustomChar( return true; } + const powerlineDefinition = powerlineDefinitions[c]; + if (powerlineDefinition) { + drawPowerlineChar(ctx, powerlineDefinition, xOffset, yOffset, scaledCellWidth, scaledCellHeight); + return true; + } + return false; } @@ -518,6 +553,38 @@ function drawBoxDrawingChar( } } +function drawPowerlineChar( + ctx: CanvasRenderingContext2D, + charDefinition: IVectorShape, + xOffset: number, + yOffset: number, + scaledCellWidth: number, + scaledCellHeight: number +): void { + ctx.beginPath(); + ctx.lineWidth = window.devicePixelRatio; + for (const instruction of charDefinition.d.split(' ')) { + const type = instruction[0]; + const f = svgToCanvasInstructionMap[type]; + if (!f) { + console.error(`Could not find drawing instructions for "${type}"`); + continue; + } + const args: string[] = instruction.substring(1).split(','); + if (!args[0] || !args[1]) { + continue; + } + f(ctx, translateArgs(args, scaledCellWidth, scaledCellHeight, xOffset, yOffset)); + } + if (charDefinition.type === VectorType.STROKE) { + ctx.strokeStyle = ctx.fillStyle; + ctx.stroke(); + } else { + ctx.fill(); + } + ctx.closePath(); +} + function clamp(value: number, max: number, min: number = 0): number { return Math.max(Math.min(value, max), min); } From d28819de41baf041b9f6755b61cbf8ed0e614a3a Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 14 Jun 2022 06:12:05 -0700 Subject: [PATCH 17/26] Add horizontal padding to powerline outline chars --- src/browser/renderer/CustomGlyphs.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/browser/renderer/CustomGlyphs.ts b/src/browser/renderer/CustomGlyphs.ts index dc9450d6..360969cb 100644 --- a/src/browser/renderer/CustomGlyphs.ts +++ b/src/browser/renderer/CustomGlyphs.ts @@ -328,6 +328,8 @@ export const boxDrawingDefinitions: { [character: string]: { [fontWeight: number interface IVectorShape { d: string; type: VectorType; + /** Padding to apply to the vector's x axis in CSS pixels. */ + horizontalPadding?: number; } const enum VectorType { @@ -347,11 +349,11 @@ export const powerlineDefinitions: { [index: string]: IVectorShape } = { // Right triangle solid '\u{E0B0}': { d: 'M0,0 L1,.5 L0,1', type: VectorType.FILL }, // Right triangle line - '\u{E0B1}': { d: 'M0,0 L1,.5 L0,1', type: VectorType.STROKE }, + '\u{E0B1}': { d: 'M0,0 L1,.5 L0,1', type: VectorType.STROKE, horizontalPadding: 0.5 }, // Left triangle solid '\u{E0B2}': { d: 'M1,0 L0,.5 L1,1', type: VectorType.FILL }, // Left triangle line - '\u{E0B3}': { d: 'M1,0 L0,.5 L1,1', type: VectorType.STROKE } + '\u{E0B3}': { d: 'M1,0 L0,.5 L1,1', type: VectorType.STROKE, horizontalPadding: 0.5 } }; /** @@ -574,7 +576,7 @@ function drawPowerlineChar( if (!args[0] || !args[1]) { continue; } - f(ctx, translateArgs(args, scaledCellWidth, scaledCellHeight, xOffset, yOffset)); + f(ctx, translateArgs(args, scaledCellWidth, scaledCellHeight, xOffset, yOffset, charDefinition.horizontalPadding)); } if (charDefinition.type === VectorType.STROKE) { ctx.strokeStyle = ctx.fillStyle; @@ -595,7 +597,7 @@ const svgToCanvasInstructionMap: { [index: string]: any } = { 'M': (ctx: CanvasRenderingContext2D, args: number[]) => ctx.moveTo(args[0], args[1]) }; -function translateArgs(args: string[], cellWidth: number, cellHeight: number, xOffset: number, yOffset: number): number[] { +function translateArgs(args: string[], cellWidth: number, cellHeight: number, xOffset: number, yOffset: number, horizontalPadding: number = 0): number[] { const result = args.map(e => parseFloat(e) || parseInt(e)); if (result.length < 2) { @@ -604,14 +606,14 @@ function translateArgs(args: string[], cellWidth: number, cellHeight: number, xO for (let x = 0; x < result.length; x += 2) { // Translate from 0-1 to 0-cellWidth - result[x] *= cellWidth; + result[x] *= cellWidth - (horizontalPadding * 2 * window.devicePixelRatio); // Ensure coordinate doesn't escape cell bounds and round to the nearest 0.5 to ensure a crisp // line at 100% devicePixelRatio if (result[x] !== 0) { result[x] = clamp(Math.round(result[x] + 0.5) - 0.5, cellWidth, 0); } // Apply the cell's offset (ie. x*cellWidth) - result[x] += xOffset; + result[x] += xOffset + (horizontalPadding * window.devicePixelRatio); } for (let y = 1; y < result.length; y += 2) { From efb939d171409acd3d6723d97c7cc0ab48727432 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 14 Jun 2022 06:51:44 -0700 Subject: [PATCH 18/26] Fix column selection issues The following parts were not respecting the column selection edge case where the start column was greater than the end column while the start row was less than the end row: - Webgl renderer - DOM renderer - Selection service (ie. getSelection(), I don't think this was a regression) Fixes #3855 --- addons/xterm-addon-webgl/src/WebglRenderer.ts | 8 ++++++-- src/browser/renderer/dom/DomRenderer.ts | 3 ++- src/browser/renderer/dom/DomRendererRowFactory.ts | 8 ++++++-- src/browser/services/SelectionService.ts | 6 +++++- 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index 8edda780..ef31a959 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -455,8 +455,12 @@ export class WebglRenderer extends Disposable implements IRenderer { } y -= this._terminal.buffer.active.viewportY; if (this._model.selection.columnSelectMode) { - return x >= this._model.selection.startCol && y >= this._model.selection.viewportCappedStartRow && - x < this._model.selection.endCol && y < this._model.selection.viewportCappedEndRow; + if (this._model.selection.startCol <= this._model.selection.endCol) { + return x >= this._model.selection.startCol && y >= this._model.selection.viewportCappedStartRow && + x < this._model.selection.endCol && y <= this._model.selection.viewportCappedEndRow; + } + return x < this._model.selection.startCol && y >= this._model.selection.viewportCappedStartRow && + x >= this._model.selection.endCol && y <= this._model.selection.viewportCappedEndRow; } return (y > this._model.selection.viewportStartRow && y < this._model.selection.viewportEndRow) || (this._model.selection.viewportStartRow === this._model.selection.viewportEndRow && y === this._model.selection.viewportStartRow && x >= this._model.selection.startCol && x < this._model.selection.endCol) || diff --git a/src/browser/renderer/dom/DomRenderer.ts b/src/browser/renderer/dom/DomRenderer.ts index a966e6f9..248f37ac 100644 --- a/src/browser/renderer/dom/DomRenderer.ts +++ b/src/browser/renderer/dom/DomRenderer.ts @@ -304,8 +304,9 @@ export class DomRenderer extends Disposable implements IRenderer { const documentFragment = document.createDocumentFragment(); if (columnSelectMode) { + const isXFlipped = start[0] > end[0]; documentFragment.appendChild( - this._createSelectionElement(viewportCappedStartRow, start[0], end[0], viewportCappedEndRow - viewportCappedStartRow + 1) + this._createSelectionElement(viewportCappedStartRow, isXFlipped ? end[0] : start[0], isXFlipped ? start[0] : end[0], viewportCappedEndRow - viewportCappedStartRow + 1) ); } else { // Draw first row diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts index c37fd485..fadf5032 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.ts @@ -322,8 +322,12 @@ export class DomRendererRowFactory { return false; } if (this._columnSelectMode) { - return x >= start[0] && y >= start[1] && - x < end[0] && y < end[1]; + if (start[0] <= end[0]) { + return x >= start[0] && y >= start[1] && + x < end[0] && y <= end[1]; + } + return x < start[0] && y >= start[1] && + x >= end[0] && y <= end[1]; } return (y > start[1] && y < end[1]) || (start[1] === end[1] && y === start[1] && x >= start[0] && x < end[0]) || diff --git a/src/browser/services/SelectionService.ts b/src/browser/services/SelectionService.ts index 57ba048f..6b5c1425 100644 --- a/src/browser/services/SelectionService.ts +++ b/src/browser/services/SelectionService.ts @@ -207,8 +207,12 @@ export class SelectionService extends Disposable implements ISelectionService { return ''; } + // For column selection it's not enough to rely on final selection's swapping of reversed + // values, it also needs the x coordinates to swap independently of the y coordinate is needed + const startCol = start[0] < end[0] ? start[0] : end[0]; + const endCol = start[0] < end[0] ? end[0] : start[0]; for (let i = start[1]; i <= end[1]; i++) { - const lineText = buffer.translateBufferLineToString(i, true, start[0], end[0]); + const lineText = buffer.translateBufferLineToString(i, true, startCol, endCol); result.push(lineText); } } else { From fb950fcbe54b392c6aef268b899c33bcc9c3122f Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 14 Jun 2022 15:09:00 -0700 Subject: [PATCH 19/26] Fix webgl clipping powerline y axis Fixes #3861 --- addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index f0099d02..5dd48034 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -322,10 +322,15 @@ export class WebglCharAtlas implements IDisposable { // Allow 1 cell width per character, with a minimum of 2 (CJK), plus some padding. This is used // to draw the glyph to the canvas as well as to restrict the bounding box search to ensure // giant ligatures (eg. =====>) don't impact overall performance. - const allowedWidth = this._config.scaledCharWidth * Math.max(chars.length, 2) + TMP_CANVAS_GLYPH_PADDING * 2; + const allowedWidth = this._config.scaledCellWidth * Math.max(chars.length, 2) + TMP_CANVAS_GLYPH_PADDING * 2; if (this._tmpCanvas.width < allowedWidth) { this._tmpCanvas.width = allowedWidth; } + // Include line height when drawing glyphs + const allowedHeight = this._config.scaledCellHeight + TMP_CANVAS_GLYPH_PADDING * 2; + if (this._tmpCanvas.height < allowedHeight) { + this._tmpCanvas.height = allowedHeight; + } this._tmpCtx.save(); this._workAttributeData.fg = fg; @@ -485,7 +490,7 @@ export class WebglCharAtlas implements IDisposable { */ private _findGlyphBoundingBox(imageData: ImageData, boundingBox: IBoundingBox, allowedWidth: number, restrictedGlyph: boolean, customGlyph: boolean): IRasterizedGlyph { boundingBox.top = 0; - const height = restrictedGlyph ? this._config.scaledCharHeight : this._tmpCanvas.height; + const height = this._config.scaledCellHeight; const width = restrictedGlyph ? this._config.scaledCharWidth : allowedWidth; let found = false; for (let y = 0; y < height; y++) { From 1639c0a031d1194fcbf5b9024665a88a7be7dc86 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 14 Jun 2022 16:45:19 -0700 Subject: [PATCH 20/26] Fix webgl min contrast ratio api tests Fixes #3847 --- addons/xterm-addon-webgl/test/WebglRenderer.api.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/addons/xterm-addon-webgl/test/WebglRenderer.api.ts b/addons/xterm-addon-webgl/test/WebglRenderer.api.ts index bf8cf80c..bf8b4cf4 100644 --- a/addons/xterm-addon-webgl/test/WebglRenderer.api.ts +++ b/addons/xterm-addon-webgl/test/WebglRenderer.api.ts @@ -688,7 +688,7 @@ describe('WebGL Renderer Integration Tests', async () => { }); }); - describe.skip('minimumContrastRatio', async () => { + describe('minimumContrastRatio', async () => { if (areTestsEnabled) { before(async () => setupBrowser()); after(async () => browser.close()); @@ -718,9 +718,10 @@ describe('WebGL Renderer Integration Tests', async () => { 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[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█` + `\\x1b[30m■\\x1b[31m■\\x1b[32m■\\x1b[33m■\\x1b[34m■\\x1b[35m■\\x1b[36m■\\x1b[37m■\\r\\n` + + `\\x1b[90m■\\x1b[91m■\\x1b[92m■\\x1b[93m■\\x1b[94m■\\x1b[95m■\\x1b[96m■\\x1b[97m■` ); // Validate before minimumContrastRatio is applied await pollFor(page, () => getCellColor(1, 1), [0x2e, 0x34, 0x36, 255]); @@ -786,9 +787,10 @@ describe('WebGL Renderer Integration Tests', async () => { 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[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█` + `\\x1b[30m■\\x1b[31m■\\x1b[32m■\\x1b[33m■\\x1b[34m■\\x1b[35m■\\x1b[36m■\\x1b[37m■\\r\\n` + + `\\x1b[90m■\\x1b[91m■\\x1b[92m■\\x1b[93m■\\x1b[94m■\\x1b[95m■\\x1b[96m■\\x1b[97m■` ); // Validate before minimumContrastRatio is applied await pollFor(page, () => getCellColor(1, 1), [0x2e, 0x34, 0x36, 255]); From 81dce7a4baf2c1df80a7c483fd3e39c210c58396 Mon Sep 17 00:00:00 2001 From: mmis1000 Date: Fri, 17 Jun 2022 01:09:50 +0800 Subject: [PATCH 21/26] Fix typo in serialize addon test --- addons/xterm-addon-serialize/test/SerializeAddon.api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/xterm-addon-serialize/test/SerializeAddon.api.ts b/addons/xterm-addon-serialize/test/SerializeAddon.api.ts index 5c4ae437..b86b066b 100644 --- a/addons/xterm-addon-serialize/test/SerializeAddon.api.ts +++ b/addons/xterm-addon-serialize/test/SerializeAddon.api.ts @@ -83,7 +83,7 @@ describe('SerializeAddon', () => { const buffer3 = await page.evaluate(`inspectBuffer(term.buffer.normal);`); await page.evaluate(`term.reset();`); - await writeRawSync(page, '1234567890n12345'); + await writeRawSync(page, '123456789012345'); const buffer4 = await page.evaluate(`inspectBuffer(term.buffer.normal);`); assert.throw(() => { From d24a27d93ec9196ee90a3eb999eab36d4581de36 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 17 Jun 2022 08:01:10 -0700 Subject: [PATCH 22/26] Fix clipping of non-powerline chars See microsoft/vscode#152478 --- addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index 5dd48034..555506fd 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -490,7 +490,7 @@ export class WebglCharAtlas implements IDisposable { */ private _findGlyphBoundingBox(imageData: ImageData, boundingBox: IBoundingBox, allowedWidth: number, restrictedGlyph: boolean, customGlyph: boolean): IRasterizedGlyph { boundingBox.top = 0; - const height = this._config.scaledCellHeight; + const height = restrictedGlyph ? this._config.scaledCellHeight : this._tmpCanvas.height; const width = restrictedGlyph ? this._config.scaledCharWidth : allowedWidth; let found = false; for (let y = 0; y < height; y++) { From 52044e9aadd9eeaab702b88bd6e177b4ce336e80 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 21 Jun 2022 10:26:02 -0800 Subject: [PATCH 23/26] clear all markers (#3868) --- src/browser/Terminal.ts | 2 +- src/browser/TestUtils.test.ts | 2 +- src/common/buffer/Buffer.ts | 13 +++++-------- src/common/buffer/Types.d.ts | 4 ++-- 4 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/browser/Terminal.ts b/src/browser/Terminal.ts index 9ab3087c..1f436c8c 100644 --- a/src/browser/Terminal.ts +++ b/src/browser/Terminal.ts @@ -1325,7 +1325,7 @@ export class Terminal extends CoreTerminal implements ITerminal { // Don't clear if it's already clear return; } - this.buffer.clearAllMarkers(0); + this.buffer.clearAllMarkers(); this.buffer.lines.set(0, this.buffer.lines.get(this.buffer.ybase + this.buffer.y)!); this.buffer.lines.length = 1; this.buffer.ydisp = 0; diff --git a/src/browser/TestUtils.test.ts b/src/browser/TestUtils.test.ts index 1ba175f3..92f90a1d 100644 --- a/src/browser/TestUtils.test.ts +++ b/src/browser/TestUtils.test.ts @@ -262,7 +262,7 @@ export class MockBuffer implements IBuffer { public clearMarkers(y: number): void { throw new Error('Method not implemented.'); } - public clearAllMarkers(excludeY: number): void { + public clearAllMarkers(): void { throw new Error('Method not implemented.'); } } diff --git a/src/common/buffer/Buffer.ts b/src/common/buffer/Buffer.ts index ab295784..c8b0d1b2 100644 --- a/src/common/buffer/Buffer.ts +++ b/src/common/buffer/Buffer.ts @@ -601,16 +601,13 @@ export class Buffer implements IBuffer { } /** - * Clears markers on all lines except for those on a particular line. - * @param excludeY The line to exclude. + * Clears markers on all lines */ - public clearAllMarkers(excludeY: number): void { + public clearAllMarkers(): void { this._isClearing = true; for (let i = 0; i < this.markers.length; i++) { - if (this.markers[i].line !== excludeY) { - this.markers[i].dispose(); - this.markers.splice(i--, 1); - } + this.markers[i].dispose(); + this.markers.splice(i--, 1); } this._isClearing = false; } @@ -671,7 +668,7 @@ export class Buffer implements IBuffer { export class BufferStringIterator implements IBufferStringIterator { private _current: number; - constructor ( + constructor( private _buffer: IBuffer, private _trimRight: boolean, private _startIndex: number = 0, diff --git a/src/common/buffer/Types.d.ts b/src/common/buffer/Types.d.ts index f26b4b26..421ddd37 100644 --- a/src/common/buffer/Types.d.ts +++ b/src/common/buffer/Types.d.ts @@ -10,7 +10,7 @@ import { IEvent } from 'common/EventEmitter'; export type BufferIndex = [number, number]; export interface IBufferStringIteratorResult { - range: {first: number, last: number}; + range: { first: number, last: number }; content: string; } @@ -46,7 +46,7 @@ export interface IBuffer { getWhitespaceCell(attr?: IAttributeData): ICellData; addMarker(y: number): IMarker; clearMarkers(y: number): void; - clearAllMarkers(excludeY: number): void; + clearAllMarkers(): void; } export interface IBufferSet extends IDisposable { From 44e4b56ced64265452737f101f4ef253ff03570b Mon Sep 17 00:00:00 2001 From: Sebastian Pfitzner Date: Wed, 22 Jun 2022 11:52:00 +0200 Subject: [PATCH 24/26] Only send wheel mouse event when scrolling a line or more --- src/browser/Terminal.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/browser/Terminal.ts b/src/browser/Terminal.ts index 1f436c8c..ebda3833 100644 --- a/src/browser/Terminal.ts +++ b/src/browser/Terminal.ts @@ -703,10 +703,13 @@ export class Terminal extends CoreTerminal implements ITerminal { but = ev.button < 3 ? ev.button : CoreMouseButton.NONE; break; case 'wheel': - // only UP/DOWN wheel events are respected - if ((ev as WheelEvent).deltaY !== 0) { - action = (ev as WheelEvent).deltaY < 0 ? CoreMouseAction.UP : CoreMouseAction.DOWN; + const amount = self.viewport!.getLinesScrolled(ev as WheelEvent) + + if (amount === 0) { + return false } + + action = (ev as WheelEvent).deltaY < 0 ? CoreMouseAction.UP : CoreMouseAction.DOWN; but = CoreMouseButton.WHEEL; break; default: From 783722c2f37212f9472452c5947f79d19014ca40 Mon Sep 17 00:00:00 2001 From: Sebastian Pfitzner Date: Wed, 22 Jun 2022 11:58:32 +0200 Subject: [PATCH 25/26] Add missing semicolons --- src/browser/Terminal.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/browser/Terminal.ts b/src/browser/Terminal.ts index ebda3833..017633b4 100644 --- a/src/browser/Terminal.ts +++ b/src/browser/Terminal.ts @@ -703,10 +703,10 @@ export class Terminal extends CoreTerminal implements ITerminal { but = ev.button < 3 ? ev.button : CoreMouseButton.NONE; break; case 'wheel': - const amount = self.viewport!.getLinesScrolled(ev as WheelEvent) + const amount = self.viewport!.getLinesScrolled(ev as WheelEvent); if (amount === 0) { - return false + return false; } action = (ev as WheelEvent).deltaY < 0 ? CoreMouseAction.UP : CoreMouseAction.DOWN; From 343e1c8a86f1081fac3571898f0f347e47fc2e07 Mon Sep 17 00:00:00 2001 From: Chris Castle Date: Fri, 24 Jun 2022 12:59:26 -0700 Subject: [PATCH 26/26] Add Render to list of xterm.js users --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 7d98de01..1359772c 100644 --- a/README.md +++ b/README.md @@ -190,6 +190,7 @@ Xterm.js is used in several world-class applications to provide great terminal e - [**WiTTY**](https://github.com/syssecfsu/witty): Web-based interactive terminal emulator that allows users to easily record, share, and replay console sessions. - [**libv86 Terminal Forwarding**](https://github.com/hello-smile6/libv86-terminal-forwarding): Peer-to-peer SSH for the web, using WebRTC via [Bugout](https://github.com/chr15m/bugout) for data transfer and [v86](https://github.com/copy/v86) for web-based virtualization. - [**hack.courses**](https://hack.courses): Interactive Linux and command-line classes using xterm.js to expose a real terminal available for everyone. +- [**Render**](https://render.com): Platform-as-a-service for your apps, websites, and databases using xterm.js to provide a command prompt for user containers and for streaming build and runtime logs. - [And much more...](https://github.com/xtermjs/xterm.js/network/dependents?package_id=UGFja2FnZS0xNjYzMjc4OQ%3D%3D) Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it on our list. Note: Please add any new contributions to the end of the list only.