From c3f46e4a6b244b043eda721e8d9714b618897721 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 31 Dec 2016 07:05:06 -0800 Subject: [PATCH 01/33] Fix scroll in tmux with max scrollback I believe this is related to when scrollBottom is not the last row in the viewport Fixes #434 --- src/xterm.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/xterm.js b/src/xterm.js index 1d220652..6fddd111 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -1234,6 +1234,15 @@ Terminal.prototype.showCursor = function() { Terminal.prototype.scroll = function() { var row; + // Make room for the new row in lines + if (this.lines.length === this.lines.maxLength) { + this.lines.trimStart(1); + this.ybase--; + if (this.ydisp !== 0) { + this.ydisp--; + } + } + this.ybase++; // TODO: Why is this done twice? @@ -1248,13 +1257,6 @@ Terminal.prototype.scroll = function() { row -= this.rows - 1 - this.scrollBottom; if (row === this.lines.length) { - // Compensate ybase and ydisp if lines has hit the maximum buffer size - if (this.lines.length === this.lines.maxLength) { - this.ybase--; - if (this.ydisp !== 0) { - this.ydisp--; - } - } // Optimization: pushing is faster than splicing when they amount to the same behavior this.lines.push(this.blankLine()); } else { From 3e1a60717967f2a661e1c1bfe77f9ff06f8debff Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 31 Dec 2016 07:36:56 -0800 Subject: [PATCH 02/33] Convert browser and generic to TS Part of #335 --- src/utils/Browser.js | 22 ---------------------- src/utils/Browser.ts | 22 ++++++++++++++++++++++ src/utils/{Generic.js => Generic.ts} | 2 +- 3 files changed, 23 insertions(+), 23 deletions(-) delete mode 100644 src/utils/Browser.js create mode 100644 src/utils/Browser.ts rename src/utils/{Generic.js => Generic.ts} (88%) diff --git a/src/utils/Browser.js b/src/utils/Browser.js deleted file mode 100644 index cd13e027..00000000 --- a/src/utils/Browser.js +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Attributes and methods to help with identifying the current browser and platform. - * @module xterm/utils/Browser - * @license MIT - */ - -import { contains } from './Generic.js'; - -let isNode = (typeof navigator == 'undefined') ? true : false; -let userAgent = (isNode) ? 'node' : navigator.userAgent; -let platform = (isNode) ? 'node' : navigator.platform; - -export let isFirefox = !!~userAgent.indexOf('Firefox'); -export let isMSIE = !!~userAgent.indexOf('MSIE') || !!~userAgent.indexOf('Trident'); - -// Find the users platform. We use this to interpret the meta key -// and ISO third level shifts. -// http://stackoverflow.com/q/19877924/577598 -export let isMac = contains(['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K'], platform); -export let isIpad = platform === 'iPad'; -export let isIphone = platform === 'iPhone'; -export let isMSWindows = contains(['Windows', 'Win16', 'Win32', 'WinCE'], platform); diff --git a/src/utils/Browser.ts b/src/utils/Browser.ts new file mode 100644 index 00000000..04da698e --- /dev/null +++ b/src/utils/Browser.ts @@ -0,0 +1,22 @@ +/** + * Attributes and methods to help with identifying the current browser and platform. + * @module xterm/utils/Browser + * @license MIT + */ + +import { contains } from './Generic'; + +const isNode = (typeof navigator === 'undefined') ? true : false; +const userAgent = (isNode) ? 'node' : navigator.userAgent; +const platform = (isNode) ? 'node' : navigator.platform; + +export const isFirefox = !!~userAgent.indexOf('Firefox'); +export const isMSIE = !!~userAgent.indexOf('MSIE') || !!~userAgent.indexOf('Trident'); + +// Find the users platform. We use this to interpret the meta key +// and ISO third level shifts. +// http://stackoverflow.com/q/19877924/577598 +export const isMac = contains(['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K'], platform); +export const isIpad = platform === 'iPad'; +export const isIphone = platform === 'iPhone'; +export const isMSWindows = contains(['Windows', 'Win16', 'Win32', 'WinCE'], platform); diff --git a/src/utils/Generic.js b/src/utils/Generic.ts similarity index 88% rename from src/utils/Generic.js rename to src/utils/Generic.ts index 42f876f3..ce09c1be 100644 --- a/src/utils/Generic.js +++ 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 let contains = function(arr, el) { +export function contains(arr: any[], el: any) { return arr.indexOf(el) >= 0; }; From 97feb3321c74b5fdc7aac35eca2bb852800d9120 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 31 Dec 2016 14:06:26 -0800 Subject: [PATCH 03/33] Improve refresh queue Use requestAnimationFrame in addition to a queue to refresh every animation frame but only when a refresh is needed. Fixes #280 Fixes #290 --- src/xterm.js | 98 +++++++++++++++++++++++++--------------------------- 1 file changed, 48 insertions(+), 50 deletions(-) diff --git a/src/xterm.js b/src/xterm.js index 1d220652..79ca633b 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -137,14 +137,8 @@ function Terminal(options) { */ this.y = 0; - /** - * Used to debounce the refresh function - */ - this.isRefreshing = false; - - /** - * Whether there is a full terminal refresh queued - */ + /** A queue of the rows to be refreshed */ + this.refreshRowsQueue = []; this.cursorState = 0; this.cursorHidden = false; @@ -407,7 +401,7 @@ Terminal.prototype.blur = function() { */ Terminal.bindBlur = function (term) { on(term.textarea, 'blur', function (ev) { - term.refresh(term.y, term.y); + term.queueRefresh(term.y, term.y); if (term.sendFocus) { term.send('\x1b[O'); } @@ -585,8 +579,13 @@ Terminal.prototype.open = function(parent) { this.viewport = new Viewport(this, this.viewportElement, this.viewportScrollArea, this.charMeasureElement); - // Draw the screen. - this.refresh(0, this.rows - 1); + // Setup loop that draws to screen + this.queueRefresh(0, this.rows - 1); + function refreshLoop() { + self.refresh(); + window.requestAnimationFrame(refreshLoop); + } + window.requestAnimationFrame(refreshLoop); // Initialize global actions that // need to be taken on the document. @@ -997,6 +996,16 @@ Terminal.flags = { INVISIBLE: 16 } +/** + * Queues a refresh between two rows (inclusive), to be done on next animation + * frame. + * @param {number} start The start row. + * @param {number} end The end row. + */ +Terminal.prototype.queueRefresh = function(start, end) { + this.refreshRowsQueue.push({ start: start, end: end }); +} + /** * Refreshes (re-renders) terminal content within two rows (inclusive) * @@ -1019,44 +1028,33 @@ Terminal.flags = { * @param {number} end The row to end at (between fromRow and terminal's height terminal - 1) * @param {boolean} queue Whether the refresh should ran right now or be queued */ -Terminal.prototype.refresh = function(start, end, queue) { - var self = this; - - // queue defaults to true - queue = (typeof queue == 'undefined') ? true : queue; - - /** - * The refresh queue allows refresh to execute only approximately 30 times a second. For - * commands that pass a significant amount of output to the write function, this prevents the - * terminal from maxing out the CPU and making the UI unresponsive. While commands can still - * run beyond what they do on the terminal, it is far better with a debounce in place as - * every single terminal manipulation does not need to be constructed in the DOM. - * - * A side-effect of this is that it makes ^C to interrupt a process seem more responsive. - */ - if (queue) { - // If refresh should be queued, order the refresh and return. - if (this._refreshIsQueued) { - // If a refresh has already been queued, just order a full refresh next - this._fullRefreshNext = true; - } else { - setTimeout(function () { - self.refresh(start, end, false); - }, 34) - this._refreshIsQueued = true; - } +Terminal.prototype.refresh = function() { + if (this.refreshRowsQueue.length === 0) { + // Don't refresh if there were no row changes return; } - // If refresh should be run right now (not be queued), release the lock - this._refreshIsQueued = false; - - // If multiple refreshes were requested, make a full refresh. - if (this._fullRefreshNext) { + var start; + var end; + if (this.refreshRowsQueue.length > 4) { + // Just do a full refresh when 5+ refreshes are queued start = 0; end = this.rows - 1; - this._fullRefreshNext = false // reset lock + } else { + // Get start and end rows that need refreshing + start = this.refreshRowsQueue[0].start; + end = this.refreshRowsQueue[0].end; + for (var i = 1; i < this.refreshRowsQueue.length; i++) { + if (this.refreshRowsQueue[i].start < start) { + start = this.refreshRowsQueue[i].start; + } + if (this.refreshRowsQueue[i].end > end) { + end = this.refreshRowsQueue[i].end; + } + } } + this.refreshRowsQueue = []; + var self = this; var x, y, i, line, out, ch, ch_width, width, data, attr, bg, fg, flags, row, parent, focused = document.activeElement; @@ -1224,7 +1222,7 @@ Terminal.prototype.refresh = function(start, end, queue) { Terminal.prototype.showCursor = function() { if (!this.cursorState) { this.cursorState = 1; - this.refresh(this.y, this.y); + this.queueRefresh(this.y, this.y); } }; @@ -1311,7 +1309,7 @@ Terminal.prototype.scrollDisp = function(disp, suppressScrollEvent) { this.emit('scroll', this.ydisp); } - this.refresh(0, this.rows - 1); + this.queueRefresh(0, this.rows - 1); }; /** @@ -2379,7 +2377,7 @@ Terminal.prototype.write = function(data) { } this.updateRange(this.y); - this.refresh(this.refreshStart, this.refreshEnd); + this.queueRefresh(this.refreshStart, this.refreshEnd); }; /** @@ -2945,7 +2943,7 @@ Terminal.prototype.resize = function(x, y) { this.scrollTop = 0; this.scrollBottom = y - 1; - this.refresh(0, this.rows - 1); + this.queueRefresh(0, this.rows - 1); this.normal = null; @@ -3074,7 +3072,7 @@ Terminal.prototype.clear = function() { for (var i = 1; i < this.rows; i++) { this.lines.push(this.blankLine()); } - this.refresh(0, this.rows - 1); + this.queueRefresh(0, this.rows - 1); this.emit('scroll', this.ydisp); }; @@ -3205,7 +3203,7 @@ Terminal.prototype.reset = function() { var customKeydownHandler = this.customKeydownHandler; Terminal.call(this, this.options); this.customKeydownHandler = customKeydownHandler; - this.refresh(0, this.rows - 1); + this.queueRefresh(0, this.rows - 1); this.viewport.syncScrollArea(); }; @@ -4324,7 +4322,7 @@ Terminal.prototype.resetMode = function(params) { // this.x = this.savedX; // this.y = this.savedY; // } - this.refresh(0, this.rows - 1); + this.queueRefresh(0, this.rows - 1); this.viewport.syncScrollArea(); this.showCursor(); } From 7234bfb6eb637fe3ed388db5b0a78949de7f06d7 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 31 Dec 2016 14:58:17 -0800 Subject: [PATCH 04/33] Move row evaluation into refreshLoop This allows the refresh public API to still function --- src/xterm.js | 66 +++++++++++++++++++++++++++------------------------- 1 file changed, 34 insertions(+), 32 deletions(-) diff --git a/src/xterm.js b/src/xterm.js index 79ca633b..cd304186 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -581,11 +581,7 @@ Terminal.prototype.open = function(parent) { // Setup loop that draws to screen this.queueRefresh(0, this.rows - 1); - function refreshLoop() { - self.refresh(); - window.requestAnimationFrame(refreshLoop); - } - window.requestAnimationFrame(refreshLoop); + this.refreshLoop(); // Initialize global actions that // need to be taken on the document. @@ -1006,6 +1002,38 @@ Terminal.prototype.queueRefresh = function(start, end) { this.refreshRowsQueue.push({ start: start, end: end }); } +/** + * Performs the refresh loop callback, calling refresh only if a refresh is + * necessary before queueing up the next one. + */ +Terminal.prototype.refreshLoop = function() { + // Don't refresh if there were no row changes + if (this.refreshRowsQueue.length > 0) { + var start; + var end; + if (this.refreshRowsQueue.length > 4) { + // Just do a full refresh when 5+ refreshes are queued + start = 0; + end = this.rows - 1; + } else { + // Get start and end rows that need refreshing + start = this.refreshRowsQueue[0].start; + end = this.refreshRowsQueue[0].end; + for (var i = 1; i < this.refreshRowsQueue.length; i++) { + if (this.refreshRowsQueue[i].start < start) { + start = this.refreshRowsQueue[i].start; + } + if (this.refreshRowsQueue[i].end > end) { + end = this.refreshRowsQueue[i].end; + } + } + } + this.refreshRowsQueue = []; + this.refresh(start, end); + } + window.requestAnimationFrame(this.refreshLoop.bind(this)); +} + /** * Refreshes (re-renders) terminal content within two rows (inclusive) * @@ -1026,34 +1054,8 @@ Terminal.prototype.queueRefresh = function(start, end) { * * @param {number} start The row to start from (between 0 and terminal's height terminal - 1) * @param {number} end The row to end at (between fromRow and terminal's height terminal - 1) - * @param {boolean} queue Whether the refresh should ran right now or be queued */ -Terminal.prototype.refresh = function() { - if (this.refreshRowsQueue.length === 0) { - // Don't refresh if there were no row changes - return; - } - - var start; - var end; - if (this.refreshRowsQueue.length > 4) { - // Just do a full refresh when 5+ refreshes are queued - start = 0; - end = this.rows - 1; - } else { - // Get start and end rows that need refreshing - start = this.refreshRowsQueue[0].start; - end = this.refreshRowsQueue[0].end; - for (var i = 1; i < this.refreshRowsQueue.length; i++) { - if (this.refreshRowsQueue[i].start < start) { - start = this.refreshRowsQueue[i].start; - } - if (this.refreshRowsQueue[i].end > end) { - end = this.refreshRowsQueue[i].end; - } - } - } - this.refreshRowsQueue = []; +Terminal.prototype.refresh = function(start, end) { var self = this; var x, y, i, line, out, ch, ch_width, width, data, attr, bg, fg, flags, row, parent, focused = document.activeElement; From 188e197e20d3c678fc6f5d478e17699caea01128 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 31 Dec 2016 15:19:54 -0800 Subject: [PATCH 05/33] Wrap wide chars in a span/class Part of #439 --- src/xterm.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/xterm.js b/src/xterm.js index 1d220652..fcb60bf5 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -1182,6 +1182,9 @@ Terminal.prototype.refresh = function(start, end, queue) { } } + if (ch_width === 2) { + out += ''; + } switch (ch) { case '&': out += '&'; @@ -1200,6 +1203,9 @@ Terminal.prototype.refresh = function(start, end, queue) { } break; } + if (ch_width === 2) { + out += ''; + } attr = data; } From 4f18d842f9ee355387628b9f9f4cbb1b0ea02e9a Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 31 Dec 2016 15:46:17 -0800 Subject: [PATCH 06/33] Add CharMeasure util class --- src/utils/CharMeasure.ts | 52 ++++++++++++++++++++++++++++++++++++++++ src/xterm.js | 6 +++++ 2 files changed, 58 insertions(+) create mode 100644 src/utils/CharMeasure.ts diff --git a/src/utils/CharMeasure.ts b/src/utils/CharMeasure.ts new file mode 100644 index 00000000..60684a2c --- /dev/null +++ b/src/utils/CharMeasure.ts @@ -0,0 +1,52 @@ +/** + * @module xterm/utils/CharMeasure + * @license MIT + */ + +import { EventEmitter } from '../EventEmitter.js'; + +/** + * Utility class that measures the size of a character. + */ +export class CharMeasure extends EventEmitter { + private _parentElement: HTMLElement; + private _measureElement: HTMLElement; + private _width: number; + private _height: number; + + constructor(parentElement: HTMLElement) { + super(); + this._parentElement = parentElement; + } + + public get width(): number { + return this._width; + } + + public get height(): number { + return this._height; + } + + public measure(): void { + const oldWidth = this._width; + const oldHeight = this._height; + + if (!this._measureElement) { + this._measureElement = document.createElement('span'); + this._measureElement.style.position = 'absolute'; + this._measureElement.style.top = '0'; + this._measureElement.style.left = '-9999em'; + this._measureElement.textContent = 'W'; + } + + this._parentElement.appendChild(this._measureElement); + const geometry = this._measureElement.getBoundingClientRect(); + this._width = geometry.width; + this._height = geometry.height; + this._parentElement.removeChild(this._measureElement); + + if (this._width !== oldWidth || this._height !== oldHeight) { + this.emit('charsizechanged'); + } + } +} diff --git a/src/xterm.js b/src/xterm.js index fcb60bf5..826531e2 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -15,6 +15,7 @@ import { EventEmitter } from './EventEmitter.js'; import { Viewport } from './Viewport.js'; import { rightClickHandler, pasteHandler, copyHandler } from './handlers/Clipboard.js'; import { CircularList } from './utils/CircularList.js'; +import { CharMeasure } from './utils/CharMeasure.js'; import * as Browser from './utils/Browser'; import * as Keyboard from './utils/Keyboard'; @@ -583,6 +584,9 @@ Terminal.prototype.open = function(parent) { } this.parent.appendChild(this.element); + this.charMeasure = new CharMeasure(this.rowContainer); + this.charMeasure.measure(); + this.viewport = new Viewport(this, this.viewportElement, this.viewportScrollArea, this.charMeasureElement); // Draw the screen. @@ -2951,6 +2955,8 @@ Terminal.prototype.resize = function(x, y) { this.scrollTop = 0; this.scrollBottom = y - 1; + this.charMeasure.measure(); + this.refresh(0, this.rows - 1); this.normal = null; From 74483fb2948bbfcb42e6d22b6a3f764144195c95 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 31 Dec 2016 15:56:53 -0800 Subject: [PATCH 07/33] Use CharMeasure in Viewport and to style wide chars Fixes #439 --- src/Viewport.ts | 25 ++++++++++++------------- src/xterm.css | 4 ++++ src/xterm.js | 18 +++++++++++++----- 3 files changed, 29 insertions(+), 18 deletions(-) diff --git a/src/Viewport.ts b/src/Viewport.ts index 3aa1319f..013ae65a 100644 --- a/src/Viewport.ts +++ b/src/Viewport.ts @@ -3,6 +3,7 @@ */ import { ITerminal } from './Interfaces'; +import { CharMeasure } from './utils/CharMeasure'; /** * Represents the viewport of a terminal, the visible area within the larger buffer of output. @@ -24,7 +25,7 @@ export class Viewport { private terminal: ITerminal, private viewportElement: HTMLElement, private scrollArea: HTMLElement, - private charMeasureElement: HTMLElement + private charMeasure: CharMeasure ) { this.currentRowHeight = 0; this.lastRecordedBufferLength = 0; @@ -43,21 +44,20 @@ export class Viewport { * @param charSize A character size measurement bounding rect object, if it doesn't exist it will * be created. */ - private refresh(charSize?: ClientRect): void { - var size = charSize || this.charMeasureElement.getBoundingClientRect(); - if (size.height > 0) { - var rowHeightChanged = size.height !== this.currentRowHeight; + private refresh(): void { + if (this.charMeasure.height > 0) { + var rowHeightChanged = this.charMeasure.height !== this.currentRowHeight; if (rowHeightChanged) { - this.currentRowHeight = size.height; - this.viewportElement.style.lineHeight = size.height + 'px'; - this.terminal.rowContainer.style.lineHeight = size.height + 'px'; + this.currentRowHeight = this.charMeasure.height; + this.viewportElement.style.lineHeight = this.charMeasure.height + 'px'; + this.terminal.rowContainer.style.lineHeight = this.charMeasure.height + 'px'; } var viewportHeightChanged = this.lastRecordedViewportHeight !== this.terminal.rows; if (rowHeightChanged || viewportHeightChanged) { this.lastRecordedViewportHeight = this.terminal.rows; - this.viewportElement.style.height = size.height * this.terminal.rows + 'px'; + this.viewportElement.style.height = this.charMeasure.height * this.terminal.rows + 'px'; } - this.scrollArea.style.height = (size.height * this.lastRecordedBufferLength) + 'px'; + this.scrollArea.style.height = (this.charMeasure.height * this.lastRecordedBufferLength) + 'px'; } } @@ -74,9 +74,8 @@ export class Viewport { this.refresh(); } else { // If size has changed, refresh viewport - var size = this.charMeasureElement.getBoundingClientRect(); - if (size.height !== this.currentRowHeight) { - this.refresh(size); + if (this.charMeasure.height !== this.currentRowHeight) { + this.refresh(); } } diff --git a/src/xterm.css b/src/xterm.css index 4877f86e..3f4f4bbe 100644 --- a/src/xterm.css +++ b/src/xterm.css @@ -116,6 +116,10 @@ overflow-y: scroll; } +.terminal .xterm-wide-char { + display: inline-block; +} + .terminal .xterm-rows { position: absolute; left: 0; diff --git a/src/xterm.js b/src/xterm.js index 826531e2..999dc08b 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -574,10 +574,8 @@ Terminal.prototype.open = function(parent) { this.compositionHelper = new CompositionHelper(this.textarea, this.compositionView, this); this.helperContainer.appendChild(this.compositionView); - this.charMeasureElement = document.createElement('div'); - this.charMeasureElement.classList.add('xterm-char-measure-element'); - this.charMeasureElement.innerHTML = 'W'; - this.helperContainer.appendChild(this.charMeasureElement); + this.charSizeStyleElement = document.createElement('style'); + this.helperContainer.appendChild(this.charSizeStyleElement); for (; i < this.rows; i++) { this.insertRow(); @@ -585,9 +583,12 @@ Terminal.prototype.open = function(parent) { this.parent.appendChild(this.element); this.charMeasure = new CharMeasure(this.rowContainer); + this.charMeasure.on('charsizechanged', function () { + self.updateCharSizeCSS(); + }); this.charMeasure.measure(); - this.viewport = new Viewport(this, this.viewportElement, this.viewportScrollArea, this.charMeasureElement); + this.viewport = new Viewport(this, this.viewportElement, this.viewportScrollArea, this.charMeasure); // Draw the screen. this.refresh(0, this.rows - 1); @@ -645,6 +646,13 @@ Terminal.loadAddon = function(addon, callback) { } }; +/** + * Updates the helper CSS class with any changes necessary after the terminal's + * character width has been changed. + */ +Terminal.prototype.updateCharSizeCSS = function() { + this.charSizeStyleElement.textContent = '.xterm-wide-char{width:' + (this.charMeasure.width * 2) + 'px;}'; +} /** * XTerm mouse events From 07a8b8f4f77d85c2a4f3b07a6ab7f730858abc9e Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 31 Dec 2016 18:06:49 -0800 Subject: [PATCH 08/33] Fix tests --- src/Viewport.test.ts | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/src/Viewport.test.ts b/src/Viewport.test.ts index 5b106b43..4fa77ec0 100644 --- a/src/Viewport.test.ts +++ b/src/Viewport.test.ts @@ -2,11 +2,11 @@ import { assert } from 'chai'; import { Viewport } from './Viewport'; describe('Viewport', () => { - var terminal; - var viewportElement; - var charMeasureElement; - var viewport; - var scrollAreaElement; + let terminal; + let viewportElement; + let charMeasure; + let viewport; + let scrollAreaElement; const CHARACTER_HEIGHT = 10; @@ -34,21 +34,17 @@ describe('Viewport', () => { height: 0 } }; - charMeasureElement = { - getBoundingClientRect: () => { - return { width: null, height: CHARACTER_HEIGHT }; - } + charMeasure = { + height: CHARACTER_HEIGHT }; - viewport = new Viewport(terminal, viewportElement, scrollAreaElement, charMeasureElement); + viewport = new Viewport(terminal, viewportElement, scrollAreaElement, charMeasure); }); describe('refresh', () => { it('should set the line-height of the terminal', () => { assert.equal(viewportElement.style.lineHeight, CHARACTER_HEIGHT + 'px'); assert.equal(terminal.rowContainer.style.lineHeight, CHARACTER_HEIGHT + 'px'); - charMeasureElement.getBoundingClientRect = () => { - return { width: null, height: 1 }; - }; + charMeasure.height = 1; viewport.refresh(); assert.equal(viewportElement.style.lineHeight, '1px'); assert.equal(terminal.rowContainer.style.lineHeight, '1px'); @@ -59,9 +55,7 @@ describe('Viewport', () => { terminal.rows = 1; viewport.refresh(); assert.equal(viewportElement.style.height, 1 * CHARACTER_HEIGHT + 'px'); - charMeasureElement.getBoundingClientRect = () => { - return { width: null, height: 20 }; - }; + charMeasure.height = 20; viewport.refresh(); assert.equal(viewportElement.style.height, 20 + 'px'); }); From 6ffb8f545b6a8f21883ebaadcb48f81e23e27ad0 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 3 Jan 2017 11:18:38 -0800 Subject: [PATCH 09/33] Rate limit Viewport.refresh This prevents 1000 scroll events from firing when the buffer is not full Fixes #444 --- src/Viewport.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/Viewport.ts b/src/Viewport.ts index 3aa1319f..b266dd3e 100644 --- a/src/Viewport.ts +++ b/src/Viewport.ts @@ -12,6 +12,7 @@ export class Viewport { private currentRowHeight: number; private lastRecordedBufferLength: number; private lastRecordedViewportHeight: number; + private isRefreshQueued: boolean; /** * Creates a new Viewport. @@ -29,12 +30,22 @@ export class Viewport { this.currentRowHeight = 0; this.lastRecordedBufferLength = 0; this.lastRecordedViewportHeight = 0; + this.isRefreshQueued = false; this.terminal.on('scroll', this.syncScrollArea.bind(this)); this.terminal.on('resize', this.syncScrollArea.bind(this)); this.viewportElement.addEventListener('scroll', this.onScroll.bind(this)); this.syncScrollArea(); + this.refreshLoop(); + } + + private refreshLoop(): void { + if (this.isRefreshQueued) { + this.refresh(); + this.isRefreshQueued = false; + } + window.requestAnimationFrame(this.refreshLoop.bind(this)); } /** @@ -68,15 +79,15 @@ export class Viewport { if (this.lastRecordedBufferLength !== this.terminal.lines.length) { // If buffer height changed this.lastRecordedBufferLength = this.terminal.lines.length; - this.refresh(); + this.isRefreshQueued = true; } else if (this.lastRecordedViewportHeight !== this.terminal.rows) { // If viewport height changed - this.refresh(); + this.isRefreshQueued = true; } else { // If size has changed, refresh viewport var size = this.charMeasureElement.getBoundingClientRect(); if (size.height !== this.currentRowHeight) { - this.refresh(size); + this.isRefreshQueued = true; } } From efdf37b887221d2b9a7951cffa59c9e5323b1022 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 3 Jan 2017 11:23:36 -0800 Subject: [PATCH 10/33] jsdoc --- src/Viewport.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Viewport.ts b/src/Viewport.ts index b266dd3e..32a71dde 100644 --- a/src/Viewport.ts +++ b/src/Viewport.ts @@ -40,6 +40,9 @@ export class Viewport { this.refreshLoop(); } + /** + * Queues a refresh to be done on next animation frame. + */ private refreshLoop(): void { if (this.isRefreshQueued) { this.refresh(); From d2ef19dffe8f9c1b1e4d26d4aa41fed1029fae5f Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 3 Jan 2017 11:26:59 -0800 Subject: [PATCH 11/33] Remove var usage from Viewport Part of #335 --- src/Viewport.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Viewport.ts b/src/Viewport.ts index 3aa1319f..b8d8cd6d 100644 --- a/src/Viewport.ts +++ b/src/Viewport.ts @@ -46,13 +46,13 @@ export class Viewport { private refresh(charSize?: ClientRect): void { var size = charSize || this.charMeasureElement.getBoundingClientRect(); if (size.height > 0) { - var rowHeightChanged = size.height !== this.currentRowHeight; + const rowHeightChanged = size.height !== this.currentRowHeight; if (rowHeightChanged) { this.currentRowHeight = size.height; this.viewportElement.style.lineHeight = size.height + 'px'; this.terminal.rowContainer.style.lineHeight = size.height + 'px'; } - var viewportHeightChanged = this.lastRecordedViewportHeight !== this.terminal.rows; + const viewportHeightChanged = this.lastRecordedViewportHeight !== this.terminal.rows; if (rowHeightChanged || viewportHeightChanged) { this.lastRecordedViewportHeight = this.terminal.rows; this.viewportElement.style.height = size.height * this.terminal.rows + 'px'; @@ -74,14 +74,14 @@ export class Viewport { this.refresh(); } else { // If size has changed, refresh viewport - var size = this.charMeasureElement.getBoundingClientRect(); + const size = this.charMeasureElement.getBoundingClientRect(); if (size.height !== this.currentRowHeight) { this.refresh(size); } } // Sync scrollTop - var scrollTop = this.terminal.ydisp * this.currentRowHeight; + const scrollTop = this.terminal.ydisp * this.currentRowHeight; if (this.viewportElement.scrollTop !== scrollTop) { this.viewportElement.scrollTop = scrollTop; } @@ -93,8 +93,8 @@ export class Viewport { * @param ev The scroll event. */ private onScroll(ev: Event) { - var newRow = Math.round(this.viewportElement.scrollTop / this.currentRowHeight); - var diff = newRow - this.terminal.ydisp; + const newRow = Math.round(this.viewportElement.scrollTop / this.currentRowHeight); + const diff = newRow - this.terminal.ydisp; this.terminal.scrollDisp(diff, true); } @@ -110,7 +110,7 @@ export class Viewport { return; } // Fallback to WheelEvent.DOM_DELTA_PIXEL - var multiplier = 1; + let multiplier = 1; if (ev.deltaMode === WheelEvent.DOM_DELTA_LINE) { multiplier = this.currentRowHeight; } else if (ev.deltaMode === WheelEvent.DOM_DELTA_PAGE) { From b9d374affa5333786b9665b94e6196e6d1ee5230 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 3 Jan 2017 11:55:54 -0800 Subject: [PATCH 12/33] Add XON/XOFF and eparate write from processing Part of #425 --- demo/app.js | 3 +++ src/xterm.js | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/demo/app.js b/demo/app.js index a4122ace..b6bde384 100644 --- a/demo/app.js +++ b/demo/app.js @@ -60,6 +60,9 @@ app.ws('/terminals/:pid', function (ws, req) { term.on('data', function(data) { try { + // XOFF - stop pty pipe + // XON will be triggered by emulator before processing data chunk + term.write('\x13'); ws.send(data); } catch (ex) { // The WebSocket is not open, ignore diff --git a/src/xterm.js b/src/xterm.js index cd304186..a29412e6 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -196,6 +196,11 @@ function Terminal(options) { this.prefix = ''; this.postfix = ''; + // user input states + this.writeBuffer = []; + this.writeInProgress = false; + this.user_xoff = false; // user pressed XOFF + // leftover surrogate high from previous write invocation this.surrogate_high = ''; @@ -1341,8 +1346,27 @@ Terminal.prototype.scrollToBottom = function() { * @param {string} text The text to write to the terminal. */ Terminal.prototype.write = function(data) { + this.writeBuffer.push(data); + if (!this.writeInProgress) { + // 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(this.writeBuffer.shift())); + } +} + +Terminal.prototype.innerWrite = function(data) { var l = data.length, i = 0, j, cs, ch, code, low, ch_width, row; + // TODO: Need to have another buffer where data is held where write can grab lines from + // When this hits a certain threshold it should send this.write('\x13') + + // XON - about to process data, thus we can get more + // dont lift XOFF if user pressed it + if (!this.user_xoff) { + this.send('\x11'); + } + this.refreshStart = this.y; this.refreshEnd = this.y; @@ -2380,6 +2404,12 @@ Terminal.prototype.write = function(data) { this.updateRange(this.y); this.queueRefresh(this.refreshStart, this.refreshEnd); + + if (this.writeBuffer.length > 0) { + this.innerWrite(this.writeBuffer.shift()); + } else { + this.writeInProgress = false; + } }; /** @@ -2423,6 +2453,12 @@ Terminal.prototype.keyDown = function(ev) { var self = this; var result = this.evaluateKeyEscapeSequence(ev); + if (result.key === '\x13') { // XOFF + this.user_xoff = true; + } else if (result.key === '\x11') { // XON + this.user_xoff = false; + } + if (result.scrollDisp) { this.scrollDisp(result.scrollDisp); return this.cancel(ev, true); @@ -2728,6 +2764,7 @@ Terminal.prototype.evaluateKeyEscapeSequence = function(ev) { } break; } + return result; }; From 6b8c43ed7ffaf1f6e75e7523d84c5150e6f127c8 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 3 Jan 2017 12:58:36 -0800 Subject: [PATCH 13/33] Send \x13 when a write buffer threadhold is reached --- demo/app.js | 3 --- src/xterm.js | 28 +++++++++++++++++++++++----- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/demo/app.js b/demo/app.js index b6bde384..a4122ace 100644 --- a/demo/app.js +++ b/demo/app.js @@ -60,9 +60,6 @@ app.ws('/terminals/:pid', function (ws, req) { term.on('data', function(data) { try { - // XOFF - stop pty pipe - // XON will be triggered by emulator before processing data chunk - term.write('\x13'); ws.send(data); } catch (ex) { // The WebSocket is not open, ignore diff --git a/src/xterm.js b/src/xterm.js index a29412e6..58392ba1 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -1347,24 +1347,38 @@ Terminal.prototype.scrollToBottom = function() { */ Terminal.prototype.write = function(data) { this.writeBuffer.push(data); - if (!this.writeInProgress) { + + // Pause pty process if the write buffer becomes too large so xterm.js can catch up + if (this.writeBuffer.length > 1000 && !this.user_xoff) { + // XOFF - stop pty pipe + // XON will be triggered by emulator before processing data chunk + this.send('\x13'); + } + + 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(this.writeBuffer.shift())); + var self = this; + setTimeout(function () { + self.innerWrite(self.writeBuffer.shift()); + }); } } Terminal.prototype.innerWrite = function(data) { var l = data.length, i = 0, j, cs, ch, code, low, ch_width, row; - +console.log('writeBuffer length: ' + this.writeBuffer.length); // TODO: Need to have another buffer where data is held where write can grab lines from // When this hits a certain threshold it should send this.write('\x13') // XON - about to process data, thus we can get more // dont lift XOFF if user pressed it if (!this.user_xoff) { - this.send('\x11'); + // Resume pty process to get more data + if (this.writeBuffer.length < 200) { + this.send('\x11'); + } } this.refreshStart = this.y; @@ -2406,7 +2420,11 @@ Terminal.prototype.innerWrite = function(data) { this.queueRefresh(this.refreshStart, this.refreshEnd); if (this.writeBuffer.length > 0) { - this.innerWrite(this.writeBuffer.shift()); + var self = this; + // Start a new async innerWrite to prevent a stack overflow + setTimeout(function () { + self.innerWrite(self.writeBuffer.shift()); + }); } else { this.writeInProgress = false; } From e66b1c57049253c5e4d7abbf17ef55b67bf4e2e2 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 3 Jan 2017 15:26:41 -0800 Subject: [PATCH 14/33] Add write buffer pause and refresh frame skip --- src/xterm.js | 104 ++++++++++++++++++++++++++++++++++----------------- 1 file changed, 69 insertions(+), 35 deletions(-) diff --git a/src/xterm.js b/src/xterm.js index 58392ba1..5bfa50a6 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -37,6 +37,19 @@ var document = (typeof window != 'undefined') ? window.document : null; */ var normal = 0, escaped = 1, csi = 2, osc = 3, charset = 4, dcs = 5, ignore = 6; +/** + * 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 = 0; + +/** + * The maximum number of refresh frames to skip when the write buffer is non- + * empty. + */ +var MAX_REFRESH_FRAME_SKIP = 6; + /** * Terminal */ @@ -199,7 +212,18 @@ function Terminal(options) { // user input states this.writeBuffer = []; this.writeInProgress = false; - this.user_xoff = false; // user pressed XOFF + this.refreshFramesSkipped = 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. + */ + 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 = ''; @@ -1014,27 +1038,36 @@ Terminal.prototype.queueRefresh = function(start, end) { Terminal.prototype.refreshLoop = function() { // Don't refresh if there were no row changes if (this.refreshRowsQueue.length > 0) { - var start; - var end; - if (this.refreshRowsQueue.length > 4) { - // Just do a full refresh when 5+ refreshes are queued - start = 0; - end = this.rows - 1; - } else { - // Get start and end rows that need refreshing - start = this.refreshRowsQueue[0].start; - end = this.refreshRowsQueue[0].end; - for (var i = 1; i < this.refreshRowsQueue.length; i++) { - if (this.refreshRowsQueue[i].start < start) { - start = this.refreshRowsQueue[i].start; - } - if (this.refreshRowsQueue[i].end > end) { - end = this.refreshRowsQueue[i].end; + // Skip MAX_REFRESH_FRAME_SKIP frames if the writeBuffer is non-empty as it + // will need to be immediately refreshed anyway. This saves a lot of + // rendering time as the viewport DOM does not need to be refreshed, no + // scroll events, no layouts, etc. + var skipFrame = this.writeBuffer.length > 0 && this.refreshFramesSkipped++ <= MAX_REFRESH_FRAME_SKIP; + + if (!skipFrame) { + this.refreshFramesSkipped = 0; + var start; + var end; + if (this.refreshRowsQueue.length > 4) { + // Just do a full refresh when 5+ refreshes are queued + start = 0; + end = this.rows - 1; + } else { + // Get start and end rows that need refreshing + start = this.refreshRowsQueue[0].start; + end = this.refreshRowsQueue[0].end; + for (var i = 1; i < this.refreshRowsQueue.length; i++) { + if (this.refreshRowsQueue[i].start < start) { + start = this.refreshRowsQueue[i].start; + } + if (this.refreshRowsQueue[i].end > end) { + end = this.refreshRowsQueue[i].end; + } } } + this.refreshRowsQueue = []; + this.refresh(start, end); } - this.refreshRowsQueue = []; - this.refresh(start, end); } window.requestAnimationFrame(this.refreshLoop.bind(this)); } @@ -1348,11 +1381,14 @@ Terminal.prototype.scrollToBottom = function() { Terminal.prototype.write = function(data) { this.writeBuffer.push(data); - // Pause pty process if the write buffer becomes too large so xterm.js can catch up - if (this.writeBuffer.length > 1000 && !this.user_xoff) { + // 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.xoffSentToCatchUp && this.writeBuffer.length > WRITE_BUFFER_PAUSE_THRESHOLD) { // XOFF - stop pty pipe // XON will be triggered by emulator before processing data chunk this.send('\x13'); + this.xoffSentToCatchUp = true; } if (!this.writeInProgress && this.writeBuffer.length > 0) { @@ -1368,17 +1404,12 @@ Terminal.prototype.write = function(data) { Terminal.prototype.innerWrite = function(data) { var l = data.length, i = 0, j, cs, ch, code, low, ch_width, row; -console.log('writeBuffer length: ' + this.writeBuffer.length); - // TODO: Need to have another buffer where data is held where write can grab lines from - // When this hits a certain threshold it should send this.write('\x13') - // XON - about to process data, thus we can get more - // dont lift XOFF if user pressed it - if (!this.user_xoff) { - // Resume pty process to get more data - if (this.writeBuffer.length < 200) { - this.send('\x11'); - } + // 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 && this.writeBuffer.length === 0) { + this.send('\x11'); + this.xoffSentToCatchUp = false; } this.refreshStart = this.y; @@ -2421,10 +2452,13 @@ console.log('writeBuffer length: ' + this.writeBuffer.length); if (this.writeBuffer.length > 0) { var self = this; + +// TODO: async makes this too slow, need to change to iterative to prevent potential stack overflow + // Start a new async innerWrite to prevent a stack overflow - setTimeout(function () { + //setTimeout(function () { self.innerWrite(self.writeBuffer.shift()); - }); + //}); } else { this.writeInProgress = false; } @@ -2472,9 +2506,9 @@ Terminal.prototype.keyDown = function(ev) { var result = this.evaluateKeyEscapeSequence(ev); if (result.key === '\x13') { // XOFF - this.user_xoff = true; + this.writeStopped = true; } else if (result.key === '\x11') { // XON - this.user_xoff = false; + this.writeStopped = false; } if (result.scrollDisp) { From dc5efa886a11d61388d06b46f0bd6f9d5e28c31d Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 3 Jan 2017 15:29:23 -0800 Subject: [PATCH 15/33] Make innerWrite iterative --- src/xterm.js | 1935 +++++++++++++++++++++++++------------------------- 1 file changed, 963 insertions(+), 972 deletions(-) diff --git a/src/xterm.js b/src/xterm.js index 5bfa50a6..0849775d 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -1397,1071 +1397,1062 @@ Terminal.prototype.write = function(data) { // Kick off an async innerWrite so more writes can come in while processing data var self = this; setTimeout(function () { - self.innerWrite(self.writeBuffer.shift()); + self.innerWrite(); }); } } -Terminal.prototype.innerWrite = function(data) { - var l = data.length, i = 0, j, cs, ch, code, low, ch_width, row; +Terminal.prototype.innerWrite = function() { + while (this.writeBuffer.length > 0) { + var data = this.writeBuffer.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 && this.writeBuffer.length === 0) { - this.send('\x11'); - this.xoffSentToCatchUp = false; - } - - this.refreshStart = this.y; - this.refreshEnd = this.y; - - // apply leftover surrogate high from last write - if (this.surrogate_high) { - data = this.surrogate_high + data; - this.surrogate_high = ''; - } - - for (; i < l; i++) { - ch = data[i]; - - // FIXME: higher chars than 0xa0 are not allowed in escape sequences - // --> maybe move to default - code = data.charCodeAt(i); - if (0xD800 <= code && code <= 0xDBFF) { - // we got a surrogate high - // get surrogate low (next 2 bytes) - low = data.charCodeAt(i+1); - if (isNaN(low)) { - // end of data stream, save surrogate high - this.surrogate_high = ch; - continue; - } - code = ((code - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000; - ch += data.charAt(i+1); + // 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 && this.writeBuffer.length === 0) { + this.send('\x11'); + this.xoffSentToCatchUp = false; } - // surrogate low - already handled above - if (0xDC00 <= code && code <= 0xDFFF) - continue; - switch (this.state) { - case normal: - switch (ch) { - case '\x07': - this.bell(); - break; - // '\n', '\v', '\f' - case '\n': - case '\x0b': - case '\x0c': - if (this.convertEol) { + this.refreshStart = this.y; + this.refreshEnd = this.y; + + // apply leftover surrogate high from last write + if (this.surrogate_high) { + data = this.surrogate_high + data; + this.surrogate_high = ''; + } + + for (; i < l; i++) { + ch = data[i]; + + // FIXME: higher chars than 0xa0 are not allowed in escape sequences + // --> maybe move to default + code = data.charCodeAt(i); + if (0xD800 <= code && code <= 0xDBFF) { + // we got a surrogate high + // get surrogate low (next 2 bytes) + low = data.charCodeAt(i+1); + if (isNaN(low)) { + // end of data stream, save surrogate high + this.surrogate_high = ch; + continue; + } + code = ((code - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000; + ch += data.charAt(i+1); + } + // surrogate low - already handled above + if (0xDC00 <= code && code <= 0xDFFF) + continue; + switch (this.state) { + case normal: + switch (ch) { + case '\x07': + this.bell(); + break; + + // '\n', '\v', '\f' + case '\n': + case '\x0b': + case '\x0c': + if (this.convertEol) { + this.x = 0; + } + this.y++; + if (this.y > this.scrollBottom) { + this.y--; + this.scroll(); + } + break; + + // '\r' + case '\r': this.x = 0; - } - this.y++; - if (this.y > this.scrollBottom) { - this.y--; - this.scroll(); - } - break; + break; - // '\r' - case '\r': - this.x = 0; - break; - - // '\b' - case '\x08': - if (this.x > 0) { - this.x--; - } - break; - - // '\t' - case '\t': - this.x = this.nextStop(); - break; - - // shift out - case '\x0e': - this.setgLevel(1); - break; - - // shift in - case '\x0f': - this.setgLevel(0); - break; - - // '\e' - case '\x1b': - this.state = escaped; - break; - - default: - // ' ' - // calculate print space - // expensive call, therefore we save width in line buffer - ch_width = wcwidth(code); - - if (ch >= ' ') { - if (this.charset && this.charset[ch]) { - ch = this.charset[ch]; + // '\b' + case '\x08': + if (this.x > 0) { + this.x--; } + break; - row = this.y + this.ybase; + // '\t' + case '\t': + this.x = this.nextStop(); + break; - // insert combining char in last cell - // FIXME: needs handling after cursor jumps - if (!ch_width && this.x) { - // dont overflow left - if (this.lines.get(row)[this.x-1]) { - if (!this.lines.get(row)[this.x-1][2]) { + // shift out + case '\x0e': + this.setgLevel(1); + break; - // found empty cell after fullwidth, need to go 2 cells back - if (this.lines.get(row)[this.x-2]) - this.lines.get(row)[this.x-2][1] += ch; + // shift in + case '\x0f': + this.setgLevel(0); + break; + // '\e' + case '\x1b': + this.state = escaped; + break; + + default: + // ' ' + // calculate print space + // expensive call, therefore we save width in line buffer + ch_width = wcwidth(code); + + if (ch >= ' ') { + if (this.charset && this.charset[ch]) { + ch = this.charset[ch]; + } + + row = this.y + this.ybase; + + // insert combining char in last cell + // FIXME: needs handling after cursor jumps + if (!ch_width && this.x) { + // dont overflow left + if (this.lines.get(row)[this.x-1]) { + if (!this.lines.get(row)[this.x-1][2]) { + + // found empty cell after fullwidth, need to go 2 cells back + if (this.lines.get(row)[this.x-2]) + this.lines.get(row)[this.x-2][1] += ch; + + } else { + this.lines.get(row)[this.x-1][1] += ch; + } + this.updateRange(this.y); + } + break; + } + + // goto next line if ch would overflow + // TODO: needs a global min terminal width of 2 + if (this.x+ch_width-1 >= this.cols) { + // autowrap - DECAWM + if (this.wraparoundMode) { + this.x = 0; + this.y++; + if (this.y > this.scrollBottom) { + this.y--; + this.scroll(); + } } else { - this.lines.get(row)[this.x-1][1] += ch; + this.x = this.cols-1; + if(ch_width===2) // FIXME: check for xterm behavior + continue; } - this.updateRange(this.y); } - break; - } + row = this.y + this.ybase; - // goto next line if ch would overflow - // TODO: needs a global min terminal width of 2 - if (this.x+ch_width-1 >= this.cols) { - // autowrap - DECAWM - if (this.wraparoundMode) { - this.x = 0; - this.y++; - if (this.y > this.scrollBottom) { - this.y--; - this.scroll(); + // insert mode: move characters to right + if (this.insertMode) { + // do this twice for a fullwidth char + for (var moves=0; moves Normal Keypad (DECKPNM). + case '>': + this.log('Switching back to normal keypad.'); + this.applicationKeypad = false; + this.viewport.syncScrollArea(); + this.state = normal; + break; + + default: + this.state = normal; + this.error('Unknown ESC control: %s.', ch); + break; + } + break; + + case charset: + switch (ch) { + case '0': // DEC Special Character and Line Drawing Set. + cs = Terminal.charsets.SCLD; + break; + case 'A': // UK + cs = Terminal.charsets.UK; + break; + case 'B': // United States (USASCII). + cs = Terminal.charsets.US; + break; + case '4': // Dutch + cs = Terminal.charsets.Dutch; + break; + case 'C': // Finnish + case '5': + cs = Terminal.charsets.Finnish; + break; + case 'R': // French + cs = Terminal.charsets.French; + break; + case 'Q': // FrenchCanadian + cs = Terminal.charsets.FrenchCanadian; + break; + case 'K': // German + cs = Terminal.charsets.German; + break; + case 'Y': // Italian + cs = Terminal.charsets.Italian; + break; + case 'E': // NorwegianDanish + case '6': + cs = Terminal.charsets.NorwegianDanish; + break; + case 'Z': // Spanish + cs = Terminal.charsets.Spanish; + break; + case 'H': // Swedish + case '7': + cs = Terminal.charsets.Swedish; + break; + case '=': // Swiss + cs = Terminal.charsets.Swiss; + break; + case '/': // ISOLatin (actually /A) + cs = Terminal.charsets.ISOLatin; + i++; + break; + default: // Default + cs = Terminal.charsets.US; + break; + } + this.setgCharset(this.gcharset, cs); + this.gcharset = null; + this.state = normal; + break; + + case osc: + // OSC Ps ; Pt ST + // OSC Ps ; Pt BEL + // Set Text Parameters. + if (ch === '\x1b' || ch === '\x07') { + if (ch === '\x1b') i++; + + this.params.push(this.currentParam); + + switch (this.params[0]) { + case 0: + case 1: + case 2: + if (this.params[1]) { + this.title = this.params[1]; + this.handleTitle(this.title); + } break; - case ')': - this.gcharset = 1; + case 3: + // set X property break; - case '*': - this.gcharset = 2; + case 4: + case 5: + // change dynamic colors break; - case '+': - this.gcharset = 3; + case 10: + case 11: + case 12: + case 13: + case 14: + case 15: + case 16: + case 17: + case 18: + case 19: + // change dynamic ui colors break; - case '-': - this.gcharset = 1; + case 46: + // change log file break; - case '.': - this.gcharset = 2; + case 50: + // dynamic font + break; + case 51: + // emacs shell + break; + case 52: + // manipulate selection data + break; + case 104: + case 105: + case 110: + case 111: + case 112: + case 113: + case 114: + case 115: + case 116: + case 117: + case 118: + // reset colors break; } - this.state = charset; - break; - // Designate G3 Character Set (VT300). - // A = ISO Latin-1 Supplemental. - // Not implemented. - case '/': - this.gcharset = 3; - this.state = charset; - i--; - break; - - // ESC N - // Single Shift Select of G2 Character Set - // ( SS2 is 0x8e). This affects next character only. - case 'N': - break; - // ESC O - // Single Shift Select of G3 Character Set - // ( SS3 is 0x8f). This affects next character only. - case 'O': - break; - // ESC n - // Invoke the G2 Character Set as GL (LS2). - case 'n': - this.setgLevel(2); - break; - // ESC o - // Invoke the G3 Character Set as GL (LS3). - case 'o': - this.setgLevel(3); - break; - // ESC | - // Invoke the G3 Character Set as GR (LS3R). - case '|': - this.setgLevel(3); - break; - // ESC } - // Invoke the G2 Character Set as GR (LS2R). - case '}': - this.setgLevel(2); - break; - // ESC ~ - // Invoke the G1 Character Set as GR (LS1R). - case '~': - this.setgLevel(1); - break; - - // ESC 7 Save Cursor (DECSC). - case '7': - this.saveCursor(); + this.params = []; + this.currentParam = 0; this.state = normal; - break; + } else { + if (!this.params.length) { + if (ch >= '0' && ch <= '9') { + this.currentParam = + this.currentParam * 10 + ch.charCodeAt(0) - 48; + } else if (ch === ';') { + this.params.push(this.currentParam); + this.currentParam = ''; + } + } else { + this.currentParam += ch; + } + } + break; - // ESC 8 Restore Cursor (DECRC). - case '8': - this.restoreCursor(); - this.state = normal; + case csi: + // '?', '>', '!' + if (ch === '?' || ch === '>' || ch === '!') { + this.prefix = ch; break; + } - // ESC # 3 DEC line height/width - case '#': - this.state = normal; - i++; + // 0 - 9 + if (ch >= '0' && ch <= '9') { + this.currentParam = this.currentParam * 10 + ch.charCodeAt(0) - 48; break; + } - // ESC H Tab Set (HTS is 0x88). - case 'H': - this.tabSet(); + // '$', '"', ' ', '\'' + if (ch === '$' || ch === '"' || ch === ' ' || ch === '\'') { + this.postfix = ch; break; - - // ESC = Application Keypad (DECKPAM). - case '=': - this.log('Serial port requested application keypad.'); - this.applicationKeypad = true; - this.viewport.syncScrollArea(); - this.state = normal; - break; - - // ESC > Normal Keypad (DECKPNM). - case '>': - this.log('Switching back to normal keypad.'); - this.applicationKeypad = false; - this.viewport.syncScrollArea(); - this.state = normal; - break; - - default: - this.state = normal; - this.error('Unknown ESC control: %s.', ch); - break; - } - break; - - case charset: - switch (ch) { - case '0': // DEC Special Character and Line Drawing Set. - cs = Terminal.charsets.SCLD; - break; - case 'A': // UK - cs = Terminal.charsets.UK; - break; - case 'B': // United States (USASCII). - cs = Terminal.charsets.US; - break; - case '4': // Dutch - cs = Terminal.charsets.Dutch; - break; - case 'C': // Finnish - case '5': - cs = Terminal.charsets.Finnish; - break; - case 'R': // French - cs = Terminal.charsets.French; - break; - case 'Q': // FrenchCanadian - cs = Terminal.charsets.FrenchCanadian; - break; - case 'K': // German - cs = Terminal.charsets.German; - break; - case 'Y': // Italian - cs = Terminal.charsets.Italian; - break; - case 'E': // NorwegianDanish - case '6': - cs = Terminal.charsets.NorwegianDanish; - break; - case 'Z': // Spanish - cs = Terminal.charsets.Spanish; - break; - case 'H': // Swedish - case '7': - cs = Terminal.charsets.Swedish; - break; - case '=': // Swiss - cs = Terminal.charsets.Swiss; - break; - case '/': // ISOLatin (actually /A) - cs = Terminal.charsets.ISOLatin; - i++; - break; - default: // Default - cs = Terminal.charsets.US; - break; - } - this.setgCharset(this.gcharset, cs); - this.gcharset = null; - this.state = normal; - break; - - case osc: - // OSC Ps ; Pt ST - // OSC Ps ; Pt BEL - // Set Text Parameters. - if (ch === '\x1b' || ch === '\x07') { - if (ch === '\x1b') i++; + } this.params.push(this.currentParam); - - switch (this.params[0]) { - case 0: - case 1: - case 2: - if (this.params[1]) { - this.title = this.params[1]; - this.handleTitle(this.title); - } - break; - case 3: - // set X property - break; - case 4: - case 5: - // change dynamic colors - break; - case 10: - case 11: - case 12: - case 13: - case 14: - case 15: - case 16: - case 17: - case 18: - case 19: - // change dynamic ui colors - break; - case 46: - // change log file - break; - case 50: - // dynamic font - break; - case 51: - // emacs shell - break; - case 52: - // manipulate selection data - break; - case 104: - case 105: - case 110: - case 111: - case 112: - case 113: - case 114: - case 115: - case 116: - case 117: - case 118: - // reset colors - break; - } - - this.params = []; this.currentParam = 0; + + // ';' + if (ch === ';') break; + this.state = normal; - } else { - if (!this.params.length) { - if (ch >= '0' && ch <= '9') { - this.currentParam = - this.currentParam * 10 + ch.charCodeAt(0) - 48; - } else if (ch === ';') { - this.params.push(this.currentParam); - this.currentParam = ''; - } - } else { - this.currentParam += ch; - } - } - break; - case csi: - // '?', '>', '!' - if (ch === '?' || ch === '>' || ch === '!') { - this.prefix = ch; - break; - } + switch (ch) { + // CSI Ps A + // Cursor Up Ps Times (default = 1) (CUU). + case 'A': + this.cursorUp(this.params); + break; - // 0 - 9 - if (ch >= '0' && ch <= '9') { - this.currentParam = this.currentParam * 10 + ch.charCodeAt(0) - 48; - break; - } + // CSI Ps B + // Cursor Down Ps Times (default = 1) (CUD). + case 'B': + this.cursorDown(this.params); + break; - // '$', '"', ' ', '\'' - if (ch === '$' || ch === '"' || ch === ' ' || ch === '\'') { - this.postfix = ch; - break; - } + // CSI Ps C + // Cursor Forward Ps Times (default = 1) (CUF). + case 'C': + this.cursorForward(this.params); + break; - this.params.push(this.currentParam); - this.currentParam = 0; + // CSI Ps D + // Cursor Backward Ps Times (default = 1) (CUB). + case 'D': + this.cursorBackward(this.params); + break; - // ';' - if (ch === ';') break; + // CSI Ps ; Ps H + // Cursor Position [row;column] (default = [1,1]) (CUP). + case 'H': + this.cursorPos(this.params); + break; - this.state = normal; + // CSI Ps J Erase in Display (ED). + case 'J': + this.eraseInDisplay(this.params); + break; - switch (ch) { - // CSI Ps A - // Cursor Up Ps Times (default = 1) (CUU). - case 'A': - this.cursorUp(this.params); - break; - - // CSI Ps B - // Cursor Down Ps Times (default = 1) (CUD). - case 'B': - this.cursorDown(this.params); - break; - - // CSI Ps C - // Cursor Forward Ps Times (default = 1) (CUF). - case 'C': - this.cursorForward(this.params); - break; - - // CSI Ps D - // Cursor Backward Ps Times (default = 1) (CUB). - case 'D': - this.cursorBackward(this.params); - break; - - // CSI Ps ; Ps H - // Cursor Position [row;column] (default = [1,1]) (CUP). - case 'H': - this.cursorPos(this.params); - break; - - // CSI Ps J Erase in Display (ED). - case 'J': - this.eraseInDisplay(this.params); - break; - - // CSI Ps K Erase in Line (EL). - case 'K': - this.eraseInLine(this.params); - break; - - // CSI Pm m Character Attributes (SGR). - case 'm': - if (!this.prefix) { - this.charAttributes(this.params); - } - break; - - // CSI Ps n Device Status Report (DSR). - case 'n': - if (!this.prefix) { - this.deviceStatus(this.params); - } - break; - - /** - * Additions - */ - - // CSI Ps @ - // Insert Ps (Blank) Character(s) (default = 1) (ICH). - case '@': - this.insertChars(this.params); - break; - - // CSI Ps E - // Cursor Next Line Ps Times (default = 1) (CNL). - case 'E': - this.cursorNextLine(this.params); - break; - - // CSI Ps F - // Cursor Preceding Line Ps Times (default = 1) (CNL). - case 'F': - this.cursorPrecedingLine(this.params); - break; - - // CSI Ps G - // Cursor Character Absolute [column] (default = [row,1]) (CHA). - case 'G': - this.cursorCharAbsolute(this.params); - break; - - // CSI Ps L - // Insert Ps Line(s) (default = 1) (IL). - case 'L': - this.insertLines(this.params); - break; - - // CSI Ps M - // Delete Ps Line(s) (default = 1) (DL). - case 'M': - this.deleteLines(this.params); - break; - - // CSI Ps P - // Delete Ps Character(s) (default = 1) (DCH). - case 'P': - this.deleteChars(this.params); - break; - - // CSI Ps X - // Erase Ps Character(s) (default = 1) (ECH). - case 'X': - this.eraseChars(this.params); - break; - - // CSI Pm ` Character Position Absolute - // [column] (default = [row,1]) (HPA). - case '`': - this.charPosAbsolute(this.params); - break; - - // 141 61 a * HPR - - // Horizontal Position Relative - case 'a': - this.HPositionRelative(this.params); - break; - - // CSI P s c - // Send Device Attributes (Primary DA). - // CSI > P s c - // Send Device Attributes (Secondary DA) - case 'c': - this.sendDeviceAttributes(this.params); - break; - - // CSI Pm d - // Line Position Absolute [row] (default = [1,column]) (VPA). - case 'd': - this.linePosAbsolute(this.params); - break; - - // 145 65 e * VPR - Vertical Position Relative - case 'e': - this.VPositionRelative(this.params); - break; - - // CSI Ps ; Ps f - // Horizontal and Vertical Position [row;column] (default = - // [1,1]) (HVP). - case 'f': - this.HVPosition(this.params); - break; - - // CSI Pm h Set Mode (SM). - // CSI ? Pm h - mouse escape codes, cursor escape codes - case 'h': - this.setMode(this.params); - break; - - // CSI Pm l Reset Mode (RM). - // CSI ? Pm l - case 'l': - this.resetMode(this.params); - break; - - // CSI Ps ; Ps r - // Set Scrolling Region [top;bottom] (default = full size of win- - // dow) (DECSTBM). - // CSI ? Pm r - case 'r': - this.setScrollRegion(this.params); - break; - - // CSI s - // Save cursor (ANSI.SYS). - case 's': - this.saveCursor(this.params); - break; - - // CSI u - // Restore cursor (ANSI.SYS). - case 'u': - this.restoreCursor(this.params); - break; - - /** - * Lesser Used - */ - - // CSI Ps I - // Cursor Forward Tabulation Ps tab stops (default = 1) (CHT). - case 'I': - this.cursorForwardTab(this.params); - break; - - // CSI Ps S Scroll up Ps lines (default = 1) (SU). - case 'S': - this.scrollUp(this.params); - break; - - // CSI Ps T Scroll down Ps lines (default = 1) (SD). - // CSI Ps ; Ps ; Ps ; Ps ; Ps T - // CSI > Ps; Ps T - case 'T': - // if (this.prefix === '>') { - // this.resetTitleModes(this.params); - // break; - // } - // if (this.params.length > 2) { - // this.initMouseTracking(this.params); - // break; - // } - if (this.params.length < 2 && !this.prefix) { - this.scrollDown(this.params); - } - break; - - // CSI Ps Z - // Cursor Backward Tabulation Ps tab stops (default = 1) (CBT). - case 'Z': - this.cursorBackwardTab(this.params); - break; - - // CSI Ps b Repeat the preceding graphic character Ps times (REP). - case 'b': - this.repeatPrecedingCharacter(this.params); - break; - - // CSI Ps g Tab Clear (TBC). - case 'g': - this.tabClear(this.params); - break; - - // CSI Pm i Media Copy (MC). - // CSI ? Pm i - // case 'i': - // this.mediaCopy(this.params); - // break; + // CSI Ps K Erase in Line (EL). + case 'K': + this.eraseInLine(this.params); + break; // CSI Pm m Character Attributes (SGR). - // CSI > Ps; Ps m - // case 'm': // duplicate - // if (this.prefix === '>') { - // this.setResources(this.params); - // } else { - // this.charAttributes(this.params); - // } - // break; + case 'm': + if (!this.prefix) { + this.charAttributes(this.params); + } + break; // CSI Ps n Device Status Report (DSR). - // CSI > Ps n - // case 'n': // duplicate - // if (this.prefix === '>') { - // this.disableModifiers(this.params); - // } else { - // this.deviceStatus(this.params); - // } - // break; + case 'n': + if (!this.prefix) { + this.deviceStatus(this.params); + } + break; - // CSI > Ps p Set pointer mode. - // CSI ! p Soft terminal reset (DECSTR). - // CSI Ps$ p - // Request ANSI mode (DECRQM). - // CSI ? Ps$ p - // Request DEC private mode (DECRQM). - // CSI Ps ; Ps " p - case 'p': - switch (this.prefix) { - // case '>': - // this.setPointerMode(this.params); - // break; - case '!': - this.softReset(this.params); - break; - // case '?': - // if (this.postfix === '$') { - // this.requestPrivateMode(this.params); - // } - // break; - // default: - // if (this.postfix === '"') { - // this.setConformanceLevel(this.params); - // } else if (this.postfix === '$') { - // this.requestAnsiMode(this.params); - // } - // break; - } - break; + /** + * Additions + */ - // CSI Ps q Load LEDs (DECLL). - // CSI Ps SP q - // CSI Ps " q - // case 'q': - // if (this.postfix === ' ') { - // this.setCursorStyle(this.params); - // break; - // } - // if (this.postfix === '"') { - // this.setCharProtectionAttr(this.params); - // break; - // } - // this.loadLEDs(this.params); - // break; + // CSI Ps @ + // Insert Ps (Blank) Character(s) (default = 1) (ICH). + case '@': + this.insertChars(this.params); + break; + + // CSI Ps E + // Cursor Next Line Ps Times (default = 1) (CNL). + case 'E': + this.cursorNextLine(this.params); + break; + + // CSI Ps F + // Cursor Preceding Line Ps Times (default = 1) (CNL). + case 'F': + this.cursorPrecedingLine(this.params); + break; + + // CSI Ps G + // Cursor Character Absolute [column] (default = [row,1]) (CHA). + case 'G': + this.cursorCharAbsolute(this.params); + break; + + // CSI Ps L + // Insert Ps Line(s) (default = 1) (IL). + case 'L': + this.insertLines(this.params); + break; + + // CSI Ps M + // Delete Ps Line(s) (default = 1) (DL). + case 'M': + this.deleteLines(this.params); + break; + + // CSI Ps P + // Delete Ps Character(s) (default = 1) (DCH). + case 'P': + this.deleteChars(this.params); + break; + + // CSI Ps X + // Erase Ps Character(s) (default = 1) (ECH). + case 'X': + this.eraseChars(this.params); + break; + + // CSI Pm ` Character Position Absolute + // [column] (default = [row,1]) (HPA). + case '`': + this.charPosAbsolute(this.params); + break; + + // 141 61 a * HPR - + // Horizontal Position Relative + case 'a': + this.HPositionRelative(this.params); + break; + + // CSI P s c + // Send Device Attributes (Primary DA). + // CSI > P s c + // Send Device Attributes (Secondary DA) + case 'c': + this.sendDeviceAttributes(this.params); + break; + + // CSI Pm d + // Line Position Absolute [row] (default = [1,column]) (VPA). + case 'd': + this.linePosAbsolute(this.params); + break; + + // 145 65 e * VPR - Vertical Position Relative + case 'e': + this.VPositionRelative(this.params); + break; + + // CSI Ps ; Ps f + // Horizontal and Vertical Position [row;column] (default = + // [1,1]) (HVP). + case 'f': + this.HVPosition(this.params); + break; + + // CSI Pm h Set Mode (SM). + // CSI ? Pm h - mouse escape codes, cursor escape codes + case 'h': + this.setMode(this.params); + break; + + // CSI Pm l Reset Mode (RM). + // CSI ? Pm l + case 'l': + this.resetMode(this.params); + break; // CSI Ps ; Ps r // Set Scrolling Region [top;bottom] (default = full size of win- // dow) (DECSTBM). // CSI ? Pm r - // CSI Pt; Pl; Pb; Pr; Ps$ r - // case 'r': // duplicate - // if (this.prefix === '?') { - // this.restorePrivateValues(this.params); - // } else if (this.postfix === '$') { - // this.setAttrInRectangle(this.params); - // } else { - // this.setScrollRegion(this.params); - // } - // break; - - // CSI s Save cursor (ANSI.SYS). - // CSI ? Pm s - // case 's': // duplicate - // if (this.prefix === '?') { - // this.savePrivateValues(this.params); - // } else { - // this.saveCursor(this.params); - // } - // break; - - // CSI Ps ; Ps ; Ps t - // CSI Pt; Pl; Pb; Pr; Ps$ t - // CSI > Ps; Ps t - // CSI Ps SP t - // case 't': - // if (this.postfix === '$') { - // this.reverseAttrInRectangle(this.params); - // } else if (this.postfix === ' ') { - // this.setWarningBellVolume(this.params); - // } else { - // if (this.prefix === '>') { - // this.setTitleModeFeature(this.params); - // } else { - // this.manipulateWindow(this.params); - // } - // } - // break; - - // CSI u Restore cursor (ANSI.SYS). - // CSI Ps SP u - // case 'u': // duplicate - // if (this.postfix === ' ') { - // this.setMarginBellVolume(this.params); - // } else { - // this.restoreCursor(this.params); - // } - // break; - - // CSI Pt; Pl; Pb; Pr; Pp; Pt; Pl; Pp$ v - // case 'v': - // if (this.postfix === '$') { - // this.copyRectagle(this.params); - // } - // break; - - // CSI Pt ; Pl ; Pb ; Pr ' w - // case 'w': - // if (this.postfix === '\'') { - // this.enableFilterRectangle(this.params); - // } - // break; - - // CSI Ps x Request Terminal Parameters (DECREQTPARM). - // CSI Ps x Select Attribute Change Extent (DECSACE). - // CSI Pc; Pt; Pl; Pb; Pr$ x - // case 'x': - // if (this.postfix === '$') { - // this.fillRectangle(this.params); - // } else { - // this.requestParameters(this.params); - // //this.__(this.params); - // } - // break; - - // CSI Ps ; Pu ' z - // CSI Pt; Pl; Pb; Pr$ z - // case 'z': - // if (this.postfix === '\'') { - // this.enableLocatorReporting(this.params); - // } else if (this.postfix === '$') { - // this.eraseRectangle(this.params); - // } - // break; - - // CSI Pm ' { - // CSI Pt; Pl; Pb; Pr$ { - // case '{': - // if (this.postfix === '\'') { - // this.setLocatorEvents(this.params); - // } else if (this.postfix === '$') { - // this.selectiveEraseRectangle(this.params); - // } - // break; - - // CSI Ps ' | - // case '|': - // if (this.postfix === '\'') { - // this.requestLocatorPosition(this.params); - // } - // break; - - // CSI P m SP } - // Insert P s Column(s) (default = 1) (DECIC), VT420 and up. - // case '}': - // if (this.postfix === ' ') { - // this.insertColumns(this.params); - // } - // break; - - // CSI P m SP ~ - // Delete P s Column(s) (default = 1) (DECDC), VT420 and up - // case '~': - // if (this.postfix === ' ') { - // this.deleteColumns(this.params); - // } - // break; - - default: - this.error('Unknown CSI code: %s.', ch); - break; - } - - this.prefix = ''; - this.postfix = ''; - break; - - case dcs: - if (ch === '\x1b' || ch === '\x07') { - if (ch === '\x1b') i++; - - switch (this.prefix) { - // User-Defined Keys (DECUDK). - case '': + case 'r': + this.setScrollRegion(this.params); break; - // Request Status String (DECRQSS). - // test: echo -e '\eP$q"p\e\\' - case '$q': - var pt = this.currentParam - , valid = false; + // CSI s + // Save cursor (ANSI.SYS). + case 's': + this.saveCursor(this.params); + break; - switch (pt) { - // DECSCA - case '"q': - pt = '0"q'; - break; + // CSI u + // Restore cursor (ANSI.SYS). + case 'u': + this.restoreCursor(this.params); + break; - // DECSCL - case '"p': - pt = '61"p'; - break; + /** + * Lesser Used + */ - // DECSTBM - case 'r': - pt = '' - + (this.scrollTop + 1) - + ';' - + (this.scrollBottom + 1) - + 'r'; - break; + // CSI Ps I + // Cursor Forward Tabulation Ps tab stops (default = 1) (CHT). + case 'I': + this.cursorForwardTab(this.params); + break; - // SGR - case 'm': - pt = '0m'; - break; + // CSI Ps S Scroll up Ps lines (default = 1) (SU). + case 'S': + this.scrollUp(this.params); + break; - default: - this.error('Unknown DCS Pt: %s.', pt); - pt = ''; - break; + // CSI Ps T Scroll down Ps lines (default = 1) (SD). + // CSI Ps ; Ps ; Ps ; Ps ; Ps T + // CSI > Ps; Ps T + case 'T': + // if (this.prefix === '>') { + // this.resetTitleModes(this.params); + // break; + // } + // if (this.params.length > 2) { + // this.initMouseTracking(this.params); + // break; + // } + if (this.params.length < 2 && !this.prefix) { + this.scrollDown(this.params); } - - this.send('\x1bP' + +valid + '$r' + pt + '\x1b\\'); break; - // Set Termcap/Terminfo Data (xterm, experimental). - case '+p': + // CSI Ps Z + // Cursor Backward Tabulation Ps tab stops (default = 1) (CBT). + case 'Z': + this.cursorBackwardTab(this.params); break; - // Request Termcap/Terminfo String (xterm, experimental) - // Regular xterm does not even respond to this sequence. - // This can cause a small glitch in vim. - // test: echo -ne '\eP+q6b64\e\\' - case '+q': - var pt = this.currentParam - , valid = false; - - this.send('\x1bP' + +valid + '+r' + pt + '\x1b\\'); + // CSI Ps b Repeat the preceding graphic character Ps times (REP). + case 'b': + this.repeatPrecedingCharacter(this.params); break; + // CSI Ps g Tab Clear (TBC). + case 'g': + this.tabClear(this.params); + break; + + // CSI Pm i Media Copy (MC). + // CSI ? Pm i + // case 'i': + // this.mediaCopy(this.params); + // break; + + // CSI Pm m Character Attributes (SGR). + // CSI > Ps; Ps m + // case 'm': // duplicate + // if (this.prefix === '>') { + // this.setResources(this.params); + // } else { + // this.charAttributes(this.params); + // } + // break; + + // CSI Ps n Device Status Report (DSR). + // CSI > Ps n + // case 'n': // duplicate + // if (this.prefix === '>') { + // this.disableModifiers(this.params); + // } else { + // this.deviceStatus(this.params); + // } + // break; + + // CSI > Ps p Set pointer mode. + // CSI ! p Soft terminal reset (DECSTR). + // CSI Ps$ p + // Request ANSI mode (DECRQM). + // CSI ? Ps$ p + // Request DEC private mode (DECRQM). + // CSI Ps ; Ps " p + case 'p': + switch (this.prefix) { + // case '>': + // this.setPointerMode(this.params); + // break; + case '!': + this.softReset(this.params); + break; + // case '?': + // if (this.postfix === '$') { + // this.requestPrivateMode(this.params); + // } + // break; + // default: + // if (this.postfix === '"') { + // this.setConformanceLevel(this.params); + // } else if (this.postfix === '$') { + // this.requestAnsiMode(this.params); + // } + // break; + } + break; + + // CSI Ps q Load LEDs (DECLL). + // CSI Ps SP q + // CSI Ps " q + // case 'q': + // if (this.postfix === ' ') { + // this.setCursorStyle(this.params); + // break; + // } + // if (this.postfix === '"') { + // this.setCharProtectionAttr(this.params); + // break; + // } + // this.loadLEDs(this.params); + // break; + + // CSI Ps ; Ps r + // Set Scrolling Region [top;bottom] (default = full size of win- + // dow) (DECSTBM). + // CSI ? Pm r + // CSI Pt; Pl; Pb; Pr; Ps$ r + // case 'r': // duplicate + // if (this.prefix === '?') { + // this.restorePrivateValues(this.params); + // } else if (this.postfix === '$') { + // this.setAttrInRectangle(this.params); + // } else { + // this.setScrollRegion(this.params); + // } + // break; + + // CSI s Save cursor (ANSI.SYS). + // CSI ? Pm s + // case 's': // duplicate + // if (this.prefix === '?') { + // this.savePrivateValues(this.params); + // } else { + // this.saveCursor(this.params); + // } + // break; + + // CSI Ps ; Ps ; Ps t + // CSI Pt; Pl; Pb; Pr; Ps$ t + // CSI > Ps; Ps t + // CSI Ps SP t + // case 't': + // if (this.postfix === '$') { + // this.reverseAttrInRectangle(this.params); + // } else if (this.postfix === ' ') { + // this.setWarningBellVolume(this.params); + // } else { + // if (this.prefix === '>') { + // this.setTitleModeFeature(this.params); + // } else { + // this.manipulateWindow(this.params); + // } + // } + // break; + + // CSI u Restore cursor (ANSI.SYS). + // CSI Ps SP u + // case 'u': // duplicate + // if (this.postfix === ' ') { + // this.setMarginBellVolume(this.params); + // } else { + // this.restoreCursor(this.params); + // } + // break; + + // CSI Pt; Pl; Pb; Pr; Pp; Pt; Pl; Pp$ v + // case 'v': + // if (this.postfix === '$') { + // this.copyRectagle(this.params); + // } + // break; + + // CSI Pt ; Pl ; Pb ; Pr ' w + // case 'w': + // if (this.postfix === '\'') { + // this.enableFilterRectangle(this.params); + // } + // break; + + // CSI Ps x Request Terminal Parameters (DECREQTPARM). + // CSI Ps x Select Attribute Change Extent (DECSACE). + // CSI Pc; Pt; Pl; Pb; Pr$ x + // case 'x': + // if (this.postfix === '$') { + // this.fillRectangle(this.params); + // } else { + // this.requestParameters(this.params); + // //this.__(this.params); + // } + // break; + + // CSI Ps ; Pu ' z + // CSI Pt; Pl; Pb; Pr$ z + // case 'z': + // if (this.postfix === '\'') { + // this.enableLocatorReporting(this.params); + // } else if (this.postfix === '$') { + // this.eraseRectangle(this.params); + // } + // break; + + // CSI Pm ' { + // CSI Pt; Pl; Pb; Pr$ { + // case '{': + // if (this.postfix === '\'') { + // this.setLocatorEvents(this.params); + // } else if (this.postfix === '$') { + // this.selectiveEraseRectangle(this.params); + // } + // break; + + // CSI Ps ' | + // case '|': + // if (this.postfix === '\'') { + // this.requestLocatorPosition(this.params); + // } + // break; + + // CSI P m SP } + // Insert P s Column(s) (default = 1) (DECIC), VT420 and up. + // case '}': + // if (this.postfix === ' ') { + // this.insertColumns(this.params); + // } + // break; + + // CSI P m SP ~ + // Delete P s Column(s) (default = 1) (DECDC), VT420 and up + // case '~': + // if (this.postfix === ' ') { + // this.deleteColumns(this.params); + // } + // break; + default: - this.error('Unknown DCS prefix: %s.', this.prefix); + this.error('Unknown CSI code: %s.', ch); break; } - this.currentParam = 0; this.prefix = ''; - this.state = normal; - } else if (!this.currentParam) { - if (!this.prefix && ch !== '$' && ch !== '+') { - this.currentParam = ch; - } else if (this.prefix.length === 2) { - this.currentParam = ch; + this.postfix = ''; + break; + + case dcs: + if (ch === '\x1b' || ch === '\x07') { + if (ch === '\x1b') i++; + + switch (this.prefix) { + // User-Defined Keys (DECUDK). + case '': + break; + + // Request Status String (DECRQSS). + // test: echo -e '\eP$q"p\e\\' + case '$q': + var pt = this.currentParam + , valid = false; + + switch (pt) { + // DECSCA + case '"q': + pt = '0"q'; + break; + + // DECSCL + case '"p': + pt = '61"p'; + break; + + // DECSTBM + case 'r': + pt = '' + + (this.scrollTop + 1) + + ';' + + (this.scrollBottom + 1) + + 'r'; + break; + + // SGR + case 'm': + pt = '0m'; + break; + + default: + this.error('Unknown DCS Pt: %s.', pt); + pt = ''; + break; + } + + this.send('\x1bP' + +valid + '$r' + pt + '\x1b\\'); + break; + + // Set Termcap/Terminfo Data (xterm, experimental). + case '+p': + break; + + // Request Termcap/Terminfo String (xterm, experimental) + // Regular xterm does not even respond to this sequence. + // This can cause a small glitch in vim. + // test: echo -ne '\eP+q6b64\e\\' + case '+q': + var pt = this.currentParam + , valid = false; + + this.send('\x1bP' + +valid + '+r' + pt + '\x1b\\'); + break; + + default: + this.error('Unknown DCS prefix: %s.', this.prefix); + break; + } + + this.currentParam = 0; + this.prefix = ''; + this.state = normal; + } else if (!this.currentParam) { + if (!this.prefix && ch !== '$' && ch !== '+') { + this.currentParam = ch; + } else if (this.prefix.length === 2) { + this.currentParam = ch; + } else { + this.prefix += ch; + } } else { - this.prefix += ch; + this.currentParam += ch; } - } else { - this.currentParam += ch; - } - break; + break; - case ignore: - // For PM and APC. - if (ch === '\x1b' || ch === '\x07') { - if (ch === '\x1b') i++; - this.state = normal; - } - break; + case ignore: + // For PM and APC. + if (ch === '\x1b' || ch === '\x07') { + if (ch === '\x1b') i++; + this.state = normal; + } + break; + } } + + this.updateRange(this.y); + this.queueRefresh(this.refreshStart, this.refreshEnd); } - - this.updateRange(this.y); - this.queueRefresh(this.refreshStart, this.refreshEnd); - - if (this.writeBuffer.length > 0) { - var self = this; - -// TODO: async makes this too slow, need to change to iterative to prevent potential stack overflow - - // Start a new async innerWrite to prevent a stack overflow - //setTimeout(function () { - self.innerWrite(self.writeBuffer.shift()); - //}); - } else { - this.writeInProgress = false; - } + this.writeInProgress = false; }; /** From 0ec7b661f2be4660821301942cfe175c89f6f40f Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 3 Jan 2017 15:38:14 -0800 Subject: [PATCH 16/33] Tweak config values --- src/xterm.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/xterm.js b/src/xterm.js index 0849775d..29a38487 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -42,13 +42,13 @@ var normal = 0, escaped = 1, csi = 2, osc = 3, charset = 4, dcs = 5, ignore = 6; * pty process. This number must be small in order for ^C and similar sequences * to be responsive. */ -var WRITE_BUFFER_PAUSE_THRESHOLD = 0; +var WRITE_BUFFER_PAUSE_THRESHOLD = 2; /** * The maximum number of refresh frames to skip when the write buffer is non- * empty. */ -var MAX_REFRESH_FRAME_SKIP = 6; +var MAX_REFRESH_FRAME_SKIP = 5; /** * Terminal @@ -1384,7 +1384,7 @@ Terminal.prototype.write = function(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.xoffSentToCatchUp && this.writeBuffer.length > WRITE_BUFFER_PAUSE_THRESHOLD) { + if (!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('\x13'); From 2b8820fdac9170388a6018270021ad74b7c324f1 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 3 Jan 2017 15:52:45 -0800 Subject: [PATCH 17/33] Further tweaks, add write batching --- src/xterm.js | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/src/xterm.js b/src/xterm.js index 29a38487..038969ae 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -42,11 +42,18 @@ var normal = 0, escaped = 1, csi = 2, osc = 3, charset = 4, dcs = 5, ignore = 6; * pty process. This number must be small in order for ^C and similar sequences * to be responsive. */ -var WRITE_BUFFER_PAUSE_THRESHOLD = 2; +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 maximum number of refresh frames to skip when the write buffer is non- - * empty. + * empty. Note that these frames may be intermingled with frames that are + * skipped via requestAnimationFrame's mechanism. */ var MAX_REFRESH_FRAME_SKIP = 5; @@ -1403,13 +1410,14 @@ Terminal.prototype.write = function(data) { } Terminal.prototype.innerWrite = function() { - while (this.writeBuffer.length > 0) { - var data = this.writeBuffer.shift(); + 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 && this.writeBuffer.length === 0) { + if (this.xoffSentToCatchUp && writeBatch.length === 0 && this.writeBuffer.length === 0) { this.send('\x11'); this.xoffSentToCatchUp = false; } @@ -2452,7 +2460,15 @@ Terminal.prototype.innerWrite = function() { this.updateRange(this.y); this.queueRefresh(this.refreshStart, this.refreshEnd); } - this.writeInProgress = false; + 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; + } }; /** From 94c01ec378c9506414222fd01af2244885fcb7af Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 4 Jan 2017 08:08:59 -0800 Subject: [PATCH 18/33] Fix tests --- src/test/escape-sequences-test.js | 6 +++++- src/test/test.js | 9 +++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/test/escape-sequences-test.js b/src/test/escape-sequences-test.js index ace2c3cb..e9183ccf 100644 --- a/src/test/escape-sequences-test.js +++ b/src/test/escape-sequences-test.js @@ -92,7 +92,11 @@ describe('xterm output comparison', function() { var from_pty = pty_write_read(in_file); // uncomment this to get log from terminal //console.log = function(){}; - xterm.write(from_pty); + + // Perform a synchronous .write(data) + xterm.writeBuffer.push(from_pty); + xterm.innerWrite(); + var from_emulator = terminalToString(xterm); console.log = CONSOLE_LOG; var expected = fs.readFileSync(filename.split('.')[0] + '.text', 'utf8'); diff --git a/src/test/test.js b/src/test/test.js index a44d2066..7c716b2a 100644 --- a/src/test/test.js +++ b/src/test/test.js @@ -14,6 +14,15 @@ describe('xterm.js', function() { xterm.compositionHelper = { keydown: function(){ return true; } }; + // Force synchronous refreshes + xterm.queueRefresh = function(start, end) { + xterm.refresh(start, end); + }; + // Force synchronous writes + xterm.write = function(data) { + xterm.writeBuffer.push(data); + xterm.innerWrite(); + }; }); describe('getOption', function() { From ff523bfc790d915f23502d97b427497c8b62189d Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 4 Jan 2017 10:30:04 -0800 Subject: [PATCH 19/33] Fix tests --- src/Viewport.test.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Viewport.test.ts b/src/Viewport.test.ts index 5b106b43..fc2cd117 100644 --- a/src/Viewport.test.ts +++ b/src/Viewport.test.ts @@ -1,6 +1,11 @@ import { assert } from 'chai'; import { Viewport } from './Viewport'; +class MockWindow { + // Disable refreshLoop in test + public requestAnimationFrame() { } +} + describe('Viewport', () => { var terminal; var viewportElement; @@ -11,6 +16,7 @@ describe('Viewport', () => { const CHARACTER_HEIGHT = 10; beforeEach(() => { + (global).window = new MockWindow(); terminal = { lines: [], rows: 0, @@ -73,10 +79,14 @@ describe('Viewport', () => { terminal.rows = 1; assert.equal(scrollAreaElement.style.height, 0 * CHARACTER_HEIGHT + 'px'); viewport.syncScrollArea(); + assert.ok(viewport.isRefreshQueued); + viewport.refresh(); assert.equal(viewportElement.style.height, 1 * CHARACTER_HEIGHT + 'px'); assert.equal(scrollAreaElement.style.height, 1 * CHARACTER_HEIGHT + 'px'); terminal.lines.push(''); viewport.syncScrollArea(); + assert.ok(viewport.isRefreshQueued); + viewport.refresh(); assert.equal(viewportElement.style.height, 1 * CHARACTER_HEIGHT + 'px'); assert.equal(scrollAreaElement.style.height, 2 * CHARACTER_HEIGHT + 'px'); }); From d9d60063d788308c98a2809e6b5086e64aa27bcc Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 4 Jan 2017 12:43:21 -0800 Subject: [PATCH 20/33] Add disableStdin option Fixes #452 --- src/xterm.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/xterm.js b/src/xterm.js index 1d220652..78c07609 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -326,7 +326,8 @@ Terminal.defaults = { scrollback: 1000, screenKeys: false, debug: false, - cancelEvents: false + cancelEvents: false, + disableStdin: false // programFeatures: false, // focusKeys: false, }; @@ -3134,6 +3135,11 @@ Terminal.prototype.is = function(term) { * @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; + } + // Input is being sent to the terminal, the terminal should focus the prompt. if (this.ybase !== this.ydisp) { this.scrollToBottom(); From 996c25a3648a899a8bb05ad5bc9df3a0b4629868 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 7 Jan 2017 16:04:46 -0800 Subject: [PATCH 21/33] Keep previous terminal events after a reset This broke in the EventEmitter ts conversion, just reverts this line to what it used to be. Fixes #451 --- src/EventEmitter.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/EventEmitter.ts b/src/EventEmitter.ts index d05e3863..1db86763 100644 --- a/src/EventEmitter.ts +++ b/src/EventEmitter.ts @@ -11,7 +11,9 @@ export class EventEmitter { private _events: {[type: string]: ListenerType[]}; constructor() { - this._events = {}; + // Restore the previous events if available, this will happen if the + // constructor is called multiple times on the same object (terminal reset). + this._events = this._events || {}; } public on(type, listener): void { From 520f2bc9d6365c7034b9519cc2b0c98c71bd6f39 Mon Sep 17 00:00:00 2001 From: Tyler Jewell Date: Sun, 8 Jan 2017 09:44:14 -0800 Subject: [PATCH 22/33] Add additional products that use xTerm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hi xTerm team. Over the past 6 months, we have been studying xTerm as a replacement for Eclipse Che's term.js. It's taken us a bit of an effort, but the work is now done! You can expect that Eclipse Che engineers (abot 60 contributors) will make direct contributions back to xTerm ongoing now that we are fully integrated in. We integrate xTerm alongside GWT. As we more natively integrate the solution, we'll make additional PRs directly back to the project. As background on the research and integration work that we did, these are the postings from our engineering team's research from the past six months. Some of these issues have already been resolved, or in the process of being resolved. We also plan a couple of blog posts to our forums (~250K followers) about xTerm integration later in Q1 after we make our 5.0.0 announcements. I could also add Codenvy to this PR as well since it has been released with the version based upon xTerm, but with Sourcelair being the initiators we didn't want to make it seem like we were trying to be competitive. This PR is about the promotion and success of xTerm, for which we are fully committed to. ## Xtermjs This document is result of investigation using xtemjs ui terminal instead of current term.js. It contains analyze pluses and minuses using xtermjs (release version 2.2.3) and technical problems. Investigation issue: https://github.com/eclipse/che/issues/3210 ## Description For now we are using our own fork of the https://github.com/chjj/term.js for user interface websocket-terminal. But actually this project is no longer maintained. So we can not get new releases from this project and we need support this script on our own. Original project contains link for a maintained fork https://github.com/sourcelair/xterm.js. This fork uses MIT license and community intensive develops this project. It has such users like: Microsoft Visual Studio Code, SourceLair, ttyd. xterm.js had already done 46 releases https://github.com/sourcelair/xterm.js/releases. ## Technical Advantages of xTerm: * xterm.js has js tests. We have not any test for old term.js. * We can periodically update xtem.js by new release. * Added parameter to cancel browser events. * Default 256 colours moved from js to css. * Added ability to set terminal theme. * Improved resize mehanizm (added resize event). * Added fit.ts script to fits terminal size to original height and width of parent div. * Fixed lost text selection from current active line(when blink is enabled). * Improved copy/paster mehanizm. * Linkify URL feature. * Implement moving back and forward across words with "Alt + ←" and "Alt + →" respectively. * Improved special key handling. * Added addons to exdens xterm.js * Improved mehanizm copy/paster from clipboard. * Fixed cross platform input problems(For IPad, Iphone, MacIntel, MacPPC and so on). * Did some work to support UTF-8 symbols. * Implemented scrollbar. * Fixed incorrect mouse position for application with pseudo-graphic user interface (for example Midnight Commander). * Fix to prevent terminal scrolling when user is looking into scrollback(similar to gnome-terminal, if the user is scrolling up to look at past output and the currently running program adds output to the terminal, the viewport of xterm.js should not scroll and interrupt what they are looking at.) * Make right-click work on all browsers. ## Broken Changes: We can move on our changes to realize copy/paste by hotkeys Ctrl + C/Ctrl +V. But xterm.js support hotkeys more common for terminal: Ctrl + Insert and Shift + Insert and maybe that's enough. Drop support for old mouse wheel APIs: all browsers have supported the WheelEvent (onwheel) for sometime now, since Firefox does not support onmousewheel which is also non-standard but works with the standard interface, it makes sense to drop support now. ## Browser Support Since xterm.js is typically implemented as a developer tool, only modern browsers are supported officially. Here is a list of the versions to support: Chrome 48+ Edge 13+ Firefox 44+ Internet Explorer 11+ Opera 35+ Safari 8+ Xterm.js works seamlessly in Electron apps and may even work on earlier versions of the browsers but these are the browsers xterm.js developers strive to keep working. ## Known major bugs: https://github.com/sourcelair/xterm.js/issues/307 https://github.com/sourcelair/xterm.js/issues/362 https://github.com/sourcelair/xterm.js/issues/348 https://github.com/sourcelair/xterm.js/issues/325 Data loss when resizing terminal (but this bug is exist in the current terminal ui in the CHE too). Community has pull request to fix this issue for xtermjs https://github.com/sourcelair/xterm.js/pull/404 . Additional information: Code base xterm.js consist of files written on native javascript and typescript. For manage js dependency and configuration used npm, typings, bower and node. Development tendency: rewrite xtemr.js completely on the Typescript(information about this included to the release notes 2.2.3). --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index b5d30041..608c48ac 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ Xterm.js is used in several world-class applications to provide great terminal e - [**Microsoft Visual Studio Code**](http://code.visualstudio.com/): Modern, versatile and powerful open source code editor that provides an integrated terminal based on xterm.js - [**ttyd**](https://github.com/tsl0922/ttyd): A command-line tool for sharing terminal over the web, with fully-featured terminal emulation based on xterm.js - [**Katacoda**](https://www.katacoda.com/): Katacoda is an Interactive Learning Platform for software developers, covering the latest Cloud Native technologies. +- [**Eclipse Che**](http://www.eclipse.org/che): Developer workspace server, cloud IDE, and Eclipse next-generation IDE Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it in our list. @@ -117,6 +118,10 @@ Visit https://lair.io/sourcelair/xterm and follow the instructions. All developm [Download Visual Studio Code](http://code.visualstudio.com/Download), clone xterm.js and you are all set. +#### [Eclipse Che](http://www.eclipse.org/che) + +You can start Eclipse Che with `docker run eclipse/che start` + ## License Agreement If you contribute code to this project, you are implicitly allowing your code to be distributed under the MIT license. You are also implicitly verifying that all code is your original work. From 3de3c0c6b69f9b3e836d57c4f08a3f716f1d110e Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 8 Jan 2017 15:49:19 -0800 Subject: [PATCH 23/33] Pull C0 escape sequences into its own file Part of #460 --- src/EscapeSequences.ts | 74 +++++++++++++++ src/xterm.js | 204 ++++++++++++++++++++--------------------- 2 files changed, 176 insertions(+), 102 deletions(-) create mode 100644 src/EscapeSequences.ts diff --git a/src/EscapeSequences.ts b/src/EscapeSequences.ts new file mode 100644 index 00000000..d4b736fd --- /dev/null +++ b/src/EscapeSequences.ts @@ -0,0 +1,74 @@ +/** + * C0 control codes + * See: https://en.wikipedia.org/wiki/C0_and_C1_control_codes + */ +export const C0 = { + /** Null (Caret: ^@, C: \0) */ + NUL: '\x00', + /** Start of Heading (Caret: ^A) */ + SOH: '\x01', + /** Start of Text (Caret: ^B) */ + STX: '\x02', + /** End of Text (Caret: ^C) */ + ETX: '\x03', + /** End of Transmission (Caret: ^D) */ + EOT: '\x04', + /** Enquiry (Caret: ^E) */ + ENQ: '\x05', + /** Acknowledge (Caret: ^F) */ + ACK: '\x06', + /** Bell (Caret: ^G, C: \a) */ + BEL: '\x07', + /** Backspace (Caret: ^H, C: \b) */ + BS: '\x08', + /** Character Tabulation, Horizontal Tabulation (Caret: ^I, C: \t) */ + HT: '\x09', + /** Line Feed (Caret: ^J, C: \n) */ + LF: '\x0a', + /** Line Tabulation, Vertical Tabulation (Caret: ^K, C: \v) */ + VT: '\x0b', + /** Form Feed (Caret: ^L, C: \f) */ + FF: '\x0c', + /** Carriage Return (Caret: ^M, C: \r) */ + CR: '\x0d', + /** Shift Out (Caret: ^N) */ + SO: '\x0e', + /** Shift In (Caret: ^O) */ + SI: '\x0f', + /** Data Link Escape (Caret: ^P) */ + DLE: '\x10', + /** Device Control One (XON) (Caret: ^Q) */ + DC1: '\x11', + /** Device Control Two (Caret: ^R) */ + DC2: '\x12', + /** Device Control Three (XOFF) (Caret: ^S) */ + DC3: '\x13', + /** Device Control Four (Caret: ^T) */ + DC4: '\x14', + /** Negative Acknowledge (Caret: ^U) */ + NAK: '\x15', + /** Synchronous Idle (Caret: ^V) */ + SYN: '\x16', + /** End of Transmission Block (Caret: ^W) */ + ETB: '\x17', + /** Cancel (Caret: ^X) */ + CAN: '\x18', + /** End of Medium (Caret: ^Y) */ + EM: '\x19', + /** Substitute (Caret: ^Z) */ + SUB: '\x1a', + /** Escape (Caret: ^[, C: \e) */ + ESC: '\x1b', + /** File Separator (Caret: ^\) */ + FS: '\x1c', + /** Group Separator (Caret: ^]) */ + GS: '\x1d', + /** Record Separator (Caret: ^^) */ + RS: '\x1e', + /** Unit Separator (Caret: ^_) */ + US: '\x1f', + /** Space */ + SP: '\x20', + /** Delete (Caret: ^?) */ + DEL: '\x7f' +}; diff --git a/src/xterm.js b/src/xterm.js index 1d220652..0d9e1ca8 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -15,6 +15,7 @@ import { EventEmitter } from './EventEmitter.js'; import { Viewport } from './Viewport.js'; import { rightClickHandler, pasteHandler, copyHandler } from './handlers/Clipboard.js'; import { CircularList } from './utils/CircularList.js'; +import { C0 } from './EscapeSequences'; import * as Browser from './utils/Browser'; import * as Keyboard from './utils/Keyboard'; @@ -384,7 +385,7 @@ Terminal.prototype.setOption = function(key, value) { Terminal.bindFocus = function (term) { on(term.textarea, 'focus', function (ev) { if (term.sendFocus) { - term.send('\x1b[I'); + term.send(C0.ESC + '[I'); } term.element.classList.add('focus'); term.showCursor(); @@ -409,7 +410,7 @@ Terminal.bindBlur = function (term) { on(term.textarea, 'blur', function (ev) { term.refresh(term.y, term.y); if (term.sendFocus) { - term.send('\x1b[O'); + term.send(C0.ESC + '[O'); } term.element.classList.remove('focus'); Terminal.focus = null; @@ -742,7 +743,7 @@ Terminal.prototype.bindMouse = function() { button &= 3; pos.x -= 32; pos.y -= 32; - var data = '\x1b[24'; + var data = C0.ESC + '[24'; if (button === 0) data += '1'; else if (button === 1) data += '3'; else if (button === 2) data += '5'; @@ -762,7 +763,7 @@ Terminal.prototype.bindMouse = function() { else if (button === 1) button = 4; else if (button === 2) button = 6; else if (button === 3) button = 3; - self.send('\x1b[' + self.send(C0.ESC + '[' + button + ';' + (button === 3 ? 4 : 0) @@ -781,14 +782,14 @@ Terminal.prototype.bindMouse = function() { pos.y -= 32; pos.x++; pos.y++; - self.send('\x1b[' + button + ';' + pos.x + ';' + pos.y + 'M'); + self.send(C0.ESC + '[' + button + ';' + pos.x + ';' + pos.y + 'M'); return; } if (self.sgrMouse) { pos.x -= 32; pos.y -= 32; - self.send('\x1b[<' + self.send(C0.ESC + '[<' + (((button & 3) === 3 ? button & ~3 : button) - 32) + ';' + pos.x @@ -804,7 +805,7 @@ Terminal.prototype.bindMouse = function() { encode(data, pos.x); encode(data, pos.y); - self.send('\x1b[M' + String.fromCharCode.apply(String, data)); + self.send(C0.ESC + '[M' + String.fromCharCode.apply(String, data)); } function getButton(ev) { @@ -1376,14 +1377,13 @@ Terminal.prototype.write = function(data) { switch (this.state) { case normal: switch (ch) { - case '\x07': + case C0.BEL: this.bell(); break; - // '\n', '\v', '\f' - case '\n': - case '\x0b': - case '\x0c': + case C0.LF: + case C0.VT: + case C0.FF: if (this.convertEol) { this.x = 0; } @@ -1395,34 +1395,34 @@ Terminal.prototype.write = function(data) { break; // '\r' - case '\r': + case C0.CR: this.x = 0; break; // '\b' - case '\x08': + case C0.BS: if (this.x > 0) { this.x--; } break; // '\t' - case '\t': + case C0.HT: this.x = this.nextStop(); break; // shift out - case '\x0e': + case C0.SO: this.setgLevel(1); break; // shift in - case '\x0f': + case C0.SI: this.setgLevel(0); break; // '\e' - case '\x1b': + case C0.ESC: this.state = escaped; break; @@ -1751,8 +1751,8 @@ Terminal.prototype.write = function(data) { // OSC Ps ; Pt ST // OSC Ps ; Pt BEL // Set Text Parameters. - if (ch === '\x1b' || ch === '\x07') { - if (ch === '\x1b') i++; + if (ch === C0.ESC || ch === C0.BEL) { + if (ch === C0.ESC) i++; this.params.push(this.currentParam); @@ -2284,8 +2284,8 @@ Terminal.prototype.write = function(data) { break; case dcs: - if (ch === '\x1b' || ch === '\x07') { - if (ch === '\x1b') i++; + if (ch === C0.ESC || ch === C0.BEL) { + if (ch === C0.ESC) i++; switch (this.prefix) { // User-Defined Keys (DECUDK). @@ -2329,7 +2329,7 @@ Terminal.prototype.write = function(data) { break; } - this.send('\x1bP' + +valid + '$r' + pt + '\x1b\\'); + this.send(C0.ESC + 'P' + +valid + '$r' + pt + C0.ESC + '\\'); break; // Set Termcap/Terminfo Data (xterm, experimental). @@ -2344,7 +2344,7 @@ Terminal.prototype.write = function(data) { var pt = this.currentParam , valid = false; - this.send('\x1bP' + +valid + '+r' + pt + '\x1b\\'); + this.send(C0.ESC + 'P' + +valid + '+r' + pt + C0.ESC + '\\'); break; default: @@ -2370,8 +2370,8 @@ Terminal.prototype.write = function(data) { case ignore: // For PM and APC. - if (ch === '\x1b' || ch === '\x07') { - if (ch === '\x1b') i++; + if (ch === C0.ESC || ch === C0.BEL) { + if (ch === C0.ESC) i++; this.state = normal; } break; @@ -2471,90 +2471,90 @@ Terminal.prototype.evaluateKeyEscapeSequence = function(ev) { case 8: // backspace if (ev.shiftKey) { - result.key = '\x08'; // ^H + result.key = C0.BS; // ^H break; } - result.key = '\x7f'; // ^? + result.key = C0.DEL; // ^? break; case 9: // tab if (ev.shiftKey) { - result.key = '\x1b[Z'; + result.key = C0.ESC + '[Z'; break; } - result.key = '\t'; + result.key = C0.HT; result.cancel = true; break; case 13: // return/enter - result.key = '\r'; + result.key = C0.CR; result.cancel = true; break; case 27: // escape - result.key = '\x1b'; + result.key = C0.ESC; result.cancel = true; break; case 37: // left-arrow if (modifiers) { - result.key = '\x1b[1;' + (modifiers + 1) + 'D'; + 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 == '\x1b[1;3D') { - result.key = (this.browser.isMac) ? '\x1bb' : '\x1b[1;5D'; + 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 = '\x1bOD'; + result.key = C0.ESC + 'OD'; } else { - result.key = '\x1b[D'; + result.key = C0.ESC + '[D'; } break; case 39: // right-arrow if (modifiers) { - result.key = '\x1b[1;' + (modifiers + 1) + 'C'; + 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 == '\x1b[1;3C') { - result.key = (this.browser.isMac) ? '\x1bf' : '\x1b[1;5C'; + 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 = '\x1bOC'; + result.key = C0.ESC + 'OC'; } else { - result.key = '\x1b[C'; + result.key = C0.ESC + '[C'; } break; case 38: // up-arrow if (modifiers) { - result.key = '\x1b[1;' + (modifiers + 1) + 'A'; + 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 == '\x1b[1;3A') { - result.key = '\x1b[1;5A'; + if (result.key == C0.ESC + '[1;3A') { + result.key = C0.ESC + '[1;5A'; } } else if (this.applicationCursor) { - result.key = '\x1bOA'; + result.key = C0.ESC + 'OA'; } else { - result.key = '\x1b[A'; + result.key = C0.ESC + '[A'; } break; case 40: // down-arrow if (modifiers) { - result.key = '\x1b[1;' + (modifiers + 1) + 'B'; + 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 == '\x1b[1;3B') { - result.key = '\x1b[1;5B'; + if (result.key == C0.ESC + '[1;3B') { + result.key = C0.ESC + '[1;5B'; } } else if (this.applicationCursor) { - result.key = '\x1bOB'; + result.key = C0.ESC + 'OB'; } else { - result.key = '\x1b[B'; + result.key = C0.ESC + '[B'; } break; case 45: @@ -2562,41 +2562,41 @@ Terminal.prototype.evaluateKeyEscapeSequence = function(ev) { if (!ev.shiftKey && !ev.ctrlKey) { // or + are used to // copy-paste on some systems. - result.key = '\x1b[2~'; + result.key = C0.ESC + '[2~'; } break; case 46: // delete if (modifiers) { - result.key = '\x1b[3;' + (modifiers + 1) + '~'; + result.key = C0.ESC + '[3;' + (modifiers + 1) + '~'; } else { - result.key = '\x1b[3~'; + result.key = C0.ESC + '[3~'; } break; case 36: // home if (modifiers) - result.key = '\x1b[1;' + (modifiers + 1) + 'H'; + result.key = C0.ESC + '[1;' + (modifiers + 1) + 'H'; else if (this.applicationCursor) - result.key = '\x1bOH'; + result.key = C0.ESC + 'OH'; else - result.key = '\x1b[H'; + result.key = C0.ESC + '[H'; break; case 35: // end if (modifiers) - result.key = '\x1b[1;' + (modifiers + 1) + 'F'; + result.key = C0.ESC + '[1;' + (modifiers + 1) + 'F'; else if (this.applicationCursor) - result.key = '\x1bOF'; + result.key = C0.ESC + 'OF'; else - result.key = '\x1b[F'; + result.key = C0.ESC + '[F'; break; case 33: // page up if (ev.shiftKey) { result.scrollDisp = -(this.rows - 1); } else { - result.key = '\x1b[5~'; + result.key = C0.ESC + '[5~'; } break; case 34: @@ -2604,92 +2604,92 @@ Terminal.prototype.evaluateKeyEscapeSequence = function(ev) { if (ev.shiftKey) { result.scrollDisp = this.rows - 1; } else { - result.key = '\x1b[6~'; + result.key = C0.ESC + '[6~'; } break; case 112: // F1-F12 if (modifiers) { - result.key = '\x1b[1;' + (modifiers + 1) + 'P'; + result.key = C0.ESC + '[1;' + (modifiers + 1) + 'P'; } else { - result.key = '\x1bOP'; + result.key = C0.ESC + 'OP'; } break; case 113: if (modifiers) { - result.key = '\x1b[1;' + (modifiers + 1) + 'Q'; + result.key = C0.ESC + '[1;' + (modifiers + 1) + 'Q'; } else { - result.key = '\x1bOQ'; + result.key = C0.ESC + 'OQ'; } break; case 114: if (modifiers) { - result.key = '\x1b[1;' + (modifiers + 1) + 'R'; + result.key = C0.ESC + '[1;' + (modifiers + 1) + 'R'; } else { - result.key = '\x1bOR'; + result.key = C0.ESC + 'OR'; } break; case 115: if (modifiers) { - result.key = '\x1b[1;' + (modifiers + 1) + 'S'; + result.key = C0.ESC + '[1;' + (modifiers + 1) + 'S'; } else { - result.key = '\x1bOS'; + result.key = C0.ESC + 'OS'; } break; case 116: if (modifiers) { - result.key = '\x1b[15;' + (modifiers + 1) + '~'; + result.key = C0.ESC + '[15;' + (modifiers + 1) + '~'; } else { - result.key = '\x1b[15~'; + result.key = C0.ESC + '[15~'; } break; case 117: if (modifiers) { - result.key = '\x1b[17;' + (modifiers + 1) + '~'; + result.key = C0.ESC + '[17;' + (modifiers + 1) + '~'; } else { - result.key = '\x1b[17~'; + result.key = C0.ESC + '[17~'; } break; case 118: if (modifiers) { - result.key = '\x1b[18;' + (modifiers + 1) + '~'; + result.key = C0.ESC + '[18;' + (modifiers + 1) + '~'; } else { - result.key = '\x1b[18~'; + result.key = C0.ESC + '[18~'; } break; case 119: if (modifiers) { - result.key = '\x1b[19;' + (modifiers + 1) + '~'; + result.key = C0.ESC + '[19;' + (modifiers + 1) + '~'; } else { - result.key = '\x1b[19~'; + result.key = C0.ESC + '[19~'; } break; case 120: if (modifiers) { - result.key = '\x1b[20;' + (modifiers + 1) + '~'; + result.key = C0.ESC + '[20;' + (modifiers + 1) + '~'; } else { - result.key = '\x1b[20~'; + result.key = C0.ESC + '[20~'; } break; case 121: if (modifiers) { - result.key = '\x1b[21;' + (modifiers + 1) + '~'; + result.key = C0.ESC + '[21;' + (modifiers + 1) + '~'; } else { - result.key = '\x1b[21~'; + result.key = C0.ESC + '[21~'; } break; case 122: if (modifiers) { - result.key = '\x1b[23;' + (modifiers + 1) + '~'; + result.key = C0.ESC + '[23;' + (modifiers + 1) + '~'; } else { - result.key = '\x1b[23~'; + result.key = C0.ESC + '[23~'; } break; case 123: if (modifiers) { - result.key = '\x1b[24;' + (modifiers + 1) + '~'; + result.key = C0.ESC + '[24;' + (modifiers + 1) + '~'; } else { - result.key = '\x1b[24~'; + result.key = C0.ESC + '[24~'; } break; default: @@ -2719,11 +2719,11 @@ Terminal.prototype.evaluateKeyEscapeSequence = function(ev) { } 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 = '\x1b' + String.fromCharCode(ev.keyCode + 32); + result.key = C0.ESC + String.fromCharCode(ev.keyCode + 32); } else if (ev.keyCode === 192) { - result.key = '\x1b`'; + result.key = C0.ESC + '`'; } else if (ev.keyCode >= 48 && ev.keyCode <= 57) { - result.key = '\x1b' + (ev.keyCode - 48); + result.key = C0.ESC + (ev.keyCode - 48); } } break; @@ -3583,11 +3583,11 @@ Terminal.prototype.deviceStatus = function(params) { switch (params[0]) { case 5: // status report - this.send('\x1b[0n'); + this.send(C0.ESC + '[0n'); break; case 6: // cursor position - this.send('\x1b[' + this.send(C0.ESC + '[' + (this.y + 1) + ';' + (this.x + 1) @@ -3600,7 +3600,7 @@ Terminal.prototype.deviceStatus = function(params) { switch (params[0]) { case 6: // cursor position - this.send('\x1b[?' + this.send(C0.ESC + '[?' + (this.y + 1) + ';' + (this.x + 1) @@ -3608,19 +3608,19 @@ Terminal.prototype.deviceStatus = function(params) { break; case 15: // no printer - // this.send('\x1b[?11n'); + // this.send(C0.ESC + '[?11n'); break; case 25: // dont support user defined keys - // this.send('\x1b[?21n'); + // this.send(C0.ESC + '[?21n'); break; case 26: // north american keyboard - // this.send('\x1b[?27;1;0;0n'); + // this.send(C0.ESC + '[?27;1;0;0n'); break; case 53: // no dec locator/mouse - // this.send('\x1b[?50n'); + // this.send(C0.ESC + '[?50n'); break; } } @@ -3871,24 +3871,24 @@ Terminal.prototype.sendDeviceAttributes = function(params) { if (this.is('xterm') || this.is('rxvt-unicode') || this.is('screen')) { - this.send('\x1b[?1;2c'); + this.send(C0.ESC + '[?1;2c'); } else if (this.is('linux')) { - this.send('\x1b[?6c'); + this.send(C0.ESC + '[?6c'); } } else if (this.prefix === '>') { // xterm and urxvt // seem to spit this // out around ~370 times (?). if (this.is('xterm')) { - this.send('\x1b[>0;276;0c'); + this.send(C0.ESC + '[>0;276;0c'); } else if (this.is('rxvt-unicode')) { - this.send('\x1b[>85;95;0c'); + this.send(C0.ESC + '[>85;95;0c'); } else if (this.is('linux')) { // not supported by linux console. // linux console echoes parameters. this.send(params[0] + 'c'); } else if (this.is('screen')) { - this.send('\x1b[>83;40003;0c'); + this.send(C0.ESC + '[>83;40003;0c'); } } }; From 1685cf2d8f4902c65da5f07475a7fd37252d420d Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 8 Jan 2017 16:02:47 -0800 Subject: [PATCH 24/33] Use namespace and const for C0 constants --- src/EscapeSequences.ts | 138 ++++++++++++++++++++--------------------- 1 file changed, 69 insertions(+), 69 deletions(-) diff --git a/src/EscapeSequences.ts b/src/EscapeSequences.ts index d4b736fd..34dfde90 100644 --- a/src/EscapeSequences.ts +++ b/src/EscapeSequences.ts @@ -1,74 +1,74 @@ /** * C0 control codes - * See: https://en.wikipedia.org/wiki/C0_and_C1_control_codes + * See = https://en.wikipedia.org/wiki/C0_and_C1_control_codes */ -export const C0 = { - /** Null (Caret: ^@, C: \0) */ - NUL: '\x00', - /** Start of Heading (Caret: ^A) */ - SOH: '\x01', - /** Start of Text (Caret: ^B) */ - STX: '\x02', - /** End of Text (Caret: ^C) */ - ETX: '\x03', - /** End of Transmission (Caret: ^D) */ - EOT: '\x04', - /** Enquiry (Caret: ^E) */ - ENQ: '\x05', - /** Acknowledge (Caret: ^F) */ - ACK: '\x06', - /** Bell (Caret: ^G, C: \a) */ - BEL: '\x07', - /** Backspace (Caret: ^H, C: \b) */ - BS: '\x08', - /** Character Tabulation, Horizontal Tabulation (Caret: ^I, C: \t) */ - HT: '\x09', - /** Line Feed (Caret: ^J, C: \n) */ - LF: '\x0a', - /** Line Tabulation, Vertical Tabulation (Caret: ^K, C: \v) */ - VT: '\x0b', - /** Form Feed (Caret: ^L, C: \f) */ - FF: '\x0c', - /** Carriage Return (Caret: ^M, C: \r) */ - CR: '\x0d', - /** Shift Out (Caret: ^N) */ - SO: '\x0e', - /** Shift In (Caret: ^O) */ - SI: '\x0f', - /** Data Link Escape (Caret: ^P) */ - DLE: '\x10', - /** Device Control One (XON) (Caret: ^Q) */ - DC1: '\x11', - /** Device Control Two (Caret: ^R) */ - DC2: '\x12', - /** Device Control Three (XOFF) (Caret: ^S) */ - DC3: '\x13', - /** Device Control Four (Caret: ^T) */ - DC4: '\x14', - /** Negative Acknowledge (Caret: ^U) */ - NAK: '\x15', - /** Synchronous Idle (Caret: ^V) */ - SYN: '\x16', - /** End of Transmission Block (Caret: ^W) */ - ETB: '\x17', - /** Cancel (Caret: ^X) */ - CAN: '\x18', - /** End of Medium (Caret: ^Y) */ - EM: '\x19', - /** Substitute (Caret: ^Z) */ - SUB: '\x1a', - /** Escape (Caret: ^[, C: \e) */ - ESC: '\x1b', - /** File Separator (Caret: ^\) */ - FS: '\x1c', - /** Group Separator (Caret: ^]) */ - GS: '\x1d', - /** Record Separator (Caret: ^^) */ - RS: '\x1e', - /** Unit Separator (Caret: ^_) */ - US: '\x1f', +export namespace C0 { + /** Null (Caret = ^@, C = \0) */ + export const NUL = '\x00'; + /** Start of Heading (Caret = ^A) */ + export const SOH = '\x01'; + /** Start of Text (Caret = ^B) */ + export const STX = '\x02'; + /** End of Text (Caret = ^C) */ + export const ETX = '\x03'; + /** End of Transmission (Caret = ^D) */ + export const EOT = '\x04'; + /** Enquiry (Caret = ^E) */ + export const ENQ = '\x05'; + /** Acknowledge (Caret = ^F) */ + export const ACK = '\x06'; + /** Bell (Caret = ^G, C = \a) */ + export const BEL = '\x07'; + /** Backspace (Caret = ^H, C = \b) */ + export const BS = '\x08'; + /** Character Tabulation, Horizontal Tabulation (Caret = ^I, C = \t) */ + export const HT = '\x09'; + /** Line Feed (Caret = ^J, C = \n) */ + export const LF = '\x0a'; + /** Line Tabulation, Vertical Tabulation (Caret = ^K, C = \v) */ + export const VT = '\x0b'; + /** Form Feed (Caret = ^L, C = \f) */ + export const FF = '\x0c'; + /** Carriage Return (Caret = ^M, C = \r) */ + export const CR = '\x0d'; + /** Shift Out (Caret = ^N) */ + export const SO = '\x0e'; + /** Shift In (Caret = ^O) */ + export const SI = '\x0f'; + /** Data Link Escape (Caret = ^P) */ + export const DLE = '\x10'; + /** Device Control One (XON) (Caret = ^Q) */ + export const DC1 = '\x11'; + /** Device Control Two (Caret = ^R) */ + export const DC2 = '\x12'; + /** Device Control Three (XOFF) (Caret = ^S) */ + export const DC3 = '\x13'; + /** Device Control Four (Caret = ^T) */ + export const DC4 = '\x14'; + /** Negative Acknowledge (Caret = ^U) */ + export const NAK = '\x15'; + /** Synchronous Idle (Caret = ^V) */ + export const SYN = '\x16'; + /** End of Transmission Block (Caret = ^W) */ + export const ETB = '\x17'; + /** Cancel (Caret = ^X) */ + export const CAN = '\x18'; + /** End of Medium (Caret = ^Y) */ + export const EM = '\x19'; + /** Substitute (Caret = ^Z) */ + export const SUB = '\x1a'; + /** Escape (Caret = ^[, C = \e) */ + export const ESC = '\x1b'; + /** File Separator (Caret = ^\) */ + export const FS = '\x1c'; + /** Group Separator (Caret = ^]) */ + export const GS = '\x1d'; + /** Record Separator (Caret = ^^) */ + export const RS = '\x1e'; + /** Unit Separator (Caret = ^_) */ + export const US = '\x1f'; /** Space */ - SP: '\x20', - /** Delete (Caret: ^?) */ - DEL: '\x7f' + export const SP = '\x20'; + /** Delete (Caret = ^?) */ + export const DEL = '\x7f'; }; From 06852cd9043df87e248a498dd0186bb4ab4ed58b Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 8 Jan 2017 16:46:23 -0800 Subject: [PATCH 25/33] Revert "Rate limit Viewport.refresh" --- src/Viewport.test.ts | 10 ---------- src/Viewport.ts | 20 +++----------------- 2 files changed, 3 insertions(+), 27 deletions(-) diff --git a/src/Viewport.test.ts b/src/Viewport.test.ts index fc2cd117..5b106b43 100644 --- a/src/Viewport.test.ts +++ b/src/Viewport.test.ts @@ -1,11 +1,6 @@ import { assert } from 'chai'; import { Viewport } from './Viewport'; -class MockWindow { - // Disable refreshLoop in test - public requestAnimationFrame() { } -} - describe('Viewport', () => { var terminal; var viewportElement; @@ -16,7 +11,6 @@ describe('Viewport', () => { const CHARACTER_HEIGHT = 10; beforeEach(() => { - (global).window = new MockWindow(); terminal = { lines: [], rows: 0, @@ -79,14 +73,10 @@ describe('Viewport', () => { terminal.rows = 1; assert.equal(scrollAreaElement.style.height, 0 * CHARACTER_HEIGHT + 'px'); viewport.syncScrollArea(); - assert.ok(viewport.isRefreshQueued); - viewport.refresh(); assert.equal(viewportElement.style.height, 1 * CHARACTER_HEIGHT + 'px'); assert.equal(scrollAreaElement.style.height, 1 * CHARACTER_HEIGHT + 'px'); terminal.lines.push(''); viewport.syncScrollArea(); - assert.ok(viewport.isRefreshQueued); - viewport.refresh(); assert.equal(viewportElement.style.height, 1 * CHARACTER_HEIGHT + 'px'); assert.equal(scrollAreaElement.style.height, 2 * CHARACTER_HEIGHT + 'px'); }); diff --git a/src/Viewport.ts b/src/Viewport.ts index 32a71dde..3aa1319f 100644 --- a/src/Viewport.ts +++ b/src/Viewport.ts @@ -12,7 +12,6 @@ export class Viewport { private currentRowHeight: number; private lastRecordedBufferLength: number; private lastRecordedViewportHeight: number; - private isRefreshQueued: boolean; /** * Creates a new Viewport. @@ -30,25 +29,12 @@ export class Viewport { this.currentRowHeight = 0; this.lastRecordedBufferLength = 0; this.lastRecordedViewportHeight = 0; - this.isRefreshQueued = false; this.terminal.on('scroll', this.syncScrollArea.bind(this)); this.terminal.on('resize', this.syncScrollArea.bind(this)); this.viewportElement.addEventListener('scroll', this.onScroll.bind(this)); this.syncScrollArea(); - this.refreshLoop(); - } - - /** - * Queues a refresh to be done on next animation frame. - */ - private refreshLoop(): void { - if (this.isRefreshQueued) { - this.refresh(); - this.isRefreshQueued = false; - } - window.requestAnimationFrame(this.refreshLoop.bind(this)); } /** @@ -82,15 +68,15 @@ export class Viewport { if (this.lastRecordedBufferLength !== this.terminal.lines.length) { // If buffer height changed this.lastRecordedBufferLength = this.terminal.lines.length; - this.isRefreshQueued = true; + this.refresh(); } else if (this.lastRecordedViewportHeight !== this.terminal.rows) { // If viewport height changed - this.isRefreshQueued = true; + this.refresh(); } else { // If size has changed, refresh viewport var size = this.charMeasureElement.getBoundingClientRect(); if (size.height !== this.currentRowHeight) { - this.isRefreshQueued = true; + this.refresh(size); } } From 6c29ccfbd1ffc90ceca07130868322b5004af13a Mon Sep 17 00:00:00 2001 From: Tyler Jewell Date: Sun, 8 Jan 2017 17:41:02 -0800 Subject: [PATCH 26/33] Add Codenvy to the list of xTerm supporters --- README.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 608c48ac..f68da0f1 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,8 @@ Xterm.js is used in several world-class applications to provide great terminal e - [**Microsoft Visual Studio Code**](http://code.visualstudio.com/): Modern, versatile and powerful open source code editor that provides an integrated terminal based on xterm.js - [**ttyd**](https://github.com/tsl0922/ttyd): A command-line tool for sharing terminal over the web, with fully-featured terminal emulation based on xterm.js - [**Katacoda**](https://www.katacoda.com/): Katacoda is an Interactive Learning Platform for software developers, covering the latest Cloud Native technologies. -- [**Eclipse Che**](http://www.eclipse.org/che): Developer workspace server, cloud IDE, and Eclipse next-generation IDE +- [**Eclipse Che**](http://www.eclipse.org/che): Developer workspace server, cloud IDE, and Eclipse next-generation IDE. +- [**Codenvy**](http://www.codenvy.com): Cloud workspaces for development teams. Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it in our list. @@ -120,7 +121,11 @@ Visit https://lair.io/sourcelair/xterm and follow the instructions. All developm #### [Eclipse Che](http://www.eclipse.org/che) -You can start Eclipse Che with `docker run eclipse/che start` +You can start Eclipse Che with `docker run eclipse/che start`. + +#### [Codenvy](http://www.codenvy.io) + +You can create a trial account or install an enterprise version with `docker run codenvy/cli start`. ## License Agreement From d9682bd6d6ad8dfff773bfa0bbb4a2289bc1f335 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 10 Jan 2017 11:36:21 -0800 Subject: [PATCH 27/33] Perform CharMeasure.measure async This fixes the edge case where getBoundingClientRect was returning a width and height of 0,0. Fixes #465 --- src/utils/CharMeasure.ts | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/utils/CharMeasure.ts b/src/utils/CharMeasure.ts index 60684a2c..6ca5046a 100644 --- a/src/utils/CharMeasure.ts +++ b/src/utils/CharMeasure.ts @@ -28,24 +28,29 @@ export class CharMeasure extends EventEmitter { } public measure(): void { - const oldWidth = this._width; - const oldHeight = this._height; - if (!this._measureElement) { this._measureElement = document.createElement('span'); this._measureElement.style.position = 'absolute'; this._measureElement.style.top = '0'; this._measureElement.style.left = '-9999em'; this._measureElement.textContent = 'W'; + this._parentElement.appendChild(this._measureElement); + // Perform _doMeasure async if the element was just attached as sometimes + // getBoundingClientRect does not return accurate values without this. + setTimeout(() => this._doMeasure(), 0); + } else { + this._doMeasure(); } + } - this._parentElement.appendChild(this._measureElement); + private _doMeasure(): void { + const oldWidth = this._width; + const oldHeight = this._height; const geometry = this._measureElement.getBoundingClientRect(); - this._width = geometry.width; - this._height = geometry.height; - this._parentElement.removeChild(this._measureElement); - if (this._width !== oldWidth || this._height !== oldHeight) { + if (this._width !== geometry.width || this._height !== geometry.height) { + this._width = geometry.width; + this._height = geometry.height; this.emit('charsizechanged'); } } From 081fe3f301a9146ae7bc20e4392a7f012496314e Mon Sep 17 00:00:00 2001 From: Paris Kasidiaris Date: Sat, 31 Dec 2016 11:52:26 +0200 Subject: [PATCH 28/33] Fix #359 - Introduce build system based on Gulp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Gulp and new dependencies to `package.json` - Add `gulpfile.js` with four tasks: - `tsc`: For building TypeScript sources - `bundle`: For bundling JavaScript modules in a monolith - `sorcery`: For resolving the source map chains back to the original TypeScript files - `build` (`default`): Runs the whole `tsc` → `bundle` → `sorcery` chain - Clean up `Dockerfile`, since `cpio` is not needed any more - Clean up not needed dependencies from `package.json` - Remove `bin/build` - Update `bin/release` to use `npm run build` instead of `./bin/build` --- Dockerfile | 5 --- bin/build | 36 ------------------- bin/prepare-release | 2 +- gulpfile.js | 87 +++++++++++++++++++++++++++++++++++++++++++++ package.json | 14 ++++++-- src/xterm.js | 12 +++---- tsconfig.json | 8 +++-- 7 files changed, 111 insertions(+), 53 deletions(-) delete mode 100755 bin/build create mode 100644 gulpfile.js diff --git a/Dockerfile b/Dockerfile index 36e821bd..1f0db1f9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,6 @@ FROM node:6.9 MAINTAINER Paris Kasidiaris -# Install cpio, used for building -RUN apt-get update \ - && apt-get install -y --no-install-recommends cpio \ - && rm -rf /var/lib/apt/lists/* - # Set the working directory WORKDIR /usr/src/app diff --git a/bin/build b/bin/build deleted file mode 100755 index a01a1b9f..00000000 --- a/bin/build +++ /dev/null @@ -1,36 +0,0 @@ -#! /usr/bin/env bash - -set -e - -# $BUILD_DIR should default to "build" -BUILD_DIR=${BUILD_DIR:=build} - -# Create the build directory -mkdir -p $BUILD_DIR - - -# Clean lib/* to prevent confusion if files were deleted in src/ -rm -rf lib/* - -# Build all TypeScript files (including tests) to lib/ -tsc - -# Concat all xterm.js files into a single file and output as a UMD to $BUILD_DIR/xterm.js -browserify ./lib/xterm.js --standalone Terminal --debug --outfile ./$BUILD_DIR/xterm.js -cat ./$BUILD_DIR/xterm.js | exorcist ./$BUILD_DIR/xterm.js.map -b ./$BUILD_DIR > ./$BUILD_DIR/xterm.temp.js -rm ./$BUILD_DIR/xterm.js -mv ./$BUILD_DIR/xterm.temp.js ./$BUILD_DIR/xterm.js - -# Resolve the chain of sourcemaps so that ./$BUILD_DIR/xterm.js.map points at ./src -sorcery -i $BUILD_DIR/xterm.js - -# Copy all CSS files from src/ to $BUILD_DIR/ and lib/ -cd src -find . -name '*.css' | cpio -pdm ../$BUILD_DIR -find . -name '*.css' | cpio -pdm ../lib -cd .. - -# Copy addons from lib/ to $BUILD_DIR/ -cd lib/addons -find . -name '*.js' | cpio -pdm ../../$BUILD_DIR/addons -cd ../.. diff --git a/bin/prepare-release b/bin/prepare-release index 6bd39d3d..464e65b4 100755 --- a/bin/prepare-release +++ b/bin/prepare-release @@ -21,7 +21,7 @@ CURRENT_BOWER_JSON_VERSION=$(cat bower.json \ # Build xterm.js into `dist` export BUILD_DIR=dist -./bin/build +npm run build # Update AUTHORS file sh bin/generate-authors diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 00000000..144b8831 --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,87 @@ +const browserify = require('browserify'); +const buffer = require('vinyl-buffer'); +const fs = require('fs-extra'); +const gulp = require('gulp'); +const merge = require('merge-stream'); +const sorcery = require('sorcery'); +const source = require('vinyl-source-stream'); +const sourcemaps = require('gulp-sourcemaps'); +const ts = require('gulp-typescript'); +const tsify = require('tsify'); + + +let buildDir = process.env.BUILD_DIR || 'build'; + + +/** + * Compile TypeScript sources to JavaScript files and create a source map file for each TypeScript + * file compiled. + */ +gulp.task('tsc', function () { + // Remove the lib/ directory to prevent confusion if files were deleted in src/ + fs.emptyDirSync('lib'); + + // Build all TypeScript files (including tests) to lib/, based on the configuration defined in + // `tsconfig.json`. + let tsProject = ts.createProject('tsconfig.json'); + let tsResult = tsProject.src().pipe(sourcemaps.init()).pipe(tsProject()); + let tsc = tsResult.js.pipe(sourcemaps.write('.', {includeContent: false, sourceRoot: ''})).pipe(gulp.dest('lib')); + + // Copy all addons from src/ to lib/ + let copyAddons = gulp.src('src/addons/**/*').pipe(gulp.dest('lib/addons')); + + // Copy stylesheets from src/ to lib/ + let copyStylesheets = gulp.src('src/**/*.css').pipe(gulp.dest('lib')); + + return merge(tsc, copyAddons, copyStylesheets); +}); + +/** + * Bundle JavaScript files produced by the `tsc` task, into a single file named `xterm.js` with + * Browserify. + */ +gulp.task('browserify', ['tsc'], function() { + // Ensure that the build directory exists + fs.ensureDirSync(buildDir); + + let browserifyOptions = { + basedir: buildDir, + debug: true, + entries: ['../lib/xterm.js'], + standalone: 'Terminal', + cache: {}, + packageCache: {} + }; + let bundleStream = browserify(browserifyOptions) + .plugin(tsify) + .bundle() + .pipe(source('xterm.js')) + .pipe(buffer()) + .pipe(sourcemaps.init({loadMaps: true, sourceRoot: '..'})) + .pipe(sourcemaps.write('./')) + .pipe(gulp.dest(buildDir)); + + // Copy all add-ons from lib/ to buildDir + let copyAddons = gulp.src('lib/addons/**/*').pipe(gulp.dest(`${buildDir}/addons`)); + + // Copy stylesheets from src/ to lib/ + let copyStylesheets = gulp.src('lib/**/*.css').pipe(gulp.dest(buildDir)); + + return merge(bundleStream, copyAddons, copyStylesheets); +}); + + +/** + * Use `sorcery` to resolve the source map chain and point back to the TypeScript files. + * (Without this task the source maps produced for the JavaScript bundle points into the + * compiled JavaScript files in lib/). + */ +gulp.task('sorcery', ['browserify'], function () { + var chain = sorcery.loadSync(`${buildDir}/xterm.js`); + var map = chain.apply(); + chain.writeSync(); +}); + +gulp.task('build', ['sorcery']); + +gulp.task('default', ['build']); diff --git a/package.json b/package.json index 734bb6fb..d5071a70 100644 --- a/package.json +++ b/package.json @@ -38,18 +38,26 @@ "browserify": "^13.1.0", "chai": "3.5.0", "docdash": "0.4.0", - "exorcist": "^0.4.0", "express": "4.13.4", "express-ws": "2.0.0-rc.1", + "fs-extra": "^1.0.0", "glob": "^7.0.5", + "gulp": "^3.9.1", + "gulp-cli": "^1.2.2", + "gulp-sourcemaps": "^1.9.1", + "gulp-typescript": "^3.1.3", "jsdoc": "3.4.3", + "merge-stream": "^1.0.1", "mocha": "2.5.3", "nodemon": "1.10.2", "pty.js": "0.3.1", "sleep": "^3.0.1", "sorcery": "^0.10.0", + "tsify": "^3.0.0", "tslint": "^4.0.2", - "typescript": "^2.0.3" + "typescript": "^2.0.3", + "vinyl-buffer": "^1.0.0", + "vinyl-source-stream": "^1.1.0" }, "scripts": { "prestart": "npm run build", @@ -58,7 +66,7 @@ "lint": "tslint src/**/*.ts", "test": "mocha --recursive ./lib", "build:docs": "jsdoc -c jsdoc.json", - "build": "./bin/build", + "build": "gulp build", "prepublish": "npm run build" } } diff --git a/src/xterm.js b/src/xterm.js index b6c659c3..2fcf5073 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -10,13 +10,13 @@ * @license MIT */ -import { CompositionHelper } from './CompositionHelper.js'; -import { EventEmitter } from './EventEmitter.js'; -import { Viewport } from './Viewport.js'; -import { rightClickHandler, pasteHandler, copyHandler } from './handlers/Clipboard.js'; -import { CircularList } from './utils/CircularList.js'; +import { CompositionHelper } from './CompositionHelper'; +import { EventEmitter } from './EventEmitter'; +import { Viewport } from './Viewport'; +import { rightClickHandler, pasteHandler, copyHandler } from './handlers/Clipboard'; +import { CircularList } from './utils/CircularList'; import { C0 } from './EscapeSequences'; -import { CharMeasure } from './utils/CharMeasure.js'; +import { CharMeasure } from './utils/CharMeasure'; import * as Browser from './utils/Browser'; import * as Keyboard from './utils/Keyboard'; diff --git a/tsconfig.json b/tsconfig.json index f4a5a1b2..f5a7d66c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,13 +7,17 @@ "outDir": "lib", "sourceMap": true }, + "include": [ + "src/**/*" + ], "exclude": [ - "addons", + "src/addons/**/*", "build", "demo", "dist", "out", "test", - "node_modules" + "node_modules", + "docs" ] } From 0f5f34e8f5367f10dc9fcf5a489f62f209f066cb Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 12 Jan 2017 10:47:57 -0800 Subject: [PATCH 29/33] Move CharMeasure element to the helper container Fixes #470 --- src/xterm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xterm.js b/src/xterm.js index b6c659c3..b92736a9 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -614,7 +614,7 @@ Terminal.prototype.open = function(parent) { } this.parent.appendChild(this.element); - this.charMeasure = new CharMeasure(this.rowContainer); + this.charMeasure = new CharMeasure(this.helperContainer); this.charMeasure.on('charsizechanged', function () { self.updateCharSizeCSS(); }); From 23169e89c8c65464c8bf16374e07b4e7669b843c Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 12 Jan 2017 11:14:14 -0800 Subject: [PATCH 30/33] Add null checks to refresh line and character fetches Fixes #473 --- src/xterm.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/xterm.js b/src/xterm.js index b6c659c3..a46d0152 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -1139,6 +1139,10 @@ Terminal.prototype.refresh = function(start, end) { row = y + this.ydisp; line = this.lines.get(row); + if (!line) { + // Continue if the line is not available, this means a resize is currently in progress + continue; + } out = ''; if (this.y === y - (this.ybase - this.ydisp) @@ -1153,6 +1157,10 @@ Terminal.prototype.refresh = function(start, end) { i = 0; for (; i < width; i++) { + if (!line[i]) { + // Continue if the character is not available, this means a resize is currently in progress + continue; + } data = line[i][0]; ch = line[i][1]; ch_width = line[i][2]; From 3de3912b96dce8ef1d900108cd64412ea19eb11a Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 12 Jan 2017 11:23:17 -0800 Subject: [PATCH 31/33] Add another null check on children[y] --- src/xterm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xterm.js b/src/xterm.js index a46d0152..b01421b1 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -1139,7 +1139,7 @@ Terminal.prototype.refresh = function(start, end) { row = y + this.ydisp; line = this.lines.get(row); - if (!line) { + if (!line || !this.children[y]) { // Continue if the line is not available, this means a resize is currently in progress continue; } From 5a932b2a20db211cc687776d4431e60ffc083c05 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 13 Jan 2017 21:27:19 -0800 Subject: [PATCH 32/33] Apply scrollback via setOption Fixes #476 --- demo/index.html | 7 ++++++- demo/main.js | 10 +++++++--- src/xterm.js | 18 ++++++++++++++++++ 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/demo/index.html b/demo/index.html index 285fe002..764c8442 100644 --- a/demo/index.html +++ b/demo/index.html @@ -16,7 +16,12 @@

Options

- +

+ +

+

+ +

Size

diff --git a/demo/main.js b/demo/main.js index 86fbd314..52da2cef 100644 --- a/demo/main.js +++ b/demo/main.js @@ -8,7 +8,8 @@ var term, var terminalContainer = document.getElementById('terminal-container'), optionElements = { - cursorBlink: document.querySelector('#option-cursor-blink') + cursorBlink: document.querySelector('#option-cursor-blink'), + scrollback: document.querySelector('#option-scrollback') }, colsElement = document.getElementById('cols'), rowsElement = document.getElementById('rows'); @@ -28,6 +29,9 @@ colsElement.addEventListener('change', setTerminalSize); rowsElement.addEventListener('change', setTerminalSize); optionElements.cursorBlink.addEventListener('change', createTerminal); +optionElements.scrollback.addEventListener('change', function () { + terminal.setOption('scrollback', parseInt(optionElements.scrollback.value, 10)); +}); createTerminal(); @@ -37,7 +41,8 @@ function createTerminal() { terminalContainer.removeChild(terminalContainer.children[0]); } term = new Terminal({ - cursorBlink: optionElements.cursorBlink.checked + cursorBlink: optionElements.cursorBlink.checked, + scrollback: parseInt(optionElements.scrollback.value, 10) }); term.on('resize', function (size) { if (!pid) { @@ -78,7 +83,6 @@ function createTerminal() { }); } - function runRealTerminal() { term.attach(socket); term._initialized = true; diff --git a/src/xterm.js b/src/xterm.js index b6c659c3..5881e131 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -405,6 +405,24 @@ Terminal.prototype.setOption = function(key, value) { if (!(key in Terminal.defaults)) { throw new Error('No option with key "' + key + '"'); } + switch (key) { + case 'scrollback': + if (this.options[key] !== value) { + if (this.lines.length > value) { + const amountToTrim = this.lines.length - value; + const needsRefresh = (this.ydisp - amountToTrim < 0); + this.lines.trimStart(amountToTrim); + this.ybase = Math.max(this.ybase - amountToTrim, 0); + this.ydisp = Math.max(this.ydisp - amountToTrim, 0); + if (needsRefresh) { + this.refresh(0, this.rows - 1); + } + } + this.lines.maxLength = value; + this.viewport.syncScrollArea(); + } + break; + } this[key] = value; this.options[key] = value; }; From ac6faf3dd892314ee56ee416219ba8620c41e544 Mon Sep 17 00:00:00 2001 From: Paris Kasidiaris Date: Sat, 14 Jan 2017 19:52:07 +0200 Subject: [PATCH 33/33] Stick gulp-sourcemaps to 1.9.1 Paths became absolute by default at https://github.com/floridoo/gulp-sourcemaps/commit/fb4027a496898b8b7992ea5af9b3d707f1d82d38 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d5071a70..2a043a85 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "glob": "^7.0.5", "gulp": "^3.9.1", "gulp-cli": "^1.2.2", - "gulp-sourcemaps": "^1.9.1", + "gulp-sourcemaps": "1.9.1", "gulp-typescript": "^3.1.3", "jsdoc": "3.4.3", "merge-stream": "^1.0.1",