Compare commits

..
5 Commits
Author SHA1 Message Date
Paris Kasidiaris fe3019765b Bump version to 3.0.2
Signed-off-by: Paris Kasidiaris <paris@sourcelair.com>
2018-01-16 17:12:15 +00:00
Paris Kasidiaris d1cf6854b5 Merge pull request #1212 from xtermjs/hotfix/3.0.2
Hotfix: 3.0.2
2018-01-16 19:10:17 +02:00
Daniel Imms 2eaa244907 Ensure renderer uses latest options at open
Fixes #1194
2018-01-16 16:01:32 +00:00
ficristo e1e5c1c44f Make addons UMD compatible 2018-01-16 15:30:53 +00:00
Paris Kasidiaris 45ed27d329 Bump version to 3.0.1
Signed-off-by: Paris Kasidiaris <paris@sourcelair.com>
2018-01-05 16:46:38 +00:00
99 changed files with 2153 additions and 6731 deletions
+5 -1
View File
@@ -19,6 +19,10 @@ fixtures/typings-test/*.js
# Directories needed for code coverage # Directories needed for code coverage
/coverage/ /coverage/
# Keep bundled code out of Git # Keep legacy files out of the repo, this can be removed when we merge v3 into master
dist/ dist/
src/utils/TestUtils.ts
src/xterm.js
# Keep the demo builds out of Git
demo/dist/ demo/dist/
+1 -1
View File
@@ -1,6 +1,7 @@
language: node_js language: node_js
os: os:
- linux - linux
- osx
node_js: node_js:
- 6 - 6
before_install: before_install:
@@ -11,7 +12,6 @@ before_install:
- npm install -g npm@5.1.0 - npm install -g npm@5.1.0
env: env:
matrix: matrix:
- NPM_COMMAND=tsc
- NPM_COMMAND=lint - NPM_COMMAND=lint
- NPM_COMMAND=test - NPM_COMMAND=test
notifications: notifications:
-15
View File
@@ -1,15 +1,12 @@
List of xterm.js contributors. Updated before every release. List of xterm.js contributors. Updated before every release.
Adrian Labbé <adrilabbelol@gmail.com>
Aleksandr Andrienko <aandrienko@codenvy.com> Aleksandr Andrienko <aandrienko@codenvy.com>
Aleksandr Andriienko <oandriie@redhat.com> Aleksandr Andriienko <oandriie@redhat.com>
Alessandro Nadalin <alessandro.nadalin@gmail.com> Alessandro Nadalin <alessandro.nadalin@gmail.com>
Alexander Olsson <noseglid@gmail.com> Alexander Olsson <noseglid@gmail.com>
Alexey Kontsevoy <alexey@gravitational.com> Alexey Kontsevoy <alexey@gravitational.com>
Andres Mejia <amejia004@gmail.com>
Anish Athalye <me@anishathalye.com> Anish Athalye <me@anishathalye.com>
Anthony Lapenna <lapenna.anthony@gmail.com> Anthony Lapenna <lapenna.anthony@gmail.com>
Antonin Stefanutti <antonin@stefanutti.fr>
Antonis Kalipetis <akalipetis@sourcelair.com> Antonis Kalipetis <akalipetis@sourcelair.com>
Anton Skshidlevsky <meefik@gmail.com> Anton Skshidlevsky <meefik@gmail.com>
Anton Yurovskykh <anton.yurovskykh@gmail.com> Anton Yurovskykh <anton.yurovskykh@gmail.com>
@@ -22,7 +19,6 @@ Bill Church <billchurch@users.noreply.github.com>
Bob Reid <bobreid@Bobs-MacBook-Pro.local> Bob Reid <bobreid@Bobs-MacBook-Pro.local>
bottleofwater <nison.mael+bottleofwater@gmail.com> bottleofwater <nison.mael+bottleofwater@gmail.com>
Brian Mock <brian@mockbrian.com> Brian Mock <brian@mockbrian.com>
Bruno Ribeiro <b.m.fernandes.ribeiro@gmail.com>
Bruno Ribeito <b.m.fernandes.ribeiro@gmail.com> Bruno Ribeito <b.m.fernandes.ribeiro@gmail.com>
Carson Anderson <carson@betterservers.com> Carson Anderson <carson@betterservers.com>
CHaBou <chabup@delean.fr> CHaBou <chabup@delean.fr>
@@ -30,9 +26,7 @@ Christian Budde Christensen <budde377@gmail.com>
Christof Marti <chrmarti@microsoft.com> Christof Marti <chrmarti@microsoft.com>
Christopher Jeffrey <chjjeffrey@gmail.com> Christopher Jeffrey <chjjeffrey@gmail.com>
coderaiser <mnemonic.enemy@gmail.com> coderaiser <mnemonic.enemy@gmail.com>
Damien Tournoud <damien@platform.sh>
Dan Brown <jdanbrown@gmail.com> Dan Brown <jdanbrown@gmail.com>
Daniel Griffen <dagriffe@microsoft.com>
Daniel Griffen <daniel@griffen.io> Daniel Griffen <daniel@griffen.io>
Daniel Imms <daimms@microsoft.com> Daniel Imms <daimms@microsoft.com>
Daniel Risacher <drisacher@gmail.com> Daniel Risacher <drisacher@gmail.com>
@@ -52,30 +46,24 @@ imoses <ido@twiggle.com>
InDieTasten <indietasten@gmail.com> InDieTasten <indietasten@gmail.com>
irokas <akasidiari@inf.uth.gr> irokas <akasidiari@inf.uth.gr>
Jakob Gillich <jakob@gillich.me> Jakob Gillich <jakob@gillich.me>
Jan Kuri <jkuri88@gmail.com>
Jean Bruenn <himself@jeanbruenn.info> Jean Bruenn <himself@jeanbruenn.info>
Jeremy Danyow <jedanyow@microsoft.com> Jeremy Danyow <jedanyow@microsoft.com>
Jianhui Zhao <jianhuizhao329@gmail.com>
Joao Moreno <jomo@microsoft.com> Joao Moreno <jomo@microsoft.com>
Joao Moreno <mail@joaomoreno.com> Joao Moreno <mail@joaomoreno.com>
Johannes Zellner <johannes@nebulon.de> Johannes Zellner <johannes@nebulon.de>
Jon Masters <jon.masters@sky.com> Jon Masters <jon.masters@sky.com>
Jörg Breitbart <jerch@rockborn.de> Jörg Breitbart <jerch@rockborn.de>
Justin Luk <jluk@users.noreply.github.com>
Justin Mecham <justin@mecham.me> Justin Mecham <justin@mecham.me>
Kirill Merkushev <lanwen@yandex.ru> Kirill Merkushev <lanwen@yandex.ru>
Krasimir Tsonev <krasimir@outset.ws>
Luca <LucaT1@users.noreply.github.com> Luca <LucaT1@users.noreply.github.com>
Lucian Buzzo <lucian.buzzo@gmail.com> Lucian Buzzo <lucian.buzzo@gmail.com>
Lukas Drgon <lukas.drgon@gmail.com> Lukas Drgon <lukas.drgon@gmail.com>
Maël Nison <nison.mael@gmail.com> Maël Nison <nison.mael@gmail.com>
Marc Dumais <marc.dumais@ericsson.com> Marc Dumais <marc.dumais@ericsson.com>
Markus F.X.J. Oberhumer <markus@oberhumer.com>
Martin Chloride <i@martincl2.me> Martin Chloride <i@martincl2.me>
Martin Koppehel <martin.koppehel@st.ovgu.de> Martin Koppehel <martin.koppehel@st.ovgu.de>
Martin Wang <jiahaow@ca.ibm.com> Martin Wang <jiahaow@ca.ibm.com>
Matt Bierner <matb@microsoft.com> Matt Bierner <matb@microsoft.com>
Matthew James <matthew.d.james87@gmail.com>
Michael Irwin <mikesir87@gmail.com> Michael Irwin <mikesir87@gmail.com>
Mikko Karvonen <mikko.karvonen@arm.com> Mikko Karvonen <mikko.karvonen@arm.com>
mofux <t.zilz@mofux.org> mofux <t.zilz@mofux.org>
@@ -87,12 +75,10 @@ Paris Kasidiaris <paris@sourcelair.com>
Peter Baumgarten <me@peterbaumgarten.com> Peter Baumgarten <me@peterbaumgarten.com>
Rick Baker <rick@ricktbaker.com> Rick Baker <rick@ricktbaker.com>
runarberg <runar@greenqloud.com> runarberg <runar@greenqloud.com>
Saad Malik <simfox3@gmail.com>
Samuel Williams <samuel.williams@oriontransfer.co.nz> Samuel Williams <samuel.williams@oriontransfer.co.nz>
Saswat Das <saswatds@users.noreply.github.com> Saswat Das <saswatds@users.noreply.github.com>
Saul Costa <saul@codevolve.com> Saul Costa <saul@codevolve.com>
Shuanglei Tao <tsl0922@gmail.com> Shuanglei Tao <tsl0922@gmail.com>
sitzmar <sitzmar@gmail.com>
Steven Silvester <steven.silvester@ieee.org> Steven Silvester <steven.silvester@ieee.org>
stuicey <stuicey@users.noreply.github.com> stuicey <stuicey@users.noreply.github.com>
t-amqi <umberos@gmail.com> t-amqi <umberos@gmail.com>
@@ -104,4 +90,3 @@ Tyler Jewell <tjewell@codenvy.com>
Vincent Woo <me@vincentwoo.com> Vincent Woo <me@vincentwoo.com>
yutaka <shigure@refy.net> yutaka <shigure@refy.net>
YuviPanda <yuvipanda@gmail.com> YuviPanda <yuvipanda@gmail.com>
ZHAO Xudong <zxdong@gmail.com>
+2 -2
View File
@@ -1,4 +1,4 @@
FROM node:8 FROM node:6
MAINTAINER Paris Kasidiaris <paris@sourcelair.com> MAINTAINER Paris Kasidiaris <paris@sourcelair.com>
# Set the working directory # Set the working directory
@@ -16,4 +16,4 @@ RUN npm install
COPY . /usr/src/app COPY . /usr/src/app
# Run the tests and build, to make sure everything is working nicely # Run the tests and build, to make sure everything is working nicely
RUN npm run build && npm run webpack && npm run test RUN npm run build && npm run test
-1
View File
@@ -1,2 +1 @@
web: npm start web: npm start
webpack: npm run watch
+9 -67
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: 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 xterm npm install
``` ```
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. 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. - [**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. - [**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. - [**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. 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. - [**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. - [**Opshell**](https://github.com/ricktbaker/opshell) Ops Helper tool to make life easier working with AWS instances across multiple organizations.
@@ -118,86 +118,32 @@ computational environment for Jupyter, supporting interactive data science and s
- [**Script Runner**](https://github.com/ioquatix/script-runner): Run scripts (or a shell) in Atom. - [**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. - [**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. - [**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.
- [**Azure Cloud Shell**](https://shell.azure.com): Azure Cloud Shell is a Microsoft-managed admin machine built on Azure, for Azure.
- [**atom-xterm**](https://atom.io/packages/atom-xterm): Atom plugin for providing terminals inside your Atom workspace.
- [**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. 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 ## Demo
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.
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).
Xterm.js [provides a pre-built Docker image](https://hub.docker.com/r/xtermjs/xterm.js/) to help run the demo easily (Git tags are built as [tagged Docker images](https://hub.docker.com/r/xtermjs/xterm.js/tags/) too).
To run the just demo (with no editing access). run the following command in your terminal:
```
docker run -p 3000:3000 xtermjs/xterm.js
```
Then open http://0.0.0.0:3000 in a web browser to access the demo.
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
foreman start # Replace foreman with "honcho", "forego" etc. depending on your runner
```
Then open http://0.0.0.0:3000 in a web browser to access the demo.
### Linux or macOS ### 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: First, be sure that a C++ compiler such as GCC-C++ or Clang is installed, then run these commands:
``` ```
npm install npm install
npm start # Run this in its own terminal npm start
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. Then open http://0.0.0.0:3000 in a web browser.
### Windows ### Windows
First, ensure [node-gyp](https://github.com/nodejs/node-gyp) is installed and configured correctly, then run the following commands in your terminal: First, ensure [node-gyp](https://github.com/nodejs/node-gyp) is installed and configured correctly, then run these commands.
``` ```
npm install npm install
npm start # Run this in its own terminal npm start
npm run watch # Run this in its own terminal
``` ```
Then open http://127.0.0.1:3000 in a web browser to access the demo. Then open http://127.0.0.1:3000 in a web browser.
*Note: Do not use ConEmu, as it seems to break the demo for some reason.* *Note: Do not use ConEmu, as it seems to break the demo for some reason.*
@@ -213,14 +159,10 @@ Xterm.js is maintained by [SourceLair](https://www.sourcelair.com/) and a few ex
To contribute either code, documentation or issues to xterm.js please read the [Contributing document](CONTRIBUTING.md) beforehand. The development of xterm.js does not require any special tool. All you need is an editor that supports JavaScript/TypeScript and a browser. You will need Node.js installed locally to get all the features working in the demo. To contribute either code, documentation or issues to xterm.js please read the [Contributing document](CONTRIBUTING.md) beforehand. The development of xterm.js does not require any special tool. All you need is an editor that supports JavaScript/TypeScript and a browser. You will need Node.js installed locally to get all the features working in the demo.
### Code structure
`src/` is roughly split up into areas of functionality such as `renderer/` that handles all rendering and `utils/` which provides general utility functions. The `shared/` folder contains code that can be used from either the main thread or a web worker thread, all code inside a `shared/` folder should only ever import other code from a `shared/` folder to minimize the amount of code run what launching a web worker.
## License Agreement ## 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. 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) 2014-2017, SourceLair, Private Company ([www.sourcelair.com](https://www.sourcelair.com/home)) (MIT License)
Copyright (c) 2012-2013, Christopher Jeffrey (MIT License) Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
+1 -13
View File
@@ -23,9 +23,6 @@
<p> <p>
<label><input type="checkbox" id="option-cursor-blink"> cursorBlink</label> <label><input type="checkbox" id="option-cursor-blink"> cursorBlink</label>
</p> </p>
<p>
<label><input type="checkbox" id="option-mac-option-is-meta"> macOptionIsMeta</label>
</p>
<p> <p>
<label> <label>
cursorStyle cursorStyle
@@ -64,19 +61,10 @@
<label for="rows">Rows</label> <label for="rows">Rows</label>
<input type="number" id="rows" /> <input type="number" id="rows" />
</div> </div>
<div style="display: inline-block; margin-right: 16px;">
<label for="padding">Padding</label>
<input type="number" id="padding" />
</div>
</div> </div>
</div> </div>
<div>
<h3>Accessibility</h3>
<p>
<label><input type="checkbox" id="option-screen-reader-mode"> screenReaderMode</label>
</p>
</div> </div>
<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> <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>
<script src="dist/bundle.js" defer ></script> <script src="dist/bundle.js" defer ></script>
</body> </body>
</html> </html>
+8 -26
View File
@@ -3,7 +3,6 @@ import * as attach from '../build/addons/attach/attach';
import * as fit from '../build/addons/fit/fit'; import * as fit from '../build/addons/fit/fit';
import * as fullscreen from '../build/addons/fullscreen/fullscreen'; import * as fullscreen from '../build/addons/fullscreen/fullscreen';
import * as search from '../build/addons/search/search'; import * as search from '../build/addons/search/search';
import * as webLinks from '../build/addons/webLinks/webLinks';
import * as winptyCompat from '../build/addons/winptyCompat/winptyCompat'; import * as winptyCompat from '../build/addons/winptyCompat/winptyCompat';
@@ -11,7 +10,6 @@ Terminal.applyAddon(attach);
Terminal.applyAddon(fit); Terminal.applyAddon(fit);
Terminal.applyAddon(fullscreen); Terminal.applyAddon(fullscreen);
Terminal.applyAddon(search); Terminal.applyAddon(search);
Terminal.applyAddon(webLinks);
Terminal.applyAddon(winptyCompat); Terminal.applyAddon(winptyCompat);
@@ -29,34 +27,28 @@ var terminalContainer = document.getElementById('terminal-container'),
optionElements = { optionElements = {
cursorBlink: document.querySelector('#option-cursor-blink'), cursorBlink: document.querySelector('#option-cursor-blink'),
cursorStyle: document.querySelector('#option-cursor-style'), cursorStyle: document.querySelector('#option-cursor-style'),
macOptionIsMeta: document.querySelector('#option-mac-option-is-meta'),
scrollback: document.querySelector('#option-scrollback'), scrollback: document.querySelector('#option-scrollback'),
tabstopwidth: document.querySelector('#option-tabstopwidth'), tabstopwidth: document.querySelector('#option-tabstopwidth'),
bellStyle: document.querySelector('#option-bell-style'), bellStyle: document.querySelector('#option-bell-style')
screenReaderMode: document.querySelector('#option-screen-reader-mode')
}, },
colsElement = document.getElementById('cols'), colsElement = document.getElementById('cols'),
rowsElement = document.getElementById('rows'), rowsElement = document.getElementById('rows');
paddingElement = document.getElementById('padding');
function setTerminalSize() { function setTerminalSize() {
var cols = parseInt(colsElement.value, 10); var cols = parseInt(colsElement.value, 10);
var rows = parseInt(rowsElement.value, 10); var rows = parseInt(rowsElement.value, 10);
var width = (cols * term.renderer.dimensions.actualCellWidth + term.viewport.scrollBarWidth).toString() + 'px'; 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 height = (rows * term.renderer.dimensions.actualCellHeight).toString() + 'px'; var height = (rows * term.renderer.dimensions.actualCellHeight).toString() + 'px';
terminalContainer.style.width = width; terminalContainer.style.width = width;
terminalContainer.style.height = height; terminalContainer.style.height = height;
term.fit(); term.resize(cols, rows);
}
function setPadding() {
term.element.style.padding = parseInt(paddingElement.value, 10).toString() + 'px';
term.fit();
} }
colsElement.addEventListener('change', setTerminalSize); colsElement.addEventListener('change', setTerminalSize);
rowsElement.addEventListener('change', setTerminalSize); rowsElement.addEventListener('change', setTerminalSize);
paddingElement.addEventListener('change', setPadding);
actionElements.findNext.addEventListener('keypress', function (e) { actionElements.findNext.addEventListener('keypress', function (e) {
if (e.key === "Enter") { if (e.key === "Enter") {
@@ -80,18 +72,12 @@ optionElements.cursorStyle.addEventListener('change', function () {
optionElements.bellStyle.addEventListener('change', function () { optionElements.bellStyle.addEventListener('change', function () {
term.setOption('bellStyle', optionElements.bellStyle.value); term.setOption('bellStyle', optionElements.bellStyle.value);
}); });
optionElements.macOptionIsMeta.addEventListener('change', function () {
term.setOption('macOptionIsMeta', optionElements.macOptionIsMeta.checked);
});
optionElements.scrollback.addEventListener('change', function () { optionElements.scrollback.addEventListener('change', function () {
term.setOption('scrollback', parseInt(optionElements.scrollback.value, 10)); term.setOption('scrollback', parseInt(optionElements.scrollback.value, 10));
}); });
optionElements.tabstopwidth.addEventListener('change', function () { optionElements.tabstopwidth.addEventListener('change', function () {
term.setOption('tabStopWidth', parseInt(optionElements.tabstopwidth.value, 10)); term.setOption('tabStopWidth', parseInt(optionElements.tabstopwidth.value, 10));
}); });
optionElements.screenReaderMode.addEventListener('change', function () {
term.setOption('screenReaderMode', optionElements.screenReaderMode.checked);
});
createTerminal(); createTerminal();
@@ -101,11 +87,9 @@ function createTerminal() {
terminalContainer.removeChild(terminalContainer.children[0]); terminalContainer.removeChild(terminalContainer.children[0]);
} }
term = new Terminal({ term = new Terminal({
macOptionIsMeta: optionElements.macOptionIsMeta.enabled,
cursorBlink: optionElements.cursorBlink.checked, cursorBlink: optionElements.cursorBlink.checked,
scrollback: parseInt(optionElements.scrollback.value, 10), scrollback: parseInt(optionElements.scrollback.value, 10),
tabStopWidth: parseInt(optionElements.tabstopwidth.value, 10), tabStopWidth: parseInt(optionElements.tabstopwidth.value, 10)
screenReaderMode: optionElements.screenReaderMode.checked
}); });
window.term = term; // Expose `term` to window for debugging purposes window.term = term; // Expose `term` to window for debugging purposes
term.on('resize', function (size) { term.on('resize', function (size) {
@@ -123,7 +107,6 @@ function createTerminal() {
term.open(terminalContainer); term.open(terminalContainer);
term.winptyCompatInit(); term.winptyCompatInit();
term.webLinksInit();
term.fit(); term.fit();
term.focus(); term.focus();
@@ -131,7 +114,6 @@ function createTerminal() {
setTimeout(function () { setTimeout(function () {
colsElement.value = term.cols; colsElement.value = term.cols;
rowsElement.value = term.rows; rowsElement.value = term.rows;
paddingElement.value = 0;
// Set terminal size again to set the specific dimensions on the demo // Set terminal size again to set the specific dimensions on the demo
setTerminalSize(); setTerminalSize();
-8
View File
@@ -2,16 +2,8 @@ version: "3"
services: services:
web: web:
image: xtermjs/xterm.js:latest
build: . build: .
volumes: volumes:
- ./:/usr/src/app - ./:/usr/src/app
ports: ports:
- ${XTERMJS_PORT:3000}:3000 - ${XTERMJS_PORT:3000}:3000
command: ["npm", "start"]
watch:
build: .
volumes:
- ./:/usr/src/app
command: ["npm", "run", "watch"]
-12
View File
@@ -144,11 +144,6 @@ namespace methods_core {
const r20: string = t.getOption('bellStyle'); const r20: string = t.getOption('bellStyle');
const r21: boolean = t.getOption('enableBold'); const r21: boolean = t.getOption('enableBold');
const r22: number = t.getOption('letterSpacing'); const r22: number = t.getOption('letterSpacing');
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(); const t: Terminal = new Terminal();
@@ -162,14 +157,9 @@ namespace methods_core {
t.setOption('debug', true); t.setOption('debug', true);
t.setOption('disableStdin', true); t.setOption('disableStdin', true);
t.setOption('enableBold', true); t.setOption('enableBold', true);
t.setOption('fontWeight', 'normal');
t.setOption('fontWeight', 'bold');
t.setOption('fontWeightBold', 'normal');
t.setOption('fontWeightBold', 'bold');
t.setOption('popOnBell', true); t.setOption('popOnBell', true);
t.setOption('screenKeys', true); t.setOption('screenKeys', true);
t.setOption('useFlowControl', true); t.setOption('useFlowControl', true);
t.setOption('allowTransparency', true);
t.setOption('visualBell', true); t.setOption('visualBell', true);
t.setOption('colors', ['a', 'b']); t.setOption('colors', ['a', 'b']);
t.setOption('letterSpacing', 1); t.setOption('letterSpacing', 1);
@@ -187,8 +177,6 @@ namespace methods_core {
t.setOption('lineHeight', 1); t.setOption('lineHeight', 1);
t.setOption('fontFamily', 'foo'); t.setOption('fontFamily', 'foo');
t.setOption('theme', {background: '#ff0000'}); t.setOption('theme', {background: '#ff0000'});
t.setOption('macOptionIsMeta', true);
t.setOption('rightClickSelectsWord', false);
} }
} }
namespace scrolling { namespace scrolling {
+4 -13
View File
@@ -23,7 +23,7 @@ const tsProject = ts.createProject('tsconfig.json');
const srcDir = tsProject.config.compilerOptions.rootDir; const srcDir = tsProject.config.compilerOptions.rootDir;
let outDir = tsProject.config.compilerOptions.outDir; let outDir = tsProject.config.compilerOptions.outDir;
const addons = fs.readdirSync(`${__dirname}/src/addons`); const addons = ['attach', 'fit', 'fullscreen', 'search', 'terminado', 'winptyCompat', 'zmodem'];
// Under some environments like TravisCI, this comes out at absolute which can // Under some environments like TravisCI, this comes out at absolute which can
// break the build. This ensures that the outDir is absolute. // break the build. This ensures that the outDir is absolute.
@@ -52,14 +52,9 @@ gulp.task('tsc', function () {
let tsProjectAddon = ts.createProject(`./src/addons/${addon}/tsconfig.json`); let tsProjectAddon = ts.createProject(`./src/addons/${addon}/tsconfig.json`);
let tsResultAddon = tsProjectAddon.src().pipe(sourcemaps.init()).pipe(tsProjectAddon()); let tsResultAddon = tsProjectAddon.src().pipe(sourcemaps.init()).pipe(tsProjectAddon());
let tscAddon = merge( let tscAddon = tsResultAddon.js
tsResultAddon.js .pipe(sourcemaps.write('.', {includeContent: false, sourceRoot: ''}))
.pipe(sourcemaps.write('.', {includeContent: false, sourceRoot: ''})) .pipe(gulp.dest(`${outDir}/addons/${addon}`));
.pipe(gulp.dest(`${outDir}/addons/${addon}`)),
tsResultAddon.dts
.pipe(sourcemaps.write('.', {includeContent: false, sourceRoot: ''}))
.pipe(gulp.dest(`${outDir}/addons/${addon}`))
)
return tscAddon; return tscAddon;
}); });
@@ -192,10 +187,6 @@ gulp.task('webpack', ['build'], function() {
.pipe(gulp.dest('demo/dist/')); .pipe(gulp.dest('demo/dist/'));
}); });
gulp.task('watch', ['webpack'], () => {
gulp.watch(['./src/*', './src/**/*'], ['webpack']);
});
/** /**
* Submit coverage results to coveralls.io * Submit coverage results to coveralls.io
*/ */
+709 -3719
View File
File diff suppressed because it is too large Load Diff
+6 -7
View File
@@ -1,7 +1,7 @@
{ {
"name": "xterm", "name": "xterm",
"description": "Full xterm terminal, in your browser", "description": "Full xterm terminal, in your browser",
"version": "3.2.0", "version": "3.0.2",
"ignore": [ "ignore": [
"demo", "demo",
"test", "test",
@@ -9,7 +9,7 @@
], ],
"main": "lib/Terminal.js", "main": "lib/Terminal.js",
"types": "typings/xterm.d.ts", "types": "typings/xterm.d.ts",
"repository": "https://github.com/xtermjs/xterm.js", "repository": "https://github.com/sourcelair/xterm.js",
"license": "MIT", "license": "MIT",
"files": [ "files": [
"*.js", "*.js",
@@ -66,8 +66,8 @@
"node-pty": "^0.7.2", "node-pty": "^0.7.2",
"nodemon": "1.10.2", "nodemon": "1.10.2",
"sorcery": "^0.10.0", "sorcery": "^0.10.0",
"tslint": "^5.9.1", "tslint": "^4.0.2",
"typescript": "~2.7.1", "typescript": "~2.4.0",
"vinyl-buffer": "^1.0.0", "vinyl-buffer": "^1.0.0",
"vinyl-source-stream": "^1.1.0", "vinyl-source-stream": "^1.1.0",
"webpack": "^3.10.0", "webpack": "^3.10.0",
@@ -75,18 +75,17 @@
"zmodem.js": "^0.1.5" "zmodem.js": "^0.1.5"
}, },
"scripts": { "scripts": {
"prestart": "gulp webpack",
"start": "node demo/app", "start": "node demo/app",
"prestart-zmodem": "npm run build", "prestart-zmodem": "npm run build",
"start-zmodem": "node build/addons/zmodem/demo/app", "start-zmodem": "node build/addons/zmodem/demo/app",
"lint": "tslint src/*.ts src/**/*.ts", "lint": "tslint src/*.ts src/**/*.ts",
"test": "gulp test", "test": "gulp test",
"build:docs": "jsdoc -c jsdoc.json", "build:docs": "jsdoc -c jsdoc.json",
"tsc": "tsc",
"build": "gulp build", "build": "gulp build",
"prepublish": "npm run build", "prepublish": "npm run build",
"coveralls": "gulp coveralls", "coveralls": "gulp coveralls",
"webpack": "gulp webpack", "webpack": "gulp webpack"
"watch": "gulp watch"
}, },
"dependencies": {} "dependencies": {}
} }
-287
View File
@@ -1,287 +0,0 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import * as Strings from './Strings';
import { ITerminal, IBuffer } from './Types';
import { isMac } from './shared/utils/Browser';
import { RenderDebouncer } from './utils/RenderDebouncer';
import { addDisposableListener } from './utils/Dom';
import { IDisposable } from 'xterm';
const MAX_ROWS_TO_READ = 20;
const ACTIVE_ITEM_ID_PREFIX = 'xterm-active-item-';
enum BoundaryPosition {
Top,
Bottom
}
export class AccessibilityManager implements IDisposable {
private _accessibilityTreeRoot: HTMLElement;
private _rowContainer: HTMLElement;
private _rowElements: HTMLElement[] = [];
private _liveRegion: HTMLElement;
private _liveRegionLineCount: number = 0;
private _renderRowsDebouncer: RenderDebouncer;
private _topBoundaryFocusListener: (e: FocusEvent) => void;
private _bottomBoundaryFocusListener: (e: FocusEvent) => void;
private _disposables: IDisposable[] = [];
/**
* This queue has a character pushed to it for keys that are pressed, if the
* next character added to the terminal is equal to the key char then it is
* not announced (added to live region) because it has already been announced
* by the textarea event (which cannot be canceled). There are some race
* condition cases if there is typing while data is streaming, but this covers
* the main case of typing into the prompt and inputting the answer to a
* question (Y/N, etc.).
*/
private _charsToConsume: string[] = [];
constructor(private _terminal: ITerminal) {
this._accessibilityTreeRoot = document.createElement('div');
this._accessibilityTreeRoot.classList.add('xterm-accessibility');
this._rowContainer = document.createElement('div');
this._rowContainer.classList.add('xterm-accessibility-tree');
for (let i = 0; i < this._terminal.rows; i++) {
this._rowElements[i] = this._createAccessibilityTreeNode();
this._rowContainer.appendChild(this._rowElements[i]);
}
this._topBoundaryFocusListener = e => this._onBoundaryFocus(e, BoundaryPosition.Top);
this._bottomBoundaryFocusListener = e => this._onBoundaryFocus(e, BoundaryPosition.Bottom);
this._rowElements[0].addEventListener('focus', this._topBoundaryFocusListener);
this._rowElements[this._rowElements.length - 1].addEventListener('focus', this._bottomBoundaryFocusListener);
this._refreshRowsDimensions();
this._accessibilityTreeRoot.appendChild(this._rowContainer);
this._renderRowsDebouncer = new RenderDebouncer(this._terminal, this._renderRows.bind(this));
this._refreshRows();
this._liveRegion = document.createElement('div');
this._liveRegion.classList.add('live-region');
this._liveRegion.setAttribute('aria-live', 'assertive');
this._accessibilityTreeRoot.appendChild(this._liveRegion);
this._terminal.element.insertAdjacentElement('afterbegin', this._accessibilityTreeRoot);
this._disposables.push(this._renderRowsDebouncer);
this._disposables.push(this._terminal.addDisposableListener('resize', data => this._onResize(data.cols, data.rows)));
this._disposables.push(this._terminal.addDisposableListener('refresh', data => this._refreshRows(data.start, data.end)));
this._disposables.push(this._terminal.addDisposableListener('scroll', data => this._refreshRows()));
// Line feed is an issue as the prompt won't be read out after a command is run
this._disposables.push(this._terminal.addDisposableListener('a11y.char', (char) => this._onChar(char)));
this._disposables.push(this._terminal.addDisposableListener('linefeed', () => this._onChar('\n')));
this._disposables.push(this._terminal.addDisposableListener('a11y.tab', spaceCount => this._onTab(spaceCount)));
this._disposables.push(this._terminal.addDisposableListener('key', keyChar => this._onKey(keyChar)));
this._disposables.push(this._terminal.addDisposableListener('blur', () => this._clearLiveRegion()));
// TODO: Maybe renderer should fire an event on terminal when the characters change and that
// should be listened to instead? That would mean that the order of events are always
// guarenteed
this._disposables.push(this._terminal.addDisposableListener('dprchange', () => this._refreshRowsDimensions()));
this._disposables.push(this._terminal.renderer.addDisposableListener('resize', () => this._refreshRowsDimensions()));
// This shouldn't be needed on modern browsers but is present in case the
// media query that drives the dprchange event isn't supported
this._disposables.push(addDisposableListener(window, 'resize', () => this._refreshRowsDimensions()));
}
public dispose(): void {
this._terminal.element.removeChild(this._accessibilityTreeRoot);
this._disposables.forEach(d => d.dispose());
this._disposables = null;
this._accessibilityTreeRoot = null;
this._rowContainer = null;
this._liveRegion = null;
this._rowContainer = null;
this._rowElements = null;
}
private _onBoundaryFocus(e: FocusEvent, position: BoundaryPosition): void {
const boundaryElement = <HTMLElement>e.target;
const beforeBoundaryElement = this._rowElements[position === BoundaryPosition.Top ? 1 : this._rowElements.length - 2];
// Don't scroll if the buffer top has reached the end in that direction
const posInSet = boundaryElement.getAttribute('aria-posinset');
const lastRowPos = position === BoundaryPosition.Top ? '1' : `${this._terminal.buffer.lines.length}`;
if (posInSet === lastRowPos) {
return;
}
// Don't scroll when the last focused item was not the second row (focus is going the other
// direction)
if (e.relatedTarget !== beforeBoundaryElement) {
return;
}
// Remove old boundary element from array
let topBoundaryElement: HTMLElement;
let bottomBoundaryElement: HTMLElement;
if (position === BoundaryPosition.Top) {
topBoundaryElement = boundaryElement;
bottomBoundaryElement = this._rowElements.pop();
this._rowContainer.removeChild(bottomBoundaryElement);
} else {
topBoundaryElement = this._rowElements.shift();
bottomBoundaryElement = boundaryElement;
this._rowContainer.removeChild(topBoundaryElement);
}
// Remove listeners from old boundary elements
topBoundaryElement.removeEventListener('focus', this._topBoundaryFocusListener);
bottomBoundaryElement.removeEventListener('focus', this._bottomBoundaryFocusListener);
// Add new element to array/DOM
if (position === BoundaryPosition.Top) {
const newElement = this._createAccessibilityTreeNode();
this._rowElements.unshift(newElement);
this._rowContainer.insertAdjacentElement('afterbegin', newElement);
} else {
const newElement = this._createAccessibilityTreeNode();
this._rowElements.push(newElement);
this._rowContainer.appendChild(newElement);
}
// Add listeners to new boundary elements
this._rowElements[0].addEventListener('focus', this._topBoundaryFocusListener);
this._rowElements[this._rowElements.length - 1].addEventListener('focus', this._bottomBoundaryFocusListener);
// Scroll up
this._terminal.scrollLines(position === BoundaryPosition.Top ? -1 : 1);
// Focus new boundary before element
this._rowElements[position === BoundaryPosition.Top ? 1 : this._rowElements.length - 2].focus();
// Prevent the standard behavior
e.preventDefault();
e.stopImmediatePropagation();
}
private _onResize(cols: number, rows: number): void {
// Remove bottom boundary listener
this._rowElements[this._rowElements.length - 1].removeEventListener('focus', this._bottomBoundaryFocusListener);
// Grow rows as required
for (let i = this._rowContainer.children.length; i < this._terminal.rows; i++) {
this._rowElements[i] = this._createAccessibilityTreeNode();
this._rowContainer.appendChild(this._rowElements[i]);
}
// Shrink rows as required
while (this._rowElements.length > rows) {
this._rowContainer.removeChild(this._rowElements.pop());
}
// Add bottom boundary listener
this._rowElements[this._rowElements.length - 1].addEventListener('focus', this._bottomBoundaryFocusListener);
this._refreshRowsDimensions();
}
public _createAccessibilityTreeNode(): HTMLElement {
const element = document.createElement('div');
element.setAttribute('role', 'listitem');
element.tabIndex = -1;
this._refreshRowDimensions(element);
return element;
}
private _onTab(spaceCount: number): void {
for (let i = 0; i < spaceCount; i++) {
this._onChar(' ');
}
}
private _onChar(char: string): void {
if (this._liveRegionLineCount < MAX_ROWS_TO_READ + 1) {
if (this._charsToConsume.length > 0) {
// Have the screen reader ignore the char if it was just input
const shiftedChar = this._charsToConsume.shift();
if (shiftedChar !== char) {
this._announceCharacter(char);
}
} else {
this._announceCharacter(char);
}
if (char === '\n') {
this._liveRegionLineCount++;
if (this._liveRegionLineCount === MAX_ROWS_TO_READ + 1) {
this._liveRegion.textContent += Strings.tooMuchOutput;
}
}
// Only detach/attach on mac as otherwise messages can go unaccounced
if (isMac) {
if (this._liveRegion.textContent.length > 0 && !this._liveRegion.parentNode) {
setTimeout(() => {
this._accessibilityTreeRoot.appendChild(this._liveRegion);
}, 0);
}
}
}
}
private _clearLiveRegion(): void {
this._liveRegion.textContent = '';
this._liveRegionLineCount = 0;
// Only detach/attach on mac as otherwise messages can go unaccounced
if (isMac) {
if (this._liveRegion.parentNode) {
this._accessibilityTreeRoot.removeChild(this._liveRegion);
}
}
}
private _onKey(keyChar: string): void {
this._clearLiveRegion();
this._charsToConsume.push(keyChar);
}
private _refreshRows(start?: number, end?: number): void {
this._renderRowsDebouncer.refresh(start, end);
}
private _renderRows(start: number, end: number): void {
const buffer: IBuffer = this._terminal.buffer;
const setSize = buffer.lines.length.toString();
for (let i = start; i <= end; i++) {
const lineData = buffer.translateBufferLineToString(buffer.ydisp + i, true);
const posInSet = (buffer.ydisp + i + 1).toString();
const element = this._rowElements[i];
element.textContent = lineData.length === 0 ? Strings.blankLine : lineData;
element.setAttribute('aria-posinset', posInSet);
element.setAttribute('aria-setsize', setSize);
}
}
private _refreshRowsDimensions(): void {
if (!this._terminal.renderer.dimensions.actualCellHeight) {
return;
}
const buffer: IBuffer = this._terminal.buffer;
for (let i = 0; i < this._terminal.rows; i++) {
this._refreshRowDimensions(this._rowElements[i]);
}
}
private _refreshRowDimensions(element: HTMLElement): void {
element.style.height = `${this._terminal.renderer.dimensions.actualCellHeight}px`;
}
private _announceCharacter(char: string): void {
if (char === ' ') {
// Always use nbsp for spaces in order to preserve the space between characters in
// voiceover's caption window
this._liveRegion.innerHTML += '&nbsp;';
} else {
this._liveRegion.textContent += char;
}
}
}
+1 -1
View File
@@ -4,7 +4,7 @@
*/ */
import { assert } from 'chai'; import { assert } from 'chai';
import { ITerminal } from './Types'; import { ITerminal } from './Interfaces';
import { Buffer } from './Buffer'; import { Buffer } from './Buffer';
import { CircularList } from './utils/CircularList'; import { CircularList } from './utils/CircularList';
import { MockTerminal } from './utils/TestUtils.test'; import { MockTerminal } from './utils/TestUtils.test';
+15 -9
View File
@@ -3,8 +3,9 @@
* @license MIT * @license MIT
*/ */
import { ITerminal, IBuffer } from './Interfaces';
import { CircularList } from './utils/CircularList'; import { CircularList } from './utils/CircularList';
import { LineData, CharData, ITerminal, IBuffer } from './Types'; import { LineData, CharData } from './Types';
export const CHAR_DATA_ATTR_INDEX = 0; export const CHAR_DATA_ATTR_INDEX = 0;
export const CHAR_DATA_CHAR_INDEX = 1; export const CHAR_DATA_CHAR_INDEX = 1;
@@ -120,6 +121,11 @@ export class Buffer implements IBuffer {
if (this._terminal.cols < newCols) { if (this._terminal.cols < newCols) {
const ch: CharData = [this._terminal.defAttr, ' ', 1, 32]; // does xterm use the default attr? const ch: CharData = [this._terminal.defAttr, ' ', 1, 32]; // does xterm use the default attr?
for (let i = 0; i < this._lines.length; i++) { for (let i = 0; i < this._lines.length; i++) {
// TODO: This should be removed, with tests setup for the case that was
// causing the underlying bug, see https://github.com/sourcelair/xterm.js/issues/824
if (this._lines.get(i) === undefined) {
this._lines.set(i, this._terminal.blankLine(undefined, undefined, newCols));
}
while (this._lines.get(i).length < newCols) { while (this._lines.get(i).length < newCols) {
this._lines.get(i).push(ch); this._lines.get(i).push(ch);
} }
@@ -176,13 +182,16 @@ export class Buffer implements IBuffer {
} }
// Make sure that the cursor stays on screen // Make sure that the cursor stays on screen
this.x = Math.min(this.x, newCols - 1); if (this.y >= newRows) {
this.y = Math.min(this.y, newRows - 1); this.y = newRows - 1;
}
if (addToY) { if (addToY) {
this.y += addToY; this.y += addToY;
} }
this.savedY = Math.min(this.savedY, newRows - 1);
this.savedX = Math.min(this.savedX, newCols - 1); if (this.x >= newCols) {
this.x = newCols - 1;
}
this.scrollTop = 0; this.scrollTop = 0;
} }
@@ -213,10 +222,7 @@ export class Buffer implements IBuffer {
// needed here because some chars are 0 characters long (eg. after wide // needed here because some chars are 0 characters long (eg. after wide
// chars) and some chars are longer than 1 characters long (eg. emojis). // chars) and some chars are longer than 1 characters long (eg. emojis).
let startIndex = startCol; let startIndex = startCol;
// Only set endCol to the line length when it is null. 0 is a valid column. endCol = endCol || line.length;
if (endCol === null) {
endCol = line.length;
}
let endIndex = endCol; let endIndex = endCol;
for (let i = 0; i < line.length; i++) { for (let i = 0; i < line.length; i++) {
+1 -1
View File
@@ -4,7 +4,7 @@
*/ */
import { assert } from 'chai'; import { assert } from 'chai';
import { ITerminal } from './Types'; import { ITerminal } from './Interfaces';
import { BufferSet } from './BufferSet'; import { BufferSet } from './BufferSet';
import { Buffer } from './Buffer'; import { Buffer } from './Buffer';
import { MockTerminal } from './utils/TestUtils.test'; import { MockTerminal } from './utils/TestUtils.test';
+4 -15
View File
@@ -3,7 +3,7 @@
* @license MIT * @license MIT
*/ */
import { ITerminal, IBufferSet } from './Types'; import { ITerminal, IBufferSet } from './Interfaces';
import { Buffer } from './Buffer'; import { Buffer } from './Buffer';
import { EventEmitter } from './EventEmitter'; import { EventEmitter } from './EventEmitter';
@@ -61,35 +61,24 @@ export class BufferSet extends EventEmitter implements IBufferSet {
* Sets the normal Buffer of the BufferSet as its currently active Buffer * Sets the normal Buffer of the BufferSet as its currently active Buffer
*/ */
public activateNormalBuffer(): void { public activateNormalBuffer(): void {
if (this._activeBuffer === this._normal) {
return;
}
// The alt buffer should always be cleared when we switch to the normal // The alt buffer should always be cleared when we switch to the normal
// buffer. This frees up memory since the alt buffer should always be new // buffer. This frees up memory since the alt buffer should always be new
// when activated. // when activated.
this._alt.clear(); this._alt.clear();
this._activeBuffer = this._normal; this._activeBuffer = this._normal;
this.emit('activate', { this.emit('activate', this._normal);
activeBuffer: this._normal,
inactiveBuffer: this._alt
});
} }
/** /**
* Sets the alt Buffer of the BufferSet as its currently active Buffer * Sets the alt Buffer of the BufferSet as its currently active Buffer
*/ */
public activateAltBuffer(): void { public activateAltBuffer(): void {
if (this._activeBuffer === this._alt) {
return;
}
// Since the alt buffer is always cleared when the normal buffer is // Since the alt buffer is always cleared when the normal buffer is
// activated, we want to fill it when switching to it. // activated, we want to fill it when switching to it.
this._alt.fillViewportRows(); this._alt.fillViewportRows();
this._activeBuffer = this._alt; this._activeBuffer = this._alt;
this.emit('activate', { this.emit('activate', this._alt);
activeBuffer: this._alt,
inactiveBuffer: this._normal
});
} }
/** /**
+12 -22
View File
@@ -63,33 +63,28 @@ export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: nu
let min = 0; let min = 0;
let max = data.length - 1; let max = data.length - 1;
let mid; let mid;
if (ucs < data[0][0] || ucs > data[max][1]) { if (ucs < data[0][0] || ucs > data[max][1])
return false; return false;
}
while (max >= min) { while (max >= min) {
mid = (min + max) >> 1; mid = (min + max) >> 1;
if (ucs > data[mid][1]) { if (ucs > data[mid][1])
min = mid + 1; min = mid + 1;
} else if (ucs < data[mid][0]) { else if (ucs < data[mid][0])
max = mid - 1; max = mid - 1;
} else { else
return true; return true;
}
} }
return false; return false;
} }
function wcwidthBMP(ucs: number): number { function wcwidthBMP(ucs: number): number {
// test for 8-bit control characters // test for 8-bit control characters
if (ucs === 0) { if (ucs === 0)
return opts.nul; return opts.nul;
} if (ucs < 32 || (ucs >= 0x7f && ucs < 0xa0))
if (ucs < 32 || (ucs >= 0x7f && ucs < 0xa0)) {
return opts.control; return opts.control;
}
// binary search in table of non-spacing characters // binary search in table of non-spacing characters
if (bisearch(ucs, COMBINING_BMP)) { if (bisearch(ucs, COMBINING_BMP))
return 0; return 0;
}
// if we arrive here, ucs is not a combining or C0/C1 control character // if we arrive here, ucs is not a combining or C0/C1 control character
if (isWideBMP(ucs)) { if (isWideBMP(ucs)) {
return 2; return 2;
@@ -111,9 +106,8 @@ export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: nu
(ucs >= 0xffe0 && ucs <= 0xffe6))); (ucs >= 0xffe0 && ucs <= 0xffe6)));
} }
function wcwidthHigh(ucs: number): 0 | 1 | 2 { function wcwidthHigh(ucs: number): 0 | 1 | 2 {
if (bisearch(ucs, COMBINING_HIGH)) { if (bisearch(ucs, COMBINING_HIGH))
return 0; return 0;
}
if ((ucs >= 0x20000 && ucs <= 0x2fffd) || (ucs >= 0x30000 && ucs <= 0x3fffd)) { if ((ucs >= 0x20000 && ucs <= 0x2fffd) || (ucs >= 0x30000 && ucs <= 0x3fffd)) {
return 2; return 2;
} }
@@ -134,9 +128,8 @@ export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: nu
for (let i = 0; i < CONTAINERSIZE; ++i) { for (let i = 0; i < CONTAINERSIZE; ++i) {
let num = 0; let num = 0;
let pos = CODEPOINTS_PER_ITEM; let pos = CODEPOINTS_PER_ITEM;
while (pos--) { while (pos--)
num = (num << 2) | wcwidthBMP(CODEPOINTS_PER_ITEM * i + pos); num = (num << 2) | wcwidthBMP(CODEPOINTS_PER_ITEM * i + pos);
}
table[i] = num; table[i] = num;
} }
return table; return table;
@@ -155,16 +148,13 @@ export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: nu
// ==> n = n & 3 e.g. 000000000000000000000000000000XX // ==> n = n & 3 e.g. 000000000000000000000000000000XX
return function (num: number): number { return function (num: number): number {
num = num | 0; // get asm.js like optimization under V8 num = num | 0; // get asm.js like optimization under V8
if (num < 32) { if (num < 32)
return control | 0; return control | 0;
} if (num < 127)
if (num < 127) {
return 1; return 1;
}
let t = table || init_table(); let t = table || init_table();
if (num < 65536) { if (num < 65536)
return t[num >> 4] >> ((num & 15) << 1) & 3; return t[num >> 4] >> ((num & 15) << 1) & 3;
}
// do a full search for high codepoints // do a full search for high codepoints
return wcwidthHigh(num); return wcwidthHigh(num);
}; };
+3 -3
View File
@@ -3,19 +3,19 @@
* @license MIT * @license MIT
*/ */
import { ICharset } from './Types'; import { Charset } from './Types';
/** /**
* The character sets supported by the terminal. These enable several languages * The character sets supported by the terminal. These enable several languages
* to be represented within the terminal with only 8-bit encoding. See ISO 2022 * to be represented within the terminal with only 8-bit encoding. See ISO 2022
* for a discussion on character sets. Only VT100 character sets are supported. * for a discussion on character sets. Only VT100 character sets are supported.
*/ */
export const CHARSETS: { [key: string]: ICharset } = {}; export const CHARSETS: { [key: string]: Charset } = {};
/** /**
* The default character set, US. * The default character set, US.
*/ */
export const DEFAULT_CHARSET: ICharset = CHARSETS['B']; export const DEFAULT_CHARSET: Charset = CHARSETS['B'];
/** /**
* DEC Special Character and Line Drawing Set. * DEC Special Character and Line Drawing Set.
+3 -3
View File
@@ -3,7 +3,7 @@
* @license MIT * @license MIT
*/ */
import { ITerminal } from './Types'; import { ITerminal } from './Interfaces';
interface IPosition { interface IPosition {
start: number; start: number;
@@ -217,7 +217,7 @@ export class CompositionHelper {
if (!dontRecurse) { if (!dontRecurse) {
setTimeout(() => this.updateCompositionElements(true), 0); setTimeout(() => this.updateCompositionElements(true), 0);
} }
} };
/** /**
* Clears the textarea's position so that the cursor does not blink on IE. * Clears the textarea's position so that the cursor does not blink on IE.
@@ -226,5 +226,5 @@ export class CompositionHelper {
private clearTextareaPosition(): void { private clearTextareaPosition(): void {
this.textarea.style.left = ''; this.textarea.style.left = '';
this.textarea.style.top = ''; this.textarea.style.top = '';
} };
} }
+1 -1
View File
@@ -76,4 +76,4 @@ export namespace C0 {
export const SP = '\x20'; export const SP = '\x20';
/** Delete (Caret = ^?) */ /** Delete (Caret = ^?) */
export const DEL = '\x7f'; export const DEL = '\x7f';
} };
+12
View File
@@ -13,6 +13,18 @@ describe('EventEmitter', () => {
eventEmitter = new EventEmitter(); eventEmitter = new EventEmitter();
}); });
describe('once', () => {
it('should trigger the listener only once', () => {
let count = 0;
const listener = () => count++;
eventEmitter.once('test', listener);
eventEmitter.emit('test');
assert.equal(count, 1);
eventEmitter.emit('test');
assert.equal(count, 1);
});
});
describe('emit', () => { describe('emit', () => {
it('should emit events to listeners', () => { it('should emit events to listeners', () => {
let count1 = 0; let count1 = 0;
+16 -26
View File
@@ -3,11 +3,10 @@
* @license MIT * @license MIT
*/ */
import { XtermListener } from './Types'; import { IEventEmitter, IListenerType } from './Interfaces';
import { IEventEmitter, IDisposable } from 'xterm';
export class EventEmitter implements IEventEmitter { export class EventEmitter implements IEventEmitter {
private _events: {[type: string]: XtermListener[]}; private _events: {[type: string]: IListenerType[]};
constructor() { constructor() {
// Restore the previous events if available, this will happen if the // Restore the previous events if available, this will happen if the
@@ -15,31 +14,12 @@ export class EventEmitter implements IEventEmitter {
this._events = this._events || {}; this._events = this._events || {};
} }
public on(type: string, listener: XtermListener): void { public on(type: string, listener: IListenerType): void {
this._events[type] = this._events[type] || []; this._events[type] = this._events[type] || [];
this._events[type].push(listener); this._events[type].push(listener);
} }
/** public off(type: string, listener: IListenerType): void {
* Adds a disposabe listener to the EventEmitter, returning the disposable.
* @param type The event type.
* @param handler The handler for the listener.
*/
public addDisposableListener(type: string, handler: XtermListener): IDisposable {
this.on(type, handler);
return {
dispose: () => {
if (!handler) {
// Already disposed
return;
}
this.off(type, handler);
handler = null;
}
};
}
public off(type: string, listener: XtermListener): void {
if (!this._events[type]) { if (!this._events[type]) {
return; return;
} }
@@ -48,7 +28,7 @@ export class EventEmitter implements IEventEmitter {
let i = obj.length; let i = obj.length;
while (i--) { while (i--) {
if (obj[i] === listener) { if (obj[i] === listener || obj[i].listener === listener) {
obj.splice(i, 1); obj.splice(i, 1);
return; return;
} }
@@ -61,6 +41,16 @@ export class EventEmitter implements IEventEmitter {
} }
} }
public once(type: string, listener: IListenerType): void {
function on(): void {
let args = Array.prototype.slice.call(arguments);
this.off(type, on);
listener.apply(this, args);
}
(<any>on).listener = listener;
this.on(type, on);
}
public emit(type: string, ...args: any[]): void { public emit(type: string, ...args: any[]): void {
if (!this._events[type]) { if (!this._events[type]) {
return; return;
@@ -71,7 +61,7 @@ export class EventEmitter implements IEventEmitter {
} }
} }
public listeners(type: string): XtermListener[] { public listeners(type: string): IListenerType[] {
return this._events[type] || []; return this._events[type] || [];
} }
+11 -17
View File
@@ -4,9 +4,10 @@
* @license MIT * @license MIT
*/ */
import { CharData, IInputHandler, IInputHandlingTerminal, ITerminal } from './Types'; import { IInputHandler, ITerminal, IInputHandlingTerminal } from './Interfaces';
import { C0 } from './EscapeSequences'; import { C0 } from './EscapeSequences';
import { DEFAULT_CHARSET } from './Charsets'; import { DEFAULT_CHARSET } from './Charsets';
import { CharData } from './Types';
import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX } from './Buffer'; import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX } from './Buffer';
import { FLAGS } from './renderer/Types'; import { FLAGS } from './renderer/Types';
import { wcwidth } from './CharWidth'; import { wcwidth } from './CharWidth';
@@ -25,21 +26,17 @@ export class InputHandler implements IInputHandler {
if (char >= ' ') { if (char >= ' ') {
// calculate print space // calculate print space
// expensive call, therefore we save width in line buffer // expensive call, therefore we save width in line buffer
const chWidth = wcwidth(code); const ch_width = wcwidth(code);
if (this._terminal.charset && this._terminal.charset[char]) { if (this._terminal.charset && this._terminal.charset[char]) {
char = this._terminal.charset[char]; char = this._terminal.charset[char];
} }
if (this._terminal.options.screenReaderMode) {
this._terminal.emit('a11y.char', char);
}
let row = this._terminal.buffer.y + this._terminal.buffer.ybase; let row = this._terminal.buffer.y + this._terminal.buffer.ybase;
// insert combining char in last cell // insert combining char in last cell
// FIXME: needs handling after cursor jumps // FIXME: needs handling after cursor jumps
if (!chWidth && this._terminal.buffer.x) { if (!ch_width && this._terminal.buffer.x) {
// dont overflow left // dont overflow left
if (this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1]) { if (this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1]) {
if (!this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1][CHAR_DATA_WIDTH_INDEX]) { if (!this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1][CHAR_DATA_WIDTH_INDEX]) {
@@ -59,7 +56,7 @@ export class InputHandler implements IInputHandler {
// goto next line if ch would overflow // goto next line if ch would overflow
// TODO: needs a global min terminal width of 2 // TODO: needs a global min terminal width of 2
if (this._terminal.buffer.x + chWidth - 1 >= this._terminal.cols) { if (this._terminal.buffer.x + ch_width - 1 >= this._terminal.cols) {
// autowrap - DECAWM // autowrap - DECAWM
if (this._terminal.wraparoundMode) { if (this._terminal.wraparoundMode) {
this._terminal.buffer.x = 0; this._terminal.buffer.x = 0;
@@ -73,9 +70,8 @@ export class InputHandler implements IInputHandler {
(<any>this._terminal.buffer.lines.get(this._terminal.buffer.y)).isWrapped = true; (<any>this._terminal.buffer.lines.get(this._terminal.buffer.y)).isWrapped = true;
} }
} else { } else {
if (chWidth === 2) { // FIXME: check for xterm behavior if (ch_width === 2) // FIXME: check for xterm behavior
return; return;
}
} }
} }
row = this._terminal.buffer.y + this._terminal.buffer.ybase; row = this._terminal.buffer.y + this._terminal.buffer.ybase;
@@ -83,7 +79,7 @@ export class InputHandler implements IInputHandler {
// insert mode: move characters to right // insert mode: move characters to right
if (this._terminal.insertMode) { if (this._terminal.insertMode) {
// do this twice for a fullwidth char // do this twice for a fullwidth char
for (let moves = 0; moves < chWidth; ++moves) { for (let moves = 0; moves < ch_width; ++moves) {
// remove last cell, if it's width is 0 // remove last cell, if it's width is 0
// we have to adjust the second last cell as well // we have to adjust the second last cell as well
const removed = this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).pop(); const removed = this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).pop();
@@ -98,12 +94,12 @@ export class InputHandler implements IInputHandler {
} }
} }
this._terminal.buffer.lines.get(row)[this._terminal.buffer.x] = [this._terminal.curAttr, char, chWidth, char.charCodeAt(0)]; this._terminal.buffer.lines.get(row)[this._terminal.buffer.x] = [this._terminal.curAttr, char, ch_width, char.charCodeAt(0)];
this._terminal.buffer.x++; this._terminal.buffer.x++;
this._terminal.updateRange(this._terminal.buffer.y); this._terminal.updateRange(this._terminal.buffer.y);
// fullwidth char - set next cell width to zero and advance cursor // fullwidth char - set next cell width to zero and advance cursor
if (chWidth === 2) { if (ch_width === 2) {
this._terminal.buffer.lines.get(row)[this._terminal.buffer.x] = [this._terminal.curAttr, '', 0, undefined]; this._terminal.buffer.lines.get(row)[this._terminal.buffer.x] = [this._terminal.curAttr, '', 0, undefined];
this._terminal.buffer.x++; this._terminal.buffer.x++;
} }
@@ -166,11 +162,7 @@ export class InputHandler implements IInputHandler {
* Horizontal Tab (HT) (Ctrl-I). * Horizontal Tab (HT) (Ctrl-I).
*/ */
public tab(): void { public tab(): void {
const originalX = this._terminal.buffer.x;
this._terminal.buffer.x = this._terminal.buffer.nextStop(); this._terminal.buffer.x = this._terminal.buffer.nextStop();
if (this._terminal.options.screenReaderMode) {
this._terminal.emit('a11y.tab', this._terminal.buffer.x - originalX);
}
} }
/** /**
@@ -936,6 +928,7 @@ export class InputHandler implements IInputHandler {
case 47: // alt screen buffer case 47: // alt screen buffer
case 1047: // alt screen buffer case 1047: // alt screen buffer
this._terminal.buffers.activateAltBuffer(); this._terminal.buffers.activateAltBuffer();
this._terminal.selectionManager.setBuffer(this._terminal.buffer);
this._terminal.viewport.syncScrollArea(); this._terminal.viewport.syncScrollArea();
this._terminal.showCursor(); this._terminal.showCursor();
break; break;
@@ -1107,6 +1100,7 @@ export class InputHandler implements IInputHandler {
// if (params[0] === 1049) { // if (params[0] === 1049) {
// this.restoreCursor(params); // this.restoreCursor(params);
// } // }
this._terminal.selectionManager.setBuffer(this._terminal.buffer);
this._terminal.refresh(0, this._terminal.rows - 1); this._terminal.refresh(0, this._terminal.rows - 1);
this._terminal.viewport.syncScrollArea(); this._terminal.viewport.syncScrollArea();
this._terminal.showCursor(); this._terminal.showCursor();
+354
View File
@@ -0,0 +1,354 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { ILinkMatcherOptions } from './Interfaces';
import { LinkMatcherHandler, LinkMatcherValidationCallback, Charset, LineData } from './Types';
import { IColorSet, IRenderer } from './renderer/Interfaces';
import { IMouseZoneManager } from './input/Interfaces';
export interface IBrowser {
isNode: boolean;
userAgent: string;
platform: string;
isFirefox: boolean;
isMSIE: boolean;
isMac: boolean;
isIpad: boolean;
isIphone: boolean;
isMSWindows: boolean;
}
export interface IBufferAccessor {
buffer: IBuffer;
}
export interface IElementAccessor {
element: HTMLElement;
}
export interface ILinkifierAccessor {
linkifier: ILinkifier;
}
export interface ITerminal extends ILinkifierAccessor, IBufferAccessor, IElementAccessor, IEventEmitter {
selectionManager: ISelectionManager;
charMeasure: ICharMeasure;
textarea: HTMLTextAreaElement;
renderer: IRenderer;
rows: number;
cols: number;
browser: IBrowser;
writeBuffer: string[];
cursorHidden: boolean;
cursorState: number;
defAttr: number;
options: ITerminalOptions;
buffers: IBufferSet;
isFocused: boolean;
mouseHelper: IMouseHelper;
bracketedPasteMode: boolean;
/**
* Emit the 'data' event and populate the given data.
* @param data The data to populate in the event.
*/
handler(data: string): void;
scrollLines(disp: number, suppressScrollEvent?: boolean): void;
cancel(ev: Event, force?: boolean): boolean | void;
log(text: string): void;
reset(): void;
showCursor(): void;
blankLine(cur?: boolean, isWrapped?: boolean, cols?: number): LineData;
refresh(start: number, end: number): void;
}
/**
* This interface encapsulates everything needed from the Terminal by the
* InputHandler. This cleanly separates the large amount of methods needed by
* InputHandler cleanly from the ITerminal interface.
*/
export interface IInputHandlingTerminal extends IEventEmitter {
element: HTMLElement;
options: ITerminalOptions;
cols: number;
rows: number;
charset: Charset;
gcharset: number;
glevel: number;
charsets: Charset[];
applicationKeypad: boolean;
applicationCursor: boolean;
originMode: boolean;
insertMode: boolean;
wraparoundMode: boolean;
bracketedPasteMode: boolean;
defAttr: number;
curAttr: number;
prefix: string;
savedCols: number;
x10Mouse: boolean;
vt200Mouse: boolean;
normalMouse: boolean;
mouseEvents: boolean;
sendFocus: boolean;
utfMouse: boolean;
sgrMouse: boolean;
urxvtMouse: boolean;
cursorHidden: boolean;
buffers: IBufferSet;
buffer: IBuffer;
viewport: IViewport;
selectionManager: ISelectionManager;
bell(): void;
focus(): void;
convertEol: boolean;
updateRange(y: number): void;
scroll(isWrapped?: boolean): void;
setgLevel(g: number): void;
eraseAttr(): number;
eraseRight(x: number, y: number): void;
eraseLine(y: number): void;
eraseLeft(x: number, y: number): void;
blankLine(cur?: boolean, isWrapped?: boolean): LineData;
is(term: string): boolean;
send(data: string): void;
setgCharset(g: number, charset: Charset): void;
resize(x: number, y: number): void;
log(text: string, data?: any): void;
reset(): void;
showCursor(): void;
refresh(start: number, end: number): void;
matchColor(r1: number, g1: number, b1: number): number;
error(text: string, data?: any): void;
setOption(key: string, value: any): void;
}
export interface ITerminalOptions {
bellSound?: string;
bellStyle?: string;
cancelEvents?: boolean;
cols?: number;
convertEol?: boolean;
cursorBlink?: boolean;
cursorStyle?: string;
debug?: boolean;
disableStdin?: boolean;
enableBold?: boolean;
fontSize?: number;
fontFamily?: string;
handler?: (data: string) => void;
letterSpacing?: number;
lineHeight?: number;
rows?: number;
screenKeys?: boolean;
scrollback?: number;
tabStopWidth?: number;
termName?: string;
theme?: ITheme;
useFlowControl?: boolean;
}
export interface IBuffer {
lines: ICircularList<LineData>;
ydisp: number;
ybase: number;
y: number;
x: number;
tabs: any;
scrollBottom: number;
scrollTop: number;
savedY: number;
savedX: number;
isCursorInViewport: boolean;
translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol?: number, endCol?: number): string;
nextStop(x?: number): number;
prevStop(x?: number): number;
}
export interface IBufferSet {
alt: IBuffer;
normal: IBuffer;
active: IBuffer;
activateNormalBuffer(): void;
activateAltBuffer(): void;
}
export interface IMouseHelper {
getCoords(event: {pageX: number, pageY: number}, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number, isSelection?: boolean): [number, number];
getRawByteCoords(event: MouseEvent, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number): { x: number, y: number };
}
export interface IViewport {
syncScrollArea(): void;
onWheel(ev: WheelEvent): void;
onTouchStart(ev: TouchEvent): void;
onTouchMove(ev: TouchEvent): void;
onThemeChanged(colors: IColorSet): void;
}
export interface ISelectionManager {
selectionText: string;
selectionStart: [number, number];
selectionEnd: [number, number];
disable(): void;
enable(): void;
setBuffer(buffer: IBuffer): void;
setSelection(row: number, col: number, length: number): void;
}
export interface ICompositionHelper {
compositionstart(): void;
compositionupdate(ev: CompositionEvent): void;
compositionend(): void;
updateCompositionElements(dontRecurse?: boolean): void;
keydown(ev: KeyboardEvent): boolean;
}
export interface ICharMeasure {
width: number;
height: number;
measure(options: ITerminalOptions): void;
}
export interface ILinkifier extends IEventEmitter {
attachToDom(mouseZoneManager: IMouseZoneManager): void;
linkifyRows(start: number, end: number): void;
setHypertextLinkHandler(handler: LinkMatcherHandler): void;
setHypertextValidationCallback(callback: LinkMatcherValidationCallback): void;
registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options?: ILinkMatcherOptions): number;
deregisterLinkMatcher(matcherId: number): boolean;
}
export interface ICircularList<T> extends IEventEmitter {
length: number;
maxLength: number;
forEach: (callbackfn: (value: T, index: number) => void) => void;
get(index: number): T;
set(index: number, value: T): void;
push(value: T): void;
pop(): T;
splice(start: number, deleteCount: number, ...items: T[]): void;
trimStart(count: number): void;
shiftElements(start: number, count: number, offset: number): void;
}
export interface IEventEmitter {
on(type: string, listener: IListenerType): void;
off(type: string, listener: IListenerType): void;
emit(type: string, data?: any): void;
}
export interface IListenerType {
(data?: any): void;
listener?: (data?: any) => void;
};
export interface ILinkMatcherOptions {
/**
* The index of the link from the regex.match(text) call. This defaults to 0
* (for regular expressions without capture groups).
*/
matchIndex?: number;
/**
* A callback that validates an individual link, returning true if valid and
* false if invalid.
*/
validationCallback?: LinkMatcherValidationCallback;
/**
* A callback that fires when the mouse hovers over a link.
*/
tooltipCallback?: LinkMatcherHandler;
/**
* A callback that fires when the mouse leaves a link that was hovered.
*/
leaveCallback?: () => void;
/**
* The priority of the link matcher, this defines the order in which the link
* matcher is evaluated relative to others, from highest to lowest. The
* default value is 0.
*/
priority?: number;
}
/**
* Handles actions generated by the parser.
*/
export interface IInputHandler {
addChar(char: string, code: number): void;
/** C0 BEL */ bell(): void;
/** C0 LF */ lineFeed(): void;
/** C0 CR */ carriageReturn(): void;
/** C0 BS */ backspace(): void;
/** C0 HT */ tab(): void;
/** C0 SO */ shiftOut(): void;
/** C0 SI */ shiftIn(): void;
/** CSI @ */ insertChars(params?: number[]): void;
/** CSI A */ cursorUp(params?: number[]): void;
/** CSI B */ cursorDown(params?: number[]): void;
/** CSI C */ cursorForward(params?: number[]): void;
/** CSI D */ cursorBackward(params?: number[]): void;
/** CSI E */ cursorNextLine(params?: number[]): void;
/** CSI F */ cursorPrecedingLine(params?: number[]): void;
/** CSI G */ cursorCharAbsolute(params?: number[]): void;
/** CSI H */ cursorPosition(params?: number[]): void;
/** CSI I */ cursorForwardTab(params?: number[]): void;
/** CSI J */ eraseInDisplay(params?: number[]): void;
/** CSI K */ eraseInLine(params?: number[]): void;
/** CSI L */ insertLines(params?: number[]): void;
/** CSI M */ deleteLines(params?: number[]): void;
/** CSI P */ deleteChars(params?: number[]): void;
/** CSI S */ scrollUp(params?: number[]): void;
/** CSI T */ scrollDown(params?: number[]): void;
/** CSI X */ eraseChars(params?: number[]): void;
/** CSI Z */ cursorBackwardTab(params?: number[]): void;
/** CSI ` */ charPosAbsolute(params?: number[]): void;
/** CSI a */ HPositionRelative(params?: number[]): void;
/** CSI b */ repeatPrecedingCharacter(params?: number[]): void;
/** CSI c */ sendDeviceAttributes(params?: number[]): void;
/** CSI d */ linePosAbsolute(params?: number[]): void;
/** CSI e */ VPositionRelative(params?: number[]): void;
/** CSI f */ HVPosition(params?: number[]): void;
/** CSI g */ tabClear(params?: number[]): void;
/** CSI h */ setMode(params?: number[]): void;
/** CSI l */ resetMode(params?: number[]): void;
/** CSI m */ charAttributes(params?: number[]): void;
/** CSI n */ deviceStatus(params?: number[]): void;
/** CSI p */ softReset(params?: number[]): void;
/** CSI q */ setCursorStyle(params?: number[]): void;
/** CSI r */ setScrollRegion(params?: number[]): void;
/** CSI s */ saveCursor(params?: number[]): void;
/** CSI u */ restoreCursor(params?: number[]): void;
}
export interface ITheme {
foreground?: string;
background?: string;
cursor?: string;
cursorAccent?: string;
selection?: string;
black?: string;
red?: string;
green?: string;
yellow?: string;
blue?: string;
magenta?: string;
cyan?: string;
white?: string;
brightBlack?: string;
brightRed?: string;
brightGreen?: string;
brightYellow?: string;
brightBlue?: string;
brightMagenta?: string;
brightCyan?: string;
brightWhite?: string;
}
+13 -6
View File
@@ -4,9 +4,10 @@
*/ */
import { assert } from 'chai'; import { assert } from 'chai';
import { IMouseZoneManager, IMouseZone } from './input/Types'; import { ITerminal, ILinkifier, IBuffer, IBufferAccessor, IElementAccessor } from './Interfaces';
import { ILinkMatcher, LineData, ITerminal, ILinkifier, IBuffer, IBufferAccessor, IElementAccessor } from './Types';
import { Linkifier } from './Linkifier'; import { Linkifier } from './Linkifier';
import { LinkMatcher, LineData } from './Types';
import { IMouseZoneManager, IMouseZone } from './input/Interfaces';
import { MockBuffer } from './utils/TestUtils.test'; import { MockBuffer } from './utils/TestUtils.test';
import { CircularList } from './utils/CircularList'; import { CircularList } from './utils/CircularList';
@@ -16,7 +17,7 @@ class TestLinkifier extends Linkifier {
Linkifier.TIME_BEFORE_LINKIFY = 0; Linkifier.TIME_BEFORE_LINKIFY = 0;
} }
public get linkMatchers(): ILinkMatcher[] { return this._linkMatchers; } public get linkMatchers(): LinkMatcher[] { return this._linkMatchers; }
public linkifyRows(): void { super.linkifyRows(0, this._terminal.buffer.lines.length - 1); } public linkifyRows(): void { super.linkifyRows(0, this._terminal.buffer.lines.length - 1); }
} }
@@ -101,6 +102,12 @@ describe('Linkifier', () => {
linkifier.attachToDom(mouseZoneManager); linkifier.attachToDom(mouseZoneManager);
}); });
describe('http links', () => {
it('should allow ~ character in URI path', (done) => {
assertLinkifiesEntireRow('http://foo.com/a~b#c~d?e~f', done);
});
});
describe('link matcher', () => { describe('link matcher', () => {
it('should match a single link', done => { it('should match a single link', done => {
assertLinkifiesRow('foo', /foo/, [{x: 0, length: 3}], done); assertLinkifiesRow('foo', /foo/, [{x: 0, length: 3}], done);
@@ -194,19 +201,19 @@ describe('Linkifier', () => {
it('should order the list from highest priority to lowest #1', () => { it('should order the list from highest priority to lowest #1', () => {
const aId = linkifier.registerLinkMatcher(/a/, () => {}, { priority: 1 }); const aId = linkifier.registerLinkMatcher(/a/, () => {}, { priority: 1 });
const bId = linkifier.registerLinkMatcher(/b/, () => {}, { priority: -1 }); const bId = linkifier.registerLinkMatcher(/b/, () => {}, { priority: -1 });
assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [aId, bId]); assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [aId, 0, bId]);
}); });
it('should order the list from highest priority to lowest #2', () => { it('should order the list from highest priority to lowest #2', () => {
const aId = linkifier.registerLinkMatcher(/a/, () => {}, { priority: -1 }); const aId = linkifier.registerLinkMatcher(/a/, () => {}, { priority: -1 });
const bId = linkifier.registerLinkMatcher(/b/, () => {}, { priority: 1 }); const bId = linkifier.registerLinkMatcher(/b/, () => {}, { priority: 1 });
assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [bId, aId]); assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [bId, 0, aId]);
}); });
it('should order items of equal priority in the order they are added', () => { it('should order items of equal priority in the order they are added', () => {
const aId = linkifier.registerLinkMatcher(/a/, () => {}, { priority: 0 }); const aId = linkifier.registerLinkMatcher(/a/, () => {}, { priority: 0 });
const bId = linkifier.registerLinkMatcher(/b/, () => {}, { priority: 0 }); const bId = linkifier.registerLinkMatcher(/b/, () => {}, { priority: 0 });
assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [aId, bId]); assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [0, aId, bId]);
}); });
}); });
}); });
+61 -23
View File
@@ -3,11 +3,36 @@
* @license MIT * @license MIT
*/ */
import { IMouseZoneManager } from './input/Types'; import { ILinkMatcherOptions, ITerminal, IBufferAccessor, ILinkifier, IElementAccessor } from './Interfaces';
import { ILinkHoverEvent, ILinkMatcher, LinkMatcherHandler, LinkMatcherValidationCallback, LineData, LinkHoverEventTypes, ILinkMatcherOptions, ITerminal, IBufferAccessor, ILinkifier, IElementAccessor } from './Types'; import { LinkMatcher, LinkMatcherHandler, LinkMatcherValidationCallback, LineData, LinkHoverEvent, LinkHoverEventTypes } from './Types';
import { IMouseZoneManager } from './input/Interfaces';
import { MouseZone } from './input/MouseZoneManager'; import { MouseZone } from './input/MouseZoneManager';
import { EventEmitter } from './EventEmitter'; import { EventEmitter } from './EventEmitter';
const protocolClause = '(https?:\\/\\/)';
const domainCharacterSet = '[\\da-z\\.-]+';
const negatedDomainCharacterSet = '[^\\da-z\\.-]+';
const domainBodyClause = '(' + domainCharacterSet + ')';
const tldClause = '([a-z\\.]{2,6})';
const ipClause = '((\\d{1,3}\\.){3}\\d{1,3})';
const localHostClause = '(localhost)';
const portClause = '(:\\d{1,5})';
const hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|' + ipClause + '|' + localHostClause + ')' + portClause + '?';
const pathClause = '(\\/[\\/\\w\\.\\-%~]*)*';
const queryStringHashFragmentCharacterSet = '[0-9\\w\\[\\]\\(\\)\\/\\?\\!#@$%&\'*+,:;~\\=\\.\\-]*';
const queryStringClause = '(\\?' + queryStringHashFragmentCharacterSet + ')?';
const hashFragmentClause = '(#' + queryStringHashFragmentCharacterSet + ')?';
const negatedPathCharacterSet = '[^\\/\\w\\.\\-%]+';
const bodyClause = hostClause + pathClause + queryStringClause + hashFragmentClause;
const start = '(?:^|' + negatedDomainCharacterSet + ')(';
const end = ')($|' + negatedPathCharacterSet + ')';
const strictUrlRegex = new RegExp(start + protocolClause + bodyClause + end);
/**
* The ID of the built in http(s) link matcher.
*/
const HYPERTEXT_LINK_MATCHER_ID = 0;
/** /**
* The Linkifier applies links to rows shortly after they have been refreshed. * The Linkifier applies links to rows shortly after they have been refreshed.
*/ */
@@ -19,11 +44,11 @@ export class Linkifier extends EventEmitter implements ILinkifier {
*/ */
protected static TIME_BEFORE_LINKIFY = 200; protected static TIME_BEFORE_LINKIFY = 200;
protected _linkMatchers: ILinkMatcher[] = []; protected _linkMatchers: LinkMatcher[] = [];
private _mouseZoneManager: IMouseZoneManager; private _mouseZoneManager: IMouseZoneManager;
private _rowsTimeoutId: number; private _rowsTimeoutId: number;
private _nextLinkMatcherId = 0; private _nextLinkMatcherId = HYPERTEXT_LINK_MATCHER_ID;
private _rowsToLinkify: {start: number, end: number}; private _rowsToLinkify: {start: number, end: number};
constructor( constructor(
@@ -34,6 +59,7 @@ export class Linkifier extends EventEmitter implements ILinkifier {
start: null, start: null,
end: null end: null
}; };
this.registerLinkMatcher(strictUrlRegex, null, { matchIndex: 1 });
} }
/** /**
@@ -56,12 +82,12 @@ export class Linkifier extends EventEmitter implements ILinkifier {
} }
// Increase range to linkify // Increase range to linkify
if (this._rowsToLinkify.start === null) { if (!this._rowsToLinkify.start) {
this._rowsToLinkify.start = start; this._rowsToLinkify.start = start;
this._rowsToLinkify.end = end; this._rowsToLinkify.end = end;
} else { } else {
this._rowsToLinkify.start = Math.min(this._rowsToLinkify.start, start); this._rowsToLinkify.start = this._rowsToLinkify.start < start ? this._rowsToLinkify.start : start;
this._rowsToLinkify.end = Math.max(this._rowsToLinkify.end, end); this._rowsToLinkify.end = this._rowsToLinkify.end > end ? this._rowsToLinkify.end : end;
} }
// Clear out any existing links on this row range // Clear out any existing links on this row range
@@ -86,6 +112,23 @@ export class Linkifier extends EventEmitter implements ILinkifier {
this._rowsToLinkify.end = null; this._rowsToLinkify.end = null;
} }
/**
* Attaches a handler for hypertext links, overriding default <a> behavior for
* tandard http(s) links.
* @param handler The handler to use, this can be cleared with null.
*/
public setHypertextLinkHandler(handler: LinkMatcherHandler): void {
this._linkMatchers[HYPERTEXT_LINK_MATCHER_ID].handler = handler;
}
/**
* Attaches a validation callback for hypertext links.
* @param callback The callback to use, this can be cleared with null.
*/
public setHypertextValidationCallback(callback: LinkMatcherValidationCallback): void {
this._linkMatchers[HYPERTEXT_LINK_MATCHER_ID].validationCallback = callback;
}
/** /**
* Registers a link matcher, allowing custom link patterns to be matched and * Registers a link matcher, allowing custom link patterns to be matched and
* handled. * handled.
@@ -97,10 +140,10 @@ export class Linkifier extends EventEmitter implements ILinkifier {
* @return The ID of the new matcher, this can be used to deregister. * @return The ID of the new matcher, this can be used to deregister.
*/ */
public registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options: ILinkMatcherOptions = {}): number { public registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options: ILinkMatcherOptions = {}): number {
if (!handler) { if (this._nextLinkMatcherId !== HYPERTEXT_LINK_MATCHER_ID && !handler) {
throw new Error('handler must be defined'); throw new Error('handler must be defined');
} }
const matcher: ILinkMatcher = { const matcher: LinkMatcher = {
id: this._nextLinkMatcherId++, id: this._nextLinkMatcherId++,
regex, regex,
handler, handler,
@@ -108,7 +151,6 @@ export class Linkifier extends EventEmitter implements ILinkifier {
validationCallback: options.validationCallback, validationCallback: options.validationCallback,
hoverTooltipCallback: options.tooltipCallback, hoverTooltipCallback: options.tooltipCallback,
hoverLeaveCallback: options.leaveCallback, hoverLeaveCallback: options.leaveCallback,
willLinkActivate: options.willLinkActivate,
priority: options.priority || 0 priority: options.priority || 0
}; };
this._addLinkMatcherToList(matcher); this._addLinkMatcherToList(matcher);
@@ -121,7 +163,7 @@ export class Linkifier extends EventEmitter implements ILinkifier {
* considered after older link matchers. * considered after older link matchers.
* @param matcher The link matcher to be added. * @param matcher The link matcher to be added.
*/ */
private _addLinkMatcherToList(matcher: ILinkMatcher): void { private _addLinkMatcherToList(matcher: LinkMatcher): void {
if (this._linkMatchers.length === 0) { if (this._linkMatchers.length === 0) {
this._linkMatchers.push(matcher); this._linkMatchers.push(matcher);
return; return;
@@ -143,7 +185,8 @@ export class Linkifier extends EventEmitter implements ILinkifier {
* @return Whether a link matcher was found and deregistered. * @return Whether a link matcher was found and deregistered.
*/ */
public deregisterLinkMatcher(matcherId: number): boolean { public deregisterLinkMatcher(matcherId: number): boolean {
for (let i = 0; i < this._linkMatchers.length; i++) { // ID 0 is the hypertext link matcher which cannot be deregistered
for (let i = 1; i < this._linkMatchers.length; i++) {
if (this._linkMatchers[i].id === matcherId) { if (this._linkMatchers[i].id === matcherId) {
this._linkMatchers.splice(i, 1); this._linkMatchers.splice(i, 1);
return true; return true;
@@ -176,9 +219,10 @@ export class Linkifier extends EventEmitter implements ILinkifier {
* @param offset The how much of the row has already been linkified. * @param offset The how much of the row has already been linkified.
* @return The link element(s) that were added. * @return The link element(s) that were added.
*/ */
private _doLinkifyRow(rowIndex: number, text: string, matcher: ILinkMatcher, offset: number = 0): void { private _doLinkifyRow(rowIndex: number, text: string, matcher: LinkMatcher, offset: number = 0): void {
// Iterate over nodes as we want to consider text nodes // Iterate over nodes as we want to consider text nodes
let result = []; let result = [];
const isHttpLinkMatcher = matcher.id === HYPERTEXT_LINK_MATCHER_ID;
// Find the first match // Find the first match
let match = text.match(matcher.regex); let match = text.match(matcher.regex);
@@ -220,7 +264,7 @@ export class Linkifier extends EventEmitter implements ILinkifier {
* @param uri The URI of the link. * @param uri The URI of the link.
* @param matcher The link matcher for the link. * @param matcher The link matcher for the link.
*/ */
private _addLink(x: number, y: number, uri: string, matcher: ILinkMatcher): void { private _addLink(x: number, y: number, uri: string, matcher: LinkMatcher): void {
this._mouseZoneManager.add(new MouseZone( this._mouseZoneManager.add(new MouseZone(
x + 1, x + 1,
x + 1 + uri.length, x + 1 + uri.length,
@@ -232,27 +276,21 @@ export class Linkifier extends EventEmitter implements ILinkifier {
window.open(uri, '_blank'); window.open(uri, '_blank');
}, },
e => { e => {
this.emit(LinkHoverEventTypes.HOVER, <ILinkHoverEvent>{ x, y, length: uri.length}); this.emit(LinkHoverEventTypes.HOVER, <LinkHoverEvent>{ x, y, length: uri.length});
this._terminal.element.style.cursor = 'pointer'; this._terminal.element.style.cursor = 'pointer';
}, },
e => { e => {
this.emit(LinkHoverEventTypes.TOOLTIP, <ILinkHoverEvent>{ x, y, length: uri.length}); this.emit(LinkHoverEventTypes.TOOLTIP, <LinkHoverEvent>{ x, y, length: uri.length});
if (matcher.hoverTooltipCallback) { if (matcher.hoverTooltipCallback) {
matcher.hoverTooltipCallback(e, uri); matcher.hoverTooltipCallback(e, uri);
} }
}, },
() => { () => {
this.emit(LinkHoverEventTypes.LEAVE, <ILinkHoverEvent>{ x, y, length: uri.length}); this.emit(LinkHoverEventTypes.LEAVE, <LinkHoverEvent>{ x, y, length: uri.length});
this._terminal.element.style.cursor = ''; this._terminal.element.style.cursor = '';
if (matcher.hoverLeaveCallback) { if (matcher.hoverLeaveCallback) {
matcher.hoverLeaveCallback(); matcher.hoverLeaveCallback();
} }
},
e => {
if (matcher.willLinkActivate) {
return matcher.willLinkActivate(e, uri);
}
return true;
} }
)); ));
} }
+2 -10
View File
@@ -5,7 +5,7 @@
*/ */
import { C0 } from './EscapeSequences'; import { C0 } from './EscapeSequences';
import { IInputHandler } from './Types'; import { IInputHandler } from './Interfaces';
import { CHARSETS, DEFAULT_CHARSET } from './Charsets'; import { CHARSETS, DEFAULT_CHARSET } from './Charsets';
const normalStateHandler: {[key: string]: (parser: Parser, handler: IInputHandler) => void} = {}; const normalStateHandler: {[key: string]: (parser: Parser, handler: IInputHandler) => void} = {};
@@ -224,9 +224,8 @@ export class Parser {
ch += data.charAt(this._position + 1); ch += data.charAt(this._position + 1);
} }
// surrogate low - already handled above // surrogate low - already handled above
if (0xDC00 <= code && code <= 0xDFFF) { if (0xDC00 <= code && code <= 0xDFFF)
continue; continue;
}
switch (this._state) { switch (this._state) {
case ParserState.NORMAL: case ParserState.NORMAL:
@@ -287,43 +286,36 @@ export class Parser {
// Single Shift Select of G2 Character Set // Single Shift Select of G2 Character Set
// ( SS2 is 0x8e). This affects next character only. // ( SS2 is 0x8e). This affects next character only.
case 'N': case 'N':
this._state = ParserState.NORMAL;
break; break;
// ESC O // ESC O
// Single Shift Select of G3 Character Set // Single Shift Select of G3 Character Set
// ( SS3 is 0x8f). This affects next character only. // ( SS3 is 0x8f). This affects next character only.
case 'O': case 'O':
this._state = ParserState.NORMAL;
break; break;
// ESC n // ESC n
// Invoke the G2 Character Set as GL (LS2). // Invoke the G2 Character Set as GL (LS2).
case 'n': case 'n':
this._terminal.setgLevel(2); this._terminal.setgLevel(2);
this._state = ParserState.NORMAL;
break; break;
// ESC o // ESC o
// Invoke the G3 Character Set as GL (LS3). // Invoke the G3 Character Set as GL (LS3).
case 'o': case 'o':
this._terminal.setgLevel(3); this._terminal.setgLevel(3);
this._state = ParserState.NORMAL;
break; break;
// ESC | // ESC |
// Invoke the G3 Character Set as GR (LS3R). // Invoke the G3 Character Set as GR (LS3R).
case '|': case '|':
this._terminal.setgLevel(3); this._terminal.setgLevel(3);
this._state = ParserState.NORMAL;
break; break;
// ESC } // ESC }
// Invoke the G2 Character Set as GR (LS2R). // Invoke the G2 Character Set as GR (LS2R).
case '}': case '}':
this._terminal.setgLevel(2); this._terminal.setgLevel(2);
this._state = ParserState.NORMAL;
break; break;
// ESC ~ // ESC ~
// Invoke the G1 Character Set as GR (LS1R). // Invoke the G1 Character Set as GR (LS1R).
case '~': case '~':
this._terminal.setgLevel(1); this._terminal.setgLevel(1);
this._state = ParserState.NORMAL;
break; break;
// ESC 7 Save Cursor (DECSC). // ESC 7 Save Cursor (DECSC).
+6 -4
View File
@@ -5,13 +5,14 @@
import jsdom = require('jsdom'); import jsdom = require('jsdom');
import { assert } from 'chai'; import { assert } from 'chai';
import { ITerminal, ICircularList, IBuffer } from './Interfaces';
import { CharMeasure } from './utils/CharMeasure'; import { CharMeasure } from './utils/CharMeasure';
import { CircularList } from './utils/CircularList'; import { CircularList } from './utils/CircularList';
import { SelectionManager } from './SelectionManager'; import { SelectionManager } from './SelectionManager';
import { SelectionModel } from './SelectionModel'; import { SelectionModel } from './SelectionModel';
import { BufferSet } from './BufferSet'; import { BufferSet } from './BufferSet';
import { LineData, CharData, ITerminal, ICircularList, IBuffer } from './Types';
import { MockTerminal } from './utils/TestUtils.test'; import { MockTerminal } from './utils/TestUtils.test';
import { LineData, CharData } from './Types';
class TestMockTerminal extends MockTerminal { class TestMockTerminal extends MockTerminal {
emit(event: string, data: any): void {} emit(event: string, data: any): void {}
@@ -20,15 +21,16 @@ class TestMockTerminal extends MockTerminal {
class TestSelectionManager extends SelectionManager { class TestSelectionManager extends SelectionManager {
constructor( constructor(
terminal: ITerminal, terminal: ITerminal,
buffer: IBuffer,
charMeasure: CharMeasure charMeasure: CharMeasure
) { ) {
super(terminal, charMeasure); super(terminal, buffer, charMeasure);
} }
public get model(): SelectionModel { return this._model; } public get model(): SelectionModel { return this._model; }
public selectLineAt(line: number): void { this._selectLineAt(line); } public selectLineAt(line: number): void { this._selectLineAt(line); }
public selectWordAt(coords: [number, number]): void { this._selectWordAt(coords, true); } public selectWordAt(coords: [number, number]): void { this._selectWordAt(coords); }
// Disable DOM interaction // Disable DOM interaction
public enable(): void {} public enable(): void {}
@@ -57,7 +59,7 @@ describe('SelectionManager', () => {
terminal.buffers = new BufferSet(terminal); terminal.buffers = new BufferSet(terminal);
terminal.buffer = terminal.buffers.active; terminal.buffer = terminal.buffers.active;
buffer = terminal.buffer; buffer = terminal.buffer;
selectionManager = new TestSelectionManager(terminal, null); selectionManager = new TestSelectionManager(terminal, buffer, null);
}); });
function stringToRow(text: string): LineData { function stringToRow(text: string): LineData {
+28 -89
View File
@@ -3,15 +3,15 @@
* @license MIT * @license MIT
*/ */
import { ITerminal, ICircularList, ISelectionManager, IBuffer, LineData, CharData, XtermListener } from './Types';
import { MouseHelper } from './utils/MouseHelper'; import { MouseHelper } from './utils/MouseHelper';
import * as Browser from './shared/utils/Browser'; import * as Browser from './utils/Browser';
import { CharMeasure } from './utils/CharMeasure'; import { CharMeasure } from './utils/CharMeasure';
import { CircularList } from './utils/CircularList'; import { CircularList } from './utils/CircularList';
import { EventEmitter } from './EventEmitter'; import { EventEmitter } from './EventEmitter';
import { ITerminal, ICircularList, ISelectionManager, IBuffer } from './Interfaces';
import { SelectionModel } from './SelectionModel'; import { SelectionModel } from './SelectionModel';
import { LineData, CharData } from './Types';
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from './Buffer'; 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 * The number of pixels the mouse needs to be above or below the viewport in
@@ -29,12 +29,6 @@ const DRAG_SCROLL_MAX_SPEED = 15;
*/ */
const DRAG_SCROLL_INTERVAL = 50; 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 * A string containing all characters that are considered word separated by the
* double click to select work logic. * double click to select work logic.
@@ -101,12 +95,10 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
private _mouseMoveListener: EventListener; private _mouseMoveListener: EventListener;
private _mouseUpListener: EventListener; private _mouseUpListener: EventListener;
private _trimListener: XtermListener;
private _mouseDownTimeStamp: number;
constructor( constructor(
private _terminal: ITerminal, private _terminal: ITerminal,
private _buffer: IBuffer,
private _charMeasure: CharMeasure private _charMeasure: CharMeasure
) { ) {
super(); super();
@@ -117,24 +109,18 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
this._activeSelectionMode = SelectionMode.NORMAL; this._activeSelectionMode = SelectionMode.NORMAL;
} }
private get _buffer(): IBuffer {
return this._terminal.buffers.active;
}
/** /**
* Initializes listener variables. * Initializes listener variables.
*/ */
private _initListeners(): void { private _initListeners(): void {
this._mouseMoveListener = event => this._onMouseMove(<MouseEvent>event); this._mouseMoveListener = event => this._onMouseMove(<MouseEvent>event);
this._mouseUpListener = event => this._onMouseUp(<MouseEvent>event); this._mouseUpListener = event => this._onMouseUp(<MouseEvent>event);
this._trimListener = (amount: number) => this._onTrim(amount);
this.initBuffersListeners(); // Only adjust the selection on trim, shiftElements is rarely used (only in
} // reverseIndex) and delete in a splice is only ever used when the same
// number of elements was just added. Given this is could actually be
public initBuffersListeners(): void { // beneficial to leave the selection as is for these cases.
this._terminal.buffer.lines.on('trim', this._trimListener); this._buffer.lines.on('trim', (amount: number) => this._onTrim(amount));
this._terminal.buffers.on('activate', e => this._onBufferActivate(e));
} }
/** /**
@@ -153,6 +139,16 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
this._enabled = true; this._enabled = true;
} }
/**
* Sets the active buffer, this should be called when the alt buffer is
* switched in or out.
* @param buffer The active buffer.
*/
public setBuffer(buffer: IBuffer): void {
this._buffer = buffer;
this.clearSelection();
}
public get selectionStart(): [number, number] { return this._model.finalSelectionStart; } public get selectionStart(): [number, number] { return this._model.finalSelectionStart; }
public get selectionEnd(): [number, number] { return this._model.finalSelectionEnd; } public get selectionEnd(): [number, number] { return this._model.finalSelectionEnd; }
@@ -253,37 +249,6 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
this.emit('refresh', { start: this._model.finalSelectionStart, end: this._model.finalSelectionEnd }); 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. * Selects all text within the terminal.
*/ */
@@ -309,7 +274,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
* @param event The mouse event. * @param event The mouse event.
*/ */
private _getMouseBufferCoords(event: MouseEvent): [number, number] { private _getMouseBufferCoords(event: MouseEvent): [number, number] {
const coords = this._terminal.mouseHelper.getCoords(event, this._terminal.screenElement, this._charMeasure, this._terminal.options.lineHeight, this._terminal.cols, this._terminal.rows, true); const coords = this._terminal.mouseHelper.getCoords(event, this._terminal.element, this._charMeasure, this._terminal.options.lineHeight, this._terminal.cols, this._terminal.rows, true);
if (!coords) { if (!coords) {
return null; return null;
} }
@@ -329,7 +294,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
* @param event The mouse event. * @param event The mouse event.
*/ */
private _getMouseEventScrollAmount(event: MouseEvent): number { private _getMouseEventScrollAmount(event: MouseEvent): number {
let offset = MouseHelper.getCoordsRelativeToElement(event, this._terminal.screenElement)[1]; let offset = MouseHelper.getCoordsRelativeToElement(event, this._terminal.element)[1];
const terminalHeight = this._terminal.rows * Math.ceil(this._charMeasure.height * this._terminal.options.lineHeight); const terminalHeight = this._terminal.rows * Math.ceil(this._charMeasure.height * this._terminal.options.lineHeight);
if (offset >= 0 && offset <= terminalHeight) { if (offset >= 0 && offset <= terminalHeight) {
return 0; return 0;
@@ -357,7 +322,6 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
* @param event The mousedown event. * @param event The mousedown event.
*/ */
public onMouseDown(event: MouseEvent): void { public onMouseDown(event: MouseEvent): void {
this._mouseDownTimeStamp = event.timeStamp;
// If we have selection, we want the context menu on right click even if the // If we have selection, we want the context menu on right click even if the
// terminal is in mouse mode. // terminal is in mouse mode.
if (event.button === 2 && this.hasSelection) { if (event.button === 2 && this.hasSelection) {
@@ -476,7 +440,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
const coords = this._getMouseBufferCoords(event); const coords = this._getMouseBufferCoords(event);
if (coords) { if (coords) {
this._activeSelectionMode = SelectionMode.WORD; this._activeSelectionMode = SelectionMode.WORD;
this._selectWordAt(coords, true); this._selectWordAt(coords);
} }
} }
@@ -564,7 +528,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
this._terminal.scrollLines(this._dragScrollAmount, false); this._terminal.scrollLines(this._dragScrollAmount, false);
// Re-evaluate selection // Re-evaluate selection
if (this._dragScrollAmount > 0) { if (this._dragScrollAmount > 0) {
this._model.selectionEnd = [this._terminal.cols - 1, Math.min(this._terminal.buffer.ydisp + this._terminal.rows, this._terminal.buffer.lines.length - 1)]; this._model.selectionEnd = [this._terminal.cols - 1, this._terminal.buffer.ydisp + this._terminal.rows];
} else { } else {
this._model.selectionEnd = [0, this._terminal.buffer.ydisp]; this._model.selectionEnd = [0, this._terminal.buffer.ydisp];
} }
@@ -577,25 +541,10 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
* @param event The mouseup event. * @param event The mouseup event.
*/ */
private _onMouseUp(event: MouseEvent): void { private _onMouseUp(event: MouseEvent): void {
let timeElapsed = event.timeStamp - this._mouseDownTimeStamp;
this._removeMouseDownListeners(); this._removeMouseDownListeners();
if (this.selectionText.length <= 1 && timeElapsed < ALT_CLICK_MOVE_CURSOR_TIME) { if (this.hasSelection)
(new AltClickHandler(event, this._terminal)).move();
} else if (this.hasSelection) {
this._terminal.emit('selection'); this._terminal.emit('selection');
}
}
private _onBufferActivate(e: {activeBuffer: IBuffer, inactiveBuffer: IBuffer}): void {
this.clearSelection();
// Only adjust the selection on trim, shiftElements is rarely used (only in
// reverseIndex) and delete in a splice is only ever used when the same
// number of elements was just added. Given this is could actually be
// beneficial to leave the selection as is for these cases.
e.inactiveBuffer.lines.off('trim', this._trimListener);
e.activeBuffer.lines.on('trim', this._trimListener);
} }
/** /**
@@ -633,12 +582,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
* Gets positional information for the word at the coordinated specified. * Gets positional information for the word at the coordinated specified.
* @param coords The coordinates to get the word at. * @param coords The coordinates to get the word at.
*/ */
private _getWordAt(coords: [number, number], allowWhitespaceOnlySelection: boolean): IWordPosition { private _getWordAt(coords: [number, number]): IWordPosition {
// Ensure coords are within viewport (eg. not within scroll bar)
if (coords[0] >= this._terminal.cols) {
return null;
}
const bufferLine = this._buffer.lines.get(coords[1]); const bufferLine = this._buffer.lines.get(coords[1]);
if (!bufferLine) { if (!bufferLine) {
return null; return null;
@@ -744,20 +688,15 @@ 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) - 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) - 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 }; return { start, length };
} }
/** /**
* Selects the word at the coordinates specified. * Selects the word at the coordinates specified.
* @param coords The coordinates to get the word at. * @param coords The coordinates to get the word at.
* @param allowWhitespaceOnlySelection If whitespace should be selected
*/ */
protected _selectWordAt(coords: [number, number], allowWhitespaceOnlySelection: boolean): void { protected _selectWordAt(coords: [number, number]): void {
const wordPosition = this._getWordAt(coords, allowWhitespaceOnlySelection); const wordPosition = this._getWordAt(coords);
if (wordPosition) { if (wordPosition) {
this._model.selectionStart = [wordPosition.start, coords[1]]; this._model.selectionStart = [wordPosition.start, coords[1]];
this._model.selectionStartLength = wordPosition.length; this._model.selectionStartLength = wordPosition.length;
@@ -769,7 +708,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
* @param coords The coordinates to get the word at. * @param coords The coordinates to get the word at.
*/ */
private _selectToWordAt(coords: [number, number]): void { private _selectToWordAt(coords: [number, number]): void {
const wordPosition = this._getWordAt(coords, true); const wordPosition = this._getWordAt(coords);
if (wordPosition) { if (wordPosition) {
this._model.selectionEnd = [this._model.areSelectionValuesReversed() ? wordPosition.start : (wordPosition.start + wordPosition.length), coords[1]]; this._model.selectionEnd = [this._model.areSelectionValuesReversed() ? wordPosition.start : (wordPosition.start + wordPosition.length), coords[1]];
} }
+2 -2
View File
@@ -4,9 +4,9 @@
*/ */
import { assert } from 'chai'; import { assert } from 'chai';
import { ITerminal } from './Types'; import { ITerminal } from './Interfaces';
import { SelectionModel } from './SelectionModel'; import { SelectionModel } from './SelectionModel';
import { BufferSet } from './BufferSet'; import {BufferSet} from './BufferSet';
import { MockTerminal } from './utils/TestUtils.test'; import { MockTerminal } from './utils/TestUtils.test';
class TestSelectionModel extends SelectionModel { class TestSelectionModel extends SelectionModel {
+1 -1
View File
@@ -3,7 +3,7 @@
* @license MIT * @license MIT
*/ */
import { ITerminal } from './Types'; import { ITerminal } from './Interfaces';
/** /**
* Represents a selection within the buffer. This model only cares about column * Represents a selection within the buffer. This model only cares about column
-60
View File
@@ -1,60 +0,0 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { ITerminal, ISoundManager } from './Types';
// Source: https://freesound.org/people/altemark/sounds/45759/
// This sound is released under the Creative Commons Attribution 3.0 Unported
// (CC BY 3.0) license. It was created by 'altemark'. No modifications have been
// made, apart from the conversion to base64.
export const DEFAULT_BELL_SOUND = 'data:audio/wav;base64,UklGRigBAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQQBAADpAFgCwAMlBZoG/wdmCcoKRAypDQ8PbRDBEQQTOxRtFYcWlBePGIUZXhoiG88bcBz7HHIdzh0WHlMeZx51HmkeUx4WHs8dah0AHXwc3hs9G4saxRnyGBIYGBcQFv8U4RPAEoYRQBACD70NWwwHC6gJOwjWBloF7gOBAhABkf8b/qv8R/ve+Xf4Ife79W/0JfPZ8Z/wde9N7ijtE+wU6xvqM+lb6H7nw+YX5mrlxuQz5Mzje+Ma49fioeKD4nXiYeJy4pHitOL04j/jn+MN5IPkFOWs5U3mDefM55/ogOl36m7rdOyE7abuyu8D8Unyj/Pg9D/2qfcb+Yn6/vuK/Qj/lAAlAg==';
export class SoundManager implements ISoundManager {
private _audioContext: AudioContext;
constructor(
private _terminal: ITerminal
) {
}
public playBellSound(): void {
const audioContextCtor: typeof AudioContext = (<any>window).AudioContext || (<any>window).webkitAudioContext;
if (!this._audioContext && audioContextCtor) {
this._audioContext = new audioContextCtor();
}
if (this._audioContext) {
const bellAudioSource = this._audioContext.createBufferSource();
const context = this._audioContext;
this._audioContext.decodeAudioData(this.base64ToArrayBuffer(this.removeMimeType(this._terminal.options.bellSound)), (buffer) => {
bellAudioSource.buffer = buffer;
bellAudioSource.connect(context.destination);
bellAudioSource.start(0);
});
} else {
console.warn('Sorry, but the Web Audio API is not supported by your browser. Please, consider upgrading to the latest version');
}
}
private base64ToArrayBuffer(base64: string): ArrayBuffer {
const binaryString = window.atob(base64);
const len = binaryString.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes.buffer;
}
private removeMimeType(dataURI: string): string {
// Split the input to get the mime-type and the data itself
const splitUri = dataURI.split(',');
// Return only the data
return splitUri[1];
}
}
-8
View File
@@ -1,8 +0,0 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
export let blankLine = 'Blank line';
export let promptLabel = 'Terminal input';
export let tooMuchOutput = 'Too much output to announce, navigate to rows manually to read';
+18 -18
View File
@@ -15,17 +15,17 @@ import { assert } from 'chai';
import { Terminal } from './Terminal'; import { Terminal } from './Terminal';
import { CHAR_DATA_CHAR_INDEX } from './Buffer'; import { CHAR_DATA_CHAR_INDEX } from './Buffer';
let primitivePty: any; let primitive_pty: any;
// fake sychronous pty write - read // fake sychronous pty write - read
// we just pipe the data from slave to master as a child program would do // we just pipe the data from slave to master as a child program would do
// pty.js opens pipe fds with O_NONBLOCK // pty.js opens pipe fds with O_NONBLOCK
// just wait 10ms instead of setting fds to blocking mode // just wait 10ms instead of setting fds to blocking mode
function ptyWriteRead(data: string, cb: (result: string) => void): void { function ptyWriteRead(data: string, cb: (result: string) => void): void {
fs.writeSync(primitivePty.slave, data); fs.writeSync(primitive_pty.slave, data);
setTimeout(() => { setTimeout(() => {
let b = new Buffer(64000); let b = new Buffer(64000);
let bytes = fs.readSync(primitivePty.master, b, 0, 64000, null); let bytes = fs.readSync(primitive_pty.master, b, 0, 64000, null);
cb(b.toString('utf8', 0, bytes)); cb(b.toString('utf8', 0, bytes));
}); });
} }
@@ -37,7 +37,7 @@ function ptyReset(cb: (result: string) => void): void {
/* debug helpers */ /* debug helpers */
// generate colorful noisy output to compare xterm and emulator cell states // generate colorful noisy output to compare xterm and emulator cell states
function formatError(input: string, output: string, expected: string): string { function formatError(in_: string, out_: string, expected: string): string {
function addLineNumber(start: number, color: string): (s: string) => string { function addLineNumber(start: number, color: string): (s: string) => string {
let counter = start || 0; let counter = start || 0;
return function(s: string): string { return function(s: string): string {
@@ -47,9 +47,9 @@ function formatError(input: string, output: string, expected: string): string {
} }
let line80 = '12345678901234567890123456789012345678901234567890123456789012345678901234567890'; let line80 = '12345678901234567890123456789012345678901234567890123456789012345678901234567890';
let s = ''; let s = '';
s += '\n\x1b[34m' + JSON.stringify(input); s += '\n\x1b[34m' + JSON.stringify(in_);
s += '\n\x1b[33m ' + line80 + '\n'; s += '\n\x1b[33m ' + line80 + '\n';
s += output.split('\n').map(addLineNumber(0, '\x1b[31m')).join('\n'); s += out_.split('\n').map(addLineNumber(0, '\x1b[31m')).join('\n');
s += '\n\x1b[33m ' + line80 + '\n'; s += '\n\x1b[33m ' + line80 + '\n';
s += expected.split('\n').map(addLineNumber(0, '\x1b[32m')).join('\n'); s += expected.split('\n').map(addLineNumber(0, '\x1b[32m')).join('\n');
return s; return s;
@@ -58,15 +58,15 @@ function formatError(input: string, output: string, expected: string): string {
// simple debug output of terminal cells // simple debug output of terminal cells
function terminalToString(term: Terminal): string { function terminalToString(term: Terminal): string {
let result = ''; let result = '';
let lineText = ''; let line_s = '';
for (let line = term.buffer.ybase; line < term.buffer.ybase + term.rows; line++) { for (let line = term.buffer.ybase; line < term.buffer.ybase + term.rows; line++) {
lineText = ''; line_s = '';
for (let cell = 0; cell < term.cols; ++cell) { for (let cell = 0; cell < term.cols; ++cell) {
lineText += term.buffer.lines.get(line)[cell][CHAR_DATA_CHAR_INDEX]; line_s += term.buffer.lines.get(line)[cell][CHAR_DATA_CHAR_INDEX];
} }
// rtrim empty cells as xterm does // rtrim empty cells as xterm does
lineText = lineText.replace(/\s+$/, ''); line_s = line_s.replace(/\s+$/, '');
result += lineText; result += line_s;
result += '\n'; result += '\n';
} }
return result; return result;
@@ -83,7 +83,7 @@ if (os.platform() !== 'win32') {
/** some helpers for pty interaction */ /** some helpers for pty interaction */
// we need a pty in between to get the termios decorations // we need a pty in between to get the termios decorations
// for the basic test cases a raw pty device is enough // for the basic test cases a raw pty device is enough
primitivePty = pty.native.open(COLS, ROWS); primitive_pty = pty.native.open(COLS, ROWS);
/** tests */ /** tests */
describe('xterm output comparison', () => { describe('xterm output comparison', () => {
@@ -118,24 +118,24 @@ if (os.platform() !== 'win32') {
((filename: string) => { ((filename: string) => {
it(filename.split('/').slice(-1)[0], done => { it(filename.split('/').slice(-1)[0], done => {
ptyReset(() => { ptyReset(() => {
let inFile = fs.readFileSync(filename, 'utf8'); let in_file = fs.readFileSync(filename, 'utf8');
ptyWriteRead(inFile, fromPty => { ptyWriteRead(in_file, from_pty => {
// uncomment this to get log from terminal // uncomment this to get log from terminal
// console.log = function(){}; // console.log = function(){};
// Perform a synchronous .write(data) // Perform a synchronous .write(data)
xterm.writeBuffer.push(fromPty); xterm.writeBuffer.push(from_pty);
xterm.innerWrite(); xterm.innerWrite();
let fromEmulator = terminalToString(xterm); let from_emulator = terminalToString(xterm);
console.log = CONSOLE_LOG; console.log = CONSOLE_LOG;
let expected = fs.readFileSync(filename.split('.')[0] + '.text', 'utf8'); let expected = fs.readFileSync(filename.split('.')[0] + '.text', 'utf8');
// Some of the tests have whitespace on the right of lines, we trim all the linex // Some of the tests have whitespace on the right of lines, we trim all the linex
// from xterm.js so ignore this for now at least. // from xterm.js so ignore this for now at least.
let expectedRightTrimmed = expected.split('\n').map(l => l.replace(/\s+$/, '')).join('\n'); let expectedRightTrimmed = expected.split('\n').map(l => l.replace(/\s+$/, '')).join('\n');
if (fromEmulator !== expectedRightTrimmed) { if (from_emulator !== expectedRightTrimmed) {
// uncomment to get noisy output // uncomment to get noisy output
throw new Error(formatError(inFile, fromEmulator, expected)); throw new Error(formatError(in_file, from_emulator, expected));
// throw new Error('mismatch'); // throw new Error('mismatch');
} }
done(); done();
-125
View File
@@ -503,9 +503,6 @@ describe('term.js addons', () => {
it('should return \\x1b[5C for alt+right', () => { it('should return \\x1b[5C for alt+right', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 39 }).key, '\x1b[1;5C'); // CSI 5 C assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 39 }).key, '\x1b[1;5C'); // CSI 5 C
}); });
it('should return \\x1ba for alt+a', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 65 }).key, '\x1ba');
});
}); });
describe('On macOS platforms', () => { describe('On macOS platforms', () => {
@@ -518,19 +515,6 @@ describe('term.js addons', () => {
it('should return \\x1bf for alt+right', () => { it('should return \\x1bf for alt+right', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 39 }).key, '\x1bf'); // CSI 5 C assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 39 }).key, '\x1bf'); // CSI 5 C
}); });
it('should return undefined for alt+a', () => {
assert.strictEqual(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 65 }).key, undefined);
});
});
describe('with macOptionIsMeta', () => {
beforeEach(() => {
term.browser.isMac = true;
term.setOption('macOptionIsMeta', true);
});
it('should return \\x1ba for alt+a', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 65 }).key, '\x1ba');
});
}); });
it('should return \\x1b[5A for alt+up', () => { it('should return \\x1b[5A for alt+up', () => {
@@ -578,99 +562,6 @@ describe('term.js addons', () => {
assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 122 }).key, '\x1b[23;5~'); assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 122 }).key, '\x1b[23;5~');
assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 123 }).key, '\x1b[24;5~'); assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 123 }).key, '\x1b[24;5~');
}); });
// Characters using ctrl+alt sequences
it('should return proper sequence for ctrl+alt+a', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, ctrlKey: true, keyCode: 65 }).key, '\x1b\x01');
});
// Characters using alt sequences (numbers)
it('should return proper sequences for alt+0', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 48 }).key, '\x1b0');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 48 }).key, '\x1b)');
});
it('should return proper sequences for alt+1', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 49 }).key, '\x1b1');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 49 }).key, '\x1b!');
});
it('should return proper sequences for alt+2', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 50 }).key, '\x1b2');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 50 }).key, '\x1b@');
});
it('should return proper sequences for alt+3', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 51 }).key, '\x1b3');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 51 }).key, '\x1b#');
});
it('should return proper sequences for alt+4', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 52 }).key, '\x1b4');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 52 }).key, '\x1b$');
});
it('should return proper sequences for alt+5', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 53 }).key, '\x1b5');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 53 }).key, '\x1b%');
});
it('should return proper sequences for alt+6', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 54 }).key, '\x1b6');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 54 }).key, '\x1b^');
});
it('should return proper sequences for alt+7', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 55 }).key, '\x1b7');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 55 }).key, '\x1b&');
});
it('should return proper sequences for alt+8', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 56 }).key, '\x1b8');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 56 }).key, '\x1b*');
});
it('should return proper sequences for alt+9', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 57 }).key, '\x1b9');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 57 }).key, '\x1b(');
});
// Characters using alt sequences (special chars)
it('should return proper sequences for alt+;', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 186 }).key, '\x1b;');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 186 }).key, '\x1b:');
});
it('should return proper sequences for alt+=', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 187 }).key, '\x1b=');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 187 }).key, '\x1b+');
});
it('should return proper sequences for alt+,', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 188 }).key, '\x1b,');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 188 }).key, '\x1b<');
});
it('should return proper sequences for alt+-', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 189 }).key, '\x1b-');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 189 }).key, '\x1b_');
});
it('should return proper sequences for alt+.', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 190 }).key, '\x1b.');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 190 }).key, '\x1b>');
});
it('should return proper sequences for alt+/', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 191 }).key, '\x1b/');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 191 }).key, '\x1b?');
});
it('should return proper sequences for alt+~', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 192 }).key, '\x1b`');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 192 }).key, '\x1b~');
});
it('should return proper sequences for alt+[', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 219 }).key, '\x1b[');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 219 }).key, '\x1b{');
});
it('should return proper sequences for alt+\\', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 220 }).key, '\x1b\\');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 220 }).key, '\x1b|');
});
it('should return proper sequences for alt+]', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 221 }).key, '\x1b]');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 221 }).key, '\x1b}');
});
it('should return proper sequences for alt+\'', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 222 }).key, '\x1b\'');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 222 }).key, '\x1b"');
});
}); });
describe('Third level shift', () => { describe('Third level shift', () => {
@@ -706,22 +597,6 @@ describe('term.js addons', () => {
}; };
}); });
describe('with macOptionIsMeta', () => {
beforeEach(() => {
term.browser.isMac = true;
term.setOption('macOptionIsMeta', true);
});
it('should interfere with the alt key on keyDown', () => {
evKeyDown.altKey = true;
evKeyDown.keyCode = 81;
assert.equal(term.keyDown(evKeyDown), false);
evKeyDown.altKey = true;
evKeyDown.keyCode = 192;
assert.equal(term.keyDown(evKeyDown), false);
});
});
describe('On Mac OS', () => { describe('On Mac OS', () => {
beforeEach(() => { beforeEach(() => {
term.browser.isMac = true; term.browser.isMac = true;
+150 -184
View File
@@ -21,9 +21,6 @@
* http://linux.die.net/man/7/urxvt * http://linux.die.net/man/7/urxvt
*/ */
import { ICharset, IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, LinkMatcherValidationCallback, CharData, LineData } from './Types';
import { IMouseZoneManager } from './input/Types';
import { IRenderer } from './renderer/Types';
import { BufferSet } from './BufferSet'; import { BufferSet } from './BufferSet';
import { Buffer, MAX_BUFFER_SIZE } from './Buffer'; import { Buffer, MAX_BUFFER_SIZE } from './Buffer';
import { CompositionHelper } from './CompositionHelper'; import { CompositionHelper } from './CompositionHelper';
@@ -38,44 +35,17 @@ import { Renderer } from './renderer/Renderer';
import { Linkifier } from './Linkifier'; import { Linkifier } from './Linkifier';
import { SelectionManager } from './SelectionManager'; import { SelectionManager } from './SelectionManager';
import { CharMeasure } from './utils/CharMeasure'; import { CharMeasure } from './utils/CharMeasure';
import * as Browser from './shared/utils/Browser'; import * as Browser from './utils/Browser';
import * as Strings from './Strings';
import { MouseHelper } from './utils/MouseHelper'; import { MouseHelper } from './utils/MouseHelper';
import { CHARSETS } from './Charsets'; import { CHARSETS } from './Charsets';
import { DEFAULT_BELL_SOUND, SoundManager } from './SoundManager'; import { CustomKeyEventHandler, Charset, LinkMatcherHandler, LinkMatcherValidationCallback, CharData, LineData } from './Types';
import { ITerminal, IBrowser, ITerminalOptions, IInputHandlingTerminal, ILinkMatcherOptions, IViewport, ICompositionHelper, ITheme, ILinkifier } from './Interfaces';
import { BellSound } from './utils/Sounds';
import { DEFAULT_ANSI_COLORS } from './renderer/ColorManager'; import { DEFAULT_ANSI_COLORS } from './renderer/ColorManager';
import { IMouseZoneManager } from './input/Interfaces';
import { MouseZoneManager } from './input/MouseZoneManager'; import { MouseZoneManager } from './input/MouseZoneManager';
import { AccessibilityManager } from './AccessibilityManager'; import { initialize as initializeCharAtlas } from './renderer/CharAtlas';
import { ScreenDprMonitor } from './utils/ScreenDprMonitor'; import { IRenderer } from './renderer/Interfaces';
import { ITheme, ILocalizableStrings } from 'xterm';
// reg + shift key mappings for digits and special chars
const KEYCODE_KEY_MAPPINGS = {
// digits 0-9
48: ['0', ')'],
49: ['1', '!'],
50: ['2', '@'],
51: ['3', '#'],
52: ['4', '$'],
53: ['5', '%'],
54: ['6', '^'],
55: ['7', '&'],
56: ['8', '*'],
57: ['9', '('],
// special chars
186: [';', ':'],
187: ['=', '+'],
188: [',', '<'],
189: ['-', '_'],
190: ['.', '>'],
191: ['/', '?'],
192: ['`', '~'],
219: ['[', '{'],
220: ['\\', '|'],
221: [']', '}'],
222: ['\'', '"']
};
// Let it work inside Node.js for automated testing purposes. // Let it work inside Node.js for automated testing purposes.
const document = (typeof window !== 'undefined') ? window.document : null; const document = (typeof window !== 'undefined') ? window.document : null;
@@ -100,27 +70,21 @@ const DEFAULT_OPTIONS: ITerminalOptions = {
termName: 'xterm', termName: 'xterm',
cursorBlink: false, cursorBlink: false,
cursorStyle: 'block', cursorStyle: 'block',
bellSound: DEFAULT_BELL_SOUND, bellSound: BellSound,
bellStyle: 'none', bellStyle: 'none',
enableBold: true, enableBold: true,
fontFamily: 'courier-new, courier, monospace', fontFamily: 'courier-new, courier, monospace',
fontSize: 15, fontSize: 15,
fontWeight: 'normal',
fontWeightBold: 'bold',
lineHeight: 1.0, lineHeight: 1.0,
letterSpacing: 0, letterSpacing: 0,
scrollback: 1000, scrollback: 1000,
screenKeys: false, screenKeys: false,
screenReaderMode: false,
debug: false, debug: false,
macOptionIsMeta: false,
cancelEvents: false, cancelEvents: false,
disableStdin: false, disableStdin: false,
useFlowControl: false, useFlowControl: false,
allowTransparency: false,
tabStopWidth: 8, tabStopWidth: 8,
theme: null, theme: null
rightClickSelectsWord: Browser.isMac
// programFeatures: false, // programFeatures: false,
// focusKeys: false, // focusKeys: false,
}; };
@@ -128,7 +92,6 @@ const DEFAULT_OPTIONS: ITerminalOptions = {
export class Terminal extends EventEmitter implements ITerminal, IInputHandlingTerminal { export class Terminal extends EventEmitter implements ITerminal, IInputHandlingTerminal {
public textarea: HTMLTextAreaElement; public textarea: HTMLTextAreaElement;
public element: HTMLElement; public element: HTMLElement;
public screenElement: HTMLElement;
/** /**
* The HTMLElement that the terminal is created in, set by Terminal.open. * The HTMLElement that the terminal is created in, set by Terminal.open.
@@ -142,7 +105,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
private helperContainer: HTMLElement; private helperContainer: HTMLElement;
private compositionView: HTMLElement; private compositionView: HTMLElement;
private charSizeStyleElement: HTMLStyleElement; private charSizeStyleElement: HTMLStyleElement;
private bellAudioElement: HTMLAudioElement;
private visualBellTimer: number; private visualBellTimer: number;
public browser: IBrowser = <any>Browser; public browser: IBrowser = <any>Browser;
@@ -168,10 +131,10 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
// charset // charset
// The current charset // The current charset
public charset: ICharset; public charset: Charset;
public gcharset: number; public gcharset: number;
public glevel: number; public glevel: number;
public charsets: ICharset[]; public charsets: Charset[];
// mouse properties // mouse properties
private decLocator: boolean; // This is unstable and never set private decLocator: boolean; // This is unstable and never set
@@ -218,25 +181,23 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
private writeStopped: boolean; private writeStopped: boolean;
// leftover surrogate high from previous write invocation // leftover surrogate high from previous write invocation
private surrogateHigh: string; private surrogate_high: string;
// Store if user went browsing history in scrollback // Store if user went browsing history in scrollback
private userScrolling: boolean; private userScrolling: boolean;
private inputHandler: InputHandler; private inputHandler: InputHandler;
public soundManager: SoundManager;
private parser: Parser; private parser: Parser;
public renderer: IRenderer; public renderer: IRenderer;
public selectionManager: SelectionManager; public selectionManager: SelectionManager;
public linkifier: ILinkifier; public linkifier: ILinkifier;
public buffers: BufferSet; public buffers: BufferSet;
public buffer: Buffer;
public viewport: IViewport; public viewport: IViewport;
private compositionHelper: ICompositionHelper; private compositionHelper: ICompositionHelper;
public charMeasure: CharMeasure; public charMeasure: CharMeasure;
private _mouseZoneManager: IMouseZoneManager; private _mouseZoneManager: IMouseZoneManager;
public mouseHelper: MouseHelper; public mouseHelper: MouseHelper;
private _accessibilityManager: AccessibilityManager;
private _screenDprMonitor: ScreenDprMonitor;
public cols: number; public cols: number;
public rows: number; public rows: number;
@@ -319,7 +280,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
this.xoffSentToCatchUp = false; this.xoffSentToCatchUp = false;
this.writeStopped = false; this.writeStopped = false;
this.surrogateHigh = ''; this.surrogate_high = '';
this.userScrolling = false; this.userScrolling = false;
this.inputHandler = new InputHandler(this); this.inputHandler = new InputHandler(this);
@@ -329,27 +290,20 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
this.selectionManager = this.selectionManager || null; this.selectionManager = this.selectionManager || null;
this.linkifier = this.linkifier || new Linkifier(this); this.linkifier = this.linkifier || new Linkifier(this);
this._mouseZoneManager = this._mouseZoneManager || null; this._mouseZoneManager = this._mouseZoneManager || null;
this.soundManager = this.soundManager || new SoundManager(this);
// Create the terminal's buffers and set the current buffer // Create the terminal's buffers and set the current buffer
this.buffers = new BufferSet(this); this.buffers = new BufferSet(this);
this.buffer = this.buffers.active; // Convenience shortcut;
this.buffers.on('activate', (buffer: Buffer) => {
this.buffer = buffer;
});
// Ensure the selection manager has the correct buffer
if (this.selectionManager) { if (this.selectionManager) {
this.selectionManager.clearSelection(); this.selectionManager.setBuffer(this.buffer);
this.selectionManager.initBuffersListeners();
} }
} }
/**
* Convenience property to active buffer.
*/
public get buffer(): Buffer {
return this.buffers.active;
}
public static get strings(): ILocalizableStrings {
return Strings;
}
/** /**
* back_color_erase feature for xterm. * back_color_erase feature for xterm.
*/ */
@@ -407,16 +361,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
value = 'block'; value = 'block';
} }
break; break;
case 'fontWeight':
if (!value) {
value = 'normal';
}
break;
case 'fontWeightBold':
if (!value) {
value = 'bold';
}
break;
case 'lineHeight': case 'lineHeight':
if (value < 1) { if (value < 1) {
console.warn(`${key} cannot be less than 1, value: ${value}`); console.warn(`${key} cannot be less than 1, value: ${value}`);
@@ -470,31 +414,18 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
case 'enableBold': case 'enableBold':
case 'letterSpacing': case 'letterSpacing':
case 'lineHeight': case 'lineHeight':
case 'fontWeight':
case 'fontWeightBold':
const didCharSizeChange = (key === 'fontWeight' || key === 'fontWeightBold' || key === 'enableBold');
// When the font changes the size of the cells may change which requires a renderer clear // When the font changes the size of the cells may change which requires a renderer clear
this.renderer.clear(); this.renderer.clear();
this.renderer.onResize(this.cols, this.rows, didCharSizeChange); this.renderer.onResize(this.cols, this.rows, false);
this.refresh(0, this.rows - 1); this.refresh(0, this.rows - 1);
// this.charMeasure.measure(this.options);
case 'scrollback': case 'scrollback':
this.buffers.resize(this.cols, this.rows); this.buffers.resize(this.cols, this.rows);
this.viewport.syncScrollArea(); this.viewport.syncScrollArea();
break; break;
case 'screenReaderMode':
if (value) {
if (!this._accessibilityManager) {
this._accessibilityManager = new AccessibilityManager(this);
}
} else {
if (this._accessibilityManager) {
this._accessibilityManager.dispose();
this._accessibilityManager = null;
}
}
break;
case 'tabStopWidth': this.buffers.setupTabStops(); break; case 'tabStopWidth': this.buffers.setupTabStops(); break;
case 'bellSound':
case 'bellStyle': this.syncBellSound(); break;
} }
// Inform renderer of changes // Inform renderer of changes
if (this.renderer) { if (this.renderer) {
@@ -512,7 +443,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
this.element.classList.add('focus'); this.element.classList.add('focus');
this.showCursor(); this.showCursor();
this.emit('focus'); this.emit('focus');
} };
/** /**
* Blur the terminal, calling the blur function on the terminal's underlying * Blur the terminal, calling the blur function on the terminal's underlying
@@ -526,9 +457,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
* Binds the desired blur behavior on a given terminal object. * Binds the desired blur behavior on a given terminal object.
*/ */
private _onTextAreaBlur(): void { private _onTextAreaBlur(): void {
// Text can safely be removed on blur. Doing it earlier could interfere with
// screen readers reading it out.
this.textarea.value = '';
this.refresh(this.buffer.y, this.buffer.y); this.refresh(this.buffer.y, this.buffer.y);
if (this.sendFocus) { if (this.sendFocus) {
this.send(C0.ESC + '[O'); this.send(C0.ESC + '[O');
@@ -561,12 +489,12 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
// Firefox doesn't appear to fire the contextmenu event on right click // Firefox doesn't appear to fire the contextmenu event on right click
on(this.element, 'mousedown', (event: MouseEvent) => { on(this.element, 'mousedown', (event: MouseEvent) => {
if (event.button === 2) { if (event.button === 2) {
rightClickHandler(event, this.textarea, this.selectionManager, this.options.rightClickSelectsWord); rightClickHandler(event, this.textarea, this.selectionManager);
} }
}); });
} else { } else {
on(this.element, 'contextmenu', (event: MouseEvent) => { on(this.element, 'contextmenu', (event: MouseEvent) => {
rightClickHandler(event, this.textarea, this.selectionManager, this.options.rightClickSelectsWord); rightClickHandler(event, this.textarea, this.selectionManager);
}); });
} }
@@ -609,8 +537,16 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
} }
}, true); }, true);
on(this.textarea, 'keydown', (ev: KeyboardEvent) => this._keyDown(ev), true); on(this.textarea, 'keydown', (ev: KeyboardEvent) => {
on(this.textarea, 'keypress', (ev: KeyboardEvent) => this._keyPress(ev), true); this._keyDown(ev);
}, true);
on(this.textarea, 'keypress', (ev: KeyboardEvent) => {
this._keyPress(ev);
// Truncate the textarea's value, since it is not needed
this.textarea.value = '';
}, true);
on(this.textarea, 'compositionstart', () => this.compositionHelper.compositionstart()); on(this.textarea, 'compositionstart', () => this.compositionHelper.compositionstart());
on(this.textarea, 'compositionupdate', (e: CompositionEvent) => this.compositionHelper.compositionupdate(e)); on(this.textarea, 'compositionupdate', (e: CompositionEvent) => this.compositionHelper.compositionupdate(e));
on(this.textarea, 'compositionend', () => this.compositionHelper.compositionend()); on(this.textarea, 'compositionend', () => this.compositionHelper.compositionend());
@@ -638,8 +574,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
this.document = this.parent.ownerDocument; this.document = this.parent.ownerDocument;
this.body = <HTMLBodyElement>this.document.body; this.body = <HTMLBodyElement>this.document.body;
this._screenDprMonitor = new ScreenDprMonitor(); initializeCharAtlas(this.document);
this._screenDprMonitor.setListener(() => this.emit('dprchange', window.devicePixelRatio));
// Create main element container // Create main element container
this.element = this.document.createElement('div'); this.element = this.document.createElement('div');
@@ -658,24 +593,18 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
this.viewportScrollArea.classList.add('xterm-scroll-area'); this.viewportScrollArea.classList.add('xterm-scroll-area');
this.viewportElement.appendChild(this.viewportScrollArea); this.viewportElement.appendChild(this.viewportScrollArea);
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');
this.screenElement.appendChild(this.helperContainer);
fragment.appendChild(this.screenElement);
this._mouseZoneManager = new MouseZoneManager(this); this._mouseZoneManager = new MouseZoneManager(this);
this.on('scroll', () => this._mouseZoneManager.clearAll()); this.on('scroll', () => this._mouseZoneManager.clearAll());
this.linkifier.attachToDom(this._mouseZoneManager); this.linkifier.attachToDom(this._mouseZoneManager);
// 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.textarea = document.createElement('textarea'); this.textarea = document.createElement('textarea');
this.textarea.classList.add('xterm-helper-textarea'); this.textarea.classList.add('xterm-helper-textarea');
// TODO: New API to set title? This could say "Terminal bash input", etc.
this.textarea.setAttribute('aria-label', Strings.promptLabel);
this.textarea.setAttribute('aria-multiline', 'false');
this.textarea.setAttribute('autocorrect', 'off'); this.textarea.setAttribute('autocorrect', 'off');
this.textarea.setAttribute('autocapitalize', 'off'); this.textarea.setAttribute('autocapitalize', 'off');
this.textarea.setAttribute('spellcheck', 'false'); this.textarea.setAttribute('spellcheck', 'false');
@@ -693,6 +622,9 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
this.helperContainer.appendChild(this.charSizeStyleElement); this.helperContainer.appendChild(this.charSizeStyleElement);
this.charMeasure = new CharMeasure(document, this.helperContainer); this.charMeasure = new CharMeasure(document, this.helperContainer);
// Preload audio, this relied on helperContainer
this.syncBellSound();
// Performance: Add viewport and helper elements from the fragment // Performance: Add viewport and helper elements from the fragment
this.element.appendChild(fragment); this.element.appendChild(fragment);
@@ -705,14 +637,11 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
this.on('resize', () => this.renderer.onResize(this.cols, this.rows, false)); this.on('resize', () => this.renderer.onResize(this.cols, this.rows, false));
this.on('blur', () => this.renderer.onBlur()); this.on('blur', () => this.renderer.onBlur());
this.on('focus', () => this.renderer.onFocus()); this.on('focus', () => this.renderer.onFocus());
this.on('dprchange', () => this.renderer.onWindowResize(window.devicePixelRatio));
// dprchange should handle this case, we need this as well for browsers that don't support the
// matchMedia query.
window.addEventListener('resize', () => this.renderer.onWindowResize(window.devicePixelRatio)); window.addEventListener('resize', () => this.renderer.onWindowResize(window.devicePixelRatio));
this.charMeasure.on('charsizechanged', () => this.renderer.onResize(this.cols, this.rows, true)); this.charMeasure.on('charsizechanged', () => this.renderer.onResize(this.cols, this.rows, true));
this.renderer.on('resize', (dimensions) => this.viewport.syncScrollArea()); this.renderer.on('resize', (dimensions) => this.viewport.syncScrollArea());
this.selectionManager = new SelectionManager(this, this.charMeasure); this.selectionManager = new SelectionManager(this, this.buffer, this.charMeasure);
this.element.addEventListener('mousedown', (e: MouseEvent) => this.selectionManager.onMouseDown(e)); this.element.addEventListener('mousedown', (e: MouseEvent) => this.selectionManager.onMouseDown(e));
this.selectionManager.on('refresh', data => this.renderer.onSelectionChanged(data.start, data.end)); this.selectionManager.on('refresh', data => this.renderer.onSelectionChanged(data.start, data.end));
this.selectionManager.on('newselection', text => { this.selectionManager.on('newselection', text => {
@@ -731,12 +660,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
this.mouseHelper = new MouseHelper(this.renderer); this.mouseHelper = new MouseHelper(this.renderer);
if (this.options.screenReaderMode) {
// Note that this must be done *after* the renderer is created in order to
// ensure the correct order of the dprchange event
this._accessibilityManager = new AccessibilityManager(this);
}
// Measure the character size // Measure the character size
this.charMeasure.measure(this.options); this.charMeasure.measure(this.options);
@@ -797,7 +720,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
button = getButton(ev); button = getButton(ev);
// get mouse coordinates // get mouse coordinates
pos = self.mouseHelper.getRawByteCoords(ev, self.screenElement, self.charMeasure, self.options.lineHeight, self.cols, self.rows); pos = self.mouseHelper.getRawByteCoords(ev, self.element, self.charMeasure, self.options.lineHeight, self.cols, self.rows);
if (!pos) return; if (!pos) return;
sendEvent(button, pos); sendEvent(button, pos);
@@ -823,7 +746,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
// ^[[M 3<^[[M@4<^[[M@5<^[[M@6<^[[M@7<^[[M#7< // ^[[M 3<^[[M@4<^[[M@5<^[[M@6<^[[M@7<^[[M#7<
function sendMove(ev: MouseEvent): void { function sendMove(ev: MouseEvent): void {
let button = pressed; let button = pressed;
let pos = self.mouseHelper.getRawByteCoords(ev, self.screenElement, self.charMeasure, self.options.lineHeight, self.cols, self.rows); let pos = self.mouseHelper.getRawByteCoords(ev, self.element, self.charMeasure, self.options.lineHeight, self.cols, self.rows);
if (!pos) return; if (!pos) return;
// buttons marked as motions // buttons marked as motions
@@ -1104,7 +1027,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
*/ */
public refresh(start: number, end: number): void { public refresh(start: number, end: number): void {
if (this.renderer) { if (this.renderer) {
this.renderer.refreshRows(start, end); this.renderer.queueRefresh(start, end);
} }
} }
@@ -1326,6 +1249,36 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
this.customKeyEventHandler = customKeyEventHandler; this.customKeyEventHandler = customKeyEventHandler;
} }
/**
* Attaches a http(s) link handler, forcing web links to behave differently to
* regular <a> tags. This will trigger a refresh as links potentially need to be
* reconstructed. Calling this with null will remove the handler.
* @param handler The handler callback function.
*/
public setHypertextLinkHandler(handler: LinkMatcherHandler): void {
if (!this.linkifier) {
throw new Error('Cannot attach a hypertext link handler before Terminal.open is called');
}
this.linkifier.setHypertextLinkHandler(handler);
// Refresh to force links to refresh
this.refresh(0, this.rows - 1);
}
/**
* Attaches a validation callback for hypertext links. This is useful to use
* validation logic or to do something with the link's element and url.
* @param callback The callback to use, this can
* be cleared with null.
*/
public setHypertextValidationCallback(callback: LinkMatcherValidationCallback): void {
if (!this.linkifier) {
throw new Error('Cannot attach a hypertext validation callback before Terminal.open is called');
}
this.linkifier.setHypertextValidationCallback(callback);
// // Refresh to force links to refresh
this.refresh(0, this.rows - 1);
}
/** /**
* Registers a link matcher, allowing custom link patterns to be matched and * Registers a link matcher, allowing custom link patterns to be matched and
* handled. * handled.
@@ -1337,9 +1290,12 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
* @return The ID of the new matcher, this can be used to deregister. * @return The ID of the new matcher, this can be used to deregister.
*/ */
public registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options?: ILinkMatcherOptions): number { public registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options?: ILinkMatcherOptions): number {
const matcherId = this.linkifier.registerLinkMatcher(regex, handler, options); if (this.linkifier) {
this.refresh(0, this.rows - 1); const matcherId = this.linkifier.registerLinkMatcher(regex, handler, options);
return matcherId; this.refresh(0, this.rows - 1);
return matcherId;
}
return 0;
} }
/** /**
@@ -1347,8 +1303,10 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
* @param matcherId The link matcher's ID (returned after register) * @param matcherId The link matcher's ID (returned after register)
*/ */
public deregisterLinkMatcher(matcherId: number): void { public deregisterLinkMatcher(matcherId: number): void {
if (this.linkifier.deregisterLinkMatcher(matcherId)) { if (this.linkifier) {
this.refresh(0, this.rows - 1); if (this.linkifier.deregisterLinkMatcher(matcherId)) {
this.refresh(0, this.rows - 1);
}
} }
} }
@@ -1416,7 +1374,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
return this.cancel(ev, true); return this.cancel(ev, true);
} }
if (this._isThirdLevelShift(this.browser, ev)) { if (isThirdLevelShift(this.browser, ev)) {
return true; return true;
} }
@@ -1437,19 +1395,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
return this.cancel(ev, true); return this.cancel(ev, true);
} }
private _isThirdLevelShift(browser: IBrowser, ev: KeyboardEvent): boolean {
const thirdLevelKey =
(browser.isMac && !this.options.macOptionIsMeta && ev.altKey && !ev.ctrlKey && !ev.metaKey) ||
(browser.isMSWindows && ev.altKey && ev.ctrlKey && !ev.metaKey);
if (ev.type === 'keypress') {
return thirdLevelKey;
}
// Don't invoke for arrows, pageDown, home, backspace, etc. (on non-keypress events)
return thirdLevelKey && (!ev.keyCode || ev.keyCode > 47);
}
/** /**
* Returns an object that determines how a KeyboardEvent should be handled. The key of the * Returns an object that determines how a KeyboardEvent should be handled. The key of the
* returned value is the new key code to pass to the PTY. * returned value is the new key code to pass to the PTY.
@@ -1504,9 +1449,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
if (ev.shiftKey) { if (ev.shiftKey) {
result.key = C0.BS; // ^H result.key = C0.BS; // ^H
break; break;
} else if (ev.altKey) {
result.key = C0.ESC + C0.DEL; // \e ^?
break;
} }
result.key = C0.DEL; // ^? result.key = C0.DEL; // ^?
break; break;
@@ -1609,23 +1551,21 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
break; break;
case 36: case 36:
// home // home
if (modifiers) { if (modifiers)
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'H'; result.key = C0.ESC + '[1;' + (modifiers + 1) + 'H';
} else if (this.applicationCursor) { else if (this.applicationCursor)
result.key = C0.ESC + 'OH'; result.key = C0.ESC + 'OH';
} else { else
result.key = C0.ESC + '[H'; result.key = C0.ESC + '[H';
}
break; break;
case 35: case 35:
// end // end
if (modifiers) { if (modifiers)
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'F'; result.key = C0.ESC + '[1;' + (modifiers + 1) + 'F';
} else if (this.applicationCursor) { else if (this.applicationCursor)
result.key = C0.ESC + 'OF'; result.key = C0.ESC + 'OF';
} else { else
result.key = C0.ESC + '[F'; result.key = C0.ESC + '[F';
}
break; break;
case 33: case 33:
// page up // page up
@@ -1752,15 +1692,14 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
// ^] - Operating System Command (OSC) // ^] - Operating System Command (OSC)
result.key = String.fromCharCode(29); result.key = String.fromCharCode(29);
} }
} else if ((!this.browser.isMac || this.options.macOptionIsMeta) && ev.altKey && !ev.metaKey) { } else if (!this.browser.isMac && ev.altKey && !ev.ctrlKey && !ev.metaKey) {
// On macOS this is a third level shift when !macOptionIsMeta. Use <Esc> instead. // On Mac this is a third level shift. Use <Esc> instead.
const keyMapping = KEYCODE_KEY_MAPPINGS[ev.keyCode]; if (ev.keyCode >= 65 && ev.keyCode <= 90) {
const key = keyMapping && keyMapping[!ev.shiftKey ? 0 : 1]; result.key = C0.ESC + String.fromCharCode(ev.keyCode + 32);
if (key) { } else if (ev.keyCode === 192) {
result.key = C0.ESC + key; result.key = C0.ESC + '`';
} else if (ev.keyCode >= 65 && ev.keyCode <= 90) { } else if (ev.keyCode >= 48 && ev.keyCode <= 57) {
const keyCode = ev.ctrlKey ? ev.keyCode - 64 : ev.keyCode + 32; result.key = C0.ESC + (ev.keyCode - 48);
result.key = C0.ESC + String.fromCharCode(keyCode);
} }
} else if (this.browser.isMac && !ev.altKey && !ev.ctrlKey && ev.metaKey) { } else if (this.browser.isMac && !ev.altKey && !ev.ctrlKey && ev.metaKey) {
if (ev.keyCode === 65) { // cmd + a if (ev.keyCode === 65) { // cmd + a
@@ -1787,7 +1726,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
* @param g * @param g
* @param charset * @param charset
*/ */
public setgCharset(g: number, charset: ICharset): void { public setgCharset(g: number, charset: Charset): void {
this.charsets[g] = charset; this.charsets[g] = charset;
if (this.glevel === g) { if (this.glevel === g) {
this.charset = charset; this.charset = charset;
@@ -1820,7 +1759,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
} }
if (!key || ( if (!key || (
(ev.altKey || ev.ctrlKey || ev.metaKey) && !this._isThirdLevelShift(this.browser, ev) (ev.altKey || ev.ctrlKey || ev.metaKey) && !isThirdLevelShift(this.browser, ev)
)) { )) {
return false; return false;
} }
@@ -1856,9 +1795,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
*/ */
public bell(): void { public bell(): void {
this.emit('bell'); this.emit('bell');
if (this.soundBell()) { if (this.soundBell()) this.bellAudioElement.play();
this.soundManager.playBellSound();
}
if (this.visualBell()) { if (this.visualBell()) {
this.element.classList.add('visual-bell-active'); this.element.classList.add('visual-bell-active');
@@ -1915,9 +1852,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
this.rows = y; this.rows = y;
this.buffers.setupTabStops(this.cols); this.buffers.setupTabStops(this.cols);
if (this.charMeasure) { this.charMeasure.measure(this.options);
this.charMeasure.measure(this.options);
}
this.refresh(0, this.rows - 1); this.refresh(0, this.rows - 1);
this.emit('resize', {cols: x, rows: y}); this.emit('resize', {cols: x, rows: y});
@@ -2138,9 +2073,11 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
this.options.cols = this.cols; this.options.cols = this.cols;
const customKeyEventHandler = this.customKeyEventHandler; const customKeyEventHandler = this.customKeyEventHandler;
const inputHandler = this.inputHandler; const inputHandler = this.inputHandler;
const buffers = this.buffers;
this.setup(); this.setup();
this.customKeyEventHandler = customKeyEventHandler; this.customKeyEventHandler = customKeyEventHandler;
this.inputHandler = inputHandler; this.inputHandler = inputHandler;
this.buffers = buffers;
this.refresh(0, this.rows - 1); this.refresh(0, this.rows - 1);
this.viewport.syncScrollArea(); this.viewport.syncScrollArea();
} }
@@ -2169,15 +2106,31 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
} }
private visualBell(): boolean { private visualBell(): boolean {
return false; return this.options.bellStyle === 'visual' ||
// return this.options.bellStyle === 'visual' || this.options.bellStyle === 'both';
// this.options.bellStyle === 'both';
} }
private soundBell(): boolean { private soundBell(): boolean {
return this.options.bellStyle === 'sound'; return this.options.bellStyle === 'sound' ||
// return this.options.bellStyle === 'sound' || this.options.bellStyle === 'both';
// this.options.bellStyle === 'both'; }
private syncBellSound(): void {
// Don't update anything if the terminal has not been opened yet
if (!this.element) {
return;
}
if (this.soundBell() && this.bellAudioElement) {
this.bellAudioElement.setAttribute('src', this.options.bellSound);
} else if (this.soundBell()) {
this.bellAudioElement = document.createElement('audio');
this.bellAudioElement.setAttribute('preload', 'auto');
this.bellAudioElement.setAttribute('src', this.options.bellSound);
this.helperContainer.appendChild(this.bellAudioElement);
} else if (this.bellAudioElement) {
this.helperContainer.removeChild(this.bellAudioElement);
}
} }
} }
@@ -2200,6 +2153,19 @@ function off(el: any, type: string, handler: (event: Event) => any, capture: boo
el.removeEventListener(type, handler, capture); el.removeEventListener(type, handler, capture);
} }
function isThirdLevelShift(browser: IBrowser, ev: KeyboardEvent): boolean {
const thirdLevelKey =
(browser.isMac && ev.altKey && !ev.ctrlKey && !ev.metaKey) ||
(browser.isMSWindows && ev.altKey && ev.ctrlKey && !ev.metaKey);
if (ev.type === 'keypress') {
return thirdLevelKey;
}
// Don't invoke for arrows, pageDown, home, backspace, etc. (on non-keypress events)
return thirdLevelKey && (!ev.keyCode || ev.keyCode > 47);
}
function wasMondifierKeyOnlyEvent(ev: KeyboardEvent): boolean { function wasMondifierKeyOnlyEvent(ev: KeyboardEvent): boolean {
return ev.keyCode === 16 || // Shift return ev.keyCode === 16 || // Shift
ev.keyCode === 17 || // Ctrl ev.keyCode === 17 || // Ctrl
@@ -2252,7 +2218,7 @@ function matchColorDistance(r1: number, g1: number, b1: number, r2: number, g2:
return Math.pow(30 * (r1 - r2), 2) return Math.pow(30 * (r1 - r2), 2)
+ Math.pow(59 * (g1 - g2), 2) + Math.pow(59 * (g1 - g2), 2)
+ Math.pow(11 * (b1 - b2), 2); + Math.pow(11 * (b1 - b2), 2);
} };
function matchColor_(r1: number, g1: number, b1: number): number { function matchColor_(r1: number, g1: number, b1: number): number {
+19 -338
View File
@@ -3,352 +3,33 @@
* @license MIT * @license MIT
*/ */
import { Terminal as PublicTerminal, ITerminalOptions as IPublicTerminalOptions, IEventEmitter as IPublicEventEmitter, IEventEmitter } from 'xterm'; export type LinkMatcher = {
import { IColorSet, IRenderer } from './renderer/Types'; id: number,
import { IMouseZoneManager } from './input/Types'; regex: RegExp,
handler: LinkMatcherHandler,
hoverTooltipCallback?: LinkMatcherHandler,
hoverLeaveCallback?: () => void,
matchIndex?: number,
validationCallback?: LinkMatcherValidationCallback,
priority?: number
};
export type LinkMatcherHandler = (event: MouseEvent, uri: string) => boolean | void;
export type LinkMatcherValidationCallback = (uri: string, callback: (isValid: boolean) => void) => void;
export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean; export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean;
export type Charset = {[key: string]: string};
export type XtermListener = (...args: any[]) => void;
export type CharData = [number, string, number, number]; export type CharData = [number, string, number, number];
export type LineData = CharData[]; export type LineData = CharData[];
export type LinkMatcherHandler = (event: MouseEvent, uri: string) => void; export type LinkHoverEvent = {
export type LinkMatcherValidationCallback = (uri: string, callback: (isValid: boolean) => void) => void; x: number,
y: number,
length: number
};
export enum LinkHoverEventTypes { export enum LinkHoverEventTypes {
HOVER = 'linkhover', HOVER = 'linkhover',
TOOLTIP = 'linktooltip', TOOLTIP = 'linktooltip',
LEAVE = 'linkleave' LEAVE = 'linkleave'
} };
/**
* This interface encapsulates everything needed from the Terminal by the
* InputHandler. This cleanly separates the large amount of methods needed by
* InputHandler cleanly from the ITerminal interface.
*/
export interface IInputHandlingTerminal extends IEventEmitter {
element: HTMLElement;
options: ITerminalOptions;
cols: number;
rows: number;
charset: ICharset;
gcharset: number;
glevel: number;
charsets: ICharset[];
applicationKeypad: boolean;
applicationCursor: boolean;
originMode: boolean;
insertMode: boolean;
wraparoundMode: boolean;
bracketedPasteMode: boolean;
defAttr: number;
curAttr: number;
prefix: string;
savedCols: number;
x10Mouse: boolean;
vt200Mouse: boolean;
normalMouse: boolean;
mouseEvents: boolean;
sendFocus: boolean;
utfMouse: boolean;
sgrMouse: boolean;
urxvtMouse: boolean;
cursorHidden: boolean;
buffers: IBufferSet;
buffer: IBuffer;
viewport: IViewport;
selectionManager: ISelectionManager;
bell(): void;
focus(): void;
convertEol: boolean;
updateRange(y: number): void;
scroll(isWrapped?: boolean): void;
setgLevel(g: number): void;
eraseAttr(): number;
eraseRight(x: number, y: number): void;
eraseLine(y: number): void;
eraseLeft(x: number, y: number): void;
blankLine(cur?: boolean, isWrapped?: boolean): LineData;
is(term: string): boolean;
send(data: string): void;
setgCharset(g: number, charset: ICharset): void;
resize(x: number, y: number): void;
log(text: string, data?: any): void;
reset(): void;
showCursor(): void;
refresh(start: number, end: number): void;
matchColor(r1: number, g1: number, b1: number): number;
error(text: string, data?: any): void;
setOption(key: string, value: any): void;
}
export interface IViewport {
scrollBarWidth: number;
syncScrollArea(): void;
onWheel(ev: WheelEvent): void;
onTouchStart(ev: TouchEvent): void;
onTouchMove(ev: TouchEvent): void;
onThemeChanged(colors: IColorSet): void;
}
export interface ICompositionHelper {
compositionstart(): void;
compositionupdate(ev: CompositionEvent): void;
compositionend(): void;
updateCompositionElements(dontRecurse?: boolean): void;
keydown(ev: KeyboardEvent): boolean;
}
/**
* Handles actions generated by the parser.
*/
export interface IInputHandler {
addChar(char: string, code: number): void;
/** C0 BEL */ bell(): void;
/** C0 LF */ lineFeed(): void;
/** C0 CR */ carriageReturn(): void;
/** C0 BS */ backspace(): void;
/** C0 HT */ tab(): void;
/** C0 SO */ shiftOut(): void;
/** C0 SI */ shiftIn(): void;
/** CSI @ */ insertChars(params?: number[]): void;
/** CSI A */ cursorUp(params?: number[]): void;
/** CSI B */ cursorDown(params?: number[]): void;
/** CSI C */ cursorForward(params?: number[]): void;
/** CSI D */ cursorBackward(params?: number[]): void;
/** CSI E */ cursorNextLine(params?: number[]): void;
/** CSI F */ cursorPrecedingLine(params?: number[]): void;
/** CSI G */ cursorCharAbsolute(params?: number[]): void;
/** CSI H */ cursorPosition(params?: number[]): void;
/** CSI I */ cursorForwardTab(params?: number[]): void;
/** CSI J */ eraseInDisplay(params?: number[]): void;
/** CSI K */ eraseInLine(params?: number[]): void;
/** CSI L */ insertLines(params?: number[]): void;
/** CSI M */ deleteLines(params?: number[]): void;
/** CSI P */ deleteChars(params?: number[]): void;
/** CSI S */ scrollUp(params?: number[]): void;
/** CSI T */ scrollDown(params?: number[]): void;
/** CSI X */ eraseChars(params?: number[]): void;
/** CSI Z */ cursorBackwardTab(params?: number[]): void;
/** CSI ` */ charPosAbsolute(params?: number[]): void;
/** CSI a */ HPositionRelative(params?: number[]): void;
/** CSI b */ repeatPrecedingCharacter(params?: number[]): void;
/** CSI c */ sendDeviceAttributes(params?: number[]): void;
/** CSI d */ linePosAbsolute(params?: number[]): void;
/** CSI e */ VPositionRelative(params?: number[]): void;
/** CSI f */ HVPosition(params?: number[]): void;
/** CSI g */ tabClear(params?: number[]): void;
/** CSI h */ setMode(params?: number[]): void;
/** CSI l */ resetMode(params?: number[]): void;
/** CSI m */ charAttributes(params?: number[]): void;
/** CSI n */ deviceStatus(params?: number[]): void;
/** CSI p */ softReset(params?: number[]): void;
/** CSI q */ setCursorStyle(params?: number[]): void;
/** CSI r */ setScrollRegion(params?: number[]): void;
/** CSI s */ saveCursor(params?: number[]): void;
/** CSI u */ restoreCursor(params?: number[]): void;
}
export interface ILinkMatcher {
id: number;
regex: RegExp;
handler: LinkMatcherHandler;
hoverTooltipCallback?: LinkMatcherHandler;
hoverLeaveCallback?: () => void;
matchIndex?: number;
validationCallback?: LinkMatcherValidationCallback;
priority?: number;
willLinkActivate?: (event: MouseEvent, uri: string) => boolean;
}
export interface ICharset {
[key: string]: string;
}
export interface ILinkHoverEvent {
x: number;
y: number;
length: number;
}
export interface ITerminal extends PublicTerminal, IElementAccessor, IBufferAccessor, ILinkifierAccessor {
screenElement: HTMLElement;
selectionManager: ISelectionManager;
charMeasure: ICharMeasure;
renderer: IRenderer;
browser: IBrowser;
writeBuffer: string[];
cursorHidden: boolean;
cursorState: number;
defAttr: number;
options: ITerminalOptions;
buffer: IBuffer;
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;
showCursor(): void;
blankLine(cur?: boolean, isWrapped?: boolean, cols?: number): LineData;
}
export interface IBufferAccessor {
buffer: IBuffer;
}
export interface IElementAccessor {
element: HTMLElement;
}
export interface ILinkifierAccessor {
linkifier: ILinkifier;
}
export interface IMouseHelper {
getCoords(event: {pageX: number, pageY: number}, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number, isSelection?: boolean): [number, number];
getRawByteCoords(event: MouseEvent, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number): { x: number, y: number };
}
export interface ICharMeasure {
width: number;
height: number;
measure(options: ITerminalOptions): void;
}
// TODO: The options that are not in the public API should be reviewed
export interface ITerminalOptions extends IPublicTerminalOptions {
cancelEvents?: boolean;
convertEol?: boolean;
debug?: boolean;
handler?: (data: string) => void;
screenKeys?: boolean;
termName?: string;
useFlowControl?: boolean;
}
export interface IBuffer {
lines: ICircularList<LineData>;
ydisp: number;
ybase: number;
y: number;
x: number;
tabs: any;
scrollBottom: number;
scrollTop: number;
hasScrollback: boolean;
savedY: number;
savedX: number;
isCursorInViewport: boolean;
translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol?: number, endCol?: number): string;
nextStop(x?: number): number;
prevStop(x?: number): number;
}
export interface IBufferSet extends IEventEmitter {
alt: IBuffer;
normal: IBuffer;
active: IBuffer;
activateNormalBuffer(): void;
activateAltBuffer(): void;
}
export interface ICircularList<T> extends IEventEmitter {
length: number;
maxLength: number;
forEach: (callbackfn: (value: T, index: number) => void) => void;
get(index: number): T;
set(index: number, value: T): void;
push(value: T): void;
pop(): T;
splice(start: number, deleteCount: number, ...items: T[]): void;
trimStart(count: number): void;
shiftElements(start: number, count: number, offset: number): void;
}
export interface ISelectionManager {
selectionText: string;
selectionStart: [number, number];
selectionEnd: [number, number];
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 {
attachToDom(mouseZoneManager: IMouseZoneManager): void;
linkifyRows(start: number, end: number): void;
registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options?: ILinkMatcherOptions): number;
deregisterLinkMatcher(matcherId: number): boolean;
}
export interface ILinkMatcherOptions {
/**
* The index of the link from the regex.match(text) call. This defaults to 0
* (for regular expressions without capture groups).
*/
matchIndex?: number;
/**
* A callback that validates an individual link, returning true if valid and
* false if invalid.
*/
validationCallback?: LinkMatcherValidationCallback;
/**
* A callback that fires when the mouse hovers over a link.
*/
tooltipCallback?: LinkMatcherHandler;
/**
* A callback that fires when the mouse leaves a link that was hovered.
*/
leaveCallback?: () => void;
/**
* The priority of the link matcher, this defines the order in which the link
* matcher is evaluated relative to others, from highest to lowest. The
* 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 {
isNode: boolean;
userAgent: string;
platform: string;
isFirefox: boolean;
isMSIE: boolean;
isMac: boolean;
isIpad: boolean;
isIphone: boolean;
isMSWindows: boolean;
}
export interface ISoundManager {
playBellSound(): void;
}
+12 -20
View File
@@ -3,18 +3,15 @@
* @license MIT * @license MIT
*/ */
import { IColorSet } from './renderer/Types'; import { ITerminal, IViewport } from './Interfaces';
import { ITerminal, IViewport } from './Types';
import { CharMeasure } from './utils/CharMeasure'; import { CharMeasure } from './utils/CharMeasure';
import { IColorSet } from './renderer/Interfaces';
const FALLBACK_SCROLL_BAR_WIDTH = 15;
/** /**
* Represents the viewport of a terminal, the visible area within the larger buffer of output. * 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. * Logic for the virtual scroll bar is included in this object.
*/ */
export class Viewport implements IViewport { export class Viewport implements IViewport {
public scrollBarWidth: number = 0;
private currentRowHeight: number = 0; private currentRowHeight: number = 0;
private lastRecordedBufferLength: number = 0; private lastRecordedBufferLength: number = 0;
private lastRecordedViewportHeight: number = 0; private lastRecordedViewportHeight: number = 0;
@@ -34,10 +31,6 @@ export class Viewport implements IViewport {
private scrollArea: HTMLElement, private scrollArea: HTMLElement,
private charMeasure: CharMeasure 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)); this.viewportElement.addEventListener('scroll', this.onScroll.bind(this));
// Perform this async to ensure the CharMeasure is ready. // Perform this async to ensure the CharMeasure is ready.
@@ -55,8 +48,13 @@ export class Viewport implements IViewport {
private refresh(): void { private refresh(): void {
if (this.charMeasure.height > 0) { if (this.charMeasure.height > 0) {
this.currentRowHeight = this.terminal.renderer.dimensions.scaledCellHeight / window.devicePixelRatio; this.currentRowHeight = this.terminal.renderer.dimensions.scaledCellHeight / window.devicePixelRatio;
this.lastRecordedViewportHeight = this.viewportElement.offsetHeight;
const newBufferHeight = Math.round(this.currentRowHeight * this.lastRecordedBufferLength) + (this.lastRecordedViewportHeight - this.terminal.renderer.dimensions.canvasHeight); 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);
if (this.lastRecordedBufferHeight !== newBufferHeight) { if (this.lastRecordedBufferHeight !== newBufferHeight) {
this.lastRecordedBufferHeight = newBufferHeight; this.lastRecordedBufferHeight = newBufferHeight;
this.scrollArea.style.height = this.lastRecordedBufferHeight + 'px'; this.scrollArea.style.height = this.lastRecordedBufferHeight + 'px';
@@ -95,12 +93,6 @@ export class Viewport implements IViewport {
* @param ev The scroll event. * @param ev The scroll event.
*/ */
private onScroll(ev: Event): void { 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 newRow = Math.round(this.viewportElement.scrollTop / this.currentRowHeight);
const diff = newRow - this.terminal.buffer.ydisp; const diff = newRow - this.terminal.buffer.ydisp;
this.terminal.scrollLines(diff, true); this.terminal.scrollLines(diff, true);
@@ -127,7 +119,7 @@ export class Viewport implements IViewport {
this.viewportElement.scrollTop += ev.deltaY * multiplier; this.viewportElement.scrollTop += ev.deltaY * multiplier;
// Prevent the page from scrolling when the terminal scrolls // Prevent the page from scrolling when the terminal scrolls
ev.preventDefault(); ev.preventDefault();
} };
/** /**
* Handles the touchstart event, recording the touch occurred. * Handles the touchstart event, recording the touch occurred.
@@ -135,7 +127,7 @@ export class Viewport implements IViewport {
*/ */
public onTouchStart(ev: TouchEvent): void { public onTouchStart(ev: TouchEvent): void {
this.lastTouchY = ev.touches[0].pageY; this.lastTouchY = ev.touches[0].pageY;
} };
/** /**
* Handles the touchmove event, scrolling the viewport if the position shifted. * Handles the touchmove event, scrolling the viewport if the position shifted.
@@ -149,5 +141,5 @@ export class Viewport implements IViewport {
} }
this.viewportElement.scrollTop += deltaY; this.viewportElement.scrollTop += deltaY;
ev.preventDefault(); ev.preventDefault();
} };
} }
-18
View File
@@ -1,18 +0,0 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*
* Implements the attach method, that attaches the terminal to a WebSocket stream.
*/
import { Terminal } from 'xterm';
export interface IAttachAddonTerminal extends Terminal {
__socket?: WebSocket;
__attachSocketBuffer?: string;
__getMessage?(ev: MessageEvent): void;
__flushBuffer?(): void;
__pushToBuffer?(data: string): void;
__sendData?(data: string): void;
}
+2 -2
View File
@@ -5,14 +5,14 @@
import { assert, expect } from 'chai'; import { assert, expect } from 'chai';
import * as attach from './attach'; import * as attach from './attach'
class MockTerminal {} class MockTerminal {}
describe('attach addon', () => { describe('attach addon', () => {
describe('apply', () => { describe('apply', () => {
it('should do register the `attach` and `detach` methods', () => { it('should do register the `attach` and `detach` methods', () => {
attach.apply(<any>MockTerminal); attach.apply(MockTerminal);
assert.equal(typeof (<any>MockTerminal).prototype.attach, 'function'); assert.equal(typeof (<any>MockTerminal).prototype.attach, 'function');
assert.equal(typeof (<any>MockTerminal).prototype.detach, 'function'); assert.equal(typeof (<any>MockTerminal).prototype.detach, 'function');
}); });
+55 -55
View File
@@ -5,118 +5,118 @@
* Implements the attach method, that attaches the terminal to a WebSocket stream. * Implements the attach method, that attaches the terminal to a WebSocket stream.
*/ */
/// <reference path="../../../typings/xterm.d.ts"/>
import { Terminal } from 'xterm';
import { IAttachAddonTerminal } from './Interfaces';
/** /**
* Attaches the given terminal to the given socket. * Attaches the given terminal to the given socket.
* *
* @param term The terminal to be attached to the given socket. * @param {Terminal} term - The terminal to be attached to the given socket.
* @param socket The socket to attach the current terminal. * @param {WebSocket} socket - The socket to attach the current terminal.
* @param bidirectional Whether the terminal should send data to the socket as well. * @param {boolean} bidirectional - Whether the terminal should send data
* @param buffered Whether the rendering of incoming data should happen instantly or at a maximum * to the socket as well.
* frequency of 1 rendering per 10ms. * @param {boolean} buffered - Whether the rendering of incoming data
* should happen instantly or at a maximum
* frequency of 1 rendering per 10ms.
*/ */
export function attach(term: Terminal, socket: WebSocket, bidirectional: boolean, buffered: boolean): void { export function attach(term, socket, bidirectional, buffered) {
const addonTerminal = <IAttachAddonTerminal>term; bidirectional = (typeof bidirectional == 'undefined') ? true : bidirectional;
bidirectional = (typeof bidirectional === 'undefined') ? true : bidirectional; term.socket = socket;
addonTerminal.__socket = socket;
addonTerminal.__flushBuffer = () => { term._flushBuffer = function() {
addonTerminal.write(addonTerminal.__attachSocketBuffer); term.write(term._attachSocketBuffer);
addonTerminal.__attachSocketBuffer = null; term._attachSocketBuffer = null;
}; };
addonTerminal.__pushToBuffer = (data: string) => { term._pushToBuffer = function(data) {
if (addonTerminal.__attachSocketBuffer) { if (term._attachSocketBuffer) {
addonTerminal.__attachSocketBuffer += data; term._attachSocketBuffer += data;
} else { } else {
addonTerminal.__attachSocketBuffer = data; term._attachSocketBuffer = data;
setTimeout(addonTerminal.__flushBuffer, 10); setTimeout(term._flushBuffer, 10);
} }
}; };
let myTextDecoder; var myTextDecoder;
addonTerminal.__getMessage = function(ev: MessageEvent): void { term._getMessage = function(ev) {
let str; var str;
if (typeof ev.data === 'object') { if (typeof ev.data === "object") {
if (ev.data instanceof ArrayBuffer) { if (ev.data instanceof ArrayBuffer) {
if (!myTextDecoder) { if (!myTextDecoder) {
myTextDecoder = new TextDecoder(); myTextDecoder = new TextDecoder();
} }
str = myTextDecoder.decode( ev.data ); str = myTextDecoder.decode( ev.data );
} else { }
throw 'TODO: handle Blob?'; else {
throw "TODO: handle Blob?";
} }
} }
if (buffered) { if (buffered) {
addonTerminal.__pushToBuffer(str || ev.data); term._pushToBuffer(str || ev.data);
} else { } else {
addonTerminal.write(str || ev.data); term.write(str || ev.data);
} }
}; };
addonTerminal.__sendData = (data: string) => { term._sendData = function(data) {
if (socket.readyState !== 1) { if (socket.readyState !== 1) {
return; return;
} }
socket.send(data); socket.send(data);
}; };
socket.addEventListener('message', addonTerminal.__getMessage); socket.addEventListener('message', term._getMessage);
if (bidirectional) { if (bidirectional) {
addonTerminal.on('data', addonTerminal.__sendData); term.on('data', term._sendData);
} }
socket.addEventListener('close', () => detach(addonTerminal, socket)); socket.addEventListener('close', term.detach.bind(term, socket));
socket.addEventListener('error', () => detach(addonTerminal, socket)); socket.addEventListener('error', term.detach.bind(term, socket));
} };
/** /**
* Detaches the given terminal from the given socket * Detaches the given terminal from the given socket
* *
* @param term The terminal to be detached from the given socket. * @param {Terminal} term - The terminal to be detached from the given socket.
* @param socket The socket from which to detach the current terminal. * @param {WebSocket} socket - The socket from which to detach the current
* terminal.
*/ */
export function detach(term: Terminal, socket: WebSocket): void { export function detach(term, socket) {
const addonTerminal = <IAttachAddonTerminal>term; term.off('data', term._sendData);
addonTerminal.off('data', addonTerminal.__sendData);
socket = (typeof socket === 'undefined') ? addonTerminal.__socket : socket; socket = (typeof socket == 'undefined') ? term.socket : socket;
if (socket) { if (socket) {
socket.removeEventListener('message', addonTerminal.__getMessage); socket.removeEventListener('message', term._getMessage);
} }
delete addonTerminal.__socket; delete term.socket;
} };
export function apply(terminalConstructor: typeof Terminal): void { export function apply(terminalConstructor) {
/** /**
* Attaches the current terminal to the given socket * Attaches the current terminal to the given socket
* *
* @param socket The socket to attach the current terminal. * @param {WebSocket} socket - The socket to attach the current terminal.
* @param bidirectional Whether the terminal should send data to the socket as well. * @param {boolean} bidirectional - Whether the terminal should send data
* @param buffered Whether the rendering of incoming data should happen instantly or at a maximum * to the socket as well.
* frequency of 1 rendering per 10ms. * @param {boolean} buffered - Whether the rendering of incoming data
* should happen instantly or at a maximum
* frequency of 1 rendering per 10ms.
*/ */
(<any>terminalConstructor.prototype).attach = function (socket: WebSocket, bidirectional: boolean, buffered: boolean): void { terminalConstructor.prototype.attach = function(socket, bidirectional, buffered) {
attach(this, socket, bidirectional, buffered); return attach(this, socket, bidirectional, buffered);
}; };
/** /**
* Detaches the current terminal from the given socket. * Detaches the current terminal from the given socket.
* *
* @param socket The socket from which to detach the current terminal. * @param {WebSocket} socket - The socket from which to detach the current
* terminal.
*/ */
(<any>terminalConstructor.prototype).detach = function (socket: WebSocket): void { terminalConstructor.prototype.detach = function(socket) {
detach(this, socket); return detach(this, socket);
}; };
} }
+1 -2
View File
@@ -5,7 +5,6 @@
"rootDir": ".", "rootDir": ".",
"outDir": "../../../lib/addons/attach/", "outDir": "../../../lib/addons/attach/",
"sourceMap": true, "sourceMap": true,
"removeComments": true, "removeComments": true
"declaration": true
} }
} }
+2 -2
View File
@@ -5,14 +5,14 @@
import { assert, expect } from 'chai'; import { assert, expect } from 'chai';
import * as fit from './fit'; import * as fit from './fit'
class MockTerminal {} class MockTerminal {}
describe('fit addon', () => { describe('fit addon', () => {
describe('apply', () => { describe('apply', () => {
it('should do register the `proposeGeometry` and `fit` methods', () => { it('should do register the `proposeGeometry` and `fit` methods', () => {
fit.apply(<any>MockTerminal); fit.apply(MockTerminal);
assert.equal(typeof (<any>MockTerminal).prototype.proposeGeometry, 'function'); assert.equal(typeof (<any>MockTerminal).prototype.proposeGeometry, 'function');
assert.equal(typeof (<any>MockTerminal).prototype.fit, 'function'); assert.equal(typeof (<any>MockTerminal).prototype.fit, 'function');
}); });
+25 -39
View File
@@ -13,57 +13,43 @@
* row and truncate its width with the current number of columns). * row and truncate its width with the current number of columns).
*/ */
/// <reference path="../../../typings/xterm.d.ts"/> export function proposeGeometry(term) {
import { Terminal } from 'xterm';
export interface IGeometry {
rows: number;
cols: number;
}
export function proposeGeometry(term: Terminal): IGeometry {
if (!term.element.parentElement) { if (!term.element.parentElement) {
return null; return null;
} }
const parentElementStyle = window.getComputedStyle(term.element.parentElement); var parentElementStyle = window.getComputedStyle(term.element.parentElement);
const parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height')); var parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height'));
const parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width'))); var parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')) - 17);
const elementStyle = window.getComputedStyle(term.element); var elementStyle = window.getComputedStyle(term.element);
const elementPadding = { var elementPaddingVer = parseInt(elementStyle.getPropertyValue('padding-top')) + parseInt(elementStyle.getPropertyValue('padding-bottom'));
top: parseInt(elementStyle.getPropertyValue('padding-top')), var elementPaddingHor = parseInt(elementStyle.getPropertyValue('padding-right')) + parseInt(elementStyle.getPropertyValue('padding-left'));
bottom: parseInt(elementStyle.getPropertyValue('padding-bottom')), var availableHeight = parentElementHeight - elementPaddingVer;
right: parseInt(elementStyle.getPropertyValue('padding-right')), var availableWidth = parentElementWidth - elementPaddingHor;
left: parseInt(elementStyle.getPropertyValue('padding-left')) var geometry = {
cols: Math.floor(availableWidth / term.renderer.dimensions.actualCellWidth),
rows: Math.floor(availableHeight / term.renderer.dimensions.actualCellHeight)
}; };
const elementPaddingVer = elementPadding.top + elementPadding.bottom;
const elementPaddingHor = elementPadding.right + elementPadding.left;
const availableHeight = parentElementHeight - elementPaddingVer;
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;
}
export function fit(term: Terminal): void { return geometry;
const geometry = proposeGeometry(term); };
export function fit(term) {
var geometry = proposeGeometry(term);
if (geometry) { if (geometry) {
// Force a full render // Force a full render
if (term.rows !== geometry.rows || term.cols !== geometry.cols) { if (term.rows !== geometry.rows || term.cols !== geometry.cols) {
(<any>term).renderer.clear(); term.renderer.clear();
term.resize(geometry.cols, geometry.rows); term.resize(geometry.cols, geometry.rows);
} }
} }
} };
export function apply(terminalConstructor: typeof Terminal): void { export function apply(terminalConstructor) {
(<any>terminalConstructor.prototype).proposeGeometry = function (): IGeometry { terminalConstructor.prototype.proposeGeometry = function() {
return proposeGeometry(this); return proposeGeometry(this);
}; }
(<any>terminalConstructor.prototype).fit = function (): void { terminalConstructor.prototype.fit = function() {
fit(this); return fit(this);
}; }
} }
+1 -2
View File
@@ -5,7 +5,6 @@
"rootDir": ".", "rootDir": ".",
"outDir": "../../../lib/addons/fit/", "outDir": "../../../lib/addons/fit/",
"sourceMap": true, "sourceMap": true,
"removeComments": true, "removeComments": true
"declaration": true
} }
} }
+2 -2
View File
@@ -5,14 +5,14 @@
import { assert, expect } from 'chai'; import { assert, expect } from 'chai';
import * as fullscreen from './fullscreen'; import * as fullscreen from './fullscreen'
class MockTerminal {} class MockTerminal {}
describe('fullscreen addon', () => { describe('fullscreen addon', () => {
describe('apply', () => { describe('apply', () => {
it('should do register the `toggleFullscreen` method', () => { it('should do register the `toggleFullscreen` method', () => {
fullscreen.apply(<any>MockTerminal); fullscreen.apply(MockTerminal);
assert.equal(typeof (<any>MockTerminal).prototype.toggleFullScreen, 'function'); assert.equal(typeof (<any>MockTerminal).prototype.toggleFullScreen, 'function');
}); });
}); });
+9 -13
View File
@@ -3,19 +3,15 @@
* @license MIT * @license MIT
*/ */
/// <reference path="../../../typings/xterm.d.ts"/>
import { Terminal } from 'xterm';
/** /**
* Toggle the given terminal's fullscreen mode. * Toggle the given terminal's fullscreen mode.
* @param term The terminal to toggle full screen mode * @param {Terminal} term - The terminal to toggle full screen mode
* @param fullscreen Toggle fullscreen on (true) or off (false) * @param {boolean} fullscreen - Toggle fullscreen on (true) or off (false)
*/ */
export function toggleFullScreen(term: Terminal, fullscreen: boolean): void { export function toggleFullScreen(term, fullscreen) {
let fn: string; var fn;
if (typeof fullscreen === 'undefined') { if (typeof fullscreen == 'undefined') {
fn = (term.element.classList.contains('fullscreen')) ? 'remove' : 'add'; fn = (term.element.classList.contains('fullscreen')) ? 'remove' : 'add';
} else if (!fullscreen) { } else if (!fullscreen) {
fn = 'remove'; fn = 'remove';
@@ -24,10 +20,10 @@ export function toggleFullScreen(term: Terminal, fullscreen: boolean): void {
} }
term.element.classList[fn]('fullscreen'); term.element.classList[fn]('fullscreen');
} };
export function apply(terminalConstructor: typeof Terminal): void { export function apply(terminalConstructor) {
(<any>terminalConstructor.prototype).toggleFullScreen = function (fullscreen: boolean): void { terminalConstructor.prototype.toggleFullScreen = function (fullscreen) {
toggleFullScreen(this, fullscreen); return toggleFullScreen(this, fullscreen);
}; };
} }
+1 -2
View File
@@ -5,7 +5,6 @@
"rootDir": ".", "rootDir": ".",
"outDir": "../../../lib/addons/fullscreen/", "outDir": "../../../lib/addons/fullscreen/",
"sourceMap": true, "sourceMap": true,
"removeComments": true, "removeComments": true
"declaration": true
} }
} }
-19
View File
@@ -1,19 +0,0 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { Terminal } from 'xterm';
export interface ISearchAddonTerminal extends Terminal {
__searchHelper?: ISearchHelper;
// TODO: Reuse ITerminal from core
buffer: any;
selectionManager: any;
}
export interface ISearchHelper {
findNext(term: string): boolean;
findPrevious(term: string): boolean;
}
+6 -20
View File
@@ -3,7 +3,8 @@
* @license MIT * @license MIT
*/ */
import { ISearchHelper, ISearchAddonTerminal } from './Interfaces'; // import { ITerminal } from '../../Interfaces';
// import { translateBufferLineToString } from '../../utils/BufferLine';
interface ISearchResult { interface ISearchResult {
term: string; term: string;
@@ -14,8 +15,8 @@ interface ISearchResult {
/** /**
* A class that knows how to search the terminal and how to display the results. * A class that knows how to search the terminal and how to display the results.
*/ */
export class SearchHelper implements ISearchHelper { export class SearchHelper {
constructor(private _terminal: ISearchAddonTerminal) { constructor(private _terminal: any) {
// TODO: Search for multiple instances on 1 line // TODO: Search for multiple instances on 1 line
// TODO: Don't use the actual selection, instead use a "find selection" so multiple instances can be highlighted // TODO: Don't use the actual selection, instead use a "find selection" so multiple instances can be highlighted
// TODO: Highlight other instances in the viewport // TODO: Highlight other instances in the viewport
@@ -113,23 +114,8 @@ export class SearchHelper implements ISearchHelper {
private _findInLine(term: string, y: number): ISearchResult { private _findInLine(term: string, y: number): ISearchResult {
const lowerStringLine = this._terminal.buffer.translateBufferLineToString(y, true).toLowerCase(); const lowerStringLine = this._terminal.buffer.translateBufferLineToString(y, true).toLowerCase();
const lowerTerm = term.toLowerCase(); const lowerTerm = term.toLowerCase();
let searchIndex = lowerStringLine.indexOf(lowerTerm); const searchIndex = lowerStringLine.indexOf(lowerTerm);
if (searchIndex >= 0) { if (searchIndex >= 0) {
const line = this._terminal.buffer.lines.get(y);
for (let i = 0; i < searchIndex; i++) {
const charData = line[i];
// Adjust the searchIndex to normalize emoji into single chars
const char = charData[1/*CHAR_DATA_CHAR_INDEX*/];
if (char.length > 1) {
searchIndex -= char.length - 1;
}
// Adjust the searchIndex for empty characters following wide unicode
// chars (eg. CJK)
const charWidth = charData[2/*CHAR_DATA_WIDTH_INDEX*/];
if (charWidth === 0) {
searchIndex++;
}
}
return { return {
term, term,
col: searchIndex, col: searchIndex,
@@ -148,7 +134,7 @@ export class SearchHelper implements ISearchHelper {
return false; return false;
} }
this._terminal.selectionManager.setSelection(result.col, result.row, result.term.length); this._terminal.selectionManager.setSelection(result.col, result.row, result.term.length);
this._terminal.scrollLines(result.row - this._terminal.buffer.ydisp); this._terminal.scrollLines(result.row - this._terminal.buffer.ydisp, false);
return true; return true;
} }
} }
+16 -21
View File
@@ -3,11 +3,8 @@
* @license MIT * @license MIT
*/ */
/// <reference path="../../../typings/xterm.d.ts"/>
import { SearchHelper } from './SearchHelper'; import { SearchHelper } from './SearchHelper';
import { Terminal } from 'xterm';
import { ISearchAddonTerminal } from './Interfaces';
/** /**
* Find the next instance of the term, then scroll to and select it. If it * Find the next instance of the term, then scroll to and select it. If it
@@ -15,13 +12,12 @@ import { ISearchAddonTerminal } from './Interfaces';
* @param term Tne search term. * @param term Tne search term.
* @return Whether a result was found. * @return Whether a result was found.
*/ */
export function findNext(terminal: Terminal, term: string): boolean { export function findNext(terminal: any, term: string): boolean {
const addonTerminal = <ISearchAddonTerminal>terminal; if (!terminal._searchHelper) {
if (!addonTerminal.__searchHelper) { terminal.searchHelper = new SearchHelper(terminal);
addonTerminal.__searchHelper = new SearchHelper(addonTerminal);
} }
return addonTerminal.__searchHelper.findNext(term); return (<SearchHelper>terminal.searchHelper).findNext(term);
} };
/** /**
* Find the previous instance of the term, then scroll to and select it. If it * Find the previous instance of the term, then scroll to and select it. If it
@@ -29,20 +25,19 @@ export function findNext(terminal: Terminal, term: string): boolean {
* @param term Tne search term. * @param term Tne search term.
* @return Whether a result was found. * @return Whether a result was found.
*/ */
export function findPrevious(terminal: Terminal, term: string): boolean { export function findPrevious(terminal: any, term: string): boolean {
const addonTerminal = <ISearchAddonTerminal>terminal; if (!terminal._searchHelper) {
if (!addonTerminal.__searchHelper) { terminal.searchHelper = new SearchHelper(terminal);
addonTerminal.__searchHelper = new SearchHelper(addonTerminal);
} }
return addonTerminal.__searchHelper.findPrevious(term); return (<SearchHelper>terminal.searchHelper).findPrevious(term);
} };
export function apply(terminalConstructor: typeof Terminal): void { export function apply(terminalConstructor) {
(<any>terminalConstructor.prototype).findNext = function(term: string): boolean { terminalConstructor.prototype.findNext = function(term) {
return findNext(this, term); return findNext(this, term);
}; }
(<any>terminalConstructor.prototype).findPrevious = function(term: string): boolean { terminalConstructor.prototype.findPrevious = function(term) {
return findPrevious(this, term); return findPrevious(this, term);
}; }
} }
+1 -2
View File
@@ -5,7 +5,6 @@
"rootDir": ".", "rootDir": ".",
"outDir": "../../../lib/addons/search/", "outDir": "../../../lib/addons/search/",
"sourceMap": true, "sourceMap": true,
"removeComments": true, "removeComments": true
"declaration": true
} }
} }
-19
View File
@@ -1,19 +0,0 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*
* Implements the attach method, that attaches the terminal to a WebSocket stream.
*/
import { Terminal } from 'xterm';
export interface ITerminadoAddonTerminal extends Terminal {
__socket?: WebSocket;
__attachSocketBuffer?: string;
__getMessage?(ev: MessageEvent): void;
__flushBuffer?(): void;
__pushToBuffer?(data: string): void;
__sendData?(data: string): void;
__setSize?(size: {rows: number, cols: number}): void;
}
+2 -2
View File
@@ -5,14 +5,14 @@
import { assert, expect } from 'chai'; import { assert, expect } from 'chai';
import * as terminado from './terminado'; import * as terminado from './terminado'
class MockTerminal {} class MockTerminal {}
describe('terminado addon', () => { describe('terminado addon', () => {
describe('apply', () => { describe('apply', () => {
it('should do register the `terminadoAttach` and `terminadoDetach` methods', () => { it('should do register the `terminadoAttach` and `terminadoDetach` methods', () => {
terminado.apply(<any>MockTerminal); terminado.apply(MockTerminal);
assert.equal(typeof (<any>MockTerminal).prototype.terminadoAttach, 'function'); assert.equal(typeof (<any>MockTerminal).prototype.terminadoAttach, 'function');
assert.equal(typeof (<any>MockTerminal).prototype.terminadoDetach, 'function'); assert.equal(typeof (<any>MockTerminal).prototype.terminadoDetach, 'function');
}); });
+51 -52
View File
@@ -6,107 +6,106 @@
* WebSocket stream. * WebSocket stream.
*/ */
/// <reference path="../../../typings/xterm.d.ts"/>
import { Terminal } from 'xterm';
import { ITerminadoAddonTerminal } from './Interfaces';
/** /**
* Attaches the given terminal to the given socket. * Attaches the given terminal to the given socket.
* *
* @param term The terminal to be attached to the given socket. * @param {Terminal} term - The terminal to be attached to the given socket.
* @param socket The socket to attach the current terminal. * @param {WebSocket} socket - The socket to attach the current terminal.
* @param bidirectional Whether the terminal should send data to the socket as well. * @param {boolean} bidirectional - Whether the terminal should send data
* @param buffered Whether the rendering of incoming data should happen instantly or at a maximum * to the socket as well.
* frequency of 1 rendering per 10ms. * @param {boolean} buffered - Whether the rendering of incoming data
* should happen instantly or at a maximum
* frequency of 1 rendering per 10ms.
*/ */
export function terminadoAttach(term: Terminal, socket: WebSocket, bidirectional: boolean, buffered: boolean): void { export function terminadoAttach(term, socket, bidirectional, buffered) {
const addonTerminal = <ITerminadoAddonTerminal>term; bidirectional = (typeof bidirectional == 'undefined') ? true : bidirectional;
bidirectional = (typeof bidirectional === 'undefined') ? true : bidirectional; term.socket = socket;
addonTerminal.__socket = socket;
addonTerminal.__flushBuffer = () => { term._flushBuffer = function() {
addonTerminal.write(addonTerminal.__attachSocketBuffer); term.write(term._attachSocketBuffer);
addonTerminal.__attachSocketBuffer = null; term._attachSocketBuffer = null;
}; };
addonTerminal.__pushToBuffer = (data: string) => { term._pushToBuffer = function(data) {
if (addonTerminal.__attachSocketBuffer) { if (term._attachSocketBuffer) {
addonTerminal.__attachSocketBuffer += data; term._attachSocketBuffer += data;
} else { } else {
addonTerminal.__attachSocketBuffer = data; term._attachSocketBuffer = data;
setTimeout(addonTerminal.__flushBuffer, 10); setTimeout(term._flushBuffer, 10);
} }
}; };
addonTerminal.__getMessage = (ev: MessageEvent) => { term._getMessage = function(ev) {
const data = JSON.parse(ev.data); var data = JSON.parse(ev.data)
if (data[0] === 'stdout') { if( data[0] == "stdout" ) {
if (buffered) { if (buffered) {
addonTerminal.__pushToBuffer(data[1]); term._pushToBuffer(data[1]);
} else { } else {
addonTerminal.write(data[1]); term.write(data[1]);
} }
} }
}; };
addonTerminal.__sendData = (data: string) => { term._sendData = function(data) {
socket.send(JSON.stringify(['stdin', data])); socket.send(JSON.stringify(['stdin', data]));
}; };
addonTerminal.__setSize = (size: {rows: number, cols: number}) => { term._setSize = function(size) {
socket.send(JSON.stringify(['set_size', size.rows, size.cols])); socket.send(JSON.stringify(['set_size', size.rows, size.cols]));
}; };
socket.addEventListener('message', addonTerminal.__getMessage); socket.addEventListener('message', term._getMessage);
if (bidirectional) { if (bidirectional) {
addonTerminal.on('data', addonTerminal.__sendData); term.on('data', term._sendData);
} }
addonTerminal.on('resize', addonTerminal.__setSize); term.on('resize', term._setSize);
socket.addEventListener('close', () => terminadoDetach(addonTerminal, socket)); socket.addEventListener('close', term.terminadoDetach.bind(term, socket));
socket.addEventListener('error', () => terminadoDetach(addonTerminal, socket)); socket.addEventListener('error', term.terminadoDetach.bind(term, socket));
} };
/** /**
* Detaches the given terminal from the given socket * Detaches the given terminal from the given socket
* *
* @param term The terminal to be detached from the given socket. * @param {Xterm} term - The terminal to be detached from the given socket.
* @param socket The socket from which to detach the current terminal. * @param {WebSocket} socket - The socket from which to detach the current
* terminal.
*/ */
export function terminadoDetach(term: Terminal, socket: WebSocket): void { export function terminadoDetach(term, socket) {
const addonTerminal = <ITerminadoAddonTerminal>term; term.off('data', term._sendData);
addonTerminal.off('data', addonTerminal.__sendData);
socket = (typeof socket === 'undefined') ? addonTerminal.__socket : socket; socket = (typeof socket == 'undefined') ? term.socket : socket;
if (socket) { if (socket) {
socket.removeEventListener('message', addonTerminal.__getMessage); socket.removeEventListener('message', term._getMessage);
} }
delete addonTerminal.__socket; delete term.socket;
} };
export function apply(terminalConstructor: typeof Terminal): void { export function apply(terminalConstructor) {
/** /**
* Attaches the current terminal to the given socket * Attaches the current terminal to the given socket
* *
* @param socket - The socket to attach the current terminal. * @param {WebSocket} socket - The socket to attach the current terminal.
* @param bidirectional - Whether the terminal should send data to the socket as well. * @param {boolean} bidirectional - Whether the terminal should send data
* @param buffered - Whether the rendering of incoming data should happen instantly or at a * to the socket as well.
* maximum frequency of 1 rendering per 10ms. * @param {boolean} buffered - Whether the rendering of incoming data
* should happen instantly or at a maximum
* frequency of 1 rendering per 10ms.
*/ */
(<any>terminalConstructor.prototype).terminadoAttach = function (socket: WebSocket, bidirectional: boolean, buffered: boolean): void { terminalConstructor.prototype.terminadoAttach = function(socket, bidirectional, buffered) {
return terminadoAttach(this, socket, bidirectional, buffered); return terminadoAttach(this, socket, bidirectional, buffered);
}; };
/** /**
* Detaches the current terminal from the given socket. * Detaches the current terminal from the given socket.
* *
* @param socket The socket from which to detach the current terminal. * @param {WebSocket} socket - The socket from which to detach the current
* terminal.
*/ */
(<any>terminalConstructor.prototype).terminadoDetach = function (socket: WebSocket): void { terminalConstructor.prototype.terminadoDetach = function(socket) {
return terminadoDetach(this, socket); return terminadoDetach(this, socket);
}; };
} }
+1 -2
View File
@@ -5,7 +5,6 @@
"rootDir": ".", "rootDir": ".",
"outDir": "../../../lib/addons/terminado/", "outDir": "../../../lib/addons/terminado/",
"sourceMap": true, "sourceMap": true,
"removeComments": true, "removeComments": true
"declaration": true
} }
} }
-5
View File
@@ -1,5 +0,0 @@
{
"name": "xterm.weblinks",
"main": "weblinks.js",
"private": true
}
-11
View File
@@ -1,11 +0,0 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"rootDir": ".",
"outDir": "../../../lib/addons/webLinks/",
"sourceMap": true,
"removeComments": true,
"declaration": true
}
}
-42
View File
@@ -1,42 +0,0 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert, expect } from 'chai';
import * as webLinks from './webLinks';
class MockTerminal {
public regex: RegExp;
public handler: (event: MouseEvent, uri: string) => void;
public options?: any;
public registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: any): number {
this.regex = regex;
this.handler = handler;
this.options = options;
return 0;
}
}
describe('webLinks addon', () => {
describe('apply', () => {
it('should do register the `webLinksInit` method', () => {
webLinks.apply(<any>MockTerminal);
assert.equal(typeof (<any>MockTerminal).prototype.webLinksInit, 'function');
});
});
it('should allow ~ character in URI path', () => {
const term = new MockTerminal();
webLinks.webLinksInit(<any>term);
const row = ' http://foo.com/a~b#c~d?e~f ';
let match = row.match(term.regex);
let uri = match[term.options.matchIndex];
assert.equal(uri, 'http://foo.com/a~b#c~d?e~f');
});
});
-48
View File
@@ -1,48 +0,0 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
/// <reference path="../../../typings/xterm.d.ts"/>
import { Terminal, ILinkMatcherOptions } from 'xterm';
const protocolClause = '(https?:\\/\\/)';
const domainCharacterSet = '[\\da-z\\.-]+';
const negatedDomainCharacterSet = '[^\\da-z\\.-]+';
const domainBodyClause = '(' + domainCharacterSet + ')';
const tldClause = '([a-z\\.]{2,6})';
const ipClause = '((\\d{1,3}\\.){3}\\d{1,3})';
const localHostClause = '(localhost)';
const portClause = '(:\\d{1,5})';
const hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|' + ipClause + '|' + localHostClause + ')' + portClause + '?';
const pathClause = '(\\/[\\/\\w\\.\\-%~]*)*';
const queryStringHashFragmentCharacterSet = '[0-9\\w\\[\\]\\(\\)\\/\\?\\!#@$%&\'*+,:;~\\=\\.\\-]*';
const queryStringClause = '(\\?' + queryStringHashFragmentCharacterSet + ')?';
const hashFragmentClause = '(#' + queryStringHashFragmentCharacterSet + ')?';
const negatedPathCharacterSet = '[^\\/\\w\\.\\-%]+';
const bodyClause = hostClause + pathClause + queryStringClause + hashFragmentClause;
const start = '(?:^|' + negatedDomainCharacterSet + ')(';
const end = ')($|' + negatedPathCharacterSet + ')';
const strictUrlRegex = new RegExp(start + protocolClause + bodyClause + end);
function handleLink(event: MouseEvent, uri: string): void {
window.open(uri, '_blank');
}
/**
* Initialize the web links addon, registering the link matcher.
* @param term The terminal to use web links within.
* @param handler A custom handler to use.
* @param options Custom options to use, matchIndex will always be ignored.
*/
export function webLinksInit(term: Terminal, handler: (event: MouseEvent, uri: string) => void = handleLink, options: ILinkMatcherOptions = {}): void {
options.matchIndex = 1;
term.registerLinkMatcher(strictUrlRegex, handler, options);
}
export function apply(terminalConstructor: typeof Terminal): void {
(<any>terminalConstructor.prototype).webLinksInit = function (handler?: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): void {
webLinksInit(this, handler, options);
};
}
-10
View File
@@ -1,10 +0,0 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { Terminal } from 'xterm';
export interface IWinptyCompatAddonTerminal extends Terminal {
buffer: any;
}
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"name": "xterm.winptycompat", "name": "xterm.winptyCompat",
"main": "winptyCompat.js", "main": "winptyCompat.js",
"private": true "private": true
} }
+1 -2
View File
@@ -5,7 +5,6 @@
"rootDir": ".", "rootDir": ".",
"outDir": "../../../lib/addons/winptyCompat/", "outDir": "../../../lib/addons/winptyCompat/",
"sourceMap": true, "sourceMap": true,
"removeComments": true, "removeComments": true
"declaration": true
} }
} }
+2 -2
View File
@@ -5,14 +5,14 @@
import { assert, expect } from 'chai'; import { assert, expect } from 'chai';
import * as winptyCompat from './winptyCompat'; import * as winptyCompat from './winptyCompat'
class MockTerminal {} class MockTerminal {}
describe('winptyCompat addon', () => { describe('winptyCompat addon', () => {
describe('apply', () => { describe('apply', () => {
it('should do register the `winptyCompatInit` method', () => { it('should do register the `winptyCompatInit` method', () => {
winptyCompat.apply(<any>MockTerminal); winptyCompat.apply(MockTerminal);
assert.equal(typeof (<any>MockTerminal).prototype.winptyCompatInit, 'function'); assert.equal(typeof (<any>MockTerminal).prototype.winptyCompatInit, 'function');
}); });
}); });
+27 -34
View File
@@ -3,43 +3,36 @@
* @license MIT * @license MIT
*/ */
/// <reference path="../../../typings/xterm.d.ts"/> export function winptyCompatInit(terminal): void {
// Don't do anything when the platform is not Windows
import { Terminal } from 'xterm'; const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].indexOf(navigator.platform) >= 0;
import { IWinptyCompatAddonTerminal } from './Interfaces'; if (!isWindows) {
return;
export function winptyCompatInit(terminal: Terminal): void {
const addonTerminal = <IWinptyCompatAddonTerminal>terminal;
// Don't do anything when the platform is not Windows
const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].indexOf(navigator.platform) >= 0;
if (!isWindows) {
return;
}
// Winpty does not support wraparound mode which means that lines will never
// be marked as wrapped. This causes issues for things like copying a line
// retaining the wrapped new line characters or if consumers are listening
// in on the data stream.
//
// The workaround for this is to listen to every incoming line feed and mark
// the line as wrapped if the last character in the previous line is not a
// space. This is certainly not without its problems, but generally on
// Windows when text reaches the end of the terminal it's likely going to be
// wrapped.
addonTerminal.on('linefeed', () => {
const line = addonTerminal.buffer.lines.get(addonTerminal.buffer.ybase + addonTerminal.buffer.y - 1);
const lastChar = line[addonTerminal.cols - 1];
if (lastChar[3] !== 32 /* ' ' */) {
const nextLine = addonTerminal.buffer.lines.get(addonTerminal.buffer.ybase + addonTerminal.buffer.y);
(<any>nextLine).isWrapped = true;
} }
});
// Winpty does not support wraparound mode which means that lines will never
// be marked as wrapped. This causes issues for things like copying a line
// retaining the wrapped new line characters or if consumers are listening
// in on the data stream.
//
// The workaround for this is to listen to every incoming line feed and mark
// the line as wrapped if the last character in the previous line is not a
// space. This is certainly not without its problems, but generally on
// Windows when text reaches the end of the terminal it's likely going to be
// wrapped.
terminal.on('linefeed', () => {
const line = terminal.buffer.lines.get(terminal.buffer.ybase + terminal.buffer.y - 1);
const lastChar = line[terminal.cols - 1];
if (lastChar[3] !== 32 /* ' ' */) {
const nextLine = terminal.buffer.lines.get(terminal.buffer.ybase + terminal.buffer.y);
(<any>nextLine).isWrapped = true;
}
});
} }
export function apply(terminalConstructor: typeof Terminal): void { export function apply(terminalConstructor) {
(<any>terminalConstructor.prototype).winptyCompatInit = function (): void { terminalConstructor.prototype.winptyCompatInit = function(): void {
winptyCompatInit(this); winptyCompatInit(this);
}; };
} }
+1 -2
View File
@@ -5,7 +5,6 @@
"rootDir": ".", "rootDir": ".",
"outDir": "../../../lib/addons/zmodem/", "outDir": "../../../lib/addons/zmodem/",
"sourceMap": true, "sourceMap": true,
"removeComments": true, "removeComments": true
"declaration": true
} }
} }
+2 -2
View File
@@ -5,14 +5,14 @@
import { assert, expect } from 'chai'; import { assert, expect } from 'chai';
import * as zmodem from './zmodem'; import * as zmodem from './zmodem'
class MockTerminal {} class MockTerminal {}
describe('zmodem addon', () => { describe('zmodem addon', () => {
describe('apply', () => { describe('apply', () => {
it('should do register the `zmodemAttach` method and `zmodemBrowser` attribute', () => { it('should do register the `zmodemAttach` method and `zmodemBrowser` attribute', () => {
zmodem.apply(<any>MockTerminal); zmodem.apply(MockTerminal);
assert.equal(typeof (<any>MockTerminal).prototype.zmodemAttach, 'function'); assert.equal(typeof (<any>MockTerminal).prototype.zmodemAttach, 'function');
assert.equal(typeof (<any>MockTerminal).prototype.zmodemBrowser, 'object'); assert.equal(typeof (<any>MockTerminal).prototype.zmodemBrowser, 'object');
}); });
+29 -31
View File
@@ -1,12 +1,3 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
/// <reference path="../../../typings/xterm.d.ts"/>
import { Terminal } from 'xterm';
/** /**
* *
* Allow xterm.js to handle ZMODEM uploads and downloads. * Allow xterm.js to handle ZMODEM uploads and downloads.
@@ -36,40 +27,47 @@ import { Terminal } from 'xterm';
* via `detach()` and a re-`attach()`.) * via `detach()` and a re-`attach()`.)
*/ */
let zmodem; let Zmodem;
export interface IZModemOptions { export function zmodemAttach(term, ws, opts) {
noTerminalWriteOutsideSession?: boolean; if (!opts) opts = {};
}
export function zmodemAttach(term: Terminal, ws: WebSocket, opts: IZModemOptions = {}): void { var senderFunc = function _ws_sender_func(octets) {
const senderFunc = (octets: ArrayLike<number>) => ws.send(new Uint8Array(octets)); ws.send(new Uint8Array(octets));
};
let zsentry; var zsentry;
function _shouldWrite(): boolean { function _shouldWrite() {
return !!zsentry.get_confirmed_session() || !opts.noTerminalWriteOutsideSession; return !!zsentry.get_confirmed_session() || !opts.noTerminalWriteOutsideSession;
} }
zsentry = new zmodem.Sentry({ zsentry = new Zmodem.Sentry( {
to_terminal: (octets: ArrayLike<number>) => { to_terminal: function _to_terminal(octets) {
if (_shouldWrite()) { if (_shouldWrite()) {
term.write( term.write(
String.fromCharCode.apply(String, octets) String.fromCharCode.apply(String, octets)
); );
} }
}, },
sender: senderFunc, sender: senderFunc,
on_retract: () => (<any>term).emit('zmodemRetract'),
on_detect: (detection: any) => (<any>term).emit('zmodemDetect', detection)
});
function handleWSMessage(evt: MessageEvent): void { on_retract: function _on_retract() {
term.emit('zmodemRetract');
},
// In testing with xterm.js’s demo the first message was on_detect: function _on_detect(detection) {
// always text even if the rest were binary. While that term.emit('zmodemDetect', detection);
// may be specific to xterm.js’s demo, ultimately we },
// should reject anything that isn’t binary. } );
function handleWSMessage(evt) {
//In testing with xterm.js’s demo the first message was
//always text even if the rest were binary. While that
//may be specific to xterm.js’s demo, ultimately we
//should reject anything that isn’t binary.
if (typeof evt.data === 'string') { if (typeof evt.data === 'string') {
if (_shouldWrite()) { if (_shouldWrite()) {
term.write(evt.data); term.write(evt.data);
@@ -84,9 +82,9 @@ export function zmodemAttach(term: Terminal, ws: WebSocket, opts: IZModemOptions
ws.addEventListener('message', handleWSMessage); ws.addEventListener('message', handleWSMessage);
} }
export function apply(terminalConstructor: typeof Terminal): void { export function apply(terminalConstructor) {
zmodem = (typeof window === 'object') ? (<any>window).ZModem : {Browser: null}; // Nullify browser for tests Zmodem = (typeof window == 'object') ? (<any>window).ZModem : {Browser: null}; // Nullify browser for tests
(<any>terminalConstructor.prototype).zmodemAttach = zmodemAttach.bind(this, this); terminalConstructor.prototype.zmodemAttach = zmodemAttach.bind(this, this);
(<any>terminalConstructor.prototype).zmodemBrowser = zmodem.Browser; terminalConstructor.prototype.zmodemBrowser = Zmodem.Browser
} }
-270
View File
@@ -1,270 +0,0 @@
/**
* 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 {
// The alt buffer should try to navigate between rows
if (!this._terminal.buffer.hasScrollback) {
return this._resetStartingRow() + this._moveToRequestedRow() + this._moveToRequestedCol();
}
// Only move horizontally for the normal buffer
return this._moveHorizontallyOnly();
}
/**
* 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));
}
private _moveHorizontallyOnly(): string {
let direction = this._horizontalDirection();
return repeat(Math.abs(this._startCol - this._endCol), 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;
}
+3 -9
View File
@@ -3,7 +3,7 @@
* @license MIT * @license MIT
*/ */
import { ITerminal, ISelectionManager } from '../Types'; import { ITerminal, ISelectionManager } from '../Interfaces';
interface IWindow extends Window { interface IWindow extends Window {
clipboardData?: { clipboardData?: {
@@ -100,7 +100,6 @@ export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextA
textarea.focus(); textarea.focus();
// Reset the terminal textarea's styling // Reset the terminal textarea's styling
// Timeout needs to be long enough for click event to be handled.
setTimeout(() => { setTimeout(() => {
textarea.style.position = null; textarea.style.position = null;
textarea.style.width = null; textarea.style.width = null;
@@ -108,7 +107,7 @@ export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextA
textarea.style.left = null; textarea.style.left = null;
textarea.style.top = null; textarea.style.top = null;
textarea.style.zIndex = null; textarea.style.zIndex = null;
}, 200); }, 4);
} }
/** /**
@@ -116,15 +115,10 @@ export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextA
* @param ev The original right click event to be handled. * @param ev The original right click event to be handled.
* @param textarea The terminal's textarea. * @param textarea The terminal's textarea.
* @param selectionManager The terminal's selection manager. * @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, shouldSelectWord: boolean): void { export function rightClickHandler(ev: MouseEvent, textarea: HTMLTextAreaElement, selectionManager: ISelectionManager): void {
moveTextAreaUnderMouseCursor(ev, textarea); moveTextAreaUnderMouseCursor(ev, textarea);
if (shouldSelectWord && !selectionManager.isClickInSelection(ev)) {
selectionManager.selectWordAtCursor(ev);
}
// Get textarea ready to copy from the context menu // Get textarea ready to copy from the context menu
textarea.value = selectionManager.selectionText; textarea.value = selectionManager.selectionText;
textarea.select(); textarea.select();
@@ -13,8 +13,7 @@ export interface IMouseZone {
x2: number; x2: number;
y: number; y: number;
clickCallback: (e: MouseEvent) => any; clickCallback: (e: MouseEvent) => any;
hoverCallback: (e: MouseEvent) => any | undefined; hoverCallback?: (e: MouseEvent) => any;
tooltipCallback: (e: MouseEvent) => any | undefined; tooltipCallback?: (e: MouseEvent) => any;
leaveCallback: () => any | undefined; leaveCallback?: () => any;
willLinkActivate: (e: MouseEvent) => boolean;
} }
+11 -12
View File
@@ -3,8 +3,8 @@
* @license MIT * @license MIT
*/ */
import { ITerminal } from '../Types'; import { IMouseZoneManager, IMouseZone } from './Interfaces';
import { IMouseZoneManager, IMouseZone } from './Types'; import { ITerminal } from '../Interfaces';
const HOVER_DURATION = 500; const HOVER_DURATION = 500;
@@ -151,10 +151,10 @@ export class MouseZoneManager implements IMouseZoneManager {
// components from handling the mouse event. // components from handling the mouse event.
const zone = this._findZoneEventAt(e); const zone = this._findZoneEventAt(e);
if (zone) { if (zone) {
if (zone.willLinkActivate(e)) { // TODO: When link modifier support is added, the event should only be
e.preventDefault(); // cancelled when the modifier is held (see #1021)
e.stopImmediatePropagation(); e.preventDefault();
} e.stopImmediatePropagation();
} }
} }
@@ -169,7 +169,7 @@ export class MouseZoneManager implements IMouseZoneManager {
} }
private _findZoneEventAt(e: MouseEvent): IMouseZone { private _findZoneEventAt(e: MouseEvent): IMouseZone {
const coords = this._terminal.mouseHelper.getCoords(e, this._terminal.screenElement, this._terminal.charMeasure, this._terminal.options.lineHeight, this._terminal.cols, this._terminal.rows); const coords = this._terminal.mouseHelper.getCoords(e, this._terminal.element, this._terminal.charMeasure, this._terminal.options.lineHeight, this._terminal.cols, this._terminal.rows);
if (!coords) { if (!coords) {
return null; return null;
} }
@@ -178,7 +178,7 @@ export class MouseZoneManager implements IMouseZoneManager {
if (zone.y === coords[1] && zone.x1 <= coords[0] && zone.x2 > coords[0]) { if (zone.y === coords[1] && zone.x1 <= coords[0] && zone.x2 > coords[0]) {
return zone; return zone;
} }
} };
return null; return null;
} }
} }
@@ -189,10 +189,9 @@ export class MouseZone implements IMouseZone {
public x2: number, public x2: number,
public y: number, public y: number,
public clickCallback: (e: MouseEvent) => any, public clickCallback: (e: MouseEvent) => any,
public hoverCallback: (e: MouseEvent) => any, public hoverCallback?: (e: MouseEvent) => any,
public tooltipCallback: (e: MouseEvent) => any, public tooltipCallback?: (e: MouseEvent) => any,
public leaveCallback: () => void, public leaveCallback?: () => void
public willLinkActivate: (e: MouseEvent) => boolean
) { ) {
} }
} }
+16 -45
View File
@@ -3,9 +3,10 @@
* @license MIT * @license MIT
*/ */
import { IRenderLayer, IColorSet, IRenderDimensions } from './Types'; import { IRenderLayer, IColorSet, IRenderDimensions } from './Interfaces';
import { CharData, ITerminal, ITerminalOptions } from '../Types'; import { ITerminal, ITerminalOptions } from '../Interfaces';
import { acquireCharAtlas, CHAR_ATLAS_CELL_SPACING } from './CharAtlas'; import { acquireCharAtlas, CHAR_ATLAS_CELL_SPACING } from './CharAtlas';
import { CharData } from '../Types';
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer'; import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer';
export const INVERTED_DEFAULT_COLOR = -1; export const INVERTED_DEFAULT_COLOR = -1;
@@ -24,25 +25,22 @@ export abstract class BaseRenderLayer implements IRenderLayer {
private _charAtlas: HTMLCanvasElement | ImageBitmap; private _charAtlas: HTMLCanvasElement | ImageBitmap;
constructor( constructor(
private _container: HTMLElement, container: HTMLElement,
id: string, id: string,
zIndex: number, zIndex: number,
private _alpha: boolean, private _alpha: boolean,
protected _colors: IColorSet protected _colors: IColorSet
) { ) {
this._canvas = document.createElement('canvas'); this._canvas = document.createElement('canvas');
this._canvas.classList.add(`xterm-${id}-layer`); this._canvas.id = `xterm-${id}-layer`;
this._canvas.style.zIndex = zIndex.toString(); this._canvas.style.zIndex = zIndex.toString();
this._initCanvas(); this._ctx = this._canvas.getContext('2d', {alpha: _alpha});
this._container.appendChild(this._canvas); this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
}
private _initCanvas(): void {
this._ctx = this._canvas.getContext('2d', {alpha: this._alpha});
// Draw the background if this is an opaque layer // Draw the background if this is an opaque layer
if (!this._alpha) { if (!_alpha) {
this.clearAll(); this.clearAll();
} }
container.appendChild(this._canvas);
} }
public onOptionsChanged(terminal: ITerminal): void {} public onOptionsChanged(terminal: ITerminal): void {}
@@ -56,25 +54,6 @@ export abstract class BaseRenderLayer implements IRenderLayer {
this._refreshCharAtlas(terminal, colorSet); 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. * Refreshes the char atlas, aquiring a new one if necessary.
* @param terminal The terminal. * @param terminal The terminal.
@@ -85,7 +64,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
return; return;
} }
this._charAtlas = null; this._charAtlas = null;
const result = acquireCharAtlas(terminal, colorSet, this._scaledCharWidth, this._scaledCharHeight); const result = acquireCharAtlas(terminal, this._colors, this._scaledCharWidth, this._scaledCharHeight);
if (result instanceof HTMLCanvasElement) { if (result instanceof HTMLCanvasElement) {
this._charAtlas = result; this._charAtlas = result;
} else { } else {
@@ -222,7 +201,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
* @param color The color of the character. * @param color The color of the character.
*/ */
protected fillCharTrueColor(terminal: ITerminal, charData: CharData, x: number, y: number): void { protected fillCharTrueColor(terminal: ITerminal, charData: CharData, x: number, y: number): void {
this._ctx.font = this._getFont(terminal, false); this._ctx.font = `${terminal.options.fontSize * window.devicePixelRatio}px ${terminal.options.fontFamily}`;
this._ctx.textBaseline = 'top'; this._ctx.textBaseline = 'top';
this._clipRow(terminal, y); this._clipRow(terminal, y);
this._ctx.fillText( this._ctx.fillText(
@@ -290,7 +269,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
charAtlasCellWidth, charAtlasCellWidth,
this._scaledCharHeight); this._scaledCharHeight);
} else { } else {
this._drawUncachedChar(terminal, char, width, fg, x, y, bold && terminal.options.enableBold, dim); this._drawUncachedChar(terminal, char, width, fg, x, y, bold, dim);
} }
// This draws the atlas (for debugging purposes) // This draws the atlas (for debugging purposes)
// this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); // this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
@@ -310,7 +289,10 @@ export abstract class BaseRenderLayer implements IRenderLayer {
*/ */
private _drawUncachedChar(terminal: ITerminal, char: string, width: number, fg: number, x: number, y: number, bold: boolean, dim: boolean): void { private _drawUncachedChar(terminal: ITerminal, char: string, width: number, fg: number, x: number, y: number, bold: boolean, dim: boolean): void {
this._ctx.save(); this._ctx.save();
this._ctx.font = this._getFont(terminal, bold); this._ctx.font = `${terminal.options.fontSize * window.devicePixelRatio}px ${terminal.options.fontFamily}`;
if (bold && terminal.options.enableBold) {
this._ctx.font = `bold ${this._ctx.font}`;
}
this._ctx.textBaseline = 'top'; this._ctx.textBaseline = 'top';
if (fg === INVERTED_DEFAULT_COLOR) { if (fg === INVERTED_DEFAULT_COLOR) {
@@ -350,16 +332,5 @@ export abstract class BaseRenderLayer implements IRenderLayer {
this._scaledCellHeight); this._scaledCellHeight);
this._ctx.clip(); this._ctx.clip();
} }
/**
* Gets the current font.
* @param terminal The terminal.
* @param isBold If we should use the bold fontWeight.
*/
protected _getFont(terminal: ITerminal, isBold: boolean): string {
const fontWeight = isBold ? terminal.options.fontWeightBold : terminal.options.fontWeight;
return `${fontWeight} ${terminal.options.fontSize * window.devicePixelRatio}px ${terminal.options.fontFamily}`;
}
} }
+121 -35
View File
@@ -3,21 +3,17 @@
* @license MIT * @license MIT
*/ */
import { ITerminal } from '../Types'; import { ITerminal, ITheme } from '../Interfaces';
import { IColorSet } from './Types'; import { IColorSet } from '../renderer/Interfaces';
import { isFirefox } from '../shared/utils/Browser'; import { isFirefox } from '../utils/Browser';
import { generateCharAtlas, ICharAtlasRequest } from '../shared/CharAtlasGenerator';
export const CHAR_ATLAS_CELL_SPACING = 1; export const CHAR_ATLAS_CELL_SPACING = 1;
interface ICharAtlasConfig { interface ICharAtlasConfig {
fontSize: number; fontSize: number;
fontFamily: string; fontFamily: string;
fontWeight: string;
fontWeightBold: string;
scaledCharWidth: number; scaledCharWidth: number;
scaledCharHeight: number; scaledCharHeight: number;
allowTransparency: boolean;
colors: IColorSet; colors: IColorSet;
} }
@@ -67,29 +63,8 @@ export function acquireCharAtlas(terminal: ITerminal, colors: IColorSet, scaledC
} }
} }
const canvasFactory = (width: number, height: number) => {
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
return canvas;
};
const charAtlasConfig: ICharAtlasRequest = {
scaledCharWidth,
scaledCharHeight,
fontSize: terminal.options.fontSize,
fontFamily: terminal.options.fontFamily,
fontWeight: terminal.options.fontWeight,
fontWeightBold: terminal.options.fontWeightBold,
background: colors.background,
foreground: colors.foreground,
ansiColors: colors.ansi,
devicePixelRatio: window.devicePixelRatio,
allowTransparency: terminal.options.allowTransparency
};
const newEntry: ICharAtlasCacheEntry = { const newEntry: ICharAtlasCacheEntry = {
bitmap: generateCharAtlas(window, canvasFactory, charAtlasConfig), bitmap: generator.generate(scaledCharWidth, scaledCharHeight, terminal.options.fontSize, terminal.options.fontFamily, colors.background, colors.foreground, colors.ansi),
config: newConfig, config: newConfig,
ownedBy: [terminal] ownedBy: [terminal]
}; };
@@ -111,9 +86,6 @@ function generateConfig(scaledCharWidth: number, scaledCharHeight: number, termi
scaledCharHeight, scaledCharHeight,
fontFamily: terminal.options.fontFamily, fontFamily: terminal.options.fontFamily,
fontSize: terminal.options.fontSize, fontSize: terminal.options.fontSize,
fontWeight: terminal.options.fontWeight,
fontWeightBold: terminal.options.fontWeightBold,
allowTransparency: terminal.options.allowTransparency,
colors: clonedColors colors: clonedColors
}; };
} }
@@ -126,11 +98,125 @@ function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean {
} }
return a.fontFamily === b.fontFamily && return a.fontFamily === b.fontFamily &&
a.fontSize === b.fontSize && a.fontSize === b.fontSize &&
a.fontWeight === b.fontWeight &&
a.fontWeightBold === b.fontWeightBold &&
a.allowTransparency === b.allowTransparency &&
a.scaledCharWidth === b.scaledCharWidth && a.scaledCharWidth === b.scaledCharWidth &&
a.scaledCharHeight === b.scaledCharHeight && a.scaledCharHeight === b.scaledCharHeight &&
a.colors.foreground === b.colors.foreground && a.colors.foreground === b.colors.foreground &&
a.colors.background === b.colors.background; a.colors.background === b.colors.background;
} }
let generator: CharAtlasGenerator;
/**
* Initializes the char atlas generator.
* @param document The document.
*/
export function initialize(document: Document): void {
if (!generator) {
generator = new CharAtlasGenerator(document);
}
}
class CharAtlasGenerator {
private _canvas: HTMLCanvasElement;
private _ctx: CanvasRenderingContext2D;
constructor(private _document: Document) {
this._canvas = this._document.createElement('canvas');
this._ctx = this._canvas.getContext('2d', {alpha: false});
this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
}
public generate(scaledCharWidth: number, scaledCharHeight: number, fontSize: number, fontFamily: string, background: string, foreground: string, ansiColors: string[]): HTMLCanvasElement | Promise<ImageBitmap> {
const cellWidth = scaledCharWidth + CHAR_ATLAS_CELL_SPACING;
const cellHeight = scaledCharHeight + CHAR_ATLAS_CELL_SPACING;
this._canvas.width = 255 * cellWidth;
this._canvas.height = (/*default+default bold*/2 + /*0-15*/16) * cellHeight;
this._ctx.fillStyle = background;
this._ctx.fillRect(0, 0, this._canvas.width, this._canvas.height);
this._ctx.save();
this._ctx.fillStyle = foreground;
this._ctx.font = `${fontSize * window.devicePixelRatio}px ${fontFamily}`;
this._ctx.textBaseline = 'top';
// Default color
for (let i = 0; i < 256; i++) {
this._ctx.save();
this._ctx.beginPath();
this._ctx.rect(i * cellWidth, 0, cellWidth, cellHeight);
this._ctx.clip();
this._ctx.fillText(String.fromCharCode(i), i * cellWidth, 0);
this._ctx.restore();
}
// Default color bold
this._ctx.save();
this._ctx.font = `bold ${this._ctx.font}`;
for (let i = 0; i < 256; i++) {
this._ctx.save();
this._ctx.beginPath();
this._ctx.rect(i * cellWidth, cellHeight, cellWidth, cellHeight);
this._ctx.clip();
this._ctx.fillText(String.fromCharCode(i), i * cellWidth, cellHeight);
this._ctx.restore();
}
this._ctx.restore();
// Colors 0-15
this._ctx.font = `${fontSize * window.devicePixelRatio}px ${fontFamily}`;
for (let colorIndex = 0; colorIndex < 16; colorIndex++) {
// colors 8-15 are bold
if (colorIndex === 8) {
this._ctx.font = `bold ${this._ctx.font}`;
}
const y = (colorIndex + 2) * cellHeight;
// Draw ascii characters
for (let i = 0; i < 256; i++) {
this._ctx.save();
this._ctx.beginPath();
this._ctx.rect(i * cellWidth, y, cellWidth, cellHeight);
this._ctx.clip();
this._ctx.fillStyle = ansiColors[colorIndex];
this._ctx.fillText(String.fromCharCode(i), i * cellWidth, y);
this._ctx.restore();
}
}
this._ctx.restore();
// Support is patchy for createImageBitmap at the moment, pass a canvas back
// if support is lacking as drawImage works there too. Firefox is also
// included here as ImageBitmap appears both buggy and has horrible
// performance (tested on v55).
if (!('createImageBitmap' in window) || isFirefox) {
// Regenerate canvas and context as they are now owned by the char atlas
const result = this._canvas;
this._canvas = this._document.createElement('canvas');
this._ctx = this._canvas.getContext('2d');
this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
return result;
}
const charAtlasImageData = this._ctx.getImageData(0, 0, this._canvas.width, this._canvas.height);
// Remove the background color from the image so characters may overlap
const r = parseInt(background.substr(1, 2), 16);
const g = parseInt(background.substr(3, 2), 16);
const b = parseInt(background.substr(5, 2), 16);
this._clearColor(charAtlasImageData, r, g, b);
const promise = window.createImageBitmap(charAtlasImageData);
// Clear the rect while the promise is in progress
this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
return promise;
}
private _clearColor(imageData: ImageData, r: number, g: number, b: number): void {
for (let offset = 0; offset < imageData.data.length; offset += 4) {
if (imageData.data[offset] === r &&
imageData.data[offset + 1] === g &&
imageData.data[offset + 2] === b) {
imageData.data[offset + 3] = 0;
}
}
}
}
+19 -3
View File
@@ -3,8 +3,8 @@
* @license MIT * @license MIT
*/ */
import { IColorSet, IColorManager } from './Types'; import { IColorSet, IColorManager } from './Interfaces';
import { ITheme } from 'xterm'; import { ITheme } from '../Interfaces';
const DEFAULT_FOREGROUND = '#ffffff'; const DEFAULT_FOREGROUND = '#ffffff';
const DEFAULT_BACKGROUND = '#000000'; const DEFAULT_BACKGROUND = '#000000';
@@ -86,7 +86,7 @@ export class ColorManager implements IColorManager {
*/ */
public setTheme(theme: ITheme): void { public setTheme(theme: ITheme): void {
this.colors.foreground = theme.foreground || DEFAULT_FOREGROUND; this.colors.foreground = theme.foreground || DEFAULT_FOREGROUND;
this.colors.background = theme.background || DEFAULT_BACKGROUND; this.colors.background = this._validateColor(theme.background, DEFAULT_BACKGROUND);
this.colors.cursor = theme.cursor || DEFAULT_CURSOR; this.colors.cursor = theme.cursor || DEFAULT_CURSOR;
this.colors.cursorAccent = theme.cursorAccent || DEFAULT_CURSOR_ACCENT; this.colors.cursorAccent = theme.cursorAccent || DEFAULT_CURSOR_ACCENT;
this.colors.selection = theme.selection || DEFAULT_SELECTION; this.colors.selection = theme.selection || DEFAULT_SELECTION;
@@ -107,4 +107,20 @@ export class ColorManager implements IColorManager {
this.colors.ansi[14] = theme.brightCyan || DEFAULT_ANSI_COLORS[14]; this.colors.ansi[14] = theme.brightCyan || DEFAULT_ANSI_COLORS[14];
this.colors.ansi[15] = theme.brightWhite || DEFAULT_ANSI_COLORS[15]; 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;
}
} }
+7 -6
View File
@@ -3,13 +3,15 @@
* @license MIT * @license MIT
*/ */
import { IColorSet, IRenderDimensions } from './Interfaces';
import { IBuffer, ICharMeasure, ITerminal, ITerminalOptions } from '../Interfaces';
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer'; import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer';
import { GridCache } from './GridCache'; import { GridCache } from './GridCache';
import { FLAGS, IColorSet, IRenderDimensions } from './Types'; import { FLAGS } from './Types';
import { BaseRenderLayer } from './BaseRenderLayer'; import { BaseRenderLayer } from './BaseRenderLayer';
import { CharData, IBuffer, ICharMeasure, ITerminal, ITerminalOptions } from '../Types'; import { CharData } from '../Types';
interface ICursorState { interface CursorState {
x: number; x: number;
y: number; y: number;
isFocused: boolean; isFocused: boolean;
@@ -23,7 +25,7 @@ interface ICursorState {
const BLINK_INTERVAL = 600; const BLINK_INTERVAL = 600;
export class CursorRenderLayer extends BaseRenderLayer { export class CursorRenderLayer extends BaseRenderLayer {
private _state: ICursorState; private _state: CursorState;
private _cursorRenderers: {[key: string]: (terminal: ITerminal, x: number, y: number, charData: CharData) => void}; private _cursorRenderers: {[key: string]: (terminal: ITerminal, x: number, y: number, charData: CharData) => void};
private _cursorBlinkStateManager: CursorBlinkStateManager; private _cursorBlinkStateManager: CursorBlinkStateManager;
private _isFocused: boolean; private _isFocused: boolean;
@@ -106,10 +108,9 @@ export class CursorRenderLayer extends BaseRenderLayer {
} }
public onGridChanged(terminal: ITerminal, startRow: number, endRow: number): void { public onGridChanged(terminal: ITerminal, startRow: number, endRow: number): void {
// Only render if the animation frame is not active
if (!this._cursorBlinkStateManager || this._cursorBlinkStateManager.isPaused) { if (!this._cursorBlinkStateManager || this._cursorBlinkStateManager.isPaused) {
this._render(terminal, false); this._render(terminal, false);
} else {
this._cursorBlinkStateManager.restartBlinkAnimation(terminal);
} }
} }
+99
View File
@@ -0,0 +1,99 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { ITerminal, ITerminalOptions, ITheme, IEventEmitter } from '../Interfaces';
export interface IRenderer extends IEventEmitter {
dimensions: IRenderDimensions;
colorManager: IColorManager;
setTheme(theme: ITheme): IColorSet;
onWindowResize(devicePixelRatio: number): void;
onResize(cols: number, rows: number, didCharSizeChange: boolean): void;
onCharSizeChanged(): void;
onBlur(): void;
onFocus(): void;
onSelectionChanged(start: [number, number], end: [number, number]): void;
onCursorMove(): void;
onOptionsChanged(): void;
clear(): void;
queueRefresh(start: number, end: number): void;
}
export interface IRenderLayer {
/**
* Called when the terminal loses focus.
*/
onBlur(terminal: ITerminal): void;
/**
* * Called when the terminal gets focus.
*/
onFocus(terminal: ITerminal): void;
/**
* Called when the cursor is moved.
*/
onCursorMove(terminal: ITerminal): void;
/**
* Called when options change.
*/
onOptionsChanged(terminal: ITerminal): void;
/**
* Called when the theme changes.
*/
onThemeChanged(terminal: ITerminal, colorSet: IColorSet): void;
/**
* Called when the data in the grid has changed (or needs to be rendered
* again).
*/
onGridChanged(terminal: ITerminal, startRow: number, endRow: number): void;
/**
* Calls when the selection changes.
*/
onSelectionChanged(terminal: ITerminal, start: [number, number], end: [number, number]): void;
/**
* Resize the render layer.
*/
resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void;
/**
* Clear the state of the render layer.
*/
reset(terminal: ITerminal): void;
}
export interface IColorManager {
colors: IColorSet;
}
export interface IColorSet {
foreground: string;
background: string;
cursor: string;
cursorAccent: string;
selection: string;
ansi: string[];
}
export interface IRenderDimensions {
scaledCharWidth: number;
scaledCharHeight: number;
scaledCellWidth: number;
scaledCellHeight: number;
scaledCharLeft: number;
scaledCharTop: number;
scaledCanvasWidth: number;
scaledCanvasHeight: number;
canvasWidth: number;
canvasHeight: number;
actualCellWidth: number;
actualCellHeight: number;
}
+9 -7
View File
@@ -3,19 +3,21 @@
* @license MIT * @license MIT
*/ */
import { ILinkHoverEvent, ITerminal, ILinkifierAccessor, IBuffer, ICharMeasure, LinkHoverEventTypes } from '../Types'; import { IColorSet, IRenderDimensions } from './Interfaces';
import { IBuffer, ICharMeasure, ITerminal, ILinkifierAccessor } from '../Interfaces';
import { CHAR_DATA_ATTR_INDEX } from '../Buffer'; import { CHAR_DATA_ATTR_INDEX } from '../Buffer';
import { GridCache } from './GridCache'; import { GridCache } from './GridCache';
import { FLAGS, IColorSet, IRenderDimensions } from './Types'; import { FLAGS } from './Types';
import { BaseRenderLayer, INVERTED_DEFAULT_COLOR } from './BaseRenderLayer'; import { BaseRenderLayer, INVERTED_DEFAULT_COLOR } from './BaseRenderLayer';
import { LinkHoverEvent, LinkHoverEventTypes } from '../Types';
export class LinkRenderLayer extends BaseRenderLayer { export class LinkRenderLayer extends BaseRenderLayer {
private _state: ILinkHoverEvent = null; private _state: LinkHoverEvent = null;
constructor(container: HTMLElement, zIndex: number, colors: IColorSet, terminal: ILinkifierAccessor) { constructor(container: HTMLElement, zIndex: number, colors: IColorSet, terminal: ILinkifierAccessor) {
super(container, 'link', zIndex, true, colors); super(container, 'link', zIndex, true, colors);
terminal.linkifier.on(LinkHoverEventTypes.HOVER, (e: ILinkHoverEvent) => this._onLinkHover(e)); terminal.linkifier.on(LinkHoverEventTypes.HOVER, (e: LinkHoverEvent) => this._onLinkHover(e));
terminal.linkifier.on(LinkHoverEventTypes.LEAVE, (e: ILinkHoverEvent) => this._onLinkLeave(e)); terminal.linkifier.on(LinkHoverEventTypes.LEAVE, (e: LinkHoverEvent) => this._onLinkLeave(e));
} }
public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void { public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void {
@@ -35,13 +37,13 @@ export class LinkRenderLayer extends BaseRenderLayer {
} }
} }
private _onLinkHover(e: ILinkHoverEvent): void { private _onLinkHover(e: LinkHoverEvent): void {
this._ctx.fillStyle = this._colors.foreground; this._ctx.fillStyle = this._colors.foreground;
this.fillBottomLineAtCells(e.x, e.y, e.length); this.fillBottomLineAtCells(e.x, e.y, e.length);
this._state = e; this._state = e;
} }
private _onLinkLeave(e: ILinkHoverEvent): void { private _onLinkLeave(e: LinkHoverEvent): void {
this._clearCurrentLink(); this._clearCurrentLink();
} }
} }
+49 -66
View File
@@ -3,28 +3,24 @@
* @license MIT * @license MIT
*/ */
import { ITerminal, ITheme } from '../Interfaces';
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer'; import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer';
import { TextRenderLayer } from './TextRenderLayer'; import { TextRenderLayer } from './TextRenderLayer';
import { SelectionRenderLayer } from './SelectionRenderLayer'; import { SelectionRenderLayer } from './SelectionRenderLayer';
import { CursorRenderLayer } from './CursorRenderLayer'; import { CursorRenderLayer } from './CursorRenderLayer';
import { ColorManager } from './ColorManager'; import { ColorManager } from './ColorManager';
import { BaseRenderLayer } from './BaseRenderLayer'; import { BaseRenderLayer } from './BaseRenderLayer';
import { IRenderLayer, IColorSet, IRenderer, IRenderDimensions } from './Types'; import { IRenderLayer, IColorSet, IRenderer, IRenderDimensions } from './Interfaces';
import { ITerminal } from '../Types';
import { LinkRenderLayer } from './LinkRenderLayer'; import { LinkRenderLayer } from './LinkRenderLayer';
import { EventEmitter } from '../EventEmitter'; import { EventEmitter } from '../EventEmitter';
import { RenderDebouncer } from '../utils/RenderDebouncer';
import { ScreenDprMonitor } from '../utils/ScreenDprMonitor';
import { ITheme } from 'xterm';
export class Renderer extends EventEmitter implements IRenderer { export class Renderer extends EventEmitter implements IRenderer {
private _renderDebouncer: RenderDebouncer; /** A queue of the rows to be refreshed */
private _refreshRowsQueue: {start: number, end: number}[] = [];
private _refreshAnimationFrame = null;
private _renderLayers: IRenderLayer[]; private _renderLayers: IRenderLayer[];
private _devicePixelRatio: number; private _devicePixelRatio: number;
private _screenDprMonitor: ScreenDprMonitor;
private _isPaused: boolean = false;
private _needsFullRefresh: boolean = false;
public colorManager: ColorManager; public colorManager: ColorManager;
public dimensions: IRenderDimensions; public dimensions: IRenderDimensions;
@@ -35,12 +31,11 @@ export class Renderer extends EventEmitter implements IRenderer {
if (theme) { if (theme) {
this.colorManager.setTheme(theme); this.colorManager.setTheme(theme);
} }
this._renderLayers = [ this._renderLayers = [
new TextRenderLayer(this._terminal.screenElement, 0, this.colorManager.colors, this._terminal.options.allowTransparency), new TextRenderLayer(this._terminal.element, 0, this.colorManager.colors),
new SelectionRenderLayer(this._terminal.screenElement, 1, this.colorManager.colors), new SelectionRenderLayer(this._terminal.element, 1, this.colorManager.colors),
new LinkRenderLayer(this._terminal.screenElement, 2, this.colorManager.colors, this._terminal), new LinkRenderLayer(this._terminal.element, 2, this.colorManager.colors, this._terminal),
new CursorRenderLayer(this._terminal.screenElement, 3, this.colorManager.colors) new CursorRenderLayer(this._terminal.element, 3, this.colorManager.colors)
]; ];
this.dimensions = { this.dimensions = {
scaledCharWidth: null, scaledCharWidth: null,
@@ -59,24 +54,6 @@ export class Renderer extends EventEmitter implements IRenderer {
this._devicePixelRatio = window.devicePixelRatio; this._devicePixelRatio = window.devicePixelRatio;
this._updateDimensions(); this._updateDimensions();
this.onOptionsChanged(); this.onOptionsChanged();
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 { public onWindowResize(devicePixelRatio: number): void {
@@ -97,11 +74,7 @@ export class Renderer extends EventEmitter implements IRenderer {
l.reset(this._terminal); l.reset(this._terminal);
}); });
if (this._isPaused) { this._terminal.refresh(0, this._terminal.rows - 1);
this._needsFullRefresh = true;
} else {
this._terminal.refresh(0, this._terminal.rows - 1);
}
return this.colorManager.colors; return this.colorManager.colors;
} }
@@ -114,15 +87,7 @@ export class Renderer extends EventEmitter implements IRenderer {
this._renderLayers.forEach(l => l.resize(this._terminal, this.dimensions, didCharSizeChange)); this._renderLayers.forEach(l => l.resize(this._terminal, this.dimensions, didCharSizeChange));
// Force a refresh // Force a refresh
if (this._isPaused) { this._terminal.refresh(0, this._terminal.rows - 1);
this._needsFullRefresh = true;
} else {
this._terminal.refresh(0, this._terminal.rows - 1);
}
// Resize the screen
this._terminal.screenElement.style.width = `${this.dimensions.canvasWidth}px`;
this._terminal.screenElement.style.height = `${this.dimensions.canvasHeight}px`;
this.emit('resize', { this.emit('resize', {
width: this.dimensions.canvasWidth, width: this.dimensions.canvasWidth,
@@ -135,35 +100,27 @@ export class Renderer extends EventEmitter implements IRenderer {
} }
public onBlur(): void { public onBlur(): void {
this._runOperation(l => l.onBlur(this._terminal)); this._renderLayers.forEach(l => l.onBlur(this._terminal));
} }
public onFocus(): void { public onFocus(): void {
this._runOperation(l => l.onFocus(this._terminal)); this._renderLayers.forEach(l => l.onFocus(this._terminal));
} }
public onSelectionChanged(start: [number, number], end: [number, number]): void { public onSelectionChanged(start: [number, number], end: [number, number]): void {
this._runOperation(l => l.onSelectionChanged(this._terminal, start, end)); this._renderLayers.forEach(l => l.onSelectionChanged(this._terminal, start, end));
} }
public onCursorMove(): void { public onCursorMove(): void {
this._runOperation(l => l.onCursorMove(this._terminal)); this._renderLayers.forEach(l => l.onCursorMove(this._terminal));
} }
public onOptionsChanged(): void { public onOptionsChanged(): void {
this._runOperation(l => l.onOptionsChanged(this._terminal)); this._renderLayers.forEach(l => l.onOptionsChanged(this._terminal));
} }
public clear(): void { public clear(): void {
this._runOperation(l => l.reset(this._terminal)); this._renderLayers.forEach(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));
}
} }
/** /**
@@ -172,19 +129,43 @@ export class Renderer extends EventEmitter implements IRenderer {
* @param {number} start The start row. * @param {number} start The start row.
* @param {number} end The end row. * @param {number} end The end row.
*/ */
public refreshRows(start: number, end: number): void { public queueRefresh(start: number, end: number): void {
if (this._isPaused) { this._refreshRowsQueue.push({ start: start, end: end });
this._needsFullRefresh = true; if (!this._refreshAnimationFrame) {
return; this._refreshAnimationFrame = window.requestAnimationFrame(this._refreshLoop.bind(this));
} }
this._renderDebouncer.refresh(start, end);
} }
/** /**
* Performs the refresh loop callback, calling refresh only if a refresh is * Performs the refresh loop callback, calling refresh only if a refresh is
* necessary before queueing up the next one. * necessary before queueing up the next one.
*/ */
private _renderRows(start: number, end: number): void { private _refreshLoop(): void {
let start;
let end;
if (this._refreshRowsQueue.length > 4) {
// Just do a full refresh when 5+ refreshes are queued
start = 0;
end = this._terminal.rows - 1;
} else {
// Get start and end rows that need refreshing
start = this._refreshRowsQueue[0].start;
end = this._refreshRowsQueue[0].end;
for (let 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._refreshAnimationFrame = null;
// Render
start = Math.max(start, 0);
end = Math.min(end, this._terminal.rows - 1);
this._renderLayers.forEach(l => l.onGridChanged(this._terminal, start, end)); this._renderLayers.forEach(l => l.onGridChanged(this._terminal, start, end));
this._terminal.emit('refresh', {start, end}); this._terminal.emit('refresh', {start, end});
} }
@@ -247,5 +228,7 @@ export class Renderer extends EventEmitter implements IRenderer {
// differ. // differ.
this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._terminal.rows; this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._terminal.rows;
this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._terminal.cols; this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._terminal.cols;
} }
} }
+3 -2
View File
@@ -3,10 +3,11 @@
* @license MIT * @license MIT
*/ */
import { IBuffer, ICharMeasure, ITerminal } from '../Types'; import { IColorSet, IRenderDimensions } from './Interfaces';
import { IBuffer, ICharMeasure, ITerminal } from '../Interfaces';
import { CHAR_DATA_ATTR_INDEX } from '../Buffer'; import { CHAR_DATA_ATTR_INDEX } from '../Buffer';
import { GridCache } from './GridCache'; import { GridCache } from './GridCache';
import { FLAGS, IColorSet, IRenderDimensions } from './Types'; import { FLAGS } from './Types';
import { BaseRenderLayer } from './BaseRenderLayer'; import { BaseRenderLayer } from './BaseRenderLayer';
export class SelectionRenderLayer extends BaseRenderLayer { export class SelectionRenderLayer extends BaseRenderLayer {
+11 -13
View File
@@ -3,10 +3,12 @@
* @license MIT * @license MIT
*/ */
import { IColorSet, IRenderDimensions } from './Interfaces';
import { IBuffer, ICharMeasure, ITerminal } from '../Interfaces';
import { CHAR_DATA_ATTR_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX } from '../Buffer'; import { CHAR_DATA_ATTR_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX } from '../Buffer';
import { FLAGS, IColorSet, IRenderDimensions } from './Types'; import { FLAGS } from './Types';
import { CharData, IBuffer, ICharMeasure, ITerminal } from '../Types';
import { GridCache } from './GridCache'; import { GridCache } from './GridCache';
import { CharData } from '../Types';
import { BaseRenderLayer, INVERTED_DEFAULT_COLOR } from './BaseRenderLayer'; import { BaseRenderLayer, INVERTED_DEFAULT_COLOR } from './BaseRenderLayer';
/** /**
@@ -22,8 +24,8 @@ export class TextRenderLayer extends BaseRenderLayer {
private _characterFont: string; private _characterFont: string;
private _characterOverlapCache: { [key: string]: boolean } = {}; private _characterOverlapCache: { [key: string]: boolean } = {};
constructor(container: HTMLElement, zIndex: number, colors: IColorSet, alpha: boolean) { constructor(container: HTMLElement, zIndex: number, colors: IColorSet) {
super(container, 'text', zIndex, alpha, colors); super(container, 'text', zIndex, false, colors);
this._state = new GridCache<CharData>(); this._state = new GridCache<CharData>();
} }
@@ -31,7 +33,7 @@ export class TextRenderLayer extends BaseRenderLayer {
super.resize(terminal, dim, charSizeChanged); super.resize(terminal, dim, charSizeChanged);
// Clear the character width cache if the font or width has changed // Clear the character width cache if the font or width has changed
const terminalFont = this._getFont(terminal, false); const terminalFont = `${terminal.options.fontSize * window.devicePixelRatio}px ${terminal.options.fontFamily}`;
if (this._characterWidth !== dim.scaledCharWidth || this._characterFont !== terminalFont) { if (this._characterWidth !== dim.scaledCharWidth || this._characterFont !== terminalFont) {
this._characterWidth = dim.scaledCharWidth; this._characterWidth = dim.scaledCharWidth;
this._characterFont = terminalFont; this._characterFont = terminalFont;
@@ -164,7 +166,7 @@ export class TextRenderLayer extends BaseRenderLayer {
this._ctx.save(); this._ctx.save();
if (flags & FLAGS.BOLD) { if (flags & FLAGS.BOLD) {
this._ctx.font = this._getFont(terminal, true); this._ctx.font = `bold ${this._ctx.font}`;
// Convert the FG color to the bold variant // Convert the FG color to the bold variant
if (fg < 8) { if (fg < 8) {
fg += 8; fg += 8;
@@ -190,13 +192,9 @@ 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.
} */
/**
* Whether a character is overlapping to the next cell.
*/
private _isOverlapping(charData: CharData): boolean { private _isOverlapping(charData: CharData): boolean {
// Only single cell characters can be overlapping, rendering issues can // Only single cell characters can be overlapping, rendering issues can
// occur without this check // occur without this check
+2 -98
View File
@@ -3,10 +3,7 @@
* @license MIT * @license MIT
*/ */
import { ITerminal } from '../Types'; /**
import { IEventEmitter, ITheme } from 'xterm';
/**
* Flags used to render terminal text properly. * Flags used to render terminal text properly.
*/ */
export enum FLAGS { export enum FLAGS {
@@ -16,97 +13,4 @@ export enum FLAGS {
INVERSE = 8, INVERSE = 8,
INVISIBLE = 16, INVISIBLE = 16,
DIM = 32 DIM = 32
} };
export interface IRenderer extends IEventEmitter {
dimensions: IRenderDimensions;
colorManager: IColorManager;
setTheme(theme: ITheme): IColorSet;
onWindowResize(devicePixelRatio: number): void;
onResize(cols: number, rows: number, didCharSizeChange: boolean): void;
onCharSizeChanged(): void;
onBlur(): void;
onFocus(): void;
onSelectionChanged(start: [number, number], end: [number, number]): void;
onCursorMove(): void;
onOptionsChanged(): void;
clear(): void;
refreshRows(start: number, end: number): void;
}
export interface IColorManager {
colors: IColorSet;
}
export interface IColorSet {
foreground: string;
background: string;
cursor: string;
cursorAccent: string;
selection: string;
ansi: string[];
}
export interface IRenderDimensions {
scaledCharWidth: number;
scaledCharHeight: number;
scaledCellWidth: number;
scaledCellHeight: number;
scaledCharLeft: number;
scaledCharTop: number;
scaledCanvasWidth: number;
scaledCanvasHeight: number;
canvasWidth: number;
canvasHeight: number;
actualCellWidth: number;
actualCellHeight: number;
}
export interface IRenderLayer {
/**
* Called when the terminal loses focus.
*/
onBlur(terminal: ITerminal): void;
/**
* * Called when the terminal gets focus.
*/
onFocus(terminal: ITerminal): void;
/**
* Called when the cursor is moved.
*/
onCursorMove(terminal: ITerminal): void;
/**
* Called when options change.
*/
onOptionsChanged(terminal: ITerminal): void;
/**
* Called when the theme changes.
*/
onThemeChanged(terminal: ITerminal, colorSet: IColorSet): void;
/**
* Called when the data in the grid has changed (or needs to be rendered
* again).
*/
onGridChanged(terminal: ITerminal, startRow: number, endRow: number): void;
/**
* Calls when the selection changes.
*/
onSelectionChanged(terminal: ITerminal, start: [number, number], end: [number, number]): void;
/**
* Resize the render layer.
*/
resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void;
/**
* Clear the state of the render layer.
*/
reset(terminal: ITerminal): void;
}
-141
View File
@@ -1,141 +0,0 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { FontWeight } from 'xterm';
import { isFirefox } from './utils/Browser';
declare const Promise: any;
export interface IOffscreenCanvas {
width: number;
height: number;
getContext(type: '2d', config?: Canvas2DContextAttributes): CanvasRenderingContext2D;
transferToImageBitmap(): ImageBitmap;
}
export interface ICharAtlasRequest {
scaledCharWidth: number;
scaledCharHeight: number;
fontSize: number;
fontFamily: string;
fontWeight: FontWeight;
fontWeightBold: FontWeight;
background: string;
foreground: string;
ansiColors: string[];
devicePixelRatio: number;
allowTransparency: boolean;
}
export const CHAR_ATLAS_CELL_SPACING = 1;
/**
* Generates a char atlas.
* @param context The window or worker context.
* @param canvasFactory A function to generate a canvas with a width or height.
* @param request The config for the new char atlas.
*/
export function generateCharAtlas(context: Window, canvasFactory: (width: number, height: number) => HTMLCanvasElement | IOffscreenCanvas, request: ICharAtlasRequest): HTMLCanvasElement | Promise<ImageBitmap> {
const cellWidth = request.scaledCharWidth + CHAR_ATLAS_CELL_SPACING;
const cellHeight = request.scaledCharHeight + CHAR_ATLAS_CELL_SPACING;
const canvas = canvasFactory(
/*255 ascii chars*/255 * cellWidth,
(/*default+default bold*/2 + /*0-15*/16) * cellHeight
);
const ctx = canvas.getContext('2d', {alpha: request.allowTransparency});
ctx.fillStyle = request.background;
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.save();
ctx.fillStyle = request.foreground;
ctx.font = getFont(request.fontWeight, request);
ctx.textBaseline = 'top';
// Default color
for (let i = 0; i < 256; i++) {
ctx.save();
ctx.beginPath();
ctx.rect(i * cellWidth, 0, cellWidth, cellHeight);
ctx.clip();
ctx.fillText(String.fromCharCode(i), i * cellWidth, 0);
ctx.restore();
}
// Default color bold
ctx.save();
ctx.font = getFont(request.fontWeightBold, request);
for (let i = 0; i < 256; i++) {
ctx.save();
ctx.beginPath();
ctx.rect(i * cellWidth, cellHeight, cellWidth, cellHeight);
ctx.clip();
ctx.fillText(String.fromCharCode(i), i * cellWidth, cellHeight);
ctx.restore();
}
ctx.restore();
// Colors 0-15
ctx.font = getFont(request.fontWeight, request);
for (let colorIndex = 0; colorIndex < 16; colorIndex++) {
// colors 8-15 are bold
if (colorIndex === 8) {
ctx.font = getFont(request.fontWeightBold, request);
}
const y = (colorIndex + 2) * cellHeight;
// Draw ascii characters
for (let i = 0; i < 256; i++) {
ctx.save();
ctx.beginPath();
ctx.rect(i * cellWidth, y, cellWidth, cellHeight);
ctx.clip();
ctx.fillStyle = request.ansiColors[colorIndex];
ctx.fillText(String.fromCharCode(i), i * cellWidth, y);
ctx.restore();
}
}
ctx.restore();
// Support is patchy for createImageBitmap at the moment, pass a canvas back
// if support is lacking as drawImage works there too. Firefox is also
// included here as ImageBitmap appears both buggy and has horrible
// performance (tested on v55).
if (!('createImageBitmap' in context) || isFirefox) {
// Don't attempt to clear background colors if createImageBitmap is not supported
if (canvas instanceof HTMLCanvasElement) {
// Just return the HTMLCanvas if it's a HTMLCanvasElement
return canvas;
} else {
// Transfer to an ImageBitmap is this is an OffscreenCanvas
return new Promise(r => r(canvas.transferToImageBitmap()));
}
}
const charAtlasImageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
// Remove the background color from the image so characters may overlap
const r = parseInt(request.background.substr(1, 2), 16);
const g = parseInt(request.background.substr(3, 2), 16);
const b = parseInt(request.background.substr(5, 2), 16);
clearColor(charAtlasImageData, r, g, b);
return context.createImageBitmap(charAtlasImageData);
}
/**
* Makes a partiicular rgb color in an ImageData completely transparent.
*/
function clearColor(imageData: ImageData, r: number, g: number, b: number): void {
for (let offset = 0; offset < imageData.data.length; offset += 4) {
if (imageData.data[offset] === r &&
imageData.data[offset + 1] === g &&
imageData.data[offset + 2] === b) {
imageData.data[offset + 3] = 0;
}
}
}
function getFont(fontWeight: FontWeight, request: ICharAtlasRequest): string {
return `${fontWeight} ${request.fontSize * request.devicePixelRatio}px ${request.fontFamily}`;
}
@@ -3,6 +3,8 @@
* @license MIT * @license MIT
*/ */
import { contains } from './Generic';
const isNode = (typeof navigator === 'undefined') ? true : false; const isNode = (typeof navigator === 'undefined') ? true : false;
const userAgent = (isNode) ? 'node' : navigator.userAgent; const userAgent = (isNode) ? 'node' : navigator.userAgent;
const platform = (isNode) ? 'node' : navigator.platform; const platform = (isNode) ? 'node' : navigator.platform;
@@ -18,12 +20,3 @@ export const isIpad = platform === 'iPad';
export const isIphone = platform === 'iPhone'; export const isIphone = platform === 'iPhone';
export const isMSWindows = contains(['Windows', 'Win16', 'Win32', 'WinCE'], platform); export const isMSWindows = contains(['Windows', 'Win16', 'Win32', 'WinCE'], platform);
export const isLinux = platform.indexOf('Linux') >= 0; export const isLinux = platform.indexOf('Linux') >= 0;
/**
* Return if the given array contains the given element
* @param {Array} array The array to search for the given element.
* @param {Object} el The element to look for into the array
*/
function contains(arr: any[], el: any): boolean {
return arr.indexOf(el) >= 0;
}
+1 -1
View File
@@ -4,8 +4,8 @@
*/ */
import jsdom = require('jsdom'); import jsdom = require('jsdom');
import { ICharMeasure, ITerminal } from '../Types';
import { assert } from 'chai'; import { assert } from 'chai';
import { ICharMeasure, ITerminal } from '../Interfaces';
import { CharMeasure } from './CharMeasure'; import { CharMeasure } from './CharMeasure';
describe('CharMeasure', () => { describe('CharMeasure', () => {
+1 -1
View File
@@ -3,8 +3,8 @@
* @license MIT * @license MIT
*/ */
import { ICharMeasure, ITerminal, ITerminalOptions } from '../Types';
import { EventEmitter } from '../EventEmitter'; import { EventEmitter } from '../EventEmitter';
import { ICharMeasure, ITerminal, ITerminalOptions } from '../Interfaces';
/** /**
* Utility class that measures the size of a character. Measurements are done in * Utility class that measures the size of a character. Measurements are done in
+1 -1
View File
@@ -4,7 +4,7 @@
*/ */
import { EventEmitter } from '../EventEmitter'; import { EventEmitter } from '../EventEmitter';
import { ICircularList } from '../Types'; import { ICircularList } from '../Interfaces';
/** /**
* Represents a circular list; a list with a maximum size that wraps around when push is called, * Represents a circular list; a list with a maximum size that wraps around when push is called,
-31
View File
@@ -1,31 +0,0 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IDisposable } from 'xterm';
/**
* Adds a disposabe listener to a node in the DOM, returning the disposable.
* @param type The event type.
* @param handler The handler for the listener.
*/
export function addDisposableListener(
node: Element | Window | Document,
type: string,
handler: (e: any) => void,
useCapture?: boolean
): IDisposable {
node.addEventListener(type, handler, useCapture);
return {
dispose: () => {
if (!handler) {
// Already disposed
return;
}
node.removeEventListener(type, handler, useCapture);
node = null;
handler = null;
}
};
}
+13
View File
@@ -0,0 +1,13 @@
/**
* Copyright (c) 2016 The xterm.js authors. All rights reserved.
* @license MIT
*/
/**
* Return if the given array contains the given element
* @param {Array} array The array to search for the given element.
* @param {Object} el The element to look for into the array
*/
export function contains(arr: any[], el: any): boolean {
return arr.indexOf(el) >= 0;
};
+3 -3
View File
@@ -3,8 +3,8 @@
* @license MIT * @license MIT
*/ */
import { ICharMeasure } from '../Types'; import { ICharMeasure } from '../Interfaces';
import { IRenderer } from '../renderer/Types'; import { IRenderer } from '../renderer/Interfaces';
export class MouseHelper { export class MouseHelper {
constructor(private _renderer: IRenderer) {} constructor(private _renderer: IRenderer) {}
@@ -24,7 +24,7 @@ export class MouseHelper {
while (element) { while (element) {
x -= element.offsetLeft; x -= element.offsetLeft;
y -= element.offsetTop; y -= element.offsetTop;
element = <HTMLElement>element.offsetParent; element = 'offsetParent' in element ? <HTMLElement>element.offsetParent : <HTMLElement>element.parentElement;
} }
element = originalElement; element = originalElement;
while (element && element !== element.ownerDocument.body) { while (element && element !== element.ownerDocument.body) {
-51
View File
@@ -1,51 +0,0 @@
import { ITerminal } from '../Types';
import { IDisposable } from 'xterm';
/**
* Debounces calls to render terminal rows using animation frames.
*/
export class RenderDebouncer implements IDisposable {
private _rowStart: number;
private _rowEnd: number;
private _animationFrame: number = null;
constructor(
private _terminal: ITerminal,
private _callback: (start: number, end: number) => void
) {
}
public dispose(): void {
if (this._animationFrame) {
window.cancelAnimationFrame(this._animationFrame);
this._animationFrame = null;
}
}
public refresh(rowStart?: number, rowEnd?: number): void {
rowStart = rowStart || 0;
rowEnd = rowEnd || this._terminal.rows - 1;
this._rowStart = this._rowStart !== undefined ? Math.min(this._rowStart, rowStart) : rowStart;
this._rowEnd = this._rowEnd !== undefined ? Math.max(this._rowEnd, rowEnd) : rowEnd;
if (this._animationFrame) {
return;
}
this._animationFrame = window.requestAnimationFrame(() => this._innerRefresh());
}
private _innerRefresh(): void {
// Clamp values
this._rowStart = Math.max(this._rowStart, 0);
this._rowEnd = Math.min(this._rowEnd, this._terminal.rows - 1);
// Run render callback
this._callback(this._rowStart, this._rowEnd);
// Reset debouncer
this._rowStart = null;
this._rowEnd = null;
this._animationFrame = null;
}
}
-55
View File
@@ -1,55 +0,0 @@
/**
* 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;
}
}
+10
View File
@@ -0,0 +1,10 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
// Source: https://freesound.org/people/altemark/sounds/45759/
// This sound is released under the Creative Commons Attribution 3.0 Unported
// (CC BY 3.0) license. It was created by 'altemark'. No modifications have been
// made, apart from the conversion to base64.
export const BellSound = 'data:audio/wav;base64,UklGRigBAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQQBAADpAFgCwAMlBZoG/wdmCcoKRAypDQ8PbRDBEQQTOxRtFYcWlBePGIUZXhoiG88bcBz7HHIdzh0WHlMeZx51HmkeUx4WHs8dah0AHXwc3hs9G4saxRnyGBIYGBcQFv8U4RPAEoYRQBACD70NWwwHC6gJOwjWBloF7gOBAhABkf8b/qv8R/ve+Xf4Ife79W/0JfPZ8Z/wde9N7ijtE+wU6xvqM+lb6H7nw+YX5mrlxuQz5Mzje+Ma49fioeKD4nXiYeJy4pHitOL04j/jn+MN5IPkFOWs5U3mDefM55/ogOl36m7rdOyE7abuyu8D8Unyj/Pg9D/2qfcb+Yn6/vuK/Qj/lAAlAg==';
+10 -91
View File
@@ -3,77 +3,13 @@
* @license MIT * @license MIT
*/ */
import { IColorSet, IRenderer, IRenderDimensions, IColorManager } from '../renderer/Types'; import { ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, IListenerType, IInputHandlingTerminal, IViewport, ICircularList, ICompositionHelper, ITheme, ILinkifier, IMouseHelper } from '../Interfaces';
import { LineData, IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, ICircularList, ILinkifier, IMouseHelper, ILinkMatcherOptions, XtermListener } from '../Types'; import { LineData } from '../Types';
import { Buffer } from '../Buffer'; import { Buffer } from '../Buffer';
import * as Browser from '../shared/utils/Browser'; import * as Browser from './Browser';
import { ITheme, IDisposable } from 'xterm'; import { IColorSet, IRenderer, IRenderDimensions, IColorManager } from '../renderer/Interfaces';
export class MockTerminal implements ITerminal { export class MockTerminal implements ITerminal {
static string: any;
getOption(key: any): any {
throw new Error('Method not implemented.');
}
setOption(key: any, value: any): void {
throw new Error('Method not implemented.');
}
blur(): void {
throw new Error('Method not implemented.');
}
focus(): void {
throw new Error('Method not implemented.');
}
resize(columns: number, rows: number): void {
throw new Error('Method not implemented.');
}
writeln(data: string): void {
throw new Error('Method not implemented.');
}
open(parent: HTMLElement): void {
throw new Error('Method not implemented.');
}
attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void {
throw new Error('Method not implemented.');
}
registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => boolean | void, options?: ILinkMatcherOptions): number {
throw new Error('Method not implemented.');
}
deregisterLinkMatcher(matcherId: number): void {
throw new Error('Method not implemented.');
}
hasSelection(): boolean {
throw new Error('Method not implemented.');
}
getSelection(): string {
throw new Error('Method not implemented.');
}
clearSelection(): void {
throw new Error('Method not implemented.');
}
selectAll(): void {
throw new Error('Method not implemented.');
}
destroy(): void {
throw new Error('Method not implemented.');
}
scrollPages(pageCount: number): void {
throw new Error('Method not implemented.');
}
scrollToTop(): void {
throw new Error('Method not implemented.');
}
scrollToBottom(): void {
throw new Error('Method not implemented.');
}
clear(): void {
throw new Error('Method not implemented.');
}
write(data: string): void {
throw new Error('Method not implemented.');
}
send(data: string): void {
throw new Error('Method not implemented.');
}
bracketedPasteMode: boolean; bracketedPasteMode: boolean;
mouseHelper: IMouseHelper; mouseHelper: IMouseHelper;
renderer: IRenderer; renderer: IRenderer;
@@ -81,7 +17,6 @@ export class MockTerminal implements ITerminal {
isFocused: boolean; isFocused: boolean;
options: ITerminalOptions = {}; options: ITerminalOptions = {};
element: HTMLElement; element: HTMLElement;
screenElement: HTMLElement;
rowContainer: HTMLElement; rowContainer: HTMLElement;
selectionContainer: HTMLElement; selectionContainer: HTMLElement;
selectionManager: ISelectionManager; selectionManager: ISelectionManager;
@@ -98,26 +33,18 @@ export class MockTerminal implements ITerminal {
scrollback: number; scrollback: number;
buffers: IBufferSet; buffers: IBufferSet;
buffer: IBuffer; buffer: IBuffer;
viewport: IViewport;
applicationCursor: boolean;
handler(data: string): void { handler(data: string): void {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
on(event: string, callback: () => void): void { on(event: string, callback: () => void): void {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
off(type: string, listener: XtermListener): void { off(type: string, listener: IListenerType): void {
throw new Error('Method not implemented.');
}
addDisposableListener(type: string, handler: XtermListener): IDisposable {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
scrollLines(disp: number, suppressScrollEvent: boolean): void { scrollLines(disp: number, suppressScrollEvent: boolean): void {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
scrollToRow(absoluteRow: number): number {
throw new Error('Method not implemented.');
}
cancel(ev: Event, force?: boolean): void { cancel(ev: Event, force?: boolean): void {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
@@ -257,18 +184,15 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal {
setOption(key: string, value: any): void { setOption(key: string, value: any): void {
this.options[key] = value; this.options[key] = value;
} }
on(type: string, listener: XtermListener): void { on(type: string, listener: IListenerType): void {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
off(type: string, listener: XtermListener): void { off(type: string, listener: IListenerType): void {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
emit(type: string, data?: any): void { emit(type: string, data?: any): void {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
addDisposableListener(type: string, handler: XtermListener): IDisposable {
throw new Error('Method not implemented.');
}
} }
export class MockBuffer implements IBuffer { export class MockBuffer implements IBuffer {
@@ -276,7 +200,6 @@ export class MockBuffer implements IBuffer {
lines: ICircularList<[number, string, number, number][]>; lines: ICircularList<[number, string, number, number][]>;
ydisp: number; ydisp: number;
ybase: number; ybase: number;
hasScrollback: boolean;
y: number; y: number;
x: number; x: number;
tabs: any; tabs: any;
@@ -297,18 +220,15 @@ export class MockBuffer implements IBuffer {
export class MockRenderer implements IRenderer { export class MockRenderer implements IRenderer {
colorManager: IColorManager; colorManager: IColorManager;
on(type: string, listener: XtermListener): void { on(type: string, listener: IListenerType): void {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
off(type: string, listener: XtermListener): void { off(type: string, listener: IListenerType): void {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
emit(type: string, data?: any): void { emit(type: string, data?: any): void {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
addDisposableListener(type: string, handler: XtermListener): IDisposable {
throw new Error('Method not implemented.');
}
dimensions: IRenderDimensions; dimensions: IRenderDimensions;
setTheme(theme: ITheme): IColorSet { return <IColorSet>{}; } setTheme(theme: ITheme): IColorSet { return <IColorSet>{}; }
onResize(cols: number, rows: number, didCharSizeChange: boolean): void {} onResize(cols: number, rows: number, didCharSizeChange: boolean): void {}
@@ -320,11 +240,10 @@ export class MockRenderer implements IRenderer {
onOptionsChanged(): void {} onOptionsChanged(): void {}
onWindowResize(devicePixelRatio: number): void {} onWindowResize(devicePixelRatio: number): void {}
clear(): void {} clear(): void {}
refreshRows(start: number, end: number): void {} queueRefresh(start: number, end: number): void {}
} }
export class MockViewport implements IViewport { export class MockViewport implements IViewport {
scrollBarWidth: number = 0;
onThemeChanged(colors: IColorSet): void { onThemeChanged(colors: IColorSet): void {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
+1 -34
View File
@@ -95,19 +95,9 @@
/* On OS X this is required in order for the scroll bar to appear fully opaque */ /* On OS X this is required in order for the scroll bar to appear fully opaque */
background-color: #000; background-color: #000;
overflow-y: scroll; overflow-y: scroll;
cursor: default;
position: absolute;
right: 0;
left: 0;
top: 0;
bottom: 0;
} }
.xterm .xterm-screen { .xterm canvas {
position: relative;
}
.xterm .xterm-screen canvas {
position: absolute; position: absolute;
left: 0; left: 0;
top: 0; top: 0;
@@ -132,26 +122,3 @@
.xterm:not(.enable-mouse-events) { .xterm:not(.enable-mouse-events) {
cursor: text; cursor: text;
} }
.xterm .xterm-accessibility,
.xterm .xterm-message {
position: absolute;
left: 0;
top: 0;
bottom: 0;
right: 0;
z-index: 100;
color: transparent;
}
.xterm .xterm-accessibility-tree:focus [id^="xterm-active-item-"] {
outline: 1px solid #F80;
}
.xterm .live-region {
position: absolute;
left: -9999px;
width: 1px;
height: 1px;
overflow: hidden;
}
+6 -23
View File
@@ -1,38 +1,27 @@
{ {
"rules": { "rules": {
"array-type": [
true,
"array"
],
"class-name": true, "class-name": true,
"comment-format": [ "comment-format": [
true, true,
"check-space" "check-space"
], ],
"curly": [
true,
"ignore-same-line"
],
"indent": [ "indent": [
true, true,
"spaces" "spaces"
], ],
"interface-name": [
true,
"always-prefix"
],
"interface-over-type-literal": true,
"typedef": [ "typedef": [
true, true,
"call-signature", "call-signature",
"parameter" "parameter"
], ],
"eofline": true, "eofline": true,
"no-duplicate-imports": true,
"no-eval": true, "no-eval": true,
"no-internal-module": true, "no-internal-module": true,
"no-trailing-whitespace": true, "no-trailing-whitespace": true,
"one-variable-per-declaration": true, "one-variable-per-declaration": [
true,
true
],
"no-unsafe-finally": true, "no-unsafe-finally": true,
"no-var-keyword": true, "no-var-keyword": true,
"quotemark": [ "quotemark": [
@@ -59,21 +48,15 @@
], ],
"variable-name": [ "variable-name": [
true, true,
"ban-keywords", "ban-keywords"
"check-format",
"allow-leading-underscore"
], ],
"whitespace": [ "whitespace": [
true, true,
"check-branch", "check-branch",
"check-decl", "check-decl",
"check-module",
"check-operator", "check-operator",
"check-rest-spread",
"check-separator", "check-separator",
"check-type", "check-type"
"check-type-operator",
"check-preblock"
] ]
} }
} }
+24 -98
View File
@@ -8,21 +8,10 @@
*/ */
declare module 'xterm' { declare module 'xterm' {
/**
* A string representing text font weight.
*/
export type FontWeight = 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900';
/** /**
* An object containing start up options for the terminal. * An object containing start up options for the terminal.
*/ */
export interface ITerminalOptions { 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'). * A data uri of the sound to use for the bell (needs bellStyle = 'sound').
*/ */
@@ -55,8 +44,6 @@ declare module 'xterm' {
/** /**
* Whether to enable the rendering of bold text. * Whether to enable the rendering of bold text.
*
* @deprecated Use fontWeight and fontWeightBold instead.
*/ */
enableBold?: boolean; enableBold?: boolean;
@@ -70,16 +57,6 @@ declare module 'xterm' {
*/ */
fontFamily?: string; fontFamily?: string;
/**
* The font weight used to render non-bold text.
*/
fontWeight?: FontWeight;
/**
* The font weight used to render bold text.
*/
fontWeightBold?: FontWeight;
/** /**
* The spacing in whole pixels between characters.. * The spacing in whole pixels between characters..
*/ */
@@ -90,29 +67,11 @@ declare module 'xterm' {
*/ */
lineHeight?: number; lineHeight?: number;
/**
* Whether to treat option as the meta key.
*/
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. * The number of rows in the terminal.
*/ */
rows?: number; rows?: number;
/**
* Whether screen reader support is enabled. When on this will expose
* supporting elements in the DOM to support NVDA on Windows and VoiceOver
* on macOS.
*/
screenReaderMode?: boolean;
/** /**
* The amount of scrollback in the terminal. Scrollback is the amount of rows * The amount of scrollback in the terminal. Scrollback is the amount of rows
* that are retained when lines are scrolled beyond the initial viewport. * that are retained when lines are scrolled beyond the initial viewport.
@@ -189,8 +148,8 @@ declare module 'xterm' {
matchIndex?: number; matchIndex?: number;
/** /**
* A callback that validates whether to create an individual link, pass * A callback that validates an individual link, returning true if valid and
* whether the link is valid to the callback. * false if invalid.
*/ */
validationCallback?: (uri: string, callback: (isValid: boolean) => void) => void; validationCallback?: (uri: string, callback: (isValid: boolean) => void) => void;
@@ -211,40 +170,12 @@ declare module 'xterm' {
* default value is 0. * default value is 0.
*/ */
priority?: number; 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 {
on(type: string, listener: (...args: any[]) => void): void;
off(type: string, listener: (...args: any[]) => void): void;
emit(type: string, data?: any): void;
addDisposableListener(type: string, handler: (...args: any[]) => void): IDisposable;
}
/**
* An object that can be disposed via a dispose function.
*/
export interface IDisposable {
dispose(): void;
}
export interface ILocalizableStrings {
blankLine: string;
promptLabel: string;
tooMuchOutput: string;
} }
/** /**
* The class that represents an xterm.js terminal. * The class that represents an xterm.js terminal.
*/ */
export class Terminal implements IEventEmitter { export class Terminal {
/** /**
* The element containing the terminal. * The element containing the terminal.
*/ */
@@ -265,11 +196,6 @@ declare module 'xterm' {
*/ */
cols: number; cols: number;
/**
* Natural language strings that can be localized.
*/
static strings: ILocalizableStrings;
/** /**
* Creates a new `Terminal` object. * Creates a new `Terminal` object.
* *
@@ -298,7 +224,7 @@ declare module 'xterm' {
* @param type The type of the event. * @param type The type of the event.
* @param listener The listener. * @param listener The listener.
*/ */
on(type: 'data', listener: (...args: any[]) => void): void; on(type: 'data', listener: (data?: string) => void): void;
/** /**
* Registers an event listener. * Registers an event listener.
* @param type The type of the event. * @param type The type of the event.
@@ -349,10 +275,6 @@ declare module 'xterm' {
*/ */
off(type: 'blur' | 'focus' | 'linefeed' | 'selection' | 'data' | 'key' | 'keypress' | 'keydown' | 'refresh' | 'resize' | 'scroll' | 'title' | string, listener: (...args: any[]) => void): void; off(type: 'blur' | 'focus' | 'linefeed' | 'selection' | 'data' | 'key' | 'keypress' | 'keydown' | 'refresh' | 'resize' | 'scroll' | 'title' | string, listener: (...args: any[]) => void): void;
emit(type: string, data?: any): void;
addDisposableListener(type: string, handler: (...args: any[]) => void): IDisposable;
/** /**
* Resizes the terminal. * Resizes the terminal.
* @param x The number of columns to resize to. * @param x The number of columns to resize to.
@@ -395,7 +317,7 @@ declare module 'xterm' {
* @param options Options for the link matcher. * @param options Options for the link matcher.
* @return The ID of the new matcher, this can be used to deregister. * @return The ID of the new matcher, this can be used to deregister.
*/ */
registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): number; registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => boolean | void, options?: ILinkMatcherOptions): number;
/** /**
* (EXPERIMENTAL) Deregisters a link matcher if it has been registered. * (EXPERIMENTAL) Deregisters a link matcher if it has been registered.
@@ -424,6 +346,22 @@ declare module 'xterm' {
*/ */
selectAll(): void; selectAll(): void;
// /**
// * Find the next instance of the term, then scroll to and select it. If it
// * doesn't exist, do nothing.
// * @param term Tne search term.
// * @return Whether a result was found.
// */
// findNext(term: string): boolean;
// /**
// * Find the previous instance of the term, then scroll to and select it. If it
// * doesn't exist, do nothing.
// * @param term Tne search term.
// * @return Whether a result was found.
// */
// findPrevious(term: string): boolean;
/** /**
* Destroys the terminal and detaches it from the DOM. * Destroys the terminal and detaches it from the DOM.
*/ */
@@ -466,12 +404,12 @@ declare module 'xterm' {
* Retrieves an option's value from the terminal. * Retrieves an option's value from the terminal.
* @param key The option key. * @param key The option key.
*/ */
getOption(key: 'bellSound' | 'bellStyle' | 'cursorStyle' | 'fontFamily' | 'fontWeight' | 'fontWeightBold'| 'termName'): string; getOption(key: 'bellSound' | 'bellStyle' | 'cursorStyle' | 'fontFamily' | 'termName'): string;
/** /**
* Retrieves an option's value from the terminal. * Retrieves an option's value from the terminal.
* @param key The option key. * @param key The option key.
*/ */
getOption(key: 'allowTransparency' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'debug' | 'disableStdin' | 'enableBold' | 'macOptionIsMeta' | 'rightClickSelectsWord' | 'popOnBell' | 'screenKeys' | 'useFlowControl' | 'visualBell'): boolean; getOption(key: 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'debug' | 'disableStdin' | 'enableBold' | 'popOnBell' | 'screenKeys' | 'useFlowControl' | 'visualBell'): boolean;
/** /**
* Retrieves an option's value from the terminal. * Retrieves an option's value from the terminal.
* @param key The option key. * @param key The option key.
@@ -499,12 +437,6 @@ declare module 'xterm' {
* @param value The option value. * @param value The option value.
*/ */
setOption(key: 'fontFamily' | 'termName' | 'bellSound', value: string): void; setOption(key: 'fontFamily' | 'termName' | 'bellSound', value: string): void;
/**
* Sets an option on the terminal.
* @param key The option key.
* @param value The option value.
*/
setOption(key: 'fontWeight' | 'fontWeightBold', value: null | 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900'): void;
/** /**
* Sets an option on the terminal. * Sets an option on the terminal.
* @param key The option key. * @param key The option key.
@@ -522,7 +454,7 @@ declare module 'xterm' {
* @param key The option key. * @param key The option key.
* @param value The option value. * @param value The option value.
*/ */
setOption(key: 'allowTransparency' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'debug' | 'disableStdin' | 'enableBold' | 'macOptionIsMeta' | 'popOnBell' | 'rightClickSelectsWord' | 'screenKeys' | 'useFlowControl' | 'visualBell', value: boolean): void; setOption(key: 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'debug' | 'disableStdin' | 'enableBold' | 'popOnBell' | 'screenKeys' | 'useFlowControl' | 'visualBell', value: boolean): void;
/** /**
* Sets an option on the terminal. * Sets an option on the terminal.
* @param key The option key. * @param key The option key.
@@ -547,12 +479,6 @@ declare module 'xterm' {
* @param value The option value. * @param value The option value.
*/ */
setOption(key: 'theme', value: ITheme): void; setOption(key: 'theme', value: ITheme): void;
/**
* Sets an option on the terminal.
* @param key The option key.
* @param value The option value.
*/
setOption(key: 'cols' | 'rows', value: number): void;
/** /**
* Sets an option on the terminal. * Sets an option on the terminal.
* @param key The option key. * @param key The option key.