diff options
author | Anthony Schneider <tonyschneider3@gmail.com> | 2022-02-11 19:40:35 -0600 |
---|---|---|
committer | Anthony Schneider <tonyschneider3@gmail.com> | 2022-02-11 19:40:35 -0600 |
commit | b52feccdcc58c1f4583c8542632d6c026335dea7 (patch) | |
tree | 5e242dd13ed4bbfff85a07109ef826f80874e2a6 /node_modules/xterm/src | |
parent | 94862321e2e4a58e3209c037e8061f0435b3aa82 (diff) |
Changed javascript to be in its own file. Began (messy) setup for terminal.
Diffstat (limited to 'node_modules/xterm/src')
103 files changed, 22796 insertions, 0 deletions
diff --git a/node_modules/xterm/src/browser/AccessibilityManager.ts b/node_modules/xterm/src/browser/AccessibilityManager.ts new file mode 100644 index 0000000..eda29c0 --- /dev/null +++ b/node_modules/xterm/src/browser/AccessibilityManager.ts @@ -0,0 +1,302 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import * as Strings from 'browser/LocalizableStrings'; +import { ITerminal, IRenderDebouncer } from 'browser/Types'; +import { IBuffer } from 'common/buffer/Types'; +import { isMac } from 'common/Platform'; +import { TimeBasedDebouncer } from 'browser/TimeBasedDebouncer'; +import { addDisposableDomListener } from 'browser/Lifecycle'; +import { Disposable } from 'common/Lifecycle'; +import { ScreenDprMonitor } from 'browser/ScreenDprMonitor'; +import { IRenderService } from 'browser/services/Services'; +import { removeElementFromParent } from 'browser/Dom'; + +const MAX_ROWS_TO_READ = 20; + +const enum BoundaryPosition { + TOP, + BOTTOM +} + +export class AccessibilityManager extends Disposable { + private _accessibilityTreeRoot: HTMLElement; + private _rowContainer: HTMLElement; + private _rowElements: HTMLElement[]; + private _liveRegion: HTMLElement; + private _liveRegionLineCount: number = 0; + + private _renderRowsDebouncer: IRenderDebouncer; + private _screenDprMonitor: ScreenDprMonitor; + + private _topBoundaryFocusListener: (e: FocusEvent) => void; + private _bottomBoundaryFocusListener: (e: FocusEvent) => void; + + /** + * This queue has a character pushed to it for keys that are pressed, if the + * next character added to the terminal is equal to the key char then it is + * not announced (added to live region) because it has already been announced + * by the textarea event (which cannot be canceled). There are some race + * condition cases if there is typing while data is streaming, but this covers + * the main case of typing into the prompt and inputting the answer to a + * question (Y/N, etc.). + */ + private _charsToConsume: string[] = []; + + private _charsToAnnounce: string = ''; + + constructor( + private readonly _terminal: ITerminal, + private readonly _renderService: IRenderService + ) { + super(); + this._accessibilityTreeRoot = document.createElement('div'); + this._accessibilityTreeRoot.setAttribute('role', 'document'); + this._accessibilityTreeRoot.classList.add('xterm-accessibility'); + this._accessibilityTreeRoot.tabIndex = 0; + + this._rowContainer = document.createElement('div'); + this._rowContainer.setAttribute('role', 'list'); + this._rowContainer.classList.add('xterm-accessibility-tree'); + this._rowElements = []; + for (let i = 0; i < this._terminal.rows; i++) { + this._rowElements[i] = this._createAccessibilityTreeNode(); + this._rowContainer.appendChild(this._rowElements[i]); + } + + this._topBoundaryFocusListener = e => this._onBoundaryFocus(e, BoundaryPosition.TOP); + this._bottomBoundaryFocusListener = e => this._onBoundaryFocus(e, BoundaryPosition.BOTTOM); + this._rowElements[0].addEventListener('focus', this._topBoundaryFocusListener); + this._rowElements[this._rowElements.length - 1].addEventListener('focus', this._bottomBoundaryFocusListener); + + this._refreshRowsDimensions(); + this._accessibilityTreeRoot.appendChild(this._rowContainer); + + this._renderRowsDebouncer = new TimeBasedDebouncer(this._renderRows.bind(this)); + this._refreshRows(); + + this._liveRegion = document.createElement('div'); + this._liveRegion.classList.add('live-region'); + this._liveRegion.setAttribute('aria-live', 'assertive'); + this._accessibilityTreeRoot.appendChild(this._liveRegion); + + if (!this._terminal.element) { + throw new Error('Cannot enable accessibility before Terminal.open'); + } + this._terminal.element.insertAdjacentElement('afterbegin', this._accessibilityTreeRoot); + + this.register(this._renderRowsDebouncer); + this.register(this._terminal.onResize(e => this._onResize(e.rows))); + this.register(this._terminal.onRender(e => this._refreshRows(e.start, e.end))); + this.register(this._terminal.onScroll(() => this._refreshRows())); + // Line feed is an issue as the prompt won't be read out after a command is run + this.register(this._terminal.onA11yChar(char => this._onChar(char))); + this.register(this._terminal.onLineFeed(() => this._onChar('\n'))); + this.register(this._terminal.onA11yTab(spaceCount => this._onTab(spaceCount))); + this.register(this._terminal.onKey(e => this._onKey(e.key))); + this.register(this._terminal.onBlur(() => this._clearLiveRegion())); + this.register(this._renderService.onDimensionsChange(() => this._refreshRowsDimensions())); + + this._screenDprMonitor = new ScreenDprMonitor(); + this.register(this._screenDprMonitor); + this._screenDprMonitor.setListener(() => this._refreshRowsDimensions()); + // This shouldn't be needed on modern browsers but is present in case the + // media query that drives the ScreenDprMonitor isn't supported + this.register(addDisposableDomListener(window, 'resize', () => this._refreshRowsDimensions())); + } + + public dispose(): void { + super.dispose(); + removeElementFromParent(this._accessibilityTreeRoot); + this._rowElements.length = 0; + } + + private _onBoundaryFocus(e: FocusEvent, position: BoundaryPosition): void { + const boundaryElement = e.target as HTMLElement; + const beforeBoundaryElement = this._rowElements[position === BoundaryPosition.TOP ? 1 : this._rowElements.length - 2]; + + // Don't scroll if the buffer top has reached the end in that direction + const posInSet = boundaryElement.getAttribute('aria-posinset'); + const lastRowPos = position === BoundaryPosition.TOP ? '1' : `${this._terminal.buffer.lines.length}`; + if (posInSet === lastRowPos) { + return; + } + + // Don't scroll when the last focused item was not the second row (focus is going the other + // direction) + if (e.relatedTarget !== beforeBoundaryElement) { + return; + } + + // Remove old boundary element from array + let topBoundaryElement: HTMLElement; + let bottomBoundaryElement: HTMLElement; + if (position === BoundaryPosition.TOP) { + topBoundaryElement = boundaryElement; + bottomBoundaryElement = this._rowElements.pop()!; + this._rowContainer.removeChild(bottomBoundaryElement); + } else { + topBoundaryElement = this._rowElements.shift()!; + bottomBoundaryElement = boundaryElement; + this._rowContainer.removeChild(topBoundaryElement); + } + + // Remove listeners from old boundary elements + topBoundaryElement.removeEventListener('focus', this._topBoundaryFocusListener); + bottomBoundaryElement.removeEventListener('focus', this._bottomBoundaryFocusListener); + + // Add new element to array/DOM + if (position === BoundaryPosition.TOP) { + const newElement = this._createAccessibilityTreeNode(); + this._rowElements.unshift(newElement); + this._rowContainer.insertAdjacentElement('afterbegin', newElement); + } else { + const newElement = this._createAccessibilityTreeNode(); + this._rowElements.push(newElement); + this._rowContainer.appendChild(newElement); + } + + // Add listeners to new boundary elements + this._rowElements[0].addEventListener('focus', this._topBoundaryFocusListener); + this._rowElements[this._rowElements.length - 1].addEventListener('focus', this._bottomBoundaryFocusListener); + + // Scroll up + this._terminal.scrollLines(position === BoundaryPosition.TOP ? -1 : 1); + + // Focus new boundary before element + this._rowElements[position === BoundaryPosition.TOP ? 1 : this._rowElements.length - 2].focus(); + + // Prevent the standard behavior + e.preventDefault(); + e.stopImmediatePropagation(); + } + + private _onResize(rows: number): void { + // Remove bottom boundary listener + this._rowElements[this._rowElements.length - 1].removeEventListener('focus', this._bottomBoundaryFocusListener); + + // Grow rows as required + for (let i = this._rowContainer.children.length; i < this._terminal.rows; i++) { + this._rowElements[i] = this._createAccessibilityTreeNode(); + this._rowContainer.appendChild(this._rowElements[i]); + } + // Shrink rows as required + while (this._rowElements.length > rows) { + this._rowContainer.removeChild(this._rowElements.pop()!); + } + + // Add bottom boundary listener + this._rowElements[this._rowElements.length - 1].addEventListener('focus', this._bottomBoundaryFocusListener); + + this._refreshRowsDimensions(); + } + + private _createAccessibilityTreeNode(): HTMLElement { + const element = document.createElement('div'); + element.setAttribute('role', 'listitem'); + element.tabIndex = -1; + this._refreshRowDimensions(element); + return element; + } + + private _onTab(spaceCount: number): void { + for (let i = 0; i < spaceCount; i++) { + this._onChar(' '); + } + } + + private _onChar(char: string): void { + if (this._liveRegionLineCount < MAX_ROWS_TO_READ + 1) { + if (this._charsToConsume.length > 0) { + // Have the screen reader ignore the char if it was just input + const shiftedChar = this._charsToConsume.shift(); + if (shiftedChar !== char) { + this._charsToAnnounce += char; + } + } else { + this._charsToAnnounce += char; + } + + if (char === '\n') { + this._liveRegionLineCount++; + if (this._liveRegionLineCount === MAX_ROWS_TO_READ + 1) { + this._liveRegion.textContent += Strings.tooMuchOutput; + } + } + + // Only detach/attach on mac as otherwise messages can go unaccounced + if (isMac) { + if (this._liveRegion.textContent && this._liveRegion.textContent.length > 0 && !this._liveRegion.parentNode) { + setTimeout(() => { + this._accessibilityTreeRoot.appendChild(this._liveRegion); + }, 0); + } + } + } + } + + private _clearLiveRegion(): void { + this._liveRegion.textContent = ''; + this._liveRegionLineCount = 0; + + // Only detach/attach on mac as otherwise messages can go unaccounced + if (isMac) { + removeElementFromParent(this._liveRegion); + } + } + + private _onKey(keyChar: string): void { + this._clearLiveRegion(); + this._charsToConsume.push(keyChar); + } + + private _refreshRows(start?: number, end?: number): void { + this._renderRowsDebouncer.refresh(start, end, this._terminal.rows); + } + + private _renderRows(start: number, end: number): void { + const buffer: IBuffer = this._terminal.buffer; + const setSize = buffer.lines.length.toString(); + for (let i = start; i <= end; i++) { + const lineData = buffer.translateBufferLineToString(buffer.ydisp + i, true); + const posInSet = (buffer.ydisp + i + 1).toString(); + const element = this._rowElements[i]; + if (element) { + if (lineData.length === 0) { + element.innerText = '\u00a0'; + } else { + element.textContent = lineData; + } + element.setAttribute('aria-posinset', posInSet); + element.setAttribute('aria-setsize', setSize); + } + } + this._announceCharacters(); + } + + private _refreshRowsDimensions(): void { + if (!this._renderService.dimensions.actualCellHeight) { + return; + } + if (this._rowElements.length !== this._terminal.rows) { + this._onResize(this._terminal.rows); + } + for (let i = 0; i < this._terminal.rows; i++) { + this._refreshRowDimensions(this._rowElements[i]); + } + } + + private _refreshRowDimensions(element: HTMLElement): void { + element.style.height = `${this._renderService.dimensions.actualCellHeight}px`; + } + + private _announceCharacters(): void { + if (this._charsToAnnounce.length === 0) { + return; + } + this._liveRegion.textContent += this._charsToAnnounce; + this._charsToAnnounce = ''; + } +} diff --git a/node_modules/xterm/src/browser/Clipboard.ts b/node_modules/xterm/src/browser/Clipboard.ts new file mode 100644 index 0000000..29e865c --- /dev/null +++ b/node_modules/xterm/src/browser/Clipboard.ts @@ -0,0 +1,99 @@ +/** + * Copyright (c) 2016 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { ISelectionService } from 'browser/services/Services'; +import { ICoreService } from 'common/services/Services'; + +/** + * Prepares text to be pasted into the terminal by normalizing the line endings + * @param text The pasted text that needs processing before inserting into the terminal + */ +export function prepareTextForTerminal(text: string): string { + return text.replace(/\r?\n/g, '\r'); +} + +/** + * Bracket text for paste, if necessary, as per https://cirw.in/blog/bracketed-paste + * @param text The pasted text to bracket + */ +export function bracketTextForPaste(text: string, bracketedPasteMode: boolean): string { + if (bracketedPasteMode) { + return '\x1b[200~' + text + '\x1b[201~'; + } + return text; +} + +/** + * Binds copy functionality to the given terminal. + * @param ev The original copy event to be handled + */ +export function copyHandler(ev: ClipboardEvent, selectionService: ISelectionService): void { + if (ev.clipboardData) { + ev.clipboardData.setData('text/plain', selectionService.selectionText); + } + // Prevent or the original text will be copied. + ev.preventDefault(); +} + +/** + * Redirect the clipboard's data to the terminal's input handler. + * @param ev The original paste event to be handled + * @param term The terminal on which to apply the handled paste event + */ +export function handlePasteEvent(ev: ClipboardEvent, textarea: HTMLTextAreaElement, coreService: ICoreService): void { + ev.stopPropagation(); + if (ev.clipboardData) { + const text = ev.clipboardData.getData('text/plain'); + paste(text, textarea, coreService); + } +} + +export function paste(text: string, textarea: HTMLTextAreaElement, coreService: ICoreService): void { + text = prepareTextForTerminal(text); + text = bracketTextForPaste(text, coreService.decPrivateModes.bracketedPasteMode); + coreService.triggerDataEvent(text, true); + textarea.value = ''; +} + +/** + * Moves the textarea under the mouse cursor and focuses it. + * @param ev The original right click event to be handled. + * @param textarea The terminal's textarea. + */ +export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextAreaElement, screenElement: HTMLElement): void { + + // Calculate textarea position relative to the screen element + const pos = screenElement.getBoundingClientRect(); + const left = ev.clientX - pos.left - 10; + const top = ev.clientY - pos.top - 10; + + // Bring textarea at the cursor position + textarea.style.width = '20px'; + textarea.style.height = '20px'; + textarea.style.left = `${left}px`; + textarea.style.top = `${top}px`; + textarea.style.zIndex = '1000'; + + textarea.focus(); +} + +/** + * Bind to right-click event and allow right-click copy and paste. + * @param ev The original right click event to be handled. + * @param textarea The terminal's textarea. + * @param selectionService The terminal's selection manager. + * @param shouldSelectWord If true and there is no selection the current word will be selected + */ +export function rightClickHandler(ev: MouseEvent, textarea: HTMLTextAreaElement, screenElement: HTMLElement, selectionService: ISelectionService, shouldSelectWord: boolean): void { + moveTextAreaUnderMouseCursor(ev, textarea, screenElement); + + if (shouldSelectWord) { + selectionService.rightClickSelect(ev); + } + + // Get textarea ready to copy from the context menu + textarea.value = selectionService.selectionText; + textarea.select(); +} diff --git a/node_modules/xterm/src/browser/Color.ts b/node_modules/xterm/src/browser/Color.ts new file mode 100644 index 0000000..32e311d --- /dev/null +++ b/node_modules/xterm/src/browser/Color.ts @@ -0,0 +1,236 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IColor } from 'browser/Types'; +import { IColorRGB } from 'common/Types'; + +/** + * Helper functions where the source type is "channels" (individual color channels as numbers). + */ +export namespace channels { + export function toCss(r: number, g: number, b: number, a?: number): string { + if (a !== undefined) { + return `#${toPaddedHex(r)}${toPaddedHex(g)}${toPaddedHex(b)}${toPaddedHex(a)}`; + } + return `#${toPaddedHex(r)}${toPaddedHex(g)}${toPaddedHex(b)}`; + } + + export function toRgba(r: number, g: number, b: number, a: number = 0xFF): number { + // Note: The aggregated number is RGBA32 (BE), thus needs to be converted to ABGR32 + // on LE systems, before it can be used for direct 32-bit buffer writes. + // >>> 0 forces an unsigned int + return (r << 24 | g << 16 | b << 8 | a) >>> 0; + } +} + +/** + * Helper functions where the source type is `IColor`. + */ +export namespace color { + export function blend(bg: IColor, fg: IColor): IColor { + const a = (fg.rgba & 0xFF) / 255; + if (a === 1) { + return { + css: fg.css, + rgba: fg.rgba + }; + } + const fgR = (fg.rgba >> 24) & 0xFF; + const fgG = (fg.rgba >> 16) & 0xFF; + const fgB = (fg.rgba >> 8) & 0xFF; + const bgR = (bg.rgba >> 24) & 0xFF; + const bgG = (bg.rgba >> 16) & 0xFF; + const bgB = (bg.rgba >> 8) & 0xFF; + const r = bgR + Math.round((fgR - bgR) * a); + const g = bgG + Math.round((fgG - bgG) * a); + const b = bgB + Math.round((fgB - bgB) * a); + const css = channels.toCss(r, g, b); + const rgba = channels.toRgba(r, g, b); + return { css, rgba }; + } + + export function isOpaque(color: IColor): boolean { + return (color.rgba & 0xFF) === 0xFF; + } + + export function ensureContrastRatio(bg: IColor, fg: IColor, ratio: number): IColor | undefined { + const result = rgba.ensureContrastRatio(bg.rgba, fg.rgba, ratio); + if (!result) { + return undefined; + } + return rgba.toColor( + (result >> 24 & 0xFF), + (result >> 16 & 0xFF), + (result >> 8 & 0xFF) + ); + } + + export function opaque(color: IColor): IColor { + const rgbaColor = (color.rgba | 0xFF) >>> 0; + const [r, g, b] = rgba.toChannels(rgbaColor); + return { + css: channels.toCss(r, g, b), + rgba: rgbaColor + }; + } + + export function opacity(color: IColor, opacity: number): IColor { + const a = Math.round(opacity * 0xFF); + const [r, g, b] = rgba.toChannels(color.rgba); + return { + css: channels.toCss(r, g, b, a), + rgba: channels.toRgba(r, g, b, a) + }; + } + + export function toColorRGB(color: IColor): IColorRGB { + return [(color.rgba >> 24) & 0xFF, (color.rgba >> 16) & 0xFF, (color.rgba >> 8) & 0xFF]; + } +} + +/** + * Helper functions where the source type is "css" (string: '#rgb', '#rgba', '#rrggbb', '#rrggbbaa'). + */ +export namespace css { + export function toColor(css: string): IColor { + switch (css.length) { + case 7: // #rrggbb + return { + css, + rgba: (parseInt(css.slice(1), 16) << 8 | 0xFF) >>> 0 + }; + case 9: // #rrggbbaa + return { + css, + rgba: parseInt(css.slice(1), 16) >>> 0 + }; + } + throw new Error('css.toColor: Unsupported css format'); + } +} + +/** + * Helper functions where the source type is "rgb" (number: 0xrrggbb). + */ +export namespace rgb { + /** + * Gets the relative luminance of an RGB color, this is useful in determining the contrast ratio + * between two colors. + * @param rgb The color to use. + * @see https://www.w3.org/TR/WCAG20/#relativeluminancedef + */ + export function relativeLuminance(rgb: number): number { + return relativeLuminance2( + (rgb >> 16) & 0xFF, + (rgb >> 8 ) & 0xFF, + (rgb ) & 0xFF); + } + + /** + * Gets the relative luminance of an RGB color, this is useful in determining the contrast ratio + * between two colors. + * @param r The red channel (0x00 to 0xFF). + * @param g The green channel (0x00 to 0xFF). + * @param b The blue channel (0x00 to 0xFF). + * @see https://www.w3.org/TR/WCAG20/#relativeluminancedef + */ + export function relativeLuminance2(r: number, g: number, b: number): number { + const rs = r / 255; + const gs = g / 255; + const bs = b / 255; + const rr = rs <= 0.03928 ? rs / 12.92 : Math.pow((rs + 0.055) / 1.055, 2.4); + const rg = gs <= 0.03928 ? gs / 12.92 : Math.pow((gs + 0.055) / 1.055, 2.4); + const rb = bs <= 0.03928 ? bs / 12.92 : Math.pow((bs + 0.055) / 1.055, 2.4); + return rr * 0.2126 + rg * 0.7152 + rb * 0.0722; + } +} + +/** + * Helper functions where the source type is "rgba" (number: 0xrrggbbaa). + */ +export namespace rgba { + export function ensureContrastRatio(bgRgba: number, fgRgba: number, ratio: number): number | undefined { + const bgL = rgb.relativeLuminance(bgRgba >> 8); + const fgL = rgb.relativeLuminance(fgRgba >> 8); + const cr = contrastRatio(bgL, fgL); + if (cr < ratio) { + if (fgL < bgL) { + return reduceLuminance(bgRgba, fgRgba, ratio); + } + return increaseLuminance(bgRgba, fgRgba, ratio); + } + return undefined; + } + + export function reduceLuminance(bgRgba: number, fgRgba: number, ratio: number): number { + // This is a naive but fast approach to reducing luminance as converting to + // HSL and back is expensive + const bgR = (bgRgba >> 24) & 0xFF; + const bgG = (bgRgba >> 16) & 0xFF; + const bgB = (bgRgba >> 8) & 0xFF; + let fgR = (fgRgba >> 24) & 0xFF; + let fgG = (fgRgba >> 16) & 0xFF; + let fgB = (fgRgba >> 8) & 0xFF; + let cr = contrastRatio(rgb.relativeLuminance2(fgR, fgB, fgG), rgb.relativeLuminance2(bgR, bgG, bgB)); + while (cr < ratio && (fgR > 0 || fgG > 0 || fgB > 0)) { + // Reduce by 10% until the ratio is hit + fgR -= Math.max(0, Math.ceil(fgR * 0.1)); + fgG -= Math.max(0, Math.ceil(fgG * 0.1)); + fgB -= Math.max(0, Math.ceil(fgB * 0.1)); + cr = contrastRatio(rgb.relativeLuminance2(fgR, fgB, fgG), rgb.relativeLuminance2(bgR, bgG, bgB)); + } + return (fgR << 24 | fgG << 16 | fgB << 8 | 0xFF) >>> 0; + } + + export function increaseLuminance(bgRgba: number, fgRgba: number, ratio: number): number { + // This is a naive but fast approach to increasing luminance as converting to + // HSL and back is expensive + const bgR = (bgRgba >> 24) & 0xFF; + const bgG = (bgRgba >> 16) & 0xFF; + const bgB = (bgRgba >> 8) & 0xFF; + let fgR = (fgRgba >> 24) & 0xFF; + let fgG = (fgRgba >> 16) & 0xFF; + let fgB = (fgRgba >> 8) & 0xFF; + let cr = contrastRatio(rgb.relativeLuminance2(fgR, fgB, fgG), rgb.relativeLuminance2(bgR, bgG, bgB)); + while (cr < ratio && (fgR < 0xFF || fgG < 0xFF || fgB < 0xFF)) { + // Increase by 10% until the ratio is hit + fgR = Math.min(0xFF, fgR + Math.ceil((255 - fgR) * 0.1)); + fgG = Math.min(0xFF, fgG + Math.ceil((255 - fgG) * 0.1)); + fgB = Math.min(0xFF, fgB + Math.ceil((255 - fgB) * 0.1)); + cr = contrastRatio(rgb.relativeLuminance2(fgR, fgB, fgG), rgb.relativeLuminance2(bgR, bgG, bgB)); + } + return (fgR << 24 | fgG << 16 | fgB << 8 | 0xFF) >>> 0; + } + + // FIXME: Move this to channels NS? + export function toChannels(value: number): [number, number, number, number] { + return [(value >> 24) & 0xFF, (value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF]; + } + + export function toColor(r: number, g: number, b: number): IColor { + return { + css: channels.toCss(r, g, b), + rgba: channels.toRgba(r, g, b) + }; + } +} + +export function toPaddedHex(c: number): string { + const s = c.toString(16); + return s.length < 2 ? '0' + s : s; +} + +/** + * Gets the contrast ratio between two relative luminance values. + * @param l1 The first relative luminance. + * @param l2 The first relative luminance. + * @see https://www.w3.org/TR/WCAG20/#contrast-ratiodef + */ +export function contrastRatio(l1: number, l2: number): number { + if (l1 < l2) { + return (l2 + 0.05) / (l1 + 0.05); + } + return (l1 + 0.05) / (l2 + 0.05); +} diff --git a/node_modules/xterm/src/browser/ColorContrastCache.ts b/node_modules/xterm/src/browser/ColorContrastCache.ts new file mode 100644 index 0000000..b96b66c --- /dev/null +++ b/node_modules/xterm/src/browser/ColorContrastCache.ts @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IColor, IColorContrastCache } from 'browser/Types'; + +export class ColorContrastCache implements IColorContrastCache { + private _color: { [bg: number]: { [fg: number]: IColor | null | undefined } | undefined } = {}; + private _rgba: { [bg: number]: { [fg: number]: string | null | undefined } | undefined } = {}; + + public clear(): void { + this._color = {}; + this._rgba = {}; + } + + public setCss(bg: number, fg: number, value: string | null): void { + if (!this._rgba[bg]) { + this._rgba[bg] = {}; + } + this._rgba[bg]![fg] = value; + } + + public getCss(bg: number, fg: number): string | null | undefined { + return this._rgba[bg] ? this._rgba[bg]![fg] : undefined; + } + + public setColor(bg: number, fg: number, value: IColor | null): void { + if (!this._color[bg]) { + this._color[bg] = {}; + } + this._color[bg]![fg] = value; + } + + public getColor(bg: number, fg: number): IColor | null | undefined { + return this._color[bg] ? this._color[bg]![fg] : undefined; + } +} diff --git a/node_modules/xterm/src/browser/ColorManager.ts b/node_modules/xterm/src/browser/ColorManager.ts new file mode 100644 index 0000000..b4b57c6 --- /dev/null +++ b/node_modules/xterm/src/browser/ColorManager.ts @@ -0,0 +1,258 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IColorManager, IColor, IColorSet, IColorContrastCache } from 'browser/Types'; +import { ITheme } from 'common/services/Services'; +import { channels, color, css } from 'browser/Color'; +import { ColorContrastCache } from 'browser/ColorContrastCache'; +import { ColorIndex } from 'common/Types'; + + +interface IRestoreColorSet { + foreground: IColor; + background: IColor; + cursor: IColor; + ansi: IColor[]; +} + + +const DEFAULT_FOREGROUND = css.toColor('#ffffff'); +const DEFAULT_BACKGROUND = css.toColor('#000000'); +const DEFAULT_CURSOR = css.toColor('#ffffff'); +const DEFAULT_CURSOR_ACCENT = css.toColor('#000000'); +const DEFAULT_SELECTION = { + css: 'rgba(255, 255, 255, 0.3)', + rgba: 0xFFFFFF4D +}; + +// An IIFE to generate DEFAULT_ANSI_COLORS. +export const DEFAULT_ANSI_COLORS = Object.freeze((() => { + const colors = [ + // dark: + css.toColor('#2e3436'), + css.toColor('#cc0000'), + css.toColor('#4e9a06'), + css.toColor('#c4a000'), + css.toColor('#3465a4'), + css.toColor('#75507b'), + css.toColor('#06989a'), + css.toColor('#d3d7cf'), + // bright: + css.toColor('#555753'), + css.toColor('#ef2929'), + css.toColor('#8ae234'), + css.toColor('#fce94f'), + css.toColor('#729fcf'), + css.toColor('#ad7fa8'), + css.toColor('#34e2e2'), + css.toColor('#eeeeec') + ]; + + // Fill in the remaining 240 ANSI colors. + // Generate colors (16-231) + const v = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff]; + for (let i = 0; i < 216; i++) { + const r = v[(i / 36) % 6 | 0]; + const g = v[(i / 6) % 6 | 0]; + const b = v[i % 6]; + colors.push({ + css: channels.toCss(r, g, b), + rgba: channels.toRgba(r, g, b) + }); + } + + // Generate greys (232-255) + for (let i = 0; i < 24; i++) { + const c = 8 + i * 10; + colors.push({ + css: channels.toCss(c, c, c), + rgba: channels.toRgba(c, c, c) + }); + } + + return colors; +})()); + +/** + * Manages the source of truth for a terminal's colors. + */ +export class ColorManager implements IColorManager { + public colors: IColorSet; + private _ctx: CanvasRenderingContext2D; + private _litmusColor: CanvasGradient; + private _contrastCache: IColorContrastCache; + private _restoreColors!: IRestoreColorSet; + + constructor(document: Document, public allowTransparency: boolean) { + const canvas = document.createElement('canvas'); + canvas.width = 1; + canvas.height = 1; + const ctx = canvas.getContext('2d'); + if (!ctx) { + throw new Error('Could not get rendering context'); + } + this._ctx = ctx; + this._ctx.globalCompositeOperation = 'copy'; + this._litmusColor = this._ctx.createLinearGradient(0, 0, 1, 1); + this._contrastCache = new ColorContrastCache(); + this.colors = { + foreground: DEFAULT_FOREGROUND, + background: DEFAULT_BACKGROUND, + cursor: DEFAULT_CURSOR, + cursorAccent: DEFAULT_CURSOR_ACCENT, + selectionTransparent: DEFAULT_SELECTION, + selectionOpaque: color.blend(DEFAULT_BACKGROUND, DEFAULT_SELECTION), + ansi: DEFAULT_ANSI_COLORS.slice(), + contrastCache: this._contrastCache + }; + this._updateRestoreColors(); + } + + public onOptionsChange(key: string): void { + if (key === 'minimumContrastRatio') { + this._contrastCache.clear(); + } + } + + /** + * Sets the terminal's theme. + * @param theme The theme to use. If a partial theme is provided then default + * colors will be used where colors are not defined. + */ + public setTheme(theme: ITheme = {}): void { + this.colors.foreground = this._parseColor(theme.foreground, DEFAULT_FOREGROUND); + this.colors.background = this._parseColor(theme.background, DEFAULT_BACKGROUND); + this.colors.cursor = this._parseColor(theme.cursor, DEFAULT_CURSOR, true); + this.colors.cursorAccent = this._parseColor(theme.cursorAccent, DEFAULT_CURSOR_ACCENT, true); + this.colors.selectionTransparent = this._parseColor(theme.selection, DEFAULT_SELECTION, true); + this.colors.selectionOpaque = color.blend(this.colors.background, this.colors.selectionTransparent); + /** + * If selection color is opaque, blend it with background with 0.3 opacity + * Issue #2737 + */ + if (color.isOpaque(this.colors.selectionTransparent)) { + const opacity = 0.3; + this.colors.selectionTransparent = color.opacity(this.colors.selectionTransparent, opacity); + } + this.colors.ansi[0] = this._parseColor(theme.black, DEFAULT_ANSI_COLORS[0]); + this.colors.ansi[1] = this._parseColor(theme.red, DEFAULT_ANSI_COLORS[1]); + this.colors.ansi[2] = this._parseColor(theme.green, DEFAULT_ANSI_COLORS[2]); + this.colors.ansi[3] = this._parseColor(theme.yellow, DEFAULT_ANSI_COLORS[3]); + this.colors.ansi[4] = this._parseColor(theme.blue, DEFAULT_ANSI_COLORS[4]); + this.colors.ansi[5] = this._parseColor(theme.magenta, DEFAULT_ANSI_COLORS[5]); + this.colors.ansi[6] = this._parseColor(theme.cyan, DEFAULT_ANSI_COLORS[6]); + this.colors.ansi[7] = this._parseColor(theme.white, DEFAULT_ANSI_COLORS[7]); + this.colors.ansi[8] = this._parseColor(theme.brightBlack, DEFAULT_ANSI_COLORS[8]); + this.colors.ansi[9] = this._parseColor(theme.brightRed, DEFAULT_ANSI_COLORS[9]); + this.colors.ansi[10] = this._parseColor(theme.brightGreen, DEFAULT_ANSI_COLORS[10]); + this.colors.ansi[11] = this._parseColor(theme.brightYellow, DEFAULT_ANSI_COLORS[11]); + this.colors.ansi[12] = this._parseColor(theme.brightBlue, DEFAULT_ANSI_COLORS[12]); + this.colors.ansi[13] = this._parseColor(theme.brightMagenta, DEFAULT_ANSI_COLORS[13]); + this.colors.ansi[14] = this._parseColor(theme.brightCyan, DEFAULT_ANSI_COLORS[14]); + this.colors.ansi[15] = this._parseColor(theme.brightWhite, DEFAULT_ANSI_COLORS[15]); + // Clear our the cache + this._contrastCache.clear(); + this._updateRestoreColors(); + } + + public restoreColor(slot?: ColorIndex): void { + // unset slot restores all ansi colors + if (slot === undefined) { + for (let i = 0; i < this._restoreColors.ansi.length; ++i) { + this.colors.ansi[i] = this._restoreColors.ansi[i]; + } + return; + } + switch (slot) { + case ColorIndex.FOREGROUND: + this.colors.foreground = this._restoreColors.foreground; + break; + case ColorIndex.BACKGROUND: + this.colors.background = this._restoreColors.background; + break; + case ColorIndex.CURSOR: + this.colors.cursor = this._restoreColors.cursor; + break; + default: + this.colors.ansi[slot] = this._restoreColors.ansi[slot]; + } + } + + private _updateRestoreColors(): void { + this._restoreColors = { + foreground: this.colors.foreground, + background: this.colors.background, + cursor: this.colors.cursor, + ansi: [...this.colors.ansi] + }; + } + + private _parseColor( + css: string | undefined, + fallback: IColor, + allowTransparency: boolean = this.allowTransparency + ): IColor { + if (css === undefined) { + return fallback; + } + + // If parsing the value results in failure, then it must be ignored, and the attribute must + // retain its previous value. + // -- https://html.spec.whatwg.org/multipage/canvas.html#fill-and-stroke-styles + this._ctx.fillStyle = this._litmusColor; + this._ctx.fillStyle = css; + if (typeof this._ctx.fillStyle !== 'string') { + console.warn(`Color: ${css} is invalid using fallback ${fallback.css}`); + return fallback; + } + + this._ctx.fillRect(0, 0, 1, 1); + const data = this._ctx.getImageData(0, 0, 1, 1).data; + + // Check if the printed color was transparent + if (data[3] !== 0xFF) { + if (!allowTransparency) { + // Ideally we'd just ignore the alpha channel, but... + // + // Browsers may not give back exactly the same RGB values we put in, because most/all + // convert the color to a pre-multiplied representation. getImageData converts that back to + // a un-premultipled representation, but the precision loss may make the RGB channels unuable + // on their own. + // + // E.g. In Chrome #12345610 turns into #10305010, and in the extreme case, 0xFFFFFF00 turns + // into 0x00000000. + // + // "Note: Due to the lossy nature of converting to and from premultiplied alpha color values, + // pixels that have just been set using putImageData() might be returned to an equivalent + // getImageData() as different values." + // -- https://html.spec.whatwg.org/multipage/canvas.html#pixel-manipulation + // + // So let's just use the fallback color in this case instead. + console.warn( + `Color: ${css} is using transparency, but allowTransparency is false. ` + + `Using fallback ${fallback.css}.` + ); + return fallback; + } + + // https://html.spec.whatwg.org/multipage/canvas.html#serialisation-of-a-color + // the color value has alpha less than 1.0, and the string is the color value in the CSS rgba() + const [r, g, b, a] = this._ctx.fillStyle.substring(5, this._ctx.fillStyle.length - 1).split(',').map(component => Number(component)); + const alpha = Math.round(a * 255); + const rgba: number = channels.toRgba(r, g, b, alpha); + return { + rgba, + css + }; + } + + return { + // https://html.spec.whatwg.org/multipage/canvas.html#serialisation-of-a-color + // if it has alpha equal to 1.0, then the string is a lowercase six-digit hex value, prefixed with a "#" character + css: this._ctx.fillStyle, + rgba: channels.toRgba(data[0], data[1], data[2], data[3]) + }; + } +} diff --git a/node_modules/xterm/src/browser/Dom.ts b/node_modules/xterm/src/browser/Dom.ts new file mode 100644 index 0000000..c558a8b --- /dev/null +++ b/node_modules/xterm/src/browser/Dom.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) 2020 The xterm.js authors. All rights reserved. + * @license MIT + */ + +export function removeElementFromParent(...elements: (HTMLElement | undefined)[]): void { + for (const e of elements) { + e?.parentElement?.removeChild(e); + } +} diff --git a/node_modules/xterm/src/browser/Lifecycle.ts b/node_modules/xterm/src/browser/Lifecycle.ts new file mode 100644 index 0000000..6e84179 --- /dev/null +++ b/node_modules/xterm/src/browser/Lifecycle.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IDisposable } from 'common/Types'; + +/** + * Adds a disposable listener to a node in the DOM, returning the disposable. + * @param type The event type. + * @param handler The handler for the listener. + */ +export function addDisposableDomListener( + node: Element | Window | Document, + type: string, + handler: (e: any) => void, + options?: boolean | AddEventListenerOptions +): IDisposable { + node.addEventListener(type, handler, options); + let disposed = false; + return { + dispose: () => { + if (disposed) { + return; + } + disposed = true; + node.removeEventListener(type, handler, options); + } + }; +} diff --git a/node_modules/xterm/src/browser/Linkifier.ts b/node_modules/xterm/src/browser/Linkifier.ts new file mode 100644 index 0000000..b17d66a --- /dev/null +++ b/node_modules/xterm/src/browser/Linkifier.ts @@ -0,0 +1,356 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { ILinkifierEvent, ILinkMatcher, LinkMatcherHandler, ILinkMatcherOptions, ILinkifier, IMouseZoneManager, IMouseZone, IRegisteredLinkMatcher } from 'browser/Types'; +import { IBufferStringIteratorResult } from 'common/buffer/Types'; +import { EventEmitter, IEvent } from 'common/EventEmitter'; +import { ILogService, IBufferService, IOptionsService, IUnicodeService } from 'common/services/Services'; + +/** + * Limit of the unwrapping line expansion (overscan) at the top and bottom + * of the actual viewport in ASCII characters. + * A limit of 2000 should match most sane urls. + */ +const OVERSCAN_CHAR_LIMIT = 2000; + +/** + * The Linkifier applies links to rows shortly after they have been refreshed. + */ +export class Linkifier implements ILinkifier { + /** + * The time to wait after a row is changed before it is linkified. This prevents + * the costly operation of searching every row multiple times, potentially a + * huge amount of times. + */ + protected static _timeBeforeLatency = 200; + + protected _linkMatchers: IRegisteredLinkMatcher[] = []; + + private _mouseZoneManager: IMouseZoneManager | undefined; + private _element: HTMLElement | undefined; + + private _rowsTimeoutId: number | undefined; + private _nextLinkMatcherId = 0; + private _rowsToLinkify: { start: number | undefined, end: number | undefined }; + + private _onShowLinkUnderline = new EventEmitter<ILinkifierEvent>(); + public get onShowLinkUnderline(): IEvent<ILinkifierEvent> { return this._onShowLinkUnderline.event; } + private _onHideLinkUnderline = new EventEmitter<ILinkifierEvent>(); + public get onHideLinkUnderline(): IEvent<ILinkifierEvent> { return this._onHideLinkUnderline.event; } + private _onLinkTooltip = new EventEmitter<ILinkifierEvent>(); + public get onLinkTooltip(): IEvent<ILinkifierEvent> { return this._onLinkTooltip.event; } + + constructor( + @IBufferService protected readonly _bufferService: IBufferService, + @ILogService private readonly _logService: ILogService, + @IUnicodeService private readonly _unicodeService: IUnicodeService + ) { + this._rowsToLinkify = { + start: undefined, + end: undefined + }; + } + + /** + * Attaches the linkifier to the DOM, enabling linkification. + * @param mouseZoneManager The mouse zone manager to register link zones with. + */ + public attachToDom(element: HTMLElement, mouseZoneManager: IMouseZoneManager): void { + this._element = element; + this._mouseZoneManager = mouseZoneManager; + } + + /** + * Queue linkification on a set of rows. + * @param start The row to linkify from (inclusive). + * @param end The row to linkify to (inclusive). + */ + public linkifyRows(start: number, end: number): void { + // Don't attempt linkify if not yet attached to DOM + if (!this._mouseZoneManager) { + return; + } + + // Increase range to linkify + if (this._rowsToLinkify.start === undefined || this._rowsToLinkify.end === undefined) { + this._rowsToLinkify.start = start; + this._rowsToLinkify.end = end; + } else { + this._rowsToLinkify.start = Math.min(this._rowsToLinkify.start, start); + this._rowsToLinkify.end = Math.max(this._rowsToLinkify.end, end); + } + + // Clear out any existing links on this row range + this._mouseZoneManager.clearAll(start, end); + + // Restart timer + if (this._rowsTimeoutId) { + clearTimeout(this._rowsTimeoutId); + } + + // Cannot use window.setTimeout since tests need to run in node + this._rowsTimeoutId = setTimeout(() => this._linkifyRows(), Linkifier._timeBeforeLatency) as any as number; + } + + /** + * Linkifies the rows requested. + */ + private _linkifyRows(): void { + this._rowsTimeoutId = undefined; + const buffer = this._bufferService.buffer; + + if (this._rowsToLinkify.start === undefined || this._rowsToLinkify.end === undefined) { + this._logService.debug('_rowToLinkify was unset before _linkifyRows was called'); + return; + } + + // Ensure the start row exists + const absoluteRowIndexStart = buffer.ydisp + this._rowsToLinkify.start; + if (absoluteRowIndexStart >= buffer.lines.length) { + return; + } + + // Invalidate bad end row values (if a resize happened) + const absoluteRowIndexEnd = buffer.ydisp + Math.min(this._rowsToLinkify.end, this._bufferService.rows) + 1; + + // Iterate over the range of unwrapped content strings within start..end + // (excluding). + // _doLinkifyRow gets full unwrapped lines with the start row as buffer offset + // for every matcher. + // The unwrapping is needed to also match content that got wrapped across + // several buffer lines. To avoid a worst case scenario where the whole buffer + // contains just a single unwrapped string we limit this line expansion beyond + // the viewport to +OVERSCAN_CHAR_LIMIT chars (overscan) at top and bottom. + // This comes with the tradeoff that matches longer than OVERSCAN_CHAR_LIMIT + // chars will not match anymore at the viewport borders. + const overscanLineLimit = Math.ceil(OVERSCAN_CHAR_LIMIT / this._bufferService.cols); + const iterator = this._bufferService.buffer.iterator( + false, absoluteRowIndexStart, absoluteRowIndexEnd, overscanLineLimit, overscanLineLimit); + while (iterator.hasNext()) { + const lineData: IBufferStringIteratorResult = iterator.next(); + for (let i = 0; i < this._linkMatchers.length; i++) { + this._doLinkifyRow(lineData.range.first, lineData.content, this._linkMatchers[i]); + } + } + + this._rowsToLinkify.start = undefined; + this._rowsToLinkify.end = undefined; + } + + /** + * Registers a link matcher, allowing custom link patterns to be matched and + * handled. + * @param regex The regular expression to search for. Specifically, this + * searches the textContent of the rows. You will want to use \s to match a + * space ' ' character for example. + * @param handler The callback when the link is called. + * @param options Options for the link matcher. + * @return The ID of the new matcher, this can be used to deregister. + */ + public registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options: ILinkMatcherOptions = {}): number { + if (!handler) { + throw new Error('handler must be defined'); + } + const matcher: IRegisteredLinkMatcher = { + id: this._nextLinkMatcherId++, + regex, + handler, + matchIndex: options.matchIndex, + validationCallback: options.validationCallback, + hoverTooltipCallback: options.tooltipCallback, + hoverLeaveCallback: options.leaveCallback, + willLinkActivate: options.willLinkActivate, + priority: options.priority || 0 + }; + this._addLinkMatcherToList(matcher); + return matcher.id; + } + + /** + * Inserts a link matcher to the list in the correct position based on the + * priority of each link matcher. New link matchers of equal priority are + * considered after older link matchers. + * @param matcher The link matcher to be added. + */ + private _addLinkMatcherToList(matcher: IRegisteredLinkMatcher): void { + if (this._linkMatchers.length === 0) { + this._linkMatchers.push(matcher); + return; + } + + for (let i = this._linkMatchers.length - 1; i >= 0; i--) { + if (matcher.priority <= this._linkMatchers[i].priority) { + this._linkMatchers.splice(i + 1, 0, matcher); + return; + } + } + + this._linkMatchers.splice(0, 0, matcher); + } + + /** + * Deregisters a link matcher if it has been registered. + * @param matcherId The link matcher's ID (returned after register) + * @return Whether a link matcher was found and deregistered. + */ + public deregisterLinkMatcher(matcherId: number): boolean { + for (let i = 0; i < this._linkMatchers.length; i++) { + if (this._linkMatchers[i].id === matcherId) { + this._linkMatchers.splice(i, 1); + return true; + } + } + return false; + } + + /** + * Linkifies a row given a specific handler. + * @param rowIndex The row index to linkify (absolute index). + * @param text string content of the unwrapped row. + * @param matcher The link matcher for this line. + */ + private _doLinkifyRow(rowIndex: number, text: string, matcher: ILinkMatcher): void { + // clone regex to do a global search on text + const rex = new RegExp(matcher.regex.source, (matcher.regex.flags || '') + 'g'); + let match; + let stringIndex = -1; + while ((match = rex.exec(text)) !== null) { + const uri = match[typeof matcher.matchIndex !== 'number' ? 0 : matcher.matchIndex]; + if (!uri) { + // something matched but does not comply with the given matchIndex + // since this is most likely a bug the regex itself we simply do nothing here + this._logService.debug('match found without corresponding matchIndex', match, matcher); + break; + } + + // Get index, match.index is for the outer match which includes negated chars + // therefore we cannot use match.index directly, instead we search the position + // of the match group in text again + // also correct regex and string search offsets for the next loop run + stringIndex = text.indexOf(uri, stringIndex + 1); + rex.lastIndex = stringIndex + uri.length; + if (stringIndex < 0) { + // invalid stringIndex (should not have happened) + break; + } + + // get the buffer index as [absolute row, col] for the match + const bufferIndex = this._bufferService.buffer.stringIndexToBufferIndex(rowIndex, stringIndex); + if (bufferIndex[0] < 0) { + // invalid bufferIndex (should not have happened) + break; + } + + const line = this._bufferService.buffer.lines.get(bufferIndex[0]); + if (!line) { + break; + } + + const attr = line.getFg(bufferIndex[1]); + const fg = attr ? (attr >> 9) & 0x1ff : undefined; + + if (matcher.validationCallback) { + matcher.validationCallback(uri, isValid => { + // Discard link if the line has already changed + if (this._rowsTimeoutId) { + return; + } + if (isValid) { + this._addLink(bufferIndex[1], bufferIndex[0] - this._bufferService.buffer.ydisp, uri, matcher, fg); + } + }); + } else { + this._addLink(bufferIndex[1], bufferIndex[0] - this._bufferService.buffer.ydisp, uri, matcher, fg); + } + } + } + + /** + * Registers a link to the mouse zone manager. + * @param x The column the link starts. + * @param y The row the link is on. + * @param uri The URI of the link. + * @param matcher The link matcher for the link. + * @param fg The link color for hover event. + */ + private _addLink(x: number, y: number, uri: string, matcher: ILinkMatcher, fg: number | undefined): void { + if (!this._mouseZoneManager || !this._element) { + return; + } + // FIXME: get cell length from buffer to avoid mismatch after Unicode version change + const width = this._unicodeService.getStringCellWidth(uri); + const x1 = x % this._bufferService.cols; + const y1 = y + Math.floor(x / this._bufferService.cols); + let x2 = (x1 + width) % this._bufferService.cols; + let y2 = y1 + Math.floor((x1 + width) / this._bufferService.cols); + if (x2 === 0) { + x2 = this._bufferService.cols; + y2--; + } + + this._mouseZoneManager.add(new MouseZone( + x1 + 1, + y1 + 1, + x2 + 1, + y2 + 1, + e => { + if (matcher.handler) { + return matcher.handler(e, uri); + } + const newWindow = window.open(); + if (newWindow) { + newWindow.opener = null; + newWindow.location.href = uri; + } else { + console.warn('Opening link blocked as opener could not be cleared'); + } + }, + () => { + this._onShowLinkUnderline.fire(this._createLinkHoverEvent(x1, y1, x2, y2, fg)); + this._element!.classList.add('xterm-cursor-pointer'); + }, + e => { + this._onLinkTooltip.fire(this._createLinkHoverEvent(x1, y1, x2, y2, fg)); + if (matcher.hoverTooltipCallback) { + // Note that IViewportRange use 1-based coordinates to align with escape sequences such + // as CUP which use 1,1 as the default for row/col + matcher.hoverTooltipCallback(e, uri, { start: { x: x1, y: y1 }, end: { x: x2, y: y2 } }); + } + }, + () => { + this._onHideLinkUnderline.fire(this._createLinkHoverEvent(x1, y1, x2, y2, fg)); + this._element!.classList.remove('xterm-cursor-pointer'); + if (matcher.hoverLeaveCallback) { + matcher.hoverLeaveCallback(); + } + }, + e => { + if (matcher.willLinkActivate) { + return matcher.willLinkActivate(e, uri); + } + return true; + } + )); + } + + private _createLinkHoverEvent(x1: number, y1: number, x2: number, y2: number, fg: number | undefined): ILinkifierEvent { + return { x1, y1, x2, y2, cols: this._bufferService.cols, fg }; + } +} + +export class MouseZone implements IMouseZone { + constructor( + public x1: number, + public y1: number, + public x2: number, + public y2: number, + public clickCallback: (e: MouseEvent) => any, + public hoverCallback: (e: MouseEvent) => any, + public tooltipCallback: (e: MouseEvent) => any, + public leaveCallback: () => void, + public willLinkActivate: (e: MouseEvent) => boolean + ) { + } +} diff --git a/node_modules/xterm/src/browser/Linkifier2.ts b/node_modules/xterm/src/browser/Linkifier2.ts new file mode 100644 index 0000000..8954293 --- /dev/null +++ b/node_modules/xterm/src/browser/Linkifier2.ts @@ -0,0 +1,392 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { ILinkifier2, ILinkProvider, IBufferCellPosition, ILink, ILinkifierEvent, ILinkDecorations, ILinkWithState } from 'browser/Types'; +import { IDisposable } from 'common/Types'; +import { IMouseService, IRenderService } from './services/Services'; +import { IBufferService } from 'common/services/Services'; +import { EventEmitter, IEvent } from 'common/EventEmitter'; +import { Disposable, getDisposeArrayDisposable, disposeArray } from 'common/Lifecycle'; +import { addDisposableDomListener } from 'browser/Lifecycle'; + +export class Linkifier2 extends Disposable implements ILinkifier2 { + private _element: HTMLElement | undefined; + private _mouseService: IMouseService | undefined; + private _renderService: IRenderService | undefined; + private _linkProviders: ILinkProvider[] = []; + public get currentLink(): ILinkWithState | undefined { return this._currentLink; } + protected _currentLink: ILinkWithState | undefined; + private _lastMouseEvent: MouseEvent | undefined; + private _linkCacheDisposables: IDisposable[] = []; + private _lastBufferCell: IBufferCellPosition | undefined; + private _isMouseOut: boolean = true; + private _activeProviderReplies: Map<Number, ILinkWithState[] | undefined> | undefined; + private _activeLine: number = -1; + + private _onShowLinkUnderline = this.register(new EventEmitter<ILinkifierEvent>()); + public get onShowLinkUnderline(): IEvent<ILinkifierEvent> { return this._onShowLinkUnderline.event; } + private _onHideLinkUnderline = this.register(new EventEmitter<ILinkifierEvent>()); + public get onHideLinkUnderline(): IEvent<ILinkifierEvent> { return this._onHideLinkUnderline.event; } + + constructor( + @IBufferService private readonly _bufferService: IBufferService + ) { + super(); + this.register(getDisposeArrayDisposable(this._linkCacheDisposables)); + } + + public registerLinkProvider(linkProvider: ILinkProvider): IDisposable { + this._linkProviders.push(linkProvider); + return { + dispose: () => { + // Remove the link provider from the list + const providerIndex = this._linkProviders.indexOf(linkProvider); + + if (providerIndex !== -1) { + this._linkProviders.splice(providerIndex, 1); + } + } + }; + } + + public attachToDom(element: HTMLElement, mouseService: IMouseService, renderService: IRenderService): void { + this._element = element; + this._mouseService = mouseService; + this._renderService = renderService; + + this.register(addDisposableDomListener(this._element, 'mouseleave', () => { + this._isMouseOut = true; + this._clearCurrentLink(); + })); + this.register(addDisposableDomListener(this._element, 'mousemove', this._onMouseMove.bind(this))); + this.register(addDisposableDomListener(this._element, 'click', this._onClick.bind(this))); + } + + private _onMouseMove(event: MouseEvent): void { + this._lastMouseEvent = event; + + if (!this._element || !this._mouseService) { + return; + } + + const position = this._positionFromMouseEvent(event, this._element, this._mouseService); + if (!position) { + return; + } + this._isMouseOut = false; + + // Ignore the event if it's an embedder created hover widget + const composedPath = event.composedPath() as HTMLElement[]; + for (let i = 0; i < composedPath.length; i++) { + const target = composedPath[i]; + // Hit Terminal.element, break and continue + if (target.classList.contains('xterm')) { + break; + } + // It's a hover, don't respect hover event + if (target.classList.contains('xterm-hover')) { + return; + } + } + + if (!this._lastBufferCell || (position.x !== this._lastBufferCell.x || position.y !== this._lastBufferCell.y)) { + this._onHover(position); + this._lastBufferCell = position; + } + } + + private _onHover(position: IBufferCellPosition): void { + // TODO: This currently does not cache link provider results across wrapped lines, activeLine should be something like `activeRange: {startY, endY}` + // Check if we need to clear the link + if (this._activeLine !== position.y) { + this._clearCurrentLink(); + this._askForLink(position, false); + return; + } + + // Check the if the link is in the mouse position + const isCurrentLinkInPosition = this._currentLink && this._linkAtPosition(this._currentLink.link, position); + if (!isCurrentLinkInPosition) { + this._clearCurrentLink(); + this._askForLink(position, true); + } + } + + private _askForLink(position: IBufferCellPosition, useLineCache: boolean): void { + if (!this._activeProviderReplies || !useLineCache) { + this._activeProviderReplies?.forEach(reply => { + reply?.forEach(linkWithState => { + if (linkWithState.link.dispose) { + linkWithState.link.dispose(); + } + }); + }); + this._activeProviderReplies = new Map(); + this._activeLine = position.y; + } + let linkProvided = false; + + // There is no link cached, so ask for one + this._linkProviders.forEach((linkProvider, i) => { + if (useLineCache) { + const existingReply = this._activeProviderReplies?.get(i); + // If there isn't a reply, the provider hasn't responded yet. + + // TODO: If there isn't a reply yet it means that the provider is still resolving. Ensuring + // provideLinks isn't triggered again saves ILink.hover firing twice though. This probably + // needs promises to get fixed + if (existingReply) { + linkProvided = this._checkLinkProviderResult(i, position, linkProvided); + } + } else { + linkProvider.provideLinks(position.y, (links: ILink[] | undefined) => { + if (this._isMouseOut) { + return; + } + const linksWithState: ILinkWithState[] | undefined = links?.map(link => ({ link })); + this._activeProviderReplies?.set(i, linksWithState); + linkProvided = this._checkLinkProviderResult(i, position, linkProvided); + + // If all providers have responded, remove lower priority links that intersect ranges of + // higher priority links + if (this._activeProviderReplies?.size === this._linkProviders.length) { + this._removeIntersectingLinks(position.y, this._activeProviderReplies); + } + }); + } + }); + } + + private _removeIntersectingLinks(y: number, replies: Map<Number, ILinkWithState[] | undefined>): void { + const occupiedCells = new Set<number>(); + for (let i = 0; i < replies.size; i++) { + const providerReply = replies.get(i); + if (!providerReply) { + continue; + } + for (let i = 0; i < providerReply.length; i++) { + const linkWithState = providerReply[i]; + const startX = linkWithState.link.range.start.y < y ? 0 : linkWithState.link.range.start.x; + const endX = linkWithState.link.range.end.y > y ? this._bufferService.cols : linkWithState.link.range.end.x; + for (let x = startX; x <= endX; x++) { + if (occupiedCells.has(x)) { + providerReply.splice(i--, 1); + break; + } + occupiedCells.add(x); + } + } + } + } + + private _checkLinkProviderResult(index: number, position: IBufferCellPosition, linkProvided: boolean): boolean { + if (!this._activeProviderReplies) { + return linkProvided; + } + + const links = this._activeProviderReplies.get(index); + + // Check if every provider before this one has come back undefined + let hasLinkBefore = false; + for (let j = 0; j < index; j++) { + if (!this._activeProviderReplies.has(j) || this._activeProviderReplies.get(j)) { + hasLinkBefore = true; + } + } + + // If all providers with higher priority came back undefined, then this provider's link for + // the position should be used + if (!hasLinkBefore && links) { + const linkAtPosition = links.find(link => this._linkAtPosition(link.link, position)); + if (linkAtPosition) { + linkProvided = true; + this._handleNewLink(linkAtPosition); + } + } + + // Check if all the providers have responded + if (this._activeProviderReplies.size === this._linkProviders.length && !linkProvided) { + // Respect the order of the link providers + for (let j = 0; j < this._activeProviderReplies.size; j++) { + const currentLink = this._activeProviderReplies.get(j)?.find(link => this._linkAtPosition(link.link, position)); + if (currentLink) { + linkProvided = true; + this._handleNewLink(currentLink); + break; + } + } + } + + return linkProvided; + } + + private _onClick(event: MouseEvent): void { + if (!this._element || !this._mouseService || !this._currentLink) { + return; + } + + const position = this._positionFromMouseEvent(event, this._element, this._mouseService); + + if (!position) { + return; + } + + if (this._linkAtPosition(this._currentLink.link, position)) { + this._currentLink.link.activate(event, this._currentLink.link.text); + } + } + + private _clearCurrentLink(startRow?: number, endRow?: number): void { + if (!this._element || !this._currentLink || !this._lastMouseEvent) { + return; + } + + // If we have a start and end row, check that the link is within it + if (!startRow || !endRow || (this._currentLink.link.range.start.y >= startRow && this._currentLink.link.range.end.y <= endRow)) { + this._linkLeave(this._element, this._currentLink.link, this._lastMouseEvent); + this._currentLink = undefined; + disposeArray(this._linkCacheDisposables); + } + } + + private _handleNewLink(linkWithState: ILinkWithState): void { + if (!this._element || !this._lastMouseEvent || !this._mouseService) { + return; + } + + const position = this._positionFromMouseEvent(this._lastMouseEvent, this._element, this._mouseService); + + if (!position) { + return; + } + + // Trigger hover if the we have a link at the position + if (this._linkAtPosition(linkWithState.link, position)) { + this._currentLink = linkWithState; + this._currentLink.state = { + decorations: { + underline: linkWithState.link.decorations === undefined ? true : linkWithState.link.decorations.underline, + pointerCursor: linkWithState.link.decorations === undefined ? true : linkWithState.link.decorations.pointerCursor + }, + isHovered: true + }; + this._linkHover(this._element, linkWithState.link, this._lastMouseEvent); + + // Add listener for tracking decorations changes + linkWithState.link.decorations = {} as ILinkDecorations; + Object.defineProperties(linkWithState.link.decorations, { + pointerCursor: { + get: () => this._currentLink?.state?.decorations.pointerCursor, + set: v => { + if (this._currentLink?.state && this._currentLink.state.decorations.pointerCursor !== v) { + this._currentLink.state.decorations.pointerCursor = v; + if (this._currentLink.state.isHovered) { + this._element?.classList.toggle('xterm-cursor-pointer', v); + } + } + } + }, + underline: { + get: () => this._currentLink?.state?.decorations.underline, + set: v => { + if (this._currentLink?.state && this._currentLink?.state?.decorations.underline !== v) { + this._currentLink.state.decorations.underline = v; + if (this._currentLink.state.isHovered) { + this._fireUnderlineEvent(linkWithState.link, v); + } + } + } + } + }); + + // Add listener for rerendering + if (this._renderService) { + this._linkCacheDisposables.push(this._renderService.onRenderedBufferChange(e => { + // When start is 0 a scroll most likely occurred, make sure links above the fold also get + // cleared. + const start = e.start === 0 ? 0 : e.start + 1 + this._bufferService.buffer.ydisp; + this._clearCurrentLink(start, e.end + 1 + this._bufferService.buffer.ydisp); + })); + } + } + } + + protected _linkHover(element: HTMLElement, link: ILink, event: MouseEvent): void { + if (this._currentLink?.state) { + this._currentLink.state.isHovered = true; + if (this._currentLink.state.decorations.underline) { + this._fireUnderlineEvent(link, true); + } + if (this._currentLink.state.decorations.pointerCursor) { + element.classList.add('xterm-cursor-pointer'); + } + } + + if (link.hover) { + link.hover(event, link.text); + } + } + + private _fireUnderlineEvent(link: ILink, showEvent: boolean): void { + const range = link.range; + const scrollOffset = this._bufferService.buffer.ydisp; + const event = this._createLinkUnderlineEvent(range.start.x - 1, range.start.y - scrollOffset - 1, range.end.x, range.end.y - scrollOffset - 1, undefined); + const emitter = showEvent ? this._onShowLinkUnderline : this._onHideLinkUnderline; + emitter.fire(event); + } + + protected _linkLeave(element: HTMLElement, link: ILink, event: MouseEvent): void { + if (this._currentLink?.state) { + this._currentLink.state.isHovered = false; + if (this._currentLink.state.decorations.underline) { + this._fireUnderlineEvent(link, false); + } + if (this._currentLink.state.decorations.pointerCursor) { + element.classList.remove('xterm-cursor-pointer'); + } + } + + if (link.leave) { + link.leave(event, link.text); + } + } + + /** + * Check if the buffer position is within the link + * @param link + * @param position + */ + private _linkAtPosition(link: ILink, position: IBufferCellPosition): boolean { + const sameLine = link.range.start.y === link.range.end.y; + const wrappedFromLeft = link.range.start.y < position.y; + const wrappedToRight = link.range.end.y > position.y; + + // If the start and end have the same y, then the position must be between start and end x + // If not, then handle each case seperately, depending on which way it wraps + return ((sameLine && link.range.start.x <= position.x && link.range.end.x >= position.x) || + (wrappedFromLeft && link.range.end.x >= position.x) || + (wrappedToRight && link.range.start.x <= position.x) || + (wrappedFromLeft && wrappedToRight)) && + link.range.start.y <= position.y && + link.range.end.y >= position.y; + } + + /** + * Get the buffer position from a mouse event + * @param event + */ + private _positionFromMouseEvent(event: MouseEvent, element: HTMLElement, mouseService: IMouseService): IBufferCellPosition | undefined { + const coords = mouseService.getCoords(event, element, this._bufferService.cols, this._bufferService.rows); + if (!coords) { + return; + } + + return { x: coords[0], y: coords[1] + this._bufferService.buffer.ydisp }; + } + + private _createLinkUnderlineEvent(x1: number, y1: number, x2: number, y2: number, fg: number | undefined): ILinkifierEvent { + return { x1, y1, x2, y2, cols: this._bufferService.cols, fg }; + } +} diff --git a/node_modules/xterm/src/browser/LocalizableStrings.ts b/node_modules/xterm/src/browser/LocalizableStrings.ts new file mode 100644 index 0000000..c0a904c --- /dev/null +++ b/node_modules/xterm/src/browser/LocalizableStrings.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +// eslint-disable-next-line prefer-const +export let promptLabel = 'Terminal input'; + +// eslint-disable-next-line prefer-const +export let tooMuchOutput = 'Too much output to announce, navigate to rows manually to read'; diff --git a/node_modules/xterm/src/browser/MouseZoneManager.ts b/node_modules/xterm/src/browser/MouseZoneManager.ts new file mode 100644 index 0000000..71ffe7c --- /dev/null +++ b/node_modules/xterm/src/browser/MouseZoneManager.ts @@ -0,0 +1,236 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { Disposable } from 'common/Lifecycle'; +import { addDisposableDomListener } from 'browser/Lifecycle'; +import { IMouseService, ISelectionService } from 'browser/services/Services'; +import { IMouseZoneManager, IMouseZone } from 'browser/Types'; +import { IBufferService, IOptionsService } from 'common/services/Services'; + +/** + * The MouseZoneManager allows components to register zones within the terminal + * that trigger hover and click callbacks. + * + * This class was intentionally made not so robust initially as the only case it + * needed to support was single-line links which never overlap. Improvements can + * be made in the future. + */ +export class MouseZoneManager extends Disposable implements IMouseZoneManager { + private _zones: IMouseZone[] = []; + + private _areZonesActive: boolean = false; + private _mouseMoveListener: (e: MouseEvent) => any; + private _mouseLeaveListener: (e: MouseEvent) => any; + private _clickListener: (e: MouseEvent) => any; + + private _tooltipTimeout: number | undefined; + private _currentZone: IMouseZone | undefined; + private _lastHoverCoords: [number | undefined, number | undefined] = [undefined, undefined]; + private _initialSelectionLength: number = 0; + + constructor( + private readonly _element: HTMLElement, + private readonly _screenElement: HTMLElement, + @IBufferService private readonly _bufferService: IBufferService, + @IMouseService private readonly _mouseService: IMouseService, + @ISelectionService private readonly _selectionService: ISelectionService, + @IOptionsService private readonly _optionsService: IOptionsService + ) { + super(); + + this.register(addDisposableDomListener(this._element, 'mousedown', e => this._onMouseDown(e))); + + // These events are expensive, only listen to it when mouse zones are active + this._mouseMoveListener = e => this._onMouseMove(e); + this._mouseLeaveListener = e => this._onMouseLeave(e); + this._clickListener = e => this._onClick(e); + } + + public dispose(): void { + super.dispose(); + this._deactivate(); + } + + public add(zone: IMouseZone): void { + this._zones.push(zone); + if (this._zones.length === 1) { + this._activate(); + } + } + + public clearAll(start?: number, end?: number): void { + // Exit if there's nothing to clear + if (this._zones.length === 0) { + return; + } + + // Clear all if start/end weren't set + if (!start || !end) { + start = 0; + end = this._bufferService.rows - 1; + } + + // Iterate through zones and clear them out if they're within the range + for (let i = 0; i < this._zones.length; i++) { + const zone = this._zones[i]; + if ((zone.y1 > start && zone.y1 <= end + 1) || + (zone.y2 > start && zone.y2 <= end + 1) || + (zone.y1 < start && zone.y2 > end + 1)) { + if (this._currentZone && this._currentZone === zone) { + this._currentZone.leaveCallback(); + this._currentZone = undefined; + } + this._zones.splice(i--, 1); + } + } + + // Deactivate the mouse zone manager if all the zones have been removed + if (this._zones.length === 0) { + this._deactivate(); + } + } + + private _activate(): void { + if (!this._areZonesActive) { + this._areZonesActive = true; + this._element.addEventListener('mousemove', this._mouseMoveListener); + this._element.addEventListener('mouseleave', this._mouseLeaveListener); + this._element.addEventListener('click', this._clickListener); + } + } + + private _deactivate(): void { + if (this._areZonesActive) { + this._areZonesActive = false; + this._element.removeEventListener('mousemove', this._mouseMoveListener); + this._element.removeEventListener('mouseleave', this._mouseLeaveListener); + this._element.removeEventListener('click', this._clickListener); + } + } + + private _onMouseMove(e: MouseEvent): void { + // TODO: Ideally this would only clear the hover state when the mouse moves + // outside of the mouse zone + if (this._lastHoverCoords[0] !== e.pageX || this._lastHoverCoords[1] !== e.pageY) { + this._onHover(e); + // Record the current coordinates + this._lastHoverCoords = [e.pageX, e.pageY]; + } + } + + private _onHover(e: MouseEvent): void { + const zone = this._findZoneEventAt(e); + + // Do nothing if the zone is the same + if (zone === this._currentZone) { + return; + } + + // Fire the hover end callback and cancel any existing timer if a new zone + // is being hovered + if (this._currentZone) { + this._currentZone.leaveCallback(); + this._currentZone = undefined; + if (this._tooltipTimeout) { + clearTimeout(this._tooltipTimeout); + } + } + + // Exit if there is not zone + if (!zone) { + return; + } + this._currentZone = zone; + + // Trigger the hover callback + if (zone.hoverCallback) { + zone.hoverCallback(e); + } + + // Restart the tooltip timeout + this._tooltipTimeout = window.setTimeout(() => this._onTooltip(e), this._optionsService.rawOptions.linkTooltipHoverDuration); + } + + private _onTooltip(e: MouseEvent): void { + this._tooltipTimeout = undefined; + const zone = this._findZoneEventAt(e); + zone?.tooltipCallback(e); + } + + private _onMouseDown(e: MouseEvent): void { + // Store current terminal selection length, to check if we're performing + // a selection operation + this._initialSelectionLength = this._getSelectionLength(); + + // Ignore the event if there are no zones active + if (!this._areZonesActive) { + return; + } + + // Find the active zone, prevent event propagation if found to prevent other + // components from handling the mouse event. + const zone = this._findZoneEventAt(e); + if (zone?.willLinkActivate(e)) { + e.preventDefault(); + e.stopImmediatePropagation(); + } + } + + private _onMouseLeave(e: MouseEvent): void { + // Fire the hover end callback and cancel any existing timer if the mouse + // leaves the terminal element + if (this._currentZone) { + this._currentZone.leaveCallback(); + this._currentZone = undefined; + if (this._tooltipTimeout) { + clearTimeout(this._tooltipTimeout); + } + } + } + + private _onClick(e: MouseEvent): void { + // Find the active zone and click it if found and no selection was + // being performed + const zone = this._findZoneEventAt(e); + const currentSelectionLength = this._getSelectionLength(); + + if (zone && currentSelectionLength === this._initialSelectionLength) { + zone.clickCallback(e); + e.preventDefault(); + e.stopImmediatePropagation(); + } + } + + private _getSelectionLength(): number { + const selectionText = this._selectionService.selectionText; + return selectionText ? selectionText.length : 0; + } + + private _findZoneEventAt(e: MouseEvent): IMouseZone | undefined { + const coords = this._mouseService.getCoords(e, this._screenElement, this._bufferService.cols, this._bufferService.rows); + if (!coords) { + return undefined; + } + const x = coords[0]; + const y = coords[1]; + for (let i = 0; i < this._zones.length; i++) { + const zone = this._zones[i]; + if (zone.y1 === zone.y2) { + // Single line link + if (y === zone.y1 && x >= zone.x1 && x < zone.x2) { + return zone; + } + } else { + // Multi-line link + if ((y === zone.y1 && x >= zone.x1) || + (y === zone.y2 && x < zone.x2) || + (y > zone.y1 && y < zone.y2)) { + return zone; + } + } + } + return undefined; + } +} diff --git a/node_modules/xterm/src/browser/RenderDebouncer.ts b/node_modules/xterm/src/browser/RenderDebouncer.ts new file mode 100644 index 0000000..0252107 --- /dev/null +++ b/node_modules/xterm/src/browser/RenderDebouncer.ts @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IRenderDebouncer } from 'browser/Types'; + +/** + * Debounces calls to render terminal rows using animation frames. + */ +export class RenderDebouncer implements IRenderDebouncer { + private _rowStart: number | undefined; + private _rowEnd: number | undefined; + private _rowCount: number | undefined; + private _animationFrame: number | undefined; + + constructor( + private _renderCallback: (start: number, end: number) => void + ) { + } + + public dispose(): void { + if (this._animationFrame) { + window.cancelAnimationFrame(this._animationFrame); + this._animationFrame = undefined; + } + } + + public refresh(rowStart: number | undefined, rowEnd: number | undefined, rowCount: number): void { + this._rowCount = rowCount; + // Get the min/max row start/end for the arg values + rowStart = rowStart !== undefined ? rowStart : 0; + rowEnd = rowEnd !== undefined ? rowEnd : this._rowCount - 1; + // Set the properties to the updated values + this._rowStart = this._rowStart !== undefined ? Math.min(this._rowStart, rowStart) : rowStart; + this._rowEnd = this._rowEnd !== undefined ? Math.max(this._rowEnd, rowEnd) : rowEnd; + + if (this._animationFrame) { + return; + } + + this._animationFrame = window.requestAnimationFrame(() => this._innerRefresh()); + } + + private _innerRefresh(): void { + // Make sure values are set + if (this._rowStart === undefined || this._rowEnd === undefined || this._rowCount === undefined) { + return; + } + + // Clamp values + const start = Math.max(this._rowStart, 0); + const end = Math.min(this._rowEnd, this._rowCount - 1); + + // Reset debouncer (this happens before render callback as the render could trigger it again) + this._rowStart = undefined; + this._rowEnd = undefined; + this._animationFrame = undefined; + + // Run render callback + this._renderCallback(start, end); + } +} diff --git a/node_modules/xterm/src/browser/ScreenDprMonitor.ts b/node_modules/xterm/src/browser/ScreenDprMonitor.ts new file mode 100644 index 0000000..27ae231 --- /dev/null +++ b/node_modules/xterm/src/browser/ScreenDprMonitor.ts @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { Disposable } from 'common/Lifecycle'; + +export type ScreenDprListener = (newDevicePixelRatio?: number, oldDevicePixelRatio?: number) => void; + +/** + * The screen device pixel ratio monitor allows listening for when the + * window.devicePixelRatio value changes. This is done not with polling but with + * the use of window.matchMedia to watch media queries. When the event fires, + * the listener will be reattached using a different media query to ensure that + * any further changes will register. + * + * The listener should fire on both window zoom changes and switching to a + * monitor with a different DPI. + */ +export class ScreenDprMonitor extends Disposable { + private _currentDevicePixelRatio: number = window.devicePixelRatio; + private _outerListener: ((this: MediaQueryList, ev: MediaQueryListEvent) => any) | undefined; + private _listener: ScreenDprListener | undefined; + private _resolutionMediaMatchList: MediaQueryList | undefined; + + public setListener(listener: ScreenDprListener): void { + if (this._listener) { + this.clearListener(); + } + this._listener = listener; + this._outerListener = () => { + if (!this._listener) { + return; + } + this._listener(window.devicePixelRatio, this._currentDevicePixelRatio); + this._updateDpr(); + }; + this._updateDpr(); + } + + public dispose(): void { + super.dispose(); + this.clearListener(); + } + + private _updateDpr(): void { + if (!this._outerListener) { + return; + } + + // Clear listeners for old DPR + this._resolutionMediaMatchList?.removeListener(this._outerListener); + + // Add listeners for new DPR + this._currentDevicePixelRatio = window.devicePixelRatio; + this._resolutionMediaMatchList = window.matchMedia(`screen and (resolution: ${window.devicePixelRatio}dppx)`); + this._resolutionMediaMatchList.addListener(this._outerListener); + } + + public clearListener(): void { + if (!this._resolutionMediaMatchList || !this._listener || !this._outerListener) { + return; + } + this._resolutionMediaMatchList.removeListener(this._outerListener); + this._resolutionMediaMatchList = undefined; + this._listener = undefined; + this._outerListener = undefined; + } +} diff --git a/node_modules/xterm/src/browser/Terminal.ts b/node_modules/xterm/src/browser/Terminal.ts new file mode 100644 index 0000000..fe5c7f7 --- /dev/null +++ b/node_modules/xterm/src/browser/Terminal.ts @@ -0,0 +1,1399 @@ +/** + * Copyright (c) 2014 The xterm.js authors. All rights reserved. + * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License) + * @license MIT + * + * Originally forked from (with the author's permission): + * Fabrice Bellard's javascript vt100 for jslinux: + * http://bellard.org/jslinux/ + * Copyright (c) 2011 Fabrice Bellard + * The original design remains. The terminal itself + * has been extended to include xterm CSI codes, among + * other features. + * + * Terminal Emulation References: + * http://vt100.net/ + * http://invisible-island.net/xterm/ctlseqs/ctlseqs.txt + * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html + * http://invisible-island.net/vttest/ + * http://www.inwap.com/pdp10/ansicode.txt + * http://linux.die.net/man/4/console_codes + * http://linux.die.net/man/7/urxvt + */ + +import { ICompositionHelper, ITerminal, IBrowser, CustomKeyEventHandler, ILinkifier, IMouseZoneManager, LinkMatcherHandler, ILinkMatcherOptions, IViewport, ILinkifier2, CharacterJoinerHandler } from 'browser/Types'; +import { IRenderer } from 'browser/renderer/Types'; +import { CompositionHelper } from 'browser/input/CompositionHelper'; +import { Viewport } from 'browser/Viewport'; +import { rightClickHandler, moveTextAreaUnderMouseCursor, handlePasteEvent, copyHandler, paste } from 'browser/Clipboard'; +import { C0 } from 'common/data/EscapeSequences'; +import { WindowsOptionsReportType } from '../common/InputHandler'; +import { Renderer } from 'browser/renderer/Renderer'; +import { Linkifier } from 'browser/Linkifier'; +import { SelectionService } from 'browser/services/SelectionService'; +import * as Browser from 'common/Platform'; +import { addDisposableDomListener } from 'browser/Lifecycle'; +import * as Strings from 'browser/LocalizableStrings'; +import { SoundService } from 'browser/services/SoundService'; +import { MouseZoneManager } from 'browser/MouseZoneManager'; +import { AccessibilityManager } from './AccessibilityManager'; +import { ITheme, IMarker, IDisposable, ISelectionPosition, ILinkProvider } from 'xterm'; +import { DomRenderer } from 'browser/renderer/dom/DomRenderer'; +import { KeyboardResultType, CoreMouseEventType, CoreMouseButton, CoreMouseAction, ITerminalOptions, ScrollSource, IColorEvent, ColorIndex, ColorRequestType } from 'common/Types'; +import { evaluateKeyboardEvent } from 'common/input/Keyboard'; +import { EventEmitter, IEvent, forwardEvent } from 'common/EventEmitter'; +import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; +import { ColorManager } from 'browser/ColorManager'; +import { RenderService } from 'browser/services/RenderService'; +import { ICharSizeService, IRenderService, IMouseService, ISelectionService, ISoundService, ICoreBrowserService, ICharacterJoinerService } from 'browser/services/Services'; +import { CharSizeService } from 'browser/services/CharSizeService'; +import { IBuffer } from 'common/buffer/Types'; +import { MouseService } from 'browser/services/MouseService'; +import { Linkifier2 } from 'browser/Linkifier2'; +import { CoreBrowserService } from 'browser/services/CoreBrowserService'; +import { CoreTerminal } from 'common/CoreTerminal'; +import { color, rgba } from 'browser/Color'; +import { CharacterJoinerService } from 'browser/services/CharacterJoinerService'; +import { toRgbString } from 'common/input/XParseColor'; + +// Let it work inside Node.js for automated testing purposes. +const document: Document = (typeof window !== 'undefined') ? window.document : null as any; + +export class Terminal extends CoreTerminal implements ITerminal { + public textarea: HTMLTextAreaElement | undefined; + public element: HTMLElement | undefined; + public screenElement: HTMLElement | undefined; + + private _document: Document | undefined; + private _viewportScrollArea: HTMLElement | undefined; + private _viewportElement: HTMLElement | undefined; + private _helperContainer: HTMLElement | undefined; + private _compositionView: HTMLElement | undefined; + + // private _visualBellTimer: number; + + public browser: IBrowser = Browser as any; + + private _customKeyEventHandler: CustomKeyEventHandler | undefined; + + // browser services + private _charSizeService: ICharSizeService | undefined; + private _mouseService: IMouseService | undefined; + private _renderService: IRenderService | undefined; + private _characterJoinerService: ICharacterJoinerService | undefined; + private _selectionService: ISelectionService | undefined; + private _soundService: ISoundService | undefined; + + /** + * Records whether the keydown event has already been handled and triggered a data event, if so + * the keypress event should not trigger a data event but should still print to the textarea so + * screen readers will announce it. + */ + private _keyDownHandled: boolean = false; + + /** + * Records whether the keypress event has already been handled and triggered a data event, if so + * the input event should not trigger a data event but should still print to the textarea so + * screen readers will announce it. + */ + private _keyPressHandled: boolean = false; + + /** + * Records whether there has been a keydown event for a dead key without a corresponding keydown + * event for the composed/alternative character. If we cancel the keydown event for the dead key, + * no events will be emitted for the final character. + */ + private _unprocessedDeadKey: boolean = false; + + public linkifier: ILinkifier; + public linkifier2: ILinkifier2; + public viewport: IViewport | undefined; + private _compositionHelper: ICompositionHelper | undefined; + private _mouseZoneManager: IMouseZoneManager | undefined; + private _accessibilityManager: AccessibilityManager | undefined; + private _colorManager: ColorManager | undefined; + private _theme: ITheme | undefined; + + private _onCursorMove = new EventEmitter<void>(); + public get onCursorMove(): IEvent<void> { return this._onCursorMove.event; } + private _onKey = new EventEmitter<{ key: string, domEvent: KeyboardEvent }>(); + public get onKey(): IEvent<{ key: string, domEvent: KeyboardEvent }> { return this._onKey.event; } + private _onRender = new EventEmitter<{ start: number, end: number }>(); + public get onRender(): IEvent<{ start: number, end: number }> { return this._onRender.event; } + private _onSelectionChange = new EventEmitter<void>(); + public get onSelectionChange(): IEvent<void> { return this._onSelectionChange.event; } + private _onTitleChange = new EventEmitter<string>(); + public get onTitleChange(): IEvent<string> { return this._onTitleChange.event; } + private _onBell = new EventEmitter<void>(); + public get onBell(): IEvent<void> { return this._onBell.event; } + + private _onFocus = new EventEmitter<void>(); + public get onFocus(): IEvent<void> { return this._onFocus.event; } + private _onBlur = new EventEmitter<void>(); + public get onBlur(): IEvent<void> { return this._onBlur.event; } + private _onA11yCharEmitter = new EventEmitter<string>(); + public get onA11yChar(): IEvent<string> { return this._onA11yCharEmitter.event; } + private _onA11yTabEmitter = new EventEmitter<number>(); + public get onA11yTab(): IEvent<number> { return this._onA11yTabEmitter.event; } + + /** + * Creates a new `Terminal` object. + * + * @param options An object containing a set of options, the available options are: + * - `cursorBlink` (boolean): Whether the terminal cursor blinks + * - `cols` (number): The number of columns of the terminal (horizontal size) + * - `rows` (number): The number of rows of the terminal (vertical size) + * + * @public + * @class Xterm Xterm + * @alias module:xterm/src/xterm + */ + constructor( + options: Partial<ITerminalOptions> = {} + ) { + super(options); + + this._setup(); + + this.linkifier = this._instantiationService.createInstance(Linkifier); + this.linkifier2 = this.register(this._instantiationService.createInstance(Linkifier2)); + + // Setup InputHandler listeners + this.register(this._inputHandler.onRequestBell(() => this.bell())); + this.register(this._inputHandler.onRequestRefreshRows((start, end) => this.refresh(start, end))); + this.register(this._inputHandler.onRequestSendFocus(() => this._reportFocus())); + this.register(this._inputHandler.onRequestReset(() => this.reset())); + this.register(this._inputHandler.onRequestWindowsOptionsReport(type => this._reportWindowsOptions(type))); + this.register(this._inputHandler.onColor((event) => this._handleColorEvent(event))); + this.register(forwardEvent(this._inputHandler.onCursorMove, this._onCursorMove)); + this.register(forwardEvent(this._inputHandler.onTitleChange, this._onTitleChange)); + this.register(forwardEvent(this._inputHandler.onA11yChar, this._onA11yCharEmitter)); + this.register(forwardEvent(this._inputHandler.onA11yTab, this._onA11yTabEmitter)); + + // Setup listeners + this.register(this._bufferService.onResize(e => this._afterResize(e.cols, e.rows))); + } + + /** + * Handle color event from inputhandler for OSC 4|104 | 10|110 | 11|111 | 12|112. + * An event from OSC 4|104 may contain multiple set or report requests, and multiple + * or none restore requests (resetting all), + * while an event from OSC 10|110 | 11|111 | 12|112 always contains a single request. + */ + private _handleColorEvent(event: IColorEvent): void { + if (!this._colorManager) return; + for (const req of event) { + let acc: 'foreground' | 'background' | 'cursor' | 'ansi' | undefined = undefined; + let ident = ''; + switch (req.index) { + case ColorIndex.FOREGROUND: // OSC 10 | 110 + acc = 'foreground'; + ident = '10'; + break; + case ColorIndex.BACKGROUND: // OSC 11 | 111 + acc = 'background'; + ident = '11'; + break; + case ColorIndex.CURSOR: // OSC 12 | 112 + acc = 'cursor'; + ident = '12'; + break; + default: // OSC 4 | 104 + // we can skip the [0..255] range check here (already done in inputhandler) + acc = 'ansi'; + ident = '4;' + req.index; + } + if (acc) { + switch (req.type) { + case ColorRequestType.REPORT: + const channels = color.toColorRGB(acc === 'ansi' + ? this._colorManager.colors.ansi[req.index] + : this._colorManager.colors[acc]); + this.coreService.triggerDataEvent(`${C0.ESC}]${ident};${toRgbString(channels)}${C0.BEL}`); + break; + case ColorRequestType.SET: + if (acc === 'ansi') this._colorManager.colors.ansi[req.index] = rgba.toColor(...req.color); + else this._colorManager.colors[acc] = rgba.toColor(...req.color); + break; + case ColorRequestType.RESTORE: + this._colorManager.restoreColor(req.index); + break; + } + } + } + this._renderService?.setColors(this._colorManager.colors); + this.viewport?.onThemeChange(this._colorManager.colors); + } + + public dispose(): void { + if (this._isDisposed) { + return; + } + super.dispose(); + this._renderService?.dispose(); + this._customKeyEventHandler = undefined; + this.write = () => { }; + this.element?.parentNode?.removeChild(this.element); + } + + protected _setup(): void { + super._setup(); + + this._customKeyEventHandler = undefined; + } + + /** + * Convenience property to active buffer. + */ + public get buffer(): IBuffer { + return this.buffers.active; + } + + /** + * Focus the terminal. Delegates focus handling to the terminal's DOM element. + */ + public focus(): void { + if (this.textarea) { + this.textarea.focus({ preventScroll: true }); + } + } + + protected _updateOptions(key: string): void { + super._updateOptions(key); + + // TODO: These listeners should be owned by individual components + switch (key) { + case 'fontFamily': + case 'fontSize': + // When the font changes the size of the cells may change which requires a renderer clear + this._renderService?.clear(); + this._charSizeService?.measure(); + break; + case 'cursorBlink': + case 'cursorStyle': + // The DOM renderer needs a row refresh to update the cursor styles + this.refresh(this.buffer.y, this.buffer.y); + break; + case 'customGlyphs': + case 'drawBoldTextInBrightColors': + case 'letterSpacing': + case 'lineHeight': + case 'fontWeight': + case 'fontWeightBold': + case 'minimumContrastRatio': + // When the font changes the size of the cells may change which requires a renderer clear + if (this._renderService) { + this._renderService.clear(); + this._renderService.onResize(this.cols, this.rows); + this.refresh(0, this.rows - 1); + } + break; + case 'rendererType': + if (this._renderService) { + this._renderService.setRenderer(this._createRenderer()); + this._renderService.onResize(this.cols, this.rows); + } + break; + case 'scrollback': + this.viewport?.syncScrollArea(); + break; + case 'screenReaderMode': + if (this.optionsService.rawOptions.screenReaderMode) { + if (!this._accessibilityManager && this._renderService) { + this._accessibilityManager = new AccessibilityManager(this, this._renderService); + } + } else { + this._accessibilityManager?.dispose(); + this._accessibilityManager = undefined; + } + break; + case 'tabStopWidth': this.buffers.setupTabStops(); break; + case 'theme': + this._setTheme(this.optionsService.rawOptions.theme); + break; + } + } + + /** + * Binds the desired focus behavior on a given terminal object. + */ + private _onTextAreaFocus(ev: KeyboardEvent): void { + if (this.coreService.decPrivateModes.sendFocus) { + this.coreService.triggerDataEvent(C0.ESC + '[I'); + } + this.updateCursorStyle(ev); + this.element!.classList.add('focus'); + this._showCursor(); + this._onFocus.fire(); + } + + /** + * Blur the terminal, calling the blur function on the terminal's underlying + * textarea. + */ + public blur(): void { + return this.textarea?.blur(); + } + + /** + * Binds the desired blur behavior on a given terminal object. + */ + private _onTextAreaBlur(): void { + // Text can safely be removed on blur. Doing it earlier could interfere with + // screen readers reading it out. + this.textarea!.value = ''; + this.refresh(this.buffer.y, this.buffer.y); + if (this.coreService.decPrivateModes.sendFocus) { + this.coreService.triggerDataEvent(C0.ESC + '[O'); + } + this.element!.classList.remove('focus'); + this._onBlur.fire(); + } + + private _syncTextArea(): void { + if (!this.textarea || !this.buffer.isCursorInViewport || this._compositionHelper!.isComposing || !this._renderService) { + return; + } + const cursorY = this.buffer.ybase + this.buffer.y; + const bufferLine = this.buffer.lines.get(cursorY); + if (!bufferLine) { + return; + } + const cursorX = Math.min(this.buffer.x, this.cols - 1); + const cellHeight = this._renderService.dimensions.actualCellHeight; + const width = bufferLine.getWidth(cursorX); + const cellWidth = this._renderService.dimensions.actualCellWidth * width; + const cursorTop = this.buffer.y * this._renderService.dimensions.actualCellHeight; + const cursorLeft = cursorX * this._renderService.dimensions.actualCellWidth; + + // Sync the textarea to the exact position of the composition view so the IME knows where the + // text is. + this.textarea.style.left = cursorLeft + 'px'; + this.textarea.style.top = cursorTop + 'px'; + this.textarea.style.width = cellWidth + 'px'; + this.textarea.style.height = cellHeight + 'px'; + this.textarea.style.lineHeight = cellHeight + 'px'; + this.textarea.style.zIndex = '-5'; + } + + /** + * Initialize default behavior + */ + private _initGlobal(): void { + this._bindKeys(); + + // Bind clipboard functionality + this.register(addDisposableDomListener(this.element!, 'copy', (event: ClipboardEvent) => { + // If mouse events are active it means the selection manager is disabled and + // copy should be handled by the host program. + if (!this.hasSelection()) { + return; + } + copyHandler(event, this._selectionService!); + })); + const pasteHandlerWrapper = (event: ClipboardEvent): void => handlePasteEvent(event, this.textarea!, this.coreService); + this.register(addDisposableDomListener(this.textarea!, 'paste', pasteHandlerWrapper)); + this.register(addDisposableDomListener(this.element!, 'paste', pasteHandlerWrapper)); + + // Handle right click context menus + if (Browser.isFirefox) { + // Firefox doesn't appear to fire the contextmenu event on right click + this.register(addDisposableDomListener(this.element!, 'mousedown', (event: MouseEvent) => { + if (event.button === 2) { + rightClickHandler(event, this.textarea!, this.screenElement!, this._selectionService!, this.options.rightClickSelectsWord); + } + })); + } else { + this.register(addDisposableDomListener(this.element!, 'contextmenu', (event: MouseEvent) => { + rightClickHandler(event, this.textarea!, this.screenElement!, this._selectionService!, this.options.rightClickSelectsWord); + })); + } + + // Move the textarea under the cursor when middle clicking on Linux to ensure + // middle click to paste selection works. This only appears to work in Chrome + // at the time is writing. + if (Browser.isLinux) { + // Use auxclick event over mousedown the latter doesn't seem to work. Note + // that the regular click event doesn't fire for the middle mouse button. + this.register(addDisposableDomListener(this.element!, 'auxclick', (event: MouseEvent) => { + if (event.button === 1) { + moveTextAreaUnderMouseCursor(event, this.textarea!, this.screenElement!); + } + })); + } + } + + /** + * Apply key handling to the terminal + */ + private _bindKeys(): void { + this.register(addDisposableDomListener(this.textarea!, 'keyup', (ev: KeyboardEvent) => this._keyUp(ev), true)); + this.register(addDisposableDomListener(this.textarea!, 'keydown', (ev: KeyboardEvent) => this._keyDown(ev), true)); + this.register(addDisposableDomListener(this.textarea!, 'keypress', (ev: KeyboardEvent) => this._keyPress(ev), true)); + this.register(addDisposableDomListener(this.textarea!, 'compositionstart', () => this._compositionHelper!.compositionstart())); + this.register(addDisposableDomListener(this.textarea!, 'compositionupdate', (e: CompositionEvent) => this._compositionHelper!.compositionupdate(e))); + this.register(addDisposableDomListener(this.textarea!, 'compositionend', () => this._compositionHelper!.compositionend())); + this.register(addDisposableDomListener(this.textarea!, 'input', (ev: InputEvent) => this._inputEvent(ev), true)); + this.register(this.onRender(() => this._compositionHelper!.updateCompositionElements())); + this.register(this.onRender(e => this._queueLinkification(e.start, e.end))); + } + + /** + * Opens the terminal within an element. + * + * @param parent The element to create the terminal within. + */ + public open(parent: HTMLElement): void { + if (!parent) { + throw new Error('Terminal requires a parent element.'); + } + + if (!parent.isConnected) { + this._logService.debug('Terminal.open was called on an element that was not attached to the DOM'); + } + + this._document = parent.ownerDocument!; + + // Create main element container + this.element = this._document.createElement('div'); + this.element.dir = 'ltr'; // xterm.css assumes LTR + this.element.classList.add('terminal'); + this.element.classList.add('xterm'); + this.element.setAttribute('tabindex', '0'); + parent.appendChild(this.element); + + // Performance: Use a document fragment to build the terminal + // viewport and helper elements detached from the DOM + const fragment = document.createDocumentFragment(); + this._viewportElement = document.createElement('div'); + this._viewportElement.classList.add('xterm-viewport'); + fragment.appendChild(this._viewportElement); + this._viewportScrollArea = document.createElement('div'); + this._viewportScrollArea.classList.add('xterm-scroll-area'); + this._viewportElement.appendChild(this._viewportScrollArea); + + this.screenElement = document.createElement('div'); + this.screenElement.classList.add('xterm-screen'); + // Create the container that will hold helpers like the textarea for + // capturing DOM Events. Then produce the helpers. + this._helperContainer = document.createElement('div'); + this._helperContainer.classList.add('xterm-helpers'); + this.screenElement.appendChild(this._helperContainer); + fragment.appendChild(this.screenElement); + + this.textarea = document.createElement('textarea'); + this.textarea.classList.add('xterm-helper-textarea'); + this.textarea.setAttribute('aria-label', Strings.promptLabel); + this.textarea.setAttribute('aria-multiline', 'false'); + this.textarea.setAttribute('autocorrect', 'off'); + this.textarea.setAttribute('autocapitalize', 'off'); + this.textarea.setAttribute('spellcheck', 'false'); + this.textarea.tabIndex = 0; + this.register(addDisposableDomListener(this.textarea, 'focus', (ev: KeyboardEvent) => this._onTextAreaFocus(ev))); + this.register(addDisposableDomListener(this.textarea, 'blur', () => this._onTextAreaBlur())); + this._helperContainer.appendChild(this.textarea); + + const coreBrowserService = this._instantiationService.createInstance(CoreBrowserService, this.textarea); + this._instantiationService.setService(ICoreBrowserService, coreBrowserService); + + this._charSizeService = this._instantiationService.createInstance(CharSizeService, this._document, this._helperContainer); + this._instantiationService.setService(ICharSizeService, this._charSizeService); + + this._theme = this.options.theme || this._theme; + this._colorManager = new ColorManager(document, this.options.allowTransparency); + this.register(this.optionsService.onOptionChange(e => this._colorManager!.onOptionsChange(e))); + this._colorManager.setTheme(this._theme); + + this._characterJoinerService = this._instantiationService.createInstance(CharacterJoinerService); + this._instantiationService.setService(ICharacterJoinerService, this._characterJoinerService); + + const renderer = this._createRenderer(); + this._renderService = this.register(this._instantiationService.createInstance(RenderService, renderer, this.rows, this.screenElement)); + this._instantiationService.setService(IRenderService, this._renderService); + this.register(this._renderService.onRenderedBufferChange(e => this._onRender.fire(e))); + this.onResize(e => this._renderService!.resize(e.cols, e.rows)); + + this._compositionView = document.createElement('div'); + this._compositionView.classList.add('composition-view'); + this._compositionHelper = this._instantiationService.createInstance(CompositionHelper, this.textarea, this._compositionView); + this._helperContainer.appendChild(this._compositionView); + + // Performance: Add viewport and helper elements from the fragment + this.element.appendChild(fragment); + + this._soundService = this._instantiationService.createInstance(SoundService); + this._instantiationService.setService(ISoundService, this._soundService); + this._mouseService = this._instantiationService.createInstance(MouseService); + this._instantiationService.setService(IMouseService, this._mouseService); + + this.viewport = this._instantiationService.createInstance(Viewport, + (amount: number) => this.scrollLines(amount, true, ScrollSource.VIEWPORT), + this._viewportElement, + this._viewportScrollArea, + this.element + ); + this.viewport.onThemeChange(this._colorManager.colors); + this.register(this._inputHandler.onRequestSyncScrollBar(() => this.viewport!.syncScrollArea())); + this.register(this.viewport); + + this.register(this.onCursorMove(() => { + this._renderService!.onCursorMove(); + this._syncTextArea(); + })); + this.register(this.onResize(() => this._renderService!.onResize(this.cols, this.rows))); + this.register(this.onBlur(() => this._renderService!.onBlur())); + this.register(this.onFocus(() => this._renderService!.onFocus())); + this.register(this._renderService.onDimensionsChange(() => this.viewport!.syncScrollArea())); + + this._selectionService = this.register(this._instantiationService.createInstance(SelectionService, + this.element, + this.screenElement, + this.linkifier2 + )); + this._instantiationService.setService(ISelectionService, this._selectionService); + this.register(this._selectionService.onRequestScrollLines(e => this.scrollLines(e.amount, e.suppressScrollEvent))); + this.register(this._selectionService.onSelectionChange(() => this._onSelectionChange.fire())); + this.register(this._selectionService.onRequestRedraw(e => this._renderService!.onSelectionChanged(e.start, e.end, e.columnSelectMode))); + this.register(this._selectionService.onLinuxMouseSelection(text => { + // If there's a new selection, put it into the textarea, focus and select it + // in order to register it as a selection on the OS. This event is fired + // only on Linux to enable middle click to paste selection. + this.textarea!.value = text; + this.textarea!.focus(); + this.textarea!.select(); + })); + this.register(this._onScroll.event(ev => { + this.viewport!.syncScrollArea(); + this._selectionService!.refresh(); + })); + this.register(addDisposableDomListener(this._viewportElement, 'scroll', () => this._selectionService!.refresh())); + + this._mouseZoneManager = this._instantiationService.createInstance(MouseZoneManager, this.element, this.screenElement); + this.register(this._mouseZoneManager); + this.register(this.onScroll(() => this._mouseZoneManager!.clearAll())); + this.linkifier.attachToDom(this.element, this._mouseZoneManager); + this.linkifier2.attachToDom(this.screenElement, this._mouseService, this._renderService); + + // This event listener must be registered aftre MouseZoneManager is created + this.register(addDisposableDomListener(this.element, 'mousedown', (e: MouseEvent) => this._selectionService!.onMouseDown(e))); + + // apply mouse event classes set by escape codes before terminal was attached + if (this.coreMouseService.areMouseEventsActive) { + this._selectionService.disable(); + this.element.classList.add('enable-mouse-events'); + } else { + this._selectionService.enable(); + } + + if (this.options.screenReaderMode) { + // Note that this must be done *after* the renderer is created in order to + // ensure the correct order of the dprchange event + this._accessibilityManager = new AccessibilityManager(this, this._renderService); + } + + // Measure the character size + this._charSizeService.measure(); + + // Setup loop that draws to screen + this.refresh(0, this.rows - 1); + + // Initialize global actions that need to be taken on the document. + this._initGlobal(); + + // Listen for mouse events and translate + // them into terminal mouse protocols. + this.bindMouse(); + } + + private _createRenderer(): IRenderer { + switch (this.options.rendererType) { + case 'canvas': return this._instantiationService.createInstance(Renderer, this._colorManager!.colors, this.screenElement!, this.linkifier, this.linkifier2); + case 'dom': return this._instantiationService.createInstance(DomRenderer, this._colorManager!.colors, this.element!, this.screenElement!, this._viewportElement!, this.linkifier, this.linkifier2); + default: throw new Error(`Unrecognized rendererType "${this.options.rendererType}"`); + } + } + + /** + * Sets the theme on the renderer. The renderer must have been initialized. + * @param theme The theme to set. + */ + private _setTheme(theme: ITheme): void { + this._theme = theme; + this._colorManager?.setTheme(theme); + this._renderService?.setColors(this._colorManager!.colors); + this.viewport?.onThemeChange(this._colorManager!.colors); + } + + /** + * Bind certain mouse events to the terminal. + * By default only 3 button + wheel up/down is ativated. For higher buttons + * no mouse report will be created. Typically the standard actions will be active. + * + * There are several reasons not to enable support for higher buttons/wheel: + * - Button 4 and 5 are typically used for history back and forward navigation, + * there is no straight forward way to supress/intercept those standard actions. + * - Support for higher buttons does not work in some platform/browser combinations. + * - Left/right wheel was not tested. + * - Emulators vary in mouse button support, typically only 3 buttons and + * wheel up/down work reliable. + * + * TODO: Move mouse event code into its own file. + */ + public bindMouse(): void { + const self = this; + const el = this.element!; + + // send event to CoreMouseService + function sendEvent(ev: MouseEvent | WheelEvent): boolean { + // get mouse coordinates + const pos = self._mouseService!.getRawByteCoords(ev, self.screenElement!, self.cols, self.rows); + if (!pos) { + return false; + } + + let but: CoreMouseButton; + let action: CoreMouseAction | undefined; + switch ((ev as any).overrideType || ev.type) { + case 'mousemove': + action = CoreMouseAction.MOVE; + if (ev.buttons === undefined) { + // buttons is not supported on macOS, try to get a value from button instead + but = CoreMouseButton.NONE; + if (ev.button !== undefined) { + but = ev.button < 3 ? ev.button : CoreMouseButton.NONE; + } + } else { + // according to MDN buttons only reports up to button 5 (AUX2) + but = ev.buttons & 1 ? CoreMouseButton.LEFT : + ev.buttons & 4 ? CoreMouseButton.MIDDLE : + ev.buttons & 2 ? CoreMouseButton.RIGHT : + CoreMouseButton.NONE; // fallback to NONE + } + break; + case 'mouseup': + action = CoreMouseAction.UP; + but = ev.button < 3 ? ev.button : CoreMouseButton.NONE; + break; + case 'mousedown': + action = CoreMouseAction.DOWN; + but = ev.button < 3 ? ev.button : CoreMouseButton.NONE; + break; + case 'wheel': + // only UP/DOWN wheel events are respected + if ((ev as WheelEvent).deltaY !== 0) { + action = (ev as WheelEvent).deltaY < 0 ? CoreMouseAction.UP : CoreMouseAction.DOWN; + } + but = CoreMouseButton.WHEEL; + break; + default: + // dont handle other event types by accident + return false; + } + + // exit if we cannot determine valid button/action values + // do nothing for higher buttons than wheel + if (action === undefined || but === undefined || but > CoreMouseButton.WHEEL) { + return false; + } + + return self.coreMouseService.triggerMouseEvent({ + col: pos.x - 33, // FIXME: why -33 here? + row: pos.y - 33, + button: but, + action, + ctrl: ev.ctrlKey, + alt: ev.altKey, + shift: ev.shiftKey + }); + } + + /** + * Event listener state handling. + * We listen to the onProtocolChange event of CoreMouseService and put + * requested listeners in `requestedEvents`. With this the listeners + * have all bits to do the event listener juggling. + * Note: 'mousedown' currently is "always on" and not managed + * by onProtocolChange. + */ + const requestedEvents: { [key: string]: ((ev: Event) => void) | null } = { + mouseup: null, + wheel: null, + mousedrag: null, + mousemove: null + }; + const eventListeners: { [key: string]: (ev: any) => void | boolean } = { + mouseup: (ev: MouseEvent) => { + sendEvent(ev); + if (!ev.buttons) { + // if no other button is held remove global handlers + this._document!.removeEventListener('mouseup', requestedEvents.mouseup!); + if (requestedEvents.mousedrag) { + this._document!.removeEventListener('mousemove', requestedEvents.mousedrag); + } + } + return this.cancel(ev); + }, + wheel: (ev: WheelEvent) => { + sendEvent(ev); + return this.cancel(ev, true); + }, + mousedrag: (ev: MouseEvent) => { + // deal only with move while a button is held + if (ev.buttons) { + sendEvent(ev); + } + }, + mousemove: (ev: MouseEvent) => { + // deal only with move without any button + if (!ev.buttons) { + sendEvent(ev); + } + } + }; + this.register(this.coreMouseService.onProtocolChange(events => { + // apply global changes on events + if (events) { + if (this.optionsService.rawOptions.logLevel === 'debug') { + this._logService.debug('Binding to mouse events:', this.coreMouseService.explainEvents(events)); + } + this.element!.classList.add('enable-mouse-events'); + this._selectionService!.disable(); + } else { + this._logService.debug('Unbinding from mouse events.'); + this.element!.classList.remove('enable-mouse-events'); + this._selectionService!.enable(); + } + + // add/remove handlers from requestedEvents + + if (!(events & CoreMouseEventType.MOVE)) { + el.removeEventListener('mousemove', requestedEvents.mousemove!); + requestedEvents.mousemove = null; + } else if (!requestedEvents.mousemove) { + el.addEventListener('mousemove', eventListeners.mousemove); + requestedEvents.mousemove = eventListeners.mousemove; + } + + if (!(events & CoreMouseEventType.WHEEL)) { + el.removeEventListener('wheel', requestedEvents.wheel!); + requestedEvents.wheel = null; + } else if (!requestedEvents.wheel) { + el.addEventListener('wheel', eventListeners.wheel, { passive: false }); + requestedEvents.wheel = eventListeners.wheel; + } + + if (!(events & CoreMouseEventType.UP)) { + this._document!.removeEventListener('mouseup', requestedEvents.mouseup!); + requestedEvents.mouseup = null; + } else if (!requestedEvents.mouseup) { + requestedEvents.mouseup = eventListeners.mouseup; + } + + if (!(events & CoreMouseEventType.DRAG)) { + this._document!.removeEventListener('mousemove', requestedEvents.mousedrag!); + requestedEvents.mousedrag = null; + } else if (!requestedEvents.mousedrag) { + requestedEvents.mousedrag = eventListeners.mousedrag; + } + })); + // force initial onProtocolChange so we dont miss early mouse requests + this.coreMouseService.activeProtocol = this.coreMouseService.activeProtocol; + + /** + * "Always on" event listeners. + */ + this.register(addDisposableDomListener(el, 'mousedown', (ev: MouseEvent) => { + ev.preventDefault(); + this.focus(); + + // Don't send the mouse button to the pty if mouse events are disabled or + // if the selection manager is having selection forced (ie. a modifier is + // held). + if (!this.coreMouseService.areMouseEventsActive || this._selectionService!.shouldForceSelection(ev)) { + return; + } + + sendEvent(ev); + + // Register additional global handlers which should keep reporting outside + // of the terminal element. + // Note: Other emulators also do this for 'mousedown' while a button + // is held, we currently limit 'mousedown' to the terminal only. + if (requestedEvents.mouseup) { + this._document!.addEventListener('mouseup', requestedEvents.mouseup); + } + if (requestedEvents.mousedrag) { + this._document!.addEventListener('mousemove', requestedEvents.mousedrag); + } + + return this.cancel(ev); + })); + + this.register(addDisposableDomListener(el, 'wheel', (ev: WheelEvent) => { + // do nothing, if app side handles wheel itself + if (requestedEvents.wheel) return; + + if (!this.buffer.hasScrollback) { + // Convert wheel events into up/down events when the buffer does not have scrollback, this + // enables scrolling in apps hosted in the alt buffer such as vim or tmux. + const amount = this.viewport!.getLinesScrolled(ev); + + // Do nothing if there's no vertical scroll + if (amount === 0) { + return; + } + + // Construct and send sequences + const sequence = C0.ESC + (this.coreService.decPrivateModes.applicationCursorKeys ? 'O' : '[') + (ev.deltaY < 0 ? 'A' : 'B'); + let data = ''; + for (let i = 0; i < Math.abs(amount); i++) { + data += sequence; + } + this.coreService.triggerDataEvent(data, true); + return this.cancel(ev, true); + } + + // normal viewport scrolling + // conditionally stop event, if the viewport still had rows to scroll within + if (this.viewport!.onWheel(ev)) { + return this.cancel(ev); + } + }, { passive: false })); + + this.register(addDisposableDomListener(el, 'touchstart', (ev: TouchEvent) => { + if (this.coreMouseService.areMouseEventsActive) return; + this.viewport!.onTouchStart(ev); + return this.cancel(ev); + }, { passive: true })); + + this.register(addDisposableDomListener(el, 'touchmove', (ev: TouchEvent) => { + if (this.coreMouseService.areMouseEventsActive) return; + if (!this.viewport!.onTouchMove(ev)) { + return this.cancel(ev); + } + }, { passive: false })); + } + + + /** + * Tells the renderer to refresh terminal content between two rows (inclusive) at the next + * opportunity. + * @param start The row to start from (between 0 and this.rows - 1). + * @param end The row to end at (between start and this.rows - 1). + */ + public refresh(start: number, end: number): void { + this._renderService?.refreshRows(start, end); + } + + /** + * Queues linkification for the specified rows. + * @param start The row to start from (between 0 and this.rows - 1). + * @param end The row to end at (between start and this.rows - 1). + */ + private _queueLinkification(start: number, end: number): void { + this.linkifier?.linkifyRows(start, end); + } + + /** + * Change the cursor style for different selection modes + */ + public updateCursorStyle(ev: KeyboardEvent): void { + if (this._selectionService?.shouldColumnSelect(ev)) { + this.element!.classList.add('column-select'); + } else { + this.element!.classList.remove('column-select'); + } + } + + /** + * Display the cursor element + */ + private _showCursor(): void { + if (!this.coreService.isCursorInitialized) { + this.coreService.isCursorInitialized = true; + this.refresh(this.buffer.y, this.buffer.y); + } + } + + public scrollLines(disp: number, suppressScrollEvent?: boolean, source = ScrollSource.TERMINAL): void { + super.scrollLines(disp, suppressScrollEvent, source); + this.refresh(0, this.rows - 1); + } + + public paste(data: string): void { + paste(data, this.textarea!, this.coreService); + } + + /** + * Attaches a custom key event handler which is run before keys are processed, + * giving consumers of xterm.js ultimate control as to what keys should be + * processed by the terminal and what keys should not. + * @param customKeyEventHandler The custom KeyboardEvent handler to attach. + * This is a function that takes a KeyboardEvent, allowing consumers to stop + * propagation and/or prevent the default action. The function returns whether + * the event should be processed by xterm.js. + */ + public attachCustomKeyEventHandler(customKeyEventHandler: CustomKeyEventHandler): void { + this._customKeyEventHandler = customKeyEventHandler; + } + + /** + * Registers a link matcher, allowing custom link patterns to be matched and + * handled. + * @param regex The regular expression to search for, specifically + * this searches the textContent of the rows. You will want to use \s to match + * a space ' ' character for example. + * @param handler The callback when the link is called. + * @param options Options for the link matcher. + * @return The ID of the new matcher, this can be used to deregister. + */ + public registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options?: ILinkMatcherOptions): number { + const matcherId = this.linkifier.registerLinkMatcher(regex, handler, options); + this.refresh(0, this.rows - 1); + return matcherId; + } + + /** + * Deregisters a link matcher if it has been registered. + * @param matcherId The link matcher's ID (returned after register) + */ + public deregisterLinkMatcher(matcherId: number): void { + if (this.linkifier.deregisterLinkMatcher(matcherId)) { + this.refresh(0, this.rows - 1); + } + } + + public registerLinkProvider(linkProvider: ILinkProvider): IDisposable { + return this.linkifier2.registerLinkProvider(linkProvider); + } + + public registerCharacterJoiner(handler: CharacterJoinerHandler): number { + if (!this._characterJoinerService) { + throw new Error('Terminal must be opened first'); + } + const joinerId = this._characterJoinerService.register(handler); + this.refresh(0, this.rows - 1); + return joinerId; + } + + public deregisterCharacterJoiner(joinerId: number): void { + if (!this._characterJoinerService) { + throw new Error('Terminal must be opened first'); + } + if (this._characterJoinerService.deregister(joinerId)) { + this.refresh(0, this.rows - 1); + } + } + + public get markers(): IMarker[] { + return this.buffer.markers; + } + + public addMarker(cursorYOffset: number): IMarker | undefined { + // Disallow markers on the alt buffer + if (this.buffer !== this.buffers.normal) { + return; + } + + return this.buffer.addMarker(this.buffer.ybase + this.buffer.y + cursorYOffset); + } + + /** + * Gets whether the terminal has an active selection. + */ + public hasSelection(): boolean { + return this._selectionService ? this._selectionService.hasSelection : false; + } + + /** + * Selects text within the terminal. + * @param column The column the selection starts at.. + * @param row The row the selection starts at. + * @param length The length of the selection. + */ + public select(column: number, row: number, length: number): void { + this._selectionService!.setSelection(column, row, length); + } + + /** + * Gets the terminal's current selection, this is useful for implementing copy + * behavior outside of xterm.js. + */ + public getSelection(): string { + return this._selectionService ? this._selectionService.selectionText : ''; + } + + public getSelectionPosition(): ISelectionPosition | undefined { + if (!this._selectionService || !this._selectionService.hasSelection) { + return undefined; + } + + return { + startColumn: this._selectionService.selectionStart![0], + startRow: this._selectionService.selectionStart![1], + endColumn: this._selectionService.selectionEnd![0], + endRow: this._selectionService.selectionEnd![1] + }; + } + + /** + * Clears the current terminal selection. + */ + public clearSelection(): void { + this._selectionService?.clearSelection(); + } + + /** + * Selects all text within the terminal. + */ + public selectAll(): void { + this._selectionService?.selectAll(); + } + + public selectLines(start: number, end: number): void { + this._selectionService?.selectLines(start, end); + } + + /** + * Handle a keydown event + * Key Resources: + * - https://developer.mozilla.org/en-US/docs/DOM/KeyboardEvent + * @param ev The keydown event to be handled. + */ + protected _keyDown(event: KeyboardEvent): boolean | undefined { + this._keyDownHandled = false; + + if (this._customKeyEventHandler && this._customKeyEventHandler(event) === false) { + return false; + } + + if (!this._compositionHelper!.keydown(event)) { + if (this.buffer.ybase !== this.buffer.ydisp) { + this._bufferService.scrollToBottom(); + } + return false; + } + + if (event.key === 'Dead' || event.key === 'AltGraph') { + this._unprocessedDeadKey = true; + } + + const result = evaluateKeyboardEvent(event, this.coreService.decPrivateModes.applicationCursorKeys, this.browser.isMac, this.options.macOptionIsMeta); + + this.updateCursorStyle(event); + + if (result.type === KeyboardResultType.PAGE_DOWN || result.type === KeyboardResultType.PAGE_UP) { + const scrollCount = this.rows - 1; + this.scrollLines(result.type === KeyboardResultType.PAGE_UP ? -scrollCount : scrollCount); + return this.cancel(event, true); + } + + if (result.type === KeyboardResultType.SELECT_ALL) { + this.selectAll(); + } + + if (this._isThirdLevelShift(this.browser, event)) { + return true; + } + + if (result.cancel) { + // The event is canceled at the end already, is this necessary? + this.cancel(event, true); + } + + if (!result.key) { + return true; + } + + if (this._unprocessedDeadKey) { + this._unprocessedDeadKey = false; + return true; + } + + // If ctrl+c or enter is being sent, clear out the textarea. This is done so that screen readers + // will announce deleted characters. This will not work 100% of the time but it should cover + // most scenarios. + if (result.key === C0.ETX || result.key === C0.CR) { + this.textarea!.value = ''; + } + + this._onKey.fire({ key: result.key, domEvent: event }); + this._showCursor(); + this.coreService.triggerDataEvent(result.key, true); + + // Cancel events when not in screen reader mode so events don't get bubbled up and handled by + // other listeners. When screen reader mode is enabled, this could cause issues if the event + // is handled at a higher level, this is a compromise in order to echo keys to the screen + // reader. + if (!this.optionsService.rawOptions.screenReaderMode) { + return this.cancel(event, true); + } + + this._keyDownHandled = true; + } + + private _isThirdLevelShift(browser: IBrowser, ev: KeyboardEvent): boolean { + const thirdLevelKey = + (browser.isMac && !this.options.macOptionIsMeta && ev.altKey && !ev.ctrlKey && !ev.metaKey) || + (browser.isWindows && ev.altKey && ev.ctrlKey && !ev.metaKey) || + (browser.isWindows && ev.getModifierState('AltGraph')); + + if (ev.type === 'keypress') { + return thirdLevelKey; + } + + // Don't invoke for arrows, pageDown, home, backspace, etc. (on non-keypress events) + return thirdLevelKey && (!ev.keyCode || ev.keyCode > 47); + } + + protected _keyUp(ev: KeyboardEvent): void { + if (this._customKeyEventHandler && this._customKeyEventHandler(ev) === false) { + return; + } + + if (!wasModifierKeyOnlyEvent(ev)) { + this.focus(); + } + + this.updateCursorStyle(ev); + this._keyPressHandled = false; + } + + /** + * Handle a keypress event. + * Key Resources: + * - https://developer.mozilla.org/en-US/docs/DOM/KeyboardEvent + * @param ev The keypress event to be handled. + */ + protected _keyPress(ev: KeyboardEvent): boolean { + let key; + + this._keyPressHandled = false; + + if (this._keyDownHandled) { + return false; + } + + if (this._customKeyEventHandler && this._customKeyEventHandler(ev) === false) { + return false; + } + + this.cancel(ev); + + if (ev.charCode) { + key = ev.charCode; + } else if (ev.which === null || ev.which === undefined) { + key = ev.keyCode; + } else if (ev.which !== 0 && ev.charCode !== 0) { + key = ev.which; + } else { + return false; + } + + if (!key || ( + (ev.altKey || ev.ctrlKey || ev.metaKey) && !this._isThirdLevelShift(this.browser, ev) + )) { + return false; + } + + key = String.fromCharCode(key); + + this._onKey.fire({ key, domEvent: ev }); + this._showCursor(); + this.coreService.triggerDataEvent(key, true); + + this._keyPressHandled = true; + + // The key was handled so clear the dead key state, otherwise certain keystrokes like arrow + // keys could be ignored + this._unprocessedDeadKey = false; + + return true; + } + + /** + * Handle an input event. + * Key Resources: + * - https://developer.mozilla.org/en-US/docs/Web/API/InputEvent + * @param ev The input event to be handled. + */ + protected _inputEvent(ev: InputEvent): boolean { + // Only support emoji IMEs when screen reader mode is disabled as the event must bubble up to + // support reading out character input which can doubling up input characters + if (ev.data && ev.inputType === 'insertText' && !ev.composed && !this.optionsService.rawOptions.screenReaderMode) { + if (this._keyPressHandled) { + return false; + } + + // The key was handled so clear the dead key state, otherwise certain keystrokes like arrow + // keys could be ignored + this._unprocessedDeadKey = false; + + const text = ev.data; + this.coreService.triggerDataEvent(text, true); + + this.cancel(ev); + return true; + } + + return false; + } + + /** + * Ring the bell. + * Note: We could do sweet things with webaudio here + */ + public bell(): void { + if (this._soundBell()) { + this._soundService?.playBellSound(); + } + + this._onBell.fire(); + + // if (this._visualBell()) { + // this.element.classList.add('visual-bell-active'); + // clearTimeout(this._visualBellTimer); + // this._visualBellTimer = window.setTimeout(() => { + // this.element.classList.remove('visual-bell-active'); + // }, 200); + // } + } + + /** + * Resizes the terminal. + * + * @param x The number of columns to resize to. + * @param y The number of rows to resize to. + */ + public resize(x: number, y: number): void { + if (x === this.cols && y === this.rows) { + // Check if we still need to measure the char size (fixes #785). + if (this._charSizeService && !this._charSizeService.hasValidSize) { + this._charSizeService.measure(); + } + return; + } + + super.resize(x, y); + } + + private _afterResize(x: number, y: number): void { + this._charSizeService?.measure(); + + // Sync the scroll area to make sure scroll events don't fire and scroll the viewport to an + // invalid location + this.viewport?.syncScrollArea(true); + } + + /** + * Clear the entire buffer, making the prompt line the new first line. + */ + public clear(): void { + if (this.buffer.ybase === 0 && this.buffer.y === 0) { + // Don't clear if it's already clear + return; + } + this.buffer.lines.set(0, this.buffer.lines.get(this.buffer.ybase + this.buffer.y)!); + this.buffer.lines.length = 1; + this.buffer.ydisp = 0; + this.buffer.ybase = 0; + this.buffer.y = 0; + for (let i = 1; i < this.rows; i++) { + this.buffer.lines.push(this.buffer.getBlankLine(DEFAULT_ATTR_DATA)); + } + this.refresh(0, this.rows - 1); + this._onScroll.fire({ position: this.buffer.ydisp, source: ScrollSource.TERMINAL }); + } + + /** + * Reset terminal. + * Note: Calling this directly from JS is synchronous but does not clear + * input buffers and does not reset the parser, thus the terminal will + * continue to apply pending input data. + * If you need in band reset (synchronous with input data) consider + * using DECSTR (soft reset, CSI ! p) or RIS instead (hard reset, ESC c). + */ + public reset(): void { + /** + * Since _setup handles a full terminal creation, we have to carry forward + * a few things that should not reset. + */ + this.options.rows = this.rows; + this.options.cols = this.cols; + const customKeyEventHandler = this._customKeyEventHandler; + + this._setup(); + super.reset(); + this._selectionService?.reset(); + + // reattach + this._customKeyEventHandler = customKeyEventHandler; + + // do a full screen refresh + this.refresh(0, this.rows - 1); + this.viewport?.syncScrollArea(); + } + + public clearTextureAtlas(): void { + this._renderService?.clearTextureAtlas(); + } + + private _reportFocus(): void { + if (this.element?.classList.contains('focus')) { + this.coreService.triggerDataEvent(C0.ESC + '[I'); + } else { + this.coreService.triggerDataEvent(C0.ESC + '[O'); + } + } + + private _reportWindowsOptions(type: WindowsOptionsReportType): void { + if (!this._renderService) { + return; + } + + switch (type) { + case WindowsOptionsReportType.GET_WIN_SIZE_PIXELS: + const canvasWidth = this._renderService.dimensions.scaledCanvasWidth.toFixed(0); + const canvasHeight = this._renderService.dimensions.scaledCanvasHeight.toFixed(0); + this.coreService.triggerDataEvent(`${C0.ESC}[4;${canvasHeight};${canvasWidth}t`); + break; + case WindowsOptionsReportType.GET_CELL_SIZE_PIXELS: + const cellWidth = this._renderService.dimensions.scaledCellWidth.toFixed(0); + const cellHeight = this._renderService.dimensions.scaledCellHeight.toFixed(0); + this.coreService.triggerDataEvent(`${C0.ESC}[6;${cellHeight};${cellWidth}t`); + break; + } + } + + // TODO: Remove cancel function and cancelEvents option + public cancel(ev: Event, force?: boolean): boolean | undefined { + if (!this.options.cancelEvents && !force) { + return; + } + ev.preventDefault(); + ev.stopPropagation(); + return false; + } + + private _visualBell(): boolean { + return false; + // return this.options.bellStyle === 'visual' || + // this.options.bellStyle === 'both'; + } + + private _soundBell(): boolean { + return this.options.bellStyle === 'sound'; + // return this.options.bellStyle === 'sound' || + // this.options.bellStyle === 'both'; + } +} + +/** + * Helpers + */ + +function wasModifierKeyOnlyEvent(ev: KeyboardEvent): boolean { + return ev.keyCode === 16 || // Shift + ev.keyCode === 17 || // Ctrl + ev.keyCode === 18; // Alt +} diff --git a/node_modules/xterm/src/browser/TimeBasedDebouncer.ts b/node_modules/xterm/src/browser/TimeBasedDebouncer.ts new file mode 100644 index 0000000..707e25c --- /dev/null +++ b/node_modules/xterm/src/browser/TimeBasedDebouncer.ts @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +const RENDER_DEBOUNCE_THRESHOLD_MS = 1000; // 1 Second + +import { IRenderDebouncer } from 'browser/Types'; + +/** + * Debounces calls to update screen readers to update at most once configurable interval of time. + */ +export class TimeBasedDebouncer implements IRenderDebouncer { + private _rowStart: number | undefined; + private _rowEnd: number | undefined; + private _rowCount: number | undefined; + + // The last moment that the Terminal was refreshed at + private _lastRefreshMs = 0; + // Whether a trailing refresh should be triggered due to a refresh request that was throttled + private _additionalRefreshRequested = false; + + private _refreshTimeoutID: number | undefined; + + constructor( + private _renderCallback: (start: number, end: number) => void, + private readonly _debounceThresholdMS = RENDER_DEBOUNCE_THRESHOLD_MS + ) { + } + + public dispose(): void { + if (this._refreshTimeoutID) { + clearTimeout(this._refreshTimeoutID); + } + } + + public refresh(rowStart: number | undefined, rowEnd: number | undefined, rowCount: number): void { + this._rowCount = rowCount; + // Get the min/max row start/end for the arg values + rowStart = rowStart !== undefined ? rowStart : 0; + rowEnd = rowEnd !== undefined ? rowEnd : this._rowCount - 1; + // Set the properties to the updated values + this._rowStart = this._rowStart !== undefined ? Math.min(this._rowStart, rowStart) : rowStart; + this._rowEnd = this._rowEnd !== undefined ? Math.max(this._rowEnd, rowEnd) : rowEnd; + + // Only refresh if the time since last refresh is above a threshold, otherwise wait for + // enough time to pass before refreshing again. + const refreshRequestTime: number = Date.now(); + if (refreshRequestTime - this._lastRefreshMs >= this._debounceThresholdMS) { + // Enough time has lapsed since the last refresh; refresh immediately + this._lastRefreshMs = refreshRequestTime; + this._innerRefresh(); + } else if (!this._additionalRefreshRequested) { + // This is the first additional request throttled; set up trailing refresh + const elapsed = refreshRequestTime - this._lastRefreshMs; + const waitPeriodBeforeTrailingRefresh = this._debounceThresholdMS - elapsed; + this._additionalRefreshRequested = true; + + this._refreshTimeoutID = window.setTimeout(() => { + this._lastRefreshMs = Date.now(); + this._innerRefresh(); + this._additionalRefreshRequested = false; + this._refreshTimeoutID = undefined; // No longer need to clear the timeout + }, waitPeriodBeforeTrailingRefresh); + } + } + + private _innerRefresh(): void { + // Make sure values are set + if (this._rowStart === undefined || this._rowEnd === undefined || this._rowCount === undefined) { + return; + } + + // Clamp values + const start = Math.max(this._rowStart, 0); + const end = Math.min(this._rowEnd, this._rowCount - 1); + + // Reset debouncer (this happens before render callback as the render could trigger it again) + this._rowStart = undefined; + this._rowEnd = undefined; + + // Run render callback + this._renderCallback(start, end); + } +} + diff --git a/node_modules/xterm/src/browser/Types.d.ts b/node_modules/xterm/src/browser/Types.d.ts new file mode 100644 index 0000000..a616584 --- /dev/null +++ b/node_modules/xterm/src/browser/Types.d.ts @@ -0,0 +1,315 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IDisposable, IMarker, ISelectionPosition } from 'xterm'; +import { IEvent } from 'common/EventEmitter'; +import { ICoreTerminal, CharData, ITerminalOptions } from 'common/Types'; +import { IMouseService, IRenderService } from './services/Services'; +import { IBuffer, IBufferSet } from 'common/buffer/Types'; +import { IFunctionIdentifier, IParams } from 'common/parser/Types'; + +export interface ITerminal extends IPublicTerminal, ICoreTerminal { + element: HTMLElement | undefined; + screenElement: HTMLElement | undefined; + browser: IBrowser; + buffer: IBuffer; + viewport: IViewport | undefined; + options: ITerminalOptions; + linkifier: ILinkifier; + linkifier2: ILinkifier2; + + onBlur: IEvent<void>; + onFocus: IEvent<void>; + onA11yChar: IEvent<string>; + onA11yTab: IEvent<number>; + + cancel(ev: Event, force?: boolean): boolean | void; +} + +// Portions of the public API that are required by the internal Terminal +export interface IPublicTerminal extends IDisposable { + textarea: HTMLTextAreaElement | undefined; + rows: number; + cols: number; + buffer: IBuffer; + markers: IMarker[]; + onCursorMove: IEvent<void>; + onData: IEvent<string>; + onBinary: IEvent<string>; + onKey: IEvent<{ key: string, domEvent: KeyboardEvent }>; + onLineFeed: IEvent<void>; + onScroll: IEvent<number>; + onSelectionChange: IEvent<void>; + onRender: IEvent<{ start: number, end: number }>; + onResize: IEvent<{ cols: number, rows: number }>; + onTitleChange: IEvent<string>; + onBell: IEvent<void>; + blur(): void; + focus(): void; + resize(columns: number, rows: number): void; + open(parent: HTMLElement): void; + attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void; + registerCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean | Promise<boolean>): IDisposable; + registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean | Promise<boolean>): IDisposable; + registerEscHandler(id: IFunctionIdentifier, callback: () => boolean | Promise<boolean>): IDisposable; + registerOscHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable; + registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): number; + deregisterLinkMatcher(matcherId: number): void; + registerLinkProvider(linkProvider: ILinkProvider): IDisposable; + registerCharacterJoiner(handler: (text: string) => [number, number][]): number; + deregisterCharacterJoiner(joinerId: number): void; + addMarker(cursorYOffset: number): IMarker | undefined; + hasSelection(): boolean; + getSelection(): string; + getSelectionPosition(): ISelectionPosition | undefined; + clearSelection(): void; + select(column: number, row: number, length: number): void; + selectAll(): void; + selectLines(start: number, end: number): void; + dispose(): void; + scrollLines(amount: number): void; + scrollPages(pageCount: number): void; + scrollToTop(): void; + scrollToBottom(): void; + scrollToLine(line: number): void; + clear(): void; + write(data: string | Uint8Array, callback?: () => void): void; + paste(data: string): void; + refresh(start: number, end: number): void; + clearTextureAtlas(): void; + reset(): void; +} + +export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean; + +export type LineData = CharData[]; + +export interface ICompositionHelper { + readonly isComposing: boolean; + compositionstart(): void; + compositionupdate(ev: CompositionEvent): void; + compositionend(): void; + updateCompositionElements(dontRecurse?: boolean): void; + keydown(ev: KeyboardEvent): boolean; +} + +export interface IBrowser { + isNode: boolean; + userAgent: string; + platform: string; + isFirefox: boolean; + isMac: boolean; + isIpad: boolean; + isIphone: boolean; + isWindows: boolean; +} + +export interface IColorManager { + colors: IColorSet; + onOptionsChange(key: string): void; +} + +export interface IColor { + css: string; + rgba: number; // 32-bit int with rgba in each byte +} + +export interface IColorSet { + foreground: IColor; + background: IColor; + cursor: IColor; + cursorAccent: IColor; + selectionTransparent: IColor; + /** The selection blended on top of background. */ + selectionOpaque: IColor; + ansi: IColor[]; + contrastCache: IColorContrastCache; +} + +export interface IColorContrastCache { + clear(): void; + setCss(bg: number, fg: number, value: string | null): void; + getCss(bg: number, fg: number): string | null | undefined; + setColor(bg: number, fg: number, value: IColor | null): void; + getColor(bg: number, fg: number): IColor | null | undefined; +} + +export interface IPartialColorSet { + foreground: IColor; + background: IColor; + cursor?: IColor; + cursorAccent?: IColor; + selection?: IColor; + ansi: IColor[]; +} + +export interface IViewport extends IDisposable { + scrollBarWidth: number; + syncScrollArea(immediate?: boolean): void; + getLinesScrolled(ev: WheelEvent): number; + onWheel(ev: WheelEvent): boolean; + onTouchStart(ev: TouchEvent): void; + onTouchMove(ev: TouchEvent): boolean; + onThemeChange(colors: IColorSet): void; +} + +export interface IViewportRange { + start: IViewportRangePosition; + end: IViewportRangePosition; +} + +export interface IViewportRangePosition { + x: number; + y: number; +} + +export type LinkMatcherHandler = (event: MouseEvent, uri: string) => void; +export type LinkMatcherHoverTooltipCallback = (event: MouseEvent, uri: string, position: IViewportRange) => void; +export type LinkMatcherValidationCallback = (uri: string, callback: (isValid: boolean) => void) => void; + +export interface ILinkMatcher { + id: number; + regex: RegExp; + handler: LinkMatcherHandler; + hoverTooltipCallback?: LinkMatcherHoverTooltipCallback; + hoverLeaveCallback?: () => void; + matchIndex?: number; + validationCallback?: LinkMatcherValidationCallback; + priority?: number; + willLinkActivate?: (event: MouseEvent, uri: string) => boolean; +} + +export interface IRegisteredLinkMatcher extends ILinkMatcher { + priority: number; +} + +export interface ILinkifierEvent { + x1: number; + y1: number; + x2: number; + y2: number; + cols: number; + fg: number | undefined; +} + +export interface ILinkifier { + onShowLinkUnderline: IEvent<ILinkifierEvent>; + onHideLinkUnderline: IEvent<ILinkifierEvent>; + onLinkTooltip: IEvent<ILinkifierEvent>; + + attachToDom(element: HTMLElement, mouseZoneManager: IMouseZoneManager): void; + linkifyRows(start: number, end: number): void; + registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options?: ILinkMatcherOptions): number; + deregisterLinkMatcher(matcherId: number): boolean; +} + +interface ILinkState { + decorations: ILinkDecorations; + isHovered: boolean; +} +export interface ILinkWithState { + link: ILink; + state?: ILinkState; +} + +export interface ILinkifier2 { + onShowLinkUnderline: IEvent<ILinkifierEvent>; + onHideLinkUnderline: IEvent<ILinkifierEvent>; + readonly currentLink: ILinkWithState | undefined; + + attachToDom(element: HTMLElement, mouseService: IMouseService, renderService: IRenderService): void; + registerLinkProvider(linkProvider: ILinkProvider): IDisposable; +} + +export interface ILinkMatcherOptions { + /** + * The index of the link from the regex.match(text) call. This defaults to 0 + * (for regular expressions without capture groups). + */ + matchIndex?: number; + /** + * A callback that validates an individual link, returning true if valid and + * false if invalid. + */ + validationCallback?: LinkMatcherValidationCallback; + /** + * A callback that fires when the mouse hovers over a link. + */ + tooltipCallback?: LinkMatcherHoverTooltipCallback; + /** + * A callback that fires when the mouse leaves a link that was hovered. + */ + leaveCallback?: () => void; + /** + * The priority of the link matcher, this defines the order in which the link + * matcher is evaluated relative to others, from highest to lowest. The + * default value is 0. + */ + priority?: number; + /** + * A callback that fires when the mousedown and click events occur that + * determines whether a link will be activated upon click. This enables + * only activating a link when a certain modifier is held down, if not the + * mouse event will continue propagation (eg. double click to select word). + */ + willLinkActivate?: (event: MouseEvent, uri: string) => boolean; +} + +export interface IMouseZoneManager extends IDisposable { + add(zone: IMouseZone): void; + clearAll(start?: number, end?: number): void; +} + +export interface IMouseZone { + x1: number; + x2: number; + y1: number; + y2: number; + clickCallback: (e: MouseEvent) => any; + hoverCallback: (e: MouseEvent) => any | undefined; + tooltipCallback: (e: MouseEvent) => any | undefined; + leaveCallback: () => any | undefined; + willLinkActivate: (e: MouseEvent) => boolean; +} + +interface ILinkProvider { + provideLinks(y: number, callback: (links: ILink[] | undefined) => void): void; +} + +interface ILink { + range: IBufferRange; + text: string; + decorations?: ILinkDecorations; + activate(event: MouseEvent, text: string): void; + hover?(event: MouseEvent, text: string): void; + leave?(event: MouseEvent, text: string): void; + dispose?(): void; +} + +interface ILinkDecorations { + pointerCursor: boolean; + underline: boolean; +} + +interface IBufferRange { + start: IBufferCellPosition; + end: IBufferCellPosition; +} + +interface IBufferCellPosition { + x: number; + y: number; +} + +export type CharacterJoinerHandler = (text: string) => [number, number][]; + +export interface ICharacterJoiner { + id: number; + handler: CharacterJoinerHandler; +} + +export interface IRenderDebouncer extends IDisposable { + refresh(rowStart: number | undefined, rowEnd: number | undefined, rowCount: number): void; +} diff --git a/node_modules/xterm/src/browser/Viewport.ts b/node_modules/xterm/src/browser/Viewport.ts new file mode 100644 index 0000000..14fab89 --- /dev/null +++ b/node_modules/xterm/src/browser/Viewport.ts @@ -0,0 +1,294 @@ +/** + * Copyright (c) 2016 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { Disposable } from 'common/Lifecycle'; +import { addDisposableDomListener } from 'browser/Lifecycle'; +import { IColorSet, IViewport } from 'browser/Types'; +import { ICharSizeService, IRenderService } from 'browser/services/Services'; +import { IBufferService, IOptionsService } from 'common/services/Services'; +import { IBuffer } from 'common/buffer/Types'; +import { IRenderDimensions } from 'browser/renderer/Types'; + +const FALLBACK_SCROLL_BAR_WIDTH = 15; + +/** + * Represents the viewport of a terminal, the visible area within the larger buffer of output. + * Logic for the virtual scroll bar is included in this object. + */ +export class Viewport extends Disposable implements IViewport { + public scrollBarWidth: number = 0; + private _currentRowHeight: number = 0; + private _currentScaledCellHeight: number = 0; + private _lastRecordedBufferLength: number = 0; + private _lastRecordedViewportHeight: number = 0; + private _lastRecordedBufferHeight: number = 0; + private _lastTouchY: number = 0; + private _lastScrollTop: number = 0; + private _lastHadScrollBar: boolean = false; + private _activeBuffer: IBuffer; + private _renderDimensions: IRenderDimensions; + + // Stores a partial line amount when scrolling, this is used to keep track of how much of a line + // is scrolled so we can "scroll" over partial lines and feel natural on touchpads. This is a + // quick fix and could have a more robust solution in place that reset the value when needed. + private _wheelPartialScroll: number = 0; + + private _refreshAnimationFrame: number | null = null; + private _ignoreNextScrollEvent: boolean = false; + + constructor( + private readonly _scrollLines: (amount: number) => void, + private readonly _viewportElement: HTMLElement, + private readonly _scrollArea: HTMLElement, + private readonly _element: HTMLElement, + @IBufferService private readonly _bufferService: IBufferService, + @IOptionsService private readonly _optionsService: IOptionsService, + @ICharSizeService private readonly _charSizeService: ICharSizeService, + @IRenderService private readonly _renderService: IRenderService + ) { + super(); + + // Measure the width of the scrollbar. If it is 0 we can assume it's an OSX overlay scrollbar. + // Unfortunately the overlay scrollbar would be hidden underneath the screen element in that case, + // therefore we account for a standard amount to make it visible + this.scrollBarWidth = (this._viewportElement.offsetWidth - this._scrollArea.offsetWidth) || FALLBACK_SCROLL_BAR_WIDTH; + this._lastHadScrollBar = true; + this.register(addDisposableDomListener(this._viewportElement, 'scroll', this._onScroll.bind(this))); + + // Track properties used in performance critical code manually to avoid using slow getters + this._activeBuffer = this._bufferService.buffer; + this.register(this._bufferService.buffers.onBufferActivate(e => this._activeBuffer = e.activeBuffer)); + this._renderDimensions = this._renderService.dimensions; + this.register(this._renderService.onDimensionsChange(e => this._renderDimensions = e)); + + // Perform this async to ensure the ICharSizeService is ready. + setTimeout(() => this.syncScrollArea(), 0); + } + + public onThemeChange(colors: IColorSet): void { + this._viewportElement.style.backgroundColor = colors.background.css; + } + + /** + * Refreshes row height, setting line-height, viewport height and scroll area height if + * necessary. + */ + private _refresh(immediate: boolean): void { + if (immediate) { + this._innerRefresh(); + if (this._refreshAnimationFrame !== null) { + cancelAnimationFrame(this._refreshAnimationFrame); + } + return; + } + if (this._refreshAnimationFrame === null) { + this._refreshAnimationFrame = requestAnimationFrame(() => this._innerRefresh()); + } + } + + private _innerRefresh(): void { + if (this._charSizeService.height > 0) { + this._currentRowHeight = this._renderService.dimensions.scaledCellHeight / window.devicePixelRatio; + this._currentScaledCellHeight = this._renderService.dimensions.scaledCellHeight; + this._lastRecordedViewportHeight = this._viewportElement.offsetHeight; + const newBufferHeight = Math.round(this._currentRowHeight * this._lastRecordedBufferLength) + (this._lastRecordedViewportHeight - this._renderService.dimensions.canvasHeight); + if (this._lastRecordedBufferHeight !== newBufferHeight) { + this._lastRecordedBufferHeight = newBufferHeight; + this._scrollArea.style.height = this._lastRecordedBufferHeight + 'px'; + } + } + + // Sync scrollTop + const scrollTop = this._bufferService.buffer.ydisp * this._currentRowHeight; + if (this._viewportElement.scrollTop !== scrollTop) { + // Ignore the next scroll event which will be triggered by setting the scrollTop as we do not + // want this event to scroll the terminal + this._ignoreNextScrollEvent = true; + this._viewportElement.scrollTop = scrollTop; + } + + // Update scroll bar width + if (this._optionsService.rawOptions.scrollback === 0) { + this.scrollBarWidth = 0; + } else { + this.scrollBarWidth = (this._viewportElement.offsetWidth - this._scrollArea.offsetWidth) || FALLBACK_SCROLL_BAR_WIDTH; + } + this._lastHadScrollBar = this.scrollBarWidth > 0; + + const elementStyle = window.getComputedStyle(this._element); + const elementPadding = parseInt(elementStyle.paddingLeft) + parseInt(elementStyle.paddingRight); + this._viewportElement.style.width = (this._renderService.dimensions.actualCellWidth * (this._bufferService.cols) + this.scrollBarWidth + (this._lastHadScrollBar ? elementPadding : 0)).toString() + 'px'; + this._refreshAnimationFrame = null; + } + + /** + * Updates dimensions and synchronizes the scroll area if necessary. + */ + public syncScrollArea(immediate: boolean = false): void { + // If buffer height changed + if (this._lastRecordedBufferLength !== this._bufferService.buffer.lines.length) { + this._lastRecordedBufferLength = this._bufferService.buffer.lines.length; + this._refresh(immediate); + return; + } + + // If viewport height changed + if (this._lastRecordedViewportHeight !== this._renderService.dimensions.canvasHeight) { + this._refresh(immediate); + return; + } + + // If the buffer position doesn't match last scroll top + if (this._lastScrollTop !== this._activeBuffer.ydisp * this._currentRowHeight) { + this._refresh(immediate); + return; + } + + // If row height changed + if (this._renderDimensions.scaledCellHeight !== this._currentScaledCellHeight) { + this._refresh(immediate); + return; + } + + // If the scroll bar visibility changed + if (this._lastHadScrollBar !== (this._optionsService.rawOptions.scrollback > 0)) { + this._refresh(immediate); + } + } + + /** + * Handles scroll events on the viewport, calculating the new viewport and requesting the + * terminal to scroll to it. + * @param ev The scroll event. + */ + private _onScroll(ev: Event): void { + // Record current scroll top position + this._lastScrollTop = this._viewportElement.scrollTop; + + // Don't attempt to scroll if the element is not visible, otherwise scrollTop will be corrupt + // which causes the terminal to scroll the buffer to the top + if (!this._viewportElement.offsetParent) { + return; + } + + // Ignore the event if it was flagged to ignore (when the source of the event is from Viewport) + if (this._ignoreNextScrollEvent) { + this._ignoreNextScrollEvent = false; + // Still trigger the scroll so lines get refreshed + this._scrollLines(0); + return; + } + + const newRow = Math.round(this._lastScrollTop / this._currentRowHeight); + const diff = newRow - this._bufferService.buffer.ydisp; + this._scrollLines(diff); + } + + /** + * Handles bubbling of scroll event in case the viewport has reached top or bottom + * @param ev The scroll event. + * @param amount The amount scrolled + */ + private _bubbleScroll(ev: Event, amount: number): boolean { + const scrollPosFromTop = this._viewportElement.scrollTop + this._lastRecordedViewportHeight; + if ((amount < 0 && this._viewportElement.scrollTop !== 0) || + (amount > 0 && scrollPosFromTop < this._lastRecordedBufferHeight)) { + if (ev.cancelable) { + ev.preventDefault(); + } + return false; + } + return true; + } + + /** + * Handles mouse wheel events by adjusting the viewport's scrollTop and delegating the actual + * scrolling to `onScroll`, this event needs to be attached manually by the consumer of + * `Viewport`. + * @param ev The mouse wheel event. + */ + public onWheel(ev: WheelEvent): boolean { + const amount = this._getPixelsScrolled(ev); + if (amount === 0) { + return false; + } + this._viewportElement.scrollTop += amount; + return this._bubbleScroll(ev, amount); + } + + private _getPixelsScrolled(ev: WheelEvent): number { + // Do nothing if it's not a vertical scroll event + if (ev.deltaY === 0 || ev.shiftKey) { + return 0; + } + + // Fallback to WheelEvent.DOM_DELTA_PIXEL + let amount = this._applyScrollModifier(ev.deltaY, ev); + if (ev.deltaMode === WheelEvent.DOM_DELTA_LINE) { + amount *= this._currentRowHeight; + } else if (ev.deltaMode === WheelEvent.DOM_DELTA_PAGE) { + amount *= this._currentRowHeight * this._bufferService.rows; + } + return amount; + } + + /** + * Gets the number of pixels scrolled by the mouse event taking into account what type of delta + * is being used. + * @param ev The mouse wheel event. + */ + public getLinesScrolled(ev: WheelEvent): number { + // Do nothing if it's not a vertical scroll event + if (ev.deltaY === 0 || ev.shiftKey) { + return 0; + } + + // Fallback to WheelEvent.DOM_DELTA_LINE + let amount = this._applyScrollModifier(ev.deltaY, ev); + if (ev.deltaMode === WheelEvent.DOM_DELTA_PIXEL) { + amount /= this._currentRowHeight + 0.0; // Prevent integer division + this._wheelPartialScroll += amount; + amount = Math.floor(Math.abs(this._wheelPartialScroll)) * (this._wheelPartialScroll > 0 ? 1 : -1); + this._wheelPartialScroll %= 1; + } else if (ev.deltaMode === WheelEvent.DOM_DELTA_PAGE) { + amount *= this._bufferService.rows; + } + return amount; + } + + private _applyScrollModifier(amount: number, ev: WheelEvent): number { + const modifier = this._optionsService.rawOptions.fastScrollModifier; + // Multiply the scroll speed when the modifier is down + if ((modifier === 'alt' && ev.altKey) || + (modifier === 'ctrl' && ev.ctrlKey) || + (modifier === 'shift' && ev.shiftKey)) { + return amount * this._optionsService.rawOptions.fastScrollSensitivity * this._optionsService.rawOptions.scrollSensitivity; + } + + return amount * this._optionsService.rawOptions.scrollSensitivity; + } + + /** + * Handles the touchstart event, recording the touch occurred. + * @param ev The touch event. + */ + public onTouchStart(ev: TouchEvent): void { + this._lastTouchY = ev.touches[0].pageY; + } + + /** + * Handles the touchmove event, scrolling the viewport if the position shifted. + * @param ev The touch event. + */ + public onTouchMove(ev: TouchEvent): boolean { + const deltaY = this._lastTouchY - ev.touches[0].pageY; + this._lastTouchY = ev.touches[0].pageY; + if (deltaY === 0) { + return false; + } + this._viewportElement.scrollTop += deltaY; + return this._bubbleScroll(ev, deltaY); + } +} diff --git a/node_modules/xterm/src/browser/input/CompositionHelper.ts b/node_modules/xterm/src/browser/input/CompositionHelper.ts new file mode 100644 index 0000000..61051b5 --- /dev/null +++ b/node_modules/xterm/src/browser/input/CompositionHelper.ts @@ -0,0 +1,237 @@ +/** + * Copyright (c) 2016 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IRenderService } from 'browser/services/Services'; +import { IBufferService, ICoreService, IOptionsService } from 'common/services/Services'; + +interface IPosition { + start: number; + end: number; +} + +/** + * Encapsulates the logic for handling compositionstart, compositionupdate and compositionend + * events, displaying the in-progress composition to the UI and forwarding the final composition + * to the handler. + */ +export class CompositionHelper { + /** + * Whether input composition is currently happening, eg. via a mobile keyboard, speech input or + * IME. This variable determines whether the compositionText should be displayed on the UI. + */ + private _isComposing: boolean; + public get isComposing(): boolean { return this._isComposing; } + + /** + * The position within the input textarea's value of the current composition. + */ + private _compositionPosition: IPosition; + + /** + * Whether a composition is in the process of being sent, setting this to false will cancel any + * in-progress composition. + */ + private _isSendingComposition: boolean; + + /** + * Data already sent due to keydown event. + */ + private _dataAlreadySent: string; + + constructor( + private readonly _textarea: HTMLTextAreaElement, + private readonly _compositionView: HTMLElement, + @IBufferService private readonly _bufferService: IBufferService, + @IOptionsService private readonly _optionsService: IOptionsService, + @ICoreService private readonly _coreService: ICoreService, + @IRenderService private readonly _renderService: IRenderService + ) { + this._isComposing = false; + this._isSendingComposition = false; + this._compositionPosition = { start: 0, end: 0 }; + this._dataAlreadySent = ''; + } + + /** + * Handles the compositionstart event, activating the composition view. + */ + public compositionstart(): void { + this._isComposing = true; + this._compositionPosition.start = this._textarea.value.length; + this._compositionView.textContent = ''; + this._dataAlreadySent = ''; + this._compositionView.classList.add('active'); + } + + /** + * Handles the compositionupdate event, updating the composition view. + * @param ev The event. + */ + public compositionupdate(ev: Pick<CompositionEvent, 'data'>): void { + this._compositionView.textContent = ev.data; + this.updateCompositionElements(); + setTimeout(() => { + this._compositionPosition.end = this._textarea.value.length; + }, 0); + } + + /** + * Handles the compositionend event, hiding the composition view and sending the composition to + * the handler. + */ + public compositionend(): void { + this._finalizeComposition(true); + } + + /** + * Handles the keydown event, routing any necessary events to the CompositionHelper functions. + * @param ev The keydown event. + * @return Whether the Terminal should continue processing the keydown event. + */ + public keydown(ev: KeyboardEvent): boolean { + if (this._isComposing || this._isSendingComposition) { + if (ev.keyCode === 229) { + // Continue composing if the keyCode is the "composition character" + return false; + } + if (ev.keyCode === 16 || ev.keyCode === 17 || ev.keyCode === 18) { + // Continue composing if the keyCode is a modifier key + return false; + } + // Finish composition immediately. This is mainly here for the case where enter is + // pressed and the handler needs to be triggered before the command is executed. + this._finalizeComposition(false); + } + + if (ev.keyCode === 229) { + // If the "composition character" is used but gets to this point it means a non-composition + // character (eg. numbers and punctuation) was pressed when the IME was active. + this._handleAnyTextareaChanges(); + return false; + } + + return true; + } + + /** + * Finalizes the composition, resuming regular input actions. This is called when a composition + * is ending. + * @param waitForPropagation Whether to wait for events to propagate before sending + * the input. This should be false if a non-composition keystroke is entered before the + * compositionend event is triggered, such as enter, so that the composition is sent before + * the command is executed. + */ + private _finalizeComposition(waitForPropagation: boolean): void { + this._compositionView.classList.remove('active'); + this._isComposing = false; + + if (!waitForPropagation) { + // Cancel any delayed composition send requests and send the input immediately. + this._isSendingComposition = false; + const input = this._textarea.value.substring(this._compositionPosition.start, this._compositionPosition.end); + this._coreService.triggerDataEvent(input, true); + } else { + // Make a deep copy of the composition position here as a new compositionstart event may + // fire before the setTimeout executes. + const currentCompositionPosition = { + start: this._compositionPosition.start, + end: this._compositionPosition.end + }; + + // Since composition* events happen before the changes take place in the textarea on most + // browsers, use a setTimeout with 0ms time to allow the native compositionend event to + // complete. This ensures the correct character is retrieved. + // This solution was used because: + // - The compositionend event's data property is unreliable, at least on Chromium + // - The last compositionupdate event's data property does not always accurately describe + // the character, a counter example being Korean where an ending consonsant can move to + // the following character if the following input is a vowel. + this._isSendingComposition = true; + setTimeout(() => { + // Ensure that the input has not already been sent + if (this._isSendingComposition) { + this._isSendingComposition = false; + let input; + // Add length of data already sent due to keydown event, + // otherwise input characters can be duplicated. (Issue #3191) + currentCompositionPosition.start += this._dataAlreadySent.length; + if (this._isComposing) { + // Use the end position to get the string if a new composition has started. + input = this._textarea.value.substring(currentCompositionPosition.start, currentCompositionPosition.end); + } else { + // Don't use the end position here in order to pick up any characters after the + // composition has finished, for example when typing a non-composition character + // (eg. 2) after a composition character. + input = this._textarea.value.substring(currentCompositionPosition.start); + } + if (input.length > 0) { + this._coreService.triggerDataEvent(input, true); + } + } + }, 0); + } + } + + /** + * Apply any changes made to the textarea after the current event chain is allowed to complete. + * This should be called when not currently composing but a keydown event with the "composition + * character" (229) is triggered, in order to allow non-composition text to be entered when an + * IME is active. + */ + private _handleAnyTextareaChanges(): void { + const oldValue = this._textarea.value; + setTimeout(() => { + // Ignore if a composition has started since the timeout + if (!this._isComposing) { + const newValue = this._textarea.value; + const diff = newValue.replace(oldValue, ''); + if (diff.length > 0) { + this._dataAlreadySent = diff; + this._coreService.triggerDataEvent(diff, true); + } + } + }, 0); + } + + /** + * Positions the composition view on top of the cursor and the textarea just below it (so the + * IME helper dialog is positioned correctly). + * @param dontRecurse Whether to use setTimeout to recursively trigger another update, this is + * necessary as the IME events across browsers are not consistently triggered. + */ + public updateCompositionElements(dontRecurse?: boolean): void { + if (!this._isComposing) { + return; + } + + if (this._bufferService.buffer.isCursorInViewport) { + const cursorX = Math.min(this._bufferService.buffer.x, this._bufferService.cols - 1); + + const cellHeight = this._renderService.dimensions.actualCellHeight; + const cursorTop = this._bufferService.buffer.y * this._renderService.dimensions.actualCellHeight; + const cursorLeft = cursorX * this._renderService.dimensions.actualCellWidth; + + this._compositionView.style.left = cursorLeft + 'px'; + this._compositionView.style.top = cursorTop + 'px'; + this._compositionView.style.height = cellHeight + 'px'; + this._compositionView.style.lineHeight = cellHeight + 'px'; + this._compositionView.style.fontFamily = this._optionsService.rawOptions.fontFamily; + this._compositionView.style.fontSize = this._optionsService.rawOptions.fontSize + 'px'; + // Sync the textarea to the exact position of the composition view so the IME knows where the + // text is. + const compositionViewBounds = this._compositionView.getBoundingClientRect(); + this._textarea.style.left = cursorLeft + 'px'; + this._textarea.style.top = cursorTop + 'px'; + // Ensure the text area is at least 1x1, otherwise certain IMEs may break + this._textarea.style.width = Math.max(compositionViewBounds.width, 1) + 'px'; + this._textarea.style.height = Math.max(compositionViewBounds.height, 1) + 'px'; + this._textarea.style.lineHeight = compositionViewBounds.height + 'px'; + } + + if (!dontRecurse) { + setTimeout(() => this.updateCompositionElements(true), 0); + } + } +} diff --git a/node_modules/xterm/src/browser/input/Mouse.ts b/node_modules/xterm/src/browser/input/Mouse.ts new file mode 100644 index 0000000..2986fb3 --- /dev/null +++ b/node_modules/xterm/src/browser/input/Mouse.ts @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +export function getCoordsRelativeToElement(event: {clientX: number, clientY: number}, element: HTMLElement): [number, number] { + const rect = element.getBoundingClientRect(); + return [event.clientX - rect.left, event.clientY - rect.top]; +} + +/** + * Gets coordinates within the terminal for a particular mouse event. The result + * is returned as an array in the form [x, y] instead of an object as it's a + * little faster and this function is used in some low level code. + * @param event The mouse event. + * @param element The terminal's container element. + * @param colCount The number of columns in the terminal. + * @param rowCount The number of rows n the terminal. + * @param isSelection Whether the request is for the selection or not. This will + * apply an offset to the x value such that the left half of the cell will + * select that cell and the right half will select the next cell. + */ +export function getCoords(event: {clientX: number, clientY: number}, element: HTMLElement, colCount: number, rowCount: number, hasValidCharSize: boolean, actualCellWidth: number, actualCellHeight: number, isSelection?: boolean): [number, number] | undefined { + // Coordinates cannot be measured if there are no valid + if (!hasValidCharSize) { + return undefined; + } + + const coords = getCoordsRelativeToElement(event, element); + if (!coords) { + return undefined; + } + + coords[0] = Math.ceil((coords[0] + (isSelection ? actualCellWidth / 2 : 0)) / actualCellWidth); + coords[1] = Math.ceil(coords[1] / actualCellHeight); + + // Ensure coordinates are within the terminal viewport. Note that selections + // need an addition point of precision to cover the end point (as characters + // cover half of one char and half of the next). + coords[0] = Math.min(Math.max(coords[0], 1), colCount + (isSelection ? 1 : 0)); + coords[1] = Math.min(Math.max(coords[1], 1), rowCount); + + return coords; +} + +/** + * Gets coordinates within the terminal for a particular mouse event, wrapping + * them to the bounds of the terminal and adding 32 to both the x and y values + * as expected by xterm. + */ +export function getRawByteCoords(coords: [number, number] | undefined): { x: number, y: number } | undefined { + if (!coords) { + return undefined; + } + + // xterm sends raw bytes and starts at 32 (SP) for each. + return { x: coords[0] + 32, y: coords[1] + 32 }; +} diff --git a/node_modules/xterm/src/browser/input/MoveToCell.ts b/node_modules/xterm/src/browser/input/MoveToCell.ts new file mode 100644 index 0000000..82e767c --- /dev/null +++ b/node_modules/xterm/src/browser/input/MoveToCell.ts @@ -0,0 +1,249 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { C0 } from 'common/data/EscapeSequences'; +import { IBufferService } from 'common/services/Services'; + +const enum Direction { + UP = 'A', + DOWN = 'B', + RIGHT = 'C', + LEFT = 'D' +} + +/** + * Concatenates all the arrow sequences together. + * Resets the starting row to an unwrapped row, moves to the requested row, + * then moves to requested col. + */ +export function moveToCellSequence(targetX: number, targetY: number, bufferService: IBufferService, applicationCursor: boolean): string { + const startX = bufferService.buffer.x; + const startY = bufferService.buffer.y; + + // The alt buffer should try to navigate between rows + if (!bufferService.buffer.hasScrollback) { + return resetStartingRow(startX, startY, targetX, targetY, bufferService, applicationCursor) + + moveToRequestedRow(startY, targetY, bufferService, applicationCursor) + + moveToRequestedCol(startX, startY, targetX, targetY, bufferService, applicationCursor); + } + + // Only move horizontally for the normal buffer + let direction; + if (startY === targetY) { + direction = startX > targetX ? Direction.LEFT : Direction.RIGHT; + return repeat(Math.abs(startX - targetX), sequence(direction, applicationCursor)); + } + direction = startY > targetY ? Direction.LEFT : Direction.RIGHT; + const rowDifference = Math.abs(startY - targetY); + const cellsToMove = colsFromRowEnd(startY > targetY ? targetX : startX, bufferService) + + (rowDifference - 1) * bufferService.cols + 1 /* wrap around 1 row */ + + colsFromRowBeginning(startY > targetY ? startX : targetX, bufferService); + return repeat(cellsToMove, sequence(direction, applicationCursor)); +} + +/** + * Find the number of cols from a row beginning to a col. + */ +function colsFromRowBeginning(currX: number, bufferService: IBufferService): number { + return currX - 1; +} + +/** + * Find the number of cols from a col to row end. + */ +function colsFromRowEnd(currX: number, bufferService: IBufferService): number { + return bufferService.cols - currX; +} + +/** + * If the initial position of the cursor is on a row that is wrapped, move the + * cursor up to the first row that is not wrapped to have accurate vertical + * positioning. + */ +function resetStartingRow(startX: number, startY: number, targetX: number, targetY: number, bufferService: IBufferService, applicationCursor: boolean): string { + if (moveToRequestedRow(startY, targetY, bufferService, applicationCursor).length === 0) { + return ''; + } + return repeat(bufferLine( + startX, startY, startX, + startY - wrappedRowsForRow(bufferService, startY), false, bufferService + ).length, sequence(Direction.LEFT, applicationCursor)); +} + +/** + * Using the reset starting and ending row, move to the requested row, + * ignoring wrapped rows + */ +function moveToRequestedRow(startY: number, targetY: number, bufferService: IBufferService, applicationCursor: boolean): string { + const startRow = startY - wrappedRowsForRow(bufferService, startY); + const endRow = targetY - wrappedRowsForRow(bufferService, targetY); + + const rowsToMove = Math.abs(startRow - endRow) - wrappedRowsCount(startY, targetY, bufferService); + + return repeat(rowsToMove, sequence(verticalDirection(startY, targetY), applicationCursor)); +} + +/** + * Move to the requested col on the ending row + */ +function moveToRequestedCol(startX: number, startY: number, targetX: number, targetY: number, bufferService: IBufferService, applicationCursor: boolean): string { + let startRow; + if (moveToRequestedRow(startY, targetY, bufferService, applicationCursor).length > 0) { + startRow = targetY - wrappedRowsForRow(bufferService, targetY); + } else { + startRow = startY; + } + + const endRow = targetY; + const direction = horizontalDirection(startX, startY, targetX, targetY, bufferService, applicationCursor); + + return repeat(bufferLine( + startX, startRow, targetX, endRow, + direction === Direction.RIGHT, bufferService + ).length, sequence(direction, applicationCursor)); +} + +/** + * Utility functions + */ + +/** + * Calculates the number of wrapped rows between the unwrapped starting and + * ending rows. These rows need to ignored since the cursor skips over them. + */ +function wrappedRowsCount(startY: number, targetY: number, bufferService: IBufferService): number { + let wrappedRows = 0; + const startRow = startY - wrappedRowsForRow(bufferService, startY); + const endRow = targetY - wrappedRowsForRow(bufferService, targetY); + + for (let i = 0; i < Math.abs(startRow - endRow); i++) { + const direction = verticalDirection(startY, targetY) === Direction.UP ? -1 : 1; + const line = bufferService.buffer.lines.get(startRow + (direction * i)); + if (line?.isWrapped) { + wrappedRows++; + } + } + + return wrappedRows; +} + +/** + * Calculates the number of wrapped rows that make up a given row. + * @param currentRow The row to determine how many wrapped rows make it up + */ +function wrappedRowsForRow(bufferService: IBufferService, currentRow: number): number { + let rowCount = 0; + let line = bufferService.buffer.lines.get(currentRow); + let lineWraps = line?.isWrapped; + + while (lineWraps && currentRow >= 0 && currentRow < bufferService.rows) { + rowCount++; + line = bufferService.buffer.lines.get(--currentRow); + lineWraps = line?.isWrapped; + } + + return rowCount; +} + +/** + * Direction determiners + */ + +/** + * Determines if the right or left arrow is needed + */ +function horizontalDirection(startX: number, startY: number, targetX: number, targetY: number, bufferService: IBufferService, applicationCursor: boolean): Direction { + let startRow; + if (moveToRequestedRow(targetX, targetY, bufferService, applicationCursor).length > 0) { + startRow = targetY - wrappedRowsForRow(bufferService, targetY); + } else { + startRow = startY; + } + + if ((startX < targetX && + startRow <= targetY) || // down/right or same y/right + (startX >= targetX && + startRow < targetY)) { // down/left or same y/left + return Direction.RIGHT; + } + return Direction.LEFT; +} + +/** + * Determines if the up or down arrow is needed + */ +function verticalDirection(startY: number, targetY: number): Direction { + return startY > targetY ? Direction.UP : Direction.DOWN; +} + +/** + * Constructs the string of chars in the buffer from a starting row and col + * to an ending row and col + * @param startCol The starting column position + * @param startRow The starting row position + * @param endCol The ending column position + * @param endRow The ending row position + * @param forward Direction to move + */ +function bufferLine( + startCol: number, + startRow: number, + endCol: number, + endRow: number, + forward: boolean, + bufferService: IBufferService +): string { + let currentCol = startCol; + let currentRow = startRow; + let bufferStr = ''; + + while (currentCol !== endCol || currentRow !== endRow) { + currentCol += forward ? 1 : -1; + + if (forward && currentCol > bufferService.cols - 1) { + bufferStr += bufferService.buffer.translateBufferLineToString( + currentRow, false, startCol, currentCol + ); + currentCol = 0; + startCol = 0; + currentRow++; + } else if (!forward && currentCol < 0) { + bufferStr += bufferService.buffer.translateBufferLineToString( + currentRow, false, 0, startCol + 1 + ); + currentCol = bufferService.cols - 1; + startCol = currentCol; + currentRow--; + } + } + + return bufferStr + bufferService.buffer.translateBufferLineToString( + currentRow, false, startCol, currentCol + ); +} + +/** + * Constructs the escape sequence for clicking an arrow + * @param direction The direction to move + */ +function sequence(direction: Direction, applicationCursor: boolean): string { + const mod = applicationCursor ? 'O' : '['; + return C0.ESC + mod + direction; +} + +/** + * Returns a string repeated a given number of times + * Polyfill from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/repeat + * @param count The number of times to repeat the string + * @param string The string that is to be repeated + */ +function repeat(count: number, str: string): string { + count = Math.floor(count); + let rpt = ''; + for (let i = 0; i < count; i++) { + rpt += str; + } + return rpt; +} diff --git a/node_modules/xterm/src/browser/public/Terminal.ts b/node_modules/xterm/src/browser/public/Terminal.ts new file mode 100644 index 0000000..117805f --- /dev/null +++ b/node_modules/xterm/src/browser/public/Terminal.ts @@ -0,0 +1,284 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { Terminal as ITerminalApi, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings, ITerminalAddon, ISelectionPosition, IBufferNamespace as IBufferNamespaceApi, IParser, ILinkProvider, IUnicodeHandling, FontWeight, IModes } from 'xterm'; +import { ITerminal } from 'browser/Types'; +import { Terminal as TerminalCore } from 'browser/Terminal'; +import * as Strings from 'browser/LocalizableStrings'; +import { IEvent } from 'common/EventEmitter'; +import { ParserApi } from 'common/public/ParserApi'; +import { UnicodeApi } from 'common/public/UnicodeApi'; +import { AddonManager } from 'common/public/AddonManager'; +import { BufferNamespaceApi } from 'common/public/BufferNamespaceApi'; +import { ITerminalOptions } from 'common/Types'; + +/** + * The set of options that only have an effect when set in the Terminal constructor. + */ +const CONSTRUCTOR_ONLY_OPTIONS = ['cols', 'rows']; + +export class Terminal implements ITerminalApi { + private _core: ITerminal; + private _addonManager: AddonManager; + private _parser: IParser | undefined; + private _buffer: BufferNamespaceApi | undefined; + private _publicOptions: ITerminalOptions; + + constructor(options?: ITerminalOptions) { + this._core = new TerminalCore(options); + this._addonManager = new AddonManager(); + + this._publicOptions = { ... this._core.options }; + const getter = (propName: string): any => { + return this._core.options[propName]; + }; + const setter = (propName: string, value: any): void => { + this._checkReadonlyOptions(propName); + this._core.options[propName] = value; + }; + + for (const propName in this._core.options) { + const desc = { + get: getter.bind(this, propName), + set: setter.bind(this, propName) + }; + Object.defineProperty(this._publicOptions, propName, desc); + } + } + + private _checkReadonlyOptions(propName: string): void { + // Throw an error if any constructor only option is modified + // from terminal.options + // Modifications from anywhere else are allowed + if (CONSTRUCTOR_ONLY_OPTIONS.includes(propName)) { + throw new Error(`Option "${propName}" can only be set in the constructor`); + } + } + + private _checkProposedApi(): void { + if (!this._core.optionsService.rawOptions.allowProposedApi) { + throw new Error('You must set the allowProposedApi option to true to use proposed API'); + } + } + + public get onBell(): IEvent<void> { return this._core.onBell; } + public get onBinary(): IEvent<string> { return this._core.onBinary; } + public get onCursorMove(): IEvent<void> { return this._core.onCursorMove; } + public get onData(): IEvent<string> { return this._core.onData; } + public get onKey(): IEvent<{ key: string, domEvent: KeyboardEvent }> { return this._core.onKey; } + public get onLineFeed(): IEvent<void> { return this._core.onLineFeed; } + public get onRender(): IEvent<{ start: number, end: number }> { return this._core.onRender; } + public get onResize(): IEvent<{ cols: number, rows: number }> { return this._core.onResize; } + public get onScroll(): IEvent<number> { return this._core.onScroll; } + public get onSelectionChange(): IEvent<void> { return this._core.onSelectionChange; } + public get onTitleChange(): IEvent<string> { return this._core.onTitleChange; } + + public get element(): HTMLElement | undefined { return this._core.element; } + public get parser(): IParser { + this._checkProposedApi(); + if (!this._parser) { + this._parser = new ParserApi(this._core); + } + return this._parser; + } + public get unicode(): IUnicodeHandling { + this._checkProposedApi(); + return new UnicodeApi(this._core); + } + public get textarea(): HTMLTextAreaElement | undefined { return this._core.textarea; } + public get rows(): number { return this._core.rows; } + public get cols(): number { return this._core.cols; } + public get buffer(): IBufferNamespaceApi { + this._checkProposedApi(); + if (!this._buffer) { + this._buffer = new BufferNamespaceApi(this._core); + } + return this._buffer; + } + public get markers(): ReadonlyArray<IMarker> { + this._checkProposedApi(); + return this._core.markers; + } + public get modes(): IModes { + const m = this._core.coreService.decPrivateModes; + let mouseTrackingMode: 'none' | 'x10' | 'vt200' | 'drag' | 'any' = 'none'; + switch (this._core.coreMouseService.activeProtocol) { + case 'X10': mouseTrackingMode = 'x10'; break; + case 'VT200': mouseTrackingMode = 'vt200'; break; + case 'DRAG': mouseTrackingMode = 'drag'; break; + case 'ANY': mouseTrackingMode = 'any'; break; + } + return { + applicationCursorKeysMode: m.applicationCursorKeys, + applicationKeypadMode: m.applicationKeypad, + bracketedPasteMode: m.bracketedPasteMode, + insertMode: this._core.coreService.modes.insertMode, + mouseTrackingMode: mouseTrackingMode, + originMode: m.origin, + reverseWraparoundMode: m.reverseWraparound, + sendFocusMode: m.sendFocus, + wraparoundMode: m.wraparound + }; + } + public get options(): ITerminalOptions { + return this._publicOptions; + } + public set options(options: ITerminalOptions) { + for (const propName in options) { + this._publicOptions[propName] = options[propName]; + } + } + public blur(): void { + this._core.blur(); + } + public focus(): void { + this._core.focus(); + } + public resize(columns: number, rows: number): void { + this._verifyIntegers(columns, rows); + this._core.resize(columns, rows); + } + public open(parent: HTMLElement): void { + this._core.open(parent); + } + public attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void { + this._core.attachCustomKeyEventHandler(customKeyEventHandler); + } + public registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): number { + this._checkProposedApi(); + return this._core.registerLinkMatcher(regex, handler, options); + } + public deregisterLinkMatcher(matcherId: number): void { + this._checkProposedApi(); + this._core.deregisterLinkMatcher(matcherId); + } + public registerLinkProvider(linkProvider: ILinkProvider): IDisposable { + this._checkProposedApi(); + return this._core.registerLinkProvider(linkProvider); + } + public registerCharacterJoiner(handler: (text: string) => [number, number][]): number { + this._checkProposedApi(); + return this._core.registerCharacterJoiner(handler); + } + public deregisterCharacterJoiner(joinerId: number): void { + this._checkProposedApi(); + this._core.deregisterCharacterJoiner(joinerId); + } + public registerMarker(cursorYOffset: number): IMarker | undefined { + this._checkProposedApi(); + this._verifyIntegers(cursorYOffset); + return this._core.addMarker(cursorYOffset); + } + public addMarker(cursorYOffset: number): IMarker | undefined { + return this.registerMarker(cursorYOffset); + } + public hasSelection(): boolean { + return this._core.hasSelection(); + } + public select(column: number, row: number, length: number): void { + this._verifyIntegers(column, row, length); + this._core.select(column, row, length); + } + public getSelection(): string { + return this._core.getSelection(); + } + public getSelectionPosition(): ISelectionPosition | undefined { + return this._core.getSelectionPosition(); + } + public clearSelection(): void { + this._core.clearSelection(); + } + public selectAll(): void { + this._core.selectAll(); + } + public selectLines(start: number, end: number): void { + this._verifyIntegers(start, end); + this._core.selectLines(start, end); + } + public dispose(): void { + this._addonManager.dispose(); + this._core.dispose(); + } + public scrollLines(amount: number): void { + this._verifyIntegers(amount); + this._core.scrollLines(amount); + } + public scrollPages(pageCount: number): void { + this._verifyIntegers(pageCount); + this._core.scrollPages(pageCount); + } + public scrollToTop(): void { + this._core.scrollToTop(); + } + public scrollToBottom(): void { + this._core.scrollToBottom(); + } + public scrollToLine(line: number): void { + this._verifyIntegers(line); + this._core.scrollToLine(line); + } + public clear(): void { + this._core.clear(); + } + public write(data: string | Uint8Array, callback?: () => void): void { + this._core.write(data, callback); + } + public writeUtf8(data: Uint8Array, callback?: () => void): void { + this._core.write(data, callback); + } + public writeln(data: string | Uint8Array, callback?: () => void): void { + this._core.write(data); + this._core.write('\r\n', callback); + } + public paste(data: string): void { + this._core.paste(data); + } + public getOption(key: 'bellSound' | 'bellStyle' | 'cursorStyle' | 'fontFamily' | 'logLevel' | 'rendererType' | 'termName' | 'wordSeparator'): string; + public getOption(key: 'allowTransparency' | 'altClickMovesCursor' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'disableStdin' | 'macOptionIsMeta' | 'rightClickSelectsWord' | 'popOnBell' | 'visualBell'): boolean; + public getOption(key: 'cols' | 'fontSize' | 'letterSpacing' | 'lineHeight' | 'rows' | 'tabStopWidth' | 'scrollback'): number; + public getOption(key: 'fontWeight' | 'fontWeightBold'): FontWeight; + public getOption(key: string): any; + public getOption(key: any): any { + return this._core.optionsService.getOption(key); + } + public setOption(key: 'bellSound' | 'fontFamily' | 'termName' | 'wordSeparator', value: string): void; + public setOption(key: 'fontWeight' | 'fontWeightBold', value: 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900' | number): void; + public setOption(key: 'logLevel', value: 'debug' | 'info' | 'warn' | 'error' | 'off'): void; + public setOption(key: 'bellStyle', value: 'none' | 'visual' | 'sound' | 'both'): void; + public setOption(key: 'cursorStyle', value: 'block' | 'underline' | 'bar'): void; + public setOption(key: 'allowTransparency' | 'altClickMovesCursor' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'disableStdin' | 'macOptionIsMeta' | 'rightClickSelectsWord' | 'popOnBell' | 'visualBell', value: boolean): void; + public setOption(key: 'fontSize' | 'letterSpacing' | 'lineHeight' | 'tabStopWidth' | 'scrollback', value: number): void; + public setOption(key: 'theme', value: ITheme): void; + public setOption(key: 'cols' | 'rows', value: number): void; + public setOption(key: string, value: any): void; + public setOption(key: any, value: any): void { + this._checkReadonlyOptions(key); + this._core.optionsService.setOption(key, value); + } + public refresh(start: number, end: number): void { + this._verifyIntegers(start, end); + this._core.refresh(start, end); + } + public reset(): void { + this._core.reset(); + } + public clearTextureAtlas(): void { + this._core.clearTextureAtlas(); + } + public loadAddon(addon: ITerminalAddon): void { + return this._addonManager.loadAddon(this, addon); + } + public static get strings(): ILocalizableStrings { + return Strings; + } + + private _verifyIntegers(...values: number[]): void { + for (const value of values) { + if (value === Infinity || isNaN(value) || value % 1 !== 0) { + throw new Error('This API only accepts integers'); + } + } + } +} 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; +} diff --git a/node_modules/xterm/src/browser/selection/SelectionModel.ts b/node_modules/xterm/src/browser/selection/SelectionModel.ts new file mode 100644 index 0000000..1d84446 --- /dev/null +++ b/node_modules/xterm/src/browser/selection/SelectionModel.ts @@ -0,0 +1,139 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IBufferService } from 'common/services/Services'; + +/** + * Represents a selection within the buffer. This model only cares about column + * and row coordinates, not wide characters. + */ +export class SelectionModel { + /** + * Whether select all is currently active. + */ + public isSelectAllActive: boolean = false; + + /** + * The minimal length of the selection from the start position. When double + * clicking on a word, the word will be selected which makes the selection + * start at the start of the word and makes this variable the length. + */ + public selectionStartLength: number = 0; + + /** + * The [x, y] position the selection starts at. + */ + public selectionStart: [number, number] | undefined; + + /** + * The [x, y] position the selection ends at. + */ + public selectionEnd: [number, number] | undefined; + + constructor( + private _bufferService: IBufferService + ) { + } + + /** + * Clears the current selection. + */ + public clearSelection(): void { + this.selectionStart = undefined; + this.selectionEnd = undefined; + this.isSelectAllActive = false; + this.selectionStartLength = 0; + } + + /** + * The final selection start, taking into consideration select all. + */ + public get finalSelectionStart(): [number, number] | undefined { + if (this.isSelectAllActive) { + return [0, 0]; + } + + if (!this.selectionEnd || !this.selectionStart) { + return this.selectionStart; + } + + return this.areSelectionValuesReversed() ? this.selectionEnd : this.selectionStart; + } + + /** + * The final selection end, taking into consideration select all, double click + * word selection and triple click line selection. + */ + public get finalSelectionEnd(): [number, number] | undefined { + if (this.isSelectAllActive) { + return [this._bufferService.cols, this._bufferService.buffer.ybase + this._bufferService.rows - 1]; + } + + if (!this.selectionStart) { + return undefined; + } + + // Use the selection start + length if the end doesn't exist or they're reversed + if (!this.selectionEnd || this.areSelectionValuesReversed()) { + const startPlusLength = this.selectionStart[0] + this.selectionStartLength; + if (startPlusLength > this._bufferService.cols) { + // Ensure the trailing EOL isn't included when the selection ends on the right edge + if (startPlusLength % this._bufferService.cols === 0) { + return [this._bufferService.cols, this.selectionStart[1] + Math.floor(startPlusLength / this._bufferService.cols) - 1]; + } + return [startPlusLength % this._bufferService.cols, this.selectionStart[1] + Math.floor(startPlusLength / this._bufferService.cols)]; + } + return [startPlusLength, this.selectionStart[1]]; + } + + // Ensure the the word/line is selected after a double/triple click + if (this.selectionStartLength) { + // Select the larger of the two when start and end are on the same line + if (this.selectionEnd[1] === this.selectionStart[1]) { + return [Math.max(this.selectionStart[0] + this.selectionStartLength, this.selectionEnd[0]), this.selectionEnd[1]]; + } + } + return this.selectionEnd; + } + + /** + * Returns whether the selection start and end are reversed. + */ + public areSelectionValuesReversed(): boolean { + const start = this.selectionStart; + const end = this.selectionEnd; + if (!start || !end) { + return false; + } + return start[1] > end[1] || (start[1] === end[1] && start[0] > end[0]); + } + + /** + * Handle the buffer being trimmed, adjust the selection position. + * @param amount The amount the buffer is being trimmed. + * @return Whether a refresh is necessary. + */ + public onTrim(amount: number): boolean { + // Adjust the selection position based on the trimmed amount. + if (this.selectionStart) { + this.selectionStart[1] -= amount; + } + if (this.selectionEnd) { + this.selectionEnd[1] -= amount; + } + + // The selection has moved off the buffer, clear it. + if (this.selectionEnd && this.selectionEnd[1] < 0) { + this.clearSelection(); + return true; + } + + // If the selection start is trimmed, ensure the start column is 0. + if (this.selectionStart && this.selectionStart[1] < 0) { + this.selectionStart[1] = 0; + } + return false; + } +} diff --git a/node_modules/xterm/src/browser/selection/Types.d.ts b/node_modules/xterm/src/browser/selection/Types.d.ts new file mode 100644 index 0000000..8adfc17 --- /dev/null +++ b/node_modules/xterm/src/browser/selection/Types.d.ts @@ -0,0 +1,15 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +export interface ISelectionRedrawRequestEvent { + start: [number, number] | undefined; + end: [number, number] | undefined; + columnSelectMode: boolean; +} + +export interface ISelectionRequestScrollLinesEvent { + amount: number; + suppressScrollEvent: boolean; +} diff --git a/node_modules/xterm/src/browser/services/CharSizeService.ts b/node_modules/xterm/src/browser/services/CharSizeService.ts new file mode 100644 index 0000000..b04e157 --- /dev/null +++ b/node_modules/xterm/src/browser/services/CharSizeService.ts @@ -0,0 +1,87 @@ +/** + * Copyright (c) 2016 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IOptionsService } from 'common/services/Services'; +import { IEvent, EventEmitter } from 'common/EventEmitter'; +import { ICharSizeService } from 'browser/services/Services'; + +export class CharSizeService implements ICharSizeService { + public serviceBrand: undefined; + + public width: number = 0; + public height: number = 0; + private _measureStrategy: IMeasureStrategy; + + public get hasValidSize(): boolean { return this.width > 0 && this.height > 0; } + + private _onCharSizeChange = new EventEmitter<void>(); + public get onCharSizeChange(): IEvent<void> { return this._onCharSizeChange.event; } + + constructor( + document: Document, + parentElement: HTMLElement, + @IOptionsService private readonly _optionsService: IOptionsService + ) { + this._measureStrategy = new DomMeasureStrategy(document, parentElement, this._optionsService); + } + + public measure(): void { + const result = this._measureStrategy.measure(); + if (result.width !== this.width || result.height !== this.height) { + this.width = result.width; + this.height = result.height; + this._onCharSizeChange.fire(); + } + } +} + +interface IMeasureStrategy { + measure(): IReadonlyMeasureResult; +} + +interface IReadonlyMeasureResult { + readonly width: number; + readonly height: number; +} + +interface IMeasureResult { + width: number; + height: number; +} + +// TODO: For supporting browsers we should also provide a CanvasCharDimensionsProvider that uses ctx.measureText +class DomMeasureStrategy implements IMeasureStrategy { + private _result: IMeasureResult = { width: 0, height: 0 }; + private _measureElement: HTMLElement; + + constructor( + private _document: Document, + private _parentElement: HTMLElement, + private _optionsService: IOptionsService + ) { + this._measureElement = this._document.createElement('span'); + this._measureElement.classList.add('xterm-char-measure-element'); + this._measureElement.textContent = 'W'; + this._measureElement.setAttribute('aria-hidden', 'true'); + this._parentElement.appendChild(this._measureElement); + } + + public measure(): IReadonlyMeasureResult { + this._measureElement.style.fontFamily = this._optionsService.rawOptions.fontFamily; + this._measureElement.style.fontSize = `${this._optionsService.rawOptions.fontSize}px`; + + // Note that this triggers a synchronous layout + const geometry = this._measureElement.getBoundingClientRect(); + + // If values are 0 then the element is likely currently display:none, in which case we should + // retain the previous value. + if (geometry.width !== 0 && geometry.height !== 0) { + this._result.width = geometry.width; + this._result.height = Math.ceil(geometry.height); + } + + return this._result; + } +} diff --git a/node_modules/xterm/src/browser/services/CharacterJoinerService.ts b/node_modules/xterm/src/browser/services/CharacterJoinerService.ts new file mode 100644 index 0000000..ca4f198 --- /dev/null +++ b/node_modules/xterm/src/browser/services/CharacterJoinerService.ts @@ -0,0 +1,339 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IBufferLine, ICellData, CharData } from 'common/Types'; +import { ICharacterJoiner } from 'browser/Types'; +import { AttributeData } from 'common/buffer/AttributeData'; +import { WHITESPACE_CELL_CHAR, Content } from 'common/buffer/Constants'; +import { CellData } from 'common/buffer/CellData'; +import { IBufferService } from 'common/services/Services'; +import { ICharacterJoinerService } from 'browser/services/Services'; + +export class JoinedCellData extends AttributeData implements ICellData { + private _width: number; + // .content carries no meaning for joined CellData, simply nullify it + // thus we have to overload all other .content accessors + public content: number = 0; + public fg: number; + public bg: number; + public combinedData: string = ''; + + constructor(firstCell: ICellData, chars: string, width: number) { + super(); + this.fg = firstCell.fg; + this.bg = firstCell.bg; + this.combinedData = chars; + this._width = width; + } + + public isCombined(): number { + // always mark joined cell data as combined + return Content.IS_COMBINED_MASK; + } + + public getWidth(): number { + return this._width; + } + + public getChars(): string { + return this.combinedData; + } + + public getCode(): number { + // code always gets the highest possible fake codepoint (read as -1) + // this is needed as code is used by caches as identifier + return 0x1FFFFF; + } + + public setFromCharData(value: CharData): void { + throw new Error('not implemented'); + } + + public getAsCharData(): CharData { + return [this.fg, this.getChars(), this.getWidth(), this.getCode()]; + } +} + +export class CharacterJoinerService implements ICharacterJoinerService { + public serviceBrand: undefined; + + private _characterJoiners: ICharacterJoiner[] = []; + private _nextCharacterJoinerId: number = 0; + private _workCell: CellData = new CellData(); + + constructor( + @IBufferService private _bufferService: IBufferService + ) { } + + public register(handler: (text: string) => [number, number][]): number { + const joiner: ICharacterJoiner = { + id: this._nextCharacterJoinerId++, + handler + }; + + this._characterJoiners.push(joiner); + return joiner.id; + } + + public deregister(joinerId: number): boolean { + for (let i = 0; i < this._characterJoiners.length; i++) { + if (this._characterJoiners[i].id === joinerId) { + this._characterJoiners.splice(i, 1); + return true; + } + } + + return false; + } + + public getJoinedCharacters(row: number): [number, number][] { + if (this._characterJoiners.length === 0) { + return []; + } + + const line = this._bufferService.buffer.lines.get(row); + if (!line || line.length === 0) { + return []; + } + + const ranges: [number, number][] = []; + const lineStr = line.translateToString(true); + + // Because some cells can be represented by multiple javascript characters, + // we track the cell and the string indexes separately. This allows us to + // translate the string ranges we get from the joiners back into cell ranges + // for use when rendering + let rangeStartColumn = 0; + let currentStringIndex = 0; + let rangeStartStringIndex = 0; + let rangeAttrFG = line.getFg(0); + let rangeAttrBG = line.getBg(0); + + for (let x = 0; x < line.getTrimmedLength(); x++) { + line.loadCell(x, this._workCell); + + if (this._workCell.getWidth() === 0) { + // If this character is of width 0, skip it. + continue; + } + + // End of range + if (this._workCell.fg !== rangeAttrFG || this._workCell.bg !== rangeAttrBG) { + // If we ended up with a sequence of more than one character, + // look for ranges to join. + if (x - rangeStartColumn > 1) { + const joinedRanges = this._getJoinedRanges( + lineStr, + rangeStartStringIndex, + currentStringIndex, + line, + rangeStartColumn + ); + for (let i = 0; i < joinedRanges.length; i++) { + ranges.push(joinedRanges[i]); + } + } + + // Reset our markers for a new range. + rangeStartColumn = x; + rangeStartStringIndex = currentStringIndex; + rangeAttrFG = this._workCell.fg; + rangeAttrBG = this._workCell.bg; + } + + currentStringIndex += this._workCell.getChars().length || WHITESPACE_CELL_CHAR.length; + } + + // Process any trailing ranges. + if (this._bufferService.cols - rangeStartColumn > 1) { + const joinedRanges = this._getJoinedRanges( + lineStr, + rangeStartStringIndex, + currentStringIndex, + line, + rangeStartColumn + ); + for (let i = 0; i < joinedRanges.length; i++) { + ranges.push(joinedRanges[i]); + } + } + + return ranges; + } + + /** + * Given a segment of a line of text, find all ranges of text that should be + * joined in a single rendering unit. Ranges are internally converted to + * column ranges, rather than string ranges. + * @param line String representation of the full line of text + * @param startIndex Start position of the range to search in the string (inclusive) + * @param endIndex End position of the range to search in the string (exclusive) + */ + private _getJoinedRanges(line: string, startIndex: number, endIndex: number, lineData: IBufferLine, startCol: number): [number, number][] { + const text = line.substring(startIndex, endIndex); + // At this point we already know that there is at least one joiner so + // we can just pull its value and assign it directly rather than + // merging it into an empty array, which incurs unnecessary writes. + let allJoinedRanges: [number, number][] = []; + try { + allJoinedRanges = this._characterJoiners[0].handler(text); + } catch (error) { + console.error(error); + } + for (let i = 1; i < this._characterJoiners.length; i++) { + // We merge any overlapping ranges across the different joiners + try { + const joinerRanges = this._characterJoiners[i].handler(text); + for (let j = 0; j < joinerRanges.length; j++) { + CharacterJoinerService._mergeRanges(allJoinedRanges, joinerRanges[j]); + } + } catch (error) { + console.error(error); + } + } + this._stringRangesToCellRanges(allJoinedRanges, lineData, startCol); + return allJoinedRanges; + } + + /** + * Modifies the provided ranges in-place to adjust for variations between + * string length and cell width so that the range represents a cell range, + * rather than the string range the joiner provides. + * @param ranges String ranges containing start (inclusive) and end (exclusive) index + * @param line Cell data for the relevant line in the terminal + * @param startCol Offset within the line to start from + */ + private _stringRangesToCellRanges(ranges: [number, number][], line: IBufferLine, startCol: number): void { + let currentRangeIndex = 0; + let currentRangeStarted = false; + let currentStringIndex = 0; + let currentRange = ranges[currentRangeIndex]; + + // If we got through all of the ranges, stop searching + if (!currentRange) { + return; + } + + for (let x = startCol; x < this._bufferService.cols; x++) { + const width = line.getWidth(x); + const length = line.getString(x).length || WHITESPACE_CELL_CHAR.length; + + // We skip zero-width characters when creating the string to join the text + // so we do the same here + if (width === 0) { + continue; + } + + // Adjust the start of the range + if (!currentRangeStarted && currentRange[0] <= currentStringIndex) { + currentRange[0] = x; + currentRangeStarted = true; + } + + // Adjust the end of the range + if (currentRange[1] <= currentStringIndex) { + currentRange[1] = x; + + // We're finished with this range, so we move to the next one + currentRange = ranges[++currentRangeIndex]; + + // If there are no more ranges left, stop searching + if (!currentRange) { + break; + } + + // Ranges can be on adjacent characters. Because the end index of the + // ranges are exclusive, this means that the index for the start of a + // range can be the same as the end index of the previous range. To + // account for the start of the next range, we check here just in case. + if (currentRange[0] <= currentStringIndex) { + currentRange[0] = x; + currentRangeStarted = true; + } else { + currentRangeStarted = false; + } + } + + // Adjust the string index based on the character length to line up with + // the column adjustment + currentStringIndex += length; + } + + // If there is still a range left at the end, it must extend all the way to + // the end of the line. + if (currentRange) { + currentRange[1] = this._bufferService.cols; + } + } + + /** + * Merges the range defined by the provided start and end into the list of + * existing ranges. The merge is done in place on the existing range for + * performance and is also returned. + * @param ranges Existing range list + * @param newRange Tuple of two numbers representing the new range to merge in. + * @returns The ranges input with the new range merged in place + */ + private static _mergeRanges(ranges: [number, number][], newRange: [number, number]): [number, number][] { + let inRange = false; + for (let i = 0; i < ranges.length; i++) { + const range = ranges[i]; + if (!inRange) { + if (newRange[1] <= range[0]) { + // Case 1: New range is before the search range + ranges.splice(i, 0, newRange); + return ranges; + } + + if (newRange[1] <= range[1]) { + // Case 2: New range is either wholly contained within the + // search range or overlaps with the front of it + range[0] = Math.min(newRange[0], range[0]); + return ranges; + } + + if (newRange[0] < range[1]) { + // Case 3: New range either wholly contains the search range + // or overlaps with the end of it + range[0] = Math.min(newRange[0], range[0]); + inRange = true; + } + + // Case 4: New range starts after the search range + continue; + } else { + if (newRange[1] <= range[0]) { + // Case 5: New range extends from previous range but doesn't + // reach the current one + ranges[i - 1][1] = newRange[1]; + return ranges; + } + + if (newRange[1] <= range[1]) { + // Case 6: New range extends from prvious range into the + // current range + ranges[i - 1][1] = Math.max(newRange[1], range[1]); + ranges.splice(i, 1); + return ranges; + } + + // Case 7: New range extends from previous range past the + // end of the current range + ranges.splice(i, 1); + i--; + } + } + + if (inRange) { + // Case 8: New range extends past the last existing range + ranges[ranges.length - 1][1] = newRange[1]; + } else { + // Case 9: New range starts after the last existing range + ranges.push(newRange); + } + + return ranges; + } +} diff --git a/node_modules/xterm/src/browser/services/CoreBrowserService.ts b/node_modules/xterm/src/browser/services/CoreBrowserService.ts new file mode 100644 index 0000000..4eabc89 --- /dev/null +++ b/node_modules/xterm/src/browser/services/CoreBrowserService.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { ICoreBrowserService } from './Services'; + +export class CoreBrowserService implements ICoreBrowserService { + public serviceBrand: undefined; + + constructor( + private _textarea: HTMLTextAreaElement + ) { + } + + public get isFocused(): boolean { + const docOrShadowRoot = this._textarea.getRootNode ? this._textarea.getRootNode() as Document | ShadowRoot : document; + return docOrShadowRoot.activeElement === this._textarea && document.hasFocus(); + } +} diff --git a/node_modules/xterm/src/browser/services/MouseService.ts b/node_modules/xterm/src/browser/services/MouseService.ts new file mode 100644 index 0000000..348ba64 --- /dev/null +++ b/node_modules/xterm/src/browser/services/MouseService.ts @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { ICharSizeService, IRenderService, IMouseService } from './Services'; +import { getCoords, getRawByteCoords } from 'browser/input/Mouse'; + +export class MouseService implements IMouseService { + public serviceBrand: undefined; + + constructor( + @IRenderService private readonly _renderService: IRenderService, + @ICharSizeService private readonly _charSizeService: ICharSizeService + ) { + } + + public getCoords(event: {clientX: number, clientY: number}, element: HTMLElement, colCount: number, rowCount: number, isSelection?: boolean): [number, number] | undefined { + return getCoords( + event, + element, + colCount, + rowCount, + this._charSizeService.hasValidSize, + this._renderService.dimensions.actualCellWidth, + this._renderService.dimensions.actualCellHeight, + isSelection + ); + } + + public getRawByteCoords(event: MouseEvent, element: HTMLElement, colCount: number, rowCount: number): { x: number, y: number } | undefined { + const coords = this.getCoords(event, element, colCount, rowCount); + return getRawByteCoords(coords); + } +} diff --git a/node_modules/xterm/src/browser/services/RenderService.ts b/node_modules/xterm/src/browser/services/RenderService.ts new file mode 100644 index 0000000..b8283e0 --- /dev/null +++ b/node_modules/xterm/src/browser/services/RenderService.ts @@ -0,0 +1,222 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IRenderer, IRenderDimensions } from 'browser/renderer/Types'; +import { RenderDebouncer } from 'browser/RenderDebouncer'; +import { EventEmitter, IEvent } from 'common/EventEmitter'; +import { Disposable } from 'common/Lifecycle'; +import { ScreenDprMonitor } from 'browser/ScreenDprMonitor'; +import { addDisposableDomListener } from 'browser/Lifecycle'; +import { IColorSet, IRenderDebouncer } from 'browser/Types'; +import { IOptionsService, IBufferService } from 'common/services/Services'; +import { ICharSizeService, IRenderService } from 'browser/services/Services'; + +interface ISelectionState { + start: [number, number] | undefined; + end: [number, number] | undefined; + columnSelectMode: boolean; +} + +export class RenderService extends Disposable implements IRenderService { + public serviceBrand: undefined; + + private _renderDebouncer: IRenderDebouncer; + private _screenDprMonitor: ScreenDprMonitor; + + private _isPaused: boolean = false; + private _needsFullRefresh: boolean = false; + private _isNextRenderRedrawOnly: boolean = true; + private _needsSelectionRefresh: boolean = false; + private _canvasWidth: number = 0; + private _canvasHeight: number = 0; + private _selectionState: ISelectionState = { + start: undefined, + end: undefined, + columnSelectMode: false + }; + + private _onDimensionsChange = new EventEmitter<IRenderDimensions>(); + public get onDimensionsChange(): IEvent<IRenderDimensions> { return this._onDimensionsChange.event; } + private _onRender = new EventEmitter<{ start: number, end: number }>(); + public get onRenderedBufferChange(): IEvent<{ start: number, end: number }> { return this._onRender.event; } + private _onRefreshRequest = new EventEmitter<{ start: number, end: number }>(); + public get onRefreshRequest(): IEvent<{ start: number, end: number }> { return this._onRefreshRequest.event; } + + public get dimensions(): IRenderDimensions { return this._renderer.dimensions; } + + constructor( + private _renderer: IRenderer, + private _rowCount: number, + screenElement: HTMLElement, + @IOptionsService optionsService: IOptionsService, + @ICharSizeService private readonly _charSizeService: ICharSizeService, + @IBufferService bufferService: IBufferService + ) { + super(); + + this.register({ dispose: () => this._renderer.dispose() }); + + this._renderDebouncer = new RenderDebouncer((start, end) => this._renderRows(start, end)); + this.register(this._renderDebouncer); + + this._screenDprMonitor = new ScreenDprMonitor(); + this._screenDprMonitor.setListener(() => this.onDevicePixelRatioChange()); + this.register(this._screenDprMonitor); + + this.register(bufferService.onResize(e => this._fullRefresh())); + this.register(optionsService.onOptionChange(() => this._renderer.onOptionsChanged())); + this.register(this._charSizeService.onCharSizeChange(() => this.onCharSizeChanged())); + + // No need to register this as renderer is explicitly disposed in RenderService.dispose + this._renderer.onRequestRedraw(e => this.refreshRows(e.start, e.end, true)); + + // dprchange should handle this case, we need this as well for browsers that don't support the + // matchMedia query. + this.register(addDisposableDomListener(window, 'resize', () => this.onDevicePixelRatioChange())); + + // Detect whether IntersectionObserver is detected and enable renderer pause + // and resume based on terminal visibility if so + if ('IntersectionObserver' in window) { + const observer = new IntersectionObserver(e => this._onIntersectionChange(e[e.length - 1]), { threshold: 0 }); + observer.observe(screenElement); + this.register({ dispose: () => observer.disconnect() }); + } + } + + private _onIntersectionChange(entry: IntersectionObserverEntry): void { + this._isPaused = entry.isIntersecting === undefined ? (entry.intersectionRatio === 0) : !entry.isIntersecting; + + // Terminal was hidden on open + if (!this._isPaused && !this._charSizeService.hasValidSize) { + this._charSizeService.measure(); + } + + if (!this._isPaused && this._needsFullRefresh) { + this.refreshRows(0, this._rowCount - 1); + this._needsFullRefresh = false; + } + } + + public refreshRows(start: number, end: number, isRedrawOnly: boolean = false): void { + if (this._isPaused) { + this._needsFullRefresh = true; + return; + } + if (!isRedrawOnly) { + this._isNextRenderRedrawOnly = false; + } + this._renderDebouncer.refresh(start, end, this._rowCount); + } + + private _renderRows(start: number, end: number): void { + this._renderer.renderRows(start, end); + + // Update selection if needed + if (this._needsSelectionRefresh) { + this._renderer.onSelectionChanged(this._selectionState.start, this._selectionState.end, this._selectionState.columnSelectMode); + this._needsSelectionRefresh = false; + } + + // Fire render event only if it was not a redraw + if (!this._isNextRenderRedrawOnly) { + this._onRender.fire({ start, end }); + } + this._isNextRenderRedrawOnly = true; + } + + public resize(cols: number, rows: number): void { + this._rowCount = rows; + this._fireOnCanvasResize(); + } + + public changeOptions(): void { + this._renderer.onOptionsChanged(); + this.refreshRows(0, this._rowCount - 1); + this._fireOnCanvasResize(); + } + + private _fireOnCanvasResize(): void { + // Don't fire the event if the dimensions haven't changed + if (this._renderer.dimensions.canvasWidth === this._canvasWidth && this._renderer.dimensions.canvasHeight === this._canvasHeight) { + return; + } + this._onDimensionsChange.fire(this._renderer.dimensions); + } + + public dispose(): void { + super.dispose(); + } + + public setRenderer(renderer: IRenderer): void { + // TODO: RenderService should be the only one to dispose the renderer + this._renderer.dispose(); + this._renderer = renderer; + this._renderer.onRequestRedraw(e => this.refreshRows(e.start, e.end, true)); + + // Force a refresh + this._needsSelectionRefresh = true; + this._fullRefresh(); + } + + private _fullRefresh(): void { + if (this._isPaused) { + this._needsFullRefresh = true; + } else { + this.refreshRows(0, this._rowCount - 1); + } + } + + public clearTextureAtlas(): void { + this._renderer?.clearTextureAtlas?.(); + this._fullRefresh(); + } + + public setColors(colors: IColorSet): void { + this._renderer.setColors(colors); + this._fullRefresh(); + } + + public onDevicePixelRatioChange(): void { + // Force char size measurement as DomMeasureStrategy(getBoundingClientRect) is not stable + // when devicePixelRatio changes + this._charSizeService.measure(); + + this._renderer.onDevicePixelRatioChange(); + this.refreshRows(0, this._rowCount - 1); + } + + public onResize(cols: number, rows: number): void { + this._renderer.onResize(cols, rows); + this._fullRefresh(); + } + + // TODO: Is this useful when we have onResize? + public onCharSizeChanged(): void { + this._renderer.onCharSizeChanged(); + } + + public onBlur(): void { + this._renderer.onBlur(); + } + + public onFocus(): void { + this._renderer.onFocus(); + } + + public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void { + this._selectionState.start = start; + this._selectionState.end = end; + this._selectionState.columnSelectMode = columnSelectMode; + this._renderer.onSelectionChanged(start, end, columnSelectMode); + } + + public onCursorMove(): void { + this._renderer.onCursorMove(); + } + + public clear(): void { + this._renderer.clear(); + } +} diff --git a/node_modules/xterm/src/browser/services/SelectionService.ts b/node_modules/xterm/src/browser/services/SelectionService.ts new file mode 100644 index 0000000..1ea2395 --- /dev/null +++ b/node_modules/xterm/src/browser/services/SelectionService.ts @@ -0,0 +1,1009 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { ISelectionRedrawRequestEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types'; +import { IBuffer } from 'common/buffer/Types'; +import { IBufferLine, IDisposable } from 'common/Types'; +import * as Browser from 'common/Platform'; +import { SelectionModel } from 'browser/selection/SelectionModel'; +import { CellData } from 'common/buffer/CellData'; +import { EventEmitter, IEvent } from 'common/EventEmitter'; +import { IMouseService, ISelectionService, IRenderService } from 'browser/services/Services'; +import { ILinkifier2 } from 'browser/Types'; +import { IBufferService, IOptionsService, ICoreService } from 'common/services/Services'; +import { getCoordsRelativeToElement } from 'browser/input/Mouse'; +import { moveToCellSequence } from 'browser/input/MoveToCell'; +import { Disposable } from 'common/Lifecycle'; +import { getRangeLength } from 'common/buffer/BufferRange'; + +/** + * The number of pixels the mouse needs to be above or below the viewport in + * order to scroll at the maximum speed. + */ +const DRAG_SCROLL_MAX_THRESHOLD = 50; + +/** + * The maximum scrolling speed + */ +const DRAG_SCROLL_MAX_SPEED = 15; + +/** + * The number of milliseconds between drag scroll updates. + */ +const DRAG_SCROLL_INTERVAL = 50; + +/** + * The maximum amount of time that can have elapsed for an alt click to move the + * cursor. + */ +const ALT_CLICK_MOVE_CURSOR_TIME = 500; + +const NON_BREAKING_SPACE_CHAR = String.fromCharCode(160); +const ALL_NON_BREAKING_SPACE_REGEX = new RegExp(NON_BREAKING_SPACE_CHAR, 'g'); + +/** + * Represents a position of a word on a line. + */ +interface IWordPosition { + start: number; + length: number; +} + +/** + * A selection mode, this drives how the selection behaves on mouse move. + */ +export const enum SelectionMode { + NORMAL, + WORD, + LINE, + COLUMN +} + +/** + * A class that manages the selection of the terminal. With help from + * SelectionModel, SelectionService handles with all logic associated with + * dealing with the selection, including handling mouse interaction, wide + * characters and fetching the actual text within the selection. Rendering is + * not handled by the SelectionService but the onRedrawRequest event is fired + * when the selection is ready to be redrawn (on an animation frame). + */ +export class SelectionService extends Disposable implements ISelectionService { + public serviceBrand: undefined; + + protected _model: SelectionModel; + + /** + * The amount to scroll every drag scroll update (depends on how far the mouse + * drag is above or below the terminal). + */ + private _dragScrollAmount: number = 0; + + /** + * The current selection mode. + */ + protected _activeSelectionMode: SelectionMode; + + /** + * A setInterval timer that is active while the mouse is down whose callback + * scrolls the viewport when necessary. + */ + private _dragScrollIntervalTimer: number | undefined; + + /** + * The animation frame ID used for refreshing the selection. + */ + private _refreshAnimationFrame: number | undefined; + + /** + * Whether selection is enabled. + */ + private _enabled = true; + + private _mouseMoveListener: EventListener; + private _mouseUpListener: EventListener; + private _trimListener: IDisposable; + private _workCell: CellData = new CellData(); + + private _mouseDownTimeStamp: number = 0; + private _oldHasSelection: boolean = false; + private _oldSelectionStart: [number, number] | undefined = undefined; + private _oldSelectionEnd: [number, number] | undefined = undefined; + + private _onLinuxMouseSelection = this.register(new EventEmitter<string>()); + public get onLinuxMouseSelection(): IEvent<string> { return this._onLinuxMouseSelection.event; } + private _onRedrawRequest = this.register(new EventEmitter<ISelectionRedrawRequestEvent>()); + public get onRequestRedraw(): IEvent<ISelectionRedrawRequestEvent> { return this._onRedrawRequest.event; } + private _onSelectionChange = this.register(new EventEmitter<void>()); + public get onSelectionChange(): IEvent<void> { return this._onSelectionChange.event; } + private _onRequestScrollLines = this.register(new EventEmitter<ISelectionRequestScrollLinesEvent>()); + public get onRequestScrollLines(): IEvent<ISelectionRequestScrollLinesEvent> { return this._onRequestScrollLines.event; } + + constructor( + private readonly _element: HTMLElement, + private readonly _screenElement: HTMLElement, + private readonly _linkifier: ILinkifier2, + @IBufferService private readonly _bufferService: IBufferService, + @ICoreService private readonly _coreService: ICoreService, + @IMouseService private readonly _mouseService: IMouseService, + @IOptionsService private readonly _optionsService: IOptionsService, + @IRenderService private readonly _renderService: IRenderService + ) { + super(); + + // Init listeners + this._mouseMoveListener = event => this._onMouseMove(event as MouseEvent); + this._mouseUpListener = event => this._onMouseUp(event as MouseEvent); + this._coreService.onUserInput(() => { + if (this.hasSelection) { + this.clearSelection(); + } + }); + this._trimListener = this._bufferService.buffer.lines.onTrim(amount => this._onTrim(amount)); + this.register(this._bufferService.buffers.onBufferActivate(e => this._onBufferActivate(e))); + + this.enable(); + + this._model = new SelectionModel(this._bufferService); + this._activeSelectionMode = SelectionMode.NORMAL; + } + + public dispose(): void { + this._removeMouseDownListeners(); + } + + public reset(): void { + this.clearSelection(); + } + + /** + * Disables the selection manager. This is useful for when terminal mouse + * are enabled. + */ + public disable(): void { + this.clearSelection(); + this._enabled = false; + } + + /** + * Enable the selection manager. + */ + public enable(): void { + this._enabled = true; + } + + public get selectionStart(): [number, number] | undefined { return this._model.finalSelectionStart; } + public get selectionEnd(): [number, number] | undefined { return this._model.finalSelectionEnd; } + + /** + * Gets whether there is an active text selection. + */ + public get hasSelection(): boolean { + const start = this._model.finalSelectionStart; + const end = this._model.finalSelectionEnd; + if (!start || !end) { + return false; + } + return start[0] !== end[0] || start[1] !== end[1]; + } + + /** + * Gets the text currently selected. + */ + public get selectionText(): string { + const start = this._model.finalSelectionStart; + const end = this._model.finalSelectionEnd; + if (!start || !end) { + return ''; + } + + const buffer = this._bufferService.buffer; + const result: string[] = []; + + if (this._activeSelectionMode === SelectionMode.COLUMN) { + // Ignore zero width selections + if (start[0] === end[0]) { + return ''; + } + + for (let i = start[1]; i <= end[1]; i++) { + const lineText = buffer.translateBufferLineToString(i, true, start[0], end[0]); + result.push(lineText); + } + } else { + // Get first row + const startRowEndCol = start[1] === end[1] ? end[0] : undefined; + result.push(buffer.translateBufferLineToString(start[1], true, start[0], startRowEndCol)); + + // Get middle rows + for (let i = start[1] + 1; i <= end[1] - 1; i++) { + const bufferLine = buffer.lines.get(i); + const lineText = buffer.translateBufferLineToString(i, true); + if (bufferLine?.isWrapped) { + result[result.length - 1] += lineText; + } else { + result.push(lineText); + } + } + + // Get final row + if (start[1] !== end[1]) { + const bufferLine = buffer.lines.get(end[1]); + const lineText = buffer.translateBufferLineToString(end[1], true, 0, end[0]); + if (bufferLine && bufferLine!.isWrapped) { + result[result.length - 1] += lineText; + } else { + result.push(lineText); + } + } + } + + // Format string by replacing non-breaking space chars with regular spaces + // and joining the array into a multi-line string. + const formattedResult = result.map(line => { + return line.replace(ALL_NON_BREAKING_SPACE_REGEX, ' '); + }).join(Browser.isWindows ? '\r\n' : '\n'); + + return formattedResult; + } + + /** + * Clears the current terminal selection. + */ + public clearSelection(): void { + this._model.clearSelection(); + this._removeMouseDownListeners(); + this.refresh(); + this._onSelectionChange.fire(); + } + + /** + * Queues a refresh, redrawing the selection on the next opportunity. + * @param isLinuxMouseSelection Whether the selection should be registered as a new + * selection on Linux. + */ + public refresh(isLinuxMouseSelection?: boolean): void { + // Queue the refresh for the renderer + if (!this._refreshAnimationFrame) { + this._refreshAnimationFrame = window.requestAnimationFrame(() => this._refresh()); + } + + // If the platform is Linux and the refresh call comes from a mouse event, + // we need to update the selection for middle click to paste selection. + if (Browser.isLinux && isLinuxMouseSelection) { + const selectionText = this.selectionText; + if (selectionText.length) { + this._onLinuxMouseSelection.fire(this.selectionText); + } + } + } + + /** + * Fires the refresh event, causing consumers to pick it up and redraw the + * selection state. + */ + private _refresh(): void { + this._refreshAnimationFrame = undefined; + this._onRedrawRequest.fire({ + start: this._model.finalSelectionStart, + end: this._model.finalSelectionEnd, + columnSelectMode: this._activeSelectionMode === SelectionMode.COLUMN + }); + } + + /** + * Checks if the current click was inside the current selection + * @param event The mouse event + */ + private _isClickInSelection(event: MouseEvent): boolean { + const coords = this._getMouseBufferCoords(event); + const start = this._model.finalSelectionStart; + const end = this._model.finalSelectionEnd; + + if (!start || !end || !coords) { + return false; + } + + return this._areCoordsInSelection(coords, start, end); + } + + protected _areCoordsInSelection(coords: [number, number], start: [number, number], end: [number, number]): boolean { + return (coords[1] > start[1] && coords[1] < end[1]) || + (start[1] === end[1] && coords[1] === start[1] && coords[0] >= start[0] && coords[0] < end[0]) || + (start[1] < end[1] && coords[1] === end[1] && coords[0] < end[0]) || + (start[1] < end[1] && coords[1] === start[1] && coords[0] >= start[0]); + } + + /** + * Selects word at the current mouse event coordinates. + * @param event The mouse event. + */ + private _selectWordAtCursor(event: MouseEvent, allowWhitespaceOnlySelection: boolean): boolean { + // Check if there is a link under the cursor first and select that if so + const range = this._linkifier.currentLink?.link?.range; + if (range) { + this._model.selectionStart = [range.start.x - 1, range.start.y - 1]; + this._model.selectionStartLength = getRangeLength(range, this._bufferService.cols); + this._model.selectionEnd = undefined; + return true; + } + + const coords = this._getMouseBufferCoords(event); + if (coords) { + this._selectWordAt(coords, allowWhitespaceOnlySelection); + this._model.selectionEnd = undefined; + return true; + } + return false; + } + + /** + * Selects all text within the terminal. + */ + public selectAll(): void { + this._model.isSelectAllActive = true; + this.refresh(); + this._onSelectionChange.fire(); + } + + public selectLines(start: number, end: number): void { + this._model.clearSelection(); + start = Math.max(start, 0); + end = Math.min(end, this._bufferService.buffer.lines.length - 1); + this._model.selectionStart = [0, start]; + this._model.selectionEnd = [this._bufferService.cols, end]; + this.refresh(); + this._onSelectionChange.fire(); + } + + /** + * Handle the buffer being trimmed, adjust the selection position. + * @param amount The amount the buffer is being trimmed. + */ + private _onTrim(amount: number): void { + const needsRefresh = this._model.onTrim(amount); + if (needsRefresh) { + this.refresh(); + } + } + + /** + * Gets the 0-based [x, y] buffer coordinates of the current mouse event. + * @param event The mouse event. + */ + private _getMouseBufferCoords(event: MouseEvent): [number, number] | undefined { + const coords = this._mouseService.getCoords(event, this._screenElement, this._bufferService.cols, this._bufferService.rows, true); + if (!coords) { + return undefined; + } + + // Convert to 0-based + coords[0]--; + coords[1]--; + + // Convert viewport coords to buffer coords + coords[1] += this._bufferService.buffer.ydisp; + return coords; + } + + /** + * Gets the amount the viewport should be scrolled based on how far out of the + * terminal the mouse is. + * @param event The mouse event. + */ + private _getMouseEventScrollAmount(event: MouseEvent): number { + let offset = getCoordsRelativeToElement(event, this._screenElement)[1]; + const terminalHeight = this._renderService.dimensions.canvasHeight; + if (offset >= 0 && offset <= terminalHeight) { + return 0; + } + if (offset > terminalHeight) { + offset -= terminalHeight; + } + + offset = Math.min(Math.max(offset, -DRAG_SCROLL_MAX_THRESHOLD), DRAG_SCROLL_MAX_THRESHOLD); + offset /= DRAG_SCROLL_MAX_THRESHOLD; + return (offset / Math.abs(offset)) + Math.round(offset * (DRAG_SCROLL_MAX_SPEED - 1)); + } + + /** + * Returns whether the selection manager should force selection, regardless of + * whether the terminal is in mouse events mode. + * @param event The mouse event. + */ + public shouldForceSelection(event: MouseEvent): boolean { + if (Browser.isMac) { + return event.altKey && this._optionsService.rawOptions.macOptionClickForcesSelection; + } + + return event.shiftKey; + } + + /** + * Handles te mousedown event, setting up for a new selection. + * @param event The mousedown event. + */ + public onMouseDown(event: MouseEvent): void { + this._mouseDownTimeStamp = event.timeStamp; + // If we have selection, we want the context menu on right click even if the + // terminal is in mouse mode. + if (event.button === 2 && this.hasSelection) { + return; + } + + // Only action the primary button + if (event.button !== 0) { + return; + } + + // Allow selection when using a specific modifier key, even when disabled + if (!this._enabled) { + if (!this.shouldForceSelection(event)) { + return; + } + + // Don't send the mouse down event to the current process, we want to select + event.stopPropagation(); + } + + // Tell the browser not to start a regular selection + event.preventDefault(); + + // Reset drag scroll state + this._dragScrollAmount = 0; + + if (this._enabled && event.shiftKey) { + this._onIncrementalClick(event); + } else { + if (event.detail === 1) { + this._onSingleClick(event); + } else if (event.detail === 2) { + this._onDoubleClick(event); + } else if (event.detail === 3) { + this._onTripleClick(event); + } + } + + this._addMouseDownListeners(); + this.refresh(true); + } + + /** + * Adds listeners when mousedown is triggered. + */ + private _addMouseDownListeners(): void { + // Listen on the document so that dragging outside of viewport works + if (this._screenElement.ownerDocument) { + this._screenElement.ownerDocument.addEventListener('mousemove', this._mouseMoveListener); + this._screenElement.ownerDocument.addEventListener('mouseup', this._mouseUpListener); + } + this._dragScrollIntervalTimer = window.setInterval(() => this._dragScroll(), DRAG_SCROLL_INTERVAL); + } + + /** + * Removes the listeners that are registered when mousedown is triggered. + */ + private _removeMouseDownListeners(): void { + if (this._screenElement.ownerDocument) { + this._screenElement.ownerDocument.removeEventListener('mousemove', this._mouseMoveListener); + this._screenElement.ownerDocument.removeEventListener('mouseup', this._mouseUpListener); + } + clearInterval(this._dragScrollIntervalTimer); + this._dragScrollIntervalTimer = undefined; + } + + /** + * Performs an incremental click, setting the selection end position to the mouse + * position. + * @param event The mouse event. + */ + private _onIncrementalClick(event: MouseEvent): void { + if (this._model.selectionStart) { + this._model.selectionEnd = this._getMouseBufferCoords(event); + } + } + + /** + * Performs a single click, resetting relevant state and setting the selection + * start position. + * @param event The mouse event. + */ + private _onSingleClick(event: MouseEvent): void { + this._model.selectionStartLength = 0; + this._model.isSelectAllActive = false; + this._activeSelectionMode = this.shouldColumnSelect(event) ? SelectionMode.COLUMN : SelectionMode.NORMAL; + + // Initialize the new selection + this._model.selectionStart = this._getMouseBufferCoords(event); + if (!this._model.selectionStart) { + return; + } + this._model.selectionEnd = undefined; + + // Ensure the line exists + const line = this._bufferService.buffer.lines.get(this._model.selectionStart[1]); + if (!line) { + return; + } + + // Return early if the click event is not in the buffer (eg. in scroll bar) + if (line.length === this._model.selectionStart[0]) { + return; + } + + // If the mouse is over the second half of a wide character, adjust the + // selection to cover the whole character + if (line.hasWidth(this._model.selectionStart[0]) === 0) { + this._model.selectionStart[0]++; + } + } + + /** + * Performs a double click, selecting the current word. + * @param event The mouse event. + */ + private _onDoubleClick(event: MouseEvent): void { + if (this._selectWordAtCursor(event, true)) { + this._activeSelectionMode = SelectionMode.WORD; + } + } + + /** + * Performs a triple click, selecting the current line and activating line + * select mode. + * @param event The mouse event. + */ + private _onTripleClick(event: MouseEvent): void { + const coords = this._getMouseBufferCoords(event); + if (coords) { + this._activeSelectionMode = SelectionMode.LINE; + this._selectLineAt(coords[1]); + } + } + + /** + * Returns whether the selection manager should operate in column select mode + * @param event the mouse or keyboard event + */ + public shouldColumnSelect(event: KeyboardEvent | MouseEvent): boolean { + return event.altKey && !(Browser.isMac && this._optionsService.rawOptions.macOptionClickForcesSelection); + } + + /** + * Handles the mousemove event when the mouse button is down, recording the + * end of the selection and refreshing the selection. + * @param event The mousemove event. + */ + private _onMouseMove(event: MouseEvent): void { + // If the mousemove listener is active it means that a selection is + // currently being made, we should stop propagation to prevent mouse events + // to be sent to the pty. + event.stopImmediatePropagation(); + + // Do nothing if there is no selection start, this can happen if the first + // click in the terminal is an incremental click + if (!this._model.selectionStart) { + return; + } + + // Record the previous position so we know whether to redraw the selection + // at the end. + const previousSelectionEnd = this._model.selectionEnd ? [this._model.selectionEnd[0], this._model.selectionEnd[1]] : null; + + // Set the initial selection end based on the mouse coordinates + this._model.selectionEnd = this._getMouseBufferCoords(event); + if (!this._model.selectionEnd) { + this.refresh(true); + return; + } + + // Select the entire line if line select mode is active. + if (this._activeSelectionMode === SelectionMode.LINE) { + if (this._model.selectionEnd[1] < this._model.selectionStart[1]) { + this._model.selectionEnd[0] = 0; + } else { + this._model.selectionEnd[0] = this._bufferService.cols; + } + } else if (this._activeSelectionMode === SelectionMode.WORD) { + this._selectToWordAt(this._model.selectionEnd); + } + + // Determine the amount of scrolling that will happen. + this._dragScrollAmount = this._getMouseEventScrollAmount(event); + + // If the cursor was above or below the viewport, make sure it's at the + // start or end of the viewport respectively. This should only happen when + // NOT in column select mode. + if (this._activeSelectionMode !== SelectionMode.COLUMN) { + if (this._dragScrollAmount > 0) { + this._model.selectionEnd[0] = this._bufferService.cols; + } else if (this._dragScrollAmount < 0) { + this._model.selectionEnd[0] = 0; + } + } + + // If the character is a wide character include the cell to the right in the + // selection. Note that selections at the very end of the line will never + // have a character. + const buffer = this._bufferService.buffer; + if (this._model.selectionEnd[1] < buffer.lines.length) { + const line = buffer.lines.get(this._model.selectionEnd[1]); + if (line && line.hasWidth(this._model.selectionEnd[0]) === 0) { + this._model.selectionEnd[0]++; + } + } + + // Only draw here if the selection changes. + if (!previousSelectionEnd || + previousSelectionEnd[0] !== this._model.selectionEnd[0] || + previousSelectionEnd[1] !== this._model.selectionEnd[1]) { + this.refresh(true); + } + } + + /** + * The callback that occurs every DRAG_SCROLL_INTERVAL ms that does the + * scrolling of the viewport. + */ + private _dragScroll(): void { + if (!this._model.selectionEnd || !this._model.selectionStart) { + return; + } + if (this._dragScrollAmount) { + this._onRequestScrollLines.fire({ amount: this._dragScrollAmount, suppressScrollEvent: false }); + // Re-evaluate selection + // If the cursor was above or below the viewport, make sure it's at the + // start or end of the viewport respectively. This should only happen when + // NOT in column select mode. + const buffer = this._bufferService.buffer; + if (this._dragScrollAmount > 0) { + if (this._activeSelectionMode !== SelectionMode.COLUMN) { + this._model.selectionEnd[0] = this._bufferService.cols; + } + this._model.selectionEnd[1] = Math.min(buffer.ydisp + this._bufferService.rows, buffer.lines.length - 1); + } else { + if (this._activeSelectionMode !== SelectionMode.COLUMN) { + this._model.selectionEnd[0] = 0; + } + this._model.selectionEnd[1] = buffer.ydisp; + } + this.refresh(); + } + } + + /** + * Handles the mouseup event, removing the mousedown listeners. + * @param event The mouseup event. + */ + private _onMouseUp(event: MouseEvent): void { + const timeElapsed = event.timeStamp - this._mouseDownTimeStamp; + + this._removeMouseDownListeners(); + + if (this.selectionText.length <= 1 && timeElapsed < ALT_CLICK_MOVE_CURSOR_TIME && event.altKey && this._optionsService.getOption('altClickMovesCursor')) { + if (this._bufferService.buffer.ybase === this._bufferService.buffer.ydisp) { + const coordinates = this._mouseService.getCoords( + event, + this._element, + this._bufferService.cols, + this._bufferService.rows, + false + ); + if (coordinates && coordinates[0] !== undefined && coordinates[1] !== undefined) { + const sequence = moveToCellSequence(coordinates[0] - 1, coordinates[1] - 1, this._bufferService, this._coreService.decPrivateModes.applicationCursorKeys); + this._coreService.triggerDataEvent(sequence, true); + } + } + } else { + this._fireEventIfSelectionChanged(); + } + } + + private _fireEventIfSelectionChanged(): void { + const start = this._model.finalSelectionStart; + const end = this._model.finalSelectionEnd; + const hasSelection = !!start && !!end && (start[0] !== end[0] || start[1] !== end[1]); + + if (!hasSelection) { + if (this._oldHasSelection) { + this._fireOnSelectionChange(start, end, hasSelection); + } + return; + } + + // Sanity check, these should not be undefined as there is a selection + if (!start || !end) { + return; + } + + if (!this._oldSelectionStart || !this._oldSelectionEnd || ( + start[0] !== this._oldSelectionStart[0] || start[1] !== this._oldSelectionStart[1] || + end[0] !== this._oldSelectionEnd[0] || end[1] !== this._oldSelectionEnd[1])) { + + this._fireOnSelectionChange(start, end, hasSelection); + } + } + + private _fireOnSelectionChange(start: [number, number] | undefined, end: [number, number] | undefined, hasSelection: boolean): void { + this._oldSelectionStart = start; + this._oldSelectionEnd = end; + this._oldHasSelection = hasSelection; + this._onSelectionChange.fire(); + } + + private _onBufferActivate(e: {activeBuffer: IBuffer, inactiveBuffer: IBuffer}): void { + this.clearSelection(); + // Only adjust the selection on trim, shiftElements is rarely used (only in + // reverseIndex) and delete in a splice is only ever used when the same + // number of elements was just added. Given this is could actually be + // beneficial to leave the selection as is for these cases. + this._trimListener.dispose(); + this._trimListener = e.activeBuffer.lines.onTrim(amount => this._onTrim(amount)); + } + + /** + * Converts a viewport column to the character index on the buffer line, the + * latter takes into account wide characters. + * @param coords The coordinates to find the 2 index for. + */ + private _convertViewportColToCharacterIndex(bufferLine: IBufferLine, coords: [number, number]): number { + let charIndex = coords[0]; + for (let i = 0; coords[0] >= i; i++) { + const length = bufferLine.loadCell(i, this._workCell).getChars().length; + if (this._workCell.getWidth() === 0) { + // Wide characters aren't included in the line string so decrement the + // index so the index is back on the wide character. + charIndex--; + } else if (length > 1 && coords[0] !== i) { + // Emojis take up multiple characters, so adjust accordingly. For these + // we don't want ot include the character at the column as we're + // returning the start index in the string, not the end index. + charIndex += length - 1; + } + } + return charIndex; + } + + public setSelection(col: number, row: number, length: number): void { + this._model.clearSelection(); + this._removeMouseDownListeners(); + this._model.selectionStart = [col, row]; + this._model.selectionStartLength = length; + this.refresh(); + } + + public rightClickSelect(ev: MouseEvent): void { + if (!this._isClickInSelection(ev)) { + if (this._selectWordAtCursor(ev, false)) { + this.refresh(true); + } + this._fireEventIfSelectionChanged(); + } + } + + /** + * Gets positional information for the word at the coordinated specified. + * @param coords The coordinates to get the word at. + */ + private _getWordAt(coords: [number, number], allowWhitespaceOnlySelection: boolean, followWrappedLinesAbove: boolean = true, followWrappedLinesBelow: boolean = true): IWordPosition | undefined { + // Ensure coords are within viewport (eg. not within scroll bar) + if (coords[0] >= this._bufferService.cols) { + return undefined; + } + + const buffer = this._bufferService.buffer; + const bufferLine = buffer.lines.get(coords[1]); + if (!bufferLine) { + return undefined; + } + + const line = buffer.translateBufferLineToString(coords[1], false); + + // Get actual index, taking into consideration wide characters + let startIndex = this._convertViewportColToCharacterIndex(bufferLine, coords); + let endIndex = startIndex; + + // Record offset to be used later + const charOffset = coords[0] - startIndex; + let leftWideCharCount = 0; + let rightWideCharCount = 0; + let leftLongCharOffset = 0; + let rightLongCharOffset = 0; + + if (line.charAt(startIndex) === ' ') { + // Expand until non-whitespace is hit + while (startIndex > 0 && line.charAt(startIndex - 1) === ' ') { + startIndex--; + } + while (endIndex < line.length && line.charAt(endIndex + 1) === ' ') { + endIndex++; + } + } else { + // Expand until whitespace is hit. This algorithm works by scanning left + // and right from the starting position, keeping both the index format + // (line) and the column format (bufferLine) in sync. When a wide + // character is hit, it is recorded and the column index is adjusted. + let startCol = coords[0]; + let endCol = coords[0]; + + // Consider the initial position, skip it and increment the wide char + // variable + if (bufferLine.getWidth(startCol) === 0) { + leftWideCharCount++; + startCol--; + } + if (bufferLine.getWidth(endCol) === 2) { + rightWideCharCount++; + endCol++; + } + + // Adjust the end index for characters whose length are > 1 (emojis) + const length = bufferLine.getString(endCol).length; + if (length > 1) { + rightLongCharOffset += length - 1; + endIndex += length - 1; + } + + // Expand the string in both directions until a space is hit + while (startCol > 0 && startIndex > 0 && !this._isCharWordSeparator(bufferLine.loadCell(startCol - 1, this._workCell))) { + bufferLine.loadCell(startCol - 1, this._workCell); + const length = this._workCell.getChars().length; + if (this._workCell.getWidth() === 0) { + // If the next character is a wide char, record it and skip the column + leftWideCharCount++; + startCol--; + } else if (length > 1) { + // If the next character's string is longer than 1 char (eg. emoji), + // adjust the index + leftLongCharOffset += length - 1; + startIndex -= length - 1; + } + startIndex--; + startCol--; + } + while (endCol < bufferLine.length && endIndex + 1 < line.length && !this._isCharWordSeparator(bufferLine.loadCell(endCol + 1, this._workCell))) { + bufferLine.loadCell(endCol + 1, this._workCell); + const length = this._workCell.getChars().length; + if (this._workCell.getWidth() === 2) { + // If the next character is a wide char, record it and skip the column + rightWideCharCount++; + endCol++; + } else if (length > 1) { + // If the next character's string is longer than 1 char (eg. emoji), + // adjust the index + rightLongCharOffset += length - 1; + endIndex += length - 1; + } + endIndex++; + endCol++; + } + } + + // Incremenet the end index so it is at the start of the next character + endIndex++; + + // Calculate the start _column_, converting the the string indexes back to + // column coordinates. + let start = + startIndex // The index of the selection's start char in the line string + + charOffset // The difference between the initial char's column and index + - leftWideCharCount // The number of wide chars left of the initial char + + leftLongCharOffset; // The number of additional chars left of the initial char added by columns with strings longer than 1 (emojis) + + // Calculate the length in _columns_, converting the the string indexes back + // to column coordinates. + let length = Math.min(this._bufferService.cols, // Disallow lengths larger than the terminal cols + endIndex // The index of the selection's end char in the line string + - startIndex // The index of the selection's start char in the line string + + leftWideCharCount // The number of wide chars left of the initial char + + rightWideCharCount // The number of wide chars right of the initial char (inclusive) + - leftLongCharOffset // The number of additional chars left of the initial char added by columns with strings longer than 1 (emojis) + - rightLongCharOffset); // The number of additional chars right of the initial char (inclusive) added by columns with strings longer than 1 (emojis) + + if (!allowWhitespaceOnlySelection && line.slice(startIndex, endIndex).trim() === '') { + return undefined; + } + + // Recurse upwards if the line is wrapped and the word wraps to the above line + if (followWrappedLinesAbove) { + if (start === 0 && bufferLine.getCodePoint(0) !== 32 /* ' ' */) { + const previousBufferLine = buffer.lines.get(coords[1] - 1); + if (previousBufferLine && bufferLine.isWrapped && previousBufferLine.getCodePoint(this._bufferService.cols - 1) !== 32 /* ' ' */) { + const previousLineWordPosition = this._getWordAt([this._bufferService.cols - 1, coords[1] - 1], false, true, false); + if (previousLineWordPosition) { + const offset = this._bufferService.cols - previousLineWordPosition.start; + start -= offset; + length += offset; + } + } + } + } + + // Recurse downwards if the line is wrapped and the word wraps to the next line + if (followWrappedLinesBelow) { + if (start + length === this._bufferService.cols && bufferLine.getCodePoint(this._bufferService.cols - 1) !== 32 /* ' ' */) { + const nextBufferLine = buffer.lines.get(coords[1] + 1); + if (nextBufferLine?.isWrapped && nextBufferLine.getCodePoint(0) !== 32 /* ' ' */) { + const nextLineWordPosition = this._getWordAt([0, coords[1] + 1], false, false, true); + if (nextLineWordPosition) { + length += nextLineWordPosition.length; + } + } + } + } + + return { start, length }; + } + + /** + * Selects the word at the coordinates specified. + * @param coords The coordinates to get the word at. + * @param allowWhitespaceOnlySelection If whitespace should be selected + */ + protected _selectWordAt(coords: [number, number], allowWhitespaceOnlySelection: boolean): void { + const wordPosition = this._getWordAt(coords, allowWhitespaceOnlySelection); + if (wordPosition) { + // Adjust negative start value + while (wordPosition.start < 0) { + wordPosition.start += this._bufferService.cols; + coords[1]--; + } + this._model.selectionStart = [wordPosition.start, coords[1]]; + this._model.selectionStartLength = wordPosition.length; + } + } + + /** + * Sets the selection end to the word at the coordinated specified. + * @param coords The coordinates to get the word at. + */ + private _selectToWordAt(coords: [number, number]): void { + const wordPosition = this._getWordAt(coords, true); + if (wordPosition) { + let endRow = coords[1]; + + // Adjust negative start value + while (wordPosition.start < 0) { + wordPosition.start += this._bufferService.cols; + endRow--; + } + + // Adjust wrapped length value, this only needs to happen when values are reversed as in that + // case we're interested in the start of the word, not the end + if (!this._model.areSelectionValuesReversed()) { + while (wordPosition.start + wordPosition.length > this._bufferService.cols) { + wordPosition.length -= this._bufferService.cols; + endRow++; + } + } + + this._model.selectionEnd = [this._model.areSelectionValuesReversed() ? wordPosition.start : wordPosition.start + wordPosition.length, endRow]; + } + } + + /** + * Gets whether the character is considered a word separator by the select + * word logic. + * @param char The character to check. + */ + private _isCharWordSeparator(cell: CellData): boolean { + // Zero width characters are never separators as they are always to the + // right of wide characters + if (cell.getWidth() === 0) { + return false; + } + return this._optionsService.rawOptions.wordSeparator.indexOf(cell.getChars()) >= 0; + } + + /** + * Selects the line specified. + * @param line The line index. + */ + protected _selectLineAt(line: number): void { + const wrappedRange = this._bufferService.buffer.getWrappedRangeForLine(line); + this._model.selectionStart = [0, wrappedRange.first]; + this._model.selectionEnd = [this._bufferService.cols, wrappedRange.last]; + this._model.selectionStartLength = 0; + } +} diff --git a/node_modules/xterm/src/browser/services/Services.ts b/node_modules/xterm/src/browser/services/Services.ts new file mode 100644 index 0000000..4928fa2 --- /dev/null +++ b/node_modules/xterm/src/browser/services/Services.ts @@ -0,0 +1,115 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IEvent } from 'common/EventEmitter'; +import { IRenderDimensions, IRenderer } from 'browser/renderer/Types'; +import { IColorSet } from 'browser/Types'; +import { ISelectionRedrawRequestEvent as ISelectionRequestRedrawEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types'; +import { createDecorator } from 'common/services/ServiceRegistry'; +import { IDisposable } from 'common/Types'; + +export const ICharSizeService = createDecorator<ICharSizeService>('CharSizeService'); +export interface ICharSizeService { + serviceBrand: undefined; + + readonly width: number; + readonly height: number; + readonly hasValidSize: boolean; + + readonly onCharSizeChange: IEvent<void>; + + measure(): void; +} + +export const ICoreBrowserService = createDecorator<ICoreBrowserService>('CoreBrowserService'); +export interface ICoreBrowserService { + serviceBrand: undefined; + + readonly isFocused: boolean; +} + +export const IMouseService = createDecorator<IMouseService>('MouseService'); +export interface IMouseService { + serviceBrand: undefined; + + getCoords(event: {clientX: number, clientY: number}, element: HTMLElement, colCount: number, rowCount: number, isSelection?: boolean): [number, number] | undefined; + getRawByteCoords(event: MouseEvent, element: HTMLElement, colCount: number, rowCount: number): { x: number, y: number } | undefined; +} + +export const IRenderService = createDecorator<IRenderService>('RenderService'); +export interface IRenderService extends IDisposable { + serviceBrand: undefined; + + onDimensionsChange: IEvent<IRenderDimensions>; + /** + * Fires when buffer changes are rendered. This does not fire when only cursor + * or selections are rendered. + */ + onRenderedBufferChange: IEvent<{ start: number, end: number }>; + onRefreshRequest: IEvent<{ start: number, end: number }>; + + dimensions: IRenderDimensions; + + refreshRows(start: number, end: number): void; + clearTextureAtlas(): void; + resize(cols: number, rows: number): void; + changeOptions(): void; + setRenderer(renderer: IRenderer): void; + setColors(colors: IColorSet): void; + onDevicePixelRatioChange(): void; + onResize(cols: number, rows: number): void; + // TODO: Is this useful when we have onResize? + onCharSizeChanged(): void; + onBlur(): void; + onFocus(): void; + onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void; + onCursorMove(): void; + clear(): void; +} + +export const ISelectionService = createDecorator<ISelectionService>('SelectionService'); +export interface ISelectionService { + serviceBrand: undefined; + + readonly selectionText: string; + readonly hasSelection: boolean; + readonly selectionStart: [number, number] | undefined; + readonly selectionEnd: [number, number] | undefined; + + readonly onLinuxMouseSelection: IEvent<string>; + readonly onRequestRedraw: IEvent<ISelectionRequestRedrawEvent>; + readonly onRequestScrollLines: IEvent<ISelectionRequestScrollLinesEvent>; + readonly onSelectionChange: IEvent<void>; + + disable(): void; + enable(): void; + reset(): void; + setSelection(row: number, col: number, length: number): void; + selectAll(): void; + selectLines(start: number, end: number): void; + clearSelection(): void; + rightClickSelect(event: MouseEvent): void; + shouldColumnSelect(event: KeyboardEvent | MouseEvent): boolean; + shouldForceSelection(event: MouseEvent): boolean; + refresh(isLinuxMouseSelection?: boolean): void; + onMouseDown(event: MouseEvent): void; +} + +export const ISoundService = createDecorator<ISoundService>('SoundService'); +export interface ISoundService { + serviceBrand: undefined; + + playBellSound(): void; +} + + +export const ICharacterJoinerService = createDecorator<ICharacterJoinerService>('CharacterJoinerService'); +export interface ICharacterJoinerService { + serviceBrand: undefined; + + register(handler: (text: string) => [number, number][]): number; + deregister(joinerId: number): boolean; + getJoinedCharacters(row: number): [number, number][]; +} diff --git a/node_modules/xterm/src/browser/services/SoundService.ts b/node_modules/xterm/src/browser/services/SoundService.ts new file mode 100644 index 0000000..a3b6800 --- /dev/null +++ b/node_modules/xterm/src/browser/services/SoundService.ts @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IOptionsService } from 'common/services/Services'; +import { ISoundService } from 'browser/services/Services'; + +export class SoundService implements ISoundService { + public serviceBrand: undefined; + + private static _audioContext: AudioContext; + + public static get audioContext(): AudioContext | null { + if (!SoundService._audioContext) { + const audioContextCtor: typeof AudioContext = (window as any).AudioContext || (window as any).webkitAudioContext; + if (!audioContextCtor) { + console.warn('Web Audio API is not supported by this browser. Consider upgrading to the latest version'); + return null; + } + SoundService._audioContext = new audioContextCtor(); + } + return SoundService._audioContext; + } + + constructor( + @IOptionsService private _optionsService: IOptionsService + ) { + } + + public playBellSound(): void { + const ctx = SoundService.audioContext; + if (!ctx) { + return; + } + const bellAudioSource = ctx.createBufferSource(); + ctx.decodeAudioData(this._base64ToArrayBuffer(this._removeMimeType(this._optionsService.rawOptions.bellSound)), (buffer) => { + bellAudioSource.buffer = buffer; + bellAudioSource.connect(ctx.destination); + bellAudioSource.start(0); + }); + } + + private _base64ToArrayBuffer(base64: string): ArrayBuffer { + const binaryString = window.atob(base64); + const len = binaryString.length; + const bytes = new Uint8Array(len); + + for (let i = 0; i < len; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + + return bytes.buffer; + } + + private _removeMimeType(dataURI: string): string { + // Split the input to get the mime-type and the data itself + const splitUri = dataURI.split(','); + + // Return only the data + return splitUri[1]; + } +} diff --git a/node_modules/xterm/src/common/CircularList.ts b/node_modules/xterm/src/common/CircularList.ts new file mode 100644 index 0000000..4d2c04e --- /dev/null +++ b/node_modules/xterm/src/common/CircularList.ts @@ -0,0 +1,239 @@ +/** + * Copyright (c) 2016 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { ICircularList } from 'common/Types'; +import { EventEmitter, IEvent } from 'common/EventEmitter'; + +export interface IInsertEvent { + index: number; + amount: number; +} + +export interface IDeleteEvent { + index: number; + amount: number; +} + +/** + * Represents a circular list; a list with a maximum size that wraps around when push is called, + * overriding values at the start of the list. + */ +export class CircularList<T> implements ICircularList<T> { + protected _array: (T | undefined)[]; + private _startIndex: number; + private _length: number; + + public onDeleteEmitter = new EventEmitter<IDeleteEvent>(); + public get onDelete(): IEvent<IDeleteEvent> { return this.onDeleteEmitter.event; } + public onInsertEmitter = new EventEmitter<IInsertEvent>(); + public get onInsert(): IEvent<IInsertEvent> { return this.onInsertEmitter.event; } + public onTrimEmitter = new EventEmitter<number>(); + public get onTrim(): IEvent<number> { return this.onTrimEmitter.event; } + + constructor( + private _maxLength: number + ) { + this._array = new Array<T>(this._maxLength); + this._startIndex = 0; + this._length = 0; + } + + public get maxLength(): number { + return this._maxLength; + } + + public set maxLength(newMaxLength: number) { + // There was no change in maxLength, return early. + if (this._maxLength === newMaxLength) { + return; + } + + // Reconstruct array, starting at index 0. Only transfer values from the + // indexes 0 to length. + const newArray = new Array<T | undefined>(newMaxLength); + for (let i = 0; i < Math.min(newMaxLength, this.length); i++) { + newArray[i] = this._array[this._getCyclicIndex(i)]; + } + this._array = newArray; + this._maxLength = newMaxLength; + this._startIndex = 0; + } + + public get length(): number { + return this._length; + } + + public set length(newLength: number) { + if (newLength > this._length) { + for (let i = this._length; i < newLength; i++) { + this._array[i] = undefined; + } + } + this._length = newLength; + } + + /** + * Gets the value at an index. + * + * Note that for performance reasons there is no bounds checking here, the index reference is + * circular so this should always return a value and never throw. + * @param index The index of the value to get. + * @return The value corresponding to the index. + */ + public get(index: number): T | undefined { + return this._array[this._getCyclicIndex(index)]; + } + + /** + * Sets the value at an index. + * + * Note that for performance reasons there is no bounds checking here, the index reference is + * circular so this should always return a value and never throw. + * @param index The index to set. + * @param value The value to set. + */ + public set(index: number, value: T | undefined): void { + this._array[this._getCyclicIndex(index)] = value; + } + + /** + * Pushes a new value onto the list, wrapping around to the start of the array, overriding index 0 + * if the maximum length is reached. + * @param value The value to push onto the list. + */ + public push(value: T): void { + this._array[this._getCyclicIndex(this._length)] = value; + if (this._length === this._maxLength) { + this._startIndex = ++this._startIndex % this._maxLength; + this.onTrimEmitter.fire(1); + } else { + this._length++; + } + } + + /** + * Advance ringbuffer index and return current element for recycling. + * Note: The buffer must be full for this method to work. + * @throws When the buffer is not full. + */ + public recycle(): T { + if (this._length !== this._maxLength) { + throw new Error('Can only recycle when the buffer is full'); + } + this._startIndex = ++this._startIndex % this._maxLength; + this.onTrimEmitter.fire(1); + return this._array[this._getCyclicIndex(this._length - 1)]!; + } + + /** + * Ringbuffer is at max length. + */ + public get isFull(): boolean { + return this._length === this._maxLength; + } + + /** + * Removes and returns the last value on the list. + * @return The popped value. + */ + public pop(): T | undefined { + return this._array[this._getCyclicIndex(this._length-- - 1)]; + } + + /** + * Deletes and/or inserts items at a particular index (in that order). Unlike + * Array.prototype.splice, this operation does not return the deleted items as a new array in + * order to save creating a new array. Note that this operation may shift all values in the list + * in the worst case. + * @param start The index to delete and/or insert. + * @param deleteCount The number of elements to delete. + * @param items The items to insert. + */ + public splice(start: number, deleteCount: number, ...items: T[]): void { + // Delete items + if (deleteCount) { + for (let i = start; i < this._length - deleteCount; i++) { + this._array[this._getCyclicIndex(i)] = this._array[this._getCyclicIndex(i + deleteCount)]; + } + this._length -= deleteCount; + this.onDeleteEmitter.fire({ index: start, amount: deleteCount }); + } + + // Add items + for (let i = this._length - 1; i >= start; i--) { + this._array[this._getCyclicIndex(i + items.length)] = this._array[this._getCyclicIndex(i)]; + } + for (let i = 0; i < items.length; i++) { + this._array[this._getCyclicIndex(start + i)] = items[i]; + } + if (items.length) { + this.onInsertEmitter.fire({ index: start, amount: items.length }); + } + + // Adjust length as needed + if (this._length + items.length > this._maxLength) { + const countToTrim = (this._length + items.length) - this._maxLength; + this._startIndex += countToTrim; + this._length = this._maxLength; + this.onTrimEmitter.fire(countToTrim); + } else { + this._length += items.length; + } + } + + /** + * Trims a number of items from the start of the list. + * @param count The number of items to remove. + */ + public trimStart(count: number): void { + if (count > this._length) { + count = this._length; + } + this._startIndex += count; + this._length -= count; + this.onTrimEmitter.fire(count); + } + + public shiftElements(start: number, count: number, offset: number): void { + if (count <= 0) { + return; + } + if (start < 0 || start >= this._length) { + throw new Error('start argument out of range'); + } + if (start + offset < 0) { + throw new Error('Cannot shift elements in list beyond index 0'); + } + + if (offset > 0) { + for (let i = count - 1; i >= 0; i--) { + this.set(start + i + offset, this.get(start + i)); + } + const expandListBy = (start + count + offset) - this._length; + if (expandListBy > 0) { + this._length += expandListBy; + while (this._length > this._maxLength) { + this._length--; + this._startIndex++; + this.onTrimEmitter.fire(1); + } + } + } else { + for (let i = 0; i < count; i++) { + this.set(start + i + offset, this.get(start + i)); + } + } + } + + /** + * Gets the cyclic index for the specified regular index. The cyclic index can then be used on the + * backing array to get the element associated with the regular index. + * @param index The regular index. + * @returns The cyclic index. + */ + private _getCyclicIndex(index: number): number { + return (this._startIndex + index) % this._maxLength; + } +} diff --git a/node_modules/xterm/src/common/Clone.ts b/node_modules/xterm/src/common/Clone.ts new file mode 100644 index 0000000..37821fe --- /dev/null +++ b/node_modules/xterm/src/common/Clone.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2016 The xterm.js authors. All rights reserved. + * @license MIT + */ + +/* + * A simple utility for cloning values + */ +export function clone<T>(val: T, depth: number = 5): T { + if (typeof val !== 'object') { + return val; + } + + // If we're cloning an array, use an array as the base, otherwise use an object + const clonedObject: any = Array.isArray(val) ? [] : {}; + + for (const key in val) { + // Recursively clone eack item unless we're at the maximum depth + clonedObject[key] = depth <= 1 ? val[key] : (val[key] && clone(val[key], depth - 1)); + } + + return clonedObject as T; +} diff --git a/node_modules/xterm/src/common/CoreTerminal.ts b/node_modules/xterm/src/common/CoreTerminal.ts new file mode 100644 index 0000000..12b374c --- /dev/null +++ b/node_modules/xterm/src/common/CoreTerminal.ts @@ -0,0 +1,297 @@ +/** + * Copyright (c) 2014-2020 The xterm.js authors. All rights reserved. + * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License) + * @license MIT + * + * Originally forked from (with the author's permission): + * Fabrice Bellard's javascript vt100 for jslinux: + * http://bellard.org/jslinux/ + * Copyright (c) 2011 Fabrice Bellard + * The original design remains. The terminal itself + * has been extended to include xterm CSI codes, among + * other features. + * + * Terminal Emulation References: + * http://vt100.net/ + * http://invisible-island.net/xterm/ctlseqs/ctlseqs.txt + * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html + * http://invisible-island.net/vttest/ + * http://www.inwap.com/pdp10/ansicode.txt + * http://linux.die.net/man/4/console_codes + * http://linux.die.net/man/7/urxvt + */ + +import { Disposable } from 'common/Lifecycle'; +import { IInstantiationService, IOptionsService, IBufferService, ILogService, ICharsetService, ICoreService, ICoreMouseService, IUnicodeService, IDirtyRowService, LogLevelEnum, ITerminalOptions } from 'common/services/Services'; +import { InstantiationService } from 'common/services/InstantiationService'; +import { LogService } from 'common/services/LogService'; +import { BufferService, MINIMUM_COLS, MINIMUM_ROWS } from 'common/services/BufferService'; +import { OptionsService } from 'common/services/OptionsService'; +import { IDisposable, IBufferLine, IAttributeData, ICoreTerminal, IKeyboardEvent, IScrollEvent, ScrollSource, ITerminalOptions as IPublicTerminalOptions } from 'common/Types'; +import { CoreService } from 'common/services/CoreService'; +import { EventEmitter, IEvent, forwardEvent } from 'common/EventEmitter'; +import { CoreMouseService } from 'common/services/CoreMouseService'; +import { DirtyRowService } from 'common/services/DirtyRowService'; +import { UnicodeService } from 'common/services/UnicodeService'; +import { CharsetService } from 'common/services/CharsetService'; +import { updateWindowsModeWrappedState } from 'common/WindowsMode'; +import { IFunctionIdentifier, IParams } from 'common/parser/Types'; +import { IBufferSet } from 'common/buffer/Types'; +import { InputHandler } from 'common/InputHandler'; +import { WriteBuffer } from 'common/input/WriteBuffer'; + +// Only trigger this warning a single time per session +let hasWriteSyncWarnHappened = false; + +export abstract class CoreTerminal extends Disposable implements ICoreTerminal { + protected readonly _instantiationService: IInstantiationService; + protected readonly _bufferService: IBufferService; + protected readonly _logService: ILogService; + protected readonly _charsetService: ICharsetService; + protected readonly _dirtyRowService: IDirtyRowService; + + public readonly coreMouseService: ICoreMouseService; + public readonly coreService: ICoreService; + public readonly unicodeService: IUnicodeService; + public readonly optionsService: IOptionsService; + + protected _inputHandler: InputHandler; + private _writeBuffer: WriteBuffer; + private _windowsMode: IDisposable | undefined; + + private _onBinary = new EventEmitter<string>(); + public get onBinary(): IEvent<string> { return this._onBinary.event; } + private _onData = new EventEmitter<string>(); + public get onData(): IEvent<string> { return this._onData.event; } + protected _onLineFeed = new EventEmitter<void>(); + public get onLineFeed(): IEvent<void> { return this._onLineFeed.event; } + private _onResize = new EventEmitter<{ cols: number, rows: number }>(); + public get onResize(): IEvent<{ cols: number, rows: number }> { return this._onResize.event; } + protected _onScroll = new EventEmitter<IScrollEvent, void>(); + /** + * Internally we track the source of the scroll but this is meaningless outside the library so + * it's filtered out. + */ + protected _onScrollApi?: EventEmitter<number, void>; + public get onScroll(): IEvent<number, void> { + if (!this._onScrollApi) { + this._onScrollApi = new EventEmitter<number, void>(); + this.register(this._onScroll.event(ev => { + this._onScrollApi?.fire(ev.position); + })); + } + return this._onScrollApi.event; + } + + public get cols(): number { return this._bufferService.cols; } + public get rows(): number { return this._bufferService.rows; } + public get buffers(): IBufferSet { return this._bufferService.buffers; } + public get options(): ITerminalOptions { return this.optionsService.options; } + public set options(options: ITerminalOptions) { + for (const key in options) { + this.optionsService.options[key] = options[key]; + } + } + + constructor( + options: Partial<ITerminalOptions> + ) { + super(); + + // Setup and initialize services + this._instantiationService = new InstantiationService(); + this.optionsService = new OptionsService(options); + this._instantiationService.setService(IOptionsService, this.optionsService); + this._bufferService = this.register(this._instantiationService.createInstance(BufferService)); + this._instantiationService.setService(IBufferService, this._bufferService); + this._logService = this._instantiationService.createInstance(LogService); + this._instantiationService.setService(ILogService, this._logService); + this.coreService = this.register(this._instantiationService.createInstance(CoreService, () => this.scrollToBottom())); + this._instantiationService.setService(ICoreService, this.coreService); + this.coreMouseService = this._instantiationService.createInstance(CoreMouseService); + this._instantiationService.setService(ICoreMouseService, this.coreMouseService); + this._dirtyRowService = this._instantiationService.createInstance(DirtyRowService); + this._instantiationService.setService(IDirtyRowService, this._dirtyRowService); + this.unicodeService = this._instantiationService.createInstance(UnicodeService); + this._instantiationService.setService(IUnicodeService, this.unicodeService); + this._charsetService = this._instantiationService.createInstance(CharsetService); + this._instantiationService.setService(ICharsetService, this._charsetService); + + // Register input handler and handle/forward events + this._inputHandler = new InputHandler(this._bufferService, this._charsetService, this.coreService, this._dirtyRowService, this._logService, this.optionsService, this.coreMouseService, this.unicodeService); + this.register(forwardEvent(this._inputHandler.onLineFeed, this._onLineFeed)); + this.register(this._inputHandler); + + // Setup listeners + this.register(forwardEvent(this._bufferService.onResize, this._onResize)); + this.register(forwardEvent(this.coreService.onData, this._onData)); + this.register(forwardEvent(this.coreService.onBinary, this._onBinary)); + this.register(this.optionsService.onOptionChange(key => this._updateOptions(key))); + this.register(this._bufferService.onScroll(event => { + this._onScroll.fire({ position: this._bufferService.buffer.ydisp, source: ScrollSource.TERMINAL }); + this._dirtyRowService.markRangeDirty(this._bufferService.buffer.scrollTop, this._bufferService.buffer.scrollBottom); + })); + this.register(this._inputHandler.onScroll(event => { + this._onScroll.fire({ position: this._bufferService.buffer.ydisp, source: ScrollSource.TERMINAL }); + this._dirtyRowService.markRangeDirty(this._bufferService.buffer.scrollTop, this._bufferService.buffer.scrollBottom); + })); + + // Setup WriteBuffer + this._writeBuffer = new WriteBuffer((data, promiseResult) => this._inputHandler.parse(data, promiseResult)); + } + + public dispose(): void { + if (this._isDisposed) { + return; + } + super.dispose(); + this._windowsMode?.dispose(); + this._windowsMode = undefined; + } + + public write(data: string | Uint8Array, callback?: () => void): void { + this._writeBuffer.write(data, callback); + } + + /** + * Write data to terminal synchonously. + * + * This method is unreliable with async parser handlers, thus should not + * be used anymore. If you need blocking semantics on data input consider + * `write` with a callback instead. + * + * @deprecated Unreliable, will be removed soon. + */ + public writeSync(data: string | Uint8Array, maxSubsequentCalls?: number): void { + if (this._logService.logLevel <= LogLevelEnum.WARN && !hasWriteSyncWarnHappened) { + this._logService.warn('writeSync is unreliable and will be removed soon.'); + hasWriteSyncWarnHappened = true; + } + this._writeBuffer.writeSync(data, maxSubsequentCalls); + } + + public resize(x: number, y: number): void { + if (isNaN(x) || isNaN(y)) { + return; + } + + x = Math.max(x, MINIMUM_COLS); + y = Math.max(y, MINIMUM_ROWS); + + this._bufferService.resize(x, y); + } + + /** + * Scroll the terminal down 1 row, creating a blank line. + * @param isWrapped Whether the new line is wrapped from the previous line. + */ + public scroll(eraseAttr: IAttributeData, isWrapped: boolean = false): void { + this._bufferService.scroll(eraseAttr, isWrapped); + } + + /** + * Scroll the display of the terminal + * @param disp The number of lines to scroll down (negative scroll up). + * @param suppressScrollEvent Don't emit the scroll event as scrollLines. This is used + * to avoid unwanted events being handled by the viewport when the event was triggered from the + * viewport originally. + */ + public scrollLines(disp: number, suppressScrollEvent?: boolean, source?: ScrollSource): void { + this._bufferService.scrollLines(disp, suppressScrollEvent, source); + } + + /** + * Scroll the display of the terminal by a number of pages. + * @param pageCount The number of pages to scroll (negative scrolls up). + */ + public scrollPages(pageCount: number): void { + this._bufferService.scrollPages(pageCount); + } + + /** + * Scrolls the display of the terminal to the top. + */ + public scrollToTop(): void { + this._bufferService.scrollToTop(); + } + + /** + * Scrolls the display of the terminal to the bottom. + */ + public scrollToBottom(): void { + this._bufferService.scrollToBottom(); + } + + public scrollToLine(line: number): void { + this._bufferService.scrollToLine(line); + } + + /** Add handler for ESC escape sequence. See xterm.d.ts for details. */ + public registerEscHandler(id: IFunctionIdentifier, callback: () => boolean | Promise<boolean>): IDisposable { + return this._inputHandler.registerEscHandler(id, callback); + } + + /** Add handler for DCS escape sequence. See xterm.d.ts for details. */ + public registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean | Promise<boolean>): IDisposable { + return this._inputHandler.registerDcsHandler(id, callback); + } + + /** Add handler for CSI escape sequence. See xterm.d.ts for details. */ + public registerCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean | Promise<boolean>): IDisposable { + return this._inputHandler.registerCsiHandler(id, callback); + } + + /** Add handler for OSC escape sequence. See xterm.d.ts for details. */ + public registerOscHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable { + return this._inputHandler.registerOscHandler(ident, callback); + } + + protected _setup(): void { + if (this.optionsService.rawOptions.windowsMode) { + this._enableWindowsMode(); + } + } + + public reset(): void { + this._inputHandler.reset(); + this._bufferService.reset(); + this._charsetService.reset(); + this.coreService.reset(); + this.coreMouseService.reset(); + } + + protected _updateOptions(key: string): void { + // TODO: These listeners should be owned by individual components + switch (key) { + case 'scrollback': + this.buffers.resize(this.cols, this.rows); + break; + case 'windowsMode': + if (this.optionsService.rawOptions.windowsMode) { + this._enableWindowsMode(); + } else { + this._windowsMode?.dispose(); + this._windowsMode = undefined; + } + break; + } + } + + protected _enableWindowsMode(): void { + if (!this._windowsMode) { + const disposables: IDisposable[] = []; + disposables.push(this.onLineFeed(updateWindowsModeWrappedState.bind(null, this._bufferService))); + disposables.push(this.registerCsiHandler({ final: 'H' }, () => { + updateWindowsModeWrappedState(this._bufferService); + return false; + })); + this._windowsMode = { + dispose: () => { + for (const d of disposables) { + d.dispose(); + } + } + }; + } + } +} diff --git a/node_modules/xterm/src/common/EventEmitter.ts b/node_modules/xterm/src/common/EventEmitter.ts new file mode 100644 index 0000000..4684809 --- /dev/null +++ b/node_modules/xterm/src/common/EventEmitter.ts @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IDisposable } from 'common/Types'; + +interface IListener<T, U = void> { + (arg1: T, arg2: U): void; +} + +export interface IEvent<T, U = void> { + (listener: (arg1: T, arg2: U) => any): IDisposable; +} + +export interface IEventEmitter<T, U = void> { + event: IEvent<T, U>; + fire(arg1: T, arg2: U): void; + dispose(): void; +} + +export class EventEmitter<T, U = void> implements IEventEmitter<T, U> { + private _listeners: IListener<T, U>[] = []; + private _event?: IEvent<T, U>; + private _disposed: boolean = false; + + public get event(): IEvent<T, U> { + if (!this._event) { + this._event = (listener: (arg1: T, arg2: U) => any) => { + this._listeners.push(listener); + const disposable = { + dispose: () => { + if (!this._disposed) { + for (let i = 0; i < this._listeners.length; i++) { + if (this._listeners[i] === listener) { + this._listeners.splice(i, 1); + return; + } + } + } + } + }; + return disposable; + }; + } + return this._event; + } + + public fire(arg1: T, arg2: U): void { + const queue: IListener<T, U>[] = []; + for (let i = 0; i < this._listeners.length; i++) { + queue.push(this._listeners[i]); + } + for (let i = 0; i < queue.length; i++) { + queue[i].call(undefined, arg1, arg2); + } + } + + public dispose(): void { + if (this._listeners) { + this._listeners.length = 0; + } + this._disposed = true; + } +} + +export function forwardEvent<T>(from: IEvent<T>, to: IEventEmitter<T>): IDisposable { + return from(e => to.fire(e)); +} diff --git a/node_modules/xterm/src/common/InputHandler.ts b/node_modules/xterm/src/common/InputHandler.ts new file mode 100644 index 0000000..68660f8 --- /dev/null +++ b/node_modules/xterm/src/common/InputHandler.ts @@ -0,0 +1,3229 @@ +/** + * Copyright (c) 2014 The xterm.js authors. All rights reserved. + * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License) + * @license MIT + */ + +import { IInputHandler, IAttributeData, IDisposable, IWindowOptions, IColorEvent, IParseStack, ColorIndex, ColorRequestType } from 'common/Types'; +import { C0, C1 } from 'common/data/EscapeSequences'; +import { CHARSETS, DEFAULT_CHARSET } from 'common/data/Charsets'; +import { EscapeSequenceParser } from 'common/parser/EscapeSequenceParser'; +import { Disposable } from 'common/Lifecycle'; +import { concat } from 'common/TypedArrayUtils'; +import { StringToUtf32, stringFromCodePoint, utf32ToString, Utf8ToUtf32 } from 'common/input/TextDecoder'; +import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; +import { EventEmitter, IEvent } from 'common/EventEmitter'; +import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IFunctionIdentifier } from 'common/parser/Types'; +import { NULL_CELL_CODE, NULL_CELL_WIDTH, Attributes, FgFlags, BgFlags, Content, UnderlineStyle } from 'common/buffer/Constants'; +import { CellData } from 'common/buffer/CellData'; +import { AttributeData } from 'common/buffer/AttributeData'; +import { ICoreService, IBufferService, IOptionsService, ILogService, IDirtyRowService, ICoreMouseService, ICharsetService, IUnicodeService, LogLevelEnum } from 'common/services/Services'; +import { OscHandler } from 'common/parser/OscParser'; +import { DcsHandler } from 'common/parser/DcsParser'; +import { IBuffer } from 'common/buffer/Types'; +import { parseColor } from 'common/input/XParseColor'; + +/** + * Map collect to glevel. Used in `selectCharset`. + */ +const GLEVEL: { [key: string]: number } = { '(': 0, ')': 1, '*': 2, '+': 3, '-': 1, '.': 2 }; + +/** + * VT commands done by the parser - FIXME: move this to the parser? + */ +// @vt: #Y ESC CSI "Control Sequence Introducer" "ESC [" "Start of a CSI sequence." +// @vt: #Y ESC OSC "Operating System Command" "ESC ]" "Start of an OSC sequence." +// @vt: #Y ESC DCS "Device Control String" "ESC P" "Start of a DCS sequence." +// @vt: #Y ESC ST "String Terminator" "ESC \" "Terminator used for string type sequences." +// @vt: #Y ESC PM "Privacy Message" "ESC ^" "Start of a privacy message." +// @vt: #Y ESC APC "Application Program Command" "ESC _" "Start of an APC sequence." +// @vt: #Y C1 CSI "Control Sequence Introducer" "\x9B" "Start of a CSI sequence." +// @vt: #Y C1 OSC "Operating System Command" "\x9D" "Start of an OSC sequence." +// @vt: #Y C1 DCS "Device Control String" "\x90" "Start of a DCS sequence." +// @vt: #Y C1 ST "String Terminator" "\x9C" "Terminator used for string type sequences." +// @vt: #Y C1 PM "Privacy Message" "\x9E" "Start of a privacy message." +// @vt: #Y C1 APC "Application Program Command" "\x9F" "Start of an APC sequence." +// @vt: #Y C0 NUL "Null" "\0, \x00" "NUL is ignored." +// @vt: #Y C0 ESC "Escape" "\e, \x1B" "Start of a sequence. Cancels any other sequence." + +/** + * Document common VT features here that are currently unsupported + */ +// @vt: #N DCS SIXEL "SIXEL Graphics" "DCS Ps ; Ps ; Ps ; q Pt ST" "Draw SIXEL image starting at cursor position." +// @vt: #N OSC 1 "Set Icon Name" "OSC 1 ; Pt BEL" "Set icon name." + +/** + * Max length of the UTF32 input buffer. Real memory consumption is 4 times higher. + */ +const MAX_PARSEBUFFER_LENGTH = 131072; + +/** + * Limit length of title and icon name stacks. + */ +const STACK_LIMIT = 10; + +// map params to window option +function paramToWindowOption(n: number, opts: IWindowOptions): boolean { + if (n > 24) { + return opts.setWinLines || false; + } + switch (n) { + case 1: return !!opts.restoreWin; + case 2: return !!opts.minimizeWin; + case 3: return !!opts.setWinPosition; + case 4: return !!opts.setWinSizePixels; + case 5: return !!opts.raiseWin; + case 6: return !!opts.lowerWin; + case 7: return !!opts.refreshWin; + case 8: return !!opts.setWinSizeChars; + case 9: return !!opts.maximizeWin; + case 10: return !!opts.fullscreenWin; + case 11: return !!opts.getWinState; + case 13: return !!opts.getWinPosition; + case 14: return !!opts.getWinSizePixels; + case 15: return !!opts.getScreenSizePixels; + case 16: return !!opts.getCellSizePixels; + case 18: return !!opts.getWinSizeChars; + case 19: return !!opts.getScreenSizeChars; + case 20: return !!opts.getIconTitle; + case 21: return !!opts.getWinTitle; + case 22: return !!opts.pushTitle; + case 23: return !!opts.popTitle; + case 24: return !!opts.setWinLines; + } + return false; +} + +export enum WindowsOptionsReportType { + GET_WIN_SIZE_PIXELS = 0, + GET_CELL_SIZE_PIXELS = 1 +} + +// create a warning log if an async handler takes longer than the limit (in ms) +const SLOW_ASYNC_LIMIT = 5000; + +/** + * DCS subparser implementations + */ + +/** + * DCS $ q Pt ST + * DECRQSS (https://vt100.net/docs/vt510-rm/DECRQSS.html) + * Request Status String (DECRQSS), VT420 and up. + * Response: DECRPSS (https://vt100.net/docs/vt510-rm/DECRPSS.html) + * + * @vt: #P[See limited support below.] DCS DECRQSS "Request Selection or Setting" "DCS $ q Pt ST" "Request several terminal settings." + * Response is in the form `ESC P 1 $ r Pt ST` for valid requests, where `Pt` contains the corresponding CSI string, + * `ESC P 0 ST` for invalid requests. + * + * Supported requests and responses: + * + * | Type | Request | Response (`Pt`) | + * | -------------------------------- | ----------------- | ----------------------------------------------------- | + * | Graphic Rendition (SGR) | `DCS $ q m ST` | always reporting `0m` (currently broken) | + * | Top and Bottom Margins (DECSTBM) | `DCS $ q r ST` | `Ps ; Ps r` | + * | Cursor Style (DECSCUSR) | `DCS $ q SP q ST` | `Ps SP q` | + * | Protection Attribute (DECSCA) | `DCS $ q " q ST` | always reporting `0 " q` (DECSCA is unsupported) | + * | Conformance Level (DECSCL) | `DCS $ q " p ST` | always reporting `61 ; 1 " p` (DECSCL is unsupported) | + * + * + * TODO: + * - fix SGR report + * - either implement DECSCA or remove the report + * - either check which conformance is better suited or remove the report completely + * --> we are currently a mixture of all up to VT400 but dont follow anyone strictly + */ +class DECRQSS implements IDcsHandler { + private _data: Uint32Array = new Uint32Array(0); + + constructor( + private _bufferService: IBufferService, + private _coreService: ICoreService, + private _logService: ILogService, + private _optionsService: IOptionsService + ) { } + + public hook(params: IParams): void { + this._data = new Uint32Array(0); + } + + public put(data: Uint32Array, start: number, end: number): void { + this._data = concat(this._data, data.subarray(start, end)); + } + + public unhook(success: boolean): boolean { + if (!success) { + this._data = new Uint32Array(0); + return true; + } + const data = utf32ToString(this._data); + this._data = new Uint32Array(0); + switch (data) { + // valid: DCS 1 $ r Pt ST (xterm) + case '"q': // DECSCA + this._coreService.triggerDataEvent(`${C0.ESC}P1$r0"q${C0.ESC}\\`); + break; + case '"p': // DECSCL + this._coreService.triggerDataEvent(`${C0.ESC}P1$r61;1"p${C0.ESC}\\`); + break; + case 'r': // DECSTBM + const pt = '' + (this._bufferService.buffer.scrollTop + 1) + + ';' + (this._bufferService.buffer.scrollBottom + 1) + 'r'; + this._coreService.triggerDataEvent(`${C0.ESC}P1$r${pt}${C0.ESC}\\`); + break; + case 'm': // SGR + // TODO: report real settings instead of 0m + this._coreService.triggerDataEvent(`${C0.ESC}P1$r0m${C0.ESC}\\`); + break; + case ' q': // DECSCUSR + const STYLES: { [key: string]: number } = { 'block': 2, 'underline': 4, 'bar': 6 }; + let style = STYLES[this._optionsService.rawOptions.cursorStyle]; + style -= this._optionsService.rawOptions.cursorBlink ? 1 : 0; + this._coreService.triggerDataEvent(`${C0.ESC}P1$r${style} q${C0.ESC}\\`); + break; + default: + // invalid: DCS 0 $ r Pt ST (xterm) + this._logService.debug('Unknown DCS $q %s', data); + this._coreService.triggerDataEvent(`${C0.ESC}P0$r${C0.ESC}\\`); + } + return true; + } +} + +/** + * DCS Ps; Ps| Pt ST + * DECUDK (https://vt100.net/docs/vt510-rm/DECUDK.html) + * not supported + * + * @vt: #N DCS DECUDK "User Defined Keys" "DCS Ps ; Ps | Pt ST" "Definitions for user-defined keys." + */ + +/** + * DCS + q Pt ST (xterm) + * Request Terminfo String + * not implemented + * + * @vt: #N DCS XTGETTCAP "Request Terminfo String" "DCS + q Pt ST" "Request Terminfo String." + */ + +/** + * DCS + p Pt ST (xterm) + * Set Terminfo Data + * not supported + * + * @vt: #N DCS XTSETTCAP "Set Terminfo Data" "DCS + p Pt ST" "Set Terminfo Data." + */ + + + +/** + * The terminal's standard implementation of IInputHandler, this handles all + * input from the Parser. + * + * Refer to http://invisible-island.net/xterm/ctlseqs/ctlseqs.html to understand + * each function's header comment. + */ +export class InputHandler extends Disposable implements IInputHandler { + private _parseBuffer: Uint32Array = new Uint32Array(4096); + private _stringDecoder: StringToUtf32 = new StringToUtf32(); + private _utf8Decoder: Utf8ToUtf32 = new Utf8ToUtf32(); + private _workCell: CellData = new CellData(); + private _windowTitle = ''; + private _iconName = ''; + protected _windowTitleStack: string[] = []; + protected _iconNameStack: string[] = []; + + private _curAttrData: IAttributeData = DEFAULT_ATTR_DATA.clone(); + private _eraseAttrDataInternal: IAttributeData = DEFAULT_ATTR_DATA.clone(); + + private _activeBuffer: IBuffer; + + private _onRequestBell = new EventEmitter<void>(); + public get onRequestBell(): IEvent<void> { return this._onRequestBell.event; } + private _onRequestRefreshRows = new EventEmitter<number, number>(); + public get onRequestRefreshRows(): IEvent<number, number> { return this._onRequestRefreshRows.event; } + private _onRequestReset = new EventEmitter<void>(); + public get onRequestReset(): IEvent<void> { return this._onRequestReset.event; } + private _onRequestSendFocus = new EventEmitter<void>(); + public get onRequestSendFocus(): IEvent<void> { return this._onRequestSendFocus.event; } + private _onRequestSyncScrollBar = new EventEmitter<void>(); + public get onRequestSyncScrollBar(): IEvent<void> { return this._onRequestSyncScrollBar.event; } + private _onRequestWindowsOptionsReport = new EventEmitter<WindowsOptionsReportType>(); + public get onRequestWindowsOptionsReport(): IEvent<WindowsOptionsReportType> { return this._onRequestWindowsOptionsReport.event; } + + private _onA11yChar = new EventEmitter<string>(); + public get onA11yChar(): IEvent<string> { return this._onA11yChar.event; } + private _onA11yTab = new EventEmitter<number>(); + public get onA11yTab(): IEvent<number> { return this._onA11yTab.event; } + private _onCursorMove = new EventEmitter<void>(); + public get onCursorMove(): IEvent<void> { return this._onCursorMove.event; } + private _onLineFeed = new EventEmitter<void>(); + public get onLineFeed(): IEvent<void> { return this._onLineFeed.event; } + private _onScroll = new EventEmitter<number>(); + public get onScroll(): IEvent<number> { return this._onScroll.event; } + private _onTitleChange = new EventEmitter<string>(); + public get onTitleChange(): IEvent<string> { return this._onTitleChange.event; } + private _onColor = new EventEmitter<IColorEvent>(); + public get onColor(): IEvent<IColorEvent> { return this._onColor.event; } + + private _parseStack: IParseStack = { + paused: false, + cursorStartX: 0, + cursorStartY: 0, + decodedLength: 0, + position: 0 + }; + + constructor( + private readonly _bufferService: IBufferService, + private readonly _charsetService: ICharsetService, + private readonly _coreService: ICoreService, + private readonly _dirtyRowService: IDirtyRowService, + private readonly _logService: ILogService, + private readonly _optionsService: IOptionsService, + private readonly _coreMouseService: ICoreMouseService, + private readonly _unicodeService: IUnicodeService, + private readonly _parser: IEscapeSequenceParser = new EscapeSequenceParser() + ) { + super(); + this.register(this._parser); + + // Track properties used in performance critical code manually to avoid using slow getters + this._activeBuffer = this._bufferService.buffer; + this.register(this._bufferService.buffers.onBufferActivate(e => this._activeBuffer = e.activeBuffer)); + + /** + * custom fallback handlers + */ + this._parser.setCsiHandlerFallback((ident, params) => { + this._logService.debug('Unknown CSI code: ', { identifier: this._parser.identToString(ident), params: params.toArray() }); + }); + this._parser.setEscHandlerFallback(ident => { + this._logService.debug('Unknown ESC code: ', { identifier: this._parser.identToString(ident) }); + }); + this._parser.setExecuteHandlerFallback(code => { + this._logService.debug('Unknown EXECUTE code: ', { code }); + }); + this._parser.setOscHandlerFallback((identifier, action, data) => { + this._logService.debug('Unknown OSC code: ', { identifier, action, data }); + }); + this._parser.setDcsHandlerFallback((ident, action, payload) => { + if (action === 'HOOK') { + payload = payload.toArray(); + } + this._logService.debug('Unknown DCS code: ', { identifier: this._parser.identToString(ident), action, payload }); + }); + + /** + * print handler + */ + this._parser.setPrintHandler((data, start, end) => this.print(data, start, end)); + + /** + * CSI handler + */ + this._parser.registerCsiHandler({ final: '@' }, params => this.insertChars(params)); + this._parser.registerCsiHandler({ intermediates: ' ', final: '@' }, params => this.scrollLeft(params)); + this._parser.registerCsiHandler({ final: 'A' }, params => this.cursorUp(params)); + this._parser.registerCsiHandler({ intermediates: ' ', final: 'A' }, params => this.scrollRight(params)); + this._parser.registerCsiHandler({ final: 'B' }, params => this.cursorDown(params)); + this._parser.registerCsiHandler({ final: 'C' }, params => this.cursorForward(params)); + this._parser.registerCsiHandler({ final: 'D' }, params => this.cursorBackward(params)); + this._parser.registerCsiHandler({ final: 'E' }, params => this.cursorNextLine(params)); + this._parser.registerCsiHandler({ final: 'F' }, params => this.cursorPrecedingLine(params)); + this._parser.registerCsiHandler({ final: 'G' }, params => this.cursorCharAbsolute(params)); + this._parser.registerCsiHandler({ final: 'H' }, params => this.cursorPosition(params)); + this._parser.registerCsiHandler({ final: 'I' }, params => this.cursorForwardTab(params)); + this._parser.registerCsiHandler({ final: 'J' }, params => this.eraseInDisplay(params)); + this._parser.registerCsiHandler({ prefix: '?', final: 'J' }, params => this.eraseInDisplay(params)); + this._parser.registerCsiHandler({ final: 'K' }, params => this.eraseInLine(params)); + this._parser.registerCsiHandler({ prefix: '?', final: 'K' }, params => this.eraseInLine(params)); + this._parser.registerCsiHandler({ final: 'L' }, params => this.insertLines(params)); + this._parser.registerCsiHandler({ final: 'M' }, params => this.deleteLines(params)); + this._parser.registerCsiHandler({ final: 'P' }, params => this.deleteChars(params)); + this._parser.registerCsiHandler({ final: 'S' }, params => this.scrollUp(params)); + this._parser.registerCsiHandler({ final: 'T' }, params => this.scrollDown(params)); + this._parser.registerCsiHandler({ final: 'X' }, params => this.eraseChars(params)); + this._parser.registerCsiHandler({ final: 'Z' }, params => this.cursorBackwardTab(params)); + this._parser.registerCsiHandler({ final: '`' }, params => this.charPosAbsolute(params)); + this._parser.registerCsiHandler({ final: 'a' }, params => this.hPositionRelative(params)); + this._parser.registerCsiHandler({ final: 'b' }, params => this.repeatPrecedingCharacter(params)); + this._parser.registerCsiHandler({ final: 'c' }, params => this.sendDeviceAttributesPrimary(params)); + this._parser.registerCsiHandler({ prefix: '>', final: 'c' }, params => this.sendDeviceAttributesSecondary(params)); + this._parser.registerCsiHandler({ final: 'd' }, params => this.linePosAbsolute(params)); + this._parser.registerCsiHandler({ final: 'e' }, params => this.vPositionRelative(params)); + this._parser.registerCsiHandler({ final: 'f' }, params => this.hVPosition(params)); + this._parser.registerCsiHandler({ final: 'g' }, params => this.tabClear(params)); + this._parser.registerCsiHandler({ final: 'h' }, params => this.setMode(params)); + this._parser.registerCsiHandler({ prefix: '?', final: 'h' }, params => this.setModePrivate(params)); + this._parser.registerCsiHandler({ final: 'l' }, params => this.resetMode(params)); + this._parser.registerCsiHandler({ prefix: '?', final: 'l' }, params => this.resetModePrivate(params)); + this._parser.registerCsiHandler({ final: 'm' }, params => this.charAttributes(params)); + this._parser.registerCsiHandler({ final: 'n' }, params => this.deviceStatus(params)); + this._parser.registerCsiHandler({ prefix: '?', final: 'n' }, params => this.deviceStatusPrivate(params)); + this._parser.registerCsiHandler({ intermediates: '!', final: 'p' }, params => this.softReset(params)); + this._parser.registerCsiHandler({ intermediates: ' ', final: 'q' }, params => this.setCursorStyle(params)); + this._parser.registerCsiHandler({ final: 'r' }, params => this.setScrollRegion(params)); + this._parser.registerCsiHandler({ final: 's' }, params => this.saveCursor(params)); + this._parser.registerCsiHandler({ final: 't' }, params => this.windowOptions(params)); + this._parser.registerCsiHandler({ final: 'u' }, params => this.restoreCursor(params)); + this._parser.registerCsiHandler({ intermediates: '\'', final: '}' }, params => this.insertColumns(params)); + this._parser.registerCsiHandler({ intermediates: '\'', final: '~' }, params => this.deleteColumns(params)); + + /** + * execute handler + */ + this._parser.setExecuteHandler(C0.BEL, () => this.bell()); + this._parser.setExecuteHandler(C0.LF, () => this.lineFeed()); + this._parser.setExecuteHandler(C0.VT, () => this.lineFeed()); + this._parser.setExecuteHandler(C0.FF, () => this.lineFeed()); + this._parser.setExecuteHandler(C0.CR, () => this.carriageReturn()); + this._parser.setExecuteHandler(C0.BS, () => this.backspace()); + this._parser.setExecuteHandler(C0.HT, () => this.tab()); + this._parser.setExecuteHandler(C0.SO, () => this.shiftOut()); + this._parser.setExecuteHandler(C0.SI, () => this.shiftIn()); + // FIXME: What do to with missing? Old code just added those to print. + + this._parser.setExecuteHandler(C1.IND, () => this.index()); + this._parser.setExecuteHandler(C1.NEL, () => this.nextLine()); + this._parser.setExecuteHandler(C1.HTS, () => this.tabSet()); + + /** + * OSC handler + */ + // 0 - icon name + title + this._parser.registerOscHandler(0, new OscHandler(data => { this.setTitle(data); this.setIconName(data); return true; })); + // 1 - icon name + this._parser.registerOscHandler(1, new OscHandler(data => this.setIconName(data))); + // 2 - title + this._parser.registerOscHandler(2, new OscHandler(data => this.setTitle(data))); + // 3 - set property X in the form "prop=value" + // 4 - Change Color Number + this._parser.registerOscHandler(4, new OscHandler(data => this.setOrReportIndexedColor(data))); + // 5 - Change Special Color Number + // 6 - Enable/disable Special Color Number c + // 7 - current directory? (not in xterm spec, see https://gitlab.com/gnachman/iterm2/issues/3939) + // 10 - Change VT100 text foreground color to Pt. + this._parser.registerOscHandler(10, new OscHandler(data => this.setOrReportFgColor(data))); + // 11 - Change VT100 text background color to Pt. + this._parser.registerOscHandler(11, new OscHandler(data => this.setOrReportBgColor(data))); + // 12 - Change text cursor color to Pt. + this._parser.registerOscHandler(12, new OscHandler(data => this.setOrReportCursorColor(data))); + // 13 - Change mouse foreground color to Pt. + // 14 - Change mouse background color to Pt. + // 15 - Change Tektronix foreground color to Pt. + // 16 - Change Tektronix background color to Pt. + // 17 - Change highlight background color to Pt. + // 18 - Change Tektronix cursor color to Pt. + // 19 - Change highlight foreground color to Pt. + // 46 - Change Log File to Pt. + // 50 - Set Font to Pt. + // 51 - reserved for Emacs shell. + // 52 - Manipulate Selection Data. + // 104 ; c - Reset Color Number c. + this._parser.registerOscHandler(104, new OscHandler(data => this.restoreIndexedColor(data))); + // 105 ; c - Reset Special Color Number c. + // 106 ; c; f - Enable/disable Special Color Number c. + // 110 - Reset VT100 text foreground color. + this._parser.registerOscHandler(110, new OscHandler(data => this.restoreFgColor(data))); + // 111 - Reset VT100 text background color. + this._parser.registerOscHandler(111, new OscHandler(data => this.restoreBgColor(data))); + // 112 - Reset text cursor color. + this._parser.registerOscHandler(112, new OscHandler(data => this.restoreCursorColor(data))); + // 113 - Reset mouse foreground color. + // 114 - Reset mouse background color. + // 115 - Reset Tektronix foreground color. + // 116 - Reset Tektronix background color. + // 117 - Reset highlight color. + // 118 - Reset Tektronix cursor color. + // 119 - Reset highlight foreground color. + + /** + * ESC handlers + */ + this._parser.registerEscHandler({ final: '7' }, () => this.saveCursor()); + this._parser.registerEscHandler({ final: '8' }, () => this.restoreCursor()); + this._parser.registerEscHandler({ final: 'D' }, () => this.index()); + this._parser.registerEscHandler({ final: 'E' }, () => this.nextLine()); + this._parser.registerEscHandler({ final: 'H' }, () => this.tabSet()); + this._parser.registerEscHandler({ final: 'M' }, () => this.reverseIndex()); + this._parser.registerEscHandler({ final: '=' }, () => this.keypadApplicationMode()); + this._parser.registerEscHandler({ final: '>' }, () => this.keypadNumericMode()); + this._parser.registerEscHandler({ final: 'c' }, () => this.fullReset()); + this._parser.registerEscHandler({ final: 'n' }, () => this.setgLevel(2)); + this._parser.registerEscHandler({ final: 'o' }, () => this.setgLevel(3)); + this._parser.registerEscHandler({ final: '|' }, () => this.setgLevel(3)); + this._parser.registerEscHandler({ final: '}' }, () => this.setgLevel(2)); + this._parser.registerEscHandler({ final: '~' }, () => this.setgLevel(1)); + this._parser.registerEscHandler({ intermediates: '%', final: '@' }, () => this.selectDefaultCharset()); + this._parser.registerEscHandler({ intermediates: '%', final: 'G' }, () => this.selectDefaultCharset()); + for (const flag in CHARSETS) { + this._parser.registerEscHandler({ intermediates: '(', final: flag }, () => this.selectCharset('(' + flag)); + this._parser.registerEscHandler({ intermediates: ')', final: flag }, () => this.selectCharset(')' + flag)); + this._parser.registerEscHandler({ intermediates: '*', final: flag }, () => this.selectCharset('*' + flag)); + this._parser.registerEscHandler({ intermediates: '+', final: flag }, () => this.selectCharset('+' + flag)); + this._parser.registerEscHandler({ intermediates: '-', final: flag }, () => this.selectCharset('-' + flag)); + this._parser.registerEscHandler({ intermediates: '.', final: flag }, () => this.selectCharset('.' + flag)); + this._parser.registerEscHandler({ intermediates: '/', final: flag }, () => this.selectCharset('/' + flag)); // TODO: supported? + } + this._parser.registerEscHandler({ intermediates: '#', final: '8' }, () => this.screenAlignmentPattern()); + + /** + * error handler + */ + this._parser.setErrorHandler((state: IParsingState) => { + this._logService.error('Parsing error: ', state); + return state; + }); + + /** + * DCS handler + */ + this._parser.registerDcsHandler({ intermediates: '$', final: 'q' }, new DECRQSS(this._bufferService, this._coreService, this._logService, this._optionsService)); + } + + public dispose(): void { + super.dispose(); + } + + /** + * Async parse support. + */ + private _preserveStack(cursorStartX: number, cursorStartY: number, decodedLength: number, position: number): void { + this._parseStack.paused = true; + this._parseStack.cursorStartX = cursorStartX; + this._parseStack.cursorStartY = cursorStartY; + this._parseStack.decodedLength = decodedLength; + this._parseStack.position = position; + } + + private _logSlowResolvingAsync(p: Promise<boolean>): void { + // log a limited warning about an async handler taking too long + if (this._logService.logLevel <= LogLevelEnum.WARN) { + Promise.race([p, new Promise((res, rej) => setTimeout(() => rej('#SLOW_TIMEOUT'), SLOW_ASYNC_LIMIT))]) + .catch(err => { + if (err !== '#SLOW_TIMEOUT') { + throw err; + } + console.warn(`async parser handler taking longer than ${SLOW_ASYNC_LIMIT} ms`); + }); + } + } + + /** + * Parse call with async handler support. + * + * Whether the stack state got preserved for the next call, is indicated by the return value: + * - undefined (void): + * all handlers were sync, no stack save, continue normally with next chunk + * - Promise\<boolean\>: + * execution stopped at async handler, stack saved, continue with + * same chunk and the promise resolve value as `promiseResult` until the method returns `undefined` + * + * Note: This method should only be called by `Terminal.write` to ensure correct execution order and + * proper continuation of async parser handlers. + */ + public parse(data: string | Uint8Array, promiseResult?: boolean): void | Promise<boolean> { + let result: void | Promise<boolean>; + let cursorStartX = this._activeBuffer.x; + let cursorStartY = this._activeBuffer.y; + let start = 0; + const wasPaused = this._parseStack.paused; + + if (wasPaused) { + // assumption: _parseBuffer never mutates between async calls + if (result = this._parser.parse(this._parseBuffer, this._parseStack.decodedLength, promiseResult)) { + this._logSlowResolvingAsync(result); + return result; + } + cursorStartX = this._parseStack.cursorStartX; + cursorStartY = this._parseStack.cursorStartY; + this._parseStack.paused = false; + if (data.length > MAX_PARSEBUFFER_LENGTH) { + start = this._parseStack.position + MAX_PARSEBUFFER_LENGTH; + } + } + + // Log debug data, the log level gate is to prevent extra work in this hot path + if (this._logService.logLevel <= LogLevelEnum.DEBUG) { + this._logService.debug(`parsing data${typeof data === 'string' ? ` "${data}"` : ''}`, typeof data === 'string' + ? data.split('').map(e => e.charCodeAt(0)) + : data + ); + } + + // resize input buffer if needed + if (this._parseBuffer.length < data.length) { + if (this._parseBuffer.length < MAX_PARSEBUFFER_LENGTH) { + this._parseBuffer = new Uint32Array(Math.min(data.length, MAX_PARSEBUFFER_LENGTH)); + } + } + + // Clear the dirty row service so we know which lines changed as a result of parsing + // Important: do not clear between async calls, otherwise we lost pending update information. + if (!wasPaused) { + this._dirtyRowService.clearRange(); + } + + // process big data in smaller chunks + if (data.length > MAX_PARSEBUFFER_LENGTH) { + for (let i = start; i < data.length; i += MAX_PARSEBUFFER_LENGTH) { + const end = i + MAX_PARSEBUFFER_LENGTH < data.length ? i + MAX_PARSEBUFFER_LENGTH : data.length; + const len = (typeof data === 'string') + ? this._stringDecoder.decode(data.substring(i, end), this._parseBuffer) + : this._utf8Decoder.decode(data.subarray(i, end), this._parseBuffer); + if (result = this._parser.parse(this._parseBuffer, len)) { + this._preserveStack(cursorStartX, cursorStartY, len, i); + this._logSlowResolvingAsync(result); + return result; + } + } + } else { + if (!wasPaused) { + const len = (typeof data === 'string') + ? this._stringDecoder.decode(data, this._parseBuffer) + : this._utf8Decoder.decode(data, this._parseBuffer); + if (result = this._parser.parse(this._parseBuffer, len)) { + this._preserveStack(cursorStartX, cursorStartY, len, 0); + this._logSlowResolvingAsync(result); + return result; + } + } + } + + if (this._activeBuffer.x !== cursorStartX || this._activeBuffer.y !== cursorStartY) { + this._onCursorMove.fire(); + } + + // Refresh any dirty rows accumulated as part of parsing + this._onRequestRefreshRows.fire(this._dirtyRowService.start, this._dirtyRowService.end); + } + + public print(data: Uint32Array, start: number, end: number): void { + let code: number; + let chWidth: number; + const charset = this._charsetService.charset; + const screenReaderMode = this._optionsService.rawOptions.screenReaderMode; + const cols = this._bufferService.cols; + const wraparoundMode = this._coreService.decPrivateModes.wraparound; + const insertMode = this._coreService.modes.insertMode; + const curAttr = this._curAttrData; + let bufferRow = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!; + + this._dirtyRowService.markDirty(this._activeBuffer.y); + + // handle wide chars: reset start_cell-1 if we would overwrite the second cell of a wide char + if (this._activeBuffer.x && end - start > 0 && bufferRow.getWidth(this._activeBuffer.x - 1) === 2) { + bufferRow.setCellFromCodePoint(this._activeBuffer.x - 1, 0, 1, curAttr.fg, curAttr.bg, curAttr.extended); + } + + for (let pos = start; pos < end; ++pos) { + code = data[pos]; + + // calculate print space + // expensive call, therefore we save width in line buffer + chWidth = this._unicodeService.wcwidth(code); + + // get charset replacement character + // charset is only defined for ASCII, therefore we only + // search for an replacement char if code < 127 + if (code < 127 && charset) { + const ch = charset[String.fromCharCode(code)]; + if (ch) { + code = ch.charCodeAt(0); + } + } + + if (screenReaderMode) { + this._onA11yChar.fire(stringFromCodePoint(code)); + } + + // insert combining char at last cursor position + // this._activeBuffer.x should never be 0 for a combining char + // since they always follow a cell consuming char + // therefore we can test for this._activeBuffer.x to avoid overflow left + if (!chWidth && this._activeBuffer.x) { + if (!bufferRow.getWidth(this._activeBuffer.x - 1)) { + // found empty cell after fullwidth, need to go 2 cells back + // it is save to step 2 cells back here + // since an empty cell is only set by fullwidth chars + bufferRow.addCodepointToCell(this._activeBuffer.x - 2, code); + } else { + bufferRow.addCodepointToCell(this._activeBuffer.x - 1, code); + } + continue; + } + + // goto next line if ch would overflow + // NOTE: To avoid costly width checks here, + // the terminal does not allow a cols < 2. + if (this._activeBuffer.x + chWidth - 1 >= cols) { + // autowrap - DECAWM + // automatically wraps to the beginning of the next line + if (wraparoundMode) { + // clear left over cells to the right + while (this._activeBuffer.x < cols) { + bufferRow.setCellFromCodePoint(this._activeBuffer.x++, 0, 1, curAttr.fg, curAttr.bg, curAttr.extended); + } + this._activeBuffer.x = 0; + this._activeBuffer.y++; + if (this._activeBuffer.y === this._activeBuffer.scrollBottom + 1) { + this._activeBuffer.y--; + this._bufferService.scroll(this._eraseAttrData(), true); + } else { + if (this._activeBuffer.y >= this._bufferService.rows) { + this._activeBuffer.y = this._bufferService.rows - 1; + } + // The line already exists (eg. the initial viewport), mark it as a + // wrapped line + this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!.isWrapped = true; + } + // row changed, get it again + bufferRow = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!; + } else { + this._activeBuffer.x = cols - 1; + if (chWidth === 2) { + // FIXME: check for xterm behavior + // What to do here? We got a wide char that does not fit into last cell + continue; + } + } + } + + // insert mode: move characters to right + if (insertMode) { + // right shift cells according to the width + bufferRow.insertCells(this._activeBuffer.x, chWidth, this._activeBuffer.getNullCell(curAttr), curAttr); + // test last cell - since the last cell has only room for + // a halfwidth char any fullwidth shifted there is lost + // and will be set to empty cell + if (bufferRow.getWidth(cols - 1) === 2) { + bufferRow.setCellFromCodePoint(cols - 1, NULL_CELL_CODE, NULL_CELL_WIDTH, curAttr.fg, curAttr.bg, curAttr.extended); + } + } + + // write current char to buffer and advance cursor + bufferRow.setCellFromCodePoint(this._activeBuffer.x++, code, chWidth, curAttr.fg, curAttr.bg, curAttr.extended); + + // fullwidth char - also set next cell to placeholder stub and advance cursor + // for graphemes bigger than fullwidth we can simply loop to zero + // we already made sure above, that this._activeBuffer.x + chWidth will not overflow right + if (chWidth > 0) { + while (--chWidth) { + // other than a regular empty cell a cell following a wide char has no width + bufferRow.setCellFromCodePoint(this._activeBuffer.x++, 0, 0, curAttr.fg, curAttr.bg, curAttr.extended); + } + } + } + // store last char in Parser.precedingCodepoint for REP to work correctly + // This needs to check whether: + // - fullwidth + surrogates: reset + // - combining: only base char gets carried on (bug in xterm?) + if (end - start > 0) { + bufferRow.loadCell(this._activeBuffer.x - 1, this._workCell); + if (this._workCell.getWidth() === 2 || this._workCell.getCode() > 0xFFFF) { + this._parser.precedingCodepoint = 0; + } else if (this._workCell.isCombined()) { + this._parser.precedingCodepoint = this._workCell.getChars().charCodeAt(0); + } else { + this._parser.precedingCodepoint = this._workCell.content; + } + } + + // handle wide chars: reset cell to the right if it is second cell of a wide char + if (this._activeBuffer.x < cols && end - start > 0 && bufferRow.getWidth(this._activeBuffer.x) === 0 && !bufferRow.hasContent(this._activeBuffer.x)) { + bufferRow.setCellFromCodePoint(this._activeBuffer.x, 0, 1, curAttr.fg, curAttr.bg, curAttr.extended); + } + + this._dirtyRowService.markDirty(this._activeBuffer.y); + } + + /** + * Forward registerCsiHandler from parser. + */ + public registerCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean | Promise<boolean>): IDisposable { + if (id.final === 't' && !id.prefix && !id.intermediates) { + // security: always check whether window option is allowed + return this._parser.registerCsiHandler(id, params => { + if (!paramToWindowOption(params.params[0], this._optionsService.rawOptions.windowOptions)) { + return true; + } + return callback(params); + }); + } + return this._parser.registerCsiHandler(id, callback); + } + + /** + * Forward registerDcsHandler from parser. + */ + public registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean | Promise<boolean>): IDisposable { + return this._parser.registerDcsHandler(id, new DcsHandler(callback)); + } + + /** + * Forward registerEscHandler from parser. + */ + public registerEscHandler(id: IFunctionIdentifier, callback: () => boolean | Promise<boolean>): IDisposable { + return this._parser.registerEscHandler(id, callback); + } + + /** + * Forward registerOscHandler from parser. + */ + public registerOscHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable { + return this._parser.registerOscHandler(ident, new OscHandler(callback)); + } + + /** + * BEL + * Bell (Ctrl-G). + * + * @vt: #Y C0 BEL "Bell" "\a, \x07" "Ring the bell." + * The behavior of the bell is further customizable with `ITerminalOptions.bellStyle` + * and `ITerminalOptions.bellSound`. + */ + public bell(): boolean { + this._onRequestBell.fire(); + return true; + } + + /** + * LF + * Line Feed or New Line (NL). (LF is Ctrl-J). + * + * @vt: #Y C0 LF "Line Feed" "\n, \x0A" "Move the cursor one row down, scrolling if needed." + * Scrolling is restricted to scroll margins and will only happen on the bottom line. + * + * @vt: #Y C0 VT "Vertical Tabulation" "\v, \x0B" "Treated as LF." + * @vt: #Y C0 FF "Form Feed" "\f, \x0C" "Treated as LF." + */ + public lineFeed(): boolean { + this._dirtyRowService.markDirty(this._activeBuffer.y); + if (this._optionsService.rawOptions.convertEol) { + this._activeBuffer.x = 0; + } + this._activeBuffer.y++; + if (this._activeBuffer.y === this._activeBuffer.scrollBottom + 1) { + this._activeBuffer.y--; + this._bufferService.scroll(this._eraseAttrData()); + } else if (this._activeBuffer.y >= this._bufferService.rows) { + this._activeBuffer.y = this._bufferService.rows - 1; + } + // If the end of the line is hit, prevent this action from wrapping around to the next line. + if (this._activeBuffer.x >= this._bufferService.cols) { + this._activeBuffer.x--; + } + this._dirtyRowService.markDirty(this._activeBuffer.y); + + this._onLineFeed.fire(); + return true; + } + + /** + * CR + * Carriage Return (Ctrl-M). + * + * @vt: #Y C0 CR "Carriage Return" "\r, \x0D" "Move the cursor to the beginning of the row." + */ + public carriageReturn(): boolean { + this._activeBuffer.x = 0; + return true; + } + + /** + * BS + * Backspace (Ctrl-H). + * + * @vt: #Y C0 BS "Backspace" "\b, \x08" "Move the cursor one position to the left." + * By default it is not possible to move the cursor past the leftmost position. + * If `reverse wrap-around` (`CSI ? 45 h`) is set, a previous soft line wrap (DECAWM) + * can be undone with BS within the scroll margins. In that case the cursor will wrap back + * to the end of the previous row. Note that it is not possible to peek back into the scrollbuffer + * with the cursor, thus at the home position (top-leftmost cell) this has no effect. + */ + public backspace(): boolean { + // reverse wrap-around is disabled + if (!this._coreService.decPrivateModes.reverseWraparound) { + this._restrictCursor(); + if (this._activeBuffer.x > 0) { + this._activeBuffer.x--; + } + return true; + } + + // reverse wrap-around is enabled + // other than for normal operation mode, reverse wrap-around allows the cursor + // to be at x=cols to be able to address the last cell of a row by BS + this._restrictCursor(this._bufferService.cols); + + if (this._activeBuffer.x > 0) { + this._activeBuffer.x--; + } else { + /** + * reverse wrap-around handling: + * Our implementation deviates from xterm on purpose. Details: + * - only previous soft NLs can be reversed (isWrapped=true) + * - only works within scrollborders (top/bottom, left/right not yet supported) + * - cannot peek into scrollbuffer + * - any cursor movement sequence keeps working as expected + */ + if (this._activeBuffer.x === 0 + && this._activeBuffer.y > this._activeBuffer.scrollTop + && this._activeBuffer.y <= this._activeBuffer.scrollBottom + && this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)?.isWrapped) { + this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!.isWrapped = false; + this._activeBuffer.y--; + this._activeBuffer.x = this._bufferService.cols - 1; + // find last taken cell - last cell can have 3 different states: + // - hasContent(true) + hasWidth(1): narrow char - we are done + // - hasWidth(0): second part of wide char - we are done + // - hasContent(false) + hasWidth(1): empty cell due to early wrapping wide char, go one cell further back + const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!; + if (line.hasWidth(this._activeBuffer.x) && !line.hasContent(this._activeBuffer.x)) { + this._activeBuffer.x--; + // We do this only once, since width=1 + hasContent=false currently happens only once before + // early wrapping of a wide char. + // This needs to be fixed once we support graphemes taking more than 2 cells. + } + } + } + this._restrictCursor(); + return true; + } + + /** + * TAB + * Horizontal Tab (HT) (Ctrl-I). + * + * @vt: #Y C0 HT "Horizontal Tabulation" "\t, \x09" "Move the cursor to the next character tab stop." + */ + public tab(): boolean { + if (this._activeBuffer.x >= this._bufferService.cols) { + return true; + } + const originalX = this._activeBuffer.x; + this._activeBuffer.x = this._activeBuffer.nextStop(); + if (this._optionsService.rawOptions.screenReaderMode) { + this._onA11yTab.fire(this._activeBuffer.x - originalX); + } + return true; + } + + /** + * SO + * Shift Out (Ctrl-N) -> Switch to Alternate Character Set. This invokes the + * G1 character set. + * + * @vt: #P[Only limited ISO-2022 charset support.] C0 SO "Shift Out" "\x0E" "Switch to an alternative character set." + */ + public shiftOut(): boolean { + this._charsetService.setgLevel(1); + return true; + } + + /** + * SI + * Shift In (Ctrl-O) -> Switch to Standard Character Set. This invokes the G0 + * character set (the default). + * + * @vt: #Y C0 SI "Shift In" "\x0F" "Return to regular character set after Shift Out." + */ + public shiftIn(): boolean { + this._charsetService.setgLevel(0); + return true; + } + + /** + * Restrict cursor to viewport size / scroll margin (origin mode). + */ + private _restrictCursor(maxCol: number = this._bufferService.cols - 1): void { + this._activeBuffer.x = Math.min(maxCol, Math.max(0, this._activeBuffer.x)); + this._activeBuffer.y = this._coreService.decPrivateModes.origin + ? Math.min(this._activeBuffer.scrollBottom, Math.max(this._activeBuffer.scrollTop, this._activeBuffer.y)) + : Math.min(this._bufferService.rows - 1, Math.max(0, this._activeBuffer.y)); + this._dirtyRowService.markDirty(this._activeBuffer.y); + } + + /** + * Set absolute cursor position. + */ + private _setCursor(x: number, y: number): void { + this._dirtyRowService.markDirty(this._activeBuffer.y); + if (this._coreService.decPrivateModes.origin) { + this._activeBuffer.x = x; + this._activeBuffer.y = this._activeBuffer.scrollTop + y; + } else { + this._activeBuffer.x = x; + this._activeBuffer.y = y; + } + this._restrictCursor(); + this._dirtyRowService.markDirty(this._activeBuffer.y); + } + + /** + * Set relative cursor position. + */ + private _moveCursor(x: number, y: number): void { + // for relative changes we have to make sure we are within 0 .. cols/rows - 1 + // before calculating the new position + this._restrictCursor(); + this._setCursor(this._activeBuffer.x + x, this._activeBuffer.y + y); + } + + /** + * CSI Ps A + * Cursor Up Ps Times (default = 1) (CUU). + * + * @vt: #Y CSI CUU "Cursor Up" "CSI Ps A" "Move cursor `Ps` times up (default=1)." + * If the cursor would pass the top scroll margin, it will stop there. + */ + public cursorUp(params: IParams): boolean { + // stop at scrollTop + const diffToTop = this._activeBuffer.y - this._activeBuffer.scrollTop; + if (diffToTop >= 0) { + this._moveCursor(0, -Math.min(diffToTop, params.params[0] || 1)); + } else { + this._moveCursor(0, -(params.params[0] || 1)); + } + return true; + } + + /** + * CSI Ps B + * Cursor Down Ps Times (default = 1) (CUD). + * + * @vt: #Y CSI CUD "Cursor Down" "CSI Ps B" "Move cursor `Ps` times down (default=1)." + * If the cursor would pass the bottom scroll margin, it will stop there. + */ + public cursorDown(params: IParams): boolean { + // stop at scrollBottom + const diffToBottom = this._activeBuffer.scrollBottom - this._activeBuffer.y; + if (diffToBottom >= 0) { + this._moveCursor(0, Math.min(diffToBottom, params.params[0] || 1)); + } else { + this._moveCursor(0, params.params[0] || 1); + } + return true; + } + + /** + * CSI Ps C + * Cursor Forward Ps Times (default = 1) (CUF). + * + * @vt: #Y CSI CUF "Cursor Forward" "CSI Ps C" "Move cursor `Ps` times forward (default=1)." + */ + public cursorForward(params: IParams): boolean { + this._moveCursor(params.params[0] || 1, 0); + return true; + } + + /** + * CSI Ps D + * Cursor Backward Ps Times (default = 1) (CUB). + * + * @vt: #Y CSI CUB "Cursor Backward" "CSI Ps D" "Move cursor `Ps` times backward (default=1)." + */ + public cursorBackward(params: IParams): boolean { + this._moveCursor(-(params.params[0] || 1), 0); + return true; + } + + /** + * CSI Ps E + * Cursor Next Line Ps Times (default = 1) (CNL). + * Other than cursorDown (CUD) also set the cursor to first column. + * + * @vt: #Y CSI CNL "Cursor Next Line" "CSI Ps E" "Move cursor `Ps` times down (default=1) and to the first column." + * Same as CUD, additionally places the cursor at the first column. + */ + public cursorNextLine(params: IParams): boolean { + this.cursorDown(params); + this._activeBuffer.x = 0; + return true; + } + + /** + * CSI Ps F + * Cursor Previous Line Ps Times (default = 1) (CPL). + * Other than cursorUp (CUU) also set the cursor to first column. + * + * @vt: #Y CSI CPL "Cursor Backward" "CSI Ps F" "Move cursor `Ps` times up (default=1) and to the first column." + * Same as CUU, additionally places the cursor at the first column. + */ + public cursorPrecedingLine(params: IParams): boolean { + this.cursorUp(params); + this._activeBuffer.x = 0; + return true; + } + + /** + * CSI Ps G + * Cursor Character Absolute [column] (default = [row,1]) (CHA). + * + * @vt: #Y CSI CHA "Cursor Horizontal Absolute" "CSI Ps G" "Move cursor to `Ps`-th column of the active row (default=1)." + */ + public cursorCharAbsolute(params: IParams): boolean { + this._setCursor((params.params[0] || 1) - 1, this._activeBuffer.y); + return true; + } + + /** + * CSI Ps ; Ps H + * Cursor Position [row;column] (default = [1,1]) (CUP). + * + * @vt: #Y CSI CUP "Cursor Position" "CSI Ps ; Ps H" "Set cursor to position [`Ps`, `Ps`] (default = [1, 1])." + * If ORIGIN mode is set, places the cursor to the absolute position within the scroll margins. + * If ORIGIN mode is not set, places the cursor to the absolute position within the viewport. + * Note that the coordinates are 1-based, thus the top left position starts at `1 ; 1`. + */ + public cursorPosition(params: IParams): boolean { + this._setCursor( + // col + (params.length >= 2) ? (params.params[1] || 1) - 1 : 0, + // row + (params.params[0] || 1) - 1 + ); + return true; + } + + /** + * CSI Pm ` Character Position Absolute + * [column] (default = [row,1]) (HPA). + * Currently same functionality as CHA. + * + * @vt: #Y CSI HPA "Horizontal Position Absolute" "CSI Ps ` " "Same as CHA." + */ + public charPosAbsolute(params: IParams): boolean { + this._setCursor((params.params[0] || 1) - 1, this._activeBuffer.y); + return true; + } + + /** + * CSI Pm a Character Position Relative + * [columns] (default = [row,col+1]) (HPR) + * + * @vt: #Y CSI HPR "Horizontal Position Relative" "CSI Ps a" "Same as CUF." + */ + public hPositionRelative(params: IParams): boolean { + this._moveCursor(params.params[0] || 1, 0); + return true; + } + + /** + * CSI Pm d Vertical Position Absolute (VPA) + * [row] (default = [1,column]) + * + * @vt: #Y CSI VPA "Vertical Position Absolute" "CSI Ps d" "Move cursor to `Ps`-th row (default=1)." + */ + public linePosAbsolute(params: IParams): boolean { + this._setCursor(this._activeBuffer.x, (params.params[0] || 1) - 1); + return true; + } + + /** + * CSI Pm e Vertical Position Relative (VPR) + * [rows] (default = [row+1,column]) + * reuse CSI Ps B ? + * + * @vt: #Y CSI VPR "Vertical Position Relative" "CSI Ps e" "Move cursor `Ps` times down (default=1)." + */ + public vPositionRelative(params: IParams): boolean { + this._moveCursor(0, params.params[0] || 1); + return true; + } + + /** + * CSI Ps ; Ps f + * Horizontal and Vertical Position [row;column] (default = + * [1,1]) (HVP). + * Same as CUP. + * + * @vt: #Y CSI HVP "Horizontal and Vertical Position" "CSI Ps ; Ps f" "Same as CUP." + */ + public hVPosition(params: IParams): boolean { + this.cursorPosition(params); + return true; + } + + /** + * CSI Ps g Tab Clear (TBC). + * Ps = 0 -> Clear Current Column (default). + * Ps = 3 -> Clear All. + * Potentially: + * Ps = 2 -> Clear Stops on Line. + * http://vt100.net/annarbor/aaa-ug/section6.html + * + * @vt: #Y CSI TBC "Tab Clear" "CSI Ps g" "Clear tab stops at current position (0) or all (3) (default=0)." + * Clearing tabstops off the active row (Ps = 2, VT100) is currently not supported. + */ + public tabClear(params: IParams): boolean { + const param = params.params[0]; + if (param === 0) { + delete this._activeBuffer.tabs[this._activeBuffer.x]; + } else if (param === 3) { + this._activeBuffer.tabs = {}; + } + return true; + } + + /** + * CSI Ps I + * Cursor Forward Tabulation Ps tab stops (default = 1) (CHT). + * + * @vt: #Y CSI CHT "Cursor Horizontal Tabulation" "CSI Ps I" "Move cursor `Ps` times tabs forward (default=1)." + */ + public cursorForwardTab(params: IParams): boolean { + if (this._activeBuffer.x >= this._bufferService.cols) { + return true; + } + let param = params.params[0] || 1; + while (param--) { + this._activeBuffer.x = this._activeBuffer.nextStop(); + } + return true; + } + + /** + * CSI Ps Z Cursor Backward Tabulation Ps tab stops (default = 1) (CBT). + * + * @vt: #Y CSI CBT "Cursor Backward Tabulation" "CSI Ps Z" "Move cursor `Ps` tabs backward (default=1)." + */ + public cursorBackwardTab(params: IParams): boolean { + if (this._activeBuffer.x >= this._bufferService.cols) { + return true; + } + let param = params.params[0] || 1; + + while (param--) { + this._activeBuffer.x = this._activeBuffer.prevStop(); + } + return true; + } + + + /** + * Helper method to erase cells in a terminal row. + * The cell gets replaced with the eraseChar of the terminal. + * @param y row index + * @param start first cell index to be erased + * @param end end - 1 is last erased cell + * @param cleanWrap clear the isWrapped flag + */ + private _eraseInBufferLine(y: number, start: number, end: number, clearWrap: boolean = false): void { + const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y)!; + line.replaceCells( + start, + end, + this._activeBuffer.getNullCell(this._eraseAttrData()), + this._eraseAttrData() + ); + if (clearWrap) { + line.isWrapped = false; + } + } + + /** + * Helper method to reset cells in a terminal row. + * The cell gets replaced with the eraseChar of the terminal and the isWrapped property is set to false. + * @param y row index + */ + private _resetBufferLine(y: number): void { + const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y)!; + line.fill(this._activeBuffer.getNullCell(this._eraseAttrData())); + line.isWrapped = false; + } + + /** + * CSI Ps J Erase in Display (ED). + * Ps = 0 -> Erase Below (default). + * Ps = 1 -> Erase Above. + * Ps = 2 -> Erase All. + * Ps = 3 -> Erase Saved Lines (xterm). + * CSI ? Ps J + * Erase in Display (DECSED). + * Ps = 0 -> Selective Erase Below (default). + * Ps = 1 -> Selective Erase Above. + * Ps = 2 -> Selective Erase All. + * + * @vt: #Y CSI ED "Erase In Display" "CSI Ps J" "Erase various parts of the viewport." + * Supported param values: + * + * | Ps | Effect | + * | -- | ------------------------------------------------------------ | + * | 0 | Erase from the cursor through the end of the viewport. | + * | 1 | Erase from the beginning of the viewport through the cursor. | + * | 2 | Erase complete viewport. | + * | 3 | Erase scrollback. | + * + * @vt: #P[Protection attributes are not supported.] CSI DECSED "Selective Erase In Display" "CSI ? Ps J" "Currently the same as ED." + */ + public eraseInDisplay(params: IParams): boolean { + this._restrictCursor(this._bufferService.cols); + let j; + switch (params.params[0]) { + case 0: + j = this._activeBuffer.y; + this._dirtyRowService.markDirty(j); + this._eraseInBufferLine(j++, this._activeBuffer.x, this._bufferService.cols, this._activeBuffer.x === 0); + for (; j < this._bufferService.rows; j++) { + this._resetBufferLine(j); + } + this._dirtyRowService.markDirty(j); + break; + case 1: + j = this._activeBuffer.y; + this._dirtyRowService.markDirty(j); + // Deleted front part of line and everything before. This line will no longer be wrapped. + this._eraseInBufferLine(j, 0, this._activeBuffer.x + 1, true); + if (this._activeBuffer.x + 1 >= this._bufferService.cols) { + // Deleted entire previous line. This next line can no longer be wrapped. + this._activeBuffer.lines.get(j + 1)!.isWrapped = false; + } + while (j--) { + this._resetBufferLine(j); + } + this._dirtyRowService.markDirty(0); + break; + case 2: + j = this._bufferService.rows; + this._dirtyRowService.markDirty(j - 1); + while (j--) { + this._resetBufferLine(j); + } + this._dirtyRowService.markDirty(0); + break; + case 3: + // Clear scrollback (everything not in viewport) + const scrollBackSize = this._activeBuffer.lines.length - this._bufferService.rows; + if (scrollBackSize > 0) { + this._activeBuffer.lines.trimStart(scrollBackSize); + this._activeBuffer.ybase = Math.max(this._activeBuffer.ybase - scrollBackSize, 0); + this._activeBuffer.ydisp = Math.max(this._activeBuffer.ydisp - scrollBackSize, 0); + // Force a scroll event to refresh viewport + this._onScroll.fire(0); + } + break; + } + return true; + } + + /** + * CSI Ps K Erase in Line (EL). + * Ps = 0 -> Erase to Right (default). + * Ps = 1 -> Erase to Left. + * Ps = 2 -> Erase All. + * CSI ? Ps K + * Erase in Line (DECSEL). + * Ps = 0 -> Selective Erase to Right (default). + * Ps = 1 -> Selective Erase to Left. + * Ps = 2 -> Selective Erase All. + * + * @vt: #Y CSI EL "Erase In Line" "CSI Ps K" "Erase various parts of the active row." + * Supported param values: + * + * | Ps | Effect | + * | -- | -------------------------------------------------------- | + * | 0 | Erase from the cursor through the end of the row. | + * | 1 | Erase from the beginning of the line through the cursor. | + * | 2 | Erase complete line. | + * + * @vt: #P[Protection attributes are not supported.] CSI DECSEL "Selective Erase In Line" "CSI ? Ps K" "Currently the same as EL." + */ + public eraseInLine(params: IParams): boolean { + this._restrictCursor(this._bufferService.cols); + switch (params.params[0]) { + case 0: + this._eraseInBufferLine(this._activeBuffer.y, this._activeBuffer.x, this._bufferService.cols, this._activeBuffer.x === 0); + break; + case 1: + this._eraseInBufferLine(this._activeBuffer.y, 0, this._activeBuffer.x + 1, false); + break; + case 2: + this._eraseInBufferLine(this._activeBuffer.y, 0, this._bufferService.cols, true); + break; + } + this._dirtyRowService.markDirty(this._activeBuffer.y); + return true; + } + + /** + * CSI Ps L + * Insert Ps Line(s) (default = 1) (IL). + * + * @vt: #Y CSI IL "Insert Line" "CSI Ps L" "Insert `Ps` blank lines at active row (default=1)." + * For every inserted line at the scroll top one line at the scroll bottom gets removed. + * The cursor is set to the first column. + * IL has no effect if the cursor is outside the scroll margins. + */ + public insertLines(params: IParams): boolean { + this._restrictCursor(); + let param = params.params[0] || 1; + + if (this._activeBuffer.y > this._activeBuffer.scrollBottom || this._activeBuffer.y < this._activeBuffer.scrollTop) { + return true; + } + + const row: number = this._activeBuffer.ybase + this._activeBuffer.y; + + const scrollBottomRowsOffset = this._bufferService.rows - 1 - this._activeBuffer.scrollBottom; + const scrollBottomAbsolute = this._bufferService.rows - 1 + this._activeBuffer.ybase - scrollBottomRowsOffset + 1; + while (param--) { + // test: echo -e '\e[44m\e[1L\e[0m' + // blankLine(true) - xterm/linux behavior + this._activeBuffer.lines.splice(scrollBottomAbsolute - 1, 1); + this._activeBuffer.lines.splice(row, 0, this._activeBuffer.getBlankLine(this._eraseAttrData())); + } + + this._dirtyRowService.markRangeDirty(this._activeBuffer.y, this._activeBuffer.scrollBottom); + this._activeBuffer.x = 0; // see https://vt100.net/docs/vt220-rm/chapter4.html - vt220 only? + return true; + } + + /** + * CSI Ps M + * Delete Ps Line(s) (default = 1) (DL). + * + * @vt: #Y CSI DL "Delete Line" "CSI Ps M" "Delete `Ps` lines at active row (default=1)." + * For every deleted line at the scroll top one blank line at the scroll bottom gets appended. + * The cursor is set to the first column. + * DL has no effect if the cursor is outside the scroll margins. + */ + public deleteLines(params: IParams): boolean { + this._restrictCursor(); + let param = params.params[0] || 1; + + if (this._activeBuffer.y > this._activeBuffer.scrollBottom || this._activeBuffer.y < this._activeBuffer.scrollTop) { + return true; + } + + const row: number = this._activeBuffer.ybase + this._activeBuffer.y; + + let j: number; + j = this._bufferService.rows - 1 - this._activeBuffer.scrollBottom; + j = this._bufferService.rows - 1 + this._activeBuffer.ybase - j; + while (param--) { + // test: echo -e '\e[44m\e[1M\e[0m' + // blankLine(true) - xterm/linux behavior + this._activeBuffer.lines.splice(row, 1); + this._activeBuffer.lines.splice(j, 0, this._activeBuffer.getBlankLine(this._eraseAttrData())); + } + + this._dirtyRowService.markRangeDirty(this._activeBuffer.y, this._activeBuffer.scrollBottom); + this._activeBuffer.x = 0; // see https://vt100.net/docs/vt220-rm/chapter4.html - vt220 only? + return true; + } + + /** + * CSI Ps @ + * Insert Ps (Blank) Character(s) (default = 1) (ICH). + * + * @vt: #Y CSI ICH "Insert Characters" "CSI Ps @" "Insert `Ps` (blank) characters (default = 1)." + * The ICH sequence inserts `Ps` blank characters. The cursor remains at the beginning of the blank characters. + * Text between the cursor and right margin moves to the right. Characters moved past the right margin are lost. + * + * + * FIXME: check against xterm - should not work outside of scroll margins (see VT520 manual) + */ + public insertChars(params: IParams): boolean { + this._restrictCursor(); + const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y); + if (line) { + line.insertCells( + this._activeBuffer.x, + params.params[0] || 1, + this._activeBuffer.getNullCell(this._eraseAttrData()), + this._eraseAttrData() + ); + this._dirtyRowService.markDirty(this._activeBuffer.y); + } + return true; + } + + /** + * CSI Ps P + * Delete Ps Character(s) (default = 1) (DCH). + * + * @vt: #Y CSI DCH "Delete Character" "CSI Ps P" "Delete `Ps` characters (default=1)." + * As characters are deleted, the remaining characters between the cursor and right margin move to the left. + * Character attributes move with the characters. The terminal adds blank characters at the right margin. + * + * + * FIXME: check against xterm - should not work outside of scroll margins (see VT520 manual) + */ + public deleteChars(params: IParams): boolean { + this._restrictCursor(); + const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y); + if (line) { + line.deleteCells( + this._activeBuffer.x, + params.params[0] || 1, + this._activeBuffer.getNullCell(this._eraseAttrData()), + this._eraseAttrData() + ); + this._dirtyRowService.markDirty(this._activeBuffer.y); + } + return true; + } + + /** + * CSI Ps S Scroll up Ps lines (default = 1) (SU). + * + * @vt: #Y CSI SU "Scroll Up" "CSI Ps S" "Scroll `Ps` lines up (default=1)." + * + * + * FIXME: scrolled out lines at top = 1 should add to scrollback (xterm) + */ + public scrollUp(params: IParams): boolean { + let param = params.params[0] || 1; + + while (param--) { + this._activeBuffer.lines.splice(this._activeBuffer.ybase + this._activeBuffer.scrollTop, 1); + this._activeBuffer.lines.splice(this._activeBuffer.ybase + this._activeBuffer.scrollBottom, 0, this._activeBuffer.getBlankLine(this._eraseAttrData())); + } + this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom); + return true; + } + + /** + * CSI Ps T Scroll down Ps lines (default = 1) (SD). + * + * @vt: #Y CSI SD "Scroll Down" "CSI Ps T" "Scroll `Ps` lines down (default=1)." + */ + public scrollDown(params: IParams): boolean { + let param = params.params[0] || 1; + + while (param--) { + this._activeBuffer.lines.splice(this._activeBuffer.ybase + this._activeBuffer.scrollBottom, 1); + this._activeBuffer.lines.splice(this._activeBuffer.ybase + this._activeBuffer.scrollTop, 0, this._activeBuffer.getBlankLine(DEFAULT_ATTR_DATA)); + } + this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom); + return true; + } + + /** + * CSI Ps SP @ Scroll left Ps columns (default = 1) (SL) ECMA-48 + * + * Notation: (Pn) + * Representation: CSI Pn 02/00 04/00 + * Parameter default value: Pn = 1 + * SL causes the data in the presentation component to be moved by n character positions + * if the line orientation is horizontal, or by n line positions if the line orientation + * is vertical, such that the data appear to move to the left; where n equals the value of Pn. + * The active presentation position is not affected by this control function. + * + * Supported: + * - always left shift (no line orientation setting respected) + * + * @vt: #Y CSI SL "Scroll Left" "CSI Ps SP @" "Scroll viewport `Ps` times to the left." + * SL moves the content of all lines within the scroll margins `Ps` times to the left. + * SL has no effect outside of the scroll margins. + */ + public scrollLeft(params: IParams): boolean { + if (this._activeBuffer.y > this._activeBuffer.scrollBottom || this._activeBuffer.y < this._activeBuffer.scrollTop) { + return true; + } + const param = params.params[0] || 1; + for (let y = this._activeBuffer.scrollTop; y <= this._activeBuffer.scrollBottom; ++y) { + const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y)!; + line.deleteCells(0, param, this._activeBuffer.getNullCell(this._eraseAttrData()), this._eraseAttrData()); + line.isWrapped = false; + } + this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom); + return true; + } + + /** + * CSI Ps SP A Scroll right Ps columns (default = 1) (SR) ECMA-48 + * + * Notation: (Pn) + * Representation: CSI Pn 02/00 04/01 + * Parameter default value: Pn = 1 + * SR causes the data in the presentation component to be moved by n character positions + * if the line orientation is horizontal, or by n line positions if the line orientation + * is vertical, such that the data appear to move to the right; where n equals the value of Pn. + * The active presentation position is not affected by this control function. + * + * Supported: + * - always right shift (no line orientation setting respected) + * + * @vt: #Y CSI SR "Scroll Right" "CSI Ps SP A" "Scroll viewport `Ps` times to the right." + * SL moves the content of all lines within the scroll margins `Ps` times to the right. + * Content at the right margin is lost. + * SL has no effect outside of the scroll margins. + */ + public scrollRight(params: IParams): boolean { + if (this._activeBuffer.y > this._activeBuffer.scrollBottom || this._activeBuffer.y < this._activeBuffer.scrollTop) { + return true; + } + const param = params.params[0] || 1; + for (let y = this._activeBuffer.scrollTop; y <= this._activeBuffer.scrollBottom; ++y) { + const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y)!; + line.insertCells(0, param, this._activeBuffer.getNullCell(this._eraseAttrData()), this._eraseAttrData()); + line.isWrapped = false; + } + this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom); + return true; + } + + /** + * CSI Pm ' } + * Insert Ps Column(s) (default = 1) (DECIC), VT420 and up. + * + * @vt: #Y CSI DECIC "Insert Columns" "CSI Ps ' }" "Insert `Ps` columns at cursor position." + * DECIC inserts `Ps` times blank columns at the cursor position for all lines with the scroll margins, + * moving content to the right. Content at the right margin is lost. + * DECIC has no effect outside the scrolling margins. + */ + public insertColumns(params: IParams): boolean { + if (this._activeBuffer.y > this._activeBuffer.scrollBottom || this._activeBuffer.y < this._activeBuffer.scrollTop) { + return true; + } + const param = params.params[0] || 1; + for (let y = this._activeBuffer.scrollTop; y <= this._activeBuffer.scrollBottom; ++y) { + const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y)!; + line.insertCells(this._activeBuffer.x, param, this._activeBuffer.getNullCell(this._eraseAttrData()), this._eraseAttrData()); + line.isWrapped = false; + } + this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom); + return true; + } + + /** + * CSI Pm ' ~ + * Delete Ps Column(s) (default = 1) (DECDC), VT420 and up. + * + * @vt: #Y CSI DECDC "Delete Columns" "CSI Ps ' ~" "Delete `Ps` columns at cursor position." + * DECDC deletes `Ps` times columns at the cursor position for all lines with the scroll margins, + * moving content to the left. Blank columns are added at the right margin. + * DECDC has no effect outside the scrolling margins. + */ + public deleteColumns(params: IParams): boolean { + if (this._activeBuffer.y > this._activeBuffer.scrollBottom || this._activeBuffer.y < this._activeBuffer.scrollTop) { + return true; + } + const param = params.params[0] || 1; + for (let y = this._activeBuffer.scrollTop; y <= this._activeBuffer.scrollBottom; ++y) { + const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y)!; + line.deleteCells(this._activeBuffer.x, param, this._activeBuffer.getNullCell(this._eraseAttrData()), this._eraseAttrData()); + line.isWrapped = false; + } + this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom); + return true; + } + + /** + * CSI Ps X + * Erase Ps Character(s) (default = 1) (ECH). + * + * @vt: #Y CSI ECH "Erase Character" "CSI Ps X" "Erase `Ps` characters from current cursor position to the right (default=1)." + * ED erases `Ps` characters from current cursor position to the right. + * ED works inside or outside the scrolling margins. + */ + public eraseChars(params: IParams): boolean { + this._restrictCursor(); + const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y); + if (line) { + line.replaceCells( + this._activeBuffer.x, + this._activeBuffer.x + (params.params[0] || 1), + this._activeBuffer.getNullCell(this._eraseAttrData()), + this._eraseAttrData() + ); + this._dirtyRowService.markDirty(this._activeBuffer.y); + } + return true; + } + + /** + * CSI Ps b Repeat the preceding graphic character Ps times (REP). + * From ECMA 48 (@see http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-048.pdf) + * Notation: (Pn) + * Representation: CSI Pn 06/02 + * Parameter default value: Pn = 1 + * REP is used to indicate that the preceding character in the data stream, + * if it is a graphic character (represented by one or more bit combinations) including SPACE, + * is to be repeated n times, where n equals the value of Pn. + * If the character preceding REP is a control function or part of a control function, + * the effect of REP is not defined by this Standard. + * + * Since we propagate the terminal as xterm-256color we have to follow xterm's behavior: + * - fullwidth + surrogate chars are ignored + * - for combining chars only the base char gets repeated + * - text attrs are applied normally + * - wrap around is respected + * - any valid sequence resets the carried forward char + * + * Note: To get reset on a valid sequence working correctly without much runtime penalty, + * the preceding codepoint is stored on the parser in `this.print` and reset during `parser.parse`. + * + * @vt: #Y CSI REP "Repeat Preceding Character" "CSI Ps b" "Repeat preceding character `Ps` times (default=1)." + * REP repeats the previous character `Ps` times advancing the cursor, also wrapping if DECAWM is set. + * REP has no effect if the sequence does not follow a printable ASCII character + * (NOOP for any other sequence in between or NON ASCII characters). + */ + public repeatPrecedingCharacter(params: IParams): boolean { + if (!this._parser.precedingCodepoint) { + return true; + } + // call print to insert the chars and handle correct wrapping + const length = params.params[0] || 1; + const data = new Uint32Array(length); + for (let i = 0; i < length; ++i) { + data[i] = this._parser.precedingCodepoint; + } + this.print(data, 0, data.length); + return true; + } + + /** + * CSI Ps c Send Device Attributes (Primary DA). + * Ps = 0 or omitted -> request attributes from terminal. The + * response depends on the decTerminalID resource setting. + * -> CSI ? 1 ; 2 c (``VT100 with Advanced Video Option'') + * -> CSI ? 1 ; 0 c (``VT101 with No Options'') + * -> CSI ? 6 c (``VT102'') + * -> CSI ? 6 0 ; 1 ; 2 ; 6 ; 8 ; 9 ; 1 5 ; c (``VT220'') + * The VT100-style response parameters do not mean anything by + * themselves. VT220 parameters do, telling the host what fea- + * tures the terminal supports: + * Ps = 1 -> 132-columns. + * Ps = 2 -> Printer. + * Ps = 6 -> Selective erase. + * Ps = 8 -> User-defined keys. + * Ps = 9 -> National replacement character sets. + * Ps = 1 5 -> Technical characters. + * Ps = 2 2 -> ANSI color, e.g., VT525. + * Ps = 2 9 -> ANSI text locator (i.e., DEC Locator mode). + * + * @vt: #Y CSI DA1 "Primary Device Attributes" "CSI c" "Send primary device attributes." + * + * + * TODO: fix and cleanup response + */ + public sendDeviceAttributesPrimary(params: IParams): boolean { + if (params.params[0] > 0) { + return true; + } + if (this._is('xterm') || this._is('rxvt-unicode') || this._is('screen')) { + this._coreService.triggerDataEvent(C0.ESC + '[?1;2c'); + } else if (this._is('linux')) { + this._coreService.triggerDataEvent(C0.ESC + '[?6c'); + } + return true; + } + + /** + * CSI > Ps c + * Send Device Attributes (Secondary DA). + * Ps = 0 or omitted -> request the terminal's identification + * code. The response depends on the decTerminalID resource set- + * ting. It should apply only to VT220 and up, but xterm extends + * this to VT100. + * -> CSI > Pp ; Pv ; Pc c + * where Pp denotes the terminal type + * Pp = 0 -> ``VT100''. + * Pp = 1 -> ``VT220''. + * and Pv is the firmware version (for xterm, this was originally + * the XFree86 patch number, starting with 95). In a DEC termi- + * nal, Pc indicates the ROM cartridge registration number and is + * always zero. + * More information: + * xterm/charproc.c - line 2012, for more information. + * vim responds with ^[[?0c or ^[[?1c after the terminal's response (?) + * + * @vt: #Y CSI DA2 "Secondary Device Attributes" "CSI > c" "Send primary device attributes." + * + * + * TODO: fix and cleanup response + */ + public sendDeviceAttributesSecondary(params: IParams): boolean { + if (params.params[0] > 0) { + return true; + } + // xterm and urxvt + // seem to spit this + // out around ~370 times (?). + if (this._is('xterm')) { + this._coreService.triggerDataEvent(C0.ESC + '[>0;276;0c'); + } else if (this._is('rxvt-unicode')) { + this._coreService.triggerDataEvent(C0.ESC + '[>85;95;0c'); + } else if (this._is('linux')) { + // not supported by linux console. + // linux console echoes parameters. + this._coreService.triggerDataEvent(params.params[0] + 'c'); + } else if (this._is('screen')) { + this._coreService.triggerDataEvent(C0.ESC + '[>83;40003;0c'); + } + return true; + } + + /** + * Evaluate if the current terminal is the given argument. + * @param term The terminal name to evaluate + */ + private _is(term: string): boolean { + return (this._optionsService.rawOptions.termName + '').indexOf(term) === 0; + } + + /** + * CSI Pm h Set Mode (SM). + * Ps = 2 -> Keyboard Action Mode (AM). + * Ps = 4 -> Insert Mode (IRM). + * Ps = 1 2 -> Send/receive (SRM). + * Ps = 2 0 -> Automatic Newline (LNM). + * + * @vt: #P[Only IRM is supported.] CSI SM "Set Mode" "CSI Pm h" "Set various terminal modes." + * Supported param values by SM: + * + * | Param | Action | Support | + * | ----- | -------------------------------------- | ------- | + * | 2 | Keyboard Action Mode (KAM). Always on. | #N | + * | 4 | Insert Mode (IRM). | #Y | + * | 12 | Send/receive (SRM). Always off. | #N | + * | 20 | Automatic Newline (LNM). Always off. | #N | + */ + public setMode(params: IParams): boolean { + for (let i = 0; i < params.length; i++) { + switch (params.params[i]) { + case 4: + this._coreService.modes.insertMode = true; + break; + case 20: + // this._t.convertEol = true; + break; + } + } + return true; + } + + /** + * CSI ? Pm h + * DEC Private Mode Set (DECSET). + * Ps = 1 -> Application Cursor Keys (DECCKM). + * Ps = 2 -> Designate USASCII for character sets G0-G3 + * (DECANM), and set VT100 mode. + * Ps = 3 -> 132 Column Mode (DECCOLM). + * Ps = 4 -> Smooth (Slow) Scroll (DECSCLM). + * Ps = 5 -> Reverse Video (DECSCNM). + * Ps = 6 -> Origin Mode (DECOM). + * Ps = 7 -> Wraparound Mode (DECAWM). + * Ps = 8 -> Auto-repeat Keys (DECARM). + * Ps = 9 -> Send Mouse X & Y on button press. See the sec- + * tion Mouse Tracking. + * Ps = 1 0 -> Show toolbar (rxvt). + * Ps = 1 2 -> Start Blinking Cursor (att610). + * Ps = 1 8 -> Print form feed (DECPFF). + * Ps = 1 9 -> Set print extent to full screen (DECPEX). + * Ps = 2 5 -> Show Cursor (DECTCEM). + * Ps = 3 0 -> Show scrollbar (rxvt). + * Ps = 3 5 -> Enable font-shifting functions (rxvt). + * Ps = 3 8 -> Enter Tektronix Mode (DECTEK). + * Ps = 4 0 -> Allow 80 -> 132 Mode. + * Ps = 4 1 -> more(1) fix (see curses resource). + * Ps = 4 2 -> Enable Nation Replacement Character sets (DECN- + * RCM). + * Ps = 4 4 -> Turn On Margin Bell. + * Ps = 4 5 -> Reverse-wraparound Mode. + * Ps = 4 6 -> Start Logging. This is normally disabled by a + * compile-time option. + * Ps = 4 7 -> Use Alternate Screen Buffer. (This may be dis- + * abled by the titeInhibit resource). + * Ps = 6 6 -> Application keypad (DECNKM). + * Ps = 6 7 -> Backarrow key sends backspace (DECBKM). + * Ps = 1 0 0 0 -> Send Mouse X & Y on button press and + * release. See the section Mouse Tracking. + * Ps = 1 0 0 1 -> Use Hilite Mouse Tracking. + * Ps = 1 0 0 2 -> Use Cell Motion Mouse Tracking. + * Ps = 1 0 0 3 -> Use All Motion Mouse Tracking. + * Ps = 1 0 0 4 -> Send FocusIn/FocusOut events. + * Ps = 1 0 0 5 -> Enable Extended Mouse Mode. + * Ps = 1 0 1 0 -> Scroll to bottom on tty output (rxvt). + * Ps = 1 0 1 1 -> Scroll to bottom on key press (rxvt). + * Ps = 1 0 3 4 -> Interpret "meta" key, sets eighth bit. + * (enables the eightBitInput resource). + * Ps = 1 0 3 5 -> Enable special modifiers for Alt and Num- + * Lock keys. (This enables the numLock resource). + * Ps = 1 0 3 6 -> Send ESC when Meta modifies a key. (This + * enables the metaSendsEscape resource). + * Ps = 1 0 3 7 -> Send DEL from the editing-keypad Delete + * key. + * Ps = 1 0 3 9 -> Send ESC when Alt modifies a key. (This + * enables the altSendsEscape resource). + * Ps = 1 0 4 0 -> Keep selection even if not highlighted. + * (This enables the keepSelection resource). + * Ps = 1 0 4 1 -> Use the CLIPBOARD selection. (This enables + * the selectToClipboard resource). + * Ps = 1 0 4 2 -> Enable Urgency window manager hint when + * Control-G is received. (This enables the bellIsUrgent + * resource). + * Ps = 1 0 4 3 -> Enable raising of the window when Control-G + * is received. (enables the popOnBell resource). + * Ps = 1 0 4 7 -> Use Alternate Screen Buffer. (This may be + * disabled by the titeInhibit resource). + * Ps = 1 0 4 8 -> Save cursor as in DECSC. (This may be dis- + * abled by the titeInhibit resource). + * Ps = 1 0 4 9 -> Save cursor as in DECSC and use Alternate + * Screen Buffer, clearing it first. (This may be disabled by + * the titeInhibit resource). This combines the effects of the 1 + * 0 4 7 and 1 0 4 8 modes. Use this with terminfo-based + * applications rather than the 4 7 mode. + * Ps = 1 0 5 0 -> Set terminfo/termcap function-key mode. + * Ps = 1 0 5 1 -> Set Sun function-key mode. + * Ps = 1 0 5 2 -> Set HP function-key mode. + * Ps = 1 0 5 3 -> Set SCO function-key mode. + * Ps = 1 0 6 0 -> Set legacy keyboard emulation (X11R6). + * Ps = 1 0 6 1 -> Set VT220 keyboard emulation. + * Ps = 2 0 0 4 -> Set bracketed paste mode. + * Modes: + * http: *vt100.net/docs/vt220-rm/chapter4.html + * + * @vt: #P[See below for supported modes.] CSI DECSET "DEC Private Set Mode" "CSI ? Pm h" "Set various terminal attributes." + * Supported param values by DECSET: + * + * | param | Action | Support | + * | ----- | ------------------------------------------------------- | --------| + * | 1 | Application Cursor Keys (DECCKM). | #Y | + * | 2 | Designate US-ASCII for character sets G0-G3 (DECANM). | #Y | + * | 3 | 132 Column Mode (DECCOLM). | #Y | + * | 6 | Origin Mode (DECOM). | #Y | + * | 7 | Auto-wrap Mode (DECAWM). | #Y | + * | 8 | Auto-repeat Keys (DECARM). Always on. | #N | + * | 9 | X10 xterm mouse protocol. | #Y | + * | 12 | Start Blinking Cursor. | #Y | + * | 25 | Show Cursor (DECTCEM). | #Y | + * | 45 | Reverse wrap-around. | #Y | + * | 47 | Use Alternate Screen Buffer. | #Y | + * | 66 | Application keypad (DECNKM). | #Y | + * | 1000 | X11 xterm mouse protocol. | #Y | + * | 1002 | Use Cell Motion Mouse Tracking. | #Y | + * | 1003 | Use All Motion Mouse Tracking. | #Y | + * | 1004 | Send FocusIn/FocusOut events | #Y | + * | 1005 | Enable UTF-8 Mouse Mode. | #N | + * | 1006 | Enable SGR Mouse Mode. | #Y | + * | 1015 | Enable urxvt Mouse Mode. | #N | + * | 1047 | Use Alternate Screen Buffer. | #Y | + * | 1048 | Save cursor as in DECSC. | #Y | + * | 1049 | Save cursor and switch to alternate buffer clearing it. | #P[Does not clear the alternate buffer.] | + * | 2004 | Set bracketed paste mode. | #Y | + * + * + * FIXME: implement DECSCNM, 1049 should clear altbuffer + */ + public setModePrivate(params: IParams): boolean { + for (let i = 0; i < params.length; i++) { + switch (params.params[i]) { + case 1: + this._coreService.decPrivateModes.applicationCursorKeys = true; + break; + case 2: + this._charsetService.setgCharset(0, DEFAULT_CHARSET); + this._charsetService.setgCharset(1, DEFAULT_CHARSET); + this._charsetService.setgCharset(2, DEFAULT_CHARSET); + this._charsetService.setgCharset(3, DEFAULT_CHARSET); + // set VT100 mode here + break; + case 3: + /** + * DECCOLM - 132 column mode. + * This is only active if 'SetWinLines' (24) is enabled + * through `options.windowsOptions`. + */ + if (this._optionsService.rawOptions.windowOptions.setWinLines) { + this._bufferService.resize(132, this._bufferService.rows); + this._onRequestReset.fire(); + } + break; + case 6: + this._coreService.decPrivateModes.origin = true; + this._setCursor(0, 0); + break; + case 7: + this._coreService.decPrivateModes.wraparound = true; + break; + case 12: + // this.cursorBlink = true; + break; + case 45: + this._coreService.decPrivateModes.reverseWraparound = true; + break; + case 66: + this._logService.debug('Serial port requested application keypad.'); + this._coreService.decPrivateModes.applicationKeypad = true; + this._onRequestSyncScrollBar.fire(); + break; + case 9: // X10 Mouse + // no release, no motion, no wheel, no modifiers. + this._coreMouseService.activeProtocol = 'X10'; + break; + case 1000: // vt200 mouse + // no motion. + this._coreMouseService.activeProtocol = 'VT200'; + break; + case 1002: // button event mouse + this._coreMouseService.activeProtocol = 'DRAG'; + break; + case 1003: // any event mouse + // any event - sends motion events, + // even if there is no button held down. + this._coreMouseService.activeProtocol = 'ANY'; + break; + case 1004: // send focusin/focusout events + // focusin: ^[[I + // focusout: ^[[O + this._coreService.decPrivateModes.sendFocus = true; + this._onRequestSendFocus.fire(); + break; + case 1005: // utf8 ext mode mouse - removed in #2507 + this._logService.debug('DECSET 1005 not supported (see #2507)'); + break; + case 1006: // sgr ext mode mouse + this._coreMouseService.activeEncoding = 'SGR'; + break; + case 1015: // urxvt ext mode mouse - removed in #2507 + this._logService.debug('DECSET 1015 not supported (see #2507)'); + break; + case 25: // show cursor + this._coreService.isCursorHidden = false; + break; + case 1048: // alt screen cursor + this.saveCursor(); + break; + case 1049: // alt screen buffer cursor + this.saveCursor(); + // FALL-THROUGH + case 47: // alt screen buffer + case 1047: // alt screen buffer + this._bufferService.buffers.activateAltBuffer(this._eraseAttrData()); + this._coreService.isCursorInitialized = true; + this._onRequestRefreshRows.fire(0, this._bufferService.rows - 1); + this._onRequestSyncScrollBar.fire(); + break; + case 2004: // bracketed paste mode (https://cirw.in/blog/bracketed-paste) + this._coreService.decPrivateModes.bracketedPasteMode = true; + break; + } + } + return true; + } + + + /** + * CSI Pm l Reset Mode (RM). + * Ps = 2 -> Keyboard Action Mode (AM). + * Ps = 4 -> Replace Mode (IRM). + * Ps = 1 2 -> Send/receive (SRM). + * Ps = 2 0 -> Normal Linefeed (LNM). + * + * @vt: #P[Only IRM is supported.] CSI RM "Reset Mode" "CSI Pm l" "Set various terminal attributes." + * Supported param values by RM: + * + * | Param | Action | Support | + * | ----- | -------------------------------------- | ------- | + * | 2 | Keyboard Action Mode (KAM). Always on. | #N | + * | 4 | Replace Mode (IRM). (default) | #Y | + * | 12 | Send/receive (SRM). Always off. | #N | + * | 20 | Normal Linefeed (LNM). Always off. | #N | + * + * + * FIXME: why is LNM commented out? + */ + public resetMode(params: IParams): boolean { + for (let i = 0; i < params.length; i++) { + switch (params.params[i]) { + case 4: + this._coreService.modes.insertMode = false; + break; + case 20: + // this._t.convertEol = false; + break; + } + } + return true; + } + + /** + * CSI ? Pm l + * DEC Private Mode Reset (DECRST). + * Ps = 1 -> Normal Cursor Keys (DECCKM). + * Ps = 2 -> Designate VT52 mode (DECANM). + * Ps = 3 -> 80 Column Mode (DECCOLM). + * Ps = 4 -> Jump (Fast) Scroll (DECSCLM). + * Ps = 5 -> Normal Video (DECSCNM). + * Ps = 6 -> Normal Cursor Mode (DECOM). + * Ps = 7 -> No Wraparound Mode (DECAWM). + * Ps = 8 -> No Auto-repeat Keys (DECARM). + * Ps = 9 -> Don't send Mouse X & Y on button press. + * Ps = 1 0 -> Hide toolbar (rxvt). + * Ps = 1 2 -> Stop Blinking Cursor (att610). + * Ps = 1 8 -> Don't print form feed (DECPFF). + * Ps = 1 9 -> Limit print to scrolling region (DECPEX). + * Ps = 2 5 -> Hide Cursor (DECTCEM). + * Ps = 3 0 -> Don't show scrollbar (rxvt). + * Ps = 3 5 -> Disable font-shifting functions (rxvt). + * Ps = 4 0 -> Disallow 80 -> 132 Mode. + * Ps = 4 1 -> No more(1) fix (see curses resource). + * Ps = 4 2 -> Disable Nation Replacement Character sets (DEC- + * NRCM). + * Ps = 4 4 -> Turn Off Margin Bell. + * Ps = 4 5 -> No Reverse-wraparound Mode. + * Ps = 4 6 -> Stop Logging. (This is normally disabled by a + * compile-time option). + * Ps = 4 7 -> Use Normal Screen Buffer. + * Ps = 6 6 -> Numeric keypad (DECNKM). + * Ps = 6 7 -> Backarrow key sends delete (DECBKM). + * Ps = 1 0 0 0 -> Don't send Mouse X & Y on button press and + * release. See the section Mouse Tracking. + * Ps = 1 0 0 1 -> Don't use Hilite Mouse Tracking. + * Ps = 1 0 0 2 -> Don't use Cell Motion Mouse Tracking. + * Ps = 1 0 0 3 -> Don't use All Motion Mouse Tracking. + * Ps = 1 0 0 4 -> Don't send FocusIn/FocusOut events. + * Ps = 1 0 0 5 -> Disable Extended Mouse Mode. + * Ps = 1 0 1 0 -> Don't scroll to bottom on tty output + * (rxvt). + * Ps = 1 0 1 1 -> Don't scroll to bottom on key press (rxvt). + * Ps = 1 0 3 4 -> Don't interpret "meta" key. (This disables + * the eightBitInput resource). + * Ps = 1 0 3 5 -> Disable special modifiers for Alt and Num- + * Lock keys. (This disables the numLock resource). + * Ps = 1 0 3 6 -> Don't send ESC when Meta modifies a key. + * (This disables the metaSendsEscape resource). + * Ps = 1 0 3 7 -> Send VT220 Remove from the editing-keypad + * Delete key. + * Ps = 1 0 3 9 -> Don't send ESC when Alt modifies a key. + * (This disables the altSendsEscape resource). + * Ps = 1 0 4 0 -> Do not keep selection when not highlighted. + * (This disables the keepSelection resource). + * Ps = 1 0 4 1 -> Use the PRIMARY selection. (This disables + * the selectToClipboard resource). + * Ps = 1 0 4 2 -> Disable Urgency window manager hint when + * Control-G is received. (This disables the bellIsUrgent + * resource). + * Ps = 1 0 4 3 -> Disable raising of the window when Control- + * G is received. (This disables the popOnBell resource). + * Ps = 1 0 4 7 -> Use Normal Screen Buffer, clearing screen + * first if in the Alternate Screen. (This may be disabled by + * the titeInhibit resource). + * Ps = 1 0 4 8 -> Restore cursor as in DECRC. (This may be + * disabled by the titeInhibit resource). + * Ps = 1 0 4 9 -> Use Normal Screen Buffer and restore cursor + * as in DECRC. (This may be disabled by the titeInhibit + * resource). This combines the effects of the 1 0 4 7 and 1 0 + * 4 8 modes. Use this with terminfo-based applications rather + * than the 4 7 mode. + * Ps = 1 0 5 0 -> Reset terminfo/termcap function-key mode. + * Ps = 1 0 5 1 -> Reset Sun function-key mode. + * Ps = 1 0 5 2 -> Reset HP function-key mode. + * Ps = 1 0 5 3 -> Reset SCO function-key mode. + * Ps = 1 0 6 0 -> Reset legacy keyboard emulation (X11R6). + * Ps = 1 0 6 1 -> Reset keyboard emulation to Sun/PC style. + * Ps = 2 0 0 4 -> Reset bracketed paste mode. + * + * @vt: #P[See below for supported modes.] CSI DECRST "DEC Private Reset Mode" "CSI ? Pm l" "Reset various terminal attributes." + * Supported param values by DECRST: + * + * | param | Action | Support | + * | ----- | ------------------------------------------------------- | ------- | + * | 1 | Normal Cursor Keys (DECCKM). | #Y | + * | 2 | Designate VT52 mode (DECANM). | #N | + * | 3 | 80 Column Mode (DECCOLM). | #B[Switches to old column width instead of 80.] | + * | 6 | Normal Cursor Mode (DECOM). | #Y | + * | 7 | No Wraparound Mode (DECAWM). | #Y | + * | 8 | No Auto-repeat Keys (DECARM). | #N | + * | 9 | Don't send Mouse X & Y on button press. | #Y | + * | 12 | Stop Blinking Cursor. | #Y | + * | 25 | Hide Cursor (DECTCEM). | #Y | + * | 45 | No reverse wrap-around. | #Y | + * | 47 | Use Normal Screen Buffer. | #Y | + * | 66 | Numeric keypad (DECNKM). | #Y | + * | 1000 | Don't send Mouse reports. | #Y | + * | 1002 | Don't use Cell Motion Mouse Tracking. | #Y | + * | 1003 | Don't use All Motion Mouse Tracking. | #Y | + * | 1004 | Don't send FocusIn/FocusOut events. | #Y | + * | 1005 | Disable UTF-8 Mouse Mode. | #N | + * | 1006 | Disable SGR Mouse Mode. | #Y | + * | 1015 | Disable urxvt Mouse Mode. | #N | + * | 1047 | Use Normal Screen Buffer (clearing screen if in alt). | #Y | + * | 1048 | Restore cursor as in DECRC. | #Y | + * | 1049 | Use Normal Screen Buffer and restore cursor. | #Y | + * | 2004 | Reset bracketed paste mode. | #Y | + * + * + * FIXME: DECCOLM is currently broken (already fixed in window options PR) + */ + public resetModePrivate(params: IParams): boolean { + for (let i = 0; i < params.length; i++) { + switch (params.params[i]) { + case 1: + this._coreService.decPrivateModes.applicationCursorKeys = false; + break; + case 3: + /** + * DECCOLM - 80 column mode. + * This is only active if 'SetWinLines' (24) is enabled + * through `options.windowsOptions`. + */ + if (this._optionsService.rawOptions.windowOptions.setWinLines) { + this._bufferService.resize(80, this._bufferService.rows); + this._onRequestReset.fire(); + } + break; + case 6: + this._coreService.decPrivateModes.origin = false; + this._setCursor(0, 0); + break; + case 7: + this._coreService.decPrivateModes.wraparound = false; + break; + case 12: + // this.cursorBlink = false; + break; + case 45: + this._coreService.decPrivateModes.reverseWraparound = false; + break; + case 66: + this._logService.debug('Switching back to normal keypad.'); + this._coreService.decPrivateModes.applicationKeypad = false; + this._onRequestSyncScrollBar.fire(); + break; + case 9: // X10 Mouse + case 1000: // vt200 mouse + case 1002: // button event mouse + case 1003: // any event mouse + this._coreMouseService.activeProtocol = 'NONE'; + break; + case 1004: // send focusin/focusout events + this._coreService.decPrivateModes.sendFocus = false; + break; + case 1005: // utf8 ext mode mouse - removed in #2507 + this._logService.debug('DECRST 1005 not supported (see #2507)'); + break; + case 1006: // sgr ext mode mouse + this._coreMouseService.activeEncoding = 'DEFAULT'; + break; + case 1015: // urxvt ext mode mouse - removed in #2507 + this._logService.debug('DECRST 1015 not supported (see #2507)'); + break; + case 25: // hide cursor + this._coreService.isCursorHidden = true; + break; + case 1048: // alt screen cursor + this.restoreCursor(); + break; + case 1049: // alt screen buffer cursor + // FALL-THROUGH + case 47: // normal screen buffer + case 1047: // normal screen buffer - clearing it first + // Ensure the selection manager has the correct buffer + this._bufferService.buffers.activateNormalBuffer(); + if (params.params[i] === 1049) { + this.restoreCursor(); + } + this._coreService.isCursorInitialized = true; + this._onRequestRefreshRows.fire(0, this._bufferService.rows - 1); + this._onRequestSyncScrollBar.fire(); + break; + case 2004: // bracketed paste mode (https://cirw.in/blog/bracketed-paste) + this._coreService.decPrivateModes.bracketedPasteMode = false; + break; + } + } + return true; + } + + /** + * Helper to write color information packed with color mode. + */ + private _updateAttrColor(color: number, mode: number, c1: number, c2: number, c3: number): number { + if (mode === 2) { + color |= Attributes.CM_RGB; + color &= ~Attributes.RGB_MASK; + color |= AttributeData.fromColorRGB([c1, c2, c3]); + } else if (mode === 5) { + color &= ~(Attributes.CM_MASK | Attributes.PCOLOR_MASK); + color |= Attributes.CM_P256 | (c1 & 0xff); + } + return color; + } + + /** + * Helper to extract and apply color params/subparams. + * Returns advance for params index. + */ + private _extractColor(params: IParams, pos: number, attr: IAttributeData): number { + // normalize params + // meaning: [target, CM, ign, val, val, val] + // RGB : [ 38/48, 2, ign, r, g, b] + // P256 : [ 38/48, 5, ign, v, ign, ign] + const accu = [0, 0, -1, 0, 0, 0]; + + // alignment placeholder for non color space sequences + let cSpace = 0; + + // return advance we took in params + let advance = 0; + + do { + accu[advance + cSpace] = params.params[pos + advance]; + if (params.hasSubParams(pos + advance)) { + const subparams = params.getSubParams(pos + advance)!; + let i = 0; + do { + if (accu[1] === 5) { + cSpace = 1; + } + accu[advance + i + 1 + cSpace] = subparams[i]; + } while (++i < subparams.length && i + advance + 1 + cSpace < accu.length); + break; + } + // exit early if can decide color mode with semicolons + if ((accu[1] === 5 && advance + cSpace >= 2) + || (accu[1] === 2 && advance + cSpace >= 5)) { + break; + } + // offset colorSpace slot for semicolon mode + if (accu[1]) { + cSpace = 1; + } + } while (++advance + pos < params.length && advance + cSpace < accu.length); + + // set default values to 0 + for (let i = 2; i < accu.length; ++i) { + if (accu[i] === -1) { + accu[i] = 0; + } + } + + // apply colors + switch (accu[0]) { + case 38: + attr.fg = this._updateAttrColor(attr.fg, accu[1], accu[3], accu[4], accu[5]); + break; + case 48: + attr.bg = this._updateAttrColor(attr.bg, accu[1], accu[3], accu[4], accu[5]); + break; + case 58: + attr.extended = attr.extended.clone(); + attr.extended.underlineColor = this._updateAttrColor(attr.extended.underlineColor, accu[1], accu[3], accu[4], accu[5]); + } + + return advance; + } + + /** + * SGR 4 subparams: + * 4:0 - equal to SGR 24 (turn off all underline) + * 4:1 - equal to SGR 4 (single underline) + * 4:2 - equal to SGR 21 (double underline) + * 4:3 - curly underline + * 4:4 - dotted underline + * 4:5 - dashed underline + */ + private _processUnderline(style: number, attr: IAttributeData): void { + // treat extended attrs as immutable, thus always clone from old one + // this is needed since the buffer only holds references to it + attr.extended = attr.extended.clone(); + + // default to 1 == single underline + if (!~style || style > 5) { + style = 1; + } + attr.extended.underlineStyle = style; + attr.fg |= FgFlags.UNDERLINE; + + // 0 deactivates underline + if (style === 0) { + attr.fg &= ~FgFlags.UNDERLINE; + } + + // update HAS_EXTENDED in BG + attr.updateExtended(); + } + + /** + * CSI Pm m Character Attributes (SGR). + * + * @vt: #P[See below for supported attributes.] CSI SGR "Select Graphic Rendition" "CSI Pm m" "Set/Reset various text attributes." + * SGR selects one or more character attributes at the same time. Multiple params (up to 32) + * are applied in order from left to right. The changed attributes are applied to all new + * characters received. If you move characters in the viewport by scrolling or any other means, + * then the attributes move with the characters. + * + * Supported param values by SGR: + * + * | Param | Meaning | Support | + * | --------- | -------------------------------------------------------- | ------- | + * | 0 | Normal (default). Resets any other preceding SGR. | #Y | + * | 1 | Bold. (also see `options.drawBoldTextInBrightColors`) | #Y | + * | 2 | Faint, decreased intensity. | #Y | + * | 3 | Italic. | #Y | + * | 4 | Underlined (see below for style support). | #Y | + * | 5 | Slowly blinking. | #N | + * | 6 | Rapidly blinking. | #N | + * | 7 | Inverse. Flips foreground and background color. | #Y | + * | 8 | Invisible (hidden). | #Y | + * | 9 | Crossed-out characters (strikethrough). | #Y | + * | 21 | Doubly underlined. | #P[Currently outputs a single underline.] | + * | 22 | Normal (neither bold nor faint). | #Y | + * | 23 | No italic. | #Y | + * | 24 | Not underlined. | #Y | + * | 25 | Steady (not blinking). | #Y | + * | 27 | Positive (not inverse). | #Y | + * | 28 | Visible (not hidden). | #Y | + * | 29 | Not Crossed-out (strikethrough). | #Y | + * | 30 | Foreground color: Black. | #Y | + * | 31 | Foreground color: Red. | #Y | + * | 32 | Foreground color: Green. | #Y | + * | 33 | Foreground color: Yellow. | #Y | + * | 34 | Foreground color: Blue. | #Y | + * | 35 | Foreground color: Magenta. | #Y | + * | 36 | Foreground color: Cyan. | #Y | + * | 37 | Foreground color: White. | #Y | + * | 38 | Foreground color: Extended color. | #P[Support for RGB and indexed colors, see below.] | + * | 39 | Foreground color: Default (original). | #Y | + * | 40 | Background color: Black. | #Y | + * | 41 | Background color: Red. | #Y | + * | 42 | Background color: Green. | #Y | + * | 43 | Background color: Yellow. | #Y | + * | 44 | Background color: Blue. | #Y | + * | 45 | Background color: Magenta. | #Y | + * | 46 | Background color: Cyan. | #Y | + * | 47 | Background color: White. | #Y | + * | 48 | Background color: Extended color. | #P[Support for RGB and indexed colors, see below.] | + * | 49 | Background color: Default (original). | #Y | + * | 90 - 97 | Bright foreground color (analogous to 30 - 37). | #Y | + * | 100 - 107 | Bright background color (analogous to 40 - 47). | #Y | + * + * Underline supports subparams to denote the style in the form `4 : x`: + * + * | x | Meaning | Support | + * | ------ | ------------------------------------------------------------- | ------- | + * | 0 | No underline. Same as `SGR 24 m`. | #Y | + * | 1 | Single underline. Same as `SGR 4 m`. | #Y | + * | 2 | Double underline. | #P[Currently outputs a single underline.] | + * | 3 | Curly underline. | #P[Currently outputs a single underline.] | + * | 4 | Dotted underline. | #P[Currently outputs a single underline.] | + * | 5 | Dashed underline. | #P[Currently outputs a single underline.] | + * | other | Single underline. Same as `SGR 4 m`. | #Y | + * + * Extended colors are supported for foreground (Ps=38) and background (Ps=48) as follows: + * + * | Ps + 1 | Meaning | Support | + * | ------ | ------------------------------------------------------------- | ------- | + * | 0 | Implementation defined. | #N | + * | 1 | Transparent. | #N | + * | 2 | RGB color as `Ps ; 2 ; R ; G ; B` or `Ps : 2 : : R : G : B`. | #Y | + * | 3 | CMY color. | #N | + * | 4 | CMYK color. | #N | + * | 5 | Indexed (256 colors) as `Ps ; 5 ; INDEX` or `Ps : 5 : INDEX`. | #Y | + * + * + * FIXME: blinking is implemented in attrs, but not working in renderers? + * FIXME: remove dead branch for p=100 + */ + public charAttributes(params: IParams): boolean { + // Optimize a single SGR0. + if (params.length === 1 && params.params[0] === 0) { + this._curAttrData.fg = DEFAULT_ATTR_DATA.fg; + this._curAttrData.bg = DEFAULT_ATTR_DATA.bg; + return true; + } + + const l = params.length; + let p; + const attr = this._curAttrData; + + for (let i = 0; i < l; i++) { + p = params.params[i]; + if (p >= 30 && p <= 37) { + // fg color 8 + attr.fg &= ~(Attributes.CM_MASK | Attributes.PCOLOR_MASK); + attr.fg |= Attributes.CM_P16 | (p - 30); + } else if (p >= 40 && p <= 47) { + // bg color 8 + attr.bg &= ~(Attributes.CM_MASK | Attributes.PCOLOR_MASK); + attr.bg |= Attributes.CM_P16 | (p - 40); + } else if (p >= 90 && p <= 97) { + // fg color 16 + attr.fg &= ~(Attributes.CM_MASK | Attributes.PCOLOR_MASK); + attr.fg |= Attributes.CM_P16 | (p - 90) | 8; + } else if (p >= 100 && p <= 107) { + // bg color 16 + attr.bg &= ~(Attributes.CM_MASK | Attributes.PCOLOR_MASK); + attr.bg |= Attributes.CM_P16 | (p - 100) | 8; + } else if (p === 0) { + // default + attr.fg = DEFAULT_ATTR_DATA.fg; + attr.bg = DEFAULT_ATTR_DATA.bg; + } else if (p === 1) { + // bold text + attr.fg |= FgFlags.BOLD; + } else if (p === 3) { + // italic text + attr.bg |= BgFlags.ITALIC; + } else if (p === 4) { + // underlined text + attr.fg |= FgFlags.UNDERLINE; + this._processUnderline(params.hasSubParams(i) ? params.getSubParams(i)![0] : UnderlineStyle.SINGLE, attr); + } else if (p === 5) { + // blink + attr.fg |= FgFlags.BLINK; + } else if (p === 7) { + // inverse and positive + // test with: echo -e '\e[31m\e[42mhello\e[7mworld\e[27mhi\e[m' + attr.fg |= FgFlags.INVERSE; + } else if (p === 8) { + // invisible + attr.fg |= FgFlags.INVISIBLE; + } else if (p === 9) { + // strikethrough + attr.fg |= FgFlags.STRIKETHROUGH; + } else if (p === 2) { + // dimmed text + attr.bg |= BgFlags.DIM; + } else if (p === 21) { + // double underline + this._processUnderline(UnderlineStyle.DOUBLE, attr); + } else if (p === 22) { + // not bold nor faint + attr.fg &= ~FgFlags.BOLD; + attr.bg &= ~BgFlags.DIM; + } else if (p === 23) { + // not italic + attr.bg &= ~BgFlags.ITALIC; + } else if (p === 24) { + // not underlined + attr.fg &= ~FgFlags.UNDERLINE; + } else if (p === 25) { + // not blink + attr.fg &= ~FgFlags.BLINK; + } else if (p === 27) { + // not inverse + attr.fg &= ~FgFlags.INVERSE; + } else if (p === 28) { + // not invisible + attr.fg &= ~FgFlags.INVISIBLE; + } else if (p === 29) { + // not strikethrough + attr.fg &= ~FgFlags.STRIKETHROUGH; + } else if (p === 39) { + // reset fg + attr.fg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK); + attr.fg |= DEFAULT_ATTR_DATA.fg & (Attributes.PCOLOR_MASK | Attributes.RGB_MASK); + } else if (p === 49) { + // reset bg + attr.bg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK); + attr.bg |= DEFAULT_ATTR_DATA.bg & (Attributes.PCOLOR_MASK | Attributes.RGB_MASK); + } else if (p === 38 || p === 48 || p === 58) { + // fg color 256 and RGB + i += this._extractColor(params, i, attr); + } else if (p === 59) { + attr.extended = attr.extended.clone(); + attr.extended.underlineColor = -1; + attr.updateExtended(); + } else if (p === 100) { // FIXME: dead branch, p=100 already handled above! + // reset fg/bg + attr.fg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK); + attr.fg |= DEFAULT_ATTR_DATA.fg & (Attributes.PCOLOR_MASK | Attributes.RGB_MASK); + attr.bg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK); + attr.bg |= DEFAULT_ATTR_DATA.bg & (Attributes.PCOLOR_MASK | Attributes.RGB_MASK); + } else { + this._logService.debug('Unknown SGR attribute: %d.', p); + } + } + return true; + } + + /** + * CSI Ps n Device Status Report (DSR). + * Ps = 5 -> Status Report. Result (``OK'') is + * CSI 0 n + * Ps = 6 -> Report Cursor Position (CPR) [row;column]. + * Result is + * CSI r ; c R + * CSI ? Ps n + * Device Status Report (DSR, DEC-specific). + * Ps = 6 -> Report Cursor Position (CPR) [row;column] as CSI + * ? r ; c R (assumes page is zero). + * Ps = 1 5 -> Report Printer status as CSI ? 1 0 n (ready). + * or CSI ? 1 1 n (not ready). + * Ps = 2 5 -> Report UDK status as CSI ? 2 0 n (unlocked) + * or CSI ? 2 1 n (locked). + * Ps = 2 6 -> Report Keyboard status as + * CSI ? 2 7 ; 1 ; 0 ; 0 n (North American). + * The last two parameters apply to VT400 & up, and denote key- + * board ready and LK01 respectively. + * Ps = 5 3 -> Report Locator status as + * CSI ? 5 3 n Locator available, if compiled-in, or + * CSI ? 5 0 n No Locator, if not. + * + * @vt: #Y CSI DSR "Device Status Report" "CSI Ps n" "Request cursor position (CPR) with `Ps` = 6." + */ + public deviceStatus(params: IParams): boolean { + switch (params.params[0]) { + case 5: + // status report + this._coreService.triggerDataEvent(`${C0.ESC}[0n`); + break; + case 6: + // cursor position + const y = this._activeBuffer.y + 1; + const x = this._activeBuffer.x + 1; + this._coreService.triggerDataEvent(`${C0.ESC}[${y};${x}R`); + break; + } + return true; + } + + // @vt: #P[Only CPR is supported.] CSI DECDSR "DEC Device Status Report" "CSI ? Ps n" "Only CPR is supported (same as DSR)." + public deviceStatusPrivate(params: IParams): boolean { + // modern xterm doesnt seem to + // respond to any of these except ?6, 6, and 5 + switch (params.params[0]) { + case 6: + // cursor position + const y = this._activeBuffer.y + 1; + const x = this._activeBuffer.x + 1; + this._coreService.triggerDataEvent(`${C0.ESC}[?${y};${x}R`); + break; + case 15: + // no printer + // this.handler(C0.ESC + '[?11n'); + break; + case 25: + // dont support user defined keys + // this.handler(C0.ESC + '[?21n'); + break; + case 26: + // north american keyboard + // this.handler(C0.ESC + '[?27;1;0;0n'); + break; + case 53: + // no dec locator/mouse + // this.handler(C0.ESC + '[?50n'); + break; + } + return true; + } + + /** + * CSI ! p Soft terminal reset (DECSTR). + * http://vt100.net/docs/vt220-rm/table4-10.html + * + * @vt: #Y CSI DECSTR "Soft Terminal Reset" "CSI ! p" "Reset several terminal attributes to initial state." + * There are two terminal reset sequences - RIS and DECSTR. While RIS performs almost a full terminal bootstrap, + * DECSTR only resets certain attributes. For most needs DECSTR should be sufficient. + * + * The following terminal attributes are reset to default values: + * - IRM is reset (dafault = false) + * - scroll margins are reset (default = viewport size) + * - erase attributes are reset to default + * - charsets are reset + * - DECSC data is reset to initial values + * - DECOM is reset to absolute mode + * + * + * FIXME: there are several more attributes missing (see VT520 manual) + */ + public softReset(params: IParams): boolean { + this._coreService.isCursorHidden = false; + this._onRequestSyncScrollBar.fire(); + this._activeBuffer.scrollTop = 0; + this._activeBuffer.scrollBottom = this._bufferService.rows - 1; + this._curAttrData = DEFAULT_ATTR_DATA.clone(); + this._coreService.reset(); + this._charsetService.reset(); + + // reset DECSC data + this._activeBuffer.savedX = 0; + this._activeBuffer.savedY = this._activeBuffer.ybase; + this._activeBuffer.savedCurAttrData.fg = this._curAttrData.fg; + this._activeBuffer.savedCurAttrData.bg = this._curAttrData.bg; + this._activeBuffer.savedCharset = this._charsetService.charset; + + // reset DECOM + this._coreService.decPrivateModes.origin = false; + return true; + } + + /** + * CSI Ps SP q Set cursor style (DECSCUSR, VT520). + * Ps = 0 -> blinking block. + * Ps = 1 -> blinking block (default). + * Ps = 2 -> steady block. + * Ps = 3 -> blinking underline. + * Ps = 4 -> steady underline. + * Ps = 5 -> blinking bar (xterm). + * Ps = 6 -> steady bar (xterm). + * + * @vt: #Y CSI DECSCUSR "Set Cursor Style" "CSI Ps SP q" "Set cursor style." + * Supported cursor styles: + * - empty, 0 or 1: steady block + * - 2: blink block + * - 3: steady underline + * - 4: blink underline + * - 5: steady bar + * - 6: blink bar + */ + public setCursorStyle(params: IParams): boolean { + const param = params.params[0] || 1; + switch (param) { + case 1: + case 2: + this._optionsService.options.cursorStyle = 'block'; + break; + case 3: + case 4: + this._optionsService.options.cursorStyle = 'underline'; + break; + case 5: + case 6: + this._optionsService.options.cursorStyle = 'bar'; + break; + } + const isBlinking = param % 2 === 1; + this._optionsService.options.cursorBlink = isBlinking; + return true; + } + + /** + * CSI Ps ; Ps r + * Set Scrolling Region [top;bottom] (default = full size of win- + * dow) (DECSTBM). + * + * @vt: #Y CSI DECSTBM "Set Top and Bottom Margin" "CSI Ps ; Ps r" "Set top and bottom margins of the viewport [top;bottom] (default = viewport size)." + */ + public setScrollRegion(params: IParams): boolean { + const top = params.params[0] || 1; + let bottom: number; + + if (params.length < 2 || (bottom = params.params[1]) > this._bufferService.rows || bottom === 0) { + bottom = this._bufferService.rows; + } + + if (bottom > top) { + this._activeBuffer.scrollTop = top - 1; + this._activeBuffer.scrollBottom = bottom - 1; + this._setCursor(0, 0); + } + return true; + } + + /** + * CSI Ps ; Ps ; Ps t - Various window manipulations and reports (xterm) + * + * Note: Only those listed below are supported. All others are left to integrators and + * need special treatment based on the embedding environment. + * + * Ps = 1 4 supported + * Report xterm text area size in pixels. + * Result is CSI 4 ; height ; width t + * Ps = 14 ; 2 not implemented + * Ps = 16 supported + * Report xterm character cell size in pixels. + * Result is CSI 6 ; height ; width t + * Ps = 18 supported + * Report the size of the text area in characters. + * Result is CSI 8 ; height ; width t + * Ps = 20 supported + * Report xterm window's icon label. + * Result is OSC L label ST + * Ps = 21 supported + * Report xterm window's title. + * Result is OSC l label ST + * Ps = 22 ; 0 -> Save xterm icon and window title on stack. supported + * Ps = 22 ; 1 -> Save xterm icon title on stack. supported + * Ps = 22 ; 2 -> Save xterm window title on stack. supported + * Ps = 23 ; 0 -> Restore xterm icon and window title from stack. supported + * Ps = 23 ; 1 -> Restore xterm icon title from stack. supported + * Ps = 23 ; 2 -> Restore xterm window title from stack. supported + * Ps >= 24 not implemented + */ + public windowOptions(params: IParams): boolean { + if (!paramToWindowOption(params.params[0], this._optionsService.rawOptions.windowOptions)) { + return true; + } + const second = (params.length > 1) ? params.params[1] : 0; + switch (params.params[0]) { + case 14: // GetWinSizePixels, returns CSI 4 ; height ; width t + if (second !== 2) { + this._onRequestWindowsOptionsReport.fire(WindowsOptionsReportType.GET_WIN_SIZE_PIXELS); + } + break; + case 16: // GetCellSizePixels, returns CSI 6 ; height ; width t + this._onRequestWindowsOptionsReport.fire(WindowsOptionsReportType.GET_CELL_SIZE_PIXELS); + break; + case 18: // GetWinSizeChars, returns CSI 8 ; height ; width t + if (this._bufferService) { + this._coreService.triggerDataEvent(`${C0.ESC}[8;${this._bufferService.rows};${this._bufferService.cols}t`); + } + break; + case 22: // PushTitle + if (second === 0 || second === 2) { + this._windowTitleStack.push(this._windowTitle); + if (this._windowTitleStack.length > STACK_LIMIT) { + this._windowTitleStack.shift(); + } + } + if (second === 0 || second === 1) { + this._iconNameStack.push(this._iconName); + if (this._iconNameStack.length > STACK_LIMIT) { + this._iconNameStack.shift(); + } + } + break; + case 23: // PopTitle + if (second === 0 || second === 2) { + if (this._windowTitleStack.length) { + this.setTitle(this._windowTitleStack.pop()!); + } + } + if (second === 0 || second === 1) { + if (this._iconNameStack.length) { + this.setIconName(this._iconNameStack.pop()!); + } + } + break; + } + return true; + } + + + /** + * CSI s + * ESC 7 + * Save cursor (ANSI.SYS). + * + * @vt: #P[TODO...] CSI SCOSC "Save Cursor" "CSI s" "Save cursor position, charmap and text attributes." + * @vt: #Y ESC SC "Save Cursor" "ESC 7" "Save cursor position, charmap and text attributes." + */ + public saveCursor(params?: IParams): boolean { + this._activeBuffer.savedX = this._activeBuffer.x; + this._activeBuffer.savedY = this._activeBuffer.ybase + this._activeBuffer.y; + this._activeBuffer.savedCurAttrData.fg = this._curAttrData.fg; + this._activeBuffer.savedCurAttrData.bg = this._curAttrData.bg; + this._activeBuffer.savedCharset = this._charsetService.charset; + return true; + } + + + /** + * CSI u + * ESC 8 + * Restore cursor (ANSI.SYS). + * + * @vt: #P[TODO...] CSI SCORC "Restore Cursor" "CSI u" "Restore cursor position, charmap and text attributes." + * @vt: #Y ESC RC "Restore Cursor" "ESC 8" "Restore cursor position, charmap and text attributes." + */ + public restoreCursor(params?: IParams): boolean { + this._activeBuffer.x = this._activeBuffer.savedX || 0; + this._activeBuffer.y = Math.max(this._activeBuffer.savedY - this._activeBuffer.ybase, 0); + this._curAttrData.fg = this._activeBuffer.savedCurAttrData.fg; + this._curAttrData.bg = this._activeBuffer.savedCurAttrData.bg; + this._charsetService.charset = (this as any)._savedCharset; + if (this._activeBuffer.savedCharset) { + this._charsetService.charset = this._activeBuffer.savedCharset; + } + this._restrictCursor(); + return true; + } + + + /** + * OSC 2; <data> ST (set window title) + * Proxy to set window title. + * + * @vt: #P[Icon name is not exposed.] OSC 0 "Set Windows Title and Icon Name" "OSC 0 ; Pt BEL" "Set window title and icon name." + * Icon name is not supported. For Window Title see below. + * + * @vt: #Y OSC 2 "Set Windows Title" "OSC 2 ; Pt BEL" "Set window title." + * xterm.js does not manipulate the title directly, instead exposes changes via the event `Terminal.onTitleChange`. + */ + public setTitle(data: string): boolean { + this._windowTitle = data; + this._onTitleChange.fire(data); + return true; + } + + /** + * OSC 1; <data> ST + * Note: Icon name is not exposed. + */ + public setIconName(data: string): boolean { + this._iconName = data; + return true; + } + + /** + * OSC 4; <num> ; <text> ST (set ANSI color <num> to <text>) + * + * @vt: #Y OSC 4 "Set ANSI color" "OSC 4 ; c ; spec BEL" "Change color number `c` to the color specified by `spec`." + * `c` is the color index between 0 and 255. The color format of `spec` is derived from `XParseColor` (see OSC 10 for supported formats). + * There may be multipe `c ; spec` pairs present in the same instruction. + * If `spec` contains `?` the terminal returns a sequence with the currently set color. + */ + public setOrReportIndexedColor(data: string): boolean { + const event: IColorEvent = []; + const slots = data.split(';'); + while (slots.length > 1) { + const idx = slots.shift() as string; + const spec = slots.shift() as string; + if (/^\d+$/.exec(idx)) { + const index = parseInt(idx); + if (0 <= index && index < 256) { + if (spec === '?') { + event.push({ type: ColorRequestType.REPORT, index }); + } else { + const color = parseColor(spec); + if (color) { + event.push({ type: ColorRequestType.SET, index, color }); + } + } + } + } + } + if (event.length) { + this._onColor.fire(event); + } + return true; + } + + // special colors - OSC 10 | 11 | 12 + private _specialColors = [ColorIndex.FOREGROUND, ColorIndex.BACKGROUND, ColorIndex.CURSOR]; + + /** + * Apply colors requests for special colors in OSC 10 | 11 | 12. + * Since these commands are stacking from multiple parameters, + * we handle them in a loop with an entry offset to `_specialColors`. + */ + private _setOrReportSpecialColor(data: string, offset: number): boolean { + const slots = data.split(';'); + for (let i = 0; i < slots.length; ++i, ++offset) { + if (offset >= this._specialColors.length) break; + if (slots[i] === '?') { + this._onColor.fire([{ type: ColorRequestType.REPORT, index: this._specialColors[offset] }]); + } else { + const color = parseColor(slots[i]); + if (color) { + this._onColor.fire([{ type: ColorRequestType.SET, index: this._specialColors[offset], color }]); + } + } + } + return true; + } + + /** + * OSC 10 ; <xcolor name>|<?> ST - set or query default foreground color + * + * @vt: #Y OSC 10 "Set or query default foreground color" "OSC 10 ; Pt BEL" "Set or query default foreground color." + * To set the color, the following color specification formats are supported: + * - `rgb:<red>/<green>/<blue>` for `<red>, <green>, <blue>` in `h | hh | hhh | hhhh`, where + * `h` is a single hexadecimal digit (case insignificant). The different widths scale + * from 4 bit (`h`) to 16 bit (`hhhh`) and get converted to 8 bit (`hh`). + * - `#RGB` - 4 bits per channel, expanded to `#R0G0B0` + * - `#RRGGBB` - 8 bits per channel + * - `#RRRGGGBBB` - 12 bits per channel, truncated to `#RRGGBB` + * - `#RRRRGGGGBBBB` - 16 bits per channel, truncated to `#RRGGBB` + * + * **Note:** X11 named colors are currently unsupported. + * + * If `Pt` contains `?` instead of a color specification, the terminal + * returns a sequence with the current default foreground color + * (use that sequence to restore the color after changes). + * + * **Note:** Other than xterm, xterm.js does not support OSC 12 - 19. + * Therefore stacking multiple `Pt` separated by `;` only works for the first two entries. + */ + public setOrReportFgColor(data: string): boolean { + return this._setOrReportSpecialColor(data, 0); + } + + /** + * OSC 11 ; <xcolor name>|<?> ST - set or query default background color + * + * @vt: #Y OSC 11 "Set or query default background color" "OSC 11 ; Pt BEL" "Same as OSC 10, but for default background." + */ + public setOrReportBgColor(data: string): boolean { + return this._setOrReportSpecialColor(data, 1); + } + + /** + * OSC 12 ; <xcolor name>|<?> ST - set or query default cursor color + * + * @vt: #Y OSC 12 "Set or query default cursor color" "OSC 12 ; Pt BEL" "Same as OSC 10, but for default cursor color." + */ + public setOrReportCursorColor(data: string): boolean { + return this._setOrReportSpecialColor(data, 2); + } + + /** + * OSC 104 ; <num> ST - restore ANSI color <num> + * + * @vt: #Y OSC 104 "Reset ANSI color" "OSC 104 ; c BEL" "Reset color number `c` to themed color." + * `c` is the color index between 0 and 255. This function restores the default color for `c` as + * specified by the loaded theme. Any number of `c` parameters may be given. + * If no parameters are given, the entire indexed color table will be reset. + */ + public restoreIndexedColor(data: string): boolean { + if (!data) { + this._onColor.fire([{ type: ColorRequestType.RESTORE }]); + return true; + } + const event: IColorEvent = []; + const slots = data.split(';'); + for (let i = 0; i < slots.length; ++i) { + if (/^\d+$/.exec(slots[i])) { + const index = parseInt(slots[i]); + if (0 <= index && index < 256) { + event.push({ type: ColorRequestType.RESTORE, index }); + } + } + } + if (event.length) { + this._onColor.fire(event); + } + return true; + } + + /** + * OSC 110 ST - restore default foreground color + * + * @vt: #Y OSC 110 "Restore default foreground color" "OSC 110 BEL" "Restore default foreground to themed color." + */ + public restoreFgColor(data: string): boolean { + this._onColor.fire([{ type: ColorRequestType.RESTORE, index: ColorIndex.FOREGROUND }]); + return true; + } + + /** + * OSC 111 ST - restore default background color + * + * @vt: #Y OSC 111 "Restore default background color" "OSC 111 BEL" "Restore default background to themed color." + */ + public restoreBgColor(data: string): boolean { + this._onColor.fire([{ type: ColorRequestType.RESTORE, index: ColorIndex.BACKGROUND }]); + return true; + } + + /** + * OSC 112 ST - restore default cursor color + * + * @vt: #Y OSC 112 "Restore default cursor color" "OSC 112 BEL" "Restore default cursor to themed color." + */ + public restoreCursorColor(data: string): boolean { + this._onColor.fire([{ type: ColorRequestType.RESTORE, index: ColorIndex.CURSOR }]); + return true; + } + + /** + * ESC E + * C1.NEL + * DEC mnemonic: NEL (https://vt100.net/docs/vt510-rm/NEL) + * Moves cursor to first position on next line. + * + * @vt: #Y C1 NEL "Next Line" "\x85" "Move the cursor to the beginning of the next row." + * @vt: #Y ESC NEL "Next Line" "ESC E" "Move the cursor to the beginning of the next row." + */ + public nextLine(): boolean { + this._activeBuffer.x = 0; + this.index(); + return true; + } + + /** + * ESC = + * DEC mnemonic: DECKPAM (https://vt100.net/docs/vt510-rm/DECKPAM.html) + * Enables the numeric keypad to send application sequences to the host. + */ + public keypadApplicationMode(): boolean { + this._logService.debug('Serial port requested application keypad.'); + this._coreService.decPrivateModes.applicationKeypad = true; + this._onRequestSyncScrollBar.fire(); + return true; + } + + /** + * ESC > + * DEC mnemonic: DECKPNM (https://vt100.net/docs/vt510-rm/DECKPNM.html) + * Enables the keypad to send numeric characters to the host. + */ + public keypadNumericMode(): boolean { + this._logService.debug('Switching back to normal keypad.'); + this._coreService.decPrivateModes.applicationKeypad = false; + this._onRequestSyncScrollBar.fire(); + return true; + } + + /** + * ESC % @ + * ESC % G + * Select default character set. UTF-8 is not supported (string are unicode anyways) + * therefore ESC % G does the same. + */ + public selectDefaultCharset(): boolean { + this._charsetService.setgLevel(0); + this._charsetService.setgCharset(0, DEFAULT_CHARSET); // US (default) + return true; + } + + /** + * ESC ( C + * Designate G0 Character Set, VT100, ISO 2022. + * ESC ) C + * Designate G1 Character Set (ISO 2022, VT100). + * ESC * C + * Designate G2 Character Set (ISO 2022, VT220). + * ESC + C + * Designate G3 Character Set (ISO 2022, VT220). + * ESC - C + * Designate G1 Character Set (VT300). + * ESC . C + * Designate G2 Character Set (VT300). + * ESC / C + * Designate G3 Character Set (VT300). C = A -> ISO Latin-1 Supplemental. - Supported? + */ + public selectCharset(collectAndFlag: string): boolean { + if (collectAndFlag.length !== 2) { + this.selectDefaultCharset(); + return true; + } + if (collectAndFlag[0] === '/') { + return true; // TODO: Is this supported? + } + this._charsetService.setgCharset(GLEVEL[collectAndFlag[0]], CHARSETS[collectAndFlag[1]] || DEFAULT_CHARSET); + return true; + } + + /** + * ESC D + * C1.IND + * DEC mnemonic: IND (https://vt100.net/docs/vt510-rm/IND.html) + * Moves the cursor down one line in the same column. + * + * @vt: #Y C1 IND "Index" "\x84" "Move the cursor one line down scrolling if needed." + * @vt: #Y ESC IND "Index" "ESC D" "Move the cursor one line down scrolling if needed." + */ + public index(): boolean { + this._restrictCursor(); + this._activeBuffer.y++; + if (this._activeBuffer.y === this._activeBuffer.scrollBottom + 1) { + this._activeBuffer.y--; + this._bufferService.scroll(this._eraseAttrData()); + } else if (this._activeBuffer.y >= this._bufferService.rows) { + this._activeBuffer.y = this._bufferService.rows - 1; + } + this._restrictCursor(); + return true; + } + + /** + * ESC H + * C1.HTS + * DEC mnemonic: HTS (https://vt100.net/docs/vt510-rm/HTS.html) + * Sets a horizontal tab stop at the column position indicated by + * the value of the active column when the terminal receives an HTS. + * + * @vt: #Y C1 HTS "Horizontal Tabulation Set" "\x88" "Places a tab stop at the current cursor position." + * @vt: #Y ESC HTS "Horizontal Tabulation Set" "ESC H" "Places a tab stop at the current cursor position." + */ + public tabSet(): boolean { + this._activeBuffer.tabs[this._activeBuffer.x] = true; + return true; + } + + /** + * ESC M + * C1.RI + * DEC mnemonic: HTS + * Moves the cursor up one line in the same column. If the cursor is at the top margin, + * the page scrolls down. + * + * @vt: #Y ESC IR "Reverse Index" "ESC M" "Move the cursor one line up scrolling if needed." + */ + public reverseIndex(): boolean { + this._restrictCursor(); + if (this._activeBuffer.y === this._activeBuffer.scrollTop) { + // possibly move the code below to term.reverseScroll(); + // test: echo -ne '\e[1;1H\e[44m\eM\e[0m' + // blankLine(true) is xterm/linux behavior + const scrollRegionHeight = this._activeBuffer.scrollBottom - this._activeBuffer.scrollTop; + this._activeBuffer.lines.shiftElements(this._activeBuffer.ybase + this._activeBuffer.y, scrollRegionHeight, 1); + this._activeBuffer.lines.set(this._activeBuffer.ybase + this._activeBuffer.y, this._activeBuffer.getBlankLine(this._eraseAttrData())); + this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom); + } else { + this._activeBuffer.y--; + this._restrictCursor(); // quickfix to not run out of bounds + } + return true; + } + + /** + * ESC c + * DEC mnemonic: RIS (https://vt100.net/docs/vt510-rm/RIS.html) + * Reset to initial state. + */ + public fullReset(): boolean { + this._parser.reset(); + this._onRequestReset.fire(); + return true; + } + + public reset(): void { + this._curAttrData = DEFAULT_ATTR_DATA.clone(); + this._eraseAttrDataInternal = DEFAULT_ATTR_DATA.clone(); + } + + /** + * back_color_erase feature for xterm. + */ + private _eraseAttrData(): IAttributeData { + this._eraseAttrDataInternal.bg &= ~(Attributes.CM_MASK | 0xFFFFFF); + this._eraseAttrDataInternal.bg |= this._curAttrData.bg & ~0xFC000000; + return this._eraseAttrDataInternal; + } + + /** + * ESC n + * ESC o + * ESC | + * ESC } + * ESC ~ + * DEC mnemonic: LS (https://vt100.net/docs/vt510-rm/LS.html) + * When you use a locking shift, the character set remains in GL or GR until + * you use another locking shift. (partly supported) + */ + public setgLevel(level: number): boolean { + this._charsetService.setgLevel(level); + return true; + } + + /** + * ESC # 8 + * DEC mnemonic: DECALN (https://vt100.net/docs/vt510-rm/DECALN.html) + * This control function fills the complete screen area with + * a test pattern (E) used for adjusting screen alignment. + * + * @vt: #Y ESC DECALN "Screen Alignment Pattern" "ESC # 8" "Fill viewport with a test pattern (E)." + */ + public screenAlignmentPattern(): boolean { + // prepare cell data + const cell = new CellData(); + cell.content = 1 << Content.WIDTH_SHIFT | 'E'.charCodeAt(0); + cell.fg = this._curAttrData.fg; + cell.bg = this._curAttrData.bg; + + + this._setCursor(0, 0); + for (let yOffset = 0; yOffset < this._bufferService.rows; ++yOffset) { + const row = this._activeBuffer.ybase + this._activeBuffer.y + yOffset; + const line = this._activeBuffer.lines.get(row); + if (line) { + line.fill(cell); + line.isWrapped = false; + } + } + this._dirtyRowService.markAllDirty(); + this._setCursor(0, 0); + return true; + } +} diff --git a/node_modules/xterm/src/common/Lifecycle.ts b/node_modules/xterm/src/common/Lifecycle.ts new file mode 100644 index 0000000..56bcfdc --- /dev/null +++ b/node_modules/xterm/src/common/Lifecycle.ts @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IDisposable } from 'common/Types'; + +/** + * A base class that can be extended to provide convenience methods for managing the lifecycle of an + * object and its components. + */ +export abstract class Disposable implements IDisposable { + protected _disposables: IDisposable[] = []; + protected _isDisposed: boolean = false; + + constructor() { + } + + /** + * Disposes the object, triggering the `dispose` method on all registered IDisposables. + */ + public dispose(): void { + this._isDisposed = true; + for (const d of this._disposables) { + d.dispose(); + } + this._disposables.length = 0; + } + + /** + * Registers a disposable object. + * @param d The disposable to register. + * @returns The disposable. + */ + public register<T extends IDisposable>(d: T): T { + this._disposables.push(d); + return d; + } + + /** + * Unregisters a disposable object if it has been registered, if not do + * nothing. + * @param d The disposable to unregister. + */ + public unregister<T extends IDisposable>(d: T): void { + const index = this._disposables.indexOf(d); + if (index !== -1) { + this._disposables.splice(index, 1); + } + } +} + +/** + * Dispose of all disposables in an array and set its length to 0. + */ +export function disposeArray(disposables: IDisposable[]): void { + for (const d of disposables) { + d.dispose(); + } + disposables.length = 0; +} + +/** + * Creates a disposable that will dispose of an array of disposables when disposed. + */ +export function getDisposeArrayDisposable(array: IDisposable[]): IDisposable { + return { dispose: () => disposeArray(array) }; +} diff --git a/node_modules/xterm/src/common/Platform.ts b/node_modules/xterm/src/common/Platform.ts new file mode 100644 index 0000000..7b823b1 --- /dev/null +++ b/node_modules/xterm/src/common/Platform.ts @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2016 The xterm.js authors. All rights reserved. + * @license MIT + */ + +interface INavigator { + userAgent: string; + language: string; + platform: string; +} + +// We're declaring a navigator global here as we expect it in all runtimes (node and browser), but +// we want this module to live in common. +declare const navigator: INavigator; + +const isNode = (typeof navigator === 'undefined') ? true : false; +const userAgent = (isNode) ? 'node' : navigator.userAgent; +const platform = (isNode) ? 'node' : navigator.platform; + +export const isFirefox = userAgent.includes('Firefox'); +export const isLegacyEdge = userAgent.includes('Edge'); +export const isSafari = /^((?!chrome|android).)*safari/i.test(userAgent); + +// Find the users platform. We use this to interpret the meta key +// and ISO third level shifts. +// http://stackoverflow.com/q/19877924/577598 +export const isMac = ['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K'].includes(platform); +export const isIpad = platform === 'iPad'; +export const isIphone = platform === 'iPhone'; +export const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].includes(platform); +export const isLinux = platform.indexOf('Linux') >= 0; diff --git a/node_modules/xterm/src/common/TypedArrayUtils.ts b/node_modules/xterm/src/common/TypedArrayUtils.ts new file mode 100644 index 0000000..158c717 --- /dev/null +++ b/node_modules/xterm/src/common/TypedArrayUtils.ts @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +export type TypedArray = Uint8Array | Uint16Array | Uint32Array | Uint8ClampedArray | Int8Array | Int16Array | Int32Array | Float32Array | Float64Array; + + +/** + * polyfill for TypedArray.fill + * This is needed to support .fill in all safari versions and IE 11. + */ +export function fill<T extends TypedArray>(array: T, value: number, start?: number, end?: number): T { + // all modern engines that support .fill + if (array.fill) { + return array.fill(value, start, end) as T; + } + return fillFallback(array, value, start, end); +} + +export function fillFallback<T extends TypedArray>(array: T, value: number, start: number = 0, end: number = array.length): T { + // safari and IE 11 + // since IE 11 does not support Array.prototype.fill either + // we cannot use the suggested polyfill from MDN + // instead we simply fall back to looping + if (start >= array.length) { + return array; + } + start = (array.length + start) % array.length; + if (end >= array.length) { + end = array.length; + } else { + end = (array.length + end) % array.length; + } + for (let i = start; i < end; ++i) { + array[i] = value; + } + return array; +} + +/** + * Concat two typed arrays `a` and `b`. + * Returns a new typed array. + */ +export function concat<T extends TypedArray>(a: T, b: T): T { + const result = new (a.constructor as any)(a.length + b.length); + result.set(a); + result.set(b, a.length); + return result; +} diff --git a/node_modules/xterm/src/common/Types.d.ts b/node_modules/xterm/src/common/Types.d.ts new file mode 100644 index 0000000..fee426e --- /dev/null +++ b/node_modules/xterm/src/common/Types.d.ts @@ -0,0 +1,483 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IFunctionIdentifier, ITerminalOptions as IPublicTerminalOptions } from 'xterm'; +import { IEvent, IEventEmitter } from 'common/EventEmitter'; +import { IDeleteEvent, IInsertEvent } from 'common/CircularList'; +import { IParams } from 'common/parser/Types'; +import { ICoreMouseService, ICoreService, IOptionsService, IUnicodeService } from 'common/services/Services'; +import { IBufferSet } from 'common/buffer/Types'; + +export interface ICoreTerminal { + coreMouseService: ICoreMouseService; + coreService: ICoreService; + optionsService: IOptionsService; + unicodeService: IUnicodeService; + buffers: IBufferSet; + options: ITerminalOptions; + registerCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean | Promise<boolean>): IDisposable; + registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean | Promise<boolean>): IDisposable; + registerEscHandler(id: IFunctionIdentifier, callback: () => boolean | Promise<boolean>): IDisposable; + registerOscHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable; +} + +export interface IDisposable { + dispose(): void; +} + +// TODO: The options that are not in the public API should be reviewed +export interface ITerminalOptions extends IPublicTerminalOptions { + [key: string]: any; + cancelEvents?: boolean; + convertEol?: boolean; + termName?: string; +} + +export type XtermListener = (...args: any[]) => void; + +/** + * A keyboard event interface which does not depend on the DOM, KeyboardEvent implicitly extends + * this event. + */ +export interface IKeyboardEvent { + altKey: boolean; + ctrlKey: boolean; + shiftKey: boolean; + metaKey: boolean; + /** @deprecated See KeyboardEvent.keyCode */ + keyCode: number; + key: string; + type: string; +} + +export interface IScrollEvent { + position: number; + source: ScrollSource; +} + +export const enum ScrollSource { + TERMINAL, + VIEWPORT, +} + +export interface ICircularList<T> { + length: number; + maxLength: number; + isFull: boolean; + + onDeleteEmitter: IEventEmitter<IDeleteEvent>; + onDelete: IEvent<IDeleteEvent>; + onInsertEmitter: IEventEmitter<IInsertEvent>; + onInsert: IEvent<IInsertEvent>; + onTrimEmitter: IEventEmitter<number>; + onTrim: IEvent<number>; + + get(index: number): T | undefined; + set(index: number, value: T): void; + push(value: T): void; + recycle(): T; + pop(): T | undefined; + splice(start: number, deleteCount: number, ...items: T[]): void; + trimStart(count: number): void; + shiftElements(start: number, count: number, offset: number): void; +} + +export const enum KeyboardResultType { + SEND_KEY, + SELECT_ALL, + PAGE_UP, + PAGE_DOWN +} + +export interface IKeyboardResult { + type: KeyboardResultType; + cancel: boolean; + key: string | undefined; +} + +export interface ICharset { + [key: string]: string | undefined; +} + +export type CharData = [number, string, number, number]; +export type IColorRGB = [number, number, number]; + +export interface IExtendedAttrs { + underlineStyle: number; + underlineColor: number; + clone(): IExtendedAttrs; + isEmpty(): boolean; +} + +/** Attribute data */ +export interface IAttributeData { + fg: number; + bg: number; + extended: IExtendedAttrs; + + clone(): IAttributeData; + + // flags + isInverse(): number; + isBold(): number; + isUnderline(): number; + isBlink(): number; + isInvisible(): number; + isItalic(): number; + isDim(): number; + isStrikethrough(): number; + + // color modes + getFgColorMode(): number; + getBgColorMode(): number; + isFgRGB(): boolean; + isBgRGB(): boolean; + isFgPalette(): boolean; + isBgPalette(): boolean; + isFgDefault(): boolean; + isBgDefault(): boolean; + isAttributeDefault(): boolean; + + // colors + getFgColor(): number; + getBgColor(): number; + + // extended attrs + hasExtendedAttrs(): number; + updateExtended(): void; + getUnderlineColor(): number; + getUnderlineColorMode(): number; + isUnderlineColorRGB(): boolean; + isUnderlineColorPalette(): boolean; + isUnderlineColorDefault(): boolean; + getUnderlineStyle(): number; +} + +/** Cell data */ +export interface ICellData extends IAttributeData { + content: number; + combinedData: string; + isCombined(): number; + getWidth(): number; + getChars(): string; + getCode(): number; + setFromCharData(value: CharData): void; + getAsCharData(): CharData; +} + +/** + * Interface for a line in the terminal buffer. + */ +export interface IBufferLine { + length: number; + isWrapped: boolean; + get(index: number): CharData; + set(index: number, value: CharData): void; + loadCell(index: number, cell: ICellData): ICellData; + setCell(index: number, cell: ICellData): void; + setCellFromCodePoint(index: number, codePoint: number, width: number, fg: number, bg: number, eAttrs: IExtendedAttrs): void; + addCodepointToCell(index: number, codePoint: number): void; + insertCells(pos: number, n: number, ch: ICellData, eraseAttr?: IAttributeData): void; + deleteCells(pos: number, n: number, fill: ICellData, eraseAttr?: IAttributeData): void; + replaceCells(start: number, end: number, fill: ICellData, eraseAttr?: IAttributeData): void; + resize(cols: number, fill: ICellData): void; + fill(fillCellData: ICellData): void; + copyFrom(line: IBufferLine): void; + clone(): IBufferLine; + getTrimmedLength(): number; + translateToString(trimRight?: boolean, startCol?: number, endCol?: number): string; + + /* direct access to cell attrs */ + getWidth(index: number): number; + hasWidth(index: number): number; + getFg(index: number): number; + getBg(index: number): number; + hasContent(index: number): number; + getCodePoint(index: number): number; + isCombined(index: number): number; + getString(index: number): string; +} + +export interface IMarker extends IDisposable { + readonly id: number; + readonly isDisposed: boolean; + readonly line: number; + onDispose: IEvent<void>; +} +export interface IModes { + insertMode: boolean; +} + +export interface IDecPrivateModes { + applicationCursorKeys: boolean; + applicationKeypad: boolean; + bracketedPasteMode: boolean; + origin: boolean; + reverseWraparound: boolean; + sendFocus: boolean; + wraparound: boolean; // defaults: xterm - true, vt100 - false +} + +export interface IRowRange { + start: number; + end: number; +} + +/** + * Interface for mouse events in the core. + */ +export const enum CoreMouseButton { + LEFT = 0, + MIDDLE = 1, + RIGHT = 2, + NONE = 3, + WHEEL = 4, + // additional buttons 1..8 + // untested! + AUX1 = 8, + AUX2 = 9, + AUX3 = 10, + AUX4 = 11, + AUX5 = 12, + AUX6 = 13, + AUX7 = 14, + AUX8 = 15 +} + +export const enum CoreMouseAction { + UP = 0, // buttons, wheel + DOWN = 1, // buttons, wheel + LEFT = 2, // wheel only + RIGHT = 3, // wheel only + MOVE = 32 // buttons only +} + +export interface ICoreMouseEvent { + /** column (zero based). */ + col: number; + /** row (zero based). */ + row: number; + /** + * Button the action occured. Due to restrictions of the tracking protocols + * it is not possible to report multiple buttons at once. + * Wheel is treated as a button. + * There are invalid combinations of buttons and actions possible + * (like move + wheel), those are silently ignored by the CoreMouseService. + */ + button: CoreMouseButton; + action: CoreMouseAction; + /** + * Modifier states. + * Protocols will add/ignore those based on specific restrictions. + */ + ctrl?: boolean; + alt?: boolean; + shift?: boolean; +} + +/** + * CoreMouseEventType + * To be reported to the browser component which events a mouse + * protocol wants to be catched and forwarded as an ICoreMouseEvent + * to CoreMouseService. + */ +export const enum CoreMouseEventType { + NONE = 0, + /** any mousedown event */ + DOWN = 1, + /** any mouseup event */ + UP = 2, + /** any mousemove event while a button is held */ + DRAG = 4, + /** any mousemove event without a button */ + MOVE = 8, + /** any wheel event */ + WHEEL = 16 +} + +/** + * Mouse protocol interface. + * A mouse protocol can be registered and activated at the CoreMouseService. + * `events` should contain a list of needed events as a hint for the browser component + * to install/remove the appropriate event handlers. + * `restrict` applies further protocol specific restrictions like not allowed + * modifiers or filtering invalid event types. + */ +export interface ICoreMouseProtocol { + events: CoreMouseEventType; + restrict: (e: ICoreMouseEvent) => boolean; +} + +/** + * CoreMouseEncoding + * The tracking encoding can be registered and activated at the CoreMouseService. + * If a ICoreMouseEvent passes all procotol restrictions it will be encoded + * with the active encoding and sent out. + * Note: Returning an empty string will supress sending a mouse report, + * which can be used to skip creating falsey reports in limited encodings + * (DEFAULT only supports up to 223 1-based as coord value). + */ +export type CoreMouseEncoding = (event: ICoreMouseEvent) => string; + +/** + * windowOptions + */ +export interface IWindowOptions { + restoreWin?: boolean; + minimizeWin?: boolean; + setWinPosition?: boolean; + setWinSizePixels?: boolean; + raiseWin?: boolean; + lowerWin?: boolean; + refreshWin?: boolean; + setWinSizeChars?: boolean; + maximizeWin?: boolean; + fullscreenWin?: boolean; + getWinState?: boolean; + getWinPosition?: boolean; + getWinSizePixels?: boolean; + getScreenSizePixels?: boolean; + getCellSizePixels?: boolean; + getWinSizeChars?: boolean; + getScreenSizeChars?: boolean; + getIconTitle?: boolean; + getWinTitle?: boolean; + pushTitle?: boolean; + popTitle?: boolean; + setWinLines?: boolean; +} + +// color events from common, used for OSC 4/10/11/12 and 104/110/111/112 +export const enum ColorRequestType { + REPORT = 0, + SET = 1, + RESTORE = 2 +} +export const enum ColorIndex { + FOREGROUND = 256, + BACKGROUND = 257, + CURSOR = 258 +} +export interface IColorReportRequest { + type: ColorRequestType.REPORT; + index: ColorIndex; +} +export interface IColorSetRequest { + type: ColorRequestType.SET; + index: ColorIndex; + color: IColorRGB; +} +export interface IColorRestoreRequest { + type: ColorRequestType.RESTORE; + index?: ColorIndex; +} +export type IColorEvent = (IColorReportRequest | IColorSetRequest | IColorRestoreRequest)[]; + + +/** + * Calls the parser and handles actions generated by the parser. + */ +export interface IInputHandler { + onTitleChange: IEvent<string>; + + parse(data: string | Uint8Array, promiseResult?: boolean): void | Promise<boolean>; + print(data: Uint32Array, start: number, end: number): void; + registerCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean | Promise<boolean>): IDisposable; + registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean | Promise<boolean>): IDisposable; + registerEscHandler(id: IFunctionIdentifier, callback: () => boolean | Promise<boolean>): IDisposable; + registerOscHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable; + + /** C0 BEL */ bell(): boolean; + /** C0 LF */ lineFeed(): boolean; + /** C0 CR */ carriageReturn(): boolean; + /** C0 BS */ backspace(): boolean; + /** C0 HT */ tab(): boolean; + /** C0 SO */ shiftOut(): boolean; + /** C0 SI */ shiftIn(): boolean; + + /** CSI @ */ insertChars(params: IParams): boolean; + /** CSI SP @ */ scrollLeft(params: IParams): boolean; + /** CSI A */ cursorUp(params: IParams): boolean; + /** CSI SP A */ scrollRight(params: IParams): boolean; + /** CSI B */ cursorDown(params: IParams): boolean; + /** CSI C */ cursorForward(params: IParams): boolean; + /** CSI D */ cursorBackward(params: IParams): boolean; + /** CSI E */ cursorNextLine(params: IParams): boolean; + /** CSI F */ cursorPrecedingLine(params: IParams): boolean; + /** CSI G */ cursorCharAbsolute(params: IParams): boolean; + /** CSI H */ cursorPosition(params: IParams): boolean; + /** CSI I */ cursorForwardTab(params: IParams): boolean; + /** CSI J */ eraseInDisplay(params: IParams): boolean; + /** CSI K */ eraseInLine(params: IParams): boolean; + /** CSI L */ insertLines(params: IParams): boolean; + /** CSI M */ deleteLines(params: IParams): boolean; + /** CSI P */ deleteChars(params: IParams): boolean; + /** CSI S */ scrollUp(params: IParams): boolean; + /** CSI T */ scrollDown(params: IParams, collect?: string): boolean; + /** CSI X */ eraseChars(params: IParams): boolean; + /** CSI Z */ cursorBackwardTab(params: IParams): boolean; + /** CSI ` */ charPosAbsolute(params: IParams): boolean; + /** CSI a */ hPositionRelative(params: IParams): boolean; + /** CSI b */ repeatPrecedingCharacter(params: IParams): boolean; + /** CSI c */ sendDeviceAttributesPrimary(params: IParams): boolean; + /** CSI > c */ sendDeviceAttributesSecondary(params: IParams): boolean; + /** CSI d */ linePosAbsolute(params: IParams): boolean; + /** CSI e */ vPositionRelative(params: IParams): boolean; + /** CSI f */ hVPosition(params: IParams): boolean; + /** CSI g */ tabClear(params: IParams): boolean; + /** CSI h */ setMode(params: IParams, collect?: string): boolean; + /** CSI l */ resetMode(params: IParams, collect?: string): boolean; + /** CSI m */ charAttributes(params: IParams): boolean; + /** CSI n */ deviceStatus(params: IParams, collect?: string): boolean; + /** CSI p */ softReset(params: IParams, collect?: string): boolean; + /** CSI q */ setCursorStyle(params: IParams, collect?: string): boolean; + /** CSI r */ setScrollRegion(params: IParams, collect?: string): boolean; + /** CSI s */ saveCursor(params: IParams): boolean; + /** CSI u */ restoreCursor(params: IParams): boolean; + /** CSI ' } */ insertColumns(params: IParams): boolean; + /** CSI ' ~ */ deleteColumns(params: IParams): boolean; + + /** OSC 0 + OSC 2 */ setTitle(data: string): boolean; + /** OSC 4 */ setOrReportIndexedColor(data: string): boolean; + /** OSC 10 */ setOrReportFgColor(data: string): boolean; + /** OSC 11 */ setOrReportBgColor(data: string): boolean; + /** OSC 12 */ setOrReportCursorColor(data: string): boolean; + /** OSC 104 */ restoreIndexedColor(data: string): boolean; + /** OSC 110 */ restoreFgColor(data: string): boolean; + /** OSC 111 */ restoreBgColor(data: string): boolean; + /** OSC 112 */ restoreCursorColor(data: string): boolean; + + /** ESC E */ nextLine(): boolean; + /** ESC = */ keypadApplicationMode(): boolean; + /** ESC > */ keypadNumericMode(): boolean; + /** ESC % G + ESC % @ */ selectDefaultCharset(): boolean; + /** ESC ( C + ESC ) C + ESC * C + ESC + C + ESC - C + ESC . C + ESC / C */ selectCharset(collectAndFlag: string): boolean; + /** ESC D */ index(): boolean; + /** ESC H */ tabSet(): boolean; + /** ESC M */ reverseIndex(): boolean; + /** ESC c */ fullReset(): boolean; + /** ESC n + ESC o + ESC | + ESC } + ESC ~ */ setgLevel(level: number): boolean; + /** ESC # 8 */ screenAlignmentPattern(): boolean; +} + +interface IParseStack { + paused: boolean; + cursorStartX: number; + cursorStartY: number; + decodedLength: number; + position: number; +} diff --git a/node_modules/xterm/src/common/WindowsMode.ts b/node_modules/xterm/src/common/WindowsMode.ts new file mode 100644 index 0000000..7cff094 --- /dev/null +++ b/node_modules/xterm/src/common/WindowsMode.ts @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { CHAR_DATA_CODE_INDEX, NULL_CELL_CODE, WHITESPACE_CELL_CODE } from 'common/buffer/Constants'; +import { IBufferService } from 'common/services/Services'; + +export function updateWindowsModeWrappedState(bufferService: IBufferService): void { + // Winpty does not support wraparound mode which means that lines will never + // be marked as wrapped. This causes issues for things like copying a line + // retaining the wrapped new line characters or if consumers are listening + // in on the data stream. + // + // The workaround for this is to listen to every incoming line feed and mark + // the line as wrapped if the last character in the previous line is not a + // space. This is certainly not without its problems, but generally on + // Windows when text reaches the end of the terminal it's likely going to be + // wrapped. + const line = bufferService.buffer.lines.get(bufferService.buffer.ybase + bufferService.buffer.y - 1); + const lastChar = line?.get(bufferService.cols - 1); + + const nextLine = bufferService.buffer.lines.get(bufferService.buffer.ybase + bufferService.buffer.y); + if (nextLine && lastChar) { + nextLine.isWrapped = (lastChar[CHAR_DATA_CODE_INDEX] !== NULL_CELL_CODE && lastChar[CHAR_DATA_CODE_INDEX] !== WHITESPACE_CELL_CODE); + } +} diff --git a/node_modules/xterm/src/common/buffer/AttributeData.ts b/node_modules/xterm/src/common/buffer/AttributeData.ts new file mode 100644 index 0000000..43d378e --- /dev/null +++ b/node_modules/xterm/src/common/buffer/AttributeData.ts @@ -0,0 +1,148 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IAttributeData, IColorRGB, IExtendedAttrs } from 'common/Types'; +import { Attributes, FgFlags, BgFlags, UnderlineStyle } from 'common/buffer/Constants'; + +export class AttributeData implements IAttributeData { + public static toColorRGB(value: number): IColorRGB { + return [ + value >>> Attributes.RED_SHIFT & 255, + value >>> Attributes.GREEN_SHIFT & 255, + value & 255 + ]; + } + + public static fromColorRGB(value: IColorRGB): number { + return (value[0] & 255) << Attributes.RED_SHIFT | (value[1] & 255) << Attributes.GREEN_SHIFT | value[2] & 255; + } + + public clone(): IAttributeData { + const newObj = new AttributeData(); + newObj.fg = this.fg; + newObj.bg = this.bg; + newObj.extended = this.extended.clone(); + return newObj; + } + + // data + public fg = 0; + public bg = 0; + public extended = new ExtendedAttrs(); + + // flags + public isInverse(): number { return this.fg & FgFlags.INVERSE; } + public isBold(): number { return this.fg & FgFlags.BOLD; } + public isUnderline(): number { return this.fg & FgFlags.UNDERLINE; } + public isBlink(): number { return this.fg & FgFlags.BLINK; } + public isInvisible(): number { return this.fg & FgFlags.INVISIBLE; } + public isItalic(): number { return this.bg & BgFlags.ITALIC; } + public isDim(): number { return this.bg & BgFlags.DIM; } + public isStrikethrough(): number { return this.fg & FgFlags.STRIKETHROUGH; } + + // color modes + public getFgColorMode(): number { return this.fg & Attributes.CM_MASK; } + public getBgColorMode(): number { return this.bg & Attributes.CM_MASK; } + public isFgRGB(): boolean { return (this.fg & Attributes.CM_MASK) === Attributes.CM_RGB; } + public isBgRGB(): boolean { return (this.bg & Attributes.CM_MASK) === Attributes.CM_RGB; } + public isFgPalette(): boolean { return (this.fg & Attributes.CM_MASK) === Attributes.CM_P16 || (this.fg & Attributes.CM_MASK) === Attributes.CM_P256; } + public isBgPalette(): boolean { return (this.bg & Attributes.CM_MASK) === Attributes.CM_P16 || (this.bg & Attributes.CM_MASK) === Attributes.CM_P256; } + public isFgDefault(): boolean { return (this.fg & Attributes.CM_MASK) === 0; } + public isBgDefault(): boolean { return (this.bg & Attributes.CM_MASK) === 0; } + public isAttributeDefault(): boolean { return this.fg === 0 && this.bg === 0; } + + // colors + public getFgColor(): number { + switch (this.fg & Attributes.CM_MASK) { + case Attributes.CM_P16: + case Attributes.CM_P256: return this.fg & Attributes.PCOLOR_MASK; + case Attributes.CM_RGB: return this.fg & Attributes.RGB_MASK; + default: return -1; // CM_DEFAULT defaults to -1 + } + } + public getBgColor(): number { + switch (this.bg & Attributes.CM_MASK) { + case Attributes.CM_P16: + case Attributes.CM_P256: return this.bg & Attributes.PCOLOR_MASK; + case Attributes.CM_RGB: return this.bg & Attributes.RGB_MASK; + default: return -1; // CM_DEFAULT defaults to -1 + } + } + + // extended attrs + public hasExtendedAttrs(): number { + return this.bg & BgFlags.HAS_EXTENDED; + } + public updateExtended(): void { + if (this.extended.isEmpty()) { + this.bg &= ~BgFlags.HAS_EXTENDED; + } else { + this.bg |= BgFlags.HAS_EXTENDED; + } + } + public getUnderlineColor(): number { + if ((this.bg & BgFlags.HAS_EXTENDED) && ~this.extended.underlineColor) { + switch (this.extended.underlineColor & Attributes.CM_MASK) { + case Attributes.CM_P16: + case Attributes.CM_P256: return this.extended.underlineColor & Attributes.PCOLOR_MASK; + case Attributes.CM_RGB: return this.extended.underlineColor & Attributes.RGB_MASK; + default: return this.getFgColor(); + } + } + return this.getFgColor(); + } + public getUnderlineColorMode(): number { + return (this.bg & BgFlags.HAS_EXTENDED) && ~this.extended.underlineColor + ? this.extended.underlineColor & Attributes.CM_MASK + : this.getFgColorMode(); + } + public isUnderlineColorRGB(): boolean { + return (this.bg & BgFlags.HAS_EXTENDED) && ~this.extended.underlineColor + ? (this.extended.underlineColor & Attributes.CM_MASK) === Attributes.CM_RGB + : this.isFgRGB(); + } + public isUnderlineColorPalette(): boolean { + return (this.bg & BgFlags.HAS_EXTENDED) && ~this.extended.underlineColor + ? (this.extended.underlineColor & Attributes.CM_MASK) === Attributes.CM_P16 + || (this.extended.underlineColor & Attributes.CM_MASK) === Attributes.CM_P256 + : this.isFgPalette(); + } + public isUnderlineColorDefault(): boolean { + return (this.bg & BgFlags.HAS_EXTENDED) && ~this.extended.underlineColor + ? (this.extended.underlineColor & Attributes.CM_MASK) === 0 + : this.isFgDefault(); + } + public getUnderlineStyle(): UnderlineStyle { + return this.fg & FgFlags.UNDERLINE + ? (this.bg & BgFlags.HAS_EXTENDED ? this.extended.underlineStyle : UnderlineStyle.SINGLE) + : UnderlineStyle.NONE; + } +} + + +/** + * Extended attributes for a cell. + * Holds information about different underline styles and color. + */ +export class ExtendedAttrs implements IExtendedAttrs { + constructor( + // underline style, NONE is empty + public underlineStyle: UnderlineStyle = UnderlineStyle.NONE, + // underline color, -1 is empty (same as FG) + public underlineColor: number = -1 + ) {} + + public clone(): IExtendedAttrs { + return new ExtendedAttrs(this.underlineStyle, this.underlineColor); + } + + /** + * Convenient method to indicate whether the object holds no additional information, + * that needs to be persistant in the buffer. + */ + public isEmpty(): boolean { + return this.underlineStyle === UnderlineStyle.NONE; + } +} diff --git a/node_modules/xterm/src/common/buffer/Buffer.ts b/node_modules/xterm/src/common/buffer/Buffer.ts new file mode 100644 index 0000000..02ce7c8 --- /dev/null +++ b/node_modules/xterm/src/common/buffer/Buffer.ts @@ -0,0 +1,681 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { CircularList, IInsertEvent } from 'common/CircularList'; +import { IBuffer, BufferIndex, IBufferStringIterator, IBufferStringIteratorResult } from 'common/buffer/Types'; +import { IBufferLine, ICellData, IAttributeData, ICharset } from 'common/Types'; +import { BufferLine, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; +import { CellData } from 'common/buffer/CellData'; +import { NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_WIDTH, WHITESPACE_CELL_CODE, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from 'common/buffer/Constants'; +import { reflowLargerApplyNewLayout, reflowLargerCreateNewLayout, reflowLargerGetLinesToRemove, reflowSmallerGetNewLineLengths, getWrappedLineTrimmedLength } from 'common/buffer/BufferReflow'; +import { Marker } from 'common/buffer/Marker'; +import { IOptionsService, IBufferService } from 'common/services/Services'; +import { DEFAULT_CHARSET } from 'common/data/Charsets'; +import { ExtendedAttrs } from 'common/buffer/AttributeData'; + +export const MAX_BUFFER_SIZE = 4294967295; // 2^32 - 1 + +/** + * This class represents a terminal buffer (an internal state of the terminal), where the + * following information is stored (in high-level): + * - text content of this particular buffer + * - cursor position + * - scroll position + */ +export class Buffer implements IBuffer { + public lines: CircularList<IBufferLine>; + public ydisp: number = 0; + public ybase: number = 0; + public y: number = 0; + public x: number = 0; + public scrollBottom: number; + public scrollTop: number; + // TODO: Type me + public tabs: any; + public savedY: number = 0; + public savedX: number = 0; + public savedCurAttrData = DEFAULT_ATTR_DATA.clone(); + public savedCharset: ICharset | undefined = DEFAULT_CHARSET; + public markers: Marker[] = []; + private _nullCell: ICellData = CellData.fromCharData([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); + private _whitespaceCell: ICellData = CellData.fromCharData([0, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_WIDTH, WHITESPACE_CELL_CODE]); + private _cols: number; + private _rows: number; + + constructor( + private _hasScrollback: boolean, + private _optionsService: IOptionsService, + private _bufferService: IBufferService + ) { + this._cols = this._bufferService.cols; + this._rows = this._bufferService.rows; + this.lines = new CircularList<IBufferLine>(this._getCorrectBufferLength(this._rows)); + this.scrollTop = 0; + this.scrollBottom = this._rows - 1; + this.setupTabStops(); + } + + public getNullCell(attr?: IAttributeData): ICellData { + if (attr) { + this._nullCell.fg = attr.fg; + this._nullCell.bg = attr.bg; + this._nullCell.extended = attr.extended; + } else { + this._nullCell.fg = 0; + this._nullCell.bg = 0; + this._nullCell.extended = new ExtendedAttrs(); + } + return this._nullCell; + } + + public getWhitespaceCell(attr?: IAttributeData): ICellData { + if (attr) { + this._whitespaceCell.fg = attr.fg; + this._whitespaceCell.bg = attr.bg; + this._whitespaceCell.extended = attr.extended; + } else { + this._whitespaceCell.fg = 0; + this._whitespaceCell.bg = 0; + this._whitespaceCell.extended = new ExtendedAttrs(); + } + return this._whitespaceCell; + } + + public getBlankLine(attr: IAttributeData, isWrapped?: boolean): IBufferLine { + return new BufferLine(this._bufferService.cols, this.getNullCell(attr), isWrapped); + } + + public get hasScrollback(): boolean { + return this._hasScrollback && this.lines.maxLength > this._rows; + } + + public get isCursorInViewport(): boolean { + const absoluteY = this.ybase + this.y; + const relativeY = absoluteY - this.ydisp; + return (relativeY >= 0 && relativeY < this._rows); + } + + /** + * Gets the correct buffer length based on the rows provided, the terminal's + * scrollback and whether this buffer is flagged to have scrollback or not. + * @param rows The terminal rows to use in the calculation. + */ + private _getCorrectBufferLength(rows: number): number { + if (!this._hasScrollback) { + return rows; + } + + const correctBufferLength = rows + this._optionsService.rawOptions.scrollback; + + return correctBufferLength > MAX_BUFFER_SIZE ? MAX_BUFFER_SIZE : correctBufferLength; + } + + /** + * Fills the buffer's viewport with blank lines. + */ + public fillViewportRows(fillAttr?: IAttributeData): void { + if (this.lines.length === 0) { + if (fillAttr === undefined) { + fillAttr = DEFAULT_ATTR_DATA; + } + let i = this._rows; + while (i--) { + this.lines.push(this.getBlankLine(fillAttr)); + } + } + } + + /** + * Clears the buffer to it's initial state, discarding all previous data. + */ + public clear(): void { + this.ydisp = 0; + this.ybase = 0; + this.y = 0; + this.x = 0; + this.lines = new CircularList<IBufferLine>(this._getCorrectBufferLength(this._rows)); + this.scrollTop = 0; + this.scrollBottom = this._rows - 1; + this.setupTabStops(); + } + + /** + * Resizes the buffer, adjusting its data accordingly. + * @param newCols The new number of columns. + * @param newRows The new number of rows. + */ + public resize(newCols: number, newRows: number): void { + // store reference to null cell with default attrs + const nullCell = this.getNullCell(DEFAULT_ATTR_DATA); + + // Increase max length if needed before adjustments to allow space to fill + // as required. + const newMaxLength = this._getCorrectBufferLength(newRows); + if (newMaxLength > this.lines.maxLength) { + this.lines.maxLength = newMaxLength; + } + + // The following adjustments should only happen if the buffer has been + // initialized/filled. + if (this.lines.length > 0) { + // Deal with columns increasing (reducing needs to happen after reflow) + if (this._cols < newCols) { + for (let i = 0; i < this.lines.length; i++) { + this.lines.get(i)!.resize(newCols, nullCell); + } + } + + // Resize rows in both directions as needed + let addToY = 0; + if (this._rows < newRows) { + for (let y = this._rows; y < newRows; y++) { + if (this.lines.length < newRows + this.ybase) { + if (this._optionsService.rawOptions.windowsMode) { + // Just add the new missing rows on Windows as conpty reprints the screen with it's + // view of the world. Once a line enters scrollback for conpty it remains there + this.lines.push(new BufferLine(newCols, nullCell)); + } else { + if (this.ybase > 0 && this.lines.length <= this.ybase + this.y + addToY + 1) { + // There is room above the buffer and there are no empty elements below the line, + // scroll up + this.ybase--; + addToY++; + if (this.ydisp > 0) { + // Viewport is at the top of the buffer, must increase downwards + this.ydisp--; + } + } else { + // Add a blank line if there is no buffer left at the top to scroll to, or if there + // are blank lines after the cursor + this.lines.push(new BufferLine(newCols, nullCell)); + } + } + } + } + } else { // (this._rows >= newRows) + for (let y = this._rows; y > newRows; y--) { + if (this.lines.length > newRows + this.ybase) { + if (this.lines.length > this.ybase + this.y + 1) { + // The line is a blank line below the cursor, remove it + this.lines.pop(); + } else { + // The line is the cursor, scroll down + this.ybase++; + this.ydisp++; + } + } + } + } + + // Reduce max length if needed after adjustments, this is done after as it + // would otherwise cut data from the bottom of the buffer. + if (newMaxLength < this.lines.maxLength) { + // Trim from the top of the buffer and adjust ybase and ydisp. + const amountToTrim = this.lines.length - newMaxLength; + if (amountToTrim > 0) { + this.lines.trimStart(amountToTrim); + this.ybase = Math.max(this.ybase - amountToTrim, 0); + this.ydisp = Math.max(this.ydisp - amountToTrim, 0); + this.savedY = Math.max(this.savedY - amountToTrim, 0); + } + this.lines.maxLength = newMaxLength; + } + + // Make sure that the cursor stays on screen + this.x = Math.min(this.x, newCols - 1); + this.y = Math.min(this.y, newRows - 1); + if (addToY) { + this.y += addToY; + } + this.savedX = Math.min(this.savedX, newCols - 1); + + this.scrollTop = 0; + } + + this.scrollBottom = newRows - 1; + + if (this._isReflowEnabled) { + this._reflow(newCols, newRows); + + // Trim the end of the line off if cols shrunk + if (this._cols > newCols) { + for (let i = 0; i < this.lines.length; i++) { + this.lines.get(i)!.resize(newCols, nullCell); + } + } + } + + this._cols = newCols; + this._rows = newRows; + } + + private get _isReflowEnabled(): boolean { + return this._hasScrollback && !this._optionsService.rawOptions.windowsMode; + } + + private _reflow(newCols: number, newRows: number): void { + if (this._cols === newCols) { + return; + } + + // Iterate through rows, ignore the last one as it cannot be wrapped + if (newCols > this._cols) { + this._reflowLarger(newCols, newRows); + } else { + this._reflowSmaller(newCols, newRows); + } + } + + private _reflowLarger(newCols: number, newRows: number): void { + const toRemove: number[] = reflowLargerGetLinesToRemove(this.lines, this._cols, newCols, this.ybase + this.y, this.getNullCell(DEFAULT_ATTR_DATA)); + if (toRemove.length > 0) { + const newLayoutResult = reflowLargerCreateNewLayout(this.lines, toRemove); + reflowLargerApplyNewLayout(this.lines, newLayoutResult.layout); + this._reflowLargerAdjustViewport(newCols, newRows, newLayoutResult.countRemoved); + } + } + + private _reflowLargerAdjustViewport(newCols: number, newRows: number, countRemoved: number): void { + const nullCell = this.getNullCell(DEFAULT_ATTR_DATA); + // Adjust viewport based on number of items removed + let viewportAdjustments = countRemoved; + while (viewportAdjustments-- > 0) { + if (this.ybase === 0) { + if (this.y > 0) { + this.y--; + } + if (this.lines.length < newRows) { + // Add an extra row at the bottom of the viewport + this.lines.push(new BufferLine(newCols, nullCell)); + } + } else { + if (this.ydisp === this.ybase) { + this.ydisp--; + } + this.ybase--; + } + } + this.savedY = Math.max(this.savedY - countRemoved, 0); + } + + private _reflowSmaller(newCols: number, newRows: number): void { + const nullCell = this.getNullCell(DEFAULT_ATTR_DATA); + // Gather all BufferLines that need to be inserted into the Buffer here so that they can be + // batched up and only committed once + const toInsert = []; + let countToInsert = 0; + // Go backwards as many lines may be trimmed and this will avoid considering them + for (let y = this.lines.length - 1; y >= 0; y--) { + // Check whether this line is a problem + let nextLine = this.lines.get(y) as BufferLine; + if (!nextLine || !nextLine.isWrapped && nextLine.getTrimmedLength() <= newCols) { + continue; + } + + // Gather wrapped lines and adjust y to be the starting line + const wrappedLines: BufferLine[] = [nextLine]; + while (nextLine.isWrapped && y > 0) { + nextLine = this.lines.get(--y) as BufferLine; + wrappedLines.unshift(nextLine); + } + + // If these lines contain the cursor don't touch them, the program will handle fixing up + // wrapped lines with the cursor + const absoluteY = this.ybase + this.y; + if (absoluteY >= y && absoluteY < y + wrappedLines.length) { + continue; + } + + const lastLineLength = wrappedLines[wrappedLines.length - 1].getTrimmedLength(); + const destLineLengths = reflowSmallerGetNewLineLengths(wrappedLines, this._cols, newCols); + const linesToAdd = destLineLengths.length - wrappedLines.length; + let trimmedLines: number; + if (this.ybase === 0 && this.y !== this.lines.length - 1) { + // If the top section of the buffer is not yet filled + trimmedLines = Math.max(0, this.y - this.lines.maxLength + linesToAdd); + } else { + trimmedLines = Math.max(0, this.lines.length - this.lines.maxLength + linesToAdd); + } + + // Add the new lines + const newLines: BufferLine[] = []; + for (let i = 0; i < linesToAdd; i++) { + const newLine = this.getBlankLine(DEFAULT_ATTR_DATA, true) as BufferLine; + newLines.push(newLine); + } + if (newLines.length > 0) { + toInsert.push({ + // countToInsert here gets the actual index, taking into account other inserted items. + // using this we can iterate through the list forwards + start: y + wrappedLines.length + countToInsert, + newLines + }); + countToInsert += newLines.length; + } + wrappedLines.push(...newLines); + + // Copy buffer data to new locations, this needs to happen backwards to do in-place + let destLineIndex = destLineLengths.length - 1; // Math.floor(cellsNeeded / newCols); + let destCol = destLineLengths[destLineIndex]; // cellsNeeded % newCols; + if (destCol === 0) { + destLineIndex--; + destCol = destLineLengths[destLineIndex]; + } + let srcLineIndex = wrappedLines.length - linesToAdd - 1; + let srcCol = lastLineLength; + while (srcLineIndex >= 0) { + const cellsToCopy = Math.min(srcCol, destCol); + if (wrappedLines[destLineIndex] === undefined) { + // Sanity check that the line exists, this has been known to fail for an unknown reason + // which would stop the reflow from happening if an exception would throw. + break; + } + wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[srcLineIndex], srcCol - cellsToCopy, destCol - cellsToCopy, cellsToCopy, true); + destCol -= cellsToCopy; + if (destCol === 0) { + destLineIndex--; + destCol = destLineLengths[destLineIndex]; + } + srcCol -= cellsToCopy; + if (srcCol === 0) { + srcLineIndex--; + const wrappedLinesIndex = Math.max(srcLineIndex, 0); + srcCol = getWrappedLineTrimmedLength(wrappedLines, wrappedLinesIndex, this._cols); + } + } + + // Null out the end of the line ends if a wide character wrapped to the following line + for (let i = 0; i < wrappedLines.length; i++) { + if (destLineLengths[i] < newCols) { + wrappedLines[i].setCell(destLineLengths[i], nullCell); + } + } + + // Adjust viewport as needed + let viewportAdjustments = linesToAdd - trimmedLines; + while (viewportAdjustments-- > 0) { + if (this.ybase === 0) { + if (this.y < newRows - 1) { + this.y++; + this.lines.pop(); + } else { + this.ybase++; + this.ydisp++; + } + } else { + // Ensure ybase does not exceed its maximum value + if (this.ybase < Math.min(this.lines.maxLength, this.lines.length + countToInsert) - newRows) { + if (this.ybase === this.ydisp) { + this.ydisp++; + } + this.ybase++; + } + } + } + this.savedY = Math.min(this.savedY + linesToAdd, this.ybase + newRows - 1); + } + + // Rearrange lines in the buffer if there are any insertions, this is done at the end rather + // than earlier so that it's a single O(n) pass through the buffer, instead of O(n^2) from many + // costly calls to CircularList.splice. + if (toInsert.length > 0) { + // Record buffer insert events and then play them back backwards so that the indexes are + // correct + const insertEvents: IInsertEvent[] = []; + + // Record original lines so they don't get overridden when we rearrange the list + const originalLines: BufferLine[] = []; + for (let i = 0; i < this.lines.length; i++) { + originalLines.push(this.lines.get(i) as BufferLine); + } + const originalLinesLength = this.lines.length; + + let originalLineIndex = originalLinesLength - 1; + let nextToInsertIndex = 0; + let nextToInsert = toInsert[nextToInsertIndex]; + this.lines.length = Math.min(this.lines.maxLength, this.lines.length + countToInsert); + let countInsertedSoFar = 0; + for (let i = Math.min(this.lines.maxLength - 1, originalLinesLength + countToInsert - 1); i >= 0; i--) { + if (nextToInsert && nextToInsert.start > originalLineIndex + countInsertedSoFar) { + // Insert extra lines here, adjusting i as needed + for (let nextI = nextToInsert.newLines.length - 1; nextI >= 0; nextI--) { + this.lines.set(i--, nextToInsert.newLines[nextI]); + } + i++; + + // Create insert events for later + insertEvents.push({ + index: originalLineIndex + 1, + amount: nextToInsert.newLines.length + }); + + countInsertedSoFar += nextToInsert.newLines.length; + nextToInsert = toInsert[++nextToInsertIndex]; + } else { + this.lines.set(i, originalLines[originalLineIndex--]); + } + } + + // Update markers + let insertCountEmitted = 0; + for (let i = insertEvents.length - 1; i >= 0; i--) { + insertEvents[i].index += insertCountEmitted; + this.lines.onInsertEmitter.fire(insertEvents[i]); + insertCountEmitted += insertEvents[i].amount; + } + const amountToTrim = Math.max(0, originalLinesLength + countToInsert - this.lines.maxLength); + if (amountToTrim > 0) { + this.lines.onTrimEmitter.fire(amountToTrim); + } + } + } + + // private _reflowSmallerGetLinesNeeded() + + /** + * Translates a string index back to a BufferIndex. + * To get the correct buffer position the string must start at `startCol` 0 + * (default in translateBufferLineToString). + * The method also works on wrapped line strings given rows were not trimmed. + * The method operates on the CharData string length, there are no + * additional content or boundary checks. Therefore the string and the buffer + * should not be altered in between. + * TODO: respect trim flag after fixing #1685 + * @param lineIndex line index the string was retrieved from + * @param stringIndex index within the string + * @param startCol column offset the string was retrieved from + */ + public stringIndexToBufferIndex(lineIndex: number, stringIndex: number, trimRight: boolean = false): BufferIndex { + while (stringIndex) { + const line = this.lines.get(lineIndex); + if (!line) { + return [-1, -1]; + } + const length = (trimRight) ? line.getTrimmedLength() : line.length; + for (let i = 0; i < length; ++i) { + if (line.get(i)[CHAR_DATA_WIDTH_INDEX]) { + // empty cells report a string length of 0, but get replaced + // with a whitespace in translateToString, thus replace with 1 + stringIndex -= line.get(i)[CHAR_DATA_CHAR_INDEX].length || 1; + } + if (stringIndex < 0) { + return [lineIndex, i]; + } + } + lineIndex++; + } + return [lineIndex, 0]; + } + + /** + * Translates a buffer line to a string, with optional start and end columns. + * Wide characters will count as two columns in the resulting string. This + * function is useful for getting the actual text underneath the raw selection + * position. + * @param line The line being translated. + * @param trimRight Whether to trim whitespace to the right. + * @param startCol The column to start at. + * @param endCol The column to end at. + */ + public translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol: number = 0, endCol?: number): string { + const line = this.lines.get(lineIndex); + if (!line) { + return ''; + } + return line.translateToString(trimRight, startCol, endCol); + } + + public getWrappedRangeForLine(y: number): { first: number, last: number } { + let first = y; + let last = y; + // Scan upwards for wrapped lines + while (first > 0 && this.lines.get(first)!.isWrapped) { + first--; + } + // Scan downwards for wrapped lines + while (last + 1 < this.lines.length && this.lines.get(last + 1)!.isWrapped) { + last++; + } + return { first, last }; + } + + /** + * Setup the tab stops. + * @param i The index to start setting up tab stops from. + */ + public setupTabStops(i?: number): void { + if (i !== null && i !== undefined) { + if (!this.tabs[i]) { + i = this.prevStop(i); + } + } else { + this.tabs = {}; + i = 0; + } + + for (; i < this._cols; i += this._optionsService.rawOptions.tabStopWidth) { + this.tabs[i] = true; + } + } + + /** + * Move the cursor to the previous tab stop from the given position (default is current). + * @param x The position to move the cursor to the previous tab stop. + */ + public prevStop(x?: number): number { + if (x === null || x === undefined) { + x = this.x; + } + while (!this.tabs[--x] && x > 0); + return x >= this._cols ? this._cols - 1 : x < 0 ? 0 : x; + } + + /** + * Move the cursor one tab stop forward from the given position (default is current). + * @param x The position to move the cursor one tab stop forward. + */ + public nextStop(x?: number): number { + if (x === null || x === undefined) { + x = this.x; + } + while (!this.tabs[++x] && x < this._cols); + return x >= this._cols ? this._cols - 1 : x < 0 ? 0 : x; + } + + public addMarker(y: number): Marker { + const marker = new Marker(y); + this.markers.push(marker); + marker.register(this.lines.onTrim(amount => { + marker.line -= amount; + // The marker should be disposed when the line is trimmed from the buffer + if (marker.line < 0) { + marker.dispose(); + } + })); + marker.register(this.lines.onInsert(event => { + if (marker.line >= event.index) { + marker.line += event.amount; + } + })); + marker.register(this.lines.onDelete(event => { + // Delete the marker if it's within the range + if (marker.line >= event.index && marker.line < event.index + event.amount) { + marker.dispose(); + } + + // Shift the marker if it's after the deleted range + if (marker.line > event.index) { + marker.line -= event.amount; + } + })); + marker.register(marker.onDispose(() => this._removeMarker(marker))); + return marker; + } + + private _removeMarker(marker: Marker): void { + this.markers.splice(this.markers.indexOf(marker), 1); + } + + public iterator(trimRight: boolean, startIndex?: number, endIndex?: number, startOverscan?: number, endOverscan?: number): IBufferStringIterator { + return new BufferStringIterator(this, trimRight, startIndex, endIndex, startOverscan, endOverscan); + } +} + +/** + * Iterator to get unwrapped content strings from the buffer. + * The iterator returns at least the string data between the borders + * `startIndex` and `endIndex` (exclusive) and will expand the lines + * by `startOverscan` to the top and by `endOverscan` to the bottom, + * if no new line was found in between. + * It will never read/return string data beyond `startIndex - startOverscan` + * or `endIndex + endOverscan`. Therefore the first and last line might be truncated. + * It is possible to always get the full string for the first and last line as well + * by setting the overscan values to the actual buffer length. This not recommended + * since it might return the whole buffer within a single string in a worst case scenario. + */ +export class BufferStringIterator implements IBufferStringIterator { + private _current: number; + + constructor ( + private _buffer: IBuffer, + private _trimRight: boolean, + private _startIndex: number = 0, + private _endIndex: number = _buffer.lines.length, + private _startOverscan: number = 0, + private _endOverscan: number = 0 + ) { + if (this._startIndex < 0) { + this._startIndex = 0; + } + if (this._endIndex > this._buffer.lines.length) { + this._endIndex = this._buffer.lines.length; + } + this._current = this._startIndex; + } + + public hasNext(): boolean { + return this._current < this._endIndex; + } + + public next(): IBufferStringIteratorResult { + const range = this._buffer.getWrappedRangeForLine(this._current); + // limit search window to overscan value at both borders + if (range.first < this._startIndex - this._startOverscan) { + range.first = this._startIndex - this._startOverscan; + } + if (range.last > this._endIndex + this._endOverscan) { + range.last = this._endIndex + this._endOverscan; + } + // limit to current buffer length + range.first = Math.max(range.first, 0); + range.last = Math.min(range.last, this._buffer.lines.length); + let content = ''; + for (let i = range.first; i <= range.last; ++i) { + content += this._buffer.translateBufferLineToString(i, this._trimRight); + } + this._current = range.last + 1; + return { range, content }; + } +} diff --git a/node_modules/xterm/src/common/buffer/BufferLine.ts b/node_modules/xterm/src/common/buffer/BufferLine.ts new file mode 100644 index 0000000..f0bf4fc --- /dev/null +++ b/node_modules/xterm/src/common/buffer/BufferLine.ts @@ -0,0 +1,441 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { CharData, IBufferLine, ICellData, IAttributeData, IExtendedAttrs } from 'common/Types'; +import { stringFromCodePoint } from 'common/input/TextDecoder'; +import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_ATTR_INDEX, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, WHITESPACE_CELL_CHAR, Content, BgFlags } from 'common/buffer/Constants'; +import { CellData } from 'common/buffer/CellData'; +import { AttributeData, ExtendedAttrs } from 'common/buffer/AttributeData'; + +/** + * buffer memory layout: + * + * | uint32_t | uint32_t | uint32_t | + * | `content` | `FG` | `BG` | + * | wcwidth(2) comb(1) codepoint(21) | flags(8) R(8) G(8) B(8) | flags(8) R(8) G(8) B(8) | + */ + + +/** typed array slots taken by one cell */ +const CELL_SIZE = 3; + +/** + * Cell member indices. + * + * Direct access: + * `content = data[column * CELL_SIZE + Cell.CONTENT];` + * `fg = data[column * CELL_SIZE + Cell.FG];` + * `bg = data[column * CELL_SIZE + Cell.BG];` + */ +const enum Cell { + CONTENT = 0, + FG = 1, // currently simply holds all known attrs + BG = 2 // currently unused +} + +export const DEFAULT_ATTR_DATA = Object.freeze(new AttributeData()); + +/** + * Typed array based bufferline implementation. + * + * There are 2 ways to insert data into the cell buffer: + * - `setCellFromCodepoint` + `addCodepointToCell` + * Use these for data that is already UTF32. + * Used during normal input in `InputHandler` for faster buffer access. + * - `setCell` + * This method takes a CellData object and stores the data in the buffer. + * Use `CellData.fromCharData` to create the CellData object (e.g. from JS string). + * + * To retrieve data from the buffer use either one of the primitive methods + * (if only one particular value is needed) or `loadCell`. For `loadCell` in a loop + * memory allocs / GC pressure can be greatly reduced by reusing the CellData object. + */ +export class BufferLine implements IBufferLine { + protected _data: Uint32Array; + protected _combined: {[index: number]: string} = {}; + protected _extendedAttrs: {[index: number]: ExtendedAttrs} = {}; + public length: number; + + constructor(cols: number, fillCellData?: ICellData, public isWrapped: boolean = false) { + this._data = new Uint32Array(cols * CELL_SIZE); + const cell = fillCellData || CellData.fromCharData([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); + for (let i = 0; i < cols; ++i) { + this.setCell(i, cell); + } + this.length = cols; + } + + /** + * Get cell data CharData. + * @deprecated + */ + public get(index: number): CharData { + const content = this._data[index * CELL_SIZE + Cell.CONTENT]; + const cp = content & Content.CODEPOINT_MASK; + return [ + this._data[index * CELL_SIZE + Cell.FG], + (content & Content.IS_COMBINED_MASK) + ? this._combined[index] + : (cp) ? stringFromCodePoint(cp) : '', + content >> Content.WIDTH_SHIFT, + (content & Content.IS_COMBINED_MASK) + ? this._combined[index].charCodeAt(this._combined[index].length - 1) + : cp + ]; + } + + /** + * Set cell data from CharData. + * @deprecated + */ + public set(index: number, value: CharData): void { + this._data[index * CELL_SIZE + Cell.FG] = value[CHAR_DATA_ATTR_INDEX]; + if (value[CHAR_DATA_CHAR_INDEX].length > 1) { + this._combined[index] = value[1]; + this._data[index * CELL_SIZE + Cell.CONTENT] = index | Content.IS_COMBINED_MASK | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT); + } else { + this._data[index * CELL_SIZE + Cell.CONTENT] = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT); + } + } + + /** + * primitive getters + * use these when only one value is needed, otherwise use `loadCell` + */ + public getWidth(index: number): number { + return this._data[index * CELL_SIZE + Cell.CONTENT] >> Content.WIDTH_SHIFT; + } + + /** Test whether content has width. */ + public hasWidth(index: number): number { + return this._data[index * CELL_SIZE + Cell.CONTENT] & Content.WIDTH_MASK; + } + + /** Get FG cell component. */ + public getFg(index: number): number { + return this._data[index * CELL_SIZE + Cell.FG]; + } + + /** Get BG cell component. */ + public getBg(index: number): number { + return this._data[index * CELL_SIZE + Cell.BG]; + } + + /** + * Test whether contains any chars. + * Basically an empty has no content, but other cells might differ in FG/BG + * from real empty cells. + * */ + public hasContent(index: number): number { + return this._data[index * CELL_SIZE + Cell.CONTENT] & Content.HAS_CONTENT_MASK; + } + + /** + * Get codepoint of the cell. + * To be in line with `code` in CharData this either returns + * a single UTF32 codepoint or the last codepoint of a combined string. + */ + public getCodePoint(index: number): number { + const content = this._data[index * CELL_SIZE + Cell.CONTENT]; + if (content & Content.IS_COMBINED_MASK) { + return this._combined[index].charCodeAt(this._combined[index].length - 1); + } + return content & Content.CODEPOINT_MASK; + } + + /** Test whether the cell contains a combined string. */ + public isCombined(index: number): number { + return this._data[index * CELL_SIZE + Cell.CONTENT] & Content.IS_COMBINED_MASK; + } + + /** Returns the string content of the cell. */ + public getString(index: number): string { + const content = this._data[index * CELL_SIZE + Cell.CONTENT]; + if (content & Content.IS_COMBINED_MASK) { + return this._combined[index]; + } + if (content & Content.CODEPOINT_MASK) { + return stringFromCodePoint(content & Content.CODEPOINT_MASK); + } + // return empty string for empty cells + return ''; + } + + /** + * Load data at `index` into `cell`. This is used to access cells in a way that's more friendly + * to GC as it significantly reduced the amount of new objects/references needed. + */ + public loadCell(index: number, cell: ICellData): ICellData { + const startIndex = index * CELL_SIZE; + cell.content = this._data[startIndex + Cell.CONTENT]; + cell.fg = this._data[startIndex + Cell.FG]; + cell.bg = this._data[startIndex + Cell.BG]; + if (cell.content & Content.IS_COMBINED_MASK) { + cell.combinedData = this._combined[index]; + } + if (cell.bg & BgFlags.HAS_EXTENDED) { + cell.extended = this._extendedAttrs[index]; + } + return cell; + } + + /** + * Set data at `index` to `cell`. + */ + public setCell(index: number, cell: ICellData): void { + if (cell.content & Content.IS_COMBINED_MASK) { + this._combined[index] = cell.combinedData; + } + if (cell.bg & BgFlags.HAS_EXTENDED) { + this._extendedAttrs[index] = cell.extended; + } + this._data[index * CELL_SIZE + Cell.CONTENT] = cell.content; + this._data[index * CELL_SIZE + Cell.FG] = cell.fg; + this._data[index * CELL_SIZE + Cell.BG] = cell.bg; + } + + /** + * Set cell data from input handler. + * Since the input handler see the incoming chars as UTF32 codepoints, + * it gets an optimized access method. + */ + public setCellFromCodePoint(index: number, codePoint: number, width: number, fg: number, bg: number, eAttrs: IExtendedAttrs): void { + if (bg & BgFlags.HAS_EXTENDED) { + this._extendedAttrs[index] = eAttrs; + } + this._data[index * CELL_SIZE + Cell.CONTENT] = codePoint | (width << Content.WIDTH_SHIFT); + this._data[index * CELL_SIZE + Cell.FG] = fg; + this._data[index * CELL_SIZE + Cell.BG] = bg; + } + + /** + * Add a codepoint to a cell from input handler. + * During input stage combining chars with a width of 0 follow and stack + * onto a leading char. Since we already set the attrs + * by the previous `setDataFromCodePoint` call, we can omit it here. + */ + public addCodepointToCell(index: number, codePoint: number): void { + let content = this._data[index * CELL_SIZE + Cell.CONTENT]; + if (content & Content.IS_COMBINED_MASK) { + // we already have a combined string, simply add + this._combined[index] += stringFromCodePoint(codePoint); + } else { + if (content & Content.CODEPOINT_MASK) { + // normal case for combining chars: + // - move current leading char + new one into combined string + // - set combined flag + this._combined[index] = stringFromCodePoint(content & Content.CODEPOINT_MASK) + stringFromCodePoint(codePoint); + content &= ~Content.CODEPOINT_MASK; // set codepoint in buffer to 0 + content |= Content.IS_COMBINED_MASK; + } else { + // should not happen - we actually have no data in the cell yet + // simply set the data in the cell buffer with a width of 1 + content = codePoint | (1 << Content.WIDTH_SHIFT); + } + this._data[index * CELL_SIZE + Cell.CONTENT] = content; + } + } + + public insertCells(pos: number, n: number, fillCellData: ICellData, eraseAttr?: IAttributeData): void { + pos %= this.length; + + // handle fullwidth at pos: reset cell one to the left if pos is second cell of a wide char + if (pos && this.getWidth(pos - 1) === 2) { + this.setCellFromCodePoint(pos - 1, 0, 1, eraseAttr?.fg || 0, eraseAttr?.bg || 0, eraseAttr?.extended || new ExtendedAttrs()); + } + + if (n < this.length - pos) { + const cell = new CellData(); + for (let i = this.length - pos - n - 1; i >= 0; --i) { + this.setCell(pos + n + i, this.loadCell(pos + i, cell)); + } + for (let i = 0; i < n; ++i) { + this.setCell(pos + i, fillCellData); + } + } else { + for (let i = pos; i < this.length; ++i) { + this.setCell(i, fillCellData); + } + } + + // handle fullwidth at line end: reset last cell if it is first cell of a wide char + if (this.getWidth(this.length - 1) === 2) { + this.setCellFromCodePoint(this.length - 1, 0, 1, eraseAttr?.fg || 0, eraseAttr?.bg || 0, eraseAttr?.extended || new ExtendedAttrs()); + } + } + + public deleteCells(pos: number, n: number, fillCellData: ICellData, eraseAttr?: IAttributeData): void { + pos %= this.length; + if (n < this.length - pos) { + const cell = new CellData(); + for (let i = 0; i < this.length - pos - n; ++i) { + this.setCell(pos + i, this.loadCell(pos + n + i, cell)); + } + for (let i = this.length - n; i < this.length; ++i) { + this.setCell(i, fillCellData); + } + } else { + for (let i = pos; i < this.length; ++i) { + this.setCell(i, fillCellData); + } + } + + // handle fullwidth at pos: + // - reset pos-1 if wide char + // - reset pos if width==0 (previous second cell of a wide char) + if (pos && this.getWidth(pos - 1) === 2) { + this.setCellFromCodePoint(pos - 1, 0, 1, eraseAttr?.fg || 0, eraseAttr?.bg || 0, eraseAttr?.extended || new ExtendedAttrs()); + } + if (this.getWidth(pos) === 0 && !this.hasContent(pos)) { + this.setCellFromCodePoint(pos, 0, 1, eraseAttr?.fg || 0, eraseAttr?.bg || 0, eraseAttr?.extended || new ExtendedAttrs()); + } + } + + public replaceCells(start: number, end: number, fillCellData: ICellData, eraseAttr?: IAttributeData): void { + // handle fullwidth at start: reset cell one to the left if start is second cell of a wide char + if (start && this.getWidth(start - 1) === 2) { + this.setCellFromCodePoint(start - 1, 0, 1, eraseAttr?.fg || 0, eraseAttr?.bg || 0, eraseAttr?.extended || new ExtendedAttrs()); + } + // handle fullwidth at last cell + 1: reset to empty cell if it is second part of a wide char + if (end < this.length && this.getWidth(end - 1) === 2) { + this.setCellFromCodePoint(end, 0, 1, eraseAttr?.fg || 0, eraseAttr?.bg || 0, eraseAttr?.extended || new ExtendedAttrs()); + } + + while (start < end && start < this.length) { + this.setCell(start++, fillCellData); + } + } + + public resize(cols: number, fillCellData: ICellData): void { + if (cols === this.length) { + return; + } + if (cols > this.length) { + const data = new Uint32Array(cols * CELL_SIZE); + if (this.length) { + if (cols * CELL_SIZE < this._data.length) { + data.set(this._data.subarray(0, cols * CELL_SIZE)); + } else { + data.set(this._data); + } + } + this._data = data; + for (let i = this.length; i < cols; ++i) { + this.setCell(i, fillCellData); + } + } else { + if (cols) { + const data = new Uint32Array(cols * CELL_SIZE); + data.set(this._data.subarray(0, cols * CELL_SIZE)); + this._data = data; + // Remove any cut off combined data, FIXME: repeat this for extended attrs + const keys = Object.keys(this._combined); + for (let i = 0; i < keys.length; i++) { + const key = parseInt(keys[i], 10); + if (key >= cols) { + delete this._combined[key]; + } + } + } else { + this._data = new Uint32Array(0); + this._combined = {}; + } + } + this.length = cols; + } + + /** fill a line with fillCharData */ + public fill(fillCellData: ICellData): void { + this._combined = {}; + this._extendedAttrs = {}; + for (let i = 0; i < this.length; ++i) { + this.setCell(i, fillCellData); + } + } + + /** alter to a full copy of line */ + public copyFrom(line: BufferLine): void { + if (this.length !== line.length) { + this._data = new Uint32Array(line._data); + } else { + // use high speed copy if lengths are equal + this._data.set(line._data); + } + this.length = line.length; + this._combined = {}; + for (const el in line._combined) { + this._combined[el] = line._combined[el]; + } + this._extendedAttrs = {}; + for (const el in line._extendedAttrs) { + this._extendedAttrs[el] = line._extendedAttrs[el]; + } + this.isWrapped = line.isWrapped; + } + + /** create a new clone */ + public clone(): IBufferLine { + const newLine = new BufferLine(0); + newLine._data = new Uint32Array(this._data); + newLine.length = this.length; + for (const el in this._combined) { + newLine._combined[el] = this._combined[el]; + } + for (const el in this._extendedAttrs) { + newLine._extendedAttrs[el] = this._extendedAttrs[el]; + } + newLine.isWrapped = this.isWrapped; + return newLine; + } + + public getTrimmedLength(): number { + for (let i = this.length - 1; i >= 0; --i) { + if ((this._data[i * CELL_SIZE + Cell.CONTENT] & Content.HAS_CONTENT_MASK)) { + return i + (this._data[i * CELL_SIZE + Cell.CONTENT] >> Content.WIDTH_SHIFT); + } + } + return 0; + } + + public copyCellsFrom(src: BufferLine, srcCol: number, destCol: number, length: number, applyInReverse: boolean): void { + const srcData = src._data; + if (applyInReverse) { + for (let cell = length - 1; cell >= 0; cell--) { + for (let i = 0; i < CELL_SIZE; i++) { + this._data[(destCol + cell) * CELL_SIZE + i] = srcData[(srcCol + cell) * CELL_SIZE + i]; + } + } + } else { + for (let cell = 0; cell < length; cell++) { + for (let i = 0; i < CELL_SIZE; i++) { + this._data[(destCol + cell) * CELL_SIZE + i] = srcData[(srcCol + cell) * CELL_SIZE + i]; + } + } + } + + // Move any combined data over as needed, FIXME: repeat for extended attrs + const srcCombinedKeys = Object.keys(src._combined); + for (let i = 0; i < srcCombinedKeys.length; i++) { + const key = parseInt(srcCombinedKeys[i], 10); + if (key >= srcCol) { + this._combined[key - srcCol + destCol] = src._combined[key]; + } + } + } + + public translateToString(trimRight: boolean = false, startCol: number = 0, endCol: number = this.length): string { + if (trimRight) { + endCol = Math.min(endCol, this.getTrimmedLength()); + } + let result = ''; + while (startCol < endCol) { + const content = this._data[startCol * CELL_SIZE + Cell.CONTENT]; + const cp = content & Content.CODEPOINT_MASK; + result += (content & Content.IS_COMBINED_MASK) ? this._combined[startCol] : (cp) ? stringFromCodePoint(cp) : WHITESPACE_CELL_CHAR; + startCol += (content >> Content.WIDTH_SHIFT) || 1; // always advance by 1 + } + return result; + } +} diff --git a/node_modules/xterm/src/common/buffer/BufferRange.ts b/node_modules/xterm/src/common/buffer/BufferRange.ts new file mode 100644 index 0000000..a49cf48 --- /dev/null +++ b/node_modules/xterm/src/common/buffer/BufferRange.ts @@ -0,0 +1,13 @@ +/** + * Copyright (c) 2021 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IBufferRange } from 'xterm'; + +export function getRangeLength(range: IBufferRange, bufferCols: number): number { + if (range.start.y > range.end.y) { + throw new Error(`Buffer range end (${range.end.x}, ${range.end.y}) cannot be before start (${range.start.x}, ${range.start.y})`); + } + return bufferCols * (range.end.y - range.start.y) + (range.end.x - range.start.x + 1); +} diff --git a/node_modules/xterm/src/common/buffer/BufferReflow.ts b/node_modules/xterm/src/common/buffer/BufferReflow.ts new file mode 100644 index 0000000..ece9a96 --- /dev/null +++ b/node_modules/xterm/src/common/buffer/BufferReflow.ts @@ -0,0 +1,220 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { BufferLine } from 'common/buffer/BufferLine'; +import { CircularList } from 'common/CircularList'; +import { IBufferLine, ICellData } from 'common/Types'; + +export interface INewLayoutResult { + layout: number[]; + countRemoved: number; +} + +/** + * Evaluates and returns indexes to be removed after a reflow larger occurs. Lines will be removed + * when a wrapped line unwraps. + * @param lines The buffer lines. + * @param newCols The columns after resize. + */ +export function reflowLargerGetLinesToRemove(lines: CircularList<IBufferLine>, oldCols: number, newCols: number, bufferAbsoluteY: number, nullCell: ICellData): number[] { + // Gather all BufferLines that need to be removed from the Buffer here so that they can be + // batched up and only committed once + const toRemove: number[] = []; + + for (let y = 0; y < lines.length - 1; y++) { + // Check if this row is wrapped + let i = y; + let nextLine = lines.get(++i) as BufferLine; + if (!nextLine.isWrapped) { + continue; + } + + // Check how many lines it's wrapped for + const wrappedLines: BufferLine[] = [lines.get(y) as BufferLine]; + while (i < lines.length && nextLine.isWrapped) { + wrappedLines.push(nextLine); + nextLine = lines.get(++i) as BufferLine; + } + + // If these lines contain the cursor don't touch them, the program will handle fixing up wrapped + // lines with the cursor + if (bufferAbsoluteY >= y && bufferAbsoluteY < i) { + y += wrappedLines.length - 1; + continue; + } + + // Copy buffer data to new locations + let destLineIndex = 0; + let destCol = getWrappedLineTrimmedLength(wrappedLines, destLineIndex, oldCols); + let srcLineIndex = 1; + let srcCol = 0; + while (srcLineIndex < wrappedLines.length) { + const srcTrimmedTineLength = getWrappedLineTrimmedLength(wrappedLines, srcLineIndex, oldCols); + const srcRemainingCells = srcTrimmedTineLength - srcCol; + const destRemainingCells = newCols - destCol; + const cellsToCopy = Math.min(srcRemainingCells, destRemainingCells); + + wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[srcLineIndex], srcCol, destCol, cellsToCopy, false); + + destCol += cellsToCopy; + if (destCol === newCols) { + destLineIndex++; + destCol = 0; + } + srcCol += cellsToCopy; + if (srcCol === srcTrimmedTineLength) { + srcLineIndex++; + srcCol = 0; + } + + // Make sure the last cell isn't wide, if it is copy it to the current dest + if (destCol === 0 && destLineIndex !== 0) { + if (wrappedLines[destLineIndex - 1].getWidth(newCols - 1) === 2) { + wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[destLineIndex - 1], newCols - 1, destCol++, 1, false); + // Null out the end of the last row + wrappedLines[destLineIndex - 1].setCell(newCols - 1, nullCell); + } + } + } + + // Clear out remaining cells or fragments could remain; + wrappedLines[destLineIndex].replaceCells(destCol, newCols, nullCell); + + // Work backwards and remove any rows at the end that only contain null cells + let countToRemove = 0; + for (let i = wrappedLines.length - 1; i > 0; i--) { + if (i > destLineIndex || wrappedLines[i].getTrimmedLength() === 0) { + countToRemove++; + } else { + break; + } + } + + if (countToRemove > 0) { + toRemove.push(y + wrappedLines.length - countToRemove); // index + toRemove.push(countToRemove); + } + + y += wrappedLines.length - 1; + } + return toRemove; +} + +/** + * Creates and return the new layout for lines given an array of indexes to be removed. + * @param lines The buffer lines. + * @param toRemove The indexes to remove. + */ +export function reflowLargerCreateNewLayout(lines: CircularList<IBufferLine>, toRemove: number[]): INewLayoutResult { + const layout: number[] = []; + // First iterate through the list and get the actual indexes to use for rows + let nextToRemoveIndex = 0; + let nextToRemoveStart = toRemove[nextToRemoveIndex]; + let countRemovedSoFar = 0; + for (let i = 0; i < lines.length; i++) { + if (nextToRemoveStart === i) { + const countToRemove = toRemove[++nextToRemoveIndex]; + + // Tell markers that there was a deletion + lines.onDeleteEmitter.fire({ + index: i - countRemovedSoFar, + amount: countToRemove + }); + + i += countToRemove - 1; + countRemovedSoFar += countToRemove; + nextToRemoveStart = toRemove[++nextToRemoveIndex]; + } else { + layout.push(i); + } + } + return { + layout, + countRemoved: countRemovedSoFar + }; +} + +/** + * Applies a new layout to the buffer. This essentially does the same as many splice calls but it's + * done all at once in a single iteration through the list since splice is very expensive. + * @param lines The buffer lines. + * @param newLayout The new layout to apply. + */ +export function reflowLargerApplyNewLayout(lines: CircularList<IBufferLine>, newLayout: number[]): void { + // Record original lines so they don't get overridden when we rearrange the list + const newLayoutLines: BufferLine[] = []; + for (let i = 0; i < newLayout.length; i++) { + newLayoutLines.push(lines.get(newLayout[i]) as BufferLine); + } + + // Rearrange the list + for (let i = 0; i < newLayoutLines.length; i++) { + lines.set(i, newLayoutLines[i]); + } + lines.length = newLayout.length; +} + +/** + * Gets the new line lengths for a given wrapped line. The purpose of this function it to pre- + * compute the wrapping points since wide characters may need to be wrapped onto the following line. + * This function will return an array of numbers of where each line wraps to, the resulting array + * will only contain the values `newCols` (when the line does not end with a wide character) and + * `newCols - 1` (when the line does end with a wide character), except for the last value which + * will contain the remaining items to fill the line. + * + * Calling this with a `newCols` value of `1` will lock up. + * + * @param wrappedLines The wrapped lines to evaluate. + * @param oldCols The columns before resize. + * @param newCols The columns after resize. + */ +export function reflowSmallerGetNewLineLengths(wrappedLines: BufferLine[], oldCols: number, newCols: number): number[] { + const newLineLengths: number[] = []; + const cellsNeeded = wrappedLines.map((l, i) => getWrappedLineTrimmedLength(wrappedLines, i, oldCols)).reduce((p, c) => p + c); + + // Use srcCol and srcLine to find the new wrapping point, use that to get the cellsAvailable and + // linesNeeded + let srcCol = 0; + let srcLine = 0; + let cellsAvailable = 0; + while (cellsAvailable < cellsNeeded) { + if (cellsNeeded - cellsAvailable < newCols) { + // Add the final line and exit the loop + newLineLengths.push(cellsNeeded - cellsAvailable); + break; + } + srcCol += newCols; + const oldTrimmedLength = getWrappedLineTrimmedLength(wrappedLines, srcLine, oldCols); + if (srcCol > oldTrimmedLength) { + srcCol -= oldTrimmedLength; + srcLine++; + } + const endsWithWide = wrappedLines[srcLine].getWidth(srcCol - 1) === 2; + if (endsWithWide) { + srcCol--; + } + const lineLength = endsWithWide ? newCols - 1 : newCols; + newLineLengths.push(lineLength); + cellsAvailable += lineLength; + } + + return newLineLengths; +} + +export function getWrappedLineTrimmedLength(lines: BufferLine[], i: number, cols: number): number { + // If this is the last row in the wrapped line, get the actual trimmed length + if (i === lines.length - 1) { + return lines[i].getTrimmedLength(); + } + // Detect whether the following line starts with a wide character and the end of the current line + // is null, if so then we can be pretty sure the null character should be excluded from the line + // length] + const endsInNull = !(lines[i].hasContent(cols - 1)) && lines[i].getWidth(cols - 1) === 1; + const followingLineStartsWithWide = lines[i + 1].getWidth(0) === 2; + if (endsInNull && followingLineStartsWithWide) { + return cols - 1; + } + return cols; +} diff --git a/node_modules/xterm/src/common/buffer/BufferSet.ts b/node_modules/xterm/src/common/buffer/BufferSet.ts new file mode 100644 index 0000000..de220e8 --- /dev/null +++ b/node_modules/xterm/src/common/buffer/BufferSet.ts @@ -0,0 +1,131 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IBuffer, IBufferSet } from 'common/buffer/Types'; +import { IAttributeData } from 'common/Types'; +import { Buffer } from 'common/buffer/Buffer'; +import { EventEmitter, IEvent } from 'common/EventEmitter'; +import { IOptionsService, IBufferService } from 'common/services/Services'; +import { Disposable } from 'common/Lifecycle'; + +/** + * The BufferSet represents the set of two buffers used by xterm terminals (normal and alt) and + * provides also utilities for working with them. + */ +export class BufferSet extends Disposable implements IBufferSet { + private _normal!: Buffer; + private _alt!: Buffer; + private _activeBuffer!: Buffer; + + private _onBufferActivate = this.register(new EventEmitter<{activeBuffer: IBuffer, inactiveBuffer: IBuffer}>()); + public get onBufferActivate(): IEvent<{activeBuffer: IBuffer, inactiveBuffer: IBuffer}> { return this._onBufferActivate.event; } + + /** + * Create a new BufferSet for the given terminal. + * @param _terminal - The terminal the BufferSet will belong to + */ + constructor( + private readonly _optionsService: IOptionsService, + private readonly _bufferService: IBufferService + ) { + super(); + this.reset(); + } + + public reset(): void { + this._normal = new Buffer(true, this._optionsService, this._bufferService); + this._normal.fillViewportRows(); + + // The alt buffer should never have scrollback. + // See http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-The-Alternate-Screen-Buffer + this._alt = new Buffer(false, this._optionsService, this._bufferService); + this._activeBuffer = this._normal; + this._onBufferActivate.fire({ + activeBuffer: this._normal, + inactiveBuffer: this._alt + }); + + this.setupTabStops(); + } + + /** + * Returns the alt Buffer of the BufferSet + */ + public get alt(): Buffer { + return this._alt; + } + + /** + * Returns the normal Buffer of the BufferSet + */ + public get active(): Buffer { + return this._activeBuffer; + } + + /** + * Returns the currently active Buffer of the BufferSet + */ + public get normal(): Buffer { + return this._normal; + } + + /** + * Sets the normal Buffer of the BufferSet as its currently active Buffer + */ + public activateNormalBuffer(): void { + if (this._activeBuffer === this._normal) { + return; + } + this._normal.x = this._alt.x; + this._normal.y = this._alt.y; + // The alt buffer should always be cleared when we switch to the normal + // buffer. This frees up memory since the alt buffer should always be new + // when activated. + this._alt.clear(); + this._activeBuffer = this._normal; + this._onBufferActivate.fire({ + activeBuffer: this._normal, + inactiveBuffer: this._alt + }); + } + + /** + * Sets the alt Buffer of the BufferSet as its currently active Buffer + */ + public activateAltBuffer(fillAttr?: IAttributeData): void { + if (this._activeBuffer === this._alt) { + return; + } + // Since the alt buffer is always cleared when the normal buffer is + // activated, we want to fill it when switching to it. + this._alt.fillViewportRows(fillAttr); + this._alt.x = this._normal.x; + this._alt.y = this._normal.y; + this._activeBuffer = this._alt; + this._onBufferActivate.fire({ + activeBuffer: this._alt, + inactiveBuffer: this._normal + }); + } + + /** + * Resizes both normal and alt buffers, adjusting their data accordingly. + * @param newCols The new number of columns. + * @param newRows The new number of rows. + */ + public resize(newCols: number, newRows: number): void { + this._normal.resize(newCols, newRows); + this._alt.resize(newCols, newRows); + } + + /** + * Setup the tab stops. + * @param i The index to start setting up tab stops from. + */ + public setupTabStops(i?: number): void { + this._normal.setupTabStops(i); + this._alt.setupTabStops(i); + } +} diff --git a/node_modules/xterm/src/common/buffer/CellData.ts b/node_modules/xterm/src/common/buffer/CellData.ts new file mode 100644 index 0000000..a87b579 --- /dev/null +++ b/node_modules/xterm/src/common/buffer/CellData.ts @@ -0,0 +1,94 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { CharData, ICellData, IExtendedAttrs } from 'common/Types'; +import { stringFromCodePoint } from 'common/input/TextDecoder'; +import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_ATTR_INDEX, Content } from 'common/buffer/Constants'; +import { AttributeData, ExtendedAttrs } from 'common/buffer/AttributeData'; + +/** + * CellData - represents a single Cell in the terminal buffer. + */ +export class CellData extends AttributeData implements ICellData { + /** Helper to create CellData from CharData. */ + public static fromCharData(value: CharData): CellData { + const obj = new CellData(); + obj.setFromCharData(value); + return obj; + } + /** Primitives from terminal buffer. */ + public content = 0; + public fg = 0; + public bg = 0; + public extended: IExtendedAttrs = new ExtendedAttrs(); + public combinedData = ''; + /** Whether cell contains a combined string. */ + public isCombined(): number { + return this.content & Content.IS_COMBINED_MASK; + } + /** Width of the cell. */ + public getWidth(): number { + return this.content >> Content.WIDTH_SHIFT; + } + /** JS string of the content. */ + public getChars(): string { + if (this.content & Content.IS_COMBINED_MASK) { + return this.combinedData; + } + if (this.content & Content.CODEPOINT_MASK) { + return stringFromCodePoint(this.content & Content.CODEPOINT_MASK); + } + return ''; + } + /** + * Codepoint of cell + * Note this returns the UTF32 codepoint of single chars, + * if content is a combined string it returns the codepoint + * of the last char in string to be in line with code in CharData. + * */ + public getCode(): number { + return (this.isCombined()) + ? this.combinedData.charCodeAt(this.combinedData.length - 1) + : this.content & Content.CODEPOINT_MASK; + } + /** Set data from CharData */ + public setFromCharData(value: CharData): void { + this.fg = value[CHAR_DATA_ATTR_INDEX]; + this.bg = 0; + let combined = false; + // surrogates and combined strings need special treatment + if (value[CHAR_DATA_CHAR_INDEX].length > 2) { + combined = true; + } + else if (value[CHAR_DATA_CHAR_INDEX].length === 2) { + const code = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0); + // if the 2-char string is a surrogate create single codepoint + // everything else is combined + if (0xD800 <= code && code <= 0xDBFF) { + const second = value[CHAR_DATA_CHAR_INDEX].charCodeAt(1); + if (0xDC00 <= second && second <= 0xDFFF) { + this.content = ((code - 0xD800) * 0x400 + second - 0xDC00 + 0x10000) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT); + } + else { + combined = true; + } + } + else { + combined = true; + } + } + else { + this.content = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT); + } + if (combined) { + this.combinedData = value[CHAR_DATA_CHAR_INDEX]; + this.content = Content.IS_COMBINED_MASK | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT); + } + } + /** Get data as CharData. */ + public getAsCharData(): CharData { + return [this.fg, this.getChars(), this.getWidth(), this.getCode()]; + } +} diff --git a/node_modules/xterm/src/common/buffer/Constants.ts b/node_modules/xterm/src/common/buffer/Constants.ts new file mode 100644 index 0000000..a2c1b88 --- /dev/null +++ b/node_modules/xterm/src/common/buffer/Constants.ts @@ -0,0 +1,139 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +export const DEFAULT_COLOR = 256; +export const DEFAULT_ATTR = (0 << 18) | (DEFAULT_COLOR << 9) | (256 << 0); + +export const CHAR_DATA_ATTR_INDEX = 0; +export const CHAR_DATA_CHAR_INDEX = 1; +export const CHAR_DATA_WIDTH_INDEX = 2; +export const CHAR_DATA_CODE_INDEX = 3; + +/** + * Null cell - a real empty cell (containing nothing). + * Note that code should always be 0 for a null cell as + * several test condition of the buffer line rely on this. + */ +export const NULL_CELL_CHAR = ''; +export const NULL_CELL_WIDTH = 1; +export const NULL_CELL_CODE = 0; + +/** + * Whitespace cell. + * This is meant as a replacement for empty cells when needed + * during rendering lines to preserve correct aligment. + */ +export const WHITESPACE_CELL_CHAR = ' '; +export const WHITESPACE_CELL_WIDTH = 1; +export const WHITESPACE_CELL_CODE = 32; + +/** + * Bitmasks for accessing data in `content`. + */ +export const enum Content { + /** + * bit 1..21 codepoint, max allowed in UTF32 is 0x10FFFF (21 bits taken) + * read: `codepoint = content & Content.codepointMask;` + * write: `content |= codepoint & Content.codepointMask;` + * shortcut if precondition `codepoint <= 0x10FFFF` is met: + * `content |= codepoint;` + */ + CODEPOINT_MASK = 0x1FFFFF, + + /** + * bit 22 flag indication whether a cell contains combined content + * read: `isCombined = content & Content.isCombined;` + * set: `content |= Content.isCombined;` + * clear: `content &= ~Content.isCombined;` + */ + IS_COMBINED_MASK = 0x200000, // 1 << 21 + + /** + * bit 1..22 mask to check whether a cell contains any string data + * we need to check for codepoint and isCombined bits to see + * whether a cell contains anything + * read: `isEmpty = !(content & Content.hasContent)` + */ + HAS_CONTENT_MASK = 0x3FFFFF, + + /** + * bit 23..24 wcwidth value of cell, takes 2 bits (ranges from 0..2) + * read: `width = (content & Content.widthMask) >> Content.widthShift;` + * `hasWidth = content & Content.widthMask;` + * as long as wcwidth is highest value in `content`: + * `width = content >> Content.widthShift;` + * write: `content |= (width << Content.widthShift) & Content.widthMask;` + * shortcut if precondition `0 <= width <= 3` is met: + * `content |= width << Content.widthShift;` + */ + WIDTH_MASK = 0xC00000, // 3 << 22 + WIDTH_SHIFT = 22 +} + +export const enum Attributes { + /** + * bit 1..8 blue in RGB, color in P256 and P16 + */ + BLUE_MASK = 0xFF, + BLUE_SHIFT = 0, + PCOLOR_MASK = 0xFF, + PCOLOR_SHIFT = 0, + + /** + * bit 9..16 green in RGB + */ + GREEN_MASK = 0xFF00, + GREEN_SHIFT = 8, + + /** + * bit 17..24 red in RGB + */ + RED_MASK = 0xFF0000, + RED_SHIFT = 16, + + /** + * bit 25..26 color mode: DEFAULT (0) | P16 (1) | P256 (2) | RGB (3) + */ + CM_MASK = 0x3000000, + CM_DEFAULT = 0, + CM_P16 = 0x1000000, + CM_P256 = 0x2000000, + CM_RGB = 0x3000000, + + /** + * bit 1..24 RGB room + */ + RGB_MASK = 0xFFFFFF +} + +export const enum FgFlags { + /** + * bit 27..32 + */ + INVERSE = 0x4000000, + BOLD = 0x8000000, + UNDERLINE = 0x10000000, + BLINK = 0x20000000, + INVISIBLE = 0x40000000, + STRIKETHROUGH = 0x80000000, +} + +export const enum BgFlags { + /** + * bit 27..32 (upper 3 unused) + */ + ITALIC = 0x4000000, + DIM = 0x8000000, + HAS_EXTENDED = 0x10000000 +} + +export const enum UnderlineStyle { + NONE = 0, + SINGLE = 1, + DOUBLE = 2, + CURLY = 3, + DOTTED = 4, + DASHED = 5 +} diff --git a/node_modules/xterm/src/common/buffer/Marker.ts b/node_modules/xterm/src/common/buffer/Marker.ts new file mode 100644 index 0000000..72c4085 --- /dev/null +++ b/node_modules/xterm/src/common/buffer/Marker.ts @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { EventEmitter, IEvent } from 'common/EventEmitter'; +import { Disposable } from 'common/Lifecycle'; +import { IMarker } from 'common/Types'; + +export class Marker extends Disposable implements IMarker { + private static _nextId = 1; + + private _id: number = Marker._nextId++; + public isDisposed: boolean = false; + + public get id(): number { return this._id; } + + private _onDispose = new EventEmitter<void>(); + public get onDispose(): IEvent<void> { return this._onDispose.event; } + + constructor( + public line: number + ) { + super(); + } + + public dispose(): void { + if (this.isDisposed) { + return; + } + this.isDisposed = true; + this.line = -1; + // Emit before super.dispose such that dispose listeners get a change to react + this._onDispose.fire(); + super.dispose(); + } +} diff --git a/node_modules/xterm/src/common/buffer/Types.d.ts b/node_modules/xterm/src/common/buffer/Types.d.ts new file mode 100644 index 0000000..cbf40a0 --- /dev/null +++ b/node_modules/xterm/src/common/buffer/Types.d.ts @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IAttributeData, ICircularList, IBufferLine, ICellData, IMarker, ICharset, IDisposable } from 'common/Types'; +import { IEvent } from 'common/EventEmitter'; + +// BufferIndex denotes a position in the buffer: [rowIndex, colIndex] +export type BufferIndex = [number, number]; + +export interface IBufferStringIteratorResult { + range: {first: number, last: number}; + content: string; +} + +export interface IBufferStringIterator { + hasNext(): boolean; + next(): IBufferStringIteratorResult; +} + +export interface IBuffer { + readonly lines: ICircularList<IBufferLine>; + ydisp: number; + ybase: number; + y: number; + x: number; + tabs: any; + scrollBottom: number; + scrollTop: number; + hasScrollback: boolean; + savedY: number; + savedX: number; + savedCharset: ICharset | undefined; + savedCurAttrData: IAttributeData; + isCursorInViewport: boolean; + markers: IMarker[]; + translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol?: number, endCol?: number): string; + getWrappedRangeForLine(y: number): { first: number, last: number }; + nextStop(x?: number): number; + prevStop(x?: number): number; + getBlankLine(attr: IAttributeData, isWrapped?: boolean): IBufferLine; + stringIndexToBufferIndex(lineIndex: number, stringIndex: number, trimRight?: boolean): number[]; + iterator(trimRight: boolean, startIndex?: number, endIndex?: number, startOverscan?: number, endOverscan?: number): IBufferStringIterator; + getNullCell(attr?: IAttributeData): ICellData; + getWhitespaceCell(attr?: IAttributeData): ICellData; + addMarker(y: number): IMarker; +} + +export interface IBufferSet extends IDisposable { + alt: IBuffer; + normal: IBuffer; + active: IBuffer; + + onBufferActivate: IEvent<{ activeBuffer: IBuffer, inactiveBuffer: IBuffer }>; + + activateNormalBuffer(): void; + activateAltBuffer(fillAttr?: IAttributeData): void; + reset(): void; + resize(newCols: number, newRows: number): void; + setupTabStops(i?: number): void; +} diff --git a/node_modules/xterm/src/common/data/Charsets.ts b/node_modules/xterm/src/common/data/Charsets.ts new file mode 100644 index 0000000..c72d5a2 --- /dev/null +++ b/node_modules/xterm/src/common/data/Charsets.ts @@ -0,0 +1,256 @@ +/** + * Copyright (c) 2016 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { ICharset } from 'common/Types'; + +/** + * The character sets supported by the terminal. These enable several languages + * to be represented within the terminal with only 8-bit encoding. See ISO 2022 + * for a discussion on character sets. Only VT100 character sets are supported. + */ +export const CHARSETS: { [key: string]: ICharset | undefined } = {}; + +/** + * The default character set, US. + */ +export const DEFAULT_CHARSET: ICharset | undefined = CHARSETS['B']; + +/** + * DEC Special Character and Line Drawing Set. + * Reference: http://vt100.net/docs/vt102-ug/table5-13.html + * A lot of curses apps use this if they see TERM=xterm. + * testing: echo -e '\e(0a\e(B' + * The xterm output sometimes seems to conflict with the + * reference above. xterm seems in line with the reference + * when running vttest however. + * The table below now uses xterm's output from vttest. + */ +CHARSETS['0'] = { + '`': '\u25c6', // '◆' + 'a': '\u2592', // '▒' + 'b': '\u2409', // '␉' (HT) + 'c': '\u240c', // '␌' (FF) + 'd': '\u240d', // '␍' (CR) + 'e': '\u240a', // '␊' (LF) + 'f': '\u00b0', // '°' + 'g': '\u00b1', // '±' + 'h': '\u2424', // '' (NL) + 'i': '\u240b', // '␋' (VT) + 'j': '\u2518', // '┘' + 'k': '\u2510', // '┐' + 'l': '\u250c', // '┌' + 'm': '\u2514', // '└' + 'n': '\u253c', // '┼' + 'o': '\u23ba', // '⎺' + 'p': '\u23bb', // '⎻' + 'q': '\u2500', // '─' + 'r': '\u23bc', // '⎼' + 's': '\u23bd', // '⎽' + 't': '\u251c', // '├' + 'u': '\u2524', // '┤' + 'v': '\u2534', // '┴' + 'w': '\u252c', // '┬' + 'x': '\u2502', // '│' + 'y': '\u2264', // '≤' + 'z': '\u2265', // '≥' + '{': '\u03c0', // 'π' + '|': '\u2260', // '≠' + '}': '\u00a3', // '£' + '~': '\u00b7' // '·' +}; + +/** + * British character set + * ESC (A + * Reference: http://vt100.net/docs/vt220-rm/table2-5.html + */ +CHARSETS['A'] = { + '#': '£' +}; + +/** + * United States character set + * ESC (B + */ +CHARSETS['B'] = undefined; + +/** + * Dutch character set + * ESC (4 + * Reference: http://vt100.net/docs/vt220-rm/table2-6.html + */ +CHARSETS['4'] = { + '#': '£', + '@': '¾', + '[': 'ij', + '\\': '½', + ']': '|', + '{': '¨', + '|': 'f', + '}': '¼', + '~': '´' +}; + +/** + * Finnish character set + * ESC (C or ESC (5 + * Reference: http://vt100.net/docs/vt220-rm/table2-7.html + */ +CHARSETS['C'] = +CHARSETS['5'] = { + '[': 'Ä', + '\\': 'Ö', + ']': 'Å', + '^': 'Ü', + '`': 'é', + '{': 'ä', + '|': 'ö', + '}': 'å', + '~': 'ü' +}; + +/** + * French character set + * ESC (R + * Reference: http://vt100.net/docs/vt220-rm/table2-8.html + */ +CHARSETS['R'] = { + '#': '£', + '@': 'à', + '[': '°', + '\\': 'ç', + ']': '§', + '{': 'é', + '|': 'ù', + '}': 'è', + '~': '¨' +}; + +/** + * French Canadian character set + * ESC (Q + * Reference: http://vt100.net/docs/vt220-rm/table2-9.html + */ +CHARSETS['Q'] = { + '@': 'à', + '[': 'â', + '\\': 'ç', + ']': 'ê', + '^': 'î', + '`': 'ô', + '{': 'é', + '|': 'ù', + '}': 'è', + '~': 'û' +}; + +/** + * German character set + * ESC (K + * Reference: http://vt100.net/docs/vt220-rm/table2-10.html + */ +CHARSETS['K'] = { + '@': '§', + '[': 'Ä', + '\\': 'Ö', + ']': 'Ü', + '{': 'ä', + '|': 'ö', + '}': 'ü', + '~': 'ß' +}; + +/** + * Italian character set + * ESC (Y + * Reference: http://vt100.net/docs/vt220-rm/table2-11.html + */ +CHARSETS['Y'] = { + '#': '£', + '@': '§', + '[': '°', + '\\': 'ç', + ']': 'é', + '`': 'ù', + '{': 'à', + '|': 'ò', + '}': 'è', + '~': 'ì' +}; + +/** + * Norwegian/Danish character set + * ESC (E or ESC (6 + * Reference: http://vt100.net/docs/vt220-rm/table2-12.html + */ +CHARSETS['E'] = +CHARSETS['6'] = { + '@': 'Ä', + '[': 'Æ', + '\\': 'Ø', + ']': 'Å', + '^': 'Ü', + '`': 'ä', + '{': 'æ', + '|': 'ø', + '}': 'å', + '~': 'ü' +}; + +/** + * Spanish character set + * ESC (Z + * Reference: http://vt100.net/docs/vt220-rm/table2-13.html + */ +CHARSETS['Z'] = { + '#': '£', + '@': '§', + '[': '¡', + '\\': 'Ñ', + ']': '¿', + '{': '°', + '|': 'ñ', + '}': 'ç' +}; + +/** + * Swedish character set + * ESC (H or ESC (7 + * Reference: http://vt100.net/docs/vt220-rm/table2-14.html + */ +CHARSETS['H'] = +CHARSETS['7'] = { + '@': 'É', + '[': 'Ä', + '\\': 'Ö', + ']': 'Å', + '^': 'Ü', + '`': 'é', + '{': 'ä', + '|': 'ö', + '}': 'å', + '~': 'ü' +}; + +/** + * Swiss character set + * ESC (= + * Reference: http://vt100.net/docs/vt220-rm/table2-15.html + */ +CHARSETS['='] = { + '#': 'ù', + '@': 'à', + '[': 'é', + '\\': 'ç', + ']': 'ê', + '^': 'î', + // eslint-disable-next-line @typescript-eslint/naming-convention + '_': 'è', + '`': 'ô', + '{': 'ä', + '|': 'ö', + '}': 'ü', + '~': 'û' +}; diff --git a/node_modules/xterm/src/common/data/EscapeSequences.ts b/node_modules/xterm/src/common/data/EscapeSequences.ts new file mode 100644 index 0000000..e35f01d --- /dev/null +++ b/node_modules/xterm/src/common/data/EscapeSequences.ts @@ -0,0 +1,150 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +/** + * C0 control codes + * See = https://en.wikipedia.org/wiki/C0_and_C1_control_codes + */ +export namespace C0 { + /** Null (Caret = ^@, C = \0) */ + export const NUL = '\x00'; + /** Start of Heading (Caret = ^A) */ + export const SOH = '\x01'; + /** Start of Text (Caret = ^B) */ + export const STX = '\x02'; + /** End of Text (Caret = ^C) */ + export const ETX = '\x03'; + /** End of Transmission (Caret = ^D) */ + export const EOT = '\x04'; + /** Enquiry (Caret = ^E) */ + export const ENQ = '\x05'; + /** Acknowledge (Caret = ^F) */ + export const ACK = '\x06'; + /** Bell (Caret = ^G, C = \a) */ + export const BEL = '\x07'; + /** Backspace (Caret = ^H, C = \b) */ + export const BS = '\x08'; + /** Character Tabulation, Horizontal Tabulation (Caret = ^I, C = \t) */ + export const HT = '\x09'; + /** Line Feed (Caret = ^J, C = \n) */ + export const LF = '\x0a'; + /** Line Tabulation, Vertical Tabulation (Caret = ^K, C = \v) */ + export const VT = '\x0b'; + /** Form Feed (Caret = ^L, C = \f) */ + export const FF = '\x0c'; + /** Carriage Return (Caret = ^M, C = \r) */ + export const CR = '\x0d'; + /** Shift Out (Caret = ^N) */ + export const SO = '\x0e'; + /** Shift In (Caret = ^O) */ + export const SI = '\x0f'; + /** Data Link Escape (Caret = ^P) */ + export const DLE = '\x10'; + /** Device Control One (XON) (Caret = ^Q) */ + export const DC1 = '\x11'; + /** Device Control Two (Caret = ^R) */ + export const DC2 = '\x12'; + /** Device Control Three (XOFF) (Caret = ^S) */ + export const DC3 = '\x13'; + /** Device Control Four (Caret = ^T) */ + export const DC4 = '\x14'; + /** Negative Acknowledge (Caret = ^U) */ + export const NAK = '\x15'; + /** Synchronous Idle (Caret = ^V) */ + export const SYN = '\x16'; + /** End of Transmission Block (Caret = ^W) */ + export const ETB = '\x17'; + /** Cancel (Caret = ^X) */ + export const CAN = '\x18'; + /** End of Medium (Caret = ^Y) */ + export const EM = '\x19'; + /** Substitute (Caret = ^Z) */ + export const SUB = '\x1a'; + /** Escape (Caret = ^[, C = \e) */ + export const ESC = '\x1b'; + /** File Separator (Caret = ^\) */ + export const FS = '\x1c'; + /** Group Separator (Caret = ^]) */ + export const GS = '\x1d'; + /** Record Separator (Caret = ^^) */ + export const RS = '\x1e'; + /** Unit Separator (Caret = ^_) */ + export const US = '\x1f'; + /** Space */ + export const SP = '\x20'; + /** Delete (Caret = ^?) */ + export const DEL = '\x7f'; +} + +/** + * C1 control codes + * See = https://en.wikipedia.org/wiki/C0_and_C1_control_codes + */ +export namespace C1 { + /** padding character */ + export const PAD = '\x80'; + /** High Octet Preset */ + export const HOP = '\x81'; + /** Break Permitted Here */ + export const BPH = '\x82'; + /** No Break Here */ + export const NBH = '\x83'; + /** Index */ + export const IND = '\x84'; + /** Next Line */ + export const NEL = '\x85'; + /** Start of Selected Area */ + export const SSA = '\x86'; + /** End of Selected Area */ + export const ESA = '\x87'; + /** Horizontal Tabulation Set */ + export const HTS = '\x88'; + /** Horizontal Tabulation With Justification */ + export const HTJ = '\x89'; + /** Vertical Tabulation Set */ + export const VTS = '\x8a'; + /** Partial Line Down */ + export const PLD = '\x8b'; + /** Partial Line Up */ + export const PLU = '\x8c'; + /** Reverse Index */ + export const RI = '\x8d'; + /** Single-Shift 2 */ + export const SS2 = '\x8e'; + /** Single-Shift 3 */ + export const SS3 = '\x8f'; + /** Device Control String */ + export const DCS = '\x90'; + /** Private Use 1 */ + export const PU1 = '\x91'; + /** Private Use 2 */ + export const PU2 = '\x92'; + /** Set Transmit State */ + export const STS = '\x93'; + /** Destructive backspace, intended to eliminate ambiguity about meaning of BS. */ + export const CCH = '\x94'; + /** Message Waiting */ + export const MW = '\x95'; + /** Start of Protected Area */ + export const SPA = '\x96'; + /** End of Protected Area */ + export const EPA = '\x97'; + /** Start of String */ + export const SOS = '\x98'; + /** Single Graphic Character Introducer */ + export const SGCI = '\x99'; + /** Single Character Introducer */ + export const SCI = '\x9a'; + /** Control Sequence Introducer */ + export const CSI = '\x9b'; + /** String Terminator */ + export const ST = '\x9c'; + /** Operating System Command */ + export const OSC = '\x9d'; + /** Privacy Message */ + export const PM = '\x9e'; + /** Application Program Command */ + export const APC = '\x9f'; +} diff --git a/node_modules/xterm/src/common/input/Keyboard.ts b/node_modules/xterm/src/common/input/Keyboard.ts new file mode 100644 index 0000000..b4b3dce --- /dev/null +++ b/node_modules/xterm/src/common/input/Keyboard.ts @@ -0,0 +1,375 @@ +/** + * Copyright (c) 2014 The xterm.js authors. All rights reserved. + * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License) + * @license MIT + */ + +import { IKeyboardEvent, IKeyboardResult, KeyboardResultType } from 'common/Types'; +import { C0 } from 'common/data/EscapeSequences'; + +// reg + shift key mappings for digits and special chars +const KEYCODE_KEY_MAPPINGS: { [key: number]: [string, string]} = { + // digits 0-9 + 48: ['0', ')'], + 49: ['1', '!'], + 50: ['2', '@'], + 51: ['3', '#'], + 52: ['4', '$'], + 53: ['5', '%'], + 54: ['6', '^'], + 55: ['7', '&'], + 56: ['8', '*'], + 57: ['9', '('], + + // special chars + 186: [';', ':'], + 187: ['=', '+'], + 188: [',', '<'], + 189: ['-', '_'], + 190: ['.', '>'], + 191: ['/', '?'], + 192: ['`', '~'], + 219: ['[', '{'], + 220: ['\\', '|'], + 221: [']', '}'], + 222: ['\'', '"'] +}; + +export function evaluateKeyboardEvent( + ev: IKeyboardEvent, + applicationCursorMode: boolean, + isMac: boolean, + macOptionIsMeta: boolean +): IKeyboardResult { + const result: IKeyboardResult = { + type: KeyboardResultType.SEND_KEY, + // Whether to cancel event propagation (NOTE: this may not be needed since the event is + // canceled at the end of keyDown + cancel: false, + // The new key even to emit + key: undefined + }; + const modifiers = (ev.shiftKey ? 1 : 0) | (ev.altKey ? 2 : 0) | (ev.ctrlKey ? 4 : 0) | (ev.metaKey ? 8 : 0); + switch (ev.keyCode) { + case 0: + if (ev.key === 'UIKeyInputUpArrow') { + if (applicationCursorMode) { + result.key = C0.ESC + 'OA'; + } else { + result.key = C0.ESC + '[A'; + } + } + else if (ev.key === 'UIKeyInputLeftArrow') { + if (applicationCursorMode) { + result.key = C0.ESC + 'OD'; + } else { + result.key = C0.ESC + '[D'; + } + } + else if (ev.key === 'UIKeyInputRightArrow') { + if (applicationCursorMode) { + result.key = C0.ESC + 'OC'; + } else { + result.key = C0.ESC + '[C'; + } + } + else if (ev.key === 'UIKeyInputDownArrow') { + if (applicationCursorMode) { + result.key = C0.ESC + 'OB'; + } else { + result.key = C0.ESC + '[B'; + } + } + break; + case 8: + // backspace + if (ev.shiftKey) { + result.key = C0.BS; // ^H + break; + } else if (ev.altKey) { + result.key = C0.ESC + C0.DEL; // \e ^? + break; + } + result.key = C0.DEL; // ^? + break; + case 9: + // tab + if (ev.shiftKey) { + result.key = C0.ESC + '[Z'; + break; + } + result.key = C0.HT; + result.cancel = true; + break; + case 13: + // return/enter + result.key = ev.altKey ? C0.ESC + C0.CR : C0.CR; + result.cancel = true; + break; + case 27: + // escape + result.key = C0.ESC; + if (ev.altKey) { + result.key = C0.ESC + C0.ESC; + } + result.cancel = true; + break; + case 37: + // left-arrow + if (ev.metaKey) { + break; + } + if (modifiers) { + result.key = C0.ESC + '[1;' + (modifiers + 1) + 'D'; + // HACK: Make Alt + left-arrow behave like Ctrl + left-arrow: move one word backwards + // http://unix.stackexchange.com/a/108106 + // macOS uses different escape sequences than linux + if (result.key === C0.ESC + '[1;3D') { + result.key = C0.ESC + (isMac ? 'b' : '[1;5D'); + } + } else if (applicationCursorMode) { + result.key = C0.ESC + 'OD'; + } else { + result.key = C0.ESC + '[D'; + } + break; + case 39: + // right-arrow + if (ev.metaKey) { + break; + } + if (modifiers) { + result.key = C0.ESC + '[1;' + (modifiers + 1) + 'C'; + // HACK: Make Alt + right-arrow behave like Ctrl + right-arrow: move one word forward + // http://unix.stackexchange.com/a/108106 + // macOS uses different escape sequences than linux + if (result.key === C0.ESC + '[1;3C') { + result.key = C0.ESC + (isMac ? 'f' : '[1;5C'); + } + } else if (applicationCursorMode) { + result.key = C0.ESC + 'OC'; + } else { + result.key = C0.ESC + '[C'; + } + break; + case 38: + // up-arrow + if (ev.metaKey) { + break; + } + if (modifiers) { + result.key = C0.ESC + '[1;' + (modifiers + 1) + 'A'; + // HACK: Make Alt + up-arrow behave like Ctrl + up-arrow + // http://unix.stackexchange.com/a/108106 + // macOS uses different escape sequences than linux + if (!isMac && result.key === C0.ESC + '[1;3A') { + result.key = C0.ESC + '[1;5A'; + } + } else if (applicationCursorMode) { + result.key = C0.ESC + 'OA'; + } else { + result.key = C0.ESC + '[A'; + } + break; + case 40: + // down-arrow + if (ev.metaKey) { + break; + } + if (modifiers) { + result.key = C0.ESC + '[1;' + (modifiers + 1) + 'B'; + // HACK: Make Alt + down-arrow behave like Ctrl + down-arrow + // http://unix.stackexchange.com/a/108106 + // macOS uses different escape sequences than linux + if (!isMac && result.key === C0.ESC + '[1;3B') { + result.key = C0.ESC + '[1;5B'; + } + } else if (applicationCursorMode) { + result.key = C0.ESC + 'OB'; + } else { + result.key = C0.ESC + '[B'; + } + break; + case 45: + // insert + if (!ev.shiftKey && !ev.ctrlKey) { + // <Ctrl> or <Shift> + <Insert> are used to + // copy-paste on some systems. + result.key = C0.ESC + '[2~'; + } + break; + case 46: + // delete + if (modifiers) { + result.key = C0.ESC + '[3;' + (modifiers + 1) + '~'; + } else { + result.key = C0.ESC + '[3~'; + } + break; + case 36: + // home + if (modifiers) { + result.key = C0.ESC + '[1;' + (modifiers + 1) + 'H'; + } else if (applicationCursorMode) { + result.key = C0.ESC + 'OH'; + } else { + result.key = C0.ESC + '[H'; + } + break; + case 35: + // end + if (modifiers) { + result.key = C0.ESC + '[1;' + (modifiers + 1) + 'F'; + } else if (applicationCursorMode) { + result.key = C0.ESC + 'OF'; + } else { + result.key = C0.ESC + '[F'; + } + break; + case 33: + // page up + if (ev.shiftKey) { + result.type = KeyboardResultType.PAGE_UP; + } else { + result.key = C0.ESC + '[5~'; + } + break; + case 34: + // page down + if (ev.shiftKey) { + result.type = KeyboardResultType.PAGE_DOWN; + } else { + result.key = C0.ESC + '[6~'; + } + break; + case 112: + // F1-F12 + if (modifiers) { + result.key = C0.ESC + '[1;' + (modifiers + 1) + 'P'; + } else { + result.key = C0.ESC + 'OP'; + } + break; + case 113: + if (modifiers) { + result.key = C0.ESC + '[1;' + (modifiers + 1) + 'Q'; + } else { + result.key = C0.ESC + 'OQ'; + } + break; + case 114: + if (modifiers) { + result.key = C0.ESC + '[1;' + (modifiers + 1) + 'R'; + } else { + result.key = C0.ESC + 'OR'; + } + break; + case 115: + if (modifiers) { + result.key = C0.ESC + '[1;' + (modifiers + 1) + 'S'; + } else { + result.key = C0.ESC + 'OS'; + } + break; + case 116: + if (modifiers) { + result.key = C0.ESC + '[15;' + (modifiers + 1) + '~'; + } else { + result.key = C0.ESC + '[15~'; + } + break; + case 117: + if (modifiers) { + result.key = C0.ESC + '[17;' + (modifiers + 1) + '~'; + } else { + result.key = C0.ESC + '[17~'; + } + break; + case 118: + if (modifiers) { + result.key = C0.ESC + '[18;' + (modifiers + 1) + '~'; + } else { + result.key = C0.ESC + '[18~'; + } + break; + case 119: + if (modifiers) { + result.key = C0.ESC + '[19;' + (modifiers + 1) + '~'; + } else { + result.key = C0.ESC + '[19~'; + } + break; + case 120: + if (modifiers) { + result.key = C0.ESC + '[20;' + (modifiers + 1) + '~'; + } else { + result.key = C0.ESC + '[20~'; + } + break; + case 121: + if (modifiers) { + result.key = C0.ESC + '[21;' + (modifiers + 1) + '~'; + } else { + result.key = C0.ESC + '[21~'; + } + break; + case 122: + if (modifiers) { + result.key = C0.ESC + '[23;' + (modifiers + 1) + '~'; + } else { + result.key = C0.ESC + '[23~'; + } + break; + case 123: + if (modifiers) { + result.key = C0.ESC + '[24;' + (modifiers + 1) + '~'; + } else { + result.key = C0.ESC + '[24~'; + } + break; + default: + // a-z and space + if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) { + if (ev.keyCode >= 65 && ev.keyCode <= 90) { + result.key = String.fromCharCode(ev.keyCode - 64); + } else if (ev.keyCode === 32) { + result.key = C0.NUL; + } else if (ev.keyCode >= 51 && ev.keyCode <= 55) { + // escape, file sep, group sep, record sep, unit sep + result.key = String.fromCharCode(ev.keyCode - 51 + 27); + } else if (ev.keyCode === 56) { + result.key = C0.DEL; + } else if (ev.keyCode === 219) { + result.key = C0.ESC; + } else if (ev.keyCode === 220) { + result.key = C0.FS; + } else if (ev.keyCode === 221) { + result.key = C0.GS; + } + } else if ((!isMac || macOptionIsMeta) && ev.altKey && !ev.metaKey) { + // On macOS this is a third level shift when !macOptionIsMeta. Use <Esc> instead. + const keyMapping = KEYCODE_KEY_MAPPINGS[ev.keyCode]; + const key = keyMapping?.[!ev.shiftKey ? 0 : 1]; + if (key) { + result.key = C0.ESC + key; + } else if (ev.keyCode >= 65 && ev.keyCode <= 90) { + const keyCode = ev.ctrlKey ? ev.keyCode - 64 : ev.keyCode + 32; + result.key = C0.ESC + String.fromCharCode(keyCode); + } + } else if (isMac && !ev.altKey && !ev.ctrlKey && !ev.shiftKey && ev.metaKey) { + if (ev.keyCode === 65) { // cmd + a + result.type = KeyboardResultType.SELECT_ALL; + } + } else if (ev.key && !ev.ctrlKey && !ev.altKey && !ev.metaKey && ev.keyCode >= 48 && ev.key.length === 1) { + // Include only keys that that result in a _single_ character; don't include num lock, volume up, etc. + result.key = ev.key; + } else if (ev.key && ev.ctrlKey) { + if (ev.key === '_') { // ^_ + result.key = C0.US; + } + } + break; + } + + return result; +} diff --git a/node_modules/xterm/src/common/input/TextDecoder.ts b/node_modules/xterm/src/common/input/TextDecoder.ts new file mode 100644 index 0000000..715e919 --- /dev/null +++ b/node_modules/xterm/src/common/input/TextDecoder.ts @@ -0,0 +1,346 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +/** + * Polyfill - Convert UTF32 codepoint into JS string. + * Note: The built-in String.fromCodePoint happens to be much slower + * due to additional sanity checks. We can avoid them since + * we always operate on legal UTF32 (granted by the input decoders) + * and use this faster version instead. + */ +export function stringFromCodePoint(codePoint: number): string { + if (codePoint > 0xFFFF) { + codePoint -= 0x10000; + return String.fromCharCode((codePoint >> 10) + 0xD800) + String.fromCharCode((codePoint % 0x400) + 0xDC00); + } + return String.fromCharCode(codePoint); +} + +/** + * Convert UTF32 char codes into JS string. + * Basically the same as `stringFromCodePoint` but for multiple codepoints + * in a loop (which is a lot faster). + */ +export function utf32ToString(data: Uint32Array, start: number = 0, end: number = data.length): string { + let result = ''; + for (let i = start; i < end; ++i) { + let codepoint = data[i]; + if (codepoint > 0xFFFF) { + // JS strings are encoded as UTF16, thus a non BMP codepoint gets converted into a surrogate pair + // conversion rules: + // - subtract 0x10000 from code point, leaving a 20 bit number + // - add high 10 bits to 0xD800 --> first surrogate + // - add low 10 bits to 0xDC00 --> second surrogate + codepoint -= 0x10000; + result += String.fromCharCode((codepoint >> 10) + 0xD800) + String.fromCharCode((codepoint % 0x400) + 0xDC00); + } else { + result += String.fromCharCode(codepoint); + } + } + return result; +} + +/** + * StringToUtf32 - decodes UTF16 sequences into UTF32 codepoints. + * To keep the decoder in line with JS strings it handles single surrogates as UCS2. + */ +export class StringToUtf32 { + private _interim: number = 0; + + /** + * Clears interim and resets decoder to clean state. + */ + public clear(): void { + this._interim = 0; + } + + /** + * Decode JS string to UTF32 codepoints. + * The methods assumes stream input and will store partly transmitted + * surrogate pairs and decode them with the next data chunk. + * Note: The method does no bound checks for target, therefore make sure + * the provided input data does not exceed the size of `target`. + * Returns the number of written codepoints in `target`. + */ + public decode(input: string, target: Uint32Array): number { + const length = input.length; + + if (!length) { + return 0; + } + + let size = 0; + let startPos = 0; + + // handle leftover surrogate high + if (this._interim) { + const second = input.charCodeAt(startPos++); + if (0xDC00 <= second && second <= 0xDFFF) { + target[size++] = (this._interim - 0xD800) * 0x400 + second - 0xDC00 + 0x10000; + } else { + // illegal codepoint (USC2 handling) + target[size++] = this._interim; + target[size++] = second; + } + this._interim = 0; + } + + for (let i = startPos; i < length; ++i) { + const code = input.charCodeAt(i); + // surrogate pair first + if (0xD800 <= code && code <= 0xDBFF) { + if (++i >= length) { + this._interim = code; + return size; + } + const second = input.charCodeAt(i); + if (0xDC00 <= second && second <= 0xDFFF) { + target[size++] = (code - 0xD800) * 0x400 + second - 0xDC00 + 0x10000; + } else { + // illegal codepoint (USC2 handling) + target[size++] = code; + target[size++] = second; + } + continue; + } + if (code === 0xFEFF) { + // BOM + continue; + } + target[size++] = code; + } + return size; + } +} + +/** + * Utf8Decoder - decodes UTF8 byte sequences into UTF32 codepoints. + */ +export class Utf8ToUtf32 { + public interim: Uint8Array = new Uint8Array(3); + + /** + * Clears interim bytes and resets decoder to clean state. + */ + public clear(): void { + this.interim.fill(0); + } + + /** + * Decodes UTF8 byte sequences in `input` to UTF32 codepoints in `target`. + * The methods assumes stream input and will store partly transmitted bytes + * and decode them with the next data chunk. + * Note: The method does no bound checks for target, therefore make sure + * the provided data chunk does not exceed the size of `target`. + * Returns the number of written codepoints in `target`. + */ + public decode(input: Uint8Array, target: Uint32Array): number { + const length = input.length; + + if (!length) { + return 0; + } + + let size = 0; + let byte1: number; + let byte2: number; + let byte3: number; + let byte4: number; + let codepoint = 0; + let startPos = 0; + + // handle leftover bytes + if (this.interim[0]) { + let discardInterim = false; + let cp = this.interim[0]; + cp &= ((((cp & 0xE0) === 0xC0)) ? 0x1F : (((cp & 0xF0) === 0xE0)) ? 0x0F : 0x07); + let pos = 0; + let tmp: number; + while ((tmp = this.interim[++pos] & 0x3F) && pos < 4) { + cp <<= 6; + cp |= tmp; + } + // missing bytes - read ahead from input + const type = (((this.interim[0] & 0xE0) === 0xC0)) ? 2 : (((this.interim[0] & 0xF0) === 0xE0)) ? 3 : 4; + const missing = type - pos; + while (startPos < missing) { + if (startPos >= length) { + return 0; + } + tmp = input[startPos++]; + if ((tmp & 0xC0) !== 0x80) { + // wrong continuation, discard interim bytes completely + startPos--; + discardInterim = true; + break; + } else { + // need to save so we can continue short inputs in next call + this.interim[pos++] = tmp; + cp <<= 6; + cp |= tmp & 0x3F; + } + } + if (!discardInterim) { + // final test is type dependent + if (type === 2) { + if (cp < 0x80) { + // wrong starter byte + startPos--; + } else { + target[size++] = cp; + } + } else if (type === 3) { + if (cp < 0x0800 || (cp >= 0xD800 && cp <= 0xDFFF) || cp === 0xFEFF) { + // illegal codepoint or BOM + } else { + target[size++] = cp; + } + } else { + if (cp < 0x010000 || cp > 0x10FFFF) { + // illegal codepoint + } else { + target[size++] = cp; + } + } + } + this.interim.fill(0); + } + + // loop through input + const fourStop = length - 4; + let i = startPos; + while (i < length) { + /** + * ASCII shortcut with loop unrolled to 4 consecutive ASCII chars. + * This is a compromise between speed gain for ASCII + * and penalty for non ASCII: + * For best ASCII performance the char should be stored directly into target, + * but even a single attempt to write to target and compare afterwards + * penalizes non ASCII really bad (-50%), thus we load the char into byteX first, + * which reduces ASCII performance by ~15%. + * This trial for ASCII reduces non ASCII performance by ~10% which seems acceptible + * compared to the gains. + * Note that this optimization only takes place for 4 consecutive ASCII chars, + * for any shorter it bails out. Worst case - all 4 bytes being read but + * thrown away due to the last being a non ASCII char (-10% performance). + */ + while (i < fourStop + && !((byte1 = input[i]) & 0x80) + && !((byte2 = input[i + 1]) & 0x80) + && !((byte3 = input[i + 2]) & 0x80) + && !((byte4 = input[i + 3]) & 0x80)) + { + target[size++] = byte1; + target[size++] = byte2; + target[size++] = byte3; + target[size++] = byte4; + i += 4; + } + + // reread byte1 + byte1 = input[i++]; + + // 1 byte + if (byte1 < 0x80) { + target[size++] = byte1; + + // 2 bytes + } else if ((byte1 & 0xE0) === 0xC0) { + if (i >= length) { + this.interim[0] = byte1; + return size; + } + byte2 = input[i++]; + if ((byte2 & 0xC0) !== 0x80) { + // wrong continuation + i--; + continue; + } + codepoint = (byte1 & 0x1F) << 6 | (byte2 & 0x3F); + if (codepoint < 0x80) { + // wrong starter byte + i--; + continue; + } + target[size++] = codepoint; + + // 3 bytes + } else if ((byte1 & 0xF0) === 0xE0) { + if (i >= length) { + this.interim[0] = byte1; + return size; + } + byte2 = input[i++]; + if ((byte2 & 0xC0) !== 0x80) { + // wrong continuation + i--; + continue; + } + if (i >= length) { + this.interim[0] = byte1; + this.interim[1] = byte2; + return size; + } + byte3 = input[i++]; + if ((byte3 & 0xC0) !== 0x80) { + // wrong continuation + i--; + continue; + } + codepoint = (byte1 & 0x0F) << 12 | (byte2 & 0x3F) << 6 | (byte3 & 0x3F); + if (codepoint < 0x0800 || (codepoint >= 0xD800 && codepoint <= 0xDFFF) || codepoint === 0xFEFF) { + // illegal codepoint or BOM, no i-- here + continue; + } + target[size++] = codepoint; + + // 4 bytes + } else if ((byte1 & 0xF8) === 0xF0) { + if (i >= length) { + this.interim[0] = byte1; + return size; + } + byte2 = input[i++]; + if ((byte2 & 0xC0) !== 0x80) { + // wrong continuation + i--; + continue; + } + if (i >= length) { + this.interim[0] = byte1; + this.interim[1] = byte2; + return size; + } + byte3 = input[i++]; + if ((byte3 & 0xC0) !== 0x80) { + // wrong continuation + i--; + continue; + } + if (i >= length) { + this.interim[0] = byte1; + this.interim[1] = byte2; + this.interim[2] = byte3; + return size; + } + byte4 = input[i++]; + if ((byte4 & 0xC0) !== 0x80) { + // wrong continuation + i--; + continue; + } + codepoint = (byte1 & 0x07) << 18 | (byte2 & 0x3F) << 12 | (byte3 & 0x3F) << 6 | (byte4 & 0x3F); + if (codepoint < 0x010000 || codepoint > 0x10FFFF) { + // illegal codepoint, no i-- here + continue; + } + target[size++] = codepoint; + } else { + // illegal byte, just skip + } + } + return size; + } +} diff --git a/node_modules/xterm/src/common/input/UnicodeV6.ts b/node_modules/xterm/src/common/input/UnicodeV6.ts new file mode 100644 index 0000000..b308203 --- /dev/null +++ b/node_modules/xterm/src/common/input/UnicodeV6.ts @@ -0,0 +1,133 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ +import { IUnicodeVersionProvider } from 'common/services/Services'; +import { fill } from 'common/TypedArrayUtils'; + +type CharWidth = 0 | 1 | 2; + +const BMP_COMBINING = [ + [0x0300, 0x036F], [0x0483, 0x0486], [0x0488, 0x0489], + [0x0591, 0x05BD], [0x05BF, 0x05BF], [0x05C1, 0x05C2], + [0x05C4, 0x05C5], [0x05C7, 0x05C7], [0x0600, 0x0603], + [0x0610, 0x0615], [0x064B, 0x065E], [0x0670, 0x0670], + [0x06D6, 0x06E4], [0x06E7, 0x06E8], [0x06EA, 0x06ED], + [0x070F, 0x070F], [0x0711, 0x0711], [0x0730, 0x074A], + [0x07A6, 0x07B0], [0x07EB, 0x07F3], [0x0901, 0x0902], + [0x093C, 0x093C], [0x0941, 0x0948], [0x094D, 0x094D], + [0x0951, 0x0954], [0x0962, 0x0963], [0x0981, 0x0981], + [0x09BC, 0x09BC], [0x09C1, 0x09C4], [0x09CD, 0x09CD], + [0x09E2, 0x09E3], [0x0A01, 0x0A02], [0x0A3C, 0x0A3C], + [0x0A41, 0x0A42], [0x0A47, 0x0A48], [0x0A4B, 0x0A4D], + [0x0A70, 0x0A71], [0x0A81, 0x0A82], [0x0ABC, 0x0ABC], + [0x0AC1, 0x0AC5], [0x0AC7, 0x0AC8], [0x0ACD, 0x0ACD], + [0x0AE2, 0x0AE3], [0x0B01, 0x0B01], [0x0B3C, 0x0B3C], + [0x0B3F, 0x0B3F], [0x0B41, 0x0B43], [0x0B4D, 0x0B4D], + [0x0B56, 0x0B56], [0x0B82, 0x0B82], [0x0BC0, 0x0BC0], + [0x0BCD, 0x0BCD], [0x0C3E, 0x0C40], [0x0C46, 0x0C48], + [0x0C4A, 0x0C4D], [0x0C55, 0x0C56], [0x0CBC, 0x0CBC], + [0x0CBF, 0x0CBF], [0x0CC6, 0x0CC6], [0x0CCC, 0x0CCD], + [0x0CE2, 0x0CE3], [0x0D41, 0x0D43], [0x0D4D, 0x0D4D], + [0x0DCA, 0x0DCA], [0x0DD2, 0x0DD4], [0x0DD6, 0x0DD6], + [0x0E31, 0x0E31], [0x0E34, 0x0E3A], [0x0E47, 0x0E4E], + [0x0EB1, 0x0EB1], [0x0EB4, 0x0EB9], [0x0EBB, 0x0EBC], + [0x0EC8, 0x0ECD], [0x0F18, 0x0F19], [0x0F35, 0x0F35], + [0x0F37, 0x0F37], [0x0F39, 0x0F39], [0x0F71, 0x0F7E], + [0x0F80, 0x0F84], [0x0F86, 0x0F87], [0x0F90, 0x0F97], + [0x0F99, 0x0FBC], [0x0FC6, 0x0FC6], [0x102D, 0x1030], + [0x1032, 0x1032], [0x1036, 0x1037], [0x1039, 0x1039], + [0x1058, 0x1059], [0x1160, 0x11FF], [0x135F, 0x135F], + [0x1712, 0x1714], [0x1732, 0x1734], [0x1752, 0x1753], + [0x1772, 0x1773], [0x17B4, 0x17B5], [0x17B7, 0x17BD], + [0x17C6, 0x17C6], [0x17C9, 0x17D3], [0x17DD, 0x17DD], + [0x180B, 0x180D], [0x18A9, 0x18A9], [0x1920, 0x1922], + [0x1927, 0x1928], [0x1932, 0x1932], [0x1939, 0x193B], + [0x1A17, 0x1A18], [0x1B00, 0x1B03], [0x1B34, 0x1B34], + [0x1B36, 0x1B3A], [0x1B3C, 0x1B3C], [0x1B42, 0x1B42], + [0x1B6B, 0x1B73], [0x1DC0, 0x1DCA], [0x1DFE, 0x1DFF], + [0x200B, 0x200F], [0x202A, 0x202E], [0x2060, 0x2063], + [0x206A, 0x206F], [0x20D0, 0x20EF], [0x302A, 0x302F], + [0x3099, 0x309A], [0xA806, 0xA806], [0xA80B, 0xA80B], + [0xA825, 0xA826], [0xFB1E, 0xFB1E], [0xFE00, 0xFE0F], + [0xFE20, 0xFE23], [0xFEFF, 0xFEFF], [0xFFF9, 0xFFFB] +]; +const HIGH_COMBINING = [ + [0x10A01, 0x10A03], [0x10A05, 0x10A06], [0x10A0C, 0x10A0F], + [0x10A38, 0x10A3A], [0x10A3F, 0x10A3F], [0x1D167, 0x1D169], + [0x1D173, 0x1D182], [0x1D185, 0x1D18B], [0x1D1AA, 0x1D1AD], + [0x1D242, 0x1D244], [0xE0001, 0xE0001], [0xE0020, 0xE007F], + [0xE0100, 0xE01EF] +]; + +// BMP lookup table, lazy initialized during first addon loading +let table: Uint8Array; + +function bisearch(ucs: number, data: number[][]): boolean { + let min = 0; + let max = data.length - 1; + let mid; + if (ucs < data[0][0] || ucs > data[max][1]) { + return false; + } + while (max >= min) { + mid = (min + max) >> 1; + if (ucs > data[mid][1]) { + min = mid + 1; + } else if (ucs < data[mid][0]) { + max = mid - 1; + } else { + return true; + } + } + return false; +} + +export class UnicodeV6 implements IUnicodeVersionProvider { + public readonly version = '6'; + + constructor() { + // init lookup table once + if (!table) { + table = new Uint8Array(65536); + fill(table, 1); + table[0] = 0; + // control chars + fill(table, 0, 1, 32); + fill(table, 0, 0x7f, 0xa0); + + // apply wide char rules first + // wide chars + fill(table, 2, 0x1100, 0x1160); + table[0x2329] = 2; + table[0x232a] = 2; + fill(table, 2, 0x2e80, 0xa4d0); + table[0x303f] = 1; // wrongly in last line + + fill(table, 2, 0xac00, 0xd7a4); + fill(table, 2, 0xf900, 0xfb00); + fill(table, 2, 0xfe10, 0xfe1a); + fill(table, 2, 0xfe30, 0xfe70); + fill(table, 2, 0xff00, 0xff61); + fill(table, 2, 0xffe0, 0xffe7); + + // apply combining last to ensure we overwrite + // wrongly wide set chars: + // the original algo evals combining first and falls + // through to wide check so we simply do here the opposite + // combining 0 + for (let r = 0; r < BMP_COMBINING.length; ++r) { + fill(table, 0, BMP_COMBINING[r][0], BMP_COMBINING[r][1] + 1); + } + } + } + + public wcwidth(num: number): CharWidth { + if (num < 32) return 0; + if (num < 127) return 1; + if (num < 65536) return table[num] as CharWidth; + if (bisearch(num, HIGH_COMBINING)) return 0; + if ((num >= 0x20000 && num <= 0x2fffd) || (num >= 0x30000 && num <= 0x3fffd)) return 2; + return 1; + } +} diff --git a/node_modules/xterm/src/common/input/WriteBuffer.ts b/node_modules/xterm/src/common/input/WriteBuffer.ts new file mode 100644 index 0000000..cc84c9a --- /dev/null +++ b/node_modules/xterm/src/common/input/WriteBuffer.ts @@ -0,0 +1,224 @@ + +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +declare const setTimeout: (handler: () => void, timeout?: number) => void; + +/** + * Safety watermark to avoid memory exhaustion and browser engine crash on fast data input. + * Enable flow control to avoid this limit and make sure that your backend correctly + * propagates this to the underlying pty. (see docs for further instructions) + * Since this limit is meant as a safety parachute to prevent browser crashs, + * it is set to a very high number. Typically xterm.js gets unresponsive with + * a 100 times lower number (>500 kB). + */ +const DISCARD_WATERMARK = 50000000; // ~50 MB + +/** + * The max number of ms to spend on writes before allowing the renderer to + * catch up with a 0ms setTimeout. A value of < 33 to keep us close to + * 30fps, and a value of < 16 to try to run at 60fps. Of course, the real FPS + * depends on the time it takes for the renderer to draw the frame. + */ +const WRITE_TIMEOUT_MS = 12; + +/** + * Threshold of max held chunks in the write buffer, that were already processed. + * This is a tradeoff between extensive write buffer shifts (bad runtime) and high + * memory consumption by data thats not used anymore. + */ +const WRITE_BUFFER_LENGTH_THRESHOLD = 50; + +// queueMicrotask polyfill for nodejs < v11 +const qmt: (cb: () => void) => void = (typeof queueMicrotask === 'undefined') + ? (cb: () => void) => { Promise.resolve().then(cb); } + : queueMicrotask; + + +export class WriteBuffer { + private _writeBuffer: (string | Uint8Array)[] = []; + private _callbacks: ((() => void) | undefined)[] = []; + private _pendingData = 0; + private _bufferOffset = 0; + private _isSyncWriting = false; + private _syncCalls = 0; + + constructor(private _action: (data: string | Uint8Array, promiseResult?: boolean) => void | Promise<boolean>) { } + + /** + * @deprecated Unreliable, to be removed soon. + */ + public writeSync(data: string | Uint8Array, maxSubsequentCalls?: number): void { + // stop writeSync recursions with maxSubsequentCalls argument + // This is dangerous to use as it will lose the current data chunk + // and return immediately. + if (maxSubsequentCalls !== undefined && this._syncCalls > maxSubsequentCalls) { + // comment next line if a whole loop block should only contain x `writeSync` calls + // (total flat vs. deep nested limit) + this._syncCalls = 0; + return; + } + // append chunk to buffer + this._pendingData += data.length; + this._writeBuffer.push(data); + this._callbacks.push(undefined); + + // increase recursion counter + this._syncCalls++; + // exit early if another writeSync loop is active + if (this._isSyncWriting) { + return; + } + this._isSyncWriting = true; + + // force sync processing on pending data chunks to avoid in-band data scrambling + // does the same as innerWrite but without event loop + // we have to do it here as single loop steps to not corrupt loop subject + // by another writeSync call triggered from _action + let chunk: string | Uint8Array | undefined; + while (chunk = this._writeBuffer.shift()) { + this._action(chunk); + const cb = this._callbacks.shift(); + if (cb) cb(); + } + // reset to avoid reprocessing of chunks with scheduled innerWrite call + // stopping scheduled innerWrite by offset > length condition + this._pendingData = 0; + this._bufferOffset = 0x7FFFFFFF; + + // allow another writeSync to loop + this._isSyncWriting = false; + this._syncCalls = 0; + } + + public write(data: string | Uint8Array, callback?: () => void): void { + if (this._pendingData > DISCARD_WATERMARK) { + throw new Error('write data discarded, use flow control to avoid losing data'); + } + + // schedule chunk processing for next event loop run + if (!this._writeBuffer.length) { + this._bufferOffset = 0; + setTimeout(() => this._innerWrite()); + } + + this._pendingData += data.length; + this._writeBuffer.push(data); + this._callbacks.push(callback); + } + + /** + * Inner write call, that enters the sliced chunk processing by timing. + * + * `lastTime` indicates, when the last _innerWrite call had started. + * It is used to aggregate async handler execution under a timeout constraint + * effectively lowering the redrawing needs, schematically: + * + * macroTask _innerWrite: + * if (Date.now() - (lastTime | 0) < WRITE_TIMEOUT_MS): + * schedule microTask _innerWrite(lastTime) + * else: + * schedule macroTask _innerWrite(0) + * + * overall execution order on task queues: + * + * macrotasks: [...] --> _innerWrite(0) --> [...] --> screenUpdate --> [...] + * m t: | + * i a: [...] + * c s: | + * r k: while < timeout: + * o s: _innerWrite(timeout) + * + * `promiseResult` depicts the promise resolve value of an async handler. + * This value gets carried forward through all saved stack states of the + * paused parser for proper continuation. + * + * Note, for pure sync code `lastTime` and `promiseResult` have no meaning. + */ + protected _innerWrite(lastTime: number = 0, promiseResult: boolean = true): void { + const startTime = lastTime || Date.now(); + while (this._writeBuffer.length > this._bufferOffset) { + const data = this._writeBuffer[this._bufferOffset]; + const result = this._action(data, promiseResult); + if (result) { + /** + * If we get a promise as return value, we re-schedule the continuation + * as thenable on the promise and exit right away. + * + * The exit here means, that we block input processing at the current active chunk, + * the exact execution position within the chunk is preserved by the saved + * stack content in InputHandler and EscapeSequenceParser. + * + * Resuming happens automatically from that saved stack state. + * Also the resolved promise value is passed along the callstack to + * `EscapeSequenceParser.parse` to correctly resume the stopped handler loop. + * + * Exceptions on async handlers will be logged to console async, but do not interrupt + * the input processing (continues with next handler at the current input position). + */ + + /** + * If a promise takes long to resolve, we should schedule continuation behind setTimeout. + * This might already be too late, if our .then enters really late (executor + prev thens took very long). + * This cannot be solved here for the handler itself (it is the handlers responsibility to slice hard work), + * but we can at least schedule a screen update as we gain control. + */ + const continuation: (r: boolean) => void = (r: boolean) => Date.now() - startTime >= WRITE_TIMEOUT_MS + ? setTimeout(() => this._innerWrite(0, r)) + : this._innerWrite(startTime, r); + + /** + * Optimization considerations: + * The continuation above favors FPS over throughput by eval'ing `startTime` on resolve. + * This might schedule too many screen updates with bad throughput drops (in case a slow + * resolving handler sliced its work properly behind setTimeout calls). We cannot spot + * this condition here, also the renderer has no way to spot nonsense updates either. + * FIXME: A proper fix for this would track the FPS at the renderer entry level separately. + * + * If favoring of FPS shows bad throughtput impact, use the following instead. It favors + * throughput by eval'ing `startTime` upfront pulling at least one more chunk into the + * current microtask queue (executed before setTimeout). + */ + // const continuation: (r: boolean) => void = Date.now() - startTime >= WRITE_TIMEOUT_MS + // ? r => setTimeout(() => this._innerWrite(0, r)) + // : r => this._innerWrite(startTime, r); + + // Handle exceptions synchronously to current band position, idea: + // 1. spawn a single microtask which we allow to throw hard + // 2. spawn a promise immediately resolving to `true` + // (executed on the same queue, thus properly aligned before continuation happens) + result.catch(err => { + qmt(() => {throw err;}); + return Promise.resolve(false); + }).then(continuation); + return; + } + + const cb = this._callbacks[this._bufferOffset]; + if (cb) cb(); + this._bufferOffset++; + this._pendingData -= data.length; + + if (Date.now() - startTime >= WRITE_TIMEOUT_MS) { + break; + } + } + if (this._writeBuffer.length > this._bufferOffset) { + // Allow renderer to catch up before processing the next batch + // trim already processed chunks if we are above threshold + if (this._bufferOffset > WRITE_BUFFER_LENGTH_THRESHOLD) { + this._writeBuffer = this._writeBuffer.slice(this._bufferOffset); + this._callbacks = this._callbacks.slice(this._bufferOffset); + this._bufferOffset = 0; + } + setTimeout(() => this._innerWrite()); + } else { + this._writeBuffer.length = 0; + this._callbacks.length = 0; + this._pendingData = 0; + this._bufferOffset = 0; + } + } +} diff --git a/node_modules/xterm/src/common/input/XParseColor.ts b/node_modules/xterm/src/common/input/XParseColor.ts new file mode 100644 index 0000000..8c023a3 --- /dev/null +++ b/node_modules/xterm/src/common/input/XParseColor.ts @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2021 The xterm.js authors. All rights reserved. + * @license MIT + */ + + +// 'rgb:' rule - matching: r/g/b | rr/gg/bb | rrr/ggg/bbb | rrrr/gggg/bbbb (hex digits) +const RGB_REX = /^([\da-f]{1})\/([\da-f]{1})\/([\da-f]{1})$|^([\da-f]{2})\/([\da-f]{2})\/([\da-f]{2})$|^([\da-f]{3})\/([\da-f]{3})\/([\da-f]{3})$|^([\da-f]{4})\/([\da-f]{4})\/([\da-f]{4})$/; +// '#...' rule - matching any hex digits +const HASH_REX = /^[\da-f]+$/; + +/** + * Parse color spec to RGB values (8 bit per channel). + * See `man xparsecolor` for details about certain format specifications. + * + * Supported formats: + * - rgb:<red>/<green>/<blue> with <red>, <green>, <blue> in h | hh | hhh | hhhh + * - #RGB, #RRGGBB, #RRRGGGBBB, #RRRRGGGGBBBB + * + * All other formats like rgbi: or device-independent string specifications + * with float numbering are not supported. + */ +export function parseColor(data: string): [number, number, number] | undefined { + if (!data) return; + // also handle uppercases + let low = data.toLowerCase(); + if (low.indexOf('rgb:') === 0) { + // 'rgb:' specifier + low = low.slice(4); + const m = RGB_REX.exec(low); + if (m) { + const base = m[1] ? 15 : m[4] ? 255 : m[7] ? 4095 : 65535; + return [ + Math.round(parseInt(m[1] || m[4] || m[7] || m[10], 16) / base * 255), + Math.round(parseInt(m[2] || m[5] || m[8] || m[11], 16) / base * 255), + Math.round(parseInt(m[3] || m[6] || m[9] || m[12], 16) / base * 255) + ]; + } + } else if (low.indexOf('#') === 0) { + // '#' specifier + low = low.slice(1); + if (HASH_REX.exec(low) && [3, 6, 9, 12].includes(low.length)) { + const adv = low.length / 3; + const result: [number, number, number] = [0, 0, 0]; + for (let i = 0; i < 3; ++i) { + const c = parseInt(low.slice(adv * i, adv * i + adv), 16); + result[i] = adv === 1 ? c << 4 : adv === 2 ? c : adv === 3 ? c >> 4 : c >> 8; + } + return result; + } + } + + // Named colors are currently not supported due to the large addition to the xterm.js bundle size + // they would add. In order to support named colors, we would need some way of optionally loading + // additional payloads so startup/download time is not bloated (see #3530). +} + +// pad hex output to requested bit width +function pad(n: number, bits: number): string { + const s = n.toString(16); + const s2 = s.length < 2 ? '0' + s : s; + switch (bits) { + case 4: + return s[0]; + case 8: + return s2; + case 12: + return (s2 + s2).slice(0, 3); + default: + return s2 + s2; + } +} + +/** + * Convert a given color to rgb:../../.. string of `bits` depth. + */ +export function toRgbString(color: [number, number, number], bits: number = 16): string { + const [r, g, b] = color; + return `rgb:${pad(r, bits)}/${pad(g, bits)}/${pad(b, bits)}`; +} diff --git a/node_modules/xterm/src/common/parser/Constants.ts b/node_modules/xterm/src/common/parser/Constants.ts new file mode 100644 index 0000000..85156c3 --- /dev/null +++ b/node_modules/xterm/src/common/parser/Constants.ts @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +/** + * Internal states of EscapeSequenceParser. + */ +export const enum ParserState { + GROUND = 0, + ESCAPE = 1, + ESCAPE_INTERMEDIATE = 2, + CSI_ENTRY = 3, + CSI_PARAM = 4, + CSI_INTERMEDIATE = 5, + CSI_IGNORE = 6, + SOS_PM_APC_STRING = 7, + OSC_STRING = 8, + DCS_ENTRY = 9, + DCS_PARAM = 10, + DCS_IGNORE = 11, + DCS_INTERMEDIATE = 12, + DCS_PASSTHROUGH = 13 +} + +/** +* Internal actions of EscapeSequenceParser. +*/ +export const enum ParserAction { + IGNORE = 0, + ERROR = 1, + PRINT = 2, + EXECUTE = 3, + OSC_START = 4, + OSC_PUT = 5, + OSC_END = 6, + CSI_DISPATCH = 7, + PARAM = 8, + COLLECT = 9, + ESC_DISPATCH = 10, + CLEAR = 11, + DCS_HOOK = 12, + DCS_PUT = 13, + DCS_UNHOOK = 14 +} + +/** + * Internal states of OscParser. + */ +export const enum OscState { + START = 0, + ID = 1, + PAYLOAD = 2, + ABORT = 3 +} + +// payload limit for OSC and DCS +export const PAYLOAD_LIMIT = 10000000; diff --git a/node_modules/xterm/src/common/parser/DcsParser.ts b/node_modules/xterm/src/common/parser/DcsParser.ts new file mode 100644 index 0000000..b66524b --- /dev/null +++ b/node_modules/xterm/src/common/parser/DcsParser.ts @@ -0,0 +1,192 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IDisposable } from 'common/Types'; +import { IDcsHandler, IParams, IHandlerCollection, IDcsParser, DcsFallbackHandlerType, ISubParserStackState } from 'common/parser/Types'; +import { utf32ToString } from 'common/input/TextDecoder'; +import { Params } from 'common/parser/Params'; +import { PAYLOAD_LIMIT } from 'common/parser/Constants'; + +const EMPTY_HANDLERS: IDcsHandler[] = []; + +export class DcsParser implements IDcsParser { + private _handlers: IHandlerCollection<IDcsHandler> = Object.create(null); + private _active: IDcsHandler[] = EMPTY_HANDLERS; + private _ident: number = 0; + private _handlerFb: DcsFallbackHandlerType = () => { }; + private _stack: ISubParserStackState = { + paused: false, + loopPosition: 0, + fallThrough: false + }; + + public dispose(): void { + this._handlers = Object.create(null); + this._handlerFb = () => { }; + this._active = EMPTY_HANDLERS; + } + + public registerHandler(ident: number, handler: IDcsHandler): IDisposable { + if (this._handlers[ident] === undefined) { + this._handlers[ident] = []; + } + const handlerList = this._handlers[ident]; + handlerList.push(handler); + return { + dispose: () => { + const handlerIndex = handlerList.indexOf(handler); + if (handlerIndex !== -1) { + handlerList.splice(handlerIndex, 1); + } + } + }; + } + + public clearHandler(ident: number): void { + if (this._handlers[ident]) delete this._handlers[ident]; + } + + public setHandlerFallback(handler: DcsFallbackHandlerType): void { + this._handlerFb = handler; + } + + public reset(): void { + // force cleanup leftover handlers + if (this._active.length) { + for (let j = this._stack.paused ? this._stack.loopPosition - 1 : this._active.length - 1; j >= 0; --j) { + this._active[j].unhook(false); + } + } + this._stack.paused = false; + this._active = EMPTY_HANDLERS; + this._ident = 0; + } + + public hook(ident: number, params: IParams): void { + // always reset leftover handlers + this.reset(); + this._ident = ident; + this._active = this._handlers[ident] || EMPTY_HANDLERS; + if (!this._active.length) { + this._handlerFb(this._ident, 'HOOK', params); + } else { + for (let j = this._active.length - 1; j >= 0; j--) { + this._active[j].hook(params); + } + } + } + + public put(data: Uint32Array, start: number, end: number): void { + if (!this._active.length) { + this._handlerFb(this._ident, 'PUT', utf32ToString(data, start, end)); + } else { + for (let j = this._active.length - 1; j >= 0; j--) { + this._active[j].put(data, start, end); + } + } + } + + public unhook(success: boolean, promiseResult: boolean = true): void | Promise<boolean> { + if (!this._active.length) { + this._handlerFb(this._ident, 'UNHOOK', success); + } else { + let handlerResult: boolean | Promise<boolean> = false; + let j = this._active.length - 1; + let fallThrough = false; + if (this._stack.paused) { + j = this._stack.loopPosition - 1; + handlerResult = promiseResult; + fallThrough = this._stack.fallThrough; + this._stack.paused = false; + } + if (!fallThrough && handlerResult === false) { + for (; j >= 0; j--) { + handlerResult = this._active[j].unhook(success); + if (handlerResult === true) { + break; + } else if (handlerResult instanceof Promise) { + this._stack.paused = true; + this._stack.loopPosition = j; + this._stack.fallThrough = false; + return handlerResult; + } + } + j--; + } + // cleanup left over handlers (fallThrough for async) + for (; j >= 0; j--) { + handlerResult = this._active[j].unhook(false); + if (handlerResult instanceof Promise) { + this._stack.paused = true; + this._stack.loopPosition = j; + this._stack.fallThrough = true; + return handlerResult; + } + } + } + this._active = EMPTY_HANDLERS; + this._ident = 0; + } +} + +// predefine empty params as [0] (ZDM) +const EMPTY_PARAMS = new Params(); +EMPTY_PARAMS.addParam(0); + +/** + * Convenient class to create a DCS handler from a single callback function. + * Note: The payload is currently limited to 50 MB (hardcoded). + */ +export class DcsHandler implements IDcsHandler { + private _data = ''; + private _params: IParams = EMPTY_PARAMS; + private _hitLimit: boolean = false; + + constructor(private _handler: (data: string, params: IParams) => boolean | Promise<boolean>) { } + + public hook(params: IParams): void { + // since we need to preserve params until `unhook`, we have to clone it + // (only borrowed from parser and spans multiple parser states) + // perf optimization: + // clone only, if we have non empty params, otherwise stick with default + this._params = (params.length > 1 || params.params[0]) ? params.clone() : EMPTY_PARAMS; + this._data = ''; + this._hitLimit = false; + } + + public put(data: Uint32Array, start: number, end: number): void { + if (this._hitLimit) { + return; + } + this._data += utf32ToString(data, start, end); + if (this._data.length > PAYLOAD_LIMIT) { + this._data = ''; + this._hitLimit = true; + } + } + + public unhook(success: boolean): boolean | Promise<boolean> { + let ret: boolean | Promise<boolean> = false; + if (this._hitLimit) { + ret = false; + } else if (success) { + ret = this._handler(this._data, this._params); + if (ret instanceof Promise) { + // need to hold data and params until `ret` got resolved + // dont care for errors, data will be freed anyway on next start + return ret.then(res => { + this._params = EMPTY_PARAMS; + this._data = ''; + this._hitLimit = false; + return res; + }); + } + } + this._params = EMPTY_PARAMS; + this._data = ''; + this._hitLimit = false; + return ret; + } +} diff --git a/node_modules/xterm/src/common/parser/EscapeSequenceParser.ts b/node_modules/xterm/src/common/parser/EscapeSequenceParser.ts new file mode 100644 index 0000000..f20a7e9 --- /dev/null +++ b/node_modules/xterm/src/common/parser/EscapeSequenceParser.ts @@ -0,0 +1,796 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IOscHandler, IHandlerCollection, CsiHandlerType, OscFallbackHandlerType, IOscParser, EscHandlerType, IDcsParser, DcsFallbackHandlerType, IFunctionIdentifier, ExecuteFallbackHandlerType, CsiFallbackHandlerType, EscFallbackHandlerType, PrintHandlerType, PrintFallbackHandlerType, ExecuteHandlerType, IParserStackState, ParserStackType, ResumableHandlersType } from 'common/parser/Types'; +import { ParserState, ParserAction } from 'common/parser/Constants'; +import { Disposable } from 'common/Lifecycle'; +import { IDisposable } from 'common/Types'; +import { fill } from 'common/TypedArrayUtils'; +import { Params } from 'common/parser/Params'; +import { OscParser } from 'common/parser/OscParser'; +import { DcsParser } from 'common/parser/DcsParser'; + +/** + * Table values are generated like this: + * index: currentState << TableValue.INDEX_STATE_SHIFT | charCode + * value: action << TableValue.TRANSITION_ACTION_SHIFT | nextState + */ +const enum TableAccess { + TRANSITION_ACTION_SHIFT = 4, + TRANSITION_STATE_MASK = 15, + INDEX_STATE_SHIFT = 8 +} + +/** + * Transition table for EscapeSequenceParser. + */ +export class TransitionTable { + public table: Uint8Array; + + constructor(length: number) { + this.table = new Uint8Array(length); + } + + /** + * Set default transition. + * @param action default action + * @param next default next state + */ + public setDefault(action: ParserAction, next: ParserState): void { + fill(this.table, action << TableAccess.TRANSITION_ACTION_SHIFT | next); + } + + /** + * Add a transition to the transition table. + * @param code input character code + * @param state current parser state + * @param action parser action to be done + * @param next next parser state + */ + public add(code: number, state: ParserState, action: ParserAction, next: ParserState): void { + this.table[state << TableAccess.INDEX_STATE_SHIFT | code] = action << TableAccess.TRANSITION_ACTION_SHIFT | next; + } + + /** + * Add transitions for multiple input character codes. + * @param codes input character code array + * @param state current parser state + * @param action parser action to be done + * @param next next parser state + */ + public addMany(codes: number[], state: ParserState, action: ParserAction, next: ParserState): void { + for (let i = 0; i < codes.length; i++) { + this.table[state << TableAccess.INDEX_STATE_SHIFT | codes[i]] = action << TableAccess.TRANSITION_ACTION_SHIFT | next; + } + } +} + + +// Pseudo-character placeholder for printable non-ascii characters (unicode). +const NON_ASCII_PRINTABLE = 0xA0; + + +/** + * VT500 compatible transition table. + * Taken from https://vt100.net/emu/dec_ansi_parser. + */ +export const VT500_TRANSITION_TABLE = (function (): TransitionTable { + const table: TransitionTable = new TransitionTable(4095); + + // range macro for byte + const BYTE_VALUES = 256; + const blueprint = Array.apply(null, Array(BYTE_VALUES)).map((unused: any, i: number) => i); + const r = (start: number, end: number): number[] => blueprint.slice(start, end); + + // Default definitions. + const PRINTABLES = r(0x20, 0x7f); // 0x20 (SP) included, 0x7F (DEL) excluded + const EXECUTABLES = r(0x00, 0x18); + EXECUTABLES.push(0x19); + EXECUTABLES.push.apply(EXECUTABLES, r(0x1c, 0x20)); + + const states: number[] = r(ParserState.GROUND, ParserState.DCS_PASSTHROUGH + 1); + let state: any; + + // set default transition + table.setDefault(ParserAction.ERROR, ParserState.GROUND); + // printables + table.addMany(PRINTABLES, ParserState.GROUND, ParserAction.PRINT, ParserState.GROUND); + // global anywhere rules + for (state in states) { + table.addMany([0x18, 0x1a, 0x99, 0x9a], state, ParserAction.EXECUTE, ParserState.GROUND); + table.addMany(r(0x80, 0x90), state, ParserAction.EXECUTE, ParserState.GROUND); + table.addMany(r(0x90, 0x98), state, ParserAction.EXECUTE, ParserState.GROUND); + table.add(0x9c, state, ParserAction.IGNORE, ParserState.GROUND); // ST as terminator + table.add(0x1b, state, ParserAction.CLEAR, ParserState.ESCAPE); // ESC + table.add(0x9d, state, ParserAction.OSC_START, ParserState.OSC_STRING); // OSC + table.addMany([0x98, 0x9e, 0x9f], state, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING); + table.add(0x9b, state, ParserAction.CLEAR, ParserState.CSI_ENTRY); // CSI + table.add(0x90, state, ParserAction.CLEAR, ParserState.DCS_ENTRY); // DCS + } + // rules for executables and 7f + table.addMany(EXECUTABLES, ParserState.GROUND, ParserAction.EXECUTE, ParserState.GROUND); + table.addMany(EXECUTABLES, ParserState.ESCAPE, ParserAction.EXECUTE, ParserState.ESCAPE); + table.add(0x7f, ParserState.ESCAPE, ParserAction.IGNORE, ParserState.ESCAPE); + table.addMany(EXECUTABLES, ParserState.OSC_STRING, ParserAction.IGNORE, ParserState.OSC_STRING); + table.addMany(EXECUTABLES, ParserState.CSI_ENTRY, ParserAction.EXECUTE, ParserState.CSI_ENTRY); + table.add(0x7f, ParserState.CSI_ENTRY, ParserAction.IGNORE, ParserState.CSI_ENTRY); + table.addMany(EXECUTABLES, ParserState.CSI_PARAM, ParserAction.EXECUTE, ParserState.CSI_PARAM); + table.add(0x7f, ParserState.CSI_PARAM, ParserAction.IGNORE, ParserState.CSI_PARAM); + table.addMany(EXECUTABLES, ParserState.CSI_IGNORE, ParserAction.EXECUTE, ParserState.CSI_IGNORE); + table.addMany(EXECUTABLES, ParserState.CSI_INTERMEDIATE, ParserAction.EXECUTE, ParserState.CSI_INTERMEDIATE); + table.add(0x7f, ParserState.CSI_INTERMEDIATE, ParserAction.IGNORE, ParserState.CSI_INTERMEDIATE); + table.addMany(EXECUTABLES, ParserState.ESCAPE_INTERMEDIATE, ParserAction.EXECUTE, ParserState.ESCAPE_INTERMEDIATE); + table.add(0x7f, ParserState.ESCAPE_INTERMEDIATE, ParserAction.IGNORE, ParserState.ESCAPE_INTERMEDIATE); + // osc + table.add(0x5d, ParserState.ESCAPE, ParserAction.OSC_START, ParserState.OSC_STRING); + table.addMany(PRINTABLES, ParserState.OSC_STRING, ParserAction.OSC_PUT, ParserState.OSC_STRING); + table.add(0x7f, ParserState.OSC_STRING, ParserAction.OSC_PUT, ParserState.OSC_STRING); + table.addMany([0x9c, 0x1b, 0x18, 0x1a, 0x07], ParserState.OSC_STRING, ParserAction.OSC_END, ParserState.GROUND); + table.addMany(r(0x1c, 0x20), ParserState.OSC_STRING, ParserAction.IGNORE, ParserState.OSC_STRING); + // sos/pm/apc does nothing + table.addMany([0x58, 0x5e, 0x5f], ParserState.ESCAPE, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING); + table.addMany(PRINTABLES, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING); + table.addMany(EXECUTABLES, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING); + table.add(0x9c, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.GROUND); + table.add(0x7f, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING); + // csi entries + table.add(0x5b, ParserState.ESCAPE, ParserAction.CLEAR, ParserState.CSI_ENTRY); + table.addMany(r(0x40, 0x7f), ParserState.CSI_ENTRY, ParserAction.CSI_DISPATCH, ParserState.GROUND); + table.addMany(r(0x30, 0x3c), ParserState.CSI_ENTRY, ParserAction.PARAM, ParserState.CSI_PARAM); + table.addMany([0x3c, 0x3d, 0x3e, 0x3f], ParserState.CSI_ENTRY, ParserAction.COLLECT, ParserState.CSI_PARAM); + table.addMany(r(0x30, 0x3c), ParserState.CSI_PARAM, ParserAction.PARAM, ParserState.CSI_PARAM); + table.addMany(r(0x40, 0x7f), ParserState.CSI_PARAM, ParserAction.CSI_DISPATCH, ParserState.GROUND); + table.addMany([0x3c, 0x3d, 0x3e, 0x3f], ParserState.CSI_PARAM, ParserAction.IGNORE, ParserState.CSI_IGNORE); + table.addMany(r(0x20, 0x40), ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.CSI_IGNORE); + table.add(0x7f, ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.CSI_IGNORE); + table.addMany(r(0x40, 0x7f), ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.GROUND); + table.addMany(r(0x20, 0x30), ParserState.CSI_ENTRY, ParserAction.COLLECT, ParserState.CSI_INTERMEDIATE); + table.addMany(r(0x20, 0x30), ParserState.CSI_INTERMEDIATE, ParserAction.COLLECT, ParserState.CSI_INTERMEDIATE); + table.addMany(r(0x30, 0x40), ParserState.CSI_INTERMEDIATE, ParserAction.IGNORE, ParserState.CSI_IGNORE); + table.addMany(r(0x40, 0x7f), ParserState.CSI_INTERMEDIATE, ParserAction.CSI_DISPATCH, ParserState.GROUND); + table.addMany(r(0x20, 0x30), ParserState.CSI_PARAM, ParserAction.COLLECT, ParserState.CSI_INTERMEDIATE); + // esc_intermediate + table.addMany(r(0x20, 0x30), ParserState.ESCAPE, ParserAction.COLLECT, ParserState.ESCAPE_INTERMEDIATE); + table.addMany(r(0x20, 0x30), ParserState.ESCAPE_INTERMEDIATE, ParserAction.COLLECT, ParserState.ESCAPE_INTERMEDIATE); + table.addMany(r(0x30, 0x7f), ParserState.ESCAPE_INTERMEDIATE, ParserAction.ESC_DISPATCH, ParserState.GROUND); + table.addMany(r(0x30, 0x50), ParserState.ESCAPE, ParserAction.ESC_DISPATCH, ParserState.GROUND); + table.addMany(r(0x51, 0x58), ParserState.ESCAPE, ParserAction.ESC_DISPATCH, ParserState.GROUND); + table.addMany([0x59, 0x5a, 0x5c], ParserState.ESCAPE, ParserAction.ESC_DISPATCH, ParserState.GROUND); + table.addMany(r(0x60, 0x7f), ParserState.ESCAPE, ParserAction.ESC_DISPATCH, ParserState.GROUND); + // dcs entry + table.add(0x50, ParserState.ESCAPE, ParserAction.CLEAR, ParserState.DCS_ENTRY); + table.addMany(EXECUTABLES, ParserState.DCS_ENTRY, ParserAction.IGNORE, ParserState.DCS_ENTRY); + table.add(0x7f, ParserState.DCS_ENTRY, ParserAction.IGNORE, ParserState.DCS_ENTRY); + table.addMany(r(0x1c, 0x20), ParserState.DCS_ENTRY, ParserAction.IGNORE, ParserState.DCS_ENTRY); + table.addMany(r(0x20, 0x30), ParserState.DCS_ENTRY, ParserAction.COLLECT, ParserState.DCS_INTERMEDIATE); + table.addMany(r(0x30, 0x3c), ParserState.DCS_ENTRY, ParserAction.PARAM, ParserState.DCS_PARAM); + table.addMany([0x3c, 0x3d, 0x3e, 0x3f], ParserState.DCS_ENTRY, ParserAction.COLLECT, ParserState.DCS_PARAM); + table.addMany(EXECUTABLES, ParserState.DCS_IGNORE, ParserAction.IGNORE, ParserState.DCS_IGNORE); + table.addMany(r(0x20, 0x80), ParserState.DCS_IGNORE, ParserAction.IGNORE, ParserState.DCS_IGNORE); + table.addMany(r(0x1c, 0x20), ParserState.DCS_IGNORE, ParserAction.IGNORE, ParserState.DCS_IGNORE); + table.addMany(EXECUTABLES, ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_PARAM); + table.add(0x7f, ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_PARAM); + table.addMany(r(0x1c, 0x20), ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_PARAM); + table.addMany(r(0x30, 0x3c), ParserState.DCS_PARAM, ParserAction.PARAM, ParserState.DCS_PARAM); + table.addMany([0x3c, 0x3d, 0x3e, 0x3f], ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_IGNORE); + table.addMany(r(0x20, 0x30), ParserState.DCS_PARAM, ParserAction.COLLECT, ParserState.DCS_INTERMEDIATE); + table.addMany(EXECUTABLES, ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_INTERMEDIATE); + table.add(0x7f, ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_INTERMEDIATE); + table.addMany(r(0x1c, 0x20), ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_INTERMEDIATE); + table.addMany(r(0x20, 0x30), ParserState.DCS_INTERMEDIATE, ParserAction.COLLECT, ParserState.DCS_INTERMEDIATE); + table.addMany(r(0x30, 0x40), ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_IGNORE); + table.addMany(r(0x40, 0x7f), ParserState.DCS_INTERMEDIATE, ParserAction.DCS_HOOK, ParserState.DCS_PASSTHROUGH); + table.addMany(r(0x40, 0x7f), ParserState.DCS_PARAM, ParserAction.DCS_HOOK, ParserState.DCS_PASSTHROUGH); + table.addMany(r(0x40, 0x7f), ParserState.DCS_ENTRY, ParserAction.DCS_HOOK, ParserState.DCS_PASSTHROUGH); + table.addMany(EXECUTABLES, ParserState.DCS_PASSTHROUGH, ParserAction.DCS_PUT, ParserState.DCS_PASSTHROUGH); + table.addMany(PRINTABLES, ParserState.DCS_PASSTHROUGH, ParserAction.DCS_PUT, ParserState.DCS_PASSTHROUGH); + table.add(0x7f, ParserState.DCS_PASSTHROUGH, ParserAction.IGNORE, ParserState.DCS_PASSTHROUGH); + table.addMany([0x1b, 0x9c, 0x18, 0x1a], ParserState.DCS_PASSTHROUGH, ParserAction.DCS_UNHOOK, ParserState.GROUND); + // special handling of unicode chars + table.add(NON_ASCII_PRINTABLE, ParserState.GROUND, ParserAction.PRINT, ParserState.GROUND); + table.add(NON_ASCII_PRINTABLE, ParserState.OSC_STRING, ParserAction.OSC_PUT, ParserState.OSC_STRING); + table.add(NON_ASCII_PRINTABLE, ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.CSI_IGNORE); + table.add(NON_ASCII_PRINTABLE, ParserState.DCS_IGNORE, ParserAction.IGNORE, ParserState.DCS_IGNORE); + table.add(NON_ASCII_PRINTABLE, ParserState.DCS_PASSTHROUGH, ParserAction.DCS_PUT, ParserState.DCS_PASSTHROUGH); + return table; +})(); + + +/** + * EscapeSequenceParser. + * This class implements the ANSI/DEC compatible parser described by + * Paul Williams (https://vt100.net/emu/dec_ansi_parser). + * + * To implement custom ANSI compliant escape sequences it is not needed to + * alter this parser, instead consider registering a custom handler. + * For non ANSI compliant sequences change the transition table with + * the optional `transitions` constructor argument and + * reimplement the `parse` method. + * + * This parser is currently hardcoded to operate in ZDM (Zero Default Mode) + * as suggested by the original parser, thus empty parameters are set to 0. + * This this is not in line with the latest ECMA-48 specification + * (ZDM was part of the early specs and got completely removed later on). + * + * Other than the original parser from vt100.net this parser supports + * sub parameters in digital parameters separated by colons. Empty sub parameters + * are set to -1 (no ZDM for sub parameters). + * + * About prefix and intermediate bytes: + * This parser follows the assumptions of the vt100.net parser with these restrictions: + * - only one prefix byte is allowed as first parameter byte, byte range 0x3c .. 0x3f + * - max. two intermediates are respected, byte range 0x20 .. 0x2f + * Note that this is not in line with ECMA-48 which does not limit either of those. + * Furthermore ECMA-48 allows the prefix byte range at any param byte position. Currently + * there are no known sequences that follow the broader definition of the specification. + * + * TODO: implement error recovery hook via error handler return values + */ +export class EscapeSequenceParser extends Disposable implements IEscapeSequenceParser { + public initialState: number; + public currentState: number; + public precedingCodepoint: number; + + // buffers over several parse calls + protected _params: Params; + protected _collect: number; + + // handler lookup containers + protected _printHandler: PrintHandlerType; + protected _executeHandlers: { [flag: number]: ExecuteHandlerType }; + protected _csiHandlers: IHandlerCollection<CsiHandlerType>; + protected _escHandlers: IHandlerCollection<EscHandlerType>; + protected _oscParser: IOscParser; + protected _dcsParser: IDcsParser; + protected _errorHandler: (state: IParsingState) => IParsingState; + + // fallback handlers + protected _printHandlerFb: PrintFallbackHandlerType; + protected _executeHandlerFb: ExecuteFallbackHandlerType; + protected _csiHandlerFb: CsiFallbackHandlerType; + protected _escHandlerFb: EscFallbackHandlerType; + protected _errorHandlerFb: (state: IParsingState) => IParsingState; + + // parser stack save for async handler support + protected _parseStack: IParserStackState = { + state: ParserStackType.NONE, + handlers: [], + handlerPos: 0, + transition: 0, + chunkPos: 0 + }; + + constructor( + protected readonly _transitions: TransitionTable = VT500_TRANSITION_TABLE + ) { + super(); + + this.initialState = ParserState.GROUND; + this.currentState = this.initialState; + this._params = new Params(); // defaults to 32 storable params/subparams + this._params.addParam(0); // ZDM + this._collect = 0; + this.precedingCodepoint = 0; + + // set default fallback handlers and handler lookup containers + this._printHandlerFb = (data, start, end): void => { }; + this._executeHandlerFb = (code: number): void => { }; + this._csiHandlerFb = (ident: number, params: IParams): void => { }; + this._escHandlerFb = (ident: number): void => { }; + this._errorHandlerFb = (state: IParsingState): IParsingState => state; + this._printHandler = this._printHandlerFb; + this._executeHandlers = Object.create(null); + this._csiHandlers = Object.create(null); + this._escHandlers = Object.create(null); + this._oscParser = new OscParser(); + this._dcsParser = new DcsParser(); + this._errorHandler = this._errorHandlerFb; + + // swallow 7bit ST (ESC+\) + this.registerEscHandler({ final: '\\' }, () => true); + } + + protected _identifier(id: IFunctionIdentifier, finalRange: number[] = [0x40, 0x7e]): number { + let res = 0; + if (id.prefix) { + if (id.prefix.length > 1) { + throw new Error('only one byte as prefix supported'); + } + res = id.prefix.charCodeAt(0); + if (res && 0x3c > res || res > 0x3f) { + throw new Error('prefix must be in range 0x3c .. 0x3f'); + } + } + if (id.intermediates) { + if (id.intermediates.length > 2) { + throw new Error('only two bytes as intermediates are supported'); + } + for (let i = 0; i < id.intermediates.length; ++i) { + const intermediate = id.intermediates.charCodeAt(i); + if (0x20 > intermediate || intermediate > 0x2f) { + throw new Error('intermediate must be in range 0x20 .. 0x2f'); + } + res <<= 8; + res |= intermediate; + } + } + if (id.final.length !== 1) { + throw new Error('final must be a single byte'); + } + const finalCode = id.final.charCodeAt(0); + if (finalRange[0] > finalCode || finalCode > finalRange[1]) { + throw new Error(`final must be in range ${finalRange[0]} .. ${finalRange[1]}`); + } + res <<= 8; + res |= finalCode; + + return res; + } + + public identToString(ident: number): string { + const res: string[] = []; + while (ident) { + res.push(String.fromCharCode(ident & 0xFF)); + ident >>= 8; + } + return res.reverse().join(''); + } + + public dispose(): void { + this._csiHandlers = Object.create(null); + this._executeHandlers = Object.create(null); + this._escHandlers = Object.create(null); + this._oscParser.dispose(); + this._dcsParser.dispose(); + } + + public setPrintHandler(handler: PrintHandlerType): void { + this._printHandler = handler; + } + public clearPrintHandler(): void { + this._printHandler = this._printHandlerFb; + } + + public registerEscHandler(id: IFunctionIdentifier, handler: EscHandlerType): IDisposable { + const ident = this._identifier(id, [0x30, 0x7e]); + if (this._escHandlers[ident] === undefined) { + this._escHandlers[ident] = []; + } + const handlerList = this._escHandlers[ident]; + handlerList.push(handler); + return { + dispose: () => { + const handlerIndex = handlerList.indexOf(handler); + if (handlerIndex !== -1) { + handlerList.splice(handlerIndex, 1); + } + } + }; + } + public clearEscHandler(id: IFunctionIdentifier): void { + if (this._escHandlers[this._identifier(id, [0x30, 0x7e])]) delete this._escHandlers[this._identifier(id, [0x30, 0x7e])]; + } + public setEscHandlerFallback(handler: EscFallbackHandlerType): void { + this._escHandlerFb = handler; + } + + public setExecuteHandler(flag: string, handler: ExecuteHandlerType): void { + this._executeHandlers[flag.charCodeAt(0)] = handler; + } + public clearExecuteHandler(flag: string): void { + if (this._executeHandlers[flag.charCodeAt(0)]) delete this._executeHandlers[flag.charCodeAt(0)]; + } + public setExecuteHandlerFallback(handler: ExecuteFallbackHandlerType): void { + this._executeHandlerFb = handler; + } + + public registerCsiHandler(id: IFunctionIdentifier, handler: CsiHandlerType): IDisposable { + const ident = this._identifier(id); + if (this._csiHandlers[ident] === undefined) { + this._csiHandlers[ident] = []; + } + const handlerList = this._csiHandlers[ident]; + handlerList.push(handler); + return { + dispose: () => { + const handlerIndex = handlerList.indexOf(handler); + if (handlerIndex !== -1) { + handlerList.splice(handlerIndex, 1); + } + } + }; + } + public clearCsiHandler(id: IFunctionIdentifier): void { + if (this._csiHandlers[this._identifier(id)]) delete this._csiHandlers[this._identifier(id)]; + } + public setCsiHandlerFallback(callback: (ident: number, params: IParams) => void): void { + this._csiHandlerFb = callback; + } + + public registerDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): IDisposable { + return this._dcsParser.registerHandler(this._identifier(id), handler); + } + public clearDcsHandler(id: IFunctionIdentifier): void { + this._dcsParser.clearHandler(this._identifier(id)); + } + public setDcsHandlerFallback(handler: DcsFallbackHandlerType): void { + this._dcsParser.setHandlerFallback(handler); + } + + public registerOscHandler(ident: number, handler: IOscHandler): IDisposable { + return this._oscParser.registerHandler(ident, handler); + } + public clearOscHandler(ident: number): void { + this._oscParser.clearHandler(ident); + } + public setOscHandlerFallback(handler: OscFallbackHandlerType): void { + this._oscParser.setHandlerFallback(handler); + } + + public setErrorHandler(callback: (state: IParsingState) => IParsingState): void { + this._errorHandler = callback; + } + public clearErrorHandler(): void { + this._errorHandler = this._errorHandlerFb; + } + + /** + * Reset parser to initial values. + * + * This can also be used to lift the improper continuation error condition + * when dealing with async handlers. Use this only as a last resort to silence + * that error when the terminal has no pending data to be processed. Note that + * the interrupted async handler might continue its work in the future messing + * up the terminal state even further. + */ + public reset(): void { + this.currentState = this.initialState; + this._oscParser.reset(); + this._dcsParser.reset(); + this._params.reset(); + this._params.addParam(0); // ZDM + this._collect = 0; + this.precedingCodepoint = 0; + // abort pending continuation from async handler + // Here the RESET type indicates, that the next parse call will + // ignore any saved stack, instead continues sync with next codepoint from GROUND + if (this._parseStack.state !== ParserStackType.NONE) { + this._parseStack.state = ParserStackType.RESET; + this._parseStack.handlers = []; // also release handlers ref + } + } + + /** + * Async parse support. + */ + protected _preserveStack( + state: ParserStackType, + handlers: ResumableHandlersType, + handlerPos: number, + transition: number, + chunkPos: number + ): void { + this._parseStack.state = state; + this._parseStack.handlers = handlers; + this._parseStack.handlerPos = handlerPos; + this._parseStack.transition = transition; + this._parseStack.chunkPos = chunkPos; + } + + /** + * Parse UTF32 codepoints in `data` up to `length`. + * + * Note: For several actions with high data load the parsing is optimized + * by using local read ahead loops with hardcoded conditions to + * avoid costly table lookups. Make sure that any change of table values + * will be reflected in the loop conditions as well and vice versa. + * Affected states/actions: + * - GROUND:PRINT + * - CSI_PARAM:PARAM + * - DCS_PARAM:PARAM + * - OSC_STRING:OSC_PUT + * - DCS_PASSTHROUGH:DCS_PUT + * + * Note on asynchronous handler support: + * Any handler returning a promise will be treated as asynchronous. + * To keep the in-band blocking working for async handlers, `parse` pauses execution, + * creates a stack save and returns the promise to the caller. + * For proper continuation of the paused state it is important + * to await the promise resolving. On resolve the parse must be repeated + * with the same chunk of data and the resolved value in `promiseResult` + * until no promise is returned. + * + * Important: With only sync handlers defined, parsing is completely synchronous as well. + * As soon as an async handler is involved, synchronous parsing is not possible anymore. + * + * Boilerplate for proper parsing of multiple chunks with async handlers: + * + * ```typescript + * async function parseMultipleChunks(chunks: Uint32Array[]): Promise<void> { + * for (const chunk of chunks) { + * let result: void | Promise<boolean>; + * let prev: boolean | undefined; + * while (result = parser.parse(chunk, chunk.length, prev)) { + * prev = await result; + * } + * } + * // finished parsing all chunks... + * } + * ``` + */ + public parse(data: Uint32Array, length: number, promiseResult?: boolean): void | Promise<boolean> { + let code = 0; + let transition = 0; + let start = 0; + let handlerResult: void | boolean | Promise<boolean>; + + // resume from async handler + if (this._parseStack.state) { + // allow sync parser reset even in continuation mode + // Note: can be used to recover parser from improper continuation error below + if (this._parseStack.state === ParserStackType.RESET) { + this._parseStack.state = ParserStackType.NONE; + start = this._parseStack.chunkPos + 1; // continue with next codepoint in GROUND + } else { + if (promiseResult === undefined || this._parseStack.state === ParserStackType.FAIL) { + /** + * Reject further parsing on improper continuation after pausing. + * This is a really bad condition with screwed up execution order and prolly messed up + * terminal state, therefore we exit hard with an exception and reject any further parsing. + * + * Note: With `Terminal.write` usage this exception should never occur, as the top level + * calls are guaranteed to handle async conditions properly. If you ever encounter this + * exception in your terminal integration it indicates, that you injected data chunks to + * `InputHandler.parse` or `EscapeSequenceParser.parse` synchronously without waiting for + * continuation of a running async handler. + * + * It is possible to get rid of this error by calling `reset`. But dont rely on that, + * as the pending async handler still might mess up the terminal later. Instead fix the faulty + * async handling, so this error will not be thrown anymore. + */ + this._parseStack.state = ParserStackType.FAIL; + throw new Error('improper continuation due to previous async handler, giving up parsing'); + } + + // we have to resume the old handler loop if: + // - return value of the promise was `false` + // - handlers are not exhausted yet + const handlers = this._parseStack.handlers; + let handlerPos = this._parseStack.handlerPos - 1; + switch (this._parseStack.state) { + case ParserStackType.CSI: + if (promiseResult === false && handlerPos > -1) { + for (; handlerPos >= 0; handlerPos--) { + handlerResult = (handlers as CsiHandlerType[])[handlerPos](this._params); + if (handlerResult === true) { + break; + } else if (handlerResult instanceof Promise) { + this._parseStack.handlerPos = handlerPos; + return handlerResult; + } + } + } + this._parseStack.handlers = []; + break; + case ParserStackType.ESC: + if (promiseResult === false && handlerPos > -1) { + for (; handlerPos >= 0; handlerPos--) { + handlerResult = (handlers as EscHandlerType[])[handlerPos](); + if (handlerResult === true) { + break; + } else if (handlerResult instanceof Promise) { + this._parseStack.handlerPos = handlerPos; + return handlerResult; + } + } + } + this._parseStack.handlers = []; + break; + case ParserStackType.DCS: + code = data[this._parseStack.chunkPos]; + handlerResult = this._dcsParser.unhook(code !== 0x18 && code !== 0x1a, promiseResult); + if (handlerResult) { + return handlerResult; + } + if (code === 0x1b) this._parseStack.transition |= ParserState.ESCAPE; + this._params.reset(); + this._params.addParam(0); // ZDM + this._collect = 0; + break; + case ParserStackType.OSC: + code = data[this._parseStack.chunkPos]; + handlerResult = this._oscParser.end(code !== 0x18 && code !== 0x1a, promiseResult); + if (handlerResult) { + return handlerResult; + } + if (code === 0x1b) this._parseStack.transition |= ParserState.ESCAPE; + this._params.reset(); + this._params.addParam(0); // ZDM + this._collect = 0; + break; + } + // cleanup before continuing with the main sync loop + this._parseStack.state = ParserStackType.NONE; + start = this._parseStack.chunkPos + 1; + this.precedingCodepoint = 0; + this.currentState = this._parseStack.transition & TableAccess.TRANSITION_STATE_MASK; + } + } + + // continue with main sync loop + + // process input string + for (let i = start; i < length; ++i) { + code = data[i]; + + // normal transition & action lookup + transition = this._transitions.table[this.currentState << TableAccess.INDEX_STATE_SHIFT | (code < 0xa0 ? code : NON_ASCII_PRINTABLE)]; + switch (transition >> TableAccess.TRANSITION_ACTION_SHIFT) { + case ParserAction.PRINT: + // read ahead with loop unrolling + // Note: 0x20 (SP) is included, 0x7F (DEL) is excluded + for (let j = i + 1; ; ++j) { + if (j >= length || (code = data[j]) < 0x20 || (code > 0x7e && code < NON_ASCII_PRINTABLE)) { + this._printHandler(data, i, j); + i = j - 1; + break; + } + if (++j >= length || (code = data[j]) < 0x20 || (code > 0x7e && code < NON_ASCII_PRINTABLE)) { + this._printHandler(data, i, j); + i = j - 1; + break; + } + if (++j >= length || (code = data[j]) < 0x20 || (code > 0x7e && code < NON_ASCII_PRINTABLE)) { + this._printHandler(data, i, j); + i = j - 1; + break; + } + if (++j >= length || (code = data[j]) < 0x20 || (code > 0x7e && code < NON_ASCII_PRINTABLE)) { + this._printHandler(data, i, j); + i = j - 1; + break; + } + } + break; + case ParserAction.EXECUTE: + if (this._executeHandlers[code]) this._executeHandlers[code](); + else this._executeHandlerFb(code); + this.precedingCodepoint = 0; + break; + case ParserAction.IGNORE: + break; + case ParserAction.ERROR: + const inject: IParsingState = this._errorHandler( + { + position: i, + code, + currentState: this.currentState, + collect: this._collect, + params: this._params, + abort: false + }); + if (inject.abort) return; + // inject values: currently not implemented + break; + case ParserAction.CSI_DISPATCH: + // Trigger CSI Handler + const handlers = this._csiHandlers[this._collect << 8 | code]; + let j = handlers ? handlers.length - 1 : -1; + for (; j >= 0; j--) { + // true means success and to stop bubbling + // a promise indicates an async handler that needs to finish before progressing + handlerResult = handlers[j](this._params); + if (handlerResult === true) { + break; + } else if (handlerResult instanceof Promise) { + this._preserveStack(ParserStackType.CSI, handlers, j, transition, i); + return handlerResult; + } + } + if (j < 0) { + this._csiHandlerFb(this._collect << 8 | code, this._params); + } + this.precedingCodepoint = 0; + break; + case ParserAction.PARAM: + // inner loop: digits (0x30 - 0x39) and ; (0x3b) and : (0x3a) + do { + switch (code) { + case 0x3b: + this._params.addParam(0); // ZDM + break; + case 0x3a: + this._params.addSubParam(-1); + break; + default: // 0x30 - 0x39 + this._params.addDigit(code - 48); + } + } while (++i < length && (code = data[i]) > 0x2f && code < 0x3c); + i--; + break; + case ParserAction.COLLECT: + this._collect <<= 8; + this._collect |= code; + break; + case ParserAction.ESC_DISPATCH: + const handlersEsc = this._escHandlers[this._collect << 8 | code]; + let jj = handlersEsc ? handlersEsc.length - 1 : -1; + for (; jj >= 0; jj--) { + // true means success and to stop bubbling + // a promise indicates an async handler that needs to finish before progressing + handlerResult = handlersEsc[jj](); + if (handlerResult === true) { + break; + } else if (handlerResult instanceof Promise) { + this._preserveStack(ParserStackType.ESC, handlersEsc, jj, transition, i); + return handlerResult; + } + } + if (jj < 0) { + this._escHandlerFb(this._collect << 8 | code); + } + this.precedingCodepoint = 0; + break; + case ParserAction.CLEAR: + this._params.reset(); + this._params.addParam(0); // ZDM + this._collect = 0; + break; + case ParserAction.DCS_HOOK: + this._dcsParser.hook(this._collect << 8 | code, this._params); + break; + case ParserAction.DCS_PUT: + // inner loop - exit DCS_PUT: 0x18, 0x1a, 0x1b, 0x7f, 0x80 - 0x9f + // unhook triggered by: 0x1b, 0x9c (success) and 0x18, 0x1a (abort) + for (let j = i + 1; ; ++j) { + if (j >= length || (code = data[j]) === 0x18 || code === 0x1a || code === 0x1b || (code > 0x7f && code < NON_ASCII_PRINTABLE)) { + this._dcsParser.put(data, i, j); + i = j - 1; + break; + } + } + break; + case ParserAction.DCS_UNHOOK: + handlerResult = this._dcsParser.unhook(code !== 0x18 && code !== 0x1a); + if (handlerResult) { + this._preserveStack(ParserStackType.DCS, [], 0, transition, i); + return handlerResult; + } + if (code === 0x1b) transition |= ParserState.ESCAPE; + this._params.reset(); + this._params.addParam(0); // ZDM + this._collect = 0; + this.precedingCodepoint = 0; + break; + case ParserAction.OSC_START: + this._oscParser.start(); + break; + case ParserAction.OSC_PUT: + // inner loop: 0x20 (SP) included, 0x7F (DEL) included + for (let j = i + 1; ; j++) { + if (j >= length || (code = data[j]) < 0x20 || (code > 0x7f && code < NON_ASCII_PRINTABLE)) { + this._oscParser.put(data, i, j); + i = j - 1; + break; + } + } + break; + case ParserAction.OSC_END: + handlerResult = this._oscParser.end(code !== 0x18 && code !== 0x1a); + if (handlerResult) { + this._preserveStack(ParserStackType.OSC, [], 0, transition, i); + return handlerResult; + } + if (code === 0x1b) transition |= ParserState.ESCAPE; + this._params.reset(); + this._params.addParam(0); // ZDM + this._collect = 0; + this.precedingCodepoint = 0; + break; + } + this.currentState = transition & TableAccess.TRANSITION_STATE_MASK; + } + } +} diff --git a/node_modules/xterm/src/common/parser/OscParser.ts b/node_modules/xterm/src/common/parser/OscParser.ts new file mode 100644 index 0000000..32710ae --- /dev/null +++ b/node_modules/xterm/src/common/parser/OscParser.ts @@ -0,0 +1,238 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IOscHandler, IHandlerCollection, OscFallbackHandlerType, IOscParser, ISubParserStackState } from 'common/parser/Types'; +import { OscState, PAYLOAD_LIMIT } from 'common/parser/Constants'; +import { utf32ToString } from 'common/input/TextDecoder'; +import { IDisposable } from 'common/Types'; + +const EMPTY_HANDLERS: IOscHandler[] = []; + +export class OscParser implements IOscParser { + private _state = OscState.START; + private _active = EMPTY_HANDLERS; + private _id = -1; + private _handlers: IHandlerCollection<IOscHandler> = Object.create(null); + private _handlerFb: OscFallbackHandlerType = () => { }; + private _stack: ISubParserStackState = { + paused: false, + loopPosition: 0, + fallThrough: false + }; + + public registerHandler(ident: number, handler: IOscHandler): IDisposable { + if (this._handlers[ident] === undefined) { + this._handlers[ident] = []; + } + const handlerList = this._handlers[ident]; + handlerList.push(handler); + return { + dispose: () => { + const handlerIndex = handlerList.indexOf(handler); + if (handlerIndex !== -1) { + handlerList.splice(handlerIndex, 1); + } + } + }; + } + public clearHandler(ident: number): void { + if (this._handlers[ident]) delete this._handlers[ident]; + } + public setHandlerFallback(handler: OscFallbackHandlerType): void { + this._handlerFb = handler; + } + + public dispose(): void { + this._handlers = Object.create(null); + this._handlerFb = () => { }; + this._active = EMPTY_HANDLERS; + } + + public reset(): void { + // force cleanup handlers if payload was already sent + if (this._state === OscState.PAYLOAD) { + for (let j = this._stack.paused ? this._stack.loopPosition - 1 : this._active.length - 1; j >= 0; --j) { + this._active[j].end(false); + } + } + this._stack.paused = false; + this._active = EMPTY_HANDLERS; + this._id = -1; + this._state = OscState.START; + } + + private _start(): void { + this._active = this._handlers[this._id] || EMPTY_HANDLERS; + if (!this._active.length) { + this._handlerFb(this._id, 'START'); + } else { + for (let j = this._active.length - 1; j >= 0; j--) { + this._active[j].start(); + } + } + } + + private _put(data: Uint32Array, start: number, end: number): void { + if (!this._active.length) { + this._handlerFb(this._id, 'PUT', utf32ToString(data, start, end)); + } else { + for (let j = this._active.length - 1; j >= 0; j--) { + this._active[j].put(data, start, end); + } + } + } + + public start(): void { + // always reset leftover handlers + this.reset(); + this._state = OscState.ID; + } + + /** + * Put data to current OSC command. + * Expects the identifier of the OSC command in the form + * OSC id ; payload ST/BEL + * Payload chunks are not further processed and get + * directly passed to the handlers. + */ + public put(data: Uint32Array, start: number, end: number): void { + if (this._state === OscState.ABORT) { + return; + } + if (this._state === OscState.ID) { + while (start < end) { + const code = data[start++]; + if (code === 0x3b) { + this._state = OscState.PAYLOAD; + this._start(); + break; + } + if (code < 0x30 || 0x39 < code) { + this._state = OscState.ABORT; + return; + } + if (this._id === -1) { + this._id = 0; + } + this._id = this._id * 10 + code - 48; + } + } + if (this._state === OscState.PAYLOAD && end - start > 0) { + this._put(data, start, end); + } + } + + /** + * Indicates end of an OSC command. + * Whether the OSC got aborted or finished normally + * is indicated by `success`. + */ + public end(success: boolean, promiseResult: boolean = true): void | Promise<boolean> { + if (this._state === OscState.START) { + return; + } + // do nothing if command was faulty + if (this._state !== OscState.ABORT) { + // if we are still in ID state and get an early end + // means that the command has no payload thus we still have + // to announce START and send END right after + if (this._state === OscState.ID) { + this._start(); + } + + if (!this._active.length) { + this._handlerFb(this._id, 'END', success); + } else { + let handlerResult: boolean | Promise<boolean> = false; + let j = this._active.length - 1; + let fallThrough = false; + if (this._stack.paused) { + j = this._stack.loopPosition - 1; + handlerResult = promiseResult; + fallThrough = this._stack.fallThrough; + this._stack.paused = false; + } + if (!fallThrough && handlerResult === false) { + for (; j >= 0; j--) { + handlerResult = this._active[j].end(success); + if (handlerResult === true) { + break; + } else if (handlerResult instanceof Promise) { + this._stack.paused = true; + this._stack.loopPosition = j; + this._stack.fallThrough = false; + return handlerResult; + } + } + j--; + } + // cleanup left over handlers + // we always have to call .end for proper cleanup, + // here we use `success` to indicate whether a handler should execute + for (; j >= 0; j--) { + handlerResult = this._active[j].end(false); + if (handlerResult instanceof Promise) { + this._stack.paused = true; + this._stack.loopPosition = j; + this._stack.fallThrough = true; + return handlerResult; + } + } + } + + } + this._active = EMPTY_HANDLERS; + this._id = -1; + this._state = OscState.START; + } +} + +/** + * Convenient class to allow attaching string based handler functions + * as OSC handlers. + */ +export class OscHandler implements IOscHandler { + private _data = ''; + private _hitLimit: boolean = false; + + constructor(private _handler: (data: string) => boolean | Promise<boolean>) { } + + public start(): void { + this._data = ''; + this._hitLimit = false; + } + + public put(data: Uint32Array, start: number, end: number): void { + if (this._hitLimit) { + return; + } + this._data += utf32ToString(data, start, end); + if (this._data.length > PAYLOAD_LIMIT) { + this._data = ''; + this._hitLimit = true; + } + } + + public end(success: boolean): boolean | Promise<boolean> { + let ret: boolean | Promise<boolean> = false; + if (this._hitLimit) { + ret = false; + } else if (success) { + ret = this._handler(this._data); + if (ret instanceof Promise) { + // need to hold data until `ret` got resolved + // dont care for errors, data will be freed anyway on next start + return ret.then(res => { + this._data = ''; + this._hitLimit = false; + return res; + }); + } + } + this._data = ''; + this._hitLimit = false; + return ret; + } +} diff --git a/node_modules/xterm/src/common/parser/Params.ts b/node_modules/xterm/src/common/parser/Params.ts new file mode 100644 index 0000000..7071453 --- /dev/null +++ b/node_modules/xterm/src/common/parser/Params.ts @@ -0,0 +1,229 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ +import { IParams, ParamsArray } from 'common/parser/Types'; + +// max value supported for a single param/subparam (clamped to positive int32 range) +const MAX_VALUE = 0x7FFFFFFF; +// max allowed subparams for a single sequence (hardcoded limitation) +const MAX_SUBPARAMS = 256; + +/** + * Params storage class. + * This type is used by the parser to accumulate sequence parameters and sub parameters + * and transmit them to the input handler actions. + * + * NOTES: + * - params object for action handlers is borrowed, use `.toArray` or `.clone` to get a copy + * - never read beyond `params.length - 1` (likely to contain arbitrary data) + * - `.getSubParams` returns a borrowed typed array, use `.getSubParamsAll` for cloned sub params + * - hardcoded limitations: + * - max. value for a single (sub) param is 2^31 - 1 (greater values are clamped to that) + * - max. 256 sub params possible + * - negative values are not allowed beside -1 (placeholder for default value) + * + * About ZDM (Zero Default Mode): + * ZDM is not orchestrated by this class. If the parser is in ZDM, + * it should add 0 for empty params, otherwise -1. This does not apply + * to subparams, empty subparams should always be added with -1. + */ +export class Params implements IParams { + // params store and length + public params: Int32Array; + public length: number; + + // sub params store and length + protected _subParams: Int32Array; + protected _subParamsLength: number; + + // sub params offsets from param: param idx --> [start, end] offset + private _subParamsIdx: Uint16Array; + private _rejectDigits: boolean; + private _rejectSubDigits: boolean; + private _digitIsSub: boolean; + + /** + * Create a `Params` type from JS array representation. + */ + public static fromArray(values: ParamsArray): Params { + const params = new Params(); + if (!values.length) { + return params; + } + // skip leading sub params + for (let i = (Array.isArray(values[0])) ? 1 : 0; i < values.length; ++i) { + const value = values[i]; + if (Array.isArray(value)) { + for (let k = 0; k < value.length; ++k) { + params.addSubParam(value[k]); + } + } else { + params.addParam(value); + } + } + return params; + } + + /** + * @param maxLength max length of storable parameters + * @param maxSubParamsLength max length of storable sub parameters + */ + constructor(public maxLength: number = 32, public maxSubParamsLength: number = 32) { + if (maxSubParamsLength > MAX_SUBPARAMS) { + throw new Error('maxSubParamsLength must not be greater than 256'); + } + this.params = new Int32Array(maxLength); + this.length = 0; + this._subParams = new Int32Array(maxSubParamsLength); + this._subParamsLength = 0; + this._subParamsIdx = new Uint16Array(maxLength); + this._rejectDigits = false; + this._rejectSubDigits = false; + this._digitIsSub = false; + } + + /** + * Clone object. + */ + public clone(): Params { + const newParams = new Params(this.maxLength, this.maxSubParamsLength); + newParams.params.set(this.params); + newParams.length = this.length; + newParams._subParams.set(this._subParams); + newParams._subParamsLength = this._subParamsLength; + newParams._subParamsIdx.set(this._subParamsIdx); + newParams._rejectDigits = this._rejectDigits; + newParams._rejectSubDigits = this._rejectSubDigits; + newParams._digitIsSub = this._digitIsSub; + return newParams; + } + + /** + * Get a JS array representation of the current parameters and sub parameters. + * The array is structured as follows: + * sequence: "1;2:3:4;5::6" + * array : [1, 2, [3, 4], 5, [-1, 6]] + */ + public toArray(): ParamsArray { + const res: ParamsArray = []; + for (let i = 0; i < this.length; ++i) { + res.push(this.params[i]); + const start = this._subParamsIdx[i] >> 8; + const end = this._subParamsIdx[i] & 0xFF; + if (end - start > 0) { + res.push(Array.prototype.slice.call(this._subParams, start, end)); + } + } + return res; + } + + /** + * Reset to initial empty state. + */ + public reset(): void { + this.length = 0; + this._subParamsLength = 0; + this._rejectDigits = false; + this._rejectSubDigits = false; + this._digitIsSub = false; + } + + /** + * Add a parameter value. + * `Params` only stores up to `maxLength` parameters, any later + * parameter will be ignored. + * Note: VT devices only stored up to 16 values, xterm seems to + * store up to 30. + */ + public addParam(value: number): void { + this._digitIsSub = false; + if (this.length >= this.maxLength) { + this._rejectDigits = true; + return; + } + if (value < -1) { + throw new Error('values lesser than -1 are not allowed'); + } + this._subParamsIdx[this.length] = this._subParamsLength << 8 | this._subParamsLength; + this.params[this.length++] = value > MAX_VALUE ? MAX_VALUE : value; + } + + /** + * Add a sub parameter value. + * The sub parameter is automatically associated with the last parameter value. + * Thus it is not possible to add a subparameter without any parameter added yet. + * `Params` only stores up to `subParamsLength` sub parameters, any later + * sub parameter will be ignored. + */ + public addSubParam(value: number): void { + this._digitIsSub = true; + if (!this.length) { + return; + } + if (this._rejectDigits || this._subParamsLength >= this.maxSubParamsLength) { + this._rejectSubDigits = true; + return; + } + if (value < -1) { + throw new Error('values lesser than -1 are not allowed'); + } + this._subParams[this._subParamsLength++] = value > MAX_VALUE ? MAX_VALUE : value; + this._subParamsIdx[this.length - 1]++; + } + + /** + * Whether parameter at index `idx` has sub parameters. + */ + public hasSubParams(idx: number): boolean { + return ((this._subParamsIdx[idx] & 0xFF) - (this._subParamsIdx[idx] >> 8) > 0); + } + + /** + * Return sub parameters for parameter at index `idx`. + * Note: The values are borrowed, thus you need to copy + * the values if you need to hold them in nonlocal scope. + */ + public getSubParams(idx: number): Int32Array | null { + const start = this._subParamsIdx[idx] >> 8; + const end = this._subParamsIdx[idx] & 0xFF; + if (end - start > 0) { + return this._subParams.subarray(start, end); + } + return null; + } + + /** + * Return all sub parameters as {idx: subparams} mapping. + * Note: The values are not borrowed. + */ + public getSubParamsAll(): {[idx: number]: Int32Array} { + const result: {[idx: number]: Int32Array} = {}; + for (let i = 0; i < this.length; ++i) { + const start = this._subParamsIdx[i] >> 8; + const end = this._subParamsIdx[i] & 0xFF; + if (end - start > 0) { + result[i] = this._subParams.slice(start, end); + } + } + return result; + } + + /** + * Add a single digit value to current parameter. + * This is used by the parser to account digits on a char by char basis. + */ + public addDigit(value: number): void { + let length; + if (this._rejectDigits + || !(length = this._digitIsSub ? this._subParamsLength : this.length) + || (this._digitIsSub && this._rejectSubDigits) + ) { + return; + } + + const store = this._digitIsSub ? this._subParams : this.params; + const cur = store[length - 1]; + store[length - 1] = ~cur ? Math.min(cur * 10 + value, MAX_VALUE) : value; + } +} diff --git a/node_modules/xterm/src/common/parser/Types.d.ts b/node_modules/xterm/src/common/parser/Types.d.ts new file mode 100644 index 0000000..3a621ea --- /dev/null +++ b/node_modules/xterm/src/common/parser/Types.d.ts @@ -0,0 +1,274 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IDisposable } from 'common/Types'; +import { ParserState } from 'common/parser/Constants'; + + +/** sequence params serialized to js arrays */ +export type ParamsArray = (number | number[])[]; + +/** Params constructor type. */ +export interface IParamsConstructor { + new(maxLength: number, maxSubParamsLength: number): IParams; + + /** create params from ParamsArray */ + fromArray(values: ParamsArray): IParams; +} + +/** Interface of Params storage class. */ +export interface IParams { + /** from ctor */ + maxLength: number; + maxSubParamsLength: number; + + /** param values and its length */ + params: Int32Array; + length: number; + + /** methods */ + clone(): IParams; + toArray(): ParamsArray; + reset(): void; + addParam(value: number): void; + addSubParam(value: number): void; + hasSubParams(idx: number): boolean; + getSubParams(idx: number): Int32Array | null; + getSubParamsAll(): {[idx: number]: Int32Array}; +} + +/** + * Internal state of EscapeSequenceParser. + * Used as argument of the error handler to allow + * introspection at runtime on parse errors. + * Return it with altered values to recover from + * faulty states (not yet supported). + * Set `abort` to `true` to abort the current parsing. + */ +export interface IParsingState { + // position in parse string + position: number; + // actual character code + code: number; + // current parser state + currentState: ParserState; + // collect buffer with intermediate characters + collect: number; + // params buffer + params: IParams; + // should abort (default: false) + abort: boolean; +} + +/** + * Command handler interfaces. + */ + +/** + * CSI handler types. + * Note: `params` is borrowed. + */ +export type CsiHandlerType = (params: IParams) => boolean | Promise<boolean>; +export type CsiFallbackHandlerType = (ident: number, params: IParams) => void; + +/** + * DCS handler types. + */ +export interface IDcsHandler { + /** + * Called when a DCS command starts. + * Prepare needed data structures here. + * Note: `params` is borrowed. + */ + hook(params: IParams): void; + /** + * Incoming payload chunk. + * Note: `params` is borrowed. + */ + put(data: Uint32Array, start: number, end: number): void; + /** + * End of DCS command. `success` indicates whether the + * command finished normally or got aborted, thus final + * execution of the command should depend on `success`. + * To save memory also cleanup data structures here. + */ + unhook(success: boolean): boolean | Promise<boolean>; +} +export type DcsFallbackHandlerType = (ident: number, action: 'HOOK' | 'PUT' | 'UNHOOK', payload?: any) => void; + +/** + * ESC handler types. + */ +export type EscHandlerType = () => boolean | Promise<boolean>; +export type EscFallbackHandlerType = (identifier: number) => void; + +/** + * EXECUTE handler types. + */ +export type ExecuteHandlerType = () => boolean; +export type ExecuteFallbackHandlerType = (ident: number) => void; + +/** + * OSC handler types. + */ +export interface IOscHandler { + /** + * Announces start of this OSC command. + * Prepare needed data structures here. + */ + start(): void; + /** + * Incoming data chunk. + * Note: Data is borrowed. + */ + put(data: Uint32Array, start: number, end: number): void; + /** + * End of OSC command. `success` indicates whether the + * command finished normally or got aborted, thus final + * execution of the command should depend on `success`. + * To save memory also cleanup data structures here. + */ + end(success: boolean): boolean | Promise<boolean>; +} +export type OscFallbackHandlerType = (ident: number, action: 'START' | 'PUT' | 'END', payload?: any) => void; + +/** + * PRINT handler types. + */ +export type PrintHandlerType = (data: Uint32Array, start: number, end: number) => void; +export type PrintFallbackHandlerType = PrintHandlerType; + + +/** +* EscapeSequenceParser interface. +*/ +export interface IEscapeSequenceParser extends IDisposable { + /** + * Preceding codepoint to get REP working correctly. + * This must be set by the print handler as last action. + * It gets reset by the parser for any valid sequence beside REP itself. + */ + precedingCodepoint: number; + + /** + * Reset the parser to its initial state (handlers are kept). + */ + reset(): void; + + /** + * Parse UTF32 codepoints in `data` up to `length`. + * @param data The data to parse. + */ + parse(data: Uint32Array, length: number, promiseResult?: boolean): void | Promise<boolean>; + + /** + * Get string from numercial function identifier `ident`. + * Useful in fallback handlers which expose the low level + * numcerical function identifier for debugging purposes. + * Note: A full back translation to `IFunctionIdentifier` + * is not implemented. + */ + identToString(ident: number): string; + + setPrintHandler(handler: PrintHandlerType): void; + clearPrintHandler(): void; + + registerEscHandler(id: IFunctionIdentifier, handler: EscHandlerType): IDisposable; + clearEscHandler(id: IFunctionIdentifier): void; + setEscHandlerFallback(handler: EscFallbackHandlerType): void; + + setExecuteHandler(flag: string, handler: ExecuteHandlerType): void; + clearExecuteHandler(flag: string): void; + setExecuteHandlerFallback(handler: ExecuteFallbackHandlerType): void; + + registerCsiHandler(id: IFunctionIdentifier, handler: CsiHandlerType): IDisposable; + clearCsiHandler(id: IFunctionIdentifier): void; + setCsiHandlerFallback(callback: CsiFallbackHandlerType): void; + + registerDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): IDisposable; + clearDcsHandler(id: IFunctionIdentifier): void; + setDcsHandlerFallback(handler: DcsFallbackHandlerType): void; + + registerOscHandler(ident: number, handler: IOscHandler): IDisposable; + clearOscHandler(ident: number): void; + setOscHandlerFallback(handler: OscFallbackHandlerType): void; + + setErrorHandler(handler: (state: IParsingState) => IParsingState): void; + clearErrorHandler(): void; +} + +/** + * Subparser interfaces. + * The subparsers are instantiated in `EscapeSequenceParser` and + * called during `EscapeSequenceParser.parse`. + */ +export interface ISubParser<T, U> extends IDisposable { + reset(): void; + registerHandler(ident: number, handler: T): IDisposable; + clearHandler(ident: number): void; + setHandlerFallback(handler: U): void; + put(data: Uint32Array, start: number, end: number): void; +} + +export interface IOscParser extends ISubParser<IOscHandler, OscFallbackHandlerType> { + start(): void; + end(success: boolean, promiseResult?: boolean): void | Promise<boolean>; +} + +export interface IDcsParser extends ISubParser<IDcsHandler, DcsFallbackHandlerType> { + hook(ident: number, params: IParams): void; + unhook(success: boolean, promiseResult?: boolean): void | Promise<boolean>; +} + +/** + * Interface to denote a specific ESC, CSI or DCS handler slot. + * The values are used to create an integer respresentation during handler + * regristation before passed to the subparsers as `ident`. + * The integer translation is made to allow a faster handler access + * in `EscapeSequenceParser.parse`. + */ +export interface IFunctionIdentifier { + prefix?: string; + intermediates?: string; + final: string; +} + +export interface IHandlerCollection<T> { + [key: string]: T[]; +} + +/** + * Types for async parser support. + */ + +// type of saved stack state in parser +export const enum ParserStackType { + NONE = 0, + FAIL, + RESET, + CSI, + ESC, + OSC, + DCS +} + +// aggregate of resumable handler lists +export type ResumableHandlersType = CsiHandlerType[] | EscHandlerType[]; + +// saved stack state of the parser +export interface IParserStackState { + state: ParserStackType; + handlers: ResumableHandlersType; + handlerPos: number; + transition: number; + chunkPos: number; +} + +// saved stack state of subparser (OSC and DCS) +export interface ISubParserStackState { + paused: boolean; + loopPosition: number; + fallThrough: boolean; +} diff --git a/node_modules/xterm/src/common/public/AddonManager.ts b/node_modules/xterm/src/common/public/AddonManager.ts new file mode 100644 index 0000000..06c7812 --- /dev/null +++ b/node_modules/xterm/src/common/public/AddonManager.ts @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { ITerminalAddon, IDisposable, Terminal } from 'xterm'; + +export interface ILoadedAddon { + instance: ITerminalAddon; + dispose: () => void; + isDisposed: boolean; +} + +export class AddonManager implements IDisposable { + protected _addons: ILoadedAddon[] = []; + + constructor() { + } + + public dispose(): void { + for (let i = this._addons.length - 1; i >= 0; i--) { + this._addons[i].instance.dispose(); + } + } + + public loadAddon(terminal: Terminal, instance: ITerminalAddon): void { + const loadedAddon: ILoadedAddon = { + instance, + dispose: instance.dispose, + isDisposed: false + }; + this._addons.push(loadedAddon); + instance.dispose = () => this._wrappedAddonDispose(loadedAddon); + instance.activate(terminal as any); + } + + private _wrappedAddonDispose(loadedAddon: ILoadedAddon): void { + if (loadedAddon.isDisposed) { + // Do nothing if already disposed + return; + } + let index = -1; + for (let i = 0; i < this._addons.length; i++) { + if (this._addons[i] === loadedAddon) { + index = i; + break; + } + } + if (index === -1) { + throw new Error('Could not dispose an addon that has not been loaded'); + } + loadedAddon.isDisposed = true; + loadedAddon.dispose.apply(loadedAddon.instance); + this._addons.splice(index, 1); + } +} diff --git a/node_modules/xterm/src/common/public/BufferApiView.ts b/node_modules/xterm/src/common/public/BufferApiView.ts new file mode 100644 index 0000000..ca9ef2d --- /dev/null +++ b/node_modules/xterm/src/common/public/BufferApiView.ts @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2021 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IBuffer as IBufferApi, IBufferLine as IBufferLineApi, IBufferCell as IBufferCellApi } from 'xterm'; +import { IBuffer } from 'common/buffer/Types'; +import { BufferLineApiView } from 'common/public/BufferLineApiView'; +import { CellData } from 'common/buffer/CellData'; + +export class BufferApiView implements IBufferApi { + constructor( + private _buffer: IBuffer, + public readonly type: 'normal' | 'alternate' + ) { } + + public init(buffer: IBuffer): BufferApiView { + this._buffer = buffer; + return this; + } + + public get cursorY(): number { return this._buffer.y; } + public get cursorX(): number { return this._buffer.x; } + public get viewportY(): number { return this._buffer.ydisp; } + public get baseY(): number { return this._buffer.ybase; } + public get length(): number { return this._buffer.lines.length; } + public getLine(y: number): IBufferLineApi | undefined { + const line = this._buffer.lines.get(y); + if (!line) { + return undefined; + } + return new BufferLineApiView(line); + } + public getNullCell(): IBufferCellApi { return new CellData(); } +} diff --git a/node_modules/xterm/src/common/public/BufferLineApiView.ts b/node_modules/xterm/src/common/public/BufferLineApiView.ts new file mode 100644 index 0000000..6037501 --- /dev/null +++ b/node_modules/xterm/src/common/public/BufferLineApiView.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2021 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { CellData } from 'common/buffer/CellData'; +import { IBufferLine, ICellData } from 'common/Types'; +import { IBufferCell as IBufferCellApi, IBufferLine as IBufferLineApi } from 'xterm'; + +export class BufferLineApiView implements IBufferLineApi { + constructor(private _line: IBufferLine) { } + + public get isWrapped(): boolean { return this._line.isWrapped; } + public get length(): number { return this._line.length; } + public getCell(x: number, cell?: IBufferCellApi): IBufferCellApi | undefined { + if (x < 0 || x >= this._line.length) { + return undefined; + } + + if (cell) { + this._line.loadCell(x, cell as ICellData); + return cell; + } + return this._line.loadCell(x, new CellData()); + } + public translateToString(trimRight?: boolean, startColumn?: number, endColumn?: number): string { + return this._line.translateToString(trimRight, startColumn, endColumn); + } +} diff --git a/node_modules/xterm/src/common/public/BufferNamespaceApi.ts b/node_modules/xterm/src/common/public/BufferNamespaceApi.ts new file mode 100644 index 0000000..d86f6bf --- /dev/null +++ b/node_modules/xterm/src/common/public/BufferNamespaceApi.ts @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2021 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IBuffer as IBufferApi, IBufferNamespace as IBufferNamespaceApi } from 'xterm'; +import { BufferApiView } from 'common/public/BufferApiView'; +import { IEvent, EventEmitter } from 'common/EventEmitter'; +import { ICoreTerminal } from 'common/Types'; + +export class BufferNamespaceApi implements IBufferNamespaceApi { + private _normal: BufferApiView; + private _alternate: BufferApiView; + private _onBufferChange = new EventEmitter<IBufferApi>(); + public get onBufferChange(): IEvent<IBufferApi> { return this._onBufferChange.event; } + + constructor(private _core: ICoreTerminal) { + this._normal = new BufferApiView(this._core.buffers.normal, 'normal'); + this._alternate = new BufferApiView(this._core.buffers.alt, 'alternate'); + this._core.buffers.onBufferActivate(() => this._onBufferChange.fire(this.active)); + } + public get active(): IBufferApi { + if (this._core.buffers.active === this._core.buffers.normal) { return this.normal; } + if (this._core.buffers.active === this._core.buffers.alt) { return this.alternate; } + throw new Error('Active buffer is neither normal nor alternate'); + } + public get normal(): IBufferApi { + return this._normal.init(this._core.buffers.normal); + } + public get alternate(): IBufferApi { + return this._alternate.init(this._core.buffers.alt); + } +} diff --git a/node_modules/xterm/src/common/public/ParserApi.ts b/node_modules/xterm/src/common/public/ParserApi.ts new file mode 100644 index 0000000..67df4be --- /dev/null +++ b/node_modules/xterm/src/common/public/ParserApi.ts @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2021 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IParams } from 'common/parser/Types'; +import { IDisposable, IFunctionIdentifier, IParser } from 'xterm'; +import { ICoreTerminal } from 'common/Types'; + +export class ParserApi implements IParser { + constructor(private _core: ICoreTerminal) { } + + public registerCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean | Promise<boolean>): IDisposable { + return this._core.registerCsiHandler(id, (params: IParams) => callback(params.toArray())); + } + public addCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean | Promise<boolean>): IDisposable { + return this.registerCsiHandler(id, callback); + } + public registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean | Promise<boolean>): IDisposable { + return this._core.registerDcsHandler(id, (data: string, params: IParams) => callback(data, params.toArray())); + } + public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean | Promise<boolean>): IDisposable { + return this.registerDcsHandler(id, callback); + } + public registerEscHandler(id: IFunctionIdentifier, handler: () => boolean | Promise<boolean>): IDisposable { + return this._core.registerEscHandler(id, handler); + } + public addEscHandler(id: IFunctionIdentifier, handler: () => boolean | Promise<boolean>): IDisposable { + return this.registerEscHandler(id, handler); + } + public registerOscHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable { + return this._core.registerOscHandler(ident, callback); + } + public addOscHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable { + return this.registerOscHandler(ident, callback); + } +} diff --git a/node_modules/xterm/src/common/public/UnicodeApi.ts b/node_modules/xterm/src/common/public/UnicodeApi.ts new file mode 100644 index 0000000..8a669a0 --- /dev/null +++ b/node_modules/xterm/src/common/public/UnicodeApi.ts @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2021 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { ICoreTerminal } from 'common/Types'; +import { IUnicodeHandling, IUnicodeVersionProvider } from 'xterm'; + +export class UnicodeApi implements IUnicodeHandling { + constructor(private _core: ICoreTerminal) { } + + public register(provider: IUnicodeVersionProvider): void { + this._core.unicodeService.register(provider); + } + + public get versions(): string[] { + return this._core.unicodeService.versions; + } + + public get activeVersion(): string { + return this._core.unicodeService.activeVersion; + } + + public set activeVersion(version: string) { + this._core.unicodeService.activeVersion = version; + } +} diff --git a/node_modules/xterm/src/common/services/BufferService.ts b/node_modules/xterm/src/common/services/BufferService.ts new file mode 100644 index 0000000..bba60dd --- /dev/null +++ b/node_modules/xterm/src/common/services/BufferService.ts @@ -0,0 +1,185 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IBufferService, IOptionsService } from 'common/services/Services'; +import { BufferSet } from 'common/buffer/BufferSet'; +import { IBufferSet, IBuffer } from 'common/buffer/Types'; +import { EventEmitter, IEvent } from 'common/EventEmitter'; +import { Disposable } from 'common/Lifecycle'; +import { IAttributeData, IBufferLine, ScrollSource } from 'common/Types'; + +export const MINIMUM_COLS = 2; // Less than 2 can mess with wide chars +export const MINIMUM_ROWS = 1; + +export class BufferService extends Disposable implements IBufferService { + public serviceBrand: any; + + public cols: number; + public rows: number; + public buffers: IBufferSet; + /** Whether the user is scrolling (locks the scroll position) */ + public isUserScrolling: boolean = false; + + private _onResize = new EventEmitter<{ cols: number, rows: number }>(); + public get onResize(): IEvent<{ cols: number, rows: number }> { return this._onResize.event; } + private _onScroll = new EventEmitter<number>(); + public get onScroll(): IEvent<number> { return this._onScroll.event; } + + public get buffer(): IBuffer { return this.buffers.active; } + + /** An IBufferline to clone/copy from for new blank lines */ + private _cachedBlankLine: IBufferLine | undefined; + + constructor( + @IOptionsService private _optionsService: IOptionsService + ) { + super(); + this.cols = Math.max(_optionsService.rawOptions.cols || 0, MINIMUM_COLS); + this.rows = Math.max(_optionsService.rawOptions.rows || 0, MINIMUM_ROWS); + this.buffers = new BufferSet(_optionsService, this); + } + + public dispose(): void { + super.dispose(); + this.buffers.dispose(); + } + + public resize(cols: number, rows: number): void { + this.cols = cols; + this.rows = rows; + this.buffers.resize(cols, rows); + this.buffers.setupTabStops(this.cols); + this._onResize.fire({ cols, rows }); + } + + public reset(): void { + this.buffers.reset(); + this.isUserScrolling = false; + } + + /** + * Scroll the terminal down 1 row, creating a blank line. + * @param isWrapped Whether the new line is wrapped from the previous line. + */ + public scroll(eraseAttr: IAttributeData, isWrapped: boolean = false): void { + const buffer = this.buffer; + + let newLine: IBufferLine | undefined; + newLine = this._cachedBlankLine; + if (!newLine || newLine.length !== this.cols || newLine.getFg(0) !== eraseAttr.fg || newLine.getBg(0) !== eraseAttr.bg) { + newLine = buffer.getBlankLine(eraseAttr, isWrapped); + this._cachedBlankLine = newLine; + } + newLine.isWrapped = isWrapped; + + const topRow = buffer.ybase + buffer.scrollTop; + const bottomRow = buffer.ybase + buffer.scrollBottom; + + if (buffer.scrollTop === 0) { + // Determine whether the buffer is going to be trimmed after insertion. + const willBufferBeTrimmed = buffer.lines.isFull; + + // Insert the line using the fastest method + if (bottomRow === buffer.lines.length - 1) { + if (willBufferBeTrimmed) { + buffer.lines.recycle().copyFrom(newLine); + } else { + buffer.lines.push(newLine.clone()); + } + } else { + buffer.lines.splice(bottomRow + 1, 0, newLine.clone()); + } + + // Only adjust ybase and ydisp when the buffer is not trimmed + if (!willBufferBeTrimmed) { + buffer.ybase++; + // Only scroll the ydisp with ybase if the user has not scrolled up + if (!this.isUserScrolling) { + buffer.ydisp++; + } + } else { + // When the buffer is full and the user has scrolled up, keep the text + // stable unless ydisp is right at the top + if (this.isUserScrolling) { + buffer.ydisp = Math.max(buffer.ydisp - 1, 0); + } + } + } else { + // scrollTop is non-zero which means no line will be going to the + // scrollback, instead we can just shift them in-place. + const scrollRegionHeight = bottomRow - topRow + 1 /* as it's zero-based */; + buffer.lines.shiftElements(topRow + 1, scrollRegionHeight - 1, -1); + buffer.lines.set(bottomRow, newLine.clone()); + } + + // Move the viewport to the bottom of the buffer unless the user is + // scrolling. + if (!this.isUserScrolling) { + buffer.ydisp = buffer.ybase; + } + + this._onScroll.fire(buffer.ydisp); + } + + /** + * Scroll the display of the terminal + * @param disp The number of lines to scroll down (negative scroll up). + * @param suppressScrollEvent Don't emit the scroll event as scrollLines. This is used + * to avoid unwanted events being handled by the viewport when the event was triggered from the + * viewport originally. + */ + public scrollLines(disp: number, suppressScrollEvent?: boolean, source?: ScrollSource): void { + const buffer = this.buffer; + if (disp < 0) { + if (buffer.ydisp === 0) { + return; + } + this.isUserScrolling = true; + } else if (disp + buffer.ydisp >= buffer.ybase) { + this.isUserScrolling = false; + } + + const oldYdisp = buffer.ydisp; + buffer.ydisp = Math.max(Math.min(buffer.ydisp + disp, buffer.ybase), 0); + + // No change occurred, don't trigger scroll/refresh + if (oldYdisp === buffer.ydisp) { + return; + } + + if (!suppressScrollEvent) { + this._onScroll.fire(buffer.ydisp); + } + } + + /** + * Scroll the display of the terminal by a number of pages. + * @param pageCount The number of pages to scroll (negative scrolls up). + */ + public scrollPages(pageCount: number): void { + this.scrollLines(pageCount * (this.rows - 1)); + } + + /** + * Scrolls the display of the terminal to the top. + */ + public scrollToTop(): void { + this.scrollLines(-this.buffer.ydisp); + } + + /** + * Scrolls the display of the terminal to the bottom. + */ + public scrollToBottom(): void { + this.scrollLines(this.buffer.ybase - this.buffer.ydisp); + } + + public scrollToLine(line: number): void { + const scrollAmount = line - this.buffer.ydisp; + if (scrollAmount !== 0) { + this.scrollLines(scrollAmount); + } + } +} diff --git a/node_modules/xterm/src/common/services/CharsetService.ts b/node_modules/xterm/src/common/services/CharsetService.ts new file mode 100644 index 0000000..c538106 --- /dev/null +++ b/node_modules/xterm/src/common/services/CharsetService.ts @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { ICharsetService } from 'common/services/Services'; +import { ICharset } from 'common/Types'; + +export class CharsetService implements ICharsetService { + public serviceBrand: any; + + public charset: ICharset | undefined; + public glevel: number = 0; + + private _charsets: (ICharset | undefined)[] = []; + + public reset(): void { + this.charset = undefined; + this._charsets = []; + this.glevel = 0; + } + + public setgLevel(g: number): void { + this.glevel = g; + this.charset = this._charsets[g]; + } + + public setgCharset(g: number, charset: ICharset | undefined): void { + this._charsets[g] = charset; + if (this.glevel === g) { + this.charset = charset; + } + } +} diff --git a/node_modules/xterm/src/common/services/CoreMouseService.ts b/node_modules/xterm/src/common/services/CoreMouseService.ts new file mode 100644 index 0000000..0b0dc36 --- /dev/null +++ b/node_modules/xterm/src/common/services/CoreMouseService.ts @@ -0,0 +1,309 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ +import { IBufferService, ICoreService, ICoreMouseService } from 'common/services/Services'; +import { EventEmitter, IEvent } from 'common/EventEmitter'; +import { ICoreMouseProtocol, ICoreMouseEvent, CoreMouseEncoding, CoreMouseEventType, CoreMouseButton, CoreMouseAction } from 'common/Types'; + +/** + * Supported default protocols. + */ +const DEFAULT_PROTOCOLS: {[key: string]: ICoreMouseProtocol} = { + /** + * NONE + * Events: none + * Modifiers: none + */ + NONE: { + events: CoreMouseEventType.NONE, + restrict: () => false + }, + /** + * X10 + * Events: mousedown + * Modifiers: none + */ + X10: { + events: CoreMouseEventType.DOWN, + restrict: (e: ICoreMouseEvent) => { + // no wheel, no move, no up + if (e.button === CoreMouseButton.WHEEL || e.action !== CoreMouseAction.DOWN) { + return false; + } + // no modifiers + e.ctrl = false; + e.alt = false; + e.shift = false; + return true; + } + }, + /** + * VT200 + * Events: mousedown / mouseup / wheel + * Modifiers: all + */ + VT200: { + events: CoreMouseEventType.DOWN | CoreMouseEventType.UP | CoreMouseEventType.WHEEL, + restrict: (e: ICoreMouseEvent) => { + // no move + if (e.action === CoreMouseAction.MOVE) { + return false; + } + return true; + } + }, + /** + * DRAG + * Events: mousedown / mouseup / wheel / mousedrag + * Modifiers: all + */ + DRAG: { + events: CoreMouseEventType.DOWN | CoreMouseEventType.UP | CoreMouseEventType.WHEEL | CoreMouseEventType.DRAG, + restrict: (e: ICoreMouseEvent) => { + // no move without button + if (e.action === CoreMouseAction.MOVE && e.button === CoreMouseButton.NONE) { + return false; + } + return true; + } + }, + /** + * ANY + * Events: all mouse related events + * Modifiers: all + */ + ANY: { + events: + CoreMouseEventType.DOWN | CoreMouseEventType.UP | CoreMouseEventType.WHEEL + | CoreMouseEventType.DRAG | CoreMouseEventType.MOVE, + restrict: (e: ICoreMouseEvent) => true + } +}; + +const enum Modifiers { + SHIFT = 4, + ALT = 8, + CTRL = 16 +} + +// helper for default encoders to generate the event code. +function eventCode(e: ICoreMouseEvent, isSGR: boolean): number { + let code = (e.ctrl ? Modifiers.CTRL : 0) | (e.shift ? Modifiers.SHIFT : 0) | (e.alt ? Modifiers.ALT : 0); + if (e.button === CoreMouseButton.WHEEL) { + code |= 64; + code |= e.action; + } else { + code |= e.button & 3; + if (e.button & 4) { + code |= 64; + } + if (e.button & 8) { + code |= 128; + } + if (e.action === CoreMouseAction.MOVE) { + code |= CoreMouseAction.MOVE; + } else if (e.action === CoreMouseAction.UP && !isSGR) { + // special case - only SGR can report button on release + // all others have to go with NONE + code |= CoreMouseButton.NONE; + } + } + return code; +} + +const S = String.fromCharCode; + +/** + * Supported default encodings. + */ +const DEFAULT_ENCODINGS: {[key: string]: CoreMouseEncoding} = { + /** + * DEFAULT - CSI M Pb Px Py + * Single byte encoding for coords and event code. + * Can encode values up to 223 (1-based). + */ + DEFAULT: (e: ICoreMouseEvent) => { + const params = [eventCode(e, false) + 32, e.col + 32, e.row + 32]; + // supress mouse report if we exceed addressible range + // Note this is handled differently by emulators + // - xterm: sends 0;0 coords instead + // - vte, konsole: no report + if (params[0] > 255 || params[1] > 255 || params[2] > 255) { + return ''; + } + return `\x1b[M${S(params[0])}${S(params[1])}${S(params[2])}`; + }, + /** + * SGR - CSI < Pb ; Px ; Py M|m + * No encoding limitation. + * Can report button on release and works with a well formed sequence. + */ + SGR: (e: ICoreMouseEvent) => { + const final = (e.action === CoreMouseAction.UP && e.button !== CoreMouseButton.WHEEL) ? 'm' : 'M'; + return `\x1b[<${eventCode(e, true)};${e.col};${e.row}${final}`; + } +}; + +/** + * CoreMouseService + * + * Provides mouse tracking reports with different protocols and encodings. + * - protocols: NONE (default), X10, VT200, DRAG, ANY + * - encodings: DEFAULT, SGR (UTF8, URXVT removed in #2507) + * + * Custom protocols/encodings can be added by `addProtocol` / `addEncoding`. + * To activate a protocol/encoding, set `activeProtocol` / `activeEncoding`. + * Switching a protocol will send a notification event `onProtocolChange` + * with a list of needed events to track. + * + * The service handles the mouse tracking state and decides whether to send + * a tracking report to the backend based on protocol and encoding limitations. + * To send a mouse event call `triggerMouseEvent`. + */ +export class CoreMouseService implements ICoreMouseService { + private _protocols: {[name: string]: ICoreMouseProtocol} = {}; + private _encodings: {[name: string]: CoreMouseEncoding} = {}; + private _activeProtocol: string = ''; + private _activeEncoding: string = ''; + private _onProtocolChange = new EventEmitter<CoreMouseEventType>(); + private _lastEvent: ICoreMouseEvent | null = null; + + constructor( + @IBufferService private readonly _bufferService: IBufferService, + @ICoreService private readonly _coreService: ICoreService + ) { + // register default protocols and encodings + for (const name of Object.keys(DEFAULT_PROTOCOLS)) this.addProtocol(name, DEFAULT_PROTOCOLS[name]); + for (const name of Object.keys(DEFAULT_ENCODINGS)) this.addEncoding(name, DEFAULT_ENCODINGS[name]); + // call reset to set defaults + this.reset(); + } + + public addProtocol(name: string, protocol: ICoreMouseProtocol): void { + this._protocols[name] = protocol; + } + + public addEncoding(name: string, encoding: CoreMouseEncoding): void { + this._encodings[name] = encoding; + } + + public get activeProtocol(): string { + return this._activeProtocol; + } + + public get areMouseEventsActive(): boolean { + return this._protocols[this._activeProtocol].events !== 0; + } + + public set activeProtocol(name: string) { + if (!this._protocols[name]) { + throw new Error(`unknown protocol "${name}"`); + } + this._activeProtocol = name; + this._onProtocolChange.fire(this._protocols[name].events); + } + + public get activeEncoding(): string { + return this._activeEncoding; + } + + public set activeEncoding(name: string) { + if (!this._encodings[name]) { + throw new Error(`unknown encoding "${name}"`); + } + this._activeEncoding = name; + } + + public reset(): void { + this.activeProtocol = 'NONE'; + this.activeEncoding = 'DEFAULT'; + this._lastEvent = null; + } + + /** + * Event to announce changes in mouse tracking. + */ + public get onProtocolChange(): IEvent<CoreMouseEventType> { + return this._onProtocolChange.event; + } + + /** + * Triggers a mouse event to be sent. + * + * Returns true if the event passed all protocol restrictions and a report + * was sent, otherwise false. The return value may be used to decide whether + * the default event action in the bowser component should be omitted. + * + * Note: The method will change values of the given event object + * to fullfill protocol and encoding restrictions. + */ + public triggerMouseEvent(e: ICoreMouseEvent): boolean { + // range check for col/row + if (e.col < 0 || e.col >= this._bufferService.cols + || e.row < 0 || e.row >= this._bufferService.rows) { + return false; + } + + // filter nonsense combinations of button + action + if (e.button === CoreMouseButton.WHEEL && e.action === CoreMouseAction.MOVE) { + return false; + } + if (e.button === CoreMouseButton.NONE && e.action !== CoreMouseAction.MOVE) { + return false; + } + if (e.button !== CoreMouseButton.WHEEL && (e.action === CoreMouseAction.LEFT || e.action === CoreMouseAction.RIGHT)) { + return false; + } + + // report 1-based coords + e.col++; + e.row++; + + // debounce move at grid level + if (e.action === CoreMouseAction.MOVE && this._lastEvent && this._compareEvents(this._lastEvent, e)) { + return false; + } + + // apply protocol restrictions + if (!this._protocols[this._activeProtocol].restrict(e)) { + return false; + } + + // encode report and send + const report = this._encodings[this._activeEncoding](e); + if (report) { + // always send DEFAULT as binary data + if (this._activeEncoding === 'DEFAULT') { + this._coreService.triggerBinaryEvent(report); + } else { + this._coreService.triggerDataEvent(report, true); + } + } + + this._lastEvent = e; + + return true; + } + + public explainEvents(events: CoreMouseEventType): {[event: string]: boolean} { + return { + down: !!(events & CoreMouseEventType.DOWN), + up: !!(events & CoreMouseEventType.UP), + drag: !!(events & CoreMouseEventType.DRAG), + move: !!(events & CoreMouseEventType.MOVE), + wheel: !!(events & CoreMouseEventType.WHEEL) + }; + } + + private _compareEvents(e1: ICoreMouseEvent, e2: ICoreMouseEvent): boolean { + if (e1.col !== e2.col) return false; + if (e1.row !== e2.row) return false; + if (e1.button !== e2.button) return false; + if (e1.action !== e2.action) return false; + if (e1.ctrl !== e2.ctrl) return false; + if (e1.alt !== e2.alt) return false; + if (e1.shift !== e2.shift) return false; + return true; + } +} diff --git a/node_modules/xterm/src/common/services/CoreService.ts b/node_modules/xterm/src/common/services/CoreService.ts new file mode 100644 index 0000000..20a3460 --- /dev/null +++ b/node_modules/xterm/src/common/services/CoreService.ts @@ -0,0 +1,92 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { ICoreService, ILogService, IOptionsService, IBufferService } from 'common/services/Services'; +import { EventEmitter, IEvent } from 'common/EventEmitter'; +import { IDecPrivateModes, IModes } from 'common/Types'; +import { clone } from 'common/Clone'; +import { Disposable } from 'common/Lifecycle'; + +const DEFAULT_MODES: IModes = Object.freeze({ + insertMode: false +}); + +const DEFAULT_DEC_PRIVATE_MODES: IDecPrivateModes = Object.freeze({ + applicationCursorKeys: false, + applicationKeypad: false, + bracketedPasteMode: false, + origin: false, + reverseWraparound: false, + sendFocus: false, + wraparound: true // defaults: xterm - true, vt100 - false +}); + +export class CoreService extends Disposable implements ICoreService { + public serviceBrand: any; + + public isCursorInitialized: boolean = false; + public isCursorHidden: boolean = false; + public modes: IModes; + public decPrivateModes: IDecPrivateModes; + + // Circular dependency, this must be unset or memory will leak after Terminal.dispose + private _scrollToBottom: (() => void) | undefined; + + private _onData = this.register(new EventEmitter<string>()); + public get onData(): IEvent<string> { return this._onData.event; } + private _onUserInput = this.register(new EventEmitter<void>()); + public get onUserInput(): IEvent<void> { return this._onUserInput.event; } + private _onBinary = this.register(new EventEmitter<string>()); + public get onBinary(): IEvent<string> { return this._onBinary.event; } + + constructor( + // TODO: Move this into a service + scrollToBottom: () => void, + @IBufferService private readonly _bufferService: IBufferService, + @ILogService private readonly _logService: ILogService, + @IOptionsService private readonly _optionsService: IOptionsService + ) { + super(); + this._scrollToBottom = scrollToBottom; + this.register({ dispose: () => this._scrollToBottom = undefined }); + this.modes = clone(DEFAULT_MODES); + this.decPrivateModes = clone(DEFAULT_DEC_PRIVATE_MODES); + } + + public reset(): void { + this.modes = clone(DEFAULT_MODES); + this.decPrivateModes = clone(DEFAULT_DEC_PRIVATE_MODES); + } + + public triggerDataEvent(data: string, wasUserInput: boolean = false): void { + // Prevents all events to pty process if stdin is disabled + if (this._optionsService.rawOptions.disableStdin) { + return; + } + + // Input is being sent to the terminal, the terminal should focus the prompt. + const buffer = this._bufferService.buffer; + if (buffer.ybase !== buffer.ydisp) { + this._scrollToBottom!(); + } + + // Fire onUserInput so listeners can react as well (eg. clear selection) + if (wasUserInput) { + this._onUserInput.fire(); + } + + // Fire onData API + this._logService.debug(`sending data "${data}"`, () => data.split('').map(e => e.charCodeAt(0))); + this._onData.fire(data); + } + + public triggerBinaryEvent(data: string): void { + if (this._optionsService.rawOptions.disableStdin) { + return; + } + this._logService.debug(`sending binary "${data}"`, () => data.split('').map(e => e.charCodeAt(0))); + this._onBinary.fire(data); + } +} diff --git a/node_modules/xterm/src/common/services/DirtyRowService.ts b/node_modules/xterm/src/common/services/DirtyRowService.ts new file mode 100644 index 0000000..1c43b67 --- /dev/null +++ b/node_modules/xterm/src/common/services/DirtyRowService.ts @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IBufferService, IDirtyRowService } from 'common/services/Services'; + +export class DirtyRowService implements IDirtyRowService { + public serviceBrand: any; + + private _start!: number; + private _end!: number; + + public get start(): number { return this._start; } + public get end(): number { return this._end; } + + constructor( + @IBufferService private readonly _bufferService: IBufferService + ) { + this.clearRange(); + } + + public clearRange(): void { + this._start = this._bufferService.buffer.y; + this._end = this._bufferService.buffer.y; + } + + public markDirty(y: number): void { + if (y < this._start) { + this._start = y; + } else if (y > this._end) { + this._end = y; + } + } + + public markRangeDirty(y1: number, y2: number): void { + if (y1 > y2) { + const temp = y1; + y1 = y2; + y2 = temp; + } + if (y1 < this._start) { + this._start = y1; + } + if (y2 > this._end) { + this._end = y2; + } + } + + public markAllDirty(): void { + this.markRangeDirty(0, this._bufferService.rows - 1); + } +} diff --git a/node_modules/xterm/src/common/services/InstantiationService.ts b/node_modules/xterm/src/common/services/InstantiationService.ts new file mode 100644 index 0000000..8280948 --- /dev/null +++ b/node_modules/xterm/src/common/services/InstantiationService.ts @@ -0,0 +1,83 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + * + * This was heavily inspired from microsoft/vscode's dependency injection system (MIT). + */ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IInstantiationService, IServiceIdentifier } from 'common/services/Services'; +import { getServiceDependencies } from 'common/services/ServiceRegistry'; + +export class ServiceCollection { + + private _entries = new Map<IServiceIdentifier<any>, any>(); + + constructor(...entries: [IServiceIdentifier<any>, any][]) { + for (const [id, service] of entries) { + this.set(id, service); + } + } + + public set<T>(id: IServiceIdentifier<T>, instance: T): T { + const result = this._entries.get(id); + this._entries.set(id, instance); + return result; + } + + public forEach(callback: (id: IServiceIdentifier<any>, instance: any) => any): void { + this._entries.forEach((value, key) => callback(key, value)); + } + + public has(id: IServiceIdentifier<any>): boolean { + return this._entries.has(id); + } + + public get<T>(id: IServiceIdentifier<T>): T | undefined { + return this._entries.get(id); + } +} + +export class InstantiationService implements IInstantiationService { + public serviceBrand: undefined; + + private readonly _services: ServiceCollection = new ServiceCollection(); + + constructor() { + this._services.set(IInstantiationService, this); + } + + public setService<T>(id: IServiceIdentifier<T>, instance: T): void { + this._services.set(id, instance); + } + + public getService<T>(id: IServiceIdentifier<T>): T | undefined { + return this._services.get(id); + } + + public createInstance<T>(ctor: any, ...args: any[]): T { + const serviceDependencies = getServiceDependencies(ctor).sort((a, b) => a.index - b.index); + + const serviceArgs: any[] = []; + for (const dependency of serviceDependencies) { + const service = this._services.get(dependency.id); + if (!service) { + throw new Error(`[createInstance] ${ctor.name} depends on UNKNOWN service ${dependency.id}.`); + } + serviceArgs.push(service); + } + + const firstServiceArgPos = serviceDependencies.length > 0 ? serviceDependencies[0].index : args.length; + + // check for argument mismatches, adjust static args if needed + if (args.length !== firstServiceArgPos) { + throw new Error(`[createInstance] First service dependency of ${ctor.name} at position ${firstServiceArgPos + 1} conflicts with ${args.length} static arguments`); + } + + // now create the instance + return new ctor(...[...args, ...serviceArgs]); + } +} diff --git a/node_modules/xterm/src/common/services/LogService.ts b/node_modules/xterm/src/common/services/LogService.ts new file mode 100644 index 0000000..d356656 --- /dev/null +++ b/node_modules/xterm/src/common/services/LogService.ts @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { ILogService, IOptionsService, LogLevelEnum } from 'common/services/Services'; + +type LogType = (message?: any, ...optionalParams: any[]) => void; + +interface IConsole { + log: LogType; + error: LogType; + info: LogType; + trace: LogType; + warn: LogType; +} + +// console is available on both node.js and browser contexts but the common +// module doesn't depend on them so we need to explicitly declare it. +declare const console: IConsole; + +const optionsKeyToLogLevel: { [key: string]: LogLevelEnum } = { + debug: LogLevelEnum.DEBUG, + info: LogLevelEnum.INFO, + warn: LogLevelEnum.WARN, + error: LogLevelEnum.ERROR, + off: LogLevelEnum.OFF +}; + +const LOG_PREFIX = 'xterm.js: '; + +export class LogService implements ILogService { + public serviceBrand: any; + + public logLevel: LogLevelEnum = LogLevelEnum.OFF; + + constructor( + @IOptionsService private readonly _optionsService: IOptionsService + ) { + this._updateLogLevel(); + this._optionsService.onOptionChange(key => { + if (key === 'logLevel') { + this._updateLogLevel(); + } + }); + } + + private _updateLogLevel(): void { + this.logLevel = optionsKeyToLogLevel[this._optionsService.rawOptions.logLevel]; + } + + private _evalLazyOptionalParams(optionalParams: any[]): void { + for (let i = 0; i < optionalParams.length; i++) { + if (typeof optionalParams[i] === 'function') { + optionalParams[i] = optionalParams[i](); + } + } + } + + private _log(type: LogType, message: string, optionalParams: any[]): void { + this._evalLazyOptionalParams(optionalParams); + type.call(console, LOG_PREFIX + message, ...optionalParams); + } + + public debug(message: string, ...optionalParams: any[]): void { + if (this.logLevel <= LogLevelEnum.DEBUG) { + this._log(console.log, message, optionalParams); + } + } + + public info(message: string, ...optionalParams: any[]): void { + if (this.logLevel <= LogLevelEnum.INFO) { + this._log(console.info, message, optionalParams); + } + } + + public warn(message: string, ...optionalParams: any[]): void { + if (this.logLevel <= LogLevelEnum.WARN) { + this._log(console.warn, message, optionalParams); + } + } + + public error(message: string, ...optionalParams: any[]): void { + if (this.logLevel <= LogLevelEnum.ERROR) { + this._log(console.error, message, optionalParams); + } + } +} diff --git a/node_modules/xterm/src/common/services/OptionsService.ts b/node_modules/xterm/src/common/services/OptionsService.ts new file mode 100644 index 0000000..43fe998 --- /dev/null +++ b/node_modules/xterm/src/common/services/OptionsService.ts @@ -0,0 +1,177 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IOptionsService, ITerminalOptions, FontWeight } from 'common/services/Services'; +import { EventEmitter, IEvent } from 'common/EventEmitter'; +import { isMac } from 'common/Platform'; + +// Source: https://freesound.org/people/altemark/sounds/45759/ +// This sound is released under the Creative Commons Attribution 3.0 Unported +// (CC BY 3.0) license. It was created by 'altemark'. No modifications have been +// made, apart from the conversion to base64. +export const DEFAULT_BELL_SOUND = 'data:audio/mp3;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU4LjMyLjEwNAAAAAAAAAAAAAAA//tQxAADB8AhSmxhIIEVCSiJrDCQBTcu3UrAIwUdkRgQbFAZC1CQEwTJ9mjRvBA4UOLD8nKVOWfh+UlK3z/177OXrfOdKl7pyn3Xf//WreyTRUoAWgBgkOAGbZHBgG1OF6zM82DWbZaUmMBptgQhGjsyYqc9ae9XFz280948NMBWInljyzsNRFLPWdnZGWrddDsjK1unuSrVN9jJsK8KuQtQCtMBjCEtImISdNKJOopIpBFpNSMbIHCSRpRR5iakjTiyzLhchUUBwCgyKiweBv/7UsQbg8isVNoMPMjAAAA0gAAABEVFGmgqK////9bP/6XCykxBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq'; + +export const DEFAULT_OPTIONS: Readonly<ITerminalOptions> = { + cols: 80, + rows: 24, + cursorBlink: false, + cursorStyle: 'block', + cursorWidth: 1, + customGlyphs: true, + bellSound: DEFAULT_BELL_SOUND, + bellStyle: 'none', + drawBoldTextInBrightColors: true, + fastScrollModifier: 'alt', + fastScrollSensitivity: 5, + fontFamily: 'courier-new, courier, monospace', + fontSize: 15, + fontWeight: 'normal', + fontWeightBold: 'bold', + lineHeight: 1.0, + linkTooltipHoverDuration: 500, + letterSpacing: 0, + logLevel: 'info', + scrollback: 1000, + scrollSensitivity: 1, + screenReaderMode: false, + macOptionIsMeta: false, + macOptionClickForcesSelection: false, + minimumContrastRatio: 1, + disableStdin: false, + allowProposedApi: true, + allowTransparency: false, + tabStopWidth: 8, + theme: {}, + rightClickSelectsWord: isMac, + rendererType: 'canvas', + windowOptions: {}, + windowsMode: false, + wordSeparator: ' ()[]{}\',"`', + altClickMovesCursor: true, + convertEol: false, + termName: 'xterm', + cancelEvents: false +}; + +const FONT_WEIGHT_OPTIONS: Extract<FontWeight, string>[] = ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900']; + +export class OptionsService implements IOptionsService { + public serviceBrand: any; + + public readonly rawOptions: ITerminalOptions; + public options: ITerminalOptions; + + private _onOptionChange = new EventEmitter<string>(); + public get onOptionChange(): IEvent<string> { return this._onOptionChange.event; } + + constructor(options: Partial<ITerminalOptions>) { + // set the default value of each option + const defaultOptions = { ...DEFAULT_OPTIONS }; + for (const key in options) { + if (key in defaultOptions) { + try { + const newValue = options[key]; + defaultOptions[key] = this._sanitizeAndValidateOption(key, newValue); + } catch (e) { + console.error(e); + } + } + } + + // set up getters and setters for each option + this.rawOptions = defaultOptions; + this.options = { ... defaultOptions }; + this._setupOptions(); + } + + private _setupOptions(): void { + const getter = (propName: string): any => { + if (!(propName in DEFAULT_OPTIONS)) { + throw new Error(`No option with key "${propName}"`); + } + return this.rawOptions[propName]; + }; + + const setter = (propName: string, value: any): void => { + if (!(propName in DEFAULT_OPTIONS)) { + throw new Error(`No option with key "${propName}"`); + } + + value = this._sanitizeAndValidateOption(propName, value); + // Don't fire an option change event if they didn't change + if (this.rawOptions[propName] !== value) { + this.rawOptions[propName] = value; + this._onOptionChange.fire(propName); + } + }; + + for (const propName in this.rawOptions) { + const desc = { + get: getter.bind(this, propName), + set: setter.bind(this, propName) + }; + Object.defineProperty(this.options, propName, desc); + } + } + + public setOption(key: string, value: any): void { + this.options[key] = value; + } + + private _sanitizeAndValidateOption(key: string, value: any): any { + switch (key) { + case 'bellStyle': + case 'cursorStyle': + case 'rendererType': + case 'wordSeparator': + if (!value) { + value = DEFAULT_OPTIONS[key]; + } + break; + case 'fontWeight': + case 'fontWeightBold': + if (typeof value === 'number' && 1 <= value && value <= 1000) { + // already valid numeric value + break; + } + value = FONT_WEIGHT_OPTIONS.includes(value) ? value : DEFAULT_OPTIONS[key]; + break; + case 'cursorWidth': + value = Math.floor(value); + // Fall through for bounds check + case 'lineHeight': + case 'tabStopWidth': + if (value < 1) { + throw new Error(`${key} cannot be less than 1, value: ${value}`); + } + break; + case 'minimumContrastRatio': + value = Math.max(1, Math.min(21, Math.round(value * 10) / 10)); + break; + case 'scrollback': + value = Math.min(value, 4294967295); + if (value < 0) { + throw new Error(`${key} cannot be less than 0, value: ${value}`); + } + break; + case 'fastScrollSensitivity': + case 'scrollSensitivity': + if (value <= 0) { + throw new Error(`${key} cannot be less than or equal to 0, value: ${value}`); + } + case 'rows': + case 'cols': + if (!value && value !== 0) { + throw new Error(`${key} must be numeric, value: ${value}`); + } + break; + } + return value; + } + + public getOption(key: string): any { + return this.options[key]; + } +} diff --git a/node_modules/xterm/src/common/services/ServiceRegistry.ts b/node_modules/xterm/src/common/services/ServiceRegistry.ts new file mode 100644 index 0000000..6510fb8 --- /dev/null +++ b/node_modules/xterm/src/common/services/ServiceRegistry.ts @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + * + * This was heavily inspired from microsoft/vscode's dependency injection system (MIT). + */ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IServiceIdentifier } from 'common/services/Services'; + +const DI_TARGET = 'di$target'; +const DI_DEPENDENCIES = 'di$dependencies'; + +export const serviceRegistry: Map<string, IServiceIdentifier<any>> = new Map(); + +export function getServiceDependencies(ctor: any): { id: IServiceIdentifier<any>, index: number, optional: boolean }[] { + return ctor[DI_DEPENDENCIES] || []; +} + +export function createDecorator<T>(id: string): IServiceIdentifier<T> { + if (serviceRegistry.has(id)) { + return serviceRegistry.get(id)!; + } + + const decorator: any = function (target: Function, key: string, index: number): any { + if (arguments.length !== 3) { + throw new Error('@IServiceName-decorator can only be used to decorate a parameter'); + } + + storeServiceDependency(decorator, target, index); + }; + + decorator.toString = () => id; + + serviceRegistry.set(id, decorator); + return decorator; +} + +function storeServiceDependency(id: Function, target: Function, index: number): void { + if ((target as any)[DI_TARGET] === target) { + (target as any)[DI_DEPENDENCIES].push({ id, index }); + } else { + (target as any)[DI_DEPENDENCIES] = [{ id, index }]; + (target as any)[DI_TARGET] = target; + } +} diff --git a/node_modules/xterm/src/common/services/Services.ts b/node_modules/xterm/src/common/services/Services.ts new file mode 100644 index 0000000..90dca98 --- /dev/null +++ b/node_modules/xterm/src/common/services/Services.ts @@ -0,0 +1,300 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IEvent } from 'common/EventEmitter'; +import { IBuffer, IBufferSet } from 'common/buffer/Types'; +import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEncoding, ICoreMouseProtocol, CoreMouseEventType, ICharset, IWindowOptions, IModes, IAttributeData, ScrollSource } from 'common/Types'; +import { createDecorator } from 'common/services/ServiceRegistry'; + +export const IBufferService = createDecorator<IBufferService>('BufferService'); +export interface IBufferService { + serviceBrand: undefined; + + readonly cols: number; + readonly rows: number; + readonly buffer: IBuffer; + readonly buffers: IBufferSet; + isUserScrolling: boolean; + onResize: IEvent<{ cols: number, rows: number }>; + onScroll: IEvent<number>; + scroll(eraseAttr: IAttributeData, isWrapped?: boolean): void; + scrollToBottom(): void; + scrollToTop(): void; + scrollToLine(line: number): void; + scrollLines(disp: number, suppressScrollEvent?: boolean, source?: ScrollSource): void; + scrollPages(pageCount: number): void; + resize(cols: number, rows: number): void; + reset(): void; +} + +export const ICoreMouseService = createDecorator<ICoreMouseService>('CoreMouseService'); +export interface ICoreMouseService { + activeProtocol: string; + activeEncoding: string; + areMouseEventsActive: boolean; + addProtocol(name: string, protocol: ICoreMouseProtocol): void; + addEncoding(name: string, encoding: CoreMouseEncoding): void; + reset(): void; + + /** + * Triggers a mouse event to be sent. + * + * Returns true if the event passed all protocol restrictions and a report + * was sent, otherwise false. The return value may be used to decide whether + * the default event action in the bowser component should be omitted. + * + * Note: The method will change values of the given event object + * to fullfill protocol and encoding restrictions. + */ + triggerMouseEvent(event: ICoreMouseEvent): boolean; + + /** + * Event to announce changes in mouse tracking. + */ + onProtocolChange: IEvent<CoreMouseEventType>; + + /** + * Human readable version of mouse events. + */ + explainEvents(events: CoreMouseEventType): { [event: string]: boolean }; +} + +export const ICoreService = createDecorator<ICoreService>('CoreService'); +export interface ICoreService { + serviceBrand: undefined; + + /** + * Initially the cursor will not be visible until the first time the terminal + * is focused. + */ + isCursorInitialized: boolean; + isCursorHidden: boolean; + + readonly modes: IModes; + readonly decPrivateModes: IDecPrivateModes; + + readonly onData: IEvent<string>; + readonly onUserInput: IEvent<void>; + readonly onBinary: IEvent<string>; + + reset(): void; + + /** + * Triggers the onData event in the public API. + * @param data The data that is being emitted. + * @param wasFromUser Whether the data originated from the user (as opposed to + * resulting from parsing incoming data). When true this will also: + * - Scroll to the bottom of the buffer.s + * - Fire the `onUserInput` event (so selection can be cleared). + */ + triggerDataEvent(data: string, wasUserInput?: boolean): void; + + /** + * Triggers the onBinary event in the public API. + * @param data The data that is being emitted. + */ + triggerBinaryEvent(data: string): void; +} + +export const ICharsetService = createDecorator<ICharsetService>('CharsetService'); +export interface ICharsetService { + serviceBrand: undefined; + + charset: ICharset | undefined; + readonly glevel: number; + + reset(): void; + + /** + * Set the G level of the terminal. + * @param g + */ + setgLevel(g: number): void; + + /** + * Set the charset for the given G level of the terminal. + * @param g + * @param charset + */ + setgCharset(g: number, charset: ICharset | undefined): void; +} + +export const IDirtyRowService = createDecorator<IDirtyRowService>('DirtyRowService'); +export interface IDirtyRowService { + serviceBrand: undefined; + + readonly start: number; + readonly end: number; + + clearRange(): void; + markDirty(y: number): void; + markRangeDirty(y1: number, y2: number): void; + markAllDirty(): void; +} + +export interface IServiceIdentifier<T> { + (...args: any[]): void; + type: T; +} + +export interface IBrandedService { + serviceBrand: undefined; +} + +type GetLeadingNonServiceArgs<Args> = + Args extends [...IBrandedService[]] ? [] + : Args extends [infer A1, ...IBrandedService[]] ? [A1] + : Args extends [infer A1, infer A2, ...IBrandedService[]] ? [A1, A2] + : Args extends [infer A1, infer A2, infer A3, ...IBrandedService[]] ? [A1, A2, A3] + : Args extends [infer A1, infer A2, infer A3, infer A4, ...IBrandedService[]] ? [A1, A2, A3, A4] + : Args extends [infer A1, infer A2, infer A3, infer A4, infer A5, ...IBrandedService[]] ? [A1, A2, A3, A4, A5] + : Args extends [infer A1, infer A2, infer A3, infer A4, infer A5, infer A6, ...IBrandedService[]] ? [A1, A2, A3, A4, A5, A6] + : Args extends [infer A1, infer A2, infer A3, infer A4, infer A5, infer A6, infer A7, ...IBrandedService[]] ? [A1, A2, A3, A4, A5, A6, A7] + : Args extends [infer A1, infer A2, infer A3, infer A4, infer A5, infer A6, infer A7, infer A8, ...IBrandedService[]] ? [A1, A2, A3, A4, A5, A6, A7, A8] + : never; + +export const IInstantiationService = createDecorator<IInstantiationService>('InstantiationService'); +export interface IInstantiationService { + serviceBrand: undefined; + + setService<T>(id: IServiceIdentifier<T>, instance: T): void; + getService<T>(id: IServiceIdentifier<T>): T | undefined; + createInstance<Ctor extends new (...args: any[]) => any, R extends InstanceType<Ctor>>(t: Ctor, ...args: GetLeadingNonServiceArgs<ConstructorParameters<Ctor>>): R; +} + +export enum LogLevelEnum { + DEBUG = 0, + INFO = 1, + WARN = 2, + ERROR = 3, + OFF = 4 +} + +export const ILogService = createDecorator<ILogService>('LogService'); +export interface ILogService { + serviceBrand: undefined; + + logLevel: LogLevelEnum; + + debug(message: any, ...optionalParams: any[]): void; + info(message: any, ...optionalParams: any[]): void; + warn(message: any, ...optionalParams: any[]): void; + error(message: any, ...optionalParams: any[]): void; +} + +export const IOptionsService = createDecorator<IOptionsService>('OptionsService'); +export interface IOptionsService { + serviceBrand: undefined; + + /** + * Read only access to the raw options object, this is an internal-only fast path for accessing + * single options without any validation as we trust TypeScript to enforce correct usage + * internally. + */ + readonly rawOptions: Readonly<ITerminalOptions>; + readonly options: ITerminalOptions; + + readonly onOptionChange: IEvent<string>; + + setOption<T>(key: string, value: T): void; + getOption<T>(key: string): T | undefined; +} + +export type FontWeight = 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900' | number; +export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'off'; + +export type RendererType = 'dom' | 'canvas'; + +export interface ITerminalOptions { + allowProposedApi: boolean; + allowTransparency: boolean; + altClickMovesCursor: boolean; + bellSound: string; + bellStyle: 'none' | 'sound' /* | 'visual' | 'both' */; + cols: number; + convertEol: boolean; + cursorBlink: boolean; + cursorStyle: 'block' | 'underline' | 'bar'; + cursorWidth: number; + customGlyphs: boolean; + disableStdin: boolean; + drawBoldTextInBrightColors: boolean; + fastScrollModifier: 'alt' | 'ctrl' | 'shift' | undefined; + fastScrollSensitivity: number; + fontSize: number; + fontFamily: string; + fontWeight: FontWeight; + fontWeightBold: FontWeight; + letterSpacing: number; + lineHeight: number; + linkTooltipHoverDuration: number; + logLevel: LogLevel; + macOptionIsMeta: boolean; + macOptionClickForcesSelection: boolean; + minimumContrastRatio: number; + rendererType: RendererType; + rightClickSelectsWord: boolean; + rows: number; + screenReaderMode: boolean; + scrollback: number; + scrollSensitivity: number; + tabStopWidth: number; + theme: ITheme; + windowsMode: boolean; + windowOptions: IWindowOptions; + wordSeparator: string; + + [key: string]: any; + cancelEvents: boolean; + termName: string; +} + +export interface ITheme { + foreground?: string; + background?: string; + cursor?: string; + cursorAccent?: string; + selection?: string; + black?: string; + red?: string; + green?: string; + yellow?: string; + blue?: string; + magenta?: string; + cyan?: string; + white?: string; + brightBlack?: string; + brightRed?: string; + brightGreen?: string; + brightYellow?: string; + brightBlue?: string; + brightMagenta?: string; + brightCyan?: string; + brightWhite?: string; +} + +export const IUnicodeService = createDecorator<IUnicodeService>('UnicodeService'); +export interface IUnicodeService { + serviceBrand: undefined; + /** Register an Unicode version provider. */ + register(provider: IUnicodeVersionProvider): void; + /** Registered Unicode versions. */ + readonly versions: string[]; + /** Currently active version. */ + activeVersion: string; + /** Event triggered, when activate version changed. */ + readonly onChange: IEvent<string>; + + /** + * Unicode version dependent + */ + wcwidth(codepoint: number): number; + getStringCellWidth(s: string): number; +} + +export interface IUnicodeVersionProvider { + readonly version: string; + wcwidth(ucs: number): 0 | 1 | 2; +} diff --git a/node_modules/xterm/src/common/services/UnicodeService.ts b/node_modules/xterm/src/common/services/UnicodeService.ts new file mode 100644 index 0000000..e96b757 --- /dev/null +++ b/node_modules/xterm/src/common/services/UnicodeService.ts @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ +import { IUnicodeService, IUnicodeVersionProvider } from 'common/services/Services'; +import { EventEmitter, IEvent } from 'common/EventEmitter'; +import { UnicodeV6 } from 'common/input/UnicodeV6'; + + +export class UnicodeService implements IUnicodeService { + public serviceBrand: any; + + private _providers: {[key: string]: IUnicodeVersionProvider} = Object.create(null); + private _active: string = ''; + private _activeProvider: IUnicodeVersionProvider; + private _onChange = new EventEmitter<string>(); + public get onChange(): IEvent<string> { return this._onChange.event; } + + constructor() { + const defaultProvider = new UnicodeV6(); + this.register(defaultProvider); + this._active = defaultProvider.version; + this._activeProvider = defaultProvider; + } + + public get versions(): string[] { + return Object.keys(this._providers); + } + + public get activeVersion(): string { + return this._active; + } + + public set activeVersion(version: string) { + if (!this._providers[version]) { + throw new Error(`unknown Unicode version "${version}"`); + } + this._active = version; + this._activeProvider = this._providers[version]; + this._onChange.fire(version); + } + + public register(provider: IUnicodeVersionProvider): void { + this._providers[provider.version] = provider; + } + + /** + * Unicode version dependent interface. + */ + public wcwidth(num: number): number { + return this._activeProvider.wcwidth(num); + } + + public getStringCellWidth(s: string): number { + let result = 0; + const length = s.length; + for (let i = 0; i < length; ++i) { + let code = s.charCodeAt(i); + // surrogate pair first + if (0xD800 <= code && code <= 0xDBFF) { + if (++i >= length) { + // this should not happen with strings retrieved from + // Buffer.translateToString as it converts from UTF-32 + // and therefore always should contain the second part + // for any other string we still have to handle it somehow: + // simply treat the lonely surrogate first as a single char (UCS-2 behavior) + return result + this.wcwidth(code); + } + const second = s.charCodeAt(i); + // convert surrogate pair to high codepoint only for valid second part (UTF-16) + // otherwise treat them independently (UCS-2 behavior) + if (0xDC00 <= second && second <= 0xDFFF) { + code = (code - 0xD800) * 0x400 + second - 0xDC00 + 0x10000; + } else { + result += this.wcwidth(second); + } + } + result += this.wcwidth(code); + } + return result; + } +} diff --git a/node_modules/xterm/src/headless/Terminal.ts b/node_modules/xterm/src/headless/Terminal.ts new file mode 100644 index 0000000..7f138ce --- /dev/null +++ b/node_modules/xterm/src/headless/Terminal.ts @@ -0,0 +1,170 @@ +/** + * Copyright (c) 2014 The xterm.js authors. All rights reserved. + * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License) + * @license MIT + * + * Originally forked from (with the author's permission): + * Fabrice Bellard's javascript vt100 for jslinux: + * http://bellard.org/jslinux/ + * Copyright (c) 2011 Fabrice Bellard + * The original design remains. The terminal itself + * has been extended to include xterm CSI codes, among + * other features. + * + * Terminal Emulation References: + * http://vt100.net/ + * http://invisible-island.net/xterm/ctlseqs/ctlseqs.txt + * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html + * http://invisible-island.net/vttest/ + * http://www.inwap.com/pdp10/ansicode.txt + * http://linux.die.net/man/4/console_codes + * http://linux.die.net/man/7/urxvt + */ + +import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; +import { IBuffer } from 'common/buffer/Types'; +import { CoreTerminal } from 'common/CoreTerminal'; +import { EventEmitter, forwardEvent, IEvent } from 'common/EventEmitter'; +import { ITerminalOptions as IInitializedTerminalOptions } from 'common/services/Services'; +import { IMarker, ITerminalOptions, ScrollSource } from 'common/Types'; + +export class Terminal extends CoreTerminal { + // TODO: We should remove options once components adopt optionsService + public get options(): IInitializedTerminalOptions { return this.optionsService.options; } + + private _onBell = new EventEmitter<void>(); + public get onBell(): IEvent<void> { return this._onBell.event; } + private _onCursorMove = new EventEmitter<void>(); + public get onCursorMove(): IEvent<void> { return this._onCursorMove.event; } + private _onTitleChange = new EventEmitter<string>(); + public get onTitleChange(): IEvent<string> { return this._onTitleChange.event; } + + private _onA11yCharEmitter = new EventEmitter<string>(); + public get onA11yChar(): IEvent<string> { return this._onA11yCharEmitter.event; } + private _onA11yTabEmitter = new EventEmitter<number>(); + public get onA11yTab(): IEvent<number> { return this._onA11yTabEmitter.event; } + + /** + * Creates a new `Terminal` object. + * + * @param options An object containing a set of options, the available options are: + * - `cursorBlink` (boolean): Whether the terminal cursor blinks + * - `cols` (number): The number of columns of the terminal (horizontal size) + * - `rows` (number): The number of rows of the terminal (vertical size) + * + * @public + * @class Xterm Xterm + * @alias module:xterm/src/xterm + */ + constructor( + options: ITerminalOptions = {} + ) { + super(options); + + this._setup(); + + // Setup InputHandler listeners + this.register(this._inputHandler.onRequestBell(() => this.bell())); + this.register(this._inputHandler.onRequestReset(() => this.reset())); + this.register(forwardEvent(this._inputHandler.onCursorMove, this._onCursorMove)); + this.register(forwardEvent(this._inputHandler.onTitleChange, this._onTitleChange)); + this.register(forwardEvent(this._inputHandler.onA11yChar, this._onA11yCharEmitter)); + this.register(forwardEvent(this._inputHandler.onA11yTab, this._onA11yTabEmitter)); + } + + public dispose(): void { + if (this._isDisposed) { + return; + } + super.dispose(); + this.write = () => { }; + } + + /** + * Convenience property to active buffer. + */ + public get buffer(): IBuffer { + return this.buffers.active; + } + + protected _updateOptions(key: string): void { + super._updateOptions(key); + + // TODO: These listeners should be owned by individual components + switch (key) { + case 'tabStopWidth': this.buffers.setupTabStops(); break; + } + } + + // TODO: Support paste here? + + public get markers(): IMarker[] { + return this.buffer.markers; + } + + public addMarker(cursorYOffset: number): IMarker | undefined { + // Disallow markers on the alt buffer + if (this.buffer !== this.buffers.normal) { + return; + } + + return this.buffer.addMarker(this.buffer.ybase + this.buffer.y + cursorYOffset); + } + + public bell(): void { + this._onBell.fire(); + } + + /** + * Resizes the terminal. + * + * @param x The number of columns to resize to. + * @param y The number of rows to resize to. + */ + public resize(x: number, y: number): void { + if (x === this.cols && y === this.rows) { + return; + } + + super.resize(x, y); + } + + /** + * Clear the entire buffer, making the prompt line the new first line. + */ + public clear(): void { + if (this.buffer.ybase === 0 && this.buffer.y === 0) { + // Don't clear if it's already clear + return; + } + this.buffer.lines.set(0, this.buffer.lines.get(this.buffer.ybase + this.buffer.y)!); + this.buffer.lines.length = 1; + this.buffer.ydisp = 0; + this.buffer.ybase = 0; + this.buffer.y = 0; + for (let i = 1; i < this.rows; i++) { + this.buffer.lines.push(this.buffer.getBlankLine(DEFAULT_ATTR_DATA)); + } + this._onScroll.fire({ position: this.buffer.ydisp, source: ScrollSource.TERMINAL }); + } + + /** + * Reset terminal. + * Note: Calling this directly from JS is synchronous but does not clear + * input buffers and does not reset the parser, thus the terminal will + * continue to apply pending input data. + * If you need in band reset (synchronous with input data) consider + * using DECSTR (soft reset, CSI ! p) or RIS instead (hard reset, ESC c). + */ + public reset(): void { + /** + * Since _setup handles a full terminal creation, we have to carry forward + * a few things that should not reset. + */ + this.options.rows = this.rows; + this.options.cols = this.cols; + + this._setup(); + super.reset(); + } +} diff --git a/node_modules/xterm/src/headless/Types.d.ts b/node_modules/xterm/src/headless/Types.d.ts new file mode 100644 index 0000000..868e5c1 --- /dev/null +++ b/node_modules/xterm/src/headless/Types.d.ts @@ -0,0 +1,31 @@ +import { IBuffer, IBufferSet } from 'common/buffer/Types'; +import { IEvent } from 'common/EventEmitter'; +import { IFunctionIdentifier, IParams } from 'common/parser/Types'; +import { ICoreTerminal, IDisposable, IMarker, ITerminalOptions } from 'common/Types'; + +export interface ITerminal extends ICoreTerminal { + rows: number; + cols: number; + buffer: IBuffer; + buffers: IBufferSet; + markers: IMarker[]; + // TODO: We should remove options once components adopt optionsService + options: ITerminalOptions; + + onCursorMove: IEvent<void>; + onData: IEvent<string>; + onBinary: IEvent<string>; + onLineFeed: IEvent<void>; + onResize: IEvent<{ cols: number, rows: number }>; + onTitleChange: IEvent<string>; + resize(columns: number, rows: number): void; + addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable; + addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean): IDisposable; + addEscHandler(id: IFunctionIdentifier, callback: () => boolean): IDisposable; + addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable; + addMarker(cursorYOffset: number): IMarker | undefined; + dispose(): void; + clear(): void; + write(data: string | Uint8Array, callback?: () => void): void; + reset(): void; +} diff --git a/node_modules/xterm/src/headless/public/Terminal.ts b/node_modules/xterm/src/headless/public/Terminal.ts new file mode 100644 index 0000000..9aa171a --- /dev/null +++ b/node_modules/xterm/src/headless/public/Terminal.ts @@ -0,0 +1,216 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IEvent } from 'common/EventEmitter'; +import { BufferNamespaceApi } from 'common/public/BufferNamespaceApi'; +import { ParserApi } from 'common/public/ParserApi'; +import { UnicodeApi } from 'common/public/UnicodeApi'; +import { IBufferNamespace as IBufferNamespaceApi, IMarker, IModes, IParser, ITerminalAddon, IUnicodeHandling, Terminal as ITerminalApi } from 'xterm-headless'; +import { Terminal as TerminalCore } from 'headless/Terminal'; +import { AddonManager } from 'common/public/AddonManager'; +import { ITerminalOptions } from 'common/Types'; + +/** + * The set of options that only have an effect when set in the Terminal constructor. + */ +const CONSTRUCTOR_ONLY_OPTIONS = ['cols', 'rows']; + +export class Terminal implements ITerminalApi { + private _core: TerminalCore; + private _addonManager: AddonManager; + private _parser: IParser | undefined; + private _buffer: BufferNamespaceApi | undefined; + private _publicOptions: ITerminalOptions; + + constructor(options?: ITerminalOptions) { + this._core = new TerminalCore(options); + this._addonManager = new AddonManager(); + + this._publicOptions = { ... this._core.options }; + const getter = (propName: string): any => { + return this._core.options[propName]; + }; + const setter = (propName: string, value: any): void => { + this._checkReadonlyOptions(propName); + this._core.options[propName] = value; + }; + + for (const propName in this._core.options) { + Object.defineProperty(this._publicOptions, propName, { + get: () => { + return this._core.options[propName]; + }, + set: (value: any) => { + this._checkReadonlyOptions(propName); + this._core.options[propName] = value; + } + }); + const desc = { + get: getter.bind(this, propName), + set: setter.bind(this, propName) + }; + Object.defineProperty(this._publicOptions, propName, desc); + } + } + + private _checkReadonlyOptions(propName: string): void { + // Throw an error if any constructor only option is modified + // from terminal.options + // Modifications from anywhere else are allowed + if (CONSTRUCTOR_ONLY_OPTIONS.includes(propName)) { + throw new Error(`Option "${propName}" can only be set in the constructor`); + } + } + + private _checkProposedApi(): void { + if (!this._core.optionsService.options.allowProposedApi) { + throw new Error('You must set the allowProposedApi option to true to use proposed API'); + } + } + + public get onBell(): IEvent<void> { return this._core.onBell; } + public get onBinary(): IEvent<string> { return this._core.onBinary; } + public get onCursorMove(): IEvent<void> { return this._core.onCursorMove; } + public get onData(): IEvent<string> { return this._core.onData; } + public get onLineFeed(): IEvent<void> { return this._core.onLineFeed; } + public get onResize(): IEvent<{ cols: number, rows: number }> { return this._core.onResize; } + public get onScroll(): IEvent<number> { return this._core.onScroll; } + public get onTitleChange(): IEvent<string> { return this._core.onTitleChange; } + + public get parser(): IParser { + this._checkProposedApi(); + if (!this._parser) { + this._parser = new ParserApi(this._core); + } + return this._parser; + } + public get unicode(): IUnicodeHandling { + this._checkProposedApi(); + return new UnicodeApi(this._core); + } + public get rows(): number { return this._core.rows; } + public get cols(): number { return this._core.cols; } + public get buffer(): IBufferNamespaceApi { + this._checkProposedApi(); + if (!this._buffer) { + this._buffer = new BufferNamespaceApi(this._core); + } + return this._buffer; + } + public get markers(): ReadonlyArray<IMarker> { + this._checkProposedApi(); + return this._core.markers; + } + public get modes(): IModes { + const m = this._core.coreService.decPrivateModes; + let mouseTrackingMode: 'none' | 'x10' | 'vt200' | 'drag' | 'any' = 'none'; + switch (this._core.coreMouseService.activeProtocol) { + case 'X10': mouseTrackingMode = 'x10'; break; + case 'VT200': mouseTrackingMode = 'vt200'; break; + case 'DRAG': mouseTrackingMode = 'drag'; break; + case 'ANY': mouseTrackingMode = 'any'; break; + } + return { + applicationCursorKeysMode: m.applicationCursorKeys, + applicationKeypadMode: m.applicationKeypad, + bracketedPasteMode: m.bracketedPasteMode, + insertMode: this._core.coreService.modes.insertMode, + mouseTrackingMode: mouseTrackingMode, + originMode: m.origin, + reverseWraparoundMode: m.reverseWraparound, + sendFocusMode: m.sendFocus, + wraparoundMode: m.wraparound + }; + } + public get options(): ITerminalOptions { + return this._publicOptions; + } + public set options(options: ITerminalOptions) { + for (const propName in options) { + this._publicOptions[propName] = options[propName]; + } + } + public resize(columns: number, rows: number): void { + this._verifyIntegers(columns, rows); + this._core.resize(columns, rows); + } + public registerMarker(cursorYOffset: number): IMarker | undefined { + this._checkProposedApi(); + this._verifyIntegers(cursorYOffset); + return this._core.addMarker(cursorYOffset); + } + public addMarker(cursorYOffset: number): IMarker | undefined { + return this.registerMarker(cursorYOffset); + } + public dispose(): void { + this._addonManager.dispose(); + this._core.dispose(); + } + public scrollLines(amount: number): void { + this._verifyIntegers(amount); + this._core.scrollLines(amount); + } + public scrollPages(pageCount: number): void { + this._verifyIntegers(pageCount); + this._core.scrollPages(pageCount); + } + public scrollToTop(): void { + this._core.scrollToTop(); + } + public scrollToBottom(): void { + this._core.scrollToBottom(); + } + public scrollToLine(line: number): void { + this._verifyIntegers(line); + this._core.scrollToLine(line); + } + public clear(): void { + this._core.clear(); + } + public write(data: string | Uint8Array, callback?: () => void): void { + this._core.write(data, callback); + } + public writeUtf8(data: Uint8Array, callback?: () => void): void { + this._core.write(data, callback); + } + public writeln(data: string | Uint8Array, callback?: () => void): void { + this._core.write(data); + this._core.write('\r\n', callback); + } + public getOption(key: 'bellSound' | 'bellStyle' | 'cursorStyle' | 'fontFamily' | 'logLevel' | 'rendererType' | 'termName' | 'wordSeparator'): string; + public getOption(key: 'allowTransparency' | 'altClickMovesCursor' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'disableStdin' | 'macOptionIsMeta' | 'rightClickSelectsWord' | 'popOnBell' | 'visualBell'): boolean; + public getOption(key: 'cols' | 'fontSize' | 'letterSpacing' | 'lineHeight' | 'rows' | 'tabStopWidth' | 'scrollback'): number; + public getOption(key: string): any; + public getOption(key: any): any { + return this._core.optionsService.getOption(key); + } + public setOption(key: 'bellSound' | 'fontFamily' | 'termName' | 'wordSeparator', value: string): void; + public setOption(key: 'fontWeight' | 'fontWeightBold', value: 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900' | number): void; + public setOption(key: 'logLevel', value: 'debug' | 'info' | 'warn' | 'error' | 'off'): void; + public setOption(key: 'bellStyle', value: 'none' | 'visual' | 'sound' | 'both'): void; + public setOption(key: 'cursorStyle', value: 'block' | 'underline' | 'bar'): void; + public setOption(key: 'allowTransparency' | 'altClickMovesCursor' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'disableStdin' | 'macOptionIsMeta' | 'rightClickSelectsWord' | 'popOnBell' | 'visualBell', value: boolean): void; + public setOption(key: 'fontSize' | 'letterSpacing' | 'lineHeight' | 'tabStopWidth' | 'scrollback', value: number): void; + public setOption(key: 'cols' | 'rows', value: number): void; + public setOption(key: string, value: any): void; + public setOption(key: any, value: any): void { + this._core.optionsService.setOption(key, value); + } + public reset(): void { + this._core.reset(); + } + public loadAddon(addon: ITerminalAddon): void { + // TODO: This could cause issues if the addon calls renderer apis + return this._addonManager.loadAddon(this as any, addon); + } + + private _verifyIntegers(...values: number[]): void { + for (const value of values) { + if (value === Infinity || isNaN(value) || value % 1 !== 0) { + throw new Error('This API only accepts integers'); + } + } + } +} |