Merge branch 'master' into 731_screen_reader

This commit is contained in:
Daniel Imms
2018-02-14 09:03:31 -08:00
committed by GitHub
37 changed files with 4164 additions and 133 deletions
+1
View File
@@ -11,6 +11,7 @@ before_install:
- npm install -g npm@5.1.0
env:
matrix:
- NPM_COMMAND=tsc
- NPM_COMMAND=lint
- NPM_COMMAND=test
notifications:
+13
View File
@@ -1,12 +1,15 @@
List of xterm.js contributors. Updated before every release.
Adrian Labbé <adrilabbelol@gmail.com>
Aleksandr Andrienko <aandrienko@codenvy.com>
Aleksandr Andriienko <oandriie@redhat.com>
Alessandro Nadalin <alessandro.nadalin@gmail.com>
Alexander Olsson <noseglid@gmail.com>
Alexey Kontsevoy <alexey@gravitational.com>
Andres Mejia <amejia004@gmail.com>
Anish Athalye <me@anishathalye.com>
Anthony Lapenna <lapenna.anthony@gmail.com>
Antonin Stefanutti <antonin@stefanutti.fr>
Antonis Kalipetis <akalipetis@sourcelair.com>
Anton Skshidlevsky <meefik@gmail.com>
Anton Yurovskykh <anton.yurovskykh@gmail.com>
@@ -19,6 +22,7 @@ Bill Church <billchurch@users.noreply.github.com>
Bob Reid <bobreid@Bobs-MacBook-Pro.local>
bottleofwater <nison.mael+bottleofwater@gmail.com>
Brian Mock <brian@mockbrian.com>
Bruno Ribeiro <b.m.fernandes.ribeiro@gmail.com>
Bruno Ribeito <b.m.fernandes.ribeiro@gmail.com>
Carson Anderson <carson@betterservers.com>
CHaBou <chabup@delean.fr>
@@ -26,6 +30,7 @@ Christian Budde Christensen <budde377@gmail.com>
Christof Marti <chrmarti@microsoft.com>
Christopher Jeffrey <chjjeffrey@gmail.com>
coderaiser <mnemonic.enemy@gmail.com>
Damien Tournoud <damien@platform.sh>
Dan Brown <jdanbrown@gmail.com>
Daniel Griffen <daniel@griffen.io>
Daniel Imms <daimms@microsoft.com>
@@ -46,20 +51,25 @@ imoses <ido@twiggle.com>
InDieTasten <indietasten@gmail.com>
irokas <akasidiari@inf.uth.gr>
Jakob Gillich <jakob@gillich.me>
Jan Kuri <jkuri88@gmail.com>
Jean Bruenn <himself@jeanbruenn.info>
Jeremy Danyow <jedanyow@microsoft.com>
Jianhui Zhao <jianhuizhao329@gmail.com>
Joao Moreno <jomo@microsoft.com>
Joao Moreno <mail@joaomoreno.com>
Johannes Zellner <johannes@nebulon.de>
Jon Masters <jon.masters@sky.com>
Jörg Breitbart <jerch@rockborn.de>
Justin Luk <jluk@users.noreply.github.com>
Justin Mecham <justin@mecham.me>
Kirill Merkushev <lanwen@yandex.ru>
Krasimir Tsonev <krasimir@outset.ws>
Luca <LucaT1@users.noreply.github.com>
Lucian Buzzo <lucian.buzzo@gmail.com>
Lukas Drgon <lukas.drgon@gmail.com>
Maël Nison <nison.mael@gmail.com>
Marc Dumais <marc.dumais@ericsson.com>
Markus F.X.J. Oberhumer <markus@oberhumer.com>
Martin Chloride <i@martincl2.me>
Martin Koppehel <martin.koppehel@st.ovgu.de>
Martin Wang <jiahaow@ca.ibm.com>
@@ -75,10 +85,12 @@ Paris Kasidiaris <paris@sourcelair.com>
Peter Baumgarten <me@peterbaumgarten.com>
Rick Baker <rick@ricktbaker.com>
runarberg <runar@greenqloud.com>
Saad Malik <simfox3@gmail.com>
Samuel Williams <samuel.williams@oriontransfer.co.nz>
Saswat Das <saswatds@users.noreply.github.com>
Saul Costa <saul@codevolve.com>
Shuanglei Tao <tsl0922@gmail.com>
sitzmar <sitzmar@gmail.com>
Steven Silvester <steven.silvester@ieee.org>
stuicey <stuicey@users.noreply.github.com>
t-amqi <umberos@gmail.com>
@@ -90,3 +102,4 @@ Tyler Jewell <tjewell@codenvy.com>
Vincent Woo <me@vincentwoo.com>
yutaka <shigure@refy.net>
YuviPanda <yuvipanda@gmail.com>
ZHAO Xudong <zxdong@gmail.com>
+1 -1
View File
@@ -1,4 +1,4 @@
FROM node:6
FROM node:8
MAINTAINER Paris Kasidiaris <paris@sourcelair.com>
# Set the working directory
+1
View File
@@ -1 +1,2 @@
web: npm start
webpack: npm run watch
+45 -10
View File
@@ -24,7 +24,7 @@ It enables applications to provide fully featured terminals to their users and c
First you need to install the module, we ship exclusively through [npm](https://www.npmjs.com/) so you need that installed and then add xterm.js as a dependency by running:
```
npm install
npm install xterm
```
To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to the head of your html page. Then create a `<div id="terminal"></div>` onto which xterm can attach itself.
@@ -110,7 +110,7 @@ Xterm.js is used in several world-class applications to provide great terminal e
- [**Selenoid UI**](https://github.com/aerokube/selenoid-ui): Simple UI for the scallable golang implementation of Selenium Hub named Selenoid. We use XTerm for streaming logs over websockets from docker containers.
- [**Portainer**](https://portainer.io): Simple management UI for Docker.
- [**SSHy**](https://github.com/stuicey/SSHy): HTML5 Based SSHv2 Web Client with E2E encryption utilising `xterm.js`, SJCL & websockets.
- [**JupyterLab**](https://github.com/jupyterlab/jupyterlab): An extensible
- [**JupyterLab**](https://github.com/jupyterlab/jupyterlab): An extensible
computational environment for Jupyter, supporting interactive data science and scientific computing across all programming languages.
- [**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.
@@ -125,32 +125,67 @@ computational environment for Jupyter, supporting interactive data science and s
- [**rtty**](https://github.com/zhaojh329/rtty): A reverse proxy WebTTY. It is composed of the client and the server.
- [**Pisth**](https://github.com/ColdGrub1384/Pisth): An SFTP and SSH client for iOS
- [**abstruse**](https://github.com/bleenco/abstruse): Abstruse CI is a continuous integration platform based on Node.JS and Docker.
- [**Microsoft SQL Operations Studio**](https://github.com/Microsoft/sqlopsstudio): A data management tool that enables working with SQL Server, Azure SQL DB and SQL DW from Windows, macOS and Linux
- [**FreeMAN**](https://github.com/matthew-matvei/freeman): A free, cross-platform file manager for power users
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.
## Demo
### Linux or macOS
Xterm.js ships with a barebones demo implementation, designed for the development and evaluation of the library only. Exposing the demo to the public as is would introduce security risks for the host.
First, be sure that a C++ compiler such as GCC-C++ or Clang is installed, then run these commands:
Below you can find instructions on how to run the demo on different platforms.
### SourceLair
SourceLair will run the demo and builder in parallel automatically. Just make sure to choose the "Node.js" project type, when cloning the xterm.js repo (or just use this shortcut; https://lair.io/xtermjs/xtermjs).
Then open your project's [Public URL](https://help.sourcelair.com/projects/the-public-url/) to access the demo.
### Docker
First, make sure you have Docker Engine 1.13.0 (or newer) and Docker Compose 1.10.0 (or newer). To run the demo and builder in parallel, run the following command in your terminal:
```
docker-compose up
```
Then open http://0.0.0.0:3000 in a web browser to access the demo. If you prefer a different port than `3000` to access the xterm.js demo, then set the `XTERMJS_PORT` environment variable to the desired port.
### Foreman (or other Procfile runner)
First, be sure that a C++ compiler such as GCC-C++ or Clang is installed, then run the following commands in your terminal:
```
npm install
npm start
foreman start # Replace foreman with "honcho", "forego" etc. depending on your runner
```
Then open http://0.0.0.0:3000 in a web browser.
Then open http://0.0.0.0:3000 in a web browser to access the demo.
### Linux or macOS
First, be sure that a C++ compiler such as GCC-C++ or Clang is installed, then run the following commands in your terminal:
```
npm install
npm start # Run this in its own terminal
npm run watch # Run this in its own terminal
```
Then open http://0.0.0.0:3000 in a web browser to access the demo.
### Windows
First, ensure [node-gyp](https://github.com/nodejs/node-gyp) is installed and configured correctly, then run these commands.
First, ensure [node-gyp](https://github.com/nodejs/node-gyp) is installed and configured correctly, then run the following commands in your terminal:
```
npm install
npm start
npm start # Run this in its own terminal
npm run watch # Run this in its own terminal
```
Then open http://127.0.0.1:3000 in a web browser.
Then open http://127.0.0.1:3000 in a web browser to access the demo.
*Note: Do not use ConEmu, as it seems to break the demo for some reason.*
@@ -174,6 +209,6 @@ To contribute either code, documentation or issues to xterm.js please read the [
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.
Copyright (c) 2017-2018, [The xterm.js authors](https://github.com/xtermjs/xterm.js/graphs/contributors) (MIT License)
Copyright (c) 2014-2017, SourceLair, Private Company ([www.sourcelair.com](https://www.sourcelair.com/home)) (MIT License)
Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
+5 -1
View File
@@ -64,6 +64,10 @@
<label for="rows">Rows</label>
<input type="number" id="rows" />
</div>
<div style="display: inline-block; margin-right: 16px;">
<label for="padding">Padding</label>
<input type="number" id="padding" />
</div>
</div>
</div>
<div>
@@ -72,7 +76,7 @@
<label><input type="checkbox" id="option-screen-reader-mode"> screenReaderMode</label>
</p>
</div>
<p><strong>Attention:</strong> The demo is a barebones implementation and is designed for xterm.js evaluation purposes only. Exposing the demo to the public as is would introduce security risks for the host.</p>
<p><strong>Attention:</strong> The demo is a barebones implementation and is designed for the development and evaluation of xterm.js only. Exposing the demo to the public as is would introduce security risks for the host.</p>
<script src="dist/bundle.js" defer ></script>
</body>
</html>
+11 -6
View File
@@ -34,23 +34,27 @@ var terminalContainer = document.getElementById('terminal-container'),
screenReaderMode: document.querySelector('#option-screen-reader-mode')
},
colsElement = document.getElementById('cols'),
rowsElement = document.getElementById('rows');
rowsElement = document.getElementById('rows'),
paddingElement = document.getElementById('padding');
function setTerminalSize() {
var cols = parseInt(colsElement.value, 10);
var rows = parseInt(rowsElement.value, 10);
var viewportElement = document.querySelector('.xterm-viewport');
var scrollBarWidth = viewportElement.offsetWidth - viewportElement.clientWidth;
var width = (cols * term.renderer.dimensions.actualCellWidth + 20 /*room for scrollbar*/).toString() + 'px';
var width = (cols * term.renderer.dimensions.actualCellWidth + term.viewport.scrollBarWidth).toString() + 'px';
var height = (rows * term.renderer.dimensions.actualCellHeight).toString() + 'px';
terminalContainer.style.width = width;
terminalContainer.style.height = height;
term.resize(cols, rows);
term.fit();
}
function setPadding() {
term.element.style.padding = parseInt(paddingElement.value, 10).toString() + 'px';
term.fit();
}
colsElement.addEventListener('change', setTerminalSize);
rowsElement.addEventListener('change', setTerminalSize);
paddingElement.addEventListener('change', setPadding);
actionElements.findNext.addEventListener('keypress', function (e) {
if (e.key === "Enter") {
@@ -124,6 +128,7 @@ function createTerminal() {
setTimeout(function () {
colsElement.value = term.cols;
rowsElement.value = term.rows;
paddingElement.value = 0;
// Set terminal size again to set the specific dimensions on the demo
setTerminalSize();
+7
View File
@@ -7,3 +7,10 @@ services:
- ./:/usr/src/app
ports:
- ${XTERMJS_PORT:3000}:3000
command: ["npm", "start"]
watch:
build: .
volumes:
- ./:/usr/src/app
command: ["npm", "run", "watch"]
+4
View File
@@ -147,6 +147,8 @@ namespace methods_core {
const r23: boolean = t.getOption('macOptionIsMeta');
const r24: string = t.getOption('fontWeight');
const r25: string = t.getOption('fontWeightBold');
const r26: boolean = t.getOption('allowTransparency');
const r27: boolean = t.getOption('rightClickSelectsWord');
}
{
const t: Terminal = new Terminal();
@@ -167,6 +169,7 @@ namespace methods_core {
t.setOption('popOnBell', true);
t.setOption('screenKeys', true);
t.setOption('useFlowControl', true);
t.setOption('allowTransparency', true);
t.setOption('visualBell', true);
t.setOption('colors', ['a', 'b']);
t.setOption('letterSpacing', 1);
@@ -185,6 +188,7 @@ namespace methods_core {
t.setOption('fontFamily', 'foo');
t.setOption('theme', {background: '#ff0000'});
t.setOption('macOptionIsMeta', true);
t.setOption('rightClickSelectsWord', false);
}
}
namespace scrolling {
+4
View File
@@ -192,6 +192,10 @@ gulp.task('webpack', ['build'], function() {
.pipe(gulp.dest('demo/dist/'));
});
gulp.task('watch', ['webpack'], () => {
gulp.watch(['./src/*', './src/**/*'], ['webpack']);
});
/**
* Submit coverage results to coveralls.io
*/
+3522 -4
View File
File diff suppressed because it is too large Load Diff
+5 -4
View File
@@ -1,7 +1,7 @@
{
"name": "xterm",
"description": "Full xterm terminal, in your browser",
"version": "3.1.0-master",
"version": "3.1.0",
"ignore": [
"demo",
"test",
@@ -67,7 +67,7 @@
"nodemon": "1.10.2",
"sorcery": "^0.10.0",
"tslint": "^5.9.1",
"typescript": "~2.4.0",
"typescript": "~2.7.1",
"vinyl-buffer": "^1.0.0",
"vinyl-source-stream": "^1.1.0",
"webpack": "^3.10.0",
@@ -75,17 +75,18 @@
"zmodem.js": "^0.1.5"
},
"scripts": {
"prestart": "gulp webpack",
"start": "node demo/app",
"prestart-zmodem": "npm run build",
"start-zmodem": "node build/addons/zmodem/demo/app",
"lint": "tslint src/*.ts src/**/*.ts",
"test": "gulp test",
"build:docs": "jsdoc -c jsdoc.json",
"tsc": "tsc",
"build": "gulp build",
"prepublish": "npm run build",
"coveralls": "gulp coveralls",
"webpack": "gulp webpack"
"webpack": "gulp webpack",
"watch": "gulp watch"
},
"dependencies": {}
}
+4 -1
View File
@@ -213,7 +213,10 @@ export class Buffer implements IBuffer {
// needed here because some chars are 0 characters long (eg. after wide
// chars) and some chars are longer than 1 characters long (eg. emojis).
let startIndex = startCol;
endCol = endCol || line.length;
// Only set endCol to the line length when it is null. 0 is a valid column.
if (endCol === null) {
endCol = line.length;
}
let endIndex = endCol;
for (let i = 0; i < line.length; i++) {
+22 -12
View File
@@ -63,28 +63,33 @@ export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: nu
let min = 0;
let max = data.length - 1;
let mid;
if (ucs < data[0][0] || ucs > data[max][1])
if (ucs < data[0][0] || ucs > data[max][1]) {
return false;
}
while (max >= min) {
mid = (min + max) >> 1;
if (ucs > data[mid][1])
if (ucs > data[mid][1]) {
min = mid + 1;
else if (ucs < data[mid][0])
} else if (ucs < data[mid][0]) {
max = mid - 1;
else
} else {
return true;
}
}
return false;
}
function wcwidthBMP(ucs: number): number {
// test for 8-bit control characters
if (ucs === 0)
if (ucs === 0) {
return opts.nul;
if (ucs < 32 || (ucs >= 0x7f && ucs < 0xa0))
}
if (ucs < 32 || (ucs >= 0x7f && ucs < 0xa0)) {
return opts.control;
}
// binary search in table of non-spacing characters
if (bisearch(ucs, COMBINING_BMP))
if (bisearch(ucs, COMBINING_BMP)) {
return 0;
}
// if we arrive here, ucs is not a combining or C0/C1 control character
if (isWideBMP(ucs)) {
return 2;
@@ -106,8 +111,9 @@ export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: nu
(ucs >= 0xffe0 && ucs <= 0xffe6)));
}
function wcwidthHigh(ucs: number): 0 | 1 | 2 {
if (bisearch(ucs, COMBINING_HIGH))
if (bisearch(ucs, COMBINING_HIGH)) {
return 0;
}
if ((ucs >= 0x20000 && ucs <= 0x2fffd) || (ucs >= 0x30000 && ucs <= 0x3fffd)) {
return 2;
}
@@ -128,8 +134,9 @@ export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: nu
for (let i = 0; i < CONTAINERSIZE; ++i) {
let num = 0;
let pos = CODEPOINTS_PER_ITEM;
while (pos--)
while (pos--) {
num = (num << 2) | wcwidthBMP(CODEPOINTS_PER_ITEM * i + pos);
}
table[i] = num;
}
return table;
@@ -148,13 +155,16 @@ export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: nu
// ==> n = n & 3 e.g. 000000000000000000000000000000XX
return function (num: number): number {
num = num | 0; // get asm.js like optimization under V8
if (num < 32)
if (num < 32) {
return control | 0;
if (num < 127)
}
if (num < 127) {
return 1;
}
let t = table || init_table();
if (num < 65536)
if (num < 65536) {
return t[num >> 4] >> ((num & 15) << 1) & 3;
}
// do a full search for high codepoints
return wcwidthHigh(num);
};
+2 -1
View File
@@ -73,8 +73,9 @@ export class InputHandler implements IInputHandler {
(<any>this._terminal.buffer.lines.get(this._terminal.buffer.y)).isWrapped = true;
}
} else {
if (chWidth === 2) // FIXME: check for xterm behavior
if (chWidth === 2) { // FIXME: check for xterm behavior
return;
}
}
}
row = this._terminal.buffer.y + this._terminal.buffer.ybase;
+7
View File
@@ -150,6 +150,7 @@ export class Linkifier extends EventEmitter implements ILinkifier {
validationCallback: options.validationCallback,
hoverTooltipCallback: options.tooltipCallback,
hoverLeaveCallback: options.leaveCallback,
willLinkActivate: options.willLinkActivate,
priority: options.priority || 0
};
this._addLinkMatcherToList(matcher);
@@ -290,6 +291,12 @@ export class Linkifier extends EventEmitter implements ILinkifier {
if (matcher.hoverLeaveCallback) {
matcher.hoverLeaveCallback();
}
},
e => {
if (matcher.willLinkActivate) {
return matcher.willLinkActivate(e, uri);
}
return true;
}
));
}
+2 -1
View File
@@ -224,8 +224,9 @@ export class Parser {
ch += data.charAt(this._position + 1);
}
// surrogate low - already handled above
if (0xDC00 <= code && code <= 0xDFFF)
if (0xDC00 <= code && code <= 0xDFFF) {
continue;
}
switch (this._state) {
case ParserState.NORMAL:
+1 -1
View File
@@ -28,7 +28,7 @@ class TestSelectionManager extends SelectionManager {
public get model(): SelectionModel { return this._model; }
public selectLineAt(line: number): void { this._selectLineAt(line); }
public selectWordAt(coords: [number, number]): void { this._selectWordAt(coords); }
public selectWordAt(coords: [number, number]): void { this._selectWordAt(coords, true); }
// Disable DOM interaction
public enable(): void {}
+59 -8
View File
@@ -11,6 +11,7 @@ import { CircularList } from './utils/CircularList';
import { EventEmitter } from './EventEmitter';
import { SelectionModel } from './SelectionModel';
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from './Buffer';
import { AltClickHandler } from './handlers/AltClickHandler';
/**
* The number of pixels the mouse needs to be above or below the viewport in
@@ -28,6 +29,12 @@ const DRAG_SCROLL_MAX_SPEED = 15;
*/
const DRAG_SCROLL_INTERVAL = 50;
/**
* The maximum amount of time that can have elapsed for an alt click to move the
* cursor.
*/
const ALT_CLICK_MOVE_CURSOR_TIME = 500;
/**
* A string containing all characters that are considered word separated by the
* double click to select work logic.
@@ -96,6 +103,8 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
private _mouseUpListener: EventListener;
private _trimListener: XtermListener;
private _mouseDownTimeStamp: number;
constructor(
private _terminal: ITerminal,
private _charMeasure: CharMeasure
@@ -244,6 +253,37 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
this.emit('refresh', { start: this._model.finalSelectionStart, end: this._model.finalSelectionEnd });
}
/**
* Checks if the current click was inside the current selection
* @param event The mouse event
*/
public isClickInSelection(event: MouseEvent): boolean {
const coords = this._getMouseBufferCoords(event);
const start = this._model.finalSelectionStart;
const end = this._model.finalSelectionEnd;
if (!start || !end) {
return false;
}
return (coords[1] > start[1] && coords[1] < end[1]) ||
(start[1] === end[1] && coords[1] === start[1] && coords[0] > start[0] && coords[0] < end[0]) ||
(start[1] < end[1] && coords[1] === end[1] && coords[0] < end[0]);
}
/**
* Selects word at the current mouse event coordinates.
* @param event The mouse event.
*/
public selectWordAtCursor(event: MouseEvent): void {
const coords = this._getMouseBufferCoords(event);
if (coords) {
this._selectWordAt(coords, false);
this._model.selectionEnd = null;
this.refresh(true);
}
}
/**
* Selects all text within the terminal.
*/
@@ -269,7 +309,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
* @param event The mouse event.
*/
private _getMouseBufferCoords(event: MouseEvent): [number, number] {
const coords = this._terminal.mouseHelper.getCoords(event, this._terminal.element, this._charMeasure, this._terminal.options.lineHeight, this._terminal.cols, this._terminal.rows, true);
const coords = this._terminal.mouseHelper.getCoords(event, this._terminal.screenElement, this._charMeasure, this._terminal.options.lineHeight, this._terminal.cols, this._terminal.rows, true);
if (!coords) {
return null;
}
@@ -289,7 +329,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
* @param event The mouse event.
*/
private _getMouseEventScrollAmount(event: MouseEvent): number {
let offset = MouseHelper.getCoordsRelativeToElement(event, this._terminal.element)[1];
let offset = MouseHelper.getCoordsRelativeToElement(event, this._terminal.screenElement)[1];
const terminalHeight = this._terminal.rows * Math.ceil(this._charMeasure.height * this._terminal.options.lineHeight);
if (offset >= 0 && offset <= terminalHeight) {
return 0;
@@ -317,6 +357,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
* @param event The mousedown event.
*/
public onMouseDown(event: MouseEvent): void {
this._mouseDownTimeStamp = event.timeStamp;
// If we have selection, we want the context menu on right click even if the
// terminal is in mouse mode.
if (event.button === 2 && this.hasSelection) {
@@ -435,7 +476,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
const coords = this._getMouseBufferCoords(event);
if (coords) {
this._activeSelectionMode = SelectionMode.WORD;
this._selectWordAt(coords);
this._selectWordAt(coords, true);
}
}
@@ -536,10 +577,15 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
* @param event The mouseup event.
*/
private _onMouseUp(event: MouseEvent): void {
let timeElapsed = event.timeStamp - this._mouseDownTimeStamp;
this._removeMouseDownListeners();
if (this.hasSelection)
if (this.selectionText.length <= 1 && timeElapsed < ALT_CLICK_MOVE_CURSOR_TIME) {
(new AltClickHandler(event, this._terminal)).move();
} else if (this.hasSelection) {
this._terminal.emit('selection');
}
}
private _onBufferActivate(e: {activeBuffer: IBuffer, inactiveBuffer: IBuffer}): void {
@@ -587,7 +633,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
* Gets positional information for the word at the coordinated specified.
* @param coords The coordinates to get the word at.
*/
private _getWordAt(coords: [number, number]): IWordPosition {
private _getWordAt(coords: [number, number], allowWhitespaceOnlySelection: boolean): IWordPosition {
const bufferLine = this._buffer.lines.get(coords[1]);
if (!bufferLine) {
return null;
@@ -693,15 +739,20 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
- leftLongCharOffset // The number of additional chars left of the initial char added by columns with strings longer than 1 (emojis)
- rightLongCharOffset); // The number of additional chars right of the initial char (inclusive) added by columns with strings longer than 1 (emojis)
if (!allowWhitespaceOnlySelection && line.slice(startIndex, endIndex).trim() === '') {
return null;
}
return { start, length };
}
/**
* Selects the word at the coordinates specified.
* @param coords The coordinates to get the word at.
* @param allowWhitespaceOnlySelection If whitespace should be selected
*/
protected _selectWordAt(coords: [number, number]): void {
const wordPosition = this._getWordAt(coords);
protected _selectWordAt(coords: [number, number], allowWhitespaceOnlySelection: boolean): void {
const wordPosition = this._getWordAt(coords, allowWhitespaceOnlySelection);
if (wordPosition) {
this._model.selectionStart = [wordPosition.start, coords[1]];
this._model.selectionStartLength = wordPosition.length;
@@ -713,7 +764,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
* @param coords The coordinates to get the word at.
*/
private _selectToWordAt(coords: [number, number]): void {
const wordPosition = this._getWordAt(coords);
const wordPosition = this._getWordAt(coords, true);
if (wordPosition) {
this._model.selectionEnd = [this._model.areSelectionValuesReversed() ? wordPosition.start : (wordPosition.start + wordPosition.length), coords[1]];
}
+27 -21
View File
@@ -89,8 +89,10 @@ const DEFAULT_OPTIONS: ITerminalOptions = {
cancelEvents: false,
disableStdin: false,
useFlowControl: false,
allowTransparency: false,
tabStopWidth: 8,
theme: null
theme: null,
rightClickSelectsWord: Browser.isMac
// programFeatures: false,
// focusKeys: false,
};
@@ -98,6 +100,7 @@ const DEFAULT_OPTIONS: ITerminalOptions = {
export class Terminal extends EventEmitter implements ITerminal, IInputHandlingTerminal {
public textarea: HTMLTextAreaElement;
public element: HTMLElement;
public screenElement: HTMLElement;
/**
* The HTMLElement that the terminal is created in, set by Terminal.open.
@@ -530,12 +533,12 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
// Firefox doesn't appear to fire the contextmenu event on right click
on(this.element, 'mousedown', (event: MouseEvent) => {
if (event.button === 2) {
rightClickHandler(event, this.textarea, this.selectionManager);
rightClickHandler(event, this.textarea, this.selectionManager, this.options.rightClickSelectsWord);
}
});
} else {
on(this.element, 'contextmenu', (event: MouseEvent) => {
rightClickHandler(event, this.textarea, this.selectionManager);
rightClickHandler(event, this.textarea, this.selectionManager, this.options.rightClickSelectsWord);
});
}
@@ -627,15 +630,18 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
this.viewportScrollArea.classList.add('xterm-scroll-area');
this.viewportElement.appendChild(this.viewportScrollArea);
this._mouseZoneManager = new MouseZoneManager(this);
this.on('scroll', () => this._mouseZoneManager.clearAll());
this.linkifier.attachToDom(this._mouseZoneManager);
this.screenElement = document.createElement('div');
this.screenElement.classList.add('xterm-screen');
// Create the container that will hold helpers like the textarea for
// capturing DOM Events. Then produce the helpers.
this.helperContainer = document.createElement('div');
this.helperContainer.classList.add('xterm-helpers');
fragment.appendChild(this.helperContainer);
this.screenElement.appendChild(this.helperContainer);
fragment.appendChild(this.screenElement);
this._mouseZoneManager = new MouseZoneManager(this);
this.on('scroll', () => this._mouseZoneManager.clearAll());
this.linkifier.attachToDom(this._mouseZoneManager);
this.textarea = document.createElement('textarea');
this.textarea.classList.add('xterm-helper-textarea');
@@ -766,7 +772,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
button = getButton(ev);
// get mouse coordinates
pos = self.mouseHelper.getRawByteCoords(ev, self.element, self.charMeasure, self.options.lineHeight, self.cols, self.rows);
pos = self.mouseHelper.getRawByteCoords(ev, self.screenElement, self.charMeasure, self.options.lineHeight, self.cols, self.rows);
if (!pos) return;
sendEvent(button, pos);
@@ -792,7 +798,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
// ^[[M 3<^[[M@4<^[[M@5<^[[M@6<^[[M@7<^[[M#7<
function sendMove(ev: MouseEvent): void {
let button = pressed;
let pos = self.mouseHelper.getRawByteCoords(ev, self.element, self.charMeasure, self.options.lineHeight, self.cols, self.rows);
let pos = self.mouseHelper.getRawByteCoords(ev, self.screenElement, self.charMeasure, self.options.lineHeight, self.cols, self.rows);
if (!pos) return;
// buttons marked as motions
@@ -1613,28 +1619,28 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
break;
case 36:
// home
if (modifiers)
if (modifiers) {
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'H';
else if (this.applicationCursor)
} else if (this.applicationCursor) {
result.key = C0.ESC + 'OH';
else
} else {
result.key = C0.ESC + '[H';
}
break;
case 35:
// end
if (modifiers)
if (modifiers) {
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'F';
else if (this.applicationCursor)
} else if (this.applicationCursor) {
result.key = C0.ESC + 'OF';
else
} else {
result.key = C0.ESC + '[F';
}
break;
case 33:
// page up
if (ev.shiftKey) {
result.scrollLines = -(this.rows - 1);
} else if (modifiers) {
result.key = C0.ESC + '[5;' + (modifiers + 1) + '~';
} else {
result.key = C0.ESC + '[5~';
}
@@ -1643,8 +1649,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
// page down
if (ev.shiftKey) {
result.scrollLines = this.rows - 1;
} else if (modifiers) {
result.key = C0.ESC + '[6;' + (modifiers + 1) + '~';
} else {
result.key = C0.ESC + '[6~';
}
@@ -1918,7 +1922,9 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
this.rows = y;
this.buffers.setupTabStops(this.cols);
this.charMeasure.measure(this.options);
if (this.charMeasure) {
this.charMeasure.measure(this.options);
}
this.refresh(0, this.rows - 1);
this.emit('resize', {cols: x, rows: y});
+15
View File
@@ -87,6 +87,7 @@ export interface IInputHandlingTerminal extends IEventEmitter {
}
export interface IViewport {
scrollBarWidth: number;
syncScrollArea(): void;
onWheel(ev: WheelEvent): void;
onTouchStart(ev: TouchEvent): void;
@@ -163,6 +164,7 @@ export interface ILinkMatcher {
matchIndex?: number;
validationCallback?: LinkMatcherValidationCallback;
priority?: number;
willLinkActivate?: (event: MouseEvent, uri: string) => boolean;
}
export interface ICharset {
@@ -176,6 +178,7 @@ export interface ILinkHoverEvent {
}
export interface ITerminal extends PublicTerminal, IElementAccessor, IBufferAccessor, ILinkifierAccessor {
screenElement: HTMLElement;
selectionManager: ISelectionManager;
charMeasure: ICharMeasure;
renderer: IRenderer;
@@ -189,13 +192,16 @@ export interface ITerminal extends PublicTerminal, IElementAccessor, IBufferAcce
buffers: IBufferSet;
isFocused: boolean;
mouseHelper: IMouseHelper;
viewport: IViewport;
bracketedPasteMode: boolean;
applicationCursor: boolean;
/**
* Emit the 'data' event and populate the given data.
* @param data The data to populate in the event.
*/
handler(data: string): void;
send(data: string): void;
scrollLines(disp: number, suppressScrollEvent?: boolean): void;
cancel(ev: Event, force?: boolean): boolean | void;
log(text: string): void;
@@ -285,6 +291,8 @@ export interface ISelectionManager {
disable(): void;
enable(): void;
setSelection(row: number, col: number, length: number): void;
isClickInSelection(event: MouseEvent): boolean;
selectWordAtCursor(event: MouseEvent): void;
}
export interface ILinkifier extends IEventEmitter {
@@ -321,6 +329,13 @@ export interface ILinkMatcherOptions {
* default value is 0.
*/
priority?: number;
/**
* A callback that fires when the mousedown and click events occur that
* determines whether a link will be activated upon click. This enables
* only activating a link when a certain modifier is held down, if not the
* mouse event will continue propagation (eg. double click to select word).
*/
willLinkActivate?: (event: MouseEvent, uri: string) => boolean;
}
export interface IBrowser {
+15 -7
View File
@@ -7,11 +7,14 @@ import { IColorSet } from './renderer/Types';
import { ITerminal, IViewport } from './Types';
import { CharMeasure } from './utils/CharMeasure';
const FALLBACK_SCROLL_BAR_WIDTH = 15;
/**
* Represents the viewport of a terminal, the visible area within the larger buffer of output.
* Logic for the virtual scroll bar is included in this object.
*/
export class Viewport implements IViewport {
public scrollBarWidth: number = 0;
private currentRowHeight: number = 0;
private lastRecordedBufferLength: number = 0;
private lastRecordedViewportHeight: number = 0;
@@ -31,6 +34,10 @@ export class Viewport implements IViewport {
private scrollArea: HTMLElement,
private charMeasure: CharMeasure
) {
// Measure the width of the scrollbar. If it is 0 we can assume it's an OSX overlay scrollbar.
// Unfortunately the overlay scrollbar would be hidden underneath the screen element in that case,
// therefore we account for a standard amount to make it visible
this.scrollBarWidth = (this.viewportElement.offsetWidth - this.scrollArea.offsetWidth) || FALLBACK_SCROLL_BAR_WIDTH;
this.viewportElement.addEventListener('scroll', this.onScroll.bind(this));
// Perform this async to ensure the CharMeasure is ready.
@@ -48,13 +55,8 @@ export class Viewport implements IViewport {
private refresh(): void {
if (this.charMeasure.height > 0) {
this.currentRowHeight = this.terminal.renderer.dimensions.scaledCellHeight / window.devicePixelRatio;
if (this.lastRecordedViewportHeight !== this.terminal.renderer.dimensions.canvasHeight) {
this.lastRecordedViewportHeight = this.terminal.renderer.dimensions.canvasHeight;
this.viewportElement.style.height = this.lastRecordedViewportHeight + 'px';
}
const newBufferHeight = Math.round(this.currentRowHeight * this.lastRecordedBufferLength);
this.lastRecordedViewportHeight = this.viewportElement.offsetHeight;
const newBufferHeight = Math.round(this.currentRowHeight * this.lastRecordedBufferLength) + (this.lastRecordedViewportHeight - this.terminal.renderer.dimensions.canvasHeight);
if (this.lastRecordedBufferHeight !== newBufferHeight) {
this.lastRecordedBufferHeight = newBufferHeight;
this.scrollArea.style.height = this.lastRecordedBufferHeight + 'px';
@@ -93,6 +95,12 @@ export class Viewport implements IViewport {
* @param ev The scroll event.
*/
private onScroll(ev: Event): void {
// Don't attempt to scroll if the element is not visible, otherwise scrollTop will be corrupt
// which causes the terminal to scroll the buffer to the top
if (!this.viewportElement.offsetParent) {
return;
}
const newRow = Math.round(this.viewportElement.scrollTop / this.currentRowHeight);
const diff = newRow - this.terminal.buffer.ydisp;
this.terminal.scrollLines(diff, true);
+10 -5
View File
@@ -28,17 +28,22 @@ export function proposeGeometry(term: Terminal): IGeometry {
}
const parentElementStyle = window.getComputedStyle(term.element.parentElement);
const parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height'));
const parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')) - 17);
const parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')));
const elementStyle = window.getComputedStyle(term.element);
const elementPaddingVer = parseInt(elementStyle.getPropertyValue('padding-top')) + parseInt(elementStyle.getPropertyValue('padding-bottom'));
const elementPaddingHor = parseInt(elementStyle.getPropertyValue('padding-right')) + parseInt(elementStyle.getPropertyValue('padding-left'));
const elementPadding = {
top: parseInt(elementStyle.getPropertyValue('padding-top')),
bottom: parseInt(elementStyle.getPropertyValue('padding-bottom')),
right: parseInt(elementStyle.getPropertyValue('padding-right')),
left: parseInt(elementStyle.getPropertyValue('padding-left'))
};
const elementPaddingVer = elementPadding.top + elementPadding.bottom;
const elementPaddingHor = elementPadding.right + elementPadding.left;
const availableHeight = parentElementHeight - elementPaddingVer;
const availableWidth = parentElementWidth - elementPaddingHor;
const availableWidth = parentElementWidth - elementPaddingHor - (<any>term).viewport.scrollBarWidth;
const geometry = {
cols: Math.floor(availableWidth / (<any>term).renderer.dimensions.actualCellWidth),
rows: Math.floor(availableHeight / (<any>term).renderer.dimensions.actualCellHeight)
};
return geometry;
}
+261
View File
@@ -0,0 +1,261 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { ITerminal, ICircularList, LineData } from '../Types';
import { C0 } from '../EscapeSequences';
enum Direction {
Up = 'A',
Down = 'B',
Right = 'C',
Left = 'D'
}
export class AltClickHandler {
private _startRow: number;
private _startCol: number;
private _endRow: number;
private _endCol: number;
private _lines: ICircularList<LineData>;
constructor(
private _mouseEvent: MouseEvent,
private _terminal: ITerminal
) {
this._lines = this._terminal.buffer.lines;
this._startCol = this._terminal.buffer.x;
this._startRow = this._terminal.buffer.y;
[this._endCol, this._endRow] = this._terminal.mouseHelper.getCoords(
this._mouseEvent,
this._terminal.element,
this._terminal.charMeasure,
this._terminal.options.lineHeight,
this._terminal.cols,
this._terminal.rows,
false
).map((coordinate: number) => {
return coordinate - 1;
});
}
/**
* Writes the escape sequences of arrows to the terminal
*/
public move(): void {
if (this._mouseEvent.altKey) {
this._terminal.send(this._arrowSequences());
}
}
/**
* Concatenates all the arrow sequences together.
* Resets the starting row to an unwrapped row, moves to the requested row,
* then moves to requested col.
*/
private _arrowSequences(): string {
return this._resetStartingRow() +
this._moveToRequestedRow() +
this._moveToRequestedCol();
}
/**
* If the initial position of the cursor is on a row that is wrapped, move the
* cursor up to the first row that is not wrapped to have accurate vertical
* positioning.
*/
private _resetStartingRow(): string {
let startRow = this._endRow - this._wrappedRowsForRow(this._endRow);
let endRow = this._endRow;
if (this._moveToRequestedRow().length === 0) {
return '';
} else {
return repeat(this._bufferLine(
this._startCol, this._startRow, this._startCol,
this._startRow - this._wrappedRowsForRow(this._startRow), false
).length, this._sequence(Direction.Left));
}
}
/**
* Using the reset starting and ending row, move to the requested row,
* ignoring wrapped rows
*/
private _moveToRequestedRow(): string {
let startRow = this._startRow - this._wrappedRowsForRow(this._startRow);
let endRow = this._endRow - this._wrappedRowsForRow(this._endRow);
let rowsToMove = Math.abs(startRow - endRow) - this._wrappedRowsCount();
return repeat(rowsToMove, this._sequence(this._verticalDirection()));
}
/**
* Move to the requested col on the ending row
*/
private _moveToRequestedCol(): string {
let startRow;
if (this._moveToRequestedRow().length > 0) {
startRow = this._endRow - this._wrappedRowsForRow(this._endRow);
} else {
startRow = this._startRow;
}
let endRow = this._endRow;
let direction = this._horizontalDirection();
return repeat(this._bufferLine(
this._startCol, startRow, this._endCol, endRow,
direction === Direction.Right
).length, this._sequence(direction));
}
/**
* Utility functions
*/
/**
* Calculates the number of wrapped rows between the unwrapped starting and
* ending rows. These rows need to ignored since the cursor skips over them.
*/
private _wrappedRowsCount(): number {
let wrappedRows = 0;
let startRow = this._startRow - this._wrappedRowsForRow(this._startRow);
let endRow = this._endRow - this._wrappedRowsForRow(this._endRow);
for (let i = 0; i < Math.abs(startRow - endRow); i++) {
let direction = this._verticalDirection() === Direction.Up ? -1 : 1;
if ((<any>this._lines.get(startRow + (direction * i))).isWrapped) {
wrappedRows++;
}
}
return wrappedRows;
}
/**
* Calculates the number of wrapped rows that make up a given row.
* @param currentRow The row to determine how many wrapped rows make it up
*/
private _wrappedRowsForRow(currentRow: number): number {
let rowCount = 0;
let lineWraps = (<any>this._lines.get(currentRow)).isWrapped;
while (lineWraps && currentRow >= 0 && currentRow < this._terminal.rows) {
rowCount++;
currentRow--;
lineWraps = (<any>this._lines.get(currentRow)).isWrapped;
}
return rowCount;
}
/**
* Direction determiners
*/
/**
* Determines if the right or left arrow is needed
*/
private _horizontalDirection(): Direction {
let startRow;
if (this._moveToRequestedRow().length > 0) {
startRow = this._endRow - this._wrappedRowsForRow(this._endRow);
} else {
startRow = this._startRow;
}
if ((this._startCol < this._endCol &&
startRow <= this._endRow) || // down/right or same y/right
(this._startCol >= this._endCol &&
startRow < this._endRow)) { // down/left or same y/left
return Direction.Right;
} else {
return Direction.Left;
}
}
/**
* Determines if the up or down arrow is needed
*/
private _verticalDirection(): Direction {
if (this._startRow > this._endRow) {
return Direction.Up;
} else {
return Direction.Down;
}
}
/**
* Constructs the string of chars in the buffer from a starting row and col
* to an ending row and col
* @param startCol The starting column position
* @param startRow The starting row position
* @param endCol The ending column position
* @param endRow The ending row position
* @param forward Direction to move
*/
private _bufferLine(
startCol: number,
startRow: number,
endCol: number,
endRow: number,
forward: boolean
): string {
let currentCol = startCol;
let currentRow = startRow;
let bufferStr = '';
while (currentCol !== endCol || currentRow !== endRow) {
currentCol += forward ? 1 : -1;
if (forward && currentCol > this._terminal.cols - 1) {
bufferStr += this._terminal.buffer.translateBufferLineToString(
currentRow, false, startCol, currentCol
);
currentCol = 0;
startCol = 0;
currentRow++;
} else if (!forward && currentCol < 0) {
bufferStr += this._terminal.buffer.translateBufferLineToString(
currentRow, false, 0, startCol + 1
);
currentCol = this._terminal.cols - 1;
startCol = currentCol;
currentRow--;
}
}
return bufferStr + this._terminal.buffer.translateBufferLineToString(
currentRow, false, startCol, currentCol
);
}
/**
* Constructs the escape sequence for clicking an arrow
* @param direction The direction to move
*/
private _sequence(direction: Direction): string {
const mod = this._terminal.applicationCursor ? 'O' : '[';
return C0.ESC + mod + direction;
}
}
/**
* Returns a string repeated a given number of times
* Polyfill from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/repeat
* @param count The number of times to repeat the string
* @param string The string that is to be repeated
*/
function repeat(count: number, str: string): string {
count = Math.floor(count);
let rpt = '';
for (let i = 0; i < count; i++) {
rpt += str;
}
return rpt;
}
+8 -2
View File
@@ -100,6 +100,7 @@ export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextA
textarea.focus();
// Reset the terminal textarea's styling
// Timeout needs to be long enough for click event to be handled.
setTimeout(() => {
textarea.style.position = null;
textarea.style.width = null;
@@ -107,7 +108,7 @@ export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextA
textarea.style.left = null;
textarea.style.top = null;
textarea.style.zIndex = null;
}, 4);
}, 200);
}
/**
@@ -115,10 +116,15 @@ export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextA
* @param ev The original right click event to be handled.
* @param textarea The terminal's textarea.
* @param selectionManager The terminal's selection manager.
* @param shouldSelectWord If true and there is no selection the current word will be selected
*/
export function rightClickHandler(ev: MouseEvent, textarea: HTMLTextAreaElement, selectionManager: ISelectionManager): void {
export function rightClickHandler(ev: MouseEvent, textarea: HTMLTextAreaElement, selectionManager: ISelectionManager, shouldSelectWord: boolean): void {
moveTextAreaUnderMouseCursor(ev, textarea);
if (shouldSelectWord && !selectionManager.isClickInSelection(ev)) {
selectionManager.selectWordAtCursor(ev);
}
// Get textarea ready to copy from the context menu
textarea.value = selectionManager.selectionText;
textarea.select();
+9 -8
View File
@@ -151,10 +151,10 @@ export class MouseZoneManager implements IMouseZoneManager {
// components from handling the mouse event.
const zone = this._findZoneEventAt(e);
if (zone) {
// TODO: When link modifier support is added, the event should only be
// cancelled when the modifier is held (see #1021)
e.preventDefault();
e.stopImmediatePropagation();
if (zone.willLinkActivate(e)) {
e.preventDefault();
e.stopImmediatePropagation();
}
}
}
@@ -169,7 +169,7 @@ export class MouseZoneManager implements IMouseZoneManager {
}
private _findZoneEventAt(e: MouseEvent): IMouseZone {
const coords = this._terminal.mouseHelper.getCoords(e, this._terminal.element, this._terminal.charMeasure, this._terminal.options.lineHeight, this._terminal.cols, this._terminal.rows);
const coords = this._terminal.mouseHelper.getCoords(e, this._terminal.screenElement, this._terminal.charMeasure, this._terminal.options.lineHeight, this._terminal.cols, this._terminal.rows);
if (!coords) {
return null;
}
@@ -189,9 +189,10 @@ export class MouseZone implements IMouseZone {
public x2: number,
public y: number,
public clickCallback: (e: MouseEvent) => any,
public hoverCallback?: (e: MouseEvent) => any,
public tooltipCallback?: (e: MouseEvent) => any,
public leaveCallback?: () => void
public hoverCallback: (e: MouseEvent) => any,
public tooltipCallback: (e: MouseEvent) => any,
public leaveCallback: () => void,
public willLinkActivate: (e: MouseEvent) => boolean
) {
}
}
+4 -3
View File
@@ -13,7 +13,8 @@ export interface IMouseZone {
x2: number;
y: number;
clickCallback: (e: MouseEvent) => any;
hoverCallback?: (e: MouseEvent) => any;
tooltipCallback?: (e: MouseEvent) => any;
leaveCallback?: () => any;
hoverCallback: (e: MouseEvent) => any | undefined;
tooltipCallback: (e: MouseEvent) => any | undefined;
leaveCallback: () => any | undefined;
willLinkActivate: (e: MouseEvent) => boolean;
}
+29 -7
View File
@@ -24,22 +24,25 @@ export abstract class BaseRenderLayer implements IRenderLayer {
private _charAtlas: HTMLCanvasElement | ImageBitmap;
constructor(
container: HTMLElement,
private _container: HTMLElement,
id: string,
zIndex: number,
private _alpha: boolean,
protected _colors: IColorSet
) {
this._canvas = document.createElement('canvas');
this._canvas.id = `xterm-${id}-layer`;
this._canvas.classList.add(`xterm-${id}-layer`);
this._canvas.style.zIndex = zIndex.toString();
this._ctx = this._canvas.getContext('2d', {alpha: _alpha});
this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
this._initCanvas();
this._container.appendChild(this._canvas);
}
private _initCanvas(): void {
this._ctx = this._canvas.getContext('2d', {alpha: this._alpha});
// Draw the background if this is an opaque layer
if (!_alpha) {
if (!this._alpha) {
this.clearAll();
}
container.appendChild(this._canvas);
}
public onOptionsChanged(terminal: ITerminal): void {}
@@ -53,6 +56,25 @@ export abstract class BaseRenderLayer implements IRenderLayer {
this._refreshCharAtlas(terminal, colorSet);
}
protected setTransparency(terminal: ITerminal, alpha: boolean): void {
// Do nothing when alpha doesn't change
if (alpha === this._alpha) {
return;
}
// Create new canvas and replace old one
const oldCanvas = this._canvas;
this._alpha = alpha;
// Cloning preserves properties
this._canvas = <HTMLCanvasElement>this._canvas.cloneNode();
this._initCanvas();
this._container.replaceChild(this._canvas, oldCanvas);
// Regenerate char atlas and force a full redraw
this._refreshCharAtlas(terminal, this._colors);
this.onGridChanged(terminal, 0, terminal.rows - 1);
}
/**
* Refreshes the char atlas, aquiring a new one if necessary.
* @param terminal The terminal.
@@ -63,7 +85,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
return;
}
this._charAtlas = null;
const result = acquireCharAtlas(terminal, this._colors, this._scaledCharWidth, this._scaledCharHeight);
const result = acquireCharAtlas(terminal, colorSet, this._scaledCharWidth, this._scaledCharHeight);
if (result instanceof HTMLCanvasElement) {
this._charAtlas = result;
} else {
+5 -1
View File
@@ -17,6 +17,7 @@ interface ICharAtlasConfig {
fontWeightBold: string;
scaledCharWidth: number;
scaledCharHeight: number;
allowTransparency: boolean;
colors: IColorSet;
}
@@ -83,7 +84,8 @@ export function acquireCharAtlas(terminal: ITerminal, colors: IColorSet, scaledC
background: colors.background,
foreground: colors.foreground,
ansiColors: colors.ansi,
devicePixelRatio: window.devicePixelRatio
devicePixelRatio: window.devicePixelRatio,
allowTransparency: terminal.options.allowTransparency
};
const newEntry: ICharAtlasCacheEntry = {
@@ -111,6 +113,7 @@ function generateConfig(scaledCharWidth: number, scaledCharHeight: number, termi
fontSize: terminal.options.fontSize,
fontWeight: terminal.options.fontWeight,
fontWeightBold: terminal.options.fontWeightBold,
allowTransparency: terminal.options.allowTransparency,
colors: clonedColors
};
}
@@ -125,6 +128,7 @@ function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean {
a.fontSize === b.fontSize &&
a.fontWeight === b.fontWeight &&
a.fontWeightBold === b.fontWeightBold &&
a.allowTransparency === b.allowTransparency &&
a.scaledCharWidth === b.scaledCharWidth &&
a.scaledCharHeight === b.scaledCharHeight &&
a.colors.foreground === b.colors.foreground &&
+1 -17
View File
@@ -86,7 +86,7 @@ export class ColorManager implements IColorManager {
*/
public setTheme(theme: ITheme): void {
this.colors.foreground = theme.foreground || DEFAULT_FOREGROUND;
this.colors.background = this._validateColor(theme.background, DEFAULT_BACKGROUND);
this.colors.background = theme.background || DEFAULT_BACKGROUND;
this.colors.cursor = theme.cursor || DEFAULT_CURSOR;
this.colors.cursorAccent = theme.cursorAccent || DEFAULT_CURSOR_ACCENT;
this.colors.selection = theme.selection || DEFAULT_SELECTION;
@@ -107,20 +107,4 @@ export class ColorManager implements IColorManager {
this.colors.ansi[14] = theme.brightCyan || DEFAULT_ANSI_COLORS[14];
this.colors.ansi[15] = theme.brightWhite || DEFAULT_ANSI_COLORS[15];
}
private _validateColor(color: string, fallback: string): string {
if (!color) {
return fallback;
}
if (color.length === 7 && color.charAt(0) === '#') {
return color;
}
if (color.length === 4 && color.charAt(0) === '#') {
const r = color.charAt(1);
const g = color.charAt(2);
const b = color.charAt(3);
return `#${r}${r}${g}${g}${b}${b}`;
}
return fallback;
}
}
+9 -4
View File
@@ -35,11 +35,12 @@ export class Renderer extends EventEmitter implements IRenderer {
if (theme) {
this.colorManager.setTheme(theme);
}
this._renderLayers = [
new TextRenderLayer(this._terminal.element, 0, this.colorManager.colors),
new SelectionRenderLayer(this._terminal.element, 1, this.colorManager.colors),
new LinkRenderLayer(this._terminal.element, 2, this.colorManager.colors, this._terminal),
new CursorRenderLayer(this._terminal.element, 3, this.colorManager.colors)
new TextRenderLayer(this._terminal.screenElement, 0, this.colorManager.colors, this._terminal.options.allowTransparency),
new SelectionRenderLayer(this._terminal.screenElement, 1, this.colorManager.colors),
new LinkRenderLayer(this._terminal.screenElement, 2, this.colorManager.colors, this._terminal),
new CursorRenderLayer(this._terminal.screenElement, 3, this.colorManager.colors)
];
this.dimensions = {
scaledCharWidth: null,
@@ -119,6 +120,10 @@ export class Renderer extends EventEmitter implements IRenderer {
this._terminal.refresh(0, this._terminal.rows - 1);
}
// Resize the screen
this._terminal.screenElement.style.width = `${this.dimensions.canvasWidth + this._terminal.viewport.scrollBarWidth}px`;
this._terminal.screenElement.style.height = `${this.dimensions.canvasHeight}px`;
this.emit('resize', {
width: this.dimensions.canvasWidth,
height: this.dimensions.canvasHeight
+6 -2
View File
@@ -22,8 +22,8 @@ export class TextRenderLayer extends BaseRenderLayer {
private _characterFont: string;
private _characterOverlapCache: { [key: string]: boolean } = {};
constructor(container: HTMLElement, zIndex: number, colors: IColorSet) {
super(container, 'text', zIndex, false, colors);
constructor(container: HTMLElement, zIndex: number, colors: IColorSet, alpha: boolean) {
super(container, 'text', zIndex, alpha, colors);
this._state = new GridCache<CharData>();
}
@@ -190,6 +190,10 @@ export class TextRenderLayer extends BaseRenderLayer {
}
}
public onOptionsChanged(terminal: ITerminal): void {
this.setTransparency(terminal, terminal.options.allowTransparency);
}
/**
* Whether a character is overlapping to the next cell.
*/
+2 -1
View File
@@ -26,6 +26,7 @@ export interface ICharAtlasRequest {
foreground: string;
ansiColors: string[];
devicePixelRatio: number;
allowTransparency: boolean;
}
export const CHAR_ATLAS_CELL_SPACING = 1;
@@ -43,7 +44,7 @@ export function generateCharAtlas(context: Window, canvasFactory: (width: number
/*255 ascii chars*/255 * cellWidth,
(/*default+default bold*/2 + /*0-15*/16) * cellHeight
);
const ctx = canvas.getContext('2d', {alpha: false});
const ctx = canvas.getContext('2d', {alpha: request.allowTransparency});
ctx.fillStyle = request.background;
ctx.fillRect(0, 0, canvas.width, canvas.height);
+7
View File
@@ -71,6 +71,9 @@ export class MockTerminal implements ITerminal {
write(data: string): void {
throw new Error('Method not implemented.');
}
send(data: string): void {
throw new Error('Method not implemented.');
}
bracketedPasteMode: boolean;
mouseHelper: IMouseHelper;
renderer: IRenderer;
@@ -78,6 +81,7 @@ export class MockTerminal implements ITerminal {
isFocused: boolean;
options: ITerminalOptions = {};
element: HTMLElement;
screenElement: HTMLElement;
rowContainer: HTMLElement;
selectionContainer: HTMLElement;
selectionManager: ISelectionManager;
@@ -94,6 +98,8 @@ export class MockTerminal implements ITerminal {
scrollback: number;
buffers: IBufferSet;
buffer: IBuffer;
viewport: IViewport;
applicationCursor: boolean;
handler(data: string): void {
throw new Error('Method not implemented.');
}
@@ -317,6 +323,7 @@ export class MockRenderer implements IRenderer {
}
export class MockViewport implements IViewport {
scrollBarWidth: number = 0;
onThemeChanged(colors: IColorSet): void {
throw new Error('Method not implemented.');
}
+10 -1
View File
@@ -96,9 +96,18 @@
background-color: #000;
overflow-y: scroll;
cursor: default;
position: absolute;
right: 0;
left: 0;
top: 0;
bottom: 0;
}
.xterm canvas {
.xterm .xterm-screen {
position: relative;
}
.xterm .xterm-screen canvas {
position: absolute;
left: 0;
top: 0;
+4
View File
@@ -9,6 +9,10 @@
true,
"check-space"
],
"curly": [
true,
"ignore-same-line"
],
"indent": [
true,
"spaces"
+26 -4
View File
@@ -17,6 +17,12 @@ declare module 'xterm' {
* An object containing start up options for the terminal.
*/
export interface ITerminalOptions {
/**
* Whether background should support non-opaque color. It must be set before
* executing open() method and can't be changed later without excuting it again.
* Warning: Enabling this option can reduce performances somewhat.
*/
allowTransparency?: boolean;
/**
* A data uri of the sound to use for the bell (needs bellStyle = 'sound').
*/
@@ -49,6 +55,8 @@ declare module 'xterm' {
/**
* Whether to enable the rendering of bold text.
*
* @deprecated Use fontWeight and fontWeightBold instead.
*/
enableBold?: boolean;
@@ -87,6 +95,12 @@ declare module 'xterm' {
*/
macOptionIsMeta?: boolean;
/**
* Whether to select the word under the cursor on right click, this is
* standard behavior in a lot of macOS applications.
*/
rightClickSelectsWord?: boolean;
/**
* The number of rows in the terminal.
*/
@@ -175,8 +189,8 @@ declare module 'xterm' {
matchIndex?: number;
/**
* A callback that validates an individual link, returning true if valid and
* false if invalid.
* A callback that validates whether to create an individual link, pass
* whether the link is valid to the callback.
*/
validationCallback?: (uri: string, callback: (isValid: boolean) => void) => void;
@@ -197,6 +211,14 @@ declare module 'xterm' {
* default value is 0.
*/
priority?: number;
/**
* A callback that fires when the mousedown and click events occur that
* determines whether a link will be activated upon click. This enables
* only activating a link when a certain modifier is held down, if not the
* mouse event will continue propagation (eg. double click to select word).
*/
willLinkActivate?: (event: MouseEvent, uri: string) => boolean;
}
export interface IEventEmitter {
@@ -449,7 +471,7 @@ declare module 'xterm' {
* Retrieves an option's value from the terminal.
* @param key The option key.
*/
getOption(key: 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'debug' | 'disableStdin' | 'enableBold' | 'macOptionIsMeta' | 'popOnBell' | 'screenKeys' | 'useFlowControl' | 'visualBell'): boolean;
getOption(key: 'allowTransparency' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'debug' | 'disableStdin' | 'enableBold' | 'macOptionIsMeta' | 'rightClickSelectsWord' | 'popOnBell' | 'screenKeys' | 'useFlowControl' | 'visualBell'): boolean;
/**
* Retrieves an option's value from the terminal.
* @param key The option key.
@@ -500,7 +522,7 @@ declare module 'xterm' {
* @param key The option key.
* @param value The option value.
*/
setOption(key: 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'debug' | 'disableStdin' | 'enableBold' | 'macOptionIsMeta' | 'popOnBell' | 'screenKeys' | 'useFlowControl' | 'visualBell', value: boolean): void;
setOption(key: 'allowTransparency' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'debug' | 'disableStdin' | 'enableBold' | 'macOptionIsMeta' | 'popOnBell' | 'rightClickSelectsWord' | 'screenKeys' | 'useFlowControl' | 'visualBell', value: boolean): void;
/**
* Sets an option on the terminal.
* @param key The option key.