diff --git a/README.md b/README.md index aa89c700..4707f195 100644 --- a/README.md +++ b/README.md @@ -85,13 +85,9 @@ The xterm.js team maintains the following addons but they can be built by anyone ## Browser Support -Since xterm.js is typically implemented as a developer tool, only modern browsers are supported officially. Here is a list of the versions we aim to support: +Since xterm.js is typically implemented as a developer tool, only modern browsers are supported officially. Specifically the latest versions of *Chrome*, *Edge*, *Firefox* and *Safari*. -- Chrome latest -- Edge latest -- Firefox latest -- Safari latest -- IE11 +We also partially support *Intenet Explorer 11*, meaning xterm.js should work for the most part, but we reserve the right to not provide workarounds specifically for it unless it's absolutely necessary to get the basic input/output flow working. Xterm.js works seamlessly in [Electron](https://electronjs.org/) apps and may even work on earlier versions of the browsers, these are the versions we strive to keep working. diff --git a/addons/xterm-addon-attach/.npmignore b/addons/xterm-addon-attach/.npmignore index e8fc8237..1c794445 100644 --- a/addons/xterm-addon-attach/.npmignore +++ b/addons/xterm-addon-attach/.npmignore @@ -2,3 +2,4 @@ **/*.api.ts tsconfig.json .yarnrc +webpack.config.js diff --git a/addons/xterm-addon-attach/package.json b/addons/xterm-addon-attach/package.json index e381730c..84740e1e 100644 --- a/addons/xterm-addon-attach/package.json +++ b/addons/xterm-addon-attach/package.json @@ -1,15 +1,18 @@ { "name": "xterm-addon-attach", - "version": "0.1.0-beta10", + "version": "0.1.0", "author": { "name": "The xterm.js authors", "url": "https://xtermjs.org/" }, - "main": "lib/AttachAddon.js", + "main": "lib/xterm-addon-attach.js", "types": "typings/xterm-addon-attach.d.ts", "license": "MIT", "scripts": { - "prepublishOnly": "../../node_modules/.bin/tsc -p src" + "build": "../../node_modules/.bin/tsc -p src", + "prepackage": "npm run build", + "package": "../../node_modules/.bin/webpack", + "prepublishOnly": "npm run package" }, "peerDependencies": { "xterm": "^3.14.0" diff --git a/addons/xterm-addon-attach/src/tsconfig.json b/addons/xterm-addon-attach/src/tsconfig.json index 9a92ab48..d875aa53 100644 --- a/addons/xterm-addon-attach/src/tsconfig.json +++ b/addons/xterm-addon-attach/src/tsconfig.json @@ -7,7 +7,7 @@ "es2015" ], "rootDir": ".", - "outDir": "../lib", + "outDir": "../out", "sourceMap": true, "removeComments": true, "strict": true diff --git a/addons/xterm-addon-attach/webpack.config.js b/addons/xterm-addon-attach/webpack.config.js new file mode 100644 index 00000000..65996f19 --- /dev/null +++ b/addons/xterm-addon-attach/webpack.config.js @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +const path = require('path'); + +const addonName = 'AttachAddon'; +const mainFile = 'xterm-addon-attach.js'; + +module.exports = { + entry: `./out/${addonName}.js`, + devtool: 'source-map', + module: { + rules: [ + { + test: /\.js$/, + use: ["source-map-loader"], + enforce: "pre", + exclude: /node_modules/ + } + ] + }, + output: { + filename: mainFile, + path: path.resolve('./lib'), + library: addonName, + libraryTarget: 'umd' + }, + mode: 'production' +}; diff --git a/addons/xterm-addon-fit/.npmignore b/addons/xterm-addon-fit/.npmignore index e8fc8237..1c794445 100644 --- a/addons/xterm-addon-fit/.npmignore +++ b/addons/xterm-addon-fit/.npmignore @@ -2,3 +2,4 @@ **/*.api.ts tsconfig.json .yarnrc +webpack.config.js diff --git a/addons/xterm-addon-fit/package.json b/addons/xterm-addon-fit/package.json index a29f3199..6e553700 100644 --- a/addons/xterm-addon-fit/package.json +++ b/addons/xterm-addon-fit/package.json @@ -1,15 +1,18 @@ { "name": "xterm-addon-fit", - "version": "0.1.0-beta2", + "version": "0.1.0", "author": { "name": "The xterm.js authors", "url": "https://xtermjs.org/" }, - "main": "lib/FitAddon.js", + "main": "lib/xterm-addon-fit.js", "types": "typings/xterm-addon-fit.d.ts", "license": "MIT", "scripts": { - "prepublishOnly": "../../node_modules/.bin/tsc -p src" + "build": "../../node_modules/.bin/tsc -p src", + "prepackage": "npm run build", + "package": "../../node_modules/.bin/webpack", + "prepublishOnly": "npm run package" }, "peerDependencies": { "xterm": "^3.14.0" diff --git a/addons/xterm-addon-fit/src/tsconfig.json b/addons/xterm-addon-fit/src/tsconfig.json index 6b914e81..5539aa56 100644 --- a/addons/xterm-addon-fit/src/tsconfig.json +++ b/addons/xterm-addon-fit/src/tsconfig.json @@ -7,7 +7,7 @@ "es2015" ], "rootDir": ".", - "outDir": "../lib", + "outDir": "../out", "sourceMap": true, "removeComments": true, "strict": true diff --git a/addons/xterm-addon-fit/webpack.config.js b/addons/xterm-addon-fit/webpack.config.js new file mode 100644 index 00000000..4b542150 --- /dev/null +++ b/addons/xterm-addon-fit/webpack.config.js @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +const path = require('path'); + +const addonName = 'FitAddon'; +const mainFile = 'xterm-addon-fit.js'; + +module.exports = { + entry: `./out/${addonName}.js`, + devtool: 'source-map', + module: { + rules: [ + { + test: /\.js$/, + use: ["source-map-loader"], + enforce: "pre", + exclude: /node_modules/ + } + ] + }, + output: { + filename: mainFile, + path: path.resolve('./lib'), + library: addonName, + libraryTarget: 'umd' + }, + mode: 'production' +}; diff --git a/addons/xterm-addon-search/.npmignore b/addons/xterm-addon-search/.npmignore index e8fc8237..1c794445 100644 --- a/addons/xterm-addon-search/.npmignore +++ b/addons/xterm-addon-search/.npmignore @@ -2,3 +2,4 @@ **/*.api.ts tsconfig.json .yarnrc +webpack.config.js diff --git a/addons/xterm-addon-search/package.json b/addons/xterm-addon-search/package.json index 38f76add..5a607fe9 100644 --- a/addons/xterm-addon-search/package.json +++ b/addons/xterm-addon-search/package.json @@ -1,15 +1,18 @@ { "name": "xterm-addon-search", - "version": "0.1.0-beta5", + "version": "0.1.0", "author": { "name": "The xterm.js authors", "url": "https://xtermjs.org/" }, - "main": "lib/SearchAddon.js", + "main": "lib/xterm-addon-search.js", "types": "typings/xterm-addon-search.d.ts", "license": "MIT", "scripts": { - "prepublishOnly": "../../node_modules/.bin/tsc -p src" + "build": "../../node_modules/.bin/tsc -p src", + "prepackage": "npm run build", + "package": "../../node_modules/.bin/webpack", + "prepublishOnly": "npm run package" }, "peerDependencies": { "xterm": "^3.14.0" diff --git a/addons/xterm-addon-search/src/tsconfig.json b/addons/xterm-addon-search/src/tsconfig.json index 58a4bacf..1cb6d4d7 100644 --- a/addons/xterm-addon-search/src/tsconfig.json +++ b/addons/xterm-addon-search/src/tsconfig.json @@ -7,7 +7,7 @@ "es6", ], "rootDir": ".", - "outDir": "../lib", + "outDir": "../out", "sourceMap": true, "removeComments": true, "strict": true diff --git a/addons/xterm-addon-search/webpack.config.js b/addons/xterm-addon-search/webpack.config.js new file mode 100644 index 00000000..726dceb6 --- /dev/null +++ b/addons/xterm-addon-search/webpack.config.js @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +const path = require('path'); + +const addonName = 'SearchAddon'; +const mainFile = 'xterm-addon-search.js'; + +module.exports = { + entry: `./out/${addonName}.js`, + devtool: 'source-map', + module: { + rules: [ + { + test: /\.js$/, + use: ["source-map-loader"], + enforce: "pre", + exclude: /node_modules/ + } + ] + }, + output: { + filename: mainFile, + path: path.resolve('./lib'), + library: addonName, + libraryTarget: 'umd' + }, + mode: 'production' +}; diff --git a/addons/xterm-addon-web-links/.npmignore b/addons/xterm-addon-web-links/.npmignore index e8fc8237..1c794445 100644 --- a/addons/xterm-addon-web-links/.npmignore +++ b/addons/xterm-addon-web-links/.npmignore @@ -2,3 +2,4 @@ **/*.api.ts tsconfig.json .yarnrc +webpack.config.js diff --git a/addons/xterm-addon-web-links/package.json b/addons/xterm-addon-web-links/package.json index 82edbe9b..fd3945d3 100644 --- a/addons/xterm-addon-web-links/package.json +++ b/addons/xterm-addon-web-links/package.json @@ -1,15 +1,18 @@ { "name": "xterm-addon-web-links", - "version": "0.1.0-beta9", + "version": "0.1.0", "author": { "name": "The xterm.js authors", "url": "https://xtermjs.org/" }, - "main": "lib/WebLinksAddon.js", + "main": "lib/xterm-addon-web-links.js", "types": "typings/xterm-addon-web-links.d.ts", "license": "MIT", "scripts": { - "prepublishOnly": "../../node_modules/.bin/tsc -p src" + "build": "../../node_modules/.bin/tsc -p src", + "prepackage": "npm run build", + "package": "../../node_modules/.bin/webpack", + "prepublishOnly": "npm run package" }, "peerDependencies": { "xterm": "^3.14.0" diff --git a/addons/xterm-addon-web-links/src/tsconfig.json b/addons/xterm-addon-web-links/src/tsconfig.json index 6b914e81..5539aa56 100644 --- a/addons/xterm-addon-web-links/src/tsconfig.json +++ b/addons/xterm-addon-web-links/src/tsconfig.json @@ -7,7 +7,7 @@ "es2015" ], "rootDir": ".", - "outDir": "../lib", + "outDir": "../out", "sourceMap": true, "removeComments": true, "strict": true diff --git a/addons/xterm-addon-web-links/webpack.config.js b/addons/xterm-addon-web-links/webpack.config.js new file mode 100644 index 00000000..fd87d07e --- /dev/null +++ b/addons/xterm-addon-web-links/webpack.config.js @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +const path = require('path'); + +const addonName = 'WebLinksAddon'; +const mainFile = 'xterm-addon-web-links.js'; + +module.exports = { + entry: `./out/${addonName}.js`, + devtool: 'source-map', + module: { + rules: [ + { + test: /\.js$/, + use: ["source-map-loader"], + enforce: "pre", + exclude: /node_modules/ + } + ] + }, + output: { + filename: mainFile, + path: path.resolve('./lib'), + library: addonName, + libraryTarget: 'umd' + }, + mode: 'production' +}; diff --git a/bin/publish.js b/bin/publish.js index 9df2e982..e03fa6ab 100644 --- a/bin/publish.js +++ b/bin/publish.js @@ -6,33 +6,76 @@ const cp = require('child_process'); const fs = require('fs'); const path = require('path'); -const packageJson = require('../package.json'); // Setup auth fs.writeFileSync(`${process.env['HOME']}/.npmrc`, `//registry.npmjs.org/:_authToken=${process.env['NPM_AUTH_TOKEN']}`); -// 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 -const packageJsonFile = path.resolve(__dirname, '..', 'package.json'); -packageJson.version = nextVersion; -fs.writeFileSync(packageJsonFile, JSON.stringify(packageJson, null, 2)); - -// Publish -const args = ['publish']; -if (!isStableRelease) { - args.push('--tag', 'beta'); +const isDryRun = process.argv.indexOf('--dry') !== -1; +if (isDryRun) { + console.log('Publish dry run'); } -const result = cp.spawn('npm', args, { stdio: 'inherit' }); -result.on('exit', code => process.exit(code)); -function getNextBetaVersion() { +const changedFiles = getChangedFilesInCommit('HEAD'); + +// Publish xterm if any files were changed outside of the addons directory +if (changedFiles.some(e => e.search(/^addons\//) === -1)) { + checkAndPublishPackage(path.resolve(__dirname, '..')); +} + +// Publish addons if any files were changed inside of the addon +const addonPackageDirs = [ + path.resolve(__dirname, '../addons/xterm-addon-attach'), + path.resolve(__dirname, '../addons/xterm-addon-fit'), + path.resolve(__dirname, '../addons/xterm-addon-search'), + path.resolve(__dirname, '../addons/xterm-addon-web-links') +]; +addonPackageDirs.forEach(p => { + const addon = path.basename(p); + if (changedFiles.some(e => e.indexOf(addon) !== -1)) { + checkAndPublishPackage(p); + } +}); + +function checkAndPublishPackage(packageDir) { + const packageJson = require(path.join(packageDir, 'package.json')); + + // Determine if this is a stable or beta release + const publishedVersions = getPublishedVersions(packageJson); + const isStableRelease = publishedVersions.indexOf(packageJson.version) === -1; + + // Get the next version + let nextVersion = isStableRelease ? packageJson.version : getNextBetaVersion(packageJson); + console.log(`Publishing version: ${nextVersion}`); + + // Set the version in package.json + const packageJsonFile = path.join(packageDir, 'package.json'); + packageJson.version = nextVersion; + console.log(`Set version of ${packageJsonFile} to ${nextVersion}`); + if (!isDryRun) { + fs.writeFileSync(packageJsonFile, JSON.stringify(packageJson, null, 2)); + } + + // Publish + const args = ['publish']; + if (!isStableRelease) { + args.push('--tag', 'beta'); + } + console.log(`Spawn: npm ${args.join(' ')}`); + if (!isDryRun) { + const result = cp.spawnSync('npm', args, { + cwd: packageDir, + stdio: 'inherit' + }); + if (result.status) { + console.error(`Spawn exited with code ${result.status}`); + process.exit(result.status); + } + } + + console.groupEnd(); +} + +function getNextBetaVersion(packageJson) { 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); @@ -40,7 +83,7 @@ function getNextBetaVersion() { const tag = 'beta'; const stableVersion = packageJson.version.split('.'); const nextStableVersion = `${stableVersion[0]}.${parseInt(stableVersion[1]) + 1}.0`; - const publishedVersions = getPublishedVersions(nextStableVersion, tag); + const publishedVersions = getPublishedVersions(packageJson, nextStableVersion, tag); if (publishedVersions.length === 0) { return `${nextStableVersion}-${tag}1`; } @@ -53,7 +96,7 @@ function getNextBetaVersion() { return `${nextStableVersion}-${tag}${latestTagVersion + 1}`; } -function getPublishedVersions(version, tag) { +function getPublishedVersions(packageJson, version, tag) { const versionsProcess = cp.spawnSync('npm', ['view', packageJson.name, 'versions', '--json']); const versionsJson = JSON.parse(versionsProcess.stdout); if (tag) { @@ -61,3 +104,11 @@ function getPublishedVersions(version, tag) { } return versionsJson; } + +function getChangedFilesInCommit(commit) { + const args = ['log', '-m', '-1', '--name-only', `--pretty=format:`, commit]; + const result = cp.spawnSync('git', args); + const output = result.stdout.toString(); + const changedFiles = output.split('\n').filter(e => e.length > 0); + return changedFiles; +} diff --git a/demo/client.ts b/demo/client.ts index f1626c4f..e0d55d57 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -9,13 +9,17 @@ // Use tsc version (yarn watch) import { Terminal } from '../out/public/Terminal'; +import { AttachAddon } from '../addons/xterm-addon-attach/out/AttachAddon'; +import { FitAddon } from '../addons/xterm-addon-fit/out/FitAddon'; +import { SearchAddon, ISearchOptions } from '../addons/xterm-addon-search/out/SearchAddon'; +import { WebLinksAddon } from '../addons/xterm-addon-web-links/out/WebLinksAddon'; + // Use webpacked version (yarn package) // import { Terminal } from '../lib/xterm'; - -import { AttachAddon } from 'xterm-addon-attach'; -import { FitAddon } from 'xterm-addon-fit'; -import { SearchAddon, ISearchOptions } from 'xterm-addon-search'; -import { WebLinksAddon } from 'xterm-addon-web-links'; +// import { AttachAddon } from 'xterm-addon-attach'; +// import { FitAddon } from 'xterm-addon-fit'; +// import { SearchAddon, ISearchOptions } from 'xterm-addon-search'; +// import { WebLinksAddon } from 'xterm-addon-web-links'; // Pulling in the module's types relies on the above, it's looks a // little weird here as we're importing "this" module diff --git a/package.json b/package.json index 9cb9e1aa..5d7cb5b4 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "description": "Full xterm terminal, in your browser", "version": "3.14.0", "main": "lib/xterm.js", + "style": "css/xterm.css", "types": "typings/xterm.d.ts", "repository": "https://github.com/xtermjs/xterm.js", "license": "MIT", diff --git a/src/Clipboard.ts b/src/Clipboard.ts index cbb0935e..9461afa1 100644 --- a/src/Clipboard.ts +++ b/src/Clipboard.ts @@ -5,15 +5,6 @@ import { ITerminal, ISelectionManager } from './Types'; -interface IWindow extends Window { - clipboardData?: { - getData(format: string): string; - setData(format: string, data: string): void; - }; -} - -declare var window: IWindow; - /** * Prepares text to be pasted into the terminal by normalizing the line endings * @param text The pasted text that needs processing before inserting into the terminal @@ -38,12 +29,7 @@ export function bracketTextForPaste(text: string, bracketedPasteMode: boolean): * @param ev The original copy event to be handled */ export function copyHandler(ev: ClipboardEvent, term: ITerminal, selectionManager: ISelectionManager): void { - if (term.browser.isMSIE) { - window.clipboardData.setData('Text', selectionManager.selectionText); - } else { - ev.clipboardData.setData('text/plain', selectionManager.selectionText); - } - + ev.clipboardData.setData('text/plain', selectionManager.selectionText); // Prevent or the original text will be copied. ev.preventDefault(); } @@ -66,16 +52,9 @@ export function pasteHandler(ev: ClipboardEvent, term: ITerminal): void { term.cancel(ev); }; - if (term.browser.isMSIE) { - if (window.clipboardData) { - text = window.clipboardData.getData('Text'); - dispatchPaste(text); - } - } else { - if (ev.clipboardData) { - text = ev.clipboardData.getData('text/plain'); - dispatchPaste(text); - } + if (ev.clipboardData) { + text = ev.clipboardData.getData('text/plain'); + dispatchPaste(text); } } diff --git a/src/InputHandler.test.ts b/src/InputHandler.test.ts index 3e5cef57..19d5f6a2 100644 --- a/src/InputHandler.test.ts +++ b/src/InputHandler.test.ts @@ -8,13 +8,17 @@ import { InputHandler } from './InputHandler'; import { MockInputHandlingTerminal, TestTerminal } from './TestUtils.test'; import { Terminal } from './Terminal'; import { IBufferLine } from 'common/Types'; -import { CellData, Attributes, AttributeData, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; +import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; +import { CellData } from 'common/buffer/CellData'; +import { Attributes } from 'common/buffer/Constants'; +import { AttributeData } from 'common/buffer/AttributeData'; describe('InputHandler', () => { describe('save and restore cursor', () => { const terminal = new MockInputHandlingTerminal(); terminal.buffer.x = 1; terminal.buffer.y = 2; + terminal.buffer.ybase = 0; terminal.curAttrData.fg = 3; const inputHandler = new InputHandler(terminal); // Save cursor position diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 5ea20caf..a2ae4e08 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -13,9 +13,12 @@ import { IDisposable } from 'xterm'; import { Disposable } from 'common/Lifecycle'; import { concat } from 'common/TypedArrayUtils'; import { StringToUtf32, stringFromCodePoint, utf32ToString, Utf8ToUtf32 } from 'common/input/TextDecoder'; -import { CellData, Attributes, FgFlags, BgFlags, AttributeData, NULL_CELL_WIDTH, NULL_CELL_CODE, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; +import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; import { EventEmitter, IEvent } from 'common/EventEmitter'; import { IParsingState, IDcsHandler, IEscapeSequenceParser } from 'common/parser/Types'; +import { NULL_CELL_CODE, NULL_CELL_WIDTH, Attributes, FgFlags, BgFlags } from 'common/buffer/Constants'; +import { CellData } from 'common/buffer/CellData'; +import { AttributeData } from 'common/buffer/AttributeData'; /** * Map collect to glevel. Used in `selectCharset`. @@ -1916,7 +1919,7 @@ export class InputHandler extends Disposable implements IInputHandler { */ public saveCursor(params: number[]): void { this._terminal.buffer.savedX = this._terminal.buffer.x; - this._terminal.buffer.savedY = this._terminal.buffer.y; + this._terminal.buffer.savedY = this._terminal.buffer.ybase + this._terminal.buffer.y; this._terminal.buffer.savedCurAttrData.fg = this._terminal.curAttrData.fg; this._terminal.buffer.savedCurAttrData.bg = this._terminal.curAttrData.bg; } @@ -1929,7 +1932,7 @@ export class InputHandler extends Disposable implements IInputHandler { */ public restoreCursor(params: number[]): void { this._terminal.buffer.x = this._terminal.buffer.savedX || 0; - this._terminal.buffer.y = this._terminal.buffer.savedY || 0; + this._terminal.buffer.y = Math.max(this._terminal.buffer.savedY - this._terminal.buffer.ybase, 0); this._terminal.curAttrData.fg = this._terminal.buffer.savedCurAttrData.fg; this._terminal.curAttrData.bg = this._terminal.buffer.savedCurAttrData.bg; } diff --git a/src/Linkifier.test.ts b/src/Linkifier.test.ts index 8b7e71a0..1ccae7fc 100644 --- a/src/Linkifier.test.ts +++ b/src/Linkifier.test.ts @@ -9,7 +9,8 @@ import { IBufferLine } from 'common/Types'; import { Linkifier } from './Linkifier'; import { MockBuffer, MockTerminal, TestTerminal } from './TestUtils.test'; import { CircularList } from 'common/CircularList'; -import { BufferLine, CellData } from 'common/buffer/BufferLine'; +import { BufferLine } from 'common/buffer/BufferLine'; +import { CellData } from 'common/buffer/CellData'; class TestLinkifier extends Linkifier { constructor(terminal: ITerminal) { diff --git a/src/MouseZoneManager.ts b/src/MouseZoneManager.ts index b2ee9b14..de724b88 100644 --- a/src/MouseZoneManager.ts +++ b/src/MouseZoneManager.ts @@ -6,6 +6,7 @@ import { ITerminal, IMouseZoneManager, IMouseZone } from './Types'; import { Disposable } from 'common/Lifecycle'; import { addDisposableDomListener } from 'browser/Lifecycle'; +import { IMouseService } from 'browser/services/Services'; const HOVER_DURATION = 500; @@ -31,7 +32,8 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager { private _initialSelectionLength: number; constructor( - private _terminal: ITerminal + private _terminal: ITerminal, + private _mouseService: IMouseService ) { super(); @@ -203,7 +205,7 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager { } private _findZoneEventAt(e: MouseEvent): IMouseZone { - const coords = this._terminal.mouseHelper.getCoords(e, this._terminal.screenElement, this._terminal.cols, this._terminal.rows); + const coords = this._mouseService.getCoords(e, this._terminal.screenElement, this._terminal.cols, this._terminal.rows); if (!coords) { return null; } diff --git a/src/SelectionManager.test.ts b/src/SelectionManager.test.ts index 5dc692af..8499497a 100644 --- a/src/SelectionManager.test.ts +++ b/src/SelectionManager.test.ts @@ -12,9 +12,10 @@ import { IBuffer } from 'common/buffer/Types'; import { IBufferLine } from 'common/Types'; import { MockTerminal } from './TestUtils.test'; import { MockOptionsService, MockBufferService } from 'common/TestUtils.test'; -import { BufferLine, CellData } from 'common/buffer/BufferLine'; +import { BufferLine } from 'common/buffer/BufferLine'; import { IBufferService } from 'common/services/Services'; -import { MockCharSizeService } from 'browser/TestUtils.test'; +import { MockCharSizeService, MockMouseService } from 'browser/TestUtils.test'; +import { CellData } from 'common/buffer/CellData'; class TestMockTerminal extends MockTerminal { emit(event: string, data: any): void {} @@ -25,7 +26,7 @@ class TestSelectionManager extends SelectionManager { terminal: ITerminal, bufferService: IBufferService ) { - super(terminal, new MockCharSizeService(10, 10), bufferService); + super(terminal, new MockCharSizeService(10, 10), bufferService, new MockMouseService()); } public get model(): SelectionModel { return this._model; } diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index 4b6a0de0..aa388d8a 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -6,15 +6,15 @@ import { ITerminal, ISelectionManager, ISelectionRedrawRequestEvent } from './Types'; import { IBuffer } from 'common/buffer/Types'; import { IBufferLine } from 'common/Types'; -import { MouseHelper } from 'browser/input/MouseHelper'; import * as Browser from 'common/Platform'; import { SelectionModel } from './SelectionModel'; import { AltClickHandler } from './handlers/AltClickHandler'; -import { CellData } from 'common/buffer/BufferLine'; +import { CellData } from 'common/buffer/CellData'; import { IDisposable } from 'xterm'; import { EventEmitter, IEvent } from 'common/EventEmitter'; -import { ICharSizeService } from 'browser/services/Services'; +import { ICharSizeService, IMouseService } from 'browser/services/Services'; import { IBufferService } from 'common/services/Services'; +import { getCoordsRelativeToElement } from 'browser/input/Mouse'; /** * The number of pixels the mouse needs to be above or below the viewport in @@ -118,9 +118,10 @@ export class SelectionManager implements ISelectionManager { public get onSelectionChange(): IEvent { return this._onSelectionChange.event; } constructor( - private _terminal: ITerminal, - private _charSizeService: ICharSizeService, - bufferService: IBufferService + private readonly _terminal: ITerminal, + private readonly _charSizeService: ICharSizeService, + readonly bufferService: IBufferService, + private readonly _mouseService: IMouseService ) { this._initListeners(); this.enable(); @@ -357,7 +358,7 @@ export class SelectionManager implements ISelectionManager { * @param event The mouse event. */ private _getMouseBufferCoords(event: MouseEvent): [number, number] { - const coords = this._terminal.mouseHelper.getCoords(event, this._terminal.screenElement, this._terminal.cols, this._terminal.rows, true); + const coords = this._mouseService.getCoords(event, this._terminal.screenElement, this._terminal.cols, this._terminal.rows, true); if (!coords) { return null; } @@ -377,7 +378,7 @@ export class SelectionManager implements ISelectionManager { * @param event The mouse event. */ private _getMouseEventScrollAmount(event: MouseEvent): number { - let offset = MouseHelper.getCoordsRelativeToElement(event, this._terminal.screenElement)[1]; + let offset = getCoordsRelativeToElement(event, this._terminal.screenElement)[1]; const terminalHeight = this._terminal.rows * Math.ceil(this._charSizeService.height * this._terminal.options.lineHeight); if (offset >= 0 && offset <= terminalHeight) { return 0; @@ -654,7 +655,7 @@ export class SelectionManager implements ISelectionManager { this._removeMouseDownListeners(); if (this.selectionText.length <= 1 && timeElapsed < ALT_CLICK_MOVE_CURSOR_TIME) { - (new AltClickHandler(event, this._terminal)).move(); + (new AltClickHandler(event, this._terminal, this._mouseService)).move(); } else if (this.hasSelection) { this._onSelectionChange.fire(); } diff --git a/src/Terminal.test.ts b/src/Terminal.test.ts index 36886da1..f3f7fc58 100644 --- a/src/Terminal.test.ts +++ b/src/Terminal.test.ts @@ -6,7 +6,8 @@ import { assert, expect } from 'chai'; import { Terminal } from './Terminal'; import { MockViewport, MockCompositionHelper, MockRenderer } from './TestUtils.test'; -import { CellData, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; +import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; +import { CellData } from 'common/buffer/CellData'; const INIT_COLS = 80; const INIT_ROWS = 24; diff --git a/src/Terminal.ts b/src/Terminal.ts index d6093316..59346505 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -34,7 +34,6 @@ import { SelectionManager } from './SelectionManager'; import * as Browser from 'common/Platform'; import { addDisposableDomListener } from 'browser/Lifecycle'; import * as Strings from './browser/LocalizableStrings'; -import { MouseHelper } from 'browser/input/MouseHelper'; import { SoundManager } from './SoundManager'; import { MouseZoneManager } from './MouseZoneManager'; import { AccessibilityManager } from './AccessibilityManager'; @@ -44,17 +43,19 @@ import { DomRenderer } from './renderer/dom/DomRenderer'; import { IKeyboardEvent, KeyboardResultType, ICharset, IBufferLine, IAttributeData } from 'common/Types'; import { evaluateKeyboardEvent } from 'common/input/Keyboard'; import { EventEmitter, IEvent } from 'common/EventEmitter'; -import { Attributes, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; +import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; import { applyWindowsMode } from './WindowsMode'; import { ColorManager } from 'browser/ColorManager'; import { RenderService } from 'browser/services/RenderService'; import { IOptionsService, IBufferService } from 'common/services/Services'; import { OptionsService } from 'common/services/OptionsService'; -import { ICharSizeService } from 'browser/services/Services'; +import { ICharSizeService, IRenderService, IMouseService } from 'browser/services/Services'; import { CharSizeService } from 'browser/services/CharSizeService'; import { BufferService, MINIMUM_COLS, MINIMUM_ROWS } from 'common/services/BufferService'; import { Disposable } from 'common/Lifecycle'; import { IBufferSet, IBuffer } from 'common/buffer/Types'; +import { Attributes } from 'common/buffer/Constants'; +import { MouseService } from 'browser/services/MouseService'; // Let it work inside Node.js for automated testing purposes. const document = (typeof window !== 'undefined') ? window.document : null; @@ -110,7 +111,8 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp // browser services private _charSizeService: ICharSizeService; - private _renderService: RenderService; + private _renderService: IRenderService; + private _mouseService: IMouseService; // modes public applicationKeypad: boolean; @@ -176,7 +178,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp public viewport: IViewport; private _compositionHelper: ICompositionHelper; private _mouseZoneManager: IMouseZoneManager; - public mouseHelper: MouseHelper; private _accessibilityManager: AccessibilityManager; private _colorManager: ColorManager; private _theme: ITheme; @@ -590,11 +591,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this.screenElement.appendChild(this._helperContainer); fragment.appendChild(this.screenElement); - this._mouseZoneManager = new MouseZoneManager(this); - this.register(this._mouseZoneManager); - this.register(this.onScroll(() => this._mouseZoneManager.clearAll())); - this.linkifier.attachToDom(this._mouseZoneManager); - this.textarea = document.createElement('textarea'); this.textarea.classList.add('xterm-helper-textarea'); this.textarea.setAttribute('aria-label', Strings.promptLabel); @@ -627,6 +623,13 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this._renderService.onRender(e => this._onRender.fire(e)); this.onResize(e => this._renderService.resize(e.cols, e.rows)); + this._mouseService = new MouseService(this._renderService, this._charSizeService); + + this._mouseZoneManager = new MouseZoneManager(this, this._mouseService); + this.register(this._mouseZoneManager); + this.register(this.onScroll(() => this._mouseZoneManager.clearAll())); + this.linkifier.attachToDom(this._mouseZoneManager); + this.viewport = new Viewport(this, this._viewportElement, this._viewportScrollArea, this._renderService.dimensions, this._charSizeService); this.viewport.onThemeChange(this._colorManager.colors); this.register(this.viewport); @@ -637,7 +640,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this.register(this.onFocus(() => this._renderService.onFocus())); this.register(this._renderService.onDimensionsChange(() => this.viewport.syncScrollArea())); - this.selectionManager = new SelectionManager(this, this._charSizeService, this._bufferService); + this.selectionManager = new SelectionManager(this, this._charSizeService, this._bufferService, this._mouseService); this.register(this.selectionManager.onSelectionChange(() => this._onSelectionChange.fire())); this.register(addDisposableDomListener(this.element, 'mousedown', (e: MouseEvent) => this.selectionManager.onMouseDown(e))); this.register(this.selectionManager.onRedrawRequest(e => this._renderService.onSelectionChanged(e.start, e.end, e.columnSelectMode))); @@ -655,7 +658,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp })); this.register(addDisposableDomListener(this._viewportElement, 'scroll', () => this.selectionManager.refresh())); - this.mouseHelper = new MouseHelper(this._renderService, this._charSizeService); // apply mouse event classes set by escape codes before terminal was attached this.element.classList.toggle('enable-mouse-events', this.mouseEvents); if (this.mouseEvents) { @@ -700,7 +702,9 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp */ private _setTheme(theme: ITheme): void { this._theme = theme; - this._colorManager.setTheme(theme); + if (this._colorManager) { + this._colorManager.setTheme(theme); + } if (this._renderService) { this._renderService.setColors(this._colorManager.colors); } @@ -735,7 +739,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp button = getButton(ev); // get mouse coordinates - pos = self.mouseHelper.getRawByteCoords(ev, self.screenElement, self.cols, self.rows); + pos = self._mouseService.getRawByteCoords(ev, self.screenElement, self.cols, self.rows); if (!pos) return; sendEvent(button, pos); @@ -761,7 +765,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp // ^[[M 3<^[[M@4<^[[M@5<^[[M@6<^[[M@7<^[[M#7< function sendMove(ev: MouseEvent): void { let button = pressed; - const pos = self.mouseHelper.getRawByteCoords(ev, self.screenElement, self.cols, self.rows); + const pos = self._mouseService.getRawByteCoords(ev, self.screenElement, self.cols, self.rows); if (!pos) return; // buttons marked as motions @@ -890,10 +894,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp : ev.which !== null && ev.which !== undefined ? ev.which - 1 : null; - - if (Browser.isMSIE) { - button = button === 1 ? 0 : button === 4 ? 1 : button; - } break; case 'mouseup': button = 3; diff --git a/src/Terminal2.test.ts b/src/Terminal2.test.ts index 0fc2f461..c8164c54 100644 --- a/src/Terminal2.test.ts +++ b/src/Terminal2.test.ts @@ -12,7 +12,8 @@ import * as path from 'path'; import * as pty from 'node-pty'; import { Terminal } from './Terminal'; import { IViewport } from './Types'; -import { CellData, WHITESPACE_CELL_CHAR } from 'common/buffer/BufferLine'; +import { CellData } from 'common/buffer/CellData'; +import { WHITESPACE_CELL_CHAR } from 'common/buffer/Constants'; class TestTerminal extends Terminal { innerWrite(): void { this._innerWrite(); } diff --git a/src/TestUtils.test.ts b/src/TestUtils.test.ts index 6b92bf0b..07f91705 100644 --- a/src/TestUtils.test.ts +++ b/src/TestUtils.test.ts @@ -11,8 +11,8 @@ import { Buffer } from 'common/buffer/Buffer'; import * as Browser from 'common/Platform'; import { IDisposable, IMarker, IEvent, ISelectionPosition } from 'xterm'; import { Terminal } from './Terminal'; -import { AttributeData } from 'common/buffer/BufferLine'; -import { IColorManager, IColorSet, IMouseHelper } from 'browser/Types'; +import { AttributeData } from 'common/buffer/AttributeData'; +import { IColorManager, IColorSet } from 'browser/Types'; import { IOptionsService } from 'common/services/Services'; import { EventEmitter } from 'common/EventEmitter'; @@ -122,7 +122,6 @@ export class MockTerminal implements ITerminal { throw new Error('Method not implemented.'); } bracketedPasteMode: boolean; - mouseHelper: IMouseHelper; renderer: IRenderer; linkifier: ILinkifier; isFocused: boolean; diff --git a/src/Types.ts b/src/Types.d.ts similarity index 98% rename from src/Types.ts rename to src/Types.d.ts index a3435614..5987d605 100644 --- a/src/Types.ts +++ b/src/Types.d.ts @@ -5,8 +5,8 @@ import { ITerminalOptions as IPublicTerminalOptions, IDisposable, IMarker, ISelectionPosition } from 'xterm'; import { ICharset, IAttributeData, CharData } from 'common/Types'; -import { IEvent, EventEmitter } from 'common/EventEmitter'; -import { IColorSet, IMouseHelper } from 'browser/Types'; +import { IEvent, IEventEmitter } from 'common/EventEmitter'; +import { IColorSet } from 'browser/Types'; import { IOptionsService } from 'common/services/Services'; import { IBuffer, IBufferSet } from 'common/buffer/Types'; @@ -54,8 +54,8 @@ export interface IInputHandlingTerminal { viewport: IViewport; selectionManager: ISelectionManager; - onA11yCharEmitter: EventEmitter; - onA11yTabEmitter: EventEmitter; + onA11yCharEmitter: IEventEmitter; + onA11yTabEmitter: IEventEmitter; bell(): void; focus(): void; @@ -204,7 +204,6 @@ export interface ITerminal extends IPublicTerminal, IElementAccessor, IBufferAcc buffer: IBuffer; buffers: IBufferSet; isFocused: boolean; - mouseHelper: IMouseHelper; viewport: IViewport; bracketedPasteMode: boolean; applicationCursor: boolean; @@ -365,7 +364,6 @@ export interface IBrowser { userAgent: string; platform: string; isFirefox: boolean; - isMSIE: boolean; isMac: boolean; isIpad: boolean; isIphone: boolean; diff --git a/src/WindowsMode.ts b/src/WindowsMode.ts index cee31de1..33b4972b 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 'common/buffer/BufferLine'; +import { CHAR_DATA_CODE_INDEX, NULL_CELL_CODE, WHITESPACE_CELL_CODE } from 'common/buffer/Constants'; export function applyWindowsMode(terminal: ITerminal): IDisposable { // Winpty does not support wraparound mode which means that lines will never diff --git a/src/browser/TestUtils.test.ts b/src/browser/TestUtils.test.ts index b286295b..d89dc391 100644 --- a/src/browser/TestUtils.test.ts +++ b/src/browser/TestUtils.test.ts @@ -4,7 +4,7 @@ */ import { IEvent, EventEmitter } from 'common/EventEmitter'; -import { ICharSizeService } from 'browser/services/Services'; +import { ICharSizeService, IMouseService } from 'browser/services/Services'; export class MockCharSizeService implements ICharSizeService { get hasValidSize(): boolean { return this.width > 0 && this.height > 0; } @@ -12,3 +12,13 @@ export class MockCharSizeService implements ICharSizeService { constructor(public width: number, public height: number) {} measure(): void {} } + +export class MockMouseService implements IMouseService { + public getCoords(event: {clientX: number, clientY: number}, element: HTMLElement, colCount: number, rowCount: number, isSelection?: boolean): [number, number] | undefined { + throw new Error('Not implemented'); + } + + public getRawByteCoords(event: MouseEvent, element: HTMLElement, colCount: number, rowCount: number): { x: number, y: number } | undefined { + throw new Error('Not implemented'); + } +} diff --git a/src/browser/Types.ts b/src/browser/Types.d.ts similarity index 53% rename from src/browser/Types.ts rename to src/browser/Types.d.ts index a1ea662c..ef725ba6 100644 --- a/src/browser/Types.ts +++ b/src/browser/Types.d.ts @@ -20,8 +20,3 @@ export interface IColorSet { selection: IColor; ansi: IColor[]; } - -export interface IMouseHelper { - getCoords(event: { clientX: number, clientY: number }, element: HTMLElement, colCount: number, rowCount: number, isSelection?: boolean): [number, number] | undefined; - getRawByteCoords(event: MouseEvent, element: HTMLElement, colCount: number, rowCount: number): { x: number | undefined, y: number | undefined }; -} diff --git a/src/browser/input/Mouse.test.ts b/src/browser/input/Mouse.test.ts new file mode 100644 index 00000000..6a908499 --- /dev/null +++ b/src/browser/input/Mouse.test.ts @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import jsdom = require('jsdom'); +import { assert } from 'chai'; +import { getCoords } from 'browser/input/Mouse'; + +const CHAR_WIDTH = 10; +const CHAR_HEIGHT = 20; + +describe('Mouse getCoords', () => { + let document: Document; + + beforeEach(() => { + document = new jsdom.JSDOM('').window.document; + }); + + it('should return the cell that was clicked', () => { + let coords: [number, number] | undefined; + coords = getCoords({ clientX: CHAR_WIDTH / 2, clientY: CHAR_HEIGHT / 2 }, document.createElement('div'), 10, 10, true, CHAR_WIDTH, CHAR_HEIGHT); + assert.deepEqual(coords, [1, 1]); + coords = getCoords({ clientX: CHAR_WIDTH, clientY: CHAR_HEIGHT }, document.createElement('div'), 10, 10, true, CHAR_WIDTH, CHAR_HEIGHT); + assert.deepEqual(coords, [1, 1]); + coords = getCoords({ clientX: CHAR_WIDTH, clientY: CHAR_HEIGHT + 1 }, document.createElement('div'), 10, 10, true, CHAR_WIDTH, CHAR_HEIGHT); + assert.deepEqual(coords, [1, 2]); + coords = getCoords({ clientX: CHAR_WIDTH + 1, clientY: CHAR_HEIGHT }, document.createElement('div'), 10, 10, true, CHAR_WIDTH, CHAR_HEIGHT); + assert.deepEqual(coords, [2, 1]); + }); + + it('should ensure the coordinates are returned within the terminal bounds', () => { + let coords: [number, number] | undefined; + coords = getCoords({ clientX: -1, clientY: -1 }, document.createElement('div'), 10, 10, true, CHAR_WIDTH, CHAR_HEIGHT); + assert.deepEqual(coords, [1, 1]); + // Event are double the cols/rows + coords = getCoords({ clientX: CHAR_WIDTH * 20, clientY: CHAR_HEIGHT * 20 }, document.createElement('div'), 10, 10, true, CHAR_WIDTH, CHAR_HEIGHT); + assert.deepEqual(coords, [10, 10], 'coordinates should never come back as larger than the terminal'); + }); +}); diff --git a/src/browser/input/Mouse.ts b/src/browser/input/Mouse.ts new file mode 100644 index 00000000..2986fb3c --- /dev/null +++ b/src/browser/input/Mouse.ts @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +export function getCoordsRelativeToElement(event: {clientX: number, clientY: number}, element: HTMLElement): [number, number] { + const rect = element.getBoundingClientRect(); + return [event.clientX - rect.left, event.clientY - rect.top]; +} + +/** + * Gets coordinates within the terminal for a particular mouse event. The result + * is returned as an array in the form [x, y] instead of an object as it's a + * little faster and this function is used in some low level code. + * @param event The mouse event. + * @param element The terminal's container element. + * @param colCount The number of columns in the terminal. + * @param rowCount The number of rows n the terminal. + * @param isSelection Whether the request is for the selection or not. This will + * apply an offset to the x value such that the left half of the cell will + * select that cell and the right half will select the next cell. + */ +export function getCoords(event: {clientX: number, clientY: number}, element: HTMLElement, colCount: number, rowCount: number, hasValidCharSize: boolean, actualCellWidth: number, actualCellHeight: number, isSelection?: boolean): [number, number] | undefined { + // Coordinates cannot be measured if there are no valid + if (!hasValidCharSize) { + return undefined; + } + + const coords = getCoordsRelativeToElement(event, element); + if (!coords) { + return undefined; + } + + coords[0] = Math.ceil((coords[0] + (isSelection ? actualCellWidth / 2 : 0)) / actualCellWidth); + coords[1] = Math.ceil(coords[1] / actualCellHeight); + + // Ensure coordinates are within the terminal viewport. Note that selections + // need an addition point of precision to cover the end point (as characters + // cover half of one char and half of the next). + coords[0] = Math.min(Math.max(coords[0], 1), colCount + (isSelection ? 1 : 0)); + coords[1] = Math.min(Math.max(coords[1], 1), rowCount); + + return coords; +} + +/** + * Gets coordinates within the terminal for a particular mouse event, wrapping + * them to the bounds of the terminal and adding 32 to both the x and y values + * as expected by xterm. + */ +export function getRawByteCoords(coords: [number, number] | undefined): { x: number, y: number } | undefined { + if (!coords) { + return undefined; + } + + // xterm sends raw bytes and starts at 32 (SP) for each. + return { x: coords[0] + 32, y: coords[1] + 32 }; +} diff --git a/src/browser/input/MouseHelper.test.ts b/src/browser/input/MouseHelper.test.ts deleted file mode 100644 index 5d4b567c..00000000 --- a/src/browser/input/MouseHelper.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Copyright (c) 2017 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import jsdom = require('jsdom'); -import { assert } from 'chai'; -import { MouseHelper } from 'browser/input/MouseHelper'; -import { MockCharSizeService } from 'browser/TestUtils.test'; - -const CHAR_WIDTH = 10; -const CHAR_HEIGHT = 20; - -describe('MouseHelper.getCoords', () => { - let document: Document; - let mouseHelper: MouseHelper; - - beforeEach(() => { - document = new jsdom.JSDOM('').window.document; - const mockRenderService = { - dimensions: { - actualCellWidth: CHAR_WIDTH, - actualCellHeight: CHAR_HEIGHT - } - }; - mouseHelper = new MouseHelper(mockRenderService as any, new MockCharSizeService(CHAR_WIDTH, CHAR_HEIGHT)); - }); - - it('should return the cell that was clicked', () => { - let coords: [number, number] | undefined; - coords = mouseHelper.getCoords({ clientX: CHAR_WIDTH / 2, clientY: CHAR_HEIGHT / 2 }, document.createElement('div'), 10, 10); - assert.deepEqual(coords, [1, 1]); - coords = mouseHelper.getCoords({ clientX: CHAR_WIDTH, clientY: CHAR_HEIGHT }, document.createElement('div'), 10, 10); - assert.deepEqual(coords, [1, 1]); - coords = mouseHelper.getCoords({ clientX: CHAR_WIDTH, clientY: CHAR_HEIGHT + 1 }, document.createElement('div'), 10, 10); - assert.deepEqual(coords, [1, 2]); - coords = mouseHelper.getCoords({ clientX: CHAR_WIDTH + 1, clientY: CHAR_HEIGHT }, document.createElement('div'), 10, 10); - assert.deepEqual(coords, [2, 1]); - }); - - it('should ensure the coordinates are returned within the terminal bounds', () => { - let coords: [number, number] | undefined; - coords = mouseHelper.getCoords({ clientX: -1, clientY: -1 }, document.createElement('div'), 10, 10); - assert.deepEqual(coords, [1, 1]); - // Event are double the cols/rows - coords = mouseHelper.getCoords({ clientX: CHAR_WIDTH * 20, clientY: CHAR_HEIGHT * 20 }, document.createElement('div'), 10, 10); - assert.deepEqual(coords, [10, 10], 'coordinates should never come back as larger than the terminal'); - }); -}); diff --git a/src/browser/input/MouseHelper.ts b/src/browser/input/MouseHelper.ts deleted file mode 100644 index b99757db..00000000 --- a/src/browser/input/MouseHelper.ts +++ /dev/null @@ -1,75 +0,0 @@ -/** - * Copyright (c) 2017 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { IMouseHelper } from 'browser/Types'; -import { RenderService } from 'browser/services/RenderService'; -import { ICharSizeService } from 'browser/services/Services'; - -export class MouseHelper implements IMouseHelper { - constructor( - private _renderService: RenderService, - private _charSizeService: ICharSizeService - ) { - } - - public static getCoordsRelativeToElement(event: {clientX: number, clientY: number}, element: HTMLElement): [number, number] { - const rect = element.getBoundingClientRect(); - return [event.clientX - rect.left, event.clientY - rect.top]; - } - - /** - * Gets coordinates within the terminal for a particular mouse event. The result - * is returned as an array in the form [x, y] instead of an object as it's a - * little faster and this function is used in some low level code. - * @param event The mouse event. - * @param element The terminal's container element. - * @param colCount The number of columns in the terminal. - * @param rowCount The number of rows n the terminal. - * @param isSelection Whether the request is for the selection or not. This will - * apply an offset to the x value such that the left half of the cell will - * select that cell and the right half will select the next cell. - */ - public getCoords(event: {clientX: number, clientY: number}, element: HTMLElement, colCount: number, rowCount: number, isSelection?: boolean): [number, number] | undefined { - // Coordinates cannot be measured if there are no valid - if (!this._charSizeService.hasValidSize) { - return undefined; - } - - const coords = MouseHelper.getCoordsRelativeToElement(event, element); - if (!coords) { - return undefined; - } - - coords[0] = Math.ceil((coords[0] + (isSelection ? this._renderService.dimensions.actualCellWidth / 2 : 0)) / this._renderService.dimensions.actualCellWidth); - coords[1] = Math.ceil(coords[1] / this._renderService.dimensions.actualCellHeight); - - // Ensure coordinates are within the terminal viewport. Note that selections - // need an addition point of precision to cover the end point (as characters - // cover half of one char and half of the next). - coords[0] = Math.min(Math.max(coords[0], 1), colCount + (isSelection ? 1 : 0)); - coords[1] = Math.min(Math.max(coords[1], 1), rowCount); - - return coords; - } - - /** - * Gets coordinates within the terminal for a particular mouse event, wrapping - * them to the bounds of the terminal and adding 32 to both the x and y values - * as expected by xterm. - * @param event The mouse event. - * @param element The terminal's container element. - * @param colCount The number of columns in the terminal. - * @param rowCount The number of rows in the terminal. - */ - public getRawByteCoords(event: MouseEvent, element: HTMLElement, colCount: number, rowCount: number): { x: number | undefined, y: number | undefined } { - const coords = this.getCoords(event, element, colCount, rowCount); - - // xterm sends raw bytes and starts at 32 (SP) for each. - const x = coords ? coords[0] + 32 : undefined; - const y = coords ? coords[1] + 32 : undefined; - - return { x, y }; - } -} diff --git a/src/browser/renderer/Types.ts b/src/browser/renderer/Types.d.ts similarity index 100% rename from src/browser/renderer/Types.ts rename to src/browser/renderer/Types.d.ts diff --git a/src/browser/services/MouseService.ts b/src/browser/services/MouseService.ts new file mode 100644 index 00000000..76968698 --- /dev/null +++ b/src/browser/services/MouseService.ts @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { ICharSizeService, IRenderService, IMouseService } from './Services'; +import { getCoords, getRawByteCoords } from 'browser/input/Mouse'; + +export class MouseService implements IMouseService { + constructor( + private readonly _renderService: IRenderService, + private readonly _charSizeService: ICharSizeService + ) { + } + + public getCoords(event: {clientX: number, clientY: number}, element: HTMLElement, colCount: number, rowCount: number, isSelection?: boolean): [number, number] | undefined { + return getCoords( + event, + element, + colCount, + rowCount, + this._charSizeService.hasValidSize, + this._renderService.dimensions.actualCellWidth, + this._renderService.dimensions.actualCellHeight, + isSelection + ); + } + + public getRawByteCoords(event: MouseEvent, element: HTMLElement, colCount: number, rowCount: number): { x: number, y: number } | undefined { + const coords = this.getCoords(event, element, colCount, rowCount); + return getRawByteCoords(coords); + } +} diff --git a/src/browser/services/RenderService.ts b/src/browser/services/RenderService.ts index 5035ed3d..b41f1ebc 100644 --- a/src/browser/services/RenderService.ts +++ b/src/browser/services/RenderService.ts @@ -43,7 +43,7 @@ export class RenderService extends Disposable implements IRenderService { this.register(this._renderDebouncer); this._screenDprMonitor = new ScreenDprMonitor(); - this._screenDprMonitor.setListener(() => this._renderer.onDevicePixelRatioChange()); + this._screenDprMonitor.setListener(() => this.onDevicePixelRatioChange()); this.register(this._screenDprMonitor); this.register(optionsService.onOptionChange(() => this._renderer.onOptionsChanged())); @@ -51,7 +51,7 @@ export class RenderService extends Disposable implements IRenderService { // dprchange should handle this case, we need this as well for browsers that don't support the // matchMedia query. - this.register(addDisposableDomListener(window, 'resize', () => this._renderer.onDevicePixelRatioChange())); + this.register(addDisposableDomListener(window, 'resize', () => this.onDevicePixelRatioChange())); // Detect whether IntersectionObserver is detected and enable renderer pause // and resume based on terminal visibility if so @@ -105,13 +105,14 @@ export class RenderService extends Disposable implements IRenderService { // TODO: RenderCoordinator should be the only one to dispose the renderer this._renderer.dispose(); this._renderer = renderer; + this.refreshRows(0, this._rowCount - 1); } private _fullRefresh(): void { if (this._isPaused) { this._needsFullRefresh = true; } else { - this.refreshRows(0, this._rowCount); + this.refreshRows(0, this._rowCount - 1); } } @@ -122,6 +123,7 @@ export class RenderService extends Disposable implements IRenderService { public onDevicePixelRatioChange(): void { this._renderer.onDevicePixelRatioChange(); + this.refreshRows(0, this._rowCount - 1); } public onResize(cols: number, rows: number): void { diff --git a/src/browser/services/Services.d.ts b/src/browser/services/Services.d.ts index 1916572c..64539b95 100644 --- a/src/browser/services/Services.d.ts +++ b/src/browser/services/Services.d.ts @@ -17,6 +17,11 @@ export interface ICharSizeService { measure(): void; } +export interface IMouseService { + getCoords(event: {clientX: number, clientY: number}, element: HTMLElement, colCount: number, rowCount: number, isSelection?: boolean): [number, number] | undefined; + getRawByteCoords(event: MouseEvent, element: HTMLElement, colCount: number, rowCount: number): { x: number, y: number } | undefined; +} + export interface IRenderService { onDimensionsChange: IEvent; onRender: IEvent<{ start: number, end: number }>; diff --git a/src/common/EventEmitter.ts b/src/common/EventEmitter.ts index efc101ce..34ac190f 100644 --- a/src/common/EventEmitter.ts +++ b/src/common/EventEmitter.ts @@ -13,7 +13,12 @@ export interface IEvent { (listener: (e: T) => any): IDisposable; } -export class EventEmitter { +export interface IEventEmitter { + event: IEvent; + fire(data: T): void; +} + +export class EventEmitter implements IEventEmitter { private _listeners: IListener[] = []; private _event?: IEvent; diff --git a/src/common/Platform.ts b/src/common/Platform.ts index ee82cff4..87d466c0 100644 --- a/src/common/Platform.ts +++ b/src/common/Platform.ts @@ -19,7 +19,6 @@ const platform = (isNode) ? 'node' : navigator.platform; export const isFirefox = !!~userAgent.indexOf('Firefox'); export const isSafari = /^((?!chrome|android).)*safari/i.test(userAgent); -export const isMSIE = !!~userAgent.indexOf('MSIE') || !!~userAgent.indexOf('Trident'); // Find the users platform. We use this to interpret the meta key // and ISO third level shifts. diff --git a/src/common/Types.ts b/src/common/Types.d.ts similarity index 87% rename from src/common/Types.ts rename to src/common/Types.d.ts index b29515d4..97bd6029 100644 --- a/src/common/Types.ts +++ b/src/common/Types.d.ts @@ -3,22 +3,13 @@ * @license MIT */ -import { IEvent, EventEmitter } from 'common/EventEmitter'; +import { IEvent, IEventEmitter } from 'common/EventEmitter'; import { IDeleteEvent, IInsertEvent } from 'common/CircularList'; -export const DEFAULT_COLOR = 256; - export interface IDisposable { dispose(): void; } -export interface IEventEmitter { - on(type: string, listener: (...args: any[]) => void): void; - off(type: string, listener: (...args: any[]) => void): void; - emit(type: string, data?: any): void; - addDisposableListener(type: string, handler: (...args: any[]) => void): IDisposable; -} - export type XtermListener = (...args: any[]) => void; /** @@ -40,11 +31,11 @@ export interface ICircularList { maxLength: number; isFull: boolean; - onDeleteEmitter: EventEmitter; + onDeleteEmitter: IEventEmitter; onDelete: IEvent; - onInsertEmitter: EventEmitter; + onInsertEmitter: IEventEmitter; onInsert: IEvent; - onTrimEmitter: EventEmitter; + onTrimEmitter: IEventEmitter; onTrim: IEvent; get(index: number): T | undefined; diff --git a/src/common/buffer/AttributeData.ts b/src/common/buffer/AttributeData.ts new file mode 100644 index 00000000..0e7e2705 --- /dev/null +++ b/src/common/buffer/AttributeData.ts @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IAttributeData, IColorRGB } from 'common/Types'; +import { Attributes, FgFlags, BgFlags } from 'common/buffer/Constants'; + +export class AttributeData implements IAttributeData { + static toColorRGB(value: number): IColorRGB { + return [ + value >>> Attributes.RED_SHIFT & 255, + value >>> Attributes.GREEN_SHIFT & 255, + value & 255 + ]; + } + static fromColorRGB(value: IColorRGB): number { + return (value[0] & 255) << Attributes.RED_SHIFT | (value[1] & 255) << Attributes.GREEN_SHIFT | value[2] & 255; + } + + public clone(): IAttributeData { + const newObj = new AttributeData(); + newObj.fg = this.fg; + newObj.bg = this.bg; + return newObj; + } + + // data + public fg: number = 0; + public bg: number = 0; + + // flags + public isInverse(): number { return this.fg & FgFlags.INVERSE; } + public isBold(): number { return this.fg & FgFlags.BOLD; } + public isUnderline(): number { return this.fg & FgFlags.UNDERLINE; } + public isBlink(): number { return this.fg & FgFlags.BLINK; } + public isInvisible(): number { return this.fg & FgFlags.INVISIBLE; } + public isItalic(): number { return this.bg & BgFlags.ITALIC; } + public isDim(): number { return this.bg & BgFlags.DIM; } + + // color modes + public getFgColorMode(): number { return this.fg & Attributes.CM_MASK; } + public getBgColorMode(): number { return this.bg & Attributes.CM_MASK; } + public isFgRGB(): boolean { return (this.fg & Attributes.CM_MASK) === Attributes.CM_RGB; } + public isBgRGB(): boolean { return (this.bg & Attributes.CM_MASK) === Attributes.CM_RGB; } + public isFgPalette(): boolean { return (this.fg & Attributes.CM_MASK) === Attributes.CM_P16 || (this.fg & Attributes.CM_MASK) === Attributes.CM_P256; } + public isBgPalette(): boolean { return (this.bg & Attributes.CM_MASK) === Attributes.CM_P16 || (this.bg & Attributes.CM_MASK) === Attributes.CM_P256; } + public isFgDefault(): boolean { return (this.fg & Attributes.CM_MASK) === 0; } + public isBgDefault(): boolean { return (this.bg & Attributes.CM_MASK) === 0; } + + // colors + public getFgColor(): number { + switch (this.fg & Attributes.CM_MASK) { + case Attributes.CM_P16: + case Attributes.CM_P256: return this.fg & Attributes.PCOLOR_MASK; + case Attributes.CM_RGB: return this.fg & Attributes.RGB_MASK; + default: return -1; // CM_DEFAULT defaults to -1 + } + } + public getBgColor(): number { + switch (this.bg & Attributes.CM_MASK) { + case Attributes.CM_P16: + case Attributes.CM_P256: return this.bg & Attributes.PCOLOR_MASK; + case Attributes.CM_RGB: return this.bg & Attributes.RGB_MASK; + default: return -1; // CM_DEFAULT defaults to -1 + } + } +} diff --git a/src/common/buffer/Buffer.test.ts b/src/common/buffer/Buffer.test.ts index 01516b3c..af5abeca 100644 --- a/src/common/buffer/Buffer.test.ts +++ b/src/common/buffer/Buffer.test.ts @@ -7,7 +7,8 @@ import { assert } from 'chai'; import { Buffer } from 'common/buffer/Buffer'; import { CircularList } from 'common/CircularList'; import { MockOptionsService, MockBufferService } from 'common/TestUtils.test'; -import { BufferLine, CellData, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; +import { BufferLine, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; +import { CellData } from 'common/buffer/CellData'; const INIT_COLS = 80; const INIT_ROWS = 24; diff --git a/src/common/buffer/Buffer.ts b/src/common/buffer/Buffer.ts index 1e6edae9..9c36a5a4 100644 --- a/src/common/buffer/Buffer.ts +++ b/src/common/buffer/Buffer.ts @@ -6,7 +6,9 @@ import { CircularList, IInsertEvent } from 'common/CircularList'; import { IBuffer, BufferIndex, IBufferStringIterator, IBufferStringIteratorResult } from 'common/buffer/Types'; import { IBufferLine, ICellData, IAttributeData } from 'common/Types'; -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 'common/buffer/BufferLine'; +import { BufferLine, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; +import { CellData } from 'common/buffer/CellData'; +import { NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_WIDTH, WHITESPACE_CELL_CODE, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from 'common/buffer/Constants'; import { reflowLargerApplyNewLayout, reflowLargerCreateNewLayout, reflowLargerGetLinesToRemove, reflowSmallerGetNewLineLengths, getWrappedLineTrimmedLength } from 'common/buffer/BufferReflow'; import { Marker } from 'common/buffer/Marker'; import { IOptionsService, IBufferService } from 'common/services/Services'; @@ -203,6 +205,7 @@ export class Buffer implements IBuffer { this.lines.trimStart(amountToTrim); this.ybase = Math.max(this.ybase - amountToTrim, 0); this.ydisp = Math.max(this.ydisp - amountToTrim, 0); + this.savedY = Math.max(this.savedY - amountToTrim, 0); } this.lines.maxLength = newMaxLength; } @@ -213,7 +216,6 @@ export class Buffer implements IBuffer { if (addToY) { this.y += addToY; } - this.savedY = Math.min(this.savedY, newRows - 1); this.savedX = Math.min(this.savedX, newCols - 1); this.scrollTop = 0; @@ -282,6 +284,7 @@ export class Buffer implements IBuffer { this.ybase--; } } + this.savedY = Math.max(this.savedY - countRemoved, 0); } private _reflowSmaller(newCols: number, newRows: number): void { @@ -393,6 +396,7 @@ export class Buffer implements IBuffer { } } } + this.savedY = Math.min(this.savedY + linesToAdd, this.ybase + newRows - 1); } // Rearrange lines in the buffer if there are any insertions, this is done at the end rather diff --git a/src/common/buffer/BufferLine.test.ts b/src/common/buffer/BufferLine.test.ts index c42b372e..ae80aa16 100644 --- a/src/common/buffer/BufferLine.test.ts +++ b/src/common/buffer/BufferLine.test.ts @@ -3,7 +3,9 @@ * @license MIT */ import * as chai from 'chai'; -import { BufferLine, CellData, Content, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, DEFAULT_ATTR } from './BufferLine'; +import { NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, DEFAULT_ATTR, Content } from 'common/buffer/Constants'; +import { BufferLine } from 'common/buffer//BufferLine'; +import { CellData } from 'common/buffer/CellData'; import { CharData, IBufferLine } from '../Types'; class TestBufferLine extends BufferLine { diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index eb0a5e99..8e742be3 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -2,33 +2,12 @@ * Copyright (c) 2018 The xterm.js authors. All rights reserved. * @license MIT */ -import { DEFAULT_COLOR, CharData, IBufferLine, ICellData, IColorRGB, IAttributeData } from 'common/Types'; + +import { CharData, IBufferLine, ICellData } from 'common/Types'; import { stringFromCodePoint } from 'common/input/TextDecoder'; - -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; +import { DEFAULT_COLOR, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_ATTR_INDEX, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, WHITESPACE_CELL_CHAR, Content } from 'common/buffer/Constants'; +import { CellData } from 'common/buffer/CellData'; +import { AttributeData } from 'common/buffer/AttributeData'; /** * buffer memory layout: @@ -56,258 +35,8 @@ const enum Cell { BG = 2 // currently unused } -/** - * Bitmasks for accessing data in `content`. - */ -export const enum Content { - /** - * bit 1..21 codepoint, max allowed in UTF32 is 0x10FFFF (21 bits taken) - * read: `codepoint = content & Content.codepointMask;` - * write: `content |= codepoint & Content.codepointMask;` - * shortcut if precondition `codepoint <= 0x10FFFF` is met: - * `content |= codepoint;` - */ - CODEPOINT_MASK = 0x1FFFFF, - - /** - * bit 22 flag indication whether a cell contains combined content - * read: `isCombined = content & Content.isCombined;` - * set: `content |= Content.isCombined;` - * clear: `content &= ~Content.isCombined;` - */ - IS_COMBINED_MASK = 0x200000, // 1 << 21 - - /** - * bit 1..22 mask to check whether a cell contains any string data - * we need to check for codepoint and isCombined bits to see - * whether a cell contains anything - * read: `isEmpty = !(content & Content.hasContent)` - */ - HAS_CONTENT_MASK = 0x3FFFFF, - - /** - * bit 23..24 wcwidth value of cell, takes 2 bits (ranges from 0..2) - * read: `width = (content & Content.widthMask) >> Content.widthShift;` - * `hasWidth = content & Content.widthMask;` - * as long as wcwidth is highest value in `content`: - * `width = content >> Content.widthShift;` - * write: `content |= (width << Content.widthShift) & Content.widthMask;` - * shortcut if precondition `0 <= width <= 3` is met: - * `content |= width << Content.widthShift;` - */ - WIDTH_MASK = 0xC00000, // 3 << 22 - WIDTH_SHIFT = 22 -} - - -export const enum Attributes { - /** - * bit 1..8 blue in RGB, color in P256 and P16 - */ - BLUE_MASK = 0xFF, - BLUE_SHIFT = 0, - PCOLOR_MASK = 0xFF, - PCOLOR_SHIFT = 0, - - /** - * bit 9..16 green in RGB - */ - GREEN_MASK = 0xFF00, - GREEN_SHIFT = 8, - - /** - * bit 17..24 red in RGB - */ - RED_MASK = 0xFF0000, - RED_SHIFT = 16, - - /** - * bit 25..26 color mode: DEFAULT (0) | P16 (1) | P256 (2) | RGB (3) - */ - CM_MASK = 0x3000000, - CM_DEFAULT = 0, - CM_P16 = 0x1000000, - CM_P256 = 0x2000000, - CM_RGB = 0x3000000, - - /** - * bit 1..24 RGB room - */ - RGB_MASK = 0xFFFFFF -} - -export const enum FgFlags { - /** - * bit 27..31 (32th bit unused) - */ - INVERSE = 0x4000000, - BOLD = 0x8000000, - UNDERLINE = 0x10000000, - BLINK = 0x20000000, - INVISIBLE = 0x40000000 -} - -export const enum BgFlags { - /** - * bit 27..32 (upper 4 unused) - */ - ITALIC = 0x4000000, - DIM = 0x8000000 -} - -export class AttributeData implements IAttributeData { - static toColorRGB(value: number): IColorRGB { - return [ - value >>> Attributes.RED_SHIFT & 255, - value >>> Attributes.GREEN_SHIFT & 255, - value & 255 - ]; - } - static fromColorRGB(value: IColorRGB): number { - return (value[0] & 255) << Attributes.RED_SHIFT | (value[1] & 255) << Attributes.GREEN_SHIFT | value[2] & 255; - } - - public clone(): IAttributeData { - const newObj = new AttributeData(); - newObj.fg = this.fg; - newObj.bg = this.bg; - return newObj; - } - - // data - public fg: number = 0; - public bg: number = 0; - - // flags - public isInverse(): number { return this.fg & FgFlags.INVERSE; } - public isBold(): number { return this.fg & FgFlags.BOLD; } - public isUnderline(): number { return this.fg & FgFlags.UNDERLINE; } - public isBlink(): number { return this.fg & FgFlags.BLINK; } - public isInvisible(): number { return this.fg & FgFlags.INVISIBLE; } - public isItalic(): number { return this.bg & BgFlags.ITALIC; } - public isDim(): number { return this.bg & BgFlags.DIM; } - - // color modes - public getFgColorMode(): number { return this.fg & Attributes.CM_MASK; } - public getBgColorMode(): number { return this.bg & Attributes.CM_MASK; } - public isFgRGB(): boolean { return (this.fg & Attributes.CM_MASK) === Attributes.CM_RGB; } - public isBgRGB(): boolean { return (this.bg & Attributes.CM_MASK) === Attributes.CM_RGB; } - public isFgPalette(): boolean { return (this.fg & Attributes.CM_MASK) === Attributes.CM_P16 || (this.fg & Attributes.CM_MASK) === Attributes.CM_P256; } - public isBgPalette(): boolean { return (this.bg & Attributes.CM_MASK) === Attributes.CM_P16 || (this.bg & Attributes.CM_MASK) === Attributes.CM_P256; } - public isFgDefault(): boolean { return (this.fg & Attributes.CM_MASK) === 0; } - public isBgDefault(): boolean { return (this.bg & Attributes.CM_MASK) === 0; } - - // colors - public getFgColor(): number { - switch (this.fg & Attributes.CM_MASK) { - case Attributes.CM_P16: - case Attributes.CM_P256: return this.fg & Attributes.PCOLOR_MASK; - case Attributes.CM_RGB: return this.fg & Attributes.RGB_MASK; - default: return -1; // CM_DEFAULT defaults to -1 - } - } - public getBgColor(): number { - switch (this.bg & Attributes.CM_MASK) { - case Attributes.CM_P16: - case Attributes.CM_P256: return this.bg & Attributes.PCOLOR_MASK; - case Attributes.CM_RGB: return this.bg & Attributes.RGB_MASK; - default: return -1; // CM_DEFAULT defaults to -1 - } - } -} - export const DEFAULT_ATTR_DATA = Object.freeze(new AttributeData()); -/** - * CellData - represents a single Cell in the terminal buffer. - */ -export class CellData extends AttributeData implements ICellData { - - /** Helper to create CellData from CharData. */ - public static fromCharData(value: CharData): CellData { - const obj = new CellData(); - obj.setFromCharData(value); - return obj; - } - - /** Primitives from terminal buffer. */ - public content: number = 0; - public fg: number = 0; - public bg: number = 0; - public combinedData: string = ''; - - /** Whether cell contains a combined string. */ - public isCombined(): number { - return this.content & Content.IS_COMBINED_MASK; - } - - /** Width of the cell. */ - public getWidth(): number { - return this.content >> Content.WIDTH_SHIFT; - } - - /** JS string of the content. */ - public getChars(): string { - if (this.content & Content.IS_COMBINED_MASK) { - return this.combinedData; - } - if (this.content & Content.CODEPOINT_MASK) { - return stringFromCodePoint(this.content & Content.CODEPOINT_MASK); - } - return ''; - } - - /** - * Codepoint of cell - * Note this returns the UTF32 codepoint of single chars, - * if content is a combined string it returns the codepoint - * of the last char in string to be in line with code in CharData. - * */ - public getCode(): number { - return (this.isCombined()) - ? this.combinedData.charCodeAt(this.combinedData.length - 1) - : this.content & Content.CODEPOINT_MASK; - } - - /** Set data from CharData */ - public setFromCharData(value: CharData): void { - this.fg = value[CHAR_DATA_ATTR_INDEX]; - this.bg = 0; - let combined = false; - - // surrogates and combined strings need special treatment - if (value[CHAR_DATA_CHAR_INDEX].length > 2) { - combined = true; - } else if (value[CHAR_DATA_CHAR_INDEX].length === 2) { - const code = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0); - // if the 2-char string is a surrogate create single codepoint - // everything else is combined - if (0xD800 <= code && code <= 0xDBFF) { - const second = value[CHAR_DATA_CHAR_INDEX].charCodeAt(1); - if (0xDC00 <= second && second <= 0xDFFF) { - this.content = ((code - 0xD800) * 0x400 + second - 0xDC00 + 0x10000) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT); - } else { - combined = true; - } - } else { - combined = true; - } - } else { - this.content = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT); - } - if (combined) { - this.combinedData = value[CHAR_DATA_CHAR_INDEX]; - this.content = Content.IS_COMBINED_MASK | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT); - } - } - - /** Get data as CharData. */ - public getAsCharData(): CharData { - return [this.fg, this.getChars(), this.getWidth(), this.getCode()]; - } -} - - /** * Typed array based bufferline implementation. * diff --git a/src/common/buffer/BufferReflow.test.ts b/src/common/buffer/BufferReflow.test.ts index af908572..b351b89c 100644 --- a/src/common/buffer/BufferReflow.test.ts +++ b/src/common/buffer/BufferReflow.test.ts @@ -3,7 +3,8 @@ * @license MIT */ import { assert } from 'chai'; -import { BufferLine, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE } from 'common/buffer/BufferLine'; +import { BufferLine } from 'common/buffer/BufferLine'; +import { NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE } from 'common/buffer/Constants'; import { reflowSmallerGetNewLineLengths } from 'common/buffer/BufferReflow'; describe('BufferReflow', () => { diff --git a/src/common/buffer/CellData.ts b/src/common/buffer/CellData.ts new file mode 100644 index 00000000..21ad2ee5 --- /dev/null +++ b/src/common/buffer/CellData.ts @@ -0,0 +1,93 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { CharData, ICellData } from 'common/Types'; +import { stringFromCodePoint } from 'common/input/TextDecoder'; +import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_ATTR_INDEX, Content } from 'common/buffer/Constants'; +import { AttributeData } from 'common/buffer/AttributeData'; + +/** + * CellData - represents a single Cell in the terminal buffer. + */ +export class CellData extends AttributeData implements ICellData { + /** Helper to create CellData from CharData. */ + public static fromCharData(value: CharData): CellData { + const obj = new CellData(); + obj.setFromCharData(value); + return obj; + } + /** Primitives from terminal buffer. */ + public content: number = 0; + public fg: number = 0; + public bg: number = 0; + public combinedData: string = ''; + /** Whether cell contains a combined string. */ + public isCombined(): number { + return this.content & Content.IS_COMBINED_MASK; + } + /** Width of the cell. */ + public getWidth(): number { + return this.content >> Content.WIDTH_SHIFT; + } + /** JS string of the content. */ + public getChars(): string { + if (this.content & Content.IS_COMBINED_MASK) { + return this.combinedData; + } + if (this.content & Content.CODEPOINT_MASK) { + return stringFromCodePoint(this.content & Content.CODEPOINT_MASK); + } + return ''; + } + /** + * Codepoint of cell + * Note this returns the UTF32 codepoint of single chars, + * if content is a combined string it returns the codepoint + * of the last char in string to be in line with code in CharData. + * */ + public getCode(): number { + return (this.isCombined()) + ? this.combinedData.charCodeAt(this.combinedData.length - 1) + : this.content & Content.CODEPOINT_MASK; + } + /** Set data from CharData */ + public setFromCharData(value: CharData): void { + this.fg = value[CHAR_DATA_ATTR_INDEX]; + this.bg = 0; + let combined = false; + // surrogates and combined strings need special treatment + if (value[CHAR_DATA_CHAR_INDEX].length > 2) { + combined = true; + } + else if (value[CHAR_DATA_CHAR_INDEX].length === 2) { + const code = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0); + // if the 2-char string is a surrogate create single codepoint + // everything else is combined + if (0xD800 <= code && code <= 0xDBFF) { + const second = value[CHAR_DATA_CHAR_INDEX].charCodeAt(1); + if (0xDC00 <= second && second <= 0xDFFF) { + this.content = ((code - 0xD800) * 0x400 + second - 0xDC00 + 0x10000) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT); + } + else { + combined = true; + } + } + else { + combined = true; + } + } + else { + this.content = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT); + } + if (combined) { + this.combinedData = value[CHAR_DATA_CHAR_INDEX]; + this.content = Content.IS_COMBINED_MASK | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT); + } + } + /** Get data as CharData. */ + public getAsCharData(): CharData { + return [this.fg, this.getChars(), this.getWidth(), this.getCode()]; + } +} diff --git a/src/common/buffer/Constants.ts b/src/common/buffer/Constants.ts new file mode 100644 index 00000000..276a5c54 --- /dev/null +++ b/src/common/buffer/Constants.ts @@ -0,0 +1,128 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +export const DEFAULT_COLOR = 256; +export const DEFAULT_ATTR = (0 << 18) | (DEFAULT_COLOR << 9) | (256 << 0); + +export const CHAR_DATA_ATTR_INDEX = 0; +export const CHAR_DATA_CHAR_INDEX = 1; +export const CHAR_DATA_WIDTH_INDEX = 2; +export const CHAR_DATA_CODE_INDEX = 3; + +/** + * Null cell - a real empty cell (containing nothing). + * Note that code should always be 0 for a null cell as + * several test condition of the buffer line rely on this. + */ +export const NULL_CELL_CHAR = ''; +export const NULL_CELL_WIDTH = 1; +export const NULL_CELL_CODE = 0; + +/** + * Whitespace cell. + * This is meant as a replacement for empty cells when needed + * during rendering lines to preserve correct aligment. + */ +export const WHITESPACE_CELL_CHAR = ' '; +export const WHITESPACE_CELL_WIDTH = 1; +export const WHITESPACE_CELL_CODE = 32; + +/** + * Bitmasks for accessing data in `content`. + */ +export const enum Content { + /** + * bit 1..21 codepoint, max allowed in UTF32 is 0x10FFFF (21 bits taken) + * read: `codepoint = content & Content.codepointMask;` + * write: `content |= codepoint & Content.codepointMask;` + * shortcut if precondition `codepoint <= 0x10FFFF` is met: + * `content |= codepoint;` + */ + CODEPOINT_MASK = 0x1FFFFF, + + /** + * bit 22 flag indication whether a cell contains combined content + * read: `isCombined = content & Content.isCombined;` + * set: `content |= Content.isCombined;` + * clear: `content &= ~Content.isCombined;` + */ + IS_COMBINED_MASK = 0x200000, // 1 << 21 + + /** + * bit 1..22 mask to check whether a cell contains any string data + * we need to check for codepoint and isCombined bits to see + * whether a cell contains anything + * read: `isEmpty = !(content & Content.hasContent)` + */ + HAS_CONTENT_MASK = 0x3FFFFF, + + /** + * bit 23..24 wcwidth value of cell, takes 2 bits (ranges from 0..2) + * read: `width = (content & Content.widthMask) >> Content.widthShift;` + * `hasWidth = content & Content.widthMask;` + * as long as wcwidth is highest value in `content`: + * `width = content >> Content.widthShift;` + * write: `content |= (width << Content.widthShift) & Content.widthMask;` + * shortcut if precondition `0 <= width <= 3` is met: + * `content |= width << Content.widthShift;` + */ + WIDTH_MASK = 0xC00000, // 3 << 22 + WIDTH_SHIFT = 22 +} + +export const enum Attributes { + /** + * bit 1..8 blue in RGB, color in P256 and P16 + */ + BLUE_MASK = 0xFF, + BLUE_SHIFT = 0, + PCOLOR_MASK = 0xFF, + PCOLOR_SHIFT = 0, + + /** + * bit 9..16 green in RGB + */ + GREEN_MASK = 0xFF00, + GREEN_SHIFT = 8, + + /** + * bit 17..24 red in RGB + */ + RED_MASK = 0xFF0000, + RED_SHIFT = 16, + + /** + * bit 25..26 color mode: DEFAULT (0) | P16 (1) | P256 (2) | RGB (3) + */ + CM_MASK = 0x3000000, + CM_DEFAULT = 0, + CM_P16 = 0x1000000, + CM_P256 = 0x2000000, + CM_RGB = 0x3000000, + + /** + * bit 1..24 RGB room + */ + RGB_MASK = 0xFFFFFF +} + +export const enum FgFlags { + /** + * bit 27..31 (32th bit unused) + */ + INVERSE = 0x4000000, + BOLD = 0x8000000, + UNDERLINE = 0x10000000, + BLINK = 0x20000000, + INVISIBLE = 0x40000000 +} + +export const enum BgFlags { + /** + * bit 27..32 (upper 4 unused) + */ + ITALIC = 0x4000000, + DIM = 0x8000000 +} diff --git a/src/common/buffer/Types.ts b/src/common/buffer/Types.d.ts similarity index 100% rename from src/common/buffer/Types.ts rename to src/common/buffer/Types.d.ts diff --git a/src/common/parser/Constants.ts b/src/common/parser/Constants.ts new file mode 100644 index 00000000..55e4a005 --- /dev/null +++ b/src/common/parser/Constants.ts @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +/** + * Internal states of EscapeSequenceParser. + */ +export const enum ParserState { + GROUND = 0, + ESCAPE = 1, + ESCAPE_INTERMEDIATE = 2, + CSI_ENTRY = 3, + CSI_PARAM = 4, + CSI_INTERMEDIATE = 5, + CSI_IGNORE = 6, + SOS_PM_APC_STRING = 7, + OSC_STRING = 8, + DCS_ENTRY = 9, + DCS_PARAM = 10, + DCS_IGNORE = 11, + DCS_INTERMEDIATE = 12, + DCS_PASSTHROUGH = 13 +} + +/** +* Internal actions of EscapeSequenceParser. +*/ +export const enum ParserAction { + IGNORE = 0, + ERROR = 1, + PRINT = 2, + EXECUTE = 3, + OSC_START = 4, + OSC_PUT = 5, + OSC_END = 6, + CSI_DISPATCH = 7, + PARAM = 8, + COLLECT = 9, + ESC_DISPATCH = 10, + CLEAR = 11, + DCS_HOOK = 12, + DCS_PUT = 13, + DCS_UNHOOK = 14 +} diff --git a/src/common/parser/EscapeSequenceParser.test.ts b/src/common/parser/EscapeSequenceParser.test.ts index 18d5c6cf..f445efc4 100644 --- a/src/common/parser/EscapeSequenceParser.test.ts +++ b/src/common/parser/EscapeSequenceParser.test.ts @@ -3,10 +3,11 @@ * @license MIT */ -import { ParserState, IDcsHandler, IParsingState } from 'common/parser/Types'; +import { IDcsHandler, IParsingState } from 'common/parser/Types'; import { EscapeSequenceParser, TransitionTable, VT500_TRANSITION_TABLE } from 'common/parser/EscapeSequenceParser'; import * as chai from 'chai'; import { StringToUtf32, stringFromCodePoint } from 'common/input/TextDecoder'; +import { ParserState } from 'common/parser/Constants'; function r(a: number, b: number): string[] { let c = b - a; diff --git a/src/common/parser/EscapeSequenceParser.ts b/src/common/parser/EscapeSequenceParser.ts index c8b631d0..d8d4e02e 100644 --- a/src/common/parser/EscapeSequenceParser.ts +++ b/src/common/parser/EscapeSequenceParser.ts @@ -3,7 +3,8 @@ * @license MIT */ -import { ParserState, ParserAction, IParsingState, IDcsHandler, IEscapeSequenceParser } from 'common/parser/Types'; +import { IParsingState, IDcsHandler, IEscapeSequenceParser } from 'common/parser/Types'; +import { ParserState, ParserAction } from 'common/parser/Constants'; import { Disposable } from 'common/Lifecycle'; import { utf32ToString } from 'common/input/TextDecoder'; import { IDisposable } from 'common/Types'; diff --git a/src/common/parser/Types.ts b/src/common/parser/Types.d.ts similarity index 84% rename from src/common/parser/Types.ts rename to src/common/parser/Types.d.ts index 87199702..47ec98e1 100644 --- a/src/common/parser/Types.ts +++ b/src/common/parser/Types.d.ts @@ -4,47 +4,7 @@ */ import { IDisposable } from 'common/Types'; - -/** - * Internal states of EscapeSequenceParser. - */ -export const enum ParserState { - GROUND = 0, - ESCAPE = 1, - ESCAPE_INTERMEDIATE = 2, - CSI_ENTRY = 3, - CSI_PARAM = 4, - CSI_INTERMEDIATE = 5, - CSI_IGNORE = 6, - SOS_PM_APC_STRING = 7, - OSC_STRING = 8, - DCS_ENTRY = 9, - DCS_PARAM = 10, - DCS_IGNORE = 11, - DCS_INTERMEDIATE = 12, - DCS_PASSTHROUGH = 13 -} - -/** -* Internal actions of EscapeSequenceParser. -*/ -export const enum ParserAction { - IGNORE = 0, - ERROR = 1, - PRINT = 2, - EXECUTE = 3, - OSC_START = 4, - OSC_PUT = 5, - OSC_END = 6, - CSI_DISPATCH = 7, - PARAM = 8, - COLLECT = 9, - ESC_DISPATCH = 10, - CLEAR = 11, - DCS_HOOK = 12, - DCS_PUT = 13, - DCS_UNHOOK = 14 -} +import { ParserState } from 'common/parser/Constants'; /** * Internal state of EscapeSequenceParser. diff --git a/src/handlers/AltClickHandler.ts b/src/handlers/AltClickHandler.ts index 506e6ee1..334b78d8 100644 --- a/src/handlers/AltClickHandler.ts +++ b/src/handlers/AltClickHandler.ts @@ -6,6 +6,7 @@ import { ITerminal } from '../Types'; import { IBufferLine, ICircularList } from 'common/Types'; import { C0 } from 'common/data/EscapeSequences'; +import { IMouseService } from 'browser/services/Services'; const enum Direction { UP = 'A', @@ -23,13 +24,14 @@ export class AltClickHandler { constructor( private _mouseEvent: MouseEvent, - private _terminal: ITerminal + private _terminal: ITerminal, + private readonly _mouseService: IMouseService ) { this._lines = this._terminal.buffer.lines; this._startCol = this._terminal.buffer.x; this._startRow = this._terminal.buffer.y; - const coordinates = this._terminal.mouseHelper.getCoords( + const coordinates = this._mouseService.getCoords( this._mouseEvent, this._terminal.element, this._terminal.cols, diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 8335f76d..5101fdd5 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -6,12 +6,15 @@ import { IRenderLayer } from './Types'; import { IRenderDimensions } from 'browser/renderer/Types'; import { ITerminal } from '../Types'; -import { ICellData, DEFAULT_COLOR } from 'common/Types'; -import { DIM_OPACITY, INVERTED_DEFAULT_COLOR, IGlyphIdentifier } from './atlas/Types'; +import { ICellData } from 'common/Types'; +import { DEFAULT_COLOR, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE } from 'common/buffer/Constants'; +import { IGlyphIdentifier } from './atlas/Types'; +import { DIM_OPACITY, INVERTED_DEFAULT_COLOR } from './atlas/Constants'; import { BaseCharAtlas } from './atlas/BaseCharAtlas'; import { acquireCharAtlas } from './atlas/CharAtlasCache'; -import { CellData, AttributeData, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE } from 'common/buffer/BufferLine'; +import { AttributeData } from 'common/buffer/AttributeData'; import { IColorSet } from 'browser/Types'; +import { CellData } from 'common/buffer/CellData'; export abstract class BaseRenderLayer implements IRenderLayer { private _canvas: HTMLCanvasElement; diff --git a/src/renderer/CharacterJoinerRegistry.test.ts b/src/renderer/CharacterJoinerRegistry.test.ts index e5ab08ea..a73672a2 100644 --- a/src/renderer/CharacterJoinerRegistry.test.ts +++ b/src/renderer/CharacterJoinerRegistry.test.ts @@ -10,8 +10,9 @@ import { CircularList } from 'common/CircularList'; import { ICharacterJoinerRegistry } from './Types'; import { CharacterJoinerRegistry } from './CharacterJoinerRegistry'; -import { BufferLine, CellData } from 'common/buffer/BufferLine'; +import { BufferLine } from 'common/buffer/BufferLine'; import { IBufferLine } from 'common/Types'; +import { CellData } from 'common/buffer/CellData'; describe('CharacterJoinerRegistry', () => { let registry: ICharacterJoinerRegistry; diff --git a/src/renderer/CharacterJoinerRegistry.ts b/src/renderer/CharacterJoinerRegistry.ts index b707d863..80fff2b1 100644 --- a/src/renderer/CharacterJoinerRegistry.ts +++ b/src/renderer/CharacterJoinerRegistry.ts @@ -6,7 +6,9 @@ import { ITerminal } from '../Types'; import { IBufferLine, ICellData, CharData } from 'common/Types'; import { ICharacterJoinerRegistry, ICharacterJoiner } from './Types'; -import { CellData, Content, AttributeData, WHITESPACE_CELL_CHAR } from 'common/buffer/BufferLine'; +import { AttributeData } from 'common/buffer/AttributeData'; +import { WHITESPACE_CELL_CHAR, Content } from 'common/buffer/Constants'; +import { CellData } from 'common/buffer/CellData'; export class JoinedCellData extends AttributeData implements ICellData { private _width: number; diff --git a/src/renderer/CursorRenderLayer.ts b/src/renderer/CursorRenderLayer.ts index 9626f772..b2d856d1 100644 --- a/src/renderer/CursorRenderLayer.ts +++ b/src/renderer/CursorRenderLayer.ts @@ -7,7 +7,7 @@ import { IRenderDimensions } from 'browser/renderer/Types'; import { BaseRenderLayer } from './BaseRenderLayer'; import { ITerminal } from '../Types'; import { ICellData } from 'common/Types'; -import { CellData } from 'common/buffer/BufferLine'; +import { CellData } from 'common/buffer/CellData'; import { IColorSet } from 'browser/Types'; interface ICursorState { diff --git a/src/renderer/LinkRenderLayer.ts b/src/renderer/LinkRenderLayer.ts index 32db3db9..99eff4a2 100644 --- a/src/renderer/LinkRenderLayer.ts +++ b/src/renderer/LinkRenderLayer.ts @@ -6,7 +6,7 @@ import { ILinkifierEvent, ITerminal, ILinkifierAccessor } from '../Types'; import { IRenderDimensions } from 'browser/renderer/Types'; import { BaseRenderLayer } from './BaseRenderLayer'; -import { INVERTED_DEFAULT_COLOR } from './atlas/Types'; +import { INVERTED_DEFAULT_COLOR } from './atlas/Constants'; import { is256Color } from './atlas/CharAtlasUtils'; import { IColorSet } from 'browser/Types'; diff --git a/src/renderer/TextRenderLayer.ts b/src/renderer/TextRenderLayer.ts index 39fcb7fc..db3bec19 100644 --- a/src/renderer/TextRenderLayer.ts +++ b/src/renderer/TextRenderLayer.ts @@ -9,9 +9,11 @@ import { ITerminal } from '../Types'; import { CharData, ICellData } from 'common/Types'; import { GridCache } from './GridCache'; import { BaseRenderLayer } from './BaseRenderLayer'; -import { CellData, AttributeData, Content, NULL_CELL_CODE } from 'common/buffer/BufferLine'; +import { AttributeData } from 'common/buffer/AttributeData'; +import { NULL_CELL_CODE, Content } from 'common/buffer/Constants'; import { JoinedCellData } from './CharacterJoinerRegistry'; import { IColorSet } from 'browser/Types'; +import { CellData } from 'common/buffer/CellData'; /** * This CharData looks like a null character, which will forc a clear and render diff --git a/src/renderer/Types.ts b/src/renderer/Types.d.ts similarity index 91% rename from src/renderer/Types.ts rename to src/renderer/Types.d.ts index 153b68e2..5f7d67a8 100644 --- a/src/renderer/Types.ts +++ b/src/renderer/Types.d.ts @@ -8,19 +8,6 @@ import { IDisposable } from 'xterm'; import { IColorSet } from 'browser/Types'; import { IRenderDimensions, CharacterJoinerHandler } from 'browser/renderer/Types'; -/** - * Flags used to render terminal text properly. - */ -export const enum FLAGS { - BOLD = 1, - UNDERLINE = 2, - BLINK = 4, - INVERSE = 8, - INVISIBLE = 16, - DIM = 32, - ITALIC = 64 -} - export interface IRenderLayer extends IDisposable { /** * Called when the terminal loses focus. diff --git a/src/renderer/atlas/CharAtlasUtils.ts b/src/renderer/atlas/CharAtlasUtils.ts index b68793f6..ee787a7b 100644 --- a/src/renderer/atlas/CharAtlasUtils.ts +++ b/src/renderer/atlas/CharAtlasUtils.ts @@ -5,7 +5,7 @@ import { ITerminal } from '../../Types'; import { ICharAtlasConfig } from './Types'; -import { DEFAULT_COLOR } from 'common/Types'; +import { DEFAULT_COLOR } from 'common/buffer/Constants'; import { IColorSet } from 'browser/Types'; export function generateConfig(scaledCharWidth: number, scaledCharHeight: number, terminal: ITerminal, colors: IColorSet): ICharAtlasConfig { diff --git a/src/renderer/atlas/Constants.ts b/src/renderer/atlas/Constants.ts new file mode 100644 index 00000000..150aad88 --- /dev/null +++ b/src/renderer/atlas/Constants.ts @@ -0,0 +1,9 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +export const INVERTED_DEFAULT_COLOR = 257; +export const DIM_OPACITY = 0.5; + +export const CHAR_ATLAS_CELL_SPACING = 1; diff --git a/src/renderer/atlas/DynamicCharAtlas.ts b/src/renderer/atlas/DynamicCharAtlas.ts index c8ccefcb..fdae4e80 100644 --- a/src/renderer/atlas/DynamicCharAtlas.ts +++ b/src/renderer/atlas/DynamicCharAtlas.ts @@ -3,7 +3,8 @@ * @license MIT */ -import { DIM_OPACITY, IGlyphIdentifier, INVERTED_DEFAULT_COLOR, ICharAtlasConfig } from './Types'; +import { IGlyphIdentifier, ICharAtlasConfig } from './Types'; +import { DIM_OPACITY, INVERTED_DEFAULT_COLOR } from './Constants'; import { BaseCharAtlas } from './BaseCharAtlas'; import { DEFAULT_ANSI_COLORS } from 'browser/ColorManager'; import { LRUMap } from './LRUMap'; diff --git a/src/renderer/atlas/Types.ts b/src/renderer/atlas/Types.d.ts similarity index 83% rename from src/renderer/atlas/Types.ts rename to src/renderer/atlas/Types.d.ts index 2cb1db40..1de843e0 100644 --- a/src/renderer/atlas/Types.ts +++ b/src/renderer/atlas/Types.d.ts @@ -6,11 +6,6 @@ import { FontWeight } from 'xterm'; import { IColorSet } from 'browser/Types'; -export const INVERTED_DEFAULT_COLOR = 257; -export const DIM_OPACITY = 0.5; - -export const CHAR_ATLAS_CELL_SPACING = 1; - export interface IGlyphIdentifier { chars: string; code: number; diff --git a/src/renderer/dom/DomRenderer.ts b/src/renderer/dom/DomRenderer.ts index 8d370336..fe6ab4d1 100644 --- a/src/renderer/dom/DomRenderer.ts +++ b/src/renderer/dom/DomRenderer.ts @@ -6,7 +6,7 @@ import { IRenderer, IRenderDimensions, CharacterJoinerHandler } from 'browser/renderer/Types'; import { ILinkifierEvent, ITerminal } from '../../Types'; import { BOLD_CLASS, ITALIC_CLASS, CURSOR_CLASS, CURSOR_STYLE_BLOCK_CLASS, CURSOR_BLINK_CLASS, CURSOR_STYLE_BAR_CLASS, CURSOR_STYLE_UNDERLINE_CLASS, DomRendererRowFactory } from './DomRendererRowFactory'; -import { INVERTED_DEFAULT_COLOR } from '../atlas/Types'; +import { INVERTED_DEFAULT_COLOR } from '../atlas/Constants'; import { Disposable } from 'common/Lifecycle'; import { IColorSet } from 'browser/Types'; import { ICharSizeService } from 'browser/services/Services'; @@ -167,9 +167,14 @@ export class DomRenderer extends Disposable implements IRenderer { // Blink animation styles += `@keyframes blink {` + - ` 0% { opacity: 1.0; }` + - ` 50% { opacity: 0.0; }` + - ` 100% { opacity: 1.0; }` + + ` 0% {` + + ` background-color: ${this._colors.cursor.css};` + + ` color: ${this._colors.cursorAccent.css};` + + ` }` + + ` 50% {` + + ` background-color: ${this._colors.cursorAccent.css};` + + ` color: ${this._colors.cursor.css};` + + ` }` + `}`; // Cursor styles += diff --git a/src/renderer/dom/DomRendererRowFactory.test.ts b/src/renderer/dom/DomRendererRowFactory.test.ts index 28ee0a7e..6ad9641c 100644 --- a/src/renderer/dom/DomRendererRowFactory.test.ts +++ b/src/renderer/dom/DomRendererRowFactory.test.ts @@ -6,9 +6,11 @@ import jsdom = require('jsdom'); import { assert } from 'chai'; import { DomRendererRowFactory } from './DomRendererRowFactory'; -import { BufferLine, CellData, FgFlags, BgFlags, Attributes, NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, DEFAULT_ATTR, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; +import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, DEFAULT_ATTR, FgFlags, BgFlags, Attributes } from 'common/buffer/Constants'; +import { BufferLine, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; import { ITerminalOptions } from '../../Types'; import { IBufferLine } from 'common/Types'; +import { CellData } from 'common/buffer/CellData'; describe('DomRendererRowFactory', () => { let dom: jsdom.JSDOM; diff --git a/src/renderer/dom/DomRendererRowFactory.ts b/src/renderer/dom/DomRendererRowFactory.ts index 8c31aa44..687d207d 100644 --- a/src/renderer/dom/DomRendererRowFactory.ts +++ b/src/renderer/dom/DomRendererRowFactory.ts @@ -5,8 +5,10 @@ import { ITerminalOptions } from '../../Types'; import { IBufferLine } from 'common/Types'; -import { INVERTED_DEFAULT_COLOR } from '../atlas/Types'; -import { CellData, AttributeData, NULL_CELL_CODE, WHITESPACE_CELL_CHAR } from 'common/buffer/BufferLine'; +import { INVERTED_DEFAULT_COLOR } from '../atlas/Constants'; +import { AttributeData } from 'common/buffer/AttributeData'; +import { NULL_CELL_CODE, WHITESPACE_CELL_CHAR } from 'common/buffer/Constants'; +import { CellData } from 'common/buffer/CellData'; export const BOLD_CLASS = 'xterm-bold'; export const DIM_CLASS = 'xterm-dim'; diff --git a/test/InputHandler.api.ts b/test/InputHandler.api.ts index 3a95feca..cbf5dbd6 100644 --- a/test/InputHandler.api.ts +++ b/test/InputHandler.api.ts @@ -292,6 +292,23 @@ describe('InputHandler Integration Tests', function(): void { assert.deepEqual(await getLinesAsArray(3), ['#', ' #', 'abcd####']); }); }); + + describe('ESC', () => { + describe('DECRC: Save cursor, ESC 7', () => { + it('should save the absolute cursor position so resizing restores to the correct position', async () => { + await page.evaluate(` + window.term.resize(10, 2); + window.term.write('1\\n\\r2\\n\\r3\\n\\r4\\n\\r5'); + window.term.write('\\x1b7\\x1b[?47h'); + `); + await page.evaluate(` + window.term.resize(10, 4); + window.term.write('\\x1b[?47l\\x1b8'); + `); + assert.deepEqual(await getCursor(), {col: 1, row: 3}); + }); + }); + }); }); async function openTerminal(options: ITerminalOptions = {}): Promise { diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 042ca537..f435367d 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -191,7 +191,7 @@ declare module 'xterm' { cursor?: string, /** The accent color of the cursor (fg color for a block cursor) */ cursorAccent?: string, - /** The selection color (can be transparent) */ + /** The selection background color (can be transparent) */ selection?: string, /** ANSI black (eg. `\x1b[30m`) */ black?: string,