Merge branch 'master' into #4121

This commit is contained in:
xie jialong 努力鸭
2023-06-07 19:23:01 +08:00
committed by GitHub
36 changed files with 680 additions and 253 deletions
-23
View File
@@ -1,23 +0,0 @@
FROM node:14-buster
# Configure apt
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update \
&& apt-get -y install --no-install-recommends apt-utils 2>&1
# Verify git and process tools are installed
RUN apt-get install -y git procps
# Install yarn
RUN apt-get install -y curl apt-transport-https lsb-release \
&& curl -sS https://dl.yarnpkg.com/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/pubkey.gpg | apt-key add - 2>/dev/null \
&& echo "deb https://dl.yarnpkg.com/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/ stable main" | tee /etc/apt/sources.list.d/yarn.list \
&& apt-get update \
&& apt-get -y install --no-install-recommends \
yarn
# Clean up
RUN apt-get autoremove -y \
&& apt-get clean -y \
&& rm -rf /var/lib/apt/lists/*
ENV DEBIAN_FRONTEND=dialog
+17 -7
View File
@@ -1,10 +1,20 @@
{
"name": "xterm.js",
"dockerFile": "Dockerfile",
"appPort": 3000,
"extensions": [
"dbaeumer.vscode-eslint",
"editorconfig.editorconfig",
"hbenl.vscode-mocha-test-adapter"
]
"image": "mcr.microsoft.com/devcontainers/typescript-node:0-18-buster",
"features": {
"ghcr.io/devcontainers/features/node:1": {} // yarn
},
"forwardPorts": [
3000
],
"postCreateCommand": "yarn install",
"customizations": {
"vscode": {
"extensions": [
"dbaeumer.vscode-eslint",
"editorconfig.editorconfig",
"hbenl.vscode-mocha-test-adapter"
]
}
}
}
+3
View File
@@ -1,5 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: FAQ
url: https://github.com/xtermjs/xterm.js/wiki/FAQ
about: See our frequently asked questions before filing a bug report
- name: Support / Q&A
url: https://github.com/xtermjs/xterm.js/discussions/categories/q-a
about: Use GitHub Discussions for community support and general Q&A
@@ -379,7 +379,17 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer
}
this._ctx.save();
this._clipRow(y);
// Draw the image, use the bitmap if it's available
// HACK: If the canvas doesn't match, delete the generator. It's not clear how this happens but
// something is wrong with either the lifecycle of _bitmapGenerator or the page canvases are
// swapped out unexpectedly
if (this._bitmapGenerator[glyph.texturePage] && this._charAtlas.pages[glyph.texturePage].canvas !== this._bitmapGenerator[glyph.texturePage]!.canvas) {
this._bitmapGenerator[glyph.texturePage]?.bitmap?.close();
delete this._bitmapGenerator[glyph.texturePage];
}
if (this._charAtlas.pages[glyph.texturePage].version !== this._bitmapGenerator[glyph.texturePage]?.version) {
if (!this._bitmapGenerator[glyph.texturePage]) {
this._bitmapGenerator[glyph.texturePage] = new BitmapGenerator(this._charAtlas.pages[glyph.texturePage].canvas);
@@ -446,11 +456,12 @@ class BitmapGenerator {
public get bitmap(): ImageBitmap | undefined { return this._bitmap; }
public version: number = -1;
constructor(private readonly _canvas: HTMLCanvasElement) {
constructor(public readonly canvas: HTMLCanvasElement) {
}
public refresh(): void {
// Clear the bitmap immediately as it's stale
this._bitmap?.close();
this._bitmap = undefined;
// Disable ImageBitmaps on Safari because of https://bugs.webkit.org/show_bug.cgi?id=149990
if (isSafari) {
@@ -466,9 +477,10 @@ class BitmapGenerator {
private _generate(): void {
if (this._state === BitmapGeneratorState.IDLE) {
this._bitmap?.close();
this._bitmap = undefined;
this._state = BitmapGeneratorState.GENERATING;
window.createImageBitmap(this._canvas).then(bitmap => {
window.createImageBitmap(this.canvas).then(bitmap => {
if (this._state === BitmapGeneratorState.GENERATING_INVALID) {
this.refresh();
} else {
+1 -1
View File
@@ -4,7 +4,7 @@
*/
import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IRenderService, ISelectionService, IThemeService } from 'browser/services/Services';
import { IColorSet, ITerminal } from 'browser/Types';
import { ITerminal } from 'browser/Types';
import { CanvasRenderer } from './CanvasRenderer';
import { IBufferService, ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
import { ITerminalAddon, Terminal } from 'xterm';
@@ -13,6 +13,7 @@ import { IEventEmitter } from 'common/EventEmitter';
import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
import { Terminal } from 'xterm';
import { toDisposable } from 'common/Lifecycle';
import { isFirefox } from 'common/Platform';
interface ICursorState {
x: number;
@@ -190,8 +191,9 @@ export class CursorRenderLayer extends BaseRenderLayer {
private _clearCursor(): void {
if (this._state) {
// Avoid potential rounding errors when device pixel ratio is less than 1
if (this._coreBrowserService.dpr < 1) {
// Avoid potential rounding errors when browser is Firefox (#4487) or device pixel ratio is
// less than 1
if (isFirefox || this._coreBrowserService.dpr < 1) {
this._clearAll();
} else {
this._clearCells(this._state.x, this._state.y, this._state.width, 1);
@@ -188,12 +188,6 @@ export class TextRenderLayer extends BaseRenderLayer {
nextFillStyle = this._themeService.colors.ansi[cell.getBgColor()].css;
}
// Apply dim to the background, this is relatively slow as the CSS is re-parsed but dim is
// rarely used
if (nextFillStyle && cell.isDim()) {
nextFillStyle = color.multiplyOpacity(css.toColor(nextFillStyle), 0.5).css;
}
// Get any decoration foreground/background overrides, this must be fetched before the early
// exist but applied after inverse
let isTop = false;
+119 -134
View File
@@ -3,9 +3,9 @@
* @license MIT
*/
import { Terminal, IDisposable, ITerminalAddon, IBufferRange, IDecoration } from 'xterm';
import { Terminal, IDisposable, ITerminalAddon, IDecoration } from 'xterm';
import { EventEmitter } from 'common/EventEmitter';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { Disposable, toDisposable, disposeArray } from 'common/Lifecycle';
export interface ISearchOptions {
regex?: boolean;
@@ -30,6 +30,10 @@ export interface ISearchPosition {
startRow: number;
}
export interface ISearchAddonOptions {
highlightLimit: number;
}
export interface ISearchResult {
term: string;
col: number;
@@ -48,15 +52,22 @@ type LineCacheEntry = [
lineOffsets: number[]
];
interface IHighlight extends IDisposable {
decoration: IDecoration;
match: ISearchResult;
}
const NON_WORD_CHARACTERS = ' ~!@#$%^&*()+`-=[]{}|\\;:"\',./<>?';
const LINES_CACHE_TIME_TO_LIVE = 15 * 1000; // 15 secs
const DEFAULT_HIGHLIGHT_LIMIT = 1000;
export class SearchAddon extends Disposable implements ITerminalAddon {
private _terminal: Terminal | undefined;
private _cachedSearchTerm: string | undefined;
private _selectedDecoration: IDecoration | undefined;
private _resultDecorations: Map<number, IDecoration[]> | undefined;
private _searchResults: Map<string, ISearchResult> | undefined;
private _highlightedLines: Set<number> = new Set();
private _highlightDecorations: IHighlight[] = [];
private _selectedDecoration: IHighlight | undefined;
private _highlightLimit: number;
private _onDataDisposable: IDisposable | undefined;
private _onResizeDisposable: IDisposable | undefined;
private _lastSearchOptions: ISearchOptions | undefined;
@@ -71,11 +82,15 @@ export class SearchAddon extends Disposable implements ITerminalAddon {
private _cursorMoveListener: IDisposable | undefined;
private _resizeListener: IDisposable | undefined;
private _resultIndex: number | undefined;
private readonly _onDidChangeResults = this.register(new EventEmitter<{ resultIndex: number, resultCount: number } | undefined>());
private readonly _onDidChangeResults = this.register(new EventEmitter<{ resultIndex: number, resultCount: number }>());
public readonly onDidChangeResults = this._onDidChangeResults.event;
constructor(options?: Partial<ISearchAddonOptions>) {
super();
this._highlightLimit = options?.highlightLimit ?? DEFAULT_HIGHLIGHT_LIMIT;
}
public activate(terminal: Terminal): void {
this._terminal = terminal;
this._onDataDisposable = this.register(this._terminal.onWriteParsed(() => this._updateMatches()));
@@ -93,24 +108,18 @@ export class SearchAddon extends Disposable implements ITerminalAddon {
}
if (this._cachedSearchTerm && this._lastSearchOptions?.decorations) {
this._highlightTimeout = setTimeout(() => {
this.findPrevious(this._cachedSearchTerm!, { ...this._lastSearchOptions, incremental: true, noScroll: true });
this._resultIndex = this._searchResults ? this._searchResults.size - 1 : -1;
this._onDidChangeResults.fire({ resultIndex: this._resultIndex, resultCount: this._searchResults?.size ?? -1 });
const term = this._cachedSearchTerm;
this._cachedSearchTerm = undefined;
this.findPrevious(term!, { ...this._lastSearchOptions, incremental: true, noScroll: true });
}, 200);
}
}
public clearDecorations(retainCachedSearchTerm?: boolean): void {
this._selectedDecoration?.dispose();
this._searchResults?.clear();
this._resultDecorations?.forEach(decorations => {
for (const d of decorations) {
d.dispose();
}
});
this._resultDecorations?.clear();
this._searchResults = undefined;
this._resultDecorations = undefined;
this.clearActiveDecoration();
disposeArray(this._highlightDecorations);
this._highlightDecorations = [];
this._highlightedLines.clear();
if (!retainCachedSearchTerm) {
this._cachedSearchTerm = undefined;
}
@@ -134,11 +143,16 @@ export class SearchAddon extends Disposable implements ITerminalAddon {
}
this._lastSearchOptions = searchOptions;
if (searchOptions?.decorations) {
if (this._resultIndex !== undefined || this._cachedSearchTerm === undefined || term !== this._cachedSearchTerm) {
if (this._cachedSearchTerm === undefined || term !== this._cachedSearchTerm) {
this._highlightAllMatches(term, searchOptions);
}
}
return this._fireResults(term, this._findNextAndSelect(term, searchOptions), searchOptions);
const found = this._findNextAndSelect(term, searchOptions);
this._fireResults(searchOptions);
this._cachedSearchTerm = term;
return found;
}
private _highlightAllMatches(term: string, searchOptions: ISearchOptions): void {
@@ -153,32 +167,30 @@ export class SearchAddon extends Disposable implements ITerminalAddon {
// new search, clear out the old decorations
this.clearDecorations(true);
this._searchResults = new Map<string, ISearchResult>();
this._resultDecorations = new Map<number, IDecoration[]>();
const resultDecorations = this._resultDecorations;
const searchResultsWithHighlight: ISearchResult[] = [];
let prevResult: ISearchResult | undefined = undefined;
let result = this._find(term, 0, 0, searchOptions);
while (result && !this._searchResults.get(`${result.row}-${result.col}`)) {
this._searchResults.set(`${result.row}-${result.col}`, result);
while (result && (prevResult?.row !== result.row || prevResult?.col !== result.col)) {
if (searchResultsWithHighlight.length >= this._highlightLimit) {
break;
}
prevResult = result;
searchResultsWithHighlight.push(prevResult);
result = this._find(
term,
result.col + result.term.length >= this._terminal.cols ? result.row + 1 : result.row,
result.col + result.term.length >= this._terminal.cols ? 0 : result.col + 1,
prevResult.col + prevResult.term.length >= this._terminal.cols ? prevResult.row + 1 : prevResult.row,
prevResult.col + prevResult.term.length >= this._terminal.cols ? 0 : prevResult.col + 1,
searchOptions
);
if (this._searchResults.size > 1000) {
this.clearDecorations();
this._resultIndex = undefined;
return;
}
for (const match of searchResultsWithHighlight) {
const decoration = this._createResultDecoration(match, searchOptions.decorations!);
if (decoration) {
this._highlightedLines.add(decoration.marker.line);
this._highlightDecorations.push({ decoration, match, dispose() { decoration.dispose(); } });
}
}
this._searchResults.forEach(result => {
const resultDecoration = this._createResultDecoration(result, searchOptions.decorations!);
if (resultDecoration) {
const decorationsForLine = resultDecorations.get(resultDecoration.marker.line) || [];
decorationsForLine.push(resultDecoration);
resultDecorations.set(resultDecoration.marker.line, decorationsForLine);
}
});
}
private _find(term: string, startRow: number, startCol: number, searchOptions?: ISearchOptions): ISearchResult | undefined {
@@ -223,26 +235,22 @@ export class SearchAddon extends Disposable implements ITerminalAddon {
if (!this._terminal || !term || term.length === 0) {
this._terminal?.clearSelection();
this.clearDecorations();
this._cachedSearchTerm = undefined;
this._resultIndex = -1;
return false;
}
if (this._cachedSearchTerm !== term) {
this._resultIndex = undefined;
this._terminal.clearSelection();
}
const prevSelectedPos = this._terminal.getSelectionPosition();
this._terminal.clearSelection();
let startCol = 0;
let startRow = 0;
let currentSelection: IBufferRange | undefined;
if (this._terminal.hasSelection()) {
const incremental = searchOptions ? searchOptions.incremental : false;
// Start from the selection end if there is a selection
// For incremental search, use existing row
currentSelection = this._terminal.getSelectionPosition()!;
startRow = incremental ? currentSelection.start.y : currentSelection.end.y;
startCol = incremental ? currentSelection.start.x : currentSelection.end.x;
if (prevSelectedPos) {
if (this._cachedSearchTerm === term) {
startCol = prevSelectedPos.end.x;
startRow = prevSelectedPos.end.y;
} else {
startCol = prevSelectedPos.start.x;
startRow = prevSelectedPos.start.y;
}
}
this._initLinesCache();
@@ -281,24 +289,12 @@ export class SearchAddon extends Disposable implements ITerminalAddon {
}
// If there is only one result, wrap back and return selection if it exists.
if (!result && currentSelection) {
searchPosition.startRow = currentSelection.start.y;
if (!result && prevSelectedPos) {
searchPosition.startRow = prevSelectedPos.start.y;
searchPosition.startCol = 0;
result = this._findInLine(term, searchPosition, searchOptions);
}
if (this._searchResults) {
if (this._searchResults.size === 0) {
this._resultIndex = -1;
} else if (this._resultIndex === undefined) {
this._resultIndex = 0;
} else {
this._resultIndex++;
if (this._resultIndex >= this._searchResults.size) {
this._resultIndex = 0;
}
}
}
// Set selection and scroll if a result was found
return this._selectResult(result, searchOptions?.decorations, searchOptions?.noScroll);
}
@@ -315,75 +311,74 @@ export class SearchAddon extends Disposable implements ITerminalAddon {
}
this._lastSearchOptions = searchOptions;
if (searchOptions?.decorations) {
if (this._resultIndex !== undefined || this._cachedSearchTerm === undefined || term !== this._cachedSearchTerm) {
if (this._cachedSearchTerm === undefined || term !== this._cachedSearchTerm) {
this._highlightAllMatches(term, searchOptions);
}
}
return this._fireResults(term, this._findPreviousAndSelect(term, searchOptions), searchOptions);
const found = this._findPreviousAndSelect(term, searchOptions);
this._fireResults(searchOptions);
this._cachedSearchTerm = term;
return found;
}
private _fireResults(term: string, found: boolean, searchOptions?: ISearchOptions): boolean {
private _fireResults(searchOptions?: ISearchOptions): void {
if (searchOptions?.decorations) {
if (this._resultIndex !== undefined && this._searchResults?.size !== undefined) {
this._onDidChangeResults.fire({ resultIndex: this._resultIndex, resultCount: this._searchResults.size });
} else {
this._onDidChangeResults.fire(undefined);
let resultIndex = -1;
if (this._selectedDecoration) {
const selectedMatch = this._selectedDecoration.match;
for (let i = 0; i < this._highlightDecorations.length; i++) {
const match = this._highlightDecorations[i].match;
if (match.row === selectedMatch.row && match.col === selectedMatch.col && match.size === selectedMatch.size) {
resultIndex = i;
break;
}
}
}
this._onDidChangeResults.fire({ resultIndex, resultCount: this._highlightDecorations.length });
}
this._cachedSearchTerm = term;
return found;
}
private _findPreviousAndSelect(term: string, searchOptions?: ISearchOptions): boolean {
if (!this._terminal) {
throw new Error('Cannot use addon until it has been loaded');
}
let result: ISearchResult | undefined;
if (!this._terminal || !term || term.length === 0) {
result = undefined;
this._terminal?.clearSelection();
this.clearDecorations();
this._resultIndex = -1;
return false;
}
if (this._cachedSearchTerm !== term) {
this._resultIndex = undefined;
this._terminal.clearSelection();
}
const prevSelectedPos = this._terminal.getSelectionPosition();
this._terminal.clearSelection();
let startRow = this._terminal.buffer.active.baseY + this._terminal.rows;
let startRow = this._terminal.buffer.active.baseY + this._terminal.rows - 1;
let startCol = this._terminal.cols;
const isReverseSearch = true;
const incremental = searchOptions ? searchOptions.incremental : false;
let currentSelection: IBufferRange | undefined;
if (this._terminal.hasSelection()) {
currentSelection = this._terminal.getSelectionPosition()!;
// Start from selection start if there is a selection
startRow = currentSelection.start.y;
startCol = currentSelection.start.x;
}
this._initLinesCache();
const searchPosition: ISearchPosition = {
startRow,
startCol
};
if (incremental) {
// Try to expand selection to right first.
result = this._findInLine(term, searchPosition, searchOptions, false);
const isOldResultHighlighted = result && result.row === startRow && result.col === startCol;
if (!isOldResultHighlighted) {
// If selection was not able to be expanded to the right, then try reverse search
if (currentSelection) {
searchPosition.startRow = currentSelection.end.y;
searchPosition.startCol = currentSelection.end.x;
let result: ISearchResult | undefined;
if (prevSelectedPos) {
searchPosition.startRow = startRow = prevSelectedPos.start.y;
searchPosition.startCol = startCol = prevSelectedPos.start.x;
if (this._cachedSearchTerm !== term) {
// Try to expand selection to right first.
result = this._findInLine(term, searchPosition, searchOptions, false);
if (!result) {
// If selection was not able to be expanded to the right, then try reverse search
searchPosition.startRow = startRow = prevSelectedPos.end.y;
searchPosition.startCol = startCol = prevSelectedPos.end.x;
}
result = this._findInLine(term, searchPosition, searchOptions, true);
}
} else {
}
if (!result) {
result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch);
}
@@ -399,8 +394,8 @@ export class SearchAddon extends Disposable implements ITerminalAddon {
}
}
// If we hit the top and didn't search from the very bottom wrap back down
if (!result && startRow !== (this._terminal.buffer.active.baseY + this._terminal.rows)) {
for (let y = (this._terminal.buffer.active.baseY + this._terminal.rows); y >= startRow; y--) {
if (!result && startRow !== (this._terminal.buffer.active.baseY + this._terminal.rows - 1)) {
for (let y = (this._terminal.buffer.active.baseY + this._terminal.rows - 1); y >= startRow; y--) {
searchPosition.startRow = y;
result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch);
if (result) {
@@ -409,22 +404,6 @@ export class SearchAddon extends Disposable implements ITerminalAddon {
}
}
if (this._searchResults) {
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;
}
}
}
// If there is only one result, return true.
if (!result && currentSelection) return true;
// Set selection and scroll if a result was found
return this._selectResult(result, searchOptions?.decorations, searchOptions?.noScroll);
}
@@ -675,7 +654,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon {
if (options) {
const marker = terminal.registerMarker(-terminal.buffer.active.baseY - terminal.buffer.active.cursorY + result.row);
if (marker) {
this._selectedDecoration = terminal.registerDecoration({
const decoration = terminal.registerDecoration({
marker,
x: result.col,
width: result.size,
@@ -685,8 +664,13 @@ export class SearchAddon extends Disposable implements ITerminalAddon {
color: options.activeMatchColorOverviewRuler
}
});
this._selectedDecoration?.onRender((e) => this._applyStyles(e, options.activeMatchBorder, true));
this._selectedDecoration?.onDispose(() => marker.dispose());
if (decoration) {
const disposables: IDisposable[] = [];
disposables.push(marker);
disposables.push(decoration.onRender((e) => this._applyStyles(e, options.activeMatchBorder, true)));
disposables.push(decoration.onDispose(() => disposeArray(disposables)));
this._selectedDecoration = { decoration, match: result, dispose() { decoration.dispose(); } };
}
}
}
@@ -709,9 +693,6 @@ export class SearchAddon extends Disposable implements ITerminalAddon {
* @returns
*/
private _applyStyles(element: HTMLElement, borderColor: string | undefined, isActiveResult: boolean): void {
if (element.clientWidth <= 0) {
return;
}
if (!element.classList.contains('xterm-find-result-decoration')) {
element.classList.add('xterm-find-result-decoration');
if (borderColor) {
@@ -740,13 +721,17 @@ export class SearchAddon extends Disposable implements ITerminalAddon {
x: result.col,
width: result.size,
backgroundColor: options.matchBackground,
overviewRulerOptions: this._resultDecorations?.get(marker.line) ? undefined : {
overviewRulerOptions: this._highlightedLines.has(marker.line) ? undefined : {
color: options.matchOverviewRuler,
position: 'center'
}
});
findResultDecoration?.onRender((e) => this._applyStyles(e, options.matchBorder, false));
findResultDecoration?.onDispose(() => marker.dispose());
if (findResultDecoration) {
const disposables: IDisposable[] = [];
disposables.push(marker);
disposables.push(findResultDecoration.onRender((e) => this._applyStyles(e, options.matchBorder, false)));
disposables.push(findResultDecoration.onDispose(() => disposeArray(disposables)));
}
return findResultDecoration;
}
}
@@ -178,7 +178,7 @@ describe('Search Tests', function (): void {
window.calls = [];
window.search.onDidChangeResults(e => window.calls.push(e));
`);
await writeSync(page, 'abc aabc');
await writeSync(page, 'd abc aabc d');
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 }
@@ -201,15 +201,64 @@ describe('Search Tests', function (): void {
{ resultCount: 2, resultIndex: 0 },
{ resultCount: 2, resultIndex: 1 }
]);
assert.deepStrictEqual(await page.evaluate(`window.search.findNext('d', { 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 },
{ 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: 2, resultIndex: 1 },
{ resultCount: 0, resultIndex: -1 }
]);
});
it('should fire with more than 1k matches', async () => {
await page.evaluate(`
window.calls = [];
window.search.onDidChangeResults(e => window.calls.push(e));
`);
const data = ('a bc'.repeat(10) + '\\n\\r').repeat(150);
await writeSync(page, data);
assert.strictEqual(await page.evaluate(`window.search.findNext('a', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 1000, resultIndex: 0 }
]);
assert.strictEqual(await page.evaluate(`window.search.findNext('a', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 1000, resultIndex: 0 },
{ resultCount: 1000, resultIndex: 1 }
]);
assert.strictEqual(await page.evaluate(`window.search.findNext('bc', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 1000, resultIndex: 0 },
{ resultCount: 1000, resultIndex: 1 },
{ resultCount: 1000, resultIndex: 1 }
]);
});
it('should fire when writing to terminal', async () => {
await page.evaluate(`
window.calls = [];
window.search.onDidChangeResults(e => window.calls.push(e));
`);
await writeSync(page, 'abc bc c\\n\\r'.repeat(2));
assert.strictEqual(await page.evaluate(`window.search.findNext('abc', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 2, resultIndex: 0 }
]);
await writeSync(page, 'abc bc c\\n\\r');
await timeout(300);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 2, resultIndex: 0 },
{ resultCount: 3, resultIndex: 0 }
]);
});
});
describe('findPrevious', () => {
it('should not fire unless the decorations option is set', async () => {
@@ -233,6 +282,7 @@ describe('Search Tests', function (): void {
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 1, resultIndex: 0 }
]);
await page.evaluate(`window.term.clearSelection()`);
assert.strictEqual(await page.evaluate(`window.search.findPrevious('b', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 1, resultIndex: 0 },
@@ -262,7 +312,7 @@ describe('Search Tests', function (): void {
window.calls = [];
window.search.onDidChangeResults(e => window.calls.push(e));
`);
await writeSync(page, 'abc aabc');
await writeSync(page, 'd abc aabc d');
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 }
@@ -285,15 +335,64 @@ describe('Search Tests', function (): void {
{ resultCount: 2, resultIndex: 1 },
{ resultCount: 2, resultIndex: 0 }
]);
assert.deepStrictEqual(await page.evaluate(`window.search.findPrevious('d', { 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 },
{ resultCount: 2, resultIndex: 1 }
]);
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: 2, resultIndex: 1 },
{ resultCount: 0, resultIndex: -1 }
]);
});
it('should fire with more than 1k matches', async () => {
await page.evaluate(`
window.calls = [];
window.search.onDidChangeResults(e => window.calls.push(e));
`);
const data = ('a bc'.repeat(10) + '\\n\\r').repeat(150);
await writeSync(page, data);
assert.strictEqual(await page.evaluate(`window.search.findPrevious('a', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 1000, resultIndex: -1 }
]);
assert.strictEqual(await page.evaluate(`window.search.findPrevious('a', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 1000, resultIndex: -1 },
{ resultCount: 1000, resultIndex: -1 }
]);
assert.strictEqual(await page.evaluate(`window.search.findPrevious('bc', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 1000, resultIndex: -1 },
{ resultCount: 1000, resultIndex: -1 },
{ resultCount: 1000, resultIndex: -1 }
]);
});
it('should fire when writing to terminal', async () => {
await page.evaluate(`
window.calls = [];
window.search.onDidChangeResults(e => window.calls.push(e));
`);
await writeSync(page, 'abc bc c\\n\\r'.repeat(2));
assert.strictEqual(await page.evaluate(`window.search.findPrevious('abc', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 2, resultIndex: 1 }
]);
await writeSync(page, 'abc bc c\\n\\r');
await timeout(300);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 2, resultIndex: 1 },
{ resultCount: 3, resultIndex: 1 }
]);
});
});
});
+20 -4
View File
@@ -75,10 +75,28 @@ declare module 'xterm-addon-search' {
activeMatchColorOverviewRuler: string;
}
/**
* Options for the search addon.
*/
export interface ISearchAddonOptions {
/**
* Max number of matches highlighted when decorations are enabled.
* Defaults to 1000 highlighted matches
*/
highlightLimit: number
}
/**
* An xterm.js addon that provides search functionality.
*/
export class SearchAddon implements ITerminalAddon {
/**
* Creates a new search addon.
* @param options Options for the search addon.
*/
constructor(options?: Partial<ISearchAddonOptions>);
/**
* Activates the addon
* @param terminal The terminal the addon is being loaded in.
@@ -121,10 +139,8 @@ declare module 'xterm-addon-search' {
/**
* When decorations are enabled, fires when
* the search results change.
* @returns -1 for resultIndex for a resultCount of 0
* and @returns undefined when the threshold of 1k results
* is exceeded and decorations are disposed of.
* @returns -1 for resultIndex when the threshold of matches is exceeded.
*/
readonly onDidChangeResults: IEvent<{ resultIndex: number, resultCount: number } | undefined>;
readonly onDidChangeResults: IEvent<{ resultIndex: number, resultCount: number }>;
}
}
@@ -77,6 +77,7 @@ function equalFlags(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): bo
return cell1.isInverse() === cell2.isInverse()
&& cell1.isBold() === cell2.isBold()
&& cell1.isUnderline() === cell2.isUnderline()
&& cell1.isOverline() === cell2.isOverline()
&& cell1.isBlink() === cell2.isBlink()
&& cell1.isInvisible() === cell2.isInvisible()
&& cell1.isItalic() === cell2.isItalic()
@@ -264,6 +265,7 @@ class StringSerializeHandler extends BaseSerializeHandler {
if (cell.isInverse() !== oldCell.isInverse()) { sgrSeq.push(cell.isInverse() ? 7 : 27); }
if (cell.isBold() !== oldCell.isBold()) { sgrSeq.push(cell.isBold() ? 1 : 22); }
if (cell.isUnderline() !== oldCell.isUnderline()) { sgrSeq.push(cell.isUnderline() ? 4 : 24); }
if (cell.isOverline() !== oldCell.isOverline()) { sgrSeq.push(cell.isOverline() ? 53 : 55); }
if (cell.isBlink() !== oldCell.isBlink()) { sgrSeq.push(cell.isBlink() ? 5 : 25); }
if (cell.isInvisible() !== oldCell.isInvisible()) { sgrSeq.push(cell.isInvisible() ? 8 : 28); }
if (cell.isItalic() !== oldCell.isItalic()) { sgrSeq.push(cell.isItalic() ? 3 : 23); }
@@ -625,7 +627,9 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
if (cell.isInverse()) { content.push('color: #000000; background-color: #BFBFBF;'); }
if (cell.isBold()) { content.push('font-weight: bold;'); }
if (cell.isUnderline()) { content.push('text-decoration: underline;'); }
if (cell.isUnderline() && cell.isOverline()) { content.push('text-decoration: overline underline;'); }
else if (cell.isUnderline()) { content.push('text-decoration: underline;'); }
else if (cell.isOverline()) { content.push('text-decoration: overline;'); }
if (cell.isBlink()) { content.push('text-decoration: blink;'); }
if (cell.isInvisible()) { content.push('visibility: hidden;'); }
if (cell.isItalic()) { content.push('font-style: italic;'); }
@@ -220,6 +220,18 @@ describe('SerializeAddon', () => {
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
});
it('serialize all rows of content with overline', async () => {
const cols = 10;
const line = '+'.repeat(cols);
const lines: string[] = [
sgr(OVERLINED) + line, // Overlined
sgr(UNDERLINED) + line, // Overlined, Underlined
sgr(NORMAL) + line // Normal
];
await writeSync(page, lines.join('\\r\\n'));
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
});
it('serialize all rows of content with color16 and style separately', async function(): Promise<any> {
const cols = 10;
const line = '+'.repeat(cols);
@@ -601,6 +613,7 @@ const BLINK = '5';
const INVERSE = '7';
const INVISIBLE = '8';
const STRIKETHROUGH = '9';
const OVERLINED = '53';
const NO_BOLD = '22';
const NO_DIM = '22';
@@ -610,3 +623,4 @@ const NO_BLINK = '25';
const NO_INVERSE = '27';
const NO_INVISIBLE = '28';
const NO_STRIKETHROUGH = '29';
const NO_OVERLINED = '55';
@@ -270,7 +270,7 @@ export class RectangleRenderer extends Disposable {
$r = (($rgba >> 24) & 0xFF) / 255;
$g = (($rgba >> 16) & 0xFF) / 255;
$b = (($rgba >> 8 ) & 0xFF) / 255;
$a = (!$isDefault && bg & BgFlags.DIM) ? DIM_OPACITY : 1;
$a = 1;
this._addRectangle(vertices.attributes, offset, $x1, $y1, (endX - startX) * this._dimensions.device.cell.width, this._dimensions.device.cell.height, $r, $g, $b, $a);
}
@@ -336,13 +336,16 @@ export class WebglRenderer extends Disposable implements IRenderer {
}
// Tell renderer the frame is beginning
// upon a model clear also refresh the full viewport model
// (also triggered by an atlas page merge, part of #4480)
if (this._glyphRenderer.beginFrame()) {
this._clearModel(true);
this._updateModel(0, this._terminal.rows - 1);
} else {
// just update changed lines to draw
this._updateModel(start, end);
}
// Update model to reflect what's drawn
this._updateModel(start, end);
// Render
this._rectangleRenderer?.render();
this._glyphRenderer?.render(this._model);
@@ -12,6 +12,7 @@ import { IEventEmitter } from 'common/EventEmitter';
import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
import { ICoreService, IOptionsService } from 'common/services/Services';
import { toDisposable } from 'common/Lifecycle';
import { isFirefox } from 'common/Platform';
interface ICursorState {
x: number;
@@ -190,8 +191,9 @@ export class CursorRenderLayer extends BaseRenderLayer {
private _clearCursor(): void {
if (this._state) {
// Avoid potential rounding errors when device pixel ratio is less than 1
if (this._coreBrowserService.dpr < 1) {
// Avoid potential rounding errors when browser is Firefox (#4487) or device pixel ratio is
// less than 1
if (isFirefox || this._coreBrowserService.dpr < 1) {
this._clearAll();
} else {
this._clearCells(this._state.x, this._state.y, this._state.width, 1);
@@ -364,6 +364,55 @@ describe('WebGL Renderer Integration Tests', async () => {
}
});
itWebgl('foreground 16-255 dim', async () => {
let data = '';
for (let y = 0; y < 240 / 16; y++) {
for (let x = 0; x < 16; x++) {
data += `\\x1b[2;38;5;${16 + y * 16 + x}m█\x1b[0m`;
}
data += '\\r\\n';
}
await writeSync(page, data);
for (let y = 0; y < 240 / 16; y++) {
for (let x = 0; x < 16; x++) {
const cssColor = COLORS_16_TO_255[y * 16 + x];
const r = parseInt(cssColor.slice(1, 3), 16);
const g = parseInt(cssColor.slice(3, 5), 16);
const b = parseInt(cssColor.slice(5, 7), 16);
// It's difficult to assert the exact color due to rounding, just ensure the color differs
// to the regular color
await pollFor(page, async () => {
const c = await getCellColor(x + 1, y + 1);
return (
(c[0] === 0 || c[0] !== r) &&
(c[1] === 0 || c[1] !== g) &&
(c[2] === 0 || c[2] !== b)
);
}, true);
}
}
});
itWebgl('background 16-255 dim', async () => {
let data = '';
for (let y = 0; y < 240 / 16; y++) {
for (let x = 0; x < 16; x++) {
data += `\\x1b[2;48;5;${16 + y * 16 + x}m \\x1b[0m`;
}
data += '\\r\\n';
}
await writeSync(page, data);
for (let y = 0; y < 240 / 16; y++) {
for (let x = 0; x < 16; x++) {
const cssColor = COLORS_16_TO_255[y * 16 + x];
const r = parseInt(cssColor.slice(1, 3), 16);
const g = parseInt(cssColor.slice(3, 5), 16);
const b = parseInt(cssColor.slice(5, 7), 16);
await pollFor(page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]);
}
}
});
itWebgl('foreground true color red', async () => {
let data = '';
for (let y = 0; y < 16; y++) {
+10 -10
View File
@@ -1,10 +1,10 @@
pr:
branches:
include: ["main", "v5"]
include: ["main"]
trigger:
branches:
include: ["main", "v5"]
include: ["main"]
jobs:
- job: Linux
@@ -13,7 +13,7 @@ jobs:
steps:
- task: NodeTool@0
inputs:
versionSpec: '14.x'
versionSpec: '18.x'
displayName: 'Install Node.js'
- task: YarnInstaller@3
inputs:
@@ -46,7 +46,7 @@ jobs:
steps:
- task: NodeTool@0
inputs:
versionSpec: '14.x'
versionSpec: '18.x'
displayName: 'Install Node.js'
- task: CacheBeta@1
inputs:
@@ -66,7 +66,7 @@ jobs:
steps:
- task: NodeTool@0
inputs:
versionSpec: '14.x'
versionSpec: '18.x'
displayName: 'Install Node.js'
- task: CacheBeta@1
inputs:
@@ -95,7 +95,7 @@ jobs:
displayName: Install required packages
- task: NodeTool@0
inputs:
versionSpec: '14.x'
versionSpec: '18.x'
displayName: 'Install Node.js'
- task: YarnInstaller@3
inputs:
@@ -111,11 +111,11 @@ jobs:
# Integration tests are too flaky on macOS https://github.com/xtermjs/xterm.js/issues/3590
# - job: macOS_IntegrationTests
# pool:
# vmImage: 'macOS-10.15'
# vmImage: 'macOS-11'
# steps:
# - task: NodeTool@0
# inputs:
# versionSpec: '14.x'
# versionSpec: '18.x'
# displayName: 'Install Node.js'
# - script: yarn --frozen-lockfile
# displayName: 'Install dependencies and build'
@@ -132,7 +132,7 @@ jobs:
steps:
- task: NodeTool@0
inputs:
versionSpec: '14.x'
versionSpec: '18.x'
displayName: 'Install Node.js'
- script: yarn --frozen-lockfile
displayName: 'Install dependencies and build'
@@ -155,7 +155,7 @@ jobs:
steps:
- task: NodeTool@0
inputs:
versionSpec: '14.x'
versionSpec: '18.x'
displayName: 'Install Node.js'
- task: YarnInstaller@3
inputs:
+19 -2
View File
@@ -149,6 +149,7 @@
right: 0;
z-index: 10;
color: transparent;
pointer-events: none;
}
.xterm .live-region {
@@ -160,7 +161,9 @@
}
.xterm-dim {
opacity: 0.5;
/* Dim should not apply to background, so the opacity of the foreground color is applied
* explicitly in the generated class and reset to 1 here */
opacity: 1 !important;
}
.xterm-underline-1 { text-decoration: underline; }
@@ -169,6 +172,16 @@
.xterm-underline-4 { text-decoration: dotted underline; }
.xterm-underline-5 { text-decoration: dashed underline; }
.xterm-overline {
text-decoration: overline;
}
.xterm-overline.xterm-underline-1 { text-decoration: overline underline; }
.xterm-overline.xterm-underline-2 { text-decoration: overline double underline; }
.xterm-overline.xterm-underline-3 { text-decoration: overline wavy underline; }
.xterm-overline.xterm-underline-4 { text-decoration: overline dotted underline; }
.xterm-overline.xterm-underline-5 { text-decoration: overline dashed underline; }
.xterm-strikethrough {
text-decoration: line-through;
}
@@ -178,8 +191,12 @@
position: absolute;
}
.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer {
z-index: 7;
}
.xterm-decoration-overview-ruler {
z-index: 7;
z-index: 8;
position: absolute;
top: 0;
right: 0;
+22 -8
View File
@@ -132,12 +132,11 @@ function setPadding(): void {
addons.fit.instance.fit();
}
function getSearchOptions(e: KeyboardEvent): ISearchOptions {
function getSearchOptions(): ISearchOptions {
return {
regex: (document.getElementById('regex') as HTMLInputElement).checked,
wholeWord: (document.getElementById('whole-word') as HTMLInputElement).checked,
caseSensitive: (document.getElementById('case-sensitive') as HTMLInputElement).checked,
incremental: e.key !== `Enter`,
decorations: (document.getElementById('highlight-all-matches') as HTMLInputElement).checked ? {
matchBackground: '#232422',
matchBorder: '#555753',
@@ -303,11 +302,23 @@ function createTerminal(): void {
addDomListener(paddingElement, 'change', setPadding);
addDomListener(actionElements.findNext, 'keyup', (e) => {
addons.search.instance.findNext(actionElements.findNext.value, getSearchOptions(e));
addDomListener(actionElements.findNext, 'keydown', (e) => {
if (e.key === 'Enter') {
addons.search.instance.findNext(actionElements.findNext.value, getSearchOptions());
e.preventDefault();
}
});
addDomListener(actionElements.findPrevious, 'keyup', (e) => {
addons.search.instance.findPrevious(actionElements.findPrevious.value, getSearchOptions(e));
addDomListener(actionElements.findNext, 'input', (e) => {
addons.search.instance.findNext(actionElements.findNext.value, getSearchOptions());
});
addDomListener(actionElements.findPrevious, 'keydown', (e) => {
if (e.key === 'Enter') {
addons.search.instance.findPrevious(actionElements.findPrevious.value, getSearchOptions());
e.preventDefault();
}
});
addDomListener(actionElements.findPrevious, 'input', (e) => {
addons.search.instance.findPrevious(actionElements.findPrevious.value, getSearchOptions());
});
addDomListener(actionElements.findNext, 'blur', (e) => {
addons.search.instance.clearActiveDecoration();
@@ -965,7 +976,9 @@ function sgrTest(): void {
{ ps: 45, name: 'Background Magenta' },
{ ps: 46, name: 'Background Cyan' },
{ ps: 47, name: 'Background White' },
{ ps: 49, name: 'Background default' }
{ ps: 49, name: 'Background default' },
{ ps: 53, name: 'Overlined' },
{ ps: 55, name: 'Not overlined' }
];
const maxNameLength = entries.reduce<number>((p, c) => Math.max(c.name.length, p), 0);
for (const e of entries) {
@@ -977,7 +990,8 @@ function sgrTest(): void {
}
const comboEntries: { ps: number[] }[] = [
{ ps: [1, 2, 3, 4, 5, 6, 7, 9] },
{ ps: [2, 41] }
{ ps: [2, 41] },
{ ps: [4, 53] }
];
term.write('\n\n\r');
term.writeln(`Combinations`);
+12 -15
View File
@@ -111,35 +111,32 @@ function startServer() {
}
// binary message buffering
function bufferUtf8(socket, timeout, maxSize) {
const dataBuffer = new Uint8Array(maxSize);
let sender = null;
const chunks = [];
let length = 0;
let sender = null;
return (data) => {
function flush() {
socket.send(Buffer.from(dataBuffer.buffer, 0, length));
chunks.push(data);
length += data.length;
if (length > maxSize || userInput) {
userInput = false;
socket.send(Buffer.concat(chunks));
chunks.length = 0;
length = 0;
if (sender) {
clearTimeout(sender);
sender = null;
}
}
if (length + data.length > maxSize) {
flush();
}
dataBuffer.set(data, length);
length += data.length;
if (length > maxSize || userInput) {
userInput = false;
flush();
} else if (!sender) {
sender = setTimeout(() => {
socket.send(Buffer.concat(chunks));
chunks.length = 0;
length = 0;
sender = null;
flush();
}, timeout);
}
};
}
const send = (USE_BINARY ? bufferUtf8 : buffer)(ws, 5, 262144);
const send = (USE_BINARY ? bufferUtf8 : buffer)(ws, 3, 262144);
// WARNING: This is a naive implementation that will not throttle the flow of data. This means
// it could flood the communication channel and make the terminal unresponsive. Learn more about
+1 -1
View File
@@ -58,7 +58,7 @@
"@types/glob": "^7.2.0",
"@types/jsdom": "^16.2.13",
"@types/mocha": "^9.0.0",
"@types/node": "^14.14.44",
"@types/node": "^18.16.0",
"@types/utf8": "^3.0.0",
"@types/webpack": "^5.28.0",
"@types/ws": "^8.2.0",
+153 -2
View File
@@ -8,16 +8,32 @@ import { ITerminal, IRenderDebouncer } from 'browser/Types';
import { isMac } from 'common/Platform';
import { TimeBasedDebouncer } from 'browser/TimeBasedDebouncer';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { ScreenDprMonitor } from 'browser/ScreenDprMonitor';
import { IRenderService } from 'browser/services/Services';
import { addDisposableDomListener } from 'browser/Lifecycle';
const MAX_ROWS_TO_READ = 20;
const enum BoundaryPosition {
TOP,
BOTTOM
}
export class AccessibilityManager extends Disposable {
private _accessibilityContainer: HTMLElement;
private _rowContainer: HTMLElement;
private _rowElements: HTMLElement[];
private _liveRegion: HTMLElement;
private _liveRegionLineCount: number = 0;
private _liveRegionDebouncer: IRenderDebouncer;
private _screenDprMonitor: ScreenDprMonitor;
private _topBoundaryFocusListener: (e: FocusEvent) => void;
private _bottomBoundaryFocusListener: (e: FocusEvent) => void;
/**
* This queue has a character pushed to it for keys that are pressed, if the
* next character added to the terminal is equal to the key char then it is
@@ -32,12 +48,30 @@ export class AccessibilityManager extends Disposable {
private _charsToAnnounce: string = '';
constructor(
private readonly _terminal: ITerminal
private readonly _terminal: ITerminal,
@IRenderService private readonly _renderService: IRenderService
) {
super();
this._accessibilityContainer = document.createElement('div');
this._accessibilityContainer.classList.add('xterm-accessibility');
this._rowContainer = document.createElement('div');
this._rowContainer.setAttribute('role', 'list');
this._rowContainer.classList.add('xterm-accessibility-tree');
this._rowElements = [];
for (let i = 0; i < this._terminal.rows; i++) {
this._rowElements[i] = this._createAccessibilityTreeNode();
this._rowContainer.appendChild(this._rowElements[i]);
}
this._topBoundaryFocusListener = e => this._handleBoundaryFocus(e, BoundaryPosition.TOP);
this._bottomBoundaryFocusListener = e => this._handleBoundaryFocus(e, BoundaryPosition.BOTTOM);
this._rowElements[0].addEventListener('focus', this._topBoundaryFocusListener);
this._rowElements[this._rowElements.length - 1].addEventListener('focus', this._bottomBoundaryFocusListener);
this._refreshRowsDimensions();
this._accessibilityContainer.appendChild(this._rowContainer);
this._liveRegion = document.createElement('div');
this._liveRegion.classList.add('live-region');
this._liveRegion.setAttribute('aria-live', 'assertive');
@@ -50,6 +84,7 @@ export class AccessibilityManager extends Disposable {
this._terminal.element.insertAdjacentElement('afterbegin', this._accessibilityContainer);
this.register(this._liveRegionDebouncer);
this.register(this._terminal.onResize(e => this._handleResize(e.rows)));
this.register(this._terminal.onRender(e => this._refreshRows(e.start, e.end)));
this.register(this._terminal.onScroll(() => this._refreshRows()));
// Line feed is an issue as the prompt won't be read out after a command is run
@@ -58,7 +93,20 @@ export class AccessibilityManager extends Disposable {
this.register(this._terminal.onA11yTab(spaceCount => this._handleTab(spaceCount)));
this.register(this._terminal.onKey(e => this._handleKey(e.key)));
this.register(this._terminal.onBlur(() => this._clearLiveRegion()));
this.register(toDisposable(() => this._accessibilityContainer.remove()));
this.register(this._renderService.onDimensionsChange(() => this._refreshRowsDimensions()));
this._screenDprMonitor = new ScreenDprMonitor(window);
this.register(this._screenDprMonitor);
this._screenDprMonitor.setListener(() => this._refreshRowsDimensions());
// This shouldn't be needed on modern browsers but is present in case the
// media query that drives the ScreenDprMonitor isn't supported
this.register(addDisposableDomListener(window, 'resize', () => this._refreshRowsDimensions()));
this._refreshRows();
this.register(toDisposable(() => {
this._accessibilityContainer.remove();
this._rowElements.length = 0;
}));
}
private _handleTab(spaceCount: number): void {
@@ -126,4 +174,107 @@ export class AccessibilityManager extends Disposable {
this._liveRegion.textContent += this._charsToAnnounce;
this._charsToAnnounce = '';
}
private _handleBoundaryFocus(e: FocusEvent, position: BoundaryPosition): void {
const boundaryElement = e.target as HTMLElement;
const beforeBoundaryElement = this._rowElements[position === BoundaryPosition.TOP ? 1 : this._rowElements.length - 2];
// Don't scroll if the buffer top has reached the end in that direction
const posInSet = boundaryElement.getAttribute('aria-posinset');
const lastRowPos = position === BoundaryPosition.TOP ? '1' : `${this._terminal.buffer.lines.length}`;
if (posInSet === lastRowPos) {
return;
}
// Don't scroll when the last focused item was not the second row (focus is going the other
// direction)
if (e.relatedTarget !== beforeBoundaryElement) {
return;
}
// Remove old boundary element from array
let topBoundaryElement: HTMLElement;
let bottomBoundaryElement: HTMLElement;
if (position === BoundaryPosition.TOP) {
topBoundaryElement = boundaryElement;
bottomBoundaryElement = this._rowElements.pop()!;
this._rowContainer.removeChild(bottomBoundaryElement);
} else {
topBoundaryElement = this._rowElements.shift()!;
bottomBoundaryElement = boundaryElement;
this._rowContainer.removeChild(topBoundaryElement);
}
// Remove listeners from old boundary elements
topBoundaryElement.removeEventListener('focus', this._topBoundaryFocusListener);
bottomBoundaryElement.removeEventListener('focus', this._bottomBoundaryFocusListener);
// Add new element to array/DOM
if (position === BoundaryPosition.TOP) {
const newElement = this._createAccessibilityTreeNode();
this._rowElements.unshift(newElement);
this._rowContainer.insertAdjacentElement('afterbegin', newElement);
} else {
const newElement = this._createAccessibilityTreeNode();
this._rowElements.push(newElement);
this._rowContainer.appendChild(newElement);
}
// Add listeners to new boundary elements
this._rowElements[0].addEventListener('focus', this._topBoundaryFocusListener);
this._rowElements[this._rowElements.length - 1].addEventListener('focus', this._bottomBoundaryFocusListener);
// Scroll up
this._terminal.scrollLines(position === BoundaryPosition.TOP ? -1 : 1);
// Focus new boundary before element
this._rowElements[position === BoundaryPosition.TOP ? 1 : this._rowElements.length - 2].focus();
// Prevent the standard behavior
e.preventDefault();
e.stopImmediatePropagation();
}
private _handleResize(rows: number): void {
// Remove bottom boundary listener
this._rowElements[this._rowElements.length - 1].removeEventListener('focus', this._bottomBoundaryFocusListener);
// Grow rows as required
for (let i = this._rowContainer.children.length; i < this._terminal.rows; i++) {
this._rowElements[i] = this._createAccessibilityTreeNode();
this._rowContainer.appendChild(this._rowElements[i]);
}
// Shrink rows as required
while (this._rowElements.length > rows) {
this._rowContainer.removeChild(this._rowElements.pop()!);
}
// Add bottom boundary listener
this._rowElements[this._rowElements.length - 1].addEventListener('focus', this._bottomBoundaryFocusListener);
this._refreshRowsDimensions();
}
private _createAccessibilityTreeNode(): HTMLElement {
const element = document.createElement('div');
element.setAttribute('role', 'listitem');
element.tabIndex = -1;
this._refreshRowDimensions(element);
return element;
}
private _refreshRowsDimensions(): void {
if (!this._renderService.dimensions.css.cell.height) {
return;
}
this._accessibilityContainer.style.width = `${this._renderService.dimensions.css.canvas.width}px`;
if (this._rowElements.length !== this._terminal.rows) {
this._handleResize(this._terminal.rows);
}
for (let i = 0; i < this._terminal.rows; i++) {
this._refreshRowDimensions(this._rowElements[i]);
}
}
private _refreshRowDimensions(element: HTMLElement): void {
element.style.height = `${this._renderService.dimensions.css.cell.height}px`;
}
}
@@ -72,6 +72,7 @@ export class BufferDecorationRenderer extends Disposable {
private _createElement(decoration: IInternalDecoration): HTMLElement {
const element = document.createElement('div');
element.classList.add('xterm-decoration');
element.classList.toggle('xterm-decoration-top-layer', decoration?.options?.layer === 'top');
element.style.width = `${Math.round((decoration.options.width || 1) * this._renderService.dimensions.css.cell.width)}px`;
element.style.height = `${(decoration.options.height || 1) * this._renderService.dimensions.css.cell.height}px`;
element.style.top = `${(decoration.marker.line - this._bufferService.buffers.active.ydisp) * this._renderService.dimensions.css.cell.height}px`;
+43 -6
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { BOLD_CLASS, CURSOR_BLINK_CLASS, CURSOR_CLASS, CURSOR_STYLE_BAR_CLASS, CURSOR_STYLE_BLOCK_CLASS, CURSOR_STYLE_UNDERLINE_CLASS, DomRendererRowFactory, ITALIC_CLASS } from 'browser/renderer/dom/DomRendererRowFactory';
import { BOLD_CLASS, CURSOR_BLINK_CLASS, CURSOR_CLASS, CURSOR_STYLE_BAR_CLASS, CURSOR_STYLE_BLOCK_CLASS, CURSOR_STYLE_UNDERLINE_CLASS, DIM_CLASS, DomRendererRowFactory, ITALIC_CLASS } from 'browser/renderer/dom/DomRendererRowFactory';
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/shared/Constants';
import { createRenderDimensions } from 'browser/renderer/shared/RendererUtils';
import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/shared/Types';
@@ -53,7 +53,7 @@ export class DomRenderer extends Disposable implements IRenderer {
@IOptionsService private readonly _optionsService: IOptionsService,
@IBufferService private readonly _bufferService: IBufferService,
@ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService,
@IThemeService themeService: IThemeService
@IThemeService private readonly _themeService: IThemeService
) {
super();
this._rowContainer = document.createElement('div');
@@ -69,8 +69,8 @@ export class DomRenderer extends Disposable implements IRenderer {
this._updateDimensions();
this.register(this._optionsService.onOptionChange(() => this._handleOptionsChanged()));
this.register(themeService.onChangeColors(e => this._injectCss(e)));
this._injectCss(themeService.colors);
this.register(this._themeService.onChangeColors(e => this._injectCss(e)));
this._injectCss(this._themeService.colors);
this._rowFactory = instantiationService.createInstance(DomRendererRowFactory, document);
@@ -149,6 +149,10 @@ export class DomRenderer extends Disposable implements IRenderer {
` font-family: ${this._optionsService.rawOptions.fontFamily};` +
` font-size: ${this._optionsService.rawOptions.fontSize}px;` +
`}`;
styles +=
`${this._terminalSelector} .${ROW_CONTAINER_CLASS} .xterm-dim {` +
` color: ${color.multiplyOpacity(colors.foreground, 0.5).css};` +
`}`;
// Text styles
styles +=
`${this._terminalSelector} span:not(.${BOLD_CLASS}) {` +
@@ -224,10 +228,12 @@ export class DomRenderer extends Disposable implements IRenderer {
for (const [i, c] of colors.ansi.entries()) {
styles +=
`${this._terminalSelector} .${FG_CLASS_PREFIX}${i} { color: ${c.css}; }` +
`${this._terminalSelector} .${FG_CLASS_PREFIX}${i}.${DIM_CLASS} { color: ${color.multiplyOpacity(c, 0.5).css}; }` +
`${this._terminalSelector} .${BG_CLASS_PREFIX}${i} { background-color: ${c.css}; }`;
}
styles +=
`${this._terminalSelector} .${FG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { color: ${color.opaque(colors.background).css}; }` +
`${this._terminalSelector} .${FG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR}.${DIM_CLASS} { color: ${color.multiplyOpacity(color.opaque(colors.background), 0.5).css}; }` +
`${this._terminalSelector} .${BG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { background-color: ${colors.foreground.css}; }`;
this._themeStyleElement.textContent = styles;
@@ -340,6 +346,8 @@ export class DomRenderer extends Disposable implements IRenderer {
private _handleOptionsChanged(): void {
// Force a refresh
this._updateDimensions();
// Refresh CSS
this._injectCss(this._themeService.colors);
}
public clear(): void {
@@ -385,8 +393,37 @@ export class DomRenderer extends Disposable implements IRenderer {
}
private _setCellUnderline(x: number, x2: number, y: number, y2: number, cols: number, enabled: boolean): void {
x = this._cellToRowElements[y][x];
x2 = this._cellToRowElements[y2][x2];
/**
* NOTE: The linkifier may send out of viewport y-values if:
* - negative y-value: the link started at a higher line
* - y-value >= maxY: the link ends at a line below viewport
*
* For negative y-values we can simply adjust x = 0,
* as higher up link start means, that everything from
* (0,0) is a link under top-down-left-right char progression
*
* Additionally there might be a small chance of out-of-sync x|y-values
* from a race condition of render updates vs. link event handler execution:
* - (sync) resize: chances terminal buffer in sync, schedules render update async
* - (async) link handler race condition: new buffer metrics, but still on old render state
* - (async) render update: brings term metrics and render state back in sync
*/
if (y < 0) x = 0;
if (y2 < 0) x2 = 0;
// avoid out-of-sync y-values, simply clamp into valid area
const maxY = this._cellToRowElements.length - 1;
y = Math.max(Math.min(y, maxY), 0);
y2 = Math.max(Math.min(y2, maxY), 0);
const elemY = this._cellToRowElements[y];
const elemY2 = this._cellToRowElements[y2];
if (x >= elemY.length || x2 >= elemY2.length) {
// avoid out-of-sync x-values
// simply exit early, gets fixed by the next render update
return;
}
x = elemY[x];
x2 = elemY2[x2];
if (x === -1 || x2 === -1) {
return;
@@ -167,6 +167,16 @@ describe('DomRendererRowFactory', () => {
});
});
it('should add class for overline', () => {
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.OVERLINE;
lineData.setCell(0, cell);
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
assert.equal(getFragmentHtml(fragment),
'<span class="xterm-overline">a</span>'
);
});
it('should add class for strikethrough', () => {
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
cell.fg = DEFAULT_ATTR_DATA.fg | FgFlags.STRIKETHROUGH;
@@ -9,7 +9,6 @@ import { NULL_CELL_CODE, WHITESPACE_CELL_CHAR, Attributes } from 'common/buffer/
import { CellData } from 'common/buffer/CellData';
import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
import { color, rgba } from 'common/Color';
import { IColorSet, ReadonlyColorSet } from 'browser/Types';
import { ICharacterJoinerService, ICoreBrowserService, IThemeService } from 'browser/services/Services';
import { JoinedCellData } from 'browser/services/CharacterJoinerService';
import { excludeFromContrastRatioDemands } from 'browser/renderer/shared/RendererUtils';
@@ -19,6 +18,7 @@ export const BOLD_CLASS = 'xterm-bold';
export const DIM_CLASS = 'xterm-dim';
export const ITALIC_CLASS = 'xterm-italic';
export const UNDERLINE_CLASS = 'xterm-underline';
export const OVERLINE_CLASS = 'xterm-overline';
export const STRIKETHROUGH_CLASS = 'xterm-strikethrough';
export const CURSOR_CLASS = 'xterm-cursor';
export const CURSOR_BLINK_CLASS = 'xterm-cursor-blink';
@@ -186,6 +186,13 @@ export class DomRendererRowFactory {
}
}
if (cell.isOverline()) {
charElement.classList.add(OVERLINE_CLASS);
if (charElement.textContent === ' ') {
charElement.textContent = '\xa0'; // = &nbsp;
}
}
if (cell.isStrikethrough()) {
charElement.classList.add(STRIKETHROUGH_CLASS);
}
+16 -9
View File
@@ -304,12 +304,6 @@ export class TextureAtlas implements ITextureAtlas {
break;
}
if (dim) {
// Blend here instead of using opacity because transparent colors mess with clipping the
// glyph's bounding box
result = color.blend(this._config.colors.background, color.multiplyOpacity(result, DIM_OPACITY));
}
return result;
}
@@ -429,12 +423,12 @@ export class TextureAtlas implements ITextureAtlas {
// 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.deviceCellWidth * Math.max(chars.length, 2) + TMP_CANVAS_GLYPH_PADDING * 2;
const allowedWidth = Math.min(this._config.deviceCellWidth * Math.max(chars.length, 2) + TMP_CANVAS_GLYPH_PADDING * 2, this._textureSize);
if (this._tmpCanvas.width < allowedWidth) {
this._tmpCanvas.width = allowedWidth;
}
// Include line height when drawing glyphs
const allowedHeight = this._config.deviceCellHeight + TMP_CANVAS_GLYPH_PADDING * 4;
const allowedHeight = Math.min(this._config.deviceCellHeight + TMP_CANVAS_GLYPH_PADDING * 4, this._textureSize);
if (this._tmpCanvas.height < allowedHeight) {
this._tmpCanvas.height = allowedHeight;
}
@@ -455,6 +449,7 @@ export class TextureAtlas implements ITextureAtlas {
const italic = !!this._workAttributeData.isItalic();
const underline = !!this._workAttributeData.isUnderline();
const strikethrough = !!this._workAttributeData.isStrikethrough();
const overline = !!this._workAttributeData.isOverline();
let fgColor = this._workAttributeData.getFgColor();
let fgColorMode = this._workAttributeData.getFgColorMode();
let bgColor = this._workAttributeData.getBgColor();
@@ -638,12 +633,24 @@ export class TextureAtlas implements ITextureAtlas {
}
}
// Overline
if (overline) {
const lineWidth = Math.max(1, Math.floor(this._config.fontSize * this._config.devicePixelRatio / 15));
const yOffset = lineWidth % 2 === 1 ? 0.5 : 0;
this._tmpCtx.lineWidth = lineWidth;
this._tmpCtx.strokeStyle = this._tmpCtx.fillStyle;
this._tmpCtx.beginPath();
this._tmpCtx.moveTo(padding, padding + yOffset);
this._tmpCtx.lineTo(padding + this._config.deviceCharWidth * chWidth, padding + yOffset);
this._tmpCtx.stroke();
}
// Draw the character
if (!customGlyph) {
this._tmpCtx.fillText(chars, padding, padding + this._config.deviceCharHeight);
}
// If this charcater is underscore and beyond the cell bounds, shift it up until it is visible
// If this character is underscore and beyond the cell bounds, shift it up until it is visible
// even on the bottom row, try for a maximum of 5 pixels.
if (chars === '_' && !this._config.allowTransparency) {
let isBeyondCellBounds = clearColor(this._tmpCtx.getImageData(padding, padding, this._config.deviceCellWidth, this._config.deviceCellHeight), backgroundColor, foregroundColor, enableClearThresholdCheck);
+3 -3
View File
@@ -2222,7 +2222,7 @@ describe('InputHandler', () => {
});
it('ANSI 2 (keyboard action mode)', async () => {
await inputHandler.parseP('\x1b[2$p');
assert.deepEqual(reportStack.pop(), '\x1b[2;3$y'); // always set
assert.deepEqual(reportStack.pop(), '\x1b[2;4$y'); // always reset
});
it('ANSI 4 (insert mode)', async () => {
await inputHandler.parseP('\x1b[4$p');
@@ -2236,7 +2236,7 @@ describe('InputHandler', () => {
});
it('ANSI 12 (send/receive)', async () => {
await inputHandler.parseP('\x1b[12$p');
assert.deepEqual(reportStack.pop(), '\x1b[12;4$y'); // always reset
assert.deepEqual(reportStack.pop(), '\x1b[12;3$y'); // always set
});
it('ANSI 20 (newline mode)', async () => {
await inputHandler.parseP('\x1b[20$p');
@@ -2280,7 +2280,7 @@ describe('InputHandler', () => {
});
it('DEC privates perma modes', async () => {
// [mode number, state value]
const perma = [[3, 0], [8, 3], [1005, 4], [1015, 4], [1048, 1]];
const perma = [[3, 0], [8, 3], [67, 4], [1005, 4], [1015, 4], [1048, 1]];
for (const [mode, value] of perma) {
await inputHandler.parseP(`\x1b[?${mode}$p`);
assert.deepEqual(reportStack.pop(), `\x1b[?${mode};${value}$y`);
+11 -2
View File
@@ -2223,9 +2223,9 @@ export class InputHandler extends Disposable implements IInputHandler {
const p = params.params[0];
if (ansi) {
if (p === 2) return f(p, V.PERMANENTLY_SET);
if (p === 2) return f(p, V.PERMANENTLY_RESET);
if (p === 4) return f(p, b2v(cs.modes.insertMode));
if (p === 12) return f(p, V.PERMANENTLY_RESET);
if (p === 12) return f(p, V.PERMANENTLY_SET);
if (p === 20) return f(p, b2v(opts.convertEol));
return f(p, V.NOT_RECOGNIZED);
}
@@ -2240,6 +2240,7 @@ export class InputHandler extends Disposable implements IInputHandler {
if (p === 25) return f(p, b2v(!cs.isCursorHidden));
if (p === 45) return f(p, b2v(dm.reverseWraparound));
if (p === 66) return f(p, b2v(dm.applicationKeypad));
if (p === 67) return f(p, V.PERMANENTLY_RESET);
if (p === 1000) return f(p, b2v(mouseProtocol === 'VT200'));
if (p === 1002) return f(p, b2v(mouseProtocol === 'DRAG'));
if (p === 1003) return f(p, b2v(mouseProtocol === 'ANY'));
@@ -2425,6 +2426,8 @@ export class InputHandler extends Disposable implements IInputHandler {
* | 47 | Background color: White. | #Y |
* | 48 | Background color: Extended color. | #P[Support for RGB and indexed colors, see below.] |
* | 49 | Background color: Default (original). | #Y |
* | 53 | Overlined. | #Y |
* | 55 | Not Overlined. | #Y |
* | 58 | Underline color: Extended color. | #P[Support for RGB and indexed colors, see below.] |
* | 90 - 97 | Bright foreground color (analogous to 30 - 37). | #Y |
* | 100 - 107 | Bright background color (analogous to 40 - 47). | #Y |
@@ -2551,6 +2554,12 @@ export class InputHandler extends Disposable implements IInputHandler {
} else if (p === 38 || p === 48 || p === 58) {
// fg color 256 and RGB
i += this._extractColor(params, i, attr);
} else if (p === 53) {
// overline
attr.bg |= BgFlags.OVERLINE;
} else if (p === 55) {
// not overline
attr.bg &= ~BgFlags.OVERLINE;
} else if (p === 59) {
attr.extended = attr.extended.clone();
attr.extended.underlineColor = -1;
+1
View File
@@ -165,6 +165,7 @@ export interface IAttributeData {
isDim(): number;
isStrikethrough(): number;
isProtected(): number;
isOverline(): number;
/**
* The color mode of the foreground color which determines how to decode {@link getFgColor},
+1
View File
@@ -47,6 +47,7 @@ export class AttributeData implements IAttributeData {
public isDim(): number { return this.bg & BgFlags.DIM; }
public isStrikethrough(): number { return this.fg & FgFlags.STRIKETHROUGH; }
public isProtected(): number { return this.bg & BgFlags.PROTECTED; }
public isOverline(): number { return this.bg & BgFlags.OVERLINE; }
// color modes
public getFgColorMode(): number { return this.fg & Attributes.CM_MASK; }
+2 -1
View File
@@ -128,7 +128,8 @@ export const enum BgFlags {
ITALIC = 0x4000000,
DIM = 0x8000000,
HAS_EXTENDED = 0x10000000,
PROTECTED = 0x20000000
PROTECTED = 0x20000000,
OVERLINE = 0x40000000
}
export const enum ExtFlags {
+1 -1
View File
@@ -22,7 +22,7 @@ describe('Headless API Tests', function (): void {
it('Proposed API check', async () => {
term = new Terminal({ allowProposedApi: false });
throws(() => term.markers, (error) => error.message === 'You must set the allowProposedApi option to true to use proposed API');
throws(() => term.markers, (error: any) => error.message === 'You must set the allowProposedApi option to true to use proposed API');
});
it('write', async () => {
+2
View File
@@ -999,6 +999,8 @@ declare module 'xterm-headless' {
isInvisible(): number;
/** Whether the cell has the strikethrough attribute (CSI 9 m). */
isStrikethrough(): number;
/** Whether the cell has the overline attribute (CSI 53 m). */
isOverline(): number;
/** Whether the cell is using the RGB foreground color mode. */
isFgRGB(): boolean;
+2
View File
@@ -1516,6 +1516,8 @@ declare module 'xterm' {
isInvisible(): number;
/** Whether the cell has the strikethrough attribute (CSI 9 m). */
isStrikethrough(): number;
/** Whether the cell has the overline attribute (CSI 53 m). */
isOverline(): number;
/** Whether the cell is using the RGB foreground color mode. */
isFgRGB(): boolean;
+4 -4
View File
@@ -425,10 +425,10 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.50.tgz#14ba5198f1754ffd0472a2f84ab433b45ee0b65e"
integrity sha512-+9axpWx2b2JCVovr7Ilgt96uc6C1zBKOQMpGtRbWT9IoR/8ue32GGMfGA4woP8QyP2gBs6GQWEVM3tCybGCxDA==
"@types/node@^14.14.44":
version "14.14.44"
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.44.tgz#df7503e6002847b834371c004b372529f3f85215"
integrity sha512-+gaugz6Oce6ZInfI/tK4Pq5wIIkJMEJUu92RB3Eu93mtj4wjjjz9EB5mLp5s1pSsLXdC/CPut/xF20ZzAQJbTA==
"@types/node@^18.16.0":
version "18.16.16"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.16.16.tgz#3b64862856c7874ccf7439e6bab872d245c86d8e"
integrity sha512-NpaM49IGQQAUlBhHMF82QH80J08os4ZmyF9MkpCzWAGuOHqE4gTEbhzd7L3l5LmWuZ6E0OiC1FweQ4tsiW35+g==
"@types/parse5@*":
version "6.0.2"