From db5b908d621fcb951717075c2cc1b2ab45f7f4b8 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 5 Aug 2017 00:22:31 -0700 Subject: [PATCH 01/22] Move xterm.js -> Terminal.ts --- src/{xterm.js => Terminal.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/{xterm.js => Terminal.ts} (100%) diff --git a/src/xterm.js b/src/Terminal.ts similarity index 100% rename from src/xterm.js rename to src/Terminal.ts From 2104dccec5f02ddfa33142900d45fee6162d9813 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 5 Aug 2017 01:58:42 -0700 Subject: [PATCH 02/22] Fix most errors in Terminal.ts --- src/Charsets.ts | 6 +- src/EventEmitter.ts | 4 + src/Interfaces.ts | 2 +- src/Terminal.ts | 3904 +++++++++++++++++++------------------ src/Types.ts | 5 + src/utils/CircularList.ts | 3 +- src/xterm.ts | 3 + 7 files changed, 1973 insertions(+), 1954 deletions(-) create mode 100644 src/xterm.ts diff --git a/src/Charsets.ts b/src/Charsets.ts index f567ef442c..c2c92e8d4a 100644 --- a/src/Charsets.ts +++ b/src/Charsets.ts @@ -2,17 +2,19 @@ * @license MIT */ +import { Charset } from './Types'; + /** * The character sets supported by the terminal. These enable several languages * to be represented within the terminal with only 8-bit encoding. See ISO 2022 * for a discussion on character sets. Only VT100 character sets are supported. */ -export const CHARSETS: {[key: string]: {[key: string]: string}} = {}; +export const CHARSETS: { [key: string]: Charset } = {}; /** * The default character set, US. */ -export const DEFAULT_CHARSET = CHARSETS['B']; +export const DEFAULT_CHARSET: Charset = CHARSETS['B']; /** * DEC Special Character and Line Drawing Set. diff --git a/src/EventEmitter.ts b/src/EventEmitter.ts index 00c1094186..3d34da7541 100644 --- a/src/EventEmitter.ts +++ b/src/EventEmitter.ts @@ -68,4 +68,8 @@ export class EventEmitter implements IEventEmitter { public listeners(type): ListenerType[] { return this._events[type] || []; } + + protected destroy(): void { + this._events = {}; + } } diff --git a/src/Interfaces.ts b/src/Interfaces.ts index f19a7f28f8..c675f7a244 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -92,8 +92,8 @@ export interface ILinkifier { export interface ICircularList extends IEventEmitter { length: number; maxLength: number; + forEach: (callbackfn: (value: T, index: number) => void) => void; - forEach(callbackfn: (value: T, index: number, array: T[]) => void): void; get(index: number): T; set(index: number, value: T): void; push(value: T): void; diff --git a/src/Terminal.ts b/src/Terminal.ts index 5aa069e796..18fcd00064 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -11,6 +11,7 @@ */ import { BufferSet } from './BufferSet'; +import { Buffer } from './Buffer'; import { CompositionHelper } from './CompositionHelper'; import { EventEmitter } from './EventEmitter'; import { Viewport } from './Viewport'; @@ -28,6 +29,11 @@ import * as Mouse from './utils/Mouse'; import { CHARSETS } from './Charsets'; import { getRawByteCoords } from './utils/Mouse'; import { translateBufferLineToString } from './utils/BufferLine'; +import { TerminalOptions, CustomKeyEventHandler, Charset } from './Types'; +import { ITerminal, IBrowser } from "./Interfaces"; + +// Declare for RequireJS in loadAddon +declare var define: any; /** * Terminal Emulation References: @@ -63,202 +69,16 @@ var WRITE_BATCH_SIZE = 300; */ var CURSOR_BLINK_INTERVAL = 600; -/** - * Terminal - */ - -/** - * Creates a new `Terminal` object. - * - * @param {object} options An object containing a set of options, the available options are: - * - `cursorBlink` (boolean): Whether the terminal cursor blinks - * - `cols` (number): The number of columns of the terminal (horizontal size) - * - `rows` (number): The number of rows of the terminal (vertical size) - * - * @public - * @class Xterm Xterm - * @alias module:xterm/src/xterm - */ -function Terminal(options) { - var self = this; - - if (!(this instanceof Terminal)) { - return new Terminal(arguments[0], arguments[1], arguments[2]); - } - - self.browser = Browser; - self.cancel = Terminal.cancel; - - EventEmitter.call(this); - - if (typeof options === 'number') { - options = { - cols: arguments[0], - rows: arguments[1], - handler: arguments[2] - }; - } - - options = options || {}; - - - Object.keys(Terminal.defaults).forEach(function(key) { - if (options[key] == null) { - options[key] = Terminal.options[key]; - - if (Terminal[key] !== Terminal.defaults[key]) { - options[key] = Terminal[key]; - } - } - self[key] = options[key]; - }); - - if (options.colors.length === 8) { - options.colors = options.colors.concat(Terminal._colors.slice(8)); - } else if (options.colors.length === 16) { - options.colors = options.colors.concat(Terminal._colors.slice(16)); - } else if (options.colors.length === 10) { - options.colors = options.colors.slice(0, -2).concat( - Terminal._colors.slice(8, -2), options.colors.slice(-2)); - } else if (options.colors.length === 18) { - options.colors = options.colors.concat( - Terminal._colors.slice(16, -2), options.colors.slice(-2)); - } - this.colors = options.colors; - - this.options = options; - - // this.context = options.context || window; - // this.document = options.document || document; - this.parent = options.body || options.parent || ( - document ? document.getElementsByTagName('body')[0] : null - ); - - this.cols = options.cols || options.geometry[0]; - this.rows = options.rows || options.geometry[1]; - this.geometry = [this.cols, this.rows]; - - if (options.handler) { - this.on('data', options.handler); - } - - this.cursorState = 0; - this.cursorHidden = false; - this.convertEol; - this.queue = ''; - this.customKeyEventHandler = null; - this.cursorBlinkInterval = null; - - // modes - this.applicationKeypad = false; - this.applicationCursor = false; - this.originMode = false; - this.insertMode = false; - this.wraparoundMode = true; // defaults: xterm - true, vt100 - false - - // charset - this.charset = null; - this.gcharset = null; - this.glevel = 0; - this.charsets = [null]; - - // mouse properties - this.decLocator; - this.x10Mouse; - this.vt200Mouse; - this.vt300Mouse; - this.normalMouse; - this.mouseEvents; - this.sendFocus; - this.utfMouse; - this.sgrMouse; - this.urxvtMouse; - - // misc - this.element; - this.children; - this.refreshStart; - this.refreshEnd; - this.savedX; - this.savedY; - this.savedCols; - - // stream - this.readable = true; - this.writable = true; - - this.defAttr = (0 << 18) | (257 << 9) | (256 << 0); - this.curAttr = this.defAttr; - - this.params = []; - this.currentParam = 0; - this.prefix = ''; - this.postfix = ''; - - this.inputHandler = new InputHandler(this); - this.parser = new Parser(this.inputHandler, this); - // Reuse renderer if the Terminal is being recreated via a Terminal.reset call. - this.renderer = this.renderer || null; - this.selectionManager = this.selectionManager || null; - this.linkifier = this.linkifier || new Linkifier(); - - // user input states - this.writeBuffer = []; - this.writeInProgress = false; - - /** - * Whether _xterm.js_ sent XOFF in order to catch up with the pty process. - * This is a distinct state from writeStopped so that if the user requested - * XOFF via ^S that it will not automatically resume when the writeBuffer goes - * below threshold. - */ - this.xoffSentToCatchUp = false; - - /** Whether writing has been stopped as a result of XOFF */ - this.writeStopped = false; - - // leftover surrogate high from previous write invocation - this.surrogate_high = ''; - - // Create the terminal's buffers and set the current buffer - this.buffers = new BufferSet(this); - this.buffer = this.buffers.active; // Convenience shortcut; - this.buffers.on('activate', function (buffer) { - this._terminal.buffer = buffer; - }); - var i = this.rows; - while (i--) { - this.buffer.lines.push(this.blankLine()); - } - // Ensure the selection manager has the correct buffer - if (this.selectionManager) { - this.selectionManager.setBuffer(this.buffer.lines); - } - this.setupStops(); - // Store if user went browsing history in scrollback - this.userScrolling = false; -} -inherits(Terminal, EventEmitter); -/** - * back_color_erase feature for xterm. - */ -Terminal.prototype.eraseAttr = function() { - // if (this.is('screen')) return this.defAttr; - return (this.defAttr & ~0x1ff) | (this.curAttr & 0x1ff); -}; -/** - * Colors - */ // Colors 0-15 -Terminal.tangoColors = [ +const tangoColors = [ // dark: '#2e3436', '#cc0000', @@ -281,8 +101,8 @@ Terminal.tangoColors = [ // Colors 0-15 + 16-255 // Much thanks to TooTallNate for writing this. -Terminal.colors = (function() { - var colors = Terminal.tangoColors.slice() +const defaultColors = (function() { + let colors = tangoColors.slice() , r = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff] , i; @@ -294,9 +114,10 @@ Terminal.colors = (function() { // 232-255 (grey) i = 0; + let c: number; for (; i < 24; i++) { - r = 8 + i * 10; - out(r, r, r); + c = 8 + i * 10; + out(c, c, c); } function out(r, g, b) { @@ -311,11 +132,11 @@ Terminal.colors = (function() { return colors; })(); -Terminal._colors = Terminal.colors.slice(); +const _colors = defaultColors.slice(); -Terminal.vcolors = (function() { +const vcolors = (function() { var out = [] - , colors = Terminal.colors + , colors = defaultColors , i = 0 , color; @@ -331,13 +152,8 @@ Terminal.vcolors = (function() { return out; })(); -/** - * Options - */ - -Terminal.defaults = { - colors: Terminal.colors, - theme: 'default', +const DEFAULT_OPTIONS: TerminalOptions = { + colors: defaultColors, convertEol: false, termName: 'xterm', geometry: [80, 24], @@ -356,1966 +172,2203 @@ Terminal.defaults = { // focusKeys: false, }; -Terminal.options = {}; - -Terminal.focus = null; -each(keys(Terminal.defaults), function(key) { - Terminal[key] = Terminal.defaults[key]; - Terminal.options[key] = Terminal.defaults[key]; -}); -/** - * Focus the terminal. Delegates focus handling to the terminal's DOM element. - */ -Terminal.prototype.focus = function() { - return this.textarea.focus(); -}; - -/** - * Retrieves an option's value from the terminal. - * @param {string} key The option key. - */ -Terminal.prototype.getOption = function(key) { - if (!(key in Terminal.defaults)) { - throw new Error('No option with key "' + key + '"'); - } - if (typeof this.options[key] !== 'undefined') { - return this.options[key]; - } - return this[key]; -}; -/** - * Sets an option on the terminal. - * @param {string} key The option key. - * @param {string} value The option value. - */ -Terminal.prototype.setOption = function(key, value) { - if (!(key in Terminal.defaults)) { - throw new Error('No option with key "' + key + '"'); - } - switch (key) { - case 'scrollback': - if (value < this.rows) { - let msg = 'Setting the scrollback value less than the number of rows '; - msg += `(${this.rows}) is not allowed.`; - console.warn(msg); - return false; - } +export class Terminal extends EventEmitter implements ITerminal { + public textarea: HTMLTextAreaElement; + public element: HTMLElement; + public rowContainer: HTMLElement; - if (this.options[key] !== value) { - if (this.buffer.lines.length > value) { - const amountToTrim = this.buffer.lines.length - value; - const needsRefresh = (this.buffer.ydisp - amountToTrim < 0); - this.buffer.lines.trimStart(amountToTrim); - this.buffer.ybase = Math.max(this.buffer.ybase - amountToTrim, 0); - this.buffer.ydisp = Math.max(this.buffer.ydisp - amountToTrim, 0); - if (needsRefresh) { - this.refresh(0, this.rows - 1); - } - } - this.buffer.lines.maxLength = value; - this.viewport.syncScrollArea(); - } - break; - } - this[key] = value; - this.options[key] = value; - switch (key) { - case 'cursorBlink': this.setCursorBlinking(value); break; - case 'cursorStyle': - this.element.classList.toggle(`xterm-cursor-style-block`, value === 'block'); - this.element.classList.toggle(`xterm-cursor-style-underline`, value === 'underline'); - this.element.classList.toggle(`xterm-cursor-style-bar`, value === 'bar'); - break; - case 'tabStopWidth': this.setupStops(); break; - } -}; + // TODO: Do we need a reference to body? There is also a body variable? + // The body of the terminal + private parent: HTMLElement; + private context: Window; + private document: Document; + private body: HTMLBodyElement; + private viewportScrollArea: HTMLElement; + private viewportElement: HTMLElement; + private selectionContainer: HTMLElement; + private helperContainer: HTMLElement; + private compositionView: HTMLElement; + private charSizeStyleElement: HTMLStyleElement; -Terminal.prototype.restartCursorBlinking = function () { - this.setCursorBlinking(this.options.cursorBlink); -}; + // TODO: This should be removed from the interface, opting for modules to pull + // in Browser for themselves. + public browser: IBrowser = Browser; -Terminal.prototype.setCursorBlinking = function (enabled) { - this.element.classList.toggle('xterm-cursor-blink', enabled); - this.clearCursorBlinkingInterval(); - if (enabled) { - var self = this; - this.cursorBlinkInterval = setInterval(function () { - self.element.classList.toggle('xterm-cursor-blink-on'); - }, CURSOR_BLINK_INTERVAL); - } -}; + private options: TerminalOptions; + private colors: any; -Terminal.prototype.clearCursorBlinkingInterval = function () { - this.element.classList.remove('xterm-cursor-blink-on'); - if (this.cursorBlinkInterval) { - clearInterval(this.cursorBlinkInterval); - this.cursorBlinkInterval = null; - } -}; + // TODO: This can be changed to an enum or boolean, 0 and 1 seem to be the only options + private cursorState: number = 0; + private cursorHidden: boolean = false; + private convertEol: boolean; + // TODO: This is the data queue for send, improve name and documentation + private queue: string = ''; + private customKeyEventHandler: CustomKeyEventHandler = null; + // The ID from a setInterval that tracks the blink animation. This animation + // is done in JS due to a Chromium bug with CSS animations that thrashed the + // CPU. + private cursorBlinkInterval: NodeJS.Timer = null; -/** - * Binds the desired focus behavior on a given terminal object. - * - * @static - */ -Terminal.bindFocus = function (term) { - on(term.textarea, 'focus', function (ev) { - if (term.sendFocus) { - term.send(C0.ESC + '[I'); - } - term.element.classList.add('focus'); - term.showCursor(); - term.restartCursorBlinking.apply(term); - Terminal.focus = term; - term.emit('focus', {terminal: term}); - }); -}; + // modes + private applicationKeypad: boolean = false; + private applicationCursor: boolean = false; + private originMode: boolean = false; + private insertMode: boolean = false; + private wraparoundMode: boolean = true; // defaults: xterm - true, vt100 - false -/** - * Blur the terminal. Delegates blur handling to the terminal's DOM element. - */ -Terminal.prototype.blur = function() { - return this.textarea.blur(); -}; + // charset + // The current charset + private charset: Charset = null; + private gcharset: number = null; + private glevel: number = 0; + private charsets: Charset[] = [null]; -/** - * Binds the desired blur behavior on a given terminal object. - * - * @static - */ -Terminal.bindBlur = function (term) { - on(term.textarea, 'blur', function (ev) { - term.refresh(term.buffer.y, term.buffer.y); - if (term.sendFocus) { - term.send(C0.ESC + '[O'); - } - term.element.classList.remove('focus'); - term.clearCursorBlinkingInterval.apply(term); - Terminal.focus = null; - term.emit('blur', {terminal: term}); - }); -}; + // mouse properties + private decLocator; + private x10Mouse; + private vt200Mouse; + private vt300Mouse; + private normalMouse; + private mouseEvents; + private sendFocus; + private utfMouse; + private sgrMouse; + private urxvtMouse; -/** - * Initialize default behavior - */ -Terminal.prototype.initGlobal = function() { - var term = this; - - Terminal.bindKeys(this); - Terminal.bindFocus(this); - Terminal.bindBlur(this); - - // Bind clipboard functionality - on(this.element, 'copy', event => { - // If mouse events are active it means the selection manager is disabled and - // copy should be handled by the host program. - if (!term.hasSelection()) { - return; - } - copyHandler(event, term, this.selectionManager); - }); - const pasteHandlerWrapper = event => pasteHandler(event, term); - on(this.textarea, 'paste', pasteHandlerWrapper); - on(this.element, 'paste', pasteHandlerWrapper); - - // Handle right click context menus - if (term.browser.isFirefox) { - // Firefox doesn't appear to fire the contextmenu event on right click - on(this.element, 'mousedown', event => { - if (event.button == 2) { - rightClickHandler(event, this.textarea, this.selectionManager); - } - }); - } else { - on(this.element, 'contextmenu', event => { - rightClickHandler(event, this.textarea, this.selectionManager); - }); - } + // misc + private children; + private refreshStart; + private refreshEnd; + private savedX; + private savedY; + private savedCols; - // Move the textarea under the cursor when middle clicking on Linux to ensure - // middle click to paste selection works. This only appears to work in Chrome - // at the time is writing. - if (term.browser.isLinux) { - // Use auxclick event over mousedown the latter doesn't seem to work. Note - // that the regular click event doesn't fire for the middle mouse button. - on(this.element, 'auxclick', event => { - if (event.button === 1) { - moveTextAreaUnderMouseCursor(event, this.textarea, this.selectionManager); - } - }); - } -}; + // stream + private readable: boolean = true; + private writable: boolean = true; -/** - * Apply key handling to the terminal - */ -Terminal.bindKeys = function(term) { - on(term.element, 'keydown', function(ev) { - if (document.activeElement != this) { - return; - } - term.keyDown(ev); - }, true); + private defAttr: number = (0 << 18) | (257 << 9) | (256 << 0); + private curAttr: number = (0 << 18) | (257 << 9) | (256 << 0); - on(term.element, 'keypress', function(ev) { - if (document.activeElement != this) { - return; - } - term.keyPress(ev); - }, true); + private params: (string | number)[] = []; + private currentParam: string | number = 0; + private prefix: string = ''; + private postfix: string = ''; - on(term.element, 'keyup', function(ev) { - if (!wasMondifierKeyOnlyEvent(ev)) { - term.focus(term); - } - }, true); + // user input states + public writeBuffer: string[] = []; + private writeInProgress: boolean = false; - on(term.textarea, 'keydown', function(ev) { - term.keyDown(ev); - }, true); + /** + * Whether _xterm.js_ sent XOFF in order to catch up with the pty process. + * This is a distinct state from writeStopped so that if the user requested + * XOFF via ^S that it will not automatically resume when the writeBuffer goes + * below threshold. + */ + private xoffSentToCatchUp: boolean = false; - on(term.textarea, 'keypress', function(ev) { - term.keyPress(ev); - // Truncate the textarea's value, since it is not needed - this.value = ''; - }, true); + /** Whether writing has been stopped as a result of XOFF */ + private writeStopped: boolean = false; - on(term.textarea, 'compositionstart', term.compositionHelper.compositionstart.bind(term.compositionHelper)); - on(term.textarea, 'compositionupdate', term.compositionHelper.compositionupdate.bind(term.compositionHelper)); - on(term.textarea, 'compositionend', term.compositionHelper.compositionend.bind(term.compositionHelper)); - term.on('refresh', term.compositionHelper.updateCompositionElements.bind(term.compositionHelper)); - term.on('refresh', function (data) { - term.queueLinkification(data.start, data.end) - }); -}; + // leftover surrogate high from previous write invocation + private surrogate_high: string = ''; + // Store if user went browsing history in scrollback + private userScrolling: boolean = false; + + private inputHandler: InputHandler; + private parser: Parser; + private renderer: Renderer; + private selectionManager: SelectionManager;; + private linkifier: Linkifier; + private buffers: BufferSet; + private buffer: Buffer; + private viewport: Viewport; + private compositionHelper: CompositionHelper; + private charMeasure: CharMeasure; + + public cols: number; + public rows: number; + public geometry: [/*cols*/number, /*rows*/number]; -/** - * Insert the given row to the terminal or produce a new one - * if no row argument is passed. Return the inserted row. - * @param {HTMLElement} row (optional) The row to append to the terminal. - */ -Terminal.prototype.insertRow = function (row) { - if (typeof row != 'object') { - row = document.createElement('div'); - } + /** + * Creates a new `Terminal` object. + * + * @param {object} options An object containing a set of options, the available options are: + * - `cursorBlink` (boolean): Whether the terminal cursor blinks + * - `cols` (number): The number of columns of the terminal (horizontal size) + * - `rows` (number): The number of rows of the terminal (vertical size) + * + * @public + * @class Xterm Xterm + * @alias module:xterm/src/xterm + */ + constructor( + options: any = {} + ) { + super(); + + let self: any = this; + + // TODO: Can this be removed? + if (!(this instanceof Terminal)) { + return new Terminal({ + cols: arguments[0], + rows: arguments[1], + handler: arguments[2] + }); + } - this.rowContainer.appendChild(row); - this.children.push(row); + Object.keys(DEFAULT_OPTIONS).forEach(function(key) { + if (options[key] == null) { + if (Terminal[key] !== DEFAULT_OPTIONS[key]) { + options[key] = Terminal[key]; + } + } + self[key] = options[key]; + }); - return row; -}; + if (options.colors.length === 8) { + options.colors = options.colors.concat(_colors.slice(8)); + } else if (options.colors.length === 16) { + options.colors = options.colors.concat(_colors.slice(16)); + } else if (options.colors.length === 10) { + options.colors = options.colors.slice(0, -2).concat( + _colors.slice(8, -2), options.colors.slice(-2)); + } else if (options.colors.length === 18) { + options.colors = options.colors.concat( + _colors.slice(16, -2), options.colors.slice(-2)); + } + this.colors = options.colors; -/** - * Opens the terminal within an element. - * - * @param {HTMLElement} parent The element to create the terminal within. - * @param {boolean} focus Focus the terminal, after it gets instantiated in the DOM - */ -Terminal.prototype.open = function(parent, focus) { - var self=this, i=0, div; - - this.parent = parent || 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.getElementsByTagName('body')[0]; - - //Create main element container - this.element = this.document.createElement('div'); - this.element.classList.add('terminal'); - this.element.classList.add('xterm'); - this.element.classList.add('xterm-theme-' + this.theme); - this.element.classList.add(`xterm-cursor-style-${this.options.cursorStyle}`); - this.setCursorBlinking(this.options.cursorBlink); - - this.element.setAttribute('tabindex', 0); - - this.viewportElement = document.createElement('div'); - this.viewportElement.classList.add('xterm-viewport'); - this.element.appendChild(this.viewportElement); - this.viewportScrollArea = document.createElement('div'); - this.viewportScrollArea.classList.add('xterm-scroll-area'); - this.viewportElement.appendChild(this.viewportScrollArea); - - // Create the selection container. - this.selectionContainer = document.createElement('div'); - this.selectionContainer.classList.add('xterm-selection'); - this.element.appendChild(this.selectionContainer); - - // Create the container that will hold the lines of the terminal and then - // produce the lines the lines. - this.rowContainer = document.createElement('div'); - this.rowContainer.classList.add('xterm-rows'); - this.element.appendChild(this.rowContainer); - this.children = []; - this.linkifier.attachToDom(document, this.children); - - // 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'); - // TODO: This should probably be inserted once it's filled to prevent an additional layout - this.element.appendChild(this.helperContainer); - this.textarea = document.createElement('textarea'); - this.textarea.classList.add('xterm-helper-textarea'); - this.textarea.setAttribute('autocorrect', 'off'); - this.textarea.setAttribute('autocapitalize', 'off'); - this.textarea.setAttribute('spellcheck', 'false'); - this.textarea.tabIndex = 0; - this.textarea.addEventListener('focus', function() { - self.emit('focus', {terminal: self}); - }); - this.textarea.addEventListener('blur', function() { - self.emit('blur', {terminal: self}); - }); - this.helperContainer.appendChild(this.textarea); + // this.context = options.context || window; + // this.document = options.document || document; + // TODO: WHy not document.body? + this.parent = document ? document.getElementsByTagName('body')[0] : null; - 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.cols = options.cols || options.geometry[0]; + this.rows = options.rows || options.geometry[1]; + this.geometry = [this.cols, this.rows]; - this.charSizeStyleElement = document.createElement('style'); - this.helperContainer.appendChild(this.charSizeStyleElement); + if (options.handler) { + this.on('data', options.handler); + } - for (; i < this.rows; i++) { - this.insertRow(); - } - this.parent.appendChild(this.element); + this.inputHandler = new InputHandler(this); + this.parser = new Parser(this.inputHandler, this); + // Reuse renderer if the Terminal is being recreated via a Terminal.reset call. + this.renderer = this.renderer || null; + this.selectionManager = this.selectionManager || null; + this.linkifier = this.linkifier || new Linkifier(); + + // Create the terminal's buffers and set the current buffer + this.buffers = new BufferSet(this); + this.buffer = this.buffers.active; // Convenience shortcut; + this.buffers.on('activate', function (buffer) { + this._terminal.buffer = buffer; + }); - this.charMeasure = new CharMeasure(document, this.helperContainer); - this.charMeasure.on('charsizechanged', function () { - self.updateCharSizeStyles(); - }); - this.charMeasure.measure(); - - this.viewport = new Viewport(this, this.viewportElement, this.viewportScrollArea, this.charMeasure); - this.renderer = new Renderer(this); - this.selectionManager = new SelectionManager( - this, this.buffer.lines, this.rowContainer, this.charMeasure - ); - this.selectionManager.on('refresh', data => { - this.renderer.refreshSelection(data.start, data.end); - }); - this.selectionManager.on('newselection', text => { - // If there's a new selection, put it into the textarea, focus and select it - // in order to register it as a selection on the OS. This event is fired - // only on Linux to enable middle click to paste selection. - this.textarea.value = text; - this.textarea.focus(); - this.textarea.select(); - }); - this.on('scroll', () => this.selectionManager.refresh()); - this.viewportElement.addEventListener('scroll', () => this.selectionManager.refresh()); + let i = this.rows; - // Setup loop that draws to screen - this.refresh(0, this.rows - 1); + while (i--) { + this.buffer.lines.push(this.blankLine()); + } + // Ensure the selection manager has the correct buffer + if (this.selectionManager) { + this.selectionManager.setBuffer(this.buffer.lines); + } - // Initialize global actions that - // need to be taken on the document. - this.initGlobal(); + this.setupStops(); + } /** - * Automatic focus functionality. - * TODO: Default to `false` starting with xterm.js 3.0. + * back_color_erase feature for xterm. */ - if (typeof focus == 'undefined') { - let message = 'You did not pass the `focus` argument in `Terminal.prototype.open()`.\n'; - - message += 'The `focus` argument now defaults to `true` but starting with xterm.js 3.0 '; - message += 'it will default to `false`.'; - - console.warn(message); - focus = true; + public eraseAttr(): any { + // if (this.is('screen')) return this.defAttr; + return (this.defAttr & ~0x1ff) | (this.curAttr & 0x1ff); } - if (focus) { - this.focus(); + /** + * Focus the terminal. Delegates focus handling to the terminal's DOM element. + */ + public focus(): void { + this.textarea.focus(); } - // Listen for mouse events and translate - // them into terminal mouse protocols. - this.bindMouse(); - /** - * This event is emitted when terminal has completed opening. - * - * @event open + * Retrieves an option's value from the terminal. + * @param {string} key The option key. */ - this.emit('open'); -}; + public getOption(key: string): any { + if (!(key in DEFAULT_OPTIONS)) { + throw new Error('No option with key "' + key + '"'); + } + if (typeof this.options[key] !== 'undefined') { + return this.options[key]; + } -/** - * Attempts to load an add-on using CommonJS or RequireJS (whichever is available). - * @param {string} addon The name of the addon to load - * @static - */ -Terminal.loadAddon = function(addon, callback) { - if (typeof exports === 'object' && typeof module === 'object') { - // CommonJS - return require('./addons/' + addon + '/' + addon); - } else if (typeof define == 'function') { - // RequireJS - return require(['./addons/' + addon + '/' + addon], callback); - } else { - console.error('Cannot load a module without a CommonJS or RequireJS environment.'); - return false; + return this[key]; } -}; - -/** - * Updates the helper CSS class with any changes necessary after the terminal's - * character width has been changed. - */ -Terminal.prototype.updateCharSizeStyles = function() { - this.charSizeStyleElement.textContent = - `.xterm-wide-char{width:${this.charMeasure.width * 2}px;}` + - `.xterm-normal-char{width:${this.charMeasure.width}px;}` + - `.xterm-rows > div{height:${this.charMeasure.height}px;}`; -} - -/** - * XTerm mouse events - * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking - * To better understand these - * the xterm code is very helpful: - * Relevant files: - * button.c, charproc.c, misc.c - * Relevant functions in xterm/button.c: - * BtnCode, EmitButtonCode, EditorButton, SendMousePosition - */ -Terminal.prototype.bindMouse = function() { - var el = this.element, self = this, pressed = 32; - // mouseup, mousedown, wheel - // left click: ^[[M 3<^[[M#3< - // wheel up: ^[[M`3> - function sendButton(ev) { - var button - , pos; - - // get the xterm-style button - button = getButton(ev); + /** + * Sets an option on the terminal. + * @param {string} key The option key. + * @param {any} value The option value. + */ + public setOption(key: string, value: any) { + // TODO: Give value a better type (boolean | string, ...) + if (!(key in DEFAULT_OPTIONS)) { + throw new Error('No option with key "' + key + '"'); + } + switch (key) { + case 'scrollback': + if (value < this.rows) { + let msg = 'Setting the scrollback value less than the number of rows '; - // get mouse coordinates - pos = getRawByteCoords(ev, self.rowContainer, self.charMeasure, self.cols, self.rows); - if (!pos) return; + msg += `(${this.rows}) is not allowed.`; - sendEvent(button, pos); + console.warn(msg); + return false; + } - switch (ev.overrideType || ev.type) { - case 'mousedown': - pressed = button; - break; - case 'mouseup': - // keep it at the left - // button, just in case. - pressed = 32; + if (this.options[key] !== value) { + if (this.buffer.lines.length > value) { + const amountToTrim = this.buffer.lines.length - value; + const needsRefresh = (this.buffer.ydisp - amountToTrim < 0); + this.buffer.lines.trimStart(amountToTrim); + this.buffer.ybase = Math.max(this.buffer.ybase - amountToTrim, 0); + this.buffer.ydisp = Math.max(this.buffer.ydisp - amountToTrim, 0); + if (needsRefresh) { + this.refresh(0, this.rows - 1); + } + } + this.buffer.lines.maxLength = value; + this.viewport.syncScrollArea(); + } break; - case 'wheel': - // nothing. don't - // interfere with - // `pressed`. + } + this[key] = value; + this.options[key] = value; + switch (key) { + case 'cursorBlink': this.setCursorBlinking(value); break; + case 'cursorStyle': + this.element.classList.toggle(`xterm-cursor-style-block`, value === 'block'); + this.element.classList.toggle(`xterm-cursor-style-underline`, value === 'underline'); + this.element.classList.toggle(`xterm-cursor-style-bar`, value === 'bar'); break; + case 'tabStopWidth': this.setupStops(); break; } } - // motion example of a left click: - // ^[[M 3<^[[M@4<^[[M@5<^[[M@6<^[[M@7<^[[M#7< - function sendMove(ev) { - var button = pressed - , pos; + private restartCursorBlinking() { + this.setCursorBlinking(this.options.cursorBlink); + } + + private setCursorBlinking(enabled) { + this.element.classList.toggle('xterm-cursor-blink', enabled); + this.clearCursorBlinkingInterval(); + if (enabled) { + this.cursorBlinkInterval = setInterval(() => { + this.element.classList.toggle('xterm-cursor-blink-on'); + }, CURSOR_BLINK_INTERVAL); + } + } - pos = getRawByteCoords(ev, self.rowContainer, self.charMeasure, self.cols, self.rows); - if (!pos) return; + private clearCursorBlinkingInterval() { + this.element.classList.remove('xterm-cursor-blink-on'); + if (this.cursorBlinkInterval) { + clearInterval(this.cursorBlinkInterval); + this.cursorBlinkInterval = null; + } + } - // buttons marked as motions - // are incremented by 32 - button += 32; + /** + * Binds the desired focus behavior on a given terminal object. + */ + private bindFocus() { + globalOn(this.textarea, 'focus', (ev) => { + if (this.sendFocus) { + this.send(C0.ESC + '[I'); + } + this.element.classList.add('focus'); + this.showCursor(); + this.restartCursorBlinking.apply(this); + // TODO: Why pass terminal here? + this.emit('focus', {terminal: this}); + }); + }; - sendEvent(button, pos); + /** + * Blur the terminal. Delegates blur handling to the terminal's DOM element. + */ + private blur() { + return this.textarea.blur(); } - // encode button and - // position to characters - function encode(data, ch) { - if (!self.utfMouse) { - if (ch === 255) return data.push(0); - if (ch > 127) ch = 127; - data.push(ch); - } else { - if (ch === 2047) return data.push(0); - if (ch < 127) { - data.push(ch); - } else { - if (ch > 2047) ch = 2047; - data.push(0xC0 | (ch >> 6)); - data.push(0x80 | (ch & 0x3F)); + /** + * Binds the desired blur behavior on a given terminal object. + */ + private bindBlur() { + on(this.textarea, 'blur', (ev) => { + this.refresh(this.buffer.y, this.buffer.y); + if (this.sendFocus) { + this.send(C0.ESC + '[O'); } - } + this.element.classList.remove('focus'); + this.clearCursorBlinkingInterval.apply(this); + // TODO: Why pass terminal here? + this.emit('blur', {terminal: this}); + }); } - // send a mouse event: - // regular/utf8: ^[[M Cb Cx Cy - // urxvt: ^[[ Cb ; Cx ; Cy M - // sgr: ^[[ Cb ; Cx ; Cy M/m - // vt300: ^[[ 24(1/3/5)~ [ Cx , Cy ] \r - // locator: CSI P e ; P b ; P r ; P c ; P p & w - function sendEvent(button, pos) { - // self.emit('mouse', { - // x: pos.x - 32, - // y: pos.x - 32, - // button: button - // }); - - if (self.vt300Mouse) { - // NOTE: Unstable. - // http://www.vt100.net/docs/vt3xx-gp/chapter15.html - button &= 3; - pos.x -= 32; - pos.y -= 32; - var data = C0.ESC + '[24'; - if (button === 0) data += '1'; - else if (button === 1) data += '3'; - else if (button === 2) data += '5'; - else if (button === 3) return; - else data += '0'; - data += '~[' + pos.x + ',' + pos.y + ']\r'; - self.send(data); - return; + /** + * Initialize default behavior + */ + private initGlobal() { + this.bindKeys(); + this.bindFocus(); + this.bindBlur(); + + // Bind clipboard functionality + on(this.element, 'copy', (event: ClipboardEvent) => { + // If mouse events are active it means the selection manager is disabled and + // copy should be handled by the host program. + if (!this.hasSelection()) { + return; + } + copyHandler(event, this, this.selectionManager); + }); + const pasteHandlerWrapper = event => pasteHandler(event, this); + on(this.textarea, 'paste', pasteHandlerWrapper); + on(this.element, 'paste', pasteHandlerWrapper); + + // Handle right click context menus + if (Browser.isFirefox) { + // Firefox doesn't appear to fire the contextmenu event on right click + on(this.element, 'mousedown', (event: MouseEvent) => { + if (event.button === 2) { + rightClickHandler(event, this.textarea, this.selectionManager); + } + }); + } else { + on(this.element, 'contextmenu', (event: MouseEvent) => { + rightClickHandler(event, this.textarea, this.selectionManager); + }); } - if (self.decLocator) { - // NOTE: Unstable. - button &= 3; - pos.x -= 32; - pos.y -= 32; - if (button === 0) button = 2; - else if (button === 1) button = 4; - else if (button === 2) button = 6; - else if (button === 3) button = 3; - self.send(C0.ESC + '[' - + button - + ';' - + (button === 3 ? 4 : 0) - + ';' - + pos.y - + ';' - + pos.x - + ';' - + (pos.page || 0) - + '&w'); - return; + // Move the textarea under the cursor when middle clicking on Linux to ensure + // middle click to paste selection works. This only appears to work in Chrome + // at the time is writing. + if (Browser.isLinux) { + // Use auxclick event over mousedown the latter doesn't seem to work. Note + // that the regular click event doesn't fire for the middle mouse button. + on(this.element, 'auxclick', (event: MouseEvent) => { + if (event.button === 1) { + moveTextAreaUnderMouseCursor(event, this.textarea); + } + }); } + } - if (self.urxvtMouse) { - pos.x -= 32; - pos.y -= 32; - pos.x++; - pos.y++; - self.send(C0.ESC + '[' + button + ';' + pos.x + ';' + pos.y + 'M'); - return; - } + /** + * Apply key handling to the terminal + */ + private bindKeys() { + const self = this; + on(this.element, 'keydown', function (ev) { + if (document.activeElement !== this) { + return; + } + self.keyDown(ev); + }, true); - if (self.sgrMouse) { - pos.x -= 32; - pos.y -= 32; - self.send(C0.ESC + '[<' - + (((button & 3) === 3 ? button & ~3 : button) - 32) - + ';' - + pos.x - + ';' - + pos.y - + ((button & 3) === 3 ? 'm' : 'M')); - return; - } + on(this.element, 'keypress', function (ev) { + if (document.activeElement !== this) { + return; + } + self.keyPress(ev); + }, true); + + on(this.element, 'keyup', function(ev) { + if (!wasMondifierKeyOnlyEvent(ev)) { + this.focus(this); + } + }, true); - var data = []; + on(this.textarea, 'keydown', function(ev) { + this.keyDown(ev); + }, true); - encode(data, button); - encode(data, pos.x); - encode(data, pos.y); + on(this.textarea, 'keypress', function(ev) { + this.keyPress(ev); + // Truncate the textarea's value, since it is not needed + this.value = ''; + }, true); - self.send(C0.ESC + '[M' + String.fromCharCode.apply(String, data)); + on(this.textarea, 'compositionstart', this.compositionHelper.compositionstart.bind(this.compositionHelper)); + on(this.textarea, 'compositionupdate', this.compositionHelper.compositionupdate.bind(this.compositionHelper)); + on(this.textarea, 'compositionend', this.compositionHelper.compositionend.bind(this.compositionHelper)); + this.on('refresh', this.compositionHelper.updateCompositionElements.bind(this.compositionHelper)); + this.on('refresh', (data) => this.queueLinkification(data.start, data.end)); } - function getButton(ev) { - var button - , shift - , meta - , ctrl - , mod; + /** + * Insert the given row to the terminal or produce a new one + * if no row argument is passed. Return the inserted row. + * @param {HTMLElement} row (optional) The row to append to the terminal. + */ + private insertRow(row?: HTMLElement) { + if (typeof row !== 'object') { + row = document.createElement('div'); + } - // two low bits: - // 0 = left - // 1 = middle - // 2 = right - // 3 = release - // wheel up/down: - // 1, and 2 - with 64 added - switch (ev.overrideType || ev.type) { - case 'mousedown': - button = ev.button != null - ? +ev.button - : ev.which != null - ? ev.which - 1 - : null; + this.rowContainer.appendChild(row); + this.children.push(row); - if (self.browser.isMSIE) { - button = button === 1 ? 0 : button === 4 ? 1 : button; - } - break; - case 'mouseup': - button = 3; - break; - case 'DOMMouseScroll': - button = ev.detail < 0 - ? 64 - : 65; - break; - case 'wheel': - button = ev.wheelDeltaY > 0 - ? 64 - : 65; - break; + return row; + }; + + /** + * Opens the terminal within an element. + * + * @param {HTMLElement} parent The element to create the terminal within. + * @param {boolean} focus Focus the terminal, after it gets instantiated in the DOM + */ + private open(parent, focus) { + let i = 0; + let div; + + this.parent = parent || this.parent; + + if (!this.parent) { + throw new Error('Terminal requires a parent element.'); } - // next three bits are the modifiers: - // 4 = shift, 8 = meta, 16 = control - shift = ev.shiftKey ? 4 : 0; - meta = ev.metaKey ? 8 : 0; - ctrl = ev.ctrlKey ? 16 : 0; - mod = shift | meta | ctrl; + // Grab global elements + this.context = this.parent.ownerDocument.defaultView; + this.document = this.parent.ownerDocument; + this.body = this.document.getElementsByTagName('body')[0]; + + // Create main element container + this.element = this.document.createElement('div'); + this.element.classList.add('terminal'); + this.element.classList.add('xterm'); + this.element.classList.add(`xterm-cursor-style-${this.options.cursorStyle}`); + this.setCursorBlinking(this.options.cursorBlink); + + this.element.setAttribute('tabindex', '0'); + + this.viewportElement = document.createElement('div'); + this.viewportElement.classList.add('xterm-viewport'); + this.element.appendChild(this.viewportElement); + this.viewportScrollArea = document.createElement('div'); + this.viewportScrollArea.classList.add('xterm-scroll-area'); + this.viewportElement.appendChild(this.viewportScrollArea); + + // Create the selection container. + this.selectionContainer = document.createElement('div'); + this.selectionContainer.classList.add('xterm-selection'); + this.element.appendChild(this.selectionContainer); + + // Create the container that will hold the lines of the terminal and then + // produce the lines the lines. + this.rowContainer = document.createElement('div'); + this.rowContainer.classList.add('xterm-rows'); + this.element.appendChild(this.rowContainer); + this.children = []; + this.linkifier.attachToDom(document, this.children); + + // 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'); + // TODO: This should probably be inserted once it's filled to prevent an additional layout + this.element.appendChild(this.helperContainer); + this.textarea = document.createElement('textarea'); + this.textarea.classList.add('xterm-helper-textarea'); + this.textarea.setAttribute('autocorrect', 'off'); + this.textarea.setAttribute('autocapitalize', 'off'); + this.textarea.setAttribute('spellcheck', 'false'); + this.textarea.tabIndex = 0; + this.textarea.addEventListener('focus', () => { + // TODO: Do we need terminal passed here? + this.emit('focus', {terminal: this}); + }); + this.textarea.addEventListener('blur', () => { + // TODO: Do we need terminal passed here? + this.emit('blur', {terminal: this}); + }); + 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); - // no mods - if (self.vt200Mouse) { - // ctrl only - mod &= ctrl; - } else if (!self.normalMouse) { - mod = 0; + this.charSizeStyleElement = document.createElement('style'); + this.helperContainer.appendChild(this.charSizeStyleElement); + + for (; i < this.rows; i++) { + this.insertRow(); } + this.parent.appendChild(this.element); + + this.charMeasure = new CharMeasure(document, this.helperContainer); + this.charMeasure.on('charsizechanged', () => { + this.updateCharSizeStyles(); + }); + this.charMeasure.measure(); - // increment to SP - button = (32 + (mod << 2)) + button; + this.viewport = new Viewport(this, this.viewportElement, this.viewportScrollArea, this.charMeasure); + this.renderer = new Renderer(this); + this.selectionManager = new SelectionManager(this, this.buffer.lines, this.rowContainer, this.charMeasure); + this.selectionManager.on('refresh', data => { + this.renderer.refreshSelection(data.start, data.end); + }); + this.selectionManager.on('newselection', text => { + // If there's a new selection, put it into the textarea, focus and select it + // in order to register it as a selection on the OS. This event is fired + // only on Linux to enable middle click to paste selection. + this.textarea.value = text; + this.textarea.focus(); + this.textarea.select(); + }); + this.on('scroll', () => this.selectionManager.refresh()); + this.viewportElement.addEventListener('scroll', () => this.selectionManager.refresh()); + + // Setup loop that draws to screen + this.refresh(0, this.rows - 1); - return button; + // Initialize global actions that + // need to be taken on the document. + this.initGlobal(); + + /** + * Automatic focus functionality. + * TODO: Default to `false` starting with xterm.js 3.0. + */ + if (typeof focus == 'undefined') { + let message = 'You did not pass the `focus` argument in `Terminal.prototype.open()`.\n'; + + message += 'The `focus` argument now defaults to `true` but starting with xterm.js 3.0 '; + message += 'it will default to `false`.'; + + console.warn(message); + focus = true; + } + + if (focus) { + this.focus(); + } + + // Listen for mouse events and translate + // them into terminal mouse protocols. + this.bindMouse(); + + /** + * This event is emitted when terminal has completed opening. + * + * @event open + */ + this.emit('open'); } - on(el, 'mousedown', function(ev) { + /** + * Attempts to load an add-on using CommonJS or RequireJS (whichever is available). + * @param {string} addon The name of the addon to load + * @static + */ + public loadAddon(addon, callback) { + if (typeof exports === 'object' && typeof module === 'object') { + // CommonJS + return require('./addons/' + addon + '/' + addon); + } else if (typeof define === 'function') { + // RequireJS + return (require)(['./addons/' + addon + '/' + addon], callback); + } else { + console.error('Cannot load a module without a CommonJS or RequireJS environment.'); + return false; + } + } - // Prevent the focus on the textarea from getting lost - // and make sure we get focused on mousedown - ev.preventDefault(); - self.focus(); + /** + * Updates the helper CSS class with any changes necessary after the terminal's + * character width has been changed. + */ + public updateCharSizeStyles() { + this.charSizeStyleElement.textContent = + `.xterm-wide-char{width:${this.charMeasure.width * 2}px;}` + + `.xterm-normal-char{width:${this.charMeasure.width}px;}` + + `.xterm-rows > div{height:${this.charMeasure.height}px;}`; + } - if (!self.mouseEvents) return; + /** + * XTerm mouse events + * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking + * To better understand these + * the xterm code is very helpful: + * Relevant files: + * button.c, charproc.c, misc.c + * Relevant functions in xterm/button.c: + * BtnCode, EmitButtonCode, EditorButton, SendMousePosition + */ + public bindMouse() { + var el = this.element, self = this, pressed = 32; + + // mouseup, mousedown, wheel + // left click: ^[[M 3<^[[M#3< + // wheel up: ^[[M`3> + function sendButton(ev) { + var button + , pos; + + // get the xterm-style button + button = getButton(ev); + + // get mouse coordinates + pos = getRawByteCoords(ev, self.rowContainer, self.charMeasure, self.cols, self.rows); + if (!pos) return; + + sendEvent(button, pos); + + switch (ev.overrideType || ev.type) { + case 'mousedown': + pressed = button; + break; + case 'mouseup': + // keep it at the left + // button, just in case. + pressed = 32; + break; + case 'wheel': + // nothing. don't + // interfere with + // `pressed`. + break; + } + } - // send the button - sendButton(ev); + // motion example of a left click: + // ^[[M 3<^[[M@4<^[[M@5<^[[M@6<^[[M@7<^[[M#7< + function sendMove(ev) { + let button = pressed + , pos; - // fix for odd bug - //if (self.vt200Mouse && !self.normalMouse) { - if (self.vt200Mouse) { - ev.overrideType = 'mouseup'; - sendButton(ev); - return self.cancel(ev); + pos = getRawByteCoords(ev, self.rowContainer, self.charMeasure, self.cols, self.rows); + if (!pos) return; + + // buttons marked as motions + // are incremented by 32 + button += 32; + + sendEvent(button, pos); } - // bind events - if (self.normalMouse) on(self.document, 'mousemove', sendMove); + // encode button and + // position to characters + function encode(data, ch) { + if (!self.utfMouse) { + if (ch === 255) return data.push(0); + if (ch > 127) ch = 127; + data.push(ch); + } else { + if (ch === 2047) return data.push(0); + if (ch < 127) { + data.push(ch); + } else { + if (ch > 2047) ch = 2047; + data.push(0xC0 | (ch >> 6)); + data.push(0x80 | (ch & 0x3F)); + } + } + } - // x10 compatibility mode can't send button releases - if (!self.x10Mouse) { - on(self.document, 'mouseup', function up(ev) { - sendButton(ev); - if (self.normalMouse) off(self.document, 'mousemove', sendMove); - off(self.document, 'mouseup', up); - return self.cancel(ev); - }); + // send a mouse event: + // regular/utf8: ^[[M Cb Cx Cy + // urxvt: ^[[ Cb ; Cx ; Cy M + // sgr: ^[[ Cb ; Cx ; Cy M/m + // vt300: ^[[ 24(1/3/5)~ [ Cx , Cy ] \r + // locator: CSI P e ; P b ; P r ; P c ; P p & w + function sendEvent(button, pos) { + // self.emit('mouse', { + // x: pos.x - 32, + // y: pos.x - 32, + // button: button + // }); + + if (self.vt300Mouse) { + // NOTE: Unstable. + // http://www.vt100.net/docs/vt3xx-gp/chapter15.html + button &= 3; + pos.x -= 32; + pos.y -= 32; + let data = C0.ESC + '[24'; + if (button === 0) data += '1'; + else if (button === 1) data += '3'; + else if (button === 2) data += '5'; + else if (button === 3) return; + else data += '0'; + data += '~[' + pos.x + ',' + pos.y + ']\r'; + self.send(data); + return; + } + + if (self.decLocator) { + // NOTE: Unstable. + button &= 3; + pos.x -= 32; + pos.y -= 32; + if (button === 0) button = 2; + else if (button === 1) button = 4; + else if (button === 2) button = 6; + else if (button === 3) button = 3; + self.send(C0.ESC + '[' + + button + + ';' + + (button === 3 ? 4 : 0) + + ';' + + pos.y + + ';' + + pos.x + + ';' + + (pos.page || 0) + + '&w'); + return; + } + + if (self.urxvtMouse) { + pos.x -= 32; + pos.y -= 32; + pos.x++; + pos.y++; + self.send(C0.ESC + '[' + button + ';' + pos.x + ';' + pos.y + 'M'); + return; + } + + if (self.sgrMouse) { + pos.x -= 32; + pos.y -= 32; + self.send(C0.ESC + '[<' + + (((button & 3) === 3 ? button & ~3 : button) - 32) + + ';' + + pos.x + + ';' + + pos.y + + ((button & 3) === 3 ? 'm' : 'M')); + return; + } + + let data = []; + + encode(data, button); + encode(data, pos.x); + encode(data, pos.y); + + self.send(C0.ESC + '[M' + String.fromCharCode.apply(String, data)); } - return self.cancel(ev); - }); + function getButton(ev) { + let button + , shift + , meta + , ctrl + , mod; + + // two low bits: + // 0 = left + // 1 = middle + // 2 = right + // 3 = release + // wheel up/down: + // 1, and 2 - with 64 added + switch (ev.overrideType || ev.type) { + case 'mousedown': + button = ev.button != null + ? +ev.button + : ev.which != null + ? ev.which - 1 + : null; + + if (Browser.isMSIE) { + button = button === 1 ? 0 : button === 4 ? 1 : button; + } + break; + case 'mouseup': + button = 3; + break; + case 'DOMMouseScroll': + button = ev.detail < 0 + ? 64 + : 65; + break; + case 'wheel': + button = ev.wheelDeltaY > 0 + ? 64 + : 65; + break; + } - //if (self.normalMouse) { - // on(self.document, 'mousemove', sendMove); - //} - - on(el, 'wheel', function(ev) { - if (!self.mouseEvents) return; - if (self.x10Mouse - || self.vt300Mouse - || self.decLocator) return; - sendButton(ev); - return self.cancel(ev); - }); + // next three bits are the modifiers: + // 4 = shift, 8 = meta, 16 = control + shift = ev.shiftKey ? 4 : 0; + meta = ev.metaKey ? 8 : 0; + ctrl = ev.ctrlKey ? 16 : 0; + mod = shift | meta | ctrl; + + // no mods + if (self.vt200Mouse) { + // ctrl only + mod &= ctrl; + } else if (!self.normalMouse) { + mod = 0; + } - // allow wheel scrolling in - // the shell for example - on(el, 'wheel', function(ev) { - if (self.mouseEvents) return; - self.viewport.onWheel(ev); - return self.cancel(ev); - }); + // increment to SP + button = (32 + (mod << 2)) + button; - on(el, 'touchstart', function(ev) { - if (self.mouseEvents) return; - self.viewport.onTouchStart(ev); - return self.cancel(ev); - }); + return button; + } - on(el, 'touchmove', function(ev) { - if (self.mouseEvents) return; - self.viewport.onTouchMove(ev); - return self.cancel(ev); - }); -}; + on(el, 'mousedown', (ev) => { -/** - * Destroys the terminal. - */ -Terminal.prototype.destroy = function() { - this.readable = false; - this.writable = false; - this._events = {}; - this.handler = function() {}; - this.write = function() {}; - if (this.element && this.element.parentNode) { - this.element.parentNode.removeChild(this.element); - } - //this.emit('close'); -}; + // Prevent the focus on the textarea from getting lost + // and make sure we get focused on mousedown + ev.preventDefault(); + this.focus(); -/** - * Tells the renderer to refresh terminal content between two rows (inclusive) at the next - * opportunity. - * @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). - */ -Terminal.prototype.refresh = function(start, end) { - if (this.renderer) { - this.renderer.queueRefresh(start, end); + if (!this.mouseEvents) return; + + // send the button + sendButton(ev); + + // fix for odd bug + //if (this.vt200Mouse && !this.normalMouse) { + if (this.vt200Mouse) { + (ev).overrideType = 'mouseup'; + sendButton(ev); + return this.cancel(ev); + } + + // bind events + if (this.normalMouse) on(this.document, 'mousemove', sendMove); + + // x10 compatibility mode can't send button releases + if (!this.x10Mouse) { + on(this.document, 'mouseup', (ev: MouseEvent) => { + sendButton(ev); + if (this.normalMouse) off(this.document, 'mousemove', sendMove); + off(this.document, 'mouseup', up); + return this.cancel(ev); + }); + } + + return this.cancel(ev); + }); + + //if (this.normalMouse) { + // on(this.document, 'mousemove', sendMove); + //} + + on(el, 'wheel', (ev) => { + if (!this.mouseEvents) return; + if (this.x10Mouse || this.vt300Mouse || this.decLocator) return; + sendButton(ev); + return this.cancel(ev); + }); + + // allow wheel scrolling in + // the shell for example + on(el, 'wheel', (ev: WheelEvent) => { + if (this.mouseEvents) return; + this.viewport.onWheel(ev); + return this.cancel(ev); + }); + + on(el, 'touchstart', (ev: TouchEvent) => { + if (this.mouseEvents) return; + this.viewport.onTouchStart(ev); + return this.cancel(ev); + }); + + on(el, 'touchmove', (ev: TouchEvent) => { + if (this.mouseEvents) return; + this.viewport.onTouchMove(ev); + return this.cancel(ev); + }); } -}; -/** - * Queues linkification for the specified rows. - * @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). - */ -Terminal.prototype.queueLinkification = function(start, end) { - if (this.linkifier) { - for (let i = start; i <= end; i++) { - this.linkifier.linkifyRow(i); + /** + * Destroys the terminal. + */ + public destroy(): void { + super.destroy(); + this.readable = false; + this.writable = false; + this.handler = function() {}; + this.write = function() {}; + if (this.element && this.element.parentNode) { + this.element.parentNode.removeChild(this.element); } + // this.emit('close'); } -}; -/** - * Display the cursor element - */ -Terminal.prototype.showCursor = function() { - if (!this.cursorState) { - this.cursorState = 1; - this.refresh(this.buffer.y, this.buffer.y); + /** + * Tells the renderer to refresh terminal content between two rows (inclusive) at the next + * opportunity. + * @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). + */ + public refresh(start: number, end: number): void { + if (this.renderer) { + this.renderer.queueRefresh(start, end); + } } -}; -/** - * Scroll the terminal down 1 row, creating a blank line. - * @param {boolean} isWrapped Whether the new line is wrapped from the previous - * line. - */ -Terminal.prototype.scroll = function(isWrapped) { - var row; + /** + * Queues linkification for the specified rows. + * @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 { + if (this.linkifier) { + for (let i = start; i <= end; i++) { + this.linkifier.linkifyRow(i); + } + } + } - // Make room for the new row in lines - if (this.buffer.lines.length === this.buffer.lines.maxLength) { - this.buffer.lines.trimStart(1); - this.buffer.ybase--; - if (this.buffer.ydisp !== 0) { - this.buffer.ydisp--; + /** + * Display the cursor element + */ + private showCursor() { + if (!this.cursorState) { + this.cursorState = 1; + this.refresh(this.buffer.y, this.buffer.y); } } - this.buffer.ybase++; + /** + * Scroll the terminal down 1 row, creating a blank line. + * @param {boolean} isWrapped Whether the new line is wrapped from the previous + * line. + */ + public scroll(isWrapped?: boolean): void { + var row; - // TODO: Why is this done twice? - if (!this.userScrolling) { - this.buffer.ydisp = this.buffer.ybase; - } + // Make room for the new row in lines + if (this.buffer.lines.length === this.buffer.lines.maxLength) { + this.buffer.lines.trimStart(1); + this.buffer.ybase--; + if (this.buffer.ydisp !== 0) { + this.buffer.ydisp--; + } + } + + this.buffer.ybase++; - // last line - row = this.buffer.ybase + this.rows - 1; + // TODO: Why is this done twice? + if (!this.userScrolling) { + this.buffer.ydisp = this.buffer.ybase; + } - // subtract the bottom scroll region - row -= this.rows - 1 - this.buffer.scrollBottom; + // last line + row = this.buffer.ybase + this.rows - 1; - if (row === this.buffer.lines.length) { - // Optimization: pushing is faster than splicing when they amount to the same behavior - this.buffer.lines.push(this.blankLine(undefined, isWrapped)); - } else { - // add our new line - this.buffer.lines.splice(row, 0, this.blankLine(undefined, isWrapped)); - } + // subtract the bottom scroll region + row -= this.rows - 1 - this.buffer.scrollBottom; - if (this.buffer.scrollTop !== 0) { - if (this.buffer.ybase !== 0) { - this.buffer.ybase--; - if (!this.userScrolling) { - this.buffer.ydisp = this.buffer.ybase; + if (row === this.buffer.lines.length) { + // Optimization: pushing is faster than splicing when they amount to the same behavior + this.buffer.lines.push(this.blankLine(undefined, isWrapped)); + } else { + // add our new line + this.buffer.lines.splice(row, 0, this.blankLine(undefined, isWrapped)); + } + + if (this.buffer.scrollTop !== 0) { + if (this.buffer.ybase !== 0) { + this.buffer.ybase--; + if (!this.userScrolling) { + this.buffer.ydisp = this.buffer.ybase; + } } + this.buffer.lines.splice(this.buffer.ybase + this.buffer.scrollTop, 1); } - this.buffer.lines.splice(this.buffer.ybase + this.buffer.scrollTop, 1); - } - // this.maxRange(); - this.updateRange(this.buffer.scrollTop); - this.updateRange(this.buffer.scrollBottom); + // this.maxRange(); + this.updateRange(this.buffer.scrollTop); + this.updateRange(this.buffer.scrollBottom); + + /** + * This event is emitted whenever the terminal is scrolled. + * The one parameter passed is the new y display position. + * + * @event scroll + */ + this.emit('scroll', this.buffer.ydisp); + } /** - * This event is emitted whenever the terminal is scrolled. - * The one parameter passed is the new y display position. - * - * @event scroll + * Scroll the display of the terminal + * @param {number} disp The number of lines to scroll down (negatives scroll up). + * @param {boolean} suppressScrollEvent Don't emit the scroll event as scrollDisp. This is used + * to avoid unwanted events being handled by the veiwport when the event was triggered from the + * viewport originally. */ - this.emit('scroll', this.buffer.ydisp); -}; + public scrollDisp(disp: number, suppressScrollEvent?: boolean): void { + if (disp < 0) { + if (this.buffer.ydisp === 0) { + return; + } + this.userScrolling = true; + } else if (disp + this.buffer.ydisp >= this.buffer.ybase) { + this.userScrolling = false; + } -/** - * Scroll the display of the terminal - * @param {number} disp The number of lines to scroll down (negatives scroll up). - * @param {boolean} suppressScrollEvent Don't emit the scroll event as scrollDisp. This is used - * to avoid unwanted events being handled by the veiwport when the event was triggered from the - * viewport originally. - */ -Terminal.prototype.scrollDisp = function(disp, suppressScrollEvent) { - if (disp < 0) { - if (this.buffer.ydisp === 0) { + const oldYdisp = this.buffer.ydisp; + this.buffer.ydisp = Math.max(Math.min(this.buffer.ydisp + disp, this.buffer.ybase), 0); + + // No change occurred, don't trigger scroll/refresh + if (oldYdisp === this.buffer.ydisp) { return; } - this.userScrolling = true; - } else if (disp + this.buffer.ydisp >= this.buffer.ybase) { - this.userScrolling = false; + + if (!suppressScrollEvent) { + this.emit('scroll', this.buffer.ydisp); + } + + this.refresh(0, this.rows - 1); } - const oldYdisp = this.buffer.ydisp; - this.buffer.ydisp = Math.max(Math.min(this.buffer.ydisp + disp, this.buffer.ybase), 0); + /** + * Scroll the display of the terminal by a number of pages. + * @param {number} pageCount The number of pages to scroll (negative scrolls up). + */ + public scrollPages(pageCount: number): void { + this.scrollDisp(pageCount * (this.rows - 1)); + } - // No change occurred, don't trigger scroll/refresh - if (oldYdisp === this.buffer.ydisp) { - return; + /** + * Scrolls the display of the terminal to the top. + */ + public scrollToTop(): void { + this.scrollDisp(-this.buffer.ydisp); } - if (!suppressScrollEvent) { - this.emit('scroll', this.buffer.ydisp); + /** + * Scrolls the display of the terminal to the bottom. + */ + public scrollToBottom(): void { + this.scrollDisp(this.buffer.ybase - this.buffer.ydisp); } - this.refresh(0, this.rows - 1); -}; + /** + * Writes text to the terminal. + * @param {string} data The text to write to the terminal. + */ + public write(data: string): void { + this.writeBuffer.push(data); + + // 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) { + // XOFF - stop pty pipe + // XON will be triggered by emulator before processing data chunk + this.send(C0.DC3); + this.xoffSentToCatchUp = true; + } -/** - * Scroll the display of the terminal by a number of pages. - * @param {number} pageCount The number of pages to scroll (negative scrolls up). - */ -Terminal.prototype.scrollPages = function(pageCount) { - this.scrollDisp(pageCount * (this.rows - 1)); -}; + if (!this.writeInProgress && this.writeBuffer.length > 0) { + // Kick off a write which will write all data in sequence recursively + this.writeInProgress = true; + // Kick off an async innerWrite so more writes can come in while processing data + setTimeout(() => { + this.innerWrite(); + }); + } + } -/** - * Scrolls the display of the terminal to the top. - */ -Terminal.prototype.scrollToTop = function() { - this.scrollDisp(-this.buffer.ydisp); -}; + private innerWrite() { + var writeBatch = this.writeBuffer.splice(0, WRITE_BATCH_SIZE); + while (writeBatch.length > 0) { + var data = writeBatch.shift(); + var l = data.length, i = 0, j, cs, ch, code, low, ch_width, row; -/** - * Scrolls the display of the terminal to the bottom. - */ -Terminal.prototype.scrollToBottom = function() { - this.scrollDisp(this.buffer.ybase - this.buffer.ydisp); -}; + // 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) { + this.send(C0.DC1); + this.xoffSentToCatchUp = false; + } -/** - * Writes text to the terminal. - * @param {string} data The text to write to the terminal. - */ -Terminal.prototype.write = function(data) { - this.writeBuffer.push(data); - - // 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) { - // XOFF - stop pty pipe - // XON will be triggered by emulator before processing data chunk - this.send(C0.DC3); - this.xoffSentToCatchUp = true; - } - - if (!this.writeInProgress && this.writeBuffer.length > 0) { - // Kick off a write which will write all data in sequence recursively - this.writeInProgress = true; - // Kick off an async innerWrite so more writes can come in while processing data - var self = this; - setTimeout(function () { - self.innerWrite(); - }); - } -}; + this.refreshStart = this.buffer.y; + this.refreshEnd = this.buffer.y; -Terminal.prototype.innerWrite = function() { - var writeBatch = this.writeBuffer.splice(0, WRITE_BATCH_SIZE); - while (writeBatch.length > 0) { - var data = writeBatch.shift(); - var l = data.length, i = 0, j, cs, ch, code, low, ch_width, row; - - // 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) { - this.send(C0.DC1); - this.xoffSentToCatchUp = false; - } - - 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. - var state = this.parser.parse(data); - this.parser.setState(state); - - this.updateRange(this.buffer.y); - this.refresh(this.refreshStart, this.refreshEnd); - } - if (this.writeBuffer.length > 0) { - // Allow renderer to catch up before processing the next batch - var self = this; - setTimeout(function () { - self.innerWrite(); - }, 0); - } else { - this.writeInProgress = false; - } -}; + // 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. + var state = this.parser.parse(data); + this.parser.setState(state); -/** - * Writes text to the terminal, followed by a break line character (\n). - * @param {string} data The text to write to the terminal. - */ -Terminal.prototype.writeln = function(data) { - this.write(data + '\r\n'); -}; + this.updateRange(this.buffer.y); + this.refresh(this.refreshStart, this.refreshEnd); + } + if (this.writeBuffer.length > 0) { + // Allow renderer to catch up before processing the next batch + var self = this; + setTimeout(function () { + self.innerWrite(); + }, 0); + } else { + this.writeInProgress = false; + } + } -/** - * DEPRECATED: only for backward compatibility. Please use attachCustomKeyEventHandler() instead. - * @param {function} customKeydownHandler The custom KeyboardEvent handler to attach. This is a - * function that takes a KeyboardEvent, allowing consumers to stop propogation and/or prevent - * the default action. The function returns whether the event should be processed by xterm.js. - */ -Terminal.prototype.attachCustomKeydownHandler = function(customKeydownHandler) { - let message = 'attachCustomKeydownHandler() is DEPRECATED and will be removed soon. Please use attachCustomKeyEventHandler() instead.'; - console.warn(message); - this.attachCustomKeyEventHandler(customKeydownHandler); -}; + /** + * Writes text to the terminal, followed by a break line character (\n). + * @param {string} data The text to write to the terminal. + */ + public writeln(data): void { + this.write(data + '\r\n'); + } -/** - * Attaches a custom key event handler which is run before keys are processed, giving consumers of - * xterm.js ultimate control as to what keys should be processed by the terminal and what keys - * should not. - * @param {function} customKeyEventHandler The custom KeyboardEvent handler to attach. This is a - * function that takes a KeyboardEvent, allowing consumers to stop propogation and/or prevent - * the default action. The function returns whether the event should be processed by xterm.js. - */ -Terminal.prototype.attachCustomKeyEventHandler = function(customKeyEventHandler) { - this.customKeyEventHandler = customKeyEventHandler; -}; + /** + * DEPRECATED: only for backward compatibility. Please use attachCustomKeyEventHandler() instead. + * @param {function} customKeydownHandler The custom KeyboardEvent handler to attach. This is a + * function that takes a KeyboardEvent, allowing consumers to stop propogation and/or prevent + * the default action. The function returns whether the event should be processed by xterm.js. + */ + public attachCustomKeydownHandler(customKeydownHandler: CustomKeyEventHandler): void { + const message = 'attachCustomKeydownHandler() is DEPRECATED and will be removed soon. Please use attachCustomKeyEventHandler() instead.'; + console.warn(message); + this.attachCustomKeyEventHandler(customKeydownHandler); + } -/** - * 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 {LinkMatcherHandler} handler The handler callback function. - */ -Terminal.prototype.setHypertextLinkHandler = function(handler) { - if (!this.linkifier) { - throw new Error('Cannot attach a hypertext link handler before Terminal.open is called'); + /** + * Attaches a custom key event handler which is run before keys are processed, giving consumers of + * xterm.js ultimate control as to what keys should be processed by the terminal and what keys + * should not. + * @param {function} customKeyEventHandler The custom KeyboardEvent handler to attach. This is a + * function that takes a KeyboardEvent, allowing consumers to stop propogation and/or prevent + * the default action. The function returns whether the event should be processed by xterm.js. + */ + public attachCustomKeyEventHandler(customKeyEventHandler: CustomKeyEventHandler): void { + this.customKeyEventHandler = customKeyEventHandler; } - 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 {LinkMatcherValidationCallback} callback The callback to use, this can - * be cleared with null. - */ -Terminal.prototype.setHypertextValidationCallback = function(callback) { - if (!this.linkifier) { - throw new Error('Cannot attach a hypertext validation callback before Terminal.open is called'); + /** + * 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 {LinkMatcherHandler} handler The handler callback function. + */ + public setHypertextLinkHandler(handler) { + 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); } - this.linkifier.setHypertextValidationCallback(callback); - // Refresh to force links to refresh - this.refresh(0, this.rows - 1); -}; -/** - * Registers a link matcher, allowing custom link patterns to be matched and - * handled. - * @param {RegExp} regex The regular expression to search for, specifically - * this searches the textContent of the rows. You will want to use \s to match - * a space ' ' character for example. - * @param {LinkMatcherHandler} handler The callback when the link is called. - * @param {LinkMatcherOptions} [options] Options for the link matcher. - * @return {number} The ID of the new matcher, this can be used to deregister. - */ -Terminal.prototype.registerLinkMatcher = function(regex, handler, options) { - if (this.linkifier) { - var matcherId = this.linkifier.registerLinkMatcher(regex, handler, options); + /** + * 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 {LinkMatcherValidationCallback} callback The callback to use, this can + * be cleared with null. + */ + public setHypertextValidationCallback(callback) { + 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); - return matcherId; } -}; -/** - * Deregisters a link matcher if it has been registered. - * @param {number} matcherId The link matcher's ID (returned after register) - */ -Terminal.prototype.deregisterLinkMatcher = function(matcherId) { - if (this.linkifier) { - if (this.linkifier.deregisterLinkMatcher(matcherId)) { + /** + * Registers a link matcher, allowing custom link patterns to be matched and + * handled. + * @param {RegExp} regex The regular expression to search for, specifically + * this searches the textContent of the rows. You will want to use \s to match + * a space ' ' character for example. + * @param {LinkMatcherHandler} handler The callback when the link is called. + * @param {LinkMatcherOptions} [options] Options for the link matcher. + * @return {number} The ID of the new matcher, this can be used to deregister. + */ + public registerLinkMatcher(regex, handler, options) { + if (this.linkifier) { + var matcherId = this.linkifier.registerLinkMatcher(regex, handler, options); this.refresh(0, this.rows - 1); + return matcherId; } } -}; -/** - * Gets whether the terminal has an active selection. - */ -Terminal.prototype.hasSelection = function() { - return this.selectionManager ? this.selectionManager.hasSelection : false; -}; + /** + * Deregisters a link matcher if it has been registered. + * @param {number} matcherId The link matcher's ID (returned after register) + */ + public deregisterLinkMatcher(matcherId) { + if (this.linkifier) { + if (this.linkifier.deregisterLinkMatcher(matcherId)) { + this.refresh(0, this.rows - 1); + } + } + } -/** - * Gets the terminal's current selection, this is useful for implementing copy - * behavior outside of xterm.js. - */ -Terminal.prototype.getSelection = function() { - return this.selectionManager ? this.selectionManager.selectionText : ''; -}; + /** + * Gets whether the terminal has an active selection. + */ + public hasSelection() { + return this.selectionManager ? this.selectionManager.hasSelection : false; + } -/** - * Clears the current terminal selection. - */ -Terminal.prototype.clearSelection = function() { - if (this.selectionManager) { - this.selectionManager.clearSelection(); + /** + * Gets the terminal's current selection, this is useful for implementing copy + * behavior outside of xterm.js. + */ + public getSelection() { + return this.selectionManager ? this.selectionManager.selectionText : ''; } -}; -/** - * Selects all text within the terminal. - */ -Terminal.prototype.selectAll = function() { - if (this.selectionManager) { - this.selectionManager.selectAll(); + /** + * Clears the current terminal selection. + */ + public clearSelection() { + if (this.selectionManager) { + this.selectionManager.clearSelection(); + } } -}; -/** - * Handle a keydown event - * Key Resources: - * - https://developer.mozilla.org/en-US/docs/DOM/KeyboardEvent - * @param {KeyboardEvent} ev The keydown event to be handled. - */ -Terminal.prototype.keyDown = function(ev) { - if (this.customKeyEventHandler && this.customKeyEventHandler(ev) === false) { - return false; + /** + * Selects all text within the terminal. + */ + public selectAll() { + if (this.selectionManager) { + this.selectionManager.selectAll(); + } } - this.restartCursorBlinking(); + /** + * Handle a keydown event + * Key Resources: + * - https://developer.mozilla.org/en-US/docs/DOM/KeyboardEvent + * @param {KeyboardEvent} ev The keydown event to be handled. + */ + private keyDown(ev) { + if (this.customKeyEventHandler && this.customKeyEventHandler(ev) === false) { + return false; + } - if (!this.compositionHelper.keydown.bind(this.compositionHelper)(ev)) { - if (this.buffer.ybase !== this.buffer.ydisp) { - this.scrollToBottom(); + this.restartCursorBlinking(); + + if (!this.compositionHelper.keydown.bind(this.compositionHelper)(ev)) { + if (this.buffer.ybase !== this.buffer.ydisp) { + this.scrollToBottom(); + } + return false; } - return false; - } - var self = this; - var result = this.evaluateKeyEscapeSequence(ev); + 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.scrollDisp) { - this.scrollDisp(result.scrollDisp); - return this.cancel(ev, true); - } + if (result.scrollDisp) { + this.scrollDisp(result.scrollDisp); + return this.cancel(ev, true); + } - if (isThirdLevelShift(this, ev)) { - return true; - } + if (isThirdLevelShift(this, ev)) { + return true; + } - if (result.cancel) { - // The event is canceled at the end already, is this necessary? - this.cancel(ev, true); - } + if (result.cancel) { + // The event is canceled at the end already, is this necessary? + this.cancel(ev, true); + } - if (!result.key) { - return true; - } + if (!result.key) { + return true; + } - this.emit('keydown', ev); - this.emit('key', result.key, ev); - this.showCursor(); - this.handler(result.key); + this.emit('keydown', ev); + this.emit('key', result.key, ev); + this.showCursor(); + this.handler(result.key); - return this.cancel(ev, true); -}; + return this.cancel(ev, true); + } -/** - * Returns an object that determines how a KeyboardEvent should be handled. The key of the - * returned value is the new key code to pass to the PTY. - * - * Reference: http://invisible-island.net/xterm/ctlseqs/ctlseqs.html - * @param {KeyboardEvent} ev The keyboard event to be translated to key escape sequence. - */ -Terminal.prototype.evaluateKeyEscapeSequence = function(ev) { - var result = { - // Whether to cancel event propogation (NOTE: this may not be needed since the event is - // canceled at the end of keyDown - cancel: false, - // The new key even to emit - key: undefined, - // The number of characters to scroll, if this is defined it will cancel the event - scrollDisp: undefined - }; - var modifiers = ev.shiftKey << 0 | ev.altKey << 1 | ev.ctrlKey << 2 | ev.metaKey << 3; - switch (ev.keyCode) { - case 8: - // backspace - if (ev.shiftKey) { - result.key = C0.BS; // ^H + /** + * Returns an object that determines how a KeyboardEvent should be handled. The key of the + * returned value is the new key code to pass to the PTY. + * + * Reference: http://invisible-island.net/xterm/ctlseqs/ctlseqs.html + * @param {KeyboardEvent} ev The keyboard event to be translated to key escape sequence. + */ + private evaluateKeyEscapeSequence(ev) { + var result = { + // Whether to cancel event propogation (NOTE: this may not be needed since the event is + // canceled at the end of keyDown + cancel: false, + // The new key even to emit + key: undefined, + // The number of characters to scroll, if this is defined it will cancel the event + scrollDisp: undefined + }; + var modifiers = ev.shiftKey << 0 | ev.altKey << 1 | ev.ctrlKey << 2 | ev.metaKey << 3; + switch (ev.keyCode) { + case 8: + // backspace + if (ev.shiftKey) { + result.key = C0.BS; // ^H + break; + } + result.key = C0.DEL; // ^? break; - } - result.key = C0.DEL; // ^? - break; - case 9: - // tab - if (ev.shiftKey) { - result.key = C0.ESC + '[Z'; + case 9: + // tab + if (ev.shiftKey) { + result.key = C0.ESC + '[Z'; + break; + } + result.key = C0.HT; + result.cancel = true; break; - } - result.key = C0.HT; - result.cancel = true; - break; - case 13: - // return/enter - result.key = C0.CR; - result.cancel = true; - break; - case 27: - // escape - result.key = C0.ESC; - result.cancel = true; - break; - case 37: - // left-arrow - if (modifiers) { - result.key = C0.ESC + '[1;' + (modifiers + 1) + 'D'; - // HACK: Make Alt + left-arrow behave like Ctrl + left-arrow: move one word backwards - // http://unix.stackexchange.com/a/108106 - // macOS uses different escape sequences than linux - if (result.key == C0.ESC + '[1;3D') { - result.key = (this.browser.isMac) ? C0.ESC + 'b' : C0.ESC + '[1;5D'; + case 13: + // return/enter + result.key = C0.CR; + result.cancel = true; + break; + case 27: + // escape + result.key = C0.ESC; + result.cancel = true; + break; + case 37: + // left-arrow + if (modifiers) { + result.key = C0.ESC + '[1;' + (modifiers + 1) + 'D'; + // HACK: Make Alt + left-arrow behave like Ctrl + left-arrow: move one word backwards + // http://unix.stackexchange.com/a/108106 + // macOS uses different escape sequences than linux + if (result.key == C0.ESC + '[1;3D') { + result.key = (this.browser.isMac) ? C0.ESC + 'b' : C0.ESC + '[1;5D'; + } + } else if (this.applicationCursor) { + result.key = C0.ESC + 'OD'; + } else { + result.key = C0.ESC + '[D'; } - } else if (this.applicationCursor) { - result.key = C0.ESC + 'OD'; - } else { - result.key = C0.ESC + '[D'; - } - break; - case 39: - // right-arrow - if (modifiers) { - result.key = C0.ESC + '[1;' + (modifiers + 1) + 'C'; - // HACK: Make Alt + right-arrow behave like Ctrl + right-arrow: move one word forward - // http://unix.stackexchange.com/a/108106 - // macOS uses different escape sequences than linux - if (result.key == C0.ESC + '[1;3C') { - result.key = (this.browser.isMac) ? C0.ESC + 'f' : C0.ESC + '[1;5C'; + break; + case 39: + // right-arrow + if (modifiers) { + result.key = C0.ESC + '[1;' + (modifiers + 1) + 'C'; + // HACK: Make Alt + right-arrow behave like Ctrl + right-arrow: move one word forward + // http://unix.stackexchange.com/a/108106 + // macOS uses different escape sequences than linux + if (result.key == C0.ESC + '[1;3C') { + result.key = (this.browser.isMac) ? C0.ESC + 'f' : C0.ESC + '[1;5C'; + } + } else if (this.applicationCursor) { + result.key = C0.ESC + 'OC'; + } else { + result.key = C0.ESC + '[C'; } - } else if (this.applicationCursor) { - result.key = C0.ESC + 'OC'; - } else { - result.key = C0.ESC + '[C'; - } - break; - case 38: - // up-arrow - if (modifiers) { - result.key = C0.ESC + '[1;' + (modifiers + 1) + 'A'; - // HACK: Make Alt + up-arrow behave like Ctrl + up-arrow - // http://unix.stackexchange.com/a/108106 - if (result.key == C0.ESC + '[1;3A') { - result.key = C0.ESC + '[1;5A'; + break; + case 38: + // up-arrow + if (modifiers) { + result.key = C0.ESC + '[1;' + (modifiers + 1) + 'A'; + // HACK: Make Alt + up-arrow behave like Ctrl + up-arrow + // http://unix.stackexchange.com/a/108106 + if (result.key == C0.ESC + '[1;3A') { + result.key = C0.ESC + '[1;5A'; + } + } else if (this.applicationCursor) { + result.key = C0.ESC + 'OA'; + } else { + result.key = C0.ESC + '[A'; } - } else if (this.applicationCursor) { - result.key = C0.ESC + 'OA'; - } else { - result.key = C0.ESC + '[A'; - } - break; - case 40: - // down-arrow - if (modifiers) { - result.key = C0.ESC + '[1;' + (modifiers + 1) + 'B'; - // HACK: Make Alt + down-arrow behave like Ctrl + down-arrow - // http://unix.stackexchange.com/a/108106 - if (result.key == C0.ESC + '[1;3B') { - result.key = C0.ESC + '[1;5B'; + break; + case 40: + // down-arrow + if (modifiers) { + result.key = C0.ESC + '[1;' + (modifiers + 1) + 'B'; + // HACK: Make Alt + down-arrow behave like Ctrl + down-arrow + // http://unix.stackexchange.com/a/108106 + if (result.key == C0.ESC + '[1;3B') { + result.key = C0.ESC + '[1;5B'; + } + } else if (this.applicationCursor) { + result.key = C0.ESC + 'OB'; + } else { + result.key = C0.ESC + '[B'; } - } else if (this.applicationCursor) { - result.key = C0.ESC + 'OB'; - } else { - result.key = C0.ESC + '[B'; - } - break; - case 45: - // insert - if (!ev.shiftKey && !ev.ctrlKey) { - // or + are used to - // copy-paste on some systems. - result.key = C0.ESC + '[2~'; - } - break; - case 46: - // delete - if (modifiers) { - result.key = C0.ESC + '[3;' + (modifiers + 1) + '~'; - } else { - result.key = C0.ESC + '[3~'; - } - break; - case 36: - // home - if (modifiers) - result.key = C0.ESC + '[1;' + (modifiers + 1) + 'H'; - else if (this.applicationCursor) - result.key = C0.ESC + 'OH'; - else - result.key = C0.ESC + '[H'; - break; - case 35: - // end - if (modifiers) - result.key = C0.ESC + '[1;' + (modifiers + 1) + 'F'; - else if (this.applicationCursor) - result.key = C0.ESC + 'OF'; - else - result.key = C0.ESC + '[F'; - break; - case 33: - // page up - if (ev.shiftKey) { - result.scrollDisp = -(this.rows - 1); - } else { - result.key = C0.ESC + '[5~'; - } - break; - case 34: - // page down - if (ev.shiftKey) { - result.scrollDisp = this.rows - 1; - } else { - result.key = C0.ESC + '[6~'; - } - break; - case 112: - // F1-F12 - if (modifiers) { - result.key = C0.ESC + '[1;' + (modifiers + 1) + 'P'; - } else { - result.key = C0.ESC + 'OP'; - } - break; - case 113: - if (modifiers) { - result.key = C0.ESC + '[1;' + (modifiers + 1) + 'Q'; - } else { - result.key = C0.ESC + 'OQ'; - } - break; - case 114: - if (modifiers) { - result.key = C0.ESC + '[1;' + (modifiers + 1) + 'R'; - } else { - result.key = C0.ESC + 'OR'; - } - break; - case 115: - if (modifiers) { - result.key = C0.ESC + '[1;' + (modifiers + 1) + 'S'; - } else { - result.key = C0.ESC + 'OS'; - } - break; - case 116: - if (modifiers) { - result.key = C0.ESC + '[15;' + (modifiers + 1) + '~'; - } else { - result.key = C0.ESC + '[15~'; - } - break; - case 117: - if (modifiers) { - result.key = C0.ESC + '[17;' + (modifiers + 1) + '~'; - } else { - result.key = C0.ESC + '[17~'; - } - break; - case 118: - if (modifiers) { - result.key = C0.ESC + '[18;' + (modifiers + 1) + '~'; - } else { - result.key = C0.ESC + '[18~'; - } - break; - case 119: - if (modifiers) { - result.key = C0.ESC + '[19;' + (modifiers + 1) + '~'; - } else { - result.key = C0.ESC + '[19~'; - } - break; - case 120: - if (modifiers) { - result.key = C0.ESC + '[20;' + (modifiers + 1) + '~'; - } else { - result.key = C0.ESC + '[20~'; - } - break; - case 121: - if (modifiers) { - result.key = C0.ESC + '[21;' + (modifiers + 1) + '~'; - } else { - result.key = C0.ESC + '[21~'; - } - break; - case 122: - if (modifiers) { - result.key = C0.ESC + '[23;' + (modifiers + 1) + '~'; - } else { - result.key = C0.ESC + '[23~'; - } - break; - case 123: - if (modifiers) { - result.key = C0.ESC + '[24;' + (modifiers + 1) + '~'; - } else { - result.key = C0.ESC + '[24~'; - } - break; - default: - // a-z and space - if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) { - if (ev.keyCode >= 65 && ev.keyCode <= 90) { - result.key = String.fromCharCode(ev.keyCode - 64); - } else if (ev.keyCode === 32) { - // NUL - result.key = String.fromCharCode(0); - } else if (ev.keyCode >= 51 && ev.keyCode <= 55) { - // escape, file sep, group sep, record sep, unit sep - result.key = String.fromCharCode(ev.keyCode - 51 + 27); - } else if (ev.keyCode === 56) { - // delete - result.key = String.fromCharCode(127); - } else if (ev.keyCode === 219) { - // ^[ - Control Sequence Introducer (CSI) - result.key = String.fromCharCode(27); - } else if (ev.keyCode === 220) { - // ^\ - String Terminator (ST) - result.key = String.fromCharCode(28); - } else if (ev.keyCode === 221) { - // ^] - Operating System Command (OSC) - result.key = String.fromCharCode(29); + break; + case 45: + // insert + if (!ev.shiftKey && !ev.ctrlKey) { + // or + are used to + // copy-paste on some systems. + result.key = C0.ESC + '[2~'; } - } else if (!this.browser.isMac && ev.altKey && !ev.ctrlKey && !ev.metaKey) { - // On Mac this is a third level shift. Use instead. - if (ev.keyCode >= 65 && ev.keyCode <= 90) { - result.key = C0.ESC + String.fromCharCode(ev.keyCode + 32); - } else if (ev.keyCode === 192) { - result.key = C0.ESC + '`'; - } else if (ev.keyCode >= 48 && ev.keyCode <= 57) { - result.key = C0.ESC + (ev.keyCode - 48); + break; + case 46: + // delete + if (modifiers) { + result.key = C0.ESC + '[3;' + (modifiers + 1) + '~'; + } else { + result.key = C0.ESC + '[3~'; } - } else if (this.browser.isMac && !ev.altKey && !ev.ctrlKey && ev.metaKey) { - if (ev.keyCode === 65) { // cmd + a - this.selectAll(); + break; + case 36: + // home + if (modifiers) + result.key = C0.ESC + '[1;' + (modifiers + 1) + 'H'; + else if (this.applicationCursor) + result.key = C0.ESC + 'OH'; + else + result.key = C0.ESC + '[H'; + break; + case 35: + // end + if (modifiers) + result.key = C0.ESC + '[1;' + (modifiers + 1) + 'F'; + else if (this.applicationCursor) + result.key = C0.ESC + 'OF'; + else + result.key = C0.ESC + '[F'; + break; + case 33: + // page up + if (ev.shiftKey) { + result.scrollDisp = -(this.rows - 1); + } else { + result.key = C0.ESC + '[5~'; } - } - break; - } - - return result; -}; - -/** - * Set the G level of the terminal - * @param g - */ -Terminal.prototype.setgLevel = function(g) { - this.glevel = g; - this.charset = this.charsets[g]; -}; + break; + case 34: + // page down + if (ev.shiftKey) { + result.scrollDisp = this.rows - 1; + } else { + result.key = C0.ESC + '[6~'; + } + break; + case 112: + // F1-F12 + if (modifiers) { + result.key = C0.ESC + '[1;' + (modifiers + 1) + 'P'; + } else { + result.key = C0.ESC + 'OP'; + } + break; + case 113: + if (modifiers) { + result.key = C0.ESC + '[1;' + (modifiers + 1) + 'Q'; + } else { + result.key = C0.ESC + 'OQ'; + } + break; + case 114: + if (modifiers) { + result.key = C0.ESC + '[1;' + (modifiers + 1) + 'R'; + } else { + result.key = C0.ESC + 'OR'; + } + break; + case 115: + if (modifiers) { + result.key = C0.ESC + '[1;' + (modifiers + 1) + 'S'; + } else { + result.key = C0.ESC + 'OS'; + } + break; + case 116: + if (modifiers) { + result.key = C0.ESC + '[15;' + (modifiers + 1) + '~'; + } else { + result.key = C0.ESC + '[15~'; + } + break; + case 117: + if (modifiers) { + result.key = C0.ESC + '[17;' + (modifiers + 1) + '~'; + } else { + result.key = C0.ESC + '[17~'; + } + break; + case 118: + if (modifiers) { + result.key = C0.ESC + '[18;' + (modifiers + 1) + '~'; + } else { + result.key = C0.ESC + '[18~'; + } + break; + case 119: + if (modifiers) { + result.key = C0.ESC + '[19;' + (modifiers + 1) + '~'; + } else { + result.key = C0.ESC + '[19~'; + } + break; + case 120: + if (modifiers) { + result.key = C0.ESC + '[20;' + (modifiers + 1) + '~'; + } else { + result.key = C0.ESC + '[20~'; + } + break; + case 121: + if (modifiers) { + result.key = C0.ESC + '[21;' + (modifiers + 1) + '~'; + } else { + result.key = C0.ESC + '[21~'; + } + break; + case 122: + if (modifiers) { + result.key = C0.ESC + '[23;' + (modifiers + 1) + '~'; + } else { + result.key = C0.ESC + '[23~'; + } + break; + case 123: + if (modifiers) { + result.key = C0.ESC + '[24;' + (modifiers + 1) + '~'; + } else { + result.key = C0.ESC + '[24~'; + } + break; + default: + // a-z and space + if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) { + if (ev.keyCode >= 65 && ev.keyCode <= 90) { + result.key = String.fromCharCode(ev.keyCode - 64); + } else if (ev.keyCode === 32) { + // NUL + result.key = String.fromCharCode(0); + } else if (ev.keyCode >= 51 && ev.keyCode <= 55) { + // escape, file sep, group sep, record sep, unit sep + result.key = String.fromCharCode(ev.keyCode - 51 + 27); + } else if (ev.keyCode === 56) { + // delete + result.key = String.fromCharCode(127); + } else if (ev.keyCode === 219) { + // ^[ - Control Sequence Introducer (CSI) + result.key = String.fromCharCode(27); + } else if (ev.keyCode === 220) { + // ^\ - String Terminator (ST) + result.key = String.fromCharCode(28); + } else if (ev.keyCode === 221) { + // ^] - Operating System Command (OSC) + result.key = String.fromCharCode(29); + } + } else if (!this.browser.isMac && ev.altKey && !ev.ctrlKey && !ev.metaKey) { + // On Mac this is a third level shift. Use instead. + if (ev.keyCode >= 65 && ev.keyCode <= 90) { + result.key = C0.ESC + String.fromCharCode(ev.keyCode + 32); + } else if (ev.keyCode === 192) { + result.key = C0.ESC + '`'; + } else if (ev.keyCode >= 48 && ev.keyCode <= 57) { + result.key = C0.ESC + (ev.keyCode - 48); + } + } else if (this.browser.isMac && !ev.altKey && !ev.ctrlKey && ev.metaKey) { + if (ev.keyCode === 65) { // cmd + a + this.selectAll(); + } + } + break; + } -/** - * Set the charset for the given G level of the terminal - * @param g - * @param charset - */ -Terminal.prototype.setgCharset = function(g, charset) { - this.charsets[g] = charset; - if (this.glevel === g) { - this.charset = charset; + return result; } -}; - -/** - * Handle a keypress event. - * Key Resources: - * - https://developer.mozilla.org/en-US/docs/DOM/KeyboardEvent - * @param {KeyboardEvent} ev The keypress event to be handled. - */ -Terminal.prototype.keyPress = function(ev) { - var key; - if (this.customKeyEventHandler && this.customKeyEventHandler(ev) === false) { - return false; + /** + * Set the G level of the terminal + * @param g + */ + public setgLevel = function(g: number) { + this.glevel = g; + this.charset = this.charsets[g]; } - this.cancel(ev); - - if (ev.charCode) { - key = ev.charCode; - } else if (ev.which == null) { - key = ev.keyCode; - } else if (ev.which !== 0 && ev.charCode !== 0) { - key = ev.which; - } else { - return false; + /** + * Set the charset for the given G level of the terminal + * @param g + * @param charset + */ + public setgCharset(g: number, charset: Charset) { + this.charsets[g] = charset; + if (this.glevel === g) { + this.charset = charset; + } } - if (!key || ( - (ev.altKey || ev.ctrlKey || ev.metaKey) && !isThirdLevelShift(this, ev) - )) { - return false; - } + /** + * Handle a keypress event. + * Key Resources: + * - https://developer.mozilla.org/en-US/docs/DOM/KeyboardEvent + * @param {KeyboardEvent} ev The keypress event to be handled. + */ + private keyPress(ev) { + var key; - key = String.fromCharCode(key); + if (this.customKeyEventHandler && this.customKeyEventHandler(ev) === false) { + return false; + } - this.emit('keypress', key, ev); - this.emit('key', key, ev); - this.showCursor(); - this.handler(key); + this.cancel(ev); - return true; -}; + if (ev.charCode) { + key = ev.charCode; + } else if (ev.which == null) { + key = ev.keyCode; + } else if (ev.which !== 0 && ev.charCode !== 0) { + key = ev.which; + } else { + return false; + } -/** - * Send data for handling to the terminal - * @param {string} data - */ -Terminal.prototype.send = function(data) { - var self = this; + if (!key || ( + (ev.altKey || ev.ctrlKey || ev.metaKey) && !isThirdLevelShift(this, ev) + )) { + return false; + } - if (!this.queue) { - setTimeout(function() { - self.handler(self.queue); - self.queue = ''; - }, 1); - } + key = String.fromCharCode(key); - this.queue += data; -}; + this.emit('keypress', key, ev); + this.emit('key', key, ev); + this.showCursor(); + this.handler(key); -/** - * Ring the bell. - * Note: We could do sweet things with webaudio here - */ -Terminal.prototype.bell = function() { - if (!this.visualBell) return; - var self = this; - this.element.style.borderColor = 'white'; - setTimeout(function() { - self.element.style.borderColor = ''; - }, 10); - if (this.popOnBell) this.focus(); -}; + return true; + } -/** - * Log the current state to the console. - */ -Terminal.prototype.log = function() { - if (!this.debug) return; - if (!this.context.console || !this.context.console.log) return; - var args = Array.prototype.slice.call(arguments); - this.context.console.log.apply(this.context.console, args); -}; + /** + * Send data for handling to the terminal + * @param {string} data + */ + private send(data) { + if (!this.queue) { + setTimeout(() => { + this.handler(this.queue); + this.queue = ''; + }, 1); + } -/** - * Log the current state as error to the console. - */ -Terminal.prototype.error = function() { - if (!this.debug) return; - if (!this.context.console || !this.context.console.error) return; - var args = Array.prototype.slice.call(arguments); - this.context.console.error.apply(this.context.console, args); -}; + this.queue += data; + } -/** - * Resizes the terminal. - * - * @param {number} x The number of columns to resize to. - * @param {number} y The number of rows to resize to. - */ -Terminal.prototype.resize = function(x, y) { - if (isNaN(x) || isNaN(y)) { - return; + /** + * Ring the bell. + * Note: We could do sweet things with webaudio here + */ + public bell() { + if (!this.options.visualBell) return; + this.element.style.borderColor = 'white'; + setTimeout(() => { + this.element.style.borderColor = ''; + }, 10); + if (this.options.popOnBell) this.focus(); } - if (y > this.getOption('scrollback')) { - this.setOption('scrollback', y) + /** + * Log the current state to the console. + */ + public log(): void { + if (!this.options.debug) return; + if (!this.context.console || !this.context.console.log) return; + const args = Array.prototype.slice.call(arguments); + this.context.console.log.apply(this.context.console, args); } - var line - , el - , i - , j - , ch - , addToY; + /** + * Log the current state as error to the console. + */ + public error(): void { + if (!this.options.debug) return; + if (!this.context.console || !this.context.console.error) return; + const args = Array.prototype.slice.call(arguments); + this.context.console.error.apply(this.context.console, args); + } - if (x === this.cols && y === this.rows) { - // Check if we still need to measure the char size (fixes #785). - if (!this.charMeasure.width || !this.charMeasure.height) { - this.charMeasure.measure(); + /** + * Resizes the terminal. + * + * @param {number} x The number of columns to resize to. + * @param {number} y The number of rows to resize to. + */ + public resize(x: number, y: number) { + if (isNaN(x) || isNaN(y)) { + return; } - return; - } - if (x < 1) x = 1; - if (y < 1) y = 1; + if (y > this.getOption('scrollback')) { + this.setOption('scrollback', y); + } - // resize cols - j = this.cols; - if (j < x) { - ch = [this.defAttr, ' ', 1]; // does xterm use the default attr? - i = this.buffer.lines.length; - while (i--) { - if (this.buffer.lines.get(i) === undefined) { - this.buffer.lines.set(i, this.blankLine()); - } - while (this.buffer.lines.get(i).length < x) { - this.buffer.lines.get(i).push(ch); + var line + , el + , i + , j + , ch + , addToY; + + if (x === this.cols && y === this.rows) { + // Check if we still need to measure the char size (fixes #785). + if (!this.charMeasure.width || !this.charMeasure.height) { + this.charMeasure.measure(); } + return; } - } - this.cols = x; - this.setupStops(this.cols); - - // resize rows - j = this.rows; - addToY = 0; - if (j < y) { - el = this.element; - while (j++ < y) { - // y is rows, not this.buffer.y - if (this.buffer.lines.length < y + this.buffer.ybase) { - if (this.buffer.ybase > 0 && this.buffer.lines.length <= this.buffer.ybase + this.buffer.y + addToY + 1) { - // There is room above the buffer and there are no empty elements below the line, - // scroll up - this.buffer.ybase--; - addToY++; - if (this.buffer.ydisp > 0) { - // Viewport is at the top of the buffer, must increase downwards - this.buffer.ydisp--; - } - } else { - // Add a blank line if there is no buffer left at the top to scroll to, or if there - // are blank lines after the cursor - this.buffer.lines.push(this.blankLine()); + if (x < 1) x = 1; + if (y < 1) y = 1; + + // resize cols + j = this.cols; + if (j < x) { + ch = [this.defAttr, ' ', 1]; // does xterm use the default attr? + i = this.buffer.lines.length; + while (i--) { + if (this.buffer.lines.get(i) === undefined) { + this.buffer.lines.set(i, this.blankLine()); + } + while (this.buffer.lines.get(i).length < x) { + this.buffer.lines.get(i).push(ch); } - } - if (this.children.length < y) { - this.insertRow(); } } - } else { // (j > y) - while (j-- > y) { - if (this.buffer.lines.length > y + this.buffer.ybase) { - if (this.buffer.lines.length > this.buffer.ybase + this.buffer.y + 1) { - // The line is a blank line below the cursor, remove it - this.buffer.lines.pop(); - } else { - // The line is the cursor, scroll down - this.buffer.ybase++; - this.buffer.ydisp++; + + this.cols = x; + this.setupStops(this.cols); + + // resize rows + j = this.rows; + addToY = 0; + if (j < y) { + el = this.element; + while (j++ < y) { + // y is rows, not this.buffer.y + if (this.buffer.lines.length < y + this.buffer.ybase) { + if (this.buffer.ybase > 0 && this.buffer.lines.length <= this.buffer.ybase + this.buffer.y + addToY + 1) { + // There is room above the buffer and there are no empty elements below the line, + // scroll up + this.buffer.ybase--; + addToY++; + if (this.buffer.ydisp > 0) { + // Viewport is at the top of the buffer, must increase downwards + this.buffer.ydisp--; + } + } else { + // Add a blank line if there is no buffer left at the top to scroll to, or if there + // are blank lines after the cursor + this.buffer.lines.push(this.blankLine()); + } + } + if (this.children.length < y) { + this.insertRow(); } } - if (this.children.length > y) { - el = this.children.shift(); - if (!el) continue; - el.parentNode.removeChild(el); + } else { // (j > y) + while (j-- > y) { + if (this.buffer.lines.length > y + this.buffer.ybase) { + if (this.buffer.lines.length > this.buffer.ybase + this.buffer.y + 1) { + // The line is a blank line below the cursor, remove it + this.buffer.lines.pop(); + } else { + // The line is the cursor, scroll down + this.buffer.ybase++; + this.buffer.ydisp++; + } + } + if (this.children.length > y) { + el = this.children.shift(); + if (!el) continue; + el.parentNode.removeChild(el); + } } } - } - this.rows = y; - - // Make sure that the cursor stays on screen - if (this.buffer.y >= y) { - this.buffer.y = y - 1; - } - if (addToY) { - this.buffer.y += addToY; - } + this.rows = y; - if (this.buffer.x >= x) { - this.buffer.x = x - 1; - } + // Make sure that the cursor stays on screen + if (this.buffer.y >= y) { + this.buffer.y = y - 1; + } + if (addToY) { + this.buffer.y += addToY; + } - this.buffer.scrollTop = 0; - this.buffer.scrollBottom = y - 1; + if (this.buffer.x >= x) { + this.buffer.x = x - 1; + } - this.charMeasure.measure(); + this.buffer.scrollTop = 0; + this.buffer.scrollBottom = y - 1; - this.refresh(0, this.rows - 1); + this.charMeasure.measure(); - this.geometry = [this.cols, this.rows]; - this.emit('resize', {terminal: this, cols: x, rows: y}); -}; + this.refresh(0, this.rows - 1); -/** - * Updates the range of rows to refresh - * @param {number} y The number of rows to refresh next. - */ -Terminal.prototype.updateRange = function(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) { - // this.refreshEnd = this.rows - 1; - // } - // } -}; + this.geometry = [this.cols, this.rows]; + this.emit('resize', {terminal: this, cols: x, rows: y}); + } -/** - * Set the range of refreshing to the maximum value - */ -Terminal.prototype.maxRange = function() { - this.refreshStart = 0; - this.refreshEnd = this.rows - 1; -}; + /** + * Updates the range of rows to refresh + * @param {number} y The number of rows to refresh next. + */ + public updateRange(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) { + // this.refreshEnd = this.rows - 1; + // } + // } + } + /** + * Set the range of refreshing to the maximum value + */ + public maxRange() { + this.refreshStart = 0; + this.refreshEnd = this.rows - 1; + } + /** + * Setup the tab stops. + * @param {number} i + */ + public setupStops(i?: number) { + if (i != null) { + if (!this.buffer.tabs[i]) { + i = this.prevStop(i); + } + } else { + this.buffer.tabs = {}; + i = 0; + } -/** - * Setup the tab stops. - * @param {number} i - */ -Terminal.prototype.setupStops = function(i) { - if (i != null) { - if (!this.buffer.tabs[i]) { - i = this.prevStop(i); + for (; i < this.cols; i += this.getOption('tabStopWidth')) { + this.buffer.tabs[i] = true; } - } else { - this.buffer.tabs = {}; - i = 0; } - for (; i < this.cols; i += this.getOption('tabStopWidth')) { - this.buffer.tabs[i] = true; + /** + * Move the cursor to the previous tab stop from the given position (default is current). + * @param {number} x The position to move the cursor to the previous tab stop. + */ + public prevStop(x) { + if (x == null) x = this.buffer.x; + while (!this.buffer.tabs[--x] && x > 0); + return x >= this.cols ? this.cols - 1 : x < 0 ? 0 : x; } -}; - - -/** - * Move the cursor to the previous tab stop from the given position (default is current). - * @param {number} x The position to move the cursor to the previous tab stop. - */ -Terminal.prototype.prevStop = function(x) { - if (x == null) x = this.buffer.x; - while (!this.buffer.tabs[--x] && x > 0); - return x >= this.cols - ? this.cols - 1 - : x < 0 ? 0 : x; -}; - -/** - * Move the cursor one tab stop forward from the given position (default is current). - * @param {number} x The position to move the cursor one tab stop forward. - */ -Terminal.prototype.nextStop = function(x) { - if (x == null) x = this.buffer.x; - while (!this.buffer.tabs[++x] && x < this.cols); - return x >= this.cols - ? this.cols - 1 - : x < 0 ? 0 : x; -}; - - -/** - * Erase in the identified line everything from "x" to the end of the line (right). - * @param {number} x The column from which to start erasing to the end of the line. - * @param {number} y The line in which to operate. - */ -Terminal.prototype.eraseRight = function(x, y) { - var line = this.buffer.lines.get(this.buffer.ybase + y); - if (!line) { - return; - } - var ch = [this.eraseAttr(), ' ', 1]; // xterm - for (; x < this.cols; x++) { - line[x] = ch; + /** + * Move the cursor one tab stop forward from the given position (default is current). + * @param {number} x The position to move the cursor one tab stop forward. + */ + public nextStop(x) { + if (x == null) x = this.buffer.x; + while (!this.buffer.tabs[++x] && x < this.cols); + return x >= this.cols + ? this.cols - 1 + : x < 0 ? 0 : x; } - this.updateRange(y); -}; - + /** + * Erase in the identified line everything from "x" to the end of the line (right). + * @param {number} x The column from which to start erasing to the end of the line. + * @param {number} y The line in which to operate. + */ + public eraseRight(x: number, y: number): void { + const line = this.buffer.lines.get(this.buffer.ybase + y); + if (!line) { + return; + } + const ch: [number, string, number] = [this.eraseAttr(), ' ', 1]; // xterm + for (; x < this.cols; x++) { + line[x] = ch; + } + this.updateRange(y); + } -/** - * Erase in the identified line everything from "x" to the start of the line (left). - * @param {number} x The column from which to start erasing to the start of the line. - * @param {number} y The line in which to operate. - */ -Terminal.prototype.eraseLeft = function(x, y) { - var line = this.buffer.lines.get(this.buffer.ybase + y); - if (!line) { - return; + /** + * Erase in the identified line everything from "x" to the start of the line (left). + * @param {number} x The column from which to start erasing to the start of the line. + * @param {number} y The line in which to operate. + */ + public eraseLeft(x: number, y: number) { + const line = this.buffer.lines.get(this.buffer.ybase + y); + if (!line) { + return; + } + const ch: [number, string, number] = [this.eraseAttr(), ' ', 1]; // xterm + x++; + while (x--) { + line[x] = ch; + } + this.updateRange(y); } - var ch = [this.eraseAttr(), ' ', 1]; // xterm - x++; - while (x--) { - line[x] = ch; + + /** + * Clears the entire buffer, making the prompt line the new first line. + */ + public clear() { + if (this.buffer.ybase === 0 && this.buffer.y === 0) { + // Don't clear if it's already clear + return; + } + this.buffer.lines.set(0, this.buffer.lines.get(this.buffer.ybase + this.buffer.y)); + this.buffer.lines.length = 1; + this.buffer.ydisp = 0; + this.buffer.ybase = 0; + this.buffer.y = 0; + for (var i = 1; i < this.rows; i++) { + this.buffer.lines.push(this.blankLine()); + } + this.refresh(0, this.rows - 1); + this.emit('scroll', this.buffer.ydisp); } - this.updateRange(y); -}; -/** - * Clears the entire buffer, making the prompt line the new first line. - */ -Terminal.prototype.clear = function() { - if (this.buffer.ybase === 0 && this.buffer.y === 0) { - // Don't clear if it's already clear - return; - } - this.buffer.lines.set(0, this.buffer.lines.get(this.buffer.ybase + this.buffer.y)); - this.buffer.lines.length = 1; - this.buffer.ydisp = 0; - this.buffer.ybase = 0; - this.buffer.y = 0; - for (var i = 1; i < this.rows; i++) { - this.buffer.lines.push(this.blankLine()); - } - this.refresh(0, this.rows - 1); - this.emit('scroll', this.buffer.ydisp); -}; + /** + * Erase all content in the given line + * @param {number} y The line to erase all of its contents. + */ + public eraseLine(y): void { + this.eraseRight(0, y); + } -/** - * Erase all content in the given line - * @param {number} y The line to erase all of its contents. - */ -Terminal.prototype.eraseLine = function(y) { - this.eraseRight(0, y); -}; + /** + * Return the data array of a blank line + * @param {boolean} cur First bunch of data for each "blank" character. + * @param {boolean} isWrapped Whether the new line is wrapped from the previous line. + */ + public blankLine(cur?, isWrapped?: boolean) { + const attr = cur ? this.eraseAttr() : this.defAttr; + const ch = [attr, ' ', 1]; // width defaults to 1 halfwidth character + const line = []; -/** - * Return the data array of a blank line - * @param {number} cur First bunch of data for each "blank" character. - * @param {boolean} isWrapped Whether the new line is wrapped from the previous line. - */ -Terminal.prototype.blankLine = function(cur, isWrapped) { - var attr = cur - ? this.eraseAttr() - : this.defAttr; + // TODO: It is not ideal that this is a property on an array, a buffer line + // class should be added that will hold this data and other useful functions. + if (isWrapped) { + (line).isWrapped = isWrapped; + } - var ch = [attr, ' ', 1] // width defaults to 1 halfwidth character - , line = [] - , i = 0; + for (let i = 0; i < this.cols; i++) { + line[i] = ch; + } - // TODO: It is not ideal that this is a property on an array, a buffer line - // class should be added that will hold this data and other useful functions. - if (isWrapped) { - line.isWrapped = isWrapped; + return line; } - for (; i < this.cols; i++) { - line[i] = ch; + /** + * If cur return the back color xterm feature attribute. Else return defAttr. + * @param {object} cur + */ + public ch(cur) { + return cur ? [this.eraseAttr(), ' ', 1] : [this.defAttr, ' ', 1]; } - return line; -}; + /** + * Evaluate if the current terminal is the given argument. + * @param {object} term The terminal to evaluate + */ + private is(term) { + // TODO: Do we need this? + const name = this.options.termName; + return (name + '').indexOf(term) === 0; + } + /** + * Emit the 'data' event and populate the given data. + * @param {string} data The data to populate in the event. + */ + private handler(data: string): void { + // Prevents all events to pty process if stdin is disabled + if (this.options.disableStdin) { + return; + } -/** - * If cur return the back color xterm feature attribute. Else return defAttr. - * @param {object} cur - */ -Terminal.prototype.ch = function(cur) { - return cur - ? [this.eraseAttr(), ' ', 1] - : [this.defAttr, ' ', 1]; -}; + // Clear the selection if the selection manager is available and has an active selection + if (this.selectionManager && this.selectionManager.hasSelection) { + this.selectionManager.clearSelection(); + } + // Input is being sent to the terminal, the terminal should focus the prompt. + if (this.buffer.ybase !== this.buffer.ydisp) { + this.scrollToBottom(); + } + this.emit('data', data); + } -/** - * Evaluate if the current terminal is the given argument. - * @param {object} term The terminal to evaluate - */ -Terminal.prototype.is = function(term) { - var name = this.termName; - return (name + '').indexOf(term) === 0; -}; + /** + * Emit the 'title' event and populate the given title. + * @param {string} title The title to populate in the event. + */ + private handleTitle(title: string) { + /** + * This event is emitted when the title of the terminal is changed + * from inside the terminal. The parameter is the new title. + * + * @event title + */ + this.emit('title', title); + } + /** + * ESC + */ -/** - * Emit the 'data' event and populate the given data. - * @param {string} data The data to populate in the event. - */ -Terminal.prototype.handler = function(data) { - // Prevents all events to pty process if stdin is disabled - if (this.options.disableStdin) { - return; + /** + * ESC D Index (IND is 0x84). + */ + public index() { + this.buffer.y++; + if (this.buffer.y > this.buffer.scrollBottom) { + this.buffer.y--; + this.scroll(); + } + // If the end of the line is hit, prevent this action from wrapping around to the next line. + if (this.buffer.x >= this.cols) { + this.buffer.x--; + } } - // Clear the selection if the selection manager is available and has an active selection - if (this.selectionManager && this.selectionManager.hasSelection) { - this.selectionManager.clearSelection(); + /** + * ESC M Reverse Index (RI is 0x8d). + * + * Move the cursor up one row, inserting a new blank line if necessary. + */ + public reverseIndex() { + var j; + if (this.buffer.y === this.buffer.scrollTop) { + // possibly move the code below to term.reverseScroll(); + // test: echo -ne '\e[1;1H\e[44m\eM\e[0m' + // blankLine(true) is xterm/linux behavior + this.buffer.lines.shiftElements(this.buffer.y + this.buffer.ybase, this.rows - 1, 1); + this.buffer.lines.set(this.buffer.y + this.buffer.ybase, this.blankLine(true)); + this.updateRange(this.buffer.scrollTop); + this.updateRange(this.buffer.scrollBottom); + } else { + this.buffer.y--; + } } - // Input is being sent to the terminal, the terminal should focus the prompt. - if (this.buffer.ybase !== this.buffer.ydisp) { - this.scrollToBottom(); + /** + * ESC c Full Reset (RIS). + */ + public reset() { + this.options.rows = this.rows; + this.options.cols = this.cols; + var customKeyEventHandler = this.customKeyEventHandler; + var cursorBlinkInterval = this.cursorBlinkInterval; + var inputHandler = this.inputHandler; + var buffers = this.buffers; + Terminal.call(this, this.options); + this.customKeyEventHandler = customKeyEventHandler; + this.cursorBlinkInterval = cursorBlinkInterval; + this.inputHandler = inputHandler; + this.buffers = buffers; + this.refresh(0, this.rows - 1); + this.viewport.syncScrollArea(); } - this.emit('data', data); -}; -/** - * Emit the 'title' event and populate the given title. - * @param {string} title The title to populate in the event. - */ -Terminal.prototype.handleTitle = function(title) { /** - * This event is emitted when the title of the terminal is changed - * from inside the terminal. The parameter is the new title. - * - * @event title + * ESC H Tab Set (HTS is 0x88). */ - this.emit('title', title); -}; + private tabSet() { + this.buffer.tabs[this.buffer.x] = true; + } + private cancel(ev: Event, force?: boolean) { + if (!this.options.cancelEvents && !force) { + return; + } + ev.preventDefault(); + ev.stopPropagation(); + return false; + } -/** - * ESC - */ + // Expose to InputHandler + // TODO: Revise when truecolor is introduced. + public matchColor(r1, g1, b1) { + var hash = (r1 << 16) | (g1 << 8) | b1; -/** - * ESC D Index (IND is 0x84). - */ -Terminal.prototype.index = function() { - this.buffer.y++; - if (this.buffer.y > this.buffer.scrollBottom) { - this.buffer.y--; - this.scroll(); - } - // If the end of the line is hit, prevent this action from wrapping around to the next line. - if (this.buffer.x >= this.cols) { - this.buffer.x--; - } -}; + if (matchColorCache[hash] != null) { + return matchColorCache[hash]; + } + let ldiff = Infinity; + let li = -1; + let i = 0; + let c; + let r2; + let g2; + let b2; + let diff; + + for (; i < vcolors.length; i++) { + c = vcolors[i]; + r2 = c[0]; + g2 = c[1]; + b2 = c[2]; + + diff = matchColorDistance(r1, g1, b1, r2, g2, b2); + + if (diff === 0) { + li = i; + break; + } -/** - * ESC M Reverse Index (RI is 0x8d). - * - * Move the cursor up one row, inserting a new blank line if necessary. - */ -Terminal.prototype.reverseIndex = function() { - var j; - if (this.buffer.y === this.buffer.scrollTop) { - // possibly move the code below to term.reverseScroll(); - // test: echo -ne '\e[1;1H\e[44m\eM\e[0m' - // blankLine(true) is xterm/linux behavior - this.buffer.lines.shiftElements(this.buffer.y + this.buffer.ybase, this.rows - 1, 1); - this.buffer.lines.set(this.buffer.y + this.buffer.ybase, this.blankLine(true)); - this.updateRange(this.buffer.scrollTop); - this.updateRange(this.buffer.scrollBottom); - } else { - this.buffer.y--; + if (diff < ldiff) { + ldiff = diff; + li = i; + } + } + + return matchColorCache[hash] = li; } -}; +} -/** - * ESC c Full Reset (RIS). - */ -Terminal.prototype.reset = function() { - this.options.rows = this.rows; - this.options.cols = this.cols; - var customKeyEventHandler = this.customKeyEventHandler; - var cursorBlinkInterval = this.cursorBlinkInterval; - var inputHandler = this.inputHandler; - var buffers = this.buffers; - Terminal.call(this, this.options); - this.customKeyEventHandler = customKeyEventHandler; - this.cursorBlinkInterval = cursorBlinkInterval; - this.inputHandler = inputHandler; - this.buffers = buffers; - this.refresh(0, this.rows - 1); - this.viewport.syncScrollArea(); -}; +// TODO: Make sure things work after this is removed +// each(keys(Terminal.defaults), function(key) { +// Terminal[key] = Terminal.defaults[key]; +// Terminal.options[key] = Terminal.defaults[key]; +// }); -/** - * ESC H Tab Set (HTS is 0x88). - */ -Terminal.prototype.tabSet = function() { - this.buffer.tabs[this.buffer.x] = true; -}; /** * Helpers */ -function on(el, type, handler, capture) { +function globalOn(el: HTMLElement | HTMLElement[], type: string, handler: (event: Event) => any, capture?: boolean) { if (!Array.isArray(el)) { el = [el]; } @@ -2323,20 +2376,13 @@ function on(el, type, handler, capture) { element.addEventListener(type, handler, capture || false); }); } +// TODO: Remove once everything is typed +const on = globalOn; function off(el, type, handler, capture) { el.removeEventListener(type, handler, capture || false); } -function cancel(ev, force) { - if (!this.cancelEvents && !force) { - return; - } - ev.preventDefault(); - ev.stopPropagation(); - return false; -} - function inherits(child, parent) { function f() { this.constructor = child; @@ -2358,7 +2404,7 @@ function isThirdLevelShift(term, ev) { (term.browser.isMac && ev.altKey && !ev.ctrlKey && !ev.metaKey) || (term.browser.isMSWindows && ev.altKey && ev.ctrlKey && !ev.metaKey); - if (ev.type == 'keypress') { + if (ev.type === 'keypress') { return thirdLevelKey; } @@ -2366,51 +2412,10 @@ function isThirdLevelShift(term, ev) { return thirdLevelKey && (!ev.keyCode || ev.keyCode > 47); } -// Expose to InputHandler (temporary) -Terminal.prototype.matchColor = matchColor; - -function matchColor(r1, g1, b1) { - var hash = (r1 << 16) | (g1 << 8) | b1; - - if (matchColor._cache[hash] != null) { - return matchColor._cache[hash]; - } - - var ldiff = Infinity - , li = -1 - , i = 0 - , c - , r2 - , g2 - , b2 - , diff; - - for (; i < Terminal.vcolors.length; i++) { - c = Terminal.vcolors[i]; - r2 = c[0]; - g2 = c[1]; - b2 = c[2]; - - diff = matchColor.distance(r1, g1, b1, r2, g2, b2); - - if (diff === 0) { - li = i; - break; - } - - if (diff < ldiff) { - ldiff = diff; - li = i; - } - } - - return matchColor._cache[hash] = li; -} - -matchColor._cache = {}; +const matchColorCache = {}; // http://stackoverflow.com/questions/1633828 -matchColor.distance = function(r1, g1, b1, r2, g2, b2) { +const matchColorDistance = function(r1, g1, b1, r2, g2, b2) { return Math.pow(30 * (r1 - r2), 2) + Math.pow(59 * (g1 - g2), 2) + Math.pow(11 * (b1 - b2), 2); @@ -2418,7 +2423,7 @@ matchColor.distance = function(r1, g1, b1, r2, g2, b2) { function each(obj, iter, con) { if (obj.forEach) return obj.forEach(iter, con); - for (var i = 0; i < obj.length; i++) { + for (let i = 0; i < obj.length; i++) { iter.call(con, obj[i], i, obj); } } @@ -2431,7 +2436,7 @@ function wasMondifierKeyOnlyEvent(ev) { function keys(obj) { if (Object.keys) return Object.keys(obj); - var key, keys = []; + let key, keys = []; for (key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { keys.push(key); @@ -2444,9 +2449,9 @@ function keys(obj) { * Expose */ -Terminal.translateBufferLineToString = translateBufferLineToString; -Terminal.EventEmitter = EventEmitter; -Terminal.inherits = inherits; +// Terminal.translateBufferLineToString = translateBufferLineToString; +// Terminal.EventEmitter = EventEmitter; +// Terminal.inherits = inherits; /** * Adds an event listener to the terminal. @@ -2454,8 +2459,7 @@ Terminal.inherits = inherits; * @param {string} event The name of the event. TODO: Document all event types * @param {function} callback The function to call when the event is triggered. */ -Terminal.on = on; -Terminal.off = off; -Terminal.cancel = cancel; +// Terminal.on = on; +// Terminal.off = off; module.exports = Terminal; diff --git a/src/Types.ts b/src/Types.ts index 896b729a81..9e4b1e1a9a 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -12,3 +12,8 @@ export type LinkMatcher = { }; export type LinkMatcherHandler = (event: MouseEvent, uri: string) => boolean | void; export type LinkMatcherValidationCallback = (uri: string, element: HTMLElement, callback: (isValid: boolean) => void) => void; + +// TODO: Make this type more specific +export type TerminalOptions = {[key: string]: any}; +export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean; +export type Charset = {[key: string]: string}; diff --git a/src/utils/CircularList.ts b/src/utils/CircularList.ts index d0b2f685a4..54850ab7c3 100644 --- a/src/utils/CircularList.ts +++ b/src/utils/CircularList.ts @@ -5,8 +5,9 @@ * @license MIT */ import { EventEmitter } from '../EventEmitter'; +import { ICircularList } from '../Interfaces'; -export class CircularList extends EventEmitter { +export class CircularList extends EventEmitter implements ICircularList { private _array: T[]; private _startIndex: number; private _length: number; diff --git a/src/xterm.ts b/src/xterm.ts new file mode 100644 index 0000000000..0c2b7319fd --- /dev/null +++ b/src/xterm.ts @@ -0,0 +1,3 @@ +import { Terminal } from './Terminal'; + +module.exports = Terminal; From 41c335c0e635d463245e6f512d144ff3467a1f3d Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 5 Aug 2017 02:22:43 -0700 Subject: [PATCH 03/22] The terminal works! --- src/Buffer.test.ts | 2 +- src/Buffer.ts | 3 +- src/Interfaces.ts | 4 +- src/SelectionManager.test.ts | 4 +- src/SelectionModel.test.ts | 4 +- src/Terminal.ts | 76 ++++++++++++++++++------------------ 6 files changed, 48 insertions(+), 45 deletions(-) diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index f68baa82ac..c4c3a267d5 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -22,7 +22,7 @@ describe('Buffer', () => { describe('constructor', () => { it('should create a CircularList with max length equal to scrollback, for its lines', () => { assert.instanceOf(buffer.lines, CircularList); - assert.equal(buffer.lines.maxLength, terminal.scrollback); + assert.equal(buffer.lines.maxLength, terminal.options.scrollback); }); it('should set the Buffer\'s scrollBottom value equal to the terminal\'s rows -1', () => { assert.equal(buffer.scrollBottom, terminal.rows - 1); diff --git a/src/Buffer.ts b/src/Buffer.ts index 12b2137d6c..b76cdc4d7a 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -36,7 +36,8 @@ export class Buffer { public scrollTop: number = 0, public tabs: any = {}, ) { - this.lines = new CircularList<[number, string, number][]>(this.terminal.scrollback); + // TODO: Don't cast terminal to any + this.lines = new CircularList<[number, string, number][]>(this.terminal.options.scrollback); this.scrollBottom = this.terminal.rows - 1; } } diff --git a/src/Interfaces.ts b/src/Interfaces.ts index c675f7a244..c24aa2eff1 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -3,7 +3,7 @@ */ import { LinkMatcherOptions } from './Interfaces'; -import { LinkMatcherHandler, LinkMatcherValidationCallback } from './Types'; +import { LinkMatcherHandler, LinkMatcherValidationCallback, TerminalOptions } from './Types'; export interface IBrowser { isNode: boolean; @@ -32,7 +32,7 @@ export interface ITerminal { cursorHidden: boolean; cursorState: number; defAttr: number; - scrollback: number; + options: TerminalOptions; buffers: IBufferSet; buffer: IBuffer; diff --git a/src/SelectionManager.test.ts b/src/SelectionManager.test.ts index c2173d77f4..94d58c3eda 100644 --- a/src/SelectionManager.test.ts +++ b/src/SelectionManager.test.ts @@ -46,8 +46,8 @@ describe('SelectionManager', () => { window = dom.window; document = window.document; rowContainer = document.createElement('div'); - terminal = { cols: 80, rows: 2 }; - terminal.scrollback = 100; + terminal = { cols: 80, rows: 2, options: {} }; + terminal.options.scrollback = 100; terminal.buffers = new BufferSet(terminal); terminal.buffer = terminal.buffers.active; bufferLines = terminal.buffer.lines; diff --git a/src/SelectionModel.test.ts b/src/SelectionModel.test.ts index 6da3874bd4..9ec36e0d01 100644 --- a/src/SelectionModel.test.ts +++ b/src/SelectionModel.test.ts @@ -22,8 +22,8 @@ describe('SelectionManager', () => { let model: TestSelectionModel; beforeEach(() => { - terminal = { cols: 80, rows: 2, ybase: 0 }; - terminal.scrollback = 10; + terminal = { cols: 80, rows: 2, ybase: 0, options: {} }; + terminal.options.scrollback = 10; terminal.buffers = new BufferSet(terminal); terminal.buffer = terminal.buffers.active; diff --git a/src/Terminal.ts b/src/Terminal.ts index 18fcd00064..19f10e7dfe 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -30,7 +30,7 @@ import { CHARSETS } from './Charsets'; import { getRawByteCoords } from './utils/Mouse'; import { translateBufferLineToString } from './utils/BufferLine'; import { TerminalOptions, CustomKeyEventHandler, Charset } from './Types'; -import { ITerminal, IBrowser } from "./Interfaces"; +import { ITerminal, IBrowser } from './Interfaces'; // Declare for RequireJS in loadAddon declare var define: any; @@ -192,7 +192,7 @@ export class Terminal extends EventEmitter implements ITerminal { private body: HTMLBodyElement; private viewportScrollArea: HTMLElement; private viewportElement: HTMLElement; - private selectionContainer: HTMLElement; + public selectionContainer: HTMLElement; private helperContainer: HTMLElement; private compositionView: HTMLElement; private charSizeStyleElement: HTMLStyleElement; @@ -201,12 +201,13 @@ export class Terminal extends EventEmitter implements ITerminal { // in Browser for themselves. public browser: IBrowser = Browser; - private options: TerminalOptions; + // TODO: Options should be private, remove from interface in favor of getOption + public options: TerminalOptions; private colors: any; // TODO: This can be changed to an enum or boolean, 0 and 1 seem to be the only options - private cursorState: number = 0; - private cursorHidden: boolean = false; + public cursorState: number = 0; + public cursorHidden: boolean = false; private convertEol: boolean; // TODO: This is the data queue for send, improve name and documentation private queue: string = ''; @@ -243,7 +244,7 @@ export class Terminal extends EventEmitter implements ITerminal { private urxvtMouse; // misc - private children; + public children: HTMLElement[]; private refreshStart; private refreshEnd; private savedX; @@ -254,13 +255,13 @@ export class Terminal extends EventEmitter implements ITerminal { private readable: boolean = true; private writable: boolean = true; - private defAttr: number = (0 << 18) | (257 << 9) | (256 << 0); - private curAttr: number = (0 << 18) | (257 << 9) | (256 << 0); + public defAttr: number = (0 << 18) | (257 << 9) | (256 << 0); + public curAttr: number = (0 << 18) | (257 << 9) | (256 << 0); - private params: (string | number)[] = []; - private currentParam: string | number = 0; - private prefix: string = ''; - private postfix: string = ''; + public params: (string | number)[] = []; + public currentParam: string | number = 0; + public prefix: string = ''; + public postfix: string = ''; // user input states public writeBuffer: string[] = []; @@ -286,13 +287,13 @@ export class Terminal extends EventEmitter implements ITerminal { private inputHandler: InputHandler; private parser: Parser; private renderer: Renderer; - private selectionManager: SelectionManager;; + public selectionManager: SelectionManager;; private linkifier: Linkifier; - private buffers: BufferSet; - private buffer: Buffer; + public buffers: BufferSet; + public buffer: Buffer; private viewport: Viewport; private compositionHelper: CompositionHelper; - private charMeasure: CharMeasure; + public charMeasure: CharMeasure; public cols: number; public rows: number; @@ -328,10 +329,9 @@ export class Terminal extends EventEmitter implements ITerminal { Object.keys(DEFAULT_OPTIONS).forEach(function(key) { if (options[key] == null) { - if (Terminal[key] !== DEFAULT_OPTIONS[key]) { - options[key] = Terminal[key]; - } + options[key] = DEFAULT_OPTIONS[key]; } + // TODO: We should move away from duplicate options on the Terminal object self[key] = options[key]; }); @@ -346,6 +346,7 @@ export class Terminal extends EventEmitter implements ITerminal { options.colors = options.colors.concat( _colors.slice(16, -2), options.colors.slice(-2)); } + this.options = options; this.colors = options.colors; // this.context = options.context || window; @@ -598,20 +599,20 @@ export class Terminal extends EventEmitter implements ITerminal { self.keyPress(ev); }, true); - on(this.element, 'keyup', function(ev) { + on(this.element, 'keyup', (ev) => { if (!wasMondifierKeyOnlyEvent(ev)) { - this.focus(this); + this.focus(); } }, true); - on(this.textarea, 'keydown', function(ev) { + on(this.textarea, 'keydown', (ev) => { this.keyDown(ev); }, true); - on(this.textarea, 'keypress', function(ev) { + on(this.textarea, 'keypress', (ev) => { this.keyPress(ev); // Truncate the textarea's value, since it is not needed - this.value = ''; + this.textarea.value = ''; }, true); on(this.textarea, 'compositionstart', this.compositionHelper.compositionstart.bind(this.compositionHelper)); @@ -1063,20 +1064,21 @@ export class Terminal extends EventEmitter implements ITerminal { // x10 compatibility mode can't send button releases if (!this.x10Mouse) { - on(this.document, 'mouseup', (ev: MouseEvent) => { + const handler = (ev: MouseEvent) => { sendButton(ev); if (this.normalMouse) off(this.document, 'mousemove', sendMove); - off(this.document, 'mouseup', up); + off(this.document, 'mouseup', handler); return this.cancel(ev); - }); + }; + on(this.document, 'mouseup', handler); } return this.cancel(ev); }); - //if (this.normalMouse) { + // if (this.normalMouse) { // on(this.document, 'mousemove', sendMove); - //} + // } on(el, 'wheel', (ev) => { if (!this.mouseEvents) return; @@ -1149,7 +1151,7 @@ export class Terminal extends EventEmitter implements ITerminal { /** * Display the cursor element */ - private showCursor() { + public showCursor() { if (!this.cursorState) { this.cursorState = 1; this.refresh(this.buffer.y, this.buffer.y); @@ -2207,7 +2209,7 @@ export class Terminal extends EventEmitter implements ITerminal { * Emit the 'data' event and populate the given data. * @param {string} data The data to populate in the event. */ - private handler(data: string): void { + public handler(data: string): void { // Prevents all events to pty process if stdin is disabled if (this.options.disableStdin) { return; @@ -2305,7 +2307,7 @@ export class Terminal extends EventEmitter implements ITerminal { this.buffer.tabs[this.buffer.x] = true; } - private cancel(ev: Event, force?: boolean) { + public cancel(ev: Event, force?: boolean) { if (!this.options.cancelEvents && !force) { return; } @@ -2368,7 +2370,7 @@ export class Terminal extends EventEmitter implements ITerminal { * Helpers */ -function globalOn(el: HTMLElement | HTMLElement[], type: string, handler: (event: Event) => any, capture?: boolean) { +function globalOn(el: any, type: string, handler: (event: Event) => any, capture?: boolean) { if (!Array.isArray(el)) { el = [el]; } @@ -2379,8 +2381,8 @@ function globalOn(el: HTMLElement | HTMLElement[], type: string, handler: (event // TODO: Remove once everything is typed const on = globalOn; -function off(el, type, handler, capture) { - el.removeEventListener(type, handler, capture || false); +function off(el, type, handler, capture: boolean = false) { + el.removeEventListener(type, handler, capture); } function inherits(child, parent) { @@ -2392,7 +2394,7 @@ function inherits(child, parent) { } function indexOf(obj, el) { - var i = obj.length; + let i = obj.length; while (i--) { if (obj[i] === el) return i; } @@ -2462,4 +2464,4 @@ function keys(obj) { // Terminal.on = on; // Terminal.off = off; -module.exports = Terminal; +// module.exports = Terminal; From 8903bddb173c89da8f5c0f5435953855ea5696d1 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 5 Aug 2017 02:28:20 -0700 Subject: [PATCH 04/22] Fix a bunch of warnings, remove dead code --- src/Terminal.ts | 96 ++++++++++++++++++++++--------------------------- 1 file changed, 42 insertions(+), 54 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index 19f10e7dfe..509152b94d 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -47,36 +47,29 @@ declare var define: any; */ // Let it work inside Node.js for automated testing purposes. -var document = (typeof window != 'undefined') ? window.document : null; +const document = (typeof window !== 'undefined') ? window.document : null; /** * The amount of write requests to queue before sending an XOFF signal to the * pty process. This number must be small in order for ^C and similar sequences * to be responsive. */ -var WRITE_BUFFER_PAUSE_THRESHOLD = 5; +const WRITE_BUFFER_PAUSE_THRESHOLD = 5; /** * The number of writes to perform in a single batch before allowing the * renderer to catch up with a 0ms setTimeout. */ -var WRITE_BATCH_SIZE = 300; +const WRITE_BATCH_SIZE = 300; /** * The time between cursor blinks. This is driven by JS rather than a CSS * animation due to a bug in Chromium that causes it to use excessive CPU time. * See https://github.com/Microsoft/vscode/issues/22900 */ -var CURSOR_BLINK_INTERVAL = 600; - - - - - - - - +const CURSOR_BLINK_INTERVAL = 600; +// TODO: Most of the color code should be removed after truecolor is implemented // Colors 0-15 const tangoColors = [ // dark: @@ -135,13 +128,11 @@ const defaultColors = (function() { const _colors = defaultColors.slice(); const vcolors = (function() { - var out = [] - , colors = defaultColors - , i = 0 - , color; + const out = []; + let color; - for (; i < 256; i++) { - color = parseInt(colors[i].substring(1), 16); + for (let i = 0; i < 256; i++) { + color = parseInt(defaultColors[i].substring(1), 16); out.push([ (color >> 16) & 0xff, (color >> 8) & 0xff, @@ -287,7 +278,7 @@ export class Terminal extends EventEmitter implements ITerminal { private inputHandler: InputHandler; private parser: Parser; private renderer: Renderer; - public selectionManager: SelectionManager;; + public selectionManager: SelectionManager; private linkifier: Linkifier; public buffers: BufferSet; public buffer: Buffer; @@ -757,7 +748,7 @@ export class Terminal extends EventEmitter implements ITerminal { * Automatic focus functionality. * TODO: Default to `false` starting with xterm.js 3.0. */ - if (typeof focus == 'undefined') { + if (typeof focus === 'undefined') { let message = 'You did not pass the `focus` argument in `Terminal.prototype.open()`.\n'; message += 'The `focus` argument now defaults to `true` but starting with xterm.js 3.0 '; @@ -823,14 +814,16 @@ export class Terminal extends EventEmitter implements ITerminal { * BtnCode, EmitButtonCode, EditorButton, SendMousePosition */ public bindMouse() { - var el = this.element, self = this, pressed = 32; + const el = this.element; + const self = this; + let pressed = 32; // mouseup, mousedown, wheel // left click: ^[[M 3<^[[M#3< // wheel up: ^[[M`3> function sendButton(ev) { - var button - , pos; + let button; + let pos; // get the xterm-style button button = getButton(ev); @@ -861,8 +854,8 @@ export class Terminal extends EventEmitter implements ITerminal { // motion example of a left click: // ^[[M 3<^[[M@4<^[[M@5<^[[M@6<^[[M@7<^[[M#7< function sendMove(ev) { - let button = pressed - , pos; + let button = pressed; + let pos; pos = getRawByteCoords(ev, self.rowContainer, self.charMeasure, self.cols, self.rows); if (!pos) return; @@ -1052,7 +1045,7 @@ export class Terminal extends EventEmitter implements ITerminal { sendButton(ev); // fix for odd bug - //if (this.vt200Mouse && !this.normalMouse) { + // if (this.vt200Mouse && !this.normalMouse) { if (this.vt200Mouse) { (ev).overrideType = 'mouseup'; sendButton(ev); @@ -1164,7 +1157,7 @@ export class Terminal extends EventEmitter implements ITerminal { * line. */ public scroll(isWrapped?: boolean): void { - var row; + let row; // Make room for the new row in lines if (this.buffer.lines.length === this.buffer.lines.maxLength) { @@ -1301,10 +1294,9 @@ export class Terminal extends EventEmitter implements ITerminal { } private innerWrite() { - var writeBatch = this.writeBuffer.splice(0, WRITE_BATCH_SIZE); + const writeBatch = this.writeBuffer.splice(0, WRITE_BATCH_SIZE); while (writeBatch.length > 0) { - var data = writeBatch.shift(); - var l = data.length, i = 0, j, cs, ch, code, low, ch_width, row; + 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. @@ -1321,7 +1313,7 @@ export class Terminal extends EventEmitter implements ITerminal { // 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. - var state = this.parser.parse(data); + const state = this.parser.parse(data); this.parser.setState(state); this.updateRange(this.buffer.y); @@ -1329,10 +1321,7 @@ export class Terminal extends EventEmitter implements ITerminal { } if (this.writeBuffer.length > 0) { // Allow renderer to catch up before processing the next batch - var self = this; - setTimeout(function () { - self.innerWrite(); - }, 0); + setTimeout(() => this.innerWrite(), 0); } else { this.writeInProgress = false; } @@ -1412,7 +1401,7 @@ export class Terminal extends EventEmitter implements ITerminal { */ public registerLinkMatcher(regex, handler, options) { if (this.linkifier) { - var matcherId = this.linkifier.registerLinkMatcher(regex, handler, options); + const matcherId = this.linkifier.registerLinkMatcher(regex, handler, options); this.refresh(0, this.rows - 1); return matcherId; } @@ -1525,7 +1514,7 @@ export class Terminal extends EventEmitter implements ITerminal { * @param {KeyboardEvent} ev The keyboard event to be translated to key escape sequence. */ private evaluateKeyEscapeSequence(ev) { - var result = { + const result = { // Whether to cancel event propogation (NOTE: this may not be needed since the event is // canceled at the end of keyDown cancel: false, @@ -1534,7 +1523,7 @@ export class Terminal extends EventEmitter implements ITerminal { // The number of characters to scroll, if this is defined it will cancel the event scrollDisp: undefined }; - var modifiers = ev.shiftKey << 0 | ev.altKey << 1 | ev.ctrlKey << 2 | ev.metaKey << 3; + const modifiers = ev.shiftKey << 0 | ev.altKey << 1 | ev.ctrlKey << 2 | ev.metaKey << 3; switch (ev.keyCode) { case 8: // backspace @@ -1570,7 +1559,7 @@ export class Terminal extends EventEmitter implements ITerminal { // HACK: Make Alt + left-arrow behave like Ctrl + left-arrow: move one word backwards // http://unix.stackexchange.com/a/108106 // macOS uses different escape sequences than linux - if (result.key == C0.ESC + '[1;3D') { + if (result.key === C0.ESC + '[1;3D') { result.key = (this.browser.isMac) ? C0.ESC + 'b' : C0.ESC + '[1;5D'; } } else if (this.applicationCursor) { @@ -1586,7 +1575,7 @@ export class Terminal extends EventEmitter implements ITerminal { // HACK: Make Alt + right-arrow behave like Ctrl + right-arrow: move one word forward // http://unix.stackexchange.com/a/108106 // macOS uses different escape sequences than linux - if (result.key == C0.ESC + '[1;3C') { + if (result.key === C0.ESC + '[1;3C') { result.key = (this.browser.isMac) ? C0.ESC + 'f' : C0.ESC + '[1;5C'; } } else if (this.applicationCursor) { @@ -1601,7 +1590,7 @@ export class Terminal extends EventEmitter implements ITerminal { result.key = C0.ESC + '[1;' + (modifiers + 1) + 'A'; // HACK: Make Alt + up-arrow behave like Ctrl + up-arrow // http://unix.stackexchange.com/a/108106 - if (result.key == C0.ESC + '[1;3A') { + if (result.key === C0.ESC + '[1;3A') { result.key = C0.ESC + '[1;5A'; } } else if (this.applicationCursor) { @@ -1616,7 +1605,7 @@ export class Terminal extends EventEmitter implements ITerminal { result.key = C0.ESC + '[1;' + (modifiers + 1) + 'B'; // HACK: Make Alt + down-arrow behave like Ctrl + down-arrow // http://unix.stackexchange.com/a/108106 - if (result.key == C0.ESC + '[1;3B') { + if (result.key === C0.ESC + '[1;3B') { result.key = C0.ESC + '[1;5B'; } } else if (this.applicationCursor) { @@ -1808,7 +1797,7 @@ export class Terminal extends EventEmitter implements ITerminal { * Set the G level of the terminal * @param g */ - public setgLevel = function(g: number) { + public setgLevel(g: number): void { this.glevel = g; this.charset = this.charsets[g]; } @@ -1818,7 +1807,7 @@ export class Terminal extends EventEmitter implements ITerminal { * @param g * @param charset */ - public setgCharset(g: number, charset: Charset) { + public setgCharset(g: number, charset: Charset): void { this.charsets[g] = charset; if (this.glevel === g) { this.charset = charset; @@ -1832,7 +1821,7 @@ export class Terminal extends EventEmitter implements ITerminal { * @param {KeyboardEvent} ev The keypress event to be handled. */ private keyPress(ev) { - var key; + let key; if (this.customKeyEventHandler && this.customKeyEventHandler(ev) === false) { return false; @@ -1929,7 +1918,7 @@ export class Terminal extends EventEmitter implements ITerminal { this.setOption('scrollback', y); } - var line + let line , el , i , j @@ -2148,7 +2137,7 @@ export class Terminal extends EventEmitter implements ITerminal { this.buffer.ydisp = 0; this.buffer.ybase = 0; this.buffer.y = 0; - for (var i = 1; i < this.rows; i++) { + for (let i = 1; i < this.rows; i++) { this.buffer.lines.push(this.blankLine()); } this.refresh(0, this.rows - 1); @@ -2266,7 +2255,6 @@ export class Terminal extends EventEmitter implements ITerminal { * Move the cursor up one row, inserting a new blank line if necessary. */ public reverseIndex() { - var j; if (this.buffer.y === this.buffer.scrollTop) { // possibly move the code below to term.reverseScroll(); // test: echo -ne '\e[1;1H\e[44m\eM\e[0m' @@ -2286,10 +2274,10 @@ export class Terminal extends EventEmitter implements ITerminal { public reset() { this.options.rows = this.rows; this.options.cols = this.cols; - var customKeyEventHandler = this.customKeyEventHandler; - var cursorBlinkInterval = this.cursorBlinkInterval; - var inputHandler = this.inputHandler; - var buffers = this.buffers; + const customKeyEventHandler = this.customKeyEventHandler; + const cursorBlinkInterval = this.cursorBlinkInterval; + const inputHandler = this.inputHandler; + const buffers = this.buffers; Terminal.call(this, this.options); this.customKeyEventHandler = customKeyEventHandler; this.cursorBlinkInterval = cursorBlinkInterval; @@ -2319,7 +2307,7 @@ export class Terminal extends EventEmitter implements ITerminal { // Expose to InputHandler // TODO: Revise when truecolor is introduced. public matchColor(r1, g1, b1) { - var hash = (r1 << 16) | (g1 << 8) | b1; + const hash = (r1 << 16) | (g1 << 8) | b1; if (matchColorCache[hash] != null) { return matchColorCache[hash]; @@ -2402,7 +2390,7 @@ function indexOf(obj, el) { } function isThirdLevelShift(term, ev) { - var thirdLevelKey = + const thirdLevelKey = (term.browser.isMac && ev.altKey && !ev.ctrlKey && !ev.metaKey) || (term.browser.isMSWindows && ev.altKey && ev.ctrlKey && !ev.metaKey); From 7d1c5d1ac6f660c800c9c8de9943cfe9dff41d83 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 5 Aug 2017 02:34:21 -0700 Subject: [PATCH 05/22] Fix tests --- src/Buffer.test.ts | 4 +++- src/BufferSet.test.ts | 4 +++- src/Terminal.ts | 2 +- src/Viewport.test.ts | 4 +++- src/test/escape-sequences-test.js | 2 +- 5 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index c4c3a267d5..9f0d84462d 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -14,7 +14,9 @@ describe('Buffer', () => { terminal = { cols: 80, rows: 24, - scrollback: 1000 + options: { + scrollback: 1000 + } }; buffer = new Buffer(terminal); }); diff --git a/src/BufferSet.test.ts b/src/BufferSet.test.ts index 2101fbc1f4..19d943b9a1 100644 --- a/src/BufferSet.test.ts +++ b/src/BufferSet.test.ts @@ -14,7 +14,9 @@ describe('BufferSet', () => { terminal = { cols: 80, rows: 24, - scrollback: 1000 + options: { + scrollback: 1000 + } }; bufferSet = new BufferSet(terminal); }); diff --git a/src/Terminal.ts b/src/Terminal.ts index 509152b94d..839ad5680f 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -779,7 +779,7 @@ export class Terminal extends EventEmitter implements ITerminal { * @param {string} addon The name of the addon to load * @static */ - public loadAddon(addon, callback) { + public static loadAddon(addon, callback) { if (typeof exports === 'object' && typeof module === 'object') { // CommonJS return require('./addons/' + addon + '/' + addon); diff --git a/src/Viewport.test.ts b/src/Viewport.test.ts index 6d09ea86a2..7c89da29f1 100644 --- a/src/Viewport.test.ts +++ b/src/Viewport.test.ts @@ -26,7 +26,9 @@ describe('Viewport', () => { height: 0 } }, - scrollback: 10 + options: { + scrollback: 10 + } }; terminal.buffers = new BufferSet(terminal); terminal.buffer = terminal.buffers.active; diff --git a/src/test/escape-sequences-test.js b/src/test/escape-sequences-test.js index b4a53a17c3..d0447451b4 100644 --- a/src/test/escape-sequences-test.js +++ b/src/test/escape-sequences-test.js @@ -80,7 +80,7 @@ describe('xterm output comparison', function() { var xterm; beforeEach(function () { - xterm = new Terminal(COLS, ROWS); + xterm = new Terminal({ cols: COLS, rows: ROWS }); xterm.refresh = function() {}; xterm.viewport = { syncScrollArea: function() {} From e6b431d92f20a48421e5721f77c7f33f4906989b Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 5 Aug 2017 02:42:49 -0700 Subject: [PATCH 06/22] Add one-variable-per-declaration tslint rule Multiple declarations per line makes it more difficult to read what exactly is declared and generally makes code more verbose and less likely to fill in types --- src/InputHandler.ts | 66 +++++++++++++++------------------- src/Parser.ts | 7 +++- src/Terminal.ts | 32 ++++++++--------- src/handlers/Clipboard.test.ts | 6 ++-- tslint.json | 4 +++ 5 files changed, 57 insertions(+), 58 deletions(-) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 64da3b3f15..e119df32c9 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -189,14 +189,12 @@ export class InputHandler implements IInputHandler { * Insert Ps (Blank) Character(s) (default = 1) (ICH). */ public insertChars(params: number[]): void { - let param, row, j, ch; - - param = params[0]; + let param = params[0]; if (param < 1) param = 1; - row = this._terminal.buffer.y + this._terminal.buffer.ybase; - j = this._terminal.buffer.x; - ch = [this._terminal.eraseAttr(), ' ', 1]; // xterm + const row = this._terminal.buffer.y + this._terminal.buffer.ybase; + let j = this._terminal.buffer.x; + const ch = [this._terminal.eraseAttr(), ' ', 1]; // xterm while (param-- && j < this._terminal.cols) { this._terminal.buffer.lines.get(row).splice(j++, 0, ch); @@ -325,9 +323,8 @@ export class InputHandler implements IInputHandler { * Cursor Position [row;column] (default = [1,1]) (CUP). */ public cursorPosition(params: number[]): void { - let row, col; - - row = params[0] - 1; + let col: number; + let row: number = params[0] - 1; if (params.length >= 2) { col = params[1] - 1; @@ -439,14 +436,13 @@ export class InputHandler implements IInputHandler { * Insert Ps Line(s) (default = 1) (IL). */ public insertLines(params: number[]): void { - let param, row, j; - - param = params[0]; + let param: number = params[0]; if (param < 1) { param = 1; } - row = this._terminal.buffer.y + this._terminal.buffer.ybase; + let row: number = this._terminal.buffer.y + this._terminal.buffer.ybase; + let j: number; j = this._terminal.rows - 1 - this._terminal.buffer.scrollBottom; j = this._terminal.rows - 1 + this._terminal.buffer.ybase - j + 1; @@ -475,14 +471,13 @@ export class InputHandler implements IInputHandler { * Delete Ps Line(s) (default = 1) (DL). */ public deleteLines(params: number[]): void { - let param, row, j; - - param = params[0]; + let param = params[0]; if (param < 1) { param = 1; } - row = this._terminal.buffer.y + this._terminal.buffer.ybase; + const row: number = this._terminal.buffer.y + this._terminal.buffer.ybase; + let j: number; j = this._terminal.rows - 1 - this._terminal.buffer.scrollBottom; j = this._terminal.rows - 1 + this._terminal.buffer.ybase - j; @@ -509,15 +504,13 @@ export class InputHandler implements IInputHandler { * Delete Ps Character(s) (default = 1) (DCH). */ public deleteChars(params: number[]): void { - let param, row, ch; - - param = params[0]; + let param: number = params[0]; if (param < 1) { param = 1; } - row = this._terminal.buffer.y + this._terminal.buffer.ybase; - ch = [this._terminal.eraseAttr(), ' ', 1]; // xterm + const row = this._terminal.buffer.y + this._terminal.buffer.ybase; + const ch = [this._terminal.eraseAttr(), ' ', 1]; // xterm while (param--) { this._terminal.buffer.lines.get(row).splice(this._terminal.buffer.x, 1); @@ -558,16 +551,14 @@ export class InputHandler implements IInputHandler { * Erase Ps Character(s) (default = 1) (ECH). */ public eraseChars(params: number[]): void { - let param, row, j, ch; - - param = params[0]; + let param = params[0]; if (param < 1) { param = 1; } - row = this._terminal.buffer.y + this._terminal.buffer.ybase; - j = this._terminal.buffer.x; - ch = [this._terminal.eraseAttr(), ' ', 1]; // xterm + const row = this._terminal.buffer.y + this._terminal.buffer.ybase; + let j = this._terminal.buffer.x; + const ch = [this._terminal.eraseAttr(), ' ', 1]; // xterm while (param-- && j < this._terminal.cols) { this._terminal.buffer.lines.get(row)[j++] = ch; @@ -619,9 +610,9 @@ export class InputHandler implements IInputHandler { * CSI Ps b Repeat the preceding graphic character Ps times (REP). */ public repeatPrecedingCharacter(params: number[]): void { - let param = params[0] || 1 - , line = this._terminal.buffer.lines.get(this._terminal.buffer.ybase + this._terminal.buffer.y) - , ch = line[this._terminal.buffer.x - 1] || [this._terminal.defAttr, ' ', 1]; + let param = params[0] || 1; + const line = this._terminal.buffer.lines.get(this._terminal.buffer.ybase + this._terminal.buffer.y); + const ch = line[this._terminal.buffer.x - 1] || [this._terminal.defAttr, ' ', 1]; while (param--) { line[this._terminal.buffer.x++] = ch; @@ -1203,14 +1194,13 @@ export class InputHandler implements IInputHandler { return; } - let l = params.length - , i = 0 - , flags = this._terminal.curAttr >> 18 - , fg = (this._terminal.curAttr >> 9) & 0x1ff - , bg = this._terminal.curAttr & 0x1ff - , p; + const l = params.length; + let flags = this._terminal.curAttr >> 18; + let fg = (this._terminal.curAttr >> 9) & 0x1ff; + let bg = this._terminal.curAttr & 0x1ff; + let p; - for (; i < l; i++) { + for (let i = 0; i < l; i++) { p = params[i]; if (p >= 30 && p <= 37) { // fg color 8 diff --git a/src/Parser.ts b/src/Parser.ts index 1c2fb6b038..bec0af7b59 100644 --- a/src/Parser.ts +++ b/src/Parser.ts @@ -182,7 +182,12 @@ export class Parser { * @param data The data to parse. */ public parse(data: string): ParserState { - let l = data.length, j, cs, ch, code, low; + const l = data.length; + let j; + let cs; + let ch; + let code; + let low; if (this._terminal.debug) { this._terminal.log('data: ' + data); diff --git a/src/Terminal.ts b/src/Terminal.ts index 839ad5680f..e163303322 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -95,9 +95,9 @@ const tangoColors = [ // Colors 0-15 + 16-255 // Much thanks to TooTallNate for writing this. const defaultColors = (function() { - let colors = tangoColors.slice() - , r = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff] - , i; + let colors = tangoColors.slice(); + let r = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff]; + let i; // 16-231 i = 0; @@ -971,11 +971,11 @@ export class Terminal extends EventEmitter implements ITerminal { } function getButton(ev) { - let button - , shift - , meta - , ctrl - , mod; + let button; + let shift; + let meta; + let ctrl; + let mod; // two low bits: // 0 = left @@ -1918,12 +1918,12 @@ export class Terminal extends EventEmitter implements ITerminal { this.setOption('scrollback', y); } - let line - , el - , i - , j - , ch - , addToY; + let line; + let el; + let i; + let j; + let ch; + let addToY; if (x === this.cols && y === this.rows) { // Check if we still need to measure the char size (fixes #785). @@ -2426,8 +2426,8 @@ function wasMondifierKeyOnlyEvent(ev) { function keys(obj) { if (Object.keys) return Object.keys(obj); - let key, keys = []; - for (key in obj) { + const keys = []; + for (let key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { keys.push(key); } diff --git a/src/handlers/Clipboard.test.ts b/src/handlers/Clipboard.test.ts index 740bee3d40..a91ab87cae 100644 --- a/src/handlers/Clipboard.test.ts +++ b/src/handlers/Clipboard.test.ts @@ -4,9 +4,9 @@ import * as Clipboard from './Clipboard'; describe('evaluatePastedTextProcessing', function () { it('should replace carriage return + line feed with line feed on windows', function () { - const pastedText = 'foo\r\nbar\r\n', - processedText = Clipboard.prepareTextForTerminal(pastedText, false), - windowsProcessedText = Clipboard.prepareTextForTerminal(pastedText, true); + const pastedText = 'foo\r\nbar\r\n'; + const processedText = Clipboard.prepareTextForTerminal(pastedText, false); + const windowsProcessedText = Clipboard.prepareTextForTerminal(pastedText, true); assert.equal(processedText, 'foo\r\nbar\r\n'); assert.equal(windowsProcessedText, 'foo\rbar\r'); diff --git a/tslint.json b/tslint.json index d571d9c5c9..f98fd4c4c0 100644 --- a/tslint.json +++ b/tslint.json @@ -13,6 +13,10 @@ "no-eval": true, "no-internal-module": true, "no-trailing-whitespace": true, + "one-variable-per-declaration": [ + true, + true + ], "no-unsafe-finally": true, "no-var-keyword": true, "quotemark": [ From da50e33aa8c9383f630c83c3defb26696292df0b Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 5 Aug 2017 02:50:44 -0700 Subject: [PATCH 07/22] Finish types on Terminal's properties --- src/Terminal.ts | 35 +++++++++++++---------------------- 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index e163303322..ebb0c68787 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -163,13 +163,6 @@ const DEFAULT_OPTIONS: TerminalOptions = { // focusKeys: false, }; - - - - - - - export class Terminal extends EventEmitter implements ITerminal { public textarea: HTMLTextAreaElement; public element: HTMLElement; @@ -223,24 +216,22 @@ export class Terminal extends EventEmitter implements ITerminal { private charsets: Charset[] = [null]; // mouse properties - private decLocator; - private x10Mouse; - private vt200Mouse; - private vt300Mouse; - private normalMouse; - private mouseEvents; - private sendFocus; - private utfMouse; - private sgrMouse; - private urxvtMouse; + private decLocator: boolean; // This is unstable and never set + private x10Mouse: boolean; + private vt200Mouse: boolean; + private vt300Mouse: boolean; // This is unstable and never set + private normalMouse: boolean; + private mouseEvents: boolean; + private sendFocus: boolean; + private utfMouse: boolean; + private sgrMouse: boolean; + private urxvtMouse: boolean; // misc public children: HTMLElement[]; - private refreshStart; - private refreshEnd; - private savedX; - private savedY; - private savedCols; + private refreshStart: number; + private refreshEnd: number; + private savedCols: boolean; // stream private readable: boolean = true; From 0b5ff47cc0f92d5fba77baa85b30adb74d37aa8e Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 5 Aug 2017 02:57:34 -0700 Subject: [PATCH 08/22] Give terminal options a proper interface --- src/Interfaces.ts | 21 +++++++++++++++++++++ src/Terminal.ts | 19 ++++++++++--------- src/Types.ts | 2 -- 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/src/Interfaces.ts b/src/Interfaces.ts index c24aa2eff1..98c7626a80 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -50,6 +50,27 @@ export interface ITerminal { showCursor(): void; } +export interface ITerminalOptions { + cancelEvents?: boolean; + colors?: string[]; + cols?: number; + convertEol?: boolean; + cursorBlink?: boolean; + cursorStyle?: string; + debug?: boolean; + disableStdin?: boolean; + geometry?: [number, number]; + handler?: (data: string) => void; + popOnBell?: boolean; + rows?: number; + screenKeys?: boolean; + scrollback?: number; + tabStopWidth?: number; + termName?: string; + useFlowControl?: boolean; + visualBell?: boolean; +} + export interface IBuffer { lines: ICircularList<[number, string, number][]>; ydisp: number; diff --git a/src/Terminal.ts b/src/Terminal.ts index ebb0c68787..79b5972ff7 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -29,8 +29,8 @@ import * as Mouse from './utils/Mouse'; import { CHARSETS } from './Charsets'; import { getRawByteCoords } from './utils/Mouse'; import { translateBufferLineToString } from './utils/BufferLine'; -import { TerminalOptions, CustomKeyEventHandler, Charset } from './Types'; -import { ITerminal, IBrowser } from './Interfaces'; +import { CustomKeyEventHandler, Charset } from './Types'; +import { ITerminal, IBrowser, ITerminalOptions } from './Interfaces'; // Declare for RequireJS in loadAddon declare var define: any; @@ -71,7 +71,7 @@ const CURSOR_BLINK_INTERVAL = 600; // TODO: Most of the color code should be removed after truecolor is implemented // Colors 0-15 -const tangoColors = [ +const tangoColors: string[] = [ // dark: '#2e3436', '#cc0000', @@ -94,7 +94,7 @@ const tangoColors = [ // Colors 0-15 + 16-255 // Much thanks to TooTallNate for writing this. -const defaultColors = (function() { +const defaultColors: string[] = (function() { let colors = tangoColors.slice(); let r = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff]; let i; @@ -125,9 +125,9 @@ const defaultColors = (function() { return colors; })(); -const _colors = defaultColors.slice(); +const _colors: string[] = defaultColors.slice(); -const vcolors = (function() { +const vcolors: number[][] = (function() { const out = []; let color; @@ -143,7 +143,7 @@ const vcolors = (function() { return out; })(); -const DEFAULT_OPTIONS: TerminalOptions = { +const DEFAULT_OPTIONS: ITerminalOptions = { colors: defaultColors, convertEol: false, termName: 'xterm', @@ -186,7 +186,7 @@ export class Terminal extends EventEmitter implements ITerminal { public browser: IBrowser = Browser; // TODO: Options should be private, remove from interface in favor of getOption - public options: TerminalOptions; + 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 @@ -294,7 +294,7 @@ export class Terminal extends EventEmitter implements ITerminal { * @alias module:xterm/src/xterm */ constructor( - options: any = {} + options: ITerminalOptions = {} ) { super(); @@ -2269,6 +2269,7 @@ export class Terminal extends EventEmitter implements ITerminal { const cursorBlinkInterval = this.cursorBlinkInterval; const inputHandler = this.inputHandler; const buffers = this.buffers; + // TODO: Need to make sure this still works Terminal.call(this, this.options); this.customKeyEventHandler = customKeyEventHandler; this.cursorBlinkInterval = cursorBlinkInterval; diff --git a/src/Types.ts b/src/Types.ts index 9e4b1e1a9a..34c6d94a05 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -13,7 +13,5 @@ export type LinkMatcher = { export type LinkMatcherHandler = (event: MouseEvent, uri: string) => boolean | void; export type LinkMatcherValidationCallback = (uri: string, element: HTMLElement, callback: (isValid: boolean) => void) => void; -// TODO: Make this type more specific -export type TerminalOptions = {[key: string]: any}; export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean; export type Charset = {[key: string]: string}; From ad25905783bc8de772139c1d11b89687fc5186a1 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 5 Aug 2017 11:20:39 -0700 Subject: [PATCH 09/22] Fix reset --- src/Interfaces.ts | 4 +- src/Terminal.ts | 155 +++++++++++++++++++++++++++------------------- 2 files changed, 95 insertions(+), 64 deletions(-) diff --git a/src/Interfaces.ts b/src/Interfaces.ts index 98c7626a80..8851edac13 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -3,7 +3,7 @@ */ import { LinkMatcherOptions } from './Interfaces'; -import { LinkMatcherHandler, LinkMatcherValidationCallback, TerminalOptions } from './Types'; +import { LinkMatcherHandler, LinkMatcherValidationCallback } from './Types'; export interface IBrowser { isNode: boolean; @@ -32,7 +32,7 @@ export interface ITerminal { cursorHidden: boolean; cursorState: number; defAttr: number; - options: TerminalOptions; + options: ITerminalOptions; buffers: IBufferSet; buffer: IBuffer; diff --git a/src/Terminal.ts b/src/Terminal.ts index 79b5972ff7..a7aabcd78c 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -190,30 +190,30 @@ export class Terminal extends EventEmitter implements ITerminal { 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 = 0; - public cursorHidden: boolean = false; + public cursorState: number; + public cursorHidden: boolean; private convertEol: boolean; // TODO: This is the data queue for send, improve name and documentation - private queue: string = ''; - private customKeyEventHandler: CustomKeyEventHandler = null; + private queue: string; + private customKeyEventHandler: CustomKeyEventHandler; // The ID from a setInterval that tracks the blink animation. This animation // is done in JS due to a Chromium bug with CSS animations that thrashed the // CPU. - private cursorBlinkInterval: NodeJS.Timer = null; + private cursorBlinkInterval: NodeJS.Timer; // modes - private applicationKeypad: boolean = false; - private applicationCursor: boolean = false; - private originMode: boolean = false; - private insertMode: boolean = false; - private wraparoundMode: boolean = true; // defaults: xterm - true, vt100 - false + private applicationKeypad: boolean; + private applicationCursor: boolean; + private originMode: boolean; + private insertMode: boolean; + private wraparoundMode: boolean; // defaults: xterm - true, vt100 - false // charset // The current charset - private charset: Charset = null; - private gcharset: number = null; - private glevel: number = 0; - private charsets: Charset[] = [null]; + private charset: Charset; + private gcharset: number; + private glevel: number; + private charsets: Charset[]; // mouse properties private decLocator: boolean; // This is unstable and never set @@ -234,20 +234,20 @@ export class Terminal extends EventEmitter implements ITerminal { private savedCols: boolean; // stream - private readable: boolean = true; - private writable: boolean = true; + private readable: boolean; + private writable: boolean; - public defAttr: number = (0 << 18) | (257 << 9) | (256 << 0); - public curAttr: number = (0 << 18) | (257 << 9) | (256 << 0); + public defAttr: number; + public curAttr: number; - public params: (string | number)[] = []; - public currentParam: string | number = 0; - public prefix: string = ''; - public postfix: string = ''; + public params: (string | number)[]; + public currentParam: string | number; + public prefix: string; + public postfix: string; // user input states - public writeBuffer: string[] = []; - private writeInProgress: boolean = false; + public writeBuffer: string[]; + private writeInProgress: boolean; /** * Whether _xterm.js_ sent XOFF in order to catch up with the pty process. @@ -255,16 +255,16 @@ export class Terminal extends EventEmitter implements ITerminal { * XOFF via ^S that it will not automatically resume when the writeBuffer goes * below threshold. */ - private xoffSentToCatchUp: boolean = false; + private xoffSentToCatchUp: boolean; /** Whether writing has been stopped as a result of XOFF */ - private writeStopped: boolean = false; + private writeStopped: boolean; // leftover surrogate high from previous write invocation - private surrogate_high: string = ''; + private surrogate_high: string; // Store if user went browsing history in scrollback - private userScrolling: boolean = false; + private userScrolling: boolean; private inputHandler: InputHandler; private parser: Parser; @@ -297,56 +297,88 @@ export class Terminal extends EventEmitter implements ITerminal { options: ITerminalOptions = {} ) { super(); + this.options = options; + this.setup(); + } - let self: any = this; - - // TODO: Can this be removed? - if (!(this instanceof Terminal)) { - return new Terminal({ - cols: arguments[0], - rows: arguments[1], - handler: arguments[2] - }); - } - - Object.keys(DEFAULT_OPTIONS).forEach(function(key) { - if (options[key] == null) { - options[key] = DEFAULT_OPTIONS[key]; + private setup(): void { + Object.keys(DEFAULT_OPTIONS).forEach((key) => { + if (this.options[key] == null) { + this.options[key] = DEFAULT_OPTIONS[key]; } // TODO: We should move away from duplicate options on the Terminal object - self[key] = options[key]; + self[key] = this.options[key]; }); - if (options.colors.length === 8) { - options.colors = options.colors.concat(_colors.slice(8)); - } else if (options.colors.length === 16) { - options.colors = options.colors.concat(_colors.slice(16)); - } else if (options.colors.length === 10) { - options.colors = options.colors.slice(0, -2).concat( - _colors.slice(8, -2), options.colors.slice(-2)); - } else if (options.colors.length === 18) { - options.colors = options.colors.concat( - _colors.slice(16, -2), options.colors.slice(-2)); + if (this.options.colors.length === 8) { + this.options.colors = this.options.colors.concat(_colors.slice(8)); + } else if (this.options.colors.length === 16) { + this.options.colors = this.options.colors.concat(_colors.slice(16)); + } else if (this.options.colors.length === 10) { + this.options.colors = this.options.colors.slice(0, -2).concat( + _colors.slice(8, -2), this.options.colors.slice(-2)); + } else if (this.options.colors.length === 18) { + this.options.colors = this.options.colors.concat( + _colors.slice(16, -2), this.options.colors.slice(-2)); } - this.options = options; - this.colors = options.colors; + this.colors = this.options.colors; // this.context = options.context || window; // this.document = options.document || document; // TODO: WHy not document.body? this.parent = document ? document.getElementsByTagName('body')[0] : null; - this.cols = options.cols || options.geometry[0]; - this.rows = options.rows || options.geometry[1]; + this.cols = this.options.cols || this.options.geometry[0]; + this.rows = this.options.rows || this.options.geometry[1]; this.geometry = [this.cols, this.rows]; - if (options.handler) { - this.on('data', options.handler); + if (this.options.handler) { + this.on('data', this.options.handler); } + this.cursorState = 0; + this.cursorHidden = false; + this.queue = ''; + this.customKeyEventHandler = null; + this.cursorBlinkInterval = null; + + // modes + this.applicationKeypad = false; + this.applicationCursor = false; + this.originMode = false; + this.insertMode = false; + this.wraparoundMode = true; // defaults: xterm - true, vt100 - false + + // charset + this.charset = null; + this.gcharset = null; + this.glevel = 0; + // 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); + + this.params = []; + this.currentParam = 0; + this.prefix = ''; + this.postfix = ''; + + // user input states + this.writeBuffer = []; + this.writeInProgress = false; + + this.xoffSentToCatchUp = false; + this.writeStopped = false; + this.surrogate_high = ''; + this.userScrolling = false; + this.inputHandler = new InputHandler(this); this.parser = new Parser(this.inputHandler, this); - // Reuse renderer if the Terminal is being recreated via a Terminal.reset call. + // Reuse renderer if the Terminal is being recreated via a reset call. this.renderer = this.renderer || null; this.selectionManager = this.selectionManager || null; this.linkifier = this.linkifier || new Linkifier(); @@ -2269,8 +2301,7 @@ export class Terminal extends EventEmitter implements ITerminal { const cursorBlinkInterval = this.cursorBlinkInterval; const inputHandler = this.inputHandler; const buffers = this.buffers; - // TODO: Need to make sure this still works - Terminal.call(this, this.options); + this.setup(); this.customKeyEventHandler = customKeyEventHandler; this.cursorBlinkInterval = cursorBlinkInterval; this.inputHandler = inputHandler; From 8a0bcf6b792dc3dd74c726978bdc2dab972559e6 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 5 Aug 2017 11:28:24 -0700 Subject: [PATCH 10/22] Clean up --- src/Terminal.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index a7aabcd78c..6cc8864e12 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -168,8 +168,9 @@ export class Terminal extends EventEmitter implements ITerminal { public element: HTMLElement; public rowContainer: HTMLElement; - // TODO: Do we need a reference to body? There is also a body variable? - // The body of the terminal + /** + * The HTMLElement that the terminal is created in, set by Terminal.open. + */ private parent: HTMLElement; private context: Window; private document: Document; @@ -307,7 +308,7 @@ export class Terminal extends EventEmitter implements ITerminal { this.options[key] = DEFAULT_OPTIONS[key]; } // TODO: We should move away from duplicate options on the Terminal object - self[key] = this.options[key]; + this[key] = this.options[key]; }); if (this.options.colors.length === 8) { @@ -326,7 +327,7 @@ export class Terminal extends EventEmitter implements ITerminal { // this.context = options.context || window; // this.document = options.document || document; // TODO: WHy not document.body? - this.parent = document ? document.getElementsByTagName('body')[0] : null; + this.parent = document ? document.body : null; this.cols = this.options.cols || this.options.geometry[0]; this.rows = this.options.rows || this.options.geometry[1]; @@ -671,7 +672,7 @@ export class Terminal extends EventEmitter implements ITerminal { // Grab global elements this.context = this.parent.ownerDocument.defaultView; this.document = this.parent.ownerDocument; - this.body = this.document.getElementsByTagName('body')[0]; + this.body = this.document.body; // Create main element container this.element = this.document.createElement('div'); @@ -763,8 +764,7 @@ export class Terminal extends EventEmitter implements ITerminal { // Setup loop that draws to screen this.refresh(0, this.rows - 1); - // Initialize global actions that - // need to be taken on the document. + // Initialize global actions that need to be taken on the document. this.initGlobal(); /** From aee474b53e36f559bbf854350018b54576a0905a Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 5 Aug 2017 12:57:29 -0700 Subject: [PATCH 11/22] Type InputHandler._terminal --- src/Buffer.ts | 4 +- src/InputHandler.test.ts | 5 ++- src/InputHandler.ts | 20 ++++----- src/Interfaces.ts | 80 ++++++++++++++++++++++++++++++++-- src/SelectionManager.ts | 4 +- src/Terminal.ts | 92 +++++++++++++++++++--------------------- src/Viewport.ts | 4 +- 7 files changed, 139 insertions(+), 70 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index b76cdc4d7a..dc0bc24f40 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -2,7 +2,7 @@ * @license MIT */ -import { ITerminal } from './Interfaces'; +import { ITerminal, IBuffer } from './Interfaces'; import { CircularList } from './utils/CircularList'; /** @@ -12,7 +12,7 @@ import { CircularList } from './utils/CircularList'; * - cursor position * - scroll position */ -export class Buffer { +export class Buffer implements IBuffer { public lines: CircularList<[number, string, number][]>; public savedY: number; diff --git a/src/InputHandler.test.ts b/src/InputHandler.test.ts index 158c07c9fe..9562429601 100644 --- a/src/InputHandler.test.ts +++ b/src/InputHandler.test.ts @@ -5,7 +5,8 @@ import { wcwidth } from './InputHandler'; describe('InputHandler', () => { describe('save and restore cursor', () => { let terminal = { buffer: { x: 1, y: 2 } }; - let inputHandler = new InputHandler(terminal); + // TODO: Create proper mock IInputHandlingTerminal test util object + let inputHandler = new InputHandler(terminal); // Save cursor position inputHandler.saveCursor([]); assert.equal(terminal.buffer.x, 1); @@ -24,7 +25,7 @@ describe('InputHandler', () => { let terminal = { setOption: (option, value) => options[option] = value }; - let inputHandler = new InputHandler(terminal); + let inputHandler = new InputHandler(terminal); inputHandler.setCursorStyle([0]); assert.equal(options['cursorStyle'], 'block'); diff --git a/src/InputHandler.ts b/src/InputHandler.ts index e119df32c9..8f566cd9a1 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -2,7 +2,7 @@ * @license MIT */ -import { IInputHandler, ITerminal } from './Interfaces'; +import { IInputHandler, ITerminal, IInputHandlingTerminal } from './Interfaces'; import { C0 } from './EscapeSequences'; import { DEFAULT_CHARSET } from './Charsets'; @@ -15,7 +15,7 @@ import { DEFAULT_CHARSET } from './Charsets'; */ export class InputHandler implements IInputHandler { // TODO: We want to type _terminal when it's pulled into TS - constructor(private _terminal: any) { } + constructor(private _terminal: IInputHandlingTerminal) { } public addChar(char: string, code: number): void { if (char >= ' ') { @@ -61,7 +61,7 @@ export class InputHandler implements IInputHandler { } else { // The line already exists (eg. the initial viewport), mark it as a // wrapped line - this._terminal.buffer.lines.get(this._terminal.buffer.y).isWrapped = true; + (this._terminal.buffer.lines.get(this._terminal.buffer.y)).isWrapped = true; } } else { if (ch_width === 2) // FIXME: check for xterm behavior @@ -105,12 +105,12 @@ export class InputHandler implements IInputHandler { * Bell (Ctrl-G). */ public bell(): void { - if (!this._terminal.visualBell) { + if (!this._terminal.options.visualBell) { return; } this._terminal.element.style.borderColor = 'white'; setTimeout(() => this._terminal.element.style.borderColor = '', 10); - if (this._terminal.popOnBell) { + if (this._terminal.options.popOnBell) { this._terminal.focus(); } } @@ -194,7 +194,7 @@ export class InputHandler implements IInputHandler { const row = this._terminal.buffer.y + this._terminal.buffer.ybase; let j = this._terminal.buffer.x; - const ch = [this._terminal.eraseAttr(), ' ', 1]; // xterm + const ch: [number, string, number] = [this._terminal.eraseAttr(), ' ', 1]; // xterm while (param-- && j < this._terminal.cols) { this._terminal.buffer.lines.get(row).splice(j++, 0, ch); @@ -510,7 +510,7 @@ export class InputHandler implements IInputHandler { } const row = this._terminal.buffer.y + this._terminal.buffer.ybase; - const ch = [this._terminal.eraseAttr(), ' ', 1]; // xterm + const ch: [number, string, number] = [this._terminal.eraseAttr(), ' ', 1]; // xterm while (param--) { this._terminal.buffer.lines.get(row).splice(this._terminal.buffer.x, 1); @@ -558,7 +558,7 @@ export class InputHandler implements IInputHandler { const row = this._terminal.buffer.y + this._terminal.buffer.ybase; let j = this._terminal.buffer.x; - const ch = [this._terminal.eraseAttr(), ' ', 1]; // xterm + const ch: [number, string, number] = [this._terminal.eraseAttr(), ' ', 1]; // xterm while (param-- && j < this._terminal.cols) { this._terminal.buffer.lines.get(row)[j++] = ch; @@ -858,7 +858,7 @@ export class InputHandler implements IInputHandler { this._terminal.insertMode = true; break; case 20: - // this._terminal.convertEol = true; + // this._t.convertEol = true; break; } } else if (this._terminal.prefix === '?') { @@ -1050,7 +1050,7 @@ export class InputHandler implements IInputHandler { this._terminal.insertMode = false; break; case 20: - // this._terminal.convertEol = false; + // this._t.convertEol = false; break; } } else if (this._terminal.prefix === '?') { diff --git a/src/Interfaces.ts b/src/Interfaces.ts index 8851edac13..b61c0057f6 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -3,7 +3,7 @@ */ import { LinkMatcherOptions } from './Interfaces'; -import { LinkMatcherHandler, LinkMatcherValidationCallback } from './Types'; +import { LinkMatcherHandler, LinkMatcherValidationCallback, Charset } from './Types'; export interface IBrowser { isNode: boolean; @@ -17,7 +17,7 @@ export interface IBrowser { isMSWindows: boolean; } -export interface ITerminal { +export interface ITerminal extends IEventEmitter { element: HTMLElement; rowContainer: HTMLElement; selectionContainer: HTMLElement; @@ -45,11 +45,73 @@ export interface ITerminal { scrollDisp(disp: number, suppressScrollEvent: boolean); cancel(ev: Event, force?: boolean); log(text: string): void; - emit(event: string, data: any); reset(): void; showCursor(): void; } +/** + * This interface encapsulates everything needed from the Terminal by the + * InputHandler. This cleanly separates the large amount of methods needed by + * InputHandler cleanly from the ITerminal interface. + */ +export interface IInputHandlingTerminal extends IEventEmitter { + element: HTMLElement; + options: ITerminalOptions; + cols: number; + rows: number; + charset: Charset; + gcharset: number; + glevel: number; + charsets: Charset[]; + applicationKeypad: boolean; + applicationCursor: boolean; + originMode: boolean; + insertMode: boolean; + wraparoundMode: boolean; + defAttr: number; + curAttr: number; + prefix: string; + savedCols: number; + x10Mouse: boolean; + vt200Mouse: boolean; + normalMouse: boolean; + mouseEvents: boolean; + sendFocus: boolean; + utfMouse: boolean; + sgrMouse: boolean; + urxvtMouse: boolean; + cursorHidden: boolean; + + buffers: IBufferSet; + buffer: IBuffer; + viewport: IViewport; + selectionManager: ISelectionManager; + + focus(): void; + convertEol: boolean; + updateRange(y: number): void; + scroll(isWrapped?: boolean): void; + nextStop(x?: number): number; + setgLevel(g: number): void; + eraseAttr(): any; + eraseRight(x: number, y: number): void; + eraseLine(y: number): void; + eraseLeft(x: number, y: number): void; + blankLine(cur?: boolean, isWrapped?: boolean): [number, string, number][]; + prevStop(x?: number): number; + is(term: string): boolean; + send(data: string): void; + setgCharset(g: number, charset: Charset): void; + resize(x: number, y: number): void; + log(text: string, data?: any): void; + reset(): void; + showCursor(): void; + refresh(start: number, end: number): void; + matchColor(r1, g1, b1): any; + error(text: string, data?: any): void; + setOption(key: string, value: any): void; +} + export interface ITerminalOptions { cancelEvents?: boolean; colors?: string[]; @@ -78,6 +140,10 @@ export interface IBuffer { y: number; x: number; tabs: any; + scrollBottom: number; + scrollTop: number; + savedY: number; + savedX: number; } export interface IBufferSet { @@ -89,11 +155,18 @@ export interface IBufferSet { activateAltBuffer(): void; } +export interface IViewport { + syncScrollArea(): void; +} + export interface ISelectionManager { selectionText: string; selectionStart: [number, number]; selectionEnd: [number, number]; + disable(): void; + enable(): void; + setBuffer(buffer: ICircularList<[number, string, number][]>): void; setSelection(row: number, col: number, length: number); } @@ -127,6 +200,7 @@ export interface ICircularList extends IEventEmitter { export interface IEventEmitter { on(type, listener): void; off(type, listener): void; + emit(type: string, data?: any): void; } export interface LinkMatcherOptions { diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index f6fb44da58..bf594f7045 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -133,7 +133,7 @@ export class SelectionManager extends EventEmitter { * Disables the selection manager. This is useful for when terminal mouse * are enabled. */ - public disable() { + public disable(): void { this.clearSelection(); this._enabled = false; } @@ -141,7 +141,7 @@ export class SelectionManager extends EventEmitter { /** * Enable the selection manager. */ - public enable() { + public enable(): void { this._enabled = true; } diff --git a/src/Terminal.ts b/src/Terminal.ts index 6cc8864e12..df313ee319 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -30,7 +30,7 @@ import { CHARSETS } from './Charsets'; import { getRawByteCoords } from './utils/Mouse'; import { translateBufferLineToString } from './utils/BufferLine'; import { CustomKeyEventHandler, Charset } from './Types'; -import { ITerminal, IBrowser, ITerminalOptions } from './Interfaces'; +import { ITerminal, IBrowser, ITerminalOptions, IInputHandlingTerminal } from './Interfaces'; // Declare for RequireJS in loadAddon declare var define: any; @@ -163,7 +163,7 @@ const DEFAULT_OPTIONS: ITerminalOptions = { // focusKeys: false, }; -export class Terminal extends EventEmitter implements ITerminal { +export class Terminal extends EventEmitter implements ITerminal, IInputHandlingTerminal { public textarea: HTMLTextAreaElement; public element: HTMLElement; public rowContainer: HTMLElement; @@ -193,7 +193,7 @@ export class Terminal extends EventEmitter implements ITerminal { // 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; - private convertEol: boolean; + public convertEol: boolean; // TODO: This is the data queue for send, improve name and documentation private queue: string; private customKeyEventHandler: CustomKeyEventHandler; @@ -203,36 +203,36 @@ export class Terminal extends EventEmitter implements ITerminal { private cursorBlinkInterval: NodeJS.Timer; // modes - private applicationKeypad: boolean; - private applicationCursor: boolean; - private originMode: boolean; - private insertMode: boolean; - private wraparoundMode: boolean; // defaults: xterm - true, vt100 - false + public applicationKeypad: boolean; + public applicationCursor: boolean; + public originMode: boolean; + public insertMode: boolean; + public wraparoundMode: boolean; // defaults: xterm - true, vt100 - false // charset // The current charset - private charset: Charset; - private gcharset: number; - private glevel: number; - private charsets: Charset[]; + public charset: Charset; + public gcharset: number; + public glevel: number; + public charsets: Charset[]; // mouse properties private decLocator: boolean; // This is unstable and never set - private x10Mouse: boolean; - private vt200Mouse: boolean; + public x10Mouse: boolean; + public vt200Mouse: boolean; private vt300Mouse: boolean; // This is unstable and never set - private normalMouse: boolean; - private mouseEvents: boolean; - private sendFocus: boolean; - private utfMouse: boolean; - private sgrMouse: boolean; - private urxvtMouse: boolean; + public normalMouse: boolean; + public mouseEvents: boolean; + public sendFocus: boolean; + public utfMouse: boolean; + public sgrMouse: boolean; + public urxvtMouse: boolean; // misc public children: HTMLElement[]; private refreshStart: number; private refreshEnd: number; - private savedCols: boolean; + public savedCols: number; // stream private readable: boolean; @@ -274,7 +274,7 @@ export class Terminal extends EventEmitter implements ITerminal { private linkifier: Linkifier; public buffers: BufferSet; public buffer: Buffer; - private viewport: Viewport; + public viewport: Viewport; private compositionHelper: CompositionHelper; public charMeasure: CharMeasure; @@ -440,7 +440,7 @@ export class Terminal extends EventEmitter implements ITerminal { * @param {string} key The option key. * @param {any} value The option value. */ - public setOption(key: string, value: any) { + public setOption(key: string, value: any): void { // TODO: Give value a better type (boolean | string, ...) if (!(key in DEFAULT_OPTIONS)) { throw new Error('No option with key "' + key + '"'); @@ -453,7 +453,7 @@ export class Terminal extends EventEmitter implements ITerminal { msg += `(${this.rows}) is not allowed.`; console.warn(msg); - return false; + return; } if (this.options[key] !== value) { @@ -1167,7 +1167,7 @@ export class Terminal extends EventEmitter implements ITerminal { /** * Display the cursor element */ - public showCursor() { + public showCursor(): void { if (!this.cursorState) { this.cursorState = 1; this.refresh(this.buffer.y, this.buffer.y); @@ -1882,7 +1882,7 @@ export class Terminal extends EventEmitter implements ITerminal { * Send data for handling to the terminal * @param {string} data */ - private send(data) { + public send(data: string): void { if (!this.queue) { setTimeout(() => { this.handler(this.queue); @@ -1909,21 +1909,19 @@ export class Terminal extends EventEmitter implements ITerminal { /** * Log the current state to the console. */ - public log(): void { + public log(text: string, data?: any): void { if (!this.options.debug) return; if (!this.context.console || !this.context.console.log) return; - const args = Array.prototype.slice.call(arguments); - this.context.console.log.apply(this.context.console, args); + this.context.console.log(text, data); } /** * Log the current state as error to the console. */ - public error(): void { + public error(text: string, data?: any): void { if (!this.options.debug) return; if (!this.context.console || !this.context.console.error) return; - const args = Array.prototype.slice.call(arguments); - this.context.console.error.apply(this.context.console, args); + this.context.console.error(text, data); } /** @@ -1932,7 +1930,7 @@ export class Terminal extends EventEmitter implements ITerminal { * @param {number} x The number of columns to resize to. * @param {number} y The number of rows to resize to. */ - public resize(x: number, y: number) { + public resize(x: number, y: number): void { if (isNaN(x) || isNaN(y)) { return; } @@ -2052,7 +2050,7 @@ export class Terminal extends EventEmitter implements ITerminal { * Updates the range of rows to refresh * @param {number} y The number of rows to refresh next. */ - public updateRange(y) { + public updateRange(y: number): void { if (y < this.refreshStart) this.refreshStart = y; if (y > this.refreshEnd) this.refreshEnd = y; // if (y > this.refreshEnd) { @@ -2094,7 +2092,7 @@ export class Terminal extends EventEmitter implements ITerminal { * Move the cursor to the previous tab stop from the given position (default is current). * @param {number} x The position to move the cursor to the previous tab stop. */ - public prevStop(x) { + public prevStop(x?: number): number { if (x == null) x = this.buffer.x; while (!this.buffer.tabs[--x] && x > 0); return x >= this.cols ? this.cols - 1 : x < 0 ? 0 : x; @@ -2104,12 +2102,10 @@ export class Terminal extends EventEmitter implements ITerminal { * Move the cursor one tab stop forward from the given position (default is current). * @param {number} x The position to move the cursor one tab stop forward. */ - public nextStop(x) { + public nextStop(x?: number): number { if (x == null) x = this.buffer.x; while (!this.buffer.tabs[++x] && x < this.cols); - return x >= this.cols - ? this.cols - 1 - : x < 0 ? 0 : x; + return x >= this.cols ? this.cols - 1 : x < 0 ? 0 : x; } /** @@ -2134,7 +2130,7 @@ export class Terminal extends EventEmitter implements ITerminal { * @param {number} x The column from which to start erasing to the start of the line. * @param {number} y The line in which to operate. */ - public eraseLeft(x: number, y: number) { + public eraseLeft(x: number, y: number): void { const line = this.buffer.lines.get(this.buffer.ybase + y); if (!line) { return; @@ -2171,7 +2167,7 @@ export class Terminal extends EventEmitter implements ITerminal { * Erase all content in the given line * @param {number} y The line to erase all of its contents. */ - public eraseLine(y): void { + public eraseLine(y: number): void { this.eraseRight(0, y); } @@ -2180,7 +2176,7 @@ export class Terminal extends EventEmitter implements ITerminal { * @param {boolean} cur First bunch of data for each "blank" character. * @param {boolean} isWrapped Whether the new line is wrapped from the previous line. */ - public blankLine(cur?, isWrapped?: boolean) { + public blankLine(cur?: boolean, isWrapped?: boolean): [number, string, number][] { const attr = cur ? this.eraseAttr() : this.defAttr; const ch = [attr, ' ', 1]; // width defaults to 1 halfwidth character @@ -2209,12 +2205,10 @@ export class Terminal extends EventEmitter implements ITerminal { /** * Evaluate if the current terminal is the given argument. - * @param {object} term The terminal to evaluate + * @param term The terminal name to evaluate */ - private is(term) { - // TODO: Do we need this? - const name = this.options.termName; - return (name + '').indexOf(term) === 0; + public is(term: string): boolean { + return (this.options.termName + '').indexOf(term) === 0; } /** @@ -2294,7 +2288,7 @@ export class Terminal extends EventEmitter implements ITerminal { /** * ESC c Full Reset (RIS). */ - public reset() { + public reset(): void { this.options.rows = this.rows; this.options.cols = this.cols; const customKeyEventHandler = this.customKeyEventHandler; @@ -2329,7 +2323,7 @@ export class Terminal extends EventEmitter implements ITerminal { // Expose to InputHandler // TODO: Revise when truecolor is introduced. - public matchColor(r1, g1, b1) { + public matchColor(r1, g1, b1): any { const hash = (r1 << 16) | (g1 << 8) | b1; if (matchColorCache[hash] != null) { diff --git a/src/Viewport.ts b/src/Viewport.ts index fe276935bb..bd667edb6f 100644 --- a/src/Viewport.ts +++ b/src/Viewport.ts @@ -2,14 +2,14 @@ * @license MIT */ -import { ITerminal } from './Interfaces'; +import { ITerminal, IViewport } from './Interfaces'; import { CharMeasure } from './utils/CharMeasure'; /** * Represents the viewport of a terminal, the visible area within the larger buffer of output. * Logic for the virtual scroll bar is included in this object. */ -export class Viewport { +export class Viewport implements IViewport { private currentRowHeight: number; private lastRecordedBufferLength: number; private lastRecordedViewportHeight: number; From f9fce53cc45eaaba805c4387f3f690efa5e11dcd Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 5 Aug 2017 13:56:38 -0700 Subject: [PATCH 12/22] Add typedef tslint rule --- src/CompositionHelper.test.ts | 16 +-- src/CompositionHelper.ts | 16 +-- src/EventEmitter.ts | 25 ++-- src/InputHandler.test.ts | 39 +++--- src/InputHandler.ts | 24 ++-- src/Interfaces.ts | 26 ++-- src/Linkifier.test.ts | 24 ++-- src/Linkifier.ts | 8 +- src/Parser.ts | 2 +- src/Renderer.ts | 12 +- src/SelectionManager.ts | 16 +-- src/Terminal.ts | 236 +++++++++++++++------------------ src/Viewport.ts | 8 +- src/handlers/Clipboard.test.ts | 4 +- src/handlers/Clipboard.ts | 17 ++- src/utils/Generic.ts | 2 +- tslint.json | 5 + 17 files changed, 230 insertions(+), 250 deletions(-) diff --git a/src/CompositionHelper.test.ts b/src/CompositionHelper.test.ts index 4a96e12a8f..70b231834b 100644 --- a/src/CompositionHelper.test.ts +++ b/src/CompositionHelper.test.ts @@ -36,7 +36,7 @@ describe('CompositionHelper', () => { return { offsetLeft: 0, offsetTop: 0 }; } }, - handler: function (text) { + handler: (text: string) => { handledText += text; } }; @@ -45,7 +45,7 @@ describe('CompositionHelper', () => { }); describe('Input', () => { - it('Should insert simple characters', function (done) { + it('Should insert simple characters', (done) => { // First character 'ㅇ' compositionHelper.compositionstart(); compositionHelper.compositionupdate({ data: 'ㅇ' }); @@ -69,7 +69,7 @@ describe('CompositionHelper', () => { }, 0); }); - it('Should insert complex characters', function (done) { + it('Should insert complex characters', (done) => { // First character '앙' compositionHelper.compositionstart(); compositionHelper.compositionupdate({ data: 'ㅇ' }); @@ -109,7 +109,7 @@ describe('CompositionHelper', () => { }, 0); }); - it('Should insert complex characters that change with following character', function (done) { + it('Should insert complex characters that change with following character', (done) => { // First character '아' compositionHelper.compositionstart(); compositionHelper.compositionupdate({ data: 'ㅇ' }); @@ -138,7 +138,7 @@ describe('CompositionHelper', () => { }, 0); }); - it('Should insert multi-characters compositions', function (done) { + it('Should insert multi-characters compositions', (done) => { // First character 'だ' compositionHelper.compositionstart(); compositionHelper.compositionupdate({ data: 'd' }); @@ -161,7 +161,7 @@ describe('CompositionHelper', () => { }, 0); }); - it('Should insert multi-character compositions that are converted to other characters with the same length', function (done) { + it('Should insert multi-character compositions that are converted to other characters with the same length', (done) => { // First character 'だ' compositionHelper.compositionstart(); compositionHelper.compositionupdate({ data: 'd' }); @@ -189,7 +189,7 @@ describe('CompositionHelper', () => { }, 0); }); - it('Should insert multi-character compositions that are converted to other characters with different lengths', function (done) { + it('Should insert multi-character compositions that are converted to other characters with different lengths', (done) => { // First character 'い' compositionHelper.compositionstart(); compositionHelper.compositionupdate({ data: 'い' }); @@ -217,7 +217,7 @@ describe('CompositionHelper', () => { }, 0); }); - it('Should insert non-composition characters input immediately after composition characters', function (done) { + it('Should insert non-composition characters input immediately after composition characters', (done) => { // First character 'ㅇ' compositionHelper.compositionstart(); compositionHelper.compositionupdate({ data: 'ㅇ' }); diff --git a/src/CompositionHelper.ts b/src/CompositionHelper.ts index 439e20857f..223a90e7c2 100644 --- a/src/CompositionHelper.ts +++ b/src/CompositionHelper.ts @@ -51,7 +51,7 @@ export class CompositionHelper { /** * Handles the compositionstart event, activating the composition view. */ - public compositionstart() { + public compositionstart(): void { this.isComposing = true; this.compositionPosition.start = this.textarea.value.length; this.compositionView.textContent = ''; @@ -62,7 +62,7 @@ export class CompositionHelper { * Handles the compositionupdate event, updating the composition view. * @param {CompositionEvent} ev The event. */ - public compositionupdate(ev: CompositionEvent) { + public compositionupdate(ev: CompositionEvent): void { this.compositionView.textContent = ev.data; this.updateCompositionElements(); setTimeout(() => { @@ -74,7 +74,7 @@ export class CompositionHelper { * Handles the compositionend event, hiding the composition view and sending the composition to * the handler. */ - public compositionend() { + public compositionend(): void { this.finalizeComposition(true); } @@ -83,7 +83,7 @@ export class CompositionHelper { * @param ev The keydown event. * @return Whether the Terminal should continue processing the keydown event. */ - public keydown(ev: KeyboardEvent) { + public keydown(ev: KeyboardEvent): boolean { if (this.isComposing || this.isSendingComposition) { if (ev.keyCode === 229) { // Continue composing if the keyCode is the "composition character" @@ -116,7 +116,7 @@ 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) { + private finalizeComposition(waitForPropogation: boolean): void { this.compositionView.classList.remove('active'); this.isComposing = false; this.clearTextareaPosition(); @@ -169,7 +169,7 @@ export class CompositionHelper { * character" (229) is triggered, in order to allow non-composition text to be entered when an * IME is active. */ - private handleAnyTextareaChanges() { + private handleAnyTextareaChanges(): void { const oldValue = this.textarea.value; setTimeout(() => { // Ignore if a composition has started since the timeout @@ -189,7 +189,7 @@ export class CompositionHelper { * @param dontRecurse Whether to use setTimeout to recursively trigger another update, this is * necessary as the IME events across browsers are not consistently triggered. */ - public updateCompositionElements(dontRecurse?: boolean) { + public updateCompositionElements(dontRecurse?: boolean): void { if (!this.isComposing) { return; } @@ -222,7 +222,7 @@ export class CompositionHelper { * Clears the textarea's position so that the cursor does not blink on IE. * @private */ - private clearTextareaPosition() { + private clearTextareaPosition(): void { this.textarea.style.left = ''; this.textarea.style.top = ''; }; diff --git a/src/EventEmitter.ts b/src/EventEmitter.ts index 3d34da7541..a1768d2d4a 100644 --- a/src/EventEmitter.ts +++ b/src/EventEmitter.ts @@ -2,15 +2,10 @@ * @license MIT */ -import { IEventEmitter } from './Interfaces'; - -interface ListenerType { - (): void; - listener?: () => void; -}; +import { IEventEmitter, IListenerType } from './Interfaces'; export class EventEmitter implements IEventEmitter { - private _events: {[type: string]: ListenerType[]}; + private _events: {[type: string]: IListenerType[]}; constructor() { // Restore the previous events if available, this will happen if the @@ -18,12 +13,12 @@ export class EventEmitter implements IEventEmitter { this._events = this._events || {}; } - public on(type, listener): void { + public on(type: string, listener: IListenerType): void { this._events[type] = this._events[type] || []; this._events[type].push(listener); } - public off(type, listener): void { + public off(type: string, listener: IListenerType): void { if (!this._events[type]) { return; } @@ -39,20 +34,20 @@ export class EventEmitter implements IEventEmitter { } } - public removeAllListeners(type): void { + public removeAllListeners(type: string): void { if (this._events[type]) { delete this._events[type]; } } - public once(type, listener): any { - function on() { + public once(type: string, listener: IListenerType): void { + function on(): void { let args = Array.prototype.slice.call(arguments); this.off(type, on); - return listener.apply(this, args); + listener.apply(this, args); } (on).listener = listener; - return this.on(type, on); + this.on(type, on); } public emit(type: string, ...args: any[]): void { @@ -65,7 +60,7 @@ export class EventEmitter implements IEventEmitter { } } - public listeners(type): ListenerType[] { + public listeners(type: string): IListenerType[] { return this._events[type] || []; } diff --git a/src/InputHandler.test.ts b/src/InputHandler.test.ts index 9562429601..b6b218dfdb 100644 --- a/src/InputHandler.test.ts +++ b/src/InputHandler.test.ts @@ -65,7 +65,7 @@ describe('InputHandler', () => { }); }); -const old_wcwidth = (function(opts) { +const old_wcwidth = (function(opts: {nul: number, control: number}): (ucs: number) => number { // extracted from https://www.cl.cam.ac.uk/%7Emgk25/ucs/wcwidth.c // combining characters const COMBINING = [ @@ -119,7 +119,7 @@ const old_wcwidth = (function(opts) { [0xE0100, 0xE01EF] ]; // binary search - function bisearch(ucs) { + function bisearch(ucs: number): boolean { let min = 0; let max = COMBINING.length - 1; let mid; @@ -136,23 +136,26 @@ const old_wcwidth = (function(opts) { } return false; } - function wcwidth(ucs) { - // test for 8-bit control characters - if (ucs === 0) - return opts.nul; - if (ucs < 32 || (ucs >= 0x7f && ucs < 0xa0)) - return opts.control; - // binary search in table of non-spacing characters - if (bisearch(ucs)) - return 0; - // if we arrive here, ucs is not a combining or C0/C1 control character - if (isWide(ucs)) { - return 2; - } - return 1; + function wcwidth(ucs: number): number { + // test for 8-bit control characters + if (ucs === 0) { + return opts.nul; + } + if (ucs < 32 || (ucs >= 0x7f && ucs < 0xa0)) { + return opts.control; + } + // binary search in table of non-spacing characters + if (bisearch(ucs)) { + return 0; + } + // if we arrive here, ucs is not a combining or C0/C1 control character + if (isWide(ucs)) { + return 2; + } + return 1; } - function isWide(ucs) { - return ( + function isWide(ucs: number): boolean { + return ( ucs >= 0x1100 && ( ucs <= 0x115f || // Hangul Jamo init. consonants ucs === 0x2329 || diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 8f566cd9a1..9376d182e5 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -221,7 +221,7 @@ export class InputHandler implements IInputHandler { * CSI Ps B * Cursor Down Ps Times (default = 1) (CUD). */ - public cursorDown(params: number[]) { + public cursorDown(params: number[]): void { let param = params[0]; if (param < 1) { param = 1; @@ -240,7 +240,7 @@ export class InputHandler implements IInputHandler { * CSI Ps C * Cursor Forward Ps Times (default = 1) (CUF). */ - public cursorForward(params: number[]) { + public cursorForward(params: number[]): void { let param = params[0]; if (param < 1) { param = 1; @@ -255,7 +255,7 @@ export class InputHandler implements IInputHandler { * CSI Ps D * Cursor Backward Ps Times (default = 1) (CUB). */ - public cursorBackward(params: number[]) { + public cursorBackward(params: number[]): void { let param = params[0]; if (param < 1) { param = 1; @@ -1460,7 +1460,7 @@ export class InputHandler implements IInputHandler { } } -export const wcwidth = (function(opts) { +export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: number) => number { // extracted from https://www.cl.cam.ac.uk/%7Emgk25/ucs/wcwidth.c // combining characters const COMBINING_BMP = [ @@ -1516,7 +1516,7 @@ export const wcwidth = (function(opts) { [0xE0100, 0xE01EF] ]; // binary search - function bisearch(ucs, data) { + function bisearch(ucs: number, data: number[][]): boolean { let min = 0; let max = data.length - 1; let mid; @@ -1533,7 +1533,7 @@ export const wcwidth = (function(opts) { } return false; } - function wcwidthBMP(ucs) { + function wcwidthBMP(ucs: number): number { // test for 8-bit control characters if (ucs === 0) return opts.nul; @@ -1548,7 +1548,7 @@ export const wcwidth = (function(opts) { } return 1; } - function isWideBMP(ucs) { + function isWideBMP(ucs: number): boolean { return ( ucs >= 0x1100 && ( ucs <= 0x115f || // Hangul Jamo init. consonants @@ -1562,7 +1562,7 @@ export const wcwidth = (function(opts) { (ucs >= 0xff00 && ucs <= 0xff60) || // Fullwidth Forms (ucs >= 0xffe0 && ucs <= 0xffe6))); } - function wcwidthHigh(ucs) { + function wcwidthHigh(ucs: number): 0 | 1 | 2 { if (bisearch(ucs, COMBINING_HIGH)) return 0; if ((ucs >= 0x20000 && ucs <= 0x2fffd) || (ucs >= 0x30000 && ucs <= 0x3fffd)) { @@ -1571,8 +1571,8 @@ export const wcwidth = (function(opts) { return 1; } const control = opts.control | 0; - let table = null; - function init_table() { + let table: number[] | Uint32Array = null; + function init_table(): number[] | Uint32Array { // lookup table for BMP const CODEPOINTS = 65536; // BMP holds 65536 codepoints const BITWIDTH = 2; // a codepoint can have a width of 0, 1 or 2 @@ -1589,7 +1589,7 @@ export const wcwidth = (function(opts) { num = (num << 2) | wcwidthBMP(CODEPOINTS_PER_ITEM * i + pos); table[i] = num; } - return table; + return table; } // get width from lookup table // position in container : num / CODEPOINTS_PER_ITEM @@ -1603,7 +1603,7 @@ export const wcwidth = (function(opts) { // ==> n = n >> m e.g. m=12 000000000000FFEEDDCCBBAA99887766 // we are only interested in 2 LSBs, cut off higher bits // ==> n = n & 3 e.g. 000000000000000000000000000000XX - return function (num) { + return function (num: number): number { num = num | 0; // get asm.js like optimization under V8 if (num < 32) return control | 0; diff --git a/src/Interfaces.ts b/src/Interfaces.ts index b61c0057f6..875cbac817 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -2,7 +2,7 @@ * @license MIT */ -import { LinkMatcherOptions } from './Interfaces'; +import { ILinkMatcherOptions } from './Interfaces'; import { LinkMatcherHandler, LinkMatcherValidationCallback, Charset } from './Types'; export interface IBrowser { @@ -40,10 +40,9 @@ export interface ITerminal extends IEventEmitter { * Emit the 'data' event and populate the given data. * @param data The data to populate in the event. */ - handler(data: string); - on(event: string, callback: () => void); - scrollDisp(disp: number, suppressScrollEvent: boolean); - cancel(ev: Event, force?: boolean); + handler(data: string): void; + scrollDisp(disp: number, suppressScrollEvent?: boolean): void; + cancel(ev: Event, force?: boolean): boolean | void; log(text: string): void; reset(): void; showCursor(): void; @@ -107,7 +106,7 @@ export interface IInputHandlingTerminal extends IEventEmitter { reset(): void; showCursor(): void; refresh(start: number, end: number): void; - matchColor(r1, g1, b1): any; + matchColor(r1: number, g1: number, b1: number): any; error(text: string, data?: any): void; setOption(key: string, value: any): void; } @@ -167,7 +166,7 @@ export interface ISelectionManager { disable(): void; enable(): void; setBuffer(buffer: ICircularList<[number, string, number][]>): void; - setSelection(row: number, col: number, length: number); + setSelection(row: number, col: number, length: number): void; } export interface ICharMeasure { @@ -179,7 +178,7 @@ export interface ICharMeasure { export interface ILinkifier { linkifyRow(rowIndex: number): void; attachHypertextLinkHandler(handler: LinkMatcherHandler): void; - registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options?: LinkMatcherOptions): number; + registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options?: ILinkMatcherOptions): number; deregisterLinkMatcher(matcherId: number): boolean; } @@ -198,12 +197,17 @@ export interface ICircularList extends IEventEmitter { } export interface IEventEmitter { - on(type, listener): void; - off(type, listener): void; + on(type: string, listener: IListenerType): void; + off(type: string, listener: IListenerType): void; emit(type: string, data?: any): void; } -export interface LinkMatcherOptions { +export interface IListenerType { + (data?: any): void; + listener?: (data?: any) => void; +}; + +export interface ILinkMatcherOptions { /** * The index of the link from the regex.match(text) call. This defaults to 0 * (for regular expressions without capture groups). diff --git a/src/Linkifier.test.ts b/src/Linkifier.test.ts index 132ce5f06e..414238f9c9 100644 --- a/src/Linkifier.test.ts +++ b/src/Linkifier.test.ts @@ -32,7 +32,7 @@ describe('Linkifier', () => { linkifier = new TestLinkifier(); }); - function addRow(html: string) { + function addRow(html: string): void { const element = document.createElement('div'); element.innerHTML = html; container.appendChild(element); @@ -57,24 +57,24 @@ describe('Linkifier', () => { document.body.appendChild(container); }); - function clickElement(element: Node) { + function clickElement(element: Node): void { const event = document.createEvent('MouseEvent'); event.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); element.dispatchEvent(event); } - function assertLinkifiesEntireRow(uri: string, done: MochaDone) { - addRow(uri); - linkifier.linkifyRow(0); - setTimeout(() => { - assert.equal((rows[0].firstChild).tagName, 'A'); - assert.equal((rows[0].firstChild).textContent, uri); - done(); - }, 0); + function assertLinkifiesEntireRow(uri: string, done: MochaDone): void { + addRow(uri); + linkifier.linkifyRow(0); + setTimeout(() => { + assert.equal((rows[0].firstChild).tagName, 'A'); + assert.equal((rows[0].firstChild).textContent, uri); + done(); + }, 0); } describe('http links', () => { - function assertLinkifiesEntireRow(uri: string, done: MochaDone) { + function assertLinkifiesEntireRow(uri: string, done: MochaDone): void { addRow(uri); linkifier.linkifyRow(0); setTimeout(() => { @@ -87,7 +87,7 @@ describe('Linkifier', () => { }); describe('link matcher', () => { - function assertLinkifiesRow(rowText: string, linkMatcherRegex: RegExp, expectedHtml: string, done: MochaDone) { + function assertLinkifiesRow(rowText: string, linkMatcherRegex: RegExp, expectedHtml: string, done: MochaDone): void { addRow(rowText); linkifier.registerLinkMatcher(linkMatcherRegex, () => {}); linkifier.linkifyRow(0); diff --git a/src/Linkifier.ts b/src/Linkifier.ts index bc4949b1d6..2323f22679 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -2,7 +2,7 @@ * @license MIT */ -import { LinkMatcherOptions } from './Interfaces'; +import { ILinkMatcherOptions } from './Interfaces'; import { LinkMatcher, LinkMatcherHandler, LinkMatcherValidationCallback } from './Types'; const INVALID_LINK_CLASS = 'xterm-invalid-link'; @@ -60,7 +60,7 @@ export class Linkifier { * @param document The document object. * @param rows The array of rows to apply links to. */ - public attachToDom(document: Document, rows: HTMLElement[]) { + public attachToDom(document: Document, rows: HTMLElement[]): void { this._document = document; this._rows = rows; } @@ -108,10 +108,10 @@ export class Linkifier { * this searches the textContent of the rows. You will want to use \s to match * a space ' ' character for example. * @param {LinkHandler} handler The callback when the link is called. - * @param {LinkMatcherOptions} [options] Options for the link matcher. + * @param {ILinkMatcherOptions} [options] Options for the link matcher. * @return {number} The ID of the new matcher, this can be used to deregister. */ - public registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options: LinkMatcherOptions = {}): number { + public registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options: ILinkMatcherOptions = {}): number { if (this._nextLinkMatcherId !== HYPERTEXT_LINK_MATCHER_ID && !handler) { throw new Error('handler must be defined'); } diff --git a/src/Parser.ts b/src/Parser.ts index bec0af7b59..90d413a738 100644 --- a/src/Parser.ts +++ b/src/Parser.ts @@ -613,7 +613,7 @@ export class Parser { * * @param param the parameter. */ - public setParam(param: number) { + public setParam(param: number): void { this._terminal.currentParam = param; } diff --git a/src/Renderer.ts b/src/Renderer.ts index 165594fd25..f706bffcb3 100644 --- a/src/Renderer.ts +++ b/src/Renderer.ts @@ -37,7 +37,7 @@ export class Renderer { // Figure out whether boldness affects // the character width of monospace fonts. if (brokenBold === null) { - brokenBold = checkBoldBroken((this._terminal).element); + brokenBold = checkBoldBroken(this._terminal.element); } this._spanElementObjectPool = new DomElementObjectPool('span'); @@ -327,7 +327,7 @@ export class Renderer { * @param start The selection start. * @param end The selection end. */ - public refreshSelection(start: [number, number], end: [number, number]) { + public refreshSelection(start: [number, number], end: [number, number]): void { // Remove all selections while (this._terminal.selectionContainer.children.length) { this._terminal.selectionContainer.removeChild(this._terminal.selectionContainer.children[0]); @@ -385,16 +385,16 @@ export class Renderer { // If bold is broken, we can't use it in the terminal. -function checkBoldBroken(terminal) { - const document = terminal.ownerDocument; +function checkBoldBroken(terminalElement: HTMLElement): boolean { + const document = terminalElement.ownerDocument; const el = document.createElement('span'); el.innerHTML = 'hello world'; - terminal.appendChild(el); + terminalElement.appendChild(el); const w1 = el.offsetWidth; const h1 = el.offsetHeight; el.style.fontWeight = 'bold'; const w2 = el.offsetWidth; const h2 = el.offsetHeight; - terminal.removeChild(el); + terminalElement.removeChild(el); return w1 !== w2 || h1 !== h2; } diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index bf594f7045..e3770c06da 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -7,7 +7,7 @@ import * as Browser from './utils/Browser'; import { CharMeasure } from './utils/CharMeasure'; import { CircularList } from './utils/CircularList'; import { EventEmitter } from './EventEmitter'; -import { ITerminal, ICircularList } from './Interfaces'; +import { ITerminal, ICircularList, ISelectionManager } from './Interfaces'; import { SelectionModel } from './SelectionModel'; import { translateBufferLineToString } from './utils/BufferLine'; @@ -66,7 +66,7 @@ enum SelectionMode { * not handled by the SelectionManager but a 'refresh' event is fired when the * selection is ready to be redrawn. */ -export class SelectionManager extends EventEmitter { +export class SelectionManager extends EventEmitter implements ISelectionManager { protected _model: SelectionModel; /** @@ -116,7 +116,7 @@ export class SelectionManager extends EventEmitter { /** * Initializes listener variables. */ - private _initListeners() { + private _initListeners(): void { this._mouseMoveListener = event => this._onMouseMove(event); this._mouseUpListener = event => this._onMouseUp(event); @@ -267,7 +267,7 @@ export class SelectionManager extends EventEmitter { * Handle the buffer being trimmed, adjust the selection position. * @param amount The amount the buffer is being trimmed. */ - private _onTrim(amount: number) { + private _onTrim(amount: number): void { const needsRefresh = this._model.onTrim(amount); if (needsRefresh) { this.refresh(); @@ -316,7 +316,7 @@ export class SelectionManager extends EventEmitter { * Handles te mousedown event, setting up for a new selection. * @param event The mousedown event. */ - private _onMouseDown(event: MouseEvent) { + private _onMouseDown(event: MouseEvent): void { // If we have selection, we want the context menu on right click even if the // terminal is in mouse mode. if (event.button === 2 && this.hasSelection) { @@ -455,7 +455,7 @@ export class SelectionManager extends EventEmitter { * end of the selection and refreshing the selection. * @param event The mousemove event. */ - private _onMouseMove(event: MouseEvent) { + private _onMouseMove(event: MouseEvent): void { // Record the previous position so we know whether to redraw the selection // at the end. const previousSelectionEnd = this._model.selectionEnd ? [this._model.selectionEnd[0], this._model.selectionEnd[1]] : null; @@ -511,7 +511,7 @@ export class SelectionManager extends EventEmitter { * The callback that occurs every DRAG_SCROLL_INTERVAL ms that does the * scrolling of the viewport. */ - private _dragScroll() { + private _dragScroll(): void { if (this._dragScrollAmount) { this._terminal.scrollDisp(this._dragScrollAmount, false); // Re-evaluate selection @@ -528,7 +528,7 @@ export class SelectionManager extends EventEmitter { * Handles the mouseup event, removing the mousedown listeners. * @param event The mouseup event. */ - private _onMouseUp(event: MouseEvent) { + private _onMouseUp(event: MouseEvent): void { this._removeMouseDownListeners(); } diff --git a/src/Terminal.ts b/src/Terminal.ts index df313ee319..b3a27be97e 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -29,8 +29,8 @@ import * as Mouse from './utils/Mouse'; import { CHARSETS } from './Charsets'; import { getRawByteCoords } from './utils/Mouse'; import { translateBufferLineToString } from './utils/BufferLine'; -import { CustomKeyEventHandler, Charset } from './Types'; -import { ITerminal, IBrowser, ITerminalOptions, IInputHandlingTerminal } from './Interfaces'; +import { CustomKeyEventHandler, Charset, LinkMatcherHandler, LinkMatcherValidationCallback } from './Types'; +import { ITerminal, IBrowser, ITerminalOptions, IInputHandlingTerminal, ILinkMatcherOptions } from './Interfaces'; // Declare for RequireJS in loadAddon declare var define: any; @@ -94,7 +94,7 @@ const tangoColors: string[] = [ // Colors 0-15 + 16-255 // Much thanks to TooTallNate for writing this. -const defaultColors: string[] = (function() { +const defaultColors: string[] = (function(): string[] { let colors = tangoColors.slice(); let r = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff]; let i; @@ -113,13 +113,13 @@ const defaultColors: string[] = (function() { out(c, c, c); } - function out(r, g, b) { + function out(r: number, g: number, b: number): void { colors.push('#' + hex(r) + hex(g) + hex(b)); } - function hex(c) { - c = c.toString(16); - return c.length < 2 ? '0' + c : c; + function hex(c: number): string { + let s = c.toString(16); + return s.length < 2 ? '0' + s : s; } return colors; @@ -127,8 +127,8 @@ const defaultColors: string[] = (function() { const _colors: string[] = defaultColors.slice(); -const vcolors: number[][] = (function() { - const out = []; +const vcolors: number[][] = (function(): number[][] { + const out: number[][] = []; let color; for (let i = 0; i < 256; i++) { @@ -387,8 +387,8 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT // Create the terminal's buffers and set the current buffer this.buffers = new BufferSet(this); this.buffer = this.buffers.active; // Convenience shortcut; - this.buffers.on('activate', function (buffer) { - this._terminal.buffer = buffer; + this.buffers.on('activate', (buffer: Buffer) => { + this.buffer = buffer; }); let i = this.rows; @@ -485,11 +485,11 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT } } - private restartCursorBlinking() { + private restartCursorBlinking(): void { this.setCursorBlinking(this.options.cursorBlink); } - private setCursorBlinking(enabled) { + private setCursorBlinking(enabled: boolean): void { this.element.classList.toggle('xterm-cursor-blink', enabled); this.clearCursorBlinkingInterval(); if (enabled) { @@ -499,7 +499,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT } } - private clearCursorBlinkingInterval() { + private clearCursorBlinkingInterval(): void { this.element.classList.remove('xterm-cursor-blink-on'); if (this.cursorBlinkInterval) { clearInterval(this.cursorBlinkInterval); @@ -510,7 +510,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT /** * Binds the desired focus behavior on a given terminal object. */ - private bindFocus() { + private bindFocus(): void { globalOn(this.textarea, 'focus', (ev) => { if (this.sendFocus) { this.send(C0.ESC + '[I'); @@ -526,14 +526,14 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT /** * Blur the terminal. Delegates blur handling to the terminal's DOM element. */ - private blur() { + private blur(): void { return this.textarea.blur(); } /** * Binds the desired blur behavior on a given terminal object. */ - private bindBlur() { + private bindBlur(): void { on(this.textarea, 'blur', (ev) => { this.refresh(this.buffer.y, this.buffer.y); if (this.sendFocus) { @@ -549,7 +549,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT /** * Initialize default behavior */ - private initGlobal() { + private initGlobal(): void { this.bindKeys(); this.bindFocus(); this.bindBlur(); @@ -598,33 +598,33 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT /** * Apply key handling to the terminal */ - private bindKeys() { + private bindKeys(): void { const self = this; - on(this.element, 'keydown', function (ev) { + on(this.element, 'keydown', function (ev: KeyboardEvent): void { if (document.activeElement !== this) { return; } self.keyDown(ev); }, true); - on(this.element, 'keypress', function (ev) { + on(this.element, 'keypress', function (ev: KeyboardEvent): void { if (document.activeElement !== this) { return; } self.keyPress(ev); }, true); - on(this.element, 'keyup', (ev) => { + on(this.element, 'keyup', (ev: KeyboardEvent) => { if (!wasMondifierKeyOnlyEvent(ev)) { this.focus(); } }, true); - on(this.textarea, 'keydown', (ev) => { + on(this.textarea, 'keydown', (ev: KeyboardEvent) => { this.keyDown(ev); }, true); - on(this.textarea, 'keypress', (ev) => { + on(this.textarea, 'keypress', (ev: KeyboardEvent) => { this.keyPress(ev); // Truncate the textarea's value, since it is not needed this.textarea.value = ''; @@ -642,7 +642,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT * if no row argument is passed. Return the inserted row. * @param {HTMLElement} row (optional) The row to append to the terminal. */ - private insertRow(row?: HTMLElement) { + private insertRow(row?: HTMLElement): HTMLElement { if (typeof row !== 'object') { row = document.createElement('div'); } @@ -659,7 +659,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT * @param {HTMLElement} parent The element to create the terminal within. * @param {boolean} focus Focus the terminal, after it gets instantiated in the DOM */ - private open(parent, focus) { + private open(parent: HTMLElement, focus?: boolean): void { let i = 0; let div; @@ -802,7 +802,8 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT * @param {string} addon The name of the addon to load * @static */ - public static loadAddon(addon, callback) { + public static loadAddon(addon: string, callback?: Function): boolean | any { + // TODO: Improve return type and documentation if (typeof exports === 'object' && typeof module === 'object') { // CommonJS return require('./addons/' + addon + '/' + addon); @@ -819,7 +820,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT * Updates the helper CSS class with any changes necessary after the terminal's * character width has been changed. */ - public updateCharSizeStyles() { + public updateCharSizeStyles(): void { this.charSizeStyleElement.textContent = `.xterm-wide-char{width:${this.charMeasure.width * 2}px;}` + `.xterm-normal-char{width:${this.charMeasure.width}px;}` + @@ -836,7 +837,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT * Relevant functions in xterm/button.c: * BtnCode, EmitButtonCode, EditorButton, SendMousePosition */ - public bindMouse() { + public bindMouse(): void { const el = this.element; const self = this; let pressed = 32; @@ -844,7 +845,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT // mouseup, mousedown, wheel // left click: ^[[M 3<^[[M#3< // wheel up: ^[[M`3> - function sendButton(ev) { + function sendButton(ev: MouseEvent | WheelEvent): void { let button; let pos; @@ -857,7 +858,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT sendEvent(button, pos); - switch (ev.overrideType || ev.type) { + switch ((ev).overrideType || ev.type) { case 'mousedown': pressed = button; break; @@ -876,11 +877,9 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT // motion example of a left click: // ^[[M 3<^[[M@4<^[[M@5<^[[M@6<^[[M@7<^[[M#7< - function sendMove(ev) { + function sendMove(ev: MouseEvent): void { let button = pressed; - let pos; - - pos = getRawByteCoords(ev, self.rowContainer, self.charMeasure, self.cols, self.rows); + let pos = getRawByteCoords(ev, self.rowContainer, self.charMeasure, self.cols, self.rows); if (!pos) return; // buttons marked as motions @@ -892,13 +891,19 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT // encode button and // position to characters - function encode(data, ch) { + function encode(data: number[], ch: number): void { if (!self.utfMouse) { - if (ch === 255) return data.push(0); + if (ch === 255) { + data.push(0); + return; + } if (ch > 127) ch = 127; data.push(ch); } else { - if (ch === 2047) return data.push(0); + if (ch === 2047) { + data.push(0); + return; + } if (ch < 127) { data.push(ch); } else { @@ -915,7 +920,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT // sgr: ^[[ Cb ; Cx ; Cy M/m // vt300: ^[[ 24(1/3/5)~ [ Cx , Cy ] \r // locator: CSI P e ; P b ; P r ; P c ; P p & w - function sendEvent(button, pos) { + function sendEvent(button: number, pos: {x: number, y: number}): void { // self.emit('mouse', { // x: pos.x - 32, // y: pos.x - 32, @@ -957,7 +962,8 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT + ';' + pos.x + ';' - + (pos.page || 0) + // Not sure what page is meant to be + + (pos).page || 0 + '&w'); return; } @@ -984,7 +990,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT return; } - let data = []; + let data: number[] = []; encode(data, button); encode(data, pos.x); @@ -993,7 +999,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT self.send(C0.ESC + '[M' + String.fromCharCode.apply(String, data)); } - function getButton(ev) { + function getButton(ev: MouseEvent): number { let button; let shift; let meta; @@ -1007,7 +1013,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT // 3 = release // wheel up/down: // 1, and 2 - with 64 added - switch (ev.overrideType || ev.type) { + switch ((ev).overrideType || ev.type) { case 'mousedown': button = ev.button != null ? +ev.button @@ -1028,7 +1034,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT : 65; break; case 'wheel': - button = ev.wheelDeltaY > 0 + button = (ev).wheelDeltaY > 0 ? 64 : 65; break; @@ -1055,7 +1061,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT return button; } - on(el, 'mousedown', (ev) => { + on(el, 'mousedown', (ev: MouseEvent) => { // Prevent the focus on the textarea from getting lost // and make sure we get focused on mousedown @@ -1082,10 +1088,12 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT 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); return this.cancel(ev); }; + // TODO: Seems dangerous calling this on document? on(this.document, 'mouseup', handler); } @@ -1096,7 +1104,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT // on(this.document, 'mousemove', sendMove); // } - on(el, 'wheel', (ev) => { + on(el, 'wheel', (ev: WheelEvent) => { if (!this.mouseEvents) return; if (this.x10Mouse || this.vt300Mouse || this.decLocator) return; sendButton(ev); @@ -1131,8 +1139,8 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT super.destroy(); this.readable = false; this.writable = false; - this.handler = function() {}; - this.write = function() {}; + this.handler = () => {}; + this.write = () => {}; if (this.element && this.element.parentNode) { this.element.parentNode.removeChild(this.element); } @@ -1316,7 +1324,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT } } - private innerWrite() { + private innerWrite(): void { const writeBatch = this.writeBuffer.splice(0, WRITE_BATCH_SIZE); while (writeBatch.length > 0) { const data = writeBatch.shift(); @@ -1354,7 +1362,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT * Writes text to the terminal, followed by a break line character (\n). * @param {string} data The text to write to the terminal. */ - public writeln(data): void { + public writeln(data: string): void { this.write(data + '\r\n'); } @@ -1386,9 +1394,9 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT * 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 {LinkMatcherHandler} handler The handler callback function. + * @param handler The handler callback function. */ - public setHypertextLinkHandler(handler) { + public setHypertextLinkHandler(handler: LinkMatcherHandler): void { if (!this.linkifier) { throw new Error('Cannot attach a hypertext link handler before Terminal.open is called'); } @@ -1400,10 +1408,10 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT /** * 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 {LinkMatcherValidationCallback} callback The callback to use, this can + * @param callback The callback to use, this can * be cleared with null. */ - public setHypertextValidationCallback(callback) { + public setHypertextValidationCallback(callback: LinkMatcherValidationCallback): void { if (!this.linkifier) { throw new Error('Cannot attach a hypertext validation callback before Terminal.open is called'); } @@ -1413,16 +1421,16 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT } /** - * Registers a link matcher, allowing custom link patterns to be matched and - * handled. - * @param {RegExp} regex The regular expression to search for, specifically - * this searches the textContent of the rows. You will want to use \s to match - * a space ' ' character for example. - * @param {LinkMatcherHandler} handler The callback when the link is called. - * @param {LinkMatcherOptions} [options] Options for the link matcher. - * @return {number} The ID of the new matcher, this can be used to deregister. + * Registers a link matcher, allowing custom link patterns to be matched and + * handled. + * @param regex The regular expression to search for, specifically + * this searches the textContent of the rows. You will want to use \s to match + * a space ' ' character for example. + * @param handler The callback when the link is called. + * @param [options] Options for the link matcher. + * @return The ID of the new matcher, this can be used to deregister. */ - public registerLinkMatcher(regex, handler, options) { + 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); @@ -1432,9 +1440,9 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT /** * Deregisters a link matcher if it has been registered. - * @param {number} matcherId The link matcher's ID (returned after register) + * @param matcherId The link matcher's ID (returned after register) */ - public deregisterLinkMatcher(matcherId) { + public deregisterLinkMatcher(matcherId: number): void { if (this.linkifier) { if (this.linkifier.deregisterLinkMatcher(matcherId)) { this.refresh(0, this.rows - 1); @@ -1445,7 +1453,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT /** * Gets whether the terminal has an active selection. */ - public hasSelection() { + public hasSelection(): boolean { return this.selectionManager ? this.selectionManager.hasSelection : false; } @@ -1453,14 +1461,14 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT * Gets the terminal's current selection, this is useful for implementing copy * behavior outside of xterm.js. */ - public getSelection() { + public getSelection(): string { return this.selectionManager ? this.selectionManager.selectionText : ''; } /** * Clears the current terminal selection. */ - public clearSelection() { + public clearSelection(): void { if (this.selectionManager) { this.selectionManager.clearSelection(); } @@ -1469,7 +1477,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT /** * Selects all text within the terminal. */ - public selectAll() { + public selectAll(): void { if (this.selectionManager) { this.selectionManager.selectAll(); } @@ -1481,7 +1489,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT * - https://developer.mozilla.org/en-US/docs/DOM/KeyboardEvent * @param {KeyboardEvent} ev The keydown event to be handled. */ - private keyDown(ev) { + private keyDown(ev: KeyboardEvent): boolean { if (this.customKeyEventHandler && this.customKeyEventHandler(ev) === false) { return false; } @@ -1508,7 +1516,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT return this.cancel(ev, true); } - if (isThirdLevelShift(this, ev)) { + if (isThirdLevelShift(ev)) { return true; } @@ -1534,10 +1542,10 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT * returned value is the new key code to pass to the PTY. * * Reference: http://invisible-island.net/xterm/ctlseqs/ctlseqs.html - * @param {KeyboardEvent} ev The keyboard event to be translated to key escape sequence. + * @param ev The keyboard event to be translated to key escape sequence. */ - private evaluateKeyEscapeSequence(ev) { - const result = { + private evaluateKeyEscapeSequence(ev: KeyboardEvent): {cancel: boolean, key: string, scrollDisp: number} { + const result: {cancel: boolean, key: string, scrollDisp: number} = { // Whether to cancel event propogation (NOTE: this may not be needed since the event is // canceled at the end of keyDown cancel: false, @@ -1546,7 +1554,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT // The number of characters to scroll, if this is defined it will cancel the event scrollDisp: undefined }; - const modifiers = ev.shiftKey << 0 | ev.altKey << 1 | ev.ctrlKey << 2 | ev.metaKey << 3; + const modifiers = (ev.shiftKey ? 1 : 0) | (ev.altKey ? 2 : 0) | (ev.ctrlKey ? 4 : 0) | (ev.metaKey ? 8 : 0); switch (ev.keyCode) { case 8: // backspace @@ -1843,7 +1851,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT * - https://developer.mozilla.org/en-US/docs/DOM/KeyboardEvent * @param {KeyboardEvent} ev The keypress event to be handled. */ - private keyPress(ev) { + private keyPress(ev: KeyboardEvent): boolean { let key; if (this.customKeyEventHandler && this.customKeyEventHandler(ev) === false) { @@ -1863,7 +1871,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT } if (!key || ( - (ev.altKey || ev.ctrlKey || ev.metaKey) && !isThirdLevelShift(this, ev) + (ev.altKey || ev.ctrlKey || ev.metaKey) && !isThirdLevelShift(ev) )) { return false; } @@ -1897,7 +1905,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT * Ring the bell. * Note: We could do sweet things with webaudio here */ - public bell() { + public bell(): void { if (!this.options.visualBell) return; this.element.style.borderColor = 'white'; setTimeout(() => { @@ -2064,7 +2072,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT /** * Set the range of refreshing to the maximum value */ - public maxRange() { + public maxRange(): void { this.refreshStart = 0; this.refreshEnd = this.rows - 1; } @@ -2073,7 +2081,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT * Setup the tab stops. * @param {number} i */ - public setupStops(i?: number) { + public setupStops(i?: number): void { if (i != null) { if (!this.buffer.tabs[i]) { i = this.prevStop(i); @@ -2146,7 +2154,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT /** * Clears the entire buffer, making the prompt line the new first line. */ - public clear() { + public clear(): void { if (this.buffer.ybase === 0 && this.buffer.y === 0) { // Don't clear if it's already clear return; @@ -2197,9 +2205,9 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT /** * If cur return the back color xterm feature attribute. Else return defAttr. - * @param {object} cur + * @param cur */ - public ch(cur) { + public ch(cur?: boolean): [number, string, number] { return cur ? [this.eraseAttr(), ' ', 1] : [this.defAttr, ' ', 1]; } @@ -2237,7 +2245,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) { + private 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. @@ -2254,7 +2262,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT /** * ESC D Index (IND is 0x84). */ - public index() { + public index(): void { this.buffer.y++; if (this.buffer.y > this.buffer.scrollBottom) { this.buffer.y--; @@ -2271,7 +2279,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT * * Move the cursor up one row, inserting a new blank line if necessary. */ - public reverseIndex() { + public reverseIndex(): void { if (this.buffer.y === this.buffer.scrollTop) { // possibly move the code below to term.reverseScroll(); // test: echo -ne '\e[1;1H\e[44m\eM\e[0m' @@ -2308,11 +2316,11 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT /** * ESC H Tab Set (HTS is 0x88). */ - private tabSet() { + private tabSet(): void { this.buffer.tabs[this.buffer.x] = true; } - public cancel(ev: Event, force?: boolean) { + public cancel(ev: Event, force?: boolean): boolean { if (!this.options.cancelEvents && !force) { return; } @@ -2323,7 +2331,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT // Expose to InputHandler // TODO: Revise when truecolor is introduced. - public matchColor(r1, g1, b1): any { + public matchColor(r1: number, g1: number, b1: number): any { const hash = (r1 << 16) | (g1 << 8) | b1; if (matchColorCache[hash] != null) { @@ -2375,41 +2383,25 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT * Helpers */ -function globalOn(el: any, type: string, handler: (event: Event) => any, capture?: boolean) { +function globalOn(el: any, type: string, handler: (event: Event) => any, capture?: boolean): void { if (!Array.isArray(el)) { el = [el]; } - el.forEach(function (element) { + el.forEach((element: HTMLElement) => { element.addEventListener(type, handler, capture || false); }); } // TODO: Remove once everything is typed const on = globalOn; -function off(el, type, handler, capture: boolean = false) { +function off(el: any, type: string, handler: (event: Event) => any, capture: boolean = false): void { el.removeEventListener(type, handler, capture); } -function inherits(child, parent) { - function f() { - this.constructor = child; - } - f.prototype = parent.prototype; - child.prototype = new f; -} - -function indexOf(obj, el) { - let i = obj.length; - while (i--) { - if (obj[i] === el) return i; - } - return -1; -} - -function isThirdLevelShift(term, ev) { +function isThirdLevelShift(ev: KeyboardEvent): boolean { const thirdLevelKey = - (term.browser.isMac && ev.altKey && !ev.ctrlKey && !ev.metaKey) || - (term.browser.isMSWindows && ev.altKey && ev.ctrlKey && !ev.metaKey); + (Browser.isMac && ev.altKey && !ev.ctrlKey && !ev.metaKey) || + (Browser.isMSWindows && ev.altKey && ev.ctrlKey && !ev.metaKey); if (ev.type === 'keypress') { return thirdLevelKey; @@ -2422,36 +2414,18 @@ function isThirdLevelShift(term, ev) { const matchColorCache = {}; // http://stackoverflow.com/questions/1633828 -const matchColorDistance = function(r1, g1, b1, r2, g2, b2) { +const matchColorDistance = function(r1: number, g1: number, b1: number, r2: number, g2: number, b2: number): number { return Math.pow(30 * (r1 - r2), 2) + Math.pow(59 * (g1 - g2), 2) + Math.pow(11 * (b1 - b2), 2); }; -function each(obj, iter, con) { - if (obj.forEach) return obj.forEach(iter, con); - for (let i = 0; i < obj.length; i++) { - iter.call(con, obj[i], i, obj); - } -} - -function wasMondifierKeyOnlyEvent(ev) { +function wasMondifierKeyOnlyEvent(ev: KeyboardEvent): boolean { return ev.keyCode === 16 || // Shift ev.keyCode === 17 || // Ctrl ev.keyCode === 18; // Alt } -function keys(obj) { - if (Object.keys) return Object.keys(obj); - const keys = []; - for (let key in obj) { - if (Object.prototype.hasOwnProperty.call(obj, key)) { - keys.push(key); - } - } - return keys; -} - /** * Expose */ diff --git a/src/Viewport.ts b/src/Viewport.ts index bd667edb6f..4ad4ddec47 100644 --- a/src/Viewport.ts +++ b/src/Viewport.ts @@ -92,7 +92,7 @@ export class Viewport implements IViewport { * terminal to scroll to it. * @param ev The scroll event. */ - private onScroll(ev: Event) { + private onScroll(ev: Event): void { const newRow = Math.round(this.viewportElement.scrollTop / this.currentRowHeight); const diff = newRow - this.terminal.buffer.ydisp; this.terminal.scrollDisp(diff, true); @@ -104,7 +104,7 @@ export class Viewport implements IViewport { * `Viewport`. * @param ev The mouse wheel event. */ - public onWheel(ev: WheelEvent) { + public onWheel(ev: WheelEvent): void { if (ev.deltaY === 0) { // Do nothing if it's not a vertical scroll event return; @@ -125,7 +125,7 @@ export class Viewport implements IViewport { * Handles the touchstart event, recording the touch occurred. * @param ev The touch event. */ - public onTouchStart(ev: TouchEvent) { + public onTouchStart(ev: TouchEvent): void { this.lastTouchY = ev.touches[0].pageY; }; @@ -133,7 +133,7 @@ export class Viewport implements IViewport { * Handles the touchmove event, scrolling the viewport if the position shifted. * @param ev The touch event. */ - public onTouchMove(ev: TouchEvent) { + public onTouchMove(ev: TouchEvent): void { let deltaY = this.lastTouchY - ev.touches[0].pageY; this.lastTouchY = ev.touches[0].pageY; if (deltaY === 0) { diff --git a/src/handlers/Clipboard.test.ts b/src/handlers/Clipboard.test.ts index a91ab87cae..e5bc3581fe 100644 --- a/src/handlers/Clipboard.test.ts +++ b/src/handlers/Clipboard.test.ts @@ -2,8 +2,8 @@ import { assert } from 'chai'; import * as Terminal from '../xterm'; import * as Clipboard from './Clipboard'; -describe('evaluatePastedTextProcessing', function () { - it('should replace carriage return + line feed with line feed on windows', function () { +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); diff --git a/src/handlers/Clipboard.ts b/src/handlers/Clipboard.ts index aa1c1400d7..ac9a7b8714 100644 --- a/src/handlers/Clipboard.ts +++ b/src/handlers/Clipboard.ts @@ -10,7 +10,7 @@ import { ITerminal, ISelectionManager } from '../Interfaces'; interface IWindow extends Window { clipboardData?: { getData(format: string): string; - setData(format: string, data: string); + setData(format: string, data: string): void; }; } @@ -31,7 +31,7 @@ export function prepareTextForTerminal(text: string, isMSWindows: boolean): stri * Binds copy functionality to the given terminal. * @param {ClipboardEvent} ev The original copy event to be handled */ -export function copyHandler(ev: ClipboardEvent, term: ITerminal, selectionManager: ISelectionManager) { +export function copyHandler(ev: ClipboardEvent, term: ITerminal, selectionManager: ISelectionManager): void { if (term.browser.isMSIE) { window.clipboardData.setData('Text', selectionManager.selectionText); } else { @@ -47,18 +47,17 @@ export function copyHandler(ev: ClipboardEvent, term: ITerminal, selectionManage * @param {ClipboardEvent} ev The original paste event to be handled * @param {Terminal} term The terminal on which to apply the handled paste event */ -export function pasteHandler(ev: ClipboardEvent, term: ITerminal) { +export function pasteHandler(ev: ClipboardEvent, term: ITerminal): void { ev.stopPropagation(); let text: string; - let dispatchPaste = function(text) { + let dispatchPaste = function(text: string): void { text = prepareTextForTerminal(text, term.browser.isMSWindows); term.handler(text); term.textarea.value = ''; term.emit('paste', text); - - return term.cancel(ev); + term.cancel(ev); }; if (term.browser.isMSIE) { @@ -79,7 +78,7 @@ export function pasteHandler(ev: ClipboardEvent, term: ITerminal) { * @param ev The original right click event to be handled. * @param textarea The terminal's textarea. */ -export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextAreaElement) { +export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextAreaElement): void { // Bring textarea at the cursor position textarea.style.position = 'fixed'; textarea.style.width = '20px'; @@ -91,7 +90,7 @@ export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextA textarea.focus(); // Reset the terminal textarea's styling - setTimeout(function () { + setTimeout(() => { textarea.style.position = null; textarea.style.width = null; textarea.style.height = null; @@ -107,7 +106,7 @@ export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextA * @param textarea The terminal's textarea. * @param selectionManager The terminal's selection manager. */ -export function rightClickHandler(ev: MouseEvent, textarea: HTMLTextAreaElement, selectionManager: ISelectionManager) { +export function rightClickHandler(ev: MouseEvent, textarea: HTMLTextAreaElement, selectionManager: ISelectionManager): void { moveTextAreaUnderMouseCursor(ev, textarea); // Get textarea ready to copy from the context menu diff --git a/src/utils/Generic.ts b/src/utils/Generic.ts index ce09c1bef0..9807232605 100644 --- a/src/utils/Generic.ts +++ b/src/utils/Generic.ts @@ -9,6 +9,6 @@ * @param {Array} array The array to search for the given element. * @param {Object} el The element to look for into the array */ -export function contains(arr: any[], el: any) { +export function contains(arr: any[], el: any): boolean { return arr.indexOf(el) >= 0; }; diff --git a/tslint.json b/tslint.json index f98fd4c4c0..4d4d4aa53c 100644 --- a/tslint.json +++ b/tslint.json @@ -9,6 +9,11 @@ true, "spaces" ], + "typedef": [ + true, + "call-signature", + "parameter" + ], "eofline": true, "no-eval": true, "no-internal-module": true, From 3d20c2f266d4a64b4ff027145382d9852955a510 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 5 Aug 2017 18:59:42 -0700 Subject: [PATCH 13/22] Resize both buffers on resize This brings in proper support to resize both buffers (#510) and fixes an exception that was caused by wrongfully not clearing the normal buffer when resizing while the alt buffer is active. There was an obscure bug in this that could have caused some great confusion later on; When switching to the alt buffer, a hard terminal reset was performed which tried to retain the buffers. However, because buffers was initialized in the Terminal constructor to a new BufferSet, the Terminal.buffer convenience pointer was pointing at a stale alt buffer which was the one actually being used, not Terminal.buffers.alt. Fixes #842 Fixes #510 --- src/Buffer.ts | 81 +++++++++++++++++++++++++++++++++++++++--- src/BufferSet.ts | 5 +++ src/Interfaces.ts | 1 + src/xterm.js | 89 +++++++---------------------------------------- 4 files changed, 94 insertions(+), 82 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index 12b2137d6c..bd3645e7f8 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -13,21 +13,21 @@ import { CircularList } from './utils/CircularList'; * - scroll position */ export class Buffer { - public lines: CircularList<[number, string, number][]>; + public readonly lines: CircularList<[number, string, number][]>; public savedY: number; public savedX: number; /** * Create a new Buffer. - * @param {Terminal} terminal - The terminal the Buffer will belong to + * @param {Terminal} _terminal - The terminal the Buffer will belong to * @param {number} ydisp - The scroll position of the Buffer in the viewport * @param {number} ybase - The scroll position of the y cursor (ybase + y = the y position within the Buffer) * @param {number} y - The cursor's y position after ybase * @param {number} x - The cursor's x position after ybase */ constructor( - private terminal: ITerminal, + private _terminal: ITerminal, public ydisp: number = 0, public ybase: number = 0, public y: number = 0, @@ -36,7 +36,78 @@ export class Buffer { public scrollTop: number = 0, public tabs: any = {}, ) { - this.lines = new CircularList<[number, string, number][]>(this.terminal.scrollback); - this.scrollBottom = this.terminal.rows - 1; + this.lines = new CircularList<[number, string, number][]>(this._terminal.scrollback); + this.scrollBottom = this._terminal.rows - 1; + } + + public resize(newCols: number, newRows: number): void { + // Don't resize the buffer if it's empty and hasn't been used yet. + if (this.lines.length === 0) { + return; + } + + // Deal with columns increasing (we don't do anything when columns reduce) + if (this._terminal.cols < newCols) { + const ch: [number, string, number] = [this._terminal.defAttr, ' ', 1]; // does xterm use the default attr? + for (let i = 0; i < this.lines.length; i++) { + if (this.lines.get(i) === undefined) { + this.lines.set(i, this._terminal.blankLine()); + } + while (this.lines.get(i).length < newCols) { + this.lines.get(i).push(ch); + } + } + } + + // Resize rows in both directions as needed + let addToY = 0; + if (this._terminal.rows < newRows) { + for (let y = this._terminal.rows; y < newRows; y++) { + if (this.lines.length < newRows + this.ybase) { + if (this.ybase > 0 && this.lines.length <= this.ybase + this.y + addToY + 1) { + // There is room above the buffer and there are no empty elements below the line, + // scroll up + this.ybase--; + addToY++; + if (this.ydisp > 0) { + // Viewport is at the top of the buffer, must increase downwards + this.ydisp--; + } + } else { + // Add a blank line if there is no buffer left at the top to scroll to, or if there + // are blank lines after the cursor + this.lines.push(this._terminal.blankLine()); + } + } + } + } else { // (this._terminal.rows >= newRows) + for (let y = this._terminal.rows; y > newRows; y--) { + if (this.lines.length > newRows + this.ybase) { + if (this.lines.length > this.ybase + this.y + 1) { + // The line is a blank line below the cursor, remove it + this.lines.pop(); + } else { + // The line is the cursor, scroll down + this.ybase++; + this.ydisp++; + } + } + } + } + + // Make sure that the cursor stays on screen + if (this.y >= newRows) { + this.y = newRows - 1; + } + if (addToY) { + this.y += addToY; + } + + if (this.x >= newCols) { + this.x = newCols - 1; + } + + this.scrollTop = 0; + this.scrollBottom = newRows - 1; } } diff --git a/src/BufferSet.ts b/src/BufferSet.ts index e86c098f5f..4a65dbfe16 100644 --- a/src/BufferSet.ts +++ b/src/BufferSet.ts @@ -65,4 +65,9 @@ export class BufferSet extends EventEmitter implements IBufferSet { this._activeBuffer = this._alt; this.emit('activate', this._alt); } + + public resize(newCols: number, newRows: number): void { + this._normal.resize(newCols, newRows); + this._alt.resize(newCols, newRows); + } } diff --git a/src/Interfaces.ts b/src/Interfaces.ts index f19a7f28f8..d463fcb795 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -48,6 +48,7 @@ export interface ITerminal { emit(event: string, data: any); reset(): void; showCursor(): void; + blankLine(cur?: boolean, isWrapped?: boolean); } export interface IBuffer { diff --git a/src/xterm.js b/src/xterm.js index 5aa069e796..92fc0df835 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -221,7 +221,7 @@ function Terminal(options) { this.surrogate_high = ''; // Create the terminal's buffers and set the current buffer - this.buffers = new BufferSet(this); + this.buffers = this.buffers || new BufferSet(this); this.buffer = this.buffers.active; // Convenience shortcut; this.buffers.on('activate', function (buffer) { this._terminal.buffer = buffer; @@ -1930,86 +1930,21 @@ Terminal.prototype.resize = function(x, y) { if (x < 1) x = 1; if (y < 1) y = 1; - // resize cols - j = this.cols; - if (j < x) { - ch = [this.defAttr, ' ', 1]; // does xterm use the default attr? - i = this.buffer.lines.length; - while (i--) { - if (this.buffer.lines.get(i) === undefined) { - this.buffer.lines.set(i, this.blankLine()); - } - while (this.buffer.lines.get(i).length < x) { - this.buffer.lines.get(i).push(ch); - } - } - } - - this.cols = x; - this.setupStops(this.cols); + this.buffers.resize(x, y); - // resize rows - j = this.rows; - addToY = 0; - if (j < y) { - el = this.element; - while (j++ < y) { - // y is rows, not this.buffer.y - if (this.buffer.lines.length < y + this.buffer.ybase) { - if (this.buffer.ybase > 0 && this.buffer.lines.length <= this.buffer.ybase + this.buffer.y + addToY + 1) { - // There is room above the buffer and there are no empty elements below the line, - // scroll up - this.buffer.ybase--; - addToY++; - if (this.buffer.ydisp > 0) { - // Viewport is at the top of the buffer, must increase downwards - this.buffer.ydisp--; - } - } else { - // Add a blank line if there is no buffer left at the top to scroll to, or if there - // are blank lines after the cursor - this.buffer.lines.push(this.blankLine()); - } - } - if (this.children.length < y) { - this.insertRow(); - } - } - } else { // (j > y) - while (j-- > y) { - if (this.buffer.lines.length > y + this.buffer.ybase) { - if (this.buffer.lines.length > this.buffer.ybase + this.buffer.y + 1) { - // The line is a blank line below the cursor, remove it - this.buffer.lines.pop(); - } else { - // The line is the cursor, scroll down - this.buffer.ybase++; - this.buffer.ydisp++; - } - } - if (this.children.length > y) { - el = this.children.shift(); - if (!el) continue; - el.parentNode.removeChild(el); - } - } - } - this.rows = y; - - // Make sure that the cursor stays on screen - if (this.buffer.y >= y) { - this.buffer.y = y - 1; - } - if (addToY) { - this.buffer.y += addToY; + // Adjust rows in the DOM to accurately reflect the new dimensions + while (this.children.length < y) { + this.insertRow(); } - - if (this.buffer.x >= x) { - this.buffer.x = x - 1; + while (this.children.length > y) { + el = this.children.shift(); + if (!el) continue; + el.parentNode.removeChild(el); } - this.buffer.scrollTop = 0; - this.buffer.scrollBottom = y - 1; + this.cols = x; + this.rows = y; + this.setupStops(this.cols); this.charMeasure.measure(); From f03d00a4977feefe2d5b92b63b6e54312ea93207 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 5 Aug 2017 19:42:52 -0700 Subject: [PATCH 14/22] Clean up buffer clean up/fill logic The alt buffer is now cleared immediated after activating the normal buffer and is filled when switching to it. The tests were failing because the alt buffer wasn't being cleared properly with the previous solution. --- src/Buffer.ts | 72 ++++++++++++++++++++++++------------ src/BufferSet.test.ts | 10 ++--- src/BufferSet.ts | 10 +++++ src/InputHandler.ts | 1 - src/Interfaces.ts | 2 +- src/SelectionManager.test.ts | 26 +++++++------ src/SelectionModel.test.ts | 5 ++- src/utils/CircularList.ts | 3 +- src/utils/TestUtils.ts | 53 ++++++++++++++++++++++++++ src/xterm.js | 9 +---- 10 files changed, 139 insertions(+), 52 deletions(-) create mode 100644 src/utils/TestUtils.ts diff --git a/src/Buffer.ts b/src/Buffer.ts index bd3645e7f8..714151d05d 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -2,7 +2,7 @@ * @license MIT */ -import { ITerminal } from './Interfaces'; +import { ITerminal, IBuffer } from './Interfaces'; import { CircularList } from './utils/CircularList'; /** @@ -12,9 +12,16 @@ import { CircularList } from './utils/CircularList'; * - cursor position * - scroll position */ -export class Buffer { - public readonly lines: CircularList<[number, string, number][]>; +export class Buffer implements IBuffer { + private _lines: CircularList<[number, string, number][]>; + public ydisp: number; + public ybase: number; + public y: number; + public x: number; + public scrollBottom: number; + public scrollTop: number; + public tabs: any; public savedY: number; public savedX: number; @@ -27,34 +34,51 @@ export class Buffer { * @param {number} x - The cursor's x position after ybase */ constructor( - private _terminal: ITerminal, - public ydisp: number = 0, - public ybase: number = 0, - public y: number = 0, - public x: number = 0, - public scrollBottom: number = 0, - public scrollTop: number = 0, - public tabs: any = {}, + private _terminal: ITerminal ) { - this.lines = new CircularList<[number, string, number][]>(this._terminal.scrollback); + this.clear(); + } + + public get lines(): CircularList<[number, string, number][]> { + return this._lines; + } + + public fillViewportRows(): void { + if (this._lines.length === 0) { + let i = this._terminal.rows; + while (i--) { + this.lines.push(this._terminal.blankLine()); + } + } + } + + public clear(): void { + this.ydisp = 0; + this.ybase = 0; + this.y = 0; + this.x = 0; + this.scrollBottom = 0; + this.scrollTop = 0; + this.tabs = {}; + this._lines = new CircularList<[number, string, number][]>(this._terminal.scrollback); this.scrollBottom = this._terminal.rows - 1; } public resize(newCols: number, newRows: number): void { // Don't resize the buffer if it's empty and hasn't been used yet. - if (this.lines.length === 0) { + if (this._lines.length === 0) { return; } // Deal with columns increasing (we don't do anything when columns reduce) if (this._terminal.cols < newCols) { const ch: [number, string, number] = [this._terminal.defAttr, ' ', 1]; // does xterm use the default attr? - for (let i = 0; i < this.lines.length; i++) { - if (this.lines.get(i) === undefined) { - this.lines.set(i, this._terminal.blankLine()); + for (let i = 0; i < this._lines.length; i++) { + if (this._lines.get(i) === undefined) { + this._lines.set(i, this._terminal.blankLine()); } - while (this.lines.get(i).length < newCols) { - this.lines.get(i).push(ch); + while (this._lines.get(i).length < newCols) { + this._lines.get(i).push(ch); } } } @@ -63,8 +87,8 @@ export class Buffer { let addToY = 0; if (this._terminal.rows < newRows) { for (let y = this._terminal.rows; y < newRows; y++) { - if (this.lines.length < newRows + this.ybase) { - if (this.ybase > 0 && this.lines.length <= this.ybase + this.y + addToY + 1) { + if (this._lines.length < newRows + this.ybase) { + if (this.ybase > 0 && this._lines.length <= this.ybase + this.y + addToY + 1) { // There is room above the buffer and there are no empty elements below the line, // scroll up this.ybase--; @@ -76,16 +100,16 @@ export class Buffer { } else { // Add a blank line if there is no buffer left at the top to scroll to, or if there // are blank lines after the cursor - this.lines.push(this._terminal.blankLine()); + this._lines.push(this._terminal.blankLine()); } } } } else { // (this._terminal.rows >= newRows) for (let y = this._terminal.rows; y > newRows; y--) { - if (this.lines.length > newRows + this.ybase) { - if (this.lines.length > this.ybase + this.y + 1) { + if (this._lines.length > newRows + this.ybase) { + if (this._lines.length > this.ybase + this.y + 1) { // The line is a blank line below the cursor, remove it - this.lines.pop(); + this._lines.pop(); } else { // The line is the cursor, scroll down this.ybase++; diff --git a/src/BufferSet.test.ts b/src/BufferSet.test.ts index 2101fbc1f4..ab814cee61 100644 --- a/src/BufferSet.test.ts +++ b/src/BufferSet.test.ts @@ -5,17 +5,17 @@ import { assert } from 'chai'; import { ITerminal } from './Interfaces'; import { BufferSet } from './BufferSet'; import { Buffer } from './Buffer'; +import { MockTerminal } from './utils/TestUtils'; describe('BufferSet', () => { let terminal: ITerminal; let bufferSet: BufferSet; beforeEach(() => { - terminal = { - cols: 80, - rows: 24, - scrollback: 1000 - }; + terminal = new MockTerminal(); + terminal.cols = 80; + terminal.rows = 24; + terminal.scrollback = 1000; bufferSet = new BufferSet(terminal); }); diff --git a/src/BufferSet.ts b/src/BufferSet.ts index 4a65dbfe16..345191cfa2 100644 --- a/src/BufferSet.ts +++ b/src/BufferSet.ts @@ -22,6 +22,7 @@ export class BufferSet extends EventEmitter implements IBufferSet { constructor(private _terminal: ITerminal) { super(); this._normal = new Buffer(this._terminal); + this._normal.fillViewportRows(); this._alt = new Buffer(this._terminal); this._activeBuffer = this._normal; } @@ -54,6 +55,11 @@ export class BufferSet extends EventEmitter implements IBufferSet { * Sets the normal Buffer of the BufferSet as its currently active Buffer */ public activateNormalBuffer(): void { + // The alt buffer should always be cleared when we switch to the normal + // buffer. This frees up memory since the alt buffer should always be new + // when activated. + this._alt.clear(); + this._activeBuffer = this._normal; this.emit('activate', this._normal); } @@ -62,6 +68,10 @@ export class BufferSet extends EventEmitter implements IBufferSet { * Sets the alt Buffer of the BufferSet as its currently active Buffer */ public activateAltBuffer(): void { + // Since the alt buffer is always cleared when the normal buffer is + // activated, we want to fill it when switching to it. + this._alt.fillViewportRows(); + this._activeBuffer = this._alt; this.emit('activate', this._alt); } diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 64da3b3f15..b915f2c0b9 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -954,7 +954,6 @@ export class InputHandler implements IInputHandler { case 47: // alt screen buffer case 1047: // alt screen buffer this._terminal.buffers.activateAltBuffer(); - this._terminal.reset(); this._terminal.viewport.syncScrollArea(); this._terminal.showCursor(); break; diff --git a/src/Interfaces.ts b/src/Interfaces.ts index d463fcb795..70a46d419a 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -93,8 +93,8 @@ export interface ILinkifier { export interface ICircularList extends IEventEmitter { length: number; maxLength: number; + forEach: (callbackfn: (value: T, index: number) => void) => void; - forEach(callbackfn: (value: T, index: number, array: T[]) => void): void; get(index: number): T; set(index: number, value: T): void; push(value: T): void; diff --git a/src/SelectionManager.test.ts b/src/SelectionManager.test.ts index c2173d77f4..7beafd977f 100644 --- a/src/SelectionManager.test.ts +++ b/src/SelectionManager.test.ts @@ -9,6 +9,7 @@ import { CircularList } from './utils/CircularList'; import { SelectionManager } from './SelectionManager'; import { SelectionModel } from './SelectionModel'; import { BufferSet } from './BufferSet'; +import { MockTerminal } from './utils/TestUtils'; class TestSelectionManager extends SelectionManager { constructor( @@ -46,7 +47,9 @@ describe('SelectionManager', () => { window = dom.window; document = window.document; rowContainer = document.createElement('div'); - terminal = { cols: 80, rows: 2 }; + terminal = new MockTerminal(); + terminal.cols = 80; + terminal.rows = 2; terminal.scrollback = 100; terminal.buffers = new BufferSet(terminal); terminal.buffer = terminal.buffers.active; @@ -64,7 +67,7 @@ describe('SelectionManager', () => { describe('_selectWordAt', () => { it('should expand selection for normal width chars', () => { - bufferLines.push(stringToRow('foo bar')); + bufferLines.set(0, stringToRow('foo bar')); selectionManager.selectWordAt([0, 0]); assert.equal(selectionManager.selectionText, 'foo'); selectionManager.selectWordAt([1, 0]); @@ -81,7 +84,7 @@ describe('SelectionManager', () => { assert.equal(selectionManager.selectionText, 'bar'); }); it('should expand selection for whitespace', () => { - bufferLines.push(stringToRow('a b')); + bufferLines.set(0, stringToRow('a b')); selectionManager.selectWordAt([0, 0]); assert.equal(selectionManager.selectionText, 'a'); selectionManager.selectWordAt([1, 0]); @@ -95,7 +98,7 @@ describe('SelectionManager', () => { }); it('should expand selection for wide characters', () => { // Wide characters use a special format - bufferLines.push([ + bufferLines.set(0, [ [null, '中', 2], [null, '', 0], [null, '文', 2], @@ -147,7 +150,7 @@ describe('SelectionManager', () => { assert.equal(selectionManager.selectionText, 'foo'); }); it('should select up to non-path characters that are commonly adjacent to paths', () => { - bufferLines.push(stringToRow('(cd)[ef]{gh}\'ij"')); + bufferLines.set(0, stringToRow('(cd)[ef]{gh}\'ij"')); selectionManager.selectWordAt([0, 0]); assert.equal(selectionManager.selectionText, '(cd'); selectionManager.selectWordAt([1, 0]); @@ -185,7 +188,7 @@ describe('SelectionManager', () => { describe('_selectLineAt', () => { it('should select the entire line', () => { - bufferLines.push(stringToRow('foo bar')); + bufferLines.set(0, stringToRow('foo bar')); selectionManager.selectLineAt(0); assert.equal(selectionManager.selectionText, 'foo bar', 'The selected text is correct'); assert.deepEqual(selectionManager.model.finalSelectionStart, [0, 0]); @@ -195,11 +198,12 @@ describe('SelectionManager', () => { describe('selectAll', () => { it('should select the entire buffer, beyond the viewport', () => { - bufferLines.push(stringToRow('1')); - bufferLines.push(stringToRow('2')); - bufferLines.push(stringToRow('3')); - bufferLines.push(stringToRow('4')); - bufferLines.push(stringToRow('5')); + bufferLines.length = 5; + bufferLines.set(0, stringToRow('1')); + bufferLines.set(1, stringToRow('2')); + bufferLines.set(2, stringToRow('3')); + bufferLines.set(3, stringToRow('4')); + bufferLines.set(4, stringToRow('5')); selectionManager.selectAll(); terminal.buffer.ybase = bufferLines.length - terminal.rows; assert.equal(selectionManager.selectionText, '1\n2\n3\n4\n5'); diff --git a/src/SelectionModel.test.ts b/src/SelectionModel.test.ts index 6da3874bd4..b087944273 100644 --- a/src/SelectionModel.test.ts +++ b/src/SelectionModel.test.ts @@ -5,6 +5,7 @@ import { assert } from 'chai'; import { ITerminal } from './Interfaces'; import { SelectionModel } from './SelectionModel'; import {BufferSet} from './BufferSet'; +import { MockTerminal } from './utils/TestUtils'; class TestSelectionModel extends SelectionModel { constructor( @@ -22,7 +23,9 @@ describe('SelectionManager', () => { let model: TestSelectionModel; beforeEach(() => { - terminal = { cols: 80, rows: 2, ybase: 0 }; + terminal = new MockTerminal(); + terminal.cols = 80; + terminal.rows = 2; terminal.scrollback = 10; terminal.buffers = new BufferSet(terminal); terminal.buffer = terminal.buffers.active; diff --git a/src/utils/CircularList.ts b/src/utils/CircularList.ts index d0b2f685a4..54850ab7c3 100644 --- a/src/utils/CircularList.ts +++ b/src/utils/CircularList.ts @@ -5,8 +5,9 @@ * @license MIT */ import { EventEmitter } from '../EventEmitter'; +import { ICircularList } from '../Interfaces'; -export class CircularList extends EventEmitter { +export class CircularList extends EventEmitter implements ICircularList { private _array: T[]; private _startIndex: number; private _length: number; diff --git a/src/utils/TestUtils.ts b/src/utils/TestUtils.ts new file mode 100644 index 0000000000..fbb172616e --- /dev/null +++ b/src/utils/TestUtils.ts @@ -0,0 +1,53 @@ +import { ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager } from '../Interfaces'; + +export class MockTerminal implements ITerminal { + public element: HTMLElement; + public rowContainer: HTMLElement; + public selectionContainer: HTMLElement; + public selectionManager: ISelectionManager; + public charMeasure: ICharMeasure; + public textarea: HTMLTextAreaElement; + public rows: number; + public cols: number; + public browser: IBrowser; + public writeBuffer: string[]; + public children: HTMLElement[]; + public cursorHidden: boolean; + public cursorState: number; + public defAttr: number; + public scrollback: number; + public buffers: IBufferSet; + public buffer: IBuffer; + + handler(data: string) { + throw new Error('Method not implemented.'); + } + on(event: string, callback: () => void) { + throw new Error('Method not implemented.'); + } + scrollDisp(disp: number, suppressScrollEvent: boolean) { + throw new Error('Method not implemented.'); + } + cancel(ev: Event, force?: boolean) { + throw new Error('Method not implemented.'); + } + log(text: string): void { + throw new Error('Method not implemented.'); + } + emit(event: string, data: any) { + throw new Error('Method not implemented.'); + } + reset(): void { + throw new Error('Method not implemented.'); + } + showCursor(): void { + throw new Error('Method not implemented.'); + } + blankLine(cur?: boolean, isWrapped?: boolean) { + const line = []; + for (let i = 0; i < this.cols; i++) { + line.push([0, ' ', 1]); + } + return line; + } +} diff --git a/src/xterm.js b/src/xterm.js index 92fc0df835..e9e3465e6b 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -221,17 +221,12 @@ function Terminal(options) { this.surrogate_high = ''; // Create the terminal's buffers and set the current buffer - this.buffers = this.buffers || new BufferSet(this); + this.buffers = new BufferSet(this); this.buffer = this.buffers.active; // Convenience shortcut; this.buffers.on('activate', function (buffer) { this._terminal.buffer = buffer; }); - var i = this.rows; - - while (i--) { - this.buffer.lines.push(this.blankLine()); - } // Ensure the selection manager has the correct buffer if (this.selectionManager) { this.selectionManager.setBuffer(this.buffer.lines); @@ -2228,12 +2223,10 @@ Terminal.prototype.reset = function() { var customKeyEventHandler = this.customKeyEventHandler; var cursorBlinkInterval = this.cursorBlinkInterval; var inputHandler = this.inputHandler; - var buffers = this.buffers; Terminal.call(this, this.options); this.customKeyEventHandler = customKeyEventHandler; this.cursorBlinkInterval = cursorBlinkInterval; this.inputHandler = inputHandler; - this.buffers = buffers; this.refresh(0, this.rows - 1); this.viewport.syncScrollArea(); }; From 13619d427ad06dc54aad67dbad7c17ec616e75d0 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 5 Aug 2017 20:11:26 -0700 Subject: [PATCH 15/22] Add a bunch of Buffer tests :tada: --- src/Buffer.test.ts | 123 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 118 insertions(+), 5 deletions(-) diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index f68baa82ac..66cf15c415 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -5,17 +5,20 @@ import { assert } from 'chai'; import { ITerminal } from './Interfaces'; import { Buffer } from './Buffer'; import { CircularList } from './utils/CircularList'; +import { MockTerminal } from './utils/TestUtils'; + +const INIT_COLS = 80; +const INIT_ROWS = 24; describe('Buffer', () => { let terminal: ITerminal; let buffer: Buffer; beforeEach(() => { - terminal = { - cols: 80, - rows: 24, - scrollback: 1000 - }; + terminal = new MockTerminal(); + terminal.cols = INIT_COLS; + terminal.rows = INIT_ROWS; + terminal.scrollback = 1000; buffer = new Buffer(terminal); }); @@ -28,4 +31,114 @@ describe('Buffer', () => { assert.equal(buffer.scrollBottom, terminal.rows - 1); }); }); + + describe('resize', () => { + describe('column size is reduced', () => { + it('should not trim the data in the buffer', () => { + buffer.fillViewportRows(); + for (let i = 0; i < INIT_ROWS; i++) { + assert.equal(buffer.lines.length, INIT_ROWS); + assert.equal(buffer.lines.get(i).length, INIT_COLS); + } + buffer.resize(INIT_COLS / 2, INIT_ROWS); + assert.equal(buffer.lines.length, INIT_ROWS); + for (let i = 0; i < INIT_ROWS; i++) { + assert.equal(buffer.lines.get(i).length, INIT_COLS); + } + }); + }); + + describe('column size is increased', () => { + it('should add pad columns', () => { + buffer.fillViewportRows(); + assert.equal(buffer.lines.length, INIT_ROWS); + for (let i = 0; i < INIT_ROWS; i++) { + assert.equal(buffer.lines.get(i).length, INIT_COLS); + } + buffer.resize(INIT_COLS + 10, INIT_ROWS); + assert.equal(buffer.lines.length, INIT_ROWS); + for (let i = 0; i < INIT_ROWS; i++) { + assert.equal(buffer.lines.get(i).length, INIT_COLS + 10); + } + }); + }); + + describe('row size reduced', () => { + it('should trim blank lines from the end', () => { + buffer.fillViewportRows(); + assert.equal(buffer.lines.length, INIT_ROWS); + buffer.resize(INIT_COLS, INIT_ROWS - 10); + assert.equal(buffer.lines.length, INIT_ROWS - 10); + }); + + it('should move the viewport down when it\'s at the end', () => { + buffer.fillViewportRows(); + assert.equal(buffer.lines.length, INIT_ROWS); + // Set cursor y to have 5 blank lines below it + buffer.y = INIT_ROWS - 5 - 1; + buffer.resize(INIT_COLS, INIT_ROWS - 10); + // Trim 5 rows + assert.equal(buffer.lines.length, INIT_ROWS - 5); + // Shift the viewport down 5 rows + assert.equal(buffer.ydisp, 5); + assert.equal(buffer.ybase, 5); + }); + }); + + describe('row size increased', () => { + describe('empty buffer', () => { + it('should add blank lines to end', () => { + buffer.fillViewportRows(); + assert.equal(buffer.ydisp, 0); + assert.equal(buffer.lines.length, INIT_ROWS); + buffer.resize(INIT_COLS, INIT_ROWS + 10); + assert.equal(buffer.ydisp, 0); + assert.equal(buffer.lines.length, INIT_ROWS + 10); + }); + }); + + describe('filled buffer', () => { + it('should show more of the buffer above', () => { + buffer.fillViewportRows(); + // Create 10 extra blank lines + for (let i = 0; i < 10; i++) { + buffer.lines.push(terminal.blankLine()); + } + // Set cursor to the bottom of the buffer + buffer.y = INIT_ROWS - 1; + // Scroll down 10 lines + buffer.ybase = 10; + buffer.ydisp = 10; + assert.equal(buffer.lines.length, INIT_ROWS + 10); + buffer.resize(INIT_COLS, INIT_ROWS + 5); + // Should be should 5 more lines + assert.equal(buffer.ydisp, 5); + assert.equal(buffer.ybase, 5); + // Should not trim the buffer + assert.equal(buffer.lines.length, INIT_ROWS + 10); + }); + + it('should show more of the buffer below when the viewport is at the top of the buffer', () => { + buffer.fillViewportRows(); + // Create 10 extra blank lines + for (let i = 0; i < 10; i++) { + buffer.lines.push(terminal.blankLine()); + } + // Set cursor to the bottom of the buffer + buffer.y = INIT_ROWS - 1; + // Scroll down 10 lines + buffer.ybase = 10; + buffer.ydisp = 0; + assert.equal(buffer.lines.length, INIT_ROWS + 10); + buffer.resize(INIT_COLS, INIT_ROWS + 5); + // The viewport should remain at the top + assert.equal(buffer.ydisp, 0); + // The buffer ybase should move up 5 lines + assert.equal(buffer.ybase, 5); + // Should not trim the buffer + assert.equal(buffer.lines.length, INIT_ROWS + 10); + }); + }); + }); + }); }); From d46003843b80416490dc2819420601037ed04060 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 5 Aug 2017 20:15:23 -0700 Subject: [PATCH 16/22] Add tests for Buffer.fillViewportRows --- src/Buffer.test.ts | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index 66cf15c415..5883a8b5dd 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -32,14 +32,24 @@ describe('Buffer', () => { }); }); + describe('fillViewportRows', () => { + it('should fill the buffer with blank lines based on the size of the viewport', () => { + const blankLineChar = terminal.blankLine()[0]; + buffer.fillViewportRows(); + assert.equal(buffer.lines.length, INIT_ROWS); + for (let y = 0; y < INIT_ROWS; y++) { + assert.equal(buffer.lines.get(y).length, INIT_COLS); + for (let x = 0; x < INIT_COLS; x++) { + assert.deepEqual(buffer.lines.get(y)[x], blankLineChar); + } + } + }); + }); + describe('resize', () => { describe('column size is reduced', () => { it('should not trim the data in the buffer', () => { buffer.fillViewportRows(); - for (let i = 0; i < INIT_ROWS; i++) { - assert.equal(buffer.lines.length, INIT_ROWS); - assert.equal(buffer.lines.get(i).length, INIT_COLS); - } buffer.resize(INIT_COLS / 2, INIT_ROWS); assert.equal(buffer.lines.length, INIT_ROWS); for (let i = 0; i < INIT_ROWS; i++) { @@ -51,10 +61,6 @@ describe('Buffer', () => { describe('column size is increased', () => { it('should add pad columns', () => { buffer.fillViewportRows(); - assert.equal(buffer.lines.length, INIT_ROWS); - for (let i = 0; i < INIT_ROWS; i++) { - assert.equal(buffer.lines.get(i).length, INIT_COLS); - } buffer.resize(INIT_COLS + 10, INIT_ROWS); assert.equal(buffer.lines.length, INIT_ROWS); for (let i = 0; i < INIT_ROWS; i++) { @@ -66,14 +72,12 @@ describe('Buffer', () => { describe('row size reduced', () => { it('should trim blank lines from the end', () => { buffer.fillViewportRows(); - assert.equal(buffer.lines.length, INIT_ROWS); buffer.resize(INIT_COLS, INIT_ROWS - 10); assert.equal(buffer.lines.length, INIT_ROWS - 10); }); it('should move the viewport down when it\'s at the end', () => { buffer.fillViewportRows(); - assert.equal(buffer.lines.length, INIT_ROWS); // Set cursor y to have 5 blank lines below it buffer.y = INIT_ROWS - 5 - 1; buffer.resize(INIT_COLS, INIT_ROWS - 10); @@ -90,7 +94,6 @@ describe('Buffer', () => { it('should add blank lines to end', () => { buffer.fillViewportRows(); assert.equal(buffer.ydisp, 0); - assert.equal(buffer.lines.length, INIT_ROWS); buffer.resize(INIT_COLS, INIT_ROWS + 10); assert.equal(buffer.ydisp, 0); assert.equal(buffer.lines.length, INIT_ROWS + 10); From 2878d978c4e38473ab3843e6c3085a7e24e47fae Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 5 Aug 2017 21:06:04 -0700 Subject: [PATCH 17/22] Fix lint --- src/Interfaces.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Interfaces.ts b/src/Interfaces.ts index e24fbd5b76..506032a104 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -46,7 +46,7 @@ export interface ITerminal extends IEventEmitter { log(text: string): void; reset(): void; showCursor(): void; - blankLine(cur?: boolean, isWrapped?: boolean); + blankLine(cur?: boolean, isWrapped?: boolean): [number, string, number]; } /** From ae3169e6ae12605e9b966182c2b1141c216d6ed4 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 5 Aug 2017 21:11:16 -0700 Subject: [PATCH 18/22] Add CharData and LineData --- src/Buffer.ts | 9 +++++---- src/InputHandler.ts | 7 ++++--- src/Interfaces.ts | 10 +++++----- src/SelectionManager.test.ts | 9 +++++---- src/SelectionManager.ts | 5 +++-- src/Terminal.ts | 14 +++++++------- src/Types.ts | 3 +++ src/utils/TestUtils.ts | 5 +++-- 8 files changed, 35 insertions(+), 27 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index 58cf8a8e5b..9a28438d80 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -4,6 +4,7 @@ import { ITerminal, IBuffer } from './Interfaces'; import { CircularList } from './utils/CircularList'; +import { LineData, CharData } from './Types'; /** * This class represents a terminal buffer (an internal state of the terminal), where the @@ -13,7 +14,7 @@ import { CircularList } from './utils/CircularList'; * - scroll position */ export class Buffer implements IBuffer { - private _lines: CircularList<[number, string, number][]>; + private _lines: CircularList; public ydisp: number; public ybase: number; @@ -39,7 +40,7 @@ export class Buffer implements IBuffer { this.clear(); } - public get lines(): CircularList<[number, string, number][]> { + public get lines(): CircularList { return this._lines; } @@ -60,7 +61,7 @@ export class Buffer implements IBuffer { this.scrollBottom = 0; this.scrollTop = 0; this.tabs = {}; - this._lines = new CircularList<[number, string, number][]>(this._terminal.options.scrollback); + this._lines = new CircularList(this._terminal.options.scrollback); this.scrollBottom = this._terminal.rows - 1; } @@ -72,7 +73,7 @@ export class Buffer implements IBuffer { // Deal with columns increasing (we don't do anything when columns reduce) if (this._terminal.cols < newCols) { - const ch: [number, string, number] = [this._terminal.defAttr, ' ', 1]; // does xterm use the default attr? + const ch: CharData = [this._terminal.defAttr, ' ', 1]; // does xterm use the default attr? for (let i = 0; i < this._lines.length; i++) { if (this._lines.get(i) === undefined) { this._lines.set(i, this._terminal.blankLine()); diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 2b4d01d011..91160134a7 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -5,6 +5,7 @@ import { IInputHandler, ITerminal, IInputHandlingTerminal } from './Interfaces'; import { C0 } from './EscapeSequences'; import { DEFAULT_CHARSET } from './Charsets'; +import { CharData } from './Types'; /** * The terminal's standard implementation of IInputHandler, this handles all @@ -194,7 +195,7 @@ export class InputHandler implements IInputHandler { const row = this._terminal.buffer.y + this._terminal.buffer.ybase; let j = this._terminal.buffer.x; - const ch: [number, string, number] = [this._terminal.eraseAttr(), ' ', 1]; // xterm + const ch: CharData = [this._terminal.eraseAttr(), ' ', 1]; // xterm while (param-- && j < this._terminal.cols) { this._terminal.buffer.lines.get(row).splice(j++, 0, ch); @@ -510,7 +511,7 @@ export class InputHandler implements IInputHandler { } const row = this._terminal.buffer.y + this._terminal.buffer.ybase; - const ch: [number, string, number] = [this._terminal.eraseAttr(), ' ', 1]; // xterm + const ch: CharData = [this._terminal.eraseAttr(), ' ', 1]; // xterm while (param--) { this._terminal.buffer.lines.get(row).splice(this._terminal.buffer.x, 1); @@ -558,7 +559,7 @@ export class InputHandler implements IInputHandler { const row = this._terminal.buffer.y + this._terminal.buffer.ybase; let j = this._terminal.buffer.x; - const ch: [number, string, number] = [this._terminal.eraseAttr(), ' ', 1]; // xterm + const ch: CharData = [this._terminal.eraseAttr(), ' ', 1]; // xterm while (param-- && j < this._terminal.cols) { this._terminal.buffer.lines.get(row)[j++] = ch; diff --git a/src/Interfaces.ts b/src/Interfaces.ts index 506032a104..63058f091c 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -3,7 +3,7 @@ */ import { ILinkMatcherOptions } from './Interfaces'; -import { LinkMatcherHandler, LinkMatcherValidationCallback, Charset } from './Types'; +import { LinkMatcherHandler, LinkMatcherValidationCallback, Charset, LineData } from './Types'; export interface IBrowser { isNode: boolean; @@ -46,7 +46,7 @@ export interface ITerminal extends IEventEmitter { log(text: string): void; reset(): void; showCursor(): void; - blankLine(cur?: boolean, isWrapped?: boolean): [number, string, number]; + blankLine(cur?: boolean, isWrapped?: boolean): LineData; } /** @@ -97,7 +97,7 @@ export interface IInputHandlingTerminal extends IEventEmitter { eraseRight(x: number, y: number): void; eraseLine(y: number): void; eraseLeft(x: number, y: number): void; - blankLine(cur?: boolean, isWrapped?: boolean): [number, string, number][]; + blankLine(cur?: boolean, isWrapped?: boolean): LineData; prevStop(x?: number): number; is(term: string): boolean; send(data: string): void; @@ -134,7 +134,7 @@ export interface ITerminalOptions { } export interface IBuffer { - lines: ICircularList<[number, string, number][]>; + lines: ICircularList; ydisp: number; ybase: number; y: number; @@ -166,7 +166,7 @@ export interface ISelectionManager { disable(): void; enable(): void; - setBuffer(buffer: ICircularList<[number, string, number][]>): void; + setBuffer(buffer: ICircularList): void; setSelection(row: number, col: number, length: number): void; } diff --git a/src/SelectionManager.test.ts b/src/SelectionManager.test.ts index f640d42416..a39c4dda07 100644 --- a/src/SelectionManager.test.ts +++ b/src/SelectionManager.test.ts @@ -10,11 +10,12 @@ import { SelectionManager } from './SelectionManager'; import { SelectionModel } from './SelectionModel'; import { BufferSet } from './BufferSet'; import { MockTerminal } from './utils/TestUtils'; +import { LineData } from './Types'; class TestSelectionManager extends SelectionManager { constructor( terminal: ITerminal, - buffer: ICircularList<[number, string, number][]>, + buffer: ICircularList, rowContainer: HTMLElement, charMeasure: CharMeasure ) { @@ -38,7 +39,7 @@ describe('SelectionManager', () => { let document: Document; let terminal: ITerminal; - let bufferLines: ICircularList<[number, string, number][]>; + let bufferLines: ICircularList; let rowContainer: HTMLElement; let selectionManager: TestSelectionManager; @@ -57,8 +58,8 @@ describe('SelectionManager', () => { selectionManager = new TestSelectionManager(terminal, bufferLines, rowContainer, null); }); - function stringToRow(text: string): [number, string, number][] { - let result: [number, string, number][] = []; + function stringToRow(text: string): LineData { + let result: LineData = []; for (let i = 0; i < text.length; i++) { result.push([0, text.charAt(i), 1]); } diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index e3770c06da..781a8eda3f 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -10,6 +10,7 @@ import { EventEmitter } from './EventEmitter'; import { ITerminal, ICircularList, ISelectionManager } from './Interfaces'; import { SelectionModel } from './SelectionModel'; import { translateBufferLineToString } from './utils/BufferLine'; +import { LineData } from './Types'; /** * The number of pixels the mouse needs to be above or below the viewport in @@ -101,7 +102,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager constructor( private _terminal: ITerminal, - private _buffer: ICircularList<[number, string, number][]>, + private _buffer: ICircularList, private _rowContainer: HTMLElement, private _charMeasure: CharMeasure ) { @@ -150,7 +151,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager * switched in or out. * @param buffer The active buffer. */ - public setBuffer(buffer: ICircularList<[number, string, number][]>): void { + public setBuffer(buffer: ICircularList): void { this._buffer = buffer; this.clearSelection(); } diff --git a/src/Terminal.ts b/src/Terminal.ts index 3431df7ff5..360c2b7e36 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -29,7 +29,7 @@ import * as Mouse from './utils/Mouse'; import { CHARSETS } from './Charsets'; import { getRawByteCoords } from './utils/Mouse'; import { translateBufferLineToString } from './utils/BufferLine'; -import { CustomKeyEventHandler, Charset, LinkMatcherHandler, LinkMatcherValidationCallback } from './Types'; +import { CustomKeyEventHandler, Charset, LinkMatcherHandler, LinkMatcherValidationCallback, CharData, LineData } from './Types'; import { ITerminal, IBrowser, ITerminalOptions, IInputHandlingTerminal, ILinkMatcherOptions } from './Interfaces'; // Declare for RequireJS in loadAddon @@ -2056,7 +2056,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT if (!line) { return; } - const ch: [number, string, number] = [this.eraseAttr(), ' ', 1]; // xterm + const ch: CharData = [this.eraseAttr(), ' ', 1]; // xterm for (; x < this.cols; x++) { line[x] = ch; } @@ -2073,7 +2073,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT if (!line) { return; } - const ch: [number, string, number] = [this.eraseAttr(), ' ', 1]; // xterm + const ch: CharData = [this.eraseAttr(), ' ', 1]; // xterm x++; while (x--) { line[x] = ch; @@ -2114,11 +2114,11 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT * @param {boolean} cur First bunch of data for each "blank" character. * @param {boolean} isWrapped Whether the new line is wrapped from the previous line. */ - public blankLine(cur?: boolean, isWrapped?: boolean): [number, string, number][] { + public blankLine(cur?: boolean, isWrapped?: boolean): LineData { const attr = cur ? this.eraseAttr() : this.defAttr; - const ch = [attr, ' ', 1]; // width defaults to 1 halfwidth character - const line = []; + const ch: CharData = [attr, ' ', 1]; // width defaults to 1 halfwidth character + const line: LineData = []; // TODO: It is not ideal that this is a property on an array, a buffer line // class should be added that will hold this data and other useful functions. @@ -2137,7 +2137,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT * If cur return the back color xterm feature attribute. Else return defAttr. * @param cur */ - public ch(cur?: boolean): [number, string, number] { + public ch(cur?: boolean): CharData { return cur ? [this.eraseAttr(), ' ', 1] : [this.defAttr, ' ', 1]; } diff --git a/src/Types.ts b/src/Types.ts index 34c6d94a05..0753d80e19 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -15,3 +15,6 @@ export type LinkMatcherValidationCallback = (uri: string, element: HTMLElement, export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean; export type Charset = {[key: string]: string}; + +export type CharData = [number, string, number]; +export type LineData = CharData[]; diff --git a/src/utils/TestUtils.ts b/src/utils/TestUtils.ts index 9fc929fe4d..51caa8fe92 100644 --- a/src/utils/TestUtils.ts +++ b/src/utils/TestUtils.ts @@ -1,4 +1,5 @@ import { ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, IListenerType } from '../Interfaces'; +import { LineData } from '../Types'; export class MockTerminal implements ITerminal { options: ITerminalOptions = {}; @@ -47,8 +48,8 @@ export class MockTerminal implements ITerminal { showCursor(): void { throw new Error('Method not implemented.'); } - blankLine(cur?: boolean, isWrapped?: boolean): [number, string, number][] { - const line: [number, string, number][] = []; + blankLine(cur?: boolean, isWrapped?: boolean): LineData { + const line: LineData = []; for (let i = 0; i < this.cols; i++) { line.push([0, ' ', 1]); } From e0809d526ef9056b4f72f565b98b719834e3fc1a Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 6 Aug 2017 00:57:54 -0700 Subject: [PATCH 19/22] Type InputHandler tests --- src/Buffer.test.ts | 1 + src/BufferSet.test.ts | 1 + src/CompositionHelper.test.ts | 4 + src/EventEmitter.test.ts | 4 + src/InputHandler.test.ts | 60 ++++++------ src/InputHandler.ts | 1 - src/Linkifier.test.ts | 1 + src/SelectionManager.test.ts | 1 + src/SelectionModel.test.ts | 1 + src/Viewport.test.ts | 4 + src/utils/CharMeasure.test.ts | 1 + src/utils/CircularList.test.ts | 4 + src/utils/CircularList.ts | 1 + src/utils/DomElementObjectPool.test.ts | 4 + src/utils/TestUtils.ts | 129 ++++++++++++++++++++++++- src/xterm.ts | 4 + 16 files changed, 189 insertions(+), 32 deletions(-) diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index 1e5e2c3b7c..99bd6d0fe5 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -1,6 +1,7 @@ /** * @license MIT */ + import { assert } from 'chai'; import { ITerminal } from './Interfaces'; import { Buffer } from './Buffer'; diff --git a/src/BufferSet.test.ts b/src/BufferSet.test.ts index 78a1935118..97c079950f 100644 --- a/src/BufferSet.test.ts +++ b/src/BufferSet.test.ts @@ -1,6 +1,7 @@ /** * @license MIT */ + import { assert } from 'chai'; import { ITerminal } from './Interfaces'; import { BufferSet } from './BufferSet'; diff --git a/src/CompositionHelper.test.ts b/src/CompositionHelper.test.ts index 70b231834b..0adb1719a6 100644 --- a/src/CompositionHelper.test.ts +++ b/src/CompositionHelper.test.ts @@ -1,3 +1,7 @@ +/** + * @license MIT + */ + import { assert } from 'chai'; import { CompositionHelper } from './CompositionHelper'; diff --git a/src/EventEmitter.test.ts b/src/EventEmitter.test.ts index 7ae381c8ab..d61f994ab5 100644 --- a/src/EventEmitter.test.ts +++ b/src/EventEmitter.test.ts @@ -1,3 +1,7 @@ +/** + * @license MIT + */ + import { assert } from 'chai'; import { EventEmitter } from './EventEmitter'; diff --git a/src/InputHandler.test.ts b/src/InputHandler.test.ts index b6b218dfdb..043b79192b 100644 --- a/src/InputHandler.test.ts +++ b/src/InputHandler.test.ts @@ -1,12 +1,18 @@ +/** + * @license MIT + */ + import { assert } from 'chai'; import { InputHandler } from './InputHandler'; import { wcwidth } from './InputHandler'; +import { MockInputHandlingTerminal } from './utils/TestUtils'; describe('InputHandler', () => { describe('save and restore cursor', () => { - let terminal = { buffer: { x: 1, y: 2 } }; - // TODO: Create proper mock IInputHandlingTerminal test util object - let inputHandler = new InputHandler(terminal); + let terminal = new MockInputHandlingTerminal(); + terminal.buffer.x = 1; + terminal.buffer.y = 2; + let inputHandler = new InputHandler(terminal); // Save cursor position inputHandler.saveCursor([]); assert.equal(terminal.buffer.x, 1); @@ -21,46 +27,42 @@ describe('InputHandler', () => { }); describe('setCursorStyle', () => { it('should call Terminal.setOption with correct params', () => { - let options = {}; - let terminal = { - setOption: (option, value) => options[option] = value - }; - let inputHandler = new InputHandler(terminal); + let terminal = new MockInputHandlingTerminal(); + let inputHandler = new InputHandler(terminal); inputHandler.setCursorStyle([0]); - assert.equal(options['cursorStyle'], 'block'); - assert.equal(options['cursorBlink'], true); + assert.equal(terminal.options['cursorStyle'], 'block'); + assert.equal(terminal.options['cursorBlink'], true); - options = {}; + terminal.options = {}; inputHandler.setCursorStyle([1]); - assert.equal(options['cursorStyle'], 'block'); - assert.equal(options['cursorBlink'], true); + assert.equal(terminal.options['cursorStyle'], 'block'); + assert.equal(terminal.options['cursorBlink'], true); - options = {}; + terminal.options = {}; inputHandler.setCursorStyle([2]); - assert.equal(options['cursorStyle'], 'block'); - assert.equal(options['cursorBlink'], false); + assert.equal(terminal.options['cursorStyle'], 'block'); + assert.equal(terminal.options['cursorBlink'], false); - options = {}; + terminal.options = {}; inputHandler.setCursorStyle([3]); - assert.equal(options['cursorStyle'], 'underline'); - assert.equal(options['cursorBlink'], true); + assert.equal(terminal.options['cursorStyle'], 'underline'); + assert.equal(terminal.options['cursorBlink'], true); - options = {}; + terminal.options = {}; inputHandler.setCursorStyle([4]); - assert.equal(options['cursorStyle'], 'underline'); - assert.equal(options['cursorBlink'], false); + assert.equal(terminal.options['cursorStyle'], 'underline'); + assert.equal(terminal.options['cursorBlink'], false); - options = {}; + terminal.options = {}; inputHandler.setCursorStyle([5]); - assert.equal(options['cursorStyle'], 'bar'); - assert.equal(options['cursorBlink'], true); + assert.equal(terminal.options['cursorStyle'], 'bar'); + assert.equal(terminal.options['cursorBlink'], true); - options = {}; + terminal.options = {}; inputHandler.setCursorStyle([6]); - assert.equal(options['cursorStyle'], 'bar'); - assert.equal(options['cursorBlink'], false); - + assert.equal(terminal.options['cursorStyle'], 'bar'); + assert.equal(terminal.options['cursorBlink'], false); }); }); }); diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 91160134a7..5362cae9af 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -15,7 +15,6 @@ import { CharData } from './Types'; * each function's header comment. */ export class InputHandler implements IInputHandler { - // TODO: We want to type _terminal when it's pulled into TS constructor(private _terminal: IInputHandlingTerminal) { } public addChar(char: string, code: number): void { diff --git a/src/Linkifier.test.ts b/src/Linkifier.test.ts index 414238f9c9..a4ed70db26 100644 --- a/src/Linkifier.test.ts +++ b/src/Linkifier.test.ts @@ -1,6 +1,7 @@ /** * @license MIT */ + import jsdom = require('jsdom'); import { assert } from 'chai'; import { ITerminal, ILinkifier } from './Interfaces'; diff --git a/src/SelectionManager.test.ts b/src/SelectionManager.test.ts index a39c4dda07..698cda7eca 100644 --- a/src/SelectionManager.test.ts +++ b/src/SelectionManager.test.ts @@ -1,6 +1,7 @@ /** * @license MIT */ + import jsdom = require('jsdom'); import { assert } from 'chai'; import { ITerminal, ICircularList } from './Interfaces'; diff --git a/src/SelectionModel.test.ts b/src/SelectionModel.test.ts index 696d12f001..9da84478bb 100644 --- a/src/SelectionModel.test.ts +++ b/src/SelectionModel.test.ts @@ -1,6 +1,7 @@ /** * @license MIT */ + import { assert } from 'chai'; import { ITerminal } from './Interfaces'; import { SelectionModel } from './SelectionModel'; diff --git a/src/Viewport.test.ts b/src/Viewport.test.ts index 7c89da29f1..8c7a914bf9 100644 --- a/src/Viewport.test.ts +++ b/src/Viewport.test.ts @@ -1,3 +1,7 @@ +/** + * @license MIT + */ + import { assert } from 'chai'; import { Viewport } from './Viewport'; import {BufferSet} from './BufferSet'; diff --git a/src/utils/CharMeasure.test.ts b/src/utils/CharMeasure.test.ts index fd4954e006..68e1b6a787 100644 --- a/src/utils/CharMeasure.test.ts +++ b/src/utils/CharMeasure.test.ts @@ -1,6 +1,7 @@ /** * @license MIT */ + import jsdom = require('jsdom'); import { assert } from 'chai'; import { ICharMeasure, ITerminal } from '../Interfaces'; diff --git a/src/utils/CircularList.test.ts b/src/utils/CircularList.test.ts index aaa4a0d92c..e27e7f7a69 100644 --- a/src/utils/CircularList.test.ts +++ b/src/utils/CircularList.test.ts @@ -1,3 +1,7 @@ +/** + * @license MIT + */ + import { assert } from 'chai'; import { CircularList } from './CircularList'; diff --git a/src/utils/CircularList.ts b/src/utils/CircularList.ts index 54850ab7c3..373d78b053 100644 --- a/src/utils/CircularList.ts +++ b/src/utils/CircularList.ts @@ -4,6 +4,7 @@ * @module xterm/utils/CircularList * @license MIT */ + import { EventEmitter } from '../EventEmitter'; import { ICircularList } from '../Interfaces'; diff --git a/src/utils/DomElementObjectPool.test.ts b/src/utils/DomElementObjectPool.test.ts index 7298f1922f..5a5d8a7841 100644 --- a/src/utils/DomElementObjectPool.test.ts +++ b/src/utils/DomElementObjectPool.test.ts @@ -1,3 +1,7 @@ +/** + * @license MIT + */ + import { assert } from 'chai'; import { DomElementObjectPool } from './DomElementObjectPool'; diff --git a/src/utils/TestUtils.ts b/src/utils/TestUtils.ts index 51caa8fe92..fcff2dcd7f 100644 --- a/src/utils/TestUtils.ts +++ b/src/utils/TestUtils.ts @@ -1,4 +1,8 @@ -import { ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, IListenerType } from '../Interfaces'; +/** + * @license MIT + */ + +import { ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, IListenerType, IInputHandlingTerminal, IViewport, ICircularList } from '../Interfaces'; import { LineData } from '../Types'; export class MockTerminal implements ITerminal { @@ -20,7 +24,6 @@ export class MockTerminal implements ITerminal { scrollback: number; buffers: IBufferSet; buffer: IBuffer; - handler(data: string): void { throw new Error('Method not implemented.'); } @@ -56,3 +59,125 @@ export class MockTerminal implements ITerminal { return line; } } + +export class MockInputHandlingTerminal implements IInputHandlingTerminal { + element: HTMLElement; + options: ITerminalOptions = {}; + cols: number; + rows: number; + charset: { [key: string]: string; }; + gcharset: number; + glevel: number; + charsets: { [key: string]: string; }[]; + applicationKeypad: boolean; + applicationCursor: boolean; + originMode: boolean; + insertMode: boolean; + wraparoundMode: boolean; + defAttr: number; + curAttr: number; + prefix: string; + savedCols: number; + x10Mouse: boolean; + vt200Mouse: boolean; + normalMouse: boolean; + mouseEvents: boolean; + sendFocus: boolean; + utfMouse: boolean; + sgrMouse: boolean; + urxvtMouse: boolean; + cursorHidden: boolean; + buffers: IBufferSet; + buffer: IBuffer = new MockBuffer(); + viewport: IViewport; + selectionManager: ISelectionManager; + focus(): void { + throw new Error('Method not implemented.'); + } + convertEol: boolean; + updateRange(y: number): void { + throw new Error('Method not implemented.'); + } + scroll(isWrapped?: boolean): void { + throw new Error('Method not implemented.'); + } + nextStop(x?: number): number { + throw new Error('Method not implemented.'); + } + setgLevel(g: number): void { + throw new Error('Method not implemented.'); + } + eraseAttr() { + throw new Error('Method not implemented.'); + } + eraseRight(x: number, y: number): void { + throw new Error('Method not implemented.'); + } + eraseLine(y: number): void { + throw new Error('Method not implemented.'); + } + eraseLeft(x: number, y: number): void { + throw new Error('Method not implemented.'); + } + blankLine(cur?: boolean, isWrapped?: boolean): [number, string, number][] { + throw new Error('Method not implemented.'); + } + prevStop(x?: number): number { + throw new Error('Method not implemented.'); + } + is(term: string): boolean { + throw new Error('Method not implemented.'); + } + send(data: string): void { + throw new Error('Method not implemented.'); + } + setgCharset(g: number, charset: { [key: string]: string; }): void { + throw new Error('Method not implemented.'); + } + resize(x: number, y: number): void { + throw new Error('Method not implemented.'); + } + log(text: string, data?: any): void { + throw new Error('Method not implemented.'); + } + reset(): void { + throw new Error('Method not implemented.'); + } + showCursor(): void { + throw new Error('Method not implemented.'); + } + refresh(start: number, end: number): void { + throw new Error('Method not implemented.'); + } + matchColor(r1: number, g1: number, b1: number) { + throw new Error('Method not implemented.'); + } + error(text: string, data?: any): void { + throw new Error('Method not implemented.'); + } + setOption(key: string, value: any): void { + this.options[key] = value; + } + on(type: string, listener: IListenerType): void { + throw new Error('Method not implemented.'); + } + off(type: string, listener: IListenerType): void { + throw new Error('Method not implemented.'); + } + emit(type: string, data?: any): void { + throw new Error('Method not implemented.'); + } +} + +export class MockBuffer implements IBuffer { + lines: ICircularList<[number, string, number][]>; + ydisp: number; + ybase: number; + y: number; + x: number; + tabs: any; + scrollBottom: number; + scrollTop: number; + savedY: number; + savedX: number; +} diff --git a/src/xterm.ts b/src/xterm.ts index 0c2b7319fd..7575186538 100644 --- a/src/xterm.ts +++ b/src/xterm.ts @@ -1,3 +1,7 @@ +/** + * @license MIT + */ + import { Terminal } from './Terminal'; module.exports = Terminal; From 7cd8d1ec7472a76521a9af1603f44f7cd6d7d2ab Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 6 Aug 2017 01:42:41 -0700 Subject: [PATCH 20/22] Resolve todos --- src/Interfaces.ts | 4 +- src/Terminal.ts | 33 +- src/utils/TestUtils.ts | 4 +- src/xterm.js | 2389 ---------------------------------------- 4 files changed, 19 insertions(+), 2411 deletions(-) delete mode 100644 src/xterm.js diff --git a/src/Interfaces.ts b/src/Interfaces.ts index 63058f091c..55317fbc83 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -93,7 +93,7 @@ export interface IInputHandlingTerminal extends IEventEmitter { scroll(isWrapped?: boolean): void; nextStop(x?: number): number; setgLevel(g: number): void; - eraseAttr(): any; + eraseAttr(): number; eraseRight(x: number, y: number): void; eraseLine(y: number): void; eraseLeft(x: number, y: number): void; @@ -107,7 +107,7 @@ export interface IInputHandlingTerminal extends IEventEmitter { reset(): void; showCursor(): void; refresh(start: number, end: number): void; - matchColor(r1: number, g1: number, b1: number): any; + matchColor(r1: number, g1: number, b1: number): number; error(text: string, data?: any): void; setOption(key: string, value: any): void; } diff --git a/src/Terminal.ts b/src/Terminal.ts index 360c2b7e36..c1fd1e7ed4 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -182,11 +182,8 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT private compositionView: HTMLElement; private charSizeStyleElement: HTMLStyleElement; - // TODO: This should be removed from the interface, opting for modules to pull - // in Browser for themselves. public browser: IBrowser = Browser; - // TODO: Options should be private, remove from interface in favor of getOption public options: ITerminalOptions; private colors: any; @@ -194,8 +191,8 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT public cursorState: number; public cursorHidden: boolean; public convertEol: boolean; - // TODO: This is the data queue for send, improve name and documentation - private queue: string; + + private sendDataQueue: string; private customKeyEventHandler: CustomKeyEventHandler; // The ID from a setInterval that tracks the blink animation. This animation // is done in JS due to a Chromium bug with CSS animations that thrashed the @@ -339,7 +336,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.cursorState = 0; this.cursorHidden = false; - this.queue = ''; + this.sendDataQueue = ''; this.customKeyEventHandler = null; this.cursorBlinkInterval = null; @@ -402,7 +399,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT /** * back_color_erase feature for xterm. */ - public eraseAttr(): any { + public eraseAttr(): number { // if (this.is('screen')) return this.defAttr; return (this.defAttr & ~0x1ff) | (this.curAttr & 0x1ff); } @@ -1886,14 +1883,14 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT * @param {string} data */ public send(data: string): void { - if (!this.queue) { + if (!this.sendDataQueue) { setTimeout(() => { - this.handler(this.queue); - this.queue = ''; + this.handler(this.sendDataQueue); + this.sendDataQueue = ''; }, 1); } - this.queue += data; + this.sendDataQueue += data; } /** @@ -2261,7 +2258,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT // Expose to InputHandler // TODO: Revise when truecolor is introduced. - public matchColor(r1: number, g1: number, b1: number): any { + public matchColor(r1: number, g1: number, b1: number): number { const hash = (r1 << 16) | (g1 << 8) | b1; if (matchColorCache[hash] != null) { @@ -2271,11 +2268,11 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT let ldiff = Infinity; let li = -1; let i = 0; - let c; - let r2; - let g2; - let b2; - let diff; + let c: number[]; + let r2: number; + let g2: number; + let b2: number; + let diff: number; for (; i < vcolors.length; i++) { c = vcolors[i]; @@ -2341,7 +2338,7 @@ function isThirdLevelShift(ev: KeyboardEvent): boolean { return thirdLevelKey && (!ev.keyCode || ev.keyCode > 47); } -const matchColorCache = {}; +const matchColorCache: {[colorRGBHash: number]: number} = {}; // http://stackoverflow.com/questions/1633828 const matchColorDistance = function(r1: number, g1: number, b1: number, r2: number, g2: number, b2: number): number { diff --git a/src/utils/TestUtils.ts b/src/utils/TestUtils.ts index fcff2dcd7f..159da055ed 100644 --- a/src/utils/TestUtils.ts +++ b/src/utils/TestUtils.ts @@ -107,7 +107,7 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal { setgLevel(g: number): void { throw new Error('Method not implemented.'); } - eraseAttr() { + eraseAttr(): number { throw new Error('Method not implemented.'); } eraseRight(x: number, y: number): void { @@ -149,7 +149,7 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal { refresh(start: number, end: number): void { throw new Error('Method not implemented.'); } - matchColor(r1: number, g1: number, b1: number) { + matchColor(r1: number, g1: number, b1: number): number { throw new Error('Method not implemented.'); } error(text: string, data?: any): void { diff --git a/src/xterm.js b/src/xterm.js deleted file mode 100644 index e9e3465e6b..0000000000 --- a/src/xterm.js +++ /dev/null @@ -1,2389 +0,0 @@ -/** - * xterm.js: xterm, in the browser - * Originally forked from (with the author's permission): - * Fabrice Bellard's javascript vt100 for jslinux: - * http://bellard.org/jslinux/ - * Copyright (c) 2011 Fabrice Bellard - * The original design remains. The terminal itself - * has been extended to include xterm CSI codes, among - * other features. - * @license MIT - */ - -import { BufferSet } from './BufferSet'; -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'; -import { Renderer } from './Renderer'; -import { Linkifier } from './Linkifier'; -import { SelectionManager } from './SelectionManager'; -import { CharMeasure } from './utils/CharMeasure'; -import * as Browser from './utils/Browser'; -import * as Mouse from './utils/Mouse'; -import { CHARSETS } from './Charsets'; -import { getRawByteCoords } from './utils/Mouse'; -import { translateBufferLineToString } from './utils/BufferLine'; - -/** - * Terminal Emulation References: - * http://vt100.net/ - * http://invisible-island.net/xterm/ctlseqs/ctlseqs.txt - * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html - * http://invisible-island.net/vttest/ - * http://www.inwap.com/pdp10/ansicode.txt - * http://linux.die.net/man/4/console_codes - * http://linux.die.net/man/7/urxvt - */ - -// Let it work inside Node.js for automated testing purposes. -var document = (typeof window != 'undefined') ? window.document : null; - -/** - * The amount of write requests to queue before sending an XOFF signal to the - * pty process. This number must be small in order for ^C and similar sequences - * to be responsive. - */ -var WRITE_BUFFER_PAUSE_THRESHOLD = 5; - -/** - * The number of writes to perform in a single batch before allowing the - * renderer to catch up with a 0ms setTimeout. - */ -var WRITE_BATCH_SIZE = 300; - -/** - * The time between cursor blinks. This is driven by JS rather than a CSS - * animation due to a bug in Chromium that causes it to use excessive CPU time. - * See https://github.com/Microsoft/vscode/issues/22900 - */ -var CURSOR_BLINK_INTERVAL = 600; - -/** - * Terminal - */ - -/** - * Creates a new `Terminal` object. - * - * @param {object} options An object containing a set of options, the available options are: - * - `cursorBlink` (boolean): Whether the terminal cursor blinks - * - `cols` (number): The number of columns of the terminal (horizontal size) - * - `rows` (number): The number of rows of the terminal (vertical size) - * - * @public - * @class Xterm Xterm - * @alias module:xterm/src/xterm - */ -function Terminal(options) { - var self = this; - - if (!(this instanceof Terminal)) { - return new Terminal(arguments[0], arguments[1], arguments[2]); - } - - self.browser = Browser; - self.cancel = Terminal.cancel; - - EventEmitter.call(this); - - if (typeof options === 'number') { - options = { - cols: arguments[0], - rows: arguments[1], - handler: arguments[2] - }; - } - - options = options || {}; - - - Object.keys(Terminal.defaults).forEach(function(key) { - if (options[key] == null) { - options[key] = Terminal.options[key]; - - if (Terminal[key] !== Terminal.defaults[key]) { - options[key] = Terminal[key]; - } - } - self[key] = options[key]; - }); - - if (options.colors.length === 8) { - options.colors = options.colors.concat(Terminal._colors.slice(8)); - } else if (options.colors.length === 16) { - options.colors = options.colors.concat(Terminal._colors.slice(16)); - } else if (options.colors.length === 10) { - options.colors = options.colors.slice(0, -2).concat( - Terminal._colors.slice(8, -2), options.colors.slice(-2)); - } else if (options.colors.length === 18) { - options.colors = options.colors.concat( - Terminal._colors.slice(16, -2), options.colors.slice(-2)); - } - this.colors = options.colors; - - this.options = options; - - // this.context = options.context || window; - // this.document = options.document || document; - this.parent = options.body || options.parent || ( - document ? document.getElementsByTagName('body')[0] : null - ); - - this.cols = options.cols || options.geometry[0]; - this.rows = options.rows || options.geometry[1]; - this.geometry = [this.cols, this.rows]; - - if (options.handler) { - this.on('data', options.handler); - } - - this.cursorState = 0; - this.cursorHidden = false; - this.convertEol; - this.queue = ''; - this.customKeyEventHandler = null; - this.cursorBlinkInterval = null; - - // modes - this.applicationKeypad = false; - this.applicationCursor = false; - this.originMode = false; - this.insertMode = false; - this.wraparoundMode = true; // defaults: xterm - true, vt100 - false - - // charset - this.charset = null; - this.gcharset = null; - this.glevel = 0; - this.charsets = [null]; - - // mouse properties - this.decLocator; - this.x10Mouse; - this.vt200Mouse; - this.vt300Mouse; - this.normalMouse; - this.mouseEvents; - this.sendFocus; - this.utfMouse; - this.sgrMouse; - this.urxvtMouse; - - // misc - this.element; - this.children; - this.refreshStart; - this.refreshEnd; - this.savedX; - this.savedY; - this.savedCols; - - // stream - this.readable = true; - this.writable = true; - - this.defAttr = (0 << 18) | (257 << 9) | (256 << 0); - this.curAttr = this.defAttr; - - this.params = []; - this.currentParam = 0; - this.prefix = ''; - this.postfix = ''; - - this.inputHandler = new InputHandler(this); - this.parser = new Parser(this.inputHandler, this); - // Reuse renderer if the Terminal is being recreated via a Terminal.reset call. - this.renderer = this.renderer || null; - this.selectionManager = this.selectionManager || null; - this.linkifier = this.linkifier || new Linkifier(); - - // user input states - this.writeBuffer = []; - this.writeInProgress = false; - - /** - * Whether _xterm.js_ sent XOFF in order to catch up with the pty process. - * This is a distinct state from writeStopped so that if the user requested - * XOFF via ^S that it will not automatically resume when the writeBuffer goes - * below threshold. - */ - this.xoffSentToCatchUp = false; - - /** Whether writing has been stopped as a result of XOFF */ - this.writeStopped = false; - - // leftover surrogate high from previous write invocation - this.surrogate_high = ''; - - // Create the terminal's buffers and set the current buffer - this.buffers = new BufferSet(this); - this.buffer = this.buffers.active; // Convenience shortcut; - this.buffers.on('activate', function (buffer) { - this._terminal.buffer = buffer; - }); - - // Ensure the selection manager has the correct buffer - if (this.selectionManager) { - this.selectionManager.setBuffer(this.buffer.lines); - } - - this.setupStops(); - - // Store if user went browsing history in scrollback - this.userScrolling = false; -} - -inherits(Terminal, EventEmitter); - -/** - * back_color_erase feature for xterm. - */ -Terminal.prototype.eraseAttr = function() { - // if (this.is('screen')) return this.defAttr; - return (this.defAttr & ~0x1ff) | (this.curAttr & 0x1ff); -}; - -/** - * Colors - */ - -// Colors 0-15 -Terminal.tangoColors = [ - // dark: - '#2e3436', - '#cc0000', - '#4e9a06', - '#c4a000', - '#3465a4', - '#75507b', - '#06989a', - '#d3d7cf', - // bright: - '#555753', - '#ef2929', - '#8ae234', - '#fce94f', - '#729fcf', - '#ad7fa8', - '#34e2e2', - '#eeeeec' -]; - -// Colors 0-15 + 16-255 -// Much thanks to TooTallNate for writing this. -Terminal.colors = (function() { - var colors = Terminal.tangoColors.slice() - , r = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff] - , i; - - // 16-231 - i = 0; - for (; i < 216; i++) { - out(r[(i / 36) % 6 | 0], r[(i / 6) % 6 | 0], r[i % 6]); - } - - // 232-255 (grey) - i = 0; - for (; i < 24; i++) { - r = 8 + i * 10; - out(r, r, r); - } - - function out(r, g, b) { - colors.push('#' + hex(r) + hex(g) + hex(b)); - } - - function hex(c) { - c = c.toString(16); - return c.length < 2 ? '0' + c : c; - } - - return colors; -})(); - -Terminal._colors = Terminal.colors.slice(); - -Terminal.vcolors = (function() { - var out = [] - , colors = Terminal.colors - , i = 0 - , color; - - for (; i < 256; i++) { - color = parseInt(colors[i].substring(1), 16); - out.push([ - (color >> 16) & 0xff, - (color >> 8) & 0xff, - color & 0xff - ]); - } - - return out; -})(); - -/** - * Options - */ - -Terminal.defaults = { - colors: Terminal.colors, - theme: 'default', - convertEol: false, - termName: 'xterm', - geometry: [80, 24], - cursorBlink: false, - cursorStyle: 'block', - visualBell: false, - popOnBell: false, - scrollback: 1000, - screenKeys: false, - debug: false, - cancelEvents: false, - disableStdin: false, - useFlowControl: false, - tabStopWidth: 8 - // programFeatures: false, - // focusKeys: false, -}; - -Terminal.options = {}; - -Terminal.focus = null; - -each(keys(Terminal.defaults), function(key) { - Terminal[key] = Terminal.defaults[key]; - Terminal.options[key] = Terminal.defaults[key]; -}); - -/** - * Focus the terminal. Delegates focus handling to the terminal's DOM element. - */ -Terminal.prototype.focus = function() { - return this.textarea.focus(); -}; - -/** - * Retrieves an option's value from the terminal. - * @param {string} key The option key. - */ -Terminal.prototype.getOption = function(key) { - if (!(key in Terminal.defaults)) { - throw new Error('No option with key "' + key + '"'); - } - - if (typeof this.options[key] !== 'undefined') { - return this.options[key]; - } - - return this[key]; -}; - -/** - * Sets an option on the terminal. - * @param {string} key The option key. - * @param {string} value The option value. - */ -Terminal.prototype.setOption = function(key, value) { - if (!(key in Terminal.defaults)) { - throw new Error('No option with key "' + key + '"'); - } - switch (key) { - case 'scrollback': - if (value < this.rows) { - let msg = 'Setting the scrollback value less than the number of rows '; - - msg += `(${this.rows}) is not allowed.`; - - console.warn(msg); - return false; - } - - if (this.options[key] !== value) { - if (this.buffer.lines.length > value) { - const amountToTrim = this.buffer.lines.length - value; - const needsRefresh = (this.buffer.ydisp - amountToTrim < 0); - this.buffer.lines.trimStart(amountToTrim); - this.buffer.ybase = Math.max(this.buffer.ybase - amountToTrim, 0); - this.buffer.ydisp = Math.max(this.buffer.ydisp - amountToTrim, 0); - if (needsRefresh) { - this.refresh(0, this.rows - 1); - } - } - this.buffer.lines.maxLength = value; - this.viewport.syncScrollArea(); - } - break; - } - this[key] = value; - this.options[key] = value; - switch (key) { - case 'cursorBlink': this.setCursorBlinking(value); break; - case 'cursorStyle': - this.element.classList.toggle(`xterm-cursor-style-block`, value === 'block'); - this.element.classList.toggle(`xterm-cursor-style-underline`, value === 'underline'); - this.element.classList.toggle(`xterm-cursor-style-bar`, value === 'bar'); - break; - case 'tabStopWidth': this.setupStops(); break; - } -}; - -Terminal.prototype.restartCursorBlinking = function () { - this.setCursorBlinking(this.options.cursorBlink); -}; - -Terminal.prototype.setCursorBlinking = function (enabled) { - this.element.classList.toggle('xterm-cursor-blink', enabled); - this.clearCursorBlinkingInterval(); - if (enabled) { - var self = this; - this.cursorBlinkInterval = setInterval(function () { - self.element.classList.toggle('xterm-cursor-blink-on'); - }, CURSOR_BLINK_INTERVAL); - } -}; - -Terminal.prototype.clearCursorBlinkingInterval = function () { - this.element.classList.remove('xterm-cursor-blink-on'); - if (this.cursorBlinkInterval) { - clearInterval(this.cursorBlinkInterval); - this.cursorBlinkInterval = null; - } -}; - -/** - * Binds the desired focus behavior on a given terminal object. - * - * @static - */ -Terminal.bindFocus = function (term) { - on(term.textarea, 'focus', function (ev) { - if (term.sendFocus) { - term.send(C0.ESC + '[I'); - } - term.element.classList.add('focus'); - term.showCursor(); - term.restartCursorBlinking.apply(term); - Terminal.focus = term; - term.emit('focus', {terminal: term}); - }); -}; - -/** - * Blur the terminal. Delegates blur handling to the terminal's DOM element. - */ -Terminal.prototype.blur = function() { - return this.textarea.blur(); -}; - -/** - * Binds the desired blur behavior on a given terminal object. - * - * @static - */ -Terminal.bindBlur = function (term) { - on(term.textarea, 'blur', function (ev) { - term.refresh(term.buffer.y, term.buffer.y); - if (term.sendFocus) { - term.send(C0.ESC + '[O'); - } - term.element.classList.remove('focus'); - term.clearCursorBlinkingInterval.apply(term); - Terminal.focus = null; - term.emit('blur', {terminal: term}); - }); -}; - -/** - * Initialize default behavior - */ -Terminal.prototype.initGlobal = function() { - var term = this; - - Terminal.bindKeys(this); - Terminal.bindFocus(this); - Terminal.bindBlur(this); - - // Bind clipboard functionality - on(this.element, 'copy', event => { - // If mouse events are active it means the selection manager is disabled and - // copy should be handled by the host program. - if (!term.hasSelection()) { - return; - } - copyHandler(event, term, this.selectionManager); - }); - const pasteHandlerWrapper = event => pasteHandler(event, term); - on(this.textarea, 'paste', pasteHandlerWrapper); - on(this.element, 'paste', pasteHandlerWrapper); - - // Handle right click context menus - if (term.browser.isFirefox) { - // Firefox doesn't appear to fire the contextmenu event on right click - on(this.element, 'mousedown', event => { - if (event.button == 2) { - rightClickHandler(event, this.textarea, this.selectionManager); - } - }); - } else { - on(this.element, 'contextmenu', event => { - rightClickHandler(event, this.textarea, this.selectionManager); - }); - } - - // Move the textarea under the cursor when middle clicking on Linux to ensure - // middle click to paste selection works. This only appears to work in Chrome - // at the time is writing. - if (term.browser.isLinux) { - // Use auxclick event over mousedown the latter doesn't seem to work. Note - // that the regular click event doesn't fire for the middle mouse button. - on(this.element, 'auxclick', event => { - if (event.button === 1) { - moveTextAreaUnderMouseCursor(event, this.textarea, this.selectionManager); - } - }); - } -}; - -/** - * Apply key handling to the terminal - */ -Terminal.bindKeys = function(term) { - on(term.element, 'keydown', function(ev) { - if (document.activeElement != this) { - return; - } - term.keyDown(ev); - }, true); - - on(term.element, 'keypress', function(ev) { - if (document.activeElement != this) { - return; - } - term.keyPress(ev); - }, true); - - on(term.element, 'keyup', function(ev) { - if (!wasMondifierKeyOnlyEvent(ev)) { - term.focus(term); - } - }, true); - - on(term.textarea, 'keydown', function(ev) { - term.keyDown(ev); - }, true); - - on(term.textarea, 'keypress', function(ev) { - term.keyPress(ev); - // Truncate the textarea's value, since it is not needed - this.value = ''; - }, true); - - on(term.textarea, 'compositionstart', term.compositionHelper.compositionstart.bind(term.compositionHelper)); - on(term.textarea, 'compositionupdate', term.compositionHelper.compositionupdate.bind(term.compositionHelper)); - on(term.textarea, 'compositionend', term.compositionHelper.compositionend.bind(term.compositionHelper)); - term.on('refresh', term.compositionHelper.updateCompositionElements.bind(term.compositionHelper)); - term.on('refresh', function (data) { - term.queueLinkification(data.start, data.end) - }); -}; - - -/** - * Insert the given row to the terminal or produce a new one - * if no row argument is passed. Return the inserted row. - * @param {HTMLElement} row (optional) The row to append to the terminal. - */ -Terminal.prototype.insertRow = function (row) { - if (typeof row != 'object') { - row = document.createElement('div'); - } - - this.rowContainer.appendChild(row); - this.children.push(row); - - return row; -}; - -/** - * Opens the terminal within an element. - * - * @param {HTMLElement} parent The element to create the terminal within. - * @param {boolean} focus Focus the terminal, after it gets instantiated in the DOM - */ -Terminal.prototype.open = function(parent, focus) { - var self=this, i=0, div; - - this.parent = parent || 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.getElementsByTagName('body')[0]; - - //Create main element container - this.element = this.document.createElement('div'); - this.element.classList.add('terminal'); - this.element.classList.add('xterm'); - this.element.classList.add('xterm-theme-' + this.theme); - this.element.classList.add(`xterm-cursor-style-${this.options.cursorStyle}`); - this.setCursorBlinking(this.options.cursorBlink); - - this.element.setAttribute('tabindex', 0); - - this.viewportElement = document.createElement('div'); - this.viewportElement.classList.add('xterm-viewport'); - this.element.appendChild(this.viewportElement); - this.viewportScrollArea = document.createElement('div'); - this.viewportScrollArea.classList.add('xterm-scroll-area'); - this.viewportElement.appendChild(this.viewportScrollArea); - - // Create the selection container. - this.selectionContainer = document.createElement('div'); - this.selectionContainer.classList.add('xterm-selection'); - this.element.appendChild(this.selectionContainer); - - // Create the container that will hold the lines of the terminal and then - // produce the lines the lines. - this.rowContainer = document.createElement('div'); - this.rowContainer.classList.add('xterm-rows'); - this.element.appendChild(this.rowContainer); - this.children = []; - this.linkifier.attachToDom(document, this.children); - - // 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'); - // TODO: This should probably be inserted once it's filled to prevent an additional layout - this.element.appendChild(this.helperContainer); - this.textarea = document.createElement('textarea'); - this.textarea.classList.add('xterm-helper-textarea'); - this.textarea.setAttribute('autocorrect', 'off'); - this.textarea.setAttribute('autocapitalize', 'off'); - this.textarea.setAttribute('spellcheck', 'false'); - this.textarea.tabIndex = 0; - this.textarea.addEventListener('focus', function() { - self.emit('focus', {terminal: self}); - }); - this.textarea.addEventListener('blur', function() { - self.emit('blur', {terminal: self}); - }); - 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.charSizeStyleElement = document.createElement('style'); - this.helperContainer.appendChild(this.charSizeStyleElement); - - for (; i < this.rows; i++) { - this.insertRow(); - } - this.parent.appendChild(this.element); - - this.charMeasure = new CharMeasure(document, this.helperContainer); - this.charMeasure.on('charsizechanged', function () { - self.updateCharSizeStyles(); - }); - this.charMeasure.measure(); - - this.viewport = new Viewport(this, this.viewportElement, this.viewportScrollArea, this.charMeasure); - this.renderer = new Renderer(this); - this.selectionManager = new SelectionManager( - this, this.buffer.lines, this.rowContainer, this.charMeasure - ); - this.selectionManager.on('refresh', data => { - this.renderer.refreshSelection(data.start, data.end); - }); - this.selectionManager.on('newselection', text => { - // If there's a new selection, put it into the textarea, focus and select it - // in order to register it as a selection on the OS. This event is fired - // only on Linux to enable middle click to paste selection. - this.textarea.value = text; - this.textarea.focus(); - this.textarea.select(); - }); - this.on('scroll', () => this.selectionManager.refresh()); - this.viewportElement.addEventListener('scroll', () => this.selectionManager.refresh()); - - // Setup loop that draws to screen - this.refresh(0, this.rows - 1); - - // Initialize global actions that - // need to be taken on the document. - this.initGlobal(); - - /** - * Automatic focus functionality. - * TODO: Default to `false` starting with xterm.js 3.0. - */ - if (typeof focus == 'undefined') { - let message = 'You did not pass the `focus` argument in `Terminal.prototype.open()`.\n'; - - message += 'The `focus` argument now defaults to `true` but starting with xterm.js 3.0 '; - message += 'it will default to `false`.'; - - console.warn(message); - focus = true; - } - - if (focus) { - this.focus(); - } - - // Listen for mouse events and translate - // them into terminal mouse protocols. - this.bindMouse(); - - /** - * This event is emitted when terminal has completed opening. - * - * @event open - */ - this.emit('open'); -}; - - -/** - * Attempts to load an add-on using CommonJS or RequireJS (whichever is available). - * @param {string} addon The name of the addon to load - * @static - */ -Terminal.loadAddon = function(addon, callback) { - if (typeof exports === 'object' && typeof module === 'object') { - // CommonJS - return require('./addons/' + addon + '/' + addon); - } else if (typeof define == 'function') { - // RequireJS - return require(['./addons/' + addon + '/' + addon], callback); - } else { - console.error('Cannot load a module without a CommonJS or RequireJS environment.'); - return false; - } -}; - -/** - * Updates the helper CSS class with any changes necessary after the terminal's - * character width has been changed. - */ -Terminal.prototype.updateCharSizeStyles = function() { - this.charSizeStyleElement.textContent = - `.xterm-wide-char{width:${this.charMeasure.width * 2}px;}` + - `.xterm-normal-char{width:${this.charMeasure.width}px;}` + - `.xterm-rows > div{height:${this.charMeasure.height}px;}`; -} - -/** - * XTerm mouse events - * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking - * To better understand these - * the xterm code is very helpful: - * Relevant files: - * button.c, charproc.c, misc.c - * Relevant functions in xterm/button.c: - * BtnCode, EmitButtonCode, EditorButton, SendMousePosition - */ -Terminal.prototype.bindMouse = function() { - var el = this.element, self = this, pressed = 32; - - // mouseup, mousedown, wheel - // left click: ^[[M 3<^[[M#3< - // wheel up: ^[[M`3> - function sendButton(ev) { - var button - , pos; - - // get the xterm-style button - button = getButton(ev); - - // get mouse coordinates - pos = getRawByteCoords(ev, self.rowContainer, self.charMeasure, self.cols, self.rows); - if (!pos) return; - - sendEvent(button, pos); - - switch (ev.overrideType || ev.type) { - case 'mousedown': - pressed = button; - break; - case 'mouseup': - // keep it at the left - // button, just in case. - pressed = 32; - break; - case 'wheel': - // nothing. don't - // interfere with - // `pressed`. - break; - } - } - - // motion example of a left click: - // ^[[M 3<^[[M@4<^[[M@5<^[[M@6<^[[M@7<^[[M#7< - function sendMove(ev) { - var button = pressed - , pos; - - pos = getRawByteCoords(ev, self.rowContainer, self.charMeasure, self.cols, self.rows); - if (!pos) return; - - // buttons marked as motions - // are incremented by 32 - button += 32; - - sendEvent(button, pos); - } - - // encode button and - // position to characters - function encode(data, ch) { - if (!self.utfMouse) { - if (ch === 255) return data.push(0); - if (ch > 127) ch = 127; - data.push(ch); - } else { - if (ch === 2047) return data.push(0); - if (ch < 127) { - data.push(ch); - } else { - if (ch > 2047) ch = 2047; - data.push(0xC0 | (ch >> 6)); - data.push(0x80 | (ch & 0x3F)); - } - } - } - - // send a mouse event: - // regular/utf8: ^[[M Cb Cx Cy - // urxvt: ^[[ Cb ; Cx ; Cy M - // sgr: ^[[ Cb ; Cx ; Cy M/m - // vt300: ^[[ 24(1/3/5)~ [ Cx , Cy ] \r - // locator: CSI P e ; P b ; P r ; P c ; P p & w - function sendEvent(button, pos) { - // self.emit('mouse', { - // x: pos.x - 32, - // y: pos.x - 32, - // button: button - // }); - - if (self.vt300Mouse) { - // NOTE: Unstable. - // http://www.vt100.net/docs/vt3xx-gp/chapter15.html - button &= 3; - pos.x -= 32; - pos.y -= 32; - var data = C0.ESC + '[24'; - if (button === 0) data += '1'; - else if (button === 1) data += '3'; - else if (button === 2) data += '5'; - else if (button === 3) return; - else data += '0'; - data += '~[' + pos.x + ',' + pos.y + ']\r'; - self.send(data); - return; - } - - if (self.decLocator) { - // NOTE: Unstable. - button &= 3; - pos.x -= 32; - pos.y -= 32; - if (button === 0) button = 2; - else if (button === 1) button = 4; - else if (button === 2) button = 6; - else if (button === 3) button = 3; - self.send(C0.ESC + '[' - + button - + ';' - + (button === 3 ? 4 : 0) - + ';' - + pos.y - + ';' - + pos.x - + ';' - + (pos.page || 0) - + '&w'); - return; - } - - if (self.urxvtMouse) { - pos.x -= 32; - pos.y -= 32; - pos.x++; - pos.y++; - self.send(C0.ESC + '[' + button + ';' + pos.x + ';' + pos.y + 'M'); - return; - } - - if (self.sgrMouse) { - pos.x -= 32; - pos.y -= 32; - self.send(C0.ESC + '[<' - + (((button & 3) === 3 ? button & ~3 : button) - 32) - + ';' - + pos.x - + ';' - + pos.y - + ((button & 3) === 3 ? 'm' : 'M')); - return; - } - - var data = []; - - encode(data, button); - encode(data, pos.x); - encode(data, pos.y); - - self.send(C0.ESC + '[M' + String.fromCharCode.apply(String, data)); - } - - function getButton(ev) { - var button - , shift - , meta - , ctrl - , mod; - - // two low bits: - // 0 = left - // 1 = middle - // 2 = right - // 3 = release - // wheel up/down: - // 1, and 2 - with 64 added - switch (ev.overrideType || ev.type) { - case 'mousedown': - button = ev.button != null - ? +ev.button - : ev.which != null - ? ev.which - 1 - : null; - - if (self.browser.isMSIE) { - button = button === 1 ? 0 : button === 4 ? 1 : button; - } - break; - case 'mouseup': - button = 3; - break; - case 'DOMMouseScroll': - button = ev.detail < 0 - ? 64 - : 65; - break; - case 'wheel': - button = ev.wheelDeltaY > 0 - ? 64 - : 65; - break; - } - - // next three bits are the modifiers: - // 4 = shift, 8 = meta, 16 = control - shift = ev.shiftKey ? 4 : 0; - meta = ev.metaKey ? 8 : 0; - ctrl = ev.ctrlKey ? 16 : 0; - mod = shift | meta | ctrl; - - // no mods - if (self.vt200Mouse) { - // ctrl only - mod &= ctrl; - } else if (!self.normalMouse) { - mod = 0; - } - - // increment to SP - button = (32 + (mod << 2)) + button; - - return button; - } - - on(el, 'mousedown', function(ev) { - - // Prevent the focus on the textarea from getting lost - // and make sure we get focused on mousedown - ev.preventDefault(); - self.focus(); - - if (!self.mouseEvents) return; - - // send the button - sendButton(ev); - - // fix for odd bug - //if (self.vt200Mouse && !self.normalMouse) { - if (self.vt200Mouse) { - ev.overrideType = 'mouseup'; - sendButton(ev); - return self.cancel(ev); - } - - // bind events - if (self.normalMouse) on(self.document, 'mousemove', sendMove); - - // x10 compatibility mode can't send button releases - if (!self.x10Mouse) { - on(self.document, 'mouseup', function up(ev) { - sendButton(ev); - if (self.normalMouse) off(self.document, 'mousemove', sendMove); - off(self.document, 'mouseup', up); - return self.cancel(ev); - }); - } - - return self.cancel(ev); - }); - - //if (self.normalMouse) { - // on(self.document, 'mousemove', sendMove); - //} - - on(el, 'wheel', function(ev) { - if (!self.mouseEvents) return; - if (self.x10Mouse - || self.vt300Mouse - || self.decLocator) return; - sendButton(ev); - return self.cancel(ev); - }); - - // allow wheel scrolling in - // the shell for example - on(el, 'wheel', function(ev) { - if (self.mouseEvents) return; - self.viewport.onWheel(ev); - return self.cancel(ev); - }); - - on(el, 'touchstart', function(ev) { - if (self.mouseEvents) return; - self.viewport.onTouchStart(ev); - return self.cancel(ev); - }); - - on(el, 'touchmove', function(ev) { - if (self.mouseEvents) return; - self.viewport.onTouchMove(ev); - return self.cancel(ev); - }); -}; - -/** - * Destroys the terminal. - */ -Terminal.prototype.destroy = function() { - this.readable = false; - this.writable = false; - this._events = {}; - this.handler = function() {}; - this.write = function() {}; - if (this.element && this.element.parentNode) { - this.element.parentNode.removeChild(this.element); - } - //this.emit('close'); -}; - -/** - * Tells the renderer to refresh terminal content between two rows (inclusive) at the next - * opportunity. - * @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). - */ -Terminal.prototype.refresh = function(start, end) { - if (this.renderer) { - this.renderer.queueRefresh(start, end); - } -}; - -/** - * Queues linkification for the specified rows. - * @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). - */ -Terminal.prototype.queueLinkification = function(start, end) { - if (this.linkifier) { - for (let i = start; i <= end; i++) { - this.linkifier.linkifyRow(i); - } - } -}; - -/** - * Display the cursor element - */ -Terminal.prototype.showCursor = function() { - if (!this.cursorState) { - this.cursorState = 1; - this.refresh(this.buffer.y, this.buffer.y); - } -}; - -/** - * Scroll the terminal down 1 row, creating a blank line. - * @param {boolean} isWrapped Whether the new line is wrapped from the previous - * line. - */ -Terminal.prototype.scroll = function(isWrapped) { - var row; - - // Make room for the new row in lines - if (this.buffer.lines.length === this.buffer.lines.maxLength) { - this.buffer.lines.trimStart(1); - this.buffer.ybase--; - if (this.buffer.ydisp !== 0) { - this.buffer.ydisp--; - } - } - - this.buffer.ybase++; - - // TODO: Why is this done twice? - if (!this.userScrolling) { - this.buffer.ydisp = this.buffer.ybase; - } - - // last line - row = this.buffer.ybase + this.rows - 1; - - // subtract the bottom scroll region - row -= this.rows - 1 - this.buffer.scrollBottom; - - if (row === this.buffer.lines.length) { - // Optimization: pushing is faster than splicing when they amount to the same behavior - this.buffer.lines.push(this.blankLine(undefined, isWrapped)); - } else { - // add our new line - this.buffer.lines.splice(row, 0, this.blankLine(undefined, isWrapped)); - } - - if (this.buffer.scrollTop !== 0) { - if (this.buffer.ybase !== 0) { - this.buffer.ybase--; - if (!this.userScrolling) { - this.buffer.ydisp = this.buffer.ybase; - } - } - this.buffer.lines.splice(this.buffer.ybase + this.buffer.scrollTop, 1); - } - - // this.maxRange(); - this.updateRange(this.buffer.scrollTop); - this.updateRange(this.buffer.scrollBottom); - - /** - * This event is emitted whenever the terminal is scrolled. - * The one parameter passed is the new y display position. - * - * @event scroll - */ - this.emit('scroll', this.buffer.ydisp); -}; - -/** - * Scroll the display of the terminal - * @param {number} disp The number of lines to scroll down (negatives scroll up). - * @param {boolean} suppressScrollEvent Don't emit the scroll event as scrollDisp. This is used - * to avoid unwanted events being handled by the veiwport when the event was triggered from the - * viewport originally. - */ -Terminal.prototype.scrollDisp = function(disp, suppressScrollEvent) { - if (disp < 0) { - if (this.buffer.ydisp === 0) { - return; - } - this.userScrolling = true; - } else if (disp + this.buffer.ydisp >= this.buffer.ybase) { - this.userScrolling = false; - } - - const oldYdisp = this.buffer.ydisp; - this.buffer.ydisp = Math.max(Math.min(this.buffer.ydisp + disp, this.buffer.ybase), 0); - - // No change occurred, don't trigger scroll/refresh - if (oldYdisp === this.buffer.ydisp) { - return; - } - - if (!suppressScrollEvent) { - this.emit('scroll', this.buffer.ydisp); - } - - this.refresh(0, this.rows - 1); -}; - -/** - * Scroll the display of the terminal by a number of pages. - * @param {number} pageCount The number of pages to scroll (negative scrolls up). - */ -Terminal.prototype.scrollPages = function(pageCount) { - this.scrollDisp(pageCount * (this.rows - 1)); -}; - -/** - * Scrolls the display of the terminal to the top. - */ -Terminal.prototype.scrollToTop = function() { - this.scrollDisp(-this.buffer.ydisp); -}; - -/** - * Scrolls the display of the terminal to the bottom. - */ -Terminal.prototype.scrollToBottom = function() { - this.scrollDisp(this.buffer.ybase - this.buffer.ydisp); -}; - -/** - * Writes text to the terminal. - * @param {string} data The text to write to the terminal. - */ -Terminal.prototype.write = function(data) { - this.writeBuffer.push(data); - - // 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) { - // XOFF - stop pty pipe - // XON will be triggered by emulator before processing data chunk - this.send(C0.DC3); - this.xoffSentToCatchUp = true; - } - - if (!this.writeInProgress && this.writeBuffer.length > 0) { - // Kick off a write which will write all data in sequence recursively - this.writeInProgress = true; - // Kick off an async innerWrite so more writes can come in while processing data - var self = this; - setTimeout(function () { - self.innerWrite(); - }); - } -}; - -Terminal.prototype.innerWrite = function() { - var writeBatch = this.writeBuffer.splice(0, WRITE_BATCH_SIZE); - while (writeBatch.length > 0) { - var data = writeBatch.shift(); - var l = data.length, i = 0, j, cs, ch, code, low, ch_width, row; - - // 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) { - this.send(C0.DC1); - this.xoffSentToCatchUp = false; - } - - 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. - var state = this.parser.parse(data); - this.parser.setState(state); - - this.updateRange(this.buffer.y); - this.refresh(this.refreshStart, this.refreshEnd); - } - if (this.writeBuffer.length > 0) { - // Allow renderer to catch up before processing the next batch - var self = this; - setTimeout(function () { - self.innerWrite(); - }, 0); - } else { - this.writeInProgress = false; - } -}; - -/** - * Writes text to the terminal, followed by a break line character (\n). - * @param {string} data The text to write to the terminal. - */ -Terminal.prototype.writeln = function(data) { - this.write(data + '\r\n'); -}; - -/** - * DEPRECATED: only for backward compatibility. Please use attachCustomKeyEventHandler() instead. - * @param {function} customKeydownHandler The custom KeyboardEvent handler to attach. This is a - * function that takes a KeyboardEvent, allowing consumers to stop propogation and/or prevent - * the default action. The function returns whether the event should be processed by xterm.js. - */ -Terminal.prototype.attachCustomKeydownHandler = function(customKeydownHandler) { - let message = 'attachCustomKeydownHandler() is DEPRECATED and will be removed soon. Please use attachCustomKeyEventHandler() instead.'; - console.warn(message); - this.attachCustomKeyEventHandler(customKeydownHandler); -}; - -/** - * Attaches a custom key event handler which is run before keys are processed, giving consumers of - * xterm.js ultimate control as to what keys should be processed by the terminal and what keys - * should not. - * @param {function} customKeyEventHandler The custom KeyboardEvent handler to attach. This is a - * function that takes a KeyboardEvent, allowing consumers to stop propogation and/or prevent - * the default action. The function returns whether the event should be processed by xterm.js. - */ -Terminal.prototype.attachCustomKeyEventHandler = function(customKeyEventHandler) { - 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 {LinkMatcherHandler} handler The handler callback function. - */ -Terminal.prototype.setHypertextLinkHandler = function(handler) { - 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 {LinkMatcherValidationCallback} callback The callback to use, this can - * be cleared with null. - */ -Terminal.prototype.setHypertextValidationCallback = function(callback) { - 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); -}; - -/** - * Registers a link matcher, allowing custom link patterns to be matched and - * handled. - * @param {RegExp} regex The regular expression to search for, specifically - * this searches the textContent of the rows. You will want to use \s to match - * a space ' ' character for example. - * @param {LinkMatcherHandler} handler The callback when the link is called. - * @param {LinkMatcherOptions} [options] Options for the link matcher. - * @return {number} The ID of the new matcher, this can be used to deregister. - */ -Terminal.prototype.registerLinkMatcher = function(regex, handler, options) { - if (this.linkifier) { - var matcherId = this.linkifier.registerLinkMatcher(regex, handler, options); - this.refresh(0, this.rows - 1); - return matcherId; - } -}; - -/** - * Deregisters a link matcher if it has been registered. - * @param {number} matcherId The link matcher's ID (returned after register) - */ -Terminal.prototype.deregisterLinkMatcher = function(matcherId) { - if (this.linkifier) { - if (this.linkifier.deregisterLinkMatcher(matcherId)) { - this.refresh(0, this.rows - 1); - } - } -}; - -/** - * Gets whether the terminal has an active selection. - */ -Terminal.prototype.hasSelection = function() { - return this.selectionManager ? this.selectionManager.hasSelection : false; -}; - -/** - * Gets the terminal's current selection, this is useful for implementing copy - * behavior outside of xterm.js. - */ -Terminal.prototype.getSelection = function() { - return this.selectionManager ? this.selectionManager.selectionText : ''; -}; - -/** - * Clears the current terminal selection. - */ -Terminal.prototype.clearSelection = function() { - if (this.selectionManager) { - this.selectionManager.clearSelection(); - } -}; - -/** - * Selects all text within the terminal. - */ -Terminal.prototype.selectAll = function() { - if (this.selectionManager) { - this.selectionManager.selectAll(); - } -}; - -/** - * Handle a keydown event - * Key Resources: - * - https://developer.mozilla.org/en-US/docs/DOM/KeyboardEvent - * @param {KeyboardEvent} ev The keydown event to be handled. - */ -Terminal.prototype.keyDown = function(ev) { - if (this.customKeyEventHandler && this.customKeyEventHandler(ev) === false) { - return false; - } - - this.restartCursorBlinking(); - - if (!this.compositionHelper.keydown.bind(this.compositionHelper)(ev)) { - if (this.buffer.ybase !== this.buffer.ydisp) { - this.scrollToBottom(); - } - return false; - } - - var self = this; - var 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.scrollDisp) { - this.scrollDisp(result.scrollDisp); - return this.cancel(ev, true); - } - - if (isThirdLevelShift(this, ev)) { - return true; - } - - if (result.cancel) { - // The event is canceled at the end already, is this necessary? - this.cancel(ev, true); - } - - if (!result.key) { - return true; - } - - this.emit('keydown', ev); - this.emit('key', result.key, ev); - this.showCursor(); - this.handler(result.key); - - return this.cancel(ev, true); -}; - -/** - * Returns an object that determines how a KeyboardEvent should be handled. The key of the - * returned value is the new key code to pass to the PTY. - * - * Reference: http://invisible-island.net/xterm/ctlseqs/ctlseqs.html - * @param {KeyboardEvent} ev The keyboard event to be translated to key escape sequence. - */ -Terminal.prototype.evaluateKeyEscapeSequence = function(ev) { - var result = { - // Whether to cancel event propogation (NOTE: this may not be needed since the event is - // canceled at the end of keyDown - cancel: false, - // The new key even to emit - key: undefined, - // The number of characters to scroll, if this is defined it will cancel the event - scrollDisp: undefined - }; - var modifiers = ev.shiftKey << 0 | ev.altKey << 1 | ev.ctrlKey << 2 | ev.metaKey << 3; - switch (ev.keyCode) { - case 8: - // backspace - if (ev.shiftKey) { - result.key = C0.BS; // ^H - break; - } - result.key = C0.DEL; // ^? - break; - case 9: - // tab - if (ev.shiftKey) { - result.key = C0.ESC + '[Z'; - break; - } - result.key = C0.HT; - result.cancel = true; - break; - case 13: - // return/enter - result.key = C0.CR; - result.cancel = true; - break; - case 27: - // escape - result.key = C0.ESC; - result.cancel = true; - break; - case 37: - // left-arrow - if (modifiers) { - result.key = C0.ESC + '[1;' + (modifiers + 1) + 'D'; - // HACK: Make Alt + left-arrow behave like Ctrl + left-arrow: move one word backwards - // http://unix.stackexchange.com/a/108106 - // macOS uses different escape sequences than linux - if (result.key == C0.ESC + '[1;3D') { - result.key = (this.browser.isMac) ? C0.ESC + 'b' : C0.ESC + '[1;5D'; - } - } else if (this.applicationCursor) { - result.key = C0.ESC + 'OD'; - } else { - result.key = C0.ESC + '[D'; - } - break; - case 39: - // right-arrow - if (modifiers) { - result.key = C0.ESC + '[1;' + (modifiers + 1) + 'C'; - // HACK: Make Alt + right-arrow behave like Ctrl + right-arrow: move one word forward - // http://unix.stackexchange.com/a/108106 - // macOS uses different escape sequences than linux - if (result.key == C0.ESC + '[1;3C') { - result.key = (this.browser.isMac) ? C0.ESC + 'f' : C0.ESC + '[1;5C'; - } - } else if (this.applicationCursor) { - result.key = C0.ESC + 'OC'; - } else { - result.key = C0.ESC + '[C'; - } - break; - case 38: - // up-arrow - if (modifiers) { - result.key = C0.ESC + '[1;' + (modifiers + 1) + 'A'; - // HACK: Make Alt + up-arrow behave like Ctrl + up-arrow - // http://unix.stackexchange.com/a/108106 - if (result.key == C0.ESC + '[1;3A') { - result.key = C0.ESC + '[1;5A'; - } - } else if (this.applicationCursor) { - result.key = C0.ESC + 'OA'; - } else { - result.key = C0.ESC + '[A'; - } - break; - case 40: - // down-arrow - if (modifiers) { - result.key = C0.ESC + '[1;' + (modifiers + 1) + 'B'; - // HACK: Make Alt + down-arrow behave like Ctrl + down-arrow - // http://unix.stackexchange.com/a/108106 - if (result.key == C0.ESC + '[1;3B') { - result.key = C0.ESC + '[1;5B'; - } - } else if (this.applicationCursor) { - result.key = C0.ESC + 'OB'; - } else { - result.key = C0.ESC + '[B'; - } - break; - case 45: - // insert - if (!ev.shiftKey && !ev.ctrlKey) { - // or + are used to - // copy-paste on some systems. - result.key = C0.ESC + '[2~'; - } - break; - case 46: - // delete - if (modifiers) { - result.key = C0.ESC + '[3;' + (modifiers + 1) + '~'; - } else { - result.key = C0.ESC + '[3~'; - } - break; - case 36: - // home - if (modifiers) - result.key = C0.ESC + '[1;' + (modifiers + 1) + 'H'; - else if (this.applicationCursor) - result.key = C0.ESC + 'OH'; - else - result.key = C0.ESC + '[H'; - break; - case 35: - // end - if (modifiers) - result.key = C0.ESC + '[1;' + (modifiers + 1) + 'F'; - else if (this.applicationCursor) - result.key = C0.ESC + 'OF'; - else - result.key = C0.ESC + '[F'; - break; - case 33: - // page up - if (ev.shiftKey) { - result.scrollDisp = -(this.rows - 1); - } else { - result.key = C0.ESC + '[5~'; - } - break; - case 34: - // page down - if (ev.shiftKey) { - result.scrollDisp = this.rows - 1; - } else { - result.key = C0.ESC + '[6~'; - } - break; - case 112: - // F1-F12 - if (modifiers) { - result.key = C0.ESC + '[1;' + (modifiers + 1) + 'P'; - } else { - result.key = C0.ESC + 'OP'; - } - break; - case 113: - if (modifiers) { - result.key = C0.ESC + '[1;' + (modifiers + 1) + 'Q'; - } else { - result.key = C0.ESC + 'OQ'; - } - break; - case 114: - if (modifiers) { - result.key = C0.ESC + '[1;' + (modifiers + 1) + 'R'; - } else { - result.key = C0.ESC + 'OR'; - } - break; - case 115: - if (modifiers) { - result.key = C0.ESC + '[1;' + (modifiers + 1) + 'S'; - } else { - result.key = C0.ESC + 'OS'; - } - break; - case 116: - if (modifiers) { - result.key = C0.ESC + '[15;' + (modifiers + 1) + '~'; - } else { - result.key = C0.ESC + '[15~'; - } - break; - case 117: - if (modifiers) { - result.key = C0.ESC + '[17;' + (modifiers + 1) + '~'; - } else { - result.key = C0.ESC + '[17~'; - } - break; - case 118: - if (modifiers) { - result.key = C0.ESC + '[18;' + (modifiers + 1) + '~'; - } else { - result.key = C0.ESC + '[18~'; - } - break; - case 119: - if (modifiers) { - result.key = C0.ESC + '[19;' + (modifiers + 1) + '~'; - } else { - result.key = C0.ESC + '[19~'; - } - break; - case 120: - if (modifiers) { - result.key = C0.ESC + '[20;' + (modifiers + 1) + '~'; - } else { - result.key = C0.ESC + '[20~'; - } - break; - case 121: - if (modifiers) { - result.key = C0.ESC + '[21;' + (modifiers + 1) + '~'; - } else { - result.key = C0.ESC + '[21~'; - } - break; - case 122: - if (modifiers) { - result.key = C0.ESC + '[23;' + (modifiers + 1) + '~'; - } else { - result.key = C0.ESC + '[23~'; - } - break; - case 123: - if (modifiers) { - result.key = C0.ESC + '[24;' + (modifiers + 1) + '~'; - } else { - result.key = C0.ESC + '[24~'; - } - break; - default: - // a-z and space - if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) { - if (ev.keyCode >= 65 && ev.keyCode <= 90) { - result.key = String.fromCharCode(ev.keyCode - 64); - } else if (ev.keyCode === 32) { - // NUL - result.key = String.fromCharCode(0); - } else if (ev.keyCode >= 51 && ev.keyCode <= 55) { - // escape, file sep, group sep, record sep, unit sep - result.key = String.fromCharCode(ev.keyCode - 51 + 27); - } else if (ev.keyCode === 56) { - // delete - result.key = String.fromCharCode(127); - } else if (ev.keyCode === 219) { - // ^[ - Control Sequence Introducer (CSI) - result.key = String.fromCharCode(27); - } else if (ev.keyCode === 220) { - // ^\ - String Terminator (ST) - result.key = String.fromCharCode(28); - } else if (ev.keyCode === 221) { - // ^] - Operating System Command (OSC) - result.key = String.fromCharCode(29); - } - } else if (!this.browser.isMac && ev.altKey && !ev.ctrlKey && !ev.metaKey) { - // On Mac this is a third level shift. Use instead. - if (ev.keyCode >= 65 && ev.keyCode <= 90) { - result.key = C0.ESC + String.fromCharCode(ev.keyCode + 32); - } else if (ev.keyCode === 192) { - result.key = C0.ESC + '`'; - } else if (ev.keyCode >= 48 && ev.keyCode <= 57) { - result.key = C0.ESC + (ev.keyCode - 48); - } - } else if (this.browser.isMac && !ev.altKey && !ev.ctrlKey && ev.metaKey) { - if (ev.keyCode === 65) { // cmd + a - this.selectAll(); - } - } - break; - } - - return result; -}; - -/** - * Set the G level of the terminal - * @param g - */ -Terminal.prototype.setgLevel = function(g) { - this.glevel = g; - this.charset = this.charsets[g]; -}; - -/** - * Set the charset for the given G level of the terminal - * @param g - * @param charset - */ -Terminal.prototype.setgCharset = function(g, charset) { - this.charsets[g] = charset; - if (this.glevel === g) { - this.charset = charset; - } -}; - -/** - * Handle a keypress event. - * Key Resources: - * - https://developer.mozilla.org/en-US/docs/DOM/KeyboardEvent - * @param {KeyboardEvent} ev The keypress event to be handled. - */ -Terminal.prototype.keyPress = function(ev) { - var key; - - if (this.customKeyEventHandler && this.customKeyEventHandler(ev) === false) { - return false; - } - - this.cancel(ev); - - if (ev.charCode) { - key = ev.charCode; - } else if (ev.which == null) { - key = ev.keyCode; - } else if (ev.which !== 0 && ev.charCode !== 0) { - key = ev.which; - } else { - return false; - } - - if (!key || ( - (ev.altKey || ev.ctrlKey || ev.metaKey) && !isThirdLevelShift(this, ev) - )) { - return false; - } - - key = String.fromCharCode(key); - - this.emit('keypress', key, ev); - this.emit('key', key, ev); - this.showCursor(); - this.handler(key); - - return true; -}; - -/** - * Send data for handling to the terminal - * @param {string} data - */ -Terminal.prototype.send = function(data) { - var self = this; - - if (!this.queue) { - setTimeout(function() { - self.handler(self.queue); - self.queue = ''; - }, 1); - } - - this.queue += data; -}; - -/** - * Ring the bell. - * Note: We could do sweet things with webaudio here - */ -Terminal.prototype.bell = function() { - if (!this.visualBell) return; - var self = this; - this.element.style.borderColor = 'white'; - setTimeout(function() { - self.element.style.borderColor = ''; - }, 10); - if (this.popOnBell) this.focus(); -}; - -/** - * Log the current state to the console. - */ -Terminal.prototype.log = function() { - if (!this.debug) return; - if (!this.context.console || !this.context.console.log) return; - var args = Array.prototype.slice.call(arguments); - this.context.console.log.apply(this.context.console, args); -}; - -/** - * Log the current state as error to the console. - */ -Terminal.prototype.error = function() { - if (!this.debug) return; - if (!this.context.console || !this.context.console.error) return; - var args = Array.prototype.slice.call(arguments); - this.context.console.error.apply(this.context.console, args); -}; - -/** - * Resizes the terminal. - * - * @param {number} x The number of columns to resize to. - * @param {number} y The number of rows to resize to. - */ -Terminal.prototype.resize = function(x, y) { - if (isNaN(x) || isNaN(y)) { - return; - } - - if (y > this.getOption('scrollback')) { - this.setOption('scrollback', y) - } - - var line - , el - , i - , j - , ch - , addToY; - - if (x === this.cols && y === this.rows) { - // Check if we still need to measure the char size (fixes #785). - if (!this.charMeasure.width || !this.charMeasure.height) { - this.charMeasure.measure(); - } - return; - } - - if (x < 1) x = 1; - if (y < 1) y = 1; - - this.buffers.resize(x, y); - - // Adjust rows in the DOM to accurately reflect the new dimensions - while (this.children.length < y) { - this.insertRow(); - } - while (this.children.length > y) { - el = this.children.shift(); - if (!el) continue; - el.parentNode.removeChild(el); - } - - this.cols = x; - this.rows = y; - this.setupStops(this.cols); - - this.charMeasure.measure(); - - this.refresh(0, this.rows - 1); - - this.geometry = [this.cols, this.rows]; - this.emit('resize', {terminal: this, cols: x, rows: y}); -}; - -/** - * Updates the range of rows to refresh - * @param {number} y The number of rows to refresh next. - */ -Terminal.prototype.updateRange = function(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) { - // this.refreshEnd = this.rows - 1; - // } - // } -}; - -/** - * Set the range of refreshing to the maximum value - */ -Terminal.prototype.maxRange = function() { - this.refreshStart = 0; - this.refreshEnd = this.rows - 1; -}; - - - -/** - * Setup the tab stops. - * @param {number} i - */ -Terminal.prototype.setupStops = function(i) { - if (i != null) { - if (!this.buffer.tabs[i]) { - i = this.prevStop(i); - } - } else { - this.buffer.tabs = {}; - i = 0; - } - - for (; i < this.cols; i += this.getOption('tabStopWidth')) { - this.buffer.tabs[i] = true; - } -}; - - -/** - * Move the cursor to the previous tab stop from the given position (default is current). - * @param {number} x The position to move the cursor to the previous tab stop. - */ -Terminal.prototype.prevStop = function(x) { - if (x == null) x = this.buffer.x; - while (!this.buffer.tabs[--x] && x > 0); - return x >= this.cols - ? this.cols - 1 - : x < 0 ? 0 : x; -}; - - -/** - * Move the cursor one tab stop forward from the given position (default is current). - * @param {number} x The position to move the cursor one tab stop forward. - */ -Terminal.prototype.nextStop = function(x) { - if (x == null) x = this.buffer.x; - while (!this.buffer.tabs[++x] && x < this.cols); - return x >= this.cols - ? this.cols - 1 - : x < 0 ? 0 : x; -}; - - -/** - * Erase in the identified line everything from "x" to the end of the line (right). - * @param {number} x The column from which to start erasing to the end of the line. - * @param {number} y The line in which to operate. - */ -Terminal.prototype.eraseRight = function(x, y) { - var line = this.buffer.lines.get(this.buffer.ybase + y); - if (!line) { - return; - } - var ch = [this.eraseAttr(), ' ', 1]; // xterm - for (; x < this.cols; x++) { - line[x] = ch; - } - this.updateRange(y); -}; - - - -/** - * Erase in the identified line everything from "x" to the start of the line (left). - * @param {number} x The column from which to start erasing to the start of the line. - * @param {number} y The line in which to operate. - */ -Terminal.prototype.eraseLeft = function(x, y) { - var line = this.buffer.lines.get(this.buffer.ybase + y); - if (!line) { - return; - } - var ch = [this.eraseAttr(), ' ', 1]; // xterm - x++; - while (x--) { - line[x] = ch; - } - this.updateRange(y); -}; - -/** - * Clears the entire buffer, making the prompt line the new first line. - */ -Terminal.prototype.clear = function() { - if (this.buffer.ybase === 0 && this.buffer.y === 0) { - // Don't clear if it's already clear - return; - } - this.buffer.lines.set(0, this.buffer.lines.get(this.buffer.ybase + this.buffer.y)); - this.buffer.lines.length = 1; - this.buffer.ydisp = 0; - this.buffer.ybase = 0; - this.buffer.y = 0; - for (var i = 1; i < this.rows; i++) { - this.buffer.lines.push(this.blankLine()); - } - this.refresh(0, this.rows - 1); - this.emit('scroll', this.buffer.ydisp); -}; - -/** - * Erase all content in the given line - * @param {number} y The line to erase all of its contents. - */ -Terminal.prototype.eraseLine = function(y) { - this.eraseRight(0, y); -}; - - -/** - * Return the data array of a blank line - * @param {number} cur First bunch of data for each "blank" character. - * @param {boolean} isWrapped Whether the new line is wrapped from the previous line. - */ -Terminal.prototype.blankLine = function(cur, isWrapped) { - var attr = cur - ? this.eraseAttr() - : this.defAttr; - - var ch = [attr, ' ', 1] // width defaults to 1 halfwidth character - , line = [] - , i = 0; - - // TODO: It is not ideal that this is a property on an array, a buffer line - // class should be added that will hold this data and other useful functions. - if (isWrapped) { - line.isWrapped = isWrapped; - } - - for (; i < this.cols; i++) { - line[i] = ch; - } - - return line; -}; - - -/** - * If cur return the back color xterm feature attribute. Else return defAttr. - * @param {object} cur - */ -Terminal.prototype.ch = function(cur) { - return cur - ? [this.eraseAttr(), ' ', 1] - : [this.defAttr, ' ', 1]; -}; - - -/** - * Evaluate if the current terminal is the given argument. - * @param {object} term The terminal to evaluate - */ -Terminal.prototype.is = function(term) { - var name = this.termName; - return (name + '').indexOf(term) === 0; -}; - - -/** - * Emit the 'data' event and populate the given data. - * @param {string} data The data to populate in the event. - */ -Terminal.prototype.handler = function(data) { - // Prevents all events to pty process if stdin is disabled - if (this.options.disableStdin) { - return; - } - - // Clear the selection if the selection manager is available and has an active selection - if (this.selectionManager && this.selectionManager.hasSelection) { - this.selectionManager.clearSelection(); - } - - // Input is being sent to the terminal, the terminal should focus the prompt. - if (this.buffer.ybase !== this.buffer.ydisp) { - this.scrollToBottom(); - } - this.emit('data', data); -}; - - -/** - * Emit the 'title' event and populate the given title. - * @param {string} title The title to populate in the event. - */ -Terminal.prototype.handleTitle = function(title) { - /** - * This event is emitted when the title of the terminal is changed - * from inside the terminal. The parameter is the new title. - * - * @event title - */ - this.emit('title', title); -}; - - -/** - * ESC - */ - -/** - * ESC D Index (IND is 0x84). - */ -Terminal.prototype.index = function() { - this.buffer.y++; - if (this.buffer.y > this.buffer.scrollBottom) { - this.buffer.y--; - this.scroll(); - } - // If the end of the line is hit, prevent this action from wrapping around to the next line. - if (this.buffer.x >= this.cols) { - this.buffer.x--; - } -}; - - -/** - * ESC M Reverse Index (RI is 0x8d). - * - * Move the cursor up one row, inserting a new blank line if necessary. - */ -Terminal.prototype.reverseIndex = function() { - var j; - if (this.buffer.y === this.buffer.scrollTop) { - // possibly move the code below to term.reverseScroll(); - // test: echo -ne '\e[1;1H\e[44m\eM\e[0m' - // blankLine(true) is xterm/linux behavior - this.buffer.lines.shiftElements(this.buffer.y + this.buffer.ybase, this.rows - 1, 1); - this.buffer.lines.set(this.buffer.y + this.buffer.ybase, this.blankLine(true)); - this.updateRange(this.buffer.scrollTop); - this.updateRange(this.buffer.scrollBottom); - } else { - this.buffer.y--; - } -}; - - -/** - * ESC c Full Reset (RIS). - */ -Terminal.prototype.reset = function() { - this.options.rows = this.rows; - this.options.cols = this.cols; - var customKeyEventHandler = this.customKeyEventHandler; - var cursorBlinkInterval = this.cursorBlinkInterval; - var inputHandler = this.inputHandler; - Terminal.call(this, this.options); - this.customKeyEventHandler = customKeyEventHandler; - this.cursorBlinkInterval = cursorBlinkInterval; - this.inputHandler = inputHandler; - this.refresh(0, this.rows - 1); - this.viewport.syncScrollArea(); -}; - - -/** - * ESC H Tab Set (HTS is 0x88). - */ -Terminal.prototype.tabSet = function() { - this.buffer.tabs[this.buffer.x] = true; -}; - -/** - * Helpers - */ - -function on(el, type, handler, capture) { - if (!Array.isArray(el)) { - el = [el]; - } - el.forEach(function (element) { - element.addEventListener(type, handler, capture || false); - }); -} - -function off(el, type, handler, capture) { - el.removeEventListener(type, handler, capture || false); -} - -function cancel(ev, force) { - if (!this.cancelEvents && !force) { - return; - } - ev.preventDefault(); - ev.stopPropagation(); - return false; -} - -function inherits(child, parent) { - function f() { - this.constructor = child; - } - f.prototype = parent.prototype; - child.prototype = new f; -} - -function indexOf(obj, el) { - var i = obj.length; - while (i--) { - if (obj[i] === el) return i; - } - return -1; -} - -function isThirdLevelShift(term, ev) { - var thirdLevelKey = - (term.browser.isMac && ev.altKey && !ev.ctrlKey && !ev.metaKey) || - (term.browser.isMSWindows && ev.altKey && ev.ctrlKey && !ev.metaKey); - - if (ev.type == 'keypress') { - return thirdLevelKey; - } - - // Don't invoke for arrows, pageDown, home, backspace, etc. (on non-keypress events) - return thirdLevelKey && (!ev.keyCode || ev.keyCode > 47); -} - -// Expose to InputHandler (temporary) -Terminal.prototype.matchColor = matchColor; - -function matchColor(r1, g1, b1) { - var hash = (r1 << 16) | (g1 << 8) | b1; - - if (matchColor._cache[hash] != null) { - return matchColor._cache[hash]; - } - - var ldiff = Infinity - , li = -1 - , i = 0 - , c - , r2 - , g2 - , b2 - , diff; - - for (; i < Terminal.vcolors.length; i++) { - c = Terminal.vcolors[i]; - r2 = c[0]; - g2 = c[1]; - b2 = c[2]; - - diff = matchColor.distance(r1, g1, b1, r2, g2, b2); - - if (diff === 0) { - li = i; - break; - } - - if (diff < ldiff) { - ldiff = diff; - li = i; - } - } - - return matchColor._cache[hash] = li; -} - -matchColor._cache = {}; - -// http://stackoverflow.com/questions/1633828 -matchColor.distance = function(r1, g1, b1, r2, g2, b2) { - return Math.pow(30 * (r1 - r2), 2) - + Math.pow(59 * (g1 - g2), 2) - + Math.pow(11 * (b1 - b2), 2); -}; - -function each(obj, iter, con) { - if (obj.forEach) return obj.forEach(iter, con); - for (var i = 0; i < obj.length; i++) { - iter.call(con, obj[i], i, obj); - } -} - -function wasMondifierKeyOnlyEvent(ev) { - return ev.keyCode === 16 || // Shift - ev.keyCode === 17 || // Ctrl - ev.keyCode === 18; // Alt -} - -function keys(obj) { - if (Object.keys) return Object.keys(obj); - var key, keys = []; - for (key in obj) { - if (Object.prototype.hasOwnProperty.call(obj, key)) { - keys.push(key); - } - } - return keys; -} - -/** - * Expose - */ - -Terminal.translateBufferLineToString = translateBufferLineToString; -Terminal.EventEmitter = EventEmitter; -Terminal.inherits = inherits; - -/** - * Adds an event listener to the terminal. - * - * @param {string} event The name of the event. TODO: Document all event types - * @param {function} callback The function to call when the event is triggered. - */ -Terminal.on = on; -Terminal.off = off; -Terminal.cancel = cancel; - -module.exports = Terminal; From 33cc78c987e429896473e2970cb0cc587e80fc83 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 6 Aug 2017 01:42:57 -0700 Subject: [PATCH 21/22] Add explicit types on set/getOption --- src/Terminal.ts | 19 +++++++++++++++---- src/Types.ts | 23 +++++++++++++++++++++++ 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index c1fd1e7ed4..06a1a3164f 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -29,7 +29,7 @@ import * as Mouse from './utils/Mouse'; import { CHARSETS } from './Charsets'; import { getRawByteCoords } from './utils/Mouse'; import { translateBufferLineToString } from './utils/BufferLine'; -import { CustomKeyEventHandler, Charset, LinkMatcherHandler, LinkMatcherValidationCallback, CharData, LineData } from './Types'; +import { CustomKeyEventHandler, Charset, LinkMatcherHandler, LinkMatcherValidationCallback, CharData, LineData, Option, StringOption, BooleanOption, StringArrayOption, NumberOption, GeometryOption, HandlerOption } from './Types'; import { ITerminal, IBrowser, ITerminalOptions, IInputHandlingTerminal, ILinkMatcherOptions } from './Interfaces'; // Declare for RequireJS in loadAddon @@ -415,7 +415,13 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT * Retrieves an option's value from the terminal. * @param {string} key The option key. */ - public getOption(key: string): any { + public getOption(key: StringOption): string; + public getOption(key: BooleanOption): boolean; + public getOption(key: StringArrayOption): number[]; + public getOption(key: NumberOption): number; + public getOption(key: GeometryOption): [number, number]; + public getOption(key: HandlerOption): (data: string) => void; + public getOption(key: Option): any { if (!(key in DEFAULT_OPTIONS)) { throw new Error('No option with key "' + key + '"'); } @@ -432,8 +438,13 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT * @param {string} key The option key. * @param {any} value The option value. */ - public setOption(key: string, value: any): void { - // TODO: Give value a better type (boolean | string, ...) + public setOption(key: StringOption, value: string): void; + public setOption(key: BooleanOption, value: boolean): void; + public setOption(key: StringArrayOption, value: number[]): void; + public setOption(key: NumberOption, value: number): void; + public setOption(key: GeometryOption, value: [number, number]): void; + public setOption(key: HandlerOption, value: (data: string) => void): void; + public setOption(key: Option, value: any): void { if (!(key in DEFAULT_OPTIONS)) { throw new Error('No option with key "' + key + '"'); } diff --git a/src/Types.ts b/src/Types.ts index 0753d80e19..0de78d5261 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -18,3 +18,26 @@ export type Charset = {[key: string]: string}; export type CharData = [number, string, number]; export type LineData = CharData[]; + +export type Option = BooleanOption | StringOption | StringArrayOption | NumberOption | GeometryOption | HandlerOption; +export type BooleanOption = + 'cancelEvents' | + 'convertEol' | + 'cursorBlink' | + 'debug' | + 'disableStdin' | + 'popOnBell' | + 'screenKeys' | + 'useFlowControl' | + 'visualBell'; +export type StringOption = + 'cursorStyle' | + 'termName'; +export type StringArrayOption = 'colors'; +export type NumberOption = + 'cols' | + 'rows' | + 'tabStopWidth' | + 'scrollback'; +export type GeometryOption = 'geometry'; +export type HandlerOption = 'handler'; From 6cf2e8753e908485192e6e7ed417df880b4aec4e Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 6 Aug 2017 01:44:27 -0700 Subject: [PATCH 22/22] Remove comments --- src/Terminal.ts | 39 --------------------------------------- src/xterm.ts | 11 +++++++++++ 2 files changed, 11 insertions(+), 39 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index 06a1a3164f..cd44e7fb76 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -35,17 +35,6 @@ import { ITerminal, IBrowser, ITerminalOptions, IInputHandlingTerminal, ILinkMat // Declare for RequireJS in loadAddon declare var define: any; -/** - * Terminal Emulation References: - * http://vt100.net/ - * http://invisible-island.net/xterm/ctlseqs/ctlseqs.txt - * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html - * http://invisible-island.net/vttest/ - * http://www.inwap.com/pdp10/ansicode.txt - * http://linux.die.net/man/4/console_codes - * http://linux.die.net/man/7/urxvt - */ - // Let it work inside Node.js for automated testing purposes. const document = (typeof window !== 'undefined') ? window.document : null; @@ -2308,15 +2297,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT } } - - -// TODO: Make sure things work after this is removed -// each(keys(Terminal.defaults), function(key) { -// Terminal[key] = Terminal.defaults[key]; -// Terminal.options[key] = Terminal.defaults[key]; -// }); - - /** * Helpers */ @@ -2363,22 +2343,3 @@ function wasMondifierKeyOnlyEvent(ev: KeyboardEvent): boolean { ev.keyCode === 17 || // Ctrl ev.keyCode === 18; // Alt } - -/** - * Expose - */ - -// Terminal.translateBufferLineToString = translateBufferLineToString; -// Terminal.EventEmitter = EventEmitter; -// Terminal.inherits = inherits; - -/** - * Adds an event listener to the terminal. - * - * @param {string} event The name of the event. TODO: Document all event types - * @param {function} callback The function to call when the event is triggered. - */ -// Terminal.on = on; -// Terminal.off = off; - -// module.exports = Terminal; diff --git a/src/xterm.ts b/src/xterm.ts index 7575186538..49e44151e4 100644 --- a/src/xterm.ts +++ b/src/xterm.ts @@ -4,4 +4,15 @@ import { Terminal } from './Terminal'; +/** + * Terminal Emulation References: + * http://vt100.net/ + * http://invisible-island.net/xterm/ctlseqs/ctlseqs.txt + * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html + * http://invisible-island.net/vttest/ + * http://www.inwap.com/pdp10/ansicode.txt + * http://linux.die.net/man/4/console_codes + * http://linux.die.net/man/7/urxvt + */ + module.exports = Terminal;