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 | 4272 +++++++++++++++++++------------------ src/Types.ts | 5 + src/utils/CircularList.ts | 3 +- src/xterm.ts | 3 + 7 files changed, 2157 insertions(+), 2138 deletions(-) create mode 100644 src/xterm.ts diff --git a/src/Charsets.ts b/src/Charsets.ts index f567ef44..c2c92e8d 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 00c10941..3d34da75 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 f19a7f28..c675f7a2 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 5aa069e7..18fcd000 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; - } - - 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.'); - } +export class Terminal extends EventEmitter implements ITerminal { + public textarea: HTMLTextAreaElement; + public element: HTMLElement; + public rowContainer: HTMLElement; - // Grab global elements - this.context = this.parent.ownerDocument.defaultView; - this.document = this.parent.ownerDocument; - this.body = this.document.getElementsByTagName('body')[0]; + // 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; - //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); + // TODO: This should be removed from the interface, opting for modules to pull + // in Browser for themselves. + public browser: IBrowser = Browser; - this.element.setAttribute('tabindex', 0); + private options: TerminalOptions; + private colors: any; - 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); + // 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; - // Create the selection container. - this.selectionContainer = document.createElement('div'); - this.selectionContainer.classList.add('xterm-selection'); - this.element.appendChild(this.selectionContainer); + // 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 - // 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); + // charset + // The current charset + private charset: Charset = null; + private gcharset: number = null; + private glevel: number = 0; + private charsets: Charset[] = [null]; - // 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); + // mouse properties + private decLocator; + private x10Mouse; + private vt200Mouse; + private vt300Mouse; + private normalMouse; + private mouseEvents; + private sendFocus; + private utfMouse; + private sgrMouse; + private urxvtMouse; - 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); + // misc + private children; + private refreshStart; + private refreshEnd; + private savedX; + private savedY; + private savedCols; - this.charSizeStyleElement = document.createElement('style'); - this.helperContainer.appendChild(this.charSizeStyleElement); + // stream + private readable: boolean = true; + private writable: boolean = true; - for (; i < this.rows; i++) { - this.insertRow(); - } - this.parent.appendChild(this.element); + private defAttr: number = (0 << 18) | (257 << 9) | (256 << 0); + private curAttr: number = (0 << 18) | (257 << 9) | (256 << 0); - this.charMeasure = new CharMeasure(document, this.helperContainer); - this.charMeasure.on('charsizechanged', function () { - self.updateCharSizeStyles(); - }); - this.charMeasure.measure(); + private params: (string | number)[] = []; + private currentParam: string | number = 0; + private prefix: string = ''; + private postfix: string = ''; - 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(); + // user input states + public writeBuffer: string[] = []; + private writeInProgress: boolean = false; /** - * Automatic focus functionality. - * TODO: Default to `false` starting with xterm.js 3.0. + * 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. */ - if (typeof focus == 'undefined') { - let message = 'You did not pass the `focus` argument in `Terminal.prototype.open()`.\n'; + private xoffSentToCatchUp: boolean = false; - message += 'The `focus` argument now defaults to `true` but starting with xterm.js 3.0 '; - message += 'it will default to `false`.'; + /** Whether writing has been stopped as a result of XOFF */ + private writeStopped: boolean = false; - console.warn(message); - focus = true; - } + // leftover surrogate high from previous write invocation + private surrogate_high: string = ''; - if (focus) { - this.focus(); - } + // Store if user went browsing history in scrollback + private userScrolling: boolean = false; - // Listen for mouse events and translate - // them into terminal mouse protocols. - this.bindMouse(); + 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]; /** - * This event is emitted when terminal has completed opening. + * Creates a new `Terminal` object. * - * @event open + * @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 */ - this.emit('open'); -}; + constructor( + options: any = {} + ) { + super(); + let self: any = this; -/** - * 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); + // TODO: Can this be removed? + if (!(this instanceof Terminal)) { + return new Terminal({ + cols: arguments[0], + rows: arguments[1], + handler: arguments[2] }); } - 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; + Object.keys(DEFAULT_OPTIONS).forEach(function(key) { + if (options[key] == null) { + if (Terminal[key] !== DEFAULT_OPTIONS[key]) { + options[key] = Terminal[key]; + } } - } - this.buffer.lines.splice(this.buffer.ybase + this.buffer.scrollTop, 1); - } + self[key] = options[key]; + }); - // this.maxRange(); - this.updateRange(this.buffer.scrollTop); - this.updateRange(this.buffer.scrollBottom); + 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; + + // 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.geometry = [this.cols, this.rows]; + + if (options.handler) { + this.on('data', options.handler); + } + + 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; + }); + + let 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(); + } /** - * This event is emitted whenever the terminal is scrolled. - * The one parameter passed is the new y display position. - * - * @event scroll + * back_color_erase feature for xterm. */ - this.emit('scroll', this.buffer.ydisp); -}; + public eraseAttr(): any { + // if (this.is('screen')) return this.defAttr; + return (this.defAttr & ~0x1ff) | (this.curAttr & 0x1ff); + } -/** - * 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; + /** + * Focus the terminal. Delegates focus handling to the terminal's DOM element. + */ + public focus(): void { + this.textarea.focus(); + } + + /** + * Retrieves an option's value from the terminal. + * @param {string} key The option key. + */ + public getOption(key: string): any { + if (!(key in DEFAULT_OPTIONS)) { + throw new Error('No option with key "' + key + '"'); } - this.userScrolling = true; - } else if (disp + this.buffer.ydisp >= this.buffer.ybase) { - this.userScrolling = false; + + if (typeof this.options[key] !== 'undefined') { + return this.options[key]; + } + + return this[key]; } - const oldYdisp = this.buffer.ydisp; - this.buffer.ydisp = Math.max(Math.min(this.buffer.ydisp + disp, this.buffer.ybase), 0); + /** + * 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 '; - // No change occurred, don't trigger scroll/refresh - if (oldYdisp === this.buffer.ydisp) { - return; + 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; + } } - if (!suppressScrollEvent) { + 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); + } + } + + private clearCursorBlinkingInterval() { + 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. + */ + 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}); + }); + }; + + /** + * Blur the terminal. Delegates blur handling to the terminal's DOM element. + */ + private blur() { + return this.textarea.blur(); + } + + /** + * 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}); + }); + } + + /** + * 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); + }); + } + + // 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); + } + }); + } + } + + /** + * 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); + + 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); + + on(this.textarea, 'keydown', function(ev) { + this.keyDown(ev); + }, true); + + on(this.textarea, 'keypress', function(ev) { + this.keyPress(ev); + // Truncate the textarea's value, since it is not needed + this.value = ''; + }, true); + + 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)); + } + + /** + * 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'); + } + + 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 + */ + 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.'); + } + + // 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); + + 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(); + + 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 + */ + 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; + } + } + + /** + * 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;}`; + } + + /** + * 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; + } + } + + // 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; + + 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; + 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)); + } + + 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; + } + + // 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', (ev) => { + + // Prevent the focus on the textarea from getting lost + // and make sure we get focused on mousedown + ev.preventDefault(); + this.focus(); + + 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); + }); + } + + /** + * 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'); + } + + /** + * 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); + } + } + + /** + * 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); + } + } + } + + /** + * Display the cursor element + */ + private showCursor() { + 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. + */ + public scroll(isWrapped?: boolean): void { + 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); } - 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; + /** + * 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. + */ + 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; } - this.refreshStart = this.buffer.y; - this.refreshEnd = this.buffer.y; + const oldYdisp = this.buffer.ydisp; + this.buffer.ydisp = Math.max(Math.min(this.buffer.ydisp + disp, this.buffer.ybase), 0); - // 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); + // No change occurred, don't trigger scroll/refresh + if (oldYdisp === this.buffer.ydisp) { + return; + } - 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; - } -}; + if (!suppressScrollEvent) { + this.emit('scroll', this.buffer.ydisp); + } -/** - * 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)) { + /** + * 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)); + } + + /** + * Scrolls the display of the terminal to the top. + */ + public scrollToTop(): void { + this.scrollDisp(-this.buffer.ydisp); + } + + /** + * Scrolls the display of the terminal to the bottom. + */ + public scrollToBottom(): void { + this.scrollDisp(this.buffer.ybase - this.buffer.ydisp); + } + + /** + * 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; + } + + 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(); + }); + } + } + + 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; + + // 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. + */ + public writeln(data): void { + 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. + */ + 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 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; + } + + /** + * 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); + } + + /** + * 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); + } + + /** + * 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; -}; - -/** - * 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(); + /** + * 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); + } } - 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; + /** + * Gets whether the terminal has an active selection. + */ + public hasSelection() { + return this.selectionManager ? this.selectionManager.hasSelection : false; } - if (result.scrollDisp) { - this.scrollDisp(result.scrollDisp); + /** + * 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 : ''; + } + + /** + * Clears the current terminal selection. + */ + public clearSelection() { + if (this.selectionManager) { + this.selectionManager.clearSelection(); + } + } + + /** + * Selects all text within the terminal. + */ + public selectAll() { + 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. + */ + private keyDown(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; + } + + 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.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); } - 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; - - // 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); - - // 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; - } - - if (this.buffer.x >= x) { - this.buffer.x = x - 1; - } - - this.buffer.scrollTop = 0; - this.buffer.scrollBottom = y - 1; - - 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. + * 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. * - * @event title + * Reference: http://invisible-island.net/xterm/ctlseqs/ctlseqs.html + * @param {KeyboardEvent} ev The keyboard event to be translated to key escape sequence. */ - this.emit('title', title); -}; + 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; + 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; + } - -/** - * 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(); + return result; } - // 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--; + + /** + * Set the G level of the terminal + * @param g + */ + public setgLevel = function(g: number) { + this.glevel = g; + this.charset = this.charsets[g]; } -}; + /** + * 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; + } + } -/** - * 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--; + /** + * 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; + + 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 + */ + private send(data) { + if (!this.queue) { + setTimeout(() => { + this.handler(this.queue); + this.queue = ''; + }, 1); + } + + this.queue += data; + } + + /** + * 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(); + } + + /** + * 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); + } + + /** + * 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); + } + + /** + * 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; + } + + 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; + + // 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); + + // 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; + } + + if (this.buffer.x >= x) { + this.buffer.x = x - 1; + } + + this.buffer.scrollTop = 0; + this.buffer.scrollBottom = y - 1; + + 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. + */ + 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; + } + + 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 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; + } + + /** + * 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. + */ + 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); + } + + /** + * 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); + } + + /** + * 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); + } + + /** + * 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 = []; + + // 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 (let i = 0; i < this.cols; i++) { + line[i] = ch; + } + + return line; + } + + /** + * 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]; + } + + /** + * 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; + } + + // 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. + */ + 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 + */ + + /** + * 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--; + } + } + + /** + * 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--; + } + } + + /** + * 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(); } -}; -/** - * 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(); -}; + /** + * ESC H Tab Set (HTS is 0x88). + */ + 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; + } + + // Expose to InputHandler + // TODO: Revise when truecolor is introduced. + public matchColor(r1, g1, b1) { + var hash = (r1 << 16) | (g1 << 8) | b1; + + 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; + } + + if (diff < ldiff) { + ldiff = diff; + li = i; + } + } + + return matchColorCache[hash] = li; + } +} -/** - * ESC H Tab Set (HTS is 0x88). - */ -Terminal.prototype.tabSet = function() { - this.buffer.tabs[this.buffer.x] = true; -}; + +// 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 */ -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 896b729a..9e4b1e1a 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 d0b2f685..54850ab7 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 00000000..0c2b7319 --- /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 f68baa82..c4c3a267 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 12b2137d..b76cdc4d 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 c675f7a2..c24aa2ef 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 c2173d77..94d58c3e 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 6da3874b..9ec36e0d 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 18fcd000..19f10e7d 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 19f10e7d..509152b9 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 c4c3a267..9f0d8446 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 2101fbc1..19d943b9 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 509152b9..839ad568 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 6d09ea86..7c89da29 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 b4a53a17..d0447451 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 64da3b3f..e119df32 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 1c2fb6b0..bec0af7b 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 839ad568..e1633033 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 740bee3d..a91ab87c 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 d571d9c5..f98fd4c4 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 e1633033..ebb0c687 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 c24aa2ef..98c7626a 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 ebb0c687..79b5972f 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 9e4b1e1a..34c6d94a 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 98c7626a..8851edac 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 79b5972f..a7aabcd7 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 a7aabcd7..6cc8864e 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 b76cdc4d..dc0bc24f 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 158c07c9..95624296 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 e119df32..8f566cd9 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 8851edac..b61c0057 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 f6fb44da..bf594f70 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 6cc8864e..df313ee3 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 fe276935..bd667edb 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 4a96e12a..70b23183 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 439e2085..223a90e7 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 3d34da75..a1768d2d 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 95624296..b6b218df 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 8f566cd9..9376d182 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 b61c0057..875cbac8 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 132ce5f0..414238f9 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 bc4949b1..2323f226 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 bec0af7b..90d413a7 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 165594fd..f706bffc 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 bf594f70..e3770c06 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 df313ee3..b3a27be9 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 bd667edb..4ad4ddec 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 a91ab87c..e5bc3581 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 aa1c1400..ac9a7b87 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 ce09c1be..98072326 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 f98fd4c4..4d4d4aa5 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 12b2137d..bd3645e7 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 e86c098f..4a65dbfe 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 f19a7f28..d463fcb7 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 5aa069e7..92fc0df8 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.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.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(); - } - } - } 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; - } - - if (this.buffer.x >= x) { - this.buffer.x = x - 1; - } - - this.buffer.scrollTop = 0; - this.buffer.scrollBottom = y - 1; + 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 bd3645e7..714151d0 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 2101fbc1..ab814cee 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 4a65dbfe..345191cf 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 64da3b3f..b915f2c0 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 d463fcb7..70a46d41 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 c2173d77..7beafd97 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 6da3874b..b0879442 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 d0b2f685..54850ab7 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 00000000..fbb17261 --- /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 92fc0df8..e9e3465e 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 f68baa82..66cf15c4 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 66cf15c4..5883a8b5 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 e24fbd5b..506032a1 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 58cf8a8e..9a28438d 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 2b4d01d0..91160134 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 506032a1..63058f09 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 f640d424..a39c4dda 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 e3770c06..781a8eda 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 3431df7f..360c2b7e 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 34c6d94a..0753d80e 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 9fc929fe..51caa8fe 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 1e5e2c3b..99bd6d0f 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 78a19351..97c07995 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 70b23183..0adb1719 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 7ae381c8..d61f994a 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 b6b218df..043b7919 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 91160134..5362cae9 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 414238f9..a4ed70db 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 a39c4dda..698cda7e 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 696d12f0..9da84478 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 7c89da29..8c7a914b 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 fd4954e0..68e1b6a7 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 aaa4a0d9..e27e7f7a 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 54850ab7..373d78b0 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 7298f192..5a5d8a78 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 51caa8fe..fcff2dcd 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 0c2b7319..75751865 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 63058f09..55317fbc 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 360c2b7e..c1fd1e7e 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 fcff2dcd..159da055 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 e9e3465e..00000000 --- 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 c1fd1e7e..06a1a316 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 0753d80e..0de78d52 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 06a1a316..cd44e7fb 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 75751865..49e44151 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;