diff --git a/.eslintrc.json b/.eslintrc.json
index 9510fc6b..e6db42e2 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -7,6 +7,7 @@
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": [
+ "demo/tsconfig.json",
"src/browser/tsconfig.json",
"src/common/tsconfig.json",
"src/headless/tsconfig.json",
@@ -95,7 +96,19 @@
{ "selector": "enumMember", "format": ["UPPER_CASE"] },
// memberLike - Allow enum-like objects to use UPPER_CASE
{ "selector": "property", "modifiers": ["public"], "format": ["camelCase", "UPPER_CASE"] },
- { "selector": "method", "modifiers": ["public"], "format": ["camelCase", "UPPER_CASE"] },
+ // restrict on* naming for events only
+ { "selector": "method", "modifiers": ["public"], "format": ["camelCase", "UPPER_CASE"], "custom": {
+ "regex": "^on[A-Z].+",
+ "match": false
+ } },
+ { "selector": "method", "modifiers": ["private"], "format": ["camelCase"], "leadingUnderscore": "require", "custom": {
+ "regex": "^on[A-Z].+",
+ "match": false
+ } },
+ { "selector": "method", "modifiers": ["protected"], "format": ["camelCase"], "leadingUnderscore": "require", "custom": {
+ "regex": "^on[A-Z].+",
+ "match": false
+ } },
// typeLike
{ "selector": "typeLike", "format": ["PascalCase"] },
{ "selector": "interface", "format": ["PascalCase"], "prefix": ["I"] }
diff --git a/README.md b/README.md
index 5c4d2084..c78ae76e 100644
--- a/README.md
+++ b/README.md
@@ -221,6 +221,6 @@ Do you use xterm.js in your application as well? Please [open a Pull Request](ht
If you contribute code to this project, you implicitly allow your code to be distributed under the MIT license. You are also implicitly verifying that all code is your original work.
-Copyright (c) 2017-2019, [The xterm.js authors](https://github.com/xtermjs/xterm.js/graphs/contributors) (MIT License)
+Copyright (c) 2017-2022, [The xterm.js authors](https://github.com/xtermjs/xterm.js/graphs/contributors) (MIT License)
Copyright (c) 2014-2017, SourceLair, Private Company ([www.sourcelair.com](https://www.sourcelair.com/home)) (MIT License)
Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
diff --git a/addons/xterm-addon-canvas/src/BaseRenderLayer.ts b/addons/xterm-addon-canvas/src/BaseRenderLayer.ts
index f37ea1b8..8f203400 100644
--- a/addons/xterm-addon-canvas/src/BaseRenderLayer.ts
+++ b/addons/xterm-addon-canvas/src/BaseRenderLayer.ts
@@ -3,25 +3,25 @@
* @license MIT
*/
-import { IRenderDimensions } from 'browser/renderer/Types';
-import { IRenderLayer } from './Types';
-import { ICellData, IColor } from 'common/Types';
-import { DEFAULT_COLOR, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE, Attributes } from 'common/buffer/Constants';
-import { IGlyphIdentifier } from './atlas/Types';
-import { DIM_OPACITY, INVERTED_DEFAULT_COLOR, TEXT_BASELINE } from 'browser/renderer/Constants';
-import { BaseCharAtlas } from './atlas/BaseCharAtlas';
-import { acquireCharAtlas } from './atlas/CharAtlasCache';
-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 { ICoreBrowserService } from 'browser/services/Services';
-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';
+import { acquireTextureAtlas } from 'browser/renderer/shared/CharAtlasCache';
+import { TEXT_BASELINE } from 'browser/renderer/shared/Constants';
+import { tryDrawCustomChar } from 'browser/renderer/shared/CustomGlyphs';
+import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
+import { IRasterizedGlyph, IRenderDimensions, ISelectionRenderModel, ITextureAtlas } from 'browser/renderer/shared/Types';
+import { createSelectionRenderModel } from 'browser/renderer/shared/SelectionRenderModel';
+import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
+import { IColorSet, ReadonlyColorSet } from 'browser/Types';
+import { CellData } from 'common/buffer/CellData';
+import { WHITESPACE_CELL_CODE } from 'common/buffer/Constants';
+import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services';
+import { ICellData } from 'common/Types';
+import { Terminal } from 'xterm';
+import { IRenderLayer } from './Types';
+import { CellColorResolver } from 'browser/renderer/shared/CellColorResolver';
+import { Disposable, toDisposable } from 'common/Lifecycle';
-export abstract class BaseRenderLayer implements IRenderLayer {
+export abstract class BaseRenderLayer extends Disposable implements IRenderLayer {
private _canvas: HTMLCanvasElement;
protected _ctx!: CanvasRenderingContext2D;
private _scaledCharWidth: number = 0;
@@ -31,49 +31,44 @@ export abstract class BaseRenderLayer implements IRenderLayer {
private _scaledCharLeft: number = 0;
private _scaledCharTop: number = 0;
- protected _selectionStart: [number, number] | undefined;
- protected _selectionEnd: [number, number] | undefined;
- protected _columnSelectMode: boolean = false;
+ protected _selectionModel: ISelectionRenderModel = createSelectionRenderModel();
+ private _cellColorResolver: CellColorResolver;
+ private _bitmapGenerator?: BitmapGenerator;
- protected _charAtlas: BaseCharAtlas | undefined;
-
- /**
- * An object that's reused when drawing glyphs in order to reduce GC.
- */
- private _currentGlyphIdentifier: IGlyphIdentifier = {
- chars: '',
- code: 0,
- bg: 0,
- fg: 0,
- bold: false,
- dim: false,
- italic: false
- };
+ protected _charAtlas!: ITextureAtlas;
public get canvas(): HTMLCanvasElement { return this._canvas; }
+ public get cacheCanvas(): HTMLCanvasElement { return this._charAtlas?.cacheCanvas!; }
constructor(
+ private readonly _terminal: Terminal,
private _container: HTMLElement,
id: string,
zIndex: number,
private _alpha: boolean,
- protected _colors: IColorSet,
- private _rendererId: number,
+ protected readonly _themeService: IThemeService,
protected readonly _bufferService: IBufferService,
protected readonly _optionsService: IOptionsService,
protected readonly _decorationService: IDecorationService,
protected readonly _coreBrowserService: ICoreBrowserService
) {
+ super();
+ this._cellColorResolver = new CellColorResolver(this._terminal, this._selectionModel, this._decorationService, this._coreBrowserService, this._themeService);
this._canvas = document.createElement('canvas');
this._canvas.classList.add(`xterm-${id}-layer`);
this._canvas.style.zIndex = zIndex.toString();
this._initCanvas();
this._container.appendChild(this._canvas);
- }
+ this._refreshCharAtlas(this._themeService.colors);
+ this.register(this._themeService.onChangeColors(e => {
+ this._refreshCharAtlas(e);
+ this.reset();
+ }));
- public dispose(): void {
- removeElementFromParent(this._canvas);
- this._charAtlas?.dispose();
+ this.register(toDisposable(() => {
+ removeElementFromParent(this._canvas);
+ this._charAtlas?.dispose();
+ }));
}
private _initCanvas(): void {
@@ -84,20 +79,13 @@ export abstract class BaseRenderLayer implements IRenderLayer {
}
}
- public onOptionsChanged(): void {}
- public onBlur(): void {}
- public onFocus(): void {}
- public onCursorMove(): void {}
- public onGridChanged(startRow: number, endRow: number): void {}
+ public handleBlur(): void {}
+ public handleFocus(): void {}
+ public handleCursorMove(): void {}
+ public handleGridChanged(startRow: number, endRow: number): void {}
- public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {
- this._selectionStart = start;
- this._selectionEnd = end;
- this._columnSelectMode = columnSelectMode;
- }
-
- public setColors(colorSet: IColorSet): void {
- this._refreshCharAtlas(colorSet);
+ public handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {
+ this._selectionModel.update(this._terminal, start, end, columnSelectMode);
}
protected _setTransparency(alpha: boolean): void {
@@ -115,20 +103,21 @@ export abstract class BaseRenderLayer implements IRenderLayer {
this._container.replaceChild(this._canvas, oldCanvas);
// Regenerate char atlas and force a full redraw
- this._refreshCharAtlas(this._colors);
- this.onGridChanged(0, this._bufferService.rows - 1);
+ this._refreshCharAtlas(this._themeService.colors);
+ this.handleGridChanged(0, this._bufferService.rows - 1);
}
/**
* Refreshes the char atlas, aquiring a new one if necessary.
* @param colorSet The color set to use for the char atlas.
*/
- private _refreshCharAtlas(colorSet: IColorSet): void {
+ private _refreshCharAtlas(colorSet: ReadonlyColorSet): void {
if (this._scaledCharWidth <= 0 && this._scaledCharHeight <= 0) {
return;
}
- this._charAtlas = acquireCharAtlas(this._optionsService.rawOptions, this._rendererId, colorSet, this._scaledCharWidth, this._scaledCharHeight, this._coreBrowserService.dpr);
+ this._charAtlas = acquireTextureAtlas(this._terminal, colorSet, this._scaledCellWidth, this._scaledCellHeight, this._scaledCharWidth, this._scaledCharHeight, this._coreBrowserService.dpr);
this._charAtlas.warmUp();
+ this._bitmapGenerator = new BitmapGenerator(this._charAtlas.cacheCanvas);
}
public resize(dim: IRenderDimensions): void {
@@ -148,13 +137,13 @@ export abstract class BaseRenderLayer implements IRenderLayer {
this._clearAll();
}
- this._refreshCharAtlas(this._colors);
+ this._refreshCharAtlas(this._themeService.colors);
}
public abstract reset(): void;
public clearTextureAtlas(): void {
- this._charAtlas?.clear();
+ this._charAtlas?.clearTexture();
}
/**
@@ -304,7 +293,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
if (this._alpha) {
this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
} else {
- this._ctx.fillStyle = this._colors.background.css;
+ this._ctx.fillStyle = this._themeService.colors.background.css;
this._ctx.fillRect(0, 0, this._canvas.width, this._canvas.height);
}
}
@@ -324,7 +313,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
width * this._scaledCellWidth,
height * this._scaledCellHeight);
} else {
- this._ctx.fillStyle = this._colors.background.css;
+ this._ctx.fillStyle = this._themeService.colors.background.css;
this._ctx.fillRect(
x * this._scaledCellWidth,
y * this._scaledCellHeight,
@@ -365,134 +354,37 @@ export abstract class BaseRenderLayer implements IRenderLayer {
/**
* Draws one or more characters at a cell. If possible this will draw using
* the character atlas to reduce draw time.
- * @param chars The character or characters.
- * @param code The character code.
- * @param width The width of the characters.
- * @param x The column to draw at.
- * @param y The row to draw at.
- * @param fg The foreground color, in the format stored within the attributes.
- * @param bg The background color, in the format stored within the attributes.
- * This is used to validate whether a cached image can be used.
- * @param bold Whether the text is bold.
*/
protected _drawChars(cell: ICellData, x: number, y: number): void {
- const contrastColor = this._getContrastColor(cell, x, y);
-
- // skip cache right away if we draw in RGB
- // Note: to avoid bad runtime JoinedCellData will be skipped
- // in the cache handler itself (atlasDidDraw == false) and
- // fall through to uncached later down below
- if (contrastColor || cell.isFgRGB() || cell.isBgRGB()) {
- this._drawUncachedChars(cell, x, y, contrastColor);
- return;
- }
-
- let fg;
- let bg;
- if (cell.isInverse()) {
- fg = (cell.isBgDefault()) ? INVERTED_DEFAULT_COLOR : cell.getBgColor();
- bg = (cell.isFgDefault()) ? INVERTED_DEFAULT_COLOR : cell.getFgColor();
+ const chars = cell.getChars();
+ this._cellColorResolver.resolve(cell, x, y);
+ let glyph: IRasterizedGlyph;
+ if (chars && chars.length > 1) {
+ glyph = this._charAtlas.getRasterizedGlyphCombinedChar(chars, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext);
} else {
- bg = (cell.isBgDefault()) ? DEFAULT_COLOR : cell.getBgColor();
- fg = (cell.isFgDefault()) ? DEFAULT_COLOR : cell.getFgColor();
+ glyph = this._charAtlas.getRasterizedGlyph(cell.getCode() || WHITESPACE_CELL_CODE, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext);
}
-
- const drawInBrightColor = this._optionsService.rawOptions.drawBoldTextInBrightColors && cell.isBold() && fg < 8;
-
- fg += drawInBrightColor ? 8 : 0;
- this._currentGlyphIdentifier.chars = cell.getChars() || WHITESPACE_CELL_CHAR;
- this._currentGlyphIdentifier.code = cell.getCode() || WHITESPACE_CELL_CODE;
- this._currentGlyphIdentifier.bg = bg;
- this._currentGlyphIdentifier.fg = fg;
- this._currentGlyphIdentifier.bold = !!cell.isBold();
- this._currentGlyphIdentifier.dim = !!cell.isDim();
- this._currentGlyphIdentifier.italic = !!cell.isItalic();
-
- // Don't try cache the glyph if it uses any decoration foreground/background override.
- let hasOverrides = false;
- this._decorationService.forEachDecorationAtCell(x, y, undefined, d => {
- if (d.backgroundColorRGB || d.foregroundColorRGB) {
- hasOverrides = true;
- }
- });
-
- const atlasDidDraw = hasOverrides ? false : this._charAtlas?.draw(this._ctx, this._currentGlyphIdentifier, x * this._scaledCellWidth + this._scaledCharLeft, y * this._scaledCellHeight + this._scaledCharTop);
-
- if (!atlasDidDraw) {
- this._drawUncachedChars(cell, x, y);
- }
- }
-
- /**
- * Draws one or more characters at one or more cells. The character(s) will be
- * clipped to ensure that they fit with the cell(s), including the cell to the
- * right if the last character is a wide character.
- * @param chars The character.
- * @param width The width of the character.
- * @param fg The foreground color, in the format stored within the attributes.
- * @param x The column to draw at.
- * @param y The row to draw at.
- */
- private _drawUncachedChars(cell: ICellData, x: number, y: number, fgOverride?: IColor): void {
this._ctx.save();
- this._ctx.font = this._getFont(!!cell.isBold(), !!cell.isItalic());
- this._ctx.textBaseline = TEXT_BASELINE;
-
- if (cell.isInverse()) {
- if (fgOverride) {
- this._ctx.fillStyle = fgOverride.css;
- } else if (cell.isBgDefault()) {
- this._ctx.fillStyle = color.opaque(this._colors.background).css;
- } else if (cell.isBgRGB()) {
- this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`;
- } else {
- let bg = cell.getBgColor();
- if (this._optionsService.rawOptions.drawBoldTextInBrightColors && cell.isBold() && bg < 8) {
- bg += 8;
- }
- this._ctx.fillStyle = this._colors.ansi[bg].css;
- }
- } else {
- if (fgOverride) {
- this._ctx.fillStyle = fgOverride.css;
- } else if (cell.isFgDefault()) {
- this._ctx.fillStyle = this._colors.foreground.css;
- } else if (cell.isFgRGB()) {
- this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`;
- } else {
- let fg = cell.getFgColor();
- if (this._optionsService.rawOptions.drawBoldTextInBrightColors && cell.isBold() && fg < 8) {
- fg += 8;
- }
- this._ctx.fillStyle = this._colors.ansi[fg].css;
- }
- }
-
this._clipRow(y);
-
- // Apply alpha to dim the character
- if (cell.isDim()) {
- this._ctx.globalAlpha = DIM_OPACITY;
+ // Draw the image, use the bitmap if it's available
+ if (this._charAtlas.hasCanvasChanged) {
+ this._bitmapGenerator?.refresh();
+ this._charAtlas.hasCanvasChanged = false;
}
-
- // Draw custom characters if applicable
- let drawSuccess = false;
- if (this._optionsService.rawOptions.customGlyphs !== false) {
- drawSuccess = tryDrawCustomChar(this._ctx, cell.getChars(), x * this._scaledCellWidth, y * this._scaledCellHeight, this._scaledCellWidth, this._scaledCellHeight, this._optionsService.rawOptions.fontSize, this._coreBrowserService.dpr);
- }
-
- // Draw the character
- if (!drawSuccess) {
- this._ctx.fillText(
- cell.getChars(),
- x * this._scaledCellWidth + this._scaledCharLeft,
- y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight);
- }
-
+ this._ctx.drawImage(
+ this._bitmapGenerator?.bitmap || this._charAtlas!.cacheCanvas,
+ glyph.texturePosition.x,
+ glyph.texturePosition.y,
+ glyph.size.x,
+ glyph.size.y,
+ x * this._scaledCellWidth - glyph.offset.x,
+ y * this._scaledCellHeight - glyph.offset.y,
+ glyph.size.x,
+ glyph.size.y
+ );
this._ctx.restore();
}
-
/**
* Clips a row to ensure no pixels will be drawn outside the cells in the row.
* @param y The row to clip.
@@ -517,137 +409,55 @@ export abstract class BaseRenderLayer implements IRenderLayer {
return `${fontStyle} ${fontWeight} ${this._optionsService.rawOptions.fontSize * this._coreBrowserService.dpr}px ${this._optionsService.rawOptions.fontFamily}`;
}
-
- private _getContrastColor(cell: CellData, x: number, y: number): IColor | undefined {
- // Get any decoration foreground/background overrides, this must be fetched before the early
- // exist but applied after inverse
- let bgOverride: number | undefined;
- let fgOverride: number | undefined;
- let isTop = false;
- this._decorationService.forEachDecorationAtCell(x, y, undefined, d => {
- if (d.options.layer !== 'top' && isTop) {
- return;
- }
- if (d.backgroundColorRGB) {
- bgOverride = d.backgroundColorRGB.rgba;
- }
- if (d.foregroundColorRGB) {
- fgOverride = d.foregroundColorRGB.rgba;
- }
- isTop = d.options.layer === 'top';
- });
-
- // Apply selection foreground if applicable
- if (!isTop) {
- if (this._colors.selectionForeground && this._isCellInSelection(x, y)) {
- fgOverride = this._colors.selectionForeground.rgba;
- }
- }
-
- if (!bgOverride && !fgOverride && (this._optionsService.rawOptions.minimumContrastRatio === 1 || excludeFromContrastRatioDemands(cell.getCode()))) {
- return undefined;
- }
-
- if (!bgOverride && !fgOverride) {
- // Try get from cache
- const adjustedColor = this._colors.contrastCache.getColor(cell.bg, cell.fg);
- if (adjustedColor !== undefined) {
- return adjustedColor || undefined;
- }
- }
-
- let fgColor = cell.getFgColor();
- let fgColorMode = cell.getFgColorMode();
- let bgColor = cell.getBgColor();
- let bgColorMode = cell.getBgColorMode();
- const isInverse = !!cell.isInverse();
- const isBold = !!cell.isInverse();
- if (isInverse) {
- const temp = fgColor;
- fgColor = bgColor;
- bgColor = temp;
- const temp2 = fgColorMode;
- fgColorMode = bgColorMode;
- bgColorMode = temp2;
- }
-
- const bgRgba = this._resolveBackgroundRgba(bgOverride !== undefined ? Attributes.CM_RGB : bgColorMode, bgOverride ?? bgColor, isInverse);
- const fgRgba = this._resolveForegroundRgba(fgColorMode, fgColor, isInverse, isBold);
- let result = rgba.ensureContrastRatio(bgOverride ?? bgRgba, fgOverride ?? fgRgba, this._optionsService.rawOptions.minimumContrastRatio);
-
- if (!result) {
- if (!fgOverride) {
- this._colors.contrastCache.setColor(cell.bg, cell.fg, null);
- return undefined;
- }
- // If it was an override and there was no contrast change, set as the result
- result = fgOverride;
- }
-
- const color: IColor = {
- css: channels.toCss(
- (result >> 24) & 0xFF,
- (result >> 16) & 0xFF,
- (result >> 8) & 0xFF
- ),
- rgba: result
- };
- if (!bgOverride && !fgOverride) {
- this._colors.contrastCache.setColor(cell.bg, cell.fg, color);
- }
-
- return color;
- }
-
- private _resolveBackgroundRgba(bgColorMode: number, bgColor: number, inverse: boolean): number {
- switch (bgColorMode) {
- case Attributes.CM_P16:
- case Attributes.CM_P256:
- return this._colors.ansi[bgColor].rgba;
- case Attributes.CM_RGB:
- return bgColor << 8;
- case Attributes.CM_DEFAULT:
- default:
- if (inverse) {
- return this._colors.foreground.rgba;
- }
- return this._colors.background.rgba;
- }
- }
-
- private _resolveForegroundRgba(fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean): number {
- switch (fgColorMode) {
- case Attributes.CM_P16:
- case Attributes.CM_P256:
- if (this._optionsService.rawOptions.drawBoldTextInBrightColors && bold && fgColor < 8) {
- fgColor += 8;
- }
- return this._colors.ansi[fgColor].rgba;
- case Attributes.CM_RGB:
- return fgColor << 8;
- case Attributes.CM_DEFAULT:
- default:
- if (inverse) {
- return this._colors.background.rgba;
- }
- return this._colors.foreground.rgba;
- }
- }
-
- private _isCellInSelection(x: number, y: number): boolean {
- const start = this._selectionStart;
- const end = this._selectionEnd;
- if (!start || !end) {
- return false;
- }
- if (this._columnSelectMode) {
- 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]) ||
- (start[1] < end[1] && y === end[1] && x < end[0]) ||
- (start[1] < end[1] && y === start[1] && x >= start[0]);
- }
}
+/**
+ * The number of milliseconds to wait before generating the ImageBitmap, this is to debounce/batch
+ * the operation as window.createImageBitmap is asynchronous.
+ */
+const GLYPH_BITMAP_COMMIT_DELAY = 100;
+
+const enum BitmapGeneratorState {
+ IDLE = 0,
+ GENERATING = 1,
+ GENERATING_INVALID = 2
+}
+
+class BitmapGenerator {
+ private _state: BitmapGeneratorState = BitmapGeneratorState.IDLE;
+ private _commitTimeout: number | undefined = undefined;
+ private _bitmap: ImageBitmap | undefined = undefined;
+ public get bitmap(): ImageBitmap | undefined { return this._bitmap; }
+
+ constructor(private readonly _canvas: HTMLCanvasElement) {
+ }
+
+ public refresh(): void {
+ // Clear the bitmap immediately as it's stale
+ this._bitmap = undefined;
+ if (this._commitTimeout === undefined) {
+ this._commitTimeout = window.setTimeout(() => this._generate(), GLYPH_BITMAP_COMMIT_DELAY);
+ }
+ if (this._state === BitmapGeneratorState.GENERATING) {
+ this._state = BitmapGeneratorState.GENERATING_INVALID;
+ }
+ }
+
+ private _generate(): void {
+ if (this._state === BitmapGeneratorState.IDLE) {
+ this._bitmap = undefined;
+ this._state = BitmapGeneratorState.GENERATING;
+ window.createImageBitmap(this._canvas).then(bitmap => {
+ if (this._state === BitmapGeneratorState.GENERATING_INVALID) {
+ this.refresh();
+ } else {
+ this._bitmap = bitmap;
+ }
+ this._state = BitmapGeneratorState.IDLE;
+ });
+ if (this._commitTimeout) {
+ this._commitTimeout = undefined;
+ }
+ }
+ }
+}
diff --git a/addons/xterm-addon-canvas/src/CanvasAddon.ts b/addons/xterm-addon-canvas/src/CanvasAddon.ts
index 9fca00a6..e39b56f1 100644
--- a/addons/xterm-addon-canvas/src/CanvasAddon.ts
+++ b/addons/xterm-addon-canvas/src/CanvasAddon.ts
@@ -3,45 +3,57 @@
* @license MIT
*/
-import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IRenderService } from 'browser/services/Services';
-import { IColorSet } from 'browser/Types';
+import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services';
+import { IColorSet, ITerminal } from 'browser/Types';
import { CanvasRenderer } from './CanvasRenderer';
import { IBufferService, ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
import { ITerminalAddon, Terminal } from 'xterm';
+import { EventEmitter, forwardEvent } from 'common/EventEmitter';
+import { Disposable, toDisposable } from 'common/Lifecycle';
-export class CanvasAddon implements ITerminalAddon {
+export class CanvasAddon extends Disposable implements ITerminalAddon {
private _terminal?: Terminal;
private _renderer?: CanvasRenderer;
- public activate(terminal: Terminal): void {
- if (!terminal.element) {
- throw new Error('Cannot activate CanvasAddon before Terminal.open');
- }
- this._terminal = terminal;
- const bufferService: IBufferService = (terminal as any)._core._bufferService;
- const renderService: IRenderService = (terminal as any)._core._renderService;
- const characterJoinerService: ICharacterJoinerService = (terminal as any)._core._characterJoinerService;
- const charSizeService: ICharSizeService = (terminal as any)._core._charSizeService;
- const coreService: ICoreService = (terminal as any)._core.coreService;
- const coreBrowserService: ICoreBrowserService = (terminal as any)._core._coreBrowserService;
- const decorationService: IDecorationService = (terminal as any)._core._decorationService;
- const optionsService: IOptionsService = (terminal as any)._core.optionsService;
- const colors: IColorSet = (terminal as any)._core._colorManager.colors;
- const screenElement: HTMLElement = (terminal as any)._core.screenElement;
- const linkifier = (terminal as any)._core.linkifier2;
- this._renderer = new CanvasRenderer(colors, screenElement, linkifier, bufferService, charSizeService, optionsService, characterJoinerService, coreService, coreBrowserService, decorationService);
- renderService.setRenderer(this._renderer);
- renderService.onResize(bufferService.cols, bufferService.rows);
+ private readonly _onChangeTextureAtlas = this.register(new EventEmitter());
+ public readonly onChangeTextureAtlas = this._onChangeTextureAtlas.event;
+
+ public get textureAtlas(): HTMLCanvasElement | undefined {
+ return this._renderer?.textureAtlas;
}
- public dispose(): void {
- if (!this._terminal) {
- throw new Error('Cannot dispose CanvasAddon because it is activated');
+ public activate(terminal: Terminal): void {
+ const core = (terminal as any)._core as ITerminal;
+ if (!terminal.element) {
+ this.register(core.onWillOpen(() => this.activate(terminal)));
+ return;
}
- const renderService: IRenderService = (this._terminal as any)._core._renderService;
- renderService.setRenderer((this._terminal as any)._core._createRenderer());
- renderService.onResize(this._terminal.cols, this._terminal.rows);
- this._renderer?.dispose();
- this._renderer = undefined;
+
+ this._terminal = terminal;
+ const coreService = core.coreService;
+ const optionsService = core.optionsService;
+ const screenElement = core.screenElement!;
+ const linkifier = core.linkifier2;
+
+ const unsafeCore = core as any;
+ const bufferService: IBufferService = unsafeCore._bufferService;
+ const renderService: IRenderService = unsafeCore._renderService;
+ const characterJoinerService: ICharacterJoinerService = unsafeCore._characterJoinerService;
+ const charSizeService: ICharSizeService = unsafeCore._charSizeService;
+ const coreBrowserService: ICoreBrowserService = unsafeCore._coreBrowserService;
+ const decorationService: IDecorationService = unsafeCore._decorationService;
+ const themeService: IThemeService = unsafeCore._themeService;
+
+ this._renderer = new CanvasRenderer(terminal, screenElement, linkifier, bufferService, charSizeService, optionsService, characterJoinerService, coreService, coreBrowserService, decorationService, themeService);
+ this.register(forwardEvent(this._renderer.onChangeTextureAtlas, this._onChangeTextureAtlas));
+ renderService.setRenderer(this._renderer);
+ renderService.handleResize(bufferService.cols, bufferService.rows);
+
+ this.register(toDisposable(() => {
+ renderService.setRenderer((this._terminal as any)._core._createRenderer());
+ renderService.handleResize(terminal.cols, terminal.rows);
+ this._renderer?.dispose();
+ this._renderer = undefined;
+ }));
}
}
diff --git a/addons/xterm-addon-canvas/src/CanvasRenderer.ts b/addons/xterm-addon-canvas/src/CanvasRenderer.ts
index b642efbc..b657270f 100644
--- a/addons/xterm-addon-canvas/src/CanvasRenderer.ts
+++ b/addons/xterm-addon-canvas/src/CanvasRenderer.ts
@@ -3,35 +3,34 @@
* @license MIT
*/
-import { TextRenderLayer } from './TextRenderLayer';
-import { SelectionRenderLayer } from './SelectionRenderLayer';
+import { removeTerminalFromCache } from 'browser/renderer/shared/CharAtlasCache';
+import { observeDevicePixelDimensions } from 'browser/renderer/shared/DevicePixelObserver';
+import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/shared/Types';
+import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IThemeService } from 'browser/services/Services';
+import { IColorSet, ILinkifier2, ReadonlyColorSet } from 'browser/Types';
+import { EventEmitter } from 'common/EventEmitter';
+import { Disposable, toDisposable } from 'common/Lifecycle';
+import { IBufferService, ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
+import { Terminal } from 'xterm';
import { CursorRenderLayer } from './CursorRenderLayer';
-import { IRenderer, IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/Types';
-import { IRenderLayer } from './Types';
import { LinkRenderLayer } from './LinkRenderLayer';
-import { Disposable } from 'common/Lifecycle';
-import { IColorSet, ILinkifier2 } from 'browser/Types';
-import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService } from 'browser/services/Services';
-import { IBufferService, IOptionsService, IDecorationService, ICoreService } from 'common/services/Services';
-import { removeTerminalFromCache } from './atlas/CharAtlasCache';
-import { EventEmitter, IEvent } from 'common/EventEmitter';
-import { observeDevicePixelDimensions } from 'browser/renderer/DevicePixelObserver';
-
-let nextRendererId = 1;
+import { SelectionRenderLayer } from './SelectionRenderLayer';
+import { TextRenderLayer } from './TextRenderLayer';
+import { IRenderLayer } from './Types';
export class CanvasRenderer extends Disposable implements IRenderer {
- private _id = nextRendererId++;
-
private _renderLayers: IRenderLayer[];
private _devicePixelRatio: number;
public dimensions: IRenderDimensions;
- private _onRequestRedraw = new EventEmitter();
- public get onRequestRedraw(): IEvent { return this._onRequestRedraw.event; }
+ private readonly _onRequestRedraw = this.register(new EventEmitter());
+ public readonly onRequestRedraw = this._onRequestRedraw.event;
+ private readonly _onChangeTextureAtlas = this.register(new EventEmitter());
+ public readonly onChangeTextureAtlas = this._onChangeTextureAtlas.event;
constructor(
- private _colors: IColorSet,
+ private readonly _terminal: Terminal,
private readonly _screenElement: HTMLElement,
linkifier2: ILinkifier2,
private readonly _bufferService: IBufferService,
@@ -40,15 +39,16 @@ export class CanvasRenderer extends Disposable implements IRenderer {
characterJoinerService: ICharacterJoinerService,
coreService: ICoreService,
private readonly _coreBrowserService: ICoreBrowserService,
- decorationService: IDecorationService
+ decorationService: IDecorationService,
+ private readonly _themeService: IThemeService
) {
super();
const allowTransparency = this._optionsService.rawOptions.allowTransparency;
this._renderLayers = [
- new TextRenderLayer(this._screenElement, 0, this._colors, allowTransparency, this._id, this._bufferService, this._optionsService, characterJoinerService, decorationService, this._coreBrowserService),
- new SelectionRenderLayer(this._screenElement, 1, this._colors, this._id, this._bufferService, this._coreBrowserService, decorationService, this._optionsService),
- new LinkRenderLayer(this._screenElement, 2, this._colors, this._id, linkifier2, this._bufferService, this._optionsService, decorationService, this._coreBrowserService),
- new CursorRenderLayer(this._screenElement, 3, this._colors, this._id, this._onRequestRedraw, this._bufferService, this._optionsService, coreService, this._coreBrowserService, decorationService)
+ new TextRenderLayer(this._terminal, this._screenElement, 0, allowTransparency, this._bufferService, this._optionsService, characterJoinerService, decorationService, this._coreBrowserService, _themeService),
+ new SelectionRenderLayer(this._terminal, this._screenElement, 1, this._bufferService, this._coreBrowserService, decorationService, this._optionsService, _themeService),
+ new LinkRenderLayer(this._terminal, this._screenElement, 2, linkifier2, this._bufferService, this._optionsService, decorationService, this._coreBrowserService, _themeService),
+ new CursorRenderLayer(this._terminal, this._screenElement, 3, this._onRequestRedraw, this._bufferService, this._optionsService, coreService, this._coreBrowserService, decorationService, _themeService)
];
this.dimensions = {
scaledCharWidth: 0,
@@ -68,37 +68,28 @@ export class CanvasRenderer extends Disposable implements IRenderer {
this._updateDimensions();
this.register(observeDevicePixelDimensions(this._renderLayers[0].canvas, this._coreBrowserService.window, (w, h) => this._setCanvasDevicePixelDimensions(w, h)));
-
- this.onOptionsChanged();
+ this.register(toDisposable(() => {
+ for (const l of this._renderLayers) {
+ l.dispose();
+ }
+ removeTerminalFromCache(this._terminal);
+ }));
}
- public dispose(): void {
- for (const l of this._renderLayers) {
- l.dispose();
- }
- super.dispose();
- removeTerminalFromCache(this._id);
+ public get textureAtlas(): HTMLCanvasElement | undefined {
+ return this._renderLayers[0].cacheCanvas;
}
- public onDevicePixelRatioChange(): void {
+ public handleDevicePixelRatioChange(): void {
// If the device pixel ratio changed, the char atlas needs to be regenerated
// and the terminal needs to refreshed
if (this._devicePixelRatio !== this._coreBrowserService.dpr) {
this._devicePixelRatio = this._coreBrowserService.dpr;
- this.onResize(this._bufferService.cols, this._bufferService.rows);
+ this.handleResize(this._bufferService.cols, this._bufferService.rows);
}
}
- public setColors(colors: IColorSet): void {
- this._colors = colors;
- // Clear layers and force a full render
- for (const l of this._renderLayers) {
- l.setColors(this._colors);
- l.reset();
- }
- }
-
- public onResize(cols: number, rows: number): void {
+ public handleResize(cols: number, rows: number): void {
// Update character and canvas dimensions
this._updateDimensions();
@@ -112,32 +103,28 @@ export class CanvasRenderer extends Disposable implements IRenderer {
this._screenElement.style.height = `${this.dimensions.canvasHeight}px`;
}
- public onCharSizeChanged(): void {
- this.onResize(this._bufferService.cols, this._bufferService.rows);
+ public handleCharSizeChanged(): void {
+ this.handleResize(this._bufferService.cols, this._bufferService.rows);
}
- public onBlur(): void {
- this._runOperation(l => l.onBlur());
+ public handleBlur(): void {
+ this._runOperation(l => l.handleBlur());
}
- public onFocus(): void {
- this._runOperation(l => l.onFocus());
+ public handleFocus(): void {
+ this._runOperation(l => l.handleFocus());
}
- public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {
- this._runOperation(l => l.onSelectionChanged(start, end, columnSelectMode));
+ public handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {
+ this._runOperation(l => l.handleSelectionChanged(start, end, columnSelectMode));
// Selection foreground requires a full re-render
- if (this._colors.selectionForeground) {
+ if (this._themeService.colors.selectionForeground) {
this._onRequestRedraw.fire({ start: 0, end: this._bufferService.rows - 1 });
}
}
- public onCursorMove(): void {
- this._runOperation(l => l.onCursorMove());
- }
-
- public onOptionsChanged(): void {
- this._runOperation(l => l.onOptionsChanged());
+ public handleCursorMove(): void {
+ this._runOperation(l => l.handleCursorMove());
}
public clear(): void {
@@ -156,7 +143,7 @@ export class CanvasRenderer extends Disposable implements IRenderer {
*/
public renderRows(start: number, end: number): void {
for (const l of this._renderLayers) {
- l.onGridChanged(start, end);
+ l.handleGridChanged(start, end);
}
}
diff --git a/addons/xterm-addon-canvas/src/CursorRenderLayer.ts b/addons/xterm-addon-canvas/src/CursorRenderLayer.ts
index 61e2d934..83806697 100644
--- a/addons/xterm-addon-canvas/src/CursorRenderLayer.ts
+++ b/addons/xterm-addon-canvas/src/CursorRenderLayer.ts
@@ -3,14 +3,16 @@
* @license MIT
*/
-import { IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/Types';
+import { IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/shared/Types';
import { BaseRenderLayer } from './BaseRenderLayer';
import { ICellData } from 'common/Types';
import { CellData } from 'common/buffer/CellData';
-import { IColorSet } from 'browser/Types';
+import { IColorSet, ReadonlyColorSet } from 'browser/Types';
import { IBufferService, IOptionsService, ICoreService, IDecorationService } from 'common/services/Services';
import { IEventEmitter } from 'common/EventEmitter';
-import { ICoreBrowserService } from 'browser/services/Services';
+import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
+import { Terminal } from 'xterm';
+import { toDisposable } from 'common/Lifecycle';
interface ICursorState {
x: number;
@@ -32,18 +34,18 @@ export class CursorRenderLayer extends BaseRenderLayer {
private _cell: ICellData = new CellData();
constructor(
+ terminal: Terminal,
container: HTMLElement,
zIndex: number,
- colors: IColorSet,
- rendererId: number,
private readonly _onRequestRedraw: IEventEmitter,
bufferService: IBufferService,
optionsService: IOptionsService,
private readonly _coreService: ICoreService,
coreBrowserService: ICoreBrowserService,
- decorationService: IDecorationService
+ decorationService: IDecorationService,
+ themeService: IThemeService
) {
- super(container, 'cursor', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService, coreBrowserService);
+ super(terminal, container, 'cursor', zIndex, true, themeService, bufferService, optionsService, decorationService, coreBrowserService);
this._state = {
x: 0,
y: 0,
@@ -56,14 +58,11 @@ export class CursorRenderLayer extends BaseRenderLayer {
'block': this._renderBlockCursor.bind(this),
'underline': this._renderUnderlineCursor.bind(this)
};
- }
-
- public dispose(): void {
- if (this._cursorBlinkStateManager) {
- this._cursorBlinkStateManager.dispose();
+ this.register(optionsService.onOptionChange(() => this._handleOptionsChanged()));
+ this.register(toDisposable(() => {
+ this._cursorBlinkStateManager?.dispose();
this._cursorBlinkStateManager = undefined;
- }
- super.dispose();
+ }));
}
public resize(dim: IRenderDimensions): void {
@@ -81,20 +80,20 @@ export class CursorRenderLayer extends BaseRenderLayer {
public reset(): void {
this._clearCursor();
this._cursorBlinkStateManager?.restartBlinkAnimation();
- this.onOptionsChanged();
+ this._handleOptionsChanged();
}
- public onBlur(): void {
+ public handleBlur(): void {
this._cursorBlinkStateManager?.pause();
this._onRequestRedraw.fire({ start: this._bufferService.buffer.y, end: this._bufferService.buffer.y });
}
- public onFocus(): void {
+ public handleFocus(): void {
this._cursorBlinkStateManager?.resume();
this._onRequestRedraw.fire({ start: this._bufferService.buffer.y, end: this._bufferService.buffer.y });
}
- public onOptionsChanged(): void {
+ private _handleOptionsChanged(): void {
if (this._optionsService.rawOptions.cursorBlink) {
if (!this._cursorBlinkStateManager) {
this._cursorBlinkStateManager = new CursorBlinkStateManager(this._coreBrowserService.isFocused, () => {
@@ -110,11 +109,11 @@ export class CursorRenderLayer extends BaseRenderLayer {
this._onRequestRedraw.fire({ start: this._bufferService.buffer.y, end: this._bufferService.buffer.y });
}
- public onCursorMove(): void {
+ public handleCursorMove(): void {
this._cursorBlinkStateManager?.restartBlinkAnimation();
}
- public onGridChanged(startRow: number, endRow: number): void {
+ public handleGridChanged(startRow: number, endRow: number): void {
if (!this._cursorBlinkStateManager || this._cursorBlinkStateManager.isPaused) {
this._render(false);
} else {
@@ -148,7 +147,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
if (!this._coreBrowserService.isFocused) {
this._clearCursor();
this._ctx.save();
- this._ctx.fillStyle = this._colors.cursor.css;
+ this._ctx.fillStyle = this._themeService.colors.cursor.css;
const cursorStyle = this._optionsService.rawOptions.cursorStyle;
if (cursorStyle && cursorStyle !== 'block') {
this._cursorRenderers[cursorStyle](cursorX, viewportRelativeCursorY, this._cell);
@@ -213,30 +212,30 @@ export class CursorRenderLayer extends BaseRenderLayer {
private _renderBarCursor(x: number, y: number, cell: ICellData): void {
this._ctx.save();
- this._ctx.fillStyle = this._colors.cursor.css;
+ this._ctx.fillStyle = this._themeService.colors.cursor.css;
this._fillLeftLineAtCell(x, y, this._optionsService.rawOptions.cursorWidth);
this._ctx.restore();
}
private _renderBlockCursor(x: number, y: number, cell: ICellData): void {
this._ctx.save();
- this._ctx.fillStyle = this._colors.cursor.css;
+ this._ctx.fillStyle = this._themeService.colors.cursor.css;
this._fillCells(x, y, cell.getWidth(), 1);
- this._ctx.fillStyle = this._colors.cursorAccent.css;
+ this._ctx.fillStyle = this._themeService.colors.cursorAccent.css;
this._fillCharTrueColor(cell, x, y);
this._ctx.restore();
}
private _renderUnderlineCursor(x: number, y: number, cell: ICellData): void {
this._ctx.save();
- this._ctx.fillStyle = this._colors.cursor.css;
+ this._ctx.fillStyle = this._themeService.colors.cursor.css;
this._fillBottomLineAtCells(x, y);
this._ctx.restore();
}
private _renderBlurCursor(x: number, y: number, cell: ICellData): void {
this._ctx.save();
- this._ctx.strokeStyle = this._colors.cursor.css;
+ this._ctx.strokeStyle = this._themeService.colors.cursor.css;
this._strokeRectAtCell(x, y, cell.getWidth(), 1);
this._ctx.restore();
}
diff --git a/addons/xterm-addon-canvas/src/LinkRenderLayer.ts b/addons/xterm-addon-canvas/src/LinkRenderLayer.ts
index c07329fd..2d4c217c 100644
--- a/addons/xterm-addon-canvas/src/LinkRenderLayer.ts
+++ b/addons/xterm-addon-canvas/src/LinkRenderLayer.ts
@@ -3,32 +3,33 @@
* @license MIT
*/
-import { IRenderDimensions } from 'browser/renderer/Types';
+import { IRenderDimensions } from 'browser/renderer/shared/Types';
import { BaseRenderLayer } from './BaseRenderLayer';
-import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/Constants';
-import { ICoreBrowserService } from 'browser/services/Services';
-import { is256Color } from './atlas/CharAtlasUtils';
-import { IColorSet, ILinkifierEvent, ILinkifier2 } from 'browser/Types';
+import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/shared/Constants';
+import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
+import { IColorSet, ILinkifierEvent, ILinkifier2, ReadonlyColorSet } from 'browser/Types';
import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services';
+import { is256Color } from 'browser/renderer/shared/CharAtlasUtils';
+import { Terminal } from 'xterm';
export class LinkRenderLayer extends BaseRenderLayer {
private _state: ILinkifierEvent | undefined;
constructor(
+ terminal: Terminal,
container: HTMLElement,
zIndex: number,
- colors: IColorSet,
- rendererId: number,
linkifier2: ILinkifier2,
bufferService: IBufferService,
optionsService: IOptionsService,
decorationService: IDecorationService,
- coreBrowserService: ICoreBrowserService
+ coreBrowserService: ICoreBrowserService,
+ themeService: IThemeService
) {
- super(container, 'link', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService, coreBrowserService);
+ super(terminal, container, 'link', zIndex, true, themeService, bufferService, optionsService, decorationService, coreBrowserService);
- linkifier2.onShowLinkUnderline(e => this._onShowLinkUnderline(e));
- linkifier2.onHideLinkUnderline(e => this._onHideLinkUnderline(e));
+ this.register(linkifier2.onShowLinkUnderline(e => this._handleShowLinkUnderline(e)));
+ this.register(linkifier2.onHideLinkUnderline(e => this._handleHideLinkUnderline(e)));
}
public resize(dim: IRenderDimensions): void {
@@ -53,14 +54,14 @@ export class LinkRenderLayer extends BaseRenderLayer {
}
}
- private _onShowLinkUnderline(e: ILinkifierEvent): void {
+ private _handleShowLinkUnderline(e: ILinkifierEvent): void {
if (e.fg === INVERTED_DEFAULT_COLOR) {
- this._ctx.fillStyle = this._colors.background.css;
+ this._ctx.fillStyle = this._themeService.colors.background.css;
} else if (e.fg && is256Color(e.fg)) {
// 256 color support
- this._ctx.fillStyle = this._colors.ansi[e.fg].css;
+ this._ctx.fillStyle = this._themeService.colors.ansi[e.fg].css;
} else {
- this._ctx.fillStyle = this._colors.foreground.css;
+ this._ctx.fillStyle = this._themeService.colors.foreground.css;
}
if (e.y1 === e.y2) {
@@ -77,7 +78,7 @@ export class LinkRenderLayer extends BaseRenderLayer {
this._state = e;
}
- private _onHideLinkUnderline(e: ILinkifierEvent): void {
+ private _handleHideLinkUnderline(e: ILinkifierEvent): void {
this._clearCurrentLink();
}
}
diff --git a/addons/xterm-addon-canvas/src/SelectionRenderLayer.ts b/addons/xterm-addon-canvas/src/SelectionRenderLayer.ts
index e90007b7..1c96dcc8 100644
--- a/addons/xterm-addon-canvas/src/SelectionRenderLayer.ts
+++ b/addons/xterm-addon-canvas/src/SelectionRenderLayer.ts
@@ -3,11 +3,12 @@
* @license MIT
*/
-import { IRenderDimensions } from 'browser/renderer/Types';
+import { IRenderDimensions } from 'browser/renderer/shared/Types';
import { BaseRenderLayer } from './BaseRenderLayer';
-import { IColorSet } from 'browser/Types';
+import { IColorSet, ReadonlyColorSet } from 'browser/Types';
import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services';
-import { ICoreBrowserService } from 'browser/services/Services';
+import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
+import { Terminal } from 'xterm';
interface ISelectionState {
start?: [number, number];
@@ -20,16 +21,16 @@ export class SelectionRenderLayer extends BaseRenderLayer {
private _state!: ISelectionState;
constructor(
+ terminal: Terminal,
container: HTMLElement,
zIndex: number,
- colors: IColorSet,
- rendererId: number,
bufferService: IBufferService,
coreBrowserService: ICoreBrowserService,
decorationService: IDecorationService,
- optionsService: IOptionsService
+ optionsService: IOptionsService,
+ themeService: IThemeService
) {
- super(container, 'selection', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService, coreBrowserService);
+ super(terminal, container, 'selection', zIndex, true, themeService, bufferService, optionsService, decorationService, coreBrowserService);
this._clearState();
}
@@ -46,8 +47,8 @@ export class SelectionRenderLayer extends BaseRenderLayer {
super.resize(dim);
// On resize use the base render layer's cached selection values since resize clears _state
// inside reset.
- if (this._selectionStart && this._selectionEnd) {
- this._redrawSelection(this._selectionStart, this._selectionEnd, this._columnSelectMode);
+ if (this._selectionModel.selectionStart && this._selectionModel.selectionEnd) {
+ this._redrawSelection(this._selectionModel.selectionStart, this._selectionModel.selectionEnd, this._selectionModel.columnSelectMode);
}
}
@@ -58,18 +59,18 @@ export class SelectionRenderLayer extends BaseRenderLayer {
}
}
- public onBlur(): void {
+ public handleBlur(): void {
this.reset();
- this._redrawSelection(this._selectionStart, this._selectionEnd, this._columnSelectMode);
+ this._redrawSelection(this._selectionModel.selectionStart, this._selectionModel.selectionEnd, this._selectionModel.columnSelectMode);
}
- public onFocus(): void {
+ public handleFocus(): void {
this.reset();
- this._redrawSelection(this._selectionStart, this._selectionEnd, this._columnSelectMode);
+ this._redrawSelection(this._selectionModel.selectionStart, this._selectionModel.selectionEnd, this._selectionModel.columnSelectMode);
}
- public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
- super.onSelectionChanged(start, end, columnSelectMode);
+ public handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
+ super.handleSelectionChanged(start, end, columnSelectMode);
this._redrawSelection(start, end, columnSelectMode);
}
@@ -101,8 +102,8 @@ export class SelectionRenderLayer extends BaseRenderLayer {
}
this._ctx.fillStyle = (this._coreBrowserService.isFocused
- ? this._colors.selectionBackgroundTransparent
- : this._colors.selectionInactiveBackgroundTransparent).css;
+ ? this._themeService.colors.selectionBackgroundTransparent
+ : this._themeService.colors.selectionInactiveBackgroundTransparent).css;
if (columnSelectMode) {
const startCol = start[0];
diff --git a/addons/xterm-addon-canvas/src/TextRenderLayer.ts b/addons/xterm-addon-canvas/src/TextRenderLayer.ts
index 95f22fce..e2a35751 100644
--- a/addons/xterm-addon-canvas/src/TextRenderLayer.ts
+++ b/addons/xterm-addon-canvas/src/TextRenderLayer.ts
@@ -3,18 +3,19 @@
* @license MIT
*/
-import { IRenderDimensions } from 'browser/renderer/Types';
+import { IRenderDimensions } from 'browser/renderer/shared/Types';
import { CharData, ICellData } from 'common/Types';
import { GridCache } from './GridCache';
import { BaseRenderLayer } from './BaseRenderLayer';
import { AttributeData } from 'common/buffer/AttributeData';
import { NULL_CELL_CODE, Content, UnderlineStyle } from 'common/buffer/Constants';
-import { IColorSet } from 'browser/Types';
+import { IColorSet, ReadonlyColorSet } from 'browser/Types';
import { CellData } from 'common/buffer/CellData';
import { IOptionsService, IBufferService, IDecorationService } from 'common/services/Services';
-import { ICharacterJoinerService, ICoreBrowserService } from 'browser/services/Services';
+import { ICharacterJoinerService, ICoreBrowserService, IThemeService } from 'browser/services/Services';
import { JoinedCellData } from 'browser/services/CharacterJoinerService';
import { color, css } from 'common/Color';
+import { Terminal } from 'xterm';
/**
* This CharData looks like a null character, which will forc a clear and render
@@ -31,19 +32,20 @@ export class TextRenderLayer extends BaseRenderLayer {
private _workCell = new CellData();
constructor(
+ terminal: Terminal,
container: HTMLElement,
zIndex: number,
- colors: IColorSet,
alpha: boolean,
- rendererId: number,
bufferService: IBufferService,
optionsService: IOptionsService,
private readonly _characterJoinerService: ICharacterJoinerService,
decorationService: IDecorationService,
- coreBrowserService: ICoreBrowserService
+ coreBrowserService: ICoreBrowserService,
+ themeService: IThemeService
) {
- super(container, 'text', zIndex, alpha, colors, rendererId, bufferService, optionsService, decorationService, coreBrowserService);
+ super(terminal, container, 'text', zIndex, alpha, themeService, bufferService, optionsService, decorationService, coreBrowserService);
this._state = new GridCache();
+ this.register(optionsService.onSpecificOptionChange('allowTransparency', value => this._setTransparency(value)));
}
public resize(dim: IRenderDimensions): void {
@@ -167,16 +169,16 @@ export class TextRenderLayer extends BaseRenderLayer {
if (cell.isInverse()) {
if (cell.isFgDefault()) {
- nextFillStyle = this._colors.foreground.css;
+ nextFillStyle = this._themeService.colors.foreground.css;
} else if (cell.isFgRGB()) {
nextFillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`;
} else {
- nextFillStyle = this._colors.ansi[cell.getFgColor()].css;
+ nextFillStyle = this._themeService.colors.ansi[cell.getFgColor()].css;
}
} else if (cell.isBgRGB()) {
nextFillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`;
} else if (cell.isBgPalette()) {
- nextFillStyle = this._colors.ansi[cell.getBgColor()].css;
+ 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
@@ -232,81 +234,10 @@ export class TextRenderLayer extends BaseRenderLayer {
}
private _drawForeground(firstRow: number, lastRow: number): void {
- this._forEachCell(firstRow, lastRow, (cell, x, y) => {
- if (cell.isInvisible()) {
- return;
- }
- this._drawChars(cell, x, y);
- if (cell.isUnderline() || cell.isStrikethrough()) {
- this._ctx.save();
-
- if (cell.isInverse()) {
- if (cell.isBgDefault()) {
- this._ctx.fillStyle = this._colors.background.css;
- } else if (cell.isBgRGB()) {
- this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`;
- } else {
- let bg = cell.getBgColor();
- if (this._optionsService.rawOptions.drawBoldTextInBrightColors && cell.isBold() && bg < 8) {
- bg += 8;
- }
- this._ctx.fillStyle = this._colors.ansi[bg].css;
- }
- } else {
- if (cell.isFgDefault()) {
- this._ctx.fillStyle = this._colors.foreground.css;
- } else if (cell.isFgRGB()) {
- this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`;
- } else {
- let fg = cell.getFgColor();
- if (this._optionsService.rawOptions.drawBoldTextInBrightColors && cell.isBold() && fg < 8) {
- fg += 8;
- }
- this._ctx.fillStyle = this._colors.ansi[fg].css;
- }
- }
-
- if (cell.isStrikethrough()) {
- this._fillMiddleLineAtCells(x, y, cell.getWidth());
- }
- if (cell.isUnderline()) {
- if (!cell.isUnderlineColorDefault()) {
- if (cell.isUnderlineColorRGB()) {
- this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getUnderlineColor()).join(',')})`;
- } else {
- let fg = cell.getUnderlineColor();
- if (this._optionsService.rawOptions.drawBoldTextInBrightColors && cell.isBold() && fg < 8) {
- fg += 8;
- }
- this._ctx.fillStyle = this._colors.ansi[fg].css;
- }
- }
- switch (cell.extended.underlineStyle) {
- case UnderlineStyle.DOUBLE:
- this._fillBottomLineAtCells(x, y, cell.getWidth(), -this._coreBrowserService.dpr);
- this._fillBottomLineAtCells(x, y, cell.getWidth(), this._coreBrowserService.dpr);
- break;
- case UnderlineStyle.CURLY:
- this._curlyUnderlineAtCell(x, y, cell.getWidth());
- break;
- case UnderlineStyle.DOTTED:
- this._dottedUnderlineAtCell(x, y, cell.getWidth());
- break;
- case UnderlineStyle.DASHED:
- this._dashedUnderlineAtCell(x, y, cell.getWidth());
- break;
- case UnderlineStyle.SINGLE:
- default:
- this._fillBottomLineAtCells(x, y, cell.getWidth());
- break;
- }
- }
- this._ctx.restore();
- }
- });
+ this._forEachCell(firstRow, lastRow, (cell, x, y) => this._drawChars(cell, x, y));
}
- public onGridChanged(firstRow: number, lastRow: number): void {
+ public handleGridChanged(firstRow: number, lastRow: number): void {
// Resize has not been called yet
if (this._state.cache.length === 0) {
return;
@@ -321,10 +252,6 @@ export class TextRenderLayer extends BaseRenderLayer {
this._drawForeground(firstRow, lastRow);
}
- public onOptionsChanged(): void {
- this._setTransparency(this._optionsService.rawOptions.allowTransparency);
- }
-
/**
* Whether a character is overlapping to the next cell.
*/
@@ -363,19 +290,4 @@ export class TextRenderLayer extends BaseRenderLayer {
this._characterOverlapCache[chars] = overlaps;
return overlaps;
}
-
- /**
- * Clear the charcater at the cell specified.
- * @param x The column of the char.
- * @param y The row of the char.
- */
- // private _clearChar(x: number, y: number): void {
- // let colsToClear = 1;
- // // Clear the adjacent character if it was wide
- // const state = this._state.cache[x][y];
- // if (state && state[CHAR_DATA_WIDTH_INDEX] === 2) {
- // colsToClear = 2;
- // }
- // this.clearCells(x, y, colsToClear, 1);
- // }
}
diff --git a/addons/xterm-addon-canvas/src/Types.d.ts b/addons/xterm-addon-canvas/src/Types.d.ts
index dda6052a..753bd127 100644
--- a/addons/xterm-addon-canvas/src/Types.d.ts
+++ b/addons/xterm-addon-canvas/src/Types.d.ts
@@ -4,7 +4,7 @@
*/
import { IDisposable } from 'common/Types';
-import { IColorSet } from 'browser/Types';
+import { IColorSet, ReadonlyColorSet } from 'browser/Types';
import { IEvent } from 'common/EventEmitter';
// TODO: Use core interfaces
@@ -41,16 +41,14 @@ export interface IRenderer extends IDisposable {
*/
readonly onRequestRedraw: IEvent;
- dispose(): void;
- setColors(colors: IColorSet): void;
- onDevicePixelRatioChange(): void;
- onResize(cols: number, rows: number): void;
- onCharSizeChanged(): void;
- onBlur(): void;
- onFocus(): void;
- onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void;
- onCursorMove(): void;
- onOptionsChanged(): void;
+ handleDevicePixelRatioChange(): void;
+ handleResize(cols: number, rows: number): void;
+ handleCharSizeChanged(): void;
+ handleBlur(): void;
+ handleFocus(): void;
+ handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void;
+ handleCursorMove(): void;
+ handleOptionsChanged(): void;
clear(): void;
renderRows(start: number, end: number): void;
clearTextureAtlas?(): void;
@@ -58,42 +56,33 @@ export interface IRenderer extends IDisposable {
export interface IRenderLayer extends IDisposable {
readonly canvas: HTMLCanvasElement;
+ readonly cacheCanvas: HTMLCanvasElement;
/**
* Called when the terminal loses focus.
*/
- onBlur(): void;
+ handleBlur(): void;
/**
* * Called when the terminal gets focus.
*/
- onFocus(): void;
+ handleFocus(): void;
/**
* Called when the cursor is moved.
*/
- onCursorMove(): void;
-
- /**
- * Called when options change.
- */
- onOptionsChanged(): void;
-
- /**
- * Called when the theme changes.
- */
- setColors(colorSet: IColorSet): void;
+ handleCursorMove(): void;
/**
* Called when the data in the grid has changed (or needs to be rendered
* again).
*/
- onGridChanged(startRow: number, endRow: number): void;
+ handleGridChanged(startRow: number, endRow: number): void;
/**
* Calls when the selection changes.
*/
- onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void;
+ handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void;
/**
* Resize the render layer.
diff --git a/addons/xterm-addon-canvas/src/atlas/BaseCharAtlas.ts b/addons/xterm-addon-canvas/src/atlas/BaseCharAtlas.ts
deleted file mode 100644
index 03cf0285..00000000
--- a/addons/xterm-addon-canvas/src/atlas/BaseCharAtlas.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-/**
- * Copyright (c) 2017 The xterm.js authors. All rights reserved.
- * @license MIT
- */
-
-import { IGlyphIdentifier } from './Types';
-import { IDisposable } from 'common/Types';
-
-export abstract class BaseCharAtlas implements IDisposable {
- private _didWarmUp: boolean = false;
-
- public dispose(): void { }
-
- /**
- * Perform any work needed to warm the cache before it can be used. May be called multiple times.
- * Implement _doWarmUp instead if you only want to get called once.
- */
- public warmUp(): void {
- if (!this._didWarmUp) {
- this._doWarmUp();
- this._didWarmUp = true;
- }
- }
-
- /**
- * Perform any work needed to warm the cache before it can be used. Used by the default
- * implementation of warmUp(), and will only be called once.
- */
- private _doWarmUp(): void { }
-
- public clear(): void { }
-
- /**
- * Called when we start drawing a new frame.
- *
- * TODO: We rely on this getting called by TextRenderLayer. This should really be called by
- * Renderer instead, but we need to make Renderer the source-of-truth for the char atlas, instead
- * of BaseRenderLayer.
- */
- public beginFrame(): void { }
-
- /**
- * May be called before warmUp finishes, however it is okay for the implementation to
- * do nothing and return false in that case.
- *
- * @param ctx Where to draw the character onto.
- * @param glyph Information about what to draw
- * @param x The position on the context to start drawing at
- * @param y The position on the context to start drawing at
- * @returns The success state. True if we drew the character.
- */
- public abstract draw(
- ctx: CanvasRenderingContext2D,
- glyph: IGlyphIdentifier,
- x: number,
- y: number
- ): boolean;
-}
diff --git a/addons/xterm-addon-canvas/src/atlas/CharAtlasCache.ts b/addons/xterm-addon-canvas/src/atlas/CharAtlasCache.ts
deleted file mode 100644
index d9349286..00000000
--- a/addons/xterm-addon-canvas/src/atlas/CharAtlasCache.ts
+++ /dev/null
@@ -1,96 +0,0 @@
-/**
- * Copyright (c) 2017 The xterm.js authors. All rights reserved.
- * @license MIT
- */
-
-import { generateConfig, configEquals } from './CharAtlasUtils';
-import { BaseCharAtlas } from './BaseCharAtlas';
-import { DynamicCharAtlas } from './DynamicCharAtlas';
-import { ICharAtlasConfig } from './Types';
-import { IColorSet } from 'browser/Types';
-import { ITerminalOptions } from 'xterm';
-
-interface ICharAtlasCacheEntry {
- atlas: BaseCharAtlas;
- config: ICharAtlasConfig;
- // N.B. This implementation potentially holds onto copies of the terminal forever, so
- // this may cause memory leaks.
- ownedBy: number[];
-}
-
-const charAtlasCache: ICharAtlasCacheEntry[] = [];
-
-/**
- * Acquires a char atlas, either generating a new one or returning an existing
- * one that is in use by another terminal.
- */
-export function acquireCharAtlas(
- options: Required,
- rendererId: number,
- colors: IColorSet,
- scaledCharWidth: number,
- scaledCharHeight: number,
- devicePixelRatio: number
-): BaseCharAtlas {
- const newConfig = generateConfig(scaledCharWidth, scaledCharHeight, options, colors, devicePixelRatio);
-
- // Check to see if the renderer already owns this config
- for (let i = 0; i < charAtlasCache.length; i++) {
- const entry = charAtlasCache[i];
- const ownedByIndex = entry.ownedBy.indexOf(rendererId);
- if (ownedByIndex >= 0) {
- if (configEquals(entry.config, newConfig)) {
- return entry.atlas;
- }
- // The configs differ, release the renderer from the entry
- if (entry.ownedBy.length === 1) {
- entry.atlas.dispose();
- charAtlasCache.splice(i, 1);
- } else {
- entry.ownedBy.splice(ownedByIndex, 1);
- }
- break;
- }
- }
-
- // Try match a char atlas from the cache
- for (let i = 0; i < charAtlasCache.length; i++) {
- const entry = charAtlasCache[i];
- if (configEquals(entry.config, newConfig)) {
- // Add the renderer to the cache entry and return
- entry.ownedBy.push(rendererId);
- return entry.atlas;
- }
- }
-
- const newEntry: ICharAtlasCacheEntry = {
- atlas: new DynamicCharAtlas(
- document,
- newConfig
- ),
- config: newConfig,
- ownedBy: [rendererId]
- };
- charAtlasCache.push(newEntry);
- return newEntry.atlas;
-}
-
-/**
- * Removes a terminal reference from the cache, allowing its memory to be freed.
- */
-export function removeTerminalFromCache(rendererId: number): void {
- for (let i = 0; i < charAtlasCache.length; i++) {
- const index = charAtlasCache[i].ownedBy.indexOf(rendererId);
- if (index !== -1) {
- if (charAtlasCache[i].ownedBy.length === 1) {
- // Remove the cache entry if it's the only renderer
- charAtlasCache[i].atlas.dispose();
- charAtlasCache.splice(i, 1);
- } else {
- // Remove the reference from the cache entry
- charAtlasCache[i].ownedBy.splice(index, 1);
- }
- break;
- }
- }
-}
diff --git a/addons/xterm-addon-canvas/src/atlas/CharAtlasUtils.ts b/addons/xterm-addon-canvas/src/atlas/CharAtlasUtils.ts
deleted file mode 100644
index b0151e30..00000000
--- a/addons/xterm-addon-canvas/src/atlas/CharAtlasUtils.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-/**
- * Copyright (c) 2017 The xterm.js authors. All rights reserved.
- * @license MIT
- */
-
-import { ICharAtlasConfig } from './Types';
-import { DEFAULT_COLOR } from 'common/buffer/Constants';
-import { IColorSet, IPartialColorSet } from 'browser/Types';
-import { ITerminalOptions } from 'xterm';
-
-export function generateConfig(scaledCharWidth: number, scaledCharHeight: number, options: Required, colors: IColorSet, devicePixelRatio: number): ICharAtlasConfig {
- // null out some fields that don't matter
- const clonedColors: IPartialColorSet = {
- foreground: colors.foreground,
- background: colors.background,
- cursor: undefined,
- cursorAccent: undefined,
- selectionBackground: undefined,
- ansi: colors.ansi.slice()
- };
- return {
- devicePixelRatio,
- scaledCharWidth,
- scaledCharHeight,
- fontFamily: options.fontFamily,
- fontSize: options.fontSize,
- fontWeight: options.fontWeight,
- fontWeightBold: options.fontWeightBold,
- allowTransparency: options.allowTransparency,
- colors: clonedColors
- };
-}
-
-export function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean {
- for (let i = 0; i < a.colors.ansi.length; i++) {
- if (a.colors.ansi[i].rgba !== b.colors.ansi[i].rgba) {
- return false;
- }
- }
- return a.devicePixelRatio === b.devicePixelRatio &&
- a.fontFamily === b.fontFamily &&
- a.fontSize === b.fontSize &&
- a.fontWeight === b.fontWeight &&
- a.fontWeightBold === b.fontWeightBold &&
- a.allowTransparency === b.allowTransparency &&
- a.scaledCharWidth === b.scaledCharWidth &&
- a.scaledCharHeight === b.scaledCharHeight &&
- a.colors.foreground === b.colors.foreground &&
- a.colors.background === b.colors.background;
-}
-
-export function is256Color(colorCode: number): boolean {
- return colorCode < DEFAULT_COLOR;
-}
diff --git a/addons/xterm-addon-canvas/src/atlas/DynamicCharAtlas.ts b/addons/xterm-addon-canvas/src/atlas/DynamicCharAtlas.ts
deleted file mode 100644
index 3098538d..00000000
--- a/addons/xterm-addon-canvas/src/atlas/DynamicCharAtlas.ts
+++ /dev/null
@@ -1,411 +0,0 @@
-/**
- * Copyright (c) 2017 The xterm.js authors. All rights reserved.
- * @license MIT
- */
-
-import { DIM_OPACITY, INVERTED_DEFAULT_COLOR, TEXT_BASELINE } from 'browser/renderer/Constants';
-import { IGlyphIdentifier, ICharAtlasConfig } from './Types';
-import { BaseCharAtlas } from './BaseCharAtlas';
-import { DEFAULT_ANSI_COLORS } from 'browser/ColorManager';
-import { LRUMap } from './LRUMap';
-import { isFirefox, isSafari } from 'common/Platform';
-import { IColor } from 'common/Types';
-import { throwIfFalsy } from 'browser/renderer/RendererUtils';
-import { color } from 'common/Color';
-
-// In practice we're probably never going to exhaust a texture this large. For debugging purposes,
-// however, it can be useful to set this to a really tiny value, to verify that LRU eviction works.
-const TEXTURE_WIDTH = 1024;
-const TEXTURE_HEIGHT = 1024;
-
-const TRANSPARENT_COLOR = {
- css: 'rgba(0, 0, 0, 0)',
- rgba: 0
-};
-
-// Drawing to the cache is expensive: If we have to draw more than this number of glyphs to the
-// cache in a single frame, give up on trying to cache anything else, and try to finish the current
-// frame ASAP.
-//
-// This helps to limit the amount of damage a program can do when it would otherwise thrash the
-// cache.
-const FRAME_CACHE_DRAW_LIMIT = 100;
-
-/**
- * The number of milliseconds to wait before generating the ImageBitmap, this is to debounce/batch
- * the operation as window.createImageBitmap is asynchronous.
- */
-const GLYPH_BITMAP_COMMIT_DELAY = 100;
-
-interface IGlyphCacheValue {
- index: number;
- isEmpty: boolean;
- inBitmap: boolean;
-}
-
-export function getGlyphCacheKey(glyph: IGlyphIdentifier): number {
- // Note that this only returns a valid key when code < 256
- // Layout:
- // 0b00000000000000000000000000000001: italic (1)
- // 0b00000000000000000000000000000010: dim (1)
- // 0b00000000000000000000000000000100: bold (1)
- // 0b00000000000000000000111111111000: fg (9)
- // 0b00000000000111111111000000000000: bg (9)
- // 0b00011111111000000000000000000000: code (8)
- // 0b11100000000000000000000000000000: unused (3)
- return glyph.code << 21 | glyph.bg << 12 | glyph.fg << 3 | (glyph.bold ? 0 : 4) + (glyph.dim ? 0 : 2) + (glyph.italic ? 0 : 1);
-}
-
-export class DynamicCharAtlas extends BaseCharAtlas {
- // An ordered map that we're using to keep track of where each glyph is in the atlas texture.
- // It's ordered so that we can determine when to remove the old entries.
- private _cacheMap: LRUMap;
-
- // The texture that the atlas is drawn to
- private _cacheCanvas: HTMLCanvasElement;
- private _cacheCtx: CanvasRenderingContext2D;
-
- // A temporary context that glyphs are drawn to before being transfered to the atlas.
- private _tmpCtx: CanvasRenderingContext2D;
-
- // The number of characters stored in the atlas by width/height
- private _width: number;
- private _height: number;
-
- private _drawToCacheCount: number = 0;
-
- // An array of glyph keys that are waiting on the bitmap to be generated.
- private _glyphsWaitingOnBitmap: IGlyphCacheValue[] = [];
-
- // The timeout that is used to batch bitmap generation so it's not requested for every new glyph.
- private _bitmapCommitTimeout: number | null = null;
-
- // The bitmap to draw from, this is much faster on other browsers than others.
- private _bitmap: ImageBitmap | null = null;
-
- constructor(document: Document, private _config: ICharAtlasConfig) {
- super();
- this._cacheCanvas = document.createElement('canvas');
- this._cacheCanvas.width = TEXTURE_WIDTH;
- this._cacheCanvas.height = TEXTURE_HEIGHT;
- // The canvas needs alpha because we use clearColor to convert the background color to alpha.
- // It might also contain some characters with transparent backgrounds if allowTransparency is
- // set.
- this._cacheCtx = throwIfFalsy(this._cacheCanvas.getContext('2d', { alpha: true }));
-
- const tmpCanvas = document.createElement('canvas');
- tmpCanvas.width = this._config.scaledCharWidth;
- tmpCanvas.height = this._config.scaledCharHeight;
- this._tmpCtx = throwIfFalsy(tmpCanvas.getContext('2d', { alpha: this._config.allowTransparency }));
-
- this._width = Math.floor(TEXTURE_WIDTH / this._config.scaledCharWidth);
- this._height = Math.floor(TEXTURE_HEIGHT / this._config.scaledCharHeight);
- const capacity = this._width * this._height;
- this._cacheMap = new LRUMap(capacity);
- this._cacheMap.prealloc(capacity);
-
- // This is useful for debugging
- // document.body.appendChild(this._cacheCanvas);
- }
-
- public dispose(): void {
- if (this._bitmapCommitTimeout !== null) {
- window.clearTimeout(this._bitmapCommitTimeout);
- this._bitmapCommitTimeout = null;
- }
- }
-
- public beginFrame(): void {
- this._drawToCacheCount = 0;
- }
-
- public clear(): void {
- if (this._cacheMap.size > 0) {
- const capacity = this._width * this._height;
- this._cacheMap = new LRUMap(capacity);
- this._cacheMap.prealloc(capacity);
- }
- this._cacheCtx.clearRect(0, 0, TEXTURE_WIDTH, TEXTURE_HEIGHT);
- this._tmpCtx.clearRect(0, 0, this._config.scaledCharWidth, this._config.scaledCharHeight);
- }
-
- public draw(
- ctx: CanvasRenderingContext2D,
- glyph: IGlyphIdentifier,
- x: number,
- y: number
- ): boolean {
- // Space is always an empty cell, special case this as it's so common
- if (glyph.code === 32) {
- return true;
- }
-
- // Exit early for uncachable glyphs
- if (!this._canCache(glyph)) {
- return false;
- }
-
- const glyphKey = getGlyphCacheKey(glyph);
- const cacheValue = this._cacheMap.get(glyphKey);
- if (cacheValue !== null && cacheValue !== undefined) {
- this._drawFromCache(ctx, cacheValue, x, y);
- return true;
- }
- if (this._drawToCacheCount < FRAME_CACHE_DRAW_LIMIT) {
- let index;
- if (this._cacheMap.size < this._cacheMap.capacity) {
- index = this._cacheMap.size;
- } else {
- // we're out of space, so our call to set will delete this item
- index = this._cacheMap.peek()!.index;
- }
- const cacheValue = this._drawToCache(glyph, index);
- this._cacheMap.set(glyphKey, cacheValue);
- this._drawFromCache(ctx, cacheValue, x, y);
- return true;
- }
- return false;
- }
-
- private _canCache(glyph: IGlyphIdentifier): boolean {
- // Only cache ascii and extended characters for now, to be safe. In the future, we could do
- // something more complicated to determine the expected width of a character.
- //
- // If we switch the renderer over to webgl at some point, we may be able to use blending modes
- // to draw overlapping glyphs from the atlas:
- // https://github.com/servo/webrender/issues/464#issuecomment-255632875
- // https://webglfundamentals.org/webgl/lessons/webgl-text-texture.html
- return glyph.code < 256;
- }
-
- private _toCoordinateX(index: number): number {
- return (index % this._width) * this._config.scaledCharWidth;
- }
-
- private _toCoordinateY(index: number): number {
- return Math.floor(index / this._width) * this._config.scaledCharHeight;
- }
-
- private _drawFromCache(
- ctx: CanvasRenderingContext2D,
- cacheValue: IGlyphCacheValue,
- x: number,
- y: number
- ): void {
- // We don't actually need to do anything if this is whitespace.
- if (cacheValue.isEmpty) {
- return;
- }
- const cacheX = this._toCoordinateX(cacheValue.index);
- const cacheY = this._toCoordinateY(cacheValue.index);
- ctx.drawImage(
- cacheValue.inBitmap ? this._bitmap! : this._cacheCanvas,
- cacheX,
- cacheY,
- this._config.scaledCharWidth,
- this._config.scaledCharHeight,
- x,
- y,
- this._config.scaledCharWidth,
- this._config.scaledCharHeight
- );
- }
-
- private _getColorFromAnsiIndex(idx: number): IColor {
- if (idx < this._config.colors.ansi.length) {
- return this._config.colors.ansi[idx];
- }
- return DEFAULT_ANSI_COLORS[idx];
- }
-
- private _getBackgroundColor(glyph: IGlyphIdentifier): IColor {
- if (this._config.allowTransparency) {
- // The background color might have some transparency, so we need to render it as fully
- // transparent in the atlas. Otherwise we'd end up drawing the transparent background twice
- // around the anti-aliased edges of the glyph, and it would look too dark.
- return TRANSPARENT_COLOR;
- }
- let result: IColor;
- if (glyph.bg === INVERTED_DEFAULT_COLOR) {
- result = this._config.colors.foreground;
- } else if (glyph.bg < 256) {
- result = this._getColorFromAnsiIndex(glyph.bg);
- } else {
- result = this._config.colors.background;
- }
- if (glyph.dim) {
- result = color.blend(this._config.colors.background, color.multiplyOpacity(result, 0.5));
- }
- return result;
- }
-
- private _getForegroundColor(glyph: IGlyphIdentifier): IColor {
- if (glyph.fg === INVERTED_DEFAULT_COLOR) {
- return color.opaque(this._config.colors.background);
- }
- if (glyph.fg < 256) {
- // 256 color support
- return this._getColorFromAnsiIndex(glyph.fg);
- }
- return this._config.colors.foreground;
- }
-
- // TODO: We do this (or something similar) in multiple places. We should split this off
- // into a shared function.
- private _drawToCache(glyph: IGlyphIdentifier, index: number): IGlyphCacheValue {
- this._drawToCacheCount++;
-
- this._tmpCtx.save();
-
- // draw the background
- const backgroundColor = this._getBackgroundColor(glyph);
- // Use a 'copy' composite operation to clear any existing glyph out of _tmpCtxWithAlpha, regardless of
- // transparency in backgroundColor
- this._tmpCtx.globalCompositeOperation = 'copy';
- this._tmpCtx.fillStyle = backgroundColor.css;
- this._tmpCtx.fillRect(0, 0, this._config.scaledCharWidth, this._config.scaledCharHeight);
- this._tmpCtx.globalCompositeOperation = 'source-over';
-
- // draw the foreground/glyph
- const fontWeight = glyph.bold ? this._config.fontWeightBold : this._config.fontWeight;
- const fontStyle = glyph.italic ? 'italic' : '';
- this._tmpCtx.font =
- `${fontStyle} ${fontWeight} ${this._config.fontSize * this._config.devicePixelRatio}px ${this._config.fontFamily}`;
- this._tmpCtx.textBaseline = TEXT_BASELINE;
-
- this._tmpCtx.fillStyle = this._getForegroundColor(glyph).css;
-
- // Apply alpha to dim the character
- if (glyph.dim) {
- this._tmpCtx.globalAlpha = DIM_OPACITY;
- }
-
- // Draw the character
- this._tmpCtx.fillText(glyph.chars, 0, this._config.scaledCharHeight);
-
- // clear the background from the character to avoid issues with drawing over the previous
- // character if it extends past it's bounds
- let imageData = this._tmpCtx.getImageData(
- 0, 0, this._config.scaledCharWidth, this._config.scaledCharHeight
- );
- let isEmpty = false;
- if (!this._config.allowTransparency) {
- isEmpty = clearColor(imageData, backgroundColor);
- }
-
- // If this charcater is underscore and empty, shift it up until it is visible, try for a maximum
- // of 5 pixels.
- if (isEmpty && glyph.chars === '_' && !this._config.allowTransparency) {
- for (let offset = 1; offset <= 5; offset++) {
- // Draw the character
- this._tmpCtx.fillText(glyph.chars, 0, this._config.scaledCharHeight - offset);
-
- // clear the background from the character to avoid issues with drawing over the previous
- // character if it extends past it's bounds
- imageData = this._tmpCtx.getImageData(
- 0, 0, this._config.scaledCharWidth, this._config.scaledCharHeight
- );
- isEmpty = clearColor(imageData, backgroundColor);
- if (!isEmpty) {
- break;
- }
- }
- }
-
- this._tmpCtx.restore();
-
- // copy the data from imageData to _cacheCanvas
- const x = this._toCoordinateX(index);
- const y = this._toCoordinateY(index);
- // putImageData doesn't do any blending, so it will overwrite any existing cache entry for us
- this._cacheCtx.putImageData(imageData, x, y);
-
- // Add the glyph and queue it to the bitmap (if the browser supports it)
- const cacheValue = {
- index,
- isEmpty,
- inBitmap: false
- };
- this._addGlyphToBitmap(cacheValue);
-
- return cacheValue;
- }
-
- private _addGlyphToBitmap(cacheValue: IGlyphCacheValue): void {
- // Support is patchy for createImageBitmap at the moment, pass a canvas back
- // if support is lacking as drawImage works there too. Firefox is also
- // included here as ImageBitmap appears both buggy and has horrible
- // performance (tested on v55).
- if (!('createImageBitmap' in window) || isFirefox || isSafari) {
- return;
- }
-
- // Add the glyph to the queue
- this._glyphsWaitingOnBitmap.push(cacheValue);
-
- // Check if bitmap generation timeout already exists
- if (this._bitmapCommitTimeout !== null) {
- return;
- }
-
- this._bitmapCommitTimeout = window.setTimeout(() => this._generateBitmap(), GLYPH_BITMAP_COMMIT_DELAY);
- }
-
- private _generateBitmap(): void {
- const glyphsMovingToBitmap = this._glyphsWaitingOnBitmap;
- this._glyphsWaitingOnBitmap = [];
- window.createImageBitmap(this._cacheCanvas).then(bitmap => {
- // Set bitmap
- this._bitmap = bitmap;
-
- // Mark all new glyphs as in bitmap, excluding glyphs that came in after
- // the bitmap was requested
- for (let i = 0; i < glyphsMovingToBitmap.length; i++) {
- const value = glyphsMovingToBitmap[i];
- // It doesn't matter if the value was already evicted, it will be
- // released from memory after this block if so.
- value.inBitmap = true;
- }
- });
- this._bitmapCommitTimeout = null;
- }
-}
-
-// This is used for debugging the renderer, just swap out `new DynamicCharAtlas` with
-// `new NoneCharAtlas`.
-export class NoneCharAtlas extends BaseCharAtlas {
- constructor(document: Document, config: ICharAtlasConfig) {
- super();
- }
-
- public draw(
- ctx: CanvasRenderingContext2D,
- glyph: IGlyphIdentifier,
- x: number,
- y: number
- ): boolean {
- return false;
- }
-}
-
-/**
- * Makes a particular rgb color and colors that are nearly the same in an ImageData completely
- * transparent.
- * @returns True if the result is "empty", meaning all pixels are fully transparent.
- */
-function clearColor(imageData: ImageData, color: IColor): boolean {
- let isEmpty = true;
- const r = color.rgba >>> 24;
- const g = color.rgba >>> 16 & 0xFF;
- const b = color.rgba >>> 8 & 0xFF;
- for (let offset = 0; offset < imageData.data.length; offset += 4) {
- if (Math.abs(imageData.data[offset] - r) +
- Math.abs(imageData.data[offset + 1] - g) +
- Math.abs(imageData.data[offset + 2] - b) < 35) {
- imageData.data[offset + 3] = 0;
- } else {
- isEmpty = false;
- }
- }
- return isEmpty;
-}
diff --git a/addons/xterm-addon-canvas/src/atlas/LRUMap.test.ts b/addons/xterm-addon-canvas/src/atlas/LRUMap.test.ts
deleted file mode 100644
index b24ad2af..00000000
--- a/addons/xterm-addon-canvas/src/atlas/LRUMap.test.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-/**
- * Copyright (c) 2017 The xterm.js authors. All rights reserved.
- * @license MIT
- */
-
-import { assert } from 'chai';
-import { LRUMap } from './LRUMap';
-
-describe('LRUMap', () => {
- it('can be used to store and retrieve values', () => {
- const map = new LRUMap(10);
- map.set(1, 'valuea');
- map.set(2, 'valueb');
- map.set(3, 'valuec');
- assert.strictEqual(map.get(1), 'valuea');
- assert.strictEqual(map.get(2), 'valueb');
- assert.strictEqual(map.get(3), 'valuec');
- });
-
- it('maintains a size from insertions', () => {
- const map = new LRUMap(10);
- assert.strictEqual(map.size, 0);
- map.set(1, 'value');
- assert.strictEqual(map.size, 1);
- map.set(2, 'value');
- assert.strictEqual(map.size, 2);
- });
-
- it('deletes the oldest entry when the capacity is exceeded', () => {
- const map = new LRUMap(4);
- map.set(1, 'value');
- map.set(2, 'value');
- map.set(3, 'value');
- map.set(4, 'value');
- map.set(5, 'value');
- assert.isNull(map.get(1));
- assert.isNotNull(map.get(2));
- assert.isNotNull(map.get(3));
- assert.isNotNull(map.get(4));
- assert.isNotNull(map.get(5));
- assert.strictEqual(map.size, 4);
- });
-
- it('prevents a recently accessed entry from getting deleted', () => {
- const map = new LRUMap(2);
- map.set(1, 'value');
- map.set(2, 'value');
- map.get(1);
- // a would normally get deleted here, except that we called get()
- map.set(3, 'value');
- assert.isNotNull(map.get(1));
- // b got deleted instead of a
- assert.isNull(map.get(2));
- assert.isNotNull(map.get(3));
- });
-
- it('supports mutation', () => {
- const map = new LRUMap(10);
- map.set(1, 'oldvalue');
- map.set(1, 'newvalue');
- // mutation doesn't change the size
- assert.strictEqual(map.size, 1);
- assert.strictEqual(map.get(1), 'newvalue');
- });
-});
diff --git a/addons/xterm-addon-canvas/src/atlas/LRUMap.ts b/addons/xterm-addon-canvas/src/atlas/LRUMap.ts
deleted file mode 100644
index f70962fe..00000000
--- a/addons/xterm-addon-canvas/src/atlas/LRUMap.ts
+++ /dev/null
@@ -1,136 +0,0 @@
-/**
- * Copyright (c) 2017 The xterm.js authors. All rights reserved.
- * @license MIT
- */
-
-interface ILinkedListNode {
- prev: ILinkedListNode | null;
- next: ILinkedListNode | null;
- key: number | null;
- value: T | null;
-}
-
-export class LRUMap {
- private _map: { [key: number]: ILinkedListNode } = {};
- private _head: ILinkedListNode | null = null;
- private _tail: ILinkedListNode | null = null;
- private _nodePool: ILinkedListNode[] = [];
- public size: number = 0;
-
- constructor(public capacity: number) { }
-
- private _unlinkNode(node: ILinkedListNode): void {
- const prev = node.prev;
- const next = node.next;
- if (node === this._head) {
- this._head = next;
- }
- if (node === this._tail) {
- this._tail = prev;
- }
- if (prev !== null) {
- prev.next = next;
- }
- if (next !== null) {
- next.prev = prev;
- }
- }
-
- private _appendNode(node: ILinkedListNode): void {
- const tail = this._tail;
- if (tail !== null) {
- tail.next = node;
- }
- node.prev = tail;
- node.next = null;
- this._tail = node;
- if (this._head === null) {
- this._head = node;
- }
- }
-
- /**
- * Preallocate a bunch of linked-list nodes. Allocating these nodes ahead of time means that
- * they're more likely to live next to each other in memory, which seems to improve performance.
- *
- * Each empty object only consumes about 60 bytes of memory, so this is pretty cheap, even for
- * large maps.
- */
- public prealloc(count: number): void {
- const nodePool = this._nodePool;
- for (let i = 0; i < count; i++) {
- nodePool.push({
- prev: null,
- next: null,
- key: null,
- value: null
- });
- }
- }
-
- public get(key: number): T | null {
- // This is unsafe: We're assuming our keyspace doesn't overlap with Object.prototype. However,
- // it's faster than calling hasOwnProperty, and in our case, it would never overlap.
- const node = this._map[key];
- if (node !== undefined) {
- this._unlinkNode(node);
- this._appendNode(node);
- return node.value;
- }
- return null;
- }
-
- /**
- * Gets a value from a key without marking it as the most recently used item.
- */
- public peekValue(key: number): T | null {
- const node = this._map[key];
- if (node !== undefined) {
- return node.value;
- }
- return null;
- }
-
- public peek(): T | null {
- const head = this._head;
- return head === null ? null : head.value;
- }
-
- public set(key: number, value: T): void {
- // This is unsafe: See note above.
- let node = this._map[key];
- if (node !== undefined) {
- // already exists, we just need to mutate it and move it to the end of the list
- node = this._map[key];
- this._unlinkNode(node);
- node.value = value;
- } else if (this.size >= this.capacity) {
- // we're out of space: recycle the head node, move it to the tail
- node = this._head!;
- this._unlinkNode(node);
- delete this._map[node.key!];
- node.key = key;
- node.value = value;
- this._map[key] = node;
- } else {
- // make a new element
- const nodePool = this._nodePool;
- if (nodePool.length > 0) {
- // use a preallocated node if we can
- node = nodePool.pop()!;
- node.key = key;
- node.value = value;
- } else {
- node = {
- prev: null,
- next: null,
- key,
- value
- };
- }
- this._map[key] = node;
- this.size++;
- }
- this._appendNode(node);
- }
-}
diff --git a/addons/xterm-addon-canvas/src/atlas/Types.d.ts b/addons/xterm-addon-canvas/src/atlas/Types.d.ts
deleted file mode 100644
index d8bc54c1..00000000
--- a/addons/xterm-addon-canvas/src/atlas/Types.d.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-/**
- * Copyright (c) 2017 The xterm.js authors. All rights reserved.
- * @license MIT
- */
-
-import { FontWeight } from 'common/services/Services';
-import { IPartialColorSet } from 'browser/Types';
-
-export interface IGlyphIdentifier {
- chars: string;
- code: number;
- bg: number;
- fg: number;
- bold: boolean;
- dim: boolean;
- italic: boolean;
-}
-
-export interface ICharAtlasConfig {
- devicePixelRatio: number;
- fontSize: number;
- fontFamily: string;
- fontWeight: FontWeight;
- fontWeightBold: FontWeight;
- scaledCharWidth: number;
- scaledCharHeight: number;
- allowTransparency: boolean;
- colors: IPartialColorSet;
-}
diff --git a/addons/xterm-addon-canvas/typings/xterm-addon-canvas.d.ts b/addons/xterm-addon-canvas/typings/xterm-addon-canvas.d.ts
index 7bbbd63d..6a2b98d4 100644
--- a/addons/xterm-addon-canvas/typings/xterm-addon-canvas.d.ts
+++ b/addons/xterm-addon-canvas/typings/xterm-addon-canvas.d.ts
@@ -12,6 +12,11 @@ declare module 'xterm-addon-canvas' {
export class CanvasAddon implements ITerminalAddon {
public textureAtlas?: HTMLCanvasElement;
+ /**
+ * An event that is fired when the texture atlas of the renderer changes.
+ */
+ public readonly onChangeTextureAtlas: IEvent;
+
constructor();
/**
diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts
index 689899ef..249dd594 100644
--- a/addons/xterm-addon-search/src/SearchAddon.ts
+++ b/addons/xterm-addon-search/src/SearchAddon.ts
@@ -5,6 +5,7 @@
import { Terminal, IDisposable, ITerminalAddon, IBufferRange, IDecoration } from 'xterm';
import { EventEmitter } from 'common/EventEmitter';
+import { Disposable, toDisposable } from 'common/Lifecycle';
export interface ISearchOptions {
regex?: boolean;
@@ -50,7 +51,7 @@ type LineCacheEntry = [
const NON_WORD_CHARACTERS = ' ~!@#$%^&*()+`-=[]{}|\\;:"\',./<>?';
const LINES_CACHE_TIME_TO_LIVE = 15 * 1000; // 15 secs
-export class SearchAddon implements ITerminalAddon {
+export class SearchAddon extends Disposable implements ITerminalAddon {
private _terminal: Terminal | undefined;
private _cachedSearchTerm: string | undefined;
private _selectedDecoration: IDecoration | undefined;
@@ -72,13 +73,18 @@ export class SearchAddon implements ITerminalAddon {
private _resultIndex: number | undefined;
- private readonly _onDidChangeResults = new EventEmitter<{ resultIndex: number, resultCount: number } | undefined>();
+ private readonly _onDidChangeResults = this.register(new EventEmitter<{ resultIndex: number, resultCount: number } | undefined>());
public readonly onDidChangeResults = this._onDidChangeResults.event;
public activate(terminal: Terminal): void {
this._terminal = terminal;
- this._onDataDisposable = this._terminal.onWriteParsed(() => this._updateMatches());
- this._onResizeDisposable = this._terminal.onResize(() => this._updateMatches());
+ this._onDataDisposable = this.register(this._terminal.onWriteParsed(() => this._updateMatches()));
+ this._onResizeDisposable = this.register(this._terminal.onResize(() => this._updateMatches()));
+ this.register(toDisposable(() => {
+ this.clearDecorations();
+ this._onDataDisposable?.dispose();
+ this._onResizeDisposable?.dispose();
+ }));
}
private _updateMatches(): void {
@@ -94,12 +100,6 @@ export class SearchAddon implements ITerminalAddon {
}
}
- public dispose(): void {
- this.clearDecorations();
- this._onDataDisposable?.dispose();
- this._onResizeDisposable?.dispose();
- }
-
public clearDecorations(retainCachedSearchTerm?: boolean): void {
this._selectedDecoration?.dispose();
this._searchResults?.clear();
diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.test.ts b/addons/xterm-addon-serialize/src/SerializeAddon.test.ts
index df4a72da..4c5dd645 100644
--- a/addons/xterm-addon-serialize/src/SerializeAddon.test.ts
+++ b/addons/xterm-addon-serialize/src/SerializeAddon.test.ts
@@ -7,9 +7,10 @@ import jsdom = require('jsdom');
import { assert } from 'chai';
import { SerializeAddon } from './SerializeAddon';
import { Terminal } from 'browser/public/Terminal';
-import { ColorManager } from 'browser/ColorManager';
import { SelectionModel } from 'browser/selection/SelectionModel';
import { IBufferService } from 'common/services/Services';
+import { OptionsService } from 'common/services/OptionsService';
+import { ThemeService } from 'browser/services/ThemeService';
function sgr(...seq: string[]): string {
return `\x1b[${seq.join(';')}m`;
@@ -44,14 +45,11 @@ class TestSelectionService {
}
describe('xterm-addon-serialize', () => {
- let cm: ColorManager;
let dom: jsdom.JSDOM;
- let document: Document;
let window: jsdom.DOMWindow;
let serializeAddon: SerializeAddon;
let terminal: Terminal;
- let selectionService: any;
before(() => {
serializeAddon = new SerializeAddon();
@@ -60,7 +58,6 @@ describe('xterm-addon-serialize', () => {
beforeEach(() => {
dom = new jsdom.JSDOM('');
window = dom.window;
- document = window.document;
(window as any).HTMLCanvasElement.prototype.getContext = () => ({
createLinearGradient(): any {
@@ -77,10 +74,8 @@ describe('xterm-addon-serialize', () => {
terminal = new Terminal({ cols: 10, rows: 2, allowProposedApi: true });
terminal.loadAddon(serializeAddon);
- selectionService = new TestSelectionService((terminal as any)._core._bufferService);
- cm = new ColorManager();
- (terminal as any)._core._colorManager = cm;
- (terminal as any)._core._selectionService = selectionService;
+ (terminal as any)._core._themeService = new ThemeService(new OptionsService({}));
+ (terminal as any)._core._selectionService = new TestSelectionService((terminal as any)._core._bufferService);
});
describe('text', () => {
diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts
index 99967374..b83ff160 100644
--- a/addons/xterm-addon-serialize/src/SerializeAddon.ts
+++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts
@@ -544,7 +544,7 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
super(buffer);
// https://github.com/xtermjs/xterm.js/issues/3601
- this._colors = (_terminal as any)._core._colorManager.colors;
+ this._colors = (_terminal as any)._core._themeService.colors;
}
private _padStart(target: string, targetLength: number, padString: string): string {
diff --git a/addons/xterm-addon-webgl/src/GlyphRenderer.ts b/addons/xterm-addon-webgl/src/GlyphRenderer.ts
index a7f4a700..511c9381 100644
--- a/addons/xterm-addon-webgl/src/GlyphRenderer.ts
+++ b/addons/xterm-addon-webgl/src/GlyphRenderer.ts
@@ -3,15 +3,15 @@
* @license MIT
*/
-import { createProgram, PROJECTION_MATRIX, throwIfFalsy } from './WebglUtils';
-import { WebglCharAtlas } from './atlas/WebglCharAtlas';
-import { IWebGL2RenderingContext, IWebGLVertexArrayObject, IRenderModel, IRasterizedGlyph } from './Types';
+import { createProgram, PROJECTION_MATRIX } from './WebglUtils';
+import { IWebGL2RenderingContext, IWebGLVertexArrayObject, IRenderModel } from './Types';
import { fill } from 'common/TypedArrayUtils';
import { NULL_CELL_CODE } from 'common/buffer/Constants';
import { Terminal } from 'xterm';
import { IColorSet } from 'browser/Types';
-import { IRenderDimensions } from 'browser/renderer/Types';
+import { IRasterizedGlyph, IRenderDimensions, ITextureAtlas } from 'browser/renderer/shared/Types';
import { Disposable, toDisposable } from 'common/Lifecycle';
+import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
interface IVertices {
attributes: Float32Array;
@@ -70,16 +70,14 @@ const INDICES_PER_CELL = 10;
const BYTES_PER_CELL = INDICES_PER_CELL * Float32Array.BYTES_PER_ELEMENT;
const CELL_POSITION_INDICES = 2;
-/** Work variables to avoid garbage collection. */
-const w: { i: number, glyph: IRasterizedGlyph | undefined, leftCellPadding: number, clippedPixels: number } = {
- i: 0,
- glyph: undefined,
- leftCellPadding: 0,
- clippedPixels: 0
-};
+// Work variables to avoid garbage collection
+let $i = 0;
+let $glyph: IRasterizedGlyph | undefined = undefined;
+let $leftCellPadding = 0;
+let $clippedPixels = 0;
-export class GlyphRenderer extends Disposable {
- private _atlas: WebglCharAtlas | undefined;
+export class GlyphRenderer extends Disposable {
+ private _atlas: ITextureAtlas | undefined;
private _program: WebGLProgram;
private _vertexArrayObject: IWebGLVertexArrayObject;
@@ -101,7 +99,6 @@ export class GlyphRenderer extends Disposable {
constructor(
private _terminal: Terminal,
- private _colors: IColorSet,
private _gl: IWebGL2RenderingContext,
private _dimensions: IRenderDimensions
) {
@@ -170,7 +167,7 @@ export class GlyphRenderer extends Disposable {
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
// Set viewport
- this.onResize();
+ this.handleResize();
}
public beginFrame(): boolean {
@@ -186,12 +183,12 @@ export class GlyphRenderer extends Disposable {
}
private _updateCell(array: Float32Array, x: number, y: number, code: number | undefined, bg: number, fg: number, ext: number, chars: string, lastBg: number): void {
- w.i = (y * this._terminal.cols + x) * INDICES_PER_CELL;
+ $i = (y * this._terminal.cols + x) * INDICES_PER_CELL;
// Exit early if this is a null character, allow space character to continue as it may have
// underline/strikethrough styles
if (code === NULL_CELL_CODE || code === undefined/* This is used for the right side of wide chars */) {
- fill(array, 0, w.i, w.i + INDICES_PER_CELL - 1 - CELL_POSITION_INDICES);
+ fill(array, 0, $i, $i + INDICES_PER_CELL - 1 - CELL_POSITION_INDICES);
return;
}
@@ -201,39 +198,39 @@ export class GlyphRenderer extends Disposable {
// Get the glyph
if (chars && chars.length > 1) {
- w.glyph = this._atlas.getRasterizedGlyphCombinedChar(chars, bg, fg, ext);
+ $glyph = this._atlas.getRasterizedGlyphCombinedChar(chars, bg, fg, ext);
} else {
- w.glyph = this._atlas.getRasterizedGlyph(code, bg, fg, ext);
+ $glyph = this._atlas.getRasterizedGlyph(code, bg, fg, ext);
}
- w.leftCellPadding = Math.floor((this._dimensions.scaledCellWidth - this._dimensions.scaledCharWidth) / 2);
- if (bg !== lastBg && w.glyph.offset.x > w.leftCellPadding) {
- w.clippedPixels = w.glyph.offset.x - w.leftCellPadding;
+ $leftCellPadding = Math.floor((this._dimensions.scaledCellWidth - this._dimensions.scaledCharWidth) / 2);
+ if (bg !== lastBg && $glyph.offset.x > $leftCellPadding) {
+ $clippedPixels = $glyph.offset.x - $leftCellPadding;
// a_origin
- array[w.i ] = -(w.glyph.offset.x - w.clippedPixels) + this._dimensions.scaledCharLeft;
- array[w.i + 1] = -w.glyph.offset.y + this._dimensions.scaledCharTop;
+ array[$i ] = -($glyph.offset.x - $clippedPixels) + this._dimensions.scaledCharLeft;
+ array[$i + 1] = -$glyph.offset.y + this._dimensions.scaledCharTop;
// a_size
- array[w.i + 2] = (w.glyph.size.x - w.clippedPixels) / this._dimensions.scaledCanvasWidth;
- array[w.i + 3] = w.glyph.size.y / this._dimensions.scaledCanvasHeight;
+ array[$i + 2] = ($glyph.size.x - $clippedPixels) / this._dimensions.scaledCanvasWidth;
+ array[$i + 3] = $glyph.size.y / this._dimensions.scaledCanvasHeight;
// a_texcoord
- array[w.i + 4] = w.glyph.texturePositionClipSpace.x + w.clippedPixels / this._atlas.cacheCanvas.width;
- array[w.i + 5] = w.glyph.texturePositionClipSpace.y;
+ array[$i + 4] = $glyph.texturePositionClipSpace.x + $clippedPixels / this._atlas.cacheCanvas.width;
+ array[$i + 5] = $glyph.texturePositionClipSpace.y;
// a_texsize
- array[w.i + 6] = w.glyph.sizeClipSpace.x - w.clippedPixels / this._atlas.cacheCanvas.width;
- array[w.i + 7] = w.glyph.sizeClipSpace.y;
+ array[$i + 6] = $glyph.sizeClipSpace.x - $clippedPixels / this._atlas.cacheCanvas.width;
+ array[$i + 7] = $glyph.sizeClipSpace.y;
} else {
// a_origin
- array[w.i ] = -w.glyph.offset.x + this._dimensions.scaledCharLeft;
- array[w.i + 1] = -w.glyph.offset.y + this._dimensions.scaledCharTop;
+ array[$i ] = -$glyph.offset.x + this._dimensions.scaledCharLeft;
+ array[$i + 1] = -$glyph.offset.y + this._dimensions.scaledCharTop;
// a_size
- array[w.i + 2] = w.glyph.size.x / this._dimensions.scaledCanvasWidth;
- array[w.i + 3] = w.glyph.size.y / this._dimensions.scaledCanvasHeight;
+ array[$i + 2] = $glyph.size.x / this._dimensions.scaledCanvasWidth;
+ array[$i + 3] = $glyph.size.y / this._dimensions.scaledCanvasHeight;
// a_texcoord
- array[w.i + 4] = w.glyph.texturePositionClipSpace.x;
- array[w.i + 5] = w.glyph.texturePositionClipSpace.y;
+ array[$i + 4] = $glyph.texturePositionClipSpace.x;
+ array[$i + 5] = $glyph.texturePositionClipSpace.y;
// a_texsize
- array[w.i + 6] = w.glyph.sizeClipSpace.x;
- array[w.i + 7] = w.glyph.sizeClipSpace.y;
+ array[$i + 6] = $glyph.sizeClipSpace.x;
+ array[$i + 7] = $glyph.sizeClipSpace.y;
}
// a_cellpos only changes on resize
}
@@ -266,7 +263,7 @@ export class GlyphRenderer extends Disposable {
}
}
- public onResize(): void {
+ public handleResize(): void {
const gl = this._gl;
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
this.clear();
@@ -323,7 +320,7 @@ export class GlyphRenderer extends Disposable {
gl.drawElementsInstanced(gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0, bufferLength / INDICES_PER_CELL);
}
- public setAtlas(atlas: WebglCharAtlas): void {
+ public setAtlas(atlas: ITextureAtlas): void {
const gl = this._gl;
this._atlas = atlas;
diff --git a/addons/xterm-addon-webgl/src/RectangleRenderer.ts b/addons/xterm-addon-webgl/src/RectangleRenderer.ts
index ef08fb7c..04368711 100644
--- a/addons/xterm-addon-webgl/src/RectangleRenderer.ts
+++ b/addons/xterm-addon-webgl/src/RectangleRenderer.ts
@@ -3,16 +3,18 @@
* @license MIT
*/
-import { createProgram, expandFloat32Array, PROJECTION_MATRIX, throwIfFalsy } from './WebglUtils';
+import { createProgram, expandFloat32Array, PROJECTION_MATRIX } from './WebglUtils';
import { IRenderModel, IWebGLVertexArrayObject, IWebGL2RenderingContext } from './Types';
import { Attributes, BgFlags, FgFlags } from 'common/buffer/Constants';
import { Terminal } from 'xterm';
import { IColor } from 'common/Types';
-import { IColorSet } from 'browser/Types';
-import { IRenderDimensions } from 'browser/renderer/Types';
+import { IColorSet, ReadonlyColorSet } from 'browser/Types';
+import { IRenderDimensions } from 'browser/renderer/shared/Types';
import { RENDER_MODEL_BG_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel';
import { Disposable, toDisposable } from 'common/Lifecycle';
-import { DIM_OPACITY } from 'browser/renderer/Constants';
+import { DIM_OPACITY } from 'browser/renderer/shared/Constants';
+import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
+import { IThemeService } from 'browser/services/Services';
const enum VertexAttribLocations {
POSITION = 0,
@@ -58,17 +60,15 @@ const BYTES_PER_RECTANGLE = INDICES_PER_RECTANGLE * Float32Array.BYTES_PER_ELEME
const INITIAL_BUFFER_RECTANGLE_CAPACITY = 20 * INDICES_PER_RECTANGLE;
-/** Work variables to avoid garbage collection. */
-const w: { rgba: number, isDefault: boolean, x1: number, y1: number, r: number, g: number, b: number, a: number } = {
- rgba: 0,
- isDefault: false,
- x1: 0,
- y1: 0,
- r: 0,
- g: 0,
- b: 0,
- a: 0
-};
+// Work variables to avoid garbage collection
+let $rgba = 0;
+let $isDefault = false;
+let $x1 = 0;
+let $y1 = 0;
+let $r = 0;
+let $g = 0;
+let $b = 0;
+let $a = 0;
export class RectangleRenderer extends Disposable {
@@ -85,9 +85,9 @@ export class RectangleRenderer extends Disposable {
constructor(
private _terminal: Terminal,
- private _colors: IColorSet,
private _gl: IWebGL2RenderingContext,
- private _dimensions: IRenderDimensions
+ private _dimensions: IRenderDimensions,
+ private readonly _themeService: IThemeService
) {
super();
@@ -134,7 +134,11 @@ export class RectangleRenderer extends Disposable {
gl.vertexAttribPointer(VertexAttribLocations.COLOR, 4, gl.FLOAT, false, BYTES_PER_RECTANGLE, 4 * Float32Array.BYTES_PER_ELEMENT);
gl.vertexAttribDivisor(VertexAttribLocations.COLOR, 1);
- this._updateCachedColors();
+ this._updateCachedColors(_themeService.colors);
+ this.register(this._themeService.onChangeColors(e => {
+ this._updateCachedColors(e);
+ this._updateViewportRectangle();
+ }));
}
public render(): void {
@@ -152,12 +156,7 @@ export class RectangleRenderer extends Disposable {
gl.drawElementsInstanced(this._gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0, this._vertices.count);
}
- public onResize(): void {
- this._updateViewportRectangle();
- }
-
- public setColors(): void {
- this._updateCachedColors();
+ public handleResize(): void {
this._updateViewportRectangle();
}
@@ -165,8 +164,8 @@ export class RectangleRenderer extends Disposable {
this._dimensions = dimensions;
}
- private _updateCachedColors(): void {
- this._bgFloat = this._colorToFloat32Array(this._colors.background);
+ private _updateCachedColors(colors: ReadonlyColorSet): void {
+ this._bgFloat = this._colorToFloat32Array(colors.background);
}
private _updateViewportRectangle(): void {
@@ -232,47 +231,47 @@ export class RectangleRenderer extends Disposable {
}
private _updateRectangle(vertices: IVertices, offset: number, fg: number, bg: number, startX: number, endX: number, y: number): void {
- w.isDefault = false;
+ $isDefault = false;
if (fg & FgFlags.INVERSE) {
switch (fg & Attributes.CM_MASK) {
case Attributes.CM_P16:
case Attributes.CM_P256:
- w.rgba = this._colors.ansi[fg & Attributes.PCOLOR_MASK].rgba;
+ $rgba = this._themeService.colors.ansi[fg & Attributes.PCOLOR_MASK].rgba;
break;
case Attributes.CM_RGB:
- w.rgba = (fg & Attributes.RGB_MASK) << 8;
+ $rgba = (fg & Attributes.RGB_MASK) << 8;
break;
case Attributes.CM_DEFAULT:
default:
- w.rgba = this._colors.foreground.rgba;
+ $rgba = this._themeService.colors.foreground.rgba;
}
} else {
switch (bg & Attributes.CM_MASK) {
case Attributes.CM_P16:
case Attributes.CM_P256:
- w.rgba = this._colors.ansi[bg & Attributes.PCOLOR_MASK].rgba;
+ $rgba = this._themeService.colors.ansi[bg & Attributes.PCOLOR_MASK].rgba;
break;
case Attributes.CM_RGB:
- w.rgba = (bg & Attributes.RGB_MASK) << 8;
+ $rgba = (bg & Attributes.RGB_MASK) << 8;
break;
case Attributes.CM_DEFAULT:
default:
- w.rgba = this._colors.background.rgba;
- w.isDefault = true;
+ $rgba = this._themeService.colors.background.rgba;
+ $isDefault = true;
}
}
if (vertices.attributes.length < offset + 4) {
vertices.attributes = expandFloat32Array(vertices.attributes, this._terminal.rows * this._terminal.cols * INDICES_PER_RECTANGLE);
}
- w.x1 = startX * this._dimensions.scaledCellWidth;
- w.y1 = y * this._dimensions.scaledCellHeight;
- w.r = ((w.rgba >> 24) & 0xFF) / 255;
- w.g = ((w.rgba >> 16) & 0xFF) / 255;
- w.b = ((w.rgba >> 8 ) & 0xFF) / 255;
- w.a = (!w.isDefault && bg & BgFlags.DIM) ? DIM_OPACITY : 1;
+ $x1 = startX * this._dimensions.scaledCellWidth;
+ $y1 = y * this._dimensions.scaledCellHeight;
+ $r = (($rgba >> 24) & 0xFF) / 255;
+ $g = (($rgba >> 16) & 0xFF) / 255;
+ $b = (($rgba >> 8 ) & 0xFF) / 255;
+ $a = (!$isDefault && bg & BgFlags.DIM) ? DIM_OPACITY : 1;
- this._addRectangle(vertices.attributes, offset, w.x1, w.y1, (endX - startX) * this._dimensions.scaledCellWidth, this._dimensions.scaledCellHeight, w.r, w.g, w.b, w.a);
+ this._addRectangle(vertices.attributes, offset, $x1, $y1, (endX - startX) * this._dimensions.scaledCellWidth, this._dimensions.scaledCellHeight, $r, $g, $b, $a);
}
private _addRectangle(array: Float32Array, offset: number, x1: number, y1: number, width: number, height: number, r: number, g: number, b: number, a: number): void {
diff --git a/addons/xterm-addon-webgl/src/RenderModel.ts b/addons/xterm-addon-webgl/src/RenderModel.ts
index 2969a6d1..b1542d98 100644
--- a/addons/xterm-addon-webgl/src/RenderModel.ts
+++ b/addons/xterm-addon-webgl/src/RenderModel.ts
@@ -3,8 +3,10 @@
* @license MIT
*/
-import { IRenderModel, ISelectionRenderModel } from './Types';
+import { IRenderModel } from './Types';
import { fill } from 'common/TypedArrayUtils';
+import { ISelectionRenderModel } from 'browser/renderer/shared/Types';
+import { createSelectionRenderModel } from 'browser/renderer/shared/SelectionRenderModel';
export const RENDER_MODEL_INDICIES_PER_CELL = 4;
export const RENDER_MODEL_BG_OFFSET = 1;
@@ -21,16 +23,7 @@ export class RenderModel implements IRenderModel {
constructor() {
this.cells = new Uint32Array(0);
this.lineLengths = new Uint32Array(0);
- this.selection = {
- hasSelection: false,
- columnSelectMode: false,
- viewportStartRow: 0,
- viewportEndRow: 0,
- viewportCappedStartRow: 0,
- viewportCappedEndRow: 0,
- startCol: 0,
- endCol: 0
- };
+ this.selection = createSelectionRenderModel();
}
public resize(cols: number, rows: number): void {
@@ -45,14 +38,4 @@ export class RenderModel implements IRenderModel {
fill(this.cells, 0, 0);
fill(this.lineLengths, 0, 0);
}
-
- public clearSelection(): void {
- this.selection.hasSelection = false;
- this.selection.viewportStartRow = 0;
- this.selection.viewportEndRow = 0;
- this.selection.viewportCappedStartRow = 0;
- this.selection.viewportCappedEndRow = 0;
- this.selection.startCol = 0;
- this.selection.endCol = 0;
- }
}
diff --git a/addons/xterm-addon-webgl/src/Types.d.ts b/addons/xterm-addon-webgl/src/Types.d.ts
index bcfa11c8..6e3a18fc 100644
--- a/addons/xterm-addon-webgl/src/Types.d.ts
+++ b/addons/xterm-addon-webgl/src/Types.d.ts
@@ -3,46 +3,7 @@
* @license MIT
*/
-/**
- * Represents a rasterized glyph within a texture atlas. Some numbers are
- * tracked in CSS pixels as well in order to reduce calculations during the
- * render loop.
- */
-export interface IRasterizedGlyph {
- /**
- * The x and y offset between the glyph's top/left and the top/left of a cell
- * in pixels.
- */
- offset: IVector;
- /**
- * the x and y position of the glyph in the texture in pixels.
- */
- texturePosition: IVector;
- /**
- * the x and y position of the glyph in the texture in clip space coordinates.
- */
- texturePositionClipSpace: IVector;
- /**
- * The width and height of the glyph in the texture in pixels.
- */
- size: IVector;
- /**
- * The width and height of the glyph in the texture in clip space coordinates.
- */
- sizeClipSpace: IVector;
-}
-
-export interface IVector {
- x: number;
- y: number;
-}
-
-export interface IBoundingBox {
- top: number;
- left: number;
- right: number;
- bottom: number;
-}
+import { ISelectionRenderModel } from 'browser/renderer/shared/Types';
export interface IRenderModel {
cells: Uint32Array;
@@ -50,17 +11,6 @@ export interface IRenderModel {
selection: ISelectionRenderModel;
}
-export interface ISelectionRenderModel {
- hasSelection: boolean;
- columnSelectMode: boolean;
- viewportStartRow: number;
- viewportEndRow: number;
- viewportCappedStartRow: number;
- viewportCappedEndRow: number;
- startCol: number;
- endCol: number;
-}
-
export interface IWebGL2RenderingContext extends WebGLRenderingContext {
vertexAttribDivisor(index: number, divisor: number): void;
createVertexArray(): IWebGLVertexArrayObject;
diff --git a/addons/xterm-addon-webgl/src/WebglAddon.ts b/addons/xterm-addon-webgl/src/WebglAddon.ts
index 5b98a048..1f8e2616 100644
--- a/addons/xterm-addon-webgl/src/WebglAddon.ts
+++ b/addons/xterm-addon-webgl/src/WebglAddon.ts
@@ -3,56 +3,63 @@
* @license MIT
*/
-import { Terminal, ITerminalAddon, IEvent } from 'xterm';
-import { WebglRenderer } from './WebglRenderer';
-import { ICharacterJoinerService, ICoreBrowserService, IRenderService } from 'browser/services/Services';
-import { IColorSet } from 'browser/Types';
+import { ICharacterJoinerService, ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services';
+import { ITerminal } from 'browser/Types';
import { EventEmitter, forwardEvent } from 'common/EventEmitter';
+import { Disposable, toDisposable } from 'common/Lifecycle';
import { isSafari } from 'common/Platform';
-import { ICoreService, IDecorationService } from 'common/services/Services';
+import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
+import { ICoreTerminal } from 'common/Types';
+import { ITerminalAddon, Terminal } from 'xterm';
+import { WebglRenderer } from './WebglRenderer';
-export class WebglAddon implements ITerminalAddon {
+export class WebglAddon extends Disposable implements ITerminalAddon {
private _terminal?: Terminal;
private _renderer?: WebglRenderer;
- private _onChangeTextureAtlas = new EventEmitter();
- public get onChangeTextureAtlas(): IEvent { return this._onChangeTextureAtlas.event; }
- private _onContextLoss = new EventEmitter();
- public get onContextLoss(): IEvent { return this._onContextLoss.event; }
+ private readonly _onChangeTextureAtlas = this.register(new EventEmitter());
+ public readonly onChangeTextureAtlas = this._onChangeTextureAtlas.event;
+ private readonly _onContextLoss = this.register(new EventEmitter());
+ public readonly onContextLoss = this._onContextLoss.event;
constructor(
private _preserveDrawingBuffer?: boolean
- ) {}
+ ) {
+ super();
+ }
public activate(terminal: Terminal): void {
- if (!terminal.element) {
- throw new Error('Cannot activate WebglAddon before Terminal.open');
- }
if (isSafari) {
throw new Error('Webgl is not currently supported on Safari');
}
- this._terminal = terminal;
- const renderService: IRenderService = (terminal as any)._core._renderService;
- const characterJoinerService: ICharacterJoinerService = (terminal as any)._core._characterJoinerService;
- const coreBrowserService: ICoreBrowserService = (terminal as any)._core._coreBrowserService;
- const coreService: ICoreService = (terminal as any)._core.coreService;
- const decorationService: IDecorationService = (terminal as any)._core._decorationService;
- const colors: IColorSet = (terminal as any)._core._colorManager.colors;
- this._renderer = new WebglRenderer(terminal, colors, characterJoinerService, coreBrowserService, coreService, decorationService, this._preserveDrawingBuffer);
- forwardEvent(this._renderer.onContextLoss, this._onContextLoss);
- forwardEvent(this._renderer.onChangeTextureAtlas, this._onChangeTextureAtlas);
- renderService.setRenderer(this._renderer);
- }
- public dispose(): void {
- if (!this._terminal) {
- throw new Error('Cannot dispose WebglAddon because it is activated');
+ const core = (terminal as any)._core as ITerminal;
+ if (!terminal.element) {
+ this.register(core.onWillOpen(() => this.activate(terminal)));
+ return;
}
- const renderService: IRenderService = (this._terminal as any)._core._renderService;
- renderService.setRenderer((this._terminal as any)._core._createRenderer());
- renderService.onResize(this._terminal.cols, this._terminal.rows);
- this._renderer?.dispose();
- this._renderer = undefined;
+
+ this._terminal = terminal;
+ const coreService: ICoreService = core.coreService;
+ const optionsService: IOptionsService = core.optionsService;
+
+ const unsafeCore = core as any;
+ const renderService: IRenderService = unsafeCore._renderService;
+ const characterJoinerService: ICharacterJoinerService = unsafeCore._characterJoinerService;
+ const coreBrowserService: ICoreBrowserService = unsafeCore._coreBrowserService;
+ const decorationService: IDecorationService = unsafeCore._decorationService;
+ const themeService: IThemeService = unsafeCore._themeService;
+
+ this._renderer = this.register(new WebglRenderer(terminal, themeService, characterJoinerService, coreBrowserService, optionsService, coreService, decorationService, this._preserveDrawingBuffer));
+ this.register(forwardEvent(this._renderer.onContextLoss, this._onContextLoss));
+ this.register(forwardEvent(this._renderer.onChangeTextureAtlas, this._onChangeTextureAtlas));
+ renderService.setRenderer(this._renderer);
+
+ this.register(toDisposable(() => {
+ const renderService: IRenderService = (this._terminal as any)._core._renderService;
+ renderService.setRenderer((this._terminal as any)._core._createRenderer());
+ renderService.handleResize(terminal.cols, terminal.rows);
+ }));
}
public get textureAtlas(): HTMLCanvasElement | undefined {
@@ -60,6 +67,6 @@ export class WebglAddon implements ITerminalAddon {
}
public clearTextureAtlas(): void {
- this._renderer?.clearCharAtlas();
+ this._renderer?.clearTextureAtlas();
}
}
diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts
index d8daa0d1..fddeb99c 100644
--- a/addons/xterm-addon-webgl/src/WebglRenderer.ts
+++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts
@@ -3,46 +3,37 @@
* @license MIT
*/
-import { GlyphRenderer } from './GlyphRenderer';
-import { LinkRenderLayer } from './renderLayer/LinkRenderLayer';
-import { CursorRenderLayer } from './renderLayer/CursorRenderLayer';
-import { acquireCharAtlas, removeTerminalFromCache } from './atlas/CharAtlasCache';
-import { WebglCharAtlas } from './atlas/WebglCharAtlas';
-import { RectangleRenderer } from './RectangleRenderer';
-import { IWebGL2RenderingContext } from './Types';
-import { RenderModel, COMBINED_CHAR_BIT_MASK, RENDER_MODEL_BG_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_EXT_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel';
-import { Disposable } from 'common/Lifecycle';
-import { Attributes, BgFlags, Content, FgFlags, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants';
-import { Terminal, IEvent } from 'xterm';
-import { IRenderLayer } from './renderLayer/Types';
-import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/Types';
-import { observeDevicePixelDimensions } from 'browser/renderer/DevicePixelObserver';
-import { ITerminal, IColorSet } from 'browser/Types';
-import { EventEmitter } from 'common/EventEmitter';
-import { CellData } from 'common/buffer/CellData';
import { addDisposableDomListener } from 'browser/Lifecycle';
-import { ICharacterJoinerService, ICoreBrowserService } from 'browser/services/Services';
-import { CharData, IBufferLine, ICellData } from 'common/Types';
+import { CellColorResolver } from 'browser/renderer/shared/CellColorResolver';
+import { acquireTextureAtlas, removeTerminalFromCache } from 'browser/renderer/shared/CharAtlasCache';
+import { observeDevicePixelDimensions } from 'browser/renderer/shared/DevicePixelObserver';
+import { IRenderDimensions, IRenderer, IRequestRedrawEvent, ITextureAtlas } from 'browser/renderer/shared/Types';
+import { ICharacterJoinerService, ICoreBrowserService, IThemeService } from 'browser/services/Services';
+import { IColorSet, ITerminal, ReadonlyColorSet } from 'browser/Types';
import { AttributeData } from 'common/buffer/AttributeData';
-import { ICoreService, IDecorationService } from 'common/services/Services';
-
-/** Work variables to avoid garbage collection. */
-const w: { fg: number, bg: number, hasFg: boolean, hasBg: boolean, isSelected: boolean } = {
- fg: 0,
- bg: 0,
- hasFg: false,
- hasBg: false,
- isSelected: false
-};
+import { CellData } from 'common/buffer/CellData';
+import { Content, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants';
+import { EventEmitter } from 'common/EventEmitter';
+import { Disposable, toDisposable } from 'common/Lifecycle';
+import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
+import { CharData, IBufferLine, ICellData } from 'common/Types';
+import { Terminal } from 'xterm';
+import { GlyphRenderer } from './GlyphRenderer';
+import { RectangleRenderer } from './RectangleRenderer';
+import { CursorRenderLayer } from './renderLayer/CursorRenderLayer';
+import { LinkRenderLayer } from './renderLayer/LinkRenderLayer';
+import { IRenderLayer } from './renderLayer/Types';
+import { COMBINED_CHAR_BIT_MASK, RenderModel, RENDER_MODEL_BG_OFFSET, RENDER_MODEL_EXT_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel';
+import { IWebGL2RenderingContext } from './Types';
export class WebglRenderer extends Disposable implements IRenderer {
private _renderLayers: IRenderLayer[];
- private _charAtlas: WebglCharAtlas | undefined;
+ private _charAtlas: ITextureAtlas | undefined;
private _devicePixelRatio: number;
private _model: RenderModel = new RenderModel();
private _workCell: CellData = new CellData();
- private _workColors: { fg: number, bg: number, ext: number } = { fg: 0, bg: 0, ext: 0 };
+ private _cellColorResolver: CellColorResolver;
private _canvas: HTMLCanvasElement;
private _gl: IWebGL2RenderingContext;
@@ -55,30 +46,34 @@ export class WebglRenderer extends Disposable implements IRenderer {
private _isAttached: boolean;
private _contextRestorationTimeout: number | undefined;
- private _onChangeTextureAtlas = new EventEmitter();
- public get onChangeTextureAtlas(): IEvent { return this._onChangeTextureAtlas.event; }
- private _onRequestRedraw = new EventEmitter();
- public get onRequestRedraw(): IEvent { return this._onRequestRedraw.event; }
-
- private _onContextLoss = new EventEmitter();
- public get onContextLoss(): IEvent { return this._onContextLoss.event; }
+ private readonly _onChangeTextureAtlas = this.register(new EventEmitter());
+ public readonly onChangeTextureAtlas = this._onChangeTextureAtlas.event;
+ private readonly _onRequestRedraw = this.register(new EventEmitter());
+ public readonly onRequestRedraw = this._onRequestRedraw.event;
+ private readonly _onContextLoss = this.register(new EventEmitter());
+ public readonly onContextLoss = this._onContextLoss.event;
constructor(
private _terminal: Terminal,
- private _colors: IColorSet,
+ private readonly _themeService: IThemeService,
private readonly _characterJoinerService: ICharacterJoinerService,
private readonly _coreBrowserService: ICoreBrowserService,
+ optionsService: IOptionsService,
coreService: ICoreService,
private readonly _decorationService: IDecorationService,
preserveDrawingBuffer?: boolean
) {
super();
+ this.register(this._themeService.onChangeColors(() => this._handleColorChange()));
+
+ this._cellColorResolver = new CellColorResolver(this._terminal, this._model.selection, this._decorationService, this._coreBrowserService, this._themeService);
+
this._core = (this._terminal as any)._core;
this._renderLayers = [
- new LinkRenderLayer(this._core.screenElement!, 2, this._colors, this._core, this._coreBrowserService),
- new CursorRenderLayer(_terminal, this._core.screenElement!, 3, this._colors, this._onRequestRedraw, this._coreBrowserService, coreService)
+ new LinkRenderLayer(this._core.screenElement!, 2, this._terminal, this._core.linkifier2, this._coreBrowserService, this._themeService),
+ new CursorRenderLayer(_terminal, this._core.screenElement!, 3, this._onRequestRedraw, this._coreBrowserService, coreService, this._themeService, optionsService)
];
this.dimensions = {
scaledCharWidth: 0,
@@ -96,6 +91,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
};
this._devicePixelRatio = this._coreBrowserService.dpr;
this._updateDimensions();
+ this.register(optionsService.onOptionChange(() => this._handleOptionsChanged()));
this._canvas = document.createElement('canvas');
@@ -139,47 +135,37 @@ export class WebglRenderer extends Disposable implements IRenderer {
this._initializeWebGLState();
this._isAttached = this._coreBrowserService.window.document.body.contains(this._core.screenElement!);
- }
- public dispose(): void {
- for (const l of this._renderLayers) {
- l.dispose();
- }
- this._canvas.parentElement?.removeChild(this._canvas);
- removeTerminalFromCache(this._terminal);
- super.dispose();
+ this.register(toDisposable(() => {
+ for (const l of this._renderLayers) {
+ l.dispose();
+ }
+ this._canvas.parentElement?.removeChild(this._canvas);
+ removeTerminalFromCache(this._terminal);
+ }));
}
public get textureAtlas(): HTMLCanvasElement | undefined {
return this._charAtlas?.cacheCanvas;
}
- public setColors(colors: IColorSet): void {
- this._colors = colors;
- // Clear layers and force a full render
- for (const l of this._renderLayers) {
- l.setColors(this._terminal, this._colors);
- l.reset(this._terminal);
- }
-
- this._rectangleRenderer.setColors();
-
+ private _handleColorChange(): void {
this._refreshCharAtlas();
// Force a full refresh
this._clearModel(true);
}
- public onDevicePixelRatioChange(): void {
+ public handleDevicePixelRatioChange(): void {
// If the device pixel ratio changed, the char atlas needs to be regenerated
// and the terminal needs to refreshed
if (this._devicePixelRatio !== this._coreBrowserService.dpr) {
this._devicePixelRatio = this._coreBrowserService.dpr;
- this.onResize(this._terminal.cols, this._terminal.rows);
+ this.handleResize(this._terminal.cols, this._terminal.rows);
}
}
- public onResize(cols: number, rows: number): void {
+ public handleResize(cols: number, rows: number): void {
// Update character and canvas dimensions
this._updateDimensions();
@@ -201,9 +187,9 @@ export class WebglRenderer extends Disposable implements IRenderer {
this._core.screenElement!.style.height = `${this.dimensions.canvasHeight}px`;
this._rectangleRenderer.setDimensions(this.dimensions);
- this._rectangleRenderer.onResize();
+ this._rectangleRenderer.handleResize();
this._glyphRenderer.setDimensions(this.dimensions);
- this._glyphRenderer.onResize();
+ this._glyphRenderer.handleResize();
this._refreshCharAtlas();
@@ -212,44 +198,41 @@ export class WebglRenderer extends Disposable implements IRenderer {
this._clearModel(false);
}
- public onCharSizeChanged(): void {
- this.onResize(this._terminal.cols, this._terminal.rows);
+ public handleCharSizeChanged(): void {
+ this.handleResize(this._terminal.cols, this._terminal.rows);
}
- public onBlur(): void {
+ public handleBlur(): void {
for (const l of this._renderLayers) {
- l.onBlur(this._terminal);
+ l.handleBlur(this._terminal);
}
// Request a redraw for active/inactive selection background
this._requestRedrawViewport();
}
- public onFocus(): void {
+ public handleFocus(): void {
for (const l of this._renderLayers) {
- l.onFocus(this._terminal);
+ l.handleFocus(this._terminal);
}
// Request a redraw for active/inactive selection background
this._requestRedrawViewport();
}
- public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
+ public handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
for (const l of this._renderLayers) {
- l.onSelectionChanged(this._terminal, start, end, columnSelectMode);
+ l.handleSelectionChanged(this._terminal, start, end, columnSelectMode);
}
- this._updateSelectionModel(start, end, columnSelectMode);
+ this._model.selection.update(this._terminal, start, end, columnSelectMode);
this._requestRedrawViewport();
}
- public onCursorMove(): void {
+ public handleCursorMove(): void {
for (const l of this._renderLayers) {
- l.onCursorMove(this._terminal);
+ l.handleCursorMove(this._terminal);
}
}
- public onOptionsChanged(): void {
- for (const l of this._renderLayers) {
- l.onOptionsChanged(this._terminal);
- }
+ private _handleOptionsChanged(): void {
this._updateDimensions();
this._refreshCharAtlas();
}
@@ -262,11 +245,11 @@ export class WebglRenderer extends Disposable implements IRenderer {
this._rectangleRenderer?.dispose();
this._glyphRenderer?.dispose();
- this._rectangleRenderer = new RectangleRenderer(this._terminal, this._colors, this._gl, this.dimensions);
- this._glyphRenderer = new GlyphRenderer(this._terminal, this._colors, this._gl, this.dimensions);
+ this._rectangleRenderer = this.register(new RectangleRenderer(this._terminal, this._gl, this.dimensions, this._themeService));
+ this._glyphRenderer = this.register(new GlyphRenderer(this._terminal, this._gl, this.dimensions));
// Update dimensions and acquire char atlas
- this.onCharSizeChanged();
+ this.handleCharSizeChanged();
}
/**
@@ -281,10 +264,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
return;
}
- const atlas = acquireCharAtlas(this._terminal, this._colors, this.dimensions.scaledCellWidth, this.dimensions.scaledCellHeight, this.dimensions.scaledCharWidth, this.dimensions.scaledCharHeight, this._coreBrowserService.dpr);
- if (!('getRasterizedGlyph' in atlas)) {
- throw new Error('The webgl renderer only works with the webgl char atlas');
- }
+ const atlas = acquireTextureAtlas(this._terminal, this._themeService.colors, this.dimensions.scaledCellWidth, this.dimensions.scaledCellHeight, this.dimensions.scaledCharWidth, this.dimensions.scaledCharHeight, this._coreBrowserService.dpr);
if (this._charAtlas !== atlas) {
this._onChangeTextureAtlas.fire(atlas.cacheCanvas);
}
@@ -305,10 +285,9 @@ export class WebglRenderer extends Disposable implements IRenderer {
}
}
- public clearCharAtlas(): void {
+ public clearTextureAtlas(): void {
this._charAtlas?.clearTexture();
this._clearModel(true);
- this._updateModel(0, this._terminal.rows - 1);
this._requestRedrawViewport();
}
@@ -340,13 +319,13 @@ export class WebglRenderer extends Disposable implements IRenderer {
// Update render layers
for (const l of this._renderLayers) {
- l.onGridChanged(this._terminal, start, end);
+ l.handleGridChanged(this._terminal, start, end);
}
// Tell renderer the frame is beginning
if (this._glyphRenderer.beginFrame()) {
this._clearModel(true);
- this._updateSelectionModel(undefined, undefined);
+ this._model.selection.clear();
}
// Update model to reflect what's drawn
@@ -382,11 +361,11 @@ export class WebglRenderer extends Disposable implements IRenderer {
this._model.lineLengths[y] = 0;
joinedRanges = this._characterJoinerService.getJoinedCharacters(row);
for (x = 0; x < terminal.cols; x++) {
- lastBg = this._workColors.bg;
+ lastBg = this._cellColorResolver.result.bg;
line.loadCell(x, cell);
if (x === 0) {
- lastBg = this._workColors.bg;
+ lastBg = this._cellColorResolver.result.bg;
}
// If true, indicates that the current character(s) to draw were joined.
@@ -417,7 +396,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
i = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
// Load colors/resolve overrides into work colors
- this._loadColorsForCell(x, row);
+ this._cellColorResolver.resolve(cell, x, row);
if (code !== NULL_CELL_CODE) {
this._model.lineLengths[y] = x + 1;
@@ -425,9 +404,9 @@ export class WebglRenderer extends Disposable implements IRenderer {
// Nothing has changed, no updates needed
if (this._model.cells[i] === code &&
- this._model.cells[i + RENDER_MODEL_BG_OFFSET] === this._workColors.bg &&
- this._model.cells[i + RENDER_MODEL_FG_OFFSET] === this._workColors.fg &&
- this._model.cells[i + RENDER_MODEL_EXT_OFFSET] === this._workColors.ext) {
+ this._model.cells[i + RENDER_MODEL_BG_OFFSET] === this._cellColorResolver.result.bg &&
+ this._model.cells[i + RENDER_MODEL_FG_OFFSET] === this._cellColorResolver.result.fg &&
+ this._model.cells[i + RENDER_MODEL_EXT_OFFSET] === this._cellColorResolver.result.ext) {
continue;
}
@@ -438,11 +417,11 @@ export class WebglRenderer extends Disposable implements IRenderer {
// Cache the results in the model
this._model.cells[i] = code;
- this._model.cells[i + RENDER_MODEL_BG_OFFSET] = this._workColors.bg;
- this._model.cells[i + RENDER_MODEL_FG_OFFSET] = this._workColors.fg;
- this._model.cells[i + RENDER_MODEL_EXT_OFFSET] = this._workColors.ext;
+ this._model.cells[i + RENDER_MODEL_BG_OFFSET] = this._cellColorResolver.result.bg;
+ this._model.cells[i + RENDER_MODEL_FG_OFFSET] = this._cellColorResolver.result.fg;
+ this._model.cells[i + RENDER_MODEL_EXT_OFFSET] = this._cellColorResolver.result.ext;
- this._glyphRenderer.updateCell(x, y, code, this._workColors.bg, this._workColors.fg, this._workColors.ext, chars, lastBg);
+ this._glyphRenderer.updateCell(x, y, code, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext, chars, lastBg);
if (isJoined) {
// Restore work cell
@@ -453,9 +432,9 @@ export class WebglRenderer extends Disposable implements IRenderer {
j = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
this._glyphRenderer.updateCell(x, y, NULL_CELL_CODE, 0, 0, 0, NULL_CELL_CHAR, 0);
this._model.cells[j] = NULL_CELL_CODE;
- this._model.cells[j + RENDER_MODEL_BG_OFFSET] = this._workColors.bg;
- this._model.cells[j + RENDER_MODEL_FG_OFFSET] = this._workColors.fg;
- this._model.cells[j + RENDER_MODEL_EXT_OFFSET] = this._workColors.ext;
+ this._model.cells[j + RENDER_MODEL_BG_OFFSET] = this._cellColorResolver.result.bg;
+ this._model.cells[j + RENDER_MODEL_FG_OFFSET] = this._cellColorResolver.result.fg;
+ this._model.cells[j + RENDER_MODEL_EXT_OFFSET] = this._cellColorResolver.result.ext;
}
}
}
@@ -463,153 +442,6 @@ export class WebglRenderer extends Disposable implements IRenderer {
this._rectangleRenderer.updateBackgrounds(this._model);
}
- /**
- * Loads colors for the cell into the work colors object. This resolves overrides/inverse if
- * necessary which is why the work cell object is not used.
- */
- private _loadColorsForCell(x: number, y: number): void {
- this._workColors.bg = this._workCell.bg;
- this._workColors.fg = this._workCell.fg;
- this._workColors.ext = this._workCell.bg & BgFlags.HAS_EXTENDED ? this._workCell.extended.ext : 0;
- // Get any foreground/background overrides, this happens on the model to avoid spreading
- // override logic throughout the different sub-renderers
-
- // Reset overrides work variables
- w.bg = 0;
- w.fg = 0;
- w.hasBg = false;
- w.hasFg = false;
- w.isSelected = false;
-
- // Apply decorations on the bottom layer
- this._decorationService.forEachDecorationAtCell(x, y, 'bottom', d => {
- if (d.backgroundColorRGB) {
- w.bg = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF;
- w.hasBg = true;
- }
- if (d.foregroundColorRGB) {
- w.fg = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF;
- w.hasFg = true;
- }
- });
-
- // Apply the selection color if needed
- w.isSelected = this._isCellSelected(x, y);
- if (w.isSelected) {
- w.bg = (this._coreBrowserService.isFocused ? this._colors.selectionBackgroundOpaque : this._colors.selectionInactiveBackgroundOpaque).rgba >> 8 & 0xFFFFFF;
- w.hasBg = true;
- if (this._colors.selectionForeground) {
- w.fg = this._colors.selectionForeground.rgba >> 8 & 0xFFFFFF;
- w.hasFg = true;
- }
- }
-
- // Apply decorations on the top layer
- this._decorationService.forEachDecorationAtCell(x, y, 'top', d => {
- if (d.backgroundColorRGB) {
- w.bg = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF;
- w.hasBg = true;
- }
- if (d.foregroundColorRGB) {
- w.fg = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF;
- w.hasFg = true;
- }
- });
-
- // Convert any overrides from rgba to the fg/bg packed format. This resolves the inverse flag
- // ahead of time in order to use the correct cache key
- if (w.hasBg) {
- if (w.isSelected) {
- // Non-RGB attributes from model + force non-dim + override + force RGB color mode
- w.bg = (this._workCell.bg & ~Attributes.RGB_MASK & ~BgFlags.DIM) | w.bg | Attributes.CM_RGB;
- } else {
- // Non-RGB attributes from model + override + force RGB color mode
- w.bg = (this._workCell.bg & ~Attributes.RGB_MASK) | w.bg | Attributes.CM_RGB;
- }
- }
- if (w.hasFg) {
- // Non-RGB attributes from model + force disable inverse + override + force RGB color mode
- w.fg = (this._workCell.fg & ~Attributes.RGB_MASK & ~FgFlags.INVERSE) | w.fg | Attributes.CM_RGB;
- }
-
- // Handle case where inverse was specified by only one of bg override or fg override was set,
- // resolving the other inverse color and setting the inverse flag if needed.
- if (this._workColors.fg & FgFlags.INVERSE) {
- if (w.hasBg && !w.hasFg) {
- // Resolve bg color type (default color has a different meaning in fg vs bg)
- if ((this._workColors.bg & Attributes.CM_MASK) === Attributes.CM_DEFAULT) {
- w.fg = (this._workColors.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | ((this._colors.background.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB;
- } else {
- w.fg = (this._workColors.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | this._workColors.bg & (Attributes.RGB_MASK | Attributes.CM_MASK);
- }
- w.hasFg = true;
- }
- if (!w.hasBg && w.hasFg) {
- // Resolve bg color type (default color has a different meaning in fg vs bg)
- if ((this._workColors.fg & Attributes.CM_MASK) === Attributes.CM_DEFAULT) {
- w.bg = (this._workColors.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | ((this._colors.foreground.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB;
- } else {
- w.bg = (this._workColors.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | this._workColors.fg & (Attributes.RGB_MASK | Attributes.CM_MASK);
- }
- w.hasBg = true;
- }
- }
-
- // Use the override if it exists
- this._workColors.bg = w.hasBg ? w.bg : this._workColors.bg;
- this._workColors.fg = w.hasFg ? w.fg : this._workColors.fg;
- }
-
- private _isCellSelected(x: number, y: number): boolean {
- if (!this._model.selection.hasSelection) {
- return false;
- }
- y -= this._terminal.buffer.active.viewportY;
- if (this._model.selection.columnSelectMode) {
- 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) ||
- (this._model.selection.viewportStartRow < this._model.selection.viewportEndRow && y === this._model.selection.viewportEndRow && x < this._model.selection.endCol) ||
- (this._model.selection.viewportStartRow < this._model.selection.viewportEndRow && y === this._model.selection.viewportStartRow && x >= this._model.selection.startCol);
- }
-
- private _updateSelectionModel(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {
- const terminal = this._terminal;
-
- // Selection does not exist
- if (!start || !end || (start[0] === end[0] && start[1] === end[1])) {
- this._model.clearSelection();
- return;
- }
-
- // Translate from buffer position to viewport position
- const viewportStartRow = start[1] - terminal.buffer.active.viewportY;
- const viewportEndRow = end[1] - terminal.buffer.active.viewportY;
- const viewportCappedStartRow = Math.max(viewportStartRow, 0);
- const viewportCappedEndRow = Math.min(viewportEndRow, terminal.rows - 1);
-
- // No need to draw the selection
- if (viewportCappedStartRow >= terminal.rows || viewportCappedEndRow < 0) {
- this._model.clearSelection();
- return;
- }
-
- this._model.selection.hasSelection = true;
- this._model.selection.columnSelectMode = columnSelectMode;
- this._model.selection.viewportStartRow = viewportStartRow;
- this._model.selection.viewportEndRow = viewportEndRow;
- this._model.selection.viewportCappedStartRow = viewportCappedStartRow;
- this._model.selection.viewportCappedEndRow = viewportCappedEndRow;
- this._model.selection.startCol = start[0];
- this._model.selection.endCol = end[0];
- }
-
/**
* Recalculates the character and canvas dimensions.
*/
diff --git a/addons/xterm-addon-webgl/src/WebglUtils.ts b/addons/xterm-addon-webgl/src/WebglUtils.ts
index 841ad067..51a27377 100644
--- a/addons/xterm-addon-webgl/src/WebglUtils.ts
+++ b/addons/xterm-addon-webgl/src/WebglUtils.ts
@@ -3,6 +3,8 @@
* @license MIT
*/
+import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
+
/**
* A matrix that when multiplies will translate 0-1 coordinates (left to right,
* top to bottom) to clip space.
@@ -49,10 +51,3 @@ export function expandFloat32Array(source: Float32Array, max: number): Float32Ar
}
return newArray;
}
-
-export function throwIfFalsy(value: T | undefined | null): T {
- if (!value) {
- throw new Error('value must not be falsy');
- }
- return value;
-}
diff --git a/addons/xterm-addon-webgl/src/atlas/Types.d.ts b/addons/xterm-addon-webgl/src/atlas/Types.d.ts
deleted file mode 100644
index 8d2870cd..00000000
--- a/addons/xterm-addon-webgl/src/atlas/Types.d.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-/**
- * Copyright (c) 2017 The xterm.js authors. All rights reserved.
- * @license MIT
- */
-
-import { FontWeight } from 'xterm';
-import { IColorSet } from 'browser/Types';
-
-export interface IGlyphIdentifier {
- chars: string;
- code: number;
- bg: number;
- fg: number;
- bold: boolean;
- dim: boolean;
- italic: boolean;
-}
-
-export interface ICharAtlasConfig {
- customGlyphs: boolean;
- devicePixelRatio: number;
- letterSpacing: number;
- lineHeight: number;
- fontSize: number;
- fontFamily: string;
- fontWeight: FontWeight;
- fontWeightBold: FontWeight;
- scaledCellWidth: number;
- scaledCellHeight: number;
- scaledCharWidth: number;
- scaledCharHeight: number;
- allowTransparency: boolean;
- drawBoldTextInBrightColors: boolean;
- minimumContrastRatio: number;
- colors: IColorSet;
-}
diff --git a/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts b/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts
index e9c0cf41..8ed12d86 100644
--- a/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts
+++ b/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts
@@ -4,17 +4,17 @@
*/
import { IRenderLayer } from './Types';
-import { acquireCharAtlas } from '../atlas/CharAtlasCache';
+import { acquireTextureAtlas } from 'browser/renderer/shared/CharAtlasCache';
import { Terminal } from 'xterm';
-import { IColorSet } from 'browser/Types';
-import { TEXT_BASELINE } from 'browser/renderer/Constants';
-import { ICoreBrowserService } from 'browser/services/Services';
-import { IRenderDimensions } from 'browser/renderer/Types';
+import { IColorSet, ReadonlyColorSet } from 'browser/Types';
+import { TEXT_BASELINE } from 'browser/renderer/shared/Constants';
+import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
+import { IRenderDimensions, ITextureAtlas } from 'browser/renderer/shared/Types';
import { CellData } from 'common/buffer/CellData';
-import { WebglCharAtlas } from 'atlas/WebglCharAtlas';
-import { throwIfFalsy } from '../WebglUtils';
+import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
+import { Disposable, toDisposable } from 'common/Lifecycle';
-export abstract class BaseRenderLayer implements IRenderLayer {
+export abstract class BaseRenderLayer extends Disposable implements IRenderLayer {
private _canvas: HTMLCanvasElement;
protected _ctx!: CanvasRenderingContext2D;
private _scaledCharWidth: number = 0;
@@ -24,28 +24,31 @@ export abstract class BaseRenderLayer implements IRenderLayer {
private _scaledCharLeft: number = 0;
private _scaledCharTop: number = 0;
- protected _charAtlas: WebglCharAtlas | undefined;
+ protected _charAtlas: ITextureAtlas | undefined;
constructor(
+ terminal: Terminal,
private _container: HTMLElement,
id: string,
zIndex: number,
private _alpha: boolean,
- protected _colors: IColorSet,
- protected readonly _coreBrowserService: ICoreBrowserService
+ protected readonly _coreBrowserService: ICoreBrowserService,
+ protected readonly _themeService: IThemeService
) {
+ super();
this._canvas = document.createElement('canvas');
this._canvas.classList.add(`xterm-${id}-layer`);
this._canvas.style.zIndex = zIndex.toString();
this._initCanvas();
this._container.appendChild(this._canvas);
- }
-
- public dispose(): void {
- this._canvas.remove();
- if (this._charAtlas) {
- this._charAtlas.dispose();
- }
+ this.register(this._themeService.onChangeColors(e => {
+ this._refreshCharAtlas(terminal, e);
+ this.reset(terminal);
+ }));
+ this.register(toDisposable(() => {
+ this._canvas.remove();
+ this._charAtlas?.dispose();
+ }));
}
private _initCanvas(): void {
@@ -56,16 +59,11 @@ export abstract class BaseRenderLayer implements IRenderLayer {
}
}
- public onOptionsChanged(terminal: Terminal): void {}
- public onBlur(terminal: Terminal): void {}
- public onFocus(terminal: Terminal): void {}
- public onCursorMove(terminal: Terminal): void {}
- public onGridChanged(terminal: Terminal, startRow: number, endRow: number): void {}
- public onSelectionChanged(terminal: Terminal, start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {}
-
- public setColors(terminal: Terminal, colorSet: IColorSet): void {
- this._refreshCharAtlas(terminal, colorSet);
- }
+ public handleBlur(terminal: Terminal): void {}
+ public handleFocus(terminal: Terminal): void {}
+ public handleCursorMove(terminal: Terminal): void {}
+ public handleGridChanged(terminal: Terminal, startRow: number, endRow: number): void {}
+ public handleSelectionChanged(terminal: Terminal, start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {}
protected _setTransparency(terminal: Terminal, alpha: boolean): void {
// Do nothing when alpha doesn't change
@@ -82,8 +80,8 @@ export abstract class BaseRenderLayer implements IRenderLayer {
this._container.replaceChild(this._canvas, oldCanvas);
// Regenerate char atlas and force a full redraw
- this._refreshCharAtlas(terminal, this._colors);
- this.onGridChanged(terminal, 0, terminal.rows - 1);
+ this._refreshCharAtlas(terminal, this._themeService.colors);
+ this.handleGridChanged(terminal, 0, terminal.rows - 1);
}
/**
@@ -91,11 +89,11 @@ export abstract class BaseRenderLayer implements IRenderLayer {
* @param terminal The terminal.
* @param colorSet The color set to use for the char atlas.
*/
- private _refreshCharAtlas(terminal: Terminal, colorSet: IColorSet): void {
+ private _refreshCharAtlas(terminal: Terminal, colorSet: ReadonlyColorSet): void {
if (this._scaledCharWidth <= 0 && this._scaledCharHeight <= 0) {
return;
}
- this._charAtlas = acquireCharAtlas(terminal, colorSet, this._scaledCellWidth, this._scaledCellHeight, this._scaledCharWidth, this._scaledCharHeight, this._coreBrowserService.dpr);
+ this._charAtlas = acquireTextureAtlas(terminal, colorSet, this._scaledCellWidth, this._scaledCellHeight, this._scaledCharWidth, this._scaledCharHeight, this._coreBrowserService.dpr);
this._charAtlas.warmUp();
}
@@ -116,7 +114,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
this._clearAll();
}
- this._refreshCharAtlas(terminal, this._colors);
+ this._refreshCharAtlas(terminal, this._themeService.colors);
}
public abstract reset(terminal: Terminal): void;
@@ -186,7 +184,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
if (this._alpha) {
this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
} else {
- this._ctx.fillStyle = this._colors.background.css;
+ this._ctx.fillStyle = this._themeService.colors.background.css;
this._ctx.fillRect(0, 0, this._canvas.width, this._canvas.height);
}
}
@@ -206,7 +204,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
width * this._scaledCellWidth,
height * this._scaledCellHeight);
} else {
- this._ctx.fillStyle = this._colors.background.css;
+ this._ctx.fillStyle = this._themeService.colors.background.css;
this._ctx.fillRect(
x * this._scaledCellWidth,
y * this._scaledCellHeight,
diff --git a/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts b/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts
index 2b274516..a6325dcb 100644
--- a/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts
+++ b/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts
@@ -7,11 +7,12 @@ import { Terminal } from 'xterm';
import { BaseRenderLayer } from './BaseRenderLayer';
import { ICellData } from 'common/Types';
import { CellData } from 'common/buffer/CellData';
-import { IColorSet } from 'browser/Types';
-import { IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/Types';
+import { IColorSet, ReadonlyColorSet } from 'browser/Types';
+import { IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/shared/Types';
import { IEventEmitter } from 'common/EventEmitter';
-import { ICoreBrowserService } from 'browser/services/Services';
-import { ICoreService } from 'common/services/Services';
+import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
+import { ICoreService, IOptionsService } from 'common/services/Services';
+import { toDisposable } from 'common/Lifecycle';
interface ICursorState {
x: number;
@@ -36,12 +37,13 @@ export class CursorRenderLayer extends BaseRenderLayer {
terminal: Terminal,
container: HTMLElement,
zIndex: number,
- colors: IColorSet,
private _onRequestRefreshRowsEvent: IEventEmitter,
coreBrowserService: ICoreBrowserService,
- private readonly _coreService: ICoreService
+ private readonly _coreService: ICoreService,
+ themeService: IThemeService,
+ optionsService: IOptionsService
) {
- super(container, 'cursor', zIndex, true, colors, coreBrowserService);
+ super(terminal, container, 'cursor', zIndex, true, coreBrowserService, themeService);
this._state = {
x: 0,
y: 0,
@@ -54,13 +56,12 @@ export class CursorRenderLayer extends BaseRenderLayer {
'block': this._renderBlockCursor.bind(this),
'underline': this._renderUnderlineCursor.bind(this)
};
- this.onOptionsChanged(terminal);
- }
-
- public override dispose(): void {
- this._cursorBlinkStateManager?.dispose();
- this._cursorBlinkStateManager = undefined;
- super.dispose();
+ this._handleOptionsChanged(terminal);
+ this.register(optionsService.onOptionChange(() => this._handleOptionsChanged(terminal)));
+ this.register(toDisposable(() => {
+ this._cursorBlinkStateManager?.dispose();
+ this._cursorBlinkStateManager = undefined;
+ }));
}
public resize(terminal: Terminal, dim: IRenderDimensions): void {
@@ -78,20 +79,20 @@ export class CursorRenderLayer extends BaseRenderLayer {
public reset(terminal: Terminal): void {
this._clearCursor();
this._cursorBlinkStateManager?.restartBlinkAnimation(terminal);
- this.onOptionsChanged(terminal);
+ this._handleOptionsChanged(terminal);
}
- public onBlur(terminal: Terminal): void {
+ public handleBlur(terminal: Terminal): void {
this._cursorBlinkStateManager?.pause();
this._onRequestRefreshRowsEvent.fire({ start: terminal.buffer.active.cursorY, end: terminal.buffer.active.cursorY });
}
- public onFocus(terminal: Terminal): void {
+ public handleFocus(terminal: Terminal): void {
this._cursorBlinkStateManager?.resume(terminal);
this._onRequestRefreshRowsEvent.fire({ start: terminal.buffer.active.cursorY, end: terminal.buffer.active.cursorY });
}
- public onOptionsChanged(terminal: Terminal): void {
+ private _handleOptionsChanged(terminal: Terminal): void {
if (terminal.options.cursorBlink) {
if (!this._cursorBlinkStateManager) {
this._cursorBlinkStateManager = new CursorBlinkStateManager(() => {
@@ -107,11 +108,11 @@ export class CursorRenderLayer extends BaseRenderLayer {
this._onRequestRefreshRowsEvent.fire({ start: terminal.buffer.active.cursorY, end: terminal.buffer.active.cursorY });
}
- public onCursorMove(terminal: Terminal): void {
+ public handleCursorMove(terminal: Terminal): void {
this._cursorBlinkStateManager?.restartBlinkAnimation(terminal);
}
- public onGridChanged(terminal: Terminal, startRow: number, endRow: number): void {
+ public handleGridChanged(terminal: Terminal, startRow: number, endRow: number): void {
if (!this._cursorBlinkStateManager || this._cursorBlinkStateManager.isPaused) {
this._render(terminal, false);
} else {
@@ -147,7 +148,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
if (!this._coreBrowserService.isFocused) {
this._clearCursor();
this._ctx.save();
- this._ctx.fillStyle = this._colors.cursor.css;
+ this._ctx.fillStyle = this._themeService.colors.cursor.css;
const cursorStyle = terminal.options.cursorStyle;
if (cursorStyle && cursorStyle !== 'block') {
this._cursorRenderers[cursorStyle](terminal, cursorX, viewportRelativeCursorY, this._cell);
@@ -212,30 +213,30 @@ export class CursorRenderLayer extends BaseRenderLayer {
private _renderBarCursor(terminal: Terminal, x: number, y: number, cell: ICellData): void {
this._ctx.save();
- this._ctx.fillStyle = this._colors.cursor.css;
+ this._ctx.fillStyle = this._themeService.colors.cursor.css;
this._fillLeftLineAtCell(x, y, terminal.options.cursorWidth);
this._ctx.restore();
}
private _renderBlockCursor(terminal: Terminal, x: number, y: number, cell: ICellData): void {
this._ctx.save();
- this._ctx.fillStyle = this._colors.cursor.css;
+ this._ctx.fillStyle = this._themeService.colors.cursor.css;
this._fillCells(x, y, cell.getWidth(), 1);
- this._ctx.fillStyle = this._colors.cursorAccent.css;
+ this._ctx.fillStyle = this._themeService.colors.cursorAccent.css;
this._fillCharTrueColor(terminal, cell, x, y);
this._ctx.restore();
}
private _renderUnderlineCursor(terminal: Terminal, x: number, y: number, cell: ICellData): void {
this._ctx.save();
- this._ctx.fillStyle = this._colors.cursor.css;
+ this._ctx.fillStyle = this._themeService.colors.cursor.css;
this._fillBottomLineAtCells(x, y);
this._ctx.restore();
}
private _renderBlurCursor(terminal: Terminal, x: number, y: number, cell: ICellData): void {
this._ctx.save();
- this._ctx.strokeStyle = this._colors.cursor.css;
+ this._ctx.strokeStyle = this._themeService.colors.cursor.css;
this._strokeRectAtCell(x, y, cell.getWidth(), 1);
this._ctx.restore();
}
diff --git a/addons/xterm-addon-webgl/src/renderLayer/LinkRenderLayer.ts b/addons/xterm-addon-webgl/src/renderLayer/LinkRenderLayer.ts
index e1ff08d8..77b02420 100644
--- a/addons/xterm-addon-webgl/src/renderLayer/LinkRenderLayer.ts
+++ b/addons/xterm-addon-webgl/src/renderLayer/LinkRenderLayer.ts
@@ -3,13 +3,13 @@
* @license MIT
*/
+import { is256Color } from 'browser/renderer/shared/CharAtlasUtils';
+import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/shared/Constants';
+import { IRenderDimensions } from 'browser/renderer/shared/Types';
+import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
+import { ILinkifier2, ILinkifierEvent } from 'browser/Types';
import { Terminal } from 'xterm';
import { BaseRenderLayer } from './BaseRenderLayer';
-import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/Constants';
-import { is256Color } from '../atlas/CharAtlasUtils';
-import { ITerminal, IColorSet, ILinkifierEvent } from 'browser/Types';
-import { IRenderDimensions } from 'browser/renderer/Types';
-import { ICoreBrowserService } from 'browser/services/Services';
export class LinkRenderLayer extends BaseRenderLayer {
private _state: ILinkifierEvent | undefined;
@@ -17,14 +17,15 @@ export class LinkRenderLayer extends BaseRenderLayer {
constructor(
container: HTMLElement,
zIndex: number,
- colors: IColorSet,
- terminal: ITerminal,
- coreBrowserService: ICoreBrowserService
+ terminal: Terminal,
+ linkifier2: ILinkifier2,
+ coreBrowserService: ICoreBrowserService,
+ themeService: IThemeService
) {
- super(container, 'link', zIndex, true, colors, coreBrowserService);
+ super(terminal, container, 'link', zIndex, true, coreBrowserService, themeService);
- terminal.linkifier2.onShowLinkUnderline(e => this._onShowLinkUnderline(e));
- terminal.linkifier2.onHideLinkUnderline(e => this._onHideLinkUnderline(e));
+ this.register(linkifier2.onShowLinkUnderline(e => this._handleShowLinkUnderline(e)));
+ this.register(linkifier2.onHideLinkUnderline(e => this._handleHideLinkUnderline(e)));
}
public resize(terminal: Terminal, dim: IRenderDimensions): void {
@@ -49,14 +50,14 @@ export class LinkRenderLayer extends BaseRenderLayer {
}
}
- private _onShowLinkUnderline(e: ILinkifierEvent): void {
+ private _handleShowLinkUnderline(e: ILinkifierEvent): void {
if (e.fg === INVERTED_DEFAULT_COLOR) {
- this._ctx.fillStyle = this._colors.background.css;
+ this._ctx.fillStyle = this._themeService.colors.background.css;
} else if (e.fg !== undefined && is256Color(e.fg)) {
// 256 color support
- this._ctx.fillStyle = this._colors.ansi[e.fg!].css;
+ this._ctx.fillStyle = this._themeService.colors.ansi[e.fg!].css;
} else {
- this._ctx.fillStyle = this._colors.foreground.css;
+ this._ctx.fillStyle = this._themeService.colors.foreground.css;
}
if (e.y1 === e.y2) {
@@ -73,7 +74,7 @@ export class LinkRenderLayer extends BaseRenderLayer {
this._state = e;
}
- private _onHideLinkUnderline(e: ILinkifierEvent): void {
+ private _handleHideLinkUnderline(e: ILinkifierEvent): void {
this._clearCurrentLink();
}
}
diff --git a/addons/xterm-addon-webgl/src/renderLayer/Types.ts b/addons/xterm-addon-webgl/src/renderLayer/Types.ts
index 70acff34..bad56091 100644
--- a/addons/xterm-addon-webgl/src/renderLayer/Types.ts
+++ b/addons/xterm-addon-webgl/src/renderLayer/Types.ts
@@ -4,45 +4,34 @@
*/
import { IDisposable, Terminal } from 'xterm';
-import { IColorSet } from 'browser/Types';
-import { IRenderDimensions } from 'browser/renderer/Types';
+import { IRenderDimensions } from 'browser/renderer/shared/Types';
export interface IRenderLayer extends IDisposable {
/**
* Called when the terminal loses focus.
*/
- onBlur(terminal: Terminal): void;
+ handleBlur(terminal: Terminal): void;
/**
* * Called when the terminal gets focus.
*/
- onFocus(terminal: Terminal): void;
+ handleFocus(terminal: Terminal): void;
/**
* Called when the cursor is moved.
*/
- onCursorMove(terminal: Terminal): void;
-
- /**
- * Called when options change.
- */
- onOptionsChanged(terminal: Terminal): void;
-
- /**
- * Called when the theme changes.
- */
- setColors(terminal: Terminal, colorSet: IColorSet): void;
+ handleCursorMove(terminal: Terminal): void;
/**
* Called when the data in the grid has changed (or needs to be rendered
* again).
*/
- onGridChanged(terminal: Terminal, startRow: number, endRow: number): void;
+ handleGridChanged(terminal: Terminal, startRow: number, endRow: number): void;
/**
* Calls when the selection changes.
*/
- onSelectionChanged(terminal: Terminal, start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void;
+ handleSelectionChanged(terminal: Terminal, start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void;
/**
* Registers a handler to join characters to render as a group
diff --git a/addons/xterm-addon-webgl/typings/xterm-addon-webgl.d.ts b/addons/xterm-addon-webgl/typings/xterm-addon-webgl.d.ts
index 74aed0cc..6865b6db 100644
--- a/addons/xterm-addon-webgl/typings/xterm-addon-webgl.d.ts
+++ b/addons/xterm-addon-webgl/typings/xterm-addon-webgl.d.ts
@@ -15,12 +15,12 @@ declare module 'xterm-addon-webgl' {
/**
* An event that is fired when the renderer loses its canvas context.
*/
- public get onContextLoss(): IEvent;
+ public readonly onContextLoss: IEvent;
/**
* An event that is fired when the texture atlas of the renderer changes.
*/
- public get onChangeTextureAtlas(): IEvent;
+ public readonly onChangeTextureAtlas: IEvent;
constructor(preserveDrawingBuffer?: boolean);
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index 1710922e..7b015064 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -9,7 +9,7 @@ trigger:
jobs:
- job: Linux
pool:
- vmImage: 'ubuntu-18.04'
+ vmImage: 'ubuntu-20.04'
steps:
- task: NodeTool@0
inputs:
diff --git a/demo/client.ts b/demo/client.ts
index 00b10b57..44a96eb3 100644
--- a/demo/client.ts
+++ b/demo/client.ts
@@ -36,15 +36,15 @@ import { Terminal as TerminalType, ITerminalOptions } from 'xterm';
export interface IWindowWithTerminal extends Window {
term: TerminalType;
- Terminal?: typeof TerminalType;
- AttachAddon?: typeof AttachAddon;
- FitAddon?: typeof FitAddon;
- SearchAddon?: typeof SearchAddon;
- SerializeAddon?: typeof SerializeAddon;
- WebLinksAddon?: typeof WebLinksAddon;
- WebglAddon?: typeof WebglAddon;
- Unicode11Addon?: typeof Unicode11Addon;
- LigaturesAddon?: typeof LigaturesAddon;
+ Terminal?: typeof TerminalType; // eslint-disable-line @typescript-eslint/naming-convention
+ AttachAddon?: typeof AttachAddon; // eslint-disable-line @typescript-eslint/naming-convention
+ FitAddon?: typeof FitAddon; // eslint-disable-line @typescript-eslint/naming-convention
+ SearchAddon?: typeof SearchAddon; // eslint-disable-line @typescript-eslint/naming-convention
+ SerializeAddon?: typeof SerializeAddon; // eslint-disable-line @typescript-eslint/naming-convention
+ WebLinksAddon?: typeof WebLinksAddon; // eslint-disable-line @typescript-eslint/naming-convention
+ WebglAddon?: typeof WebglAddon; // eslint-disable-line @typescript-eslint/naming-convention
+ Unicode11Addon?: typeof Unicode11Addon; // eslint-disable-line @typescript-eslint/naming-convention
+ LigaturesAddon?: typeof LigaturesAddon; // eslint-disable-line @typescript-eslint/naming-convention
}
declare let window: IWindowWithTerminal;
@@ -59,27 +59,29 @@ type AddonType = 'attach' | 'canvas' | 'fit' | 'search' | 'serialize' | 'unicode
interface IDemoAddon {
name: T;
canChange: boolean;
- ctor:
+ ctor: (
T extends 'attach' ? typeof AttachAddon :
- T extends 'canvas' ? typeof CanvasAddon :
- T extends 'fit' ? typeof FitAddon :
- T extends 'search' ? typeof SearchAddon :
- T extends 'serialize' ? typeof SerializeAddon :
- T extends 'web-links' ? typeof WebLinksAddon :
- T extends 'unicode11' ? typeof Unicode11Addon :
- T extends 'ligatures' ? typeof LigaturesAddon :
- typeof WebglAddon;
- instance?:
+ T extends 'canvas' ? typeof CanvasAddon :
+ T extends 'fit' ? typeof FitAddon :
+ T extends 'search' ? typeof SearchAddon :
+ T extends 'serialize' ? typeof SerializeAddon :
+ T extends 'web-links' ? typeof WebLinksAddon :
+ T extends 'unicode11' ? typeof Unicode11Addon :
+ T extends 'ligatures' ? typeof LigaturesAddon :
+ typeof WebglAddon
+ );
+ instance?: (
T extends 'attach' ? AttachAddon :
- T extends 'canvas' ? CanvasAddon :
- T extends 'fit' ? FitAddon :
- T extends 'search' ? SearchAddon :
- T extends 'serialize' ? SerializeAddon :
- T extends 'web-links' ? WebLinksAddon :
- T extends 'webgl' ? WebglAddon :
- T extends 'unicode11' ? typeof Unicode11Addon :
- T extends 'ligatures' ? typeof LigaturesAddon :
- never;
+ T extends 'canvas' ? CanvasAddon :
+ T extends 'fit' ? FitAddon :
+ T extends 'search' ? SearchAddon :
+ T extends 'serialize' ? SerializeAddon :
+ T extends 'web-links' ? WebLinksAddon :
+ T extends 'webgl' ? WebglAddon :
+ T extends 'unicode11' ? typeof Unicode11Addon :
+ T extends 'ligatures' ? typeof LigaturesAddon :
+ never
+ );
}
const addons: { [T in AddonType]: IDemoAddon } = {
@@ -96,12 +98,12 @@ const addons: { [T in AddonType]: IDemoAddon } = {
let terminalContainer = document.getElementById('terminal-container');
const actionElements = {
- find: document.querySelector('#find'),
- findNext: document.querySelector('#find-next'),
- findPrevious: document.querySelector('#find-previous'),
+ find: document.querySelector('#find') as HTMLInputElement,
+ findNext: document.querySelector('#find-next') as HTMLInputElement,
+ findPrevious: document.querySelector('#find-previous') as HTMLInputElement,
findResults: document.querySelector('#find-results')
};
-const paddingElement = document.getElementById('padding');
+const paddingElement = document.getElementById('padding') as HTMLInputElement;
const xtermjsTheme = {
foreground: '#F8F8F8',
@@ -146,7 +148,7 @@ function getSearchOptions(e: KeyboardEvent): ISearchOptions {
};
}
-const disposeRecreateButtonHandler = () => {
+const disposeRecreateButtonHandler: () => void = () => {
// If the terminal exists dispose of it, otherwise recreate it
if (term) {
term.dispose();
@@ -169,7 +171,7 @@ const disposeRecreateButtonHandler = () => {
}
};
-const createNewWindowButtonHandler = () => {
+const createNewWindowButtonHandler: () => void = () => {
if (term) {
disposeRecreateButtonHandler();
}
@@ -196,7 +198,7 @@ const createNewWindowButtonHandler = () => {
}
});
}
-}
+};
if (document.location.pathname === '/test') {
window.Terminal = Terminal;
@@ -220,6 +222,7 @@ if (document.location.pathname === '/test') {
document.getElementById('underline-test').addEventListener('click', underlineTest);
document.getElementById('ansi-colors').addEventListener('click', ansiColorsTest);
document.getElementById('osc-hyperlinks').addEventListener('click', addAnsiHyperlink);
+ document.getElementById('sgr-test').addEventListener('click', sgrTest);
document.getElementById('add-decoration').addEventListener('click', addDecoration);
document.getElementById('add-overview-ruler').addEventListener('click', addOverviewRuler);
}
@@ -267,14 +270,17 @@ function createTerminal(): void {
protocol = (location.protocol === 'https:') ? 'wss://' : 'ws://';
socketURL = protocol + location.hostname + ((location.port) ? (':' + location.port) : '') + '/terminals/';
- term.open(terminalContainer);
addons.fit.instance!.fit();
- try {
- typedTerm.loadAddon(addons.webgl.instance);
- setTimeout(() => {
+ typedTerm.loadAddon(addons.webgl.instance);
+ setTimeout(() => {
+ if (addons.webgl.instance !== undefined) {
addTextureAtlas(addons.webgl.instance.textureAtlas);
addons.webgl.instance.onChangeTextureAtlas(e => addTextureAtlas(e));
- }, 0);
+ }
+ }, 0);
+
+ try { // try-catch to allow the demo to load if webgl is not supported
+ term.open(terminalContainer);
}
catch {
addons.webgl.instance = undefined;
@@ -300,8 +306,8 @@ function createTerminal(): void {
setTimeout(() => {
initOptions(term);
// TODO: Clean this up, opt-cols/rows doesn't exist anymore
- (document.getElementById(`opt-cols`)).value = term.cols;
- (document.getElementById(`opt-rows`)).value = term.rows;
+ (document.getElementById(`opt-cols`) as HTMLInputElement).value = term.cols;
+ (document.getElementById(`opt-rows`) as HTMLInputElement).value = term.rows;
paddingElement.value = '0';
// Set terminal size again to set the specific dimensions on the demo
@@ -427,14 +433,14 @@ function initOptions(term: TerminalType): void {
// Attach listeners
booleanOptions.forEach(o => {
- const input = document.getElementById(`opt-${o}`);
+ const input = document.getElementById(`opt-${o}`) as HTMLInputElement;
addDomListener(input, 'change', () => {
console.log('change', o, input.checked);
term.options[o] = input.checked;
});
});
numberOptions.forEach(o => {
- const input = document.getElementById(`opt-${o}`);
+ const input = document.getElementById(`opt-${o}`) as HTMLInputElement;
addDomListener(input, 'change', () => {
console.log('change', o, input.value);
if (o === 'rows') {
@@ -456,7 +462,7 @@ function initOptions(term: TerminalType): void {
});
});
Object.keys(stringOptions).forEach(o => {
- const input = document.getElementById(`opt-${o}`);
+ const input = document.getElementById(`opt-${o}`) as HTMLInputElement;
addDomListener(input, 'change', () => {
console.log('change', o, input.value);
let value: any = input.value;
@@ -489,7 +495,7 @@ function initOptions(term: TerminalType): void {
magenta: '#b168df',
red: '#da6771',
white: '#efefef',
- yellow: '#fff099',
+ yellow: '#fff099'
};
break;
case 'light':
@@ -513,7 +519,7 @@ function initOptions(term: TerminalType): void {
magenta: '#bc05bc',
red: '#cd3131',
white: '#555555',
- yellow: '#949800',
+ yellow: '#949800'
};
break;
}
@@ -545,7 +551,15 @@ function initAddons(term: TerminalType): void {
try {
term.loadAddon(addon.instance);
if (name === 'webgl') {
- (addon.instance as WebglAddon).onChangeTextureAtlas(e => addTextureAtlas(e));
+ setTimeout(() => {
+ addTextureAtlas(addons.webgl.instance.textureAtlas);
+ addons.webgl.instance.onChangeTextureAtlas(e => addTextureAtlas(e));
+ }, 0);
+ } else if (name === 'canvas') {
+ setTimeout(() => {
+ addTextureAtlas(addons.canvas.instance.textureAtlas);
+ addons.canvas.instance.onChangeTextureAtlas(e => addTextureAtlas(e));
+ }, 0);
} else if (name === 'unicode11') {
term.unicode.activeVersion = '11';
} else if (name === 'search') {
@@ -559,7 +573,9 @@ function initAddons(term: TerminalType): void {
}
} else {
if (name === 'webgl') {
- (addon.instance as WebglAddon).textureAtlas.remove();
+ addons.webgl.instance.textureAtlas.remove();
+ } else if (name === 'canvas') {
+ addons.canvas.instance.textureAtlas.remove();
} else if (name === 'unicode11') {
term.unicode.activeVersion = '6';
}
@@ -584,7 +600,7 @@ function initAddons(term: TerminalType): void {
container.appendChild(fragment);
}
-function updateFindResults(e: { resultIndex: number, resultCount: number } | undefined) {
+function updateFindResults(e: { resultIndex: number, resultCount: number } | undefined): void {
let content: string;
if (e === undefined) {
content = 'undefined';
@@ -600,8 +616,8 @@ function addDomListener(element: HTMLElement, type: string, handler: (...args: a
}
function updateTerminalSize(): void {
- const cols = parseInt((document.getElementById(`opt-cols`)).value, 10);
- const rows = parseInt((document.getElementById(`opt-rows`)).value, 10);
+ const cols = parseInt((document.getElementById(`opt-cols`) as HTMLInputElement).value, 10);
+ const rows = parseInt((document.getElementById(`opt-rows`) as HTMLInputElement).value, 10);
const width = (cols * term._core._renderService.dimensions.actualCellWidth + term._core.viewport.scrollBarWidth).toString() + 'px';
const height = (rows * term._core._renderService.dimensions.actualCellHeight).toString() + 'px';
terminalContainer.style.width = width;
@@ -625,21 +641,21 @@ function htmlSerializeButtonHandler(): void {
document.getElementById('htmlserialize-output').innerText = output;
// Deprecated, but the most supported for now.
- function listener(e: any) {
- e.clipboardData.setData("text/html", output);
+ function listener(e: any): void {
+ e.clipboardData.setData('text/html', output);
e.preventDefault();
}
- document.addEventListener("copy", listener);
- document.execCommand("copy");
- document.removeEventListener("copy", listener);
- document.getElementById("htmlserialize-output-result").innerText = "Copied to clipboard";
+ document.addEventListener('copy', listener);
+ document.execCommand('copy');
+ document.removeEventListener('copy', listener);
+ document.getElementById('htmlserialize-output-result').innerText = 'Copied to clipboard';
}
-function addTextureAtlas(e: HTMLCanvasElement) {
+function addTextureAtlas(e: HTMLCanvasElement): void {
document.querySelector('#texture-atlas').replaceChildren(e);
}
-function writeCustomGlyphHandler() {
+function writeCustomGlyphHandler(): void {
term.write('\n\r');
term.write('\n\r');
term.write('Box styles: ┎┰┒┍┯┑╓╥╖╒╤╕ ┏┳┓┌┲┓┌┬┐┏┱┐\n\r');
@@ -684,7 +700,7 @@ function writeCustomGlyphHandler() {
window.scrollTo(0, 0);
}
-function loadTest() {
+function loadTest(): void {
const rendererName = addons.webgl.instance ? 'webgl' : !!addons.canvas.instance ? 'canvas' : 'dom';
const testData = [];
let byteCount = 0;
@@ -717,7 +733,7 @@ function loadTest() {
});
}
-function powerlineSymbolTest() {
+function powerlineSymbolTest(): void {
function s(char: string): string {
return `${char} \x1b[7m${char}\x1b[0m `;
}
@@ -791,7 +807,7 @@ function powerlineSymbolTest() {
term.writeln('nf-mdi-github_face (\\uFbd9) \ufbd9');
}
-function underlineTest() {
+function underlineTest(): void {
function u(style: number): string {
return `\x1b[4:${style}m`;
}
@@ -801,7 +817,7 @@ function underlineTest() {
term.write('\n\n\r');
term.writeln('Underline styles:');
term.writeln('');
- function showSequence(id: number, name: string) {
+ function showSequence(id: number, name: string): string {
let alphabet = '';
for (let i = 97; i < 123; i++) {
alphabet += String.fromCharCode(i);
@@ -848,7 +864,7 @@ function underlineTest() {
term.write('\x1b[0m\n\r');
}
-function ansiColorsTest() {
+function ansiColorsTest(): void {
term.writeln(`\x1b[0m\n\n\rStandard colors: Bright colors:`);
for (let i = 0; i < 16; i++) {
term.write(`\x1b[48;5;${i}m ${i.toString().padEnd(2, ' ').padStart(3, ' ')} \x1b[0m`);
@@ -873,7 +889,66 @@ function ansiColorsTest() {
}
}
-function addAnsiHyperlink() {
+function writeTestString(): string {
+ let alphabet = '';
+ for (let i = 97; i < 123; i++) {
+ alphabet += String.fromCharCode(i);
+ }
+ let numbers = '';
+ for (let i = 0; i < 10; i++) {
+ numbers += i.toString();
+ }
+ return `${alphabet} ${numbers} 汉语 한국어 👽`;
+}
+const testString = writeTestString();
+
+function sgrTest(): void {
+ term.write('\n\n\r');
+ term.writeln(`Character Attributes (SGR, Select Graphic Rendition)`);
+ const entries: { ps: number, name: string }[] = [
+ { ps: 0, name: 'Normal' },
+ { ps: 1, name: 'Bold' },
+ { ps: 2, name: 'Faint/dim' },
+ { ps: 3, name: 'Italicized' },
+ { ps: 4, name: 'Underlined' },
+ { ps: 5, name: 'Blink' },
+ { ps: 7, name: 'Inverse' },
+ { ps: 8, name: 'Invisible' },
+ { ps: 9, name: 'Crossed-out characters' },
+ { ps: 21, name: 'Doubly-underlined' },
+ { ps: 22, name: 'Normal' },
+ { ps: 23, name: 'Not italicized' },
+ { ps: 24, name: 'Not underlined' },
+ { ps: 25, name: 'Steady (not blink)' },
+ { ps: 27, name: 'Positive (not inverse)' },
+ { ps: 28, name: 'Visible (not hidden)' },
+ { ps: 29, name: 'Not crossed-out' },
+ { ps: 30, name: 'Foreground Black' },
+ { ps: 31, name: 'Foreground Red' },
+ { ps: 32, name: 'Foreground Green' },
+ { ps: 33, name: 'Foreground Yellow' },
+ { ps: 34, name: 'Foreground Blue' },
+ { ps: 35, name: 'Foreground Magenta' },
+ { ps: 36, name: 'Foreground Cyan' },
+ { ps: 37, name: 'Foreground White' },
+ { ps: 39, name: 'Foreground default' },
+ { ps: 40, name: 'Background Black' },
+ { ps: 41, name: 'Background Red' },
+ { ps: 42, name: 'Background Green' },
+ { ps: 43, name: 'Background Yellow' },
+ { ps: 44, name: 'Background Blue' },
+ { ps: 45, name: 'Background Magenta' },
+ { ps: 46, name: 'Background Cyan' },
+ { ps: 47, name: 'Background White' },
+ { ps: 49, name: 'Background default' }
+ ];
+ const maxNameLength = entries.reduce((p, c) => Math.max(c.name.length, p), 0);
+ for (const e of entries) {
+ term.writeln(`\x1b[0m\x1b[${e.ps}m ${e.ps.toString().padEnd(2, ' ')} ${e.name.padEnd(maxNameLength, ' ')} - ${testString}\x1b[0m`);
+ }
+}
+
+function addAnsiHyperlink(): void {
term.write('\n\n\r');
term.writeln(`Regular link with no id:`);
term.writeln('\x1b]8;;https://github.com\x07GitHub\x1b]8;;\x07');
@@ -893,7 +968,7 @@ function addAnsiHyperlink() {
term.write('\x1b[3A\x1b[1C\x1b]8;;https://xtermjs.org\x07xter\x1b[B\x1b[4Dm.js\x1b]8;;\x07\x1b[2B\x1b[5D');
}
-function addDecoration() {
+function addDecoration(): void {
term.options['overviewRulerWidth'] = 15;
const marker = term.registerMarker(1);
const decoration = term.registerDecoration({
@@ -908,7 +983,7 @@ function addDecoration() {
});
}
-function addOverviewRuler() {
+function addOverviewRuler(): void {
term.options['overviewRulerWidth'] = 15;
term.registerDecoration({ marker: term.registerMarker(1), overviewRulerOptions: { color: '#ef2929' } });
term.registerDecoration({ marker: term.registerMarker(3), overviewRulerOptions: { color: '#8ae234' } });
diff --git a/demo/index.html b/demo/index.html
index 41289221..a65b8bbc 100644
--- a/demo/index.html
+++ b/demo/index.html
@@ -79,6 +79,7 @@
+
diff --git a/demo/server.js b/demo/server.js
index 8bb684a2..0e82f9e9 100644
--- a/demo/server.js
+++ b/demo/server.js
@@ -16,8 +16,7 @@ function startServer() {
var app = express();
expressWs(app);
- var terminals = {},
- logs = {};
+ var terminals = {};
app.use('/xterm.css', express.static(__dirname + '/../css/xterm.css'));
app.get('/logo.png', (req, res) => {
@@ -55,10 +54,6 @@ function startServer() {
console.log('Created terminal with PID: ' + term.pid);
terminals[term.pid] = term;
- logs[term.pid] = '';
- term.on('data', function(data) {
- logs[term.pid] += data;
- });
res.send(term.pid.toString());
res.end();
});
@@ -77,16 +72,26 @@ function startServer() {
app.ws('/terminals/:pid', function (ws, req) {
var term = terminals[parseInt(req.params.pid)];
console.log('Connected to terminal ' + term.pid);
- ws.send(logs[term.pid]);
+
+ // unbuffered delivery after user input
+ let userInput = false;
// string message buffering
- function buffer(socket, timeout) {
+ function buffer(socket, timeout, maxSize) {
let s = '';
let sender = null;
return (data) => {
s += data;
- if (!sender) {
- sender = queueMicrotask(() => {
+ if (s.length > maxSize || userInput) {
+ userInput = false;
+ socket.send(s);
+ s = '';
+ if (sender) {
+ clearTimeout(sender);
+ sender = null;
+ }
+ } else if (!sender) {
+ sender = setTimeout(() => {
socket.send(s);
s = '';
sender = null;
@@ -95,15 +100,24 @@ function startServer() {
};
}
// binary message buffering
- function bufferUtf8(socket, timeout) {
+ function bufferUtf8(socket, timeout, maxSize) {
let buffer = [];
let sender = null;
let length = 0;
return (data) => {
buffer.push(data);
length += data.length;
- if (!sender) {
- sender = queueMicrotask(() => {
+ if (length > maxSize || userInput) {
+ userInput = false;
+ socket.send(Buffer.concat(buffer, length));
+ buffer = [];
+ length = 0;
+ if (sender) {
+ clearTimeout(sender);
+ sender = null;
+ }
+ } else if (!sender) {
+ sender = setTimeout(() => {
socket.send(Buffer.concat(buffer, length));
buffer = [];
sender = null;
@@ -112,27 +126,23 @@ function startServer() {
}
};
}
- const send = USE_BINARY ? bufferUtf8(ws, 5) : buffer(ws, 5);
+ const send = (USE_BINARY ? bufferUtf8 : buffer)(ws, 5, 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
// the problem and how to implement flow control at https://xtermjs.org/docs/guides/flowcontrol/
term.on('data', function(data) {
- try {
- send(data);
- } catch (ex) {
- // The WebSocket is not open, ignore
- }
+ send(data);
});
ws.on('message', function(msg) {
term.write(msg);
+ userInput = true;
});
ws.on('close', function () {
term.kill();
console.log('Closed terminal ' + term.pid);
// Clean things up
delete terminals[term.pid];
- delete logs[term.pid];
});
});
diff --git a/src/browser/AccessibilityManager.ts b/src/browser/AccessibilityManager.ts
index eba283d5..d0c9f601 100644
--- a/src/browser/AccessibilityManager.ts
+++ b/src/browser/AccessibilityManager.ts
@@ -9,7 +9,7 @@ import { IBuffer } from 'common/buffer/Types';
import { isMac } from 'common/Platform';
import { TimeBasedDebouncer } from 'browser/TimeBasedDebouncer';
import { addDisposableDomListener } from 'browser/Lifecycle';
-import { Disposable } from 'common/Lifecycle';
+import { Disposable, toDisposable } from 'common/Lifecycle';
import { ScreenDprMonitor } from 'browser/ScreenDprMonitor';
import { IRenderService } from 'browser/services/Services';
import { removeElementFromParent } from 'browser/Dom';
@@ -65,8 +65,8 @@ export class AccessibilityManager extends Disposable {
this._rowContainer.appendChild(this._rowElements[i]);
}
- this._topBoundaryFocusListener = e => this._onBoundaryFocus(e, BoundaryPosition.TOP);
- this._bottomBoundaryFocusListener = e => this._onBoundaryFocus(e, BoundaryPosition.BOTTOM);
+ 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);
@@ -87,14 +87,14 @@ export class AccessibilityManager extends Disposable {
this._terminal.element.insertAdjacentElement('afterbegin', this._accessibilityTreeRoot);
this.register(this._renderRowsDebouncer);
- this.register(this._terminal.onResize(e => this._onResize(e.rows)));
+ 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
- this.register(this._terminal.onA11yChar(char => this._onChar(char)));
- this.register(this._terminal.onLineFeed(() => this._onChar('\n')));
- this.register(this._terminal.onA11yTab(spaceCount => this._onTab(spaceCount)));
- this.register(this._terminal.onKey(e => this._onKey(e.key)));
+ this.register(this._terminal.onA11yChar(char => this._handleChar(char)));
+ this.register(this._terminal.onLineFeed(() => this._handleChar('\n')));
+ 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(this._renderService.onDimensionsChange(() => this._refreshRowsDimensions()));
@@ -104,15 +104,13 @@ export class AccessibilityManager extends Disposable {
// 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.register(toDisposable(() => {
+ removeElementFromParent(this._accessibilityTreeRoot);
+ this._rowElements.length = 0;
+ }));
}
- public dispose(): void {
- super.dispose();
- removeElementFromParent(this._accessibilityTreeRoot);
- this._rowElements.length = 0;
- }
-
- private _onBoundaryFocus(e: FocusEvent, position: BoundaryPosition): void {
+ 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];
@@ -172,7 +170,7 @@ export class AccessibilityManager extends Disposable {
e.stopImmediatePropagation();
}
- private _onResize(rows: number): void {
+ private _handleResize(rows: number): void {
// Remove bottom boundary listener
this._rowElements[this._rowElements.length - 1].removeEventListener('focus', this._bottomBoundaryFocusListener);
@@ -200,13 +198,13 @@ export class AccessibilityManager extends Disposable {
return element;
}
- private _onTab(spaceCount: number): void {
+ private _handleTab(spaceCount: number): void {
for (let i = 0; i < spaceCount; i++) {
- this._onChar(' ');
+ this._handleChar(' ');
}
}
- private _onChar(char: string): void {
+ private _handleChar(char: string): void {
if (this._liveRegionLineCount < MAX_ROWS_TO_READ + 1) {
if (this._charsToConsume.length > 0) {
// Have the screen reader ignore the char if it was just input
@@ -246,7 +244,7 @@ export class AccessibilityManager extends Disposable {
}
}
- private _onKey(keyChar: string): void {
+ private _handleKey(keyChar: string): void {
this._clearLiveRegion();
this._charsToConsume.push(keyChar);
}
@@ -280,7 +278,7 @@ export class AccessibilityManager extends Disposable {
return;
}
if (this._rowElements.length !== this._terminal.rows) {
- this._onResize(this._terminal.rows);
+ this._handleResize(this._terminal.rows);
}
for (let i = 0; i < this._terminal.rows; i++) {
this._refreshRowDimensions(this._rowElements[i]);
diff --git a/src/browser/ColorManager.test.ts b/src/browser/ColorManager.test.ts
deleted file mode 100644
index cf60a1f5..00000000
--- a/src/browser/ColorManager.test.ts
+++ /dev/null
@@ -1,366 +0,0 @@
-/**
- * Copyright (c) 2017 The xterm.js authors. All rights reserved.
- * @license MIT
- */
-
-import jsdom = require('jsdom');
-import { assert } from 'chai';
-import { ColorManager, DEFAULT_ANSI_COLORS } from 'browser/ColorManager';
-
-describe('ColorManager', () => {
- let cm: ColorManager;
- let dom: jsdom.JSDOM;
- let document: Document;
- let window: jsdom.DOMWindow;
-
- beforeEach(() => {
- dom = new jsdom.JSDOM('');
- window = dom.window;
- document = window.document;
- (window as any).HTMLCanvasElement.prototype.getContext = () => ({
- createLinearGradient(): any {
- return null;
- },
-
- fillRect(): void { },
-
- getImageData(): any {
- return {data: [0, 0, 0, 0xFF]};
- }
- });
- cm = new ColorManager();
- });
-
- describe('constructor', () => {
- it('should fill all colors with values', () => {
- for (const key of Object.keys(cm.colors)) {
- if (key !== 'ansi' && key !== 'contrastCache' && key !== 'selectionForeground') {
- // A #rrggbb or rgba(...)
- assert.ok((cm.colors as any)[key].css.length >= 7);
- }
- }
- assert.equal(cm.colors.ansi.length, 256);
- });
-
- it('should fill 240 colors with expected values', () => {
- assert.equal(cm.colors.ansi[16].css, '#000000');
- assert.equal(cm.colors.ansi[17].css, '#00005f');
- assert.equal(cm.colors.ansi[18].css, '#000087');
- assert.equal(cm.colors.ansi[19].css, '#0000af');
- assert.equal(cm.colors.ansi[20].css, '#0000d7');
- assert.equal(cm.colors.ansi[21].css, '#0000ff');
- assert.equal(cm.colors.ansi[22].css, '#005f00');
- assert.equal(cm.colors.ansi[23].css, '#005f5f');
- assert.equal(cm.colors.ansi[24].css, '#005f87');
- assert.equal(cm.colors.ansi[25].css, '#005faf');
- assert.equal(cm.colors.ansi[26].css, '#005fd7');
- assert.equal(cm.colors.ansi[27].css, '#005fff');
- assert.equal(cm.colors.ansi[28].css, '#008700');
- assert.equal(cm.colors.ansi[29].css, '#00875f');
- assert.equal(cm.colors.ansi[30].css, '#008787');
- assert.equal(cm.colors.ansi[31].css, '#0087af');
- assert.equal(cm.colors.ansi[32].css, '#0087d7');
- assert.equal(cm.colors.ansi[33].css, '#0087ff');
- assert.equal(cm.colors.ansi[34].css, '#00af00');
- assert.equal(cm.colors.ansi[35].css, '#00af5f');
- assert.equal(cm.colors.ansi[36].css, '#00af87');
- assert.equal(cm.colors.ansi[37].css, '#00afaf');
- assert.equal(cm.colors.ansi[38].css, '#00afd7');
- assert.equal(cm.colors.ansi[39].css, '#00afff');
- assert.equal(cm.colors.ansi[40].css, '#00d700');
- assert.equal(cm.colors.ansi[41].css, '#00d75f');
- assert.equal(cm.colors.ansi[42].css, '#00d787');
- assert.equal(cm.colors.ansi[43].css, '#00d7af');
- assert.equal(cm.colors.ansi[44].css, '#00d7d7');
- assert.equal(cm.colors.ansi[45].css, '#00d7ff');
- assert.equal(cm.colors.ansi[46].css, '#00ff00');
- assert.equal(cm.colors.ansi[47].css, '#00ff5f');
- assert.equal(cm.colors.ansi[48].css, '#00ff87');
- assert.equal(cm.colors.ansi[49].css, '#00ffaf');
- assert.equal(cm.colors.ansi[50].css, '#00ffd7');
- assert.equal(cm.colors.ansi[51].css, '#00ffff');
- assert.equal(cm.colors.ansi[52].css, '#5f0000');
- assert.equal(cm.colors.ansi[53].css, '#5f005f');
- assert.equal(cm.colors.ansi[54].css, '#5f0087');
- assert.equal(cm.colors.ansi[55].css, '#5f00af');
- assert.equal(cm.colors.ansi[56].css, '#5f00d7');
- assert.equal(cm.colors.ansi[57].css, '#5f00ff');
- assert.equal(cm.colors.ansi[58].css, '#5f5f00');
- assert.equal(cm.colors.ansi[59].css, '#5f5f5f');
- assert.equal(cm.colors.ansi[60].css, '#5f5f87');
- assert.equal(cm.colors.ansi[61].css, '#5f5faf');
- assert.equal(cm.colors.ansi[62].css, '#5f5fd7');
- assert.equal(cm.colors.ansi[63].css, '#5f5fff');
- assert.equal(cm.colors.ansi[64].css, '#5f8700');
- assert.equal(cm.colors.ansi[65].css, '#5f875f');
- assert.equal(cm.colors.ansi[66].css, '#5f8787');
- assert.equal(cm.colors.ansi[67].css, '#5f87af');
- assert.equal(cm.colors.ansi[68].css, '#5f87d7');
- assert.equal(cm.colors.ansi[69].css, '#5f87ff');
- assert.equal(cm.colors.ansi[70].css, '#5faf00');
- assert.equal(cm.colors.ansi[71].css, '#5faf5f');
- assert.equal(cm.colors.ansi[72].css, '#5faf87');
- assert.equal(cm.colors.ansi[73].css, '#5fafaf');
- assert.equal(cm.colors.ansi[74].css, '#5fafd7');
- assert.equal(cm.colors.ansi[75].css, '#5fafff');
- assert.equal(cm.colors.ansi[76].css, '#5fd700');
- assert.equal(cm.colors.ansi[77].css, '#5fd75f');
- assert.equal(cm.colors.ansi[78].css, '#5fd787');
- assert.equal(cm.colors.ansi[79].css, '#5fd7af');
- assert.equal(cm.colors.ansi[80].css, '#5fd7d7');
- assert.equal(cm.colors.ansi[81].css, '#5fd7ff');
- assert.equal(cm.colors.ansi[82].css, '#5fff00');
- assert.equal(cm.colors.ansi[83].css, '#5fff5f');
- assert.equal(cm.colors.ansi[84].css, '#5fff87');
- assert.equal(cm.colors.ansi[85].css, '#5fffaf');
- assert.equal(cm.colors.ansi[86].css, '#5fffd7');
- assert.equal(cm.colors.ansi[87].css, '#5fffff');
- assert.equal(cm.colors.ansi[88].css, '#870000');
- assert.equal(cm.colors.ansi[89].css, '#87005f');
- assert.equal(cm.colors.ansi[90].css, '#870087');
- assert.equal(cm.colors.ansi[91].css, '#8700af');
- assert.equal(cm.colors.ansi[92].css, '#8700d7');
- assert.equal(cm.colors.ansi[93].css, '#8700ff');
- assert.equal(cm.colors.ansi[94].css, '#875f00');
- assert.equal(cm.colors.ansi[95].css, '#875f5f');
- assert.equal(cm.colors.ansi[96].css, '#875f87');
- assert.equal(cm.colors.ansi[97].css, '#875faf');
- assert.equal(cm.colors.ansi[98].css, '#875fd7');
- assert.equal(cm.colors.ansi[99].css, '#875fff');
- assert.equal(cm.colors.ansi[100].css, '#878700');
- assert.equal(cm.colors.ansi[101].css, '#87875f');
- assert.equal(cm.colors.ansi[102].css, '#878787');
- assert.equal(cm.colors.ansi[103].css, '#8787af');
- assert.equal(cm.colors.ansi[104].css, '#8787d7');
- assert.equal(cm.colors.ansi[105].css, '#8787ff');
- assert.equal(cm.colors.ansi[106].css, '#87af00');
- assert.equal(cm.colors.ansi[107].css, '#87af5f');
- assert.equal(cm.colors.ansi[108].css, '#87af87');
- assert.equal(cm.colors.ansi[109].css, '#87afaf');
- assert.equal(cm.colors.ansi[110].css, '#87afd7');
- assert.equal(cm.colors.ansi[111].css, '#87afff');
- assert.equal(cm.colors.ansi[112].css, '#87d700');
- assert.equal(cm.colors.ansi[113].css, '#87d75f');
- assert.equal(cm.colors.ansi[114].css, '#87d787');
- assert.equal(cm.colors.ansi[115].css, '#87d7af');
- assert.equal(cm.colors.ansi[116].css, '#87d7d7');
- assert.equal(cm.colors.ansi[117].css, '#87d7ff');
- assert.equal(cm.colors.ansi[118].css, '#87ff00');
- assert.equal(cm.colors.ansi[119].css, '#87ff5f');
- assert.equal(cm.colors.ansi[120].css, '#87ff87');
- assert.equal(cm.colors.ansi[121].css, '#87ffaf');
- assert.equal(cm.colors.ansi[122].css, '#87ffd7');
- assert.equal(cm.colors.ansi[123].css, '#87ffff');
- assert.equal(cm.colors.ansi[124].css, '#af0000');
- assert.equal(cm.colors.ansi[125].css, '#af005f');
- assert.equal(cm.colors.ansi[126].css, '#af0087');
- assert.equal(cm.colors.ansi[127].css, '#af00af');
- assert.equal(cm.colors.ansi[128].css, '#af00d7');
- assert.equal(cm.colors.ansi[129].css, '#af00ff');
- assert.equal(cm.colors.ansi[130].css, '#af5f00');
- assert.equal(cm.colors.ansi[131].css, '#af5f5f');
- assert.equal(cm.colors.ansi[132].css, '#af5f87');
- assert.equal(cm.colors.ansi[133].css, '#af5faf');
- assert.equal(cm.colors.ansi[134].css, '#af5fd7');
- assert.equal(cm.colors.ansi[135].css, '#af5fff');
- assert.equal(cm.colors.ansi[136].css, '#af8700');
- assert.equal(cm.colors.ansi[137].css, '#af875f');
- assert.equal(cm.colors.ansi[138].css, '#af8787');
- assert.equal(cm.colors.ansi[139].css, '#af87af');
- assert.equal(cm.colors.ansi[140].css, '#af87d7');
- assert.equal(cm.colors.ansi[141].css, '#af87ff');
- assert.equal(cm.colors.ansi[142].css, '#afaf00');
- assert.equal(cm.colors.ansi[143].css, '#afaf5f');
- assert.equal(cm.colors.ansi[144].css, '#afaf87');
- assert.equal(cm.colors.ansi[145].css, '#afafaf');
- assert.equal(cm.colors.ansi[146].css, '#afafd7');
- assert.equal(cm.colors.ansi[147].css, '#afafff');
- assert.equal(cm.colors.ansi[148].css, '#afd700');
- assert.equal(cm.colors.ansi[149].css, '#afd75f');
- assert.equal(cm.colors.ansi[150].css, '#afd787');
- assert.equal(cm.colors.ansi[151].css, '#afd7af');
- assert.equal(cm.colors.ansi[152].css, '#afd7d7');
- assert.equal(cm.colors.ansi[153].css, '#afd7ff');
- assert.equal(cm.colors.ansi[154].css, '#afff00');
- assert.equal(cm.colors.ansi[155].css, '#afff5f');
- assert.equal(cm.colors.ansi[156].css, '#afff87');
- assert.equal(cm.colors.ansi[157].css, '#afffaf');
- assert.equal(cm.colors.ansi[158].css, '#afffd7');
- assert.equal(cm.colors.ansi[159].css, '#afffff');
- assert.equal(cm.colors.ansi[160].css, '#d70000');
- assert.equal(cm.colors.ansi[161].css, '#d7005f');
- assert.equal(cm.colors.ansi[162].css, '#d70087');
- assert.equal(cm.colors.ansi[163].css, '#d700af');
- assert.equal(cm.colors.ansi[164].css, '#d700d7');
- assert.equal(cm.colors.ansi[165].css, '#d700ff');
- assert.equal(cm.colors.ansi[166].css, '#d75f00');
- assert.equal(cm.colors.ansi[167].css, '#d75f5f');
- assert.equal(cm.colors.ansi[168].css, '#d75f87');
- assert.equal(cm.colors.ansi[169].css, '#d75faf');
- assert.equal(cm.colors.ansi[170].css, '#d75fd7');
- assert.equal(cm.colors.ansi[171].css, '#d75fff');
- assert.equal(cm.colors.ansi[172].css, '#d78700');
- assert.equal(cm.colors.ansi[173].css, '#d7875f');
- assert.equal(cm.colors.ansi[174].css, '#d78787');
- assert.equal(cm.colors.ansi[175].css, '#d787af');
- assert.equal(cm.colors.ansi[176].css, '#d787d7');
- assert.equal(cm.colors.ansi[177].css, '#d787ff');
- assert.equal(cm.colors.ansi[178].css, '#d7af00');
- assert.equal(cm.colors.ansi[179].css, '#d7af5f');
- assert.equal(cm.colors.ansi[180].css, '#d7af87');
- assert.equal(cm.colors.ansi[181].css, '#d7afaf');
- assert.equal(cm.colors.ansi[182].css, '#d7afd7');
- assert.equal(cm.colors.ansi[183].css, '#d7afff');
- assert.equal(cm.colors.ansi[184].css, '#d7d700');
- assert.equal(cm.colors.ansi[185].css, '#d7d75f');
- assert.equal(cm.colors.ansi[186].css, '#d7d787');
- assert.equal(cm.colors.ansi[187].css, '#d7d7af');
- assert.equal(cm.colors.ansi[188].css, '#d7d7d7');
- assert.equal(cm.colors.ansi[189].css, '#d7d7ff');
- assert.equal(cm.colors.ansi[190].css, '#d7ff00');
- assert.equal(cm.colors.ansi[191].css, '#d7ff5f');
- assert.equal(cm.colors.ansi[192].css, '#d7ff87');
- assert.equal(cm.colors.ansi[193].css, '#d7ffaf');
- assert.equal(cm.colors.ansi[194].css, '#d7ffd7');
- assert.equal(cm.colors.ansi[195].css, '#d7ffff');
- assert.equal(cm.colors.ansi[196].css, '#ff0000');
- assert.equal(cm.colors.ansi[197].css, '#ff005f');
- assert.equal(cm.colors.ansi[198].css, '#ff0087');
- assert.equal(cm.colors.ansi[199].css, '#ff00af');
- assert.equal(cm.colors.ansi[200].css, '#ff00d7');
- assert.equal(cm.colors.ansi[201].css, '#ff00ff');
- assert.equal(cm.colors.ansi[202].css, '#ff5f00');
- assert.equal(cm.colors.ansi[203].css, '#ff5f5f');
- assert.equal(cm.colors.ansi[204].css, '#ff5f87');
- assert.equal(cm.colors.ansi[205].css, '#ff5faf');
- assert.equal(cm.colors.ansi[206].css, '#ff5fd7');
- assert.equal(cm.colors.ansi[207].css, '#ff5fff');
- assert.equal(cm.colors.ansi[208].css, '#ff8700');
- assert.equal(cm.colors.ansi[209].css, '#ff875f');
- assert.equal(cm.colors.ansi[210].css, '#ff8787');
- assert.equal(cm.colors.ansi[211].css, '#ff87af');
- assert.equal(cm.colors.ansi[212].css, '#ff87d7');
- assert.equal(cm.colors.ansi[213].css, '#ff87ff');
- assert.equal(cm.colors.ansi[214].css, '#ffaf00');
- assert.equal(cm.colors.ansi[215].css, '#ffaf5f');
- assert.equal(cm.colors.ansi[216].css, '#ffaf87');
- assert.equal(cm.colors.ansi[217].css, '#ffafaf');
- assert.equal(cm.colors.ansi[218].css, '#ffafd7');
- assert.equal(cm.colors.ansi[219].css, '#ffafff');
- assert.equal(cm.colors.ansi[220].css, '#ffd700');
- assert.equal(cm.colors.ansi[221].css, '#ffd75f');
- assert.equal(cm.colors.ansi[222].css, '#ffd787');
- assert.equal(cm.colors.ansi[223].css, '#ffd7af');
- assert.equal(cm.colors.ansi[224].css, '#ffd7d7');
- assert.equal(cm.colors.ansi[225].css, '#ffd7ff');
- assert.equal(cm.colors.ansi[226].css, '#ffff00');
- assert.equal(cm.colors.ansi[227].css, '#ffff5f');
- assert.equal(cm.colors.ansi[228].css, '#ffff87');
- assert.equal(cm.colors.ansi[229].css, '#ffffaf');
- assert.equal(cm.colors.ansi[230].css, '#ffffd7');
- assert.equal(cm.colors.ansi[231].css, '#ffffff');
- assert.equal(cm.colors.ansi[232].css, '#080808');
- assert.equal(cm.colors.ansi[233].css, '#121212');
- assert.equal(cm.colors.ansi[234].css, '#1c1c1c');
- assert.equal(cm.colors.ansi[235].css, '#262626');
- assert.equal(cm.colors.ansi[236].css, '#303030');
- assert.equal(cm.colors.ansi[237].css, '#3a3a3a');
- assert.equal(cm.colors.ansi[238].css, '#444444');
- assert.equal(cm.colors.ansi[239].css, '#4e4e4e');
- assert.equal(cm.colors.ansi[240].css, '#585858');
- assert.equal(cm.colors.ansi[241].css, '#626262');
- assert.equal(cm.colors.ansi[242].css, '#6c6c6c');
- assert.equal(cm.colors.ansi[243].css, '#767676');
- assert.equal(cm.colors.ansi[244].css, '#808080');
- assert.equal(cm.colors.ansi[245].css, '#8a8a8a');
- assert.equal(cm.colors.ansi[246].css, '#949494');
- assert.equal(cm.colors.ansi[247].css, '#9e9e9e');
- assert.equal(cm.colors.ansi[248].css, '#a8a8a8');
- assert.equal(cm.colors.ansi[249].css, '#b2b2b2');
- assert.equal(cm.colors.ansi[250].css, '#bcbcbc');
- assert.equal(cm.colors.ansi[251].css, '#c6c6c6');
- assert.equal(cm.colors.ansi[252].css, '#d0d0d0');
- assert.equal(cm.colors.ansi[253].css, '#dadada');
- assert.equal(cm.colors.ansi[254].css, '#e4e4e4');
- assert.equal(cm.colors.ansi[255].css, '#eeeeee');
- });
- });
-
- describe('setTheme', () => {
- it('should not throw when not setting all colors', () => {
- assert.doesNotThrow(() => {
- cm.setTheme({});
- });
- });
-
- it('should set a partial set of colors, using the default if not present', () => {
- assert.equal(cm.colors.background.css, '#000000');
- assert.equal(cm.colors.foreground.css, '#ffffff');
- cm.setTheme({
- background: '#FF0000',
- foreground: '#00FF00'
- });
- assert.equal(cm.colors.background.css, '#FF0000');
- assert.equal(cm.colors.foreground.css, '#00FF00');
- cm.setTheme({
- background: '#0000FF'
- });
- assert.equal(cm.colors.background.css, '#0000FF');
- // FG reverts back to default
- assert.equal(cm.colors.foreground.css, '#ffffff');
- });
-
- it('should set all extended ansi colors in reverse order', () => {
- cm.setTheme({
- extendedAnsi: DEFAULT_ANSI_COLORS.map(a => a.css).slice().reverse()
- });
-
- for (let ansiColor = 16; ansiColor <= 255; ansiColor++) {
- assert.equal(cm.colors.ansi[ansiColor].css, DEFAULT_ANSI_COLORS[255 + 16 - ansiColor].css);
- }
- });
-
- it('should set one extended ansi color and keep the other default', () => {
- cm.setTheme({
- extendedAnsi: ['#ffffff']
- });
-
- assert.equal(cm.colors.ansi[16].css, '#ffffff');
- assert.equal(cm.colors.ansi[17].css, DEFAULT_ANSI_COLORS[17].css);
- });
-
- it('should set extended ansi colors to the default when they are unset', () => {
- cm.setTheme({
- extendedAnsi: ['#ffffff']
- });
- assert.equal(cm.colors.ansi[16].css, '#ffffff');
-
- cm.setTheme({
- extendedAnsi: []
- });
- assert.equal(cm.colors.ansi[16].css, DEFAULT_ANSI_COLORS[16].css);
-
- cm.setTheme({
- extendedAnsi: ['#ffffff']
- });
- assert.equal(cm.colors.ansi[16].css, '#ffffff');
-
- cm.setTheme({});
- assert.equal(cm.colors.ansi[16].css, DEFAULT_ANSI_COLORS[16].css);
- });
-
- it('should set extended ansi colors to the default when they are partially unset', () => {
- cm.setTheme({
- extendedAnsi: ['#ffffff', '#000000']
- });
- assert.equal(cm.colors.ansi[16].css, '#ffffff');
- assert.equal(cm.colors.ansi[17].css, '#000000');
-
- cm.setTheme({
- extendedAnsi: ['#ffffff']
- });
- assert.equal(cm.colors.ansi[16].css, '#ffffff');
- assert.equal(cm.colors.ansi[17].css, DEFAULT_ANSI_COLORS[17].css);
- });
- });
-});
diff --git a/src/browser/ColorManager.ts b/src/browser/ColorManager.ts
deleted file mode 100644
index a22a423f..00000000
--- a/src/browser/ColorManager.ts
+++ /dev/null
@@ -1,221 +0,0 @@
-/**
- * Copyright (c) 2017 The xterm.js authors. All rights reserved.
- * @license MIT
- */
-
-import { IColorManager, IColorSet, IColorContrastCache } from 'browser/Types';
-import { ITheme } from 'common/services/Services';
-import { channels, color, css } from 'common/Color';
-import { ColorContrastCache } from 'browser/ColorContrastCache';
-import { ColorIndex, IColor } from 'common/Types';
-
-
-interface IRestoreColorSet {
- foreground: IColor;
- background: IColor;
- cursor: IColor;
- ansi: IColor[];
-}
-
-
-const DEFAULT_FOREGROUND = css.toColor('#ffffff');
-const DEFAULT_BACKGROUND = css.toColor('#000000');
-const DEFAULT_CURSOR = css.toColor('#ffffff');
-const DEFAULT_CURSOR_ACCENT = css.toColor('#000000');
-const DEFAULT_SELECTION = {
- css: 'rgba(255, 255, 255, 0.3)',
- rgba: 0xFFFFFF4D
-};
-
-// An IIFE to generate DEFAULT_ANSI_COLORS.
-export const DEFAULT_ANSI_COLORS = Object.freeze((() => {
- const colors = [
- // dark:
- css.toColor('#2e3436'),
- css.toColor('#cc0000'),
- css.toColor('#4e9a06'),
- css.toColor('#c4a000'),
- css.toColor('#3465a4'),
- css.toColor('#75507b'),
- css.toColor('#06989a'),
- css.toColor('#d3d7cf'),
- // bright:
- css.toColor('#555753'),
- css.toColor('#ef2929'),
- css.toColor('#8ae234'),
- css.toColor('#fce94f'),
- css.toColor('#729fcf'),
- css.toColor('#ad7fa8'),
- css.toColor('#34e2e2'),
- css.toColor('#eeeeec')
- ];
-
- // Fill in the remaining 240 ANSI colors.
- // Generate colors (16-231)
- const v = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff];
- for (let i = 0; i < 216; i++) {
- const r = v[(i / 36) % 6 | 0];
- const g = v[(i / 6) % 6 | 0];
- const b = v[i % 6];
- colors.push({
- css: channels.toCss(r, g, b),
- rgba: channels.toRgba(r, g, b)
- });
- }
-
- // Generate greys (232-255)
- for (let i = 0; i < 24; i++) {
- const c = 8 + i * 10;
- colors.push({
- css: channels.toCss(c, c, c),
- rgba: channels.toRgba(c, c, c)
- });
- }
-
- return colors;
-})());
-
-/**
- * Manages the source of truth for a terminal's colors.
- */
-export class ColorManager implements IColorManager {
- public colors: IColorSet;
-
- private _contrastCache: IColorContrastCache;
- private _restoreColors!: IRestoreColorSet;
-
- constructor() {
- this._contrastCache = new ColorContrastCache();
- this.colors = {
- foreground: DEFAULT_FOREGROUND,
- background: DEFAULT_BACKGROUND,
- cursor: DEFAULT_CURSOR,
- cursorAccent: DEFAULT_CURSOR_ACCENT,
- selectionForeground: undefined,
- selectionBackgroundTransparent: DEFAULT_SELECTION,
- selectionBackgroundOpaque: color.blend(DEFAULT_BACKGROUND, DEFAULT_SELECTION),
- selectionInactiveBackgroundTransparent: DEFAULT_SELECTION,
- selectionInactiveBackgroundOpaque: color.blend(DEFAULT_BACKGROUND, DEFAULT_SELECTION),
- ansi: DEFAULT_ANSI_COLORS.slice(),
- contrastCache: this._contrastCache
- };
- this._updateRestoreColors();
- }
-
- public onOptionsChange(key: string, value: any): void {
- switch (key) {
- case 'minimumContrastRatio':
- this._contrastCache.clear();
- break;
- }
- }
-
- /**
- * Sets the terminal's theme.
- * @param theme The theme to use. If a partial theme is provided then default
- * colors will be used where colors are not defined.
- */
- public setTheme(theme: ITheme = {}): void {
- this.colors.foreground = this._parseColor(theme.foreground, DEFAULT_FOREGROUND);
- this.colors.background = this._parseColor(theme.background, DEFAULT_BACKGROUND);
- this.colors.cursor = this._parseColor(theme.cursor, DEFAULT_CURSOR);
- this.colors.cursorAccent = this._parseColor(theme.cursorAccent, DEFAULT_CURSOR_ACCENT);
- this.colors.selectionBackgroundTransparent = this._parseColor(theme.selectionBackground, DEFAULT_SELECTION);
- this.colors.selectionBackgroundOpaque = color.blend(this.colors.background, this.colors.selectionBackgroundTransparent);
- this.colors.selectionInactiveBackgroundTransparent = this._parseColor(theme.selectionInactiveBackground, this.colors.selectionBackgroundTransparent);
- this.colors.selectionInactiveBackgroundOpaque = color.blend(this.colors.background, this.colors.selectionInactiveBackgroundTransparent);
- const nullColor: IColor = {
- css: '',
- rgba: 0
- };
- this.colors.selectionForeground = theme.selectionForeground ? this._parseColor(theme.selectionForeground, nullColor) : undefined;
- if (this.colors.selectionForeground === nullColor) {
- this.colors.selectionForeground = undefined;
- }
-
- /**
- * If selection color is opaque, blend it with background with 0.3 opacity
- * Issue #2737
- */
- if (color.isOpaque(this.colors.selectionBackgroundTransparent)) {
- const opacity = 0.3;
- this.colors.selectionBackgroundTransparent = color.opacity(this.colors.selectionBackgroundTransparent, opacity);
- }
- if (color.isOpaque(this.colors.selectionInactiveBackgroundTransparent)) {
- const opacity = 0.3;
- this.colors.selectionInactiveBackgroundTransparent = color.opacity(this.colors.selectionInactiveBackgroundTransparent, opacity);
- }
- this.colors.ansi = DEFAULT_ANSI_COLORS.slice();
- this.colors.ansi[0] = this._parseColor(theme.black, DEFAULT_ANSI_COLORS[0]);
- this.colors.ansi[1] = this._parseColor(theme.red, DEFAULT_ANSI_COLORS[1]);
- this.colors.ansi[2] = this._parseColor(theme.green, DEFAULT_ANSI_COLORS[2]);
- this.colors.ansi[3] = this._parseColor(theme.yellow, DEFAULT_ANSI_COLORS[3]);
- this.colors.ansi[4] = this._parseColor(theme.blue, DEFAULT_ANSI_COLORS[4]);
- this.colors.ansi[5] = this._parseColor(theme.magenta, DEFAULT_ANSI_COLORS[5]);
- this.colors.ansi[6] = this._parseColor(theme.cyan, DEFAULT_ANSI_COLORS[6]);
- this.colors.ansi[7] = this._parseColor(theme.white, DEFAULT_ANSI_COLORS[7]);
- this.colors.ansi[8] = this._parseColor(theme.brightBlack, DEFAULT_ANSI_COLORS[8]);
- this.colors.ansi[9] = this._parseColor(theme.brightRed, DEFAULT_ANSI_COLORS[9]);
- this.colors.ansi[10] = this._parseColor(theme.brightGreen, DEFAULT_ANSI_COLORS[10]);
- this.colors.ansi[11] = this._parseColor(theme.brightYellow, DEFAULT_ANSI_COLORS[11]);
- this.colors.ansi[12] = this._parseColor(theme.brightBlue, DEFAULT_ANSI_COLORS[12]);
- this.colors.ansi[13] = this._parseColor(theme.brightMagenta, DEFAULT_ANSI_COLORS[13]);
- this.colors.ansi[14] = this._parseColor(theme.brightCyan, DEFAULT_ANSI_COLORS[14]);
- this.colors.ansi[15] = this._parseColor(theme.brightWhite, DEFAULT_ANSI_COLORS[15]);
- if (theme.extendedAnsi) {
- const colorCount = Math.min(this.colors.ansi.length - 16, theme.extendedAnsi.length);
- for (let i = 0; i < colorCount; i++) {
- this.colors.ansi[i + 16] = this._parseColor(theme.extendedAnsi[i], DEFAULT_ANSI_COLORS[i + 16]);
- }
- }
- // Clear our the cache
- this._contrastCache.clear();
- this._updateRestoreColors();
- }
-
- public restoreColor(slot?: ColorIndex): void {
- // unset slot restores all ansi colors
- if (slot === undefined) {
- for (let i = 0; i < this._restoreColors.ansi.length; ++i) {
- this.colors.ansi[i] = this._restoreColors.ansi[i];
- }
- return;
- }
- switch (slot) {
- case ColorIndex.FOREGROUND:
- this.colors.foreground = this._restoreColors.foreground;
- break;
- case ColorIndex.BACKGROUND:
- this.colors.background = this._restoreColors.background;
- break;
- case ColorIndex.CURSOR:
- this.colors.cursor = this._restoreColors.cursor;
- break;
- default:
- this.colors.ansi[slot] = this._restoreColors.ansi[slot];
- }
- }
-
- private _updateRestoreColors(): void {
- this._restoreColors = {
- foreground: this.colors.foreground,
- background: this.colors.background,
- cursor: this.colors.cursor,
- ansi: this.colors.ansi.slice()
- };
- }
-
- private _parseColor(
- cssString: string | undefined,
- fallback: IColor
- ): IColor {
- if (cssString !== undefined) {
- try {
- return css.toColor(cssString);
- } catch {
- // no-op
- }
- }
- return fallback;
- }
-}
diff --git a/src/browser/Linkifier2.ts b/src/browser/Linkifier2.ts
index 9c978949..236efb22 100644
--- a/src/browser/Linkifier2.ts
+++ b/src/browser/Linkifier2.ts
@@ -8,7 +8,7 @@ import { IDisposable } from 'common/Types';
import { IMouseService, IRenderService } from './services/Services';
import { IBufferService } from 'common/services/Services';
import { EventEmitter, IEvent } from 'common/EventEmitter';
-import { Disposable, getDisposeArrayDisposable, disposeArray } from 'common/Lifecycle';
+import { Disposable, getDisposeArrayDisposable, disposeArray, toDisposable } from 'common/Lifecycle';
import { addDisposableDomListener } from 'browser/Lifecycle';
export class Linkifier2 extends Disposable implements ILinkifier2 {
@@ -26,21 +26,19 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
private _activeProviderReplies: Map | undefined;
private _activeLine: number = -1;
- private _onShowLinkUnderline = this.register(new EventEmitter());
- public get onShowLinkUnderline(): IEvent { return this._onShowLinkUnderline.event; }
- private _onHideLinkUnderline = this.register(new EventEmitter());
- public get onHideLinkUnderline(): IEvent { return this._onHideLinkUnderline.event; }
+ private readonly _onShowLinkUnderline = this.register(new EventEmitter());
+ public readonly onShowLinkUnderline = this._onShowLinkUnderline.event;
+ private readonly _onHideLinkUnderline = this.register(new EventEmitter());
+ public readonly onHideLinkUnderline = this._onHideLinkUnderline.event;
constructor(
@IBufferService private readonly _bufferService: IBufferService
) {
super();
this.register(getDisposeArrayDisposable(this._linkCacheDisposables));
- }
-
- public dispose(): void {
- super.dispose();
- this._lastMouseEvent = undefined;
+ this.register(toDisposable(() => {
+ this._lastMouseEvent = undefined;
+ }));
}
public registerLinkProvider(linkProvider: ILinkProvider): IDisposable {
@@ -66,12 +64,12 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
this._isMouseOut = true;
this._clearCurrentLink();
}));
- this.register(addDisposableDomListener(this._element, 'mousemove', this._onMouseMove.bind(this)));
+ this.register(addDisposableDomListener(this._element, 'mousemove', this._handleMouseMove.bind(this)));
this.register(addDisposableDomListener(this._element, 'mousedown', this._handleMouseDown.bind(this)));
this.register(addDisposableDomListener(this._element, 'mouseup', this._handleMouseUp.bind(this)));
}
- private _onMouseMove(event: MouseEvent): void {
+ private _handleMouseMove(event: MouseEvent): void {
this._lastMouseEvent = event;
if (!this._element || !this._mouseService) {
@@ -99,12 +97,12 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
}
if (!this._lastBufferCell || (position.x !== this._lastBufferCell.x || position.y !== this._lastBufferCell.y)) {
- this._onHover(position);
+ this._handleHover(position);
this._lastBufferCell = position;
}
}
- private _onHover(position: IBufferCellPosition): void {
+ private _handleHover(position: IBufferCellPosition): void {
// TODO: This currently does not cache link provider results across wrapped lines, activeLine should be something like `activeRange: {startY, endY}`
// Check if we need to clear the link
if (this._activeLine !== position.y) {
diff --git a/src/browser/ScreenDprMonitor.ts b/src/browser/ScreenDprMonitor.ts
index 8129da07..1c3f31b7 100644
--- a/src/browser/ScreenDprMonitor.ts
+++ b/src/browser/ScreenDprMonitor.ts
@@ -3,7 +3,7 @@
* @license MIT
*/
-import { Disposable } from 'common/Lifecycle';
+import { Disposable, toDisposable } from 'common/Lifecycle';
export type ScreenDprListener = (newDevicePixelRatio?: number, oldDevicePixelRatio?: number) => void;
@@ -26,6 +26,9 @@ export class ScreenDprMonitor extends Disposable {
constructor(private _parentWindow: Window) {
super();
this._currentDevicePixelRatio = this._parentWindow.devicePixelRatio;
+ this.register(toDisposable(() => {
+ this.clearListener();
+ }));
}
public setListener(listener: ScreenDprListener): void {
@@ -43,11 +46,6 @@ export class ScreenDprMonitor extends Disposable {
this._updateDpr();
}
- public dispose(): void {
- super.dispose();
- this.clearListener();
- }
-
private _updateDpr(): void {
if (!this._outerListener) {
return;
diff --git a/src/browser/Terminal.test.ts b/src/browser/Terminal.test.ts
index eaf420ed..a9d0b772 100644
--- a/src/browser/Terminal.test.ts
+++ b/src/browser/Terminal.test.ts
@@ -1378,7 +1378,6 @@ describe('Terminal', () => {
assert.deepEqual(disposeStack, [markers[0], markers[1]]);
// trimmed marker objs should be disposed
assert.deepEqual(disposeStack.map(el => el.isDisposed), [true, true]);
- assert.deepEqual(disposeStack.map(el => (el as any)._isDisposed), [true, true]);
// trimmed markers should contain line -1
assert.deepEqual(disposeStack.map(el => el.line), [-1, -1]);
});
diff --git a/src/browser/Terminal.ts b/src/browser/Terminal.ts
index afdac748..9056cbe3 100644
--- a/src/browser/Terminal.ts
+++ b/src/browser/Terminal.ts
@@ -22,7 +22,7 @@
*/
import { ICompositionHelper, ITerminal, IBrowser, CustomKeyEventHandler, IViewport, ILinkifier2, CharacterJoinerHandler, IBufferRange } from 'browser/Types';
-import { IRenderer } from 'browser/renderer/Types';
+import { IRenderer } from 'browser/renderer/shared/Types';
import { CompositionHelper } from 'browser/input/CompositionHelper';
import { Viewport } from 'browser/Viewport';
import { rightClickHandler, moveTextAreaUnderMouseCursor, handlePasteEvent, copyHandler, paste } from 'browser/Clipboard';
@@ -39,9 +39,8 @@ import { KeyboardResultType, CoreMouseEventType, CoreMouseButton, CoreMouseActio
import { evaluateKeyboardEvent } from 'common/input/Keyboard';
import { EventEmitter, IEvent, forwardEvent } from 'common/EventEmitter';
import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
-import { ColorManager } from 'browser/ColorManager';
import { RenderService } from 'browser/services/RenderService';
-import { ICharSizeService, IRenderService, IMouseService, ISelectionService, ICoreBrowserService, ICharacterJoinerService } from 'browser/services/Services';
+import { ICharSizeService, IRenderService, IMouseService, ISelectionService, ICoreBrowserService, ICharacterJoinerService, IThemeService } from 'browser/services/Services';
import { CharSizeService } from 'browser/services/CharSizeService';
import { IBuffer } from 'common/buffer/Types';
import { MouseService } from 'browser/services/MouseService';
@@ -56,6 +55,8 @@ import { OverviewRulerRenderer } from 'browser/decorations/OverviewRulerRenderer
import { DecorationService } from 'common/services/DecorationService';
import { IDecorationService } from 'common/services/Services';
import { OscLinkProvider } from 'browser/OscLinkProvider';
+import { toDisposable } from 'common/Lifecycle';
+import { ThemeService } from 'browser/services/ThemeService';
// Let it work inside Node.js for automated testing purposes.
const document: Document = (typeof window !== 'undefined') ? window.document : null as any;
@@ -85,6 +86,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
private _coreBrowserService: ICoreBrowserService | undefined;
private _mouseService: IMouseService | undefined;
private _renderService: IRenderService | undefined;
+ private _themeService: IThemeService | undefined;
private _characterJoinerService: ICharacterJoinerService | undefined;
private _selectionService: ISelectionService | undefined;
@@ -119,30 +121,30 @@ export class Terminal extends CoreTerminal implements ITerminal {
public viewport: IViewport | undefined;
private _compositionHelper: ICompositionHelper | undefined;
private _accessibilityManager: AccessibilityManager | undefined;
- private _colorManager: ColorManager | undefined;
- private _theme: ITheme | undefined;
- private _onCursorMove = new EventEmitter();
- public get onCursorMove(): IEvent { return this._onCursorMove.event; }
- private _onKey = new EventEmitter<{ key: string, domEvent: KeyboardEvent }>();
- public get onKey(): IEvent<{ key: string, domEvent: KeyboardEvent }> { return this._onKey.event; }
- private _onRender = new EventEmitter<{ start: number, end: number }>();
- public get onRender(): IEvent<{ start: number, end: number }> { return this._onRender.event; }
- private _onSelectionChange = new EventEmitter();
- public get onSelectionChange(): IEvent { return this._onSelectionChange.event; }
- private _onTitleChange = new EventEmitter();
- public get onTitleChange(): IEvent { return this._onTitleChange.event; }
- private _onBell = new EventEmitter();
- public get onBell(): IEvent { return this._onBell.event; }
+ private readonly _onCursorMove = this.register(new EventEmitter());
+ public readonly onCursorMove = this._onCursorMove.event;
+ private readonly _onKey = this.register(new EventEmitter<{ key: string, domEvent: KeyboardEvent }>());
+ public readonly onKey = this._onKey.event;
+ private readonly _onRender = this.register(new EventEmitter<{ start: number, end: number }>());
+ public readonly onRender = this._onRender.event;
+ private readonly _onSelectionChange = this.register(new EventEmitter());
+ public readonly onSelectionChange = this._onSelectionChange.event;
+ private readonly _onTitleChange = this.register(new EventEmitter());
+ public readonly onTitleChange = this._onTitleChange.event;
+ private readonly _onBell = this.register(new EventEmitter());
+ public readonly onBell = this._onBell.event;
- private _onFocus = new EventEmitter();
+ private _onFocus = this.register(new EventEmitter());
public get onFocus(): IEvent { return this._onFocus.event; }
- private _onBlur = new EventEmitter();
+ private _onBlur = this.register(new EventEmitter());
public get onBlur(): IEvent { return this._onBlur.event; }
- private _onA11yCharEmitter = new EventEmitter();
+ private _onA11yCharEmitter = this.register(new EventEmitter());
public get onA11yChar(): IEvent { return this._onA11yCharEmitter.event; }
- private _onA11yTabEmitter = new EventEmitter();
+ private _onA11yTabEmitter = this.register(new EventEmitter());
public get onA11yTab(): IEvent { return this._onA11yTabEmitter.event; }
+ private _onWillOpen = this.register(new EventEmitter());
+ public get onWillOpen(): IEvent { return this._onWillOpen.event; }
/**
* Creates a new `Terminal` object.
@@ -182,6 +184,11 @@ export class Terminal extends CoreTerminal implements ITerminal {
// Setup listeners
this.register(this._bufferService.onResize(e => this._afterResize(e.cols, e.rows)));
+
+ this.register(toDisposable(() => {
+ this._customKeyEventHandler = undefined;
+ this.element?.parentNode?.removeChild(this.element);
+ }));
}
/**
@@ -191,9 +198,9 @@ export class Terminal extends CoreTerminal implements ITerminal {
* while an event from OSC 10|110 | 11|111 | 12|112 always contains a single request.
*/
private _handleColorEvent(event: IColorEvent): void {
- if (!this._colorManager) return;
+ if (!this._themeService) return;
for (const req of event) {
- let acc: 'foreground' | 'background' | 'cursor' | 'ansi' | undefined = undefined;
+ let acc: 'foreground' | 'background' | 'cursor' | 'ansi';
let ident = '';
switch (req.index) {
case ColorIndex.FOREGROUND: // OSC 10 | 110
@@ -216,32 +223,23 @@ export class Terminal extends CoreTerminal implements ITerminal {
switch (req.type) {
case ColorRequestType.REPORT:
const channels = color.toColorRGB(acc === 'ansi'
- ? this._colorManager.colors.ansi[req.index]
- : this._colorManager.colors[acc]);
+ ? this._themeService.colors.ansi[req.index]
+ : this._themeService.colors[acc]);
this.coreService.triggerDataEvent(`${C0.ESC}]${ident};${toRgbString(channels)}${C1_ESCAPED.ST}`);
break;
case ColorRequestType.SET:
- if (acc === 'ansi') this._colorManager.colors.ansi[req.index] = rgba.toColor(...req.color);
- else this._colorManager.colors[acc] = rgba.toColor(...req.color);
+ if (acc === 'ansi') {
+ this._themeService.modifyColors(colors => colors.ansi[req.index] = rgba.toColor(...req.color));
+ } else {
+ const narrowedAcc = acc;
+ this._themeService.modifyColors(colors => colors[narrowedAcc] = rgba.toColor(...req.color));
+ }
break;
case ColorRequestType.RESTORE:
- this._colorManager.restoreColor(req.index);
+ this._themeService.restoreColor(req.index);
break;
}
}
- this._renderService?.setColors(this._colorManager.colors);
- this.viewport?.onThemeChange(this._colorManager.colors);
- }
-
- public dispose(): void {
- if (this._isDisposed) {
- return;
- }
- super.dispose();
- this._renderService?.dispose();
- this._customKeyEventHandler = undefined;
- this.write = () => { };
- this.element?.parentNode?.removeChild(this.element);
}
protected _setup(): void {
@@ -266,60 +264,21 @@ export class Terminal extends CoreTerminal implements ITerminal {
}
}
- protected _updateOptions(key: string): void {
- super._updateOptions(key);
-
- // TODO: These listeners should be owned by individual components
- switch (key) {
- case 'fontFamily':
- case 'fontSize':
- // When the font changes the size of the cells may change which requires a renderer clear
- this._renderService?.clear();
- this._charSizeService?.measure();
- break;
- case 'cursorBlink':
- case 'cursorStyle':
- // The DOM renderer needs a row refresh to update the cursor styles
- this.refresh(this.buffer.y, this.buffer.y);
- break;
- case 'customGlyphs':
- case 'drawBoldTextInBrightColors':
- case 'letterSpacing':
- case 'lineHeight':
- case 'fontWeight':
- case 'fontWeightBold':
- case 'minimumContrastRatio':
- // When the font changes the size of the cells may change which requires a renderer clear
- if (this._renderService) {
- this._renderService.clear();
- this._renderService.onResize(this.cols, this.rows);
- this.refresh(0, this.rows - 1);
- }
- break;
- case 'scrollback':
- this.viewport?.syncScrollArea();
- break;
- case 'screenReaderMode':
- if (this.optionsService.rawOptions.screenReaderMode) {
- if (!this._accessibilityManager && this._renderService) {
- this._accessibilityManager = new AccessibilityManager(this, this._renderService);
- }
- } else {
- this._accessibilityManager?.dispose();
- this._accessibilityManager = undefined;
- }
- break;
- case 'tabStopWidth': this.buffers.setupTabStops(); break;
- case 'theme':
- this._setTheme(this.optionsService.rawOptions.theme);
- break;
+ private _handleScreenReaderModeOptionChange(value: boolean): void {
+ if (value) {
+ if (!this._accessibilityManager && this._renderService) {
+ this._accessibilityManager = new AccessibilityManager(this, this._renderService);
+ }
+ } else {
+ this._accessibilityManager?.dispose();
+ this._accessibilityManager = undefined;
}
}
/**
* Binds the desired focus behavior on a given terminal object.
*/
- private _onTextAreaFocus(ev: KeyboardEvent): void {
+ private _handleTextAreaFocus(ev: KeyboardEvent): void {
if (this.coreService.decPrivateModes.sendFocus) {
this.coreService.triggerDataEvent(C0.ESC + '[I');
}
@@ -340,7 +299,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
/**
* Binds the desired blur behavior on a given terminal object.
*/
- private _onTextAreaBlur(): void {
+ private _handleTextAreaBlur(): void {
// Text can safely be removed on blur. Doing it earlier could interfere with
// screen readers reading it out.
this.textarea!.value = '';
@@ -491,8 +450,8 @@ export class Terminal extends CoreTerminal implements ITerminal {
this.textarea.setAttribute('autocapitalize', 'off');
this.textarea.setAttribute('spellcheck', 'false');
this.textarea.tabIndex = 0;
- this.register(addDisposableDomListener(this.textarea, 'focus', (ev: KeyboardEvent) => this._onTextAreaFocus(ev)));
- this.register(addDisposableDomListener(this.textarea, 'blur', () => this._onTextAreaBlur()));
+ this.register(addDisposableDomListener(this.textarea, 'focus', (ev: KeyboardEvent) => this._handleTextAreaFocus(ev)));
+ this.register(addDisposableDomListener(this.textarea, 'blur', () => this._handleTextAreaBlur()));
this._helperContainer.appendChild(this.textarea);
this._coreBrowserService = this._instantiationService.createInstance(CoreBrowserService, this.textarea, this._document.defaultView ?? window);
@@ -501,16 +460,13 @@ export class Terminal extends CoreTerminal implements ITerminal {
this._charSizeService = this._instantiationService.createInstance(CharSizeService, this._document, this._helperContainer);
this._instantiationService.setService(ICharSizeService, this._charSizeService);
- this._theme = this.options.theme || this._theme;
- this._colorManager = new ColorManager();
- this.register(this.optionsService.onOptionChange(e => this._colorManager!.onOptionsChange(e, this.optionsService.rawOptions[e])));
- this._colorManager.setTheme(this._theme);
+ this._themeService = this._instantiationService.createInstance(ThemeService);
+ this._instantiationService.setService(IThemeService, this._themeService);
this._characterJoinerService = this._instantiationService.createInstance(CharacterJoinerService);
this._instantiationService.setService(ICharacterJoinerService, this._characterJoinerService);
- const renderer = this._createRenderer();
- this._renderService = this.register(this._instantiationService.createInstance(RenderService, renderer, this.rows, this.screenElement));
+ this._renderService = this.register(this._instantiationService.createInstance(RenderService, this.rows, this.screenElement));
this._instantiationService.setService(IRenderService, this._renderService);
this.register(this._renderService.onRenderedViewportChange(e => this._onRender.fire(e)));
this.onResize(e => this._renderService!.resize(e.cols, e.rows));
@@ -523,26 +479,32 @@ export class Terminal extends CoreTerminal implements ITerminal {
// Performance: Add viewport and helper elements from the fragment
this.element.appendChild(fragment);
+ try {
+ this._onWillOpen.fire(this.element);
+ }
+ catch { /* fails to load addon for some reason */ }
+ if (!this._renderService.hasRenderer()) {
+ this._renderService.setRenderer(this._createRenderer());
+ }
+
this._mouseService = this._instantiationService.createInstance(MouseService);
this._instantiationService.setService(IMouseService, this._mouseService);
this.viewport = this._instantiationService.createInstance(Viewport,
(amount: number) => this.scrollLines(amount, true, ScrollSource.VIEWPORT),
this._viewportElement,
- this._viewportScrollArea,
- this.element
+ this._viewportScrollArea
);
- this.viewport.onThemeChange(this._colorManager.colors);
this.register(this._inputHandler.onRequestSyncScrollBar(() => this.viewport!.syncScrollArea()));
this.register(this.viewport);
this.register(this.onCursorMove(() => {
- this._renderService!.onCursorMove();
+ this._renderService!.handleCursorMove();
this._syncTextArea();
}));
- this.register(this.onResize(() => this._renderService!.onResize(this.cols, this.rows)));
- this.register(this.onBlur(() => this._renderService!.onBlur()));
- this.register(this.onFocus(() => this._renderService!.onFocus()));
+ this.register(this.onResize(() => this._renderService!.handleResize(this.cols, this.rows)));
+ this.register(this.onBlur(() => this._renderService!.handleBlur()));
+ this.register(this.onFocus(() => this._renderService!.handleFocus()));
this.register(this._renderService.onDimensionsChange(() => this.viewport!.syncScrollArea()));
this._selectionService = this.register(this._instantiationService.createInstance(SelectionService,
@@ -553,7 +515,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
this._instantiationService.setService(ISelectionService, this._selectionService);
this.register(this._selectionService.onRequestScrollLines(e => this.scrollLines(e.amount, e.suppressScrollEvent)));
this.register(this._selectionService.onSelectionChange(() => this._onSelectionChange.fire()));
- this.register(this._selectionService.onRequestRedraw(e => this._renderService!.onSelectionChanged(e.start, e.end, e.columnSelectMode)));
+ this.register(this._selectionService.onRequestRedraw(e => this._renderService!.handleSelectionChanged(e.start, e.end, e.columnSelectMode)));
this.register(this._selectionService.onLinuxMouseSelection(text => {
// If there's a new selection, put it into the textarea, focus and select it
// in order to register it as a selection on the OS. This event is fired
@@ -570,7 +532,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
this.linkifier2.attachToDom(this.screenElement, this._mouseService, this._renderService);
this.register(this._instantiationService.createInstance(BufferDecorationRenderer, this.screenElement));
- this.register(addDisposableDomListener(this.element, 'mousedown', (e: MouseEvent) => this._selectionService!.onMouseDown(e)));
+ this.register(addDisposableDomListener(this.element, 'mousedown', (e: MouseEvent) => this._selectionService!.handleMouseDown(e)));
// apply mouse event classes set by escape codes before terminal was attached
if (this.coreMouseService.areMouseEventsActive) {
@@ -585,12 +547,13 @@ export class Terminal extends CoreTerminal implements ITerminal {
// ensure the correct order of the dprchange event
this._accessibilityManager = new AccessibilityManager(this, this._renderService);
}
+ this.register(this.optionsService.onSpecificOptionChange('screenReaderMode', e => this._handleScreenReaderModeOptionChange(e)));
if (this.options.overviewRulerWidth) {
this._overviewRulerRenderer = this.register(this._instantiationService.createInstance(OverviewRulerRenderer, this._viewportElement, this.screenElement));
}
- this.optionsService.onOptionChange(() => {
- if (!this._overviewRulerRenderer && this.options.overviewRulerWidth && this._viewportElement && this.screenElement) {
+ this.optionsService.onSpecificOptionChange('overviewRulerWidth', value => {
+ if (!this._overviewRulerRenderer && value && this._viewportElement && this.screenElement) {
this._overviewRulerRenderer = this.register(this._instantiationService.createInstance(OverviewRulerRenderer, this._viewportElement, this.screenElement));
}
});
@@ -609,18 +572,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
}
private _createRenderer(): IRenderer {
- return this._instantiationService.createInstance(DomRenderer, this._colorManager!.colors, this.element!, this.screenElement!, this._viewportElement!, this.linkifier2);
- }
-
- /**
- * Sets the theme on the renderer. The renderer must have been initialized.
- * @param theme The theme to set.
- */
- private _setTheme(theme: ITheme): void {
- this._theme = theme;
- this._colorManager?.setTheme(theme);
- this._renderService?.setColors(this._colorManager!.colors);
- this.viewport?.onThemeChange(this._colorManager!.colors);
+ return this._instantiationService.createInstance(DomRenderer, this.element!, this.screenElement!, this._viewportElement!, this.linkifier2);
}
/**
@@ -859,20 +811,20 @@ export class Terminal extends CoreTerminal implements ITerminal {
// normal viewport scrolling
// conditionally stop event, if the viewport still had rows to scroll within
- if (this.viewport!.onWheel(ev)) {
+ if (this.viewport!.handleWheel(ev)) {
return this.cancel(ev);
}
}, { passive: false }));
this.register(addDisposableDomListener(el, 'touchstart', (ev: TouchEvent) => {
if (this.coreMouseService.areMouseEventsActive) return;
- this.viewport!.onTouchStart(ev);
+ this.viewport!.handleTouchStart(ev);
return this.cancel(ev);
}, { passive: true }));
this.register(addDisposableDomListener(el, 'touchmove', (ev: TouchEvent) => {
if (this.coreMouseService.areMouseEventsActive) return;
- if (!this.viewport!.onTouchMove(ev)) {
+ if (!this.viewport!.handleTouchMove(ev)) {
return this.cancel(ev);
}
}, { passive: false }));
diff --git a/src/browser/TestUtils.test.ts b/src/browser/TestUtils.test.ts
index 0b5e00c1..97ad90d8 100644
--- a/src/browser/TestUtils.test.ts
+++ b/src/browser/TestUtils.test.ts
@@ -5,11 +5,11 @@
import { IDisposable, IMarker, ILinkProvider, IDecorationOptions, IDecoration } from 'xterm';
import { IEvent, EventEmitter } from 'common/EventEmitter';
-import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IMouseService, IRenderService, ISelectionService } from 'browser/services/Services';
-import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/Types';
-import { IColorSet, ITerminal, ILinkifier2, IBrowser, IViewport, IColorManager, ICompositionHelper, CharacterJoinerHandler, IBufferRange } from 'browser/Types';
+import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IMouseService, IRenderService, ISelectionService, IThemeService } from 'browser/services/Services';
+import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/shared/Types';
+import { IColorSet, ITerminal, ILinkifier2, IBrowser, IViewport, ICompositionHelper, CharacterJoinerHandler, IBufferRange, ReadonlyColorSet } from 'browser/Types';
import { IBuffer, IBufferStringIterator, IBufferSet } from 'common/buffer/Types';
-import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener, ICharset, ITerminalOptions } from 'common/Types';
+import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener, ICharset, ITerminalOptions, ColorIndex } from 'common/Types';
import { Buffer } from 'common/buffer/Buffer';
import * as Browser from 'common/Platform';
import { Terminal } from 'browser/Terminal';
@@ -17,6 +17,7 @@ import { IUnicodeService, IOptionsService, ICoreService, ICoreMouseService } fro
import { IFunctionIdentifier, IParams } from 'common/parser/Types';
import { AttributeData } from 'common/buffer/AttributeData';
import { ISelectionRedrawRequestEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types';
+import { css } from 'common/Color';
export class TestTerminal extends Terminal {
public get curAttrData(): IAttributeData { return (this as any)._inputHandler._curAttrData; }
@@ -41,6 +42,7 @@ export class MockTerminal implements ITerminal {
public onTitleChange!: IEvent;
public onBell!: IEvent;
public onScroll!: IEvent;
+ public onWillOpen!: IEvent;
public onKey!: IEvent<{ key: string, domEvent: KeyboardEvent }>;
public onRender!: IEvent<{ start: number, end: number }>;
public onResize!: IEvent<{ cols: number, rows: number }>;
@@ -264,7 +266,6 @@ export class MockRenderer implements IRenderer {
public dispose(): void {
throw new Error('Method not implemented.');
}
- public colorManager!: IColorManager;
public on(type: string, listener: XtermListener): void {
throw new Error('Method not implemented.');
}
@@ -278,20 +279,17 @@ export class MockRenderer implements IRenderer {
throw new Error('Method not implemented.');
}
public dimensions!: IRenderDimensions;
- public setColors(colors: IColorSet): void {
- throw new Error('Method not implemented.');
- }
public registerDecoration(decorationOptions: IDecorationOptions): IDecoration {
throw new Error('Method not implemented.');
}
- public onResize(cols: number, rows: number): void { }
- public onCharSizeChanged(): void { }
- public onBlur(): void { }
- public onFocus(): void { }
- public onSelectionChanged(start: [number, number], end: [number, number]): void { }
- public onCursorMove(): void { }
- public onOptionsChanged(): void { }
- public onDevicePixelRatioChange(): void { }
+ public handleResize(cols: number, rows: number): void { }
+ public handleCharSizeChanged(): void { }
+ public handleBlur(): void { }
+ public handleFocus(): void { }
+ public handleSelectionChanged(start: [number, number], end: [number, number]): void { }
+ public handleCursorMove(): void { }
+ public handleOptionsChanged(): void { }
+ public handleDevicePixelRatioChange(): void { }
public clear(): void { }
public renderRows(start: number, end: number): void { }
}
@@ -301,16 +299,16 @@ export class MockViewport implements IViewport {
throw new Error('Method not implemented.');
}
public scrollBarWidth: number = 0;
- public onThemeChange(colors: IColorSet): void {
+ public handleThemeChange(colors: IColorSet): void {
throw new Error('Method not implemented.');
}
- public onWheel(ev: WheelEvent): boolean {
+ public handleWheel(ev: WheelEvent): boolean {
throw new Error('Method not implemented.');
}
- public onTouchStart(ev: TouchEvent): void {
+ public handleTouchStart(ev: TouchEvent): void {
throw new Error('Method not implemented.');
}
- public onTouchMove(ev: TouchEvent): boolean {
+ public handleTouchMove(ev: TouchEvent): boolean {
throw new Error('Method not implemented.');
}
public syncScrollArea(): void { }
@@ -400,31 +398,31 @@ export class MockRenderService implements IRenderService {
public resize(cols: number, rows: number): void {
throw new Error('Method not implemented.');
}
+ public hasRenderer(): boolean {
+ throw new Error('Method not implemented.');
+ }
public setRenderer(renderer: IRenderer): void {
throw new Error('Method not implemented.');
}
- public setColors(colors: IColorSet): void {
+ public handleDevicePixelRatioChange(): void {
throw new Error('Method not implemented.');
}
- public onDevicePixelRatioChange(): void {
+ public handleResize(cols: number, rows: number): void {
throw new Error('Method not implemented.');
}
- public onResize(cols: number, rows: number): void {
+ public handleCharSizeChanged(): void {
throw new Error('Method not implemented.');
}
- public onCharSizeChanged(): void {
+ public handleBlur(): void {
throw new Error('Method not implemented.');
}
- public onBlur(): void {
+ public handleFocus(): void {
throw new Error('Method not implemented.');
}
- public onFocus(): void {
+ public handleSelectionChanged(start: [number, number], end: [number, number], columnSelectMode: boolean): void {
throw new Error('Method not implemented.');
}
- public onSelectionChanged(start: [number, number], end: [number, number], columnSelectMode: boolean): void {
- throw new Error('Method not implemented.');
- }
- public onCursorMove(): void {
+ public handleCursorMove(): void {
throw new Error('Method not implemented.');
}
public clear(): void {
@@ -494,10 +492,45 @@ export class MockSelectionService implements ISelectionService {
public refresh(isLinuxMouseSelection?: boolean): void {
throw new Error('Method not implemented.');
}
- public onMouseDown(event: MouseEvent): void {
+ public handleMouseDown(event: MouseEvent): void {
throw new Error('Method not implemented.');
}
public isCellInSelection(x: number, y: number): boolean {
return false;
}
}
+
+export class MockThemeService implements IThemeService{
+ public serviceBrand: undefined;
+ public onChangeColors = new EventEmitter().event;
+ public restoreColor(slot?: ColorIndex | undefined): void {
+ throw new Error('Method not implemented.');
+ }
+ public modifyColors(callback: (colors: IColorSet) => void): void {
+ throw new Error('Method not implemented.');
+ }
+ public colors: ReadonlyColorSet = {
+ background: css.toColor('#010101'),
+ foreground: css.toColor('#020202'),
+ ansi: [
+ // dark:
+ css.toColor('#2e3436'),
+ css.toColor('#cc0000'),
+ css.toColor('#4e9a06'),
+ css.toColor('#c4a000'),
+ css.toColor('#3465a4'),
+ css.toColor('#75507b'),
+ css.toColor('#06989a'),
+ css.toColor('#d3d7cf'),
+ // bright:
+ css.toColor('#555753'),
+ css.toColor('#ef2929'),
+ css.toColor('#8ae234'),
+ css.toColor('#fce94f'),
+ css.toColor('#729fcf'),
+ css.toColor('#ad7fa8'),
+ css.toColor('#34e2e2'),
+ css.toColor('#eeeeec')
+ ]
+ } as any;
+}
diff --git a/src/browser/Types.d.ts b/src/browser/Types.d.ts
index 48461a5e..cfd7156f 100644
--- a/src/browser/Types.d.ts
+++ b/src/browser/Types.d.ts
@@ -23,6 +23,7 @@ export interface ITerminal extends IPublicTerminal, ICoreTerminal {
onFocus: IEvent;
onA11yChar: IEvent;
onA11yTab: IEvent;
+ onWillOpen: IEvent;
cancel(ev: Event, force?: boolean): boolean | void;
}
@@ -105,11 +106,6 @@ export interface IBrowser {
isWindows: boolean;
}
-export interface IColorManager {
- colors: IColorSet;
- onOptionsChange(key: string, value: any): void;
-}
-
export interface IColorSet {
foreground: IColor;
background: IColor;
@@ -125,6 +121,8 @@ export interface IColorSet {
contrastCache: IColorContrastCache;
}
+export type ReadonlyColorSet = Readonly> & { ansi: Readonly['ansi']> };
+
export interface IColorContrastCache {
clear(): void;
setCss(bg: number, fg: number, value: string | null): void;
@@ -146,10 +144,9 @@ export interface IViewport extends IDisposable {
scrollBarWidth: number;
syncScrollArea(immediate?: boolean): void;
getLinesScrolled(ev: WheelEvent): number;
- onWheel(ev: WheelEvent): boolean;
- onTouchStart(ev: TouchEvent): void;
- onTouchMove(ev: TouchEvent): boolean;
- onThemeChange(colors: IColorSet): void;
+ handleWheel(ev: WheelEvent): boolean;
+ handleTouchStart(ev: TouchEvent): void;
+ handleTouchMove(ev: TouchEvent): boolean;
}
export interface ILinkifierEvent {
diff --git a/src/browser/Viewport.ts b/src/browser/Viewport.ts
index f95e0bb4..700c9e22 100644
--- a/src/browser/Viewport.ts
+++ b/src/browser/Viewport.ts
@@ -5,11 +5,11 @@
import { Disposable } from 'common/Lifecycle';
import { addDisposableDomListener } from 'browser/Lifecycle';
-import { IColorSet, IViewport } from 'browser/Types';
-import { ICharSizeService, ICoreBrowserService, IRenderService } from 'browser/services/Services';
+import { IColorSet, IViewport, ReadonlyColorSet } from 'browser/Types';
+import { ICharSizeService, ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services';
import { IBufferService, IOptionsService } from 'common/services/Services';
import { IBuffer } from 'common/buffer/Types';
-import { IRenderDimensions } from 'browser/renderer/Types';
+import { IRenderDimensions } from 'browser/renderer/shared/Types';
const FALLBACK_SCROLL_BAR_WIDTH = 15;
@@ -52,12 +52,12 @@ export class Viewport extends Disposable implements IViewport {
private readonly _scrollLines: (amount: number) => void,
private readonly _viewportElement: HTMLElement,
private readonly _scrollArea: HTMLElement,
- private readonly _element: HTMLElement,
@IBufferService private readonly _bufferService: IBufferService,
@IOptionsService private readonly _optionsService: IOptionsService,
@ICharSizeService private readonly _charSizeService: ICharSizeService,
@IRenderService private readonly _renderService: IRenderService,
- @ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService
+ @ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService,
+ @IThemeService themeService: IThemeService
) {
super();
@@ -65,7 +65,7 @@ export class Viewport extends Disposable implements IViewport {
// Unfortunately the overlay scrollbar would be hidden underneath the screen element in that case,
// therefore we account for a standard amount to make it visible
this.scrollBarWidth = (this._viewportElement.offsetWidth - this._scrollArea.offsetWidth) || FALLBACK_SCROLL_BAR_WIDTH;
- this.register(addDisposableDomListener(this._viewportElement, 'scroll', this._onScroll.bind(this)));
+ this.register(addDisposableDomListener(this._viewportElement, 'scroll', this._handleScroll.bind(this)));
// Track properties used in performance critical code manually to avoid using slow getters
this._activeBuffer = this._bufferService.buffer;
@@ -73,11 +73,15 @@ export class Viewport extends Disposable implements IViewport {
this._renderDimensions = this._renderService.dimensions;
this.register(this._renderService.onDimensionsChange(e => this._renderDimensions = e));
+ this._handleThemeChange(themeService.colors);
+ this.register(themeService.onChangeColors(e => this._handleThemeChange(e)));
+ this.register(this._optionsService.onSpecificOptionChange('scrollback', () => this.syncScrollArea()));
+
// Perform this async to ensure the ICharSizeService is ready.
setTimeout(() => this.syncScrollArea(), 0);
}
- public onThemeChange(colors: IColorSet): void {
+ private _handleThemeChange(colors: ReadonlyColorSet): void {
this._viewportElement.style.backgroundColor = colors.background.css;
}
@@ -157,7 +161,7 @@ export class Viewport extends Disposable implements IViewport {
* terminal to scroll to it.
* @param ev The scroll event.
*/
- private _onScroll(ev: Event): void {
+ private _handleScroll(ev: Event): void {
// Record current scroll top position
this._lastScrollTop = this._viewportElement.scrollTop;
@@ -234,7 +238,7 @@ export class Viewport extends Disposable implements IViewport {
* `Viewport`.
* @param ev The mouse wheel event.
*/
- public onWheel(ev: WheelEvent): boolean {
+ public handleWheel(ev: WheelEvent): boolean {
const amount = this._getPixelsScrolled(ev);
if (amount === 0) {
return false;
@@ -315,7 +319,7 @@ export class Viewport extends Disposable implements IViewport {
* Handles the touchstart event, recording the touch occurred.
* @param ev The touch event.
*/
- public onTouchStart(ev: TouchEvent): void {
+ public handleTouchStart(ev: TouchEvent): void {
this._lastTouchY = ev.touches[0].pageY;
}
@@ -323,7 +327,7 @@ export class Viewport extends Disposable implements IViewport {
* Handles the touchmove event, scrolling the viewport if the position shifted.
* @param ev The touch event.
*/
- public onTouchMove(ev: TouchEvent): boolean {
+ public handleTouchMove(ev: TouchEvent): boolean {
const deltaY = this._lastTouchY - ev.touches[0].pageY;
this._lastTouchY = ev.touches[0].pageY;
if (deltaY === 0) {
diff --git a/src/browser/decorations/BufferDecorationRenderer.ts b/src/browser/decorations/BufferDecorationRenderer.ts
index 7fcc5ea9..5836e266 100644
--- a/src/browser/decorations/BufferDecorationRenderer.ts
+++ b/src/browser/decorations/BufferDecorationRenderer.ts
@@ -5,7 +5,7 @@
import { addDisposableDomListener } from 'browser/Lifecycle';
import { IRenderService } from 'browser/services/Services';
-import { Disposable } from 'common/Lifecycle';
+import { Disposable, toDisposable } from 'common/Lifecycle';
import { IBufferService, IDecorationService, IInternalDecoration } from 'common/services/Services';
export class BufferDecorationRenderer extends Disposable {
@@ -39,12 +39,10 @@ export class BufferDecorationRenderer extends Disposable {
}));
this.register(this._decorationService.onDecorationRegistered(() => this._queueRefresh()));
this.register(this._decorationService.onDecorationRemoved(decoration => this._removeDecoration(decoration)));
- }
-
- public override dispose(): void {
- this._container.remove();
- this._decorationElements.clear();
- super.dispose();
+ this.register(toDisposable(() => {
+ this._container.remove();
+ this._decorationElements.clear();
+ }));
}
private _queueRefresh(): void {
diff --git a/src/browser/decorations/OverviewRulerRenderer.ts b/src/browser/decorations/OverviewRulerRenderer.ts
index e7db50c5..90166960 100644
--- a/src/browser/decorations/OverviewRulerRenderer.ts
+++ b/src/browser/decorations/OverviewRulerRenderer.ts
@@ -6,7 +6,7 @@
import { ColorZoneStore, IColorZone, IColorZoneStore } from 'browser/decorations/ColorZoneStore';
import { addDisposableDomListener } from 'browser/Lifecycle';
import { ICoreBrowserService, IRenderService } from 'browser/services/Services';
-import { Disposable } from 'common/Lifecycle';
+import { Disposable, toDisposable } from 'common/Lifecycle';
import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services';
// Helper objects to avoid excessive calculation and garbage collection during rendering. These are
@@ -68,6 +68,9 @@ export class OverviewRulerRenderer extends Disposable {
this._registerDecorationListeners();
this._registerBufferChangeListeners();
this._registerDimensionChangeListeners();
+ this.register(toDisposable(() => {
+ this._canvas?.remove();
+ }));
}
/**
@@ -107,24 +110,13 @@ export class OverviewRulerRenderer extends Disposable {
}
}));
// overview ruler width changed
- this.register(this._optionsService.onOptionChange(o => {
- if (o === 'overviewRulerWidth') {
- this._queueRefresh(true);
- }
- }));
+ this.register(this._optionsService.onSpecificOptionChange('overviewRulerWidth', () => this._queueRefresh(true)));
// device pixel ratio changed
- this.register(addDisposableDomListener(this._coreBrowseService.window, 'resize', () => {
- this._queueRefresh(true);
- }));
+ this.register(addDisposableDomListener(this._coreBrowseService.window, 'resize', () => this._queueRefresh(true)));
// set the canvas dimensions
this._queueRefresh(true);
}
- public override dispose(): void {
- this._canvas?.remove();
- super.dispose();
- }
-
private _refreshDrawConstants(): void {
// width
const outerWidth = Math.floor(this._canvas.width / 3);
diff --git a/src/browser/renderer/Types.d.ts b/src/browser/renderer/Types.d.ts
deleted file mode 100644
index cb1a85b4..00000000
--- a/src/browser/renderer/Types.d.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-/**
- * Copyright (c) 2019 The xterm.js authors. All rights reserved.
- * @license MIT
- */
-
-import { IDisposable } from 'common/Types';
-import { IColorSet } from 'browser/Types';
-import { IEvent } from 'common/EventEmitter';
-
-export interface IRenderDimensions {
- scaledCharWidth: number;
- scaledCharHeight: number;
- scaledCellWidth: number;
- scaledCellHeight: number;
- scaledCharLeft: number;
- scaledCharTop: number;
- scaledCanvasWidth: number;
- scaledCanvasHeight: number;
- canvasWidth: number;
- canvasHeight: number;
- actualCellWidth: number;
- actualCellHeight: number;
-}
-
-export interface IRequestRedrawEvent {
- start: number;
- end: number;
-}
-
-/**
- * Note that IRenderer implementations should emit the refresh event after
- * rendering rows to the screen.
- */
-export interface IRenderer extends IDisposable {
- readonly dimensions: IRenderDimensions;
-
- /**
- * Fires when the renderer is requesting to be redrawn on the next animation
- * frame but is _not_ a result of content changing (eg. selection changes).
- */
- readonly onRequestRedraw: IEvent;
-
- dispose(): void;
- setColors(colors: IColorSet): void;
- onDevicePixelRatioChange(): void;
- onResize(cols: number, rows: number): void;
- onCharSizeChanged(): void;
- onBlur(): void;
- onFocus(): void;
- onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void;
- onCursorMove(): void;
- onOptionsChanged(): void;
- clear(): void;
- renderRows(start: number, end: number): void;
- clearTextureAtlas?(): void;
-}
diff --git a/src/browser/renderer/dom/DomRenderer.ts b/src/browser/renderer/dom/DomRenderer.ts
index 8df6b302..02738b5b 100644
--- a/src/browser/renderer/dom/DomRenderer.ts
+++ b/src/browser/renderer/dom/DomRenderer.ts
@@ -3,12 +3,12 @@
* @license MIT
*/
-import { IRenderer, IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/Types';
+import { IRenderer, IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/shared/Types';
import { BOLD_CLASS, ITALIC_CLASS, CURSOR_CLASS, CURSOR_STYLE_BLOCK_CLASS, CURSOR_BLINK_CLASS, CURSOR_STYLE_BAR_CLASS, CURSOR_STYLE_UNDERLINE_CLASS, DomRendererRowFactory } from 'browser/renderer/dom/DomRendererRowFactory';
-import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/Constants';
-import { Disposable } from 'common/Lifecycle';
-import { IColorSet, ILinkifierEvent, ILinkifier2 } from 'browser/Types';
-import { ICharSizeService, ICoreBrowserService } from 'browser/services/Services';
+import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/shared/Constants';
+import { Disposable, toDisposable } from 'common/Lifecycle';
+import { IColorSet, ILinkifierEvent, ILinkifier2, ReadonlyColorSet } from 'browser/Types';
+import { ICharSizeService, ICoreBrowserService, IThemeService } from 'browser/services/Services';
import { IOptionsService, IBufferService, IInstantiationService } from 'common/services/Services';
import { EventEmitter, IEvent } from 'common/EventEmitter';
import { color } from 'common/Color';
@@ -40,10 +40,9 @@ export class DomRenderer extends Disposable implements IRenderer {
public dimensions: IRenderDimensions;
- public get onRequestRedraw(): IEvent { return new EventEmitter().event; }
+ public readonly onRequestRedraw = this.register(new EventEmitter()).event;
constructor(
- private _colors: IColorSet,
private readonly _element: HTMLElement,
private readonly _screenElement: HTMLElement,
private readonly _viewportElement: HTMLElement,
@@ -52,7 +51,8 @@ export class DomRenderer extends Disposable implements IRenderer {
@ICharSizeService private readonly _charSizeService: ICharSizeService,
@IOptionsService private readonly _optionsService: IOptionsService,
@IBufferService private readonly _bufferService: IBufferService,
- @ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService
+ @ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService,
+ @IThemeService themeService: IThemeService
) {
super();
this._rowContainer = document.createElement('div');
@@ -79,26 +79,27 @@ export class DomRenderer extends Disposable implements IRenderer {
actualCellHeight: 0
};
this._updateDimensions();
- this._injectCss();
+ this.register(this._optionsService.onOptionChange(() => this._handleOptionsChanged()));
- this._rowFactory = instantiationService.createInstance(DomRendererRowFactory, document, this._colors);
+ this.register(themeService.onChangeColors(e => this._injectCss(e)));
+ this._injectCss(themeService.colors);
+
+ this._rowFactory = instantiationService.createInstance(DomRendererRowFactory, document);
this._element.classList.add(TERMINAL_CLASS_PREFIX + this._terminalClass);
this._screenElement.appendChild(this._rowContainer);
this._screenElement.appendChild(this._selectionContainer);
- this.register(this._linkifier2.onShowLinkUnderline(e => this._onLinkHover(e)));
- this.register(this._linkifier2.onHideLinkUnderline(e => this._onLinkLeave(e)));
- }
+ this.register(this._linkifier2.onShowLinkUnderline(e => this._handleLinkHover(e)));
+ this.register(this._linkifier2.onHideLinkUnderline(e => this._handleLinkLeave(e)));
- public dispose(): void {
- this._element.classList.remove(TERMINAL_CLASS_PREFIX + this._terminalClass);
+ this.register(toDisposable(() => {
+ this._element.classList.remove(TERMINAL_CLASS_PREFIX + this._terminalClass);
- // Outside influences such as React unmounts may manipulate the DOM before our disposal.
- // https://github.com/xtermjs/xterm.js/issues/2960
- removeElementFromParent(this._rowContainer, this._selectionContainer, this._themeStyleElement, this._dimensionsStyleElement);
-
- super.dispose();
+ // Outside influences such as React unmounts may manipulate the DOM before our disposal.
+ // https://github.com/xtermjs/xterm.js/issues/2960
+ removeElementFromParent(this._rowContainer, this._selectionContainer, this._themeStyleElement, this._dimensionsStyleElement);
+ }));
}
private _updateDimensions(): void {
@@ -144,12 +145,7 @@ export class DomRenderer extends Disposable implements IRenderer {
this._screenElement.style.height = `${this.dimensions.canvasHeight}px`;
}
- public setColors(colors: IColorSet): void {
- this._colors = colors;
- this._injectCss();
- }
-
- private _injectCss(): void {
+ private _injectCss(colors: ReadonlyColorSet): void {
if (!this._themeStyleElement) {
this._themeStyleElement = document.createElement('style');
this._screenElement.appendChild(this._themeStyleElement);
@@ -158,7 +154,7 @@ export class DomRenderer extends Disposable implements IRenderer {
// Base CSS
let styles =
`${this._terminalSelector} .${ROW_CONTAINER_CLASS} {` +
- ` color: ${this._colors.foreground.css};` +
+ ` color: ${colors.foreground.css};` +
` font-family: ${this._optionsService.rawOptions.fontFamily};` +
` font-size: ${this._optionsService.rawOptions.fontSize}px;` +
`}`;
@@ -183,18 +179,18 @@ export class DomRenderer extends Disposable implements IRenderer {
styles +=
`@keyframes blink_block` + `_` + this._terminalClass + ` {` +
` 0% {` +
- ` background-color: ${this._colors.cursor.css};` +
- ` color: ${this._colors.cursorAccent.css};` +
+ ` background-color: ${colors.cursor.css};` +
+ ` color: ${colors.cursorAccent.css};` +
` }` +
` 50% {` +
- ` background-color: ${this._colors.cursorAccent.css};` +
- ` color: ${this._colors.cursor.css};` +
+ ` background-color: ${colors.cursorAccent.css};` +
+ ` color: ${colors.cursor.css};` +
` }` +
`}`;
// Cursor
styles +=
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}:not(.${FOCUS_CLASS}) .${CURSOR_CLASS}.${CURSOR_STYLE_BLOCK_CLASS} {` +
- ` outline: 1px solid ${this._colors.cursor.css};` +
+ ` outline: 1px solid ${colors.cursor.css};` +
` outline-offset: -1px;` +
`}` +
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_BLINK_CLASS}:not(.${CURSOR_STYLE_BLOCK_CLASS}) {` +
@@ -204,14 +200,14 @@ export class DomRenderer extends Disposable implements IRenderer {
` animation: blink_block` + `_` + this._terminalClass + ` 1s step-end infinite;` +
`}` +
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_BLOCK_CLASS} {` +
- ` background-color: ${this._colors.cursor.css};` +
- ` color: ${this._colors.cursorAccent.css};` +
+ ` background-color: ${colors.cursor.css};` +
+ ` color: ${colors.cursorAccent.css};` +
`}` +
`${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_BAR_CLASS} {` +
- ` box-shadow: ${this._optionsService.rawOptions.cursorWidth}px 0 0 ${this._colors.cursor.css} inset;` +
+ ` box-shadow: ${this._optionsService.rawOptions.cursorWidth}px 0 0 ${colors.cursor.css} inset;` +
`}` +
`${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_UNDERLINE_CLASS} {` +
- ` box-shadow: 0 -1px 0 ${this._colors.cursor.css} inset;` +
+ ` box-shadow: 0 -1px 0 ${colors.cursor.css} inset;` +
`}`;
// Selection
styles +=
@@ -224,26 +220,26 @@ export class DomRenderer extends Disposable implements IRenderer {
`}` +
`${this._terminalSelector}.focus .${SELECTION_CLASS} div {` +
` position: absolute;` +
- ` background-color: ${this._colors.selectionBackgroundOpaque.css};` +
+ ` background-color: ${colors.selectionBackgroundOpaque.css};` +
`}` +
`${this._terminalSelector} .${SELECTION_CLASS} div {` +
` position: absolute;` +
- ` background-color: ${this._colors.selectionInactiveBackgroundOpaque.css};` +
+ ` background-color: ${colors.selectionInactiveBackgroundOpaque.css};` +
`}`;
// Colors
- this._colors.ansi.forEach((c, i) => {
+ for (const [i, c] of colors.ansi.entries()) {
styles +=
`${this._terminalSelector} .${FG_CLASS_PREFIX}${i} { color: ${c.css}; }` +
`${this._terminalSelector} .${BG_CLASS_PREFIX}${i} { background-color: ${c.css}; }`;
- });
+ }
styles +=
- `${this._terminalSelector} .${FG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { color: ${color.opaque(this._colors.background).css}; }` +
- `${this._terminalSelector} .${BG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { background-color: ${this._colors.foreground.css}; }`;
+ `${this._terminalSelector} .${FG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { color: ${color.opaque(colors.background).css}; }` +
+ `${this._terminalSelector} .${BG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { background-color: ${colors.foreground.css}; }`;
this._themeStyleElement.textContent = styles;
}
- public onDevicePixelRatioChange(): void {
+ public handleDevicePixelRatioChange(): void {
this._updateDimensions();
}
@@ -260,30 +256,30 @@ export class DomRenderer extends Disposable implements IRenderer {
}
}
- public onResize(cols: number, rows: number): void {
+ public handleResize(cols: number, rows: number): void {
this._refreshRowElements(cols, rows);
this._updateDimensions();
}
- public onCharSizeChanged(): void {
+ public handleCharSizeChanged(): void {
this._updateDimensions();
}
- public onBlur(): void {
+ public handleBlur(): void {
this._rowContainer.classList.remove(FOCUS_CLASS);
}
- public onFocus(): void {
+ public handleFocus(): void {
this._rowContainer.classList.add(FOCUS_CLASS);
}
- public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
+ public handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
// Remove all selections
while (this._selectionContainer.children.length) {
this._selectionContainer.removeChild(this._selectionContainer.children[0]);
}
- this._rowFactory.onSelectionChanged(start, end, columnSelectMode);
+ this._rowFactory.handleSelectionChanged(start, end, columnSelectMode);
this.renderRows(0, this._bufferService.rows - 1);
// Selection does not exist
@@ -343,14 +339,13 @@ export class DomRenderer extends Disposable implements IRenderer {
return element;
}
- public onCursorMove(): void {
+ public handleCursorMove(): void {
// No-op, the cursor is drawn when rows are drawn
}
- public onOptionsChanged(): void {
+ private _handleOptionsChanged(): void {
// Force a refresh
this._updateDimensions();
- this._injectCss();
}
public clear(): void {
@@ -378,11 +373,11 @@ export class DomRenderer extends Disposable implements IRenderer {
return `.${TERMINAL_CLASS_PREFIX}${this._terminalClass}`;
}
- private _onLinkHover(e: ILinkifierEvent): void {
+ private _handleLinkHover(e: ILinkifierEvent): void {
this._setCellUnderline(e.x1, e.x2, e.y1, e.y2, e.cols, true);
}
- private _onLinkLeave(e: ILinkifierEvent): void {
+ private _handleLinkLeave(e: ILinkifierEvent): void {
this._setCellUnderline(e.x1, e.x2, e.y1, e.y2, e.cols, false);
}
diff --git a/src/browser/renderer/dom/DomRendererRowFactory.test.ts b/src/browser/renderer/dom/DomRendererRowFactory.test.ts
index 776ff701..fbb48536 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, MockCoreBrowserService } from 'browser/TestUtils.test';
+import { MockCharacterJoinerService, MockCoreBrowserService, MockThemeService } from 'browser/TestUtils.test';
describe('DomRendererRowFactory', () => {
let dom: jsdom.JSDOM;
@@ -23,35 +23,12 @@ describe('DomRendererRowFactory', () => {
dom = new jsdom.JSDOM('');
rowFactory = new DomRendererRowFactory(
dom.window.document,
- {
- background: css.toColor('#010101'),
- foreground: css.toColor('#020202'),
- ansi: [
- // dark:
- css.toColor('#2e3436'),
- css.toColor('#cc0000'),
- css.toColor('#4e9a06'),
- css.toColor('#c4a000'),
- css.toColor('#3465a4'),
- css.toColor('#75507b'),
- css.toColor('#06989a'),
- css.toColor('#d3d7cf'),
- // bright:
- css.toColor('#555753'),
- css.toColor('#ef2929'),
- css.toColor('#8ae234'),
- css.toColor('#fce94f'),
- css.toColor('#729fcf'),
- css.toColor('#ad7fa8'),
- css.toColor('#34e2e2'),
- css.toColor('#eeeeec')
- ]
- } as any,
new MockCharacterJoinerService(),
new MockOptionsService({ drawBoldTextInBrightColors: true }),
new MockCoreBrowserService(),
new MockCoreService(),
- new MockDecorationService()
+ new MockDecorationService(),
+ new MockThemeService()
);
lineData = createEmptyLineData(2);
});
@@ -299,7 +276,7 @@ describe('DomRendererRowFactory', () => {
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);
+ rowFactory.handleSelectionChanged([1, 0], [2, 0], false);
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
'ab'
@@ -307,7 +284,7 @@ describe('DomRendererRowFactory', () => {
});
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);
+ rowFactory.handleSelectionChanged([0, 0], [2, 0], false);
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
' a'
diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts
index b6ee7bf1..14b26c92 100644
--- a/src/browser/renderer/dom/DomRendererRowFactory.ts
+++ b/src/browser/renderer/dom/DomRendererRowFactory.ts
@@ -4,15 +4,15 @@
*/
import { IBufferLine, ICellData, IColor } from 'common/Types';
-import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/Constants';
+import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/shared/Constants';
import { NULL_CELL_CODE, WHITESPACE_CELL_CHAR, Attributes } from 'common/buffer/Constants';
import { CellData } from 'common/buffer/CellData';
import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
import { color, rgba } from 'common/Color';
-import { IColorSet } from 'browser/Types';
-import { ICharacterJoinerService, ICoreBrowserService } from 'browser/services/Services';
+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/RendererUtils';
+import { excludeFromContrastRatioDemands } from 'browser/renderer/shared/RendererUtils';
import { AttributeData } from 'common/buffer/AttributeData';
export const BOLD_CLASS = 'xterm-bold';
@@ -35,20 +35,16 @@ export class DomRendererRowFactory {
constructor(
private readonly _document: Document,
- private _colors: IColorSet,
@ICharacterJoinerService private readonly _characterJoinerService: ICharacterJoinerService,
@IOptionsService private readonly _optionsService: IOptionsService,
@ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService,
@ICoreService private readonly _coreService: ICoreService,
- @IDecorationService private readonly _decorationService: IDecorationService
+ @IDecorationService private readonly _decorationService: IDecorationService,
+ @IThemeService private readonly _themeService: IThemeService
) {
}
- public setColors(colors: IColorSet): void {
- this._colors = colors;
- }
-
- public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
+ public handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
this._selectionStart = start;
this._selectionEnd = end;
this._columnSelectMode = columnSelectMode;
@@ -71,6 +67,8 @@ export class DomRendererRowFactory {
}
}
+ const colors = this._themeService.colors;
+
for (let x = 0; x < lineLength; x++) {
lineData.loadCell(x, this._workCell);
let width = this._workCell.getWidth();
@@ -176,7 +174,7 @@ export class DomRendererRowFactory {
if (this._optionsService.rawOptions.drawBoldTextInBrightColors && cell.isBold() && fg < 8) {
fg += 8;
}
- charElement.style.textDecorationColor = this._colors.ansi[fg].css;
+ charElement.style.textDecorationColor = colors.ansi[fg].css;
}
}
}
@@ -224,17 +222,17 @@ export class DomRendererRowFactory {
// Apply selection foreground if applicable
const isInSelection = this._isCellInSelection(x, row);
if (!isTop) {
- if (this._colors.selectionForeground && isInSelection) {
+ if (colors.selectionForeground && isInSelection) {
fgColorMode = Attributes.CM_RGB;
- fg = this._colors.selectionForeground.rgba >> 8 & 0xFFFFFF;
- fgOverride = this._colors.selectionForeground;
+ fg = colors.selectionForeground.rgba >> 8 & 0xFFFFFF;
+ fgOverride = colors.selectionForeground;
}
}
// If in the selection, force the element to be above the selection to improve contrast and
// support opaque selections
if (isInSelection) {
- bgOverride = this._coreBrowserService.isFocused ? this._colors.selectionBackgroundOpaque : this._colors.selectionInactiveBackgroundOpaque;
+ bgOverride = this._coreBrowserService.isFocused ? colors.selectionBackgroundOpaque : colors.selectionInactiveBackgroundOpaque;
isTop = true;
}
@@ -248,7 +246,7 @@ export class DomRendererRowFactory {
switch (bgColorMode) {
case Attributes.CM_P16:
case Attributes.CM_P256:
- resolvedBg = this._colors.ansi[bg];
+ resolvedBg = colors.ansi[bg];
charElement.classList.add(`xterm-bg-${bg}`);
break;
case Attributes.CM_RGB:
@@ -258,10 +256,10 @@ export class DomRendererRowFactory {
case Attributes.CM_DEFAULT:
default:
if (isInverse) {
- resolvedBg = this._colors.foreground;
+ resolvedBg = colors.foreground;
charElement.classList.add(`xterm-bg-${INVERTED_DEFAULT_COLOR}`);
} else {
- resolvedBg = this._colors.background;
+ resolvedBg = colors.background;
}
}
@@ -279,7 +277,7 @@ export class DomRendererRowFactory {
if (cell.isBold() && fg < 8 && this._optionsService.rawOptions.drawBoldTextInBrightColors) {
fg += 8;
}
- if (!this._applyMinimumContrast(charElement, resolvedBg, this._colors.ansi[fg], cell, bgOverride, undefined)) {
+ if (!this._applyMinimumContrast(charElement, resolvedBg, colors.ansi[fg], cell, bgOverride, undefined)) {
charElement.classList.add(`xterm-fg-${fg}`);
}
break;
@@ -295,7 +293,7 @@ export class DomRendererRowFactory {
break;
case Attributes.CM_DEFAULT:
default:
- if (!this._applyMinimumContrast(charElement, resolvedBg, this._colors.foreground, cell, bgOverride, undefined)) {
+ if (!this._applyMinimumContrast(charElement, resolvedBg, colors.foreground, cell, bgOverride, undefined)) {
if (isInverse) {
charElement.classList.add(`xterm-fg-${INVERTED_DEFAULT_COLOR}`);
}
@@ -317,13 +315,13 @@ 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(bg.rgba, fg.rgba);
+ adjustedColor = this._themeService.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);
- this._colors.contrastCache.setColor((bgOverride || bg).rgba, (fgOverride || fg).rgba, adjustedColor ?? null);
+ this._themeService.colors.contrastCache.setColor((bgOverride || bg).rgba, (fgOverride || fg).rgba, adjustedColor ?? null);
}
if (adjustedColor) {
diff --git a/src/browser/renderer/shared/CellColorResolver.ts b/src/browser/renderer/shared/CellColorResolver.ts
new file mode 100644
index 00000000..ae5019e2
--- /dev/null
+++ b/src/browser/renderer/shared/CellColorResolver.ts
@@ -0,0 +1,137 @@
+import { ISelectionRenderModel } from 'browser/renderer/shared/Types';
+import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
+import { IColorSet, ReadonlyColorSet } from 'browser/Types';
+import { Attributes, BgFlags, FgFlags } from 'common/buffer/Constants';
+import { IDecorationService } from 'common/services/Services';
+import { ICellData } from 'common/Types';
+import { Terminal } from 'xterm';
+
+// Work variables to avoid garbage collection
+let $fg = 0;
+let $bg = 0;
+let $hasFg = false;
+let $hasBg = false;
+let $isSelected = false;
+let $colors: ReadonlyColorSet | undefined;
+
+export class CellColorResolver {
+ /**
+ * The shared result of the {@link resolve} call. This is only safe to use immediately after as
+ * any other calls will share object.
+ */
+ public readonly result: { fg: number, bg: number, ext: number } = {
+ fg: 0,
+ bg: 0,
+ ext: 0
+ };
+
+ constructor(
+ private readonly _terminal: Terminal,
+ private readonly _selectionRenderModel: ISelectionRenderModel,
+ private readonly _decorationService: IDecorationService,
+ private readonly _coreBrowserService: ICoreBrowserService,
+ private readonly _themeService: IThemeService
+ ) {
+ }
+
+ /**
+ * Resolves colors for the cell, putting the result into the shared {@link result}. This resolves
+ * overrides, inverse and selection for the cell which can then be used to feed into the renderer.
+ */
+ public resolve(cell: ICellData, x: number, y: number): void {
+ this.result.bg = cell.bg;
+ this.result.fg = cell.fg;
+ this.result.ext = cell.bg & BgFlags.HAS_EXTENDED ? cell.extended.ext : 0;
+ // Get any foreground/background overrides, this happens on the model to avoid spreading
+ // override logic throughout the different sub-renderers
+
+ // Reset overrides work variables
+ $bg = 0;
+ $fg = 0;
+ $hasBg = false;
+ $hasFg = false;
+ $isSelected = false;
+ $colors = this._themeService.colors;
+
+ // Apply decorations on the bottom layer
+ this._decorationService.forEachDecorationAtCell(x, y, 'bottom', d => {
+ if (d.backgroundColorRGB) {
+ $bg = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF;
+ $hasBg = true;
+ }
+ if (d.foregroundColorRGB) {
+ $fg = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF;
+ $hasFg = true;
+ }
+ });
+
+ // Apply the selection color if needed
+ $isSelected = this._selectionRenderModel.isCellSelected(this._terminal, x, y);
+ if ($isSelected) {
+ $bg = (this._coreBrowserService.isFocused ? $colors.selectionBackgroundOpaque : $colors.selectionInactiveBackgroundOpaque).rgba >> 8 & 0xFFFFFF;
+ $hasBg = true;
+ if ($colors.selectionForeground) {
+ $fg = $colors.selectionForeground.rgba >> 8 & 0xFFFFFF;
+ $hasFg = true;
+ }
+ }
+
+ // Apply decorations on the top layer
+ this._decorationService.forEachDecorationAtCell(x, y, 'top', d => {
+ if (d.backgroundColorRGB) {
+ $bg = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF;
+ $hasBg = true;
+ }
+ if (d.foregroundColorRGB) {
+ $fg = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF;
+ $hasFg = true;
+ }
+ });
+
+ // Convert any overrides from rgba to the fg/bg packed format. This resolves the inverse flag
+ // ahead of time in order to use the correct cache key
+ if ($hasBg) {
+ if ($isSelected) {
+ // Non-RGB attributes from model + force non-dim + override + force RGB color mode
+ $bg = (cell.bg & ~Attributes.RGB_MASK & ~BgFlags.DIM) | $bg | Attributes.CM_RGB;
+ } else {
+ // Non-RGB attributes from model + override + force RGB color mode
+ $bg = (cell.bg & ~Attributes.RGB_MASK) | $bg | Attributes.CM_RGB;
+ }
+ }
+ if ($hasFg) {
+ // Non-RGB attributes from model + force disable inverse + override + force RGB color mode
+ $fg = (cell.fg & ~Attributes.RGB_MASK & ~FgFlags.INVERSE) | $fg | Attributes.CM_RGB;
+ }
+
+ // Handle case where inverse was specified by only one of bg override or fg override was set,
+ // resolving the other inverse color and setting the inverse flag if needed.
+ if (this.result.fg & FgFlags.INVERSE) {
+ if ($hasBg && !$hasFg) {
+ // Resolve bg color type (default color has a different meaning in fg vs bg)
+ if ((this.result.bg & Attributes.CM_MASK) === Attributes.CM_DEFAULT) {
+ $fg = (this.result.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | (($colors.background.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB;
+ } else {
+ $fg = (this.result.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | this.result.bg & (Attributes.RGB_MASK | Attributes.CM_MASK);
+ }
+ $hasFg = true;
+ }
+ if (!$hasBg && $hasFg) {
+ // Resolve bg color type (default color has a different meaning in fg vs bg)
+ if ((this.result.fg & Attributes.CM_MASK) === Attributes.CM_DEFAULT) {
+ $bg = (this.result.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | (($colors.foreground.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB;
+ } else {
+ $bg = (this.result.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | this.result.fg & (Attributes.RGB_MASK | Attributes.CM_MASK);
+ }
+ $hasBg = true;
+ }
+ }
+
+ // Release object
+ $colors = undefined;
+
+ // Use the override if it exists
+ this.result.bg = $hasBg ? $bg : this.result.bg;
+ this.result.fg = $hasFg ? $fg : this.result.fg;
+ }
+}
diff --git a/addons/xterm-addon-webgl/src/atlas/CharAtlasCache.ts b/src/browser/renderer/shared/CharAtlasCache.ts
similarity index 80%
rename from addons/xterm-addon-webgl/src/atlas/CharAtlasCache.ts
rename to src/browser/renderer/shared/CharAtlasCache.ts
index 893362c8..e953dc46 100644
--- a/addons/xterm-addon-webgl/src/atlas/CharAtlasCache.ts
+++ b/src/browser/renderer/shared/CharAtlasCache.ts
@@ -3,21 +3,21 @@
* @license MIT
*/
-import { generateConfig, configEquals } from './CharAtlasUtils';
-import { WebglCharAtlas } from './WebglCharAtlas';
-import { ICharAtlasConfig } from './Types';
+import { TextureAtlas } from 'browser/renderer/shared/TextureAtlas';
import { Terminal } from 'xterm';
-import { IColorSet, ITerminal } from 'browser/Types';
+import { ITerminal, ReadonlyColorSet } from 'browser/Types';
+import { ICharAtlasConfig, ITextureAtlas } from 'browser/renderer/shared/Types';
+import { generateConfig, configEquals } from 'browser/renderer/shared/CharAtlasUtils';
-interface ICharAtlasCacheEntry {
- atlas: WebglCharAtlas;
+interface ITextureAtlasCacheEntry {
+ atlas: ITextureAtlas;
config: ICharAtlasConfig;
// N.B. This implementation potentially holds onto copies of the terminal forever, so
// this may cause memory leaks.
ownedBy: Terminal[];
}
-const charAtlasCache: ICharAtlasCacheEntry[] = [];
+const charAtlasCache: ITextureAtlasCacheEntry[] = [];
/**
* Acquires a char atlas, either generating a new one or returning an existing
@@ -25,15 +25,15 @@ const charAtlasCache: ICharAtlasCacheEntry[] = [];
* @param terminal The terminal.
* @param colors The colors to use.
*/
-export function acquireCharAtlas(
+export function acquireTextureAtlas(
terminal: Terminal,
- colors: IColorSet,
+ colors: ReadonlyColorSet,
scaledCellWidth: number,
scaledCellHeight: number,
scaledCharWidth: number,
scaledCharHeight: number,
devicePixelRatio: number
-): WebglCharAtlas {
+): ITextureAtlas {
const newConfig = generateConfig(scaledCellWidth, scaledCellHeight, scaledCharWidth, scaledCharHeight, terminal, colors, devicePixelRatio);
// Check to see if the terminal already owns this config
@@ -66,8 +66,8 @@ export function acquireCharAtlas(
}
const core: ITerminal = (terminal as any)._core;
- const newEntry: ICharAtlasCacheEntry = {
- atlas: new WebglCharAtlas(document, newConfig, core.unicodeService),
+ const newEntry: ITextureAtlasCacheEntry = {
+ atlas: new TextureAtlas(document, newConfig, core.unicodeService),
config: newConfig,
ownedBy: [terminal]
};
diff --git a/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts b/src/browser/renderer/shared/CharAtlasUtils.ts
similarity index 87%
rename from addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts
rename to src/browser/renderer/shared/CharAtlasUtils.ts
index 83f82fa7..e443168e 100644
--- a/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts
+++ b/src/browser/renderer/shared/CharAtlasUtils.ts
@@ -5,16 +5,11 @@
import { ICharAtlasConfig } from './Types';
import { Attributes } from 'common/buffer/Constants';
-import { Terminal, FontWeight } from 'xterm';
-import { IColorSet } from 'browser/Types';
-import { IColor } from 'common/Types';
+import { Terminal } from 'xterm';
+import { IColorSet, ReadonlyColorSet } from 'browser/Types';
+import { NULL_COLOR } from 'common/Color';
-const NULL_COLOR: IColor = {
- css: '',
- rgba: 0
-};
-
-export function generateConfig(scaledCellWidth: number, scaledCellHeight: number, scaledCharWidth: number, scaledCharHeight: number, terminal: Terminal, colors: IColorSet, devicePixelRatio: number): ICharAtlasConfig {
+export function generateConfig(scaledCellWidth: number, scaledCellHeight: number, scaledCharWidth: number, scaledCharHeight: number, terminal: Terminal, colors: ReadonlyColorSet, devicePixelRatio: number): ICharAtlasConfig {
// null out some fields that don't matter
const clonedColors: IColorSet = {
foreground: colors.foreground,
@@ -70,8 +65,8 @@ export function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean
a.scaledCharHeight === b.scaledCharHeight &&
a.drawBoldTextInBrightColors === b.drawBoldTextInBrightColors &&
a.minimumContrastRatio === b.minimumContrastRatio &&
- a.colors.foreground === b.colors.foreground &&
- a.colors.background === b.colors.background;
+ a.colors.foreground.rgba === b.colors.foreground.rgba &&
+ a.colors.background.rgba === b.colors.background.rgba;
}
export function is256Color(colorCode: number): boolean {
diff --git a/src/browser/renderer/Constants.ts b/src/browser/renderer/shared/Constants.ts
similarity index 100%
rename from src/browser/renderer/Constants.ts
rename to src/browser/renderer/shared/Constants.ts
diff --git a/src/browser/renderer/CustomGlyphs.ts b/src/browser/renderer/shared/CustomGlyphs.ts
similarity index 99%
rename from src/browser/renderer/CustomGlyphs.ts
rename to src/browser/renderer/shared/CustomGlyphs.ts
index 32256cf4..b8725685 100644
--- a/src/browser/renderer/CustomGlyphs.ts
+++ b/src/browser/renderer/shared/CustomGlyphs.ts
@@ -3,7 +3,7 @@
* @license MIT
*/
-import { throwIfFalsy } from 'browser/renderer/RendererUtils';
+import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
interface IBlockVector {
x: number;
diff --git a/src/browser/renderer/DevicePixelObserver.ts b/src/browser/renderer/shared/DevicePixelObserver.ts
similarity index 100%
rename from src/browser/renderer/DevicePixelObserver.ts
rename to src/browser/renderer/shared/DevicePixelObserver.ts
diff --git a/src/browser/renderer/shared/README.md b/src/browser/renderer/shared/README.md
new file mode 100644
index 00000000..58084235
--- /dev/null
+++ b/src/browser/renderer/shared/README.md
@@ -0,0 +1 @@
+This folder contains files that are shared between the renderer addons, but not necessarily bundled into the `xterm` module.
diff --git a/src/browser/renderer/RendererUtils.ts b/src/browser/renderer/shared/RendererUtils.ts
similarity index 100%
rename from src/browser/renderer/RendererUtils.ts
rename to src/browser/renderer/shared/RendererUtils.ts
diff --git a/src/browser/renderer/shared/SelectionRenderModel.ts b/src/browser/renderer/shared/SelectionRenderModel.ts
new file mode 100644
index 00000000..db375778
--- /dev/null
+++ b/src/browser/renderer/shared/SelectionRenderModel.ts
@@ -0,0 +1,91 @@
+/**
+ * Copyright (c) 2022 The xterm.js authors. All rights reserved.
+ * @license MIT
+ */
+
+import { ISelectionRenderModel } from 'browser/renderer/shared/Types';
+import { Terminal } from 'xterm';
+
+class SelectionRenderModel implements ISelectionRenderModel {
+ public hasSelection!: boolean;
+ public columnSelectMode!: boolean;
+ public viewportStartRow!: number;
+ public viewportEndRow!: number;
+ public viewportCappedStartRow!: number;
+ public viewportCappedEndRow!: number;
+ public startCol!: number;
+ public endCol!: number;
+ public selectionStart: [number, number] | undefined;
+ public selectionEnd: [number, number] | undefined;
+
+ constructor() {
+ this.clear();
+ }
+
+ public clear(): void {
+ this.hasSelection = false;
+ this.columnSelectMode = false;
+ this.viewportStartRow = 0;
+ this.viewportEndRow = 0;
+ this.viewportCappedStartRow = 0;
+ this.viewportCappedEndRow = 0;
+ this.startCol = 0;
+ this.endCol = 0;
+ this.selectionStart = undefined;
+ this.selectionEnd = undefined;
+ }
+
+ public update(terminal: Terminal, start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {
+ this.selectionStart = start;
+ this.selectionEnd = end;
+ // Selection does not exist
+ if (!start || !end || (start[0] === end[0] && start[1] === end[1])) {
+ this.clear();
+ return;
+ }
+
+ // Translate from buffer position to viewport position
+ const viewportStartRow = start[1] - terminal.buffer.active.viewportY;
+ const viewportEndRow = end[1] - terminal.buffer.active.viewportY;
+ const viewportCappedStartRow = Math.max(viewportStartRow, 0);
+ const viewportCappedEndRow = Math.min(viewportEndRow, terminal.rows - 1);
+
+ // No need to draw the selection
+ if (viewportCappedStartRow >= terminal.rows || viewportCappedEndRow < 0) {
+ this.clear();
+ return;
+ }
+
+ this.hasSelection = true;
+ this.columnSelectMode = columnSelectMode;
+ this.viewportStartRow = viewportStartRow;
+ this.viewportEndRow = viewportEndRow;
+ this.viewportCappedStartRow = viewportCappedStartRow;
+ this.viewportCappedEndRow = viewportCappedEndRow;
+ this.startCol = start[0];
+ this.endCol = end[0];
+ }
+
+ public isCellSelected(terminal: Terminal, x: number, y: number): boolean {
+ if (!this.hasSelection) {
+ return false;
+ }
+ y -= terminal.buffer.active.viewportY;
+ if (this.columnSelectMode) {
+ if (this.startCol <= this.endCol) {
+ return x >= this.startCol && y >= this.viewportCappedStartRow &&
+ x < this.endCol && y <= this.viewportCappedEndRow;
+ }
+ return x < this.startCol && y >= this.viewportCappedStartRow &&
+ x >= this.endCol && y <= this.viewportCappedEndRow;
+ }
+ return (y > this.viewportStartRow && y < this.viewportEndRow) ||
+ (this.viewportStartRow === this.viewportEndRow && y === this.viewportStartRow && x >= this.startCol && x < this.endCol) ||
+ (this.viewportStartRow < this.viewportEndRow && y === this.viewportEndRow && x < this.endCol) ||
+ (this.viewportStartRow < this.viewportEndRow && y === this.viewportStartRow && x >= this.startCol);
+ }
+}
+
+export function createSelectionRenderModel(): ISelectionRenderModel {
+ return new SelectionRenderModel();
+}
diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/src/browser/renderer/shared/TextureAtlas.ts
similarity index 97%
rename from addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts
rename to src/browser/renderer/shared/TextureAtlas.ts
index 4764ede4..d450cfdc 100644
--- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts
+++ b/src/browser/renderer/shared/TextureAtlas.ts
@@ -3,20 +3,17 @@
* @license MIT
*/
-import { ICharAtlasConfig } from './Types';
-import { DIM_OPACITY, TEXT_BASELINE } from 'browser/renderer/Constants';
-import { IRasterizedGlyph, IBoundingBox } from '../Types';
+import { DIM_OPACITY, TEXT_BASELINE } from 'browser/renderer/shared/Constants';
import { DEFAULT_COLOR, Attributes, DEFAULT_EXT, UnderlineStyle } from 'common/buffer/Constants';
-import { throwIfFalsy } from '../WebglUtils';
import { IColor } from 'common/Types';
-import { IDisposable } from 'xterm';
import { AttributeData } from 'common/buffer/AttributeData';
-import { color, rgba } from 'common/Color';
-import { tryDrawCustomChar } from 'browser/renderer/CustomGlyphs';
-import { excludeFromContrastRatioDemands, isPowerlineGlyph, isRestrictedPowerlineGlyph } from 'browser/renderer/RendererUtils';
+import { color, NULL_COLOR, rgba } from 'common/Color';
+import { tryDrawCustomChar } from 'browser/renderer/shared/CustomGlyphs';
+import { excludeFromContrastRatioDemands, isPowerlineGlyph, isRestrictedPowerlineGlyph, throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
import { IUnicodeService } from 'common/services/Services';
import { FourKeyMap } from 'common/MultiKeyMap';
import { IdleTaskQueue } from 'common/TaskQueue';
+import { IBoundingBox, ICharAtlasConfig, IRasterizedGlyph, ITextureAtlas } from 'browser/renderer/shared/Types';
// For debugging purposes, it can be useful to set this to a really tiny value,
// to verify that LRU eviction works.
@@ -29,12 +26,6 @@ const TEXTURE_HEIGHT = 1024;
* this prevent juggling multiple textures in the GL context.
*/
const TEXTURE_CAPACITY = Math.floor(TEXTURE_HEIGHT * 0.8);
-
-const TRANSPARENT_COLOR = {
- css: 'rgba(0, 0, 0, 0)',
- rgba: 0
-};
-
/**
* A shared object which is used to draw nothing for a particular cell.
*/
@@ -54,12 +45,10 @@ interface ICharAtlasActiveRow {
height: number;
}
-/** Work variables to avoid garbage collection. */
-const w: { glyph: IRasterizedGlyph | undefined } = {
- glyph: undefined
-};
+// Work variables to avoid garbage collection
+let $glyph = undefined;
-export class WebglCharAtlas implements IDisposable {
+export class TextureAtlas implements ITextureAtlas {
private _didWarmUp: boolean = false;
private _cacheMap: FourKeyMap = new FourKeyMap();
@@ -164,6 +153,7 @@ export class WebglCharAtlas implements IDisposable {
this._currentRow.height = 0;
this._fixedRows.length = 0;
this._didWarmUp = false;
+ this.hasCanvasChanged = true;
}
public getRasterizedGlyphCombinedChar(chars: string, bg: number, fg: number, ext: number): IRasterizedGlyph {
@@ -184,12 +174,12 @@ export class WebglCharAtlas implements IDisposable {
fg: number,
ext: number
): IRasterizedGlyph {
- w.glyph = cacheMap.get(key, bg, fg, ext);
- if (!w.glyph) {
- w.glyph = this._drawToCache(key, bg, fg, ext);
- cacheMap.set(key, bg, fg, ext, w.glyph);
+ $glyph = cacheMap.get(key, bg, fg, ext);
+ if (!$glyph) {
+ $glyph = this._drawToCache(key, bg, fg, ext);
+ cacheMap.set(key, bg, fg, ext, $glyph);
}
- return w.glyph;
+ return $glyph;
}
private _getColorFromAnsiIndex(idx: number): IColor {
@@ -204,7 +194,7 @@ export class WebglCharAtlas implements IDisposable {
// The background color might have some transparency, so we need to render it as fully
// transparent in the atlas. Otherwise we'd end up drawing the transparent background twice
// around the anti-aliased edges of the glyph, and it would look too dark.
- return TRANSPARENT_COLOR;
+ return NULL_COLOR;
}
let result: IColor;
@@ -347,6 +337,9 @@ export class WebglCharAtlas implements IDisposable {
private _drawToCache(codeOrChars: number | string, bg: number, fg: number, ext: number): IRasterizedGlyph {
const chars = typeof codeOrChars === 'number' ? String.fromCharCode(codeOrChars) : codeOrChars;
+ // Uncomment for debugging
+ // console.log(`draw to cache "${chars}"`, bg, fg, ext);
+
this.hasCanvasChanged = true;
// Allow 1 cell width per character, with a minimum of 2 (CJK), plus some padding. This is used
diff --git a/src/browser/renderer/shared/Types.d.ts b/src/browser/renderer/shared/Types.d.ts
new file mode 100644
index 00000000..61a90890
--- /dev/null
+++ b/src/browser/renderer/shared/Types.d.ts
@@ -0,0 +1,154 @@
+/**
+ * Copyright (c) 2017 The xterm.js authors. All rights reserved.
+ * @license MIT
+ */
+
+import { FontWeight, Terminal } from 'xterm';
+import { IColorSet, ReadonlyColorSet } from 'browser/Types';
+import { IDisposable } from 'common/Types';
+import { IEvent } from 'common/EventEmitter';
+
+export interface ICharAtlasConfig {
+ customGlyphs: boolean;
+ devicePixelRatio: number;
+ letterSpacing: number;
+ lineHeight: number;
+ fontSize: number;
+ fontFamily: string;
+ fontWeight: FontWeight;
+ fontWeightBold: FontWeight;
+ scaledCellWidth: number;
+ scaledCellHeight: number;
+ scaledCharWidth: number;
+ scaledCharHeight: number;
+ allowTransparency: boolean;
+ drawBoldTextInBrightColors: boolean;
+ minimumContrastRatio: number;
+ colors: IColorSet;
+}
+
+export interface IRenderDimensions {
+ scaledCharWidth: number;
+ scaledCharHeight: number;
+ scaledCellWidth: number;
+ scaledCellHeight: number;
+ scaledCharLeft: number;
+ scaledCharTop: number;
+ scaledCanvasWidth: number;
+ scaledCanvasHeight: number;
+ canvasWidth: number;
+ canvasHeight: number;
+ actualCellWidth: number;
+ actualCellHeight: number;
+}
+
+export interface IRequestRedrawEvent {
+ start: number;
+ end: number;
+}
+
+/**
+ * Note that IRenderer implementations should emit the refresh event after
+ * rendering rows to the screen.
+ */
+export interface IRenderer extends IDisposable {
+ readonly dimensions: IRenderDimensions;
+
+ /**
+ * Fires when the renderer is requesting to be redrawn on the next animation
+ * frame but is _not_ a result of content changing (eg. selection changes).
+ */
+ readonly onRequestRedraw: IEvent;
+
+ dispose(): void;
+ handleDevicePixelRatioChange(): void;
+ handleResize(cols: number, rows: number): void;
+ handleCharSizeChanged(): void;
+ handleBlur(): void;
+ handleFocus(): void;
+ handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void;
+ handleCursorMove(): void;
+ clear(): void;
+ renderRows(start: number, end: number): void;
+ clearTextureAtlas?(): void;
+}
+
+export interface ITextureAtlas extends IDisposable {
+ readonly cacheCanvas: HTMLCanvasElement;
+ hasCanvasChanged: boolean;
+
+ /**
+ * Warm up the texture atlas, adding common glyphs to avoid slowing early frame.
+ */
+ warmUp(): void;
+
+ /**
+ * Call when a frame is being drawn, this will return true if the atlas was cleared to make room
+ * for a new set of glyphs.
+ */
+ beginFrame(): boolean;
+
+ /**
+ * Clear all glyphs from the texture atlas.
+ */
+ clearTexture(): void;
+ getRasterizedGlyph(code: number, bg: number, fg: number, ext: number): IRasterizedGlyph;
+ getRasterizedGlyphCombinedChar(chars: string, bg: number, fg: number, ext: number): IRasterizedGlyph;
+}
+
+/**
+ * Represents a rasterized glyph within a texture atlas. Some numbers are
+ * tracked in CSS pixels as well in order to reduce calculations during the
+ * render loop.
+ */
+export interface IRasterizedGlyph {
+ /**
+ * The x and y offset between the glyph's top/left and the top/left of a cell
+ * in pixels.
+ */
+ offset: IVector;
+ /**
+ * the x and y position of the glyph in the texture in pixels.
+ */
+ texturePosition: IVector;
+ /**
+ * the x and y position of the glyph in the texture in clip space coordinates.
+ */
+ texturePositionClipSpace: IVector;
+ /**
+ * The width and height of the glyph in the texture in pixels.
+ */
+ size: IVector;
+ /**
+ * The width and height of the glyph in the texture in clip space coordinates.
+ */
+ sizeClipSpace: IVector;
+}
+
+export interface IVector {
+ x: number;
+ y: number;
+}
+
+export interface IBoundingBox {
+ top: number;
+ left: number;
+ right: number;
+ bottom: number;
+}
+
+export interface ISelectionRenderModel {
+ readonly hasSelection: boolean;
+ readonly columnSelectMode: boolean;
+ readonly viewportStartRow: number;
+ readonly viewportEndRow: number;
+ readonly viewportCappedStartRow: number;
+ readonly viewportCappedEndRow: number;
+ readonly startCol: number;
+ readonly endCol: number;
+ readonly selectionStart: [number, number] | undefined;
+ readonly selectionEnd: [number, number] | undefined;
+ clear(): void;
+ update(terminal: Terminal, start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode?: boolean): void;
+ isCellSelected(terminal: Terminal, x: number, y: number): boolean;
+}
diff --git a/src/browser/selection/SelectionModel.test.ts b/src/browser/selection/SelectionModel.test.ts
index 5ce3e316..79ff372b 100644
--- a/src/browser/selection/SelectionModel.test.ts
+++ b/src/browser/selection/SelectionModel.test.ts
@@ -50,17 +50,17 @@ describe('SelectionModel', () => {
it('should trim a portion of the selection when a part of it is trimmed', () => {
model.selectionStart = [0, 0];
model.selectionEnd = [10, 2];
- model.onTrim(1);
+ model.handleTrim(1);
assert.deepEqual(model.finalSelectionStart, [0, 0]);
assert.deepEqual(model.finalSelectionEnd, [10, 1]);
- model.onTrim(1);
+ model.handleTrim(1);
assert.deepEqual(model.finalSelectionStart, [0, 0]);
assert.deepEqual(model.finalSelectionEnd, [10, 0]);
});
it('should clear selection when it is trimmed in its entirety', () => {
model.selectionStart = [0, 0];
model.selectionEnd = [10, 0];
- model.onTrim(1);
+ model.handleTrim(1);
assert.deepEqual(model.finalSelectionStart, undefined);
assert.deepEqual(model.finalSelectionEnd, undefined);
});
diff --git a/src/browser/selection/SelectionModel.ts b/src/browser/selection/SelectionModel.ts
index 6c8abbfd..041c7b24 100644
--- a/src/browser/selection/SelectionModel.ts
+++ b/src/browser/selection/SelectionModel.ts
@@ -120,7 +120,7 @@ export class SelectionModel {
* @param amount The amount the buffer is being trimmed.
* @return Whether a refresh is necessary.
*/
- public onTrim(amount: number): boolean {
+ public handleTrim(amount: number): boolean {
// Adjust the selection position based on the trimmed amount.
if (this.selectionStart) {
this.selectionStart[1] -= amount;
diff --git a/src/browser/services/CharSizeService.ts b/src/browser/services/CharSizeService.ts
index b04e157f..45bbe840 100644
--- a/src/browser/services/CharSizeService.ts
+++ b/src/browser/services/CharSizeService.ts
@@ -4,10 +4,12 @@
*/
import { IOptionsService } from 'common/services/Services';
-import { IEvent, EventEmitter } from 'common/EventEmitter';
+import { EventEmitter } from 'common/EventEmitter';
import { ICharSizeService } from 'browser/services/Services';
+import { Disposable } from 'common/Lifecycle';
+import { ITerminalOptions } from 'common/Types';
-export class CharSizeService implements ICharSizeService {
+export class CharSizeService extends Disposable implements ICharSizeService {
public serviceBrand: undefined;
public width: number = 0;
@@ -16,15 +18,17 @@ export class CharSizeService implements ICharSizeService {
public get hasValidSize(): boolean { return this.width > 0 && this.height > 0; }
- private _onCharSizeChange = new EventEmitter();
- public get onCharSizeChange(): IEvent { return this._onCharSizeChange.event; }
+ private readonly _onCharSizeChange = this.register(new EventEmitter());
+ public readonly onCharSizeChange = this._onCharSizeChange.event;
constructor(
document: Document,
parentElement: HTMLElement,
@IOptionsService private readonly _optionsService: IOptionsService
) {
+ super();
this._measureStrategy = new DomMeasureStrategy(document, parentElement, this._optionsService);
+ this.register(this._optionsService.onMultipleOptionChange(['fontFamily', 'fontSize'], () => this.measure()));
}
public measure(): void {
diff --git a/src/browser/services/RenderService.ts b/src/browser/services/RenderService.ts
index 97258609..190967db 100644
--- a/src/browser/services/RenderService.ts
+++ b/src/browser/services/RenderService.ts
@@ -3,16 +3,17 @@
* @license MIT
*/
-import { IRenderer, IRenderDimensions } from 'browser/renderer/Types';
+import { IRenderer, IRenderDimensions } from 'browser/renderer/shared/Types';
import { RenderDebouncer } from 'browser/RenderDebouncer';
import { EventEmitter, IEvent } from 'common/EventEmitter';
import { Disposable } from 'common/Lifecycle';
import { ScreenDprMonitor } from 'browser/ScreenDprMonitor';
import { addDisposableDomListener } from 'browser/Lifecycle';
-import { IColorSet, IRenderDebouncerWithCallback } from 'browser/Types';
+import { IColorSet, IRenderDebouncerWithCallback, ReadonlyColorSet } from 'browser/Types';
import { IOptionsService, IBufferService, IDecorationService } from 'common/services/Services';
-import { ICharSizeService, ICoreBrowserService, IRenderService } from 'browser/services/Services';
+import { ICharSizeService, ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services';
import { DebouncedIdleTask } from 'common/TaskQueue';
+import { ThemeService } from 'browser/services/ThemeService';
interface ISelectionState {
start: [number, number] | undefined;
@@ -23,6 +24,7 @@ interface ISelectionState {
export class RenderService extends Disposable implements IRenderService {
public serviceBrand: undefined;
+ private _renderer: IRenderer | undefined;
private _renderDebouncer: IRenderDebouncerWithCallback;
private _screenDprMonitor: ScreenDprMonitor;
private _pausedResizeTask = new DebouncedIdleTask();
@@ -39,42 +41,42 @@ export class RenderService extends Disposable implements IRenderService {
columnSelectMode: false
};
- private _onDimensionsChange = new EventEmitter();
- public get onDimensionsChange(): IEvent { return this._onDimensionsChange.event; }
- private _onRenderedViewportChange = new EventEmitter<{ start: number, end: number }>();
- public get onRenderedViewportChange(): IEvent<{ start: number, end: number }> { return this._onRenderedViewportChange.event; }
- private _onRender = new EventEmitter<{ start: number, end: number }>();
- public get onRender(): IEvent<{ start: number, end: number }> { return this._onRender.event; }
- private _onRefreshRequest = new EventEmitter<{ start: number, end: number }>();
- public get onRefreshRequest(): IEvent<{ start: number, end: number }> { return this._onRefreshRequest.event; }
+ private readonly _onDimensionsChange = this.register(new EventEmitter());
+ public readonly onDimensionsChange = this._onDimensionsChange.event;
+ private readonly _onRenderedViewportChange = this.register(new EventEmitter<{ start: number, end: number }>());
+ public readonly onRenderedViewportChange = this._onRenderedViewportChange.event;
+ private readonly _onRender = this.register(new EventEmitter<{ start: number, end: number }>());
+ public readonly onRender = this._onRender.event;
+ private readonly _onRefreshRequest = this.register(new EventEmitter<{ start: number, end: number }>());
+ public readonly onRefreshRequest = this._onRefreshRequest.event;
- public get dimensions(): IRenderDimensions { return this._renderer.dimensions; }
+ public get dimensions(): IRenderDimensions { return this._renderer!.dimensions; }
constructor(
- private _renderer: IRenderer,
private _rowCount: number,
screenElement: HTMLElement,
@IOptionsService optionsService: IOptionsService,
@ICharSizeService private readonly _charSizeService: ICharSizeService,
@IDecorationService decorationService: IDecorationService,
@IBufferService bufferService: IBufferService,
- @ICoreBrowserService coreBrowserService: ICoreBrowserService
+ @ICoreBrowserService coreBrowserService: ICoreBrowserService,
+ @IThemeService themeService: IThemeService
) {
super();
- this.register({ dispose: () => this._renderer.dispose() });
+ this.register({ dispose: () => this._renderer?.dispose() });
this._renderDebouncer = new RenderDebouncer(coreBrowserService.window, (start, end) => this._renderRows(start, end));
this.register(this._renderDebouncer);
this._screenDprMonitor = new ScreenDprMonitor(coreBrowserService.window);
- this._screenDprMonitor.setListener(() => this.onDevicePixelRatioChange());
+ this._screenDprMonitor.setListener(() => this.handleDevicePixelRatioChange());
this.register(this._screenDprMonitor);
this.register(bufferService.onResize(() => this._fullRefresh()));
this.register(bufferService.buffers.onBufferActivate(() => this._renderer?.clear()));
this.register(optionsService.onOptionChange(() => this._handleOptionsChanged()));
- this.register(this._charSizeService.onCharSizeChange(() => this.onCharSizeChanged()));
+ this.register(this._charSizeService.onCharSizeChange(() => this.handleCharSizeChanged()));
// Do a full refresh whenever any decoration is added or removed. This may not actually result
// in changes but since decorations should be used sparingly or added/removed all in the same
@@ -82,23 +84,45 @@ export class RenderService extends Disposable implements IRenderService {
this.register(decorationService.onDecorationRegistered(() => this._fullRefresh()));
this.register(decorationService.onDecorationRemoved(() => this._fullRefresh()));
- // No need to register this as renderer is explicitly disposed in RenderService.dispose
- this._renderer.onRequestRedraw(e => this.refreshRows(e.start, e.end, true));
+ // Clear the renderer when the a change that could affect glyphs occurs
+ this.register(optionsService.onMultipleOptionChange([
+ 'customGlyphs',
+ 'drawBoldTextInBrightColors',
+ 'letterSpacing',
+ 'lineHeight',
+ 'fontFamily',
+ 'fontSize',
+ 'fontWeight',
+ 'fontWeightBold',
+ 'minimumContrastRatio'
+ ], () => {
+ this.clear();
+ this.handleResize(bufferService.cols, bufferService.rows);
+ this._fullRefresh();
+ }));
+
+ // Refresh the cursor line when the cursor changes
+ this.register(optionsService.onMultipleOptionChange([
+ 'cursorBlink',
+ 'cursorStyle'
+ ], () => this.refreshRows(bufferService.buffer.y, bufferService.buffer.y, true)));
// dprchange should handle this case, we need this as well for browsers that don't support the
// matchMedia query.
- this.register(addDisposableDomListener(coreBrowserService.window, 'resize', () => this.onDevicePixelRatioChange()));
+ this.register(addDisposableDomListener(coreBrowserService.window, 'resize', () => this.handleDevicePixelRatioChange()));
+
+ this.register(themeService.onChangeColors(() => this._fullRefresh()));
// Detect whether IntersectionObserver is detected and enable renderer pause
// and resume based on terminal visibility if so
if ('IntersectionObserver' in coreBrowserService.window) {
- const observer = new coreBrowserService.window.IntersectionObserver(e => this._onIntersectionChange(e[e.length - 1]), { threshold: 0 });
+ const observer = new coreBrowserService.window.IntersectionObserver(e => this._handleIntersectionChange(e[e.length - 1]), { threshold: 0 });
observer.observe(screenElement);
this.register({ dispose: () => observer.disconnect() });
}
}
- private _onIntersectionChange(entry: IntersectionObserverEntry): void {
+ private _handleIntersectionChange(entry: IntersectionObserverEntry): void {
this._isPaused = entry.isIntersecting === undefined ? (entry.intersectionRatio === 0) : !entry.isIntersecting;
// Terminal was hidden on open
@@ -125,11 +149,14 @@ export class RenderService extends Disposable implements IRenderService {
}
private _renderRows(start: number, end: number): void {
+ if (!this._renderer) {
+ return;
+ }
this._renderer.renderRows(start, end);
// Update selection if needed
if (this._needsSelectionRefresh) {
- this._renderer.onSelectionChanged(this._selectionState.start, this._selectionState.end, this._selectionState.columnSelectMode);
+ this._renderer.handleSelectionChanged(this._selectionState.start, this._selectionState.end, this._selectionState.columnSelectMode);
this._needsSelectionRefresh = false;
}
@@ -147,12 +174,17 @@ export class RenderService extends Disposable implements IRenderService {
}
private _handleOptionsChanged(): void {
- this._renderer.onOptionsChanged();
+ if (!this._renderer) {
+ return;
+ }
this.refreshRows(0, this._rowCount - 1);
this._fireOnCanvasResize();
}
private _fireOnCanvasResize(): void {
+ if (!this._renderer) {
+ return;
+ }
// Don't fire the event if the dimensions haven't changed
if (this._renderer.dimensions.canvasWidth === this._canvasWidth && this._renderer.dimensions.canvasHeight === this._canvasHeight) {
return;
@@ -160,13 +192,13 @@ export class RenderService extends Disposable implements IRenderService {
this._onDimensionsChange.fire(this._renderer.dimensions);
}
- public dispose(): void {
- super.dispose();
+ public hasRenderer(): boolean {
+ return !!this._renderer;
}
public setRenderer(renderer: IRenderer): void {
// TODO: RenderService should be the only one to dispose the renderer
- this._renderer.dispose();
+ this._renderer?.dispose();
this._renderer = renderer;
this._renderer.onRequestRedraw(e => this.refreshRows(e.start, e.end, true));
@@ -188,58 +220,62 @@ export class RenderService extends Disposable implements IRenderService {
}
public clearTextureAtlas(): void {
- this._renderer?.clearTextureAtlas?.();
+ if (!this._renderer) {
+ return;
+ }
+ this._renderer.clearTextureAtlas?.();
this._fullRefresh();
}
- public setColors(colors: IColorSet): void {
- this._renderer.setColors(colors);
- this._fullRefresh();
- }
-
- public onDevicePixelRatioChange(): void {
+ public handleDevicePixelRatioChange(): void {
// Force char size measurement as DomMeasureStrategy(getBoundingClientRect) is not stable
// when devicePixelRatio changes
this._charSizeService.measure();
- this._renderer.onDevicePixelRatioChange();
+ if (!this._renderer) {
+ return;
+ }
+ this._renderer.handleDevicePixelRatioChange();
this.refreshRows(0, this._rowCount - 1);
}
- public onResize(cols: number, rows: number): void {
+ public handleResize(cols: number, rows: number): void {
+ if (!this._renderer) {
+ return;
+ }
if (this._isPaused) {
- this._pausedResizeTask.set(() => this._renderer.onResize(cols, rows));
+ this._pausedResizeTask.set(() => this._renderer!.handleResize(cols, rows));
} else {
- this._renderer.onResize(cols, rows);
+ this._renderer.handleResize(cols, rows);
}
this._fullRefresh();
}
// TODO: Is this useful when we have onResize?
- public onCharSizeChanged(): void {
- this._renderer.onCharSizeChanged();
+ public handleCharSizeChanged(): void {
+ this._renderer?.handleCharSizeChanged();
}
- public onBlur(): void {
- this._renderer.onBlur();
+ public handleBlur(): void {
+ this._renderer?.handleBlur();
}
- public onFocus(): void {
- this._renderer.onFocus();
+ public handleFocus(): void {
+ this._renderer?.handleFocus();
}
- public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
+ public handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
this._selectionState.start = start;
this._selectionState.end = end;
this._selectionState.columnSelectMode = columnSelectMode;
- this._renderer.onSelectionChanged(start, end, columnSelectMode);
+ this._renderer?.handleSelectionChanged(start, end, columnSelectMode);
}
- public onCursorMove(): void {
- this._renderer.onCursorMove();
+ public handleCursorMove(): void {
+ this._renderer?.handleCursorMove();
}
public clear(): void {
- this._renderer.clear();
+ this._renderer?.clear();
}
}
diff --git a/src/browser/services/SelectionService.ts b/src/browser/services/SelectionService.ts
index 4ee1ffa1..868fadda 100644
--- a/src/browser/services/SelectionService.ts
+++ b/src/browser/services/SelectionService.ts
@@ -15,7 +15,7 @@ import { IBufferRange, ILinkifier2 } from 'browser/Types';
import { IBufferService, IOptionsService, ICoreService } from 'common/services/Services';
import { getCoordsRelativeToElement } from 'browser/input/Mouse';
import { moveToCellSequence } from 'browser/input/MoveToCell';
-import { Disposable } from 'common/Lifecycle';
+import { Disposable, toDisposable } from 'common/Lifecycle';
import { getRangeLength } from 'common/buffer/BufferRange';
/**
@@ -111,14 +111,14 @@ export class SelectionService extends Disposable implements ISelectionService {
private _oldSelectionStart: [number, number] | undefined = undefined;
private _oldSelectionEnd: [number, number] | undefined = undefined;
- private _onLinuxMouseSelection = this.register(new EventEmitter());
- public get onLinuxMouseSelection(): IEvent { return this._onLinuxMouseSelection.event; }
- private _onRedrawRequest = this.register(new EventEmitter());
- public get onRequestRedraw(): IEvent { return this._onRedrawRequest.event; }
- private _onSelectionChange = this.register(new EventEmitter());
- public get onSelectionChange(): IEvent { return this._onSelectionChange.event; }
- private _onRequestScrollLines = this.register(new EventEmitter());
- public get onRequestScrollLines(): IEvent { return this._onRequestScrollLines.event; }
+ private readonly _onLinuxMouseSelection = this.register(new EventEmitter());
+ public readonly onLinuxMouseSelection = this._onLinuxMouseSelection.event;
+ private readonly _onRedrawRequest = this.register(new EventEmitter());
+ public readonly onRequestRedraw = this._onRedrawRequest.event;
+ private readonly _onSelectionChange = this.register(new EventEmitter());
+ public readonly onSelectionChange = this._onSelectionChange.event;
+ private readonly _onRequestScrollLines = this.register(new EventEmitter());
+ public readonly onRequestScrollLines = this._onRequestScrollLines.event;
constructor(
private readonly _element: HTMLElement,
@@ -134,24 +134,24 @@ export class SelectionService extends Disposable implements ISelectionService {
super();
// Init listeners
- this._mouseMoveListener = event => this._onMouseMove(event as MouseEvent);
- this._mouseUpListener = event => this._onMouseUp(event as MouseEvent);
+ this._mouseMoveListener = event => this._handleMouseMove(event as MouseEvent);
+ this._mouseUpListener = event => this._handleMouseUp(event as MouseEvent);
this._coreService.onUserInput(() => {
if (this.hasSelection) {
this.clearSelection();
}
});
- this._trimListener = this._bufferService.buffer.lines.onTrim(amount => this._onTrim(amount));
- this.register(this._bufferService.buffers.onBufferActivate(e => this._onBufferActivate(e)));
+ this._trimListener = this._bufferService.buffer.lines.onTrim(amount => this._handleTrim(amount));
+ this.register(this._bufferService.buffers.onBufferActivate(e => this._handleBufferActivate(e)));
this.enable();
this._model = new SelectionModel(this._bufferService);
this._activeSelectionMode = SelectionMode.NORMAL;
- }
- public dispose(): void {
- this._removeMouseDownListeners();
+ this.register(toDisposable(() => {
+ this._removeMouseDownListeners();
+ }));
}
public reset(): void {
@@ -375,8 +375,8 @@ export class SelectionService extends Disposable implements ISelectionService {
* Handle the buffer being trimmed, adjust the selection position.
* @param amount The amount the buffer is being trimmed.
*/
- private _onTrim(amount: number): void {
- const needsRefresh = this._model.onTrim(amount);
+ private _handleTrim(amount: number): void {
+ const needsRefresh = this._model.handleTrim(amount);
if (needsRefresh) {
this.refresh();
}
@@ -438,7 +438,7 @@ export class SelectionService extends Disposable implements ISelectionService {
* Handles te mousedown event, setting up for a new selection.
* @param event The mousedown event.
*/
- public onMouseDown(event: MouseEvent): void {
+ public handleMouseDown(event: MouseEvent): void {
this._mouseDownTimeStamp = event.timeStamp;
// If we have selection, we want the context menu on right click even if the
// terminal is in mouse mode.
@@ -468,14 +468,14 @@ export class SelectionService extends Disposable implements ISelectionService {
this._dragScrollAmount = 0;
if (this._enabled && event.shiftKey) {
- this._onIncrementalClick(event);
+ this._handleIncrementalClick(event);
} else {
if (event.detail === 1) {
- this._onSingleClick(event);
+ this._handleSingleClick(event);
} else if (event.detail === 2) {
- this._onDoubleClick(event);
+ this._handleDoubleClick(event);
} else if (event.detail === 3) {
- this._onTripleClick(event);
+ this._handleTripleClick(event);
}
}
@@ -512,7 +512,7 @@ export class SelectionService extends Disposable implements ISelectionService {
* position.
* @param event The mouse event.
*/
- private _onIncrementalClick(event: MouseEvent): void {
+ private _handleIncrementalClick(event: MouseEvent): void {
if (this._model.selectionStart) {
this._model.selectionEnd = this._getMouseBufferCoords(event);
}
@@ -523,7 +523,7 @@ export class SelectionService extends Disposable implements ISelectionService {
* start position.
* @param event The mouse event.
*/
- private _onSingleClick(event: MouseEvent): void {
+ private _handleSingleClick(event: MouseEvent): void {
this._model.selectionStartLength = 0;
this._model.isSelectAllActive = false;
this._activeSelectionMode = this.shouldColumnSelect(event) ? SelectionMode.COLUMN : SelectionMode.NORMAL;
@@ -557,7 +557,7 @@ export class SelectionService extends Disposable implements ISelectionService {
* Performs a double click, selecting the current word.
* @param event The mouse event.
*/
- private _onDoubleClick(event: MouseEvent): void {
+ private _handleDoubleClick(event: MouseEvent): void {
if (this._selectWordAtCursor(event, true)) {
this._activeSelectionMode = SelectionMode.WORD;
}
@@ -568,7 +568,7 @@ export class SelectionService extends Disposable implements ISelectionService {
* select mode.
* @param event The mouse event.
*/
- private _onTripleClick(event: MouseEvent): void {
+ private _handleTripleClick(event: MouseEvent): void {
const coords = this._getMouseBufferCoords(event);
if (coords) {
this._activeSelectionMode = SelectionMode.LINE;
@@ -589,7 +589,7 @@ export class SelectionService extends Disposable implements ISelectionService {
* end of the selection and refreshing the selection.
* @param event The mousemove event.
*/
- private _onMouseMove(event: MouseEvent): void {
+ private _handleMouseMove(event: MouseEvent): void {
// If the mousemove listener is active it means that a selection is
// currently being made, we should stop propagation to prevent mouse events
// to be sent to the pty.
@@ -690,7 +690,7 @@ export class SelectionService extends Disposable implements ISelectionService {
* Handles the mouseup event, removing the mousedown listeners.
* @param event The mouseup event.
*/
- private _onMouseUp(event: MouseEvent): void {
+ private _handleMouseUp(event: MouseEvent): void {
const timeElapsed = event.timeStamp - this._mouseDownTimeStamp;
this._removeMouseDownListeners();
@@ -746,14 +746,14 @@ export class SelectionService extends Disposable implements ISelectionService {
this._onSelectionChange.fire();
}
- private _onBufferActivate(e: {activeBuffer: IBuffer, inactiveBuffer: IBuffer}): void {
+ private _handleBufferActivate(e: {activeBuffer: IBuffer, inactiveBuffer: IBuffer}): void {
this.clearSelection();
// Only adjust the selection on trim, shiftElements is rarely used (only in
// reverseIndex) and delete in a splice is only ever used when the same
// number of elements was just added. Given this is could actually be
// beneficial to leave the selection as is for these cases.
this._trimListener.dispose();
- this._trimListener = e.activeBuffer.lines.onTrim(amount => this._onTrim(amount));
+ this._trimListener = e.activeBuffer.lines.onTrim(amount => this._handleTrim(amount));
}
/**
diff --git a/src/browser/services/Services.ts b/src/browser/services/Services.ts
index ab91f8a3..76a0f7df 100644
--- a/src/browser/services/Services.ts
+++ b/src/browser/services/Services.ts
@@ -4,11 +4,12 @@
*/
import { IEvent } from 'common/EventEmitter';
-import { IRenderDimensions, IRenderer } from 'browser/renderer/Types';
-import { IColorSet } from 'browser/Types';
+import { IRenderDimensions, IRenderer } from 'browser/renderer/shared/Types';
+import { IColorSet, ReadonlyColorSet } from 'browser/Types';
import { ISelectionRedrawRequestEvent as ISelectionRequestRedrawEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types';
import { createDecorator } from 'common/services/ServiceRegistry';
-import { IDisposable } from 'common/Types';
+import { ColorIndex, IDisposable } from 'common/Types';
+import { ITheme } from 'common/services/Services';
export const ICharSizeService = createDecorator('CharSizeService');
export interface ICharSizeService {
@@ -71,16 +72,15 @@ export interface IRenderService extends IDisposable {
refreshRows(start: number, end: number): void;
clearTextureAtlas(): void;
resize(cols: number, rows: number): void;
+ hasRenderer(): boolean;
setRenderer(renderer: IRenderer): void;
- setColors(colors: IColorSet): void;
- onDevicePixelRatioChange(): void;
- onResize(cols: number, rows: number): void;
- // TODO: Is this useful when we have onResize?
- onCharSizeChanged(): void;
- onBlur(): void;
- onFocus(): void;
- onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void;
- onCursorMove(): void;
+ handleDevicePixelRatioChange(): void;
+ handleResize(cols: number, rows: number): void;
+ handleCharSizeChanged(): void;
+ handleBlur(): void;
+ handleFocus(): void;
+ handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void;
+ handleCursorMove(): void;
clear(): void;
}
@@ -109,7 +109,7 @@ export interface ISelectionService {
shouldColumnSelect(event: KeyboardEvent | MouseEvent): boolean;
shouldForceSelection(event: MouseEvent): boolean;
refresh(isLinuxMouseSelection?: boolean): void;
- onMouseDown(event: MouseEvent): void;
+ handleMouseDown(event: MouseEvent): void;
isCellInSelection(x: number, y: number): boolean;
}
@@ -121,3 +121,19 @@ export interface ICharacterJoinerService {
deregister(joinerId: number): boolean;
getJoinedCharacters(row: number): [number, number][];
}
+
+export const IThemeService = createDecorator('ThemeService');
+export interface IThemeService {
+ serviceBrand: undefined;
+
+ readonly colors: ReadonlyColorSet;
+
+ readonly onChangeColors: IEvent;
+
+ restoreColor(slot?: ColorIndex): void;
+ /**
+ * Allows external modifying of colors in the theme, this is used instead of {@link colors} to
+ * prevent accidental writes.
+ */
+ modifyColors(callback: (colors: IColorSet) => void): void;
+}
diff --git a/src/browser/services/ThemeService.test.ts b/src/browser/services/ThemeService.test.ts
new file mode 100644
index 00000000..f2b2def3
--- /dev/null
+++ b/src/browser/services/ThemeService.test.ts
@@ -0,0 +1,367 @@
+/**
+ * Copyright (c) 2017 The xterm.js authors. All rights reserved.
+ * @license MIT
+ */
+
+import jsdom = require('jsdom');
+import { assert } from 'chai';
+import { ThemeService, DEFAULT_ANSI_COLORS } from 'browser/services/ThemeService';
+import { OptionsService } from 'common/services/OptionsService';
+
+describe('ThemeService', () => {
+ let themeService: ThemeService;
+ let dom: jsdom.JSDOM;
+ let window: jsdom.DOMWindow;
+ let optionsService: OptionsService;
+
+ beforeEach(() => {
+ dom = new jsdom.JSDOM('');
+ window = dom.window;
+ (window as any).HTMLCanvasElement.prototype.getContext = () => ({
+ createLinearGradient(): any {
+ return null;
+ },
+
+ fillRect(): void { },
+
+ getImageData(): any {
+ return {data: [0, 0, 0, 0xFF]};
+ }
+ });
+ optionsService = new OptionsService({});
+ themeService = new ThemeService(optionsService);
+ });
+
+ describe('constructor', () => {
+ it('should fill all colors with values', () => {
+ for (const key of Object.keys(themeService.colors)) {
+ if (key !== 'ansi' && key !== 'contrastCache' && key !== 'selectionForeground') {
+ // A #rrggbb or rgba(...)
+ assert.ok((themeService.colors as any)[key].css.length >= 7);
+ }
+ }
+ assert.equal(themeService.colors.ansi.length, 256);
+ });
+
+ it('should fill 240 colors with expected values', () => {
+ assert.equal(themeService.colors.ansi[16].css, '#000000');
+ assert.equal(themeService.colors.ansi[17].css, '#00005f');
+ assert.equal(themeService.colors.ansi[18].css, '#000087');
+ assert.equal(themeService.colors.ansi[19].css, '#0000af');
+ assert.equal(themeService.colors.ansi[20].css, '#0000d7');
+ assert.equal(themeService.colors.ansi[21].css, '#0000ff');
+ assert.equal(themeService.colors.ansi[22].css, '#005f00');
+ assert.equal(themeService.colors.ansi[23].css, '#005f5f');
+ assert.equal(themeService.colors.ansi[24].css, '#005f87');
+ assert.equal(themeService.colors.ansi[25].css, '#005faf');
+ assert.equal(themeService.colors.ansi[26].css, '#005fd7');
+ assert.equal(themeService.colors.ansi[27].css, '#005fff');
+ assert.equal(themeService.colors.ansi[28].css, '#008700');
+ assert.equal(themeService.colors.ansi[29].css, '#00875f');
+ assert.equal(themeService.colors.ansi[30].css, '#008787');
+ assert.equal(themeService.colors.ansi[31].css, '#0087af');
+ assert.equal(themeService.colors.ansi[32].css, '#0087d7');
+ assert.equal(themeService.colors.ansi[33].css, '#0087ff');
+ assert.equal(themeService.colors.ansi[34].css, '#00af00');
+ assert.equal(themeService.colors.ansi[35].css, '#00af5f');
+ assert.equal(themeService.colors.ansi[36].css, '#00af87');
+ assert.equal(themeService.colors.ansi[37].css, '#00afaf');
+ assert.equal(themeService.colors.ansi[38].css, '#00afd7');
+ assert.equal(themeService.colors.ansi[39].css, '#00afff');
+ assert.equal(themeService.colors.ansi[40].css, '#00d700');
+ assert.equal(themeService.colors.ansi[41].css, '#00d75f');
+ assert.equal(themeService.colors.ansi[42].css, '#00d787');
+ assert.equal(themeService.colors.ansi[43].css, '#00d7af');
+ assert.equal(themeService.colors.ansi[44].css, '#00d7d7');
+ assert.equal(themeService.colors.ansi[45].css, '#00d7ff');
+ assert.equal(themeService.colors.ansi[46].css, '#00ff00');
+ assert.equal(themeService.colors.ansi[47].css, '#00ff5f');
+ assert.equal(themeService.colors.ansi[48].css, '#00ff87');
+ assert.equal(themeService.colors.ansi[49].css, '#00ffaf');
+ assert.equal(themeService.colors.ansi[50].css, '#00ffd7');
+ assert.equal(themeService.colors.ansi[51].css, '#00ffff');
+ assert.equal(themeService.colors.ansi[52].css, '#5f0000');
+ assert.equal(themeService.colors.ansi[53].css, '#5f005f');
+ assert.equal(themeService.colors.ansi[54].css, '#5f0087');
+ assert.equal(themeService.colors.ansi[55].css, '#5f00af');
+ assert.equal(themeService.colors.ansi[56].css, '#5f00d7');
+ assert.equal(themeService.colors.ansi[57].css, '#5f00ff');
+ assert.equal(themeService.colors.ansi[58].css, '#5f5f00');
+ assert.equal(themeService.colors.ansi[59].css, '#5f5f5f');
+ assert.equal(themeService.colors.ansi[60].css, '#5f5f87');
+ assert.equal(themeService.colors.ansi[61].css, '#5f5faf');
+ assert.equal(themeService.colors.ansi[62].css, '#5f5fd7');
+ assert.equal(themeService.colors.ansi[63].css, '#5f5fff');
+ assert.equal(themeService.colors.ansi[64].css, '#5f8700');
+ assert.equal(themeService.colors.ansi[65].css, '#5f875f');
+ assert.equal(themeService.colors.ansi[66].css, '#5f8787');
+ assert.equal(themeService.colors.ansi[67].css, '#5f87af');
+ assert.equal(themeService.colors.ansi[68].css, '#5f87d7');
+ assert.equal(themeService.colors.ansi[69].css, '#5f87ff');
+ assert.equal(themeService.colors.ansi[70].css, '#5faf00');
+ assert.equal(themeService.colors.ansi[71].css, '#5faf5f');
+ assert.equal(themeService.colors.ansi[72].css, '#5faf87');
+ assert.equal(themeService.colors.ansi[73].css, '#5fafaf');
+ assert.equal(themeService.colors.ansi[74].css, '#5fafd7');
+ assert.equal(themeService.colors.ansi[75].css, '#5fafff');
+ assert.equal(themeService.colors.ansi[76].css, '#5fd700');
+ assert.equal(themeService.colors.ansi[77].css, '#5fd75f');
+ assert.equal(themeService.colors.ansi[78].css, '#5fd787');
+ assert.equal(themeService.colors.ansi[79].css, '#5fd7af');
+ assert.equal(themeService.colors.ansi[80].css, '#5fd7d7');
+ assert.equal(themeService.colors.ansi[81].css, '#5fd7ff');
+ assert.equal(themeService.colors.ansi[82].css, '#5fff00');
+ assert.equal(themeService.colors.ansi[83].css, '#5fff5f');
+ assert.equal(themeService.colors.ansi[84].css, '#5fff87');
+ assert.equal(themeService.colors.ansi[85].css, '#5fffaf');
+ assert.equal(themeService.colors.ansi[86].css, '#5fffd7');
+ assert.equal(themeService.colors.ansi[87].css, '#5fffff');
+ assert.equal(themeService.colors.ansi[88].css, '#870000');
+ assert.equal(themeService.colors.ansi[89].css, '#87005f');
+ assert.equal(themeService.colors.ansi[90].css, '#870087');
+ assert.equal(themeService.colors.ansi[91].css, '#8700af');
+ assert.equal(themeService.colors.ansi[92].css, '#8700d7');
+ assert.equal(themeService.colors.ansi[93].css, '#8700ff');
+ assert.equal(themeService.colors.ansi[94].css, '#875f00');
+ assert.equal(themeService.colors.ansi[95].css, '#875f5f');
+ assert.equal(themeService.colors.ansi[96].css, '#875f87');
+ assert.equal(themeService.colors.ansi[97].css, '#875faf');
+ assert.equal(themeService.colors.ansi[98].css, '#875fd7');
+ assert.equal(themeService.colors.ansi[99].css, '#875fff');
+ assert.equal(themeService.colors.ansi[100].css, '#878700');
+ assert.equal(themeService.colors.ansi[101].css, '#87875f');
+ assert.equal(themeService.colors.ansi[102].css, '#878787');
+ assert.equal(themeService.colors.ansi[103].css, '#8787af');
+ assert.equal(themeService.colors.ansi[104].css, '#8787d7');
+ assert.equal(themeService.colors.ansi[105].css, '#8787ff');
+ assert.equal(themeService.colors.ansi[106].css, '#87af00');
+ assert.equal(themeService.colors.ansi[107].css, '#87af5f');
+ assert.equal(themeService.colors.ansi[108].css, '#87af87');
+ assert.equal(themeService.colors.ansi[109].css, '#87afaf');
+ assert.equal(themeService.colors.ansi[110].css, '#87afd7');
+ assert.equal(themeService.colors.ansi[111].css, '#87afff');
+ assert.equal(themeService.colors.ansi[112].css, '#87d700');
+ assert.equal(themeService.colors.ansi[113].css, '#87d75f');
+ assert.equal(themeService.colors.ansi[114].css, '#87d787');
+ assert.equal(themeService.colors.ansi[115].css, '#87d7af');
+ assert.equal(themeService.colors.ansi[116].css, '#87d7d7');
+ assert.equal(themeService.colors.ansi[117].css, '#87d7ff');
+ assert.equal(themeService.colors.ansi[118].css, '#87ff00');
+ assert.equal(themeService.colors.ansi[119].css, '#87ff5f');
+ assert.equal(themeService.colors.ansi[120].css, '#87ff87');
+ assert.equal(themeService.colors.ansi[121].css, '#87ffaf');
+ assert.equal(themeService.colors.ansi[122].css, '#87ffd7');
+ assert.equal(themeService.colors.ansi[123].css, '#87ffff');
+ assert.equal(themeService.colors.ansi[124].css, '#af0000');
+ assert.equal(themeService.colors.ansi[125].css, '#af005f');
+ assert.equal(themeService.colors.ansi[126].css, '#af0087');
+ assert.equal(themeService.colors.ansi[127].css, '#af00af');
+ assert.equal(themeService.colors.ansi[128].css, '#af00d7');
+ assert.equal(themeService.colors.ansi[129].css, '#af00ff');
+ assert.equal(themeService.colors.ansi[130].css, '#af5f00');
+ assert.equal(themeService.colors.ansi[131].css, '#af5f5f');
+ assert.equal(themeService.colors.ansi[132].css, '#af5f87');
+ assert.equal(themeService.colors.ansi[133].css, '#af5faf');
+ assert.equal(themeService.colors.ansi[134].css, '#af5fd7');
+ assert.equal(themeService.colors.ansi[135].css, '#af5fff');
+ assert.equal(themeService.colors.ansi[136].css, '#af8700');
+ assert.equal(themeService.colors.ansi[137].css, '#af875f');
+ assert.equal(themeService.colors.ansi[138].css, '#af8787');
+ assert.equal(themeService.colors.ansi[139].css, '#af87af');
+ assert.equal(themeService.colors.ansi[140].css, '#af87d7');
+ assert.equal(themeService.colors.ansi[141].css, '#af87ff');
+ assert.equal(themeService.colors.ansi[142].css, '#afaf00');
+ assert.equal(themeService.colors.ansi[143].css, '#afaf5f');
+ assert.equal(themeService.colors.ansi[144].css, '#afaf87');
+ assert.equal(themeService.colors.ansi[145].css, '#afafaf');
+ assert.equal(themeService.colors.ansi[146].css, '#afafd7');
+ assert.equal(themeService.colors.ansi[147].css, '#afafff');
+ assert.equal(themeService.colors.ansi[148].css, '#afd700');
+ assert.equal(themeService.colors.ansi[149].css, '#afd75f');
+ assert.equal(themeService.colors.ansi[150].css, '#afd787');
+ assert.equal(themeService.colors.ansi[151].css, '#afd7af');
+ assert.equal(themeService.colors.ansi[152].css, '#afd7d7');
+ assert.equal(themeService.colors.ansi[153].css, '#afd7ff');
+ assert.equal(themeService.colors.ansi[154].css, '#afff00');
+ assert.equal(themeService.colors.ansi[155].css, '#afff5f');
+ assert.equal(themeService.colors.ansi[156].css, '#afff87');
+ assert.equal(themeService.colors.ansi[157].css, '#afffaf');
+ assert.equal(themeService.colors.ansi[158].css, '#afffd7');
+ assert.equal(themeService.colors.ansi[159].css, '#afffff');
+ assert.equal(themeService.colors.ansi[160].css, '#d70000');
+ assert.equal(themeService.colors.ansi[161].css, '#d7005f');
+ assert.equal(themeService.colors.ansi[162].css, '#d70087');
+ assert.equal(themeService.colors.ansi[163].css, '#d700af');
+ assert.equal(themeService.colors.ansi[164].css, '#d700d7');
+ assert.equal(themeService.colors.ansi[165].css, '#d700ff');
+ assert.equal(themeService.colors.ansi[166].css, '#d75f00');
+ assert.equal(themeService.colors.ansi[167].css, '#d75f5f');
+ assert.equal(themeService.colors.ansi[168].css, '#d75f87');
+ assert.equal(themeService.colors.ansi[169].css, '#d75faf');
+ assert.equal(themeService.colors.ansi[170].css, '#d75fd7');
+ assert.equal(themeService.colors.ansi[171].css, '#d75fff');
+ assert.equal(themeService.colors.ansi[172].css, '#d78700');
+ assert.equal(themeService.colors.ansi[173].css, '#d7875f');
+ assert.equal(themeService.colors.ansi[174].css, '#d78787');
+ assert.equal(themeService.colors.ansi[175].css, '#d787af');
+ assert.equal(themeService.colors.ansi[176].css, '#d787d7');
+ assert.equal(themeService.colors.ansi[177].css, '#d787ff');
+ assert.equal(themeService.colors.ansi[178].css, '#d7af00');
+ assert.equal(themeService.colors.ansi[179].css, '#d7af5f');
+ assert.equal(themeService.colors.ansi[180].css, '#d7af87');
+ assert.equal(themeService.colors.ansi[181].css, '#d7afaf');
+ assert.equal(themeService.colors.ansi[182].css, '#d7afd7');
+ assert.equal(themeService.colors.ansi[183].css, '#d7afff');
+ assert.equal(themeService.colors.ansi[184].css, '#d7d700');
+ assert.equal(themeService.colors.ansi[185].css, '#d7d75f');
+ assert.equal(themeService.colors.ansi[186].css, '#d7d787');
+ assert.equal(themeService.colors.ansi[187].css, '#d7d7af');
+ assert.equal(themeService.colors.ansi[188].css, '#d7d7d7');
+ assert.equal(themeService.colors.ansi[189].css, '#d7d7ff');
+ assert.equal(themeService.colors.ansi[190].css, '#d7ff00');
+ assert.equal(themeService.colors.ansi[191].css, '#d7ff5f');
+ assert.equal(themeService.colors.ansi[192].css, '#d7ff87');
+ assert.equal(themeService.colors.ansi[193].css, '#d7ffaf');
+ assert.equal(themeService.colors.ansi[194].css, '#d7ffd7');
+ assert.equal(themeService.colors.ansi[195].css, '#d7ffff');
+ assert.equal(themeService.colors.ansi[196].css, '#ff0000');
+ assert.equal(themeService.colors.ansi[197].css, '#ff005f');
+ assert.equal(themeService.colors.ansi[198].css, '#ff0087');
+ assert.equal(themeService.colors.ansi[199].css, '#ff00af');
+ assert.equal(themeService.colors.ansi[200].css, '#ff00d7');
+ assert.equal(themeService.colors.ansi[201].css, '#ff00ff');
+ assert.equal(themeService.colors.ansi[202].css, '#ff5f00');
+ assert.equal(themeService.colors.ansi[203].css, '#ff5f5f');
+ assert.equal(themeService.colors.ansi[204].css, '#ff5f87');
+ assert.equal(themeService.colors.ansi[205].css, '#ff5faf');
+ assert.equal(themeService.colors.ansi[206].css, '#ff5fd7');
+ assert.equal(themeService.colors.ansi[207].css, '#ff5fff');
+ assert.equal(themeService.colors.ansi[208].css, '#ff8700');
+ assert.equal(themeService.colors.ansi[209].css, '#ff875f');
+ assert.equal(themeService.colors.ansi[210].css, '#ff8787');
+ assert.equal(themeService.colors.ansi[211].css, '#ff87af');
+ assert.equal(themeService.colors.ansi[212].css, '#ff87d7');
+ assert.equal(themeService.colors.ansi[213].css, '#ff87ff');
+ assert.equal(themeService.colors.ansi[214].css, '#ffaf00');
+ assert.equal(themeService.colors.ansi[215].css, '#ffaf5f');
+ assert.equal(themeService.colors.ansi[216].css, '#ffaf87');
+ assert.equal(themeService.colors.ansi[217].css, '#ffafaf');
+ assert.equal(themeService.colors.ansi[218].css, '#ffafd7');
+ assert.equal(themeService.colors.ansi[219].css, '#ffafff');
+ assert.equal(themeService.colors.ansi[220].css, '#ffd700');
+ assert.equal(themeService.colors.ansi[221].css, '#ffd75f');
+ assert.equal(themeService.colors.ansi[222].css, '#ffd787');
+ assert.equal(themeService.colors.ansi[223].css, '#ffd7af');
+ assert.equal(themeService.colors.ansi[224].css, '#ffd7d7');
+ assert.equal(themeService.colors.ansi[225].css, '#ffd7ff');
+ assert.equal(themeService.colors.ansi[226].css, '#ffff00');
+ assert.equal(themeService.colors.ansi[227].css, '#ffff5f');
+ assert.equal(themeService.colors.ansi[228].css, '#ffff87');
+ assert.equal(themeService.colors.ansi[229].css, '#ffffaf');
+ assert.equal(themeService.colors.ansi[230].css, '#ffffd7');
+ assert.equal(themeService.colors.ansi[231].css, '#ffffff');
+ assert.equal(themeService.colors.ansi[232].css, '#080808');
+ assert.equal(themeService.colors.ansi[233].css, '#121212');
+ assert.equal(themeService.colors.ansi[234].css, '#1c1c1c');
+ assert.equal(themeService.colors.ansi[235].css, '#262626');
+ assert.equal(themeService.colors.ansi[236].css, '#303030');
+ assert.equal(themeService.colors.ansi[237].css, '#3a3a3a');
+ assert.equal(themeService.colors.ansi[238].css, '#444444');
+ assert.equal(themeService.colors.ansi[239].css, '#4e4e4e');
+ assert.equal(themeService.colors.ansi[240].css, '#585858');
+ assert.equal(themeService.colors.ansi[241].css, '#626262');
+ assert.equal(themeService.colors.ansi[242].css, '#6c6c6c');
+ assert.equal(themeService.colors.ansi[243].css, '#767676');
+ assert.equal(themeService.colors.ansi[244].css, '#808080');
+ assert.equal(themeService.colors.ansi[245].css, '#8a8a8a');
+ assert.equal(themeService.colors.ansi[246].css, '#949494');
+ assert.equal(themeService.colors.ansi[247].css, '#9e9e9e');
+ assert.equal(themeService.colors.ansi[248].css, '#a8a8a8');
+ assert.equal(themeService.colors.ansi[249].css, '#b2b2b2');
+ assert.equal(themeService.colors.ansi[250].css, '#bcbcbc');
+ assert.equal(themeService.colors.ansi[251].css, '#c6c6c6');
+ assert.equal(themeService.colors.ansi[252].css, '#d0d0d0');
+ assert.equal(themeService.colors.ansi[253].css, '#dadada');
+ assert.equal(themeService.colors.ansi[254].css, '#e4e4e4');
+ assert.equal(themeService.colors.ansi[255].css, '#eeeeee');
+ });
+ });
+
+ describe('setTheme', () => {
+ it('should not throw when not setting all colors', () => {
+ assert.doesNotThrow(() => {
+ optionsService.options.theme = {};
+ });
+ });
+
+ it('should set a partial set of colors, using the default if not present', () => {
+ assert.equal(themeService.colors.background.css, '#000000');
+ assert.equal(themeService.colors.foreground.css, '#ffffff');
+ optionsService.options.theme = {
+ background: '#FF0000',
+ foreground: '#00FF00'
+ };
+ assert.equal(themeService.colors.background.css, '#FF0000');
+ assert.equal(themeService.colors.foreground.css, '#00FF00');
+ optionsService.options.theme = {
+ background: '#0000FF'
+ };
+ assert.equal(themeService.colors.background.css, '#0000FF');
+ // FG reverts back to default
+ assert.equal(themeService.colors.foreground.css, '#ffffff');
+ });
+
+ it('should set all extended ansi colors in reverse order', () => {
+ optionsService.options.theme = {
+ extendedAnsi: DEFAULT_ANSI_COLORS.map(a => a.css).slice().reverse()
+ };
+
+ for (let ansiColor = 16; ansiColor <= 255; ansiColor++) {
+ assert.equal(themeService.colors.ansi[ansiColor].css, DEFAULT_ANSI_COLORS[255 + 16 - ansiColor].css);
+ }
+ });
+
+ it('should set one extended ansi color and keep the other default', () => {
+ optionsService.options.theme = {
+ extendedAnsi: ['#ffffff']
+ };
+
+ assert.equal(themeService.colors.ansi[16].css, '#ffffff');
+ assert.equal(themeService.colors.ansi[17].css, DEFAULT_ANSI_COLORS[17].css);
+ });
+
+ it('should set extended ansi colors to the default when they are unset', () => {
+ optionsService.options.theme = {
+ extendedAnsi: ['#ffffff']
+ };
+ assert.equal(themeService.colors.ansi[16].css, '#ffffff');
+
+ optionsService.options.theme = {
+ extendedAnsi: []
+ };
+ assert.equal(themeService.colors.ansi[16].css, DEFAULT_ANSI_COLORS[16].css);
+
+ optionsService.options.theme = {
+ extendedAnsi: ['#ffffff']
+ };
+ assert.equal(themeService.colors.ansi[16].css, '#ffffff');
+
+ optionsService.options.theme = {};
+ assert.equal(themeService.colors.ansi[16].css, DEFAULT_ANSI_COLORS[16].css);
+ });
+
+ it('should set extended ansi colors to the default when they are partially unset', () => {
+ optionsService.options.theme = {
+ extendedAnsi: ['#ffffff', '#000000']
+ };
+ assert.equal(themeService.colors.ansi[16].css, '#ffffff');
+ assert.equal(themeService.colors.ansi[17].css, '#000000');
+
+ optionsService.options.theme = {
+ extendedAnsi: ['#ffffff']
+ };
+ assert.equal(themeService.colors.ansi[16].css, '#ffffff');
+ assert.equal(themeService.colors.ansi[17].css, DEFAULT_ANSI_COLORS[17].css);
+ });
+ });
+});
diff --git a/src/browser/services/ThemeService.ts b/src/browser/services/ThemeService.ts
new file mode 100644
index 00000000..ac0104d5
--- /dev/null
+++ b/src/browser/services/ThemeService.ts
@@ -0,0 +1,235 @@
+/**
+ * Copyright (c) 2022 The xterm.js authors. All rights reserved.
+ * @license MIT
+ */
+
+import { ColorContrastCache } from 'browser/ColorContrastCache';
+import { IThemeService } from 'browser/services/Services';
+import { IColorContrastCache, IColorSet, ReadonlyColorSet } from 'browser/Types';
+import { channels, color, css, NULL_COLOR } from 'common/Color';
+import { EventEmitter } from 'common/EventEmitter';
+import { Disposable } from 'common/Lifecycle';
+import { IOptionsService, ITheme } from 'common/services/Services';
+import { ColorIndex, IColor } from 'common/Types';
+
+interface IRestoreColorSet {
+ foreground: IColor;
+ background: IColor;
+ cursor: IColor;
+ ansi: IColor[];
+}
+
+
+const DEFAULT_FOREGROUND = css.toColor('#ffffff');
+const DEFAULT_BACKGROUND = css.toColor('#000000');
+const DEFAULT_CURSOR = css.toColor('#ffffff');
+const DEFAULT_CURSOR_ACCENT = css.toColor('#000000');
+const DEFAULT_SELECTION = {
+ css: 'rgba(255, 255, 255, 0.3)',
+ rgba: 0xFFFFFF4D
+};
+
+// An IIFE to generate DEFAULT_ANSI_COLORS.
+export const DEFAULT_ANSI_COLORS = Object.freeze((() => {
+ const colors = [
+ // dark:
+ css.toColor('#2e3436'),
+ css.toColor('#cc0000'),
+ css.toColor('#4e9a06'),
+ css.toColor('#c4a000'),
+ css.toColor('#3465a4'),
+ css.toColor('#75507b'),
+ css.toColor('#06989a'),
+ css.toColor('#d3d7cf'),
+ // bright:
+ css.toColor('#555753'),
+ css.toColor('#ef2929'),
+ css.toColor('#8ae234'),
+ css.toColor('#fce94f'),
+ css.toColor('#729fcf'),
+ css.toColor('#ad7fa8'),
+ css.toColor('#34e2e2'),
+ css.toColor('#eeeeec')
+ ];
+
+ // Fill in the remaining 240 ANSI colors.
+ // Generate colors (16-231)
+ const v = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff];
+ for (let i = 0; i < 216; i++) {
+ const r = v[(i / 36) % 6 | 0];
+ const g = v[(i / 6) % 6 | 0];
+ const b = v[i % 6];
+ colors.push({
+ css: channels.toCss(r, g, b),
+ rgba: channels.toRgba(r, g, b)
+ });
+ }
+
+ // Generate greys (232-255)
+ for (let i = 0; i < 24; i++) {
+ const c = 8 + i * 10;
+ colors.push({
+ css: channels.toCss(c, c, c),
+ rgba: channels.toRgba(c, c, c)
+ });
+ }
+
+ return colors;
+})());
+
+export class ThemeService extends Disposable implements IThemeService {
+ public serviceBrand: undefined;
+
+ private _colors: IColorSet;
+ private _contrastCache: IColorContrastCache;
+ private _restoreColors!: IRestoreColorSet;
+
+ public get colors(): ReadonlyColorSet { return this._colors; }
+
+ private readonly _onChangeColors = this.register(new EventEmitter());
+ public readonly onChangeColors = this._onChangeColors.event;
+
+ constructor(
+ @IOptionsService private readonly _optionsService: IOptionsService
+ ) {
+ super();
+
+ this._contrastCache = new ColorContrastCache();
+ this._colors = {
+ foreground: DEFAULT_FOREGROUND,
+ background: DEFAULT_BACKGROUND,
+ cursor: DEFAULT_CURSOR,
+ cursorAccent: DEFAULT_CURSOR_ACCENT,
+ selectionForeground: undefined,
+ selectionBackgroundTransparent: DEFAULT_SELECTION,
+ selectionBackgroundOpaque: color.blend(DEFAULT_BACKGROUND, DEFAULT_SELECTION),
+ selectionInactiveBackgroundTransparent: DEFAULT_SELECTION,
+ selectionInactiveBackgroundOpaque: color.blend(DEFAULT_BACKGROUND, DEFAULT_SELECTION),
+ ansi: DEFAULT_ANSI_COLORS.slice(),
+ contrastCache: this._contrastCache
+ };
+ this._updateRestoreColors();
+ this._setTheme(this._optionsService.rawOptions.theme);
+
+ this.register(this._optionsService.onSpecificOptionChange('minimumContrastRatio', () => this._contrastCache.clear()));
+ this.register(this._optionsService.onSpecificOptionChange('theme', () => this._setTheme(this._optionsService.rawOptions.theme)));
+ }
+
+ /**
+ * Sets the terminal's theme.
+ * @param theme The theme to use. If a partial theme is provided then default
+ * colors will be used where colors are not defined.
+ */
+ private _setTheme(theme: ITheme = {}): void {
+ const colors = this._colors;
+ colors.foreground = parseColor(theme.foreground, DEFAULT_FOREGROUND);
+ colors.background = parseColor(theme.background, DEFAULT_BACKGROUND);
+ colors.cursor = parseColor(theme.cursor, DEFAULT_CURSOR);
+ colors.cursorAccent = parseColor(theme.cursorAccent, DEFAULT_CURSOR_ACCENT);
+ colors.selectionBackgroundTransparent = parseColor(theme.selectionBackground, DEFAULT_SELECTION);
+ colors.selectionBackgroundOpaque = color.blend(colors.background, colors.selectionBackgroundTransparent);
+ colors.selectionInactiveBackgroundTransparent = parseColor(theme.selectionInactiveBackground, colors.selectionBackgroundTransparent);
+ colors.selectionInactiveBackgroundOpaque = color.blend(colors.background, colors.selectionInactiveBackgroundTransparent);
+ colors.selectionForeground = theme.selectionForeground ? parseColor(theme.selectionForeground, NULL_COLOR) : undefined;
+ if (colors.selectionForeground === NULL_COLOR) {
+ colors.selectionForeground = undefined;
+ }
+
+ /**
+ * If selection color is opaque, blend it with background with 0.3 opacity
+ * Issue #2737
+ */
+ if (color.isOpaque(colors.selectionBackgroundTransparent)) {
+ const opacity = 0.3;
+ colors.selectionBackgroundTransparent = color.opacity(colors.selectionBackgroundTransparent, opacity);
+ }
+ if (color.isOpaque(colors.selectionInactiveBackgroundTransparent)) {
+ const opacity = 0.3;
+ colors.selectionInactiveBackgroundTransparent = color.opacity(colors.selectionInactiveBackgroundTransparent, opacity);
+ }
+ colors.ansi = DEFAULT_ANSI_COLORS.slice();
+ colors.ansi[0] = parseColor(theme.black, DEFAULT_ANSI_COLORS[0]);
+ colors.ansi[1] = parseColor(theme.red, DEFAULT_ANSI_COLORS[1]);
+ colors.ansi[2] = parseColor(theme.green, DEFAULT_ANSI_COLORS[2]);
+ colors.ansi[3] = parseColor(theme.yellow, DEFAULT_ANSI_COLORS[3]);
+ colors.ansi[4] = parseColor(theme.blue, DEFAULT_ANSI_COLORS[4]);
+ colors.ansi[5] = parseColor(theme.magenta, DEFAULT_ANSI_COLORS[5]);
+ colors.ansi[6] = parseColor(theme.cyan, DEFAULT_ANSI_COLORS[6]);
+ colors.ansi[7] = parseColor(theme.white, DEFAULT_ANSI_COLORS[7]);
+ colors.ansi[8] = parseColor(theme.brightBlack, DEFAULT_ANSI_COLORS[8]);
+ colors.ansi[9] = parseColor(theme.brightRed, DEFAULT_ANSI_COLORS[9]);
+ colors.ansi[10] = parseColor(theme.brightGreen, DEFAULT_ANSI_COLORS[10]);
+ colors.ansi[11] = parseColor(theme.brightYellow, DEFAULT_ANSI_COLORS[11]);
+ colors.ansi[12] = parseColor(theme.brightBlue, DEFAULT_ANSI_COLORS[12]);
+ colors.ansi[13] = parseColor(theme.brightMagenta, DEFAULT_ANSI_COLORS[13]);
+ colors.ansi[14] = parseColor(theme.brightCyan, DEFAULT_ANSI_COLORS[14]);
+ colors.ansi[15] = parseColor(theme.brightWhite, DEFAULT_ANSI_COLORS[15]);
+ if (theme.extendedAnsi) {
+ const colorCount = Math.min(colors.ansi.length - 16, theme.extendedAnsi.length);
+ for (let i = 0; i < colorCount; i++) {
+ colors.ansi[i + 16] = parseColor(theme.extendedAnsi[i], DEFAULT_ANSI_COLORS[i + 16]);
+ }
+ }
+ // Clear our the cache
+ this._contrastCache.clear();
+ this._updateRestoreColors();
+ this._onChangeColors.fire(this.colors);
+ }
+
+ public restoreColor(slot?: ColorIndex): void {
+ this._restoreColor(slot);
+ this._onChangeColors.fire(this.colors);
+ }
+
+ private _restoreColor(slot: ColorIndex | undefined): void {
+ // unset slot restores all ansi colors
+ if (slot === undefined) {
+ for (let i = 0; i < this._restoreColors.ansi.length; ++i) {
+ this._colors.ansi[i] = this._restoreColors.ansi[i];
+ }
+ return;
+ }
+ switch (slot) {
+ case ColorIndex.FOREGROUND:
+ this._colors.foreground = this._restoreColors.foreground;
+ break;
+ case ColorIndex.BACKGROUND:
+ this._colors.background = this._restoreColors.background;
+ break;
+ case ColorIndex.CURSOR:
+ this._colors.cursor = this._restoreColors.cursor;
+ break;
+ default:
+ this._colors.ansi[slot] = this._restoreColors.ansi[slot];
+ }
+ }
+
+ public modifyColors(callback: (colors: IColorSet) => void): void {
+ callback(this._colors);
+ // Assume the change happened
+ this._onChangeColors.fire(this.colors);
+ }
+
+ private _updateRestoreColors(): void {
+ this._restoreColors = {
+ foreground: this._colors.foreground,
+ background: this._colors.background,
+ cursor: this._colors.cursor,
+ ansi: this._colors.ansi.slice()
+ };
+ }
+}
+
+function parseColor(
+ cssString: string | undefined,
+ fallback: IColor
+): IColor {
+ if (cssString !== undefined) {
+ try {
+ return css.toColor(cssString);
+ } catch {
+ // no-op
+ }
+ }
+ return fallback;
+}
diff --git a/src/common/CircularList.ts b/src/common/CircularList.ts
index 4d2c04ec..b7e1e075 100644
--- a/src/common/CircularList.ts
+++ b/src/common/CircularList.ts
@@ -5,6 +5,7 @@
import { ICircularList } from 'common/Types';
import { EventEmitter, IEvent } from 'common/EventEmitter';
+import { Disposable } from 'common/Lifecycle';
export interface IInsertEvent {
index: number;
@@ -20,21 +21,22 @@ export interface IDeleteEvent {
* Represents a circular list; a list with a maximum size that wraps around when push is called,
* overriding values at the start of the list.
*/
-export class CircularList implements ICircularList {
+export class CircularList extends Disposable implements ICircularList {
protected _array: (T | undefined)[];
private _startIndex: number;
private _length: number;
- public onDeleteEmitter = new EventEmitter();
- public get onDelete(): IEvent { return this.onDeleteEmitter.event; }
- public onInsertEmitter = new EventEmitter();
- public get onInsert(): IEvent { return this.onInsertEmitter.event; }
- public onTrimEmitter = new EventEmitter();
- public get onTrim(): IEvent { return this.onTrimEmitter.event; }
+ public readonly onDeleteEmitter = this.register(new EventEmitter());
+ public readonly onDelete = this.onDeleteEmitter.event;
+ public readonly onInsertEmitter = this.register(new EventEmitter());
+ public readonly onInsert = this.onInsertEmitter.event;
+ public readonly onTrimEmitter = this.register(new EventEmitter());
+ public readonly onTrim = this.onTrimEmitter.event;
constructor(
private _maxLength: number
) {
+ super();
this._array = new Array(this._maxLength);
this._startIndex = 0;
this._length = 0;
diff --git a/src/common/Color.ts b/src/common/Color.ts
index 6e70671b..0d7bffe0 100644
--- a/src/common/Color.ts
+++ b/src/common/Color.ts
@@ -11,6 +11,11 @@ let $g = 0;
let $b = 0;
let $a = 0;
+export const NULL_COLOR: IColor = {
+ css: '#00000000',
+ rgba: 0
+};
+
/**
* Helper functions where the source type is "channels" (individual color channels as numbers).
*/
@@ -129,7 +134,7 @@ export namespace css {
*/
export function toColor(css: string): IColor {
// Formats: #rgb[a] and #rrggbb[aa]
- if (css.match(/#[0-9a-f]{3,8}/i)) {
+ if (css.match(/#[\da-f]{3,8}/i)) {
switch (css.length) {
case 4: { // #rgb
$r = parseInt(css.slice(1, 2).repeat(2), 16);
diff --git a/src/common/CoreTerminal.ts b/src/common/CoreTerminal.ts
index d7cb0f7e..f4a7d301 100644
--- a/src/common/CoreTerminal.ts
+++ b/src/common/CoreTerminal.ts
@@ -21,8 +21,8 @@
* http://linux.die.net/man/7/urxvt
*/
-import { Disposable } from 'common/Lifecycle';
-import { IInstantiationService, IOptionsService, IBufferService, ILogService, ICharsetService, ICoreService, ICoreMouseService, IUnicodeService, IDirtyRowService, LogLevelEnum, ITerminalOptions, IOscLinkService } from 'common/services/Services';
+import { Disposable, toDisposable } from 'common/Lifecycle';
+import { IInstantiationService, IOptionsService, IBufferService, ILogService, ICharsetService, ICoreService, ICoreMouseService, IUnicodeService, LogLevelEnum, ITerminalOptions, IOscLinkService } from 'common/services/Services';
import { InstantiationService } from 'common/services/InstantiationService';
import { LogService } from 'common/services/LogService';
import { BufferService, MINIMUM_COLS, MINIMUM_ROWS } from 'common/services/BufferService';
@@ -31,7 +31,6 @@ import { IDisposable, IAttributeData, ICoreTerminal, IScrollEvent, ScrollSource
import { CoreService } from 'common/services/CoreService';
import { EventEmitter, IEvent, forwardEvent } from 'common/EventEmitter';
import { CoreMouseService } from 'common/services/CoreMouseService';
-import { DirtyRowService } from 'common/services/DirtyRowService';
import { UnicodeService } from 'common/services/UnicodeService';
import { CharsetService } from 'common/services/CharsetService';
import { updateWindowsModeWrappedState } from 'common/WindowsMode';
@@ -49,7 +48,6 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
protected readonly _bufferService: IBufferService;
protected readonly _logService: ILogService;
protected readonly _charsetService: ICharsetService;
- protected readonly _dirtyRowService: IDirtyRowService;
protected readonly _oscLinkService: IOscLinkService;
public readonly coreMouseService: ICoreMouseService;
@@ -61,28 +59,29 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
private _writeBuffer: WriteBuffer;
private _windowsMode: IDisposable | undefined;
- private _onBinary = new EventEmitter();
- public get onBinary(): IEvent { return this._onBinary.event; }
- private _onData = new EventEmitter();
- public get onData(): IEvent { return this._onData.event; }
- protected _onLineFeed = new EventEmitter();
- public get onLineFeed(): IEvent { return this._onLineFeed.event; }
- private _onResize = new EventEmitter<{ cols: number, rows: number }>();
- public get onResize(): IEvent<{ cols: number, rows: number }> { return this._onResize.event; }
- protected _onScroll = new EventEmitter();
- public get onWriteParsed(): IEvent { return this._onWriteParsed.event; }
- protected _onWriteParsed = new EventEmitter();
+ private readonly _onBinary = this.register(new EventEmitter());
+ public readonly onBinary = this._onBinary.event;
+ private readonly _onData = this.register(new EventEmitter());
+ public readonly onData = this._onData.event;
+ protected _onLineFeed = this.register(new EventEmitter());
+ public readonly onLineFeed = this._onLineFeed.event;
+ private readonly _onResize = this.register(new EventEmitter<{ cols: number, rows: number }>());
+ public readonly onResize = this._onResize.event;
+ protected readonly _onWriteParsed = this.register(new EventEmitter());
+ public readonly onWriteParsed = this._onWriteParsed.event;
+
/**
* Internally we track the source of the scroll but this is meaningless outside the library so
* it's filtered out.
*/
protected _onScrollApi?: EventEmitter;
+ protected _onScroll = this.register(new EventEmitter());
public get onScroll(): IEvent {
if (!this._onScrollApi) {
- this._onScrollApi = new EventEmitter();
- this.register(this._onScroll.event(ev => {
+ this._onScrollApi = this.register(new EventEmitter());
+ this._onScroll.event(ev => {
this._onScrollApi?.fire(ev.position);
- }));
+ });
}
return this._onScrollApi.event;
}
@@ -104,19 +103,17 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
// Setup and initialize services
this._instantiationService = new InstantiationService();
- this.optionsService = new OptionsService(options);
+ this.optionsService = this.register(new OptionsService(options));
this._instantiationService.setService(IOptionsService, this.optionsService);
this._bufferService = this.register(this._instantiationService.createInstance(BufferService));
this._instantiationService.setService(IBufferService, this._bufferService);
- this._logService = this._instantiationService.createInstance(LogService);
+ this._logService = this.register(this._instantiationService.createInstance(LogService));
this._instantiationService.setService(ILogService, this._logService);
this.coreService = this.register(this._instantiationService.createInstance(CoreService, () => this.scrollToBottom()));
this._instantiationService.setService(ICoreService, this.coreService);
- this.coreMouseService = this._instantiationService.createInstance(CoreMouseService);
+ this.coreMouseService = this.register(this._instantiationService.createInstance(CoreMouseService));
this._instantiationService.setService(ICoreMouseService, this.coreMouseService);
- this._dirtyRowService = this._instantiationService.createInstance(DirtyRowService);
- this._instantiationService.setService(IDirtyRowService, this._dirtyRowService);
- this.unicodeService = this._instantiationService.createInstance(UnicodeService);
+ this.unicodeService = this.register(this._instantiationService.createInstance(UnicodeService));
this._instantiationService.setService(IUnicodeService, this.unicodeService);
this._charsetService = this._instantiationService.createInstance(CharsetService);
this._instantiationService.setService(ICharsetService, this._charsetService);
@@ -124,7 +121,7 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
this._instantiationService.setService(IOscLinkService, this._oscLinkService);
// Register input handler and handle/forward events
- this._inputHandler = new InputHandler(this._bufferService, this._charsetService, this.coreService, this._dirtyRowService, this._logService, this.optionsService, this._oscLinkService, this.coreMouseService, this.unicodeService);
+ this._inputHandler = this.register(new InputHandler(this._bufferService, this._charsetService, this.coreService, this._logService, this.optionsService, this._oscLinkService, this.coreMouseService, this.unicodeService));
this.register(forwardEvent(this._inputHandler.onLineFeed, this._onLineFeed));
this.register(this._inputHandler);
@@ -132,28 +129,25 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
this.register(forwardEvent(this._bufferService.onResize, this._onResize));
this.register(forwardEvent(this.coreService.onData, this._onData));
this.register(forwardEvent(this.coreService.onBinary, this._onBinary));
- this.register(this.optionsService.onOptionChange(key => this._updateOptions(key)));
+ this.register(this.coreService.onUserInput(() => this._writeBuffer.handleUserInput()));
+ this.register(this.optionsService.onSpecificOptionChange('windowsMode', e => this._handleWindowsModeOptionChange(e)));
this.register(this._bufferService.onScroll(event => {
this._onScroll.fire({ position: this._bufferService.buffer.ydisp, source: ScrollSource.TERMINAL });
- this._dirtyRowService.markRangeDirty(this._bufferService.buffer.scrollTop, this._bufferService.buffer.scrollBottom);
+ this._inputHandler.markRangeDirty(this._bufferService.buffer.scrollTop, this._bufferService.buffer.scrollBottom);
}));
this.register(this._inputHandler.onScroll(event => {
this._onScroll.fire({ position: this._bufferService.buffer.ydisp, source: ScrollSource.TERMINAL });
- this._dirtyRowService.markRangeDirty(this._bufferService.buffer.scrollTop, this._bufferService.buffer.scrollBottom);
+ this._inputHandler.markRangeDirty(this._bufferService.buffer.scrollTop, this._bufferService.buffer.scrollBottom);
}));
// Setup WriteBuffer
- this._writeBuffer = new WriteBuffer((data, promiseResult) => this._inputHandler.parse(data, promiseResult));
+ this._writeBuffer = this.register(new WriteBuffer((data, promiseResult) => this._inputHandler.parse(data, promiseResult)));
this.register(forwardEvent(this._writeBuffer.onWriteParsed, this._onWriteParsed));
- }
- public dispose(): void {
- if (this._isDisposed) {
- return;
- }
- super.dispose();
- this._windowsMode?.dispose();
- this._windowsMode = undefined;
+ this.register(toDisposable(() => {
+ this._windowsMode?.dispose();
+ this._windowsMode = undefined;
+ }));
}
public write(data: string | Uint8Array, callback?: () => void): void {
@@ -267,20 +261,12 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
this.coreMouseService.reset();
}
- protected _updateOptions(key: string): void {
- // TODO: These listeners should be owned by individual components
- switch (key) {
- case 'scrollback':
- this.buffers.resize(this.cols, this.rows);
- break;
- case 'windowsMode':
- if (this.optionsService.rawOptions.windowsMode) {
- this._enableWindowsMode();
- } else {
- this._windowsMode?.dispose();
- this._windowsMode = undefined;
- }
- break;
+ private _handleWindowsModeOptionChange(value: boolean): void {
+ if (value) {
+ this._enableWindowsMode();
+ } else {
+ this._windowsMode?.dispose();
+ this._windowsMode = undefined;
}
}
diff --git a/src/common/InputHandler.test.ts b/src/common/InputHandler.test.ts
index f734b002..d9127e7b 100644
--- a/src/common/InputHandler.test.ts
+++ b/src/common/InputHandler.test.ts
@@ -11,7 +11,7 @@ import { CellData } from 'common/buffer/CellData';
import { Attributes, UnderlineStyle } from 'common/buffer/Constants';
import { AttributeData } from 'common/buffer/AttributeData';
import { Params } from 'common/parser/Params';
-import { MockCoreService, MockBufferService, MockDirtyRowService, MockOptionsService, MockLogService, MockCoreMouseService, MockCharsetService, MockUnicodeService, MockOscLinkService } from 'common/TestUtils.test';
+import { MockCoreService, MockBufferService, MockOptionsService, MockLogService, MockCoreMouseService, MockCharsetService, MockUnicodeService, MockOscLinkService } from 'common/TestUtils.test';
import { IBufferService, ICoreService } from 'common/services/Services';
import { DEFAULT_OPTIONS } from 'common/services/OptionsService';
import { clone } from 'common/Clone';
@@ -67,7 +67,7 @@ describe('InputHandler', () => {
bufferService.resize(80, 30);
coreService = new CoreService(() => { }, bufferService, new MockLogService(), optionsService);
- inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockDirtyRowService(), new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
+ inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
});
describe('SL/SR/DECIC/DECDC', () => {
@@ -236,7 +236,7 @@ describe('InputHandler', () => {
describe('setMode', () => {
it('should toggle bracketedPasteMode', () => {
const coreService = new MockCoreService();
- const inputHandler = new TestInputHandler(new MockBufferService(80, 30), new MockCharsetService(), coreService, new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
+ const inputHandler = new TestInputHandler(new MockBufferService(80, 30), new MockCharsetService(), coreService, new MockLogService(), new MockOptionsService(), new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
// Set bracketed paste mode
inputHandler.setModePrivate(Params.fromArray([2004]));
assert.equal(coreService.decPrivateModes.bracketedPasteMode, true);
@@ -258,7 +258,6 @@ describe('InputHandler', () => {
bufferService,
new MockCharsetService(),
new MockCoreService(),
- new MockDirtyRowService(),
new MockLogService(),
new MockOptionsService(),
new MockOscLinkService(),
@@ -305,7 +304,6 @@ describe('InputHandler', () => {
bufferService,
new MockCharsetService(),
new MockCoreService(),
- new MockDirtyRowService(),
new MockLogService(),
new MockOptionsService(),
new MockOscLinkService(),
@@ -356,7 +354,6 @@ describe('InputHandler', () => {
bufferService,
new MockCharsetService(),
new MockCoreService(),
- new MockDirtyRowService(),
new MockLogService(),
new MockOptionsService(),
new MockOscLinkService(),
@@ -394,7 +391,6 @@ describe('InputHandler', () => {
bufferService,
new MockCharsetService(),
new MockCoreService(),
- new MockDirtyRowService(),
new MockLogService(),
new MockOptionsService(),
new MockOscLinkService(),
@@ -445,7 +441,6 @@ describe('InputHandler', () => {
bufferService,
new MockCharsetService(),
new MockCoreService(),
- new MockDirtyRowService(),
new MockLogService(),
new MockOptionsService(),
new MockOscLinkService(),
@@ -572,7 +567,6 @@ describe('InputHandler', () => {
new MockBufferService(80, 30),
new MockCharsetService(),
new MockCoreService(),
- new MockDirtyRowService(),
new MockLogService(),
new MockOptionsService(),
new MockOscLinkService(),
@@ -599,7 +593,7 @@ describe('InputHandler', () => {
beforeEach(() => {
bufferService = new MockBufferService(80, 30);
- handler = new TestInputHandler(bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
+ handler = new TestInputHandler(bufferService, new MockCharsetService(), new MockCoreService(), new MockLogService(), new MockOptionsService(), new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
});
it('should handle DECSET/DECRST 47 (alt screen buffer)', async () => {
await handler.parseP('\x1b[?47h\r\n\x1b[31mJUNK\x1b[?47lTEST');
@@ -796,7 +790,7 @@ describe('InputHandler', () => {
describe('colon notation', () => {
let inputHandler2: TestInputHandler;
beforeEach(() => {
- inputHandler2 = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockDirtyRowService(), new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
+ inputHandler2 = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
});
describe('should equal to semicolon', () => {
it('CSI 38:2::50:100:150 m', async () => {
@@ -2278,7 +2272,7 @@ describe('InputHandler - async handlers', () => {
coreService = new CoreService(() => { }, bufferService, new MockLogService(), optionsService);
coreService.onData(data => { console.log(data); });
- inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockDirtyRowService(), new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
+ inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
});
it('async CUP with CPR check', async () => {
diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts
index b599bb7e..b91b446c 100644
--- a/src/common/InputHandler.ts
+++ b/src/common/InputHandler.ts
@@ -12,11 +12,11 @@ import { Disposable } from 'common/Lifecycle';
import { StringToUtf32, stringFromCodePoint, Utf8ToUtf32 } from 'common/input/TextDecoder';
import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
import { EventEmitter, IEvent } from 'common/EventEmitter';
-import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IFunctionIdentifier } from 'common/parser/Types';
+import { IParsingState, IEscapeSequenceParser, IParams, IFunctionIdentifier } from 'common/parser/Types';
import { NULL_CELL_CODE, NULL_CELL_WIDTH, Attributes, FgFlags, BgFlags, Content, UnderlineStyle } from 'common/buffer/Constants';
import { CellData } from 'common/buffer/CellData';
import { AttributeData } from 'common/buffer/AttributeData';
-import { ICoreService, IBufferService, IOptionsService, ILogService, IDirtyRowService, ICoreMouseService, ICharsetService, IUnicodeService, LogLevelEnum, IOscLinkService } from 'common/services/Services';
+import { ICoreService, IBufferService, IOptionsService, ILogService, ICoreMouseService, ICharsetService, IUnicodeService, LogLevelEnum, IOscLinkService } from 'common/services/Services';
import { OscHandler } from 'common/parser/OscParser';
import { DcsHandler } from 'common/parser/DcsParser';
import { IBuffer } from 'common/buffer/Types';
@@ -104,6 +104,8 @@ export enum WindowsOptionsReportType {
// create a warning log if an async handler takes longer than the limit (in ms)
const SLOW_ASYNC_LIMIT = 5000;
+// Work variables to avoid garbage collection
+let $temp = 0;
/**
* The terminal's standard implementation of IInputHandler, this handles all
@@ -120,6 +122,7 @@ export class InputHandler extends Disposable implements IInputHandler {
private _windowTitle = '';
private _iconName = '';
private _currentLinkId?: number;
+ private _dirtyRowTracker: IDirtyRowTracker;
protected _windowTitleStack: string[] = [];
protected _iconNameStack: string[] = [];
@@ -129,33 +132,33 @@ export class InputHandler extends Disposable implements IInputHandler {
private _activeBuffer: IBuffer;
- private _onRequestBell = new EventEmitter();
- public get onRequestBell(): IEvent { return this._onRequestBell.event; }
- private _onRequestRefreshRows = new EventEmitter();
- public get onRequestRefreshRows(): IEvent { return this._onRequestRefreshRows.event; }
- private _onRequestReset = new EventEmitter();
- public get onRequestReset(): IEvent { return this._onRequestReset.event; }
- private _onRequestSendFocus = new EventEmitter();
- public get onRequestSendFocus(): IEvent { return this._onRequestSendFocus.event; }
- private _onRequestSyncScrollBar = new EventEmitter();
- public get onRequestSyncScrollBar(): IEvent { return this._onRequestSyncScrollBar.event; }
- private _onRequestWindowsOptionsReport = new EventEmitter();
- public get onRequestWindowsOptionsReport(): IEvent { return this._onRequestWindowsOptionsReport.event; }
+ private readonly _onRequestBell = this.register(new EventEmitter());
+ public readonly onRequestBell = this._onRequestBell.event;
+ private readonly _onRequestRefreshRows = this.register(new EventEmitter());
+ public readonly onRequestRefreshRows = this._onRequestRefreshRows.event;
+ private readonly _onRequestReset = this.register(new EventEmitter());
+ public readonly onRequestReset = this._onRequestReset.event;
+ private readonly _onRequestSendFocus = this.register(new EventEmitter());
+ public readonly onRequestSendFocus = this._onRequestSendFocus.event;
+ private readonly _onRequestSyncScrollBar = this.register(new EventEmitter());
+ public readonly onRequestSyncScrollBar = this._onRequestSyncScrollBar.event;
+ private readonly _onRequestWindowsOptionsReport = this.register(new EventEmitter());
+ public readonly onRequestWindowsOptionsReport = this._onRequestWindowsOptionsReport.event;
- private _onA11yChar = new EventEmitter();
- public get onA11yChar(): IEvent { return this._onA11yChar.event; }
- private _onA11yTab = new EventEmitter();
- public get onA11yTab(): IEvent { return this._onA11yTab.event; }
- private _onCursorMove = new EventEmitter();
- public get onCursorMove(): IEvent { return this._onCursorMove.event; }
- private _onLineFeed = new EventEmitter();
- public get onLineFeed(): IEvent { return this._onLineFeed.event; }
- private _onScroll = new EventEmitter();
- public get onScroll(): IEvent { return this._onScroll.event; }
- private _onTitleChange = new EventEmitter();
- public get onTitleChange(): IEvent { return this._onTitleChange.event; }
- private _onColor = new EventEmitter();
- public get onColor(): IEvent { return this._onColor.event; }
+ private readonly _onA11yChar = this.register(new EventEmitter());
+ public readonly onA11yChar = this._onA11yChar.event;
+ private readonly _onA11yTab = this.register(new EventEmitter());
+ public readonly onA11yTab = this._onA11yTab.event;
+ private readonly _onCursorMove = this.register(new EventEmitter());
+ public readonly onCursorMove = this._onCursorMove.event;
+ private readonly _onLineFeed = this.register(new EventEmitter());
+ public readonly onLineFeed = this._onLineFeed.event;
+ private readonly _onScroll = this.register(new EventEmitter());
+ public readonly onScroll = this._onScroll.event;
+ private readonly _onTitleChange = this.register(new EventEmitter());
+ public readonly onTitleChange = this._onTitleChange.event;
+ private readonly _onColor = this.register(new EventEmitter());
+ public readonly onColor = this._onColor.event;
private _parseStack: IParseStack = {
paused: false,
@@ -169,7 +172,6 @@ export class InputHandler extends Disposable implements IInputHandler {
private readonly _bufferService: IBufferService,
private readonly _charsetService: ICharsetService,
private readonly _coreService: ICoreService,
- private readonly _dirtyRowService: IDirtyRowService,
private readonly _logService: ILogService,
private readonly _optionsService: IOptionsService,
private readonly _oscLinkService: IOscLinkService,
@@ -179,6 +181,7 @@ export class InputHandler extends Disposable implements IInputHandler {
) {
super();
this.register(this._parser);
+ this._dirtyRowTracker = new DirtyRowTracker(this._bufferService);
// Track properties used in performance critical code manually to avoid using slow getters
this._activeBuffer = this._bufferService.buffer;
@@ -379,10 +382,6 @@ export class InputHandler extends Disposable implements IInputHandler {
this._parser.registerDcsHandler({ intermediates: '$', final: 'q' }, new DcsHandler((data, params) => this.requestStatusString(data, params)));
}
- public dispose(): void {
- super.dispose();
- }
-
/**
* Async parse support.
*/
@@ -459,7 +458,7 @@ export class InputHandler extends Disposable implements IInputHandler {
// Clear the dirty row service so we know which lines changed as a result of parsing
// Important: do not clear between async calls, otherwise we lost pending update information.
if (!wasPaused) {
- this._dirtyRowService.clearRange();
+ this._dirtyRowTracker.clearRange();
}
// process big data in smaller chunks
@@ -493,7 +492,7 @@ export class InputHandler extends Disposable implements IInputHandler {
}
// Refresh any dirty rows accumulated as part of parsing
- this._onRequestRefreshRows.fire(this._dirtyRowService.start, this._dirtyRowService.end);
+ this._onRequestRefreshRows.fire(this._dirtyRowTracker.start, this._dirtyRowTracker.end);
}
public print(data: Uint32Array, start: number, end: number): void {
@@ -507,7 +506,7 @@ export class InputHandler extends Disposable implements IInputHandler {
const curAttr = this._curAttrData;
let bufferRow = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!;
- this._dirtyRowService.markDirty(this._activeBuffer.y);
+ this._dirtyRowTracker.markDirty(this._activeBuffer.y);
// handle wide chars: reset start_cell-1 if we would overwrite the second cell of a wide char
if (this._activeBuffer.x && end - start > 0 && bufferRow.getWidth(this._activeBuffer.x - 1) === 2) {
@@ -635,7 +634,7 @@ export class InputHandler extends Disposable implements IInputHandler {
bufferRow.setCellFromCodePoint(this._activeBuffer.x, 0, 1, curAttr.fg, curAttr.bg, curAttr.extended);
}
- this._dirtyRowService.markDirty(this._activeBuffer.y);
+ this._dirtyRowTracker.markDirty(this._activeBuffer.y);
}
/**
@@ -699,7 +698,7 @@ export class InputHandler extends Disposable implements IInputHandler {
* @vt: #Y C0 FF "Form Feed" "\f, \x0C" "Treated as LF."
*/
public lineFeed(): boolean {
- this._dirtyRowService.markDirty(this._activeBuffer.y);
+ this._dirtyRowTracker.markDirty(this._activeBuffer.y);
if (this._optionsService.rawOptions.convertEol) {
this._activeBuffer.x = 0;
}
@@ -714,7 +713,7 @@ export class InputHandler extends Disposable implements IInputHandler {
if (this._activeBuffer.x >= this._bufferService.cols) {
this._activeBuffer.x--;
}
- this._dirtyRowService.markDirty(this._activeBuffer.y);
+ this._dirtyRowTracker.markDirty(this._activeBuffer.y);
this._onLineFeed.fire();
return true;
@@ -842,14 +841,14 @@ export class InputHandler extends Disposable implements IInputHandler {
this._activeBuffer.y = this._coreService.decPrivateModes.origin
? Math.min(this._activeBuffer.scrollBottom, Math.max(this._activeBuffer.scrollTop, this._activeBuffer.y))
: Math.min(this._bufferService.rows - 1, Math.max(0, this._activeBuffer.y));
- this._dirtyRowService.markDirty(this._activeBuffer.y);
+ this._dirtyRowTracker.markDirty(this._activeBuffer.y);
}
/**
* Set absolute cursor position.
*/
private _setCursor(x: number, y: number): void {
- this._dirtyRowService.markDirty(this._activeBuffer.y);
+ this._dirtyRowTracker.markDirty(this._activeBuffer.y);
if (this._coreService.decPrivateModes.origin) {
this._activeBuffer.x = x;
this._activeBuffer.y = this._activeBuffer.scrollTop + y;
@@ -858,7 +857,7 @@ export class InputHandler extends Disposable implements IInputHandler {
this._activeBuffer.y = y;
}
this._restrictCursor();
- this._dirtyRowService.markDirty(this._activeBuffer.y);
+ this._dirtyRowTracker.markDirty(this._activeBuffer.y);
}
/**
@@ -1178,16 +1177,16 @@ export class InputHandler extends Disposable implements IInputHandler {
switch (params.params[0]) {
case 0:
j = this._activeBuffer.y;
- this._dirtyRowService.markDirty(j);
+ this._dirtyRowTracker.markDirty(j);
this._eraseInBufferLine(j++, this._activeBuffer.x, this._bufferService.cols, this._activeBuffer.x === 0, respectProtect);
for (; j < this._bufferService.rows; j++) {
this._resetBufferLine(j, respectProtect);
}
- this._dirtyRowService.markDirty(j);
+ this._dirtyRowTracker.markDirty(j);
break;
case 1:
j = this._activeBuffer.y;
- this._dirtyRowService.markDirty(j);
+ this._dirtyRowTracker.markDirty(j);
// Deleted front part of line and everything before. This line will no longer be wrapped.
this._eraseInBufferLine(j, 0, this._activeBuffer.x + 1, true, respectProtect);
if (this._activeBuffer.x + 1 >= this._bufferService.cols) {
@@ -1197,15 +1196,15 @@ export class InputHandler extends Disposable implements IInputHandler {
while (j--) {
this._resetBufferLine(j, respectProtect);
}
- this._dirtyRowService.markDirty(0);
+ this._dirtyRowTracker.markDirty(0);
break;
case 2:
j = this._bufferService.rows;
- this._dirtyRowService.markDirty(j - 1);
+ this._dirtyRowTracker.markDirty(j - 1);
while (j--) {
this._resetBufferLine(j, respectProtect);
}
- this._dirtyRowService.markDirty(0);
+ this._dirtyRowTracker.markDirty(0);
break;
case 3:
// Clear scrollback (everything not in viewport)
@@ -1257,7 +1256,7 @@ export class InputHandler extends Disposable implements IInputHandler {
this._eraseInBufferLine(this._activeBuffer.y, 0, this._bufferService.cols, true, respectProtect);
break;
}
- this._dirtyRowService.markDirty(this._activeBuffer.y);
+ this._dirtyRowTracker.markDirty(this._activeBuffer.y);
return true;
}
@@ -1289,7 +1288,7 @@ export class InputHandler extends Disposable implements IInputHandler {
this._activeBuffer.lines.splice(row, 0, this._activeBuffer.getBlankLine(this._eraseAttrData()));
}
- this._dirtyRowService.markRangeDirty(this._activeBuffer.y, this._activeBuffer.scrollBottom);
+ this._dirtyRowTracker.markRangeDirty(this._activeBuffer.y, this._activeBuffer.scrollBottom);
this._activeBuffer.x = 0; // see https://vt100.net/docs/vt220-rm/chapter4.html - vt220 only?
return true;
}
@@ -1323,7 +1322,7 @@ export class InputHandler extends Disposable implements IInputHandler {
this._activeBuffer.lines.splice(j, 0, this._activeBuffer.getBlankLine(this._eraseAttrData()));
}
- this._dirtyRowService.markRangeDirty(this._activeBuffer.y, this._activeBuffer.scrollBottom);
+ this._dirtyRowTracker.markRangeDirty(this._activeBuffer.y, this._activeBuffer.scrollBottom);
this._activeBuffer.x = 0; // see https://vt100.net/docs/vt220-rm/chapter4.html - vt220 only?
return true;
}
@@ -1349,7 +1348,7 @@ export class InputHandler extends Disposable implements IInputHandler {
this._activeBuffer.getNullCell(this._eraseAttrData()),
this._eraseAttrData()
);
- this._dirtyRowService.markDirty(this._activeBuffer.y);
+ this._dirtyRowTracker.markDirty(this._activeBuffer.y);
}
return true;
}
@@ -1375,7 +1374,7 @@ export class InputHandler extends Disposable implements IInputHandler {
this._activeBuffer.getNullCell(this._eraseAttrData()),
this._eraseAttrData()
);
- this._dirtyRowService.markDirty(this._activeBuffer.y);
+ this._dirtyRowTracker.markDirty(this._activeBuffer.y);
}
return true;
}
@@ -1395,7 +1394,7 @@ export class InputHandler extends Disposable implements IInputHandler {
this._activeBuffer.lines.splice(this._activeBuffer.ybase + this._activeBuffer.scrollTop, 1);
this._activeBuffer.lines.splice(this._activeBuffer.ybase + this._activeBuffer.scrollBottom, 0, this._activeBuffer.getBlankLine(this._eraseAttrData()));
}
- this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
+ this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
return true;
}
@@ -1411,7 +1410,7 @@ export class InputHandler extends Disposable implements IInputHandler {
this._activeBuffer.lines.splice(this._activeBuffer.ybase + this._activeBuffer.scrollBottom, 1);
this._activeBuffer.lines.splice(this._activeBuffer.ybase + this._activeBuffer.scrollTop, 0, this._activeBuffer.getBlankLine(DEFAULT_ATTR_DATA));
}
- this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
+ this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
return true;
}
@@ -1443,7 +1442,7 @@ export class InputHandler extends Disposable implements IInputHandler {
line.deleteCells(0, param, this._activeBuffer.getNullCell(this._eraseAttrData()), this._eraseAttrData());
line.isWrapped = false;
}
- this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
+ this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
return true;
}
@@ -1476,7 +1475,7 @@ export class InputHandler extends Disposable implements IInputHandler {
line.insertCells(0, param, this._activeBuffer.getNullCell(this._eraseAttrData()), this._eraseAttrData());
line.isWrapped = false;
}
- this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
+ this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
return true;
}
@@ -1499,7 +1498,7 @@ export class InputHandler extends Disposable implements IInputHandler {
line.insertCells(this._activeBuffer.x, param, this._activeBuffer.getNullCell(this._eraseAttrData()), this._eraseAttrData());
line.isWrapped = false;
}
- this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
+ this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
return true;
}
@@ -1522,7 +1521,7 @@ export class InputHandler extends Disposable implements IInputHandler {
line.deleteCells(this._activeBuffer.x, param, this._activeBuffer.getNullCell(this._eraseAttrData()), this._eraseAttrData());
line.isWrapped = false;
}
- this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
+ this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
return true;
}
@@ -1544,7 +1543,7 @@ export class InputHandler extends Disposable implements IInputHandler {
this._activeBuffer.getNullCell(this._eraseAttrData()),
this._eraseAttrData()
);
- this._dirtyRowService.markDirty(this._activeBuffer.y);
+ this._dirtyRowTracker.markDirty(this._activeBuffer.y);
}
return true;
}
@@ -3220,7 +3219,7 @@ export class InputHandler extends Disposable implements IInputHandler {
const scrollRegionHeight = this._activeBuffer.scrollBottom - this._activeBuffer.scrollTop;
this._activeBuffer.lines.shiftElements(this._activeBuffer.ybase + this._activeBuffer.y, scrollRegionHeight, 1);
this._activeBuffer.lines.set(this._activeBuffer.ybase + this._activeBuffer.y, this._activeBuffer.getBlankLine(this._eraseAttrData()));
- this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
+ this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
} else {
this._activeBuffer.y--;
this._restrictCursor(); // quickfix to not run out of bounds
@@ -3293,7 +3292,7 @@ export class InputHandler extends Disposable implements IInputHandler {
line.isWrapped = false;
}
}
- this._dirtyRowService.markAllDirty();
+ this._dirtyRowTracker.markAllDirty();
this._setCursor(0, 0);
return true;
}
@@ -3344,4 +3343,60 @@ export class InputHandler extends Disposable implements IInputHandler {
if (data === ' q') return f(`P1$r${STYLES[opts.cursorStyle] - (opts.cursorBlink ? 1 : 0)} q`);
return f(`P0$r`);
}
+
+ public markRangeDirty(y1: number, y2: number): void {
+ this._dirtyRowTracker.markRangeDirty(y1, y2);
+ }
+}
+
+export interface IDirtyRowTracker {
+ readonly start: number;
+ readonly end: number;
+
+ clearRange(): void;
+ markDirty(y: number): void;
+ markRangeDirty(y1: number, y2: number): void;
+ markAllDirty(): void;
+}
+
+class DirtyRowTracker implements IDirtyRowTracker {
+ public start!: number;
+ public end!: number;
+
+ constructor(
+ @IBufferService private readonly _bufferService: IBufferService
+ ) {
+ this.clearRange();
+ }
+
+ public clearRange(): void {
+ this.start = this._bufferService.buffer.y;
+ this.end = this._bufferService.buffer.y;
+ }
+
+ public markDirty(y: number): void {
+ if (y < this.start) {
+ this.start = y;
+ } else if (y > this.end) {
+ this.end = y;
+ }
+ }
+
+ public markRangeDirty(y1: number, y2: number): void {
+ if (y1 > y2) {
+ $temp = y1;
+ y1 = y2;
+ y2 = $temp;
+ }
+ if (y1 < this.start) {
+ this.start = y1;
+ }
+ if (y2 > this.end) {
+ this.end = y2;
+ }
+ }
+
+ public markAllDirty(): void {
+ this.markRangeDirty(0, this._bufferService.rows - 1);
+ }
}
diff --git a/src/common/Lifecycle.ts b/src/common/Lifecycle.ts
index b3a7cc21..7ccc8aa7 100644
--- a/src/common/Lifecycle.ts
+++ b/src/common/Lifecycle.ts
@@ -17,15 +17,18 @@ export abstract class Disposable implements IDisposable {
}
/**
- * Disposes the object, triggering the `dispose` method on all registered IDisposables.
+ * Disposes the object, triggering the `dispose` method on all registered IDisposables. This is a
+ * readonly property instead of a method to prevent subclasses overriding it which is an easy
+ * mistake that can introduce memory leaks. If a class extends Disposable, all dispose calls
+ * should be done via {@link register}.
*/
- public dispose(): void {
+ public readonly dispose = (): void => {
this._isDisposed = true;
for (const d of this._disposables) {
d.dispose();
}
this._disposables.length = 0;
- }
+ };
/**
* Registers a disposable object.
diff --git a/src/common/TestUtils.test.ts b/src/common/TestUtils.test.ts
index 8fb71a5d..3aa0f694 100644
--- a/src/common/TestUtils.test.ts
+++ b/src/common/TestUtils.test.ts
@@ -3,13 +3,13 @@
* @license MIT
*/
-import { IBufferService, ICoreService, ILogService, IOptionsService, ITerminalOptions, IDirtyRowService, ICoreMouseService, ICharsetService, IUnicodeService, IUnicodeVersionProvider, LogLevelEnum, IDecorationService, IInternalDecoration, IOscLinkService } from 'common/services/Services';
+import { IBufferService, ICoreService, ILogService, IOptionsService, ITerminalOptions, ICoreMouseService, ICharsetService, IUnicodeService, IUnicodeVersionProvider, LogLevelEnum, IDecorationService, IInternalDecoration, IOscLinkService } from 'common/services/Services';
import { IEvent, EventEmitter } from 'common/EventEmitter';
import { clone } from 'common/Clone';
import { DEFAULT_OPTIONS } from 'common/services/OptionsService';
import { IBufferSet, IBuffer } from 'common/buffer/Types';
import { BufferSet } from 'common/buffer/BufferSet';
-import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEventType, ICharset, IModes, IAttributeData, IOscLinkData } from 'common/Types';
+import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEventType, ICharset, IModes, IAttributeData, IOscLinkData, IDisposable } from 'common/Types';
import { UnicodeV6 } from 'common/input/UnicodeV6';
import { IDecorationOptions, IDecoration } from 'xterm';
@@ -100,16 +100,6 @@ export class MockCoreService implements ICoreService {
public triggerBinaryEvent(data: string): void { }
}
-export class MockDirtyRowService implements IDirtyRowService {
- public serviceBrand: any;
- public start: number = 0;
- public end: number = 0;
- public clearRange(): void { }
- public markDirty(y: number): void { }
- public markRangeDirty(y1: number, y2: number): void { }
- public markAllDirty(): void { }
-}
-
export class MockLogService implements ILogService {
public serviceBrand: any;
public logLevel = LogLevelEnum.DEBUG;
@@ -123,7 +113,7 @@ export class MockOptionsService implements IOptionsService {
public serviceBrand: any;
public readonly rawOptions: Required = clone(DEFAULT_OPTIONS);
public options: Required = this.rawOptions;
- public onOptionChange: IEvent = new EventEmitter().event;
+ public onOptionChange: IEvent = new EventEmitter().event;
constructor(testOptions?: Partial) {
if (testOptions) {
for (const key of Object.keys(testOptions)) {
@@ -131,6 +121,22 @@ export class MockOptionsService implements IOptionsService {
}
}
}
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ public onSpecificOptionChange(key: T, listener: (arg1: ITerminalOptions[T]) => any): IDisposable {
+ return this.onOptionChange(eventKey => {
+ if (eventKey === key) {
+ listener(this.rawOptions[key]);
+ }
+ });
+ }
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ public onMultipleOptionChange(keys: (keyof ITerminalOptions)[], listener: () => any): IDisposable {
+ return this.onOptionChange(eventKey => {
+ if (keys.indexOf(eventKey) !== -1) {
+ listener();
+ }
+ });
+ }
public setOptions(options: ITerminalOptions): void {
for (const key of Object.keys(options)) {
this.options[key] = options[key];
diff --git a/src/common/buffer/Buffer.ts b/src/common/buffer/Buffer.ts
index 83866304..d53a9466 100644
--- a/src/common/buffer/Buffer.ts
+++ b/src/common/buffer/Buffer.ts
@@ -168,6 +168,7 @@ export class Buffer implements IBuffer {
// Deal with columns increasing (reducing needs to happen after reflow)
if (this._cols < newCols) {
for (let i = 0; i < this.lines.length; i++) {
+ // +boolean for fast 0 or 1 conversion
needsCleanup |= +this.lines.get(i)!.resize(newCols, nullCell);
}
}
@@ -247,6 +248,7 @@ export class Buffer implements IBuffer {
// Trim the end of the line off if cols shrunk
if (this._cols > newCols) {
for (let i = 0; i < this.lines.length; i++) {
+ // +boolean for fast 0 or 1 conversion
needsCleanup |= +this.lines.get(i)!.resize(newCols, nullCell);
}
}
diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts
index 2e4fbca9..2971d309 100644
--- a/src/common/buffer/BufferLine.ts
+++ b/src/common/buffer/BufferLine.ts
@@ -37,10 +37,8 @@ const enum Cell {
export const DEFAULT_ATTR_DATA = Object.freeze(new AttributeData());
-/** Work variables to avoid garbage collection. */
-const w: { startIndex: number } = {
- startIndex: 0
-};
+// Work variables to avoid garbage collection
+let $startIndex = 0;
/** Factor when to cleanup underlying array buffer after shrinking. */
const CLEANUP_THRESHOLD = 2;
@@ -181,10 +179,10 @@ export class BufferLine implements IBufferLine {
* to GC as it significantly reduced the amount of new objects/references needed.
*/
public loadCell(index: number, cell: ICellData): ICellData {
- w.startIndex = index * CELL_SIZE;
- cell.content = this._data[w.startIndex + Cell.CONTENT];
- cell.fg = this._data[w.startIndex + Cell.FG];
- cell.bg = this._data[w.startIndex + Cell.BG];
+ $startIndex = index * CELL_SIZE;
+ cell.content = this._data[$startIndex + Cell.CONTENT];
+ cell.fg = this._data[$startIndex + Cell.FG];
+ cell.bg = this._data[$startIndex + Cell.BG];
if (cell.content & Content.IS_COMBINED_MASK) {
cell.combinedData = this._combined[index];
}
diff --git a/src/common/buffer/BufferSet.ts b/src/common/buffer/BufferSet.ts
index f940bb8f..bc7aa58e 100644
--- a/src/common/buffer/BufferSet.ts
+++ b/src/common/buffer/BufferSet.ts
@@ -19,8 +19,8 @@ export class BufferSet extends Disposable implements IBufferSet {
private _alt!: Buffer;
private _activeBuffer!: Buffer;
- private _onBufferActivate = this.register(new EventEmitter<{activeBuffer: IBuffer, inactiveBuffer: IBuffer}>());
- public get onBufferActivate(): IEvent<{activeBuffer: IBuffer, inactiveBuffer: IBuffer}> { return this._onBufferActivate.event; }
+ private readonly _onBufferActivate = this.register(new EventEmitter<{activeBuffer: IBuffer, inactiveBuffer: IBuffer}>());
+ public readonly onBufferActivate = this._onBufferActivate.event;
/**
* Create a new BufferSet for the given terminal.
@@ -32,6 +32,8 @@ export class BufferSet extends Disposable implements IBufferSet {
) {
super();
this.reset();
+ this.register(this._optionsService.onSpecificOptionChange('scrollback', () => this.resize(this._bufferService.cols, this._bufferService.rows)));
+ this.register(this._optionsService.onSpecificOptionChange('tabStopWidth', () => this.setupTabStops()));
}
public reset(): void {
@@ -119,6 +121,7 @@ export class BufferSet extends Disposable implements IBufferSet {
public resize(newCols: number, newRows: number): void {
this._normal.resize(newCols, newRows);
this._alt.resize(newCols, newRows);
+ this.setupTabStops(newCols);
}
/**
diff --git a/src/common/buffer/Marker.ts b/src/common/buffer/Marker.ts
index 72c4085c..0629e26a 100644
--- a/src/common/buffer/Marker.ts
+++ b/src/common/buffer/Marker.ts
@@ -3,25 +3,25 @@
* @license MIT
*/
-import { EventEmitter, IEvent } from 'common/EventEmitter';
-import { Disposable } from 'common/Lifecycle';
-import { IMarker } from 'common/Types';
+import { EventEmitter } from 'common/EventEmitter';
+import { disposeArray } from 'common/Lifecycle';
+import { IDisposable, IMarker } from 'common/Types';
-export class Marker extends Disposable implements IMarker {
+export class Marker implements IMarker {
private static _nextId = 1;
- private _id: number = Marker._nextId++;
public isDisposed: boolean = false;
+ private _disposables: IDisposable[] = [];
+ private _id: number = Marker._nextId++;
public get id(): number { return this._id; }
- private _onDispose = new EventEmitter();
- public get onDispose(): IEvent { return this._onDispose.event; }
+ private readonly _onDispose = this.register(new EventEmitter());
+ public readonly onDispose = this._onDispose.event;
constructor(
public line: number
) {
- super();
}
public dispose(): void {
@@ -32,6 +32,12 @@ export class Marker extends Disposable implements IMarker {
this.line = -1;
// Emit before super.dispose such that dispose listeners get a change to react
this._onDispose.fire();
- super.dispose();
+ disposeArray(this._disposables);
+ this._disposables.length = 0;
+ }
+
+ public register(disposable: T): T {
+ this._disposables.push(disposable);
+ return disposable;
}
}
diff --git a/src/common/input/TextDecoder.test.ts b/src/common/input/TextDecoder.test.ts
index da1760a2..b8db8a03 100644
--- a/src/common/input/TextDecoder.test.ts
+++ b/src/common/input/TextDecoder.test.ts
@@ -121,35 +121,42 @@ describe('text encodings', () => {
describe('Utf8ToUtf32 decoder', () => {
describe('full codepoint test', () => {
-
- it('0..65535 (1/2/3 byte sequences)', () => {
- const decoder = new Utf8ToUtf32();
- const target = new Uint32Array(5);
- for (let i = 0; i < 65536; ++i) {
- // skip surrogate pairs and a BOM
- if ((i >= 0xD800 && i <= 0xDFFF) || i === 0xFEFF) {
- continue;
+ function formatRange(min: number, max: number): string {
+ return `${min}..${max} (0x${min.toString(16).toUpperCase()}..0x${max.toString(16).toUpperCase()})`;
+ }
+ for (let min = 0; min < 65535; min += 10000) {
+ const max = Math.min(min + 10000, 65536);
+ it(`${formatRange(min, max)} (1/2/3 byte sequences)`, () => {
+ const decoder = new Utf8ToUtf32();
+ const target = new Uint32Array(5);
+ for (let i = min; i < max; ++i) {
+ // skip surrogate pairs and a BOM
+ if ((i >= 0xD800 && i <= 0xDFFF) || i === 0xFEFF) {
+ continue;
+ }
+ const utf8Data = fromByteString(encode(String.fromCharCode(i)));
+ const length = decoder.decode(utf8Data, target);
+ assert.equal(length, 1);
+ assert.equal(toString(target, length), String.fromCharCode(i));
+ decoder.clear();
}
- const utf8Data = fromByteString(encode(String.fromCharCode(i)));
- const length = decoder.decode(utf8Data, target);
- assert.equal(length, 1);
- assert.equal(toString(target, length), String.fromCharCode(i));
- decoder.clear();
- }
- });
-
- it('65536..0x10FFFF (4 byte sequences)', function (): void {
- this.timeout(20000);
- const decoder = new Utf8ToUtf32();
- const target = new Uint32Array(5);
- for (let i = 65536; i < 0x10FFFF; ++i) {
- const utf8Data = fromByteString(encode(stringFromCodePoint(i)));
- const length = decoder.decode(utf8Data, target);
- assert.equal(length, 1);
- assert.equal(target[0], i);
- decoder.clear();
- }
- });
+ });
+ }
+ for (let minRaw = 60000; minRaw < 0x10FFFF; minRaw += 10000) {
+ const min = Math.max(minRaw, 65536);
+ const max = Math.min(minRaw + 10000, 0x10FFFF);
+ it(`${formatRange(min, max)} (4 byte sequences)`, function (): void {
+ const decoder = new Utf8ToUtf32();
+ const target = new Uint32Array(5);
+ for (let i = min; i < max; ++i) {
+ const utf8Data = fromByteString(encode(stringFromCodePoint(i)));
+ const length = decoder.decode(utf8Data, target);
+ assert.equal(length, 1);
+ assert.equal(target[0], i);
+ decoder.clear();
+ }
+ });
+ }
it('0xFEFF(BOM)', () => {
const decoder = new Utf8ToUtf32();
diff --git a/src/common/input/WriteBuffer.ts b/src/common/input/WriteBuffer.ts
index 2cdf4e3c..68dbc6f7 100644
--- a/src/common/input/WriteBuffer.ts
+++ b/src/common/input/WriteBuffer.ts
@@ -5,6 +5,7 @@
*/
import { EventEmitter, IEvent } from 'common/EventEmitter';
+import { Disposable } from 'common/Lifecycle';
declare const setTimeout: (handler: () => void, timeout?: number) => void;
@@ -33,17 +34,25 @@ const WRITE_TIMEOUT_MS = 12;
*/
const WRITE_BUFFER_LENGTH_THRESHOLD = 50;
-export class WriteBuffer {
+export class WriteBuffer extends Disposable {
private _writeBuffer: (string | Uint8Array)[] = [];
private _callbacks: ((() => void) | undefined)[] = [];
private _pendingData = 0;
private _bufferOffset = 0;
private _isSyncWriting = false;
private _syncCalls = 0;
- public get onWriteParsed(): IEvent { return this._onWriteParsed.event; }
- private _onWriteParsed = new EventEmitter();
+ private _didUserInput = false;
- constructor(private _action: (data: string | Uint8Array, promiseResult?: boolean) => void | Promise) { }
+ private readonly _onWriteParsed = this.register(new EventEmitter());
+ public readonly onWriteParsed = this._onWriteParsed.event;
+
+ constructor(private _action: (data: string | Uint8Array, promiseResult?: boolean) => void | Promise) {
+ super();
+ }
+
+ public handleUserInput(): void {
+ this._didUserInput = true;
+ }
/**
* @deprecated Unreliable, to be removed soon.
@@ -99,7 +108,20 @@ export class WriteBuffer {
// schedule chunk processing for next event loop run
if (!this._writeBuffer.length) {
this._bufferOffset = 0;
- queueMicrotask(() => this._innerWrite());
+
+ // If this is the first write call after the user has done some input,
+ // parse it immediately to minimize input latency,
+ // otherwise schedule for the next event
+ if (this._didUserInput) {
+ this._didUserInput = false;
+ this._pendingData += data.length;
+ this._writeBuffer.push(data);
+ this._callbacks.push(callback);
+ this._innerWrite();
+ return;
+ }
+
+ setTimeout(() => this._innerWrite());
}
this._pendingData += data.length;
diff --git a/src/common/parser/EscapeSequenceParser.test.ts b/src/common/parser/EscapeSequenceParser.test.ts
index 4b23fa00..76e88dea 100644
--- a/src/common/parser/EscapeSequenceParser.test.ts
+++ b/src/common/parser/EscapeSequenceParser.test.ts
@@ -87,7 +87,7 @@ class TestEscapeSequenceParser extends EscapeSequenceParser {
}
}
public mockOscParser(): void {
- this._oscParser = oscPutParser;
+ (this as any)._oscParser = oscPutParser;
}
public identifier(id: IFunctionIdentifier): number {
return this._identifier(id);
diff --git a/src/common/parser/EscapeSequenceParser.ts b/src/common/parser/EscapeSequenceParser.ts
index f20a7e91..5bcd2dd3 100644
--- a/src/common/parser/EscapeSequenceParser.ts
+++ b/src/common/parser/EscapeSequenceParser.ts
@@ -5,7 +5,7 @@
import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IOscHandler, IHandlerCollection, CsiHandlerType, OscFallbackHandlerType, IOscParser, EscHandlerType, IDcsParser, DcsFallbackHandlerType, IFunctionIdentifier, ExecuteFallbackHandlerType, CsiFallbackHandlerType, EscFallbackHandlerType, PrintHandlerType, PrintFallbackHandlerType, ExecuteHandlerType, IParserStackState, ParserStackType, ResumableHandlersType } from 'common/parser/Types';
import { ParserState, ParserAction } from 'common/parser/Constants';
-import { Disposable } from 'common/Lifecycle';
+import { Disposable, toDisposable } from 'common/Lifecycle';
import { IDisposable } from 'common/Types';
import { fill } from 'common/TypedArrayUtils';
import { Params } from 'common/parser/Params';
@@ -242,8 +242,8 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
protected _executeHandlers: { [flag: number]: ExecuteHandlerType };
protected _csiHandlers: IHandlerCollection;
protected _escHandlers: IHandlerCollection;
- protected _oscParser: IOscParser;
- protected _dcsParser: IDcsParser;
+ protected readonly _oscParser: IOscParser;
+ protected readonly _dcsParser: IDcsParser;
protected _errorHandler: (state: IParsingState) => IParsingState;
// fallback handlers
@@ -284,8 +284,13 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
this._executeHandlers = Object.create(null);
this._csiHandlers = Object.create(null);
this._escHandlers = Object.create(null);
- this._oscParser = new OscParser();
- this._dcsParser = new DcsParser();
+ this.register(toDisposable(() => {
+ this._csiHandlers = Object.create(null);
+ this._executeHandlers = Object.create(null);
+ this._escHandlers = Object.create(null);
+ }));
+ this._oscParser = this.register(new OscParser());
+ this._dcsParser = this.register(new DcsParser());
this._errorHandler = this._errorHandlerFb;
// swallow 7bit ST (ESC+\)
@@ -338,14 +343,6 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
return res.reverse().join('');
}
- public dispose(): void {
- this._csiHandlers = Object.create(null);
- this._executeHandlers = Object.create(null);
- this._escHandlers = Object.create(null);
- this._oscParser.dispose();
- this._dcsParser.dispose();
- }
-
public setPrintHandler(handler: PrintHandlerType): void {
this._printHandler = handler;
}
diff --git a/src/common/public/BufferNamespaceApi.ts b/src/common/public/BufferNamespaceApi.ts
index d86f6bf5..9e49ce2b 100644
--- a/src/common/public/BufferNamespaceApi.ts
+++ b/src/common/public/BufferNamespaceApi.ts
@@ -5,14 +5,15 @@
import { IBuffer as IBufferApi, IBufferNamespace as IBufferNamespaceApi } from 'xterm';
import { BufferApiView } from 'common/public/BufferApiView';
-import { IEvent, EventEmitter } from 'common/EventEmitter';
+import { EventEmitter } from 'common/EventEmitter';
import { ICoreTerminal } from 'common/Types';
export class BufferNamespaceApi implements IBufferNamespaceApi {
private _normal: BufferApiView;
private _alternate: BufferApiView;
- private _onBufferChange = new EventEmitter();
- public get onBufferChange(): IEvent { return this._onBufferChange.event; }
+
+ private readonly _onBufferChange = new EventEmitter();
+ public readonly onBufferChange = this._onBufferChange.event;
constructor(private _core: ICoreTerminal) {
this._normal = new BufferApiView(this._core.buffers.normal, 'normal');
diff --git a/src/common/services/BufferService.ts b/src/common/services/BufferService.ts
index e3b7dcd8..3f15f242 100644
--- a/src/common/services/BufferService.ts
+++ b/src/common/services/BufferService.ts
@@ -6,7 +6,7 @@
import { IBufferService, IOptionsService } from 'common/services/Services';
import { BufferSet } from 'common/buffer/BufferSet';
import { IBufferSet, IBuffer } from 'common/buffer/Types';
-import { EventEmitter, IEvent } from 'common/EventEmitter';
+import { EventEmitter, IEventEmitter, IEvent } from 'common/EventEmitter';
import { Disposable } from 'common/Lifecycle';
import { IAttributeData, IBufferLine, ScrollSource } from 'common/Types';
@@ -22,10 +22,10 @@ export class BufferService extends Disposable implements IBufferService {
/** Whether the user is scrolling (locks the scroll position) */
public isUserScrolling: boolean = false;
- private _onResize = new EventEmitter<{ cols: number, rows: number }>();
- public get onResize(): IEvent<{ cols: number, rows: number }> { return this._onResize.event; }
- private _onScroll = new EventEmitter();
- public get onScroll(): IEvent { return this._onScroll.event; }
+ private readonly _onResize = this.register(new EventEmitter<{ cols: number, rows: number }>());
+ public readonly onResize = this._onResize.event;
+ private readonly _onScroll = this.register(new EventEmitter());
+ public readonly onScroll = this._onScroll.event;
public get buffer(): IBuffer { return this.buffers.active; }
@@ -36,19 +36,14 @@ export class BufferService extends Disposable implements IBufferService {
super();
this.cols = Math.max(optionsService.rawOptions.cols || 0, MINIMUM_COLS);
this.rows = Math.max(optionsService.rawOptions.rows || 0, MINIMUM_ROWS);
- this.buffers = new BufferSet(optionsService, this);
- }
-
- public dispose(): void {
- super.dispose();
- this.buffers.dispose();
+ this.buffers = this.register(new BufferSet(optionsService, this));
}
public resize(cols: number, rows: number): void {
this.cols = cols;
this.rows = rows;
this.buffers.resize(cols, rows);
- this.buffers.setupTabStops(this.cols);
+ // TODO: This doesn't fire when scrollback changes - add a resize event to BufferSet and forward event
this._onResize.fire({ cols, rows });
}
diff --git a/src/common/services/CoreMouseService.ts b/src/common/services/CoreMouseService.ts
index 54e991f8..d955370e 100644
--- a/src/common/services/CoreMouseService.ts
+++ b/src/common/services/CoreMouseService.ts
@@ -5,6 +5,7 @@
import { IBufferService, ICoreService, ICoreMouseService } from 'common/services/Services';
import { EventEmitter, IEvent } from 'common/EventEmitter';
import { ICoreMouseProtocol, ICoreMouseEvent, CoreMouseEncoding, CoreMouseEventType, CoreMouseButton, CoreMouseAction } from 'common/Types';
+import { Disposable } from 'common/Lifecycle';
/**
* Supported default protocols.
@@ -165,18 +166,21 @@ const DEFAULT_ENCODINGS: { [key: string]: CoreMouseEncoding } = {
* a tracking report to the backend based on protocol and encoding limitations.
* To send a mouse event call `triggerMouseEvent`.
*/
-export class CoreMouseService implements ICoreMouseService {
+export class CoreMouseService extends Disposable implements ICoreMouseService {
private _protocols: { [name: string]: ICoreMouseProtocol } = {};
private _encodings: { [name: string]: CoreMouseEncoding } = {};
private _activeProtocol: string = '';
private _activeEncoding: string = '';
- private _onProtocolChange = new EventEmitter();
private _lastEvent: ICoreMouseEvent | null = null;
+ private readonly _onProtocolChange = this.register(new EventEmitter());
+ public readonly onProtocolChange = this._onProtocolChange.event;
+
constructor(
@IBufferService private readonly _bufferService: IBufferService,
@ICoreService private readonly _coreService: ICoreService
) {
+ super();
// register default protocols and encodings
for (const name of Object.keys(DEFAULT_PROTOCOLS)) this.addProtocol(name, DEFAULT_PROTOCOLS[name]);
for (const name of Object.keys(DEFAULT_ENCODINGS)) this.addEncoding(name, DEFAULT_ENCODINGS[name]);
@@ -225,13 +229,6 @@ export class CoreMouseService implements ICoreMouseService {
this._lastEvent = null;
}
- /**
- * Event to announce changes in mouse tracking.
- */
- public get onProtocolChange(): IEvent {
- return this._onProtocolChange.event;
- }
-
/**
* Triggers a mouse event to be sent.
*
diff --git a/src/common/services/CoreService.ts b/src/common/services/CoreService.ts
index 20a34603..9282197b 100644
--- a/src/common/services/CoreService.ts
+++ b/src/common/services/CoreService.ts
@@ -34,12 +34,12 @@ export class CoreService extends Disposable implements ICoreService {
// Circular dependency, this must be unset or memory will leak after Terminal.dispose
private _scrollToBottom: (() => void) | undefined;
- private _onData = this.register(new EventEmitter());
- public get onData(): IEvent { return this._onData.event; }
- private _onUserInput = this.register(new EventEmitter());
- public get onUserInput(): IEvent { return this._onUserInput.event; }
- private _onBinary = this.register(new EventEmitter());
- public get onBinary(): IEvent { return this._onBinary.event; }
+ private readonly _onData = this.register(new EventEmitter());
+ public readonly onData = this._onData.event;
+ private readonly _onUserInput = this.register(new EventEmitter());
+ public readonly onUserInput = this._onUserInput.event;
+ private readonly _onBinary = this.register(new EventEmitter());
+ public readonly onBinary = this._onBinary.event;
constructor(
// TODO: Move this into a service
diff --git a/src/common/services/DecorationService.ts b/src/common/services/DecorationService.ts
index e5d115a1..c27e7b2f 100644
--- a/src/common/services/DecorationService.ts
+++ b/src/common/services/DecorationService.ts
@@ -5,17 +5,15 @@
import { css } from 'common/Color';
import { EventEmitter } from 'common/EventEmitter';
-import { Disposable } from 'common/Lifecycle';
+import { Disposable, toDisposable } from 'common/Lifecycle';
import { IDecorationService, IInternalDecoration } from 'common/services/Services';
import { SortedList } from 'common/SortedList';
import { IColor } from 'common/Types';
import { IDecorationOptions, IDecoration, IMarker, IEvent } from 'xterm';
-/** Work variables to avoid garbage collection. */
-const w = {
- xmin: 0,
- xmax: 0
-};
+// Work variables to avoid garbage collection
+let $xmin = 0;
+let $xmax = 0;
export class DecorationService extends Disposable implements IDecorationService {
public serviceBrand: any;
@@ -27,13 +25,23 @@ export class DecorationService extends Disposable implements IDecorationService
*/
private readonly _decorations: SortedList = new SortedList(e => e?.marker.line);
- private _onDecorationRegistered = this.register(new EventEmitter());
- public get onDecorationRegistered(): IEvent { return this._onDecorationRegistered.event; }
- private _onDecorationRemoved = this.register(new EventEmitter());
- public get onDecorationRemoved(): IEvent { return this._onDecorationRemoved.event; }
+ private readonly _onDecorationRegistered = this.register(new EventEmitter());
+ public readonly onDecorationRegistered = this._onDecorationRegistered.event;
+ private readonly _onDecorationRemoved = this.register(new EventEmitter());
+ public readonly onDecorationRemoved = this._onDecorationRemoved.event;
public get decorations(): IterableIterator { return this._decorations.values(); }
+ constructor() {
+ super();
+
+ this.register(toDisposable(() => {
+ for (const d of this._decorations.values()) {
+ this._onDecorationRemoved.fire(d);
+ }
+ this.reset();
+ }));
+ }
public registerDecoration(options: IDecorationOptions): IDecoration | undefined {
if (options.marker.isDisposed) {
return undefined;
@@ -76,32 +84,26 @@ export class DecorationService extends Disposable implements IDecorationService
public forEachDecorationAtCell(x: number, line: number, layer: 'bottom' | 'top' | undefined, callback: (decoration: IInternalDecoration) => void): void {
this._decorations.forEachByKey(line, d => {
- w.xmin = d.options.x ?? 0;
- w.xmax = w.xmin + (d.options.width ?? 1);
- if (x >= w.xmin && x < w.xmax && (!layer || (d.options.layer ?? 'bottom') === layer)) {
+ $xmin = d.options.x ?? 0;
+ $xmax = $xmin + (d.options.width ?? 1);
+ if (x >= $xmin && x < $xmax && (!layer || (d.options.layer ?? 'bottom') === layer)) {
callback(d);
}
});
}
-
- public dispose(): void {
- for (const d of this._decorations.values()) {
- this._onDecorationRemoved.fire(d);
- }
- this.reset();
- }
}
class Decoration extends Disposable implements IInternalDecoration {
public readonly marker: IMarker;
public element: HTMLElement | undefined;
- public isDisposed: boolean = false;
public readonly onRenderEmitter = this.register(new EventEmitter());
public readonly onRender = this.onRenderEmitter.event;
- private _onDispose = this.register(new EventEmitter());
+ private readonly _onDispose = this.register(new EventEmitter());
public readonly onDispose = this._onDispose.event;
+ public get isDisposed(): boolean { return this._isDisposed; }
+
private _cachedBg: IColor | undefined | null = null;
public get backgroundColorRGB(): IColor | undefined {
if (this._cachedBg === null) {
@@ -134,14 +136,12 @@ class Decoration extends Disposable implements IInternalDecoration {
if (this.options.overviewRulerOptions && !this.options.overviewRulerOptions.position) {
this.options.overviewRulerOptions.position = 'full';
}
- }
- public override dispose(): void {
- if (this._isDisposed) {
- return;
- }
- this._isDisposed = true;
- this._onDispose.fire();
- super.dispose();
+ this.register(toDisposable(() => {
+ if (this._isDisposed) {
+ return;
+ }
+ this._onDispose.fire();
+ }));
}
}
diff --git a/src/common/services/DirtyRowService.ts b/src/common/services/DirtyRowService.ts
deleted file mode 100644
index 1c43b67e..00000000
--- a/src/common/services/DirtyRowService.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-/**
- * Copyright (c) 2019 The xterm.js authors. All rights reserved.
- * @license MIT
- */
-
-import { IBufferService, IDirtyRowService } from 'common/services/Services';
-
-export class DirtyRowService implements IDirtyRowService {
- public serviceBrand: any;
-
- private _start!: number;
- private _end!: number;
-
- public get start(): number { return this._start; }
- public get end(): number { return this._end; }
-
- constructor(
- @IBufferService private readonly _bufferService: IBufferService
- ) {
- this.clearRange();
- }
-
- public clearRange(): void {
- this._start = this._bufferService.buffer.y;
- this._end = this._bufferService.buffer.y;
- }
-
- public markDirty(y: number): void {
- if (y < this._start) {
- this._start = y;
- } else if (y > this._end) {
- this._end = y;
- }
- }
-
- public markRangeDirty(y1: number, y2: number): void {
- if (y1 > y2) {
- const temp = y1;
- y1 = y2;
- y2 = temp;
- }
- if (y1 < this._start) {
- this._start = y1;
- }
- if (y2 > this._end) {
- this._end = y2;
- }
- }
-
- public markAllDirty(): void {
- this.markRangeDirty(0, this._bufferService.rows - 1);
- }
-}
diff --git a/src/common/services/InstantiationService.ts b/src/common/services/InstantiationService.ts
index 8280948a..375e442d 100644
--- a/src/common/services/InstantiationService.ts
+++ b/src/common/services/InstantiationService.ts
@@ -29,7 +29,9 @@ export class ServiceCollection {
}
public forEach(callback: (id: IServiceIdentifier, instance: any) => any): void {
- this._entries.forEach((value, key) => callback(key, value));
+ for (const [key, value] of this._entries.entries()) {
+ callback(key, value);
+ }
}
public has(id: IServiceIdentifier): boolean {
diff --git a/src/common/services/LogService.ts b/src/common/services/LogService.ts
index d3566567..4b56a097 100644
--- a/src/common/services/LogService.ts
+++ b/src/common/services/LogService.ts
@@ -3,6 +3,7 @@
* @license MIT
*/
+import { Disposable } from 'common/Lifecycle';
import { ILogService, IOptionsService, LogLevelEnum } from 'common/services/Services';
type LogType = (message?: any, ...optionalParams: any[]) => void;
@@ -29,7 +30,7 @@ const optionsKeyToLogLevel: { [key: string]: LogLevelEnum } = {
const LOG_PREFIX = 'xterm.js: ';
-export class LogService implements ILogService {
+export class LogService extends Disposable implements ILogService {
public serviceBrand: any;
public logLevel: LogLevelEnum = LogLevelEnum.OFF;
@@ -37,12 +38,9 @@ export class LogService implements ILogService {
constructor(
@IOptionsService private readonly _optionsService: IOptionsService
) {
+ super();
this._updateLogLevel();
- this._optionsService.onOptionChange(key => {
- if (key === 'logLevel') {
- this._updateLogLevel();
- }
- });
+ this.register(this._optionsService.onSpecificOptionChange('logLevel', () => this._updateLogLevel()));
}
private _updateLogLevel(): void {
diff --git a/src/common/services/OptionsService.test.ts b/src/common/services/OptionsService.test.ts
index a65cb986..004f7316 100644
--- a/src/common/services/OptionsService.test.ts
+++ b/src/common/services/OptionsService.test.ts
@@ -5,6 +5,7 @@
import { assert } from 'chai';
import { OptionsService, DEFAULT_OPTIONS } from 'common/services/OptionsService';
+import { IDisposable } from 'common/Types';
describe('OptionsService', () => {
describe('constructor', () => {
@@ -59,7 +60,7 @@ describe('OptionsService', () => {
});
it('normalizes invalid fontWeight option values', () => {
service.options.fontWeight = 350;
- assert.doesNotThrow(() => service.options.fontWeight = 10000), 'fontWeight should be normalized instead of throwing';
+ assert.doesNotThrow(() => service.options.fontWeight = 10000, 'fontWeight should be normalized instead of throwing');
assert.equal(service.options.fontWeight, DEFAULT_OPTIONS.fontWeight, 'Values greater than 1000 should be reset to default');
service.options.fontWeight = 350;
@@ -71,4 +72,79 @@ describe('OptionsService', () => {
assert.equal(service.options.fontWeight, DEFAULT_OPTIONS.fontWeight, 'Wrong string literals should be reset to default');
});
});
+ describe('onOptionChange', () => {
+ let service: OptionsService;
+ beforeEach(() => {
+ service = new OptionsService({});
+ });
+ it('should fire on any option change', async () => {
+ let disposable: IDisposable;
+ await new Promise(r => {
+ disposable = service.onOptionChange(e => {
+ assert.strictEqual(e, 'cursorWidth');
+ r();
+ });
+ service.options.cursorWidth = 10;
+ });
+ disposable!.dispose();
+ await new Promise(r => {
+ service.onOptionChange(e => {
+ assert.strictEqual(e, 'scrollback');
+ r();
+ });
+ service.options.scrollback = 20;
+ });
+ });
+ });
+ describe('onSpecificOptionChange', () => {
+ let service: OptionsService;
+ beforeEach(() => {
+ service = new OptionsService({});
+ });
+ it('should fire only on a specific option change', async () => {
+ await new Promise(r => {
+ service.onSpecificOptionChange('scrollback', e => {
+ assert.strictEqual(e, 20);
+ r();
+ });
+ service.options.cursorWidth = 10;
+ service.options.scrollback = 20;
+ });
+ });
+ });
+ describe('onSpecificOptionChange', () => {
+ let service: OptionsService;
+ beforeEach(() => {
+ service = new OptionsService({});
+ });
+ it('should fire only on a specific option change', async () => {
+ await new Promise