diff --git a/AUTHORS b/AUTHORS index 984b896f..1bd4c87d 100644 --- a/AUTHORS +++ b/AUTHORS @@ -19,45 +19,63 @@ Bill Church Bob Reid bottleofwater Brian Mock +Bruno Ribeito Carson Anderson CHaBou Christian Budde Christensen Christof Marti Christopher Jeffrey coderaiser +Dan Brown +Daniel Griffen Daniel Imms Daniel Risacher Dan Kaplun Darin Morrison +dcylabs +Dominik Csapak Edgar Andrés Margffoy Tuay Elliot Saba +Exile +Felipe Gasper +ficristo Gary Ritchie hiro-su Ian Lewis imoses InDieTasten irokas +Jakob Gillich Jean Bruenn Jeremy Danyow Joao Moreno Joao Moreno +Johannes Zellner Jon Masters Jörg Breitbart Justin Mecham Kirill Merkushev +Luca Lucian Buzzo +Lukas Drgon Maël Nison +Marc Dumais Martin Chloride +Martin Koppehel Martin Wang Matt Bierner Michael Irwin Mikko Karvonen mofux Nicolas Ramz +npezza93 Oleksandr Andriienko Paris Kasidiaris Paris Kasidiaris +Peter Baumgarten +Rick Baker runarberg +Samuel Williams Saswat Das Saul Costa Shuanglei Tao diff --git a/README.md b/README.md index 3ff9b205..1719efb2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # [![xterm.js logo](logo-full.png)](https://xtermjs.org) -[![xterm.js build status](https://api.travis-ci.org/sourcelair/xterm.js.svg)](https://travis-ci.org/sourcelair/xterm.js) [![Coverage Status](https://coveralls.io/repos/github/sourcelair/xterm.js/badge.svg)](https://coveralls.io/github/sourcelair/xterm.js) [![Gitter](https://badges.gitter.im/sourcelair/xterm.js.svg)](https://gitter.im/sourcelair/xterm.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [![jsDelivr Hits](https://data.jsdelivr.com/v1/package/npm/xterm/badge?style=rounded)](https://www.jsdelivr.com/package/npm/xterm) +[![xterm.js build status](https://api.travis-ci.org/xtermjs/xterm.js.svg)](https://travis-ci.org/xtermjs/xterm.js) [![Coverage Status](https://coveralls.io/repos/github/sourcelair/xterm.js/badge.svg)](https://coveralls.io/github/sourcelair/xterm.js) [![Gitter](https://badges.gitter.im/sourcelair/xterm.js.svg)](https://gitter.im/sourcelair/xterm.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [![jsDelivr Hits](https://data.jsdelivr.com/v1/package/npm/xterm/badge?style=rounded)](https://www.jsdelivr.com/package/npm/xterm) Xterm.js is a terminal front-end component written in JavaScript that works in the browser. @@ -115,6 +115,11 @@ computational environment for Jupyter, supporting interactive data science and s - [**Theia**](https://github.com/theia-ide/theia): Theia is a cloud & desktop IDE framework implemented in TypeScript. - [**Opshell**](https://github.com/ricktbaker/opshell) Ops Helper tool to make life easier working with AWS instances across multiple organizations. - [**Proxmox VE**](https://www.proxmox.com/en/proxmox-ve): Proxmox VE is a complete open-source platform for enterprise virtualization. It uses xterm.js for container terminals and the host shell. +- [**Script Runner**](https://github.com/ioquatix/script-runner): Run scripts (or a shell) in Atom. +- [**Whack Whack Terminal**](https://github.com/Microsoft/WhackWhackTerminal): Terminal emulator for Visual Studio 2017. +- [**VTerm**](https://github.com/vterm/vterm): Extensible terminal emulator based on Electron and React. +- [**electerm**](http://electerm.html5beta.com): electerm is a terminal/ssh/sftp client(mac, win, linux) based on electron/node-pty/xterm. +- [**Kubebox**](https://github.com/astefanutti/kubebox): Terminal console for Kubernetes clusters. 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. diff --git a/package.json b/package.json index e4bb08c1..1dfadf9f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "xterm", "description": "Full xterm terminal, in your browser", - "version": "2.9.1", + "version": "3.0.0", "ignore": [ "demo", "test", @@ -9,7 +9,7 @@ ], "main": "lib/Terminal.js", "types": "typings/xterm.d.ts", - "repository": "https://github.com/sourcelair/xterm.js", + "repository": "https://github.com/xtermjs/xterm.js", "license": "MIT", "files": [ "*.js", @@ -35,7 +35,8 @@ "src/*.ts", "src/**/*.js", "src/**/*.js.map", - "src/**/*.ts" + "src/**/*.ts", + "typings/*.d.ts" ], "devDependencies": { "@types/chai": "^3.4.34", diff --git a/src/Parser.ts b/src/Parser.ts index b6493fea..6f4b0932 100644 --- a/src/Parser.ts +++ b/src/Parser.ts @@ -286,36 +286,43 @@ export class Parser { // Single Shift Select of G2 Character Set // ( SS2 is 0x8e). This affects next character only. case 'N': + this._state = ParserState.NORMAL; break; // ESC O // Single Shift Select of G3 Character Set // ( SS3 is 0x8f). This affects next character only. case 'O': + this._state = ParserState.NORMAL; break; // ESC n // Invoke the G2 Character Set as GL (LS2). case 'n': this._terminal.setgLevel(2); + this._state = ParserState.NORMAL; break; // ESC o // Invoke the G3 Character Set as GL (LS3). case 'o': this._terminal.setgLevel(3); + this._state = ParserState.NORMAL; break; // ESC | // Invoke the G3 Character Set as GR (LS3R). case '|': this._terminal.setgLevel(3); + this._state = ParserState.NORMAL; break; // ESC } // Invoke the G2 Character Set as GR (LS2R). case '}': this._terminal.setgLevel(2); + this._state = ParserState.NORMAL; break; // ESC ~ // Invoke the G1 Character Set as GR (LS1R). case '~': this._terminal.setgLevel(1); + this._state = ParserState.NORMAL; break; // ESC 7 Save Cursor (DECSC). diff --git a/src/SelectionManager.test.ts b/src/SelectionManager.test.ts index 8c6ea3e1..a10dc669 100644 --- a/src/SelectionManager.test.ts +++ b/src/SelectionManager.test.ts @@ -14,6 +14,10 @@ import { BufferSet } from './BufferSet'; import { MockTerminal } from './utils/TestUtils.test'; import { LineData, CharData } from './Types'; +class TestMockTerminal extends MockTerminal { + emit(event: string, data: any): void {} +} + class TestSelectionManager extends SelectionManager { constructor( terminal: ITerminal, @@ -48,7 +52,7 @@ describe('SelectionManager', () => { dom = new jsdom.JSDOM(''); window = dom.window; document = window.document; - terminal = new MockTerminal(); + terminal = new TestMockTerminal(); terminal.cols = 80; terminal.rows = 2; terminal.options.scrollback = 100; diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index 387ce8d1..cd6fc996 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -255,7 +255,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager public selectAll(): void { this._model.isSelectAllActive = true; this.refresh(); - this.emit('selection'); + this._terminal.emit('selection'); } /** @@ -528,7 +528,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager this._terminal.scrollLines(this._dragScrollAmount, false); // Re-evaluate selection if (this._dragScrollAmount > 0) { - this._model.selectionEnd = [this._terminal.cols - 1, this._terminal.buffer.ydisp + this._terminal.rows]; + this._model.selectionEnd = [this._terminal.cols - 1, Math.min(this._terminal.buffer.ydisp + this._terminal.rows, this._terminal.buffer.lines.length - 1)]; } else { this._model.selectionEnd = [0, this._terminal.buffer.ydisp]; } @@ -544,7 +544,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager this._removeMouseDownListeners(); if (this.hasSelection) - this.emit('selection'); + this._terminal.emit('selection'); } /** diff --git a/src/Terminal.ts b/src/Terminal.ts index dbffb8a0..118b3fc1 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -647,7 +647,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.on('resize', () => this.renderer.onResize(this.cols, this.rows, false)); this.on('blur', () => this.renderer.onBlur()); this.on('focus', () => this.renderer.onFocus()); - window.addEventListener('resize', () => this.renderer.onWindowResize(window.devicePixelRatio)); this.charMeasure.on('charsizechanged', () => this.renderer.onResize(this.cols, this.rows, true)); this.renderer.on('resize', (dimensions) => this.viewport.syncScrollArea()); @@ -1463,6 +1462,9 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT if (ev.shiftKey) { result.key = C0.BS; // ^H break; + } else if (ev.altKey) { + result.key = C0.ESC + C0.DEL; // \e ^? + break; } result.key = C0.DEL; // ^? break; diff --git a/src/addons/attach/attach.ts b/src/addons/attach/attach.ts index aade7291..4e0d5941 100644 --- a/src/addons/attach/attach.ts +++ b/src/addons/attach/attach.ts @@ -59,6 +59,9 @@ export function attach(term, socket, bidirectional, buffered) { }; term._sendData = function(data) { + if (socket.readyState !== 1) { + return; + } socket.send(data); }; diff --git a/src/addons/fit/fit.ts b/src/addons/fit/fit.ts index f8e02015..b87ccc07 100644 --- a/src/addons/fit/fit.ts +++ b/src/addons/fit/fit.ts @@ -34,7 +34,7 @@ export function proposeGeometry(term) { }; export function fit(term) { - var geometry = exports.proposeGeometry(term); + var geometry = proposeGeometry(term); if (geometry) { // Force a full render if (term.rows !== geometry.rows || term.cols !== geometry.cols) { diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index 26a57033..e828fbb9 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -14,12 +14,16 @@ import { IRenderLayer, IColorSet, IRenderer, IRenderDimensions } from './Interfa import { LinkRenderLayer } from './LinkRenderLayer'; import { EventEmitter } from '../EventEmitter'; import { RenderDebouncer } from '../utils/RenderDebouncer'; +import { ScreenDprMonitor } from '../utils/ScreenDprMonitor'; export class Renderer extends EventEmitter implements IRenderer { private _renderDebouncer: RenderDebouncer; private _renderLayers: IRenderLayer[]; private _devicePixelRatio: number; + private _screenDprMonitor: ScreenDprMonitor; + private _isPaused: boolean = false; + private _needsFullRefresh: boolean = false; public colorManager: ColorManager; public dimensions: IRenderDimensions; @@ -53,6 +57,23 @@ export class Renderer extends EventEmitter implements IRenderer { this._devicePixelRatio = window.devicePixelRatio; this._updateDimensions(); this._renderDebouncer = new RenderDebouncer(this._terminal, this._renderRows.bind(this)); + + this._screenDprMonitor = new ScreenDprMonitor(); + this._screenDprMonitor.setListener(() => this.onWindowResize(window.devicePixelRatio)); + + // Detect whether IntersectionObserver is detected and enable renderer pause + // and resume based on terminal visibility if so + if ('IntersectionObserver' in window) { + const observer = new IntersectionObserver(e => this.onIntersectionChange(e[0]), {threshold: 0}); + observer.observe(this._terminal.element); + } + } + + public onIntersectionChange(entry: IntersectionObserverEntry): void { + this._isPaused = entry.intersectionRatio === 0; + if (!this._isPaused && this._needsFullRefresh) { + this._terminal.refresh(0, this._terminal.rows - 1); + } } public onWindowResize(devicePixelRatio: number): void { @@ -73,7 +94,11 @@ export class Renderer extends EventEmitter implements IRenderer { l.reset(this._terminal); }); - this._terminal.refresh(0, this._terminal.rows - 1); + if (this._isPaused) { + this._needsFullRefresh = true; + } else { + this._terminal.refresh(0, this._terminal.rows - 1); + } return this.colorManager.colors; } @@ -86,7 +111,11 @@ export class Renderer extends EventEmitter implements IRenderer { this._renderLayers.forEach(l => l.resize(this._terminal, this.dimensions, didCharSizeChange)); // Force a refresh - this._terminal.refresh(0, this._terminal.rows - 1); + if (this._isPaused) { + this._needsFullRefresh = true; + } else { + this._terminal.refresh(0, this._terminal.rows - 1); + } this.emit('resize', { width: this.dimensions.canvasWidth, @@ -99,27 +128,35 @@ export class Renderer extends EventEmitter implements IRenderer { } public onBlur(): void { - this._renderLayers.forEach(l => l.onBlur(this._terminal)); + this._runOperation(l => l.onBlur(this._terminal)); } public onFocus(): void { - this._renderLayers.forEach(l => l.onFocus(this._terminal)); + this._runOperation(l => l.onFocus(this._terminal)); } public onSelectionChanged(start: [number, number], end: [number, number]): void { - this._renderLayers.forEach(l => l.onSelectionChanged(this._terminal, start, end)); + this._runOperation(l => l.onSelectionChanged(this._terminal, start, end)); } public onCursorMove(): void { - this._renderLayers.forEach(l => l.onCursorMove(this._terminal)); + this._runOperation(l => l.onCursorMove(this._terminal)); } public onOptionsChanged(): void { - this._renderLayers.forEach(l => l.onOptionsChanged(this._terminal)); + this._runOperation(l => l.onOptionsChanged(this._terminal)); } public clear(): void { - this._renderLayers.forEach(l => l.reset(this._terminal)); + this._runOperation(l => l.reset(this._terminal)); + } + + private _runOperation(operation: (layer: IRenderLayer) => void): void { + if (this._isPaused) { + this._needsFullRefresh = true; + } else { + this._renderLayers.forEach(l => operation(l)); + } } /** @@ -129,6 +166,10 @@ export class Renderer extends EventEmitter implements IRenderer { * @param {number} end The end row. */ public refreshRows(start: number, end: number): void { + if (this._isPaused) { + this._needsFullRefresh = true; + return; + } this._renderDebouncer.refresh(start, end); } diff --git a/src/utils/ScreenDprMonitor.ts b/src/utils/ScreenDprMonitor.ts new file mode 100644 index 00000000..15f3ac00 --- /dev/null +++ b/src/utils/ScreenDprMonitor.ts @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +export type ScreenDprListener = (newDevicePixelRatio?: number, oldDevicePixelRatio?: number) => void; + +/** + * The screen device pixel ratio monitor allows listening for when the + * window.devicePixelRatio value changes. This is done not with polling but with + * the use of window.matchMedia to watch media queries. When the event fires, + * the listener will be reattached using a different media query to ensure that + * any further changes will register. + * + * The listener should fire on both window zoom changes and switching to a + * monitor with a different DPI. + */ +export class ScreenDprMonitor { + private _currentDevicePixelRatio: number; + private _outerListener: MediaQueryListListener; + private _listener: ScreenDprListener; + private _resolutionMediaMatchList: MediaQueryList; + + public setListener(listener: ScreenDprListener): void { + if (this._listener) { + this.clearListener(); + } + this._listener = listener; + this._outerListener = () => { + this._listener(window.devicePixelRatio, this._currentDevicePixelRatio); + this._updateDpr(); + }; + this._updateDpr(); + } + + private _updateDpr(): void { + // Clear listeners for old DPR + if (this._resolutionMediaMatchList) { + this._resolutionMediaMatchList.removeListener(this._outerListener); + } + // Add listeners for new DPR + this._currentDevicePixelRatio = window.devicePixelRatio; + this._resolutionMediaMatchList = window.matchMedia(`screen and (resolution: ${window.devicePixelRatio}dppx)`); + this._resolutionMediaMatchList.addListener(this._outerListener); + } + + public clearListener(): void { + if (!this._listener) { + return; + } + this._resolutionMediaMatchList.removeListener(this._outerListener); + this._listener = null; + this._outerListener = null; + } +} diff --git a/src/xterm.css b/src/xterm.css index 085d910a..68d0fd7d 100644 --- a/src/xterm.css +++ b/src/xterm.css @@ -95,6 +95,7 @@ /* On OS X this is required in order for the scroll bar to appear fully opaque */ background-color: #000; overflow-y: scroll; + cursor: default; } .xterm canvas {