From aaaec75d7361907daf549c35f646a343d8efee78 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 26 Apr 2018 13:54:20 -0700 Subject: [PATCH 01/27] Add remaining naming conventions into tslint --- src/AccessibilityManager.ts | 2 +- src/Buffer.ts | 4 +- src/CharWidth.ts | 4 +- src/InputHandler.ts | 6 +-- src/Linkifier.test.ts | 6 +-- src/Linkifier.ts | 2 +- src/Terminal.integration.ts | 12 +++--- src/Terminal.ts | 77 +++++++++++++++++-------------------- src/addons/zmodem/zmodem.ts | 6 +-- tslint.json | 11 +++++- 10 files changed, 67 insertions(+), 63 deletions(-) diff --git a/src/AccessibilityManager.ts b/src/AccessibilityManager.ts index a7e205e3..ea300767 100644 --- a/src/AccessibilityManager.ts +++ b/src/AccessibilityManager.ts @@ -179,7 +179,7 @@ export class AccessibilityManager implements IDisposable { this._refreshRowsDimensions(); } - public _createAccessibilityTreeNode(): HTMLElement { + private _createAccessibilityTreeNode(): HTMLElement { const element = document.createElement('div'); element.setAttribute('role', 'listitem'); element.tabIndex = -1; diff --git a/src/Buffer.ts b/src/Buffer.ts index 1ea303b2..89a32586 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -323,9 +323,9 @@ export class Buffer implements IBuffer { } export class Marker extends EventEmitter implements IMarker { - private static NEXT_ID = 1; + private static _nextId = 1; - private _id: number = Marker.NEXT_ID++; + private _id: number = Marker._nextId++; public isDisposed: boolean = false; public disposables: IDisposable[] = []; diff --git a/src/CharWidth.ts b/src/CharWidth.ts index 90673b2b..4ab6a32f 100644 --- a/src/CharWidth.ts +++ b/src/CharWidth.ts @@ -121,7 +121,7 @@ export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: nu } const control = opts.control | 0; let table: number[] | Uint32Array = null; - function init_table(): number[] | Uint32Array { + function initTable(): number[] | Uint32Array { // lookup table for BMP const CODEPOINTS = 65536; // BMP holds 65536 codepoints const BITWIDTH = 2; // a codepoint can have a width of 0, 1 or 2 @@ -161,7 +161,7 @@ export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: nu if (num < 127) { return 1; } - let t = table || init_table(); + let t = table || initTable(); if (num < 65536) { return t[num >> 4] >> ((num & 15) << 1) & 3; } diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 4df3e695..01c1df4c 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -622,7 +622,7 @@ export class InputHandler implements IInputHandler { * [columns] (default = [row,col+1]) (HPR) * reuse CSI Ps C ? */ - public HPositionRelative(params: number[]): void { + public hPositionRelative(params: number[]): void { let param = params[0]; if (param < 1) { param = 1; @@ -736,7 +736,7 @@ export class InputHandler implements IInputHandler { * [rows] (default = [row+1,column]) * reuse CSI Ps B ? */ - public VPositionRelative(params: number[]): void { + public vPositionRelative(params: number[]): void { let param = params[0]; if (param < 1) { param = 1; @@ -756,7 +756,7 @@ export class InputHandler implements IInputHandler { * Horizontal and Vertical Position [row;column] (default = * [1,1]) (HVP). */ - public HVPosition(params: number[]): void { + public hVPosition(params: number[]): void { if (params[0] < 1) params[0] = 1; if (params[1] < 1) params[1] = 1; diff --git a/src/Linkifier.test.ts b/src/Linkifier.test.ts index 61245296..a96f7466 100644 --- a/src/Linkifier.test.ts +++ b/src/Linkifier.test.ts @@ -11,9 +11,9 @@ import { MockBuffer, MockTerminal } from './utils/TestUtils.test'; import { CircularList } from './utils/CircularList'; class TestLinkifier extends Linkifier { - constructor(_terminal: ITerminal) { - super(_terminal); - Linkifier.TIME_BEFORE_LINKIFY = 0; + constructor(terminal: ITerminal) { + super(terminal); + (Linkifier).TIME_BEFORE_LINKIFY = 0; } public get linkMatchers(): ILinkMatcher[] { return this._linkMatchers; } diff --git a/src/Linkifier.ts b/src/Linkifier.ts index 2a19c8d8..869812c0 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -17,7 +17,7 @@ export class Linkifier extends EventEmitter implements ILinkifier { * the costly operation of searching every row multiple times, potentially a * huge amount of times. */ - protected static TIME_BEFORE_LINKIFY = 200; + protected static readonly TIME_BEFORE_LINKIFY = 200; protected _linkMatchers: ILinkMatcher[] = []; diff --git a/src/Terminal.integration.ts b/src/Terminal.integration.ts index fdf27acf..f986121c 100644 --- a/src/Terminal.integration.ts +++ b/src/Terminal.integration.ts @@ -74,23 +74,23 @@ function terminalToString(term: Terminal): string { // Skip tests on Windows since pty.open isn't supported if (os.platform() !== 'win32') { - let CONSOLE_LOG = console.log; + const consoleLog = console.log; // expect files need terminal at 80x25! - let COLS = 80; - let ROWS = 25; + const cols = 80; + const rows = 25; /** some helpers for pty interaction */ // we need a pty in between to get the termios decorations // for the basic test cases a raw pty device is enough - primitivePty = pty.native.open(COLS, ROWS); + primitivePty = pty.native.open(cols, rows); /** tests */ describe('xterm output comparison', () => { let xterm; beforeEach(() => { - xterm = new Terminal({ cols: COLS, rows: ROWS }); + xterm = new Terminal({ cols: cols, rows: rows }); xterm.refresh = () => {}; xterm.viewport = { syncScrollArea: () => {} @@ -128,7 +128,7 @@ if (os.platform() !== 'win32') { xterm._innerWrite(); let fromEmulator = terminalToString(xterm); - console.log = CONSOLE_LOG; + console.log = consoleLog; let expected = fs.readFileSync(filename.split('.')[0] + '.text', 'utf8'); // Some of the tests have whitespace on the right of lines, we trim all the linex // from xterm.js so ignore this for now at least. diff --git a/src/Terminal.ts b/src/Terminal.ts index ba1fff3d..ccf5a6fd 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -2210,7 +2210,42 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II // TODO: Remove when true color is implemented public matchColor(r1: number, g1: number, b1: number): number { - return matchColor_(r1, g1, b1); + const hash = (r1 << 16) | (g1 << 8) | b1; + + if (matchColorCache[hash] != null) { + return matchColorCache[hash]; + } + + let ldiff = Infinity; + let li = -1; + let i = 0; + let c: number; + let r2: number; + let g2: number; + let b2: number; + let diff: number; + + for (; i < DEFAULT_ANSI_COLORS.length; i++) { + c = DEFAULT_ANSI_COLORS[i].rgba; + r2 = c >>> 24; + g2 = c >>> 16 & 0xFF; + b2 = c >>> 8 & 0xFF; + // assume that alpha is 0xFF + + diff = matchColorDistance(r1, g1, b1, r2, g2, b2); + + if (diff === 0) { + li = i; + break; + } + + if (diff < ldiff) { + ldiff = diff; + li = i; + } + } + + return matchColorCache[hash] = li; } private _visualBell(): boolean { @@ -2266,43 +2301,3 @@ function matchColorDistance(r1: number, g1: number, b1: number, r2: number, g2: + Math.pow(59 * (g1 - g2), 2) + Math.pow(11 * (b1 - b2), 2); } - - -function matchColor_(r1: number, g1: number, b1: number): number { - const hash = (r1 << 16) | (g1 << 8) | b1; - - if (matchColorCache[hash] != null) { - return matchColorCache[hash]; - } - - let ldiff = Infinity; - let li = -1; - let i = 0; - let c: number; - let r2: number; - let g2: number; - let b2: number; - let diff: number; - - for (; i < DEFAULT_ANSI_COLORS.length; i++) { - c = DEFAULT_ANSI_COLORS[i].rgba; - r2 = c >>> 24; - g2 = c >>> 16 & 0xFF; - b2 = c >>> 8 & 0xFF; - // assume that alpha is 0xFF - - diff = matchColorDistance(r1, g1, b1, r2, g2, b2); - - if (diff === 0) { - li = i; - break; - } - - if (diff < ldiff) { - ldiff = diff; - li = i; - } - } - - return matchColorCache[hash] = li; -} diff --git a/src/addons/zmodem/zmodem.ts b/src/addons/zmodem/zmodem.ts index 4f2f4a91..d5921d92 100644 --- a/src/addons/zmodem/zmodem.ts +++ b/src/addons/zmodem/zmodem.ts @@ -48,13 +48,13 @@ function zmodemAttach(ws: WebSocket, opts: IZmodemOptions = {}): void { let zsentry; - function _shouldWrite(): boolean { + function shouldWrite(): boolean { return !!zsentry.get_confirmed_session() || !opts.noTerminalWriteOutsideSession; } zsentry = new zmodem.Sentry({ to_terminal: (octets: ArrayLike) => { - if (_shouldWrite()) { + if (shouldWrite()) { term.write( String.fromCharCode.apply(String, octets) ); @@ -72,7 +72,7 @@ function zmodemAttach(ws: WebSocket, opts: IZmodemOptions = {}): void { // may be specific to xterm.js’s demo, ultimately we // should reject anything that isn’t binary. if (typeof evt.data === 'string') { - if (_shouldWrite()) { + if (shouldWrite()) { term.write(evt.data); } } diff --git a/tslint.json b/tslint.json index ac1b9c95..88106926 100644 --- a/tslint.json +++ b/tslint.json @@ -93,7 +93,16 @@ "naming-convention": [ true, - {"type": "property", "modifiers": ["public", "static", "const"], "format": "UPPER_CASE"} + {"type": "default", "format": "camelCase", "leadingUnderscore": "forbid"}, + {"type": "type", "format": "PascalCase"}, + {"type": "class", "format": "PascalCase"}, + {"type": "property", "modifiers": ["const"], "format": "UPPER_CASE"}, + {"type": "member", "modifiers": ["protected"], "format": "camelCase", "leadingUnderscore": "allow"}, + // TODO: Change allow to require when there aren't many PRs out + // {"type": "member", "modifiers": ["protected"], "format": "camelCase", "leadingUnderscore": "require"}, + {"type": "member", "modifiers": ["private"], "format": "camelCase", "leadingUnderscore": "require"}, + {"type": "variable", "modifiers": ["const"], "format": ["camelCase", "UPPER_CASE"]}, + {"type": "interface", "prefix": "I"} ], "no-else-after-return": { "options": "allow-else-if" From 16393b228dc66f655b16f6b34469396c2865659f Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 2 May 2018 13:32:22 -0700 Subject: [PATCH 02/27] Initial DOM renderer implementation --- src/Terminal.ts | 10 +- src/Viewport.ts | 1 + src/renderer/Renderer.ts | 1 + src/renderer/dom/DomRenderer.ts | 224 ++++++++++++++++++++++++++++++++ typings/xterm.d.ts | 10 ++ 5 files changed, 244 insertions(+), 2 deletions(-) create mode 100644 src/renderer/dom/DomRenderer.ts diff --git a/src/Terminal.ts b/src/Terminal.ts index ba1fff3d..ce0b32a8 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -49,6 +49,7 @@ import { AccessibilityManager } from './AccessibilityManager'; import { ScreenDprMonitor } from './utils/ScreenDprMonitor'; import { ITheme, ILocalizableStrings, IMarker, IDisposable } from 'xterm'; import { removeTerminalFromCache } from './renderer/atlas/CharAtlas'; +import { DomRenderer } from './renderer/dom/DomRenderer'; // reg + shift key mappings for digits and special chars const KEYCODE_KEY_MAPPINGS = { @@ -121,7 +122,8 @@ const DEFAULT_OPTIONS: ITerminalOptions = { allowTransparency: false, tabStopWidth: 8, theme: null, - rightClickSelectsWord: Browser.isMac + rightClickSelectsWord: Browser.isMac, + rendererType: 'canvas' }; export class Terminal extends EventEmitter implements ITerminal, IDisposable, IInputHandlingTerminal { @@ -703,7 +705,11 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II // Performance: Add viewport and helper elements from the fragment this.element.appendChild(fragment); - this.renderer = new Renderer(this, this.options.theme); + if (this.options.rendererType === 'canvas') { + this.renderer = new Renderer(this, this.options.theme); + } else { + this.renderer = new DomRenderer(this, this.options.theme); + } this.options.theme = null; this.viewport = new Viewport(this, this._viewportElement, this._viewportScrollArea, this.charMeasure); this.viewport.onThemeChanged(this.renderer.colorManager.colors); diff --git a/src/Viewport.ts b/src/Viewport.ts index c951d6b5..433e267e 100644 --- a/src/Viewport.ts +++ b/src/Viewport.ts @@ -64,6 +64,7 @@ export class Viewport implements IViewport { const newBufferHeight = Math.round(this._currentRowHeight * this._lastRecordedBufferLength) + (this._lastRecordedViewportHeight - this._terminal.renderer.dimensions.canvasHeight); if (this._lastRecordedBufferHeight !== newBufferHeight) { this._lastRecordedBufferHeight = newBufferHeight; + console.log('set scroll area height to ', this._currentRowHeight, this._lastRecordedBufferLength, this._lastRecordedBufferHeight); this._scrollArea.style.height = this._lastRecordedBufferHeight + 'px'; } } diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index 9404a461..9db1bdc5 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -15,6 +15,7 @@ import { RenderDebouncer } from '../utils/RenderDebouncer'; import { ScreenDprMonitor } from '../utils/ScreenDprMonitor'; import { ITheme } from 'xterm'; +// TODO: Rename to CanvasRenderer and move this, render layers and atlas into ./src/renderer/canvas export class Renderer extends EventEmitter implements IRenderer { private _renderDebouncer: RenderDebouncer; diff --git a/src/renderer/dom/DomRenderer.ts b/src/renderer/dom/DomRenderer.ts new file mode 100644 index 00000000..3fd3a00b --- /dev/null +++ b/src/renderer/dom/DomRenderer.ts @@ -0,0 +1,224 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IRenderer, IRenderDimensions, IColorSet, FLAGS } from '../Types'; +import { ITerminal } from '../../Types'; +import { ITheme } from 'xterm'; +import { EventEmitter } from '../../EventEmitter'; +import { ColorManager } from '../ColorManager'; +import { INVERTED_DEFAULT_COLOR } from '../atlas/Types'; +import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_ATTR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX } from '../../Buffer'; + +const ROW_CONTAINER_CLASS = 'xterm-rows'; + +export class DomRenderer extends EventEmitter implements IRenderer { + public dimensions: IRenderDimensions; + public colorManager: ColorManager; + + private _styleElement: HTMLStyleElement; + private _rowContainer: HTMLElement; + private _rowElements: HTMLElement[] = []; + + // TODO: Theme/ColorManager might be better owned by Terminal not IRenderer to reduce duplication? + constructor(private _terminal: ITerminal, theme: ITheme | undefined) { + super(); + const allowTransparency = this._terminal.options.allowTransparency; + this.colorManager = new ColorManager(document, allowTransparency); + this.setTheme(theme); + + this._rowContainer = document.createElement('div'); + this._rowContainer.classList.add(ROW_CONTAINER_CLASS); + this._refreshRowElements(this._terminal.rows, this._terminal.cols); + + // TODO: Should IRendererDimensions lose canvas-related concepts? + this.dimensions = { + scaledCharWidth: null, + scaledCharHeight: null, + scaledCellWidth: null, + scaledCellHeight: null, + scaledCharLeft: null, + scaledCharTop: null, + scaledCanvasWidth: null, + scaledCanvasHeight: null, + canvasWidth: null, + canvasHeight: null, + actualCellWidth: null, + actualCellHeight: null + }; + this._updateDimensions(); + + this._terminal.screenElement.appendChild(this._rowContainer); + } + + private _updateDimensions(): void { + this.dimensions.scaledCharWidth = this._terminal.charMeasure.width * window.devicePixelRatio; + this.dimensions.scaledCharHeight = this._terminal.charMeasure.height * window.devicePixelRatio; + this.dimensions.scaledCellWidth = this._terminal.charMeasure.width * window.devicePixelRatio; + this.dimensions.scaledCellHeight = this._terminal.charMeasure.height * window.devicePixelRatio; + // TODO: Support line height and letter spacing + this.dimensions.scaledCharLeft = 0; + this.dimensions.scaledCharTop = 0; + this.dimensions.scaledCanvasWidth = this.dimensions.scaledCellWidth * this._terminal.cols; + this.dimensions.scaledCanvasHeight = this.dimensions.scaledCellHeight * this._terminal.rows; + this.dimensions.canvasWidth = this._terminal.charMeasure.width * this._terminal.cols; + this.dimensions.canvasHeight = this._terminal.charMeasure.height * this._terminal.rows; + this.dimensions.actualCellWidth = this._terminal.charMeasure.width; + this.dimensions.actualCellHeight = this._terminal.charMeasure.height; + + this._rowElements.forEach(element => { + element.style.width = `${this.dimensions.canvasWidth}px`; + element.style.height = `${this._terminal.charMeasure.height}px`; + }); + + console.log('_updateDimensions', this.dimensions); + } + + public setTheme(theme: ITheme | undefined): IColorSet { + console.log('setTheme'); + if (theme) { + this.colorManager.setTheme(theme); + } + + // TODO: CSS selectors would need to use some ID otherwise it will affect other terminals + this._styleElement = document.createElement('style'); + let styles = + `.xterm .${ROW_CONTAINER_CLASS} {` + + ` color: ${this.colorManager.colors.foreground.css};` + + ` background-color: ${this.colorManager.colors.background.css};` + + `}` + + `.xterm .${ROW_CONTAINER_CLASS} span {` + + ` display: inline-block;` + + `}`; + this.colorManager.colors.ansi.forEach((c, i) => { + styles += + `.xterm .xterm-fg-${i} { color: ${c.css}; }` + + `.xterm .xterm-bg-${i} { background-color: ${c.css}; }`; + }); + this._styleElement.innerHTML = styles; + this._terminal.screenElement.appendChild(this._styleElement); + return this.colorManager.colors; + } + + public onWindowResize(devicePixelRatio: number): void { + console.log('onWindowResize', arguments); + } + + private _refreshRowElements(cols: number, rows: number): void { + console.log('resize', cols, rows); + // 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()); + } + // console.log('refresh row elements', rows, this._rowElements.length); + } + + public onResize(cols: number, rows: number): void { + console.log('onResize', arguments); + this._refreshRowElements(cols, rows); + this._updateDimensions(); + } + + // TODO: onCharSizeChanged is no longer called :'( + public onCharSizeChanged(): void { + this._updateDimensions(); + } + + public onBlur(): void { + console.log('onBlur', arguments); + } + + public onFocus(): void { + console.log('onFocus', arguments); + } + + public onSelectionChanged(start: [number, number], end: [number, number]): void { + console.log('onSelectionChanged', arguments); + } + + public onCursorMove(): void { + console.log('onCursorMove', arguments); + } + + public onOptionsChanged(): void { + console.log('onOptionsChanged', arguments); + } + + public clear(): void { + console.log('clear', arguments); + this._rowElements.forEach(e => e.innerHTML = ''); + } + + public refreshRows(start: number, end: number): void { + console.log('refreshRows', arguments); + const terminal = this._terminal; + + for (let y = start; y <= end; y++) { + const rowElement = this._rowElements[y]; + rowElement.innerHTML = ''; + + const row = y + terminal.buffer.ydisp; + const line = terminal.buffer.lines.get(row); + for (let x = 0; x < terminal.cols; x++) { + const charData = line[x]; + const code: number = charData[CHAR_DATA_CODE_INDEX]; + const char: string = charData[CHAR_DATA_CHAR_INDEX]; + const attr: number = charData[CHAR_DATA_ATTR_INDEX]; + let width: number = charData[CHAR_DATA_WIDTH_INDEX]; + + // The character to the left is a wide character, drawing is owned by + // the char at x-1 + if (width === 0) { + continue; + } + + const charElement = document.createElement('span'); + // TODO: Move standard width to