diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 000000000..4d4e605b9 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,22 @@ +FROM node:10 + +# Configure apt +ENV DEBIAN_FRONTEND=noninteractive +RUN apt-get update \ + && apt-get -y install --no-install-recommends apt-utils 2>&1 + +# Verify git and process tools are installed +RUN apt-get install -y git procps + +# Install yarn +RUN apt-get install -y curl apt-transport-https lsb-release \ + && curl -sS https://dl.yarnpkg.com/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/pubkey.gpg | apt-key add - 2>/dev/null \ + && echo "deb https://dl.yarnpkg.com/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/ stable main" | tee /etc/apt/sources.list.d/yarn.list \ + && apt-get update \ + && apt-get -y install --no-install-recommends yarn + +# Clean up +RUN apt-get autoremove -y \ + && apt-get clean -y \ + && rm -rf /var/lib/apt/lists/* +ENV DEBIAN_FRONTEND=dialog diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..b00a26936 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,9 @@ +{ + "name": "xterm.js", + "dockerFile": "Dockerfile", + "appPort": 3000, + "extensions": [ + "editorconfig.editorconfig", + "ms-vscode.vscode-typescript-tslint-plugin" + ] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 000000000..2f752faf0 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,51 @@ +{ + "version": "2.0.0", + "presentation": { + "echo": false, + "reveal": "always", + "focus": false, + "panel": "dedicated", + "showReuseMessage": true + }, + "tasks": [ + { + "type": "npm", + "script": "test", + "group": "test", + "problemMatcher": [] + }, + { + "type": "npm", + "script": "watch", + "group": "build", + "isBackground": true, + "problemMatcher": [], + "presentation": { + "group": "vscode" + } + }, + { + "type": "npm", + "script": "start", + "group": "build", + "isBackground": true, + "problemMatcher": [], + "presentation": { + "group": "vscode" + } + }, + { + "label": "Start demo", + "dependsOn": ["npm: watch", "npm: start"], + "group": { + "kind": "build", + "isDefault": true + }, + "isBackground": true, + "problemMatcher": [], + "presentation": { + "group": "vscode" + } + } + ] +} diff --git a/README.md b/README.md index b2e9f02f2..83170eff5 100644 --- a/README.md +++ b/README.md @@ -156,7 +156,8 @@ Xterm.js is used in several world-class applications to provide great terminal e - [**Jumpserver**](https://github.com/jumpserver/luna): Jumpserver Luna project, Jumpserver is a bastion server project, Luna use xterm.js for web terminal emulation. - [**LxdMosaic**](https://github.com/turtle0x1/LxdMosaic): Uses xterm.js to give terminal access to containers through LXD - [**CodeInterview.io**](https://codeinterview.io): A coding interview platform in 25+ languages and many web frameworks. Uses xterm.js to provide shell access. -- [**Bastillion**](https://www.bastillion.io): Bastillion is an open-source web-based SSH console that centrally manages administrative access to systems. +- [**Bastillion**](https://www.bastillion.io): Bastillion is an open-source web-based SSH console that centrally manages administrative access to systems. +- [**PHP App Server**](https://github.com/cubiclesoft/php-app-server/): Create lightweight, installable almost-native applications for desktop OSes. ExecTerminal (nicely wraps the xterm.js Terminal), TerminalManager, and RunProcessSDK are self-contained, reusable ES5+ compliant Javascript components. [And much more...](https://github.com/xtermjs/xterm.js/network/dependents) @@ -186,6 +187,6 @@ You can read the [guide on the wiki](https://github.com/xtermjs/xterm.js/wiki/Co If you contribute code to this project, you are implicitly allowing your code to be distributed under the MIT license. You are also implicitly verifying that all code is your original work. -Copyright (c) 2017-2018, [The xterm.js authors](https://github.com/xtermjs/xterm.js/graphs/contributors) (MIT License)
+Copyright (c) 2017-2019, [The xterm.js authors](https://github.com/xtermjs/xterm.js/graphs/contributors) (MIT License)
Copyright (c) 2014-2017, SourceLair, Private Company ([www.sourcelair.com](https://www.sourcelair.com/home)) (MIT License)
Copyright (c) 2012-2013, Christopher Jeffrey (MIT License) diff --git a/bin/publish.js b/bin/publish.js index ec7bf173d..9df2e982d 100644 --- a/bin/publish.js +++ b/bin/publish.js @@ -11,9 +11,12 @@ const packageJson = require('../package.json'); // Setup auth fs.writeFileSync(`${process.env['HOME']}/.npmrc`, `//registry.npmjs.org/:_authToken=${process.env['NPM_AUTH_TOKEN']}`); -// Get the version -const tag = 'beta' -const nextVersion = getNextVersion(tag); +// Determine if this is a stable or beta release +const publishedVersions = getPublishedVersions(); +const isStableRelease = publishedVersions.indexOf(packageJson.version) === -1; + +// Get the next version +let nextVersion = isStableRelease ? packageJson.version : getNextBetaVersion(); console.log(`Publishing version: ${nextVersion}`); // Set the version in package.json @@ -22,29 +25,39 @@ packageJson.version = nextVersion; fs.writeFileSync(packageJsonFile, JSON.stringify(packageJson, null, 2)); // Publish -const result = cp.spawn('npm', ['publish', '--tag', tag], { - stdio: 'inherit' -}); +const args = ['publish']; +if (!isStableRelease) { + args.push('--tag', 'beta'); +} +const result = cp.spawn('npm', args, { stdio: 'inherit' }); result.on('exit', code => process.exit(code)); -function getNextVersion(tag) { +function getNextBetaVersion() { if (!/^[0-9]+\.[0-9]+\.[0-9]+$/.exec(packageJson.version)) { console.error('The package.json version must be of the form x.y.z'); process.exit(1); } + const tag = 'beta'; const stableVersion = packageJson.version.split('.'); - const nextStableVersion = `${stableVersion[0]}.${parseInt(stableVersion[1]) + 1}.${stableVersion[2]}`; + const nextStableVersion = `${stableVersion[0]}.${parseInt(stableVersion[1]) + 1}.0`; const publishedVersions = getPublishedVersions(nextStableVersion, tag); if (publishedVersions.length === 0) { - return `${packageJson.version}-${tag}1`; + return `${nextStableVersion}-${tag}1`; } - const latestPublishedVersion = publishedVersions.sort((a, b) => b.localeCompare(a))[0]; + const latestPublishedVersion = publishedVersions.sort((a, b) => { + const aVersion = parseInt(a.substr(a.search(/[0-9]+$/))); + const bVersion = parseInt(b.substr(b.search(/[0-9]+$/))); + return aVersion > bVersion ? -1 : 1; + })[0]; const latestTagVersion = parseInt(latestPublishedVersion.substr(latestPublishedVersion.search(/[0-9]+$/)), 10); return `${nextStableVersion}-${tag}${latestTagVersion + 1}`; } function getPublishedVersions(version, tag) { - const versionsProcess = cp.spawnSync('npm', ['view', 'xterm', 'versions', '--json']); + const versionsProcess = cp.spawnSync('npm', ['view', packageJson.name, 'versions', '--json']); const versionsJson = JSON.parse(versionsProcess.stdout); - return versionsJson.filter(v => !v.search(new RegExp(`${version}-${tag}[0-9]+`))); + if (tag) { + return versionsJson.filter(v => !v.search(new RegExp(`${version}-${tag}[0-9]+`))); + } + return versionsJson; } diff --git a/demo/client.ts b/demo/client.ts index 9179486c2..83e4a2ce1 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -260,8 +260,11 @@ function initOptions(term: TerminalType): void { console.log('change', o, input.value); if (o === 'cols' || o === 'rows') { updateTerminalSize(); + } else if (o === 'lineHeight') { + term.setOption(o, parseFloat(input.value)); + updateTerminalSize(); } else { - term.setOption(o, o === 'lineHeight' ? parseFloat(input.value) : parseInt(input.value, 10)); + term.setOption(o, parseInt(input.value)); } }); }); diff --git a/fixtures/typings-test/typings-test.ts b/fixtures/typings-test/typings-test.ts index 87d911c2a..65c478c61 100644 --- a/fixtures/typings-test/typings-test.ts +++ b/fixtures/typings-test/typings-test.ts @@ -21,7 +21,7 @@ namespace constructor { 'disableStdin': false, 'rows': 1, 'scrollback': 10, - 'tabStopWidth': 2, + 'tabStopWidth': 2 }); } } @@ -119,8 +119,8 @@ namespace methods_core { const t: Terminal = new Terminal(); t.attachCustomKeyEventHandler((e: KeyboardEvent) => true); t.attachCustomKeyEventHandler((e: KeyboardEvent) => false); - const d1: IDisposable = t.addCsiHandler("x", - (params: number[], collect: string): boolean => params[0]===1); + const d1: IDisposable = t.addCsiHandler('x', + (params: number[], collect: string): boolean => params[0] === 1); d1.dispose(); const d2: IDisposable = t.addOscHandler(199, (data: string): boolean => true); @@ -155,6 +155,7 @@ namespace methods_core { const r25: string = t.getOption('fontWeightBold'); const r26: boolean = t.getOption('allowTransparency'); const r27: boolean = t.getOption('rightClickSelectsWord'); + const r28: boolean = t.getOption('windowsMode'); } { const t: Terminal = new Terminal(); @@ -177,6 +178,7 @@ namespace methods_core { t.setOption('useFlowControl', true); t.setOption('allowTransparency', true); t.setOption('visualBell', true); + t.setOption('windowsMode', true); t.setOption('colors', ['a', 'b']); t.setOption('letterSpacing', 1); t.setOption('cols', 1); diff --git a/package.json b/package.json index 7f4f129bf..4e55940ec 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "xterm", "description": "Full xterm terminal, in your browser", - "version": "3.12.0", + "version": "3.13.0", "main": "lib/public/Terminal.js", "types": "typings/xterm.d.ts", "repository": "https://github.com/xtermjs/xterm.js", @@ -59,8 +59,9 @@ "mocha": "gulp test", "prebuild": "tsc -b ./src/tsconfig.all.json", "build": "gulp build", - "prepublish": "npm run build", + "prepare": "npm run prebuild", + "prepublishOnly": "npm run build", "coveralls": "nyc report --reporter=text-lcov | coveralls", "watch": "tsc -b -w ./src/tsconfig.all.json --preserveWatchOutput" } -} \ No newline at end of file +} diff --git a/src/AccessibilityManager.ts b/src/AccessibilityManager.ts index e2115797f..1f44c1321 100644 --- a/src/AccessibilityManager.ts +++ b/src/AccessibilityManager.ts @@ -61,7 +61,7 @@ export class AccessibilityManager extends Disposable { this._refreshRowsDimensions(); this._accessibilityTreeRoot.appendChild(this._rowContainer); - this._renderRowsDebouncer = new RenderDebouncer(this._terminal, this._renderRows.bind(this)); + this._renderRowsDebouncer = new RenderDebouncer(this._renderRows.bind(this)); this._refreshRows(); this._liveRegion = document.createElement('div'); @@ -239,7 +239,7 @@ export class AccessibilityManager extends Disposable { } private _refreshRows(start?: number, end?: number): void { - this._renderRowsDebouncer.refresh(start, end); + this._renderRowsDebouncer.refresh(start, end, this._terminal.rows); } private _renderRows(start: number, end: number): void { diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index d5a27bf1a..087d0ae52 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -5,10 +5,10 @@ import { assert, expect } from 'chai'; import { ITerminal } from './Types'; -import { Buffer, DEFAULT_ATTR_DATA } from './Buffer'; +import { Buffer } from './Buffer'; import { CircularList } from './common/CircularList'; -import { MockTerminal, TestTerminal } from './ui/TestUtils.test'; -import { BufferLine, CellData } from './BufferLine'; +import { MockTerminal, TestTerminal } from './TestUtils.test'; +import { BufferLine, CellData, DEFAULT_ATTR_DATA } from './core/buffer/BufferLine'; const INIT_COLS = 80; const INIT_ROWS = 24; diff --git a/src/Buffer.ts b/src/Buffer.ts index c1c08c85a..d9117fdae 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -4,42 +4,16 @@ */ import { CircularList, IInsertEvent } from './common/CircularList'; -import { ITerminal, IBuffer, IBufferLine, BufferIndex, IBufferStringIterator, IBufferStringIteratorResult, ICellData, IAttributeData } from './Types'; +import { ITerminal, IBuffer, BufferIndex, IBufferStringIterator, IBufferStringIteratorResult } from './Types'; +import { IBufferLine, ICellData, IAttributeData } from './core/Types'; import { IMarker } from 'xterm'; -import { BufferLine, CellData, AttributeData } from './BufferLine'; +import { BufferLine, CellData, 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, DEFAULT_ATTR_DATA } from './core/buffer/BufferLine'; import { reflowLargerApplyNewLayout, reflowLargerCreateNewLayout, reflowLargerGetLinesToRemove, reflowSmallerGetNewLineLengths, getWrappedLineTrimmedLength } from './BufferReflow'; -import { DEFAULT_COLOR } from './renderer/atlas/Types'; import { EventEmitter2, IEvent } from './common/EventEmitter2'; -import { Disposable } from '../lib/common/Lifecycle'; +import { Disposable } from './common/Lifecycle'; -export const DEFAULT_ATTR = (0 << 18) | (DEFAULT_COLOR << 9) | (256 << 0); - -export const DEFAULT_ATTR_DATA = new AttributeData(); - -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; export const MAX_BUFFER_SIZE = 4294967295; // 2^32 - 1 -/** - * 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; - /** * This class represents a terminal buffer (an internal state of the terminal), where the * following information is stored (in high-level): diff --git a/src/BufferReflow.test.ts b/src/BufferReflow.test.ts index 9c978dc0a..91454e2aa 100644 --- a/src/BufferReflow.test.ts +++ b/src/BufferReflow.test.ts @@ -3,9 +3,8 @@ * @license MIT */ import { assert } from 'chai'; -import { BufferLine } from './BufferLine'; +import { BufferLine, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE } from './core/buffer/BufferLine'; import { reflowSmallerGetNewLineLengths } from './BufferReflow'; -import { NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE } from './Buffer'; describe('BufferReflow', () => { describe('reflowSmallerGetNewLineLengths', () => { diff --git a/src/BufferReflow.ts b/src/BufferReflow.ts index 40e16c744..c10689671 100644 --- a/src/BufferReflow.ts +++ b/src/BufferReflow.ts @@ -3,9 +3,9 @@ * @license MIT */ -import { BufferLine } from './BufferLine'; +import { BufferLine } from './core/buffer/BufferLine'; import { CircularList } from './common/CircularList'; -import { IBufferLine, ICellData } from './Types'; +import { IBufferLine, ICellData } from './core/Types'; export interface INewLayoutResult { layout: number[]; diff --git a/src/BufferSet.test.ts b/src/BufferSet.test.ts index 576a8ca47..cdc220b3f 100644 --- a/src/BufferSet.test.ts +++ b/src/BufferSet.test.ts @@ -7,7 +7,7 @@ import { assert } from 'chai'; import { ITerminal } from './Types'; import { BufferSet } from './BufferSet'; import { Buffer } from './Buffer'; -import { MockTerminal } from './ui/TestUtils.test'; +import { MockTerminal } from './TestUtils.test'; describe('BufferSet', () => { let terminal: ITerminal; diff --git a/src/BufferSet.ts b/src/BufferSet.ts index ba885a0e9..f22b92dca 100644 --- a/src/BufferSet.ts +++ b/src/BufferSet.ts @@ -3,7 +3,8 @@ * @license MIT */ -import { ITerminal, IBufferSet, IAttributeData, IBuffer } from './Types'; +import { ITerminal, IBufferSet, IBuffer } from './Types'; +import { IAttributeData } from './core/Types'; import { Buffer } from './Buffer'; import { EventEmitter2, IEvent } from './common/EventEmitter2'; diff --git a/src/ui/CharMeasure.test.ts b/src/CharMeasure.test.ts similarity index 97% rename from src/ui/CharMeasure.test.ts rename to src/CharMeasure.test.ts index a3cb3b3b4..5fd17eb24 100644 --- a/src/ui/CharMeasure.test.ts +++ b/src/CharMeasure.test.ts @@ -4,7 +4,7 @@ */ import jsdom = require('jsdom'); -import { ICharMeasure } from '../Types'; +import { ICharMeasure } from './Types'; import { assert } from 'chai'; import { CharMeasure } from './CharMeasure'; diff --git a/src/ui/CharMeasure.ts b/src/CharMeasure.ts similarity index 93% rename from src/ui/CharMeasure.ts rename to src/CharMeasure.ts index c37df312a..efee89729 100644 --- a/src/ui/CharMeasure.ts +++ b/src/CharMeasure.ts @@ -3,8 +3,8 @@ * @license MIT */ -import { ICharMeasure, ITerminalOptions } from '../Types'; -import { EventEmitter2, IEvent } from '../common/EventEmitter2'; +import { ICharMeasure, ITerminalOptions } from './Types'; +import { EventEmitter2, IEvent } from './common/EventEmitter2'; /** * Utility class that measures the size of a character. Measurements are done in diff --git a/src/CharWidth.test.ts b/src/CharWidth.test.ts index 8608c6fa3..17822fa28 100644 --- a/src/CharWidth.test.ts +++ b/src/CharWidth.test.ts @@ -3,12 +3,11 @@ * @license MIT */ -import { TestTerminal } from './ui/TestUtils.test'; +import { TestTerminal } from './TestUtils.test'; import { assert } from 'chai'; import { getStringCellWidth, wcwidth } from './CharWidth'; import { IBuffer } from './Types'; -import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from './Buffer'; -import { CellData } from './BufferLine'; +import { CellData, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from './core/buffer/BufferLine'; describe('getStringCellWidth', function(): void { diff --git a/src/ui/Clipboard.test.ts b/src/Clipboard.test.ts similarity index 100% rename from src/ui/Clipboard.test.ts rename to src/Clipboard.test.ts diff --git a/src/ui/Clipboard.ts b/src/Clipboard.ts similarity index 98% rename from src/ui/Clipboard.ts rename to src/Clipboard.ts index 2570a8b17..7a32badcd 100644 --- a/src/ui/Clipboard.ts +++ b/src/Clipboard.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { ITerminal, ISelectionManager } from '../Types'; +import { ITerminal, ISelectionManager } from './Types'; interface IWindow extends Window { clipboardData?: { diff --git a/src/InputHandler.test.ts b/src/InputHandler.test.ts index 01596f808..f2e8d93e9 100644 --- a/src/InputHandler.test.ts +++ b/src/InputHandler.test.ts @@ -5,11 +5,10 @@ import { assert, expect } from 'chai'; import { InputHandler } from './InputHandler'; -import { MockInputHandlingTerminal, TestTerminal } from './ui/TestUtils.test'; -import { DEFAULT_ATTR_DATA } from './Buffer'; +import { MockInputHandlingTerminal, TestTerminal } from './TestUtils.test'; import { Terminal } from './Terminal'; -import { IBufferLine } from './Types'; -import { CellData, Attributes, AttributeData } from './BufferLine'; +import { IBufferLine } from './core/Types'; +import { CellData, Attributes, AttributeData, DEFAULT_ATTR_DATA } from './core/buffer/BufferLine'; describe('InputHandler', () => { describe('save and restore cursor', () => { diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 529123d7c..ac4bee936 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -7,14 +7,13 @@ import { IInputHandler, IDcsHandler, IEscapeSequenceParser, IInputHandlingTerminal } from './Types'; import { C0, C1 } from './common/data/EscapeSequences'; import { CHARSETS, DEFAULT_CHARSET } from './core/data/Charsets'; -import { NULL_CELL_WIDTH, NULL_CELL_CODE, DEFAULT_ATTR_DATA } from './Buffer'; import { wcwidth } from './CharWidth'; import { EscapeSequenceParser } from './EscapeSequenceParser'; import { IDisposable } from 'xterm'; import { Disposable } from './common/Lifecycle'; import { concat } from './common/TypedArrayUtils'; -import { StringToUtf32, stringFromCodePoint, Utf8ToUtf32, utf32ToString } from './core/input/TextDecoder'; -import { CellData, Attributes, FgFlags, BgFlags, AttributeData } from './BufferLine'; +import { StringToUtf32, stringFromCodePoint, utf32ToString, Utf8ToUtf32 } from './core/input/TextDecoder'; +import { CellData, Attributes, FgFlags, BgFlags, AttributeData, NULL_CELL_WIDTH, NULL_CELL_CODE, DEFAULT_ATTR_DATA } from './core/buffer/BufferLine'; import { EventEmitter2, IEvent } from './common/EventEmitter2'; /** @@ -105,7 +104,7 @@ class DECRQSS implements IDcsHandler { export class InputHandler extends Disposable implements IInputHandler { private _parseBuffer: Uint32Array = new Uint32Array(4096); private _stringDecoder: StringToUtf32 = new StringToUtf32(); - private _utf8Decoder = new Utf8ToUtf32(); + private _utf8Decoder: Utf8ToUtf32 = new Utf8ToUtf32(); private _workCell: CellData = new CellData(); private _onCursorMove = new EventEmitter2(); diff --git a/src/Linkifier.test.ts b/src/Linkifier.test.ts index c7bbbeb83..6a2f0ee92 100644 --- a/src/Linkifier.test.ts +++ b/src/Linkifier.test.ts @@ -4,12 +4,12 @@ */ import { assert } from 'chai'; -import { IMouseZoneManager, IMouseZone } from './ui/Types'; -import { ILinkMatcher, ITerminal, IBufferLine } from './Types'; +import { IMouseZoneManager, IMouseZone, ILinkMatcher, ITerminal } from './Types'; +import { IBufferLine } from './core/Types'; import { Linkifier } from './Linkifier'; -import { MockBuffer, MockTerminal, TestTerminal } from './ui/TestUtils.test'; +import { MockBuffer, MockTerminal, TestTerminal } from './TestUtils.test'; import { CircularList } from './common/CircularList'; -import { BufferLine, CellData } from './BufferLine'; +import { BufferLine, CellData } from './core/buffer/BufferLine'; class TestLinkifier extends Linkifier { constructor(terminal: ITerminal) { diff --git a/src/Linkifier.ts b/src/Linkifier.ts index 8c57e1b22..9744929e1 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -3,9 +3,8 @@ * @license MIT */ -import { IMouseZoneManager } from './ui/Types'; -import { ILinkifierEvent, ILinkMatcher, LinkMatcherHandler, ILinkMatcherOptions, ILinkifier, ITerminal, IBufferStringIteratorResult } from './Types'; -import { MouseZone } from './ui/MouseZoneManager'; +import { ILinkifierEvent, ILinkMatcher, LinkMatcherHandler, ILinkMatcherOptions, ILinkifier, ITerminal, IBufferStringIteratorResult, IMouseZoneManager } from './Types'; +import { MouseZone } from './MouseZoneManager'; import { getStringCellWidth } from './CharWidth'; import { EventEmitter2, IEvent } from './common/EventEmitter2'; diff --git a/src/ui/MouseHelper.test.ts b/src/MouseHelper.test.ts similarity index 100% rename from src/ui/MouseHelper.test.ts rename to src/MouseHelper.test.ts diff --git a/src/ui/MouseHelper.ts b/src/MouseHelper.ts similarity index 96% rename from src/ui/MouseHelper.ts rename to src/MouseHelper.ts index e36e7f17c..fb91c9ef8 100644 --- a/src/ui/MouseHelper.ts +++ b/src/MouseHelper.ts @@ -3,8 +3,8 @@ * @license MIT */ -import { ICharMeasure, IMouseHelper } from '../Types'; -import { IRenderer } from '../renderer/Types'; +import { ICharMeasure, IMouseHelper } from './Types'; +import { IRenderer } from './renderer/Types'; export class MouseHelper implements IMouseHelper { constructor(private _renderer: IRenderer) {} diff --git a/src/ui/MouseZoneManager.ts b/src/MouseZoneManager.ts similarity index 97% rename from src/ui/MouseZoneManager.ts rename to src/MouseZoneManager.ts index 372dccc57..2f293df14 100644 --- a/src/ui/MouseZoneManager.ts +++ b/src/MouseZoneManager.ts @@ -3,10 +3,9 @@ * @license MIT */ -import { ITerminal } from '../Types'; -import { IMouseZoneManager, IMouseZone } from './Types'; -import { Disposable } from '../common/Lifecycle'; -import { addDisposableDomListener } from './Lifecycle'; +import { ITerminal, IMouseZoneManager, IMouseZone } from './Types'; +import { Disposable } from './common/Lifecycle'; +import { addDisposableDomListener } from './ui/Lifecycle'; const HOVER_DURATION = 500; diff --git a/src/SelectionManager.test.ts b/src/SelectionManager.test.ts index d65f97160..24ba01005 100644 --- a/src/SelectionManager.test.ts +++ b/src/SelectionManager.test.ts @@ -4,13 +4,14 @@ */ import { assert } from 'chai'; -import { CharMeasure } from './ui/CharMeasure'; +import { CharMeasure } from './CharMeasure'; import { SelectionManager, SelectionMode } from './SelectionManager'; import { SelectionModel } from './SelectionModel'; import { BufferSet } from './BufferSet'; -import { ITerminal, IBuffer, IBufferLine } from './Types'; -import { MockTerminal } from './ui/TestUtils.test'; -import { BufferLine, CellData } from './BufferLine'; +import { ITerminal, IBuffer } from './Types'; +import { IBufferLine } from './core/Types'; +import { MockTerminal } from './TestUtils.test'; +import { BufferLine, CellData } from './core/buffer/BufferLine'; class TestMockTerminal extends MockTerminal { emit(event: string, data: any): void {} diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index eed206cc6..dabcc9a4d 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -3,13 +3,14 @@ * @license MIT */ -import { ITerminal, ISelectionManager, IBuffer, IBufferLine, ISelectionRedrawRequestEvent } from './Types'; -import { MouseHelper } from './ui/MouseHelper'; +import { ITerminal, ISelectionManager, IBuffer, ISelectionRedrawRequestEvent } from './Types'; +import { IBufferLine } from './core/Types'; +import { MouseHelper } from './MouseHelper'; import * as Browser from './common/Platform'; -import { CharMeasure } from './ui/CharMeasure'; +import { CharMeasure } from './CharMeasure'; import { SelectionModel } from './SelectionModel'; import { AltClickHandler } from './handlers/AltClickHandler'; -import { CellData } from './BufferLine'; +import { CellData } from './core/buffer/BufferLine'; import { IDisposable } from 'xterm'; import { EventEmitter2, IEvent } from './common/EventEmitter2'; diff --git a/src/SelectionModel.test.ts b/src/SelectionModel.test.ts index d49f41d0c..c2b261a98 100644 --- a/src/SelectionModel.test.ts +++ b/src/SelectionModel.test.ts @@ -7,7 +7,7 @@ import { assert } from 'chai'; import { ITerminal } from './Types'; import { SelectionModel } from './SelectionModel'; import { BufferSet } from './BufferSet'; -import { MockTerminal } from './ui/TestUtils.test'; +import { MockTerminal } from './TestUtils.test'; class TestSelectionModel extends SelectionModel { constructor( diff --git a/src/Terminal.integration.ts b/src/Terminal.integration.ts index 100430066..5e12d2a30 100644 --- a/src/Terminal.integration.ts +++ b/src/Terminal.integration.ts @@ -13,9 +13,8 @@ import * as path from 'path'; import * as pty from 'node-pty'; import { assert } from 'chai'; import { Terminal } from './Terminal'; -import { WHITESPACE_CELL_CHAR } from './Buffer'; import { IViewport } from './Types'; -import { CellData } from './BufferLine'; +import { CellData, WHITESPACE_CELL_CHAR } from './core/buffer/BufferLine'; class TestTerminal extends Terminal { innerWrite(): void { this._innerWrite(); } diff --git a/src/Terminal.test.ts b/src/Terminal.test.ts index dd8e32e18..9b5fa9c8b 100644 --- a/src/Terminal.test.ts +++ b/src/Terminal.test.ts @@ -5,9 +5,8 @@ import { assert, expect } from 'chai'; import { Terminal } from './Terminal'; -import { MockViewport, MockCompositionHelper, MockRenderer } from './ui/TestUtils.test'; -import { DEFAULT_ATTR_DATA } from './Buffer'; -import { CellData } from './BufferLine'; +import { MockViewport, MockCompositionHelper, MockRenderer } from './TestUtils.test'; +import { CellData, DEFAULT_ATTR_DATA } from './core/buffer/BufferLine'; const INIT_COLS = 80; const INIT_ROWS = 24; diff --git a/src/Terminal.ts b/src/Terminal.ts index b0533a9f6..2cb1dcf08 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -21,27 +21,26 @@ * http://linux.die.net/man/7/urxvt */ -import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharacterJoinerHandler, IBufferLine, IAttributeData } from './Types'; -import { IMouseZoneManager } from './ui/Types'; +import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharacterJoinerHandler, IMouseZoneManager } from './Types'; import { IRenderer } from './renderer/Types'; import { BufferSet } from './BufferSet'; -import { Buffer, MAX_BUFFER_SIZE, DEFAULT_ATTR_DATA } from './Buffer'; +import { Buffer, MAX_BUFFER_SIZE } from './Buffer'; import { CompositionHelper } from './CompositionHelper'; import { EventEmitter } from './common/EventEmitter'; import { Viewport } from './Viewport'; -import { rightClickHandler, moveTextAreaUnderMouseCursor, pasteHandler, copyHandler } from './ui/Clipboard'; +import { rightClickHandler, moveTextAreaUnderMouseCursor, pasteHandler, copyHandler } from './Clipboard'; import { C0 } from './common/data/EscapeSequences'; import { InputHandler } from './InputHandler'; import { Renderer } from './renderer/Renderer'; import { Linkifier } from './Linkifier'; import { SelectionManager } from './SelectionManager'; -import { CharMeasure } from './ui/CharMeasure'; +import { CharMeasure } from './CharMeasure'; import * as Browser from './common/Platform'; import { addDisposableDomListener } from './ui/Lifecycle'; import * as Strings from './Strings'; -import { MouseHelper } from './ui/MouseHelper'; +import { MouseHelper } from './MouseHelper'; import { DEFAULT_BELL_SOUND, SoundManager } from './SoundManager'; -import { MouseZoneManager } from './ui/MouseZoneManager'; +import { MouseZoneManager } from './MouseZoneManager'; import { AccessibilityManager } from './AccessibilityManager'; import { ScreenDprMonitor } from './ui/ScreenDprMonitor'; import { ITheme, IMarker, IDisposable } from 'xterm'; @@ -49,10 +48,10 @@ import { removeTerminalFromCache } from './renderer/atlas/CharAtlasCache'; import { DomRenderer } from './renderer/dom/DomRenderer'; import { IKeyboardEvent } from './common/Types'; import { evaluateKeyboardEvent } from './core/input/Keyboard'; -import { KeyboardResultType, ICharset } from './core/Types'; +import { KeyboardResultType, ICharset, IBufferLine, IAttributeData } from './core/Types'; import { clone } from './common/Clone'; import { EventEmitter2, IEvent } from './common/EventEmitter2'; -import { Attributes } from './BufferLine'; +import { Attributes, DEFAULT_ATTR_DATA } from './core/buffer/BufferLine'; import { applyWindowsMode } from './WindowsMode'; // Let it work inside Node.js for automated testing purposes. diff --git a/src/ui/TestUtils.test.ts b/src/TestUtils.test.ts similarity index 96% rename from src/ui/TestUtils.test.ts rename to src/TestUtils.test.ts index d535e6c81..ceed4bd5d 100644 --- a/src/ui/TestUtils.test.ts +++ b/src/TestUtils.test.ts @@ -3,14 +3,15 @@ * @license MIT */ -import { IColorSet, IRenderer, IRenderDimensions, IColorManager } from '../renderer/Types'; -import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, ILinkifier, IMouseHelper, ILinkMatcherOptions, CharacterJoinerHandler, IBufferLine, IBufferStringIterator, ICellData, IAttributeData } from '../Types'; -import { ICircularList, XtermListener } from '../common/Types'; -import { Buffer } from '../Buffer'; -import * as Browser from '../common/Platform'; +import { IColorSet, IRenderer, IRenderDimensions, IColorManager } from './renderer/Types'; +import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, ILinkifier, IMouseHelper, ILinkMatcherOptions, CharacterJoinerHandler, IBufferStringIterator } from './Types'; +import { IBufferLine, ICellData, IAttributeData } from './core/Types'; +import { ICircularList, XtermListener } from './common/Types'; +import { Buffer } from './Buffer'; +import * as Browser from './common/Platform'; import { ITheme, IDisposable, IMarker, IEvent } from 'xterm'; -import { Terminal } from '../Terminal'; -import { AttributeData } from '../BufferLine'; +import { Terminal } from './Terminal'; +import { AttributeData } from './core/buffer/BufferLine'; export class TestTerminal extends Terminal { writeSync(data: string): void { diff --git a/src/Types.ts b/src/Types.ts index 2139b7030..91fe991b4 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -5,14 +5,12 @@ import { Terminal as PublicTerminal, ITerminalOptions as IPublicTerminalOptions, IEventEmitter, IDisposable } from 'xterm'; import { IColorSet, IRenderer } from './renderer/Types'; -import { IMouseZoneManager } from './ui/Types'; -import { ICharset } from './core/Types'; +import { ICharset, IAttributeData, ICellData, IBufferLine, CharData } from './core/Types'; import { ICircularList } from './common/Types'; import { IEvent } from './common/EventEmitter2'; export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean; -export type CharData = [number, string, number, number]; export type LineData = CharData[]; export type LinkMatcherHandler = (event: MouseEvent, uri: string) => void; @@ -527,81 +525,19 @@ export interface IEscapeSequenceParser extends IDisposable { clearErrorHandler(): void; } -/** RGB color type */ -export type IColorRGB = [number, number, number]; - -/** Attribute data */ -export interface IAttributeData { - fg: number; - bg: number; - - clone(): IAttributeData; - - // flags - isInverse(): number; - isBold(): number; - isUnderline(): number; - isBlink(): number; - isInvisible(): number; - isItalic(): number; - isDim(): number; - - // color modes - getFgColorMode(): number; - getBgColorMode(): number; - isFgRGB(): boolean; - isBgRGB(): boolean; - isFgPalette(): boolean; - isBgPalette(): boolean; - isFgDefault(): boolean; - isBgDefault(): boolean; - - // colors - getFgColor(): number; - getBgColor(): number; +export interface IMouseZoneManager extends IDisposable { + add(zone: IMouseZone): void; + clearAll(start?: number, end?: number): void; } -/** 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): void; - addCodepointToCell(index: number, codePoint: number): void; - insertCells(pos: number, n: number, ch: ICellData): void; - deleteCells(pos: number, n: number, fill: ICellData): void; - replaceCells(start: number, end: number, fill: ICellData): 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 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; } diff --git a/src/Viewport.ts b/src/Viewport.ts index 50d044571..6bf92531c 100644 --- a/src/Viewport.ts +++ b/src/Viewport.ts @@ -5,7 +5,7 @@ import { IColorSet } from './renderer/Types'; import { ITerminal, IViewport } from './Types'; -import { CharMeasure } from './ui/CharMeasure'; +import { CharMeasure } from './CharMeasure'; import { Disposable } from './common/Lifecycle'; import { addDisposableDomListener } from './ui/Lifecycle'; diff --git a/src/WindowsMode.ts b/src/WindowsMode.ts index ac1d193e4..d7b4bcaec 100644 --- a/src/WindowsMode.ts +++ b/src/WindowsMode.ts @@ -5,7 +5,7 @@ import { IDisposable } from 'xterm'; import { ITerminal } from './Types'; -import { CHAR_DATA_CODE_INDEX, NULL_CELL_CODE, WHITESPACE_CELL_CODE } from './Buffer'; +import { CHAR_DATA_CODE_INDEX, NULL_CELL_CODE, WHITESPACE_CELL_CODE } from './core/buffer/BufferLine'; export function applyWindowsMode(terminal: ITerminal): IDisposable { // Winpty does not support wraparound mode which means that lines will never diff --git a/src/addons/webLinks/webLinks.test.ts b/src/addons/webLinks/webLinks.test.ts index 1e8a4ae7e..da5569ab8 100644 --- a/src/addons/webLinks/webLinks.test.ts +++ b/src/addons/webLinks/webLinks.test.ts @@ -28,63 +28,185 @@ describe('webLinks addon', () => { }); }); - it('should allow ~ character in URI path', () => { - const term = new MockTerminal(); - webLinks.webLinksInit(term); + describe('should allow simple URI path', () => { + it('foo.com', () => { + const term = new MockTerminal(); + webLinks.webLinksInit(term); - const row = ' http://foo.com/a~b#c~d?e~f '; + const row = ' http://foo.com '; - const match = row.match(term.regex); - const uri = match[term.options.matchIndex]; + const match = row.match(term.regex); + const uri = match[term.options.matchIndex]; - assert.equal(uri, 'http://foo.com/a~b#c~d?e~f'); + assert.equal(uri, 'http://foo.com'); + }); + + it('bar.io', () => { + const term = new MockTerminal(); + webLinks.webLinksInit(term); + + const row = ' http://bar.io '; + + const match = row.match(term.regex); + const uri = match[term.options.matchIndex]; + + assert.equal(uri, 'http://bar.io'); + }); }); - it('should allow : character in URI path', () => { - const term = new MockTerminal(); - webLinks.webLinksInit(term); + describe('should allow ~ character in URI path', () => { + it('foo.com', () => { + const term = new MockTerminal(); + webLinks.webLinksInit(term); - const row = ' http://foo.com/colon:test '; + const row = ' http://foo.com/a~b#c~d?e~f '; - const match = row.match(term.regex); - const uri = match[term.options.matchIndex]; + const match = row.match(term.regex); + const uri = match[term.options.matchIndex]; - assert.equal(uri, 'http://foo.com/colon:test'); + assert.equal(uri, 'http://foo.com/a~b#c~d?e~f'); + }); + + it('bar.io', () => { + const term = new MockTerminal(); + webLinks.webLinksInit(term); + + const row = ' http://bar.io/a~b#c~d?e~f '; + + const match = row.match(term.regex); + const uri = match[term.options.matchIndex]; + + assert.equal(uri, 'http://bar.io/a~b#c~d?e~f'); + }); }); - it('should not allow : character at the end of a URI path', () => { - const term = new MockTerminal(); - webLinks.webLinksInit(term); + describe('should allow : character in URI path', () => { + it('foo.com', () => { + const term = new MockTerminal(); + webLinks.webLinksInit(term); - const row = ' http://foo.com/colon:test: '; + const row = ' http://foo.com/colon:test '; - const match = row.match(term.regex); - const uri = match[term.options.matchIndex]; + const match = row.match(term.regex); + const uri = match[term.options.matchIndex]; - assert.equal(uri, 'http://foo.com/colon:test'); + assert.equal(uri, 'http://foo.com/colon:test'); + }); + + it('bar.io', () => { + const term = new MockTerminal(); + webLinks.webLinksInit(term); + + const row = ' http://bar.io/colon:test '; + + const match = row.match(term.regex); + const uri = match[term.options.matchIndex]; + + assert.equal(uri, 'http://bar.io/colon:test'); + }); }); - it('should not allow " character at the end of a URI enclosed with ""', () => { - const term = new MockTerminal(); - webLinks.webLinksInit(term); + describe('should not allow : character at the end of a URI path', () => { + it('foo.com', () => { + const term = new MockTerminal(); + webLinks.webLinksInit(term); - const row = '"http://foo.com/"'; + const row = ' http://foo.com/colon:test: '; - const match = row.match(term.regex); - const uri = match[term.options.matchIndex]; + const match = row.match(term.regex); + const uri = match[term.options.matchIndex]; - assert.equal(uri, 'http://foo.com/'); + assert.equal(uri, 'http://foo.com/colon:test'); + }); + + it('bar.io', () => { + const term = new MockTerminal(); + webLinks.webLinksInit(term); + + const row = ' http://bar.io/colon:test: '; + + const match = row.match(term.regex); + const uri = match[term.options.matchIndex]; + + assert.equal(uri, 'http://bar.io/colon:test'); + }); }); - it('should not allow \' character at the end of a URI enclosed with \'\'', () => { - const term = new MockTerminal(); - webLinks.webLinksInit(term); + describe('should not allow " character at the end of a URI enclosed with ""', () => { + it('foo.com', () => { + const term = new MockTerminal(); + webLinks.webLinksInit(term); - const row = '\'http://foo.com/\''; + const row = '"http://foo.com/"'; - const match = row.match(term.regex); - const uri = match[term.options.matchIndex]; + const match = row.match(term.regex); + const uri = match[term.options.matchIndex]; - assert.equal(uri, 'http://foo.com/'); + assert.equal(uri, 'http://foo.com/'); + }); + + it('bar.io', () => { + const term = new MockTerminal(); + webLinks.webLinksInit(term); + + const row = '"http://bar.io/"'; + + const match = row.match(term.regex); + const uri = match[term.options.matchIndex]; + + assert.equal(uri, 'http://bar.io/'); + }); + }); + + describe('should not allow \' character at the end of a URI enclosed with \'\'', () => { + it('foo.com', () => { + const term = new MockTerminal(); + webLinks.webLinksInit(term); + + const row = '\'http://foo.com/\''; + + const match = row.match(term.regex); + const uri = match[term.options.matchIndex]; + + assert.equal(uri, 'http://foo.com/'); + }); + + it('bar.io', () => { + const term = new MockTerminal(); + webLinks.webLinksInit(term); + + const row = '\'http://bar.io/\''; + + const match = row.match(term.regex); + const uri = match[term.options.matchIndex]; + + assert.equal(uri, 'http://bar.io/'); + }); + }); + + describe('should allow + character in URI path', () => { + it('foo.com', () => { + const term = new MockTerminal(); + webLinks.webLinksInit(term); + + const row = 'http://foo.com/subpath/+/id'; + + const match = row.match(term.regex); + const uri = match[term.options.matchIndex]; + + assert.equal(uri, 'http://foo.com/subpath/+/id'); + }); + + it('bar.io', () => { + const term = new MockTerminal(); + webLinks.webLinksInit(term); + + const row = 'http://bar.io/subpath/+/id'; + + const match = row.match(term.regex); + const uri = match[term.options.matchIndex]; + + assert.equal(uri, 'http://bar.io/subpath/+/id'); + }); }); }); diff --git a/src/addons/webLinks/webLinks.ts b/src/addons/webLinks/webLinks.ts index f0d69cc58..8a0fec097 100644 --- a/src/addons/webLinks/webLinks.ts +++ b/src/addons/webLinks/webLinks.ts @@ -14,7 +14,8 @@ const ipClause = '((\\d{1,3}\\.){3}\\d{1,3})'; const localHostClause = '(localhost)'; const portClause = '(:\\d{1,5})'; const hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|' + ipClause + '|' + localHostClause + ')' + portClause + '?'; -const pathClause = '(\\/[\\/\\w\\.\\-%~:]*)*([^:"\'\\s])'; +const pathCharacterSet = '(\\/[\\/\\w\\.\\-%~:+]*)*([^:"\'\\s])'; +const pathClause = '(' + pathCharacterSet + ')?'; const queryStringHashFragmentCharacterSet = '[0-9\\w\\[\\]\\(\\)\\/\\?\\!#@$%&\'*+,:;~\\=\\.\\-]*'; const queryStringClause = '(\\?' + queryStringHashFragmentCharacterSet + ')?'; const hashFragmentClause = '(#' + queryStringHashFragmentCharacterSet + ')?'; diff --git a/src/common/Types.ts b/src/common/Types.ts index b2111bfc7..717de359d 100644 --- a/src/common/Types.ts +++ b/src/common/Types.ts @@ -6,6 +6,8 @@ import { IEvent, EventEmitter2 } from './EventEmitter2'; import { IDeleteEvent, IInsertEvent } from './CircularList'; +export const DEFAULT_COLOR = 256; + export interface IDisposable { dispose(): void; } diff --git a/src/core/Types.ts b/src/core/Types.ts index 4001e4488..ae9274b84 100644 --- a/src/core/Types.ts +++ b/src/core/Types.ts @@ -19,3 +19,82 @@ export interface IKeyboardResult { export interface ICharset { [key: string]: string; } + +export type CharData = [number, string, number, number]; +export type IColorRGB = [number, number, number]; + +/** Attribute data */ +export interface IAttributeData { + fg: number; + bg: number; + + clone(): IAttributeData; + + // flags + isInverse(): number; + isBold(): number; + isUnderline(): number; + isBlink(): number; + isInvisible(): number; + isItalic(): number; + isDim(): number; + + // color modes + getFgColorMode(): number; + getBgColorMode(): number; + isFgRGB(): boolean; + isBgRGB(): boolean; + isFgPalette(): boolean; + isBgPalette(): boolean; + isFgDefault(): boolean; + isBgDefault(): boolean; + + // colors + getFgColor(): number; + getBgColor(): 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): void; + addCodepointToCell(index: number, codePoint: number): void; + insertCells(pos: number, n: number, ch: ICellData): void; + deleteCells(pos: number, n: number, fill: ICellData): void; + replaceCells(start: number, end: number, fill: ICellData): 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; +} diff --git a/src/BufferLine.test.ts b/src/core/buffer/BufferLine.test.ts similarity index 95% rename from src/BufferLine.test.ts rename to src/core/buffer/BufferLine.test.ts index 5b029cb80..c42b372e2 100644 --- a/src/BufferLine.test.ts +++ b/src/core/buffer/BufferLine.test.ts @@ -3,10 +3,8 @@ * @license MIT */ import * as chai from 'chai'; -import { BufferLine, CellData, Content } from './BufferLine'; -import { CharData, IBufferLine } from './Types'; -import { NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, DEFAULT_ATTR } from './Buffer'; - +import { BufferLine, CellData, Content, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, DEFAULT_ATTR } from './BufferLine'; +import { CharData, IBufferLine } from '../Types'; class TestBufferLine extends BufferLine { public get combined(): {[index: number]: string} { @@ -57,7 +55,7 @@ describe('BufferLine', function(): void { chai.expect(line.length).equals(10); chai.expect(line.loadCell(0, new CellData()).getAsCharData()).eql([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); chai.expect(line.isWrapped).equals(false); - line = new TestBufferLine(10, null, true); + line = new TestBufferLine(10, undefined, true); chai.expect(line.length).equals(10); chai.expect(line.loadCell(0, new CellData()).getAsCharData()).eql([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); chai.expect(line.isWrapped).equals(true); @@ -127,7 +125,7 @@ describe('BufferLine', function(): void { ]); }); it('clone', function(): void { - const line = new TestBufferLine(5, null, true); + const line = new TestBufferLine(5, undefined, true); line.setCell(0, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)])); line.setCell(1, CellData.fromCharData([2, 'b', 0, 'b'.charCodeAt(0)])); line.setCell(2, CellData.fromCharData([3, 'c', 0, 'c'.charCodeAt(0)])); @@ -167,27 +165,27 @@ describe('BufferLine', function(): void { it('enlarge(false)', function(): void { const line = new TestBufferLine(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false); line.resize(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)])); - chai.expect(line.toArray()).eql(Array(10).fill([1, 'a', 0, 'a'.charCodeAt(0)])); + chai.expect(line.toArray()).eql((Array(10) as any).fill([1, 'a', 0, 'a'.charCodeAt(0)])); }); it('enlarge(true)', function(): void { const line = new TestBufferLine(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false); line.resize(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)])); - chai.expect(line.toArray()).eql(Array(10).fill([1, 'a', 0, 'a'.charCodeAt(0)])); + chai.expect(line.toArray()).eql((Array(10) as any).fill([1, 'a', 0, 'a'.charCodeAt(0)])); }); it('shrink(true) - should apply new size', function(): void { const line = new TestBufferLine(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false); line.resize(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)])); - chai.expect(line.toArray()).eql(Array(5).fill([1, 'a', 0, 'a'.charCodeAt(0)])); + chai.expect(line.toArray()).eql((Array(5) as any).fill([1, 'a', 0, 'a'.charCodeAt(0)])); }); it('shrink to 0 length', function(): void { const line = new TestBufferLine(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false); line.resize(0, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)])); - chai.expect(line.toArray()).eql(Array(0).fill([1, 'a', 0, 'a'.charCodeAt(0)])); + chai.expect(line.toArray()).eql((Array(0) as any).fill([1, 'a', 0, 'a'.charCodeAt(0)])); }); it('should remove combining data on replaced cells after shrinking then enlarging', () => { const line = new TestBufferLine(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false); - line.set(2, [ null, '😁', 1, '😁'.charCodeAt(0) ]); - line.set(9, [ null, '😁', 1, '😁'.charCodeAt(0) ]); + line.set(2, [ 0, '😁', 1, '😁'.charCodeAt(0) ]); + line.set(9, [ 0, '😁', 1, '😁'.charCodeAt(0) ]); chai.expect(line.translateToString()).eql('aa😁aaaaaa😁'); chai.expect(Object.keys(line.combined).length).eql(2); line.resize(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)])); @@ -224,7 +222,7 @@ describe('BufferLine', function(): void { const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); line.setCell(2, CellData.fromCharData([1, '1', 2, '1'.charCodeAt(0)])); - line.setCell(3, CellData.fromCharData([0, '', 0, undefined])); + line.setCell(3, CellData.fromCharData([0, '', 0, 0])); chai.expect(line.getTrimmedLength()).equal(4); // also counts null cell after fullwidth }); }); @@ -284,11 +282,11 @@ describe('BufferLine', function(): void { const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); line.setCell(2, CellData.fromCharData([1, '1', 2, '1'.charCodeAt(0)])); - line.setCell(3, CellData.fromCharData([0, '', 0, undefined])); + line.setCell(3, CellData.fromCharData([0, '', 0, 0])); line.setCell(5, CellData.fromCharData([1, '1', 2, '1'.charCodeAt(0)])); - line.setCell(6, CellData.fromCharData([0, '', 0, undefined])); + line.setCell(6, CellData.fromCharData([0, '', 0, 0])); line.setCell(7, CellData.fromCharData([1, '1', 2, '1'.charCodeAt(0)])); - line.setCell(8, CellData.fromCharData([0, '', 0, undefined])); + line.setCell(8, CellData.fromCharData([0, '', 0, 0])); chai.expect(line.translateToString(false)).equal('a 1 11 '); chai.expect(line.translateToString(true)).equal('a 1 11'); chai.expect(line.translateToString(false, 0, 7)).equal('a 1 1'); diff --git a/src/BufferLine.ts b/src/core/buffer/BufferLine.ts similarity index 94% rename from src/BufferLine.ts rename to src/core/buffer/BufferLine.ts index d2f7af402..154cc5984 100644 --- a/src/BufferLine.ts +++ b/src/core/buffer/BufferLine.ts @@ -2,10 +2,34 @@ * Copyright (c) 2018 The xterm.js authors. All rights reserved. * @license MIT */ -import { CharData, IBufferLine, ICellData, IColorRGB, IAttributeData } from './Types'; -import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, WHITESPACE_CELL_CHAR, CHAR_DATA_ATTR_INDEX } from './Buffer'; -import { stringFromCodePoint } from './core/input/TextDecoder'; +import { CharData, IBufferLine, ICellData, IColorRGB, IAttributeData } from '../Types'; +import { stringFromCodePoint } from '../input/TextDecoder'; +import { DEFAULT_COLOR } from '../../common/Types'; +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; /** * buffer memory layout: @@ -193,6 +217,8 @@ export class AttributeData implements IAttributeData { } } +export const DEFAULT_ATTR_DATA = new AttributeData(); + /** * CellData - represents a single Cell in the terminal buffer. */ @@ -299,17 +325,15 @@ export class CellData extends AttributeData implements ICellData { * memory allocs / GC pressure can be greatly reduced by reusing the CellData object. */ export class BufferLine implements IBufferLine { - protected _data: Uint32Array | null = null; + protected _data: Uint32Array; protected _combined: {[index: number]: string} = {}; public length: number; constructor(cols: number, fillCellData?: ICellData, public isWrapped: boolean = false) { - if (cols) { - 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._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; } @@ -547,7 +571,7 @@ export class BufferLine implements IBufferLine { } } } else { - this._data = null; + this._data = new Uint32Array(0); this._combined = {}; } } diff --git a/src/core/input/Keyboard.ts b/src/core/input/Keyboard.ts index 5e4c4e617..36097f912 100644 --- a/src/core/input/Keyboard.ts +++ b/src/core/input/Keyboard.ts @@ -352,6 +352,10 @@ export function evaluateKeyboardEvent( } 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; } diff --git a/src/core/input/TextDecoder.ts b/src/core/input/TextDecoder.ts index 69b2b66b9..b9ac19ce0 100644 --- a/src/core/input/TextDecoder.ts +++ b/src/core/input/TextDecoder.ts @@ -6,6 +6,10 @@ /** * 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) { @@ -16,6 +20,31 @@ export function stringFromCodePoint(codePoint: number): string { } +/** + * 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 string 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. @@ -315,28 +344,3 @@ export class Utf8ToUtf32 { return size; } } - - -/** - * 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 string 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; -} diff --git a/src/handlers/AltClickHandler.ts b/src/handlers/AltClickHandler.ts index 9286421e2..5cf60fa62 100644 --- a/src/handlers/AltClickHandler.ts +++ b/src/handlers/AltClickHandler.ts @@ -3,7 +3,8 @@ * @license MIT */ -import { ITerminal, IBufferLine } from '../Types'; +import { ITerminal } from '../Types'; +import { IBufferLine } from '../core/Types'; import { ICircularList } from '../common/Types'; import { C0 } from '../common/data/EscapeSequences'; diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index bfd684e11..d613da33e 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -30,7 +30,7 @@ export class Terminal implements ITerminalApi { public get textarea(): HTMLTextAreaElement { return this._core.textarea; } public get rows(): number { return this._core.rows; } public get cols(): number { return this._core.cols; } - public get markers(): IMarker[] { return this._core.markers; } + public get markers(): ReadonlyArray { return this._core.markers; } public blur(): void { this._core.blur(); } diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 008b216a9..ce50a6511 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -4,12 +4,14 @@ */ import { IRenderLayer, IColorSet, IRenderDimensions } from './Types'; -import { ITerminal, ICellData } from '../Types'; -import { DIM_OPACITY, INVERTED_DEFAULT_COLOR, IGlyphIdentifier, DEFAULT_COLOR } from './atlas/Types'; +import { ITerminal } from '../Types'; +import { ICellData } from '../core/Types'; +import { DEFAULT_COLOR } from '../common/Types'; +import { DIM_OPACITY, INVERTED_DEFAULT_COLOR, IGlyphIdentifier } from './atlas/Types'; import BaseCharAtlas from './atlas/BaseCharAtlas'; import { acquireCharAtlas } from './atlas/CharAtlasCache'; -import { CellData, AttributeData } from '../BufferLine'; -import { WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE } from '../Buffer'; +import { CellData, AttributeData, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE } from '../core/buffer/BufferLine'; +import { JoinedCellData } from './CharacterJoinerRegistry'; export abstract class BaseRenderLayer implements IRenderLayer { private _canvas: HTMLCanvasElement; @@ -261,7 +263,10 @@ export abstract class BaseRenderLayer implements IRenderLayer { protected drawChars(terminal: ITerminal, cell: ICellData, x: number, y: number): void { // skip cache right away if we draw in RGB - if (cell.isFgRGB() || cell.isBgRGB()) { + // Note: to avoid bad runtime JoinedCellData will be skipped + // in the cache handler (atlasDidDraw == false) itself and + // fall through to uncached later down below + if (cell.isFgRGB() || cell.isBgRGB() || cell instanceof JoinedCellData) { this._drawUncachedChars(terminal, cell, x, y); return; } @@ -322,14 +327,18 @@ export abstract class BaseRenderLayer implements IRenderLayer { } else { this._ctx.fillStyle = this._colors.ansi[cell.getBgColor()].css; } - } else if (cell.isFgRGB()) { - this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`; - } else if (cell.isFgPalette()) { - let fg = cell.getFgColor(); - if (terminal.options.drawBoldTextInBrightColors && cell.isBold() && fg < 8) { - fg += 8; + } 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 (terminal.options.drawBoldTextInBrightColors && cell.isBold() && fg < 8) { + fg += 8; + } + this._ctx.fillStyle = this._colors.ansi[fg].css; } - this._ctx.fillStyle = this._colors.ansi[fg].css; } this._clipRow(terminal, y); diff --git a/src/renderer/CharacterJoinerRegistry.test.ts b/src/renderer/CharacterJoinerRegistry.test.ts index effdbfaaf..948985a46 100644 --- a/src/renderer/CharacterJoinerRegistry.test.ts +++ b/src/renderer/CharacterJoinerRegistry.test.ts @@ -1,12 +1,17 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + import { assert } from 'chai'; -import { MockTerminal, MockBuffer } from '../ui/TestUtils.test'; +import { MockTerminal, MockBuffer } from '../TestUtils.test'; import { CircularList } from '../common/CircularList'; import { ICharacterJoinerRegistry } from './Types'; import { CharacterJoinerRegistry } from './CharacterJoinerRegistry'; -import { BufferLine, CellData } from '../BufferLine'; -import { IBufferLine } from '../Types'; +import { BufferLine, CellData } from '../core/buffer/BufferLine'; +import { IBufferLine } from '../core/Types'; describe('CharacterJoinerRegistry', () => { let registry: ICharacterJoinerRegistry; diff --git a/src/renderer/CharacterJoinerRegistry.ts b/src/renderer/CharacterJoinerRegistry.ts index 8c521dd9d..3302c3121 100644 --- a/src/renderer/CharacterJoinerRegistry.ts +++ b/src/renderer/CharacterJoinerRegistry.ts @@ -1,7 +1,57 @@ -import { ITerminal, IBufferLine } from '../Types'; +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { ITerminal } from '../Types'; +import { IBufferLine, ICellData, CharData } from '../core/Types'; import { ICharacterJoinerRegistry, ICharacterJoiner } from './Types'; -import { CellData } from '../BufferLine'; -import { WHITESPACE_CELL_CHAR } from '../Buffer'; +import { CellData, Content, AttributeData, WHITESPACE_CELL_CHAR } from '../core/buffer/BufferLine'; + +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 CharacterJoinerRegistry implements ICharacterJoinerRegistry { diff --git a/src/renderer/CursorRenderLayer.ts b/src/renderer/CursorRenderLayer.ts index c3b751fa9..3e8af02de 100644 --- a/src/renderer/CursorRenderLayer.ts +++ b/src/renderer/CursorRenderLayer.ts @@ -5,8 +5,9 @@ import { IColorSet, IRenderDimensions } from './Types'; import { BaseRenderLayer } from './BaseRenderLayer'; -import { ITerminal, ICellData } from '../Types'; -import { CellData } from '../BufferLine'; +import { ITerminal } from '../Types'; +import { ICellData } from '../core/Types'; +import { CellData } from '../core/buffer/BufferLine'; interface ICursorState { x: number; diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index 2a205be36..14ce72b35 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -68,7 +68,7 @@ export class Renderer extends Disposable implements IRenderer { this._updateDimensions(); this.onOptionsChanged(); - this._renderDebouncer = new RenderDebouncer(this._terminal, this._renderRows.bind(this)); + this._renderDebouncer = new RenderDebouncer(this._renderRows.bind(this)); this._screenDprMonitor = new ScreenDprMonitor(); this._screenDprMonitor.setListener(() => this.onWindowResize(window.devicePixelRatio)); this.register(this._screenDprMonitor); @@ -194,7 +194,7 @@ export class Renderer extends Disposable implements IRenderer { this._needsFullRefresh = true; return; } - this._renderDebouncer.refresh(start, end); + this._renderDebouncer.refresh(start, end, this._terminal.rows); } /** diff --git a/src/renderer/TextRenderLayer.ts b/src/renderer/TextRenderLayer.ts index 2547ecb24..8be5665c4 100644 --- a/src/renderer/TextRenderLayer.ts +++ b/src/renderer/TextRenderLayer.ts @@ -3,12 +3,13 @@ * @license MIT */ -import { NULL_CELL_CODE } from '../Buffer'; import { IColorSet, IRenderDimensions, ICharacterJoinerRegistry } from './Types'; -import { CharData, ITerminal, ICellData } from '../Types'; +import { ITerminal } from '../Types'; +import { CharData, ICellData } from '../core/Types'; import { GridCache } from './GridCache'; import { BaseRenderLayer } from './BaseRenderLayer'; -import { CellData, AttributeData, Content } from '../BufferLine'; +import { CellData, AttributeData, Content, NULL_CELL_CODE } from '../core/buffer/BufferLine'; +import { JoinedCellData } from './CharacterJoinerRegistry'; /** * This CharData looks like a null character, which will forc a clear and render @@ -89,15 +90,12 @@ export class TextRenderLayer extends BaseRenderLayer { // We already know the exact start and end column of the joined range, // so we get the string and width representing it directly - cell = CellData.fromCharData([ - 0, + + cell = new JoinedCellData( + this._workCell, line.translateToString(true, range[0], range[1]), - range[1] - range[0], - 0xFFFFFF - ]); - // hacky: patch attrs - cell.fg = this._workCell.fg; - cell.bg = this._workCell.bg; + range[1] - range[0] + ); // Skip over the cells occupied by this range in the loop lastCharX = range[1] - 1; @@ -217,10 +215,18 @@ export class TextRenderLayer extends BaseRenderLayer { } else { this._ctx.fillStyle = this._colors.ansi[cell.getBgColor()].css; } - } else if (cell.isFgRGB()) { - this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`; - } else if (cell.isFgPalette()) { - this._ctx.fillStyle = this._colors.ansi[cell.getFgColor()].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 (terminal.options.drawBoldTextInBrightColors && cell.isBold() && fg < 8) { + fg += 8; + } + this._ctx.fillStyle = this._colors.ansi[fg].css; + } } this.fillBottomLineAtCells(x, y, cell.getWidth()); diff --git a/src/renderer/atlas/CharAtlasUtils.ts b/src/renderer/atlas/CharAtlasUtils.ts index 5b1add396..c2eb2e1ef 100644 --- a/src/renderer/atlas/CharAtlasUtils.ts +++ b/src/renderer/atlas/CharAtlasUtils.ts @@ -5,7 +5,8 @@ import { ITerminal } from '../../Types'; import { IColorSet } from '../Types'; -import { DEFAULT_COLOR, ICharAtlasConfig } from './Types'; +import { ICharAtlasConfig } from './Types'; +import { DEFAULT_COLOR } from '../../common/Types'; export function generateConfig(scaledCharWidth: number, scaledCharHeight: number, terminal: ITerminal, colors: IColorSet): ICharAtlasConfig { // null out some fields that don't matter diff --git a/src/renderer/atlas/StaticCharAtlas.ts b/src/renderer/atlas/StaticCharAtlas.ts index b54c833ef..66beb363f 100644 --- a/src/renderer/atlas/StaticCharAtlas.ts +++ b/src/renderer/atlas/StaticCharAtlas.ts @@ -3,10 +3,11 @@ * @license MIT */ -import { DIM_OPACITY, IGlyphIdentifier, DEFAULT_COLOR, ICharAtlasConfig, CHAR_ATLAS_CELL_SPACING } from './Types'; +import { DIM_OPACITY, IGlyphIdentifier, ICharAtlasConfig, CHAR_ATLAS_CELL_SPACING } from './Types'; import { generateStaticCharAtlasTexture } from './CharAtlasGenerator'; import BaseCharAtlas from './BaseCharAtlas'; import { is256Color } from './CharAtlasUtils'; +import { DEFAULT_COLOR } from '../../common/Types'; export default class StaticCharAtlas extends BaseCharAtlas { private _texture: HTMLCanvasElement | ImageBitmap; diff --git a/src/renderer/atlas/Types.ts b/src/renderer/atlas/Types.ts index 38923b2f5..2459c72ff 100644 --- a/src/renderer/atlas/Types.ts +++ b/src/renderer/atlas/Types.ts @@ -6,7 +6,6 @@ import { FontWeight } from 'xterm'; import { IColorSet } from '../Types'; -export const DEFAULT_COLOR = 256; export const INVERTED_DEFAULT_COLOR = 257; export const DIM_OPACITY = 0.5; diff --git a/src/renderer/dom/DomRenderer.ts b/src/renderer/dom/DomRenderer.ts index 1d879fcd1..c192034c0 100644 --- a/src/renderer/dom/DomRenderer.ts +++ b/src/renderer/dom/DomRenderer.ts @@ -80,7 +80,7 @@ export class DomRenderer extends Disposable implements IRenderer { }; this._updateDimensions(); - this._renderDebouncer = new RenderDebouncer(this._terminal, this._renderRows.bind(this)); + this._renderDebouncer = new RenderDebouncer(this._renderRows.bind(this)); this._rowFactory = new DomRendererRowFactory(_terminal.options, document); this._terminal.element.classList.add(TERMINAL_CLASS_PREFIX + this._terminalClass); @@ -101,7 +101,7 @@ export class DomRenderer extends Disposable implements IRenderer { } private _updateDimensions(): void { - this.dimensions.scaledCharWidth = Math.floor(this._terminal.charMeasure.width * window.devicePixelRatio); + this.dimensions.scaledCharWidth = this._terminal.charMeasure.width * window.devicePixelRatio; this.dimensions.scaledCharHeight = Math.ceil(this._terminal.charMeasure.height * window.devicePixelRatio); this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._terminal.options.letterSpacing); this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._terminal.options.lineHeight); @@ -174,9 +174,9 @@ export class DomRenderer extends Disposable implements IRenderer { // Blink animation styles += `@keyframes blink {` + - ` 0 % { opacity: 1.0; }` + + ` 0% { opacity: 1.0; }` + ` 50% { opacity: 0.0; }` + - ` 100 % { opacity: 1.0; }` + + ` 100% { opacity: 1.0; }` + `}`; // Cursor styles += @@ -340,7 +340,7 @@ export class DomRenderer extends Disposable implements IRenderer { } public refreshRows(start: number, end: number): void { - this._renderDebouncer.refresh(start, end); + this._renderDebouncer.refresh(start, end, this._terminal.rows); } private _renderRows(start: number, end: number): void { diff --git a/src/renderer/dom/DomRendererRowFactory.test.ts b/src/renderer/dom/DomRendererRowFactory.test.ts index 076f5d6a3..b25305952 100644 --- a/src/renderer/dom/DomRendererRowFactory.test.ts +++ b/src/renderer/dom/DomRendererRowFactory.test.ts @@ -6,9 +6,9 @@ import jsdom = require('jsdom'); import { assert } from 'chai'; import { DomRendererRowFactory } from './DomRendererRowFactory'; -import { DEFAULT_ATTR, NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, DEFAULT_ATTR_DATA } from '../../Buffer'; -import { BufferLine, CellData, FgFlags, BgFlags, Attributes } from '../../BufferLine'; -import { IBufferLine, ITerminalOptions } from '../../Types'; +import { BufferLine, CellData, FgFlags, BgFlags, Attributes, NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, DEFAULT_ATTR, DEFAULT_ATTR_DATA } from '../../core/buffer/BufferLine'; +import { ITerminalOptions } from '../../Types'; +import { IBufferLine } from '../../core/Types'; describe('DomRendererRowFactory', () => { let dom: jsdom.JSDOM; diff --git a/src/renderer/dom/DomRendererRowFactory.ts b/src/renderer/dom/DomRendererRowFactory.ts index 60e2d5098..85ef586e2 100644 --- a/src/renderer/dom/DomRendererRowFactory.ts +++ b/src/renderer/dom/DomRendererRowFactory.ts @@ -3,10 +3,10 @@ * @license MIT */ -import { NULL_CELL_CODE, WHITESPACE_CELL_CHAR } from '../../Buffer'; -import { IBufferLine, ITerminalOptions } from '../../Types'; +import { ITerminalOptions } from '../../Types'; +import { IBufferLine } from '../../core/Types'; import { INVERTED_DEFAULT_COLOR } from '../atlas/Types'; -import { CellData, AttributeData } from '../../BufferLine'; +import { CellData, AttributeData, NULL_CELL_CODE, WHITESPACE_CELL_CHAR } from '../../core/buffer/BufferLine'; export const BOLD_CLASS = 'xterm-bold'; export const DIM_CLASS = 'xterm-dim'; diff --git a/src/tsconfig.json b/src/tsconfig.json index f0b1c7490..1e8230a2d 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -24,6 +24,7 @@ ], "references": [ { "path": "./common" }, - { "path": "./core" } + { "path": "./core" }, + { "path": "./ui" } ] } diff --git a/src/ui/Lifecycle.ts b/src/ui/Lifecycle.ts index 9f058106a..4d36a1f12 100644 --- a/src/ui/Lifecycle.ts +++ b/src/ui/Lifecycle.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { IDisposable } from 'xterm'; +import { IDisposable } from '../common/Types'; /** * Adds a disposable listener to a node in the DOM, returning the disposable. @@ -24,8 +24,6 @@ export function addDisposableDomListener( return; } node.removeEventListener(type, handler, useCapture); - node = null; - handler = null; } }; } diff --git a/src/ui/RenderDebouncer.ts b/src/ui/RenderDebouncer.ts index 775b7f748..ee2ab16e7 100644 --- a/src/ui/RenderDebouncer.ts +++ b/src/ui/RenderDebouncer.ts @@ -1,37 +1,39 @@ -import { ITerminal } from '../Types'; -import { IDisposable } from 'xterm'; +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IDisposable } from '../common/Types'; /** * Debounces calls to render terminal rows using animation frames. */ export class RenderDebouncer implements IDisposable { - private _rowStart: number; - private _rowEnd: number; - private _animationFrame: number = null; + private _rowStart: number | undefined; + private _rowEnd: number | undefined; + private _rowCount: number | undefined; + private _animationFrame: number | undefined; constructor( - private _terminal: ITerminal, - private _callback: (start: number, end: number) => void + private _renderCallback: (start: number, end: number) => void ) { } public dispose(): void { if (this._animationFrame) { window.cancelAnimationFrame(this._animationFrame); - this._animationFrame = null; + this._animationFrame = undefined; } } - public refresh(rowStart: number, rowEnd: number): void { + public refresh(rowStart: number, rowEnd: number, rowCount: number): void { + this._rowCount = rowCount; // Get the min/max row start/end for the arg values - rowStart = rowStart !== null && rowStart !== undefined ? rowStart : 0; - rowEnd = rowEnd !== null && rowEnd !== undefined ? rowEnd : this._terminal.rows - 1; - // Check whether the row start/end values have already been set - const isRowStartSet = this._rowStart !== undefined && this._rowStart !== null; - const isRowEndSet = this._rowEnd !== undefined && this._rowEnd !== null; + rowStart = rowStart !== undefined ? rowStart : 0; + rowEnd = rowEnd !== undefined ? rowEnd : this._rowCount - 1; // Set the properties to the updated values - this._rowStart = isRowStartSet ? Math.min(this._rowStart, rowStart) : rowStart; - this._rowEnd = isRowEndSet ? Math.max(this._rowEnd, rowEnd) : rowEnd; + 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; @@ -41,16 +43,21 @@ export class RenderDebouncer implements IDisposable { } private _innerRefresh(): void { + // Make sure values are set + if (this._rowStart === undefined || this._rowEnd === undefined || this._rowCount === undefined) { + return; + } + // Clamp values this._rowStart = Math.max(this._rowStart, 0); - this._rowEnd = Math.min(this._rowEnd, this._terminal.rows - 1); + this._rowEnd = Math.min(this._rowEnd, this._rowCount - 1); // Run render callback - this._callback(this._rowStart, this._rowEnd); + this._renderCallback(this._rowStart, this._rowEnd); // Reset debouncer - this._rowStart = null; - this._rowEnd = null; - this._animationFrame = null; + this._rowStart = undefined; + this._rowEnd = undefined; + this._animationFrame = undefined; } } diff --git a/src/ui/ScreenDprMonitor.ts b/src/ui/ScreenDprMonitor.ts index d66eeb64f..37c6a0dab 100644 --- a/src/ui/ScreenDprMonitor.ts +++ b/src/ui/ScreenDprMonitor.ts @@ -18,10 +18,10 @@ export type ScreenDprListener = (newDevicePixelRatio?: number, oldDevicePixelRat * monitor with a different DPI. */ export class ScreenDprMonitor extends Disposable { - private _currentDevicePixelRatio: number; - private _outerListener: (this: MediaQueryList, ev: MediaQueryListEvent) => any; - private _listener: ScreenDprListener; - private _resolutionMediaMatchList: MediaQueryList; + 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) { @@ -29,6 +29,9 @@ export class ScreenDprMonitor extends Disposable { } this._listener = listener; this._outerListener = () => { + if (!this._listener) { + return; + } this._listener(window.devicePixelRatio, this._currentDevicePixelRatio); this._updateDpr(); }; @@ -41,10 +44,13 @@ export class ScreenDprMonitor extends Disposable { } private _updateDpr(): void { - // Clear listeners for old DPR - if (this._resolutionMediaMatchList) { - this._resolutionMediaMatchList.removeListener(this._outerListener); + if (!this._resolutionMediaMatchList || !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)`); @@ -52,11 +58,12 @@ export class ScreenDprMonitor extends Disposable { } public clearListener(): void { - if (!this._listener) { + if (!this._resolutionMediaMatchList || !this._listener || !this._outerListener) { return; } this._resolutionMediaMatchList.removeListener(this._outerListener); - this._listener = null; - this._outerListener = null; + this._resolutionMediaMatchList = undefined; + this._listener = undefined; + this._outerListener = undefined; } } diff --git a/src/ui/Types.ts b/src/ui/Types.ts deleted file mode 100644 index 89dfa1a4f..000000000 --- a/src/ui/Types.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright (c) 2017 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { IDisposable } from 'xterm'; - -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; -} diff --git a/src/ui/tsconfig.json b/src/ui/tsconfig.json new file mode 100644 index 000000000..b3613eeff --- /dev/null +++ b/src/ui/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../tsconfig-library-base", + "compilerOptions": { + "lib": [ + "dom", + "es5", + ], + "outDir": "../../lib", + "types": [ + "../../node_modules/@types/mocha" + ] + }, + "include": [ + "./Lifecycle.ts", + "./RenderDebouncer.ts", + "./ScreenDprMonitor.ts" + ], + "references": [ + { "path": "../common" } + ] +} \ No newline at end of file diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 2ba8851df..2280e3ad9 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -359,7 +359,7 @@ declare module 'xterm' { * (EXPERIMENTAL) Get all markers registered against the buffer. If the alt * buffer is active this will always return []. */ - readonly markers: IMarker[]; + readonly markers: ReadonlyArray; /** * Natural language strings that can be localized. @@ -594,6 +594,7 @@ declare module 'xterm' { * @return An IDisposable you can call to remove this handler. */ addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable; + /** * (EXPERIMENTAL) Registers a link matcher, allowing custom link patterns to * be matched and handled. @@ -752,7 +753,7 @@ declare module 'xterm' { * Retrieves an option's value from the terminal. * @param key The option key. */ - getOption(key: 'allowTransparency' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'debug' | 'disableStdin' | 'enableBold' | 'macOptionIsMeta' | 'rightClickSelectsWord' | 'popOnBell' | 'screenKeys' | 'useFlowControl' | 'visualBell'): boolean; + getOption(key: 'allowTransparency' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'debug' | 'disableStdin' | 'enableBold' | 'macOptionIsMeta' | 'rightClickSelectsWord' | 'popOnBell' | 'screenKeys' | 'useFlowControl' | 'visualBell' | 'windowsMode'): boolean; /** * Retrieves an option's value from the terminal. * @param key The option key. @@ -803,7 +804,7 @@ declare module 'xterm' { * @param key The option key. * @param value The option value. */ - setOption(key: 'allowTransparency' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'debug' | 'disableStdin' | 'enableBold' | 'macOptionIsMeta' | 'popOnBell' | 'rightClickSelectsWord' | 'screenKeys' | 'useFlowControl' | 'visualBell', value: boolean): void; + setOption(key: 'allowTransparency' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'debug' | 'disableStdin' | 'enableBold' | 'macOptionIsMeta' | 'popOnBell' | 'rightClickSelectsWord' | 'screenKeys' | 'useFlowControl' | 'visualBell' | 'windowsMode', value: boolean): void; /** * Sets an option on the terminal. * @param key The option key.