diff --git a/AUTHORS b/AUTHORS index 47328bc9..bcf1c5b8 100644 --- a/AUTHORS +++ b/AUTHORS @@ -32,6 +32,7 @@ Christopher Jeffrey coderaiser Damien Tournoud Dan Brown +Daniel Griffen Daniel Griffen Daniel Imms Daniel Risacher @@ -74,6 +75,7 @@ Martin Chloride Martin Koppehel Martin Wang Matt Bierner +Matthew James Michael Irwin Mikko Karvonen mofux diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8b33d183..4aebdf20 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -50,3 +50,7 @@ By contributing code to xterm.js you holder has explicitly granted the right to use it like this, through a compatible open source license or through a direct agreement with you.) + +### Third party dependencies + +We prefer to not include any non-dev third party dependencies in order to keep our code minimal, performant and secure. If you plan on adding a dependency on a third party library it's a good idea to discuss the need in an issue with the maintainers first. diff --git a/Dockerfile b/Dockerfile index 1c72e679..1e3a262c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,4 +16,4 @@ RUN npm install COPY . /usr/src/app # Run the tests and build, to make sure everything is working nicely -RUN npm run build && npm run test +RUN npm run build && npm run webpack && npm run test diff --git a/README.md b/README.md index cdaf0d71..26f98382 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,7 @@ computational environment for Jupyter, supporting interactive data science and s - [**abstruse**](https://github.com/bleenco/abstruse): Abstruse CI is a continuous integration platform based on Node.JS and Docker. - [**Microsoft SQL Operations Studio**](https://github.com/Microsoft/sqlopsstudio): A data management tool that enables working with SQL Server, Azure SQL DB and SQL DW from Windows, macOS and Linux - [**FreeMAN**](https://github.com/matthew-matvei/freeman): A free, cross-platform file manager for power users +- [**Fluent Terminal**](https://github.com/felixse/FluentTerminal): A terminal emulator based on UWP and web technologies. Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it in our list. @@ -144,7 +145,19 @@ Then open your project's [Public URL](https://help.sourcelair.com/projects/the-p ### Docker -First, make sure you have Docker Engine 1.13.0 (or newer) and Docker Compose 1.10.0 (or newer). To run the demo and builder in parallel, run the following command in your terminal: +First, make sure you have Docker Engine 1.13.0 (or newer) and Docker Compose 1.10.0 (or newer). + +Xterm.js [provides a pre-built Docker image](https://hub.docker.com/r/xtermjs/xterm.js/) to help run the demo easily (Git tags are built as [tagged Docker images](https://hub.docker.com/r/xtermjs/xterm.js/tags/) too). + +To run the just demo (with no editing access). run the following command in your terminal: + +``` +docker run -p 3000:3000 xtermjs/xterm.js +``` + +Then open http://0.0.0.0:3000 in a web browser to access the demo. + +To run the demo and builder in parallel, run the following command in your terminal: ``` docker-compose up diff --git a/demo/main.js b/demo/main.js index 7feb7939..d9c0151a 100644 --- a/demo/main.js +++ b/demo/main.js @@ -3,6 +3,7 @@ import * as attach from '../build/addons/attach/attach'; import * as fit from '../build/addons/fit/fit'; import * as fullscreen from '../build/addons/fullscreen/fullscreen'; import * as search from '../build/addons/search/search'; +import * as webLinks from '../build/addons/webLinks/webLinks'; import * as winptyCompat from '../build/addons/winptyCompat/winptyCompat'; @@ -10,6 +11,7 @@ Terminal.applyAddon(attach); Terminal.applyAddon(fit); Terminal.applyAddon(fullscreen); Terminal.applyAddon(search); +Terminal.applyAddon(webLinks); Terminal.applyAddon(winptyCompat); @@ -121,6 +123,7 @@ function createTerminal() { term.open(terminalContainer); term.winptyCompatInit(); + term.webLinksInit(); term.fit(); term.focus(); diff --git a/docker-compose.yml b/docker-compose.yml index effd975c..8e6a2f46 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,6 +2,7 @@ version: "3" services: web: + image: xtermjs/xterm.js:latest build: . volumes: - ./:/usr/src/app diff --git a/gulpfile.js b/gulpfile.js index 4353e46d..af4fdce0 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -23,7 +23,7 @@ const tsProject = ts.createProject('tsconfig.json'); const srcDir = tsProject.config.compilerOptions.rootDir; let outDir = tsProject.config.compilerOptions.outDir; -const addons = ['attach', 'fit', 'fullscreen', 'search', 'terminado', 'winptyCompat', 'zmodem']; +const addons = fs.readdirSync(`${__dirname}/src/addons`); // Under some environments like TravisCI, this comes out at absolute which can // break the build. This ensures that the outDir is absolute. diff --git a/package-lock.json b/package-lock.json index 03bbd115..59522f3d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "xterm", - "version": "3.1.0-master", + "version": "3.2.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 079333de..594c31a7 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "xterm", "description": "Full xterm terminal, in your browser", - "version": "3.1.0", + "version": "3.2.0", "ignore": [ "demo", "test", diff --git a/src/AccessibilityManager.ts b/src/AccessibilityManager.ts index cb915516..1df6a7d2 100644 --- a/src/AccessibilityManager.ts +++ b/src/AccessibilityManager.ts @@ -11,7 +11,6 @@ import { addDisposableListener } from './utils/Dom'; import { IDisposable } from 'xterm'; const MAX_ROWS_TO_READ = 20; -const ACTIVE_ITEM_ID_PREFIX = 'xterm-active-item-'; enum BoundaryPosition { Top, @@ -21,7 +20,7 @@ enum BoundaryPosition { export class AccessibilityManager implements IDisposable { private _accessibilityTreeRoot: HTMLElement; private _rowContainer: HTMLElement; - private _rowElements: HTMLElement[] = []; + private _rowElements: HTMLElement[]; private _liveRegion: HTMLElement; private _liveRegionLineCount: number = 0; @@ -49,6 +48,7 @@ export class AccessibilityManager implements IDisposable { this._rowContainer = document.createElement('div'); this._rowContainer.classList.add('xterm-accessibility-tree'); + this._rowElements = []; for (let i = 0; i < this._terminal.rows; i++) { this._rowElements[i] = this._createAccessibilityTreeNode(); this._rowContainer.appendChild(this._rowElements[i]); @@ -93,14 +93,10 @@ export class AccessibilityManager implements IDisposable { } public dispose(): void { - this._terminal.element.removeChild(this._accessibilityTreeRoot); this._disposables.forEach(d => d.dispose()); - this._disposables = null; - this._accessibilityTreeRoot = null; - this._rowContainer = null; - this._liveRegion = null; - this._rowContainer = null; - this._rowElements = null; + this._disposables.length = 0; + this._terminal.element.removeChild(this._accessibilityTreeRoot); + this._rowElements.length = 0; } private _onBoundaryFocus(e: FocusEvent, position: BoundaryPosition): void { @@ -125,10 +121,10 @@ export class AccessibilityManager implements IDisposable { let bottomBoundaryElement: HTMLElement; if (position === BoundaryPosition.Top) { topBoundaryElement = boundaryElement; - bottomBoundaryElement = this._rowElements.pop(); + bottomBoundaryElement = this._rowElements.pop(); this._rowContainer.removeChild(bottomBoundaryElement); } else { - topBoundaryElement = this._rowElements.shift(); + topBoundaryElement = this._rowElements.shift(); bottomBoundaryElement = boundaryElement; this._rowContainer.removeChild(topBoundaryElement); } @@ -174,7 +170,7 @@ export class AccessibilityManager implements IDisposable { } // Shrink rows as required while (this._rowElements.length > rows) { - this._rowContainer.removeChild(this._rowElements.pop()); + this._rowContainer.removeChild(this._rowElements.pop()); } // Add bottom boundary listener @@ -218,7 +214,7 @@ export class AccessibilityManager implements IDisposable { // Only detach/attach on mac as otherwise messages can go unaccounced if (isMac) { - if (this._liveRegion.textContent.length > 0 && !this._liveRegion.parentNode) { + if (this._liveRegion.textContent && this._liveRegion.textContent.length > 0 && !this._liveRegion.parentNode) { setTimeout(() => { this._accessibilityTreeRoot.appendChild(this._liveRegion); }, 0); @@ -265,7 +261,6 @@ export class AccessibilityManager implements IDisposable { if (!this._terminal.renderer.dimensions.actualCellHeight) { return; } - const buffer: IBuffer = this._terminal.buffer; for (let i = 0; i < this._terminal.rows; i++) { this._refreshRowDimensions(this._rowElements[i]); } diff --git a/src/CompositionHelper.ts b/src/CompositionHelper.ts index 2aa0449f..387d3f8f 100644 --- a/src/CompositionHelper.ts +++ b/src/CompositionHelper.ts @@ -20,43 +20,43 @@ export class CompositionHelper { * Whether input composition is currently happening, eg. via a mobile keyboard, speech input or * IME. This variable determines whether the compositionText should be displayed on the UI. */ - private isComposing: boolean; + private _isComposing: boolean; /** * The position within the input textarea's value of the current composition. */ - private compositionPosition: IPosition; + private _compositionPosition: IPosition; /** * Whether a composition is in the process of being sent, setting this to false will cancel any * in-progress composition. */ - private isSendingComposition: boolean; + private _isSendingComposition: boolean; /** * Creates a new CompositionHelper. - * @param textarea The textarea that xterm uses for input. - * @param compositionView The element to display the in-progress composition in. - * @param terminal The Terminal to forward the finished composition to. + * @param _textarea The textarea that xterm uses for input. + * @param _compositionView The element to display the in-progress composition in. + * @param _terminal The Terminal to forward the finished composition to. */ constructor( - private textarea: HTMLTextAreaElement, - private compositionView: HTMLElement, - private terminal: ITerminal + private _textarea: HTMLTextAreaElement, + private _compositionView: HTMLElement, + private _terminal: ITerminal ) { - this.isComposing = false; - this.isSendingComposition = false; - this.compositionPosition = { start: null, end: null }; + this._isComposing = false; + this._isSendingComposition = false; + this._compositionPosition = { start: null, end: null }; } /** * Handles the compositionstart event, activating the composition view. */ public compositionstart(): void { - this.isComposing = true; - this.compositionPosition.start = this.textarea.value.length; - this.compositionView.textContent = ''; - this.compositionView.classList.add('active'); + this._isComposing = true; + this._compositionPosition.start = this._textarea.value.length; + this._compositionView.textContent = ''; + this._compositionView.classList.add('active'); } /** @@ -64,10 +64,10 @@ export class CompositionHelper { * @param {CompositionEvent} ev The event. */ public compositionupdate(ev: CompositionEvent): void { - this.compositionView.textContent = ev.data; + this._compositionView.textContent = ev.data; this.updateCompositionElements(); setTimeout(() => { - this.compositionPosition.end = this.textarea.value.length; + this._compositionPosition.end = this._textarea.value.length; }, 0); } @@ -76,7 +76,7 @@ export class CompositionHelper { * the handler. */ public compositionend(): void { - this.finalizeComposition(true); + this._finalizeComposition(true); } /** @@ -85,7 +85,7 @@ export class CompositionHelper { * @return Whether the Terminal should continue processing the keydown event. */ public keydown(ev: KeyboardEvent): boolean { - if (this.isComposing || this.isSendingComposition) { + if (this._isComposing || this._isSendingComposition) { if (ev.keyCode === 229) { // Continue composing if the keyCode is the "composition character" return false; @@ -95,14 +95,14 @@ export class CompositionHelper { } else { // Finish composition immediately. This is mainly here for the case where enter is // pressed and the handler needs to be triggered before the command is executed. - this.finalizeComposition(false); + this._finalizeComposition(false); } } if (ev.keyCode === 229) { // If the "composition character" is used but gets to this point it means a non-composition // character (eg. numbers and punctuation) was pressed when the IME was active. - this.handleAnyTextareaChanges(); + this._handleAnyTextareaChanges(); return false; } @@ -117,22 +117,22 @@ export class CompositionHelper { * compositionend event is triggered, such as enter, so that the composition is send before * the command is executed. */ - private finalizeComposition(waitForPropogation: boolean): void { - this.compositionView.classList.remove('active'); - this.isComposing = false; - this.clearTextareaPosition(); + private _finalizeComposition(waitForPropogation: boolean): void { + this._compositionView.classList.remove('active'); + this._isComposing = false; + this._clearTextareaPosition(); if (!waitForPropogation) { // Cancel any delayed composition send requests and send the input immediately. - this.isSendingComposition = false; - const input = this.textarea.value.substring(this.compositionPosition.start, this.compositionPosition.end); - this.terminal.handler(input); + this._isSendingComposition = false; + const input = this._textarea.value.substring(this._compositionPosition.start, this._compositionPosition.end); + this._terminal.handler(input); } else { // Make a deep copy of the composition position here as a new compositionstart event may // fire before the setTimeout executes. const currentCompositionPosition = { - start: this.compositionPosition.start, - end: this.compositionPosition.end, + start: this._compositionPosition.start, + end: this._compositionPosition.end, }; // Since composition* events happen before the changes take place in the textarea on most @@ -143,22 +143,22 @@ export class CompositionHelper { // - The last compositionupdate event's data property does not always accurately describe // the character, a counter example being Korean where an ending consonsant can move to // the following character if the following input is a vowel. - this.isSendingComposition = true; + this._isSendingComposition = true; setTimeout(() => { // Ensure that the input has not already been sent - if (this.isSendingComposition) { - this.isSendingComposition = false; + if (this._isSendingComposition) { + this._isSendingComposition = false; let input; - if (this.isComposing) { + if (this._isComposing) { // Use the end position to get the string if a new composition has started. - input = this.textarea.value.substring(currentCompositionPosition.start, currentCompositionPosition.end); + input = this._textarea.value.substring(currentCompositionPosition.start, currentCompositionPosition.end); } else { // Don't use the end position here in order to pick up any characters after the // composition has finished, for example when typing a non-composition character // (eg. 2) after a composition character. - input = this.textarea.value.substring(currentCompositionPosition.start); + input = this._textarea.value.substring(currentCompositionPosition.start); } - this.terminal.handler(input); + this._terminal.handler(input); } }, 0); } @@ -170,15 +170,15 @@ export class CompositionHelper { * character" (229) is triggered, in order to allow non-composition text to be entered when an * IME is active. */ - private handleAnyTextareaChanges(): void { - const oldValue = this.textarea.value; + private _handleAnyTextareaChanges(): void { + const oldValue = this._textarea.value; setTimeout(() => { // Ignore if a composition has started since the timeout - if (!this.isComposing) { - const newValue = this.textarea.value; + if (!this._isComposing) { + const newValue = this._textarea.value; const diff = newValue.replace(oldValue, ''); if (diff.length > 0) { - this.terminal.handler(diff); + this._terminal.handler(diff); } } }, 0); @@ -191,27 +191,27 @@ export class CompositionHelper { * necessary as the IME events across browsers are not consistently triggered. */ public updateCompositionElements(dontRecurse?: boolean): void { - if (!this.isComposing) { + if (!this._isComposing) { return; } - if (this.terminal.buffer.isCursorInViewport) { - const cellHeight = Math.ceil(this.terminal.charMeasure.height * this.terminal.options.lineHeight); - const cursorTop = this.terminal.buffer.y * cellHeight; - const cursorLeft = this.terminal.buffer.x * this.terminal.charMeasure.width; + if (this._terminal.buffer.isCursorInViewport) { + const cellHeight = Math.ceil(this._terminal.charMeasure.height * this._terminal.options.lineHeight); + const cursorTop = this._terminal.buffer.y * cellHeight; + const cursorLeft = this._terminal.buffer.x * this._terminal.charMeasure.width; - this.compositionView.style.left = cursorLeft + 'px'; - this.compositionView.style.top = cursorTop + 'px'; - this.compositionView.style.height = cellHeight + 'px'; - this.compositionView.style.lineHeight = cellHeight + 'px'; + this._compositionView.style.left = cursorLeft + 'px'; + this._compositionView.style.top = cursorTop + 'px'; + this._compositionView.style.height = cellHeight + 'px'; + this._compositionView.style.lineHeight = cellHeight + 'px'; // Sync the textarea to the exact position of the composition view so the IME knows where the // text is. - const compositionViewBounds = this.compositionView.getBoundingClientRect(); - this.textarea.style.left = cursorLeft + 'px'; - this.textarea.style.top = cursorTop + 'px'; - this.textarea.style.width = compositionViewBounds.width + 'px'; - this.textarea.style.height = compositionViewBounds.height + 'px'; - this.textarea.style.lineHeight = compositionViewBounds.height + 'px'; + const compositionViewBounds = this._compositionView.getBoundingClientRect(); + this._textarea.style.left = cursorLeft + 'px'; + this._textarea.style.top = cursorTop + 'px'; + this._textarea.style.width = compositionViewBounds.width + 'px'; + this._textarea.style.height = compositionViewBounds.height + 'px'; + this._textarea.style.lineHeight = compositionViewBounds.height + 'px'; } if (!dontRecurse) { @@ -223,8 +223,8 @@ export class CompositionHelper { * Clears the textarea's position so that the cursor does not blink on IE. * @private */ - private clearTextareaPosition(): void { - this.textarea.style.left = ''; - this.textarea.style.top = ''; + private _clearTextareaPosition(): void { + this._textarea.style.left = ''; + this._textarea.style.top = ''; } } diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 18338ae7..acf7af1f 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -4,7 +4,7 @@ * @license MIT */ -import { CharData, IInputHandler, IInputHandlingTerminal, ITerminal } from './Types'; +import { CharData, IInputHandler, IInputHandlingTerminal } from './Types'; import { C0 } from './EscapeSequences'; import { DEFAULT_CHARSET } from './Charsets'; import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX } from './Buffer'; diff --git a/src/Linkifier.test.ts b/src/Linkifier.test.ts index 14b1ac0f..1aaf02c9 100644 --- a/src/Linkifier.test.ts +++ b/src/Linkifier.test.ts @@ -5,7 +5,7 @@ import { assert } from 'chai'; import { IMouseZoneManager, IMouseZone } from './input/Types'; -import { ILinkMatcher, LineData, ITerminal, ILinkifier, IBuffer, IBufferAccessor, IElementAccessor } from './Types'; +import { ILinkMatcher, LineData, IBufferAccessor, IElementAccessor } from './Types'; import { Linkifier } from './Linkifier'; import { MockBuffer } from './utils/TestUtils.test'; import { CircularList } from './utils/CircularList'; @@ -59,17 +59,6 @@ describe('Linkifier', () => { terminal.buffer.lines.push(stringToRow(text)); } - function assertLinkifiesEntireRow(uri: string, done: MochaDone): void { - addRow(uri); - linkifier.linkifyRows(); - setTimeout(() => { - assert.equal(mouseZoneManager.zones[0].x1, 1); - assert.equal(mouseZoneManager.zones[0].x2, uri.length + 1); - assert.equal(mouseZoneManager.zones[0].y, terminal.buffer.lines.length); - done(); - }, 0); - } - function assertLinkifiesRow(rowText: string, linkMatcherRegex: RegExp, links: {x: number, length: number}[], done: MochaDone): void { addRow(rowText); linkifier.registerLinkMatcher(linkMatcherRegex, () => {}); @@ -101,12 +90,6 @@ describe('Linkifier', () => { linkifier.attachToDom(mouseZoneManager); }); - describe('http links', () => { - it('should allow ~ character in URI path', (done) => { - assertLinkifiesEntireRow('http://foo.com/a~b#c~d?e~f', done); - }); - }); - describe('link matcher', () => { it('should match a single link', done => { assertLinkifiesRow('foo', /foo/, [{x: 0, length: 3}], done); @@ -200,19 +183,19 @@ describe('Linkifier', () => { it('should order the list from highest priority to lowest #1', () => { const aId = linkifier.registerLinkMatcher(/a/, () => {}, { priority: 1 }); const bId = linkifier.registerLinkMatcher(/b/, () => {}, { priority: -1 }); - assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [aId, 0, bId]); + assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [aId, bId]); }); it('should order the list from highest priority to lowest #2', () => { const aId = linkifier.registerLinkMatcher(/a/, () => {}, { priority: -1 }); const bId = linkifier.registerLinkMatcher(/b/, () => {}, { priority: 1 }); - assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [bId, 0, aId]); + assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [bId, aId]); }); it('should order items of equal priority in the order they are added', () => { const aId = linkifier.registerLinkMatcher(/a/, () => {}, { priority: 0 }); const bId = linkifier.registerLinkMatcher(/b/, () => {}, { priority: 0 }); - assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [0, aId, bId]); + assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [aId, bId]); }); }); }); diff --git a/src/Linkifier.ts b/src/Linkifier.ts index a9617952..93eb9063 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -4,34 +4,10 @@ */ import { IMouseZoneManager } from './input/Types'; -import { ILinkHoverEvent, ILinkMatcher, LinkMatcherHandler, LinkMatcherValidationCallback, LineData, LinkHoverEventTypes, ILinkMatcherOptions, ITerminal, IBufferAccessor, ILinkifier, IElementAccessor } from './Types'; +import { ILinkHoverEvent, ILinkMatcher, LinkMatcherHandler, LinkHoverEventTypes, ILinkMatcherOptions, IBufferAccessor, ILinkifier, IElementAccessor } from './Types'; import { MouseZone } from './input/MouseZoneManager'; import { EventEmitter } from './EventEmitter'; -const protocolClause = '(https?:\\/\\/)'; -const domainCharacterSet = '[\\da-z\\.-]+'; -const negatedDomainCharacterSet = '[^\\da-z\\.-]+'; -const domainBodyClause = '(' + domainCharacterSet + ')'; -const tldClause = '([a-z\\.]{2,6})'; -const ipClause = '((\\d{1,3}\\.){3}\\d{1,3})'; -const localHostClause = '(localhost)'; -const portClause = '(:\\d{1,5})'; -const hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|' + ipClause + '|' + localHostClause + ')' + portClause + '?'; -const pathClause = '(\\/[\\/\\w\\.\\-%~]*)*'; -const queryStringHashFragmentCharacterSet = '[0-9\\w\\[\\]\\(\\)\\/\\?\\!#@$%&\'*+,:;~\\=\\.\\-]*'; -const queryStringClause = '(\\?' + queryStringHashFragmentCharacterSet + ')?'; -const hashFragmentClause = '(#' + queryStringHashFragmentCharacterSet + ')?'; -const negatedPathCharacterSet = '[^\\/\\w\\.\\-%]+'; -const bodyClause = hostClause + pathClause + queryStringClause + hashFragmentClause; -const start = '(?:^|' + negatedDomainCharacterSet + ')('; -const end = ')($|' + negatedPathCharacterSet + ')'; -const strictUrlRegex = new RegExp(start + protocolClause + bodyClause + end); - -/** - * The ID of the built in http(s) link matcher. - */ -const HYPERTEXT_LINK_MATCHER_ID = 0; - /** * The Linkifier applies links to rows shortly after they have been refreshed. */ @@ -47,7 +23,7 @@ export class Linkifier extends EventEmitter implements ILinkifier { private _mouseZoneManager: IMouseZoneManager; private _rowsTimeoutId: number; - private _nextLinkMatcherId = HYPERTEXT_LINK_MATCHER_ID; + private _nextLinkMatcherId = 0; private _rowsToLinkify: {start: number, end: number}; constructor( @@ -58,7 +34,6 @@ export class Linkifier extends EventEmitter implements ILinkifier { start: null, end: null }; - this.registerLinkMatcher(strictUrlRegex, null, { matchIndex: 1 }); } /** @@ -111,23 +86,6 @@ export class Linkifier extends EventEmitter implements ILinkifier { this._rowsToLinkify.end = null; } - /** - * Attaches a handler for hypertext links, overriding default behavior for - * tandard http(s) links. - * @param handler The handler to use, this can be cleared with null. - */ - public setHypertextLinkHandler(handler: LinkMatcherHandler): void { - this._linkMatchers[HYPERTEXT_LINK_MATCHER_ID].handler = handler; - } - - /** - * Attaches a validation callback for hypertext links. - * @param callback The callback to use, this can be cleared with null. - */ - public setHypertextValidationCallback(callback: LinkMatcherValidationCallback): void { - this._linkMatchers[HYPERTEXT_LINK_MATCHER_ID].validationCallback = callback; - } - /** * Registers a link matcher, allowing custom link patterns to be matched and * handled. @@ -139,7 +97,7 @@ export class Linkifier extends EventEmitter implements ILinkifier { * @return The ID of the new matcher, this can be used to deregister. */ public registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options: ILinkMatcherOptions = {}): number { - if (this._nextLinkMatcherId !== HYPERTEXT_LINK_MATCHER_ID && !handler) { + if (!handler) { throw new Error('handler must be defined'); } const matcher: ILinkMatcher = { @@ -185,8 +143,7 @@ export class Linkifier extends EventEmitter implements ILinkifier { * @return Whether a link matcher was found and deregistered. */ public deregisterLinkMatcher(matcherId: number): boolean { - // ID 0 is the hypertext link matcher which cannot be deregistered - for (let i = 1; i < this._linkMatchers.length; i++) { + for (let i = 0; i < this._linkMatchers.length; i++) { if (this._linkMatchers[i].id === matcherId) { this._linkMatchers.splice(i, 1); return true; @@ -220,10 +177,6 @@ export class Linkifier extends EventEmitter implements ILinkifier { * @return The link element(s) that were added. */ private _doLinkifyRow(rowIndex: number, text: string, matcher: ILinkMatcher, offset: number = 0): void { - // Iterate over nodes as we want to consider text nodes - let result = []; - const isHttpLinkMatcher = matcher.id === HYPERTEXT_LINK_MATCHER_ID; - // Find the first match let match = text.match(matcher.regex); if (!match || match.length === 0) { @@ -277,7 +230,7 @@ export class Linkifier extends EventEmitter implements ILinkifier { }, e => { this.emit(LinkHoverEventTypes.HOVER, { x, y, length: uri.length}); - this._terminal.element.style.cursor = 'pointer'; + this._terminal.element.classList.add('xterm-cursor-pointer'); }, e => { this.emit(LinkHoverEventTypes.TOOLTIP, { x, y, length: uri.length}); @@ -287,7 +240,7 @@ export class Linkifier extends EventEmitter implements ILinkifier { }, () => { this.emit(LinkHoverEventTypes.LEAVE, { x, y, length: uri.length}); - this._terminal.element.style.cursor = ''; + this._terminal.element.classList.remove('xterm-cursor-pointer'); if (matcher.hoverLeaveCallback) { matcher.hoverLeaveCallback(); } diff --git a/src/Parser.ts b/src/Parser.ts index 03b4a39e..21e5a612 100644 --- a/src/Parser.ts +++ b/src/Parser.ts @@ -5,7 +5,7 @@ */ import { C0 } from './EscapeSequences'; -import { IInputHandler } from './Types'; +import { IInputHandler, IInputHandlingTerminal } from './Types'; import { CHARSETS, DEFAULT_CHARSET } from './Charsets'; const normalStateHandler: {[key: string]: (parser: Parser, handler: IInputHandler) => void} = {}; @@ -185,7 +185,6 @@ export class Parser { */ public parse(data: string): ParserState { const l = data.length; - let j; let cs; let ch; let code; @@ -346,7 +345,7 @@ export class Parser { // ESC H Tab Set (HTS is 0x88). case 'H': - this._terminal.tabSet(); + (this._terminal).tabSet(); this._state = ParserState.NORMAL; break; diff --git a/src/SelectionManager.test.ts b/src/SelectionManager.test.ts index a76947e5..3dae2c31 100644 --- a/src/SelectionManager.test.ts +++ b/src/SelectionManager.test.ts @@ -3,14 +3,12 @@ * @license MIT */ -import jsdom = require('jsdom'); import { assert } from 'chai'; import { CharMeasure } from './utils/CharMeasure'; -import { CircularList } from './utils/CircularList'; import { SelectionManager } from './SelectionManager'; import { SelectionModel } from './SelectionModel'; import { BufferSet } from './BufferSet'; -import { LineData, CharData, ITerminal, ICircularList, IBuffer } from './Types'; +import { LineData, CharData, ITerminal, IBuffer } from './Types'; import { MockTerminal } from './utils/TestUtils.test'; class TestMockTerminal extends MockTerminal { @@ -37,19 +35,11 @@ class TestSelectionManager extends SelectionManager { } describe('SelectionManager', () => { - let dom: jsdom.JSDOM; - let window: Window; - let document: Document; - let terminal: ITerminal; let buffer: IBuffer; - let rowContainer: HTMLElement; let selectionManager: TestSelectionManager; beforeEach(() => { - dom = new jsdom.JSDOM(''); - window = dom.window; - document = window.document; terminal = new TestMockTerminal(); terminal.cols = 80; terminal.rows = 2; diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index eaf813c9..506e87d4 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -3,11 +3,10 @@ * @license MIT */ -import { ITerminal, ICircularList, ISelectionManager, IBuffer, LineData, CharData, XtermListener } from './Types'; +import { ITerminal, ISelectionManager, IBuffer, CharData, XtermListener } from './Types'; import { MouseHelper } from './utils/MouseHelper'; import * as Browser from './shared/utils/Browser'; import { CharMeasure } from './utils/CharMeasure'; -import { CircularList } from './utils/CircularList'; import { EventEmitter } from './EventEmitter'; import { SelectionModel } from './SelectionModel'; import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from './Buffer'; @@ -634,6 +633,11 @@ export class SelectionManager extends EventEmitter implements ISelectionManager * @param coords The coordinates to get the word at. */ private _getWordAt(coords: [number, number], allowWhitespaceOnlySelection: boolean): IWordPosition { + // Ensure coords are within viewport (eg. not within scroll bar) + if (coords[0] >= this._terminal.cols) { + return null; + } + const bufferLine = this._buffer.lines.get(coords[1]); if (!bufferLine) { return null; diff --git a/src/SelectionModel.test.ts b/src/SelectionModel.test.ts index ed483dbe..85486bab 100644 --- a/src/SelectionModel.test.ts +++ b/src/SelectionModel.test.ts @@ -18,9 +18,6 @@ class TestSelectionModel extends SelectionModel { } describe('SelectionManager', () => { - let window: Window; - let document: Document; - let terminal: ITerminal; let model: TestSelectionModel; diff --git a/src/SoundManager.ts b/src/SoundManager.ts index 1c50cbbc..4139c207 100644 --- a/src/SoundManager.ts +++ b/src/SoundManager.ts @@ -28,7 +28,7 @@ export class SoundManager implements ISoundManager { if (this._audioContext) { const bellAudioSource = this._audioContext.createBufferSource(); const context = this._audioContext; - this._audioContext.decodeAudioData(this.base64ToArrayBuffer(this.removeMimeType(this._terminal.options.bellSound)), (buffer) => { + this._audioContext.decodeAudioData(this._base64ToArrayBuffer(this._removeMimeType(this._terminal.options.bellSound)), (buffer) => { bellAudioSource.buffer = buffer; bellAudioSource.connect(context.destination); bellAudioSource.start(0); @@ -38,7 +38,7 @@ export class SoundManager implements ISoundManager { } } - private base64ToArrayBuffer(base64: string): ArrayBuffer { + private _base64ToArrayBuffer(base64: string): ArrayBuffer { const binaryString = window.atob(base64); const len = binaryString.length; const bytes = new Uint8Array(len); @@ -50,7 +50,7 @@ export class SoundManager implements ISoundManager { return bytes.buffer; } - private removeMimeType(dataURI: string): string { + private _removeMimeType(dataURI: string): string { // Split the input to get the mime-type and the data itself const splitUri = dataURI.split(','); diff --git a/src/Terminal.integration.ts b/src/Terminal.integration.ts index 283c5bc0..fdf27acf 100644 --- a/src/Terminal.integration.ts +++ b/src/Terminal.integration.ts @@ -125,7 +125,7 @@ if (os.platform() !== 'win32') { // Perform a synchronous .write(data) xterm.writeBuffer.push(fromPty); - xterm.innerWrite(); + xterm._innerWrite(); let fromEmulator = terminalToString(xterm); console.log = CONSOLE_LOG; diff --git a/src/Terminal.test.ts b/src/Terminal.test.ts index 12467d84..0db9545f 100644 --- a/src/Terminal.test.ts +++ b/src/Terminal.test.ts @@ -29,11 +29,11 @@ describe('term.js addons', () => { term.refresh = () => {}; (term).renderer = new MockRenderer(); term.viewport = new MockViewport(); - (term).compositionHelper = new MockCompositionHelper(); + (term)._compositionHelper = new MockCompositionHelper(); // Force synchronous writes term.write = (data) => { term.writeBuffer.push(data); - (term).innerWrite(); + (term)._innerWrite(); }; (term).element = { classList: { diff --git a/src/Terminal.ts b/src/Terminal.ts index 721996c9..a0d6ed6f 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -21,7 +21,7 @@ * http://linux.die.net/man/7/urxvt */ -import { ICharset, IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, LinkMatcherValidationCallback, CharData, LineData } from './Types'; +import { ICharset, IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharData, LineData } from './Types'; import { IMouseZoneManager } from './input/Types'; import { IRenderer } from './renderer/Types'; import { BufferSet } from './BufferSet'; @@ -30,7 +30,6 @@ import { CompositionHelper } from './CompositionHelper'; import { EventEmitter } from './EventEmitter'; import { Viewport } from './Viewport'; import { rightClickHandler, moveTextAreaUnderMouseCursor, pasteHandler, copyHandler } from './handlers/Clipboard'; -import { CircularList } from './utils/CircularList'; import { C0 } from './EscapeSequences'; import { InputHandler } from './InputHandler'; import { Parser } from './Parser'; @@ -41,7 +40,6 @@ import { CharMeasure } from './utils/CharMeasure'; import * as Browser from './shared/utils/Browser'; import * as Strings from './Strings'; import { MouseHelper } from './utils/MouseHelper'; -import { CHARSETS } from './Charsets'; import { DEFAULT_BELL_SOUND, SoundManager } from './SoundManager'; import { DEFAULT_ANSI_COLORS } from './renderer/ColorManager'; import { MouseZoneManager } from './input/MouseZoneManager'; @@ -120,7 +118,7 @@ const DEFAULT_OPTIONS: ITerminalOptions = { allowTransparency: false, tabStopWidth: 8, theme: null, - rightClickSelectsWord: Browser.isMac + rightClickSelectsWord: Browser.isMac, // programFeatures: false, // focusKeys: false, }; @@ -133,30 +131,27 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT /** * The HTMLElement that the terminal is created in, set by Terminal.open. */ - private parent: HTMLElement; - private context: Window; - private document: Document; - private body: HTMLBodyElement; - private viewportScrollArea: HTMLElement; - private viewportElement: HTMLElement; - private helperContainer: HTMLElement; - private compositionView: HTMLElement; - private charSizeStyleElement: HTMLStyleElement; + private _parent: HTMLElement; + private _context: Window; + private _document: Document; + private _viewportScrollArea: HTMLElement; + private _viewportElement: HTMLElement; + private _helperContainer: HTMLElement; + private _compositionView: HTMLElement; - private visualBellTimer: number; + private _visualBellTimer: number; public browser: IBrowser = Browser; public options: ITerminalOptions; - private colors: any; // TODO: This can be changed to an enum or boolean, 0 and 1 seem to be the only options public cursorState: number; public cursorHidden: boolean; public convertEol: boolean; - private sendDataQueue: string; - private customKeyEventHandler: CustomKeyEventHandler; + private _sendDataQueue: string; + private _customKeyEventHandler: CustomKeyEventHandler; // modes public applicationKeypad: boolean; @@ -174,10 +169,10 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT public charsets: ICharset[]; // mouse properties - private decLocator: boolean; // This is unstable and never set + private _decLocator: boolean; // This is unstable and never set public x10Mouse: boolean; public vt200Mouse: boolean; - private vt300Mouse: boolean; // This is unstable and never set + private _vt300Mouse: boolean; // This is unstable and never set public normalMouse: boolean; public mouseEvents: boolean; public sendFocus: boolean; @@ -186,14 +181,10 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT public urxvtMouse: boolean; // misc - private refreshStart: number; - private refreshEnd: number; + private _refreshStart: number; + private _refreshEnd: number; public savedCols: number; - // stream - private readable: boolean; - private writable: boolean; - public defAttr: number; public curAttr: number; @@ -204,7 +195,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT // user input states public writeBuffer: string[]; - private writeInProgress: boolean; + private _writeInProgress: boolean; /** * Whether _xterm.js_ sent XOFF in order to catch up with the pty process. @@ -212,26 +203,23 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT * XOFF via ^S that it will not automatically resume when the writeBuffer goes * below threshold. */ - private xoffSentToCatchUp: boolean; + private _xoffSentToCatchUp: boolean; /** Whether writing has been stopped as a result of XOFF */ - private writeStopped: boolean; - - // leftover surrogate high from previous write invocation - private surrogateHigh: string; + // private _writeStopped: boolean; // Store if user went browsing history in scrollback - private userScrolling: boolean; + private _userScrolling: boolean; - private inputHandler: InputHandler; + private _inputHandler: InputHandler; public soundManager: SoundManager; - private parser: Parser; + private _parser: Parser; public renderer: IRenderer; public selectionManager: SelectionManager; public linkifier: ILinkifier; public buffers: BufferSet; public viewport: IViewport; - private compositionHelper: ICompositionHelper; + private _compositionHelper: ICompositionHelper; public charMeasure: CharMeasure; private _mouseZoneManager: IMouseZoneManager; public mouseHelper: MouseHelper; @@ -258,10 +246,10 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT ) { super(); this.options = options; - this.setup(); + this._setup(); } - private setup(): void { + private _setup(): void { Object.keys(DEFAULT_OPTIONS).forEach((key) => { if (this.options[key] == null) { this.options[key] = DEFAULT_OPTIONS[key]; @@ -273,7 +261,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT // this.context = options.context || window; // this.document = options.document || document; // TODO: WHy not document.body? - this.parent = document ? document.body : null; + this._parent = document ? document.body : null; this.cols = this.options.cols; this.rows = this.options.rows; @@ -284,8 +272,8 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.cursorState = 0; this.cursorHidden = false; - this.sendDataQueue = ''; - this.customKeyEventHandler = null; + this._sendDataQueue = ''; + this._customKeyEventHandler = null; // modes this.applicationKeypad = false; @@ -302,9 +290,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT // TODO: Can this be just []? this.charsets = [null]; - this.readable = true; - this.writable = true; - this.defAttr = (0 << 18) | (257 << 9) | (256 << 0); this.curAttr = (0 << 18) | (257 << 9) | (256 << 0); @@ -315,15 +300,14 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT // user input states this.writeBuffer = []; - this.writeInProgress = false; + this._writeInProgress = false; - this.xoffSentToCatchUp = false; - this.writeStopped = false; - this.surrogateHigh = ''; - this.userScrolling = false; + this._xoffSentToCatchUp = false; + // this._writeStopped = false; + this._userScrolling = false; - this.inputHandler = new InputHandler(this); - this.parser = new Parser(this.inputHandler, this); + this._inputHandler = new InputHandler(this); + this._parser = new Parser(this._inputHandler, this); // Reuse renderer if the Terminal is being recreated via a reset call. this.renderer = this.renderer || null; this.selectionManager = this.selectionManager || null; @@ -472,11 +456,9 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT case 'lineHeight': case 'fontWeight': case 'fontWeightBold': - const didCharSizeChange = (key === 'fontWeight' || key === 'fontWeightBold' || key === 'enableBold'); - // When the font changes the size of the cells may change which requires a renderer clear this.renderer.clear(); - this.renderer.onResize(this.cols, this.rows, didCharSizeChange); + this.renderer.onResize(this.cols, this.rows); this.refresh(0, this.rows - 1); case 'scrollback': this.buffers.resize(this.cols, this.rows); @@ -540,8 +522,8 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT /** * Initialize default behavior */ - private initGlobal(): void { - this.bindKeys(); + private _initGlobal(): void { + this._bindKeys(); // Bind clipboard functionality on(this.element, 'copy', (event: ClipboardEvent) => { @@ -587,7 +569,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT /** * Apply key handling to the terminal */ - private bindKeys(): void { + private _bindKeys(): void { const self = this; on(this.element, 'keydown', function (ev: KeyboardEvent): void { if (document.activeElement !== this) { @@ -611,11 +593,11 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT on(this.textarea, 'keydown', (ev: KeyboardEvent) => this._keyDown(ev), true); on(this.textarea, 'keypress', (ev: KeyboardEvent) => this._keyPress(ev), true); - on(this.textarea, 'compositionstart', () => this.compositionHelper.compositionstart()); - on(this.textarea, 'compositionupdate', (e: CompositionEvent) => this.compositionHelper.compositionupdate(e)); - on(this.textarea, 'compositionend', () => this.compositionHelper.compositionend()); - this.on('refresh', () => this.compositionHelper.updateCompositionElements()); - this.on('refresh', (data) => this.queueLinkification(data.start, data.end)); + on(this.textarea, 'compositionstart', () => this._compositionHelper.compositionstart()); + on(this.textarea, 'compositionupdate', (e: CompositionEvent) => this._compositionHelper.compositionupdate(e)); + on(this.textarea, 'compositionend', () => this._compositionHelper.compositionend()); + this.on('refresh', () => this._compositionHelper.updateCompositionElements()); + this.on('refresh', (data) => this._queueLinkification(data.start, data.end)); } /** @@ -624,47 +606,43 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT * @param {HTMLElement} parent The element to create the terminal within. */ public open(parent: HTMLElement): void { - let i = 0; - let div; + this._parent = parent || this._parent; - this.parent = parent || this.parent; - - if (!this.parent) { + if (!this._parent) { throw new Error('Terminal requires a parent element.'); } // Grab global elements - this.context = this.parent.ownerDocument.defaultView; - this.document = this.parent.ownerDocument; - this.body = this.document.body; + this._context = this._parent.ownerDocument.defaultView; + this._document = this._parent.ownerDocument; this._screenDprMonitor = new ScreenDprMonitor(); this._screenDprMonitor.setListener(() => this.emit('dprchange', window.devicePixelRatio)); // Create main element container - this.element = this.document.createElement('div'); + this.element = this._document.createElement('div'); this.element.classList.add('terminal'); this.element.classList.add('xterm'); this.element.setAttribute('tabindex', '0'); - this.parent.appendChild(this.element); + this._parent.appendChild(this.element); // Performance: Use a document fragment to build the terminal // viewport and helper elements detached from the DOM const fragment = document.createDocumentFragment(); - this.viewportElement = document.createElement('div'); - this.viewportElement.classList.add('xterm-viewport'); - fragment.appendChild(this.viewportElement); - this.viewportScrollArea = document.createElement('div'); - this.viewportScrollArea.classList.add('xterm-scroll-area'); - this.viewportElement.appendChild(this.viewportScrollArea); + this._viewportElement = document.createElement('div'); + this._viewportElement.classList.add('xterm-viewport'); + fragment.appendChild(this._viewportElement); + this._viewportScrollArea = document.createElement('div'); + this._viewportScrollArea.classList.add('xterm-scroll-area'); + this._viewportElement.appendChild(this._viewportScrollArea); this.screenElement = document.createElement('div'); this.screenElement.classList.add('xterm-screen'); // Create the container that will hold helpers like the textarea for // capturing DOM Events. Then produce the helpers. - this.helperContainer = document.createElement('div'); - this.helperContainer.classList.add('xterm-helpers'); - this.screenElement.appendChild(this.helperContainer); + this._helperContainer = document.createElement('div'); + this._helperContainer.classList.add('xterm-helpers'); + this.screenElement.appendChild(this._helperContainer); fragment.appendChild(this.screenElement); this._mouseZoneManager = new MouseZoneManager(this); @@ -682,34 +660,32 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.textarea.tabIndex = 0; this.textarea.addEventListener('focus', () => this._onTextAreaFocus()); this.textarea.addEventListener('blur', () => this._onTextAreaBlur()); - this.helperContainer.appendChild(this.textarea); + this._helperContainer.appendChild(this.textarea); - this.compositionView = document.createElement('div'); - this.compositionView.classList.add('composition-view'); - this.compositionHelper = new CompositionHelper(this.textarea, this.compositionView, this); - this.helperContainer.appendChild(this.compositionView); + this._compositionView = document.createElement('div'); + this._compositionView.classList.add('composition-view'); + this._compositionHelper = new CompositionHelper(this.textarea, this._compositionView, this); + this._helperContainer.appendChild(this._compositionView); - this.charSizeStyleElement = document.createElement('style'); - this.helperContainer.appendChild(this.charSizeStyleElement); - this.charMeasure = new CharMeasure(document, this.helperContainer); + this.charMeasure = new CharMeasure(document, this._helperContainer); // Performance: Add viewport and helper elements from the fragment this.element.appendChild(fragment); this.renderer = new Renderer(this, this.options.theme); this.options.theme = null; - this.viewport = new Viewport(this, this.viewportElement, this.viewportScrollArea, this.charMeasure); + this.viewport = new Viewport(this, this._viewportElement, this._viewportScrollArea, this.charMeasure); this.viewport.onThemeChanged(this.renderer.colorManager.colors); this.on('cursormove', () => this.renderer.onCursorMove()); - this.on('resize', () => this.renderer.onResize(this.cols, this.rows, false)); + this.on('resize', () => this.renderer.onResize(this.cols, this.rows)); this.on('blur', () => this.renderer.onBlur()); this.on('focus', () => this.renderer.onFocus()); this.on('dprchange', () => this.renderer.onWindowResize(window.devicePixelRatio)); // dprchange should handle this case, we need this as well for browsers that don't support the // matchMedia query. window.addEventListener('resize', () => this.renderer.onWindowResize(window.devicePixelRatio)); - this.charMeasure.on('charsizechanged', () => this.renderer.onResize(this.cols, this.rows, true)); + this.charMeasure.on('charsizechanged', () => this.renderer.onResize(this.cols, this.rows)); this.renderer.on('resize', (dimensions) => this.viewport.syncScrollArea()); this.selectionManager = new SelectionManager(this, this.charMeasure); @@ -727,7 +703,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.viewport.syncScrollArea(); this.selectionManager.refresh(); }); - this.viewportElement.addEventListener('scroll', () => this.selectionManager.refresh()); + this._viewportElement.addEventListener('scroll', () => this.selectionManager.refresh()); this.mouseHelper = new MouseHelper(this.renderer); @@ -744,7 +720,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.refresh(0, this.rows - 1); // Initialize global actions that need to be taken on the document. - this.initGlobal(); + this._initGlobal(); // Listen for mouse events and translate // them into terminal mouse protocols. @@ -871,7 +847,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT // button: button // }); - if (self.vt300Mouse) { + if (self._vt300Mouse) { // NOTE: Unstable. // http://www.vt100.net/docs/vt3xx-gp/chapter15.html button &= 3; @@ -888,7 +864,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT return; } - if (self.decLocator) { + if (self._decLocator) { // NOTE: Unstable. button &= 3; pos.x -= 32; @@ -1031,19 +1007,19 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT } // bind events - if (this.normalMouse) on(this.document, 'mousemove', sendMove); + if (this.normalMouse) on(this._document, 'mousemove', sendMove); // x10 compatibility mode can't send button releases if (!this.x10Mouse) { const handler = (ev: MouseEvent) => { sendButton(ev); // TODO: Seems dangerous calling this on document? - if (this.normalMouse) off(this.document, 'mousemove', sendMove); - off(this.document, 'mouseup', handler); + if (this.normalMouse) off(this._document, 'mousemove', sendMove); + off(this._document, 'mouseup', handler); return this.cancel(ev); }; // TODO: Seems dangerous calling this on document? - on(this.document, 'mouseup', handler); + on(this._document, 'mouseup', handler); } return this.cancel(ev); @@ -1054,8 +1030,28 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT // } on(el, 'wheel', (ev: WheelEvent) => { - if (!this.mouseEvents) return; - if (this.x10Mouse || this.vt300Mouse || this.decLocator) return; + if (!this.mouseEvents) { + // Convert wheel events into up/down events when the buffer does not have scrollback, this + // enables scrolling in apps hosted in the alt buffer such as vim or tmux. + if (!this.buffer.hasScrollback) { + const amount = this.viewport.getLinesScrolled(ev); + + // Do nothing if there's no vertical scroll + if (amount === 0) { + return; + } + + // Construct and send sequences + const sequence = C0.ESC + (this.applicationCursor ? 'O' : '[') + ( ev.deltaY < 0 ? 'A' : 'B'); + let data = ''; + for (let i = 0; i < Math.abs(amount); i++) { + data += sequence; + } + this.send(data); + } + return; + } + if (this.x10Mouse || this._vt300Mouse || this._decLocator) return; sendButton(ev); ev.preventDefault(); }); @@ -1086,8 +1082,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT */ public destroy(): void { super.destroy(); - this.readable = false; - this.writable = false; this.handler = () => {}; this.write = () => {}; if (this.element && this.element.parentNode) { @@ -1113,7 +1107,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT * @param {number} start The row to start from (between 0 and this.rows - 1). * @param {number} end The row to end at (between start and this.rows - 1). */ - private queueLinkification(start: number, end: number): void { + private _queueLinkification(start: number, end: number): void { if (this.linkifier) { this.linkifier.linkifyRows(start, end); } @@ -1153,13 +1147,13 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT if (!willBufferBeTrimmed) { this.buffer.ybase++; // Only scroll the ydisp with ybase if the user has not scrolled up - if (!this.userScrolling) { + if (!this._userScrolling) { this.buffer.ydisp++; } } else { // When the buffer is full and the user has scrolled up, keep the text // stable unless ydisp is right at the top - if (this.userScrolling) { + if (this._userScrolling) { this.buffer.ydisp = Math.max(this.buffer.ydisp - 1, 0); } } @@ -1173,7 +1167,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT // Move the viewport to the bottom of the buffer unless the user is // scrolling. - if (!this.userScrolling) { + if (!this._userScrolling) { this.buffer.ydisp = this.buffer.ybase; } @@ -1202,9 +1196,9 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT if (this.buffer.ydisp === 0) { return; } - this.userScrolling = true; + this._userScrolling = true; } else if (disp + this.buffer.ydisp >= this.buffer.ybase) { - this.userScrolling = false; + this._userScrolling = false; } const oldYdisp = this.buffer.ydisp; @@ -1254,54 +1248,54 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT // Send XOFF to pause the pty process if the write buffer becomes too large so // xterm.js can catch up before more data is sent. This is necessary in order // to keep signals such as ^C responsive. - if (this.options.useFlowControl && !this.xoffSentToCatchUp && this.writeBuffer.length >= WRITE_BUFFER_PAUSE_THRESHOLD) { + if (this.options.useFlowControl && !this._xoffSentToCatchUp && this.writeBuffer.length >= WRITE_BUFFER_PAUSE_THRESHOLD) { // XOFF - stop pty pipe // XON will be triggered by emulator before processing data chunk this.send(C0.DC3); - this.xoffSentToCatchUp = true; + this._xoffSentToCatchUp = true; } - if (!this.writeInProgress && this.writeBuffer.length > 0) { + if (!this._writeInProgress && this.writeBuffer.length > 0) { // Kick off a write which will write all data in sequence recursively - this.writeInProgress = true; + this._writeInProgress = true; // Kick off an async innerWrite so more writes can come in while processing data setTimeout(() => { - this.innerWrite(); + this._innerWrite(); }); } } - private innerWrite(): void { + private _innerWrite(): void { const writeBatch = this.writeBuffer.splice(0, WRITE_BATCH_SIZE); while (writeBatch.length > 0) { const data = writeBatch.shift(); // If XOFF was sent in order to catch up with the pty process, resume it if // the writeBuffer is empty to allow more data to come in. - if (this.xoffSentToCatchUp && writeBatch.length === 0 && this.writeBuffer.length === 0) { + if (this._xoffSentToCatchUp && writeBatch.length === 0 && this.writeBuffer.length === 0) { this.send(C0.DC1); - this.xoffSentToCatchUp = false; + this._xoffSentToCatchUp = false; } - this.refreshStart = this.buffer.y; - this.refreshEnd = this.buffer.y; + this._refreshStart = this.buffer.y; + this._refreshEnd = this.buffer.y; // HACK: Set the parser state based on it's state at the time of return. // This works around the bug #662 which saw the parser state reset in the // middle of parsing escape sequence in two chunks. For some reason the // state of the parser resets to 0 after exiting parser.parse. This change // just sets the state back based on the correct return statement. - const state = this.parser.parse(data); - this.parser.setState(state); + const state = this._parser.parse(data); + this._parser.setState(state); this.updateRange(this.buffer.y); - this.refresh(this.refreshStart, this.refreshEnd); + this.refresh(this._refreshStart, this._refreshEnd); } if (this.writeBuffer.length > 0) { // Allow renderer to catch up before processing the next batch - setTimeout(() => this.innerWrite(), 0); + setTimeout(() => this._innerWrite(), 0); } else { - this.writeInProgress = false; + this._writeInProgress = false; } } @@ -1323,37 +1317,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT * the event should be processed by xterm.js. */ public attachCustomKeyEventHandler(customKeyEventHandler: CustomKeyEventHandler): void { - this.customKeyEventHandler = customKeyEventHandler; - } - - /** - * Attaches a http(s) link handler, forcing web links to behave differently to - * regular tags. This will trigger a refresh as links potentially need to be - * reconstructed. Calling this with null will remove the handler. - * @param handler The handler callback function. - */ - public setHypertextLinkHandler(handler: LinkMatcherHandler): void { - if (!this.linkifier) { - throw new Error('Cannot attach a hypertext link handler before Terminal.open is called'); - } - this.linkifier.setHypertextLinkHandler(handler); - // Refresh to force links to refresh - this.refresh(0, this.rows - 1); - } - - /** - * Attaches a validation callback for hypertext links. This is useful to use - * validation logic or to do something with the link's element and url. - * @param callback The callback to use, this can - * be cleared with null. - */ - public setHypertextValidationCallback(callback: LinkMatcherValidationCallback): void { - if (!this.linkifier) { - throw new Error('Cannot attach a hypertext validation callback before Terminal.open is called'); - } - this.linkifier.setHypertextValidationCallback(callback); - // // Refresh to force links to refresh - this.refresh(0, this.rows - 1); + this._customKeyEventHandler = customKeyEventHandler; } /** @@ -1367,12 +1331,9 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT * @return The ID of the new matcher, this can be used to deregister. */ public registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options?: ILinkMatcherOptions): number { - if (this.linkifier) { - const matcherId = this.linkifier.registerLinkMatcher(regex, handler, options); - this.refresh(0, this.rows - 1); - return matcherId; - } - return 0; + const matcherId = this.linkifier.registerLinkMatcher(regex, handler, options); + this.refresh(0, this.rows - 1); + return matcherId; } /** @@ -1380,10 +1341,8 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT * @param matcherId The link matcher's ID (returned after register) */ public deregisterLinkMatcher(matcherId: number): void { - if (this.linkifier) { - if (this.linkifier.deregisterLinkMatcher(matcherId)) { - this.refresh(0, this.rows - 1); - } + if (this.linkifier.deregisterLinkMatcher(matcherId)) { + this.refresh(0, this.rows - 1); } } @@ -1427,11 +1386,11 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT * @param {KeyboardEvent} ev The keydown event to be handled. */ protected _keyDown(ev: KeyboardEvent): boolean { - if (this.customKeyEventHandler && this.customKeyEventHandler(ev) === false) { + if (this._customKeyEventHandler && this._customKeyEventHandler(ev) === false) { return false; } - if (!this.compositionHelper.keydown(ev)) { + if (!this._compositionHelper.keydown(ev)) { if (this.buffer.ybase !== this.buffer.ydisp) { this.scrollToBottom(); } @@ -1440,11 +1399,11 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT const result = this._evaluateKeyEscapeSequence(ev); - if (result.key === C0.DC3) { // XOFF - this.writeStopped = true; - } else if (result.key === C0.DC1) { // XON - this.writeStopped = false; - } + // if (result.key === C0.DC3) { // XOFF + // this._writeStopped = true; + // } else if (result.key === C0.DC1) { // XON + // this._writeStopped = false; + // } if (result.scrollLines) { this.scrollLines(result.scrollLines); @@ -1838,7 +1797,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT protected _keyPress(ev: KeyboardEvent): boolean { let key; - if (this.customKeyEventHandler && this.customKeyEventHandler(ev) === false) { + if (this._customKeyEventHandler && this._customKeyEventHandler(ev) === false) { return false; } @@ -1875,14 +1834,14 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT * @param {string} data */ public send(data: string): void { - if (!this.sendDataQueue) { + if (!this._sendDataQueue) { setTimeout(() => { - this.handler(this.sendDataQueue); - this.sendDataQueue = ''; + this.handler(this._sendDataQueue); + this._sendDataQueue = ''; }, 1); } - this.sendDataQueue += data; + this._sendDataQueue += data; } /** @@ -1891,14 +1850,14 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT */ public bell(): void { this.emit('bell'); - if (this.soundBell()) { + if (this._soundBell()) { this.soundManager.playBellSound(); } - if (this.visualBell()) { + if (this._visualBell()) { this.element.classList.add('visual-bell-active'); - clearTimeout(this.visualBellTimer); - this.visualBellTimer = window.setTimeout(() => { + clearTimeout(this._visualBellTimer); + this._visualBellTimer = window.setTimeout(() => { this.element.classList.remove('visual-bell-active'); }, 200); } @@ -1909,8 +1868,8 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT */ public log(text: string, data?: any): void { if (!this.options.debug) return; - if (!this.context.console || !this.context.console.log) return; - this.context.console.log(text, data); + if (!this._context.console || !this._context.console.log) return; + this._context.console.log(text, data); } /** @@ -1918,8 +1877,8 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT */ public error(text: string, data?: any): void { if (!this.options.debug) return; - if (!this.context.console || !this.context.console.error) return; - this.context.console.error(text, data); + if (!this._context.console || !this._context.console.error) return; + this._context.console.error(text, data); } /** @@ -1963,8 +1922,8 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT * @param {number} y The number of rows to refresh next. */ public updateRange(y: number): void { - if (y < this.refreshStart) this.refreshStart = y; - if (y > this.refreshEnd) this.refreshEnd = y; + if (y < this._refreshStart) this._refreshStart = y; + if (y > this._refreshEnd) this._refreshEnd = y; // if (y > this.refreshEnd) { // this.refreshEnd = y; // if (y > this.rows - 1) { @@ -1977,8 +1936,8 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT * Set the range of refreshing to the maximum value */ public maxRange(): void { - this.refreshStart = 0; - this.refreshEnd = this.rows - 1; + this._refreshStart = 0; + this._refreshEnd = this.rows - 1; } /** @@ -2116,7 +2075,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT * Emit the 'title' event and populate the given title. * @param {string} title The title to populate in the event. */ - private handleTitle(title: string): void { + public handleTitle(title: string): void { /** * This event is emitted when the title of the terminal is changed * from inside the terminal. The parameter is the new title. @@ -2171,11 +2130,11 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT public reset(): void { this.options.rows = this.rows; this.options.cols = this.cols; - const customKeyEventHandler = this.customKeyEventHandler; - const inputHandler = this.inputHandler; - this.setup(); - this.customKeyEventHandler = customKeyEventHandler; - this.inputHandler = inputHandler; + const customKeyEventHandler = this._customKeyEventHandler; + const inputHandler = this._inputHandler; + this._setup(); + this._customKeyEventHandler = customKeyEventHandler; + this._inputHandler = inputHandler; this.refresh(0, this.rows - 1); this.viewport.syncScrollArea(); } @@ -2184,7 +2143,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT /** * ESC H Tab Set (HTS is 0x88). */ - private tabSet(): void { + public tabSet(): void { this.buffer.tabs[this.buffer.x] = true; } @@ -2203,13 +2162,13 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT return matchColor_(r1, g1, b1); } - private visualBell(): boolean { + private _visualBell(): boolean { return false; // return this.options.bellStyle === 'visual' || // this.options.bellStyle === 'both'; } - private soundBell(): boolean { + private _soundBell(): boolean { return this.options.bellStyle === 'sound'; // return this.options.bellStyle === 'sound' || // this.options.bellStyle === 'both'; diff --git a/src/Types.ts b/src/Types.ts index 8061d1ac..dd1b22ca 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { Terminal as PublicTerminal, ITerminalOptions as IPublicTerminalOptions, IEventEmitter as IPublicEventEmitter, IEventEmitter } from 'xterm'; +import { Terminal as PublicTerminal, ITerminalOptions as IPublicTerminalOptions, IEventEmitter } from 'xterm'; import { IColorSet, IRenderer } from './renderer/Types'; import { IMouseZoneManager } from './input/Types'; @@ -14,7 +14,7 @@ export type XtermListener = (...args: any[]) => void; export type CharData = [number, string, number, number]; export type LineData = CharData[]; -export type LinkMatcherHandler = (event: MouseEvent, uri: string) => boolean | void; +export type LinkMatcherHandler = (event: MouseEvent, uri: string) => void; export type LinkMatcherValidationCallback = (uri: string, callback: (isValid: boolean) => void) => void; export enum LinkHoverEventTypes { @@ -84,11 +84,13 @@ export interface IInputHandlingTerminal extends IEventEmitter { matchColor(r1: number, g1: number, b1: number): number; error(text: string, data?: any): void; setOption(key: string, value: any): void; + tabSet(): void; } export interface IViewport { scrollBarWidth: number; syncScrollArea(): void; + getLinesScrolled(ev: WheelEvent): number; onWheel(ev: WheelEvent): void; onTouchStart(ev: TouchEvent): void; onTouchMove(ev: TouchEvent): void; @@ -252,6 +254,7 @@ export interface IBuffer { tabs: any; scrollBottom: number; scrollTop: number; + hasScrollback: boolean; savedY: number; savedX: number; isCursorInViewport: boolean; @@ -297,8 +300,6 @@ export interface ISelectionManager { export interface ILinkifier extends IEventEmitter { attachToDom(mouseZoneManager: IMouseZoneManager): void; linkifyRows(start: number, end: number): void; - setHypertextLinkHandler(handler: LinkMatcherHandler): void; - setHypertextValidationCallback(callback: LinkMatcherValidationCallback): void; registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options?: ILinkMatcherOptions): number; deregisterLinkMatcher(matcherId: number): boolean; } diff --git a/src/Viewport.ts b/src/Viewport.ts index 35a2aa68..4d135b5a 100644 --- a/src/Viewport.ts +++ b/src/Viewport.ts @@ -15,51 +15,56 @@ const FALLBACK_SCROLL_BAR_WIDTH = 15; */ export class Viewport implements IViewport { public scrollBarWidth: number = 0; - private currentRowHeight: number = 0; - private lastRecordedBufferLength: number = 0; - private lastRecordedViewportHeight: number = 0; - private lastRecordedBufferHeight: number = 0; - private lastTouchY: number; + private _currentRowHeight: number = 0; + private _lastRecordedBufferLength: number = 0; + private _lastRecordedViewportHeight: number = 0; + private _lastRecordedBufferHeight: number = 0; + private _lastTouchY: number; + + // Stores a partial line amount when scrolling, this is used to keep track of how much of a line + // is scrolled so we can "scroll" over partial lines and feel natural on touchpads. This is a + // quick fix and could have a more robust solution in place that reset the value when needed. + private _wheelPartialScroll: number = 0; /** * Creates a new Viewport. - * @param terminal The terminal this viewport belongs to. - * @param viewportElement The DOM element acting as the viewport. - * @param scrollArea The DOM element acting as the scroll area. - * @param charMeasure A DOM element used to measure the character size of. the terminal. + * @param _terminal The terminal this viewport belongs to. + * @param _viewportElement The DOM element acting as the viewport. + * @param _scrollArea The DOM element acting as the scroll area. + * @param _charMeasure A DOM element used to measure the character size of. the terminal. */ constructor( - private terminal: ITerminal, - private viewportElement: HTMLElement, - private scrollArea: HTMLElement, - private charMeasure: CharMeasure + private _terminal: ITerminal, + private _viewportElement: HTMLElement, + private _scrollArea: HTMLElement, + private _charMeasure: CharMeasure ) { // Measure the width of the scrollbar. If it is 0 we can assume it's an OSX overlay scrollbar. // Unfortunately the overlay scrollbar would be hidden underneath the screen element in that case, // therefore we account for a standard amount to make it visible - this.scrollBarWidth = (this.viewportElement.offsetWidth - this.scrollArea.offsetWidth) || FALLBACK_SCROLL_BAR_WIDTH; - this.viewportElement.addEventListener('scroll', this.onScroll.bind(this)); + this.scrollBarWidth = (this._viewportElement.offsetWidth - this._scrollArea.offsetWidth) || FALLBACK_SCROLL_BAR_WIDTH; + this._viewportElement.addEventListener('scroll', this._onScroll.bind(this)); // Perform this async to ensure the CharMeasure is ready. setTimeout(() => this.syncScrollArea(), 0); } public onThemeChanged(colors: IColorSet): void { - this.viewportElement.style.backgroundColor = colors.background; + this._viewportElement.style.backgroundColor = colors.background; } /** * Refreshes row height, setting line-height, viewport height and scroll area height if * necessary. */ - private refresh(): void { - if (this.charMeasure.height > 0) { - this.currentRowHeight = this.terminal.renderer.dimensions.scaledCellHeight / window.devicePixelRatio; - this.lastRecordedViewportHeight = this.viewportElement.offsetHeight; - const newBufferHeight = Math.round(this.currentRowHeight * this.lastRecordedBufferLength) + (this.lastRecordedViewportHeight - this.terminal.renderer.dimensions.canvasHeight); - if (this.lastRecordedBufferHeight !== newBufferHeight) { - this.lastRecordedBufferHeight = newBufferHeight; - this.scrollArea.style.height = this.lastRecordedBufferHeight + 'px'; + private _refresh(): void { + if (this._charMeasure.height > 0) { + this._currentRowHeight = this._terminal.renderer.dimensions.scaledCellHeight / window.devicePixelRatio; + this._lastRecordedViewportHeight = this._viewportElement.offsetHeight; + const newBufferHeight = Math.round(this._currentRowHeight * this._lastRecordedBufferLength) + (this._lastRecordedViewportHeight - this._terminal.renderer.dimensions.canvasHeight); + if (this._lastRecordedBufferHeight !== newBufferHeight) { + this._lastRecordedBufferHeight = newBufferHeight; + this._scrollArea.style.height = this._lastRecordedBufferHeight + 'px'; } } } @@ -68,24 +73,24 @@ export class Viewport implements IViewport { * Updates dimensions and synchronizes the scroll area if necessary. */ public syncScrollArea(): void { - if (this.lastRecordedBufferLength !== this.terminal.buffer.lines.length) { + if (this._lastRecordedBufferLength !== this._terminal.buffer.lines.length) { // If buffer height changed - this.lastRecordedBufferLength = this.terminal.buffer.lines.length; - this.refresh(); - } else if (this.lastRecordedViewportHeight !== (this.terminal).renderer.dimensions.canvasHeight) { + this._lastRecordedBufferLength = this._terminal.buffer.lines.length; + this._refresh(); + } else if (this._lastRecordedViewportHeight !== (this._terminal).renderer.dimensions.canvasHeight) { // If viewport height changed - this.refresh(); + this._refresh(); } else { // If size has changed, refresh viewport - if (this.terminal.renderer.dimensions.scaledCellHeight / window.devicePixelRatio !== this.currentRowHeight) { - this.refresh(); + if (this._terminal.renderer.dimensions.scaledCellHeight / window.devicePixelRatio !== this._currentRowHeight) { + this._refresh(); } } // Sync scrollTop - const scrollTop = this.terminal.buffer.ydisp * this.currentRowHeight; - if (this.viewportElement.scrollTop !== scrollTop) { - this.viewportElement.scrollTop = scrollTop; + const scrollTop = this._terminal.buffer.ydisp * this._currentRowHeight; + if (this._viewportElement.scrollTop !== scrollTop) { + this._viewportElement.scrollTop = scrollTop; } } @@ -94,16 +99,16 @@ export class Viewport implements IViewport { * terminal to scroll to it. * @param ev The scroll event. */ - private onScroll(ev: Event): void { + private _onScroll(ev: Event): void { // Don't attempt to scroll if the element is not visible, otherwise scrollTop will be corrupt // which causes the terminal to scroll the buffer to the top - if (!this.viewportElement.offsetParent) { + if (!this._viewportElement.offsetParent) { return; } - const newRow = Math.round(this.viewportElement.scrollTop / this.currentRowHeight); - const diff = newRow - this.terminal.buffer.ydisp; - this.terminal.scrollLines(diff, true); + const newRow = Math.round(this._viewportElement.scrollTop / this._currentRowHeight); + const diff = newRow - this._terminal.buffer.ydisp; + this._terminal.scrollLines(diff, true); } /** @@ -113,28 +118,61 @@ export class Viewport implements IViewport { * @param ev The mouse wheel event. */ public onWheel(ev: WheelEvent): void { - if (ev.deltaY === 0) { - // Do nothing if it's not a vertical scroll event + const amount = this._getPixelsScrolled(ev); + if (amount === 0) { return; } - // Fallback to WheelEvent.DOM_DELTA_PIXEL - let multiplier = 1; - if (ev.deltaMode === WheelEvent.DOM_DELTA_LINE) { - multiplier = this.currentRowHeight; - } else if (ev.deltaMode === WheelEvent.DOM_DELTA_PAGE) { - multiplier = this.currentRowHeight * this.terminal.rows; - } - this.viewportElement.scrollTop += ev.deltaY * multiplier; + this._viewportElement.scrollTop += amount; // Prevent the page from scrolling when the terminal scrolls ev.preventDefault(); } + private _getPixelsScrolled(ev: WheelEvent): number { + // Do nothing if it's not a vertical scroll event + if (ev.deltaY === 0) { + return 0; + } + + // Fallback to WheelEvent.DOM_DELTA_PIXEL + let amount = ev.deltaY; + if (ev.deltaMode === WheelEvent.DOM_DELTA_LINE) { + amount *= this._currentRowHeight; + } else if (ev.deltaMode === WheelEvent.DOM_DELTA_PAGE) { + amount *= this._currentRowHeight * this._terminal.rows; + } + return amount; + } + + /** + * Gets the number of pixels scrolled by the mouse event taking into account what type of delta + * is being used. + * @param ev The mouse wheel event. + */ + public getLinesScrolled(ev: WheelEvent): number { + // Do nothing if it's not a vertical scroll event + if (ev.deltaY === 0) { + return 0; + } + + // Fallback to WheelEvent.DOM_DELTA_LINE + let amount = ev.deltaY; + if (ev.deltaMode === WheelEvent.DOM_DELTA_PIXEL) { + amount /= this._currentRowHeight + 0.0; // Prevent integer division + this._wheelPartialScroll += amount; + amount = Math.floor(Math.abs(this._wheelPartialScroll)) * (this._wheelPartialScroll > 0 ? 1 : -1); + this._wheelPartialScroll %= 1; + } else if (ev.deltaMode === WheelEvent.DOM_DELTA_PAGE) { + amount *= this._terminal.rows; + } + return amount; + } + /** * Handles the touchstart event, recording the touch occurred. * @param ev The touch event. */ public onTouchStart(ev: TouchEvent): void { - this.lastTouchY = ev.touches[0].pageY; + this._lastTouchY = ev.touches[0].pageY; } /** @@ -142,12 +180,12 @@ export class Viewport implements IViewport { * @param ev The touch event. */ public onTouchMove(ev: TouchEvent): void { - let deltaY = this.lastTouchY - ev.touches[0].pageY; - this.lastTouchY = ev.touches[0].pageY; + let deltaY = this._lastTouchY - ev.touches[0].pageY; + this._lastTouchY = ev.touches[0].pageY; if (deltaY === 0) { return; } - this.viewportElement.scrollTop += deltaY; + this._viewportElement.scrollTop += deltaY; ev.preventDefault(); } } diff --git a/src/addons/webLinks/package.json b/src/addons/webLinks/package.json new file mode 100644 index 00000000..f200cab4 --- /dev/null +++ b/src/addons/webLinks/package.json @@ -0,0 +1,5 @@ +{ + "name": "xterm.weblinks", + "main": "weblinks.js", + "private": true +} diff --git a/src/addons/webLinks/tsconfig.json b/src/addons/webLinks/tsconfig.json new file mode 100644 index 00000000..7549370b --- /dev/null +++ b/src/addons/webLinks/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es5", + "rootDir": ".", + "outDir": "../../../lib/addons/webLinks/", + "sourceMap": true, + "removeComments": true, + "declaration": true + } +} diff --git a/src/addons/webLinks/webLinks.test.ts b/src/addons/webLinks/webLinks.test.ts new file mode 100644 index 00000000..b3c76703 --- /dev/null +++ b/src/addons/webLinks/webLinks.test.ts @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { assert, expect } from 'chai'; + +import * as webLinks from './webLinks'; + +class MockTerminal { + public regex: RegExp; + public handler: (event: MouseEvent, uri: string) => void; + public options?: any; + + public registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: any): number { + this.regex = regex; + this.handler = handler; + this.options = options; + return 0; + } +} + +describe('webLinks addon', () => { + describe('apply', () => { + it('should do register the `webLinksInit` method', () => { + webLinks.apply(MockTerminal); + assert.equal(typeof (MockTerminal).prototype.webLinksInit, 'function'); + }); + }); + + it('should allow ~ character in URI path', () => { + const term = new MockTerminal(); + webLinks.webLinksInit(term); + + const row = ' http://foo.com/a~b#c~d?e~f '; + + let match = row.match(term.regex); + let uri = match[term.options.matchIndex]; + + assert.equal(uri, 'http://foo.com/a~b#c~d?e~f'); + }); +}); diff --git a/src/addons/webLinks/webLinks.ts b/src/addons/webLinks/webLinks.ts new file mode 100644 index 00000000..7ff87c60 --- /dev/null +++ b/src/addons/webLinks/webLinks.ts @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +/// + +import { Terminal, ILinkMatcherOptions } from 'xterm'; + +const protocolClause = '(https?:\\/\\/)'; +const domainCharacterSet = '[\\da-z\\.-]+'; +const negatedDomainCharacterSet = '[^\\da-z\\.-]+'; +const domainBodyClause = '(' + domainCharacterSet + ')'; +const tldClause = '([a-z\\.]{2,6})'; +const ipClause = '((\\d{1,3}\\.){3}\\d{1,3})'; +const localHostClause = '(localhost)'; +const portClause = '(:\\d{1,5})'; +const hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|' + ipClause + '|' + localHostClause + ')' + portClause + '?'; +const pathClause = '(\\/[\\/\\w\\.\\-%~]*)*'; +const queryStringHashFragmentCharacterSet = '[0-9\\w\\[\\]\\(\\)\\/\\?\\!#@$%&\'*+,:;~\\=\\.\\-]*'; +const queryStringClause = '(\\?' + queryStringHashFragmentCharacterSet + ')?'; +const hashFragmentClause = '(#' + queryStringHashFragmentCharacterSet + ')?'; +const negatedPathCharacterSet = '[^\\/\\w\\.\\-%]+'; +const bodyClause = hostClause + pathClause + queryStringClause + hashFragmentClause; +const start = '(?:^|' + negatedDomainCharacterSet + ')('; +const end = ')($|' + negatedPathCharacterSet + ')'; +const strictUrlRegex = new RegExp(start + protocolClause + bodyClause + end); + +function handleLink(event: MouseEvent, uri: string): void { + window.open(uri, '_blank'); +} + +/** + * Initialize the web links addon, registering the link matcher. + * @param term The terminal to use web links within. + * @param handler A custom handler to use. + * @param options Custom options to use, matchIndex will always be ignored. + */ +export function webLinksInit(term: Terminal, handler: (event: MouseEvent, uri: string) => void = handleLink, options: ILinkMatcherOptions = {}): void { + options.matchIndex = 1; + term.registerLinkMatcher(strictUrlRegex, handler, options); +} + +export function apply(terminalConstructor: typeof Terminal): void { + (terminalConstructor.prototype).webLinksInit = function (handler?: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): void { + webLinksInit(this, handler, options); + }; +} diff --git a/src/addons/winptyCompat/package.json b/src/addons/winptyCompat/package.json index 2e9fc164..fc929497 100644 --- a/src/addons/winptyCompat/package.json +++ b/src/addons/winptyCompat/package.json @@ -1,5 +1,5 @@ { - "name": "xterm.winptyCompat", + "name": "xterm.winptycompat", "main": "winptyCompat.js", "private": true } diff --git a/src/handlers/AltClickHandler.ts b/src/handlers/AltClickHandler.ts index c9c51cbe..f77637ea 100644 --- a/src/handlers/AltClickHandler.ts +++ b/src/handlers/AltClickHandler.ts @@ -56,9 +56,13 @@ export class AltClickHandler { * then moves to requested col. */ private _arrowSequences(): string { - return this._resetStartingRow() + - this._moveToRequestedRow() + - this._moveToRequestedCol(); + // The alt buffer should try to navigate between rows + if (!this._terminal.buffer.hasScrollback) { + return this._resetStartingRow() + this._moveToRequestedRow() + this._moveToRequestedCol(); + } + + // Only move horizontally for the normal buffer + return this._moveHorizontallyOnly(); } /** @@ -67,9 +71,6 @@ export class AltClickHandler { * positioning. */ private _resetStartingRow(): string { - let startRow = this._endRow - this._wrappedRowsForRow(this._endRow); - let endRow = this._endRow; - if (this._moveToRequestedRow().length === 0) { return ''; } else { @@ -113,6 +114,11 @@ export class AltClickHandler { ).length, this._sequence(direction)); } + private _moveHorizontallyOnly(): string { + let direction = this._horizontalDirection(); + return repeat(Math.abs(this._startCol - this._endCol), this._sequence(direction)); + } + /** * Utility functions */ diff --git a/src/handlers/Clipboard.test.ts b/src/handlers/Clipboard.test.ts index d35e5fa4..07c0f66e 100644 --- a/src/handlers/Clipboard.test.ts +++ b/src/handlers/Clipboard.test.ts @@ -4,17 +4,22 @@ */ import { assert } from 'chai'; -import * as Terminal from '../Terminal'; import * as Clipboard from './Clipboard'; describe('evaluatePastedTextProcessing', () => { - it('should replace carriage return + line feed with line feed on windows', () => { - const pastedText = 'foo\r\nbar\r\n'; - const processedText = Clipboard.prepareTextForTerminal(pastedText, false); - const windowsProcessedText = Clipboard.prepareTextForTerminal(pastedText, true); + it('should replace carriage return and/or line feed with carriage return', () => { + const pastedText = { + unix: 'foo\nbar\n', + windows: 'foo\r\nbar\r\n' + }; - assert.equal(processedText, 'foo\r\nbar\r\n'); - assert.equal(windowsProcessedText, 'foo\rbar\r'); + const processedText = { + unix: Clipboard.prepareTextForTerminal(pastedText.unix), + windows: Clipboard.prepareTextForTerminal(pastedText.windows) + }; + + assert.equal(processedText.unix, 'foo\rbar\r'); + assert.equal(processedText.windows, 'foo\rbar\r'); }); it('should bracket pasted text in bracketedPasteMode', () => { const pastedText = 'foo bar'; diff --git a/src/handlers/Clipboard.ts b/src/handlers/Clipboard.ts index 7ac97714..23925007 100644 --- a/src/handlers/Clipboard.ts +++ b/src/handlers/Clipboard.ts @@ -18,11 +18,8 @@ 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 */ -export function prepareTextForTerminal(text: string, isMSWindows: boolean): string { - if (isMSWindows) { - return text.replace(/\r?\n/g, '\r'); - } - return text; +export function prepareTextForTerminal(text: string): string { + return text.replace(/\r?\n/g, '\r'); } /** @@ -62,7 +59,7 @@ export function pasteHandler(ev: ClipboardEvent, term: ITerminal): void { let text: string; let dispatchPaste = function(text: string): void { - text = prepareTextForTerminal(text, term.browser.isMSWindows); + text = prepareTextForTerminal(text); text = bracketTextForPaste(text, term.bracketedPasteMode); term.handler(text); term.textarea.value = ''; diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 9423f65d..281b3ee9 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -4,12 +4,11 @@ */ import { IRenderLayer, IColorSet, IRenderDimensions } from './Types'; -import { CharData, ITerminal, ITerminalOptions } from '../Types'; -import { acquireCharAtlas, CHAR_ATLAS_CELL_SPACING } from './CharAtlas'; -import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer'; - -export const INVERTED_DEFAULT_COLOR = -1; -const DIM_OPACITY = 0.5; +import { CharData, ITerminal } from '../Types'; +import { DIM_OPACITY, INVERTED_DEFAULT_COLOR } from './atlas/Types'; +import { CHAR_ATLAS_CELL_SPACING } from '../shared/atlas/Types'; +import { acquireCharAtlas } from './atlas/CharAtlas'; +import { CHAR_DATA_CHAR_INDEX } from '../Buffer'; export abstract class BaseRenderLayer implements IRenderLayer { private _canvas: HTMLCanvasElement; @@ -93,7 +92,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { } } - public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void { + public resize(terminal: ITerminal, dim: IRenderDimensions): void { this._scaledCellWidth = dim.scaledCellWidth; this._scaledCellHeight = dim.scaledCellHeight; this._scaledCharWidth = dim.scaledCharWidth; @@ -110,9 +109,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { this.clearAll(); } - if (charSizeChanged) { - this._refreshCharAtlas(terminal, this._colors); - } + this._refreshCharAtlas(terminal, this._colors); } public abstract reset(terminal: ITerminal): void; diff --git a/src/renderer/CharAtlas.ts b/src/renderer/CharAtlas.ts deleted file mode 100644 index ff8990b3..00000000 --- a/src/renderer/CharAtlas.ts +++ /dev/null @@ -1,136 +0,0 @@ -/** - * Copyright (c) 2017 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { ITerminal } from '../Types'; -import { IColorSet } from './Types'; -import { isFirefox } from '../shared/utils/Browser'; -import { generateCharAtlas, ICharAtlasRequest } from '../shared/CharAtlasGenerator'; - -export const CHAR_ATLAS_CELL_SPACING = 1; - -interface ICharAtlasConfig { - fontSize: number; - fontFamily: string; - fontWeight: string; - fontWeightBold: string; - scaledCharWidth: number; - scaledCharHeight: number; - allowTransparency: boolean; - colors: IColorSet; -} - -interface ICharAtlasCacheEntry { - bitmap: HTMLCanvasElement | Promise; - config: ICharAtlasConfig; - ownedBy: ITerminal[]; -} - -let charAtlasCache: ICharAtlasCacheEntry[] = []; - -/** - * Acquires a char atlas, either generating a new one or returning an existing - * one that is in use by another terminal. - * @param terminal The terminal. - * @param colors The colors to use. - */ -export function acquireCharAtlas(terminal: ITerminal, colors: IColorSet, scaledCharWidth: number, scaledCharHeight: number): HTMLCanvasElement | Promise { - const newConfig = generateConfig(scaledCharWidth, scaledCharHeight, terminal, colors); - - // Check to see if the terminal already owns this config - for (let i = 0; i < charAtlasCache.length; i++) { - const entry = charAtlasCache[i]; - const ownedByIndex = entry.ownedBy.indexOf(terminal); - if (ownedByIndex >= 0) { - if (configEquals(entry.config, newConfig)) { - return entry.bitmap; - } else { - // The configs differ, release the terminal from the entry - if (entry.ownedBy.length === 1) { - charAtlasCache.splice(i, 1); - } else { - entry.ownedBy.splice(ownedByIndex, 1); - } - break; - } - } - } - - // Try match a char atlas from the cache - for (let i = 0; i < charAtlasCache.length; i++) { - const entry = charAtlasCache[i]; - if (configEquals(entry.config, newConfig)) { - // Add the terminal to the cache entry and return - entry.ownedBy.push(terminal); - return entry.bitmap; - } - } - - const canvasFactory = (width: number, height: number) => { - const canvas = document.createElement('canvas'); - canvas.width = width; - canvas.height = height; - return canvas; - }; - - const charAtlasConfig: ICharAtlasRequest = { - scaledCharWidth, - scaledCharHeight, - fontSize: terminal.options.fontSize, - fontFamily: terminal.options.fontFamily, - fontWeight: terminal.options.fontWeight, - fontWeightBold: terminal.options.fontWeightBold, - background: colors.background, - foreground: colors.foreground, - ansiColors: colors.ansi, - devicePixelRatio: window.devicePixelRatio, - allowTransparency: terminal.options.allowTransparency - }; - - const newEntry: ICharAtlasCacheEntry = { - bitmap: generateCharAtlas(window, canvasFactory, charAtlasConfig), - config: newConfig, - ownedBy: [terminal] - }; - charAtlasCache.push(newEntry); - return newEntry.bitmap; -} - -function generateConfig(scaledCharWidth: number, scaledCharHeight: number, terminal: ITerminal, colors: IColorSet): ICharAtlasConfig { - const clonedColors = { - foreground: colors.foreground, - background: colors.background, - cursor: null, - cursorAccent: null, - selection: null, - ansi: colors.ansi.slice(0, 16) - }; - return { - scaledCharWidth, - scaledCharHeight, - fontFamily: terminal.options.fontFamily, - fontSize: terminal.options.fontSize, - fontWeight: terminal.options.fontWeight, - fontWeightBold: terminal.options.fontWeightBold, - allowTransparency: terminal.options.allowTransparency, - colors: clonedColors - }; -} - -function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean { - for (let i = 0; i < a.colors.ansi.length; i++) { - if (a.colors.ansi[i] !== b.colors.ansi[i]) { - return false; - } - } - return a.fontFamily === b.fontFamily && - a.fontSize === b.fontSize && - a.fontWeight === b.fontWeight && - a.fontWeightBold === b.fontWeightBold && - a.allowTransparency === b.allowTransparency && - a.scaledCharWidth === b.scaledCharWidth && - a.scaledCharHeight === b.scaledCharHeight && - a.colors.foreground === b.colors.foreground && - a.colors.background === b.colors.background; -} diff --git a/src/renderer/ColorManager.test.ts b/src/renderer/ColorManager.test.ts index 22407604..2dc60408 100644 --- a/src/renderer/ColorManager.test.ts +++ b/src/renderer/ColorManager.test.ts @@ -3,14 +3,21 @@ * @license MIT */ +import jsdom = require('jsdom'); import { assert } from 'chai'; import { ColorManager } from './ColorManager'; describe('ColorManager', () => { let cm: ColorManager; + let dom: jsdom.JSDOM; + let document: Document; + let window: Window; beforeEach(() => { - cm = new ColorManager(); + dom = new jsdom.JSDOM(''); + window = dom.window; + document = window.document; + cm = new ColorManager(document); }); describe('constructor', () => { diff --git a/src/renderer/ColorManager.ts b/src/renderer/ColorManager.ts index 90ef0431..ddb928a5 100644 --- a/src/renderer/ColorManager.ts +++ b/src/renderer/ColorManager.ts @@ -67,8 +67,10 @@ function toPaddedHex(c: number): string { */ export class ColorManager implements IColorManager { public colors: IColorSet; + private _document: Document; - constructor() { + constructor(document: Document) { + this._document = document; this.colors = { foreground: DEFAULT_FOREGROUND, background: DEFAULT_BACKGROUND, @@ -85,26 +87,54 @@ export class ColorManager implements IColorManager { * colors will be used where colors are not defined. */ public setTheme(theme: ITheme): void { - this.colors.foreground = theme.foreground || DEFAULT_FOREGROUND; - this.colors.background = theme.background || DEFAULT_BACKGROUND; - this.colors.cursor = theme.cursor || DEFAULT_CURSOR; - this.colors.cursorAccent = theme.cursorAccent || DEFAULT_CURSOR_ACCENT; - this.colors.selection = theme.selection || DEFAULT_SELECTION; - this.colors.ansi[0] = theme.black || DEFAULT_ANSI_COLORS[0]; - this.colors.ansi[1] = theme.red || DEFAULT_ANSI_COLORS[1]; - this.colors.ansi[2] = theme.green || DEFAULT_ANSI_COLORS[2]; - this.colors.ansi[3] = theme.yellow || DEFAULT_ANSI_COLORS[3]; - this.colors.ansi[4] = theme.blue || DEFAULT_ANSI_COLORS[4]; - this.colors.ansi[5] = theme.magenta || DEFAULT_ANSI_COLORS[5]; - this.colors.ansi[6] = theme.cyan || DEFAULT_ANSI_COLORS[6]; - this.colors.ansi[7] = theme.white || DEFAULT_ANSI_COLORS[7]; - this.colors.ansi[8] = theme.brightBlack || DEFAULT_ANSI_COLORS[8]; - this.colors.ansi[9] = theme.brightRed || DEFAULT_ANSI_COLORS[9]; - this.colors.ansi[10] = theme.brightGreen || DEFAULT_ANSI_COLORS[10]; - this.colors.ansi[11] = theme.brightYellow || DEFAULT_ANSI_COLORS[11]; - this.colors.ansi[12] = theme.brightBlue || DEFAULT_ANSI_COLORS[12]; - this.colors.ansi[13] = theme.brightMagenta || DEFAULT_ANSI_COLORS[13]; - this.colors.ansi[14] = theme.brightCyan || DEFAULT_ANSI_COLORS[14]; - this.colors.ansi[15] = theme.brightWhite || DEFAULT_ANSI_COLORS[15]; + this.colors.foreground = this._validateColor(theme.foreground, DEFAULT_FOREGROUND); + this.colors.background = this._validateColor(theme.background, DEFAULT_BACKGROUND); + this.colors.cursor = this._validateColor(theme.cursor, DEFAULT_CURSOR); + this.colors.cursorAccent = this._validateColor(theme.cursorAccent, DEFAULT_CURSOR_ACCENT); + this.colors.selection = this._validateColor(theme.selection, DEFAULT_SELECTION); + this.colors.ansi[0] = this._validateColor(theme.black, DEFAULT_ANSI_COLORS[0]); + this.colors.ansi[1] = this._validateColor(theme.red, DEFAULT_ANSI_COLORS[1]); + this.colors.ansi[2] = this._validateColor(theme.green, DEFAULT_ANSI_COLORS[2]); + this.colors.ansi[3] = this._validateColor(theme.yellow, DEFAULT_ANSI_COLORS[3]); + this.colors.ansi[4] = this._validateColor(theme.blue, DEFAULT_ANSI_COLORS[4]); + this.colors.ansi[5] = this._validateColor(theme.magenta, DEFAULT_ANSI_COLORS[5]); + this.colors.ansi[6] = this._validateColor(theme.cyan, DEFAULT_ANSI_COLORS[6]); + this.colors.ansi[7] = this._validateColor(theme.white, DEFAULT_ANSI_COLORS[7]); + this.colors.ansi[8] = this._validateColor(theme.brightBlack, DEFAULT_ANSI_COLORS[8]); + this.colors.ansi[9] = this._validateColor(theme.brightRed, DEFAULT_ANSI_COLORS[9]); + this.colors.ansi[10] = this._validateColor(theme.brightGreen, DEFAULT_ANSI_COLORS[10]); + this.colors.ansi[11] = this._validateColor(theme.brightYellow, DEFAULT_ANSI_COLORS[11]); + this.colors.ansi[12] = this._validateColor(theme.brightBlue, DEFAULT_ANSI_COLORS[12]); + this.colors.ansi[13] = this._validateColor(theme.brightMagenta, DEFAULT_ANSI_COLORS[13]); + this.colors.ansi[14] = this._validateColor(theme.brightCyan, DEFAULT_ANSI_COLORS[14]); + this.colors.ansi[15] = this._validateColor(theme.brightWhite, DEFAULT_ANSI_COLORS[15]); + } + + private _validateColor(color: string, fallback: string): string { + if (!color) { + return fallback; + } + + const isColorValid = this._isColorValid(color); + + if (!isColorValid) { + console.warn(`Color: ${color} is invalid using fallback ${fallback}`); + } + + return isColorValid ? color : fallback; + } + + private _isColorValid(color: string): boolean { + const litmus = 'red'; + const d = this._document.createElement('div'); + d.style.color = litmus; + d.style.color = color; + + // Element's style.color will be reverted to litmus or set to '' if an invalid color is given + if (color !== litmus && (d.style.color === litmus || d.style.color === '')) { + return false; + } + + return true; } } diff --git a/src/renderer/CursorRenderLayer.ts b/src/renderer/CursorRenderLayer.ts index d430b81d..bfd215ad 100644 --- a/src/renderer/CursorRenderLayer.ts +++ b/src/renderer/CursorRenderLayer.ts @@ -3,11 +3,10 @@ * @license MIT */ -import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer'; -import { GridCache } from './GridCache'; -import { FLAGS, IColorSet, IRenderDimensions } from './Types'; +import { CHAR_DATA_WIDTH_INDEX } from '../Buffer'; +import { IColorSet, IRenderDimensions } from './Types'; import { BaseRenderLayer } from './BaseRenderLayer'; -import { CharData, IBuffer, ICharMeasure, ITerminal, ITerminalOptions } from '../Types'; +import { CharData, ITerminal } from '../Types'; interface ICursorState { x: number; @@ -26,7 +25,6 @@ export class CursorRenderLayer extends BaseRenderLayer { private _state: ICursorState; private _cursorRenderers: {[key: string]: (terminal: ITerminal, x: number, y: number, charData: CharData) => void}; private _cursorBlinkStateManager: CursorBlinkStateManager; - private _isFocused: boolean; constructor(container: HTMLElement, zIndex: number, colors: IColorSet) { super(container, 'cursor', zIndex, true, colors); @@ -45,8 +43,8 @@ export class CursorRenderLayer extends BaseRenderLayer { // TODO: Consider initial options? Maybe onOptionsChanged should be called at the end of open? } - public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void { - super.resize(terminal, dim, charSizeChanged); + public resize(terminal: ITerminal, dim: IRenderDimensions): void { + super.resize(terminal, dim); // Resizing the canvas discards the contents of the canvas so clear state this._state = { x: null, @@ -237,7 +235,7 @@ class CursorBlinkStateManager { constructor( terminal: ITerminal, - private renderCallback: () => void + private _renderCallback: () => void ) { this.isCursorVisible = true; if (terminal.isFocused) { @@ -272,7 +270,7 @@ class CursorBlinkStateManager { this.isCursorVisible = true; if (!this._animationFrame) { this._animationFrame = window.requestAnimationFrame(() => { - this.renderCallback(); + this._renderCallback(); this._animationFrame = null; }); } @@ -303,7 +301,7 @@ class CursorBlinkStateManager { // Hide the cursor this.isCursorVisible = false; this._animationFrame = window.requestAnimationFrame(() => { - this.renderCallback(); + this._renderCallback(); this._animationFrame = null; }); @@ -322,7 +320,7 @@ class CursorBlinkStateManager { // Invert visibility and render this.isCursorVisible = !this.isCursorVisible; this._animationFrame = window.requestAnimationFrame(() => { - this.renderCallback(); + this._renderCallback(); this._animationFrame = null; }); }, BLINK_INTERVAL); diff --git a/src/renderer/LinkRenderLayer.ts b/src/renderer/LinkRenderLayer.ts index 61352f15..f94a47f8 100644 --- a/src/renderer/LinkRenderLayer.ts +++ b/src/renderer/LinkRenderLayer.ts @@ -3,11 +3,9 @@ * @license MIT */ -import { ILinkHoverEvent, ITerminal, ILinkifierAccessor, IBuffer, ICharMeasure, LinkHoverEventTypes } from '../Types'; -import { CHAR_DATA_ATTR_INDEX } from '../Buffer'; -import { GridCache } from './GridCache'; -import { FLAGS, IColorSet, IRenderDimensions } from './Types'; -import { BaseRenderLayer, INVERTED_DEFAULT_COLOR } from './BaseRenderLayer'; +import { ILinkHoverEvent, ITerminal, ILinkifierAccessor, LinkHoverEventTypes } from '../Types'; +import { IColorSet, IRenderDimensions } from './Types'; +import { BaseRenderLayer } from './BaseRenderLayer'; export class LinkRenderLayer extends BaseRenderLayer { private _state: ILinkHoverEvent = null; @@ -18,8 +16,8 @@ export class LinkRenderLayer extends BaseRenderLayer { terminal.linkifier.on(LinkHoverEventTypes.LEAVE, (e: ILinkHoverEvent) => this._onLinkLeave(e)); } - public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void { - super.resize(terminal, dim, charSizeChanged); + public resize(terminal: ITerminal, dim: IRenderDimensions): void { + super.resize(terminal, dim); // Resizing the canvas discards the contents of the canvas so clear state this._state = null; } diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index fa1e34e6..1ce2c9b4 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -3,12 +3,10 @@ * @license MIT */ -import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer'; import { TextRenderLayer } from './TextRenderLayer'; import { SelectionRenderLayer } from './SelectionRenderLayer'; import { CursorRenderLayer } from './CursorRenderLayer'; import { ColorManager } from './ColorManager'; -import { BaseRenderLayer } from './BaseRenderLayer'; import { IRenderLayer, IColorSet, IRenderer, IRenderDimensions } from './Types'; import { ITerminal } from '../Types'; import { LinkRenderLayer } from './LinkRenderLayer'; @@ -31,7 +29,7 @@ export class Renderer extends EventEmitter implements IRenderer { constructor(private _terminal: ITerminal, theme: ITheme) { super(); - this.colorManager = new ColorManager(); + this.colorManager = new ColorManager(document); if (theme) { this.colorManager.setTheme(theme); } @@ -84,7 +82,7 @@ export class Renderer extends EventEmitter implements IRenderer { // and the terminal needs to refreshed if (this._devicePixelRatio !== devicePixelRatio) { this._devicePixelRatio = devicePixelRatio; - this.onResize(this._terminal.cols, this._terminal.rows, true); + this.onResize(this._terminal.cols, this._terminal.rows); } } @@ -106,12 +104,12 @@ export class Renderer extends EventEmitter implements IRenderer { return this.colorManager.colors; } - public onResize(cols: number, rows: number, didCharSizeChange: boolean): void { + public onResize(cols: number, rows: number): void { // Update character and canvas dimensions this._updateDimensions(); // Resize all render layers - this._renderLayers.forEach(l => l.resize(this._terminal, this.dimensions, didCharSizeChange)); + this._renderLayers.forEach(l => l.resize(this._terminal, this.dimensions)); // Force a refresh if (this._isPaused) { @@ -131,7 +129,7 @@ export class Renderer extends EventEmitter implements IRenderer { } public onCharSizeChanged(): void { - this.onResize(this._terminal.cols, this._terminal.rows, true); + this.onResize(this._terminal.cols, this._terminal.rows); } public onBlur(): void { diff --git a/src/renderer/SelectionRenderLayer.ts b/src/renderer/SelectionRenderLayer.ts index 53fc9b39..7a6a5af8 100644 --- a/src/renderer/SelectionRenderLayer.ts +++ b/src/renderer/SelectionRenderLayer.ts @@ -3,10 +3,8 @@ * @license MIT */ -import { IBuffer, ICharMeasure, ITerminal } from '../Types'; -import { CHAR_DATA_ATTR_INDEX } from '../Buffer'; -import { GridCache } from './GridCache'; -import { FLAGS, IColorSet, IRenderDimensions } from './Types'; +import { ITerminal } from '../Types'; +import { IColorSet, IRenderDimensions } from './Types'; import { BaseRenderLayer } from './BaseRenderLayer'; export class SelectionRenderLayer extends BaseRenderLayer { @@ -20,8 +18,8 @@ export class SelectionRenderLayer extends BaseRenderLayer { }; } - public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void { - super.resize(terminal, dim, charSizeChanged); + public resize(terminal: ITerminal, dim: IRenderDimensions): void { + super.resize(terminal, dim); // Resizing the canvas discards the contents of the canvas so clear state this._state = { start: null, diff --git a/src/renderer/TextRenderLayer.ts b/src/renderer/TextRenderLayer.ts index a2ab9f19..27ce9f79 100644 --- a/src/renderer/TextRenderLayer.ts +++ b/src/renderer/TextRenderLayer.ts @@ -5,16 +5,17 @@ import { CHAR_DATA_ATTR_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX } from '../Buffer'; import { FLAGS, IColorSet, IRenderDimensions } from './Types'; -import { CharData, IBuffer, ICharMeasure, ITerminal } from '../Types'; +import { CharData, ITerminal } from '../Types'; +import { INVERTED_DEFAULT_COLOR } from './atlas/Types'; import { GridCache } from './GridCache'; -import { BaseRenderLayer, INVERTED_DEFAULT_COLOR } from './BaseRenderLayer'; +import { BaseRenderLayer } from './BaseRenderLayer'; /** * This CharData looks like a null character, which will forc a clear and render * when the character changes (a regular space ' ' character may not as it's * drawn state is a cleared cell). */ -const OVERLAP_OWNED_CHAR_DATA: CharData = [null, '', 0, -1]; +// const OVERLAP_OWNED_CHAR_DATA: CharData = [null, '', 0, -1]; export class TextRenderLayer extends BaseRenderLayer { private _state: GridCache; @@ -27,8 +28,8 @@ export class TextRenderLayer extends BaseRenderLayer { this._state = new GridCache(); } - public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void { - super.resize(terminal, dim, charSizeChanged); + public resize(terminal: ITerminal, dim: IRenderDimensions): void { + super.resize(terminal, dim); // Clear the character width cache if the font or width has changed const terminalFont = this._getFont(terminal, false); @@ -238,13 +239,13 @@ export class TextRenderLayer extends BaseRenderLayer { * @param x The column of the char. * @param y The row of the char. */ - private _clearChar(x: number, y: number): void { - let colsToClear = 1; - // Clear the adjacent character if it was wide - const state = this._state.cache[x][y]; - if (state && state[CHAR_DATA_WIDTH_INDEX] === 2) { - colsToClear = 2; - } - this.clearCells(x, y, colsToClear, 1); - } + // private _clearChar(x: number, y: number): void { + // let colsToClear = 1; + // // Clear the adjacent character if it was wide + // const state = this._state.cache[x][y]; + // if (state && state[CHAR_DATA_WIDTH_INDEX] === 2) { + // colsToClear = 2; + // } + // this.clearCells(x, y, colsToClear, 1); + // } } diff --git a/src/renderer/Types.ts b/src/renderer/Types.ts index 6f6e5f0e..8c464bec 100644 --- a/src/renderer/Types.ts +++ b/src/renderer/Types.ts @@ -5,6 +5,7 @@ import { ITerminal } from '../Types'; import { IEventEmitter, ITheme } from 'xterm'; +import { IColorSet } from '../shared/Types'; /** * Flags used to render terminal text properly. @@ -24,7 +25,7 @@ export interface IRenderer extends IEventEmitter { setTheme(theme: ITheme): IColorSet; onWindowResize(devicePixelRatio: number): void; - onResize(cols: number, rows: number, didCharSizeChange: boolean): void; + onResize(cols: number, rows: number): void; onCharSizeChanged(): void; onBlur(): void; onFocus(): void; @@ -39,14 +40,8 @@ export interface IColorManager { colors: IColorSet; } -export interface IColorSet { - foreground: string; - background: string; - cursor: string; - cursorAccent: string; - selection: string; - ansi: string[]; -} +// TODO: We should probably rewrite the imports for IColorSet, but there's a lot of them +export { IColorSet }; export interface IRenderDimensions { scaledCharWidth: number; @@ -103,7 +98,7 @@ export interface IRenderLayer { /** * Resize the render layer. */ - resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void; + resize(terminal: ITerminal, dim: IRenderDimensions): void; /** * Clear the state of the render layer. diff --git a/src/renderer/atlas/CharAtlas.ts b/src/renderer/atlas/CharAtlas.ts new file mode 100644 index 00000000..2e417c09 --- /dev/null +++ b/src/renderer/atlas/CharAtlas.ts @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { ITerminal } from '../../Types'; +import { IColorSet } from '../Types'; +import { ICharAtlasConfig } from '../../shared/atlas/Types'; +import { generateCharAtlas } from '../../shared/atlas/CharAtlasGenerator'; +import { generateConfig, configEquals } from './CharAtlasUtils'; + +interface ICharAtlasCacheEntry { + bitmap: HTMLCanvasElement | Promise; + config: ICharAtlasConfig; + ownedBy: ITerminal[]; +} + +let charAtlasCache: ICharAtlasCacheEntry[] = []; + +/** + * Acquires a char atlas, either generating a new one or returning an existing + * one that is in use by another terminal. + * @param terminal The terminal. + * @param colors The colors to use. + */ +export function acquireCharAtlas(terminal: ITerminal, colors: IColorSet, scaledCharWidth: number, scaledCharHeight: number): HTMLCanvasElement | Promise { + const newConfig = generateConfig(scaledCharWidth, scaledCharHeight, terminal, colors); + + // Check to see if the terminal already owns this config + for (let i = 0; i < charAtlasCache.length; i++) { + const entry = charAtlasCache[i]; + const ownedByIndex = entry.ownedBy.indexOf(terminal); + if (ownedByIndex >= 0) { + if (configEquals(entry.config, newConfig)) { + return entry.bitmap; + } else { + // The configs differ, release the terminal from the entry + if (entry.ownedBy.length === 1) { + charAtlasCache.splice(i, 1); + } else { + entry.ownedBy.splice(ownedByIndex, 1); + } + break; + } + } + } + + // Try match a char atlas from the cache + for (let i = 0; i < charAtlasCache.length; i++) { + const entry = charAtlasCache[i]; + if (configEquals(entry.config, newConfig)) { + // Add the terminal to the cache entry and return + entry.ownedBy.push(terminal); + return entry.bitmap; + } + } + + const canvasFactory = (width: number, height: number) => { + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + return canvas; + }; + + const newEntry: ICharAtlasCacheEntry = { + bitmap: generateCharAtlas(window, canvasFactory, newConfig), + config: newConfig, + ownedBy: [terminal] + }; + charAtlasCache.push(newEntry); + return newEntry.bitmap; +} diff --git a/src/renderer/atlas/CharAtlasUtils.ts b/src/renderer/atlas/CharAtlasUtils.ts new file mode 100644 index 00000000..89c735eb --- /dev/null +++ b/src/renderer/atlas/CharAtlasUtils.ts @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { ITerminal } from '../../Types'; +import { IColorSet } from '../Types'; +import { ICharAtlasConfig } from '../../shared/atlas/Types'; + +export function generateConfig(scaledCharWidth: number, scaledCharHeight: number, terminal: ITerminal, colors: IColorSet): ICharAtlasConfig { + const clonedColors = { + foreground: colors.foreground, + background: colors.background, + cursor: null, + cursorAccent: null, + selection: null, + ansi: colors.ansi.slice(0, 16) + }; + return { + devicePixelRatio: window.devicePixelRatio, + scaledCharWidth, + scaledCharHeight, + fontFamily: terminal.options.fontFamily, + fontSize: terminal.options.fontSize, + fontWeight: terminal.options.fontWeight, + fontWeightBold: terminal.options.fontWeightBold, + allowTransparency: terminal.options.allowTransparency, + colors: clonedColors + }; +} + +export function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean { + for (let i = 0; i < a.colors.ansi.length; i++) { + if (a.colors.ansi[i] !== b.colors.ansi[i]) { + return false; + } + } + return a.devicePixelRatio === b.devicePixelRatio && + a.fontFamily === b.fontFamily && + a.fontSize === b.fontSize && + a.fontWeight === b.fontWeight && + a.fontWeightBold === b.fontWeightBold && + a.allowTransparency === b.allowTransparency && + a.scaledCharWidth === b.scaledCharWidth && + a.scaledCharHeight === b.scaledCharHeight && + a.colors.foreground === b.colors.foreground && + a.colors.background === b.colors.background; +} diff --git a/src/renderer/atlas/Types.ts b/src/renderer/atlas/Types.ts new file mode 100644 index 00000000..34f01d39 --- /dev/null +++ b/src/renderer/atlas/Types.ts @@ -0,0 +1,7 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +export const INVERTED_DEFAULT_COLOR = -1; +export const DIM_OPACITY = 0.5; diff --git a/src/shared/Types.ts b/src/shared/Types.ts new file mode 100644 index 00000000..0407c2ef --- /dev/null +++ b/src/shared/Types.ts @@ -0,0 +1,13 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +export interface IColorSet { + foreground: string; + background: string; + cursor: string; + cursorAccent: string; + selection: string; + ansi: string[]; +} diff --git a/src/shared/CharAtlasGenerator.ts b/src/shared/atlas/CharAtlasGenerator.ts similarity index 70% rename from src/shared/CharAtlasGenerator.ts rename to src/shared/atlas/CharAtlasGenerator.ts index 96cf6cc6..10112efa 100644 --- a/src/shared/CharAtlasGenerator.ts +++ b/src/shared/atlas/CharAtlasGenerator.ts @@ -4,7 +4,8 @@ */ import { FontWeight } from 'xterm'; -import { isFirefox } from './utils/Browser'; +import { CHAR_ATLAS_CELL_SPACING, ICharAtlasConfig } from './Types'; +import { isFirefox } from '../utils/Browser'; declare const Promise: any; @@ -15,43 +16,27 @@ export interface IOffscreenCanvas { transferToImageBitmap(): ImageBitmap; } -export interface ICharAtlasRequest { - scaledCharWidth: number; - scaledCharHeight: number; - fontSize: number; - fontFamily: string; - fontWeight: FontWeight; - fontWeightBold: FontWeight; - background: string; - foreground: string; - ansiColors: string[]; - devicePixelRatio: number; - allowTransparency: boolean; -} - -export const CHAR_ATLAS_CELL_SPACING = 1; - /** * Generates a char atlas. * @param context The window or worker context. * @param canvasFactory A function to generate a canvas with a width or height. * @param request The config for the new char atlas. */ -export function generateCharAtlas(context: Window, canvasFactory: (width: number, height: number) => HTMLCanvasElement | IOffscreenCanvas, request: ICharAtlasRequest): HTMLCanvasElement | Promise { - const cellWidth = request.scaledCharWidth + CHAR_ATLAS_CELL_SPACING; - const cellHeight = request.scaledCharHeight + CHAR_ATLAS_CELL_SPACING; +export function generateCharAtlas(context: Window, canvasFactory: (width: number, height: number) => HTMLCanvasElement | IOffscreenCanvas, config: ICharAtlasConfig): HTMLCanvasElement | Promise { + const cellWidth = config.scaledCharWidth + CHAR_ATLAS_CELL_SPACING; + const cellHeight = config.scaledCharHeight + CHAR_ATLAS_CELL_SPACING; const canvas = canvasFactory( /*255 ascii chars*/255 * cellWidth, (/*default+default bold*/2 + /*0-15*/16) * cellHeight ); - const ctx = canvas.getContext('2d', {alpha: request.allowTransparency}); + const ctx = canvas.getContext('2d', {alpha: config.allowTransparency}); - ctx.fillStyle = request.background; + ctx.fillStyle = config.colors.background; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.save(); - ctx.fillStyle = request.foreground; - ctx.font = getFont(request.fontWeight, request); + ctx.fillStyle = config.colors.foreground; + ctx.font = getFont(config.fontWeight, config); ctx.textBaseline = 'top'; // Default color @@ -65,7 +50,7 @@ export function generateCharAtlas(context: Window, canvasFactory: (width: number } // Default color bold ctx.save(); - ctx.font = getFont(request.fontWeightBold, request); + ctx.font = getFont(config.fontWeightBold, config); for (let i = 0; i < 256; i++) { ctx.save(); ctx.beginPath(); @@ -77,11 +62,11 @@ export function generateCharAtlas(context: Window, canvasFactory: (width: number ctx.restore(); // Colors 0-15 - ctx.font = getFont(request.fontWeight, request); + ctx.font = getFont(config.fontWeight, config); for (let colorIndex = 0; colorIndex < 16; colorIndex++) { // colors 8-15 are bold if (colorIndex === 8) { - ctx.font = getFont(request.fontWeightBold, request); + ctx.font = getFont(config.fontWeightBold, config); } const y = (colorIndex + 2) * cellHeight; // Draw ascii characters @@ -90,7 +75,7 @@ export function generateCharAtlas(context: Window, canvasFactory: (width: number ctx.beginPath(); ctx.rect(i * cellWidth, y, cellWidth, cellHeight); ctx.clip(); - ctx.fillStyle = request.ansiColors[colorIndex]; + ctx.fillStyle = config.colors.ansi[colorIndex]; ctx.fillText(String.fromCharCode(i), i * cellWidth, y); ctx.restore(); } @@ -115,9 +100,9 @@ export function generateCharAtlas(context: Window, canvasFactory: (width: number const charAtlasImageData = ctx.getImageData(0, 0, canvas.width, canvas.height); // Remove the background color from the image so characters may overlap - const r = parseInt(request.background.substr(1, 2), 16); - const g = parseInt(request.background.substr(3, 2), 16); - const b = parseInt(request.background.substr(5, 2), 16); + const r = parseInt(config.colors.background.substr(1, 2), 16); + const g = parseInt(config.colors.background.substr(3, 2), 16); + const b = parseInt(config.colors.background.substr(5, 2), 16); clearColor(charAtlasImageData, r, g, b); return context.createImageBitmap(charAtlasImageData); @@ -136,6 +121,6 @@ function clearColor(imageData: ImageData, r: number, g: number, b: number): void } } -function getFont(fontWeight: FontWeight, request: ICharAtlasRequest): string { - return `${fontWeight} ${request.fontSize * request.devicePixelRatio}px ${request.fontFamily}`; +function getFont(fontWeight: FontWeight, config: ICharAtlasConfig): string { + return `${fontWeight} ${config.fontSize * config.devicePixelRatio}px ${config.fontFamily}`; } diff --git a/src/shared/atlas/Types.ts b/src/shared/atlas/Types.ts new file mode 100644 index 00000000..4a66d554 --- /dev/null +++ b/src/shared/atlas/Types.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { FontWeight } from 'xterm'; +import { IColorSet } from '../Types'; + +export const CHAR_ATLAS_CELL_SPACING = 1; + +export interface ICharAtlasConfig { + devicePixelRatio: number; + fontSize: number; + fontFamily: string; + fontWeight: FontWeight; + fontWeightBold: FontWeight; + scaledCharWidth: number; + scaledCharHeight: number; + allowTransparency: boolean; + colors: IColorSet; +} diff --git a/src/utils/CharMeasure.test.ts b/src/utils/CharMeasure.test.ts index e4f4e1d6..a3cb3b3b 100644 --- a/src/utils/CharMeasure.test.ts +++ b/src/utils/CharMeasure.test.ts @@ -4,7 +4,7 @@ */ import jsdom = require('jsdom'); -import { ICharMeasure, ITerminal } from '../Types'; +import { ICharMeasure } from '../Types'; import { assert } from 'chai'; import { CharMeasure } from './CharMeasure'; diff --git a/src/utils/CharMeasure.ts b/src/utils/CharMeasure.ts index 91cfce5f..5ad1de76 100644 --- a/src/utils/CharMeasure.ts +++ b/src/utils/CharMeasure.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { ICharMeasure, ITerminal, ITerminalOptions } from '../Types'; +import { ICharMeasure, ITerminalOptions } from '../Types'; import { EventEmitter } from '../EventEmitter'; /** @@ -23,10 +23,7 @@ export class CharMeasure extends EventEmitter implements ICharMeasure { this._document = document; this._parentElement = parentElement; this._measureElement = this._document.createElement('span'); - this._measureElement.style.position = 'absolute'; - this._measureElement.style.top = '0'; - this._measureElement.style.left = '-9999em'; - this._measureElement.style.lineHeight = 'normal'; + this._measureElement.classList.add('xterm-char-measure-element'); this._measureElement.textContent = 'W'; this._measureElement.setAttribute('aria-hidden', 'true'); this._parentElement.appendChild(this._measureElement); @@ -41,7 +38,7 @@ export class CharMeasure extends EventEmitter implements ICharMeasure { } public measure(options: ITerminalOptions): void { - this._measureElement.style.fontFamily = options.fontFamily; + this._measureElement.style.fontFamily = options.fontFamily; this._measureElement.style.fontSize = `${options.fontSize}px`; const geometry = this._measureElement.getBoundingClientRect(); // The element is likely currently display:none, we should retain the @@ -55,5 +52,4 @@ export class CharMeasure extends EventEmitter implements ICharMeasure { this.emit('charsizechanged'); } } - } diff --git a/src/utils/CircularList.test.ts b/src/utils/CircularList.test.ts index 6bb35113..4c07b16c 100644 --- a/src/utils/CircularList.test.ts +++ b/src/utils/CircularList.test.ts @@ -6,10 +6,6 @@ import { assert } from 'chai'; import { CircularList } from './CircularList'; -class TestCircularList extends CircularList { - public get array(): T[] { return this._array; } -} - describe('CircularList', () => { describe('push', () => { it('should push values onto the array', () => { diff --git a/src/utils/TestUtils.test.ts b/src/utils/TestUtils.test.ts index 8ba16179..3c60d695 100644 --- a/src/utils/TestUtils.test.ts +++ b/src/utils/TestUtils.test.ts @@ -269,6 +269,9 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal { addDisposableListener(type: string, handler: XtermListener): IDisposable { throw new Error('Method not implemented.'); } + tabSet(): void { + throw new Error('Method not implemented.'); + } } export class MockBuffer implements IBuffer { @@ -276,6 +279,7 @@ export class MockBuffer implements IBuffer { lines: ICircularList<[number, string, number, number][]>; ydisp: number; ybase: number; + hasScrollback: boolean; y: number; x: number; tabs: any; @@ -310,7 +314,7 @@ export class MockRenderer implements IRenderer { } dimensions: IRenderDimensions; setTheme(theme: ITheme): IColorSet { return {}; } - onResize(cols: number, rows: number, didCharSizeChange: boolean): void {} + onResize(cols: number, rows: number): void {} onCharSizeChanged(): void {} onBlur(): void {} onFocus(): void {} @@ -337,6 +341,9 @@ export class MockViewport implements IViewport { throw new Error('Method not implemented.'); } syncScrollArea(): void { } + getLinesScrolled(ev: WheelEvent): number { + throw new Error('Method not implemented.'); + } } export class MockCompositionHelper implements ICompositionHelper { diff --git a/src/xterm.css b/src/xterm.css index 16eb283e..eec41a05 100644 --- a/src/xterm.css +++ b/src/xterm.css @@ -117,11 +117,13 @@ visibility: hidden; } -.xterm .xterm-char-measure-element { +.xterm-char-measure-element { display: inline-block; visibility: hidden; position: absolute; + top: 0; left: -9999em; + line-height: normal; } .xterm.enable-mouse-events { @@ -144,10 +146,6 @@ color: transparent; } -.xterm .xterm-accessibility-tree:focus [id^="xterm-active-item-"] { - outline: 1px solid #F80; -} - .xterm .live-region { position: absolute; left: -9999px; @@ -155,3 +153,7 @@ height: 1px; overflow: hidden; } + +.xterm-cursor-pointer { + cursor: pointer; +} diff --git a/tsconfig.json b/tsconfig.json index 38bfd2fa..ffd9ab68 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,7 +5,8 @@ "rootDir": "src", "outDir": "lib", "sourceMap": true, - "removeComments": true + "removeComments": true, + "noUnusedLocals": true }, "include": [ "src/**/*" diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 32c8b0a0..1e5999c9 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -23,6 +23,7 @@ declare module 'xterm' { * Warning: Enabling this option can reduce performances somewhat. */ allowTransparency?: boolean; + /** * A data uri of the sound to use for the bell (needs bellStyle = 'sound'). */ @@ -55,7 +56,7 @@ declare module 'xterm' { /** * Whether to enable the rendering of bold text. - * + * * @deprecated Use fontWeight and fontWeightBold instead. */ enableBold?: boolean; @@ -395,7 +396,7 @@ declare module 'xterm' { * @param options Options for the link matcher. * @return The ID of the new matcher, this can be used to deregister. */ - registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => boolean | void, options?: ILinkMatcherOptions): number; + registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): number; /** * (EXPERIMENTAL) Deregisters a link matcher if it has been registered.