aboutsummaryrefslogtreecommitdiffstats
path: root/node_modules/xterm/src/browser/renderer
diff options
context:
space:
mode:
authorAnthony Schneider <tonyschneider3@gmail.com>2022-02-11 19:40:35 -0600
committerAnthony Schneider <tonyschneider3@gmail.com>2022-02-11 19:40:35 -0600
commitb52feccdcc58c1f4583c8542632d6c026335dea7 (patch)
tree5e242dd13ed4bbfff85a07109ef826f80874e2a6 /node_modules/xterm/src/browser/renderer
parent94862321e2e4a58e3209c037e8061f0435b3aa82 (diff)
Changed javascript to be in its own file. Began (messy) setup for terminal.
Diffstat (limited to 'node_modules/xterm/src/browser/renderer')
-rw-r--r--node_modules/xterm/src/browser/renderer/BaseRenderLayer.ts513
-rw-r--r--node_modules/xterm/src/browser/renderer/CursorRenderLayer.ts377
-rw-r--r--node_modules/xterm/src/browser/renderer/CustomGlyphs.ts563
-rw-r--r--node_modules/xterm/src/browser/renderer/GridCache.ts33
-rw-r--r--node_modules/xterm/src/browser/renderer/LinkRenderLayer.ts83
-rw-r--r--node_modules/xterm/src/browser/renderer/Renderer.ts215
-rw-r--r--node_modules/xterm/src/browser/renderer/RendererUtils.ts11
-rw-r--r--node_modules/xterm/src/browser/renderer/SelectionRenderLayer.ts128
-rw-r--r--node_modules/xterm/src/browser/renderer/TextRenderLayer.ts330
-rw-r--r--node_modules/xterm/src/browser/renderer/Types.d.ts109
-rw-r--r--node_modules/xterm/src/browser/renderer/atlas/BaseCharAtlas.ts58
-rw-r--r--node_modules/xterm/src/browser/renderer/atlas/CharAtlasCache.ts95
-rw-r--r--node_modules/xterm/src/browser/renderer/atlas/CharAtlasUtils.ts54
-rw-r--r--node_modules/xterm/src/browser/renderer/atlas/Constants.ts15
-rw-r--r--node_modules/xterm/src/browser/renderer/atlas/DynamicCharAtlas.ts404
-rw-r--r--node_modules/xterm/src/browser/renderer/atlas/LRUMap.ts136
-rw-r--r--node_modules/xterm/src/browser/renderer/atlas/Types.d.ts29
-rw-r--r--node_modules/xterm/src/browser/renderer/dom/DomRenderer.ts400
-rw-r--r--node_modules/xterm/src/browser/renderer/dom/DomRendererRowFactory.ts259
19 files changed, 3812 insertions, 0 deletions
diff --git a/node_modules/xterm/src/browser/renderer/BaseRenderLayer.ts b/node_modules/xterm/src/browser/renderer/BaseRenderLayer.ts
new file mode 100644
index 0000000..629e943
--- /dev/null
+++ b/node_modules/xterm/src/browser/renderer/BaseRenderLayer.ts
@@ -0,0 +1,513 @@
+/**
+ * Copyright (c) 2017 The xterm.js authors. All rights reserved.
+ * @license MIT
+ */
+
+import { IRenderDimensions, IRenderLayer } from 'browser/renderer/Types';
+import { ICellData } from 'common/Types';
+import { DEFAULT_COLOR, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE, Attributes } from 'common/buffer/Constants';
+import { IGlyphIdentifier } from 'browser/renderer/atlas/Types';
+import { DIM_OPACITY, INVERTED_DEFAULT_COLOR, TEXT_BASELINE } from 'browser/renderer/atlas/Constants';
+import { BaseCharAtlas } from 'browser/renderer/atlas/BaseCharAtlas';
+import { acquireCharAtlas } from 'browser/renderer/atlas/CharAtlasCache';
+import { AttributeData } from 'common/buffer/AttributeData';
+import { IColorSet, IColor } from 'browser/Types';
+import { CellData } from 'common/buffer/CellData';
+import { IBufferService, IOptionsService } from 'common/services/Services';
+import { throwIfFalsy } from 'browser/renderer/RendererUtils';
+import { channels, color, rgba } from 'browser/Color';
+import { removeElementFromParent } from 'browser/Dom';
+import { tryDrawCustomChar } from 'browser/renderer/CustomGlyphs';
+
+export abstract class BaseRenderLayer implements IRenderLayer {
+ private _canvas: HTMLCanvasElement;
+ protected _ctx!: CanvasRenderingContext2D;
+ private _scaledCharWidth: number = 0;
+ private _scaledCharHeight: number = 0;
+ private _scaledCellWidth: number = 0;
+ private _scaledCellHeight: number = 0;
+ private _scaledCharLeft: number = 0;
+ private _scaledCharTop: number = 0;
+
+ 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
+ };
+
+ constructor(
+ private _container: HTMLElement,
+ id: string,
+ zIndex: number,
+ private _alpha: boolean,
+ protected _colors: IColorSet,
+ private _rendererId: number,
+ protected readonly _bufferService: IBufferService,
+ protected readonly _optionsService: IOptionsService
+ ) {
+ 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 {
+ removeElementFromParent(this._canvas);
+ this._charAtlas?.dispose();
+ }
+
+ private _initCanvas(): void {
+ this._ctx = throwIfFalsy(this._canvas.getContext('2d', { alpha: this._alpha }));
+ // Draw the background if this is an opaque layer
+ if (!this._alpha) {
+ this._clearAll();
+ }
+ }
+
+ public onOptionsChanged(): void {}
+ public onBlur(): void {}
+ public onFocus(): void {}
+ public onCursorMove(): void {}
+ public onGridChanged(startRow: number, endRow: number): void {}
+ public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {}
+
+ public setColors(colorSet: IColorSet): void {
+ this._refreshCharAtlas(colorSet);
+ }
+
+ protected _setTransparency(alpha: boolean): void {
+ // Do nothing when alpha doesn't change
+ if (alpha === this._alpha) {
+ return;
+ }
+
+ // Create new canvas and replace old one
+ const oldCanvas = this._canvas;
+ this._alpha = alpha;
+ // Cloning preserves properties
+ this._canvas = this._canvas.cloneNode() as HTMLCanvasElement;
+ this._initCanvas();
+ 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);
+ }
+
+ /**
+ * 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 {
+ if (this._scaledCharWidth <= 0 && this._scaledCharHeight <= 0) {
+ return;
+ }
+ this._charAtlas = acquireCharAtlas(this._optionsService.rawOptions, this._rendererId, colorSet, this._scaledCharWidth, this._scaledCharHeight);
+ this._charAtlas.warmUp();
+ }
+
+ public resize(dim: IRenderDimensions): void {
+ this._scaledCellWidth = dim.scaledCellWidth;
+ this._scaledCellHeight = dim.scaledCellHeight;
+ this._scaledCharWidth = dim.scaledCharWidth;
+ this._scaledCharHeight = dim.scaledCharHeight;
+ this._scaledCharLeft = dim.scaledCharLeft;
+ this._scaledCharTop = dim.scaledCharTop;
+ this._canvas.width = dim.scaledCanvasWidth;
+ this._canvas.height = dim.scaledCanvasHeight;
+ this._canvas.style.width = `${dim.canvasWidth}px`;
+ this._canvas.style.height = `${dim.canvasHeight}px`;
+
+ // Draw the background if this is an opaque layer
+ if (!this._alpha) {
+ this._clearAll();
+ }
+
+ this._refreshCharAtlas(this._colors);
+ }
+
+ public abstract reset(): void;
+
+ public clearTextureAtlas(): void {
+ this._charAtlas?.clear();
+ }
+
+ /**
+ * Fills 1+ cells completely. This uses the existing fillStyle on the context.
+ * @param x The column to start at.
+ * @param y The row to start at
+ * @param width The number of columns to fill.
+ * @param height The number of rows to fill.
+ */
+ protected _fillCells(x: number, y: number, width: number, height: number): void {
+ this._ctx.fillRect(
+ x * this._scaledCellWidth,
+ y * this._scaledCellHeight,
+ width * this._scaledCellWidth,
+ height * this._scaledCellHeight);
+ }
+
+ /**
+ * Fills a 1px line (2px on HDPI) at the middle of the cell. This uses the
+ * existing fillStyle on the context.
+ * @param x The column to fill.
+ * @param y The row to fill.
+ */
+ protected _fillMiddleLineAtCells(x: number, y: number, width: number = 1): void {
+ const cellOffset = Math.ceil(this._scaledCellHeight * 0.5);
+ this._ctx.fillRect(
+ x * this._scaledCellWidth,
+ (y + 1) * this._scaledCellHeight - cellOffset - window.devicePixelRatio,
+ width * this._scaledCellWidth,
+ window.devicePixelRatio);
+ }
+
+ /**
+ * Fills a 1px line (2px on HDPI) at the bottom of the cell. This uses the
+ * existing fillStyle on the context.
+ * @param x The column to fill.
+ * @param y The row to fill.
+ */
+ protected _fillBottomLineAtCells(x: number, y: number, width: number = 1): void {
+ this._ctx.fillRect(
+ x * this._scaledCellWidth,
+ (y + 1) * this._scaledCellHeight - window.devicePixelRatio - 1 /* Ensure it's drawn within the cell */,
+ width * this._scaledCellWidth,
+ window.devicePixelRatio);
+ }
+
+ /**
+ * Fills a 1px line (2px on HDPI) at the left of the cell. This uses the
+ * existing fillStyle on the context.
+ * @param x The column to fill.
+ * @param y The row to fill.
+ */
+ protected _fillLeftLineAtCell(x: number, y: number, width: number): void {
+ this._ctx.fillRect(
+ x * this._scaledCellWidth,
+ y * this._scaledCellHeight,
+ window.devicePixelRatio * width,
+ this._scaledCellHeight);
+ }
+
+ /**
+ * Strokes a 1px rectangle (2px on HDPI) around a cell. This uses the existing
+ * strokeStyle on the context.
+ * @param x The column to fill.
+ * @param y The row to fill.
+ */
+ protected _strokeRectAtCell(x: number, y: number, width: number, height: number): void {
+ this._ctx.lineWidth = window.devicePixelRatio;
+ this._ctx.strokeRect(
+ x * this._scaledCellWidth + window.devicePixelRatio / 2,
+ y * this._scaledCellHeight + (window.devicePixelRatio / 2),
+ width * this._scaledCellWidth - window.devicePixelRatio,
+ (height * this._scaledCellHeight) - window.devicePixelRatio);
+ }
+
+ /**
+ * Clears the entire canvas.
+ */
+ protected _clearAll(): void {
+ if (this._alpha) {
+ this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
+ } else {
+ this._ctx.fillStyle = this._colors.background.css;
+ this._ctx.fillRect(0, 0, this._canvas.width, this._canvas.height);
+ }
+ }
+
+ /**
+ * Clears 1+ cells completely.
+ * @param x The column to start at.
+ * @param y The row to start at.
+ * @param width The number of columns to clear.
+ * @param height The number of rows to clear.
+ */
+ protected _clearCells(x: number, y: number, width: number, height: number): void {
+ if (this._alpha) {
+ this._ctx.clearRect(
+ x * this._scaledCellWidth,
+ y * this._scaledCellHeight,
+ width * this._scaledCellWidth,
+ height * this._scaledCellHeight);
+ } else {
+ this._ctx.fillStyle = this._colors.background.css;
+ this._ctx.fillRect(
+ x * this._scaledCellWidth,
+ y * this._scaledCellHeight,
+ width * this._scaledCellWidth,
+ height * this._scaledCellHeight);
+ }
+ }
+
+ /**
+ * Draws a truecolor character at the cell. The character will be clipped to
+ * ensure that it fits with the cell, including the cell to the right if it's
+ * a wide character. This uses the existing fillStyle on the context.
+ * @param cell The cell data for the character to draw.
+ * @param x The column to draw at.
+ * @param y The row to draw at.
+ * @param color The color of the character.
+ */
+ protected _fillCharTrueColor(cell: CellData, x: number, y: number): void {
+ this._ctx.font = this._getFont(false, false);
+ this._ctx.textBaseline = TEXT_BASELINE;
+ this._clipRow(y);
+
+ // 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);
+ }
+
+ // Draw the character
+ if (!drawSuccess) {
+ this._ctx.fillText(
+ cell.getChars(),
+ x * this._scaledCellWidth + this._scaledCharLeft,
+ y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight);
+ }
+ }
+
+ /**
+ * 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);
+
+ // 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();
+ } else {
+ bg = (cell.isBgDefault()) ? DEFAULT_COLOR : cell.getBgColor();
+ fg = (cell.isFgDefault()) ? DEFAULT_COLOR : cell.getFgColor();
+ }
+
+ 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();
+ const atlasDidDraw = 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 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);
+ }
+
+ // Draw the character
+ if (!drawSuccess) {
+ this._ctx.fillText(
+ cell.getChars(),
+ x * this._scaledCellWidth + this._scaledCharLeft,
+ y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight);
+ }
+
+ 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.
+ */
+ private _clipRow(y: number): void {
+ this._ctx.beginPath();
+ this._ctx.rect(
+ 0,
+ y * this._scaledCellHeight,
+ this._bufferService.cols * this._scaledCellWidth,
+ this._scaledCellHeight);
+ this._ctx.clip();
+ }
+
+ /**
+ * Gets the current font.
+ * @param isBold If we should use the bold fontWeight.
+ */
+ protected _getFont(isBold: boolean, isItalic: boolean): string {
+ const fontWeight = isBold ? this._optionsService.rawOptions.fontWeightBold : this._optionsService.rawOptions.fontWeight;
+ const fontStyle = isItalic ? 'italic' : '';
+
+ return `${fontStyle} ${fontWeight} ${this._optionsService.rawOptions.fontSize * window.devicePixelRatio}px ${this._optionsService.rawOptions.fontFamily}`;
+ }
+
+ private _getContrastColor(cell: CellData): IColor | undefined {
+ if (this._optionsService.rawOptions.minimumContrastRatio === 1) {
+ return undefined;
+ }
+
+ // Try get from cache first
+ 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(bgColorMode, bgColor, isInverse);
+ const fgRgba = this._resolveForegroundRgba(fgColorMode, fgColor, isInverse, isBold);
+ const result = rgba.ensureContrastRatio(bgRgba, fgRgba, this._optionsService.rawOptions.minimumContrastRatio);
+
+ if (!result) {
+ this._colors.contrastCache.setColor(cell.bg, cell.fg, null);
+ return undefined;
+ }
+
+ const color: IColor = {
+ css: channels.toCss(
+ (result >> 24) & 0xFF,
+ (result >> 16) & 0xFF,
+ (result >> 8) & 0xFF
+ ),
+ rgba: result
+ };
+ 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;
+ }
+ }
+}
+
diff --git a/node_modules/xterm/src/browser/renderer/CursorRenderLayer.ts b/node_modules/xterm/src/browser/renderer/CursorRenderLayer.ts
new file mode 100644
index 0000000..ea419cb
--- /dev/null
+++ b/node_modules/xterm/src/browser/renderer/CursorRenderLayer.ts
@@ -0,0 +1,377 @@
+/**
+ * Copyright (c) 2017 The xterm.js authors. All rights reserved.
+ * @license MIT
+ */
+
+import { IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/Types';
+import { BaseRenderLayer } from 'browser/renderer/BaseRenderLayer';
+import { ICellData } from 'common/Types';
+import { CellData } from 'common/buffer/CellData';
+import { IColorSet } from 'browser/Types';
+import { IBufferService, IOptionsService, ICoreService } from 'common/services/Services';
+import { IEventEmitter } from 'common/EventEmitter';
+import { ICoreBrowserService } from 'browser/services/Services';
+
+interface ICursorState {
+ x: number;
+ y: number;
+ isFocused: boolean;
+ style: string;
+ width: number;
+}
+
+/**
+ * The time between cursor blinks.
+ */
+const BLINK_INTERVAL = 600;
+
+export class CursorRenderLayer extends BaseRenderLayer {
+ private _state: ICursorState;
+ private _cursorRenderers: {[key: string]: (x: number, y: number, cell: ICellData) => void};
+ private _cursorBlinkStateManager: CursorBlinkStateManager | undefined;
+ private _cell: ICellData = new CellData();
+
+ constructor(
+ container: HTMLElement,
+ zIndex: number,
+ colors: IColorSet,
+ rendererId: number,
+ private _onRequestRedraw: IEventEmitter<IRequestRedrawEvent>,
+ @IBufferService bufferService: IBufferService,
+ @IOptionsService optionsService: IOptionsService,
+ @ICoreService private readonly _coreService: ICoreService,
+ @ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService
+ ) {
+ super(container, 'cursor', zIndex, true, colors, rendererId, bufferService, optionsService);
+ this._state = {
+ x: 0,
+ y: 0,
+ isFocused: false,
+ style: '',
+ width: 0
+ };
+ this._cursorRenderers = {
+ 'bar': this._renderBarCursor.bind(this),
+ 'block': this._renderBlockCursor.bind(this),
+ 'underline': this._renderUnderlineCursor.bind(this)
+ };
+ }
+
+ public dispose(): void {
+ if (this._cursorBlinkStateManager) {
+ this._cursorBlinkStateManager.dispose();
+ this._cursorBlinkStateManager = undefined;
+ }
+ super.dispose();
+ }
+
+ public resize(dim: IRenderDimensions): void {
+ super.resize(dim);
+ // Resizing the canvas discards the contents of the canvas so clear state
+ this._state = {
+ x: 0,
+ y: 0,
+ isFocused: false,
+ style: '',
+ width: 0
+ };
+ }
+
+ public reset(): void {
+ this._clearCursor();
+ this._cursorBlinkStateManager?.restartBlinkAnimation();
+ this.onOptionsChanged();
+ }
+
+ public onBlur(): void {
+ this._cursorBlinkStateManager?.pause();
+ this._onRequestRedraw.fire({ start: this._bufferService.buffer.y, end: this._bufferService.buffer.y });
+ }
+
+ public onFocus(): void {
+ this._cursorBlinkStateManager?.resume();
+ this._onRequestRedraw.fire({ start: this._bufferService.buffer.y, end: this._bufferService.buffer.y });
+ }
+
+ public onOptionsChanged(): void {
+ if (this._optionsService.rawOptions.cursorBlink) {
+ if (!this._cursorBlinkStateManager) {
+ this._cursorBlinkStateManager = new CursorBlinkStateManager(this._coreBrowserService.isFocused, () => {
+ this._render(true);
+ });
+ }
+ } else {
+ this._cursorBlinkStateManager?.dispose();
+ this._cursorBlinkStateManager = undefined;
+ }
+ // Request a refresh from the terminal as management of rendering is being
+ // moved back to the terminal
+ this._onRequestRedraw.fire({ start: this._bufferService.buffer.y, end: this._bufferService.buffer.y });
+ }
+
+ public onCursorMove(): void {
+ this._cursorBlinkStateManager?.restartBlinkAnimation();
+ }
+
+ public onGridChanged(startRow: number, endRow: number): void {
+ if (!this._cursorBlinkStateManager || this._cursorBlinkStateManager.isPaused) {
+ this._render(false);
+ } else {
+ this._cursorBlinkStateManager.restartBlinkAnimation();
+ }
+ }
+
+ private _render(triggeredByAnimationFrame: boolean): void {
+ // Don't draw the cursor if it's hidden
+ if (!this._coreService.isCursorInitialized || this._coreService.isCursorHidden) {
+ this._clearCursor();
+ return;
+ }
+
+ const cursorY = this._bufferService.buffer.ybase + this._bufferService.buffer.y;
+ const viewportRelativeCursorY = cursorY - this._bufferService.buffer.ydisp;
+
+ // Don't draw the cursor if it's off-screen
+ if (viewportRelativeCursorY < 0 || viewportRelativeCursorY >= this._bufferService.rows) {
+ this._clearCursor();
+ return;
+ }
+
+ // in case cursor.x == cols adjust visual cursor to cols - 1
+ const cursorX = Math.min(this._bufferService.buffer.x, this._bufferService.cols - 1);
+ this._bufferService.buffer.lines.get(cursorY)!.loadCell(cursorX, this._cell);
+ if (this._cell.content === undefined) {
+ return;
+ }
+
+ if (!this._coreBrowserService.isFocused) {
+ this._clearCursor();
+ this._ctx.save();
+ this._ctx.fillStyle = this._colors.cursor.css;
+ const cursorStyle = this._optionsService.rawOptions.cursorStyle;
+ if (cursorStyle && cursorStyle !== 'block') {
+ this._cursorRenderers[cursorStyle](cursorX, viewportRelativeCursorY, this._cell);
+ } else {
+ this._renderBlurCursor(cursorX, viewportRelativeCursorY, this._cell);
+ }
+ this._ctx.restore();
+ this._state.x = cursorX;
+ this._state.y = viewportRelativeCursorY;
+ this._state.isFocused = false;
+ this._state.style = cursorStyle;
+ this._state.width = this._cell.getWidth();
+ return;
+ }
+
+ // Don't draw the cursor if it's blinking
+ if (this._cursorBlinkStateManager && !this._cursorBlinkStateManager.isCursorVisible) {
+ this._clearCursor();
+ return;
+ }
+
+ if (this._state) {
+ // The cursor is already in the correct spot, don't redraw
+ if (this._state.x === cursorX &&
+ this._state.y === viewportRelativeCursorY &&
+ this._state.isFocused === this._coreBrowserService.isFocused &&
+ this._state.style === this._optionsService.rawOptions.cursorStyle &&
+ this._state.width === this._cell.getWidth()) {
+ return;
+ }
+ this._clearCursor();
+ }
+
+ this._ctx.save();
+ this._cursorRenderers[this._optionsService.rawOptions.cursorStyle || 'block'](cursorX, viewportRelativeCursorY, this._cell);
+ this._ctx.restore();
+
+ this._state.x = cursorX;
+ this._state.y = viewportRelativeCursorY;
+ this._state.isFocused = false;
+ this._state.style = this._optionsService.rawOptions.cursorStyle;
+ this._state.width = this._cell.getWidth();
+ }
+
+ private _clearCursor(): void {
+ if (this._state) {
+ // Avoid potential rounding errors when device pixel ratio is less than 1
+ if (window.devicePixelRatio < 1) {
+ this._clearAll();
+ } else {
+ this._clearCells(this._state.x, this._state.y, this._state.width, 1);
+ }
+ this._state = {
+ x: 0,
+ y: 0,
+ isFocused: false,
+ style: '',
+ width: 0
+ };
+ }
+ }
+
+ private _renderBarCursor(x: number, y: number, cell: ICellData): void {
+ this._ctx.save();
+ this._ctx.fillStyle = this._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._fillCells(x, y, cell.getWidth(), 1);
+ this._ctx.fillStyle = this._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._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._strokeRectAtCell(x, y, cell.getWidth(), 1);
+ this._ctx.restore();
+ }
+}
+
+class CursorBlinkStateManager {
+ public isCursorVisible: boolean;
+
+ private _animationFrame: number | undefined;
+ private _blinkStartTimeout: number | undefined;
+ private _blinkInterval: number | undefined;
+
+ /**
+ * The time at which the animation frame was restarted, this is used on the
+ * next render to restart the timers so they don't need to restart the timers
+ * multiple times over a short period.
+ */
+ private _animationTimeRestarted: number | undefined;
+
+ constructor(
+ isFocused: boolean,
+ private _renderCallback: () => void
+ ) {
+ this.isCursorVisible = true;
+ if (isFocused) {
+ this._restartInterval();
+ }
+ }
+
+ public get isPaused(): boolean { return !(this._blinkStartTimeout || this._blinkInterval); }
+
+ public dispose(): void {
+ if (this._blinkInterval) {
+ window.clearInterval(this._blinkInterval);
+ this._blinkInterval = undefined;
+ }
+ if (this._blinkStartTimeout) {
+ window.clearTimeout(this._blinkStartTimeout);
+ this._blinkStartTimeout = undefined;
+ }
+ if (this._animationFrame) {
+ window.cancelAnimationFrame(this._animationFrame);
+ this._animationFrame = undefined;
+ }
+ }
+
+ public restartBlinkAnimation(): void {
+ if (this.isPaused) {
+ return;
+ }
+ // Save a timestamp so that the restart can be done on the next interval
+ this._animationTimeRestarted = Date.now();
+ // Force a cursor render to ensure it's visible and in the correct position
+ this.isCursorVisible = true;
+ if (!this._animationFrame) {
+ this._animationFrame = window.requestAnimationFrame(() => {
+ this._renderCallback();
+ this._animationFrame = undefined;
+ });
+ }
+ }
+
+ private _restartInterval(timeToStart: number = BLINK_INTERVAL): void {
+ // Clear any existing interval
+ if (this._blinkInterval) {
+ window.clearInterval(this._blinkInterval);
+ this._blinkInterval = undefined;
+ }
+
+ // Setup the initial timeout which will hide the cursor, this is done before
+ // the regular interval is setup in order to support restarting the blink
+ // animation in a lightweight way (without thrashing clearInterval and
+ // setInterval).
+ this._blinkStartTimeout = window.setTimeout(() => {
+ // Check if another animation restart was requested while this was being
+ // started
+ if (this._animationTimeRestarted) {
+ const time = BLINK_INTERVAL - (Date.now() - this._animationTimeRestarted);
+ this._animationTimeRestarted = undefined;
+ if (time > 0) {
+ this._restartInterval(time);
+ return;
+ }
+ }
+
+ // Hide the cursor
+ this.isCursorVisible = false;
+ this._animationFrame = window.requestAnimationFrame(() => {
+ this._renderCallback();
+ this._animationFrame = undefined;
+ });
+
+ // Setup the blink interval
+ this._blinkInterval = window.setInterval(() => {
+ // Adjust the animation time if it was restarted
+ if (this._animationTimeRestarted) {
+ // calc time diff
+ // Make restart interval do a setTimeout initially?
+ const time = BLINK_INTERVAL - (Date.now() - this._animationTimeRestarted);
+ this._animationTimeRestarted = undefined;
+ this._restartInterval(time);
+ return;
+ }
+
+ // Invert visibility and render
+ this.isCursorVisible = !this.isCursorVisible;
+ this._animationFrame = window.requestAnimationFrame(() => {
+ this._renderCallback();
+ this._animationFrame = undefined;
+ });
+ }, BLINK_INTERVAL);
+ }, timeToStart);
+ }
+
+ public pause(): void {
+ this.isCursorVisible = true;
+ if (this._blinkInterval) {
+ window.clearInterval(this._blinkInterval);
+ this._blinkInterval = undefined;
+ }
+ if (this._blinkStartTimeout) {
+ window.clearTimeout(this._blinkStartTimeout);
+ this._blinkStartTimeout = undefined;
+ }
+ if (this._animationFrame) {
+ window.cancelAnimationFrame(this._animationFrame);
+ this._animationFrame = undefined;
+ }
+ }
+
+ public resume(): void {
+ // Clear out any existing timers just in case
+ this.pause();
+
+ this._animationTimeRestarted = undefined;
+ this._restartInterval();
+ this.restartBlinkAnimation();
+ }
+}
diff --git a/node_modules/xterm/src/browser/renderer/CustomGlyphs.ts b/node_modules/xterm/src/browser/renderer/CustomGlyphs.ts
new file mode 100644
index 0000000..7756279
--- /dev/null
+++ b/node_modules/xterm/src/browser/renderer/CustomGlyphs.ts
@@ -0,0 +1,563 @@
+/**
+ * Copyright (c) 2021 The xterm.js authors. All rights reserved.
+ * @license MIT
+ */
+
+import { throwIfFalsy } from 'browser/renderer/RendererUtils';
+
+interface IBlockVector {
+ x: number;
+ y: number;
+ w: number;
+ h: number;
+}
+
+export const blockElementDefinitions: { [index: string]: IBlockVector[] | undefined } = {
+ // Block elements (0x2580-0x2590)
+ '▀': [{ x: 0, y: 0, w: 8, h: 4 }], // UPPER HALF BLOCK
+ '▁': [{ x: 0, y: 7, w: 8, h: 1 }], // LOWER ONE EIGHTH BLOCK
+ '▂': [{ x: 0, y: 6, w: 8, h: 2 }], // LOWER ONE QUARTER BLOCK
+ '▃': [{ x: 0, y: 5, w: 8, h: 3 }], // LOWER THREE EIGHTHS BLOCK
+ '▄': [{ x: 0, y: 4, w: 8, h: 4 }], // LOWER HALF BLOCK
+ '▅': [{ x: 0, y: 3, w: 8, h: 5 }], // LOWER FIVE EIGHTHS BLOCK
+ '▆': [{ x: 0, y: 2, w: 8, h: 6 }], // LOWER THREE QUARTERS BLOCK
+ '▇': [{ x: 0, y: 1, w: 8, h: 7 }], // LOWER SEVEN EIGHTHS BLOCK
+ '█': [{ x: 0, y: 0, w: 8, h: 8 }], // FULL BLOCK
+ '▉': [{ x: 0, y: 0, w: 7, h: 8 }], // LEFT SEVEN EIGHTHS BLOCK
+ '▊': [{ x: 0, y: 0, w: 6, h: 8 }], // LEFT THREE QUARTERS BLOCK
+ '▋': [{ x: 0, y: 0, w: 5, h: 8 }], // LEFT FIVE EIGHTHS BLOCK
+ '▌': [{ x: 0, y: 0, w: 4, h: 8 }], // LEFT HALF BLOCK
+ '▍': [{ x: 0, y: 0, w: 3, h: 8 }], // LEFT THREE EIGHTHS BLOCK
+ '▎': [{ x: 0, y: 0, w: 2, h: 8 }], // LEFT ONE QUARTER BLOCK
+ '▏': [{ x: 0, y: 0, w: 1, h: 8 }], // LEFT ONE EIGHTH BLOCK
+ '▐': [{ x: 4, y: 0, w: 4, h: 8 }], // RIGHT HALF BLOCK
+
+ // Block elements (0x2594-0x2595)
+ '▔': [{ x: 0, y: 0, w: 9, h: 1 }], // UPPER ONE EIGHTH BLOCK
+ '▕': [{ x: 7, y: 0, w: 1, h: 8 }], // RIGHT ONE EIGHTH BLOCK
+
+ // Terminal graphic characters (0x2596-0x259F)
+ '▖': [{ x: 0, y: 4, w: 4, h: 4 }], // QUADRANT LOWER LEFT
+ '▗': [{ x: 4, y: 4, w: 4, h: 4 }], // QUADRANT LOWER RIGHT
+ '▘': [{ x: 0, y: 0, w: 4, h: 4 }], // QUADRANT UPPER LEFT
+ '▙': [{ x: 0, y: 0, w: 4, h: 8 }, { x: 0, y: 4, w: 8, h: 4 }], // QUADRANT UPPER LEFT AND LOWER LEFT AND LOWER RIGHT
+ '▚': [{ x: 0, y: 0, w: 4, h: 4 }, { x: 4, y: 4, w: 4, h: 4 }], // QUADRANT UPPER LEFT AND LOWER RIGHT
+ '▛': [{ x: 0, y: 0, w: 4, h: 8 }, { x: 0, y: 0, w: 4, h: 8 }], // QUADRANT UPPER LEFT AND UPPER RIGHT AND LOWER LEFT
+ '▜': [{ x: 0, y: 0, w: 8, h: 4 }, { x: 4, y: 0, w: 4, h: 8 }], // QUADRANT UPPER LEFT AND UPPER RIGHT AND LOWER RIGHT
+ '▝': [{ x: 4, y: 0, w: 4, h: 4 }], // QUADRANT UPPER RIGHT
+ '▞': [{ x: 4, y: 0, w: 4, h: 4 }, { x: 0, y: 4, w: 4, h: 4 }], // QUADRANT UPPER RIGHT AND LOWER LEFT
+ '▟': [{ x: 4, y: 0, w: 4, h: 8 }, { x: 0, y: 4, w: 8, h: 4 }], // QUADRANT UPPER RIGHT AND LOWER LEFT AND LOWER RIGHT
+
+ // VERTICAL ONE EIGHTH BLOCK-2 through VERTICAL ONE EIGHTH BLOCK-7
+ '\u{1FB70}': [{ x: 1, y: 0, w: 1, h: 8 }],
+ '\u{1FB71}': [{ x: 2, y: 0, w: 1, h: 8 }],
+ '\u{1FB72}': [{ x: 3, y: 0, w: 1, h: 8 }],
+ '\u{1FB73}': [{ x: 4, y: 0, w: 1, h: 8 }],
+ '\u{1FB74}': [{ x: 5, y: 0, w: 1, h: 8 }],
+ '\u{1FB75}': [{ x: 6, y: 0, w: 1, h: 8 }],
+
+ // HORIZONTAL ONE EIGHTH BLOCK-2 through HORIZONTAL ONE EIGHTH BLOCK-7
+ '\u{1FB76}': [{ x: 0, y: 1, w: 8, h: 1 }],
+ '\u{1FB77}': [{ x: 0, y: 2, w: 8, h: 1 }],
+ '\u{1FB78}': [{ x: 0, y: 3, w: 8, h: 1 }],
+ '\u{1FB79}': [{ x: 0, y: 4, w: 8, h: 1 }],
+ '\u{1FB7A}': [{ x: 0, y: 5, w: 8, h: 1 }],
+ '\u{1FB7B}': [{ x: 0, y: 6, w: 8, h: 1 }],
+
+ // LEFT AND LOWER ONE EIGHTH BLOCK
+ '\u{1FB7C}': [{ x: 0, y: 0, w: 1, h: 8 }, { x: 0, y: 7, w: 8, h: 1 }],
+ // LEFT AND UPPER ONE EIGHTH BLOCK
+ '\u{1FB7D}': [{ x: 0, y: 0, w: 1, h: 8 }, { x: 0, y: 0, w: 8, h: 1 }],
+ // RIGHT AND UPPER ONE EIGHTH BLOCK
+ '\u{1FB7E}': [{ x: 7, y: 0, w: 1, h: 8 }, { x: 0, y: 0, w: 8, h: 1 }],
+ // RIGHT AND LOWER ONE EIGHTH BLOCK
+ '\u{1FB7F}': [{ x: 7, y: 0, w: 1, h: 8 }, { x: 0, y: 7, w: 8, h: 1 }],
+ // UPPER AND LOWER ONE EIGHTH BLOCK
+ '\u{1FB80}': [{ x: 0, y: 0, w: 8, h: 1 }, { x: 0, y: 7, w: 8, h: 1 }],
+ // HORIZONTAL ONE EIGHTH BLOCK-1358
+ '\u{1FB81}': [{ x: 0, y: 0, w: 8, h: 1 }, { x: 0, y: 2, w: 8, h: 1 }, { x: 0, y: 4, w: 8, h: 1 }, { x: 0, y: 7, w: 8, h: 1 }],
+
+ // UPPER ONE QUARTER BLOCK
+ '\u{1FB82}': [{ x: 0, y: 0, w: 8, h: 2 }],
+ // UPPER THREE EIGHTHS BLOCK
+ '\u{1FB83}': [{ x: 0, y: 0, w: 8, h: 3 }],
+ // UPPER FIVE EIGHTHS BLOCK
+ '\u{1FB84}': [{ x: 0, y: 0, w: 8, h: 5 }],
+ // UPPER THREE QUARTERS BLOCK
+ '\u{1FB85}': [{ x: 0, y: 0, w: 8, h: 6 }],
+ // UPPER SEVEN EIGHTHS BLOCK
+ '\u{1FB86}': [{ x: 0, y: 0, w: 8, h: 7 }],
+
+ // RIGHT ONE QUARTER BLOCK
+ '\u{1FB87}': [{ x: 6, y: 0, w: 2, h: 8 }],
+ // RIGHT THREE EIGHTHS B0OCK
+ '\u{1FB88}': [{ x: 5, y: 0, w: 3, h: 8 }],
+ // RIGHT FIVE EIGHTHS BL0CK
+ '\u{1FB89}': [{ x: 3, y: 0, w: 5, h: 8 }],
+ // RIGHT THREE QUARTERS 0LOCK
+ '\u{1FB8A}': [{ x: 2, y: 0, w: 6, h: 8 }],
+ // RIGHT SEVEN EIGHTHS B0OCK
+ '\u{1FB8B}': [{ x: 1, y: 0, w: 7, h: 8 }],
+
+ // CHECKER BOARD FILL
+ '\u{1FB95}': [
+ { x: 0, y: 0, w: 2, h: 2 }, { x: 4, y: 0, w: 2, h: 2 },
+ { x: 2, y: 2, w: 2, h: 2 }, { x: 6, y: 2, w: 2, h: 2 },
+ { x: 0, y: 4, w: 2, h: 2 }, { x: 4, y: 4, w: 2, h: 2 },
+ { x: 2, y: 6, w: 2, h: 2 }, { x: 6, y: 6, w: 2, h: 2 }
+ ],
+ // INVERSE CHECKER BOARD FILL
+ '\u{1FB96}': [
+ { x: 2, y: 0, w: 2, h: 2 }, { x: 6, y: 0, w: 2, h: 2 },
+ { x: 0, y: 2, w: 2, h: 2 }, { x: 4, y: 2, w: 2, h: 2 },
+ { x: 2, y: 4, w: 2, h: 2 }, { x: 6, y: 4, w: 2, h: 2 },
+ { x: 0, y: 6, w: 2, h: 2 }, { x: 4, y: 6, w: 2, h: 2 }
+ ],
+ // HEAVY HORIZONTAL FILL (upper middle and lower one quarter block)
+ '\u{1FB97}': [{ x: 0, y: 2, w: 8, h: 2 }, { x: 0, y: 6, w: 8, h: 2 }]
+};
+
+type PatternDefinition = number[][];
+
+/**
+ * Defines the repeating pattern used by special characters, the pattern is made up of a 2d array of
+ * pixel values to be filled (1) or not filled (0).
+ */
+const patternCharacterDefinitions: { [key: string]: PatternDefinition | undefined } = {
+ // Shade characters (0x2591-0x2593)
+ '░': [ // LIGHT SHADE (25%)
+ [1, 0, 0, 0],
+ [0, 0, 0, 0],
+ [0, 0, 1, 0],
+ [0, 0, 0, 0]
+ ],
+ '▒': [ // MEDIUM SHADE (50%)
+ [1, 0],
+ [0, 0],
+ [0, 1],
+ [0, 0]
+ ],
+ '▓': [ // DARK SHADE (75%)
+ [0, 1],
+ [1, 1],
+ [1, 0],
+ [1, 1]
+ ]
+};
+
+const enum Shapes {
+ /** │ */ TOP_TO_BOTTOM = 'M.5,0 L.5,1',
+ /** ─ */ LEFT_TO_RIGHT = 'M0,.5 L1,.5',
+
+ /** └ */ TOP_TO_RIGHT = 'M.5,0 L.5,.5 L1,.5',
+ /** ┘ */ TOP_TO_LEFT = 'M.5,0 L.5,.5 L0,.5',
+ /** ┐ */ LEFT_TO_BOTTOM = 'M0,.5 L.5,.5 L.5,1',
+ /** ┌ */ RIGHT_TO_BOTTOM = 'M0.5,1 L.5,.5 L1,.5',
+
+ /** ╵ */ MIDDLE_TO_TOP = 'M.5,.5 L.5,0',
+ /** ╴ */ MIDDLE_TO_LEFT = 'M.5,.5 L0,.5',
+ /** ╶ */ MIDDLE_TO_RIGHT = 'M.5,.5 L1,.5',
+ /** ╷ */ MIDDLE_TO_BOTTOM = 'M.5,.5 L.5,1',
+
+ /** ┴ */ T_TOP = 'M0,.5 L1,.5 M.5,.5 L.5,0',
+ /** ┤ */ T_LEFT = 'M.5,0 L.5,1 M.5,.5 L0,.5',
+ /** ├ */ T_RIGHT = 'M.5,0 L.5,1 M.5,.5 L1,.5',
+ /** ┬ */ T_BOTTOM = 'M0,.5 L1,.5 M.5,.5 L.5,1',
+
+ /** ┼ */ CROSS = 'M0,.5 L1,.5 M.5,0 L.5,1',
+
+ /** ╌ */ TWO_DASHES_HORIZONTAL = 'M.1,.5 L.4,.5 M.6,.5 L.9,.5', // .2 empty, .3 filled
+ /** ┄ */ THREE_DASHES_HORIZONTAL = 'M.0667,.5 L.2667,.5 M.4,.5 L.6,.5 M.7333,.5 L.9333,.5', // .1333 empty, .2 filled
+ /** ┉ */ FOUR_DASHES_HORIZONTAL = 'M.05,.5 L.2,.5 M.3,.5 L.45,.5 M.55,.5 L.7,.5 M.8,.5 L.95,.5', // .1 empty, .15 filled
+ /** ╎ */ TWO_DASHES_VERTICAL = 'M.5,.1 L.5,.4 M.5,.6 L.5,.9',
+ /** ┆ */ THREE_DASHES_VERTICAL = 'M.5,.0667 L.5,.2667 M.5,.4 L.5,.6 M.5,.7333 L.5,.9333',
+ /** ┊ */ FOUR_DASHES_VERTICAL = 'M.5,.05 L.5,.2 M.5,.3 L.5,.45 L.5,.55 M.5,.7 L.5,.95',
+}
+
+const enum Style {
+ NORMAL = 1,
+ BOLD = 3
+}
+
+/**
+ * This contains the definitions of all box drawing characters in the format of SVG paths (ie. the
+ * svg d attribute).
+ */
+export const boxDrawingDefinitions: { [character: string]: { [fontWeight: number]: string | ((xp: number, yp: number) => string) } | undefined } = {
+ // Uniform normal and bold
+ '─': { [Style.NORMAL]: Shapes.LEFT_TO_RIGHT },
+ '━': { [Style.BOLD]: Shapes.LEFT_TO_RIGHT },
+ '│': { [Style.NORMAL]: Shapes.TOP_TO_BOTTOM },
+ '┃': { [Style.BOLD]: Shapes.TOP_TO_BOTTOM },
+ '┌': { [Style.NORMAL]: Shapes.RIGHT_TO_BOTTOM },
+ '┏': { [Style.BOLD]: Shapes.RIGHT_TO_BOTTOM },
+ '┐': { [Style.NORMAL]: Shapes.LEFT_TO_BOTTOM },
+ '┓': { [Style.BOLD]: Shapes.LEFT_TO_BOTTOM },
+ '└': { [Style.NORMAL]: Shapes.TOP_TO_RIGHT },
+ '┗': { [Style.BOLD]: Shapes.TOP_TO_RIGHT },
+ '┘': { [Style.NORMAL]: Shapes.TOP_TO_LEFT },
+ '┛': { [Style.BOLD]: Shapes.TOP_TO_LEFT },
+ '├': { [Style.NORMAL]: Shapes.T_RIGHT },
+ '┣': { [Style.BOLD]: Shapes.T_RIGHT },
+ '┤': { [Style.NORMAL]: Shapes.T_LEFT },
+ '┫': { [Style.BOLD]: Shapes.T_LEFT },
+ '┬': { [Style.NORMAL]: Shapes.T_BOTTOM },
+ '┳': { [Style.BOLD]: Shapes.T_BOTTOM },
+ '┴': { [Style.NORMAL]: Shapes.T_TOP },
+ '┻': { [Style.BOLD]: Shapes.T_TOP },
+ '┼': { [Style.NORMAL]: Shapes.CROSS },
+ '╋': { [Style.BOLD]: Shapes.CROSS },
+ '╴': { [Style.NORMAL]: Shapes.MIDDLE_TO_LEFT },
+ '╸': { [Style.BOLD]: Shapes.MIDDLE_TO_LEFT },
+ '╵': { [Style.NORMAL]: Shapes.MIDDLE_TO_TOP },
+ '╹': { [Style.BOLD]: Shapes.MIDDLE_TO_TOP },
+ '╶': { [Style.NORMAL]: Shapes.MIDDLE_TO_RIGHT },
+ '╺': { [Style.BOLD]: Shapes.MIDDLE_TO_RIGHT },
+ '╷': { [Style.NORMAL]: Shapes.MIDDLE_TO_BOTTOM },
+ '╻': { [Style.BOLD]: Shapes.MIDDLE_TO_BOTTOM },
+
+ // Double border
+ '═': { [Style.NORMAL]: (xp, yp) => `M0,${.5 - yp} L1,${.5 - yp} M0,${.5 + yp} L1,${.5 + yp}` },
+ '║': { [Style.NORMAL]: (xp, yp) => `M${.5 - xp},0 L${.5 - xp},1 M${.5 + xp},0 L${.5 + xp},1` },
+ '╒': { [Style.NORMAL]: (xp, yp) => `M.5,1 L.5,${.5 - yp} L1,${.5 - yp} M.5,${.5 + yp} L1,${.5 + yp}` },
+ '╓': { [Style.NORMAL]: (xp, yp) => `M${.5 - xp},1 L${.5 - xp},.5 L1,.5 M${.5 + xp},.5 L${.5 + xp},1` },
+ '╔': { [Style.NORMAL]: (xp, yp) => `M1,${.5 - yp} L${.5 - xp},${.5 - yp} L${.5 - xp},1 M1,${.5 + yp} L${.5 + xp},${.5 + yp} L${.5 + xp},1` },
+ '╕': { [Style.NORMAL]: (xp, yp) => `M0,${.5 - yp} L.5,${.5 - yp} L.5,1 M0,${.5 + yp} L.5,${.5 + yp}` },
+ '╖': { [Style.NORMAL]: (xp, yp) => `M${.5 + xp},1 L${.5 + xp},.5 L0,.5 M${.5 - xp},.5 L${.5 - xp},1` },
+ '╗': { [Style.NORMAL]: (xp, yp) => `M0,${.5 + yp} L${.5 - xp},${.5 + yp} L${.5 - xp},1 M0,${.5 - yp} L${.5 + xp},${.5 - yp} L${.5 + xp},1` },
+ '╘': { [Style.NORMAL]: (xp, yp) => `M.5,0 L.5,${.5 + yp} L1,${.5 + yp} M.5,${.5 - yp} L1,${.5 - yp}` },
+ '╙': { [Style.NORMAL]: (xp, yp) => `M1,.5 L${.5 - xp},.5 L${.5 - xp},0 M${.5 + xp},.5 L${.5 + xp},0` },
+ '╚': { [Style.NORMAL]: (xp, yp) => `M1,${.5 - yp} L${.5 + xp},${.5 - yp} L${.5 + xp},0 M1,${.5 + yp} L${.5 - xp},${.5 + yp} L${.5 - xp},0` },
+ '╛': { [Style.NORMAL]: (xp, yp) => `M0,${.5 + yp} L.5,${.5 + yp} L.5,0 M0,${.5 - yp} L.5,${.5 - yp}` },
+ '╜': { [Style.NORMAL]: (xp, yp) => `M0,.5 L${.5 + xp},.5 L${.5 + xp},0 M${.5 - xp},.5 L${.5 - xp},0` },
+ '╝': { [Style.NORMAL]: (xp, yp) => `M0,${.5 - yp} L${.5 - xp},${.5 - yp} L${.5 - xp},0 M0,${.5 + yp} L${.5 + xp},${.5 + yp} L${.5 + xp},0` },
+ '╞': { [Style.NORMAL]: (xp, yp) => `${Shapes.TOP_TO_BOTTOM} M.5,${.5 - yp} L1,${.5 - yp} M.5,${.5 + yp} L1,${.5 + yp}` },
+ '╟': { [Style.NORMAL]: (xp, yp) => `M${.5 - xp},0 L${.5 - xp},1 M${.5 + xp},0 L${.5 + xp},1 M${.5 + xp},.5 L1,.5` },
+ '╠': { [Style.NORMAL]: (xp, yp) => `M${.5 - xp},0 L${.5 - xp},1 M1,${.5 + yp} L${.5 + xp},${.5 + yp} L${.5 + xp},1 M1,${.5 - yp} L${.5 + xp},${.5 - yp} L${.5 + xp},0` },
+ '╡': { [Style.NORMAL]: (xp, yp) => `${Shapes.TOP_TO_BOTTOM} M0,${.5 - yp} L.5,${.5 - yp} M0,${.5 + yp} L.5,${.5 + yp}` },
+ '╢': { [Style.NORMAL]: (xp, yp) => `M0,.5 L${.5 - xp},.5 M${.5 - xp},0 L${.5 - xp},1 M${.5 + xp},0 L${.5 + xp},1` },
+ '╣': { [Style.NORMAL]: (xp, yp) => `M${.5 + xp},0 L${.5 + xp},1 M0,${.5 + yp} L${.5 - xp},${.5 + yp} L${.5 - xp},1 M0,${.5 - yp} L${.5 - xp},${.5 - yp} L${.5 - xp},0` },
+ '╤': { [Style.NORMAL]: (xp, yp) => `M0,${.5 - yp} L1,${.5 - yp} M0,${.5 + yp} L1,${.5 + yp} M.5,${.5 + yp} L.5,1` },
+ '╥': { [Style.NORMAL]: (xp, yp) => `${Shapes.LEFT_TO_RIGHT} M${.5 - xp},.5 L${.5 - xp},1 M${.5 + xp},.5 L${.5 + xp},1` },
+ '╦': { [Style.NORMAL]: (xp, yp) => `M0,${.5 - yp} L1,${.5 - yp} M0,${.5 + yp} L${.5 - xp},${.5 + yp} L${.5 - xp},1 M1,${.5 + yp} L${.5 + xp},${.5 + yp} L${.5 + xp},1` },
+ '╧': { [Style.NORMAL]: (xp, yp) => `M.5,0 L.5,${.5 - yp} M0,${.5 - yp} L1,${.5 - yp} M0,${.5 + yp} L1,${.5 + yp}` },
+ '╨': { [Style.NORMAL]: (xp, yp) => `${Shapes.LEFT_TO_RIGHT} M${.5 - xp},.5 L${.5 - xp},0 M${.5 + xp},.5 L${.5 + xp},0` },
+ '╩': { [Style.NORMAL]: (xp, yp) => `M0,${.5 + yp} L1,${.5 + yp} M0,${.5 - yp} L${.5 - xp},${.5 - yp} L${.5 - xp},0 M1,${.5 - yp} L${.5 + xp},${.5 - yp} L${.5 + xp},0` },
+ '╪': { [Style.NORMAL]: (xp, yp) => `${Shapes.TOP_TO_BOTTOM} M0,${.5 - yp} L1,${.5 - yp} M0,${.5 + yp} L1,${.5 + yp}` },
+ '╫': { [Style.NORMAL]: (xp, yp) => `${Shapes.LEFT_TO_RIGHT} M${.5 - xp},0 L${.5 - xp},1 M${.5 + xp},0 L${.5 + xp},1` },
+ '╬': { [Style.NORMAL]: (xp, yp) => `M0,${.5 + yp} L${.5 - xp},${.5 + yp} L${.5 - xp},1 M1,${.5 + yp} L${.5 + xp},${.5 + yp} L${.5 + xp},1 M0,${.5 - yp} L${.5 - xp},${.5 - yp} L${.5 - xp},0 M1,${.5 - yp} L${.5 + xp},${.5 - yp} L${.5 + xp},0` },
+
+ // Diagonal
+ '╱': { [Style.NORMAL]: 'M1,0 L0,1' },
+ '╲': { [Style.NORMAL]: 'M0,0 L1,1' },
+ '╳': { [Style.NORMAL]: 'M1,0 L0,1 M0,0 L1,1' },
+
+ // Mixed weight
+ '╼': { [Style.NORMAL]: Shapes.MIDDLE_TO_LEFT, [Style.BOLD]: Shapes.MIDDLE_TO_RIGHT },
+ '╽': { [Style.NORMAL]: Shapes.MIDDLE_TO_TOP, [Style.BOLD]: Shapes.MIDDLE_TO_BOTTOM },
+ '╾': { [Style.NORMAL]: Shapes.MIDDLE_TO_RIGHT, [Style.BOLD]: Shapes.MIDDLE_TO_LEFT },
+ '╿': { [Style.NORMAL]: Shapes.MIDDLE_TO_BOTTOM, [Style.BOLD]: Shapes.MIDDLE_TO_TOP },
+ '┍': { [Style.NORMAL]: Shapes.MIDDLE_TO_BOTTOM, [Style.BOLD]: Shapes.MIDDLE_TO_RIGHT },
+ '┎': { [Style.NORMAL]: Shapes.MIDDLE_TO_RIGHT, [Style.BOLD]: Shapes.MIDDLE_TO_BOTTOM },
+ '┑': { [Style.NORMAL]: Shapes.MIDDLE_TO_BOTTOM, [Style.BOLD]: Shapes.MIDDLE_TO_LEFT },
+ '┒': { [Style.NORMAL]: Shapes.MIDDLE_TO_LEFT, [Style.BOLD]: Shapes.MIDDLE_TO_BOTTOM },
+ '┕': { [Style.NORMAL]: Shapes.MIDDLE_TO_TOP, [Style.BOLD]: Shapes.MIDDLE_TO_RIGHT },
+ '┖': { [Style.NORMAL]: Shapes.MIDDLE_TO_RIGHT, [Style.BOLD]: Shapes.MIDDLE_TO_TOP },
+ '┙': { [Style.NORMAL]: Shapes.MIDDLE_TO_TOP, [Style.BOLD]: Shapes.MIDDLE_TO_LEFT },
+ '┚': { [Style.NORMAL]: Shapes.MIDDLE_TO_LEFT, [Style.BOLD]: Shapes.MIDDLE_TO_TOP },
+ '┝': { [Style.NORMAL]: Shapes.TOP_TO_BOTTOM, [Style.BOLD]: Shapes.MIDDLE_TO_RIGHT },
+ '┞': { [Style.NORMAL]: Shapes.RIGHT_TO_BOTTOM, [Style.BOLD]: Shapes.MIDDLE_TO_TOP },
+ '┟': { [Style.NORMAL]: Shapes.TOP_TO_RIGHT, [Style.BOLD]: Shapes.MIDDLE_TO_BOTTOM },
+ '┠': { [Style.NORMAL]: Shapes.MIDDLE_TO_RIGHT, [Style.BOLD]: Shapes.TOP_TO_BOTTOM },
+ '┡': { [Style.NORMAL]: Shapes.MIDDLE_TO_BOTTOM, [Style.BOLD]: Shapes.TOP_TO_RIGHT },
+ '┢': { [Style.NORMAL]: Shapes.MIDDLE_TO_TOP, [Style.BOLD]: Shapes.RIGHT_TO_BOTTOM },
+ '┥': { [Style.NORMAL]: Shapes.TOP_TO_BOTTOM, [Style.BOLD]: Shapes.MIDDLE_TO_LEFT },
+ '┦': { [Style.NORMAL]: Shapes.LEFT_TO_BOTTOM, [Style.BOLD]: Shapes.MIDDLE_TO_TOP },
+ '┧': { [Style.NORMAL]: Shapes.TOP_TO_LEFT, [Style.BOLD]: Shapes.MIDDLE_TO_BOTTOM },
+ '┨': { [Style.NORMAL]: Shapes.MIDDLE_TO_LEFT, [Style.BOLD]: Shapes.TOP_TO_BOTTOM },
+ '┩': { [Style.NORMAL]: Shapes.MIDDLE_TO_BOTTOM, [Style.BOLD]: Shapes.TOP_TO_LEFT },
+ '┪': { [Style.NORMAL]: Shapes.MIDDLE_TO_TOP, [Style.BOLD]: Shapes.LEFT_TO_BOTTOM },
+ '┭': { [Style.NORMAL]: Shapes.RIGHT_TO_BOTTOM, [Style.BOLD]: Shapes.MIDDLE_TO_LEFT },
+ '┮': { [Style.NORMAL]: Shapes.LEFT_TO_BOTTOM, [Style.BOLD]: Shapes.MIDDLE_TO_RIGHT },
+ '┯': { [Style.NORMAL]: Shapes.MIDDLE_TO_BOTTOM, [Style.BOLD]: Shapes.LEFT_TO_RIGHT },
+ '┰': { [Style.NORMAL]: Shapes.LEFT_TO_RIGHT, [Style.BOLD]: Shapes.MIDDLE_TO_BOTTOM },
+ '┱': { [Style.NORMAL]: Shapes.MIDDLE_TO_RIGHT, [Style.BOLD]: Shapes.LEFT_TO_BOTTOM },
+ '┲': { [Style.NORMAL]: Shapes.MIDDLE_TO_LEFT, [Style.BOLD]: Shapes.RIGHT_TO_BOTTOM },
+ '┵': { [Style.NORMAL]: Shapes.TOP_TO_RIGHT, [Style.BOLD]: Shapes.MIDDLE_TO_LEFT },
+ '┶': { [Style.NORMAL]: Shapes.TOP_TO_LEFT, [Style.BOLD]: Shapes.MIDDLE_TO_RIGHT },
+ '┷': { [Style.NORMAL]: Shapes.MIDDLE_TO_TOP, [Style.BOLD]: Shapes.LEFT_TO_RIGHT },
+ '┸': { [Style.NORMAL]: Shapes.LEFT_TO_RIGHT, [Style.BOLD]: Shapes.MIDDLE_TO_TOP },
+ '┹': { [Style.NORMAL]: Shapes.MIDDLE_TO_RIGHT, [Style.BOLD]: Shapes.TOP_TO_LEFT },
+ '┺': { [Style.NORMAL]: Shapes.MIDDLE_TO_LEFT, [Style.BOLD]: Shapes.TOP_TO_RIGHT },
+ '┽': { [Style.NORMAL]: `${Shapes.TOP_TO_BOTTOM} ${Shapes.MIDDLE_TO_RIGHT}`, [Style.BOLD]: Shapes.MIDDLE_TO_LEFT },
+ '┾': { [Style.NORMAL]: `${Shapes.TOP_TO_BOTTOM} ${Shapes.MIDDLE_TO_LEFT}`, [Style.BOLD]: Shapes.MIDDLE_TO_RIGHT },
+ '┿': { [Style.NORMAL]: Shapes.TOP_TO_BOTTOM, [Style.BOLD]: Shapes.LEFT_TO_RIGHT },
+ '╀': { [Style.NORMAL]: `${Shapes.LEFT_TO_RIGHT} ${Shapes.MIDDLE_TO_BOTTOM}`, [Style.BOLD]: Shapes.MIDDLE_TO_TOP },
+ '╁': { [Style.NORMAL]: `${Shapes.MIDDLE_TO_TOP} ${Shapes.LEFT_TO_RIGHT}`, [Style.BOLD]: Shapes.MIDDLE_TO_BOTTOM },
+ '╂': { [Style.NORMAL]: Shapes.LEFT_TO_RIGHT, [Style.BOLD]: Shapes.TOP_TO_BOTTOM },
+ '╃': { [Style.NORMAL]: Shapes.RIGHT_TO_BOTTOM, [Style.BOLD]: Shapes.TOP_TO_LEFT },
+ '╄': { [Style.NORMAL]: Shapes.LEFT_TO_BOTTOM, [Style.BOLD]: Shapes.TOP_TO_RIGHT },
+ '╅': { [Style.NORMAL]: Shapes.TOP_TO_RIGHT, [Style.BOLD]: Shapes.LEFT_TO_BOTTOM },
+ '╆': { [Style.NORMAL]: Shapes.TOP_TO_LEFT, [Style.BOLD]: Shapes.RIGHT_TO_BOTTOM },
+ '╇': { [Style.NORMAL]: Shapes.MIDDLE_TO_BOTTOM, [Style.BOLD]: `${Shapes.MIDDLE_TO_TOP} ${Shapes.LEFT_TO_RIGHT}` },
+ '╈': { [Style.NORMAL]: Shapes.MIDDLE_TO_TOP, [Style.BOLD]: `${Shapes.LEFT_TO_RIGHT} ${Shapes.MIDDLE_TO_BOTTOM}` },
+ '╉': { [Style.NORMAL]: Shapes.MIDDLE_TO_RIGHT, [Style.BOLD]: `${Shapes.TOP_TO_BOTTOM} ${Shapes.MIDDLE_TO_LEFT}` },
+ '╊': { [Style.NORMAL]: Shapes.MIDDLE_TO_LEFT, [Style.BOLD]: `${Shapes.TOP_TO_BOTTOM} ${Shapes.MIDDLE_TO_RIGHT}` },
+
+ // Dashed
+ '╌': { [Style.NORMAL]: Shapes.TWO_DASHES_HORIZONTAL },
+ '╍': { [Style.BOLD]: Shapes.TWO_DASHES_HORIZONTAL },
+ '┄': { [Style.NORMAL]: Shapes.THREE_DASHES_HORIZONTAL },
+ '┅': { [Style.BOLD]: Shapes.THREE_DASHES_HORIZONTAL },
+ '┈': { [Style.NORMAL]: Shapes.FOUR_DASHES_HORIZONTAL },
+ '┉': { [Style.BOLD]: Shapes.FOUR_DASHES_HORIZONTAL },
+ '╎': { [Style.NORMAL]: Shapes.TWO_DASHES_VERTICAL },
+ '╏': { [Style.BOLD]: Shapes.TWO_DASHES_VERTICAL },
+ '┆': { [Style.NORMAL]: Shapes.THREE_DASHES_VERTICAL },
+ '┇': { [Style.BOLD]: Shapes.THREE_DASHES_VERTICAL },
+ '┊': { [Style.NORMAL]: Shapes.FOUR_DASHES_VERTICAL },
+ '┋': { [Style.BOLD]: Shapes.FOUR_DASHES_VERTICAL },
+
+ // Curved
+ '╭': { [Style.NORMAL]: 'C.5,1,.5,.5,1,.5' },
+ '╮': { [Style.NORMAL]: 'C.5,1,.5,.5,0,.5' },
+ '╯': { [Style.NORMAL]: 'C.5,0,.5,.5,0,.5' },
+ '╰': { [Style.NORMAL]: 'C.5,0,.5,.5,1,.5' }
+};
+
+/**
+ * Try drawing a custom block element or box drawing character, returning whether it was
+ * successfully drawn.
+ */
+export function tryDrawCustomChar(
+ ctx: CanvasRenderingContext2D,
+ c: string,
+ xOffset: number,
+ yOffset: number,
+ scaledCellWidth: number,
+ scaledCellHeight: number
+): boolean {
+ const blockElementDefinition = blockElementDefinitions[c];
+ if (blockElementDefinition) {
+ drawBlockElementChar(ctx, blockElementDefinition, xOffset, yOffset, scaledCellWidth, scaledCellHeight);
+ return true;
+ }
+
+ const patternDefinition = patternCharacterDefinitions[c];
+ if (patternDefinition) {
+ drawPatternChar(ctx, patternDefinition, xOffset, yOffset, scaledCellWidth, scaledCellHeight);
+ return true;
+ }
+
+ const boxDrawingDefinition = boxDrawingDefinitions[c];
+ if (boxDrawingDefinition) {
+ drawBoxDrawingChar(ctx, boxDrawingDefinition, xOffset, yOffset, scaledCellWidth, scaledCellHeight);
+ return true;
+ }
+
+ return false;
+}
+
+function drawBlockElementChar(
+ ctx: CanvasRenderingContext2D,
+ charDefinition: IBlockVector[],
+ xOffset: number,
+ yOffset: number,
+ scaledCellWidth: number,
+ scaledCellHeight: number
+): void {
+ for (let i = 0; i < charDefinition.length; i++) {
+ const box = charDefinition[i];
+ const xEighth = scaledCellWidth / 8;
+ const yEighth = scaledCellHeight / 8;
+ ctx.fillRect(
+ xOffset + box.x * xEighth,
+ yOffset + box.y * yEighth,
+ box.w * xEighth,
+ box.h * yEighth
+ );
+ }
+}
+
+const cachedPatterns: Map<PatternDefinition, Map</* fillStyle */string, CanvasPattern>> = new Map();
+
+function drawPatternChar(
+ ctx: CanvasRenderingContext2D,
+ charDefinition: number[][],
+ xOffset: number,
+ yOffset: number,
+ scaledCellWidth: number,
+ scaledCellHeight: number
+): void {
+ let patternSet = cachedPatterns.get(charDefinition);
+ if (!patternSet) {
+ patternSet = new Map();
+ cachedPatterns.set(charDefinition, patternSet);
+ }
+ const fillStyle = ctx.fillStyle;
+ if (typeof fillStyle !== 'string') {
+ throw new Error(`Unexpected fillStyle type "${fillStyle}"`);
+ }
+ let pattern = patternSet.get(fillStyle);
+ if (!pattern) {
+ const width = charDefinition[0].length;
+ const height = charDefinition.length;
+ const tmpCanvas = document.createElement('canvas');
+ tmpCanvas.width = width;
+ tmpCanvas.height = height;
+ const tmpCtx = throwIfFalsy(tmpCanvas.getContext('2d'));
+ const imageData = new ImageData(width, height);
+
+ // Extract rgba from fillStyle
+ let r: number;
+ let g: number;
+ let b: number;
+ let a: number;
+ if (fillStyle.startsWith('#')) {
+ r = parseInt(fillStyle.substr(1, 2), 16);
+ g = parseInt(fillStyle.substr(3, 2), 16);
+ b = parseInt(fillStyle.substr(5, 2), 16);
+ a = fillStyle.length > 7 && parseInt(fillStyle.substr(7, 2), 16) || 1;
+ } else if (fillStyle.startsWith('rgba')) {
+ ([r, g, b, a] = fillStyle.substring(5, fillStyle.length - 1).split(',').map(e => parseFloat(e)));
+ } else {
+ throw new Error(`Unexpected fillStyle color format "${fillStyle}" when drawing pattern glyph`);
+ }
+
+ for (let y = 0; y < height; y++) {
+ for (let x = 0; x < width; x++) {
+ imageData.data[(y * width + x) * 4 ] = r;
+ imageData.data[(y * width + x) * 4 + 1] = g;
+ imageData.data[(y * width + x) * 4 + 2] = b;
+ imageData.data[(y * width + x) * 4 + 3] = charDefinition[y][x] * (a * 255);
+ }
+ }
+ tmpCtx.putImageData(imageData, 0, 0);
+ pattern = throwIfFalsy(ctx.createPattern(tmpCanvas, null));
+ patternSet.set(fillStyle, pattern);
+ }
+ ctx.fillStyle = pattern;
+ ctx.fillRect(xOffset, yOffset, scaledCellWidth, scaledCellHeight);
+}
+
+/**
+ * Draws the following box drawing characters by mapping a subset of SVG d attribute instructions to
+ * canvas draw calls.
+ *
+ * Box styles: ┎┰┒┍┯┑╓╥╖╒╤╕ ┏┳┓┌┲┓┌┬┐┏┱┐
+ * ┌─┬─┐ ┏━┳━┓ ╔═╦═╗ ┠╂┨┝┿┥╟╫╢╞╪╡ ┡╇┩├╊┫┢╈┪┣╉┤
+ * │ │ │ ┃ ┃ ┃ ║ ║ ║ ┖┸┚┕┷┙╙╨╜╘╧╛ └┴┘└┺┛┗┻┛┗┹┘
+ * ├─┼─┤ ┣━╋━┫ ╠═╬═╣ ┏┱┐┌┲┓┌┬┐┌┬┐ ┏┳┓┌┮┓┌┬┐┏┭┐
+ * │ │ │ ┃ ┃ ┃ ║ ║ ║ ┡╃┤├╄┩├╆┪┢╅┤ ┞╀┦├┾┫┟╁┧┣┽┤
+ * └─┴─┘ ┗━┻━┛ ╚═╩═╝ └┴┘└┴┘└┺┛┗┹┘ └┴┘└┶┛┗┻┛┗┵┘
+ *
+ * Other:
+ * ╭─╮ ╲ ╱ ╷╻╎╏┆┇┊┋ ╺╾╴ ╌╌╌ ┄┄┄ ┈┈┈
+ * │ │ ╳ ╽╿╎╏┆┇┊┋ ╶╼╸ ╍╍╍ ┅┅┅ ┉┉┉
+ * ╰─╯ ╱ ╲ ╹╵╎╏┆┇┊┋
+ *
+ * All box drawing characters:
+ * ─ ━ │ ┃ ┄ ┅ ┆ ┇ ┈ ┉ ┊ ┋ ┌ ┍ ┎ ┏
+ * ┐ ┑ ┒ ┓ └ ┕ ┖ ┗ ┘ ┙ ┚ ┛ ├ ┝ ┞ ┟
+ * ┠ ┡ ┢ ┣ ┤ ┥ ┦ ┧ ┨ ┩ ┪ ┫ ┬ ┭ ┮ ┯
+ * ┰ ┱ ┲ ┳ ┴ ┵ ┶ ┷ ┸ ┹ ┺ ┻ ┼ ┽ ┾ ┿
+ * ╀ ╁ ╂ ╃ ╄ ╅ ╆ ╇ ╈ ╉ ╊ ╋ ╌ ╍ ╎ ╏
+ * ═ ║ ╒ ╓ ╔ ╕ ╖ ╗ ╘ ╙ ╚ ╛ ╜ ╝ ╞ ╟
+ * ╠ ╡ ╢ ╣ ╤ ╥ ╦ ╧ ╨ ╩ ╪ ╫ ╬ ╭ ╮ ╯
+ * ╰ ╱ ╲ ╳ ╴ ╵ ╶ ╷ ╸ ╹ ╺ ╻ ╼ ╽ ╾ ╿
+ *
+ * ---
+ *
+ * Box drawing alignment tests: █
+ * ▉
+ * ╔══╦══╗ ┌──┬──┐ ╭──┬──╮ ╭──┬──╮ ┏━━┳━━┓ ┎┒┏┑ ╷ ╻ ┏┯┓ ┌┰┐ ▊ ╱╲╱╲╳╳╳
+ * ║┌─╨─┐║ │╔═╧═╗│ │╒═╪═╕│ │╓─╁─╖│ ┃┌─╂─┐┃ ┗╃╄┙ ╶┼╴╺╋╸┠┼┨ ┝╋┥ ▋ ╲╱╲╱╳╳╳
+ * ║│╲ ╱│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╿ │┃ ┍╅╆┓ ╵ ╹ ┗┷┛ └┸┘ ▌ ╱╲╱╲╳╳╳
+ * ╠╡ ╳ ╞╣ ├╢ ╟┤ ├┼─┼─┼┤ ├╫─╂─╫┤ ┣┿╾┼╼┿┫ ┕┛┖┚ ┌┄┄┐ ╎ ┏┅┅┓ ┋ ▍ ╲╱╲╱╳╳╳
+ * ║│╱ ╲│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╽ │┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▎
+ * ║└─╥─┘║ │╚═╤═╝│ │╘═╪═╛│ │╙─╀─╜│ ┃└─╂─┘┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▏
+ * ╚══╩══╝ └──┴──┘ ╰──┴──╯ ╰──┴──╯ ┗━━┻━━┛ └╌╌┘ ╎ ┗╍╍┛ ┋ ▁▂▃▄▅▆▇█
+ *
+ * Source: https://www.w3.org/2001/06/utf-8-test/UTF-8-demo.html
+ */
+function drawBoxDrawingChar(
+ ctx: CanvasRenderingContext2D,
+ charDefinition: { [fontWeight: number]: string | ((xp: number, yp: number) => string) },
+ xOffset: number,
+ yOffset: number,
+ scaledCellWidth: number,
+ scaledCellHeight: number
+): void {
+ ctx.strokeStyle = ctx.fillStyle;
+ for (const [fontWeight, instructions] of Object.entries(charDefinition)) {
+ ctx.beginPath();
+ ctx.lineWidth = window.devicePixelRatio * Number.parseInt(fontWeight);
+ let actualInstructions: string;
+ if (typeof instructions === 'function') {
+ const xp = .15;
+ const yp = .15 / scaledCellHeight * scaledCellWidth;
+ actualInstructions = instructions(xp, yp);
+ } else {
+ actualInstructions = instructions;
+ }
+ for (const instruction of actualInstructions.split(' ')) {
+ const type = instruction[0];
+ const f = svgToCanvasInstructionMap[type];
+ if (!f) {
+ console.error(`Could not find drawing instructions for "${type}"`);
+ continue;
+ }
+ const args: string[] = instruction.substring(1).split(',');
+ if (!args[0] || !args[1]) {
+ continue;
+ }
+ f(ctx, translateArgs(args, scaledCellWidth, scaledCellHeight, xOffset, yOffset));
+ }
+ ctx.stroke();
+ ctx.closePath();
+ }
+}
+
+function clamp(value: number, max: number, min: number = 0): number {
+ return Math.max(Math.min(value, max), min);
+}
+
+const svgToCanvasInstructionMap: { [index: string]: any } = {
+ 'C': (ctx: CanvasRenderingContext2D, args: number[]) => ctx.bezierCurveTo(args[0], args[1], args[2], args[3], args[4], args[5]),
+ 'L': (ctx: CanvasRenderingContext2D, args: number[]) => ctx.lineTo(args[0], args[1]),
+ 'M': (ctx: CanvasRenderingContext2D, args: number[]) => ctx.moveTo(args[0], args[1])
+};
+
+function translateArgs(args: string[], cellWidth: number, cellHeight: number, xOffset: number, yOffset: number): number[] {
+ const result = args.map(e => parseFloat(e) || parseInt(e));
+
+ if (result.length < 2) {
+ throw new Error('Too few arguments for instruction');
+ }
+
+ for (let x = 0; x < result.length; x += 2) {
+ // Translate from 0-1 to 0-cellWidth
+ result[x] *= cellWidth;
+ // Ensure coordinate doesn't escape cell bounds and round to the nearest 0.5 to ensure a crisp
+ // line at 100% devicePixelRatio
+ if (result[x] !== 0) {
+ result[x] = clamp(Math.round(result[x] + 0.5) - 0.5, cellWidth, 0);
+ }
+ // Apply the cell's offset (ie. x*cellWidth)
+ result[x] += xOffset;
+ }
+
+ for (let y = 1; y < result.length; y += 2) {
+ // Translate from 0-1 to 0-cellHeight
+ result[y] *= cellHeight;
+ // Ensure coordinate doesn't escape cell bounds and round to the nearest 0.5 to ensure a crisp
+ // line at 100% devicePixelRatio
+ if (result[y] !== 0) {
+ result[y] = clamp(Math.round(result[y] + 0.5) - 0.5, cellHeight, 0);
+ }
+ // Apply the cell's offset (ie. x*cellHeight)
+ result[y] += yOffset;
+ }
+
+ return result;
+}
diff --git a/node_modules/xterm/src/browser/renderer/GridCache.ts b/node_modules/xterm/src/browser/renderer/GridCache.ts
new file mode 100644
index 0000000..b48798d
--- /dev/null
+++ b/node_modules/xterm/src/browser/renderer/GridCache.ts
@@ -0,0 +1,33 @@
+/**
+ * Copyright (c) 2017 The xterm.js authors. All rights reserved.
+ * @license MIT
+ */
+
+export class GridCache<T> {
+ public cache: (T | undefined)[][];
+
+ public constructor() {
+ this.cache = [];
+ }
+
+ public resize(width: number, height: number): void {
+ for (let x = 0; x < width; x++) {
+ if (this.cache.length <= x) {
+ this.cache.push([]);
+ }
+ for (let y = this.cache[x].length; y < height; y++) {
+ this.cache[x].push(undefined);
+ }
+ this.cache[x].length = height;
+ }
+ this.cache.length = width;
+ }
+
+ public clear(): void {
+ for (let x = 0; x < this.cache.length; x++) {
+ for (let y = 0; y < this.cache[x].length; y++) {
+ this.cache[x][y] = undefined;
+ }
+ }
+ }
+}
diff --git a/node_modules/xterm/src/browser/renderer/LinkRenderLayer.ts b/node_modules/xterm/src/browser/renderer/LinkRenderLayer.ts
new file mode 100644
index 0000000..2492f92
--- /dev/null
+++ b/node_modules/xterm/src/browser/renderer/LinkRenderLayer.ts
@@ -0,0 +1,83 @@
+/**
+ * Copyright (c) 2017 The xterm.js authors. All rights reserved.
+ * @license MIT
+ */
+
+import { IRenderDimensions } from 'browser/renderer/Types';
+import { BaseRenderLayer } from './BaseRenderLayer';
+import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants';
+import { is256Color } from 'browser/renderer/atlas/CharAtlasUtils';
+import { IColorSet, ILinkifierEvent, ILinkifier, ILinkifier2 } from 'browser/Types';
+import { IBufferService, IOptionsService } from 'common/services/Services';
+
+export class LinkRenderLayer extends BaseRenderLayer {
+ private _state: ILinkifierEvent | undefined;
+
+ constructor(
+ container: HTMLElement,
+ zIndex: number,
+ colors: IColorSet,
+ rendererId: number,
+ linkifier: ILinkifier,
+ linkifier2: ILinkifier2,
+ @IBufferService bufferService: IBufferService,
+ @IOptionsService optionsService: IOptionsService
+ ) {
+ super(container, 'link', zIndex, true, colors, rendererId, bufferService, optionsService);
+ linkifier.onShowLinkUnderline(e => this._onShowLinkUnderline(e));
+ linkifier.onHideLinkUnderline(e => this._onHideLinkUnderline(e));
+
+ linkifier2.onShowLinkUnderline(e => this._onShowLinkUnderline(e));
+ linkifier2.onHideLinkUnderline(e => this._onHideLinkUnderline(e));
+ }
+
+ public resize(dim: IRenderDimensions): void {
+ super.resize(dim);
+ // Resizing the canvas discards the contents of the canvas so clear state
+ this._state = undefined;
+ }
+
+ public reset(): void {
+ this._clearCurrentLink();
+ }
+
+ private _clearCurrentLink(): void {
+ if (this._state) {
+ this._clearCells(this._state.x1, this._state.y1, this._state.cols - this._state.x1, 1);
+ const middleRowCount = this._state.y2 - this._state.y1 - 1;
+ if (middleRowCount > 0) {
+ this._clearCells(0, this._state.y1 + 1, this._state.cols, middleRowCount);
+ }
+ this._clearCells(0, this._state.y2, this._state.x2, 1);
+ this._state = undefined;
+ }
+ }
+
+ private _onShowLinkUnderline(e: ILinkifierEvent): void {
+ if (e.fg === INVERTED_DEFAULT_COLOR) {
+ this._ctx.fillStyle = this._colors.background.css;
+ } else if (e.fg && is256Color(e.fg)) {
+ // 256 color support
+ this._ctx.fillStyle = this._colors.ansi[e.fg].css;
+ } else {
+ this._ctx.fillStyle = this._colors.foreground.css;
+ }
+
+ if (e.y1 === e.y2) {
+ // Single line link
+ this._fillBottomLineAtCells(e.x1, e.y1, e.x2 - e.x1);
+ } else {
+ // Multi-line link
+ this._fillBottomLineAtCells(e.x1, e.y1, e.cols - e.x1);
+ for (let y = e.y1 + 1; y < e.y2; y++) {
+ this._fillBottomLineAtCells(0, y, e.cols);
+ }
+ this._fillBottomLineAtCells(0, e.y2, e.x2);
+ }
+ this._state = e;
+ }
+
+ private _onHideLinkUnderline(e: ILinkifierEvent): void {
+ this._clearCurrentLink();
+ }
+}
diff --git a/node_modules/xterm/src/browser/renderer/Renderer.ts b/node_modules/xterm/src/browser/renderer/Renderer.ts
new file mode 100644
index 0000000..7a64257
--- /dev/null
+++ b/node_modules/xterm/src/browser/renderer/Renderer.ts
@@ -0,0 +1,215 @@
+/**
+ * Copyright (c) 2017 The xterm.js authors. All rights reserved.
+ * @license MIT
+ */
+
+import { TextRenderLayer } from 'browser/renderer/TextRenderLayer';
+import { SelectionRenderLayer } from 'browser/renderer/SelectionRenderLayer';
+import { CursorRenderLayer } from 'browser/renderer/CursorRenderLayer';
+import { IRenderLayer, IRenderer, IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/Types';
+import { LinkRenderLayer } from 'browser/renderer/LinkRenderLayer';
+import { Disposable } from 'common/Lifecycle';
+import { IColorSet, ILinkifier, ILinkifier2 } from 'browser/Types';
+import { ICharSizeService, ICoreBrowserService } from 'browser/services/Services';
+import { IBufferService, IOptionsService, ICoreService, IInstantiationService } from 'common/services/Services';
+import { removeTerminalFromCache } from 'browser/renderer/atlas/CharAtlasCache';
+import { EventEmitter, IEvent } from 'common/EventEmitter';
+
+let nextRendererId = 1;
+
+export class Renderer extends Disposable implements IRenderer {
+ private _id = nextRendererId++;
+
+ private _renderLayers: IRenderLayer[];
+ private _devicePixelRatio: number;
+
+ public dimensions: IRenderDimensions;
+
+ private _onRequestRedraw = new EventEmitter<IRequestRedrawEvent>();
+ public get onRequestRedraw(): IEvent<IRequestRedrawEvent> { return this._onRequestRedraw.event; }
+
+ constructor(
+ private _colors: IColorSet,
+ private readonly _screenElement: HTMLElement,
+ linkifier: ILinkifier,
+ linkifier2: ILinkifier2,
+ @IInstantiationService instantiationService: IInstantiationService,
+ @IBufferService private readonly _bufferService: IBufferService,
+ @ICharSizeService private readonly _charSizeService: ICharSizeService,
+ @IOptionsService private readonly _optionsService: IOptionsService
+ ) {
+ super();
+ const allowTransparency = this._optionsService.rawOptions.allowTransparency;
+ this._renderLayers = [
+ instantiationService.createInstance(TextRenderLayer, this._screenElement, 0, this._colors, allowTransparency, this._id),
+ instantiationService.createInstance(SelectionRenderLayer, this._screenElement, 1, this._colors, this._id),
+ instantiationService.createInstance(LinkRenderLayer, this._screenElement, 2, this._colors, this._id, linkifier, linkifier2),
+ instantiationService.createInstance(CursorRenderLayer, this._screenElement, 3, this._colors, this._id, this._onRequestRedraw)
+ ];
+ this.dimensions = {
+ scaledCharWidth: 0,
+ scaledCharHeight: 0,
+ scaledCellWidth: 0,
+ scaledCellHeight: 0,
+ scaledCharLeft: 0,
+ scaledCharTop: 0,
+ scaledCanvasWidth: 0,
+ scaledCanvasHeight: 0,
+ canvasWidth: 0,
+ canvasHeight: 0,
+ actualCellWidth: 0,
+ actualCellHeight: 0
+ };
+ this._devicePixelRatio = window.devicePixelRatio;
+ this._updateDimensions();
+ this.onOptionsChanged();
+ }
+
+ public dispose(): void {
+ for (const l of this._renderLayers) {
+ l.dispose();
+ }
+ super.dispose();
+ removeTerminalFromCache(this._id);
+ }
+
+ public onDevicePixelRatioChange(): void {
+ // If the device pixel ratio changed, the char atlas needs to be regenerated
+ // and the terminal needs to refreshed
+ if (this._devicePixelRatio !== window.devicePixelRatio) {
+ this._devicePixelRatio = window.devicePixelRatio;
+ this.onResize(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 {
+ // Update character and canvas dimensions
+ this._updateDimensions();
+
+ // Resize all render layers
+ for (const l of this._renderLayers) {
+ l.resize(this.dimensions);
+ }
+
+ // Resize the screen
+ this._screenElement.style.width = `${this.dimensions.canvasWidth}px`;
+ this._screenElement.style.height = `${this.dimensions.canvasHeight}px`;
+ }
+
+ public onCharSizeChanged(): void {
+ this.onResize(this._bufferService.cols, this._bufferService.rows);
+ }
+
+ public onBlur(): void {
+ this._runOperation(l => l.onBlur());
+ }
+
+ public onFocus(): void {
+ this._runOperation(l => l.onFocus());
+ }
+
+ public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {
+ this._runOperation(l => l.onSelectionChanged(start, end, columnSelectMode));
+ }
+
+ public onCursorMove(): void {
+ this._runOperation(l => l.onCursorMove());
+ }
+
+ public onOptionsChanged(): void {
+ this._runOperation(l => l.onOptionsChanged());
+ }
+
+ public clear(): void {
+ this._runOperation(l => l.reset());
+ }
+
+ private _runOperation(operation: (layer: IRenderLayer) => void): void {
+ for (const l of this._renderLayers) {
+ operation(l);
+ }
+ }
+
+ /**
+ * Performs the refresh loop callback, calling refresh only if a refresh is
+ * necessary before queueing up the next one.
+ */
+ public renderRows(start: number, end: number): void {
+ for (const l of this._renderLayers) {
+ l.onGridChanged(start, end);
+ }
+ }
+
+ public clearTextureAtlas(): void {
+ for (const layer of this._renderLayers) {
+ layer.clearTextureAtlas();
+ }
+ }
+
+ /**
+ * Recalculates the character and canvas dimensions.
+ */
+ private _updateDimensions(): void {
+ if (!this._charSizeService.hasValidSize) {
+ return;
+ }
+
+ // Calculate the scaled character width. Width is floored as it must be
+ // drawn to an integer grid in order for the CharAtlas "stamps" to not be
+ // blurry. When text is drawn to the grid not using the CharAtlas, it is
+ // clipped to ensure there is no overlap with the next cell.
+ this.dimensions.scaledCharWidth = Math.floor(this._charSizeService.width * window.devicePixelRatio);
+
+ // Calculate the scaled character height. Height is ceiled in case
+ // devicePixelRatio is a floating point number in order to ensure there is
+ // enough space to draw the character to the cell.
+ this.dimensions.scaledCharHeight = Math.ceil(this._charSizeService.height * window.devicePixelRatio);
+
+ // Calculate the scaled cell height, if lineHeight is not 1 then the value
+ // will be floored because since lineHeight can never be lower then 1, there
+ // is a guarentee that the scaled line height will always be larger than
+ // scaled char height.
+ this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._optionsService.rawOptions.lineHeight);
+
+ // Calculate the y coordinate within a cell that text should draw from in
+ // order to draw in the center of a cell.
+ this.dimensions.scaledCharTop = this._optionsService.rawOptions.lineHeight === 1 ? 0 : Math.round((this.dimensions.scaledCellHeight - this.dimensions.scaledCharHeight) / 2);
+
+ // Calculate the scaled cell width, taking the letterSpacing into account.
+ this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._optionsService.rawOptions.letterSpacing);
+
+ // Calculate the x coordinate with a cell that text should draw from in
+ // order to draw in the center of a cell.
+ this.dimensions.scaledCharLeft = Math.floor(this._optionsService.rawOptions.letterSpacing / 2);
+
+ // Recalculate the canvas dimensions; scaled* define the actual number of
+ // pixel in the canvas
+ this.dimensions.scaledCanvasHeight = this._bufferService.rows * this.dimensions.scaledCellHeight;
+ this.dimensions.scaledCanvasWidth = this._bufferService.cols * this.dimensions.scaledCellWidth;
+
+ // The the size of the canvas on the page. It's very important that this
+ // rounds to nearest integer and not ceils as browsers often set
+ // window.devicePixelRatio as something like 1.100000023841858, when it's
+ // actually 1.1. Ceiling causes blurriness as the backing canvas image is 1
+ // pixel too large for the canvas element size.
+ this.dimensions.canvasHeight = Math.round(this.dimensions.scaledCanvasHeight / window.devicePixelRatio);
+ this.dimensions.canvasWidth = Math.round(this.dimensions.scaledCanvasWidth / window.devicePixelRatio);
+
+ // Get the _actual_ dimensions of an individual cell. This needs to be
+ // derived from the canvasWidth/Height calculated above which takes into
+ // account window.devicePixelRatio. ICharSizeService.width/height by itself
+ // is insufficient when the page is not at 100% zoom level as it's measured
+ // in CSS pixels, but the actual char size on the canvas can differ.
+ this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._bufferService.rows;
+ this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._bufferService.cols;
+ }
+}
diff --git a/node_modules/xterm/src/browser/renderer/RendererUtils.ts b/node_modules/xterm/src/browser/renderer/RendererUtils.ts
new file mode 100644
index 0000000..48fd26a
--- /dev/null
+++ b/node_modules/xterm/src/browser/renderer/RendererUtils.ts
@@ -0,0 +1,11 @@
+/**
+ * Copyright (c) 2019 The xterm.js authors. All rights reserved.
+ * @license MIT
+ */
+
+export function throwIfFalsy<T>(value: T | undefined | null): T {
+ if (!value) {
+ throw new Error('value must not be falsy');
+ }
+ return value;
+}
diff --git a/node_modules/xterm/src/browser/renderer/SelectionRenderLayer.ts b/node_modules/xterm/src/browser/renderer/SelectionRenderLayer.ts
new file mode 100644
index 0000000..9054e3c
--- /dev/null
+++ b/node_modules/xterm/src/browser/renderer/SelectionRenderLayer.ts
@@ -0,0 +1,128 @@
+/**
+ * Copyright (c) 2017 The xterm.js authors. All rights reserved.
+ * @license MIT
+ */
+
+import { IRenderDimensions } from 'browser/renderer/Types';
+import { BaseRenderLayer } from 'browser/renderer/BaseRenderLayer';
+import { IColorSet } from 'browser/Types';
+import { IBufferService, IOptionsService } from 'common/services/Services';
+
+interface ISelectionState {
+ start?: [number, number];
+ end?: [number, number];
+ columnSelectMode?: boolean;
+ ydisp?: number;
+}
+
+export class SelectionRenderLayer extends BaseRenderLayer {
+ private _state!: ISelectionState;
+
+ constructor(
+ container: HTMLElement,
+ zIndex: number,
+ colors: IColorSet,
+ rendererId: number,
+ @IBufferService bufferService: IBufferService,
+ @IOptionsService optionsService: IOptionsService
+ ) {
+ super(container, 'selection', zIndex, true, colors, rendererId, bufferService, optionsService);
+ this._clearState();
+ }
+
+ private _clearState(): void {
+ this._state = {
+ start: undefined,
+ end: undefined,
+ columnSelectMode: undefined,
+ ydisp: undefined
+ };
+ }
+
+ public resize(dim: IRenderDimensions): void {
+ super.resize(dim);
+ // Resizing the canvas discards the contents of the canvas so clear state
+ this._clearState();
+ }
+
+ public reset(): void {
+ if (this._state.start && this._state.end) {
+ this._clearState();
+ this._clearAll();
+ }
+ }
+
+ public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
+ // Selection has not changed
+ if (!this._didStateChange(start, end, columnSelectMode, this._bufferService.buffer.ydisp)) {
+ return;
+ }
+
+ // Remove all selections
+ this._clearAll();
+
+ // Selection does not exist
+ if (!start || !end) {
+ this._clearState();
+ return;
+ }
+
+ // Translate from buffer position to viewport position
+ const viewportStartRow = start[1] - this._bufferService.buffer.ydisp;
+ const viewportEndRow = end[1] - this._bufferService.buffer.ydisp;
+ const viewportCappedStartRow = Math.max(viewportStartRow, 0);
+ const viewportCappedEndRow = Math.min(viewportEndRow, this._bufferService.rows - 1);
+
+ // No need to draw the selection
+ if (viewportCappedStartRow >= this._bufferService.rows || viewportCappedEndRow < 0) {
+ this._state.ydisp = this._bufferService.buffer.ydisp;
+ return;
+ }
+
+ this._ctx.fillStyle = this._colors.selectionTransparent.css;
+
+ if (columnSelectMode) {
+ const startCol = start[0];
+ const width = end[0] - startCol;
+ const height = viewportCappedEndRow - viewportCappedStartRow + 1;
+ this._fillCells(startCol, viewportCappedStartRow, width, height);
+ } else {
+ // Draw first row
+ const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0;
+ const startRowEndCol = viewportCappedStartRow === viewportEndRow ? end[0] : this._bufferService.cols;
+ this._fillCells(startCol, viewportCappedStartRow, startRowEndCol - startCol, 1);
+
+ // Draw middle rows
+ const middleRowsCount = Math.max(viewportCappedEndRow - viewportCappedStartRow - 1, 0);
+ this._fillCells(0, viewportCappedStartRow + 1, this._bufferService.cols, middleRowsCount);
+
+ // Draw final row
+ if (viewportCappedStartRow !== viewportCappedEndRow) {
+ // Only draw viewportEndRow if it's not the same as viewportStartRow
+ const endCol = viewportEndRow === viewportCappedEndRow ? end[0] : this._bufferService.cols;
+ this._fillCells(0, viewportCappedEndRow, endCol, 1);
+ }
+ }
+
+ // Save state for next render
+ this._state.start = [start[0], start[1]];
+ this._state.end = [end[0], end[1]];
+ this._state.columnSelectMode = columnSelectMode;
+ this._state.ydisp = this._bufferService.buffer.ydisp;
+ }
+
+ private _didStateChange(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean, ydisp: number): boolean {
+ return !this._areCoordinatesEqual(start, this._state.start) ||
+ !this._areCoordinatesEqual(end, this._state.end) ||
+ columnSelectMode !== this._state.columnSelectMode ||
+ ydisp !== this._state.ydisp;
+ }
+
+ private _areCoordinatesEqual(coord1: [number, number] | undefined, coord2: [number, number] | undefined): boolean {
+ if (!coord1 || !coord2) {
+ return false;
+ }
+
+ return coord1[0] === coord2[0] && coord1[1] === coord2[1];
+ }
+}
diff --git a/node_modules/xterm/src/browser/renderer/TextRenderLayer.ts b/node_modules/xterm/src/browser/renderer/TextRenderLayer.ts
new file mode 100644
index 0000000..33d942f
--- /dev/null
+++ b/node_modules/xterm/src/browser/renderer/TextRenderLayer.ts
@@ -0,0 +1,330 @@
+/**
+ * Copyright (c) 2017 The xterm.js authors. All rights reserved.
+ * @license MIT
+ */
+
+import { IRenderDimensions } from 'browser/renderer/Types';
+import { CharData, ICellData } from 'common/Types';
+import { GridCache } from 'browser/renderer/GridCache';
+import { BaseRenderLayer } from 'browser/renderer/BaseRenderLayer';
+import { AttributeData } from 'common/buffer/AttributeData';
+import { NULL_CELL_CODE, Content } from 'common/buffer/Constants';
+import { IColorSet } from 'browser/Types';
+import { CellData } from 'common/buffer/CellData';
+import { IOptionsService, IBufferService } from 'common/services/Services';
+import { ICharacterJoinerService } from 'browser/services/Services';
+import { JoinedCellData } from 'browser/services/CharacterJoinerService';
+
+/**
+ * This CharData looks like a null character, which will forc a clear and render
+ * when the character changes (a regular space ' ' character may not as it's
+ * drawn state is a cleared cell).
+ */
+// const OVERLAP_OWNED_CHAR_DATA: CharData = [null, '', 0, -1];
+
+export class TextRenderLayer extends BaseRenderLayer {
+ private _state: GridCache<CharData>;
+ private _characterWidth: number = 0;
+ private _characterFont: string = '';
+ private _characterOverlapCache: { [key: string]: boolean } = {};
+ private _workCell = new CellData();
+
+ constructor(
+ container: HTMLElement,
+ zIndex: number,
+ colors: IColorSet,
+ alpha: boolean,
+ rendererId: number,
+ @IBufferService bufferService: IBufferService,
+ @IOptionsService optionsService: IOptionsService,
+ @ICharacterJoinerService private readonly _characterJoinerService: ICharacterJoinerService
+ ) {
+ super(container, 'text', zIndex, alpha, colors, rendererId, bufferService, optionsService);
+ this._state = new GridCache<CharData>();
+ }
+
+ public resize(dim: IRenderDimensions): void {
+ super.resize(dim);
+
+ // Clear the character width cache if the font or width has changed
+ const terminalFont = this._getFont(false, false);
+ if (this._characterWidth !== dim.scaledCharWidth || this._characterFont !== terminalFont) {
+ this._characterWidth = dim.scaledCharWidth;
+ this._characterFont = terminalFont;
+ this._characterOverlapCache = {};
+ }
+ // Resizing the canvas discards the contents of the canvas so clear state
+ this._state.clear();
+ this._state.resize(this._bufferService.cols, this._bufferService.rows);
+ }
+
+ public reset(): void {
+ this._state.clear();
+ this._clearAll();
+ }
+
+ private _forEachCell(
+ firstRow: number,
+ lastRow: number,
+ callback: (
+ cell: ICellData,
+ x: number,
+ y: number
+ ) => void
+ ): void {
+ for (let y = firstRow; y <= lastRow; y++) {
+ const row = y + this._bufferService.buffer.ydisp;
+ const line = this._bufferService.buffer.lines.get(row);
+ const joinedRanges = this._characterJoinerService.getJoinedCharacters(row);
+ for (let x = 0; x < this._bufferService.cols; x++) {
+ line!.loadCell(x, this._workCell);
+ let cell = this._workCell;
+
+ // If true, indicates that the current character(s) to draw were joined.
+ let isJoined = false;
+ let lastCharX = x;
+
+ // The character to the left is a wide character, drawing is owned by
+ // the char at x-1
+ if (cell.getWidth() === 0) {
+ continue;
+ }
+
+ // Process any joined character ranges as needed. Because of how the
+ // ranges are produced, we know that they are valid for the characters
+ // and attributes of our input.
+ if (joinedRanges.length > 0 && x === joinedRanges[0][0]) {
+ isJoined = true;
+ const range = joinedRanges.shift()!;
+
+ // We already know the exact start and end column of the joined range,
+ // so we get the string and width representing it directly
+ cell = new JoinedCellData(
+ this._workCell,
+ line!.translateToString(true, range[0], range[1]),
+ range[1] - range[0]
+ );
+
+ // Skip over the cells occupied by this range in the loop
+ lastCharX = range[1] - 1;
+ }
+
+ // If the character is an overlapping char and the character to the
+ // right is a space, take ownership of the cell to the right. We skip
+ // this check for joined characters because their rendering likely won't
+ // yield the same result as rendering the last character individually.
+ if (!isJoined && this._isOverlapping(cell)) {
+ // If the character is overlapping, we want to force a re-render on every
+ // frame. This is specifically to work around the case where two
+ // overlaping chars `a` and `b` are adjacent, the cursor is moved to b and a
+ // space is added. Without this, the first half of `b` would never
+ // get removed, and `a` would not re-render because it thinks it's
+ // already in the correct state.
+ // this._state.cache[x][y] = OVERLAP_OWNED_CHAR_DATA;
+ if (lastCharX < line!.length - 1 && line!.getCodePoint(lastCharX + 1) === NULL_CELL_CODE) {
+ // patch width to 2
+ cell.content &= ~Content.WIDTH_MASK;
+ cell.content |= 2 << Content.WIDTH_SHIFT;
+ // this._clearChar(x + 1, y);
+ // The overlapping char's char data will force a clear and render when the
+ // overlapping char is no longer to the left of the character and also when
+ // the space changes to another character.
+ // this._state.cache[x + 1][y] = OVERLAP_OWNED_CHAR_DATA;
+ }
+ }
+
+ callback(
+ cell,
+ x,
+ y
+ );
+
+ x = lastCharX;
+ }
+ }
+ }
+
+ /**
+ * Draws the background for a specified range of columns. Tries to batch adjacent cells of the
+ * same color together to reduce draw calls.
+ */
+ private _drawBackground(firstRow: number, lastRow: number): void {
+ const ctx = this._ctx;
+ const cols = this._bufferService.cols;
+ let startX: number = 0;
+ let startY: number = 0;
+ let prevFillStyle: string | null = null;
+
+ ctx.save();
+
+ this._forEachCell(firstRow, lastRow, (cell, x, y) => {
+ // libvte and xterm both draw the background (but not foreground) of invisible characters,
+ // so we should too.
+ let nextFillStyle = null; // null represents default background color
+
+ if (cell.isInverse()) {
+ if (cell.isFgDefault()) {
+ nextFillStyle = this._colors.foreground.css;
+ } else if (cell.isFgRGB()) {
+ nextFillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`;
+ } else {
+ nextFillStyle = this._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;
+ }
+
+ if (prevFillStyle === null) {
+ // This is either the first iteration, or the default background was set. Either way, we
+ // don't need to draw anything.
+ startX = x;
+ startY = y;
+ }
+
+ if (y !== startY) {
+ // our row changed, draw the previous row
+ ctx.fillStyle = prevFillStyle || '';
+ this._fillCells(startX, startY, cols - startX, 1);
+ startX = x;
+ startY = y;
+ } else if (prevFillStyle !== nextFillStyle) {
+ // our color changed, draw the previous characters in this row
+ ctx.fillStyle = prevFillStyle || '';
+ this._fillCells(startX, startY, x - startX, 1);
+ startX = x;
+ startY = y;
+ }
+
+ prevFillStyle = nextFillStyle;
+ });
+
+ // flush the last color we encountered
+ if (prevFillStyle !== null) {
+ ctx.fillStyle = prevFillStyle;
+ this._fillCells(startX, startY, cols - startX, 1);
+ }
+
+ ctx.restore();
+ }
+
+ 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()) {
+ this._fillBottomLineAtCells(x, y, cell.getWidth());
+ }
+ this._ctx.restore();
+ }
+ });
+ }
+
+ public onGridChanged(firstRow: number, lastRow: number): void {
+ // Resize has not been called yet
+ if (this._state.cache.length === 0) {
+ return;
+ }
+
+ if (this._charAtlas) {
+ this._charAtlas.beginFrame();
+ }
+
+ this._clearCells(0, firstRow, this._bufferService.cols, lastRow - firstRow + 1);
+ this._drawBackground(firstRow, lastRow);
+ this._drawForeground(firstRow, lastRow);
+ }
+
+ public onOptionsChanged(): void {
+ this._setTransparency(this._optionsService.rawOptions.allowTransparency);
+ }
+
+ /**
+ * Whether a character is overlapping to the next cell.
+ */
+ private _isOverlapping(cell: ICellData): boolean {
+ // Only single cell characters can be overlapping, rendering issues can
+ // occur without this check
+ if (cell.getWidth() !== 1) {
+ return false;
+ }
+
+ // We assume that any ascii character will not overlap
+ if (cell.getCode() < 256) {
+ return false;
+ }
+
+ const chars = cell.getChars();
+
+ // Deliver from cache if available
+ if (this._characterOverlapCache.hasOwnProperty(chars)) {
+ return this._characterOverlapCache[chars];
+ }
+
+ // Setup the font
+ this._ctx.save();
+ this._ctx.font = this._characterFont;
+
+ // Measure the width of the character, but Math.floor it
+ // because that is what the renderer does when it calculates
+ // the character dimensions we are comparing against
+ const overlaps = Math.floor(this._ctx.measureText(chars).width) > this._characterWidth;
+
+ // Restore the original context
+ this._ctx.restore();
+
+ // Cache and return
+ 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/node_modules/xterm/src/browser/renderer/Types.d.ts b/node_modules/xterm/src/browser/renderer/Types.d.ts
new file mode 100644
index 0000000..6818a92
--- /dev/null
+++ b/node_modules/xterm/src/browser/renderer/Types.d.ts
@@ -0,0 +1,109 @@
+/**
+ * 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<IRequestRedrawEvent>;
+
+ 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;
+}
+
+export interface IRenderLayer extends IDisposable {
+ /**
+ * Called when the terminal loses focus.
+ */
+ onBlur(): void;
+
+ /**
+ * * Called when the terminal gets focus.
+ */
+ onFocus(): void;
+
+ /**
+ * Called when the cursor is moved.
+ */
+ onCursorMove(): void;
+
+ /**
+ * Called when options change.
+ */
+ onOptionsChanged(): void;
+
+ /**
+ * Called when the theme changes.
+ */
+ setColors(colorSet: IColorSet): void;
+
+ /**
+ * Called when the data in the grid has changed (or needs to be rendered
+ * again).
+ */
+ onGridChanged(startRow: number, endRow: number): void;
+
+ /**
+ * Calls when the selection changes.
+ */
+ onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void;
+
+ /**
+ * Resize the render layer.
+ */
+ resize(dim: IRenderDimensions): void;
+
+ /**
+ * Clear the state of the render layer.
+ */
+ reset(): void;
+
+ /**
+ * Clears the texture atlas.
+ */
+ clearTextureAtlas(): void;
+}
diff --git a/node_modules/xterm/src/browser/renderer/atlas/BaseCharAtlas.ts b/node_modules/xterm/src/browser/renderer/atlas/BaseCharAtlas.ts
new file mode 100644
index 0000000..83c30d2
--- /dev/null
+++ b/node_modules/xterm/src/browser/renderer/atlas/BaseCharAtlas.ts
@@ -0,0 +1,58 @@
+/**
+ * Copyright (c) 2017 The xterm.js authors. All rights reserved.
+ * @license MIT
+ */
+
+import { IGlyphIdentifier } from 'browser/renderer/atlas/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/node_modules/xterm/src/browser/renderer/atlas/CharAtlasCache.ts b/node_modules/xterm/src/browser/renderer/atlas/CharAtlasCache.ts
new file mode 100644
index 0000000..257835b
--- /dev/null
+++ b/node_modules/xterm/src/browser/renderer/atlas/CharAtlasCache.ts
@@ -0,0 +1,95 @@
+/**
+ * Copyright (c) 2017 The xterm.js authors. All rights reserved.
+ * @license MIT
+ */
+
+import { generateConfig, configEquals } from 'browser/renderer/atlas/CharAtlasUtils';
+import { BaseCharAtlas } from 'browser/renderer/atlas/BaseCharAtlas';
+import { DynamicCharAtlas } from 'browser/renderer/atlas/DynamicCharAtlas';
+import { ICharAtlasConfig } from 'browser/renderer/atlas/Types';
+import { IColorSet } from 'browser/Types';
+import { ITerminalOptions } from 'common/services/Services';
+
+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: ITerminalOptions,
+ rendererId: number,
+ colors: IColorSet,
+ scaledCharWidth: number,
+ scaledCharHeight: number
+): BaseCharAtlas {
+ const newConfig = generateConfig(scaledCharWidth, scaledCharHeight, options, colors);
+
+ // 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/node_modules/xterm/src/browser/renderer/atlas/CharAtlasUtils.ts b/node_modules/xterm/src/browser/renderer/atlas/CharAtlasUtils.ts
new file mode 100644
index 0000000..be92727
--- /dev/null
+++ b/node_modules/xterm/src/browser/renderer/atlas/CharAtlasUtils.ts
@@ -0,0 +1,54 @@
+/**
+ * Copyright (c) 2017 The xterm.js authors. All rights reserved.
+ * @license MIT
+ */
+
+import { ICharAtlasConfig } from 'browser/renderer/atlas/Types';
+import { DEFAULT_COLOR } from 'common/buffer/Constants';
+import { IColorSet, IPartialColorSet } from 'browser/Types';
+import { ITerminalOptions } from 'common/services/Services';
+
+export function generateConfig(scaledCharWidth: number, scaledCharHeight: number, options: ITerminalOptions, colors: IColorSet): ICharAtlasConfig {
+ // null out some fields that don't matter
+ const clonedColors: IPartialColorSet = {
+ foreground: colors.foreground,
+ background: colors.background,
+ cursor: undefined,
+ cursorAccent: undefined,
+ selection: undefined,
+ ansi: [...colors.ansi]
+ };
+ return {
+ devicePixelRatio: window.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/node_modules/xterm/src/browser/renderer/atlas/Constants.ts b/node_modules/xterm/src/browser/renderer/atlas/Constants.ts
new file mode 100644
index 0000000..c1701e9
--- /dev/null
+++ b/node_modules/xterm/src/browser/renderer/atlas/Constants.ts
@@ -0,0 +1,15 @@
+/**
+ * Copyright (c) 2017 The xterm.js authors. All rights reserved.
+ * @license MIT
+ */
+
+import { isFirefox, isLegacyEdge } from 'common/Platform';
+
+export const INVERTED_DEFAULT_COLOR = 257;
+export const DIM_OPACITY = 0.5;
+// The text baseline is set conditionally by browser. Using 'ideographic' for Firefox or Legacy Edge would
+// result in truncated text (Issue 3353). Using 'bottom' for Chrome would result in slightly
+// unaligned Powerline fonts (PR 3356#issuecomment-850928179).
+export const TEXT_BASELINE: CanvasTextBaseline = isFirefox || isLegacyEdge ? 'bottom' : 'ideographic';
+
+export const CHAR_ATLAS_CELL_SPACING = 1;
diff --git a/node_modules/xterm/src/browser/renderer/atlas/DynamicCharAtlas.ts b/node_modules/xterm/src/browser/renderer/atlas/DynamicCharAtlas.ts
new file mode 100644
index 0000000..118dbcd
--- /dev/null
+++ b/node_modules/xterm/src/browser/renderer/atlas/DynamicCharAtlas.ts
@@ -0,0 +1,404 @@
+/**
+ * Copyright (c) 2017 The xterm.js authors. All rights reserved.
+ * @license MIT
+ */
+
+import { DIM_OPACITY, INVERTED_DEFAULT_COLOR, TEXT_BASELINE } from 'browser/renderer/atlas/Constants';
+import { IGlyphIdentifier, ICharAtlasConfig } from 'browser/renderer/atlas/Types';
+import { BaseCharAtlas } from 'browser/renderer/atlas/BaseCharAtlas';
+import { DEFAULT_ANSI_COLORS } from 'browser/ColorManager';
+import { LRUMap } from 'browser/renderer/atlas/LRUMap';
+import { isFirefox, isSafari } from 'common/Platform';
+import { IColor } from 'browser/Types';
+import { throwIfFalsy } from 'browser/renderer/RendererUtils';
+import { color } from 'browser/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<IGlyphCacheValue>;
+
+ // 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;
+ }
+ if (glyph.bg === INVERTED_DEFAULT_COLOR) {
+ return this._config.colors.foreground;
+ }
+ if (glyph.bg < 256) {
+ return this._getColorFromAnsiIndex(glyph.bg);
+ }
+ return this._config.colors.background;
+ }
+
+ 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 partiicular rgb color 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 (imageData.data[offset] === r &&
+ imageData.data[offset + 1] === g &&
+ imageData.data[offset + 2] === b) {
+ imageData.data[offset + 3] = 0;
+ } else {
+ isEmpty = false;
+ }
+ }
+ return isEmpty;
+}
diff --git a/node_modules/xterm/src/browser/renderer/atlas/LRUMap.ts b/node_modules/xterm/src/browser/renderer/atlas/LRUMap.ts
new file mode 100644
index 0000000..f70962f
--- /dev/null
+++ b/node_modules/xterm/src/browser/renderer/atlas/LRUMap.ts
@@ -0,0 +1,136 @@
+/**
+ * Copyright (c) 2017 The xterm.js authors. All rights reserved.
+ * @license MIT
+ */
+
+interface ILinkedListNode<T> {
+ prev: ILinkedListNode<T> | null;
+ next: ILinkedListNode<T> | null;
+ key: number | null;
+ value: T | null;
+}
+
+export class LRUMap<T> {
+ private _map: { [key: number]: ILinkedListNode<T> } = {};
+ private _head: ILinkedListNode<T> | null = null;
+ private _tail: ILinkedListNode<T> | null = null;
+ private _nodePool: ILinkedListNode<T>[] = [];
+ public size: number = 0;
+
+ constructor(public capacity: number) { }
+
+ private _unlinkNode(node: ILinkedListNode<T>): 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<T>): 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/node_modules/xterm/src/browser/renderer/atlas/Types.d.ts b/node_modules/xterm/src/browser/renderer/atlas/Types.d.ts
new file mode 100644
index 0000000..d8bc54c
--- /dev/null
+++ b/node_modules/xterm/src/browser/renderer/atlas/Types.d.ts
@@ -0,0 +1,29 @@
+/**
+ * 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/node_modules/xterm/src/browser/renderer/dom/DomRenderer.ts b/node_modules/xterm/src/browser/renderer/dom/DomRenderer.ts
new file mode 100644
index 0000000..ee28339
--- /dev/null
+++ b/node_modules/xterm/src/browser/renderer/dom/DomRenderer.ts
@@ -0,0 +1,400 @@
+/**
+ * Copyright (c) 2018 The xterm.js authors. All rights reserved.
+ * @license MIT
+ */
+
+import { IRenderer, IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/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/atlas/Constants';
+import { Disposable } from 'common/Lifecycle';
+import { IColorSet, ILinkifierEvent, ILinkifier, ILinkifier2 } from 'browser/Types';
+import { ICharSizeService } from 'browser/services/Services';
+import { IOptionsService, IBufferService, IInstantiationService } from 'common/services/Services';
+import { EventEmitter, IEvent } from 'common/EventEmitter';
+import { color } from 'browser/Color';
+import { removeElementFromParent } from 'browser/Dom';
+
+const TERMINAL_CLASS_PREFIX = 'xterm-dom-renderer-owner-';
+const ROW_CONTAINER_CLASS = 'xterm-rows';
+const FG_CLASS_PREFIX = 'xterm-fg-';
+const BG_CLASS_PREFIX = 'xterm-bg-';
+const FOCUS_CLASS = 'xterm-focus';
+const SELECTION_CLASS = 'xterm-selection';
+
+let nextTerminalId = 1;
+
+/**
+ * A fallback renderer for when canvas is slow. This is not meant to be
+ * particularly fast or feature complete, more just stable and usable for when
+ * canvas is not an option.
+ */
+export class DomRenderer extends Disposable implements IRenderer {
+ private _rowFactory: DomRendererRowFactory;
+ private _terminalClass: number = nextTerminalId++;
+
+ private _themeStyleElement!: HTMLStyleElement;
+ private _dimensionsStyleElement!: HTMLStyleElement;
+ private _rowContainer: HTMLElement;
+ private _rowElements: HTMLElement[] = [];
+ private _selectionContainer: HTMLElement;
+
+ public dimensions: IRenderDimensions;
+
+ public get onRequestRedraw(): IEvent<IRequestRedrawEvent> { return new EventEmitter<IRequestRedrawEvent>().event; }
+
+ constructor(
+ private _colors: IColorSet,
+ private readonly _element: HTMLElement,
+ private readonly _screenElement: HTMLElement,
+ private readonly _viewportElement: HTMLElement,
+ private readonly _linkifier: ILinkifier,
+ private readonly _linkifier2: ILinkifier2,
+ @IInstantiationService instantiationService: IInstantiationService,
+ @ICharSizeService private readonly _charSizeService: ICharSizeService,
+ @IOptionsService private readonly _optionsService: IOptionsService,
+ @IBufferService private readonly _bufferService: IBufferService
+ ) {
+ super();
+ this._rowContainer = document.createElement('div');
+ this._rowContainer.classList.add(ROW_CONTAINER_CLASS);
+ this._rowContainer.style.lineHeight = 'normal';
+ this._rowContainer.setAttribute('aria-hidden', 'true');
+ this._refreshRowElements(this._bufferService.cols, this._bufferService.rows);
+ this._selectionContainer = document.createElement('div');
+ this._selectionContainer.classList.add(SELECTION_CLASS);
+ this._selectionContainer.setAttribute('aria-hidden', 'true');
+
+ this.dimensions = {
+ scaledCharWidth: 0,
+ scaledCharHeight: 0,
+ scaledCellWidth: 0,
+ scaledCellHeight: 0,
+ scaledCharLeft: 0,
+ scaledCharTop: 0,
+ scaledCanvasWidth: 0,
+ scaledCanvasHeight: 0,
+ canvasWidth: 0,
+ canvasHeight: 0,
+ actualCellWidth: 0,
+ actualCellHeight: 0
+ };
+ this._updateDimensions();
+ this._injectCss();
+
+ this._rowFactory = instantiationService.createInstance(DomRendererRowFactory, document, this._colors);
+
+ this._element.classList.add(TERMINAL_CLASS_PREFIX + this._terminalClass);
+ this._screenElement.appendChild(this._rowContainer);
+ this._screenElement.appendChild(this._selectionContainer);
+
+ this._linkifier.onShowLinkUnderline(e => this._onLinkHover(e));
+ this._linkifier.onHideLinkUnderline(e => this._onLinkLeave(e));
+
+ this._linkifier2.onShowLinkUnderline(e => this._onLinkHover(e));
+ this._linkifier2.onHideLinkUnderline(e => this._onLinkLeave(e));
+ }
+
+ public dispose(): void {
+ 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();
+ }
+
+ private _updateDimensions(): void {
+ this.dimensions.scaledCharWidth = this._charSizeService.width * window.devicePixelRatio;
+ this.dimensions.scaledCharHeight = Math.ceil(this._charSizeService.height * window.devicePixelRatio);
+ this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._optionsService.rawOptions.letterSpacing);
+ this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._optionsService.rawOptions.lineHeight);
+ this.dimensions.scaledCharLeft = 0;
+ this.dimensions.scaledCharTop = 0;
+ this.dimensions.scaledCanvasWidth = this.dimensions.scaledCellWidth * this._bufferService.cols;
+ this.dimensions.scaledCanvasHeight = this.dimensions.scaledCellHeight * this._bufferService.rows;
+ this.dimensions.canvasWidth = Math.round(this.dimensions.scaledCanvasWidth / window.devicePixelRatio);
+ this.dimensions.canvasHeight = Math.round(this.dimensions.scaledCanvasHeight / window.devicePixelRatio);
+ this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._bufferService.cols;
+ this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._bufferService.rows;
+
+ for (const element of this._rowElements) {
+ element.style.width = `${this.dimensions.canvasWidth}px`;
+ element.style.height = `${this.dimensions.actualCellHeight}px`;
+ element.style.lineHeight = `${this.dimensions.actualCellHeight}px`;
+ // Make sure rows don't overflow onto following row
+ element.style.overflow = 'hidden';
+ }
+
+ if (!this._dimensionsStyleElement) {
+ this._dimensionsStyleElement = document.createElement('style');
+ this._screenElement.appendChild(this._dimensionsStyleElement);
+ }
+
+ const styles =
+ `${this._terminalSelector} .${ROW_CONTAINER_CLASS} span {` +
+ ` display: inline-block;` +
+ ` height: 100%;` +
+ ` vertical-align: top;` +
+ ` width: ${this.dimensions.actualCellWidth}px` +
+ `}`;
+
+ this._dimensionsStyleElement.textContent = styles;
+
+ this._selectionContainer.style.height = this._viewportElement.style.height;
+ this._screenElement.style.width = `${this.dimensions.canvasWidth}px`;
+ this._screenElement.style.height = `${this.dimensions.canvasHeight}px`;
+ }
+
+ public setColors(colors: IColorSet): void {
+ this._colors = colors;
+ this._injectCss();
+ }
+
+ private _injectCss(): void {
+ if (!this._themeStyleElement) {
+ this._themeStyleElement = document.createElement('style');
+ this._screenElement.appendChild(this._themeStyleElement);
+ }
+
+ // Base CSS
+ let styles =
+ `${this._terminalSelector} .${ROW_CONTAINER_CLASS} {` +
+ ` color: ${this._colors.foreground.css};` +
+ ` font-family: ${this._optionsService.rawOptions.fontFamily};` +
+ ` font-size: ${this._optionsService.rawOptions.fontSize}px;` +
+ `}`;
+ // Text styles
+ styles +=
+ `${this._terminalSelector} span:not(.${BOLD_CLASS}) {` +
+ ` font-weight: ${this._optionsService.rawOptions.fontWeight};` +
+ `}` +
+ `${this._terminalSelector} span.${BOLD_CLASS} {` +
+ ` font-weight: ${this._optionsService.rawOptions.fontWeightBold};` +
+ `}` +
+ `${this._terminalSelector} span.${ITALIC_CLASS} {` +
+ ` font-style: italic;` +
+ `}`;
+ // Blink animation
+ styles +=
+ `@keyframes blink_box_shadow` + `_` + this._terminalClass + ` {` +
+ ` 50% {` +
+ ` box-shadow: none;` +
+ ` }` +
+ `}`;
+ styles +=
+ `@keyframes blink_block` + `_` + this._terminalClass + ` {` +
+ ` 0% {` +
+ ` background-color: ${this._colors.cursor.css};` +
+ ` color: ${this._colors.cursorAccent.css};` +
+ ` }` +
+ ` 50% {` +
+ ` background-color: ${this._colors.cursorAccent.css};` +
+ ` color: ${this._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-offset: -1px;` +
+ `}` +
+ `${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_BLINK_CLASS}:not(.${CURSOR_STYLE_BLOCK_CLASS}) {` +
+ ` animation: blink_box_shadow` + `_` + this._terminalClass + ` 1s step-end infinite;` +
+ `}` +
+ `${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_BLINK_CLASS}.${CURSOR_STYLE_BLOCK_CLASS} {` +
+ ` 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};` +
+ `}` +
+ `${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;` +
+ `}` +
+ `${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_UNDERLINE_CLASS} {` +
+ ` box-shadow: 0 -1px 0 ${this._colors.cursor.css} inset;` +
+ `}`;
+ // Selection
+ styles +=
+ `${this._terminalSelector} .${SELECTION_CLASS} {` +
+ ` position: absolute;` +
+ ` top: 0;` +
+ ` left: 0;` +
+ ` z-index: 1;` +
+ ` pointer-events: none;` +
+ `}` +
+ `${this._terminalSelector} .${SELECTION_CLASS} div {` +
+ ` position: absolute;` +
+ ` background-color: ${this._colors.selectionTransparent.css};` +
+ `}`;
+ // Colors
+ this._colors.ansi.forEach((c, i) => {
+ 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._themeStyleElement.textContent = styles;
+ }
+
+ public onDevicePixelRatioChange(): void {
+ this._updateDimensions();
+ }
+
+ private _refreshRowElements(cols: number, rows: number): void {
+ // Add missing elements
+ for (let i = this._rowElements.length; i <= rows; i++) {
+ const row = document.createElement('div');
+ this._rowContainer.appendChild(row);
+ this._rowElements.push(row);
+ }
+ // Remove excess elements
+ while (this._rowElements.length > rows) {
+ this._rowContainer.removeChild(this._rowElements.pop()!);
+ }
+ }
+
+ public onResize(cols: number, rows: number): void {
+ this._refreshRowElements(cols, rows);
+ this._updateDimensions();
+ }
+
+ public onCharSizeChanged(): void {
+ this._updateDimensions();
+ }
+
+ public onBlur(): void {
+ this._rowContainer.classList.remove(FOCUS_CLASS);
+ }
+
+ public onFocus(): void {
+ this._rowContainer.classList.add(FOCUS_CLASS);
+ }
+
+ public onSelectionChanged(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]);
+ }
+
+ // Selection does not exist
+ if (!start || !end) {
+ return;
+ }
+
+ // Translate from buffer position to viewport position
+ const viewportStartRow = start[1] - this._bufferService.buffer.ydisp;
+ const viewportEndRow = end[1] - this._bufferService.buffer.ydisp;
+ const viewportCappedStartRow = Math.max(viewportStartRow, 0);
+ const viewportCappedEndRow = Math.min(viewportEndRow, this._bufferService.rows - 1);
+
+ // No need to draw the selection
+ if (viewportCappedStartRow >= this._bufferService.rows || viewportCappedEndRow < 0) {
+ return;
+ }
+
+ // Create the selections
+ const documentFragment = document.createDocumentFragment();
+
+ if (columnSelectMode) {
+ documentFragment.appendChild(
+ this._createSelectionElement(viewportCappedStartRow, start[0], end[0], viewportCappedEndRow - viewportCappedStartRow + 1)
+ );
+ } else {
+ // Draw first row
+ const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0;
+ const endCol = viewportCappedStartRow === viewportEndRow ? end[0] : this._bufferService.cols;
+ documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow, startCol, endCol));
+ // Draw middle rows
+ const middleRowsCount = viewportCappedEndRow - viewportCappedStartRow - 1;
+ documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow + 1, 0, this._bufferService.cols, middleRowsCount));
+ // Draw final row
+ if (viewportCappedStartRow !== viewportCappedEndRow) {
+ // Only draw viewportEndRow if it's not the same as viewporttartRow
+ const endCol = viewportEndRow === viewportCappedEndRow ? end[0] : this._bufferService.cols;
+ documentFragment.appendChild(this._createSelectionElement(viewportCappedEndRow, 0, endCol));
+ }
+ }
+ this._selectionContainer.appendChild(documentFragment);
+ }
+
+ /**
+ * Creates a selection element at the specified position.
+ * @param row The row of the selection.
+ * @param colStart The start column.
+ * @param colEnd The end columns.
+ */
+ private _createSelectionElement(row: number, colStart: number, colEnd: number, rowCount: number = 1): HTMLElement {
+ const element = document.createElement('div');
+ element.style.height = `${rowCount * this.dimensions.actualCellHeight}px`;
+ element.style.top = `${row * this.dimensions.actualCellHeight}px`;
+ element.style.left = `${colStart * this.dimensions.actualCellWidth}px`;
+ element.style.width = `${this.dimensions.actualCellWidth * (colEnd - colStart)}px`;
+ return element;
+ }
+
+ public onCursorMove(): void {
+ // No-op, the cursor is drawn when rows are drawn
+ }
+
+ public onOptionsChanged(): void {
+ // Force a refresh
+ this._updateDimensions();
+ this._injectCss();
+ }
+
+ public clear(): void {
+ for (const e of this._rowElements) {
+ e.innerText = '';
+ }
+ }
+
+ public renderRows(start: number, end: number): void {
+ const cursorAbsoluteY = this._bufferService.buffer.ybase + this._bufferService.buffer.y;
+ const cursorX = Math.min(this._bufferService.buffer.x, this._bufferService.cols - 1);
+ const cursorBlink = this._optionsService.rawOptions.cursorBlink;
+
+ for (let y = start; y <= end; y++) {
+ const rowElement = this._rowElements[y];
+ rowElement.innerText = '';
+
+ const row = y + this._bufferService.buffer.ydisp;
+ const lineData = this._bufferService.buffer.lines.get(row);
+ const cursorStyle = this._optionsService.rawOptions.cursorStyle;
+ rowElement.appendChild(this._rowFactory.createRow(lineData!, row, row === cursorAbsoluteY, cursorStyle, cursorX, cursorBlink, this.dimensions.actualCellWidth, this._bufferService.cols));
+ }
+ }
+
+ private get _terminalSelector(): string {
+ return `.${TERMINAL_CLASS_PREFIX}${this._terminalClass}`;
+ }
+
+ private _onLinkHover(e: ILinkifierEvent): void {
+ this._setCellUnderline(e.x1, e.x2, e.y1, e.y2, e.cols, true);
+ }
+
+ private _onLinkLeave(e: ILinkifierEvent): void {
+ this._setCellUnderline(e.x1, e.x2, e.y1, e.y2, e.cols, false);
+ }
+
+ private _setCellUnderline(x: number, x2: number, y: number, y2: number, cols: number, enabled: boolean): void {
+ while (x !== x2 || y !== y2) {
+ const row = this._rowElements[y];
+ if (!row) {
+ return;
+ }
+ const span = row.children[x] as HTMLElement;
+ if (span) {
+ span.style.textDecoration = enabled ? 'underline' : 'none';
+ }
+ if (++x >= cols) {
+ x = 0;
+ y++;
+ }
+ }
+ }
+}
diff --git a/node_modules/xterm/src/browser/renderer/dom/DomRendererRowFactory.ts b/node_modules/xterm/src/browser/renderer/dom/DomRendererRowFactory.ts
new file mode 100644
index 0000000..fda800a
--- /dev/null
+++ b/node_modules/xterm/src/browser/renderer/dom/DomRendererRowFactory.ts
@@ -0,0 +1,259 @@
+/**
+ * Copyright (c) 2018 The xterm.js authors. All rights reserved.
+ * @license MIT
+ */
+
+import { IBufferLine } from 'common/Types';
+import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants';
+import { NULL_CELL_CODE, WHITESPACE_CELL_CHAR, Attributes } from 'common/buffer/Constants';
+import { CellData } from 'common/buffer/CellData';
+import { ICoreService, IOptionsService } from 'common/services/Services';
+import { color, rgba } from 'browser/Color';
+import { IColorSet, IColor } from 'browser/Types';
+import { ICharacterJoinerService } from 'browser/services/Services';
+import { JoinedCellData } from 'browser/services/CharacterJoinerService';
+
+export const BOLD_CLASS = 'xterm-bold';
+export const DIM_CLASS = 'xterm-dim';
+export const ITALIC_CLASS = 'xterm-italic';
+export const UNDERLINE_CLASS = 'xterm-underline';
+export const STRIKETHROUGH_CLASS = 'xterm-strikethrough';
+export const CURSOR_CLASS = 'xterm-cursor';
+export const CURSOR_BLINK_CLASS = 'xterm-cursor-blink';
+export const CURSOR_STYLE_BLOCK_CLASS = 'xterm-cursor-block';
+export const CURSOR_STYLE_BAR_CLASS = 'xterm-cursor-bar';
+export const CURSOR_STYLE_UNDERLINE_CLASS = 'xterm-cursor-underline';
+
+export class DomRendererRowFactory {
+ private _workCell: CellData = new CellData();
+
+ constructor(
+ private readonly _document: Document,
+ private _colors: IColorSet,
+ @ICharacterJoinerService private readonly _characterJoinerService: ICharacterJoinerService,
+ @IOptionsService private readonly _optionsService: IOptionsService,
+ @ICoreService private readonly _coreService: ICoreService
+ ) {
+ }
+
+ public setColors(colors: IColorSet): void {
+ this._colors = colors;
+ }
+
+ public createRow(lineData: IBufferLine, row: number, isCursorRow: boolean, cursorStyle: string | undefined, cursorX: number, cursorBlink: boolean, cellWidth: number, cols: number): DocumentFragment {
+ const fragment = this._document.createDocumentFragment();
+
+ const joinedRanges = this._characterJoinerService.getJoinedCharacters(row);
+ // Find the line length first, this prevents the need to output a bunch of
+ // empty cells at the end. This cannot easily be integrated into the main
+ // loop below because of the colCount feature (which can be removed after we
+ // properly support reflow and disallow data to go beyond the right-side of
+ // the viewport).
+ let lineLength = 0;
+ for (let x = Math.min(lineData.length, cols) - 1; x >= 0; x--) {
+ if (lineData.loadCell(x, this._workCell).getCode() !== NULL_CELL_CODE || (isCursorRow && x === cursorX)) {
+ lineLength = x + 1;
+ break;
+ }
+ }
+
+ for (let x = 0; x < lineLength; x++) {
+ lineData.loadCell(x, this._workCell);
+ let width = this._workCell.getWidth();
+
+ // The character to the left is a wide character, drawing is owned by the char at x-1
+ if (width === 0) {
+ continue;
+ }
+
+ // If true, indicates that the current character(s) to draw were joined.
+ let isJoined = false;
+ let lastCharX = x;
+
+ // Process any joined character ranges as needed. Because of how the
+ // ranges are produced, we know that they are valid for the characters
+ // and attributes of our input.
+ let cell = this._workCell;
+ if (joinedRanges.length > 0 && x === joinedRanges[0][0]) {
+ isJoined = true;
+ const range = joinedRanges.shift()!;
+
+ // We already know the exact start and end column of the joined range,
+ // so we get the string and width representing it directly
+ cell = new JoinedCellData(
+ this._workCell,
+ lineData.translateToString(true, range[0], range[1]),
+ range[1] - range[0]
+ );
+
+ // Skip over the cells occupied by this range in the loop
+ lastCharX = range[1] - 1;
+
+ // Recalculate width
+ width = cell.getWidth();
+ }
+
+ const charElement = this._document.createElement('span');
+ if (width > 1) {
+ charElement.style.width = `${cellWidth * width}px`;
+ }
+
+ if (isJoined) {
+ // Ligatures in the DOM renderer must use display inline, as they may not show with
+ // inline-block if they are outside the bounds of the element
+ charElement.style.display = 'inline';
+
+ // The DOM renderer colors the background of the cursor but for ligatures all cells are
+ // joined. The workaround here is to show a cursor around the whole ligature so it shows up,
+ // the cursor looks the same when on any character of the ligature though
+ if (cursorX >= x && cursorX <= lastCharX) {
+ cursorX = x;
+ }
+ }
+
+ if (!this._coreService.isCursorHidden && isCursorRow && x === cursorX) {
+ charElement.classList.add(CURSOR_CLASS);
+
+ if (cursorBlink) {
+ charElement.classList.add(CURSOR_BLINK_CLASS);
+ }
+
+ switch (cursorStyle) {
+ case 'bar':
+ charElement.classList.add(CURSOR_STYLE_BAR_CLASS);
+ break;
+ case 'underline':
+ charElement.classList.add(CURSOR_STYLE_UNDERLINE_CLASS);
+ break;
+ default:
+ charElement.classList.add(CURSOR_STYLE_BLOCK_CLASS);
+ break;
+ }
+ }
+
+ if (cell.isBold()) {
+ charElement.classList.add(BOLD_CLASS);
+ }
+
+ if (cell.isItalic()) {
+ charElement.classList.add(ITALIC_CLASS);
+ }
+
+ if (cell.isDim()) {
+ charElement.classList.add(DIM_CLASS);
+ }
+
+ if (cell.isUnderline()) {
+ charElement.classList.add(UNDERLINE_CLASS);
+ }
+
+ if (cell.isInvisible()) {
+ charElement.textContent = WHITESPACE_CELL_CHAR;
+ } else {
+ charElement.textContent = cell.getChars() || WHITESPACE_CELL_CHAR;
+ }
+
+ if (cell.isStrikethrough()) {
+ charElement.classList.add(STRIKETHROUGH_CLASS);
+ }
+
+ let fg = cell.getFgColor();
+ let fgColorMode = cell.getFgColorMode();
+ let bg = cell.getBgColor();
+ let bgColorMode = cell.getBgColorMode();
+ const isInverse = !!cell.isInverse();
+ if (isInverse) {
+ const temp = fg;
+ fg = bg;
+ bg = temp;
+ const temp2 = fgColorMode;
+ fgColorMode = bgColorMode;
+ bgColorMode = temp2;
+ }
+
+ // Foreground
+ switch (fgColorMode) {
+ case Attributes.CM_P16:
+ case Attributes.CM_P256:
+ if (cell.isBold() && fg < 8 && this._optionsService.rawOptions.drawBoldTextInBrightColors) {
+ fg += 8;
+ }
+ if (!this._applyMinimumContrast(charElement, this._colors.background, this._colors.ansi[fg])) {
+ charElement.classList.add(`xterm-fg-${fg}`);
+ }
+ break;
+ case Attributes.CM_RGB:
+ const color = rgba.toColor(
+ (fg >> 16) & 0xFF,
+ (fg >> 8) & 0xFF,
+ (fg ) & 0xFF
+ );
+ if (!this._applyMinimumContrast(charElement, this._colors.background, color)) {
+ this._addStyle(charElement, `color:#${padStart(fg.toString(16), '0', 6)}`);
+ }
+ break;
+ case Attributes.CM_DEFAULT:
+ default:
+ if (!this._applyMinimumContrast(charElement, this._colors.background, this._colors.foreground)) {
+ if (isInverse) {
+ charElement.classList.add(`xterm-fg-${INVERTED_DEFAULT_COLOR}`);
+ }
+ }
+ }
+
+ // Background
+ switch (bgColorMode) {
+ case Attributes.CM_P16:
+ case Attributes.CM_P256:
+ charElement.classList.add(`xterm-bg-${bg}`);
+ break;
+ case Attributes.CM_RGB:
+ this._addStyle(charElement, `background-color:#${padStart(bg.toString(16), '0', 6)}`);
+ break;
+ case Attributes.CM_DEFAULT:
+ default:
+ if (isInverse) {
+ charElement.classList.add(`xterm-bg-${INVERTED_DEFAULT_COLOR}`);
+ }
+ }
+
+ fragment.appendChild(charElement);
+
+ x = lastCharX;
+ }
+ return fragment;
+ }
+
+ private _applyMinimumContrast(element: HTMLElement, bg: IColor, fg: IColor): boolean {
+ if (this._optionsService.rawOptions.minimumContrastRatio === 1) {
+ return false;
+ }
+
+ // Try get from cache first
+ let adjustedColor = this._colors.contrastCache.getColor(this._workCell.bg, this._workCell.fg);
+
+ // Calculate and store in cache
+ if (adjustedColor === undefined) {
+ adjustedColor = color.ensureContrastRatio(bg, fg, this._optionsService.rawOptions.minimumContrastRatio);
+ this._colors.contrastCache.setColor(this._workCell.bg, this._workCell.fg, adjustedColor ?? null);
+ }
+
+ if (adjustedColor) {
+ this._addStyle(element, `color:${adjustedColor.css}`);
+ return true;
+ }
+
+ return false;
+ }
+
+ private _addStyle(element: HTMLElement, style: string): void {
+ element.setAttribute('style', `${element.getAttribute('style') || ''}${style};`);
+ }
+}
+
+function padStart(text: string, padChar: string, length: number): string {
+ while (text.length < length) {
+ text = padChar + text;
+ }
+ return text;
+}