Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45ed27d329 |
+5
-1
@@ -19,6 +19,10 @@ fixtures/typings-test/*.js
|
||||
# Directories needed for code 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/
|
||||
src/utils/TestUtils.ts
|
||||
src/xterm.js
|
||||
|
||||
# Keep the demo builds out of Git
|
||||
demo/dist/
|
||||
|
||||
+1
-1
@@ -1,6 +1,7 @@
|
||||
language: node_js
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
node_js:
|
||||
- 6
|
||||
before_install:
|
||||
@@ -11,7 +12,6 @@ before_install:
|
||||
- npm install -g npm@5.1.0
|
||||
env:
|
||||
matrix:
|
||||
- NPM_COMMAND=tsc
|
||||
- NPM_COMMAND=lint
|
||||
- NPM_COMMAND=test
|
||||
notifications:
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
List of xterm.js contributors. Updated before every release.
|
||||
|
||||
Adrian Labbé <adrilabbelol@gmail.com>
|
||||
Aleksandr Andrienko <aandrienko@codenvy.com>
|
||||
Aleksandr Andriienko <oandriie@redhat.com>
|
||||
Alessandro Nadalin <alessandro.nadalin@gmail.com>
|
||||
Alexander Olsson <noseglid@gmail.com>
|
||||
Alexandre Petit-Pas <petitpasalexandre@gmail.com>
|
||||
Alexey Kontsevoy <alexey@gravitational.com>
|
||||
Andres Mejia <amejia004@gmail.com>
|
||||
Anish Athalye <me@anishathalye.com>
|
||||
Anthony Lapenna <lapenna.anthony@gmail.com>
|
||||
Antonin Stefanutti <antonin@stefanutti.fr>
|
||||
Antonis Kalipetis <akalipetis@sourcelair.com>
|
||||
Anton Skshidlevsky <meefik@gmail.com>
|
||||
Anton Yurovskykh <anton.yurovskykh@gmail.com>
|
||||
@@ -19,12 +15,10 @@ Austin Robertson <austinrobertson@gmail.com>
|
||||
ayapi <colors.aya@gmail.com>
|
||||
Ben Hall <ben@benhall.me.uk>
|
||||
Benjamin Fischer <benjamin.fischer@rwth-aachen.de>
|
||||
Benjamin Woodruff <github@benjam.info>
|
||||
Bill Church <billchurch@users.noreply.github.com>
|
||||
Bob Reid <bobreid@Bobs-MacBook-Pro.local>
|
||||
bottleofwater <nison.mael+bottleofwater@gmail.com>
|
||||
Brian Mock <brian@mockbrian.com>
|
||||
Bruno Ribeiro <b.m.fernandes.ribeiro@gmail.com>
|
||||
Bruno Ribeito <b.m.fernandes.ribeiro@gmail.com>
|
||||
Carson Anderson <carson@betterservers.com>
|
||||
CHaBou <chabup@delean.fr>
|
||||
@@ -32,9 +26,7 @@ Christian Budde Christensen <budde377@gmail.com>
|
||||
Christof Marti <chrmarti@microsoft.com>
|
||||
Christopher Jeffrey <chjjeffrey@gmail.com>
|
||||
coderaiser <mnemonic.enemy@gmail.com>
|
||||
Damien Tournoud <damien@platform.sh>
|
||||
Dan Brown <jdanbrown@gmail.com>
|
||||
Daniel Griffen <dagriffe@microsoft.com>
|
||||
Daniel Griffen <daniel@griffen.io>
|
||||
Daniel Imms <daimms@microsoft.com>
|
||||
Daniel Risacher <drisacher@gmail.com>
|
||||
@@ -46,7 +38,6 @@ Edgar Andrés Margffoy Tuay <andfoy@gmail.com>
|
||||
Elliot Saba <staticfloat@gmail.com>
|
||||
Exile <exile@chamber.ee>
|
||||
Felipe Gasper <felipe@felipegasper.com>
|
||||
Felix <30559812+felixse@users.noreply.github.com>
|
||||
ficristo <ficristo.work@gmail.com>
|
||||
Gary Ritchie <gary@rstudio.com>
|
||||
hiro-su <h.sugipon@gmail.com>
|
||||
@@ -55,31 +46,24 @@ imoses <ido@twiggle.com>
|
||||
InDieTasten <indietasten@gmail.com>
|
||||
irokas <akasidiari@inf.uth.gr>
|
||||
Jakob Gillich <jakob@gillich.me>
|
||||
Jan Kuri <jkuri88@gmail.com>
|
||||
Jean Bruenn <himself@jeanbruenn.info>
|
||||
Jeremy Danyow <jedanyow@microsoft.com>
|
||||
Jianhui Zhao <jianhuizhao329@gmail.com>
|
||||
Joao Moreno <jomo@microsoft.com>
|
||||
Joao Moreno <mail@joaomoreno.com>
|
||||
Johannes Zellner <johannes@nebulon.de>
|
||||
Jon Masters <jon.masters@sky.com>
|
||||
Jörg Breitbart <jerch@rockborn.de>
|
||||
Justin Luk <jluk@users.noreply.github.com>
|
||||
Justin Mecham <justin@mecham.me>
|
||||
Kirill Merkushev <lanwen@yandex.ru>
|
||||
Krasimir Tsonev <krasimir@outset.ws>
|
||||
Luca <LucaT1@users.noreply.github.com>
|
||||
Lucian Buzzo <lucian.buzzo@gmail.com>
|
||||
Lukas Drgon <lukas.drgon@gmail.com>
|
||||
Maël Nison <nison.mael@gmail.com>
|
||||
Marc Dumais <marc.dumais@ericsson.com>
|
||||
Marek Libra <mlibra@redhat.com>
|
||||
Markus F.X.J. Oberhumer <markus@oberhumer.com>
|
||||
Martin Chloride <i@martincl2.me>
|
||||
Martin Koppehel <martin.koppehel@st.ovgu.de>
|
||||
Martin Wang <jiahaow@ca.ibm.com>
|
||||
Matt Bierner <matb@microsoft.com>
|
||||
Matthew James <matthew.d.james87@gmail.com>
|
||||
Michael Irwin <mikesir87@gmail.com>
|
||||
Mikko Karvonen <mikko.karvonen@arm.com>
|
||||
mofux <t.zilz@mofux.org>
|
||||
@@ -89,15 +73,12 @@ Oleksandr Andriienko <oandriie@redhat.com>
|
||||
Paris Kasidiaris <pariskasidiaris@gmail.com>
|
||||
Paris Kasidiaris <paris@sourcelair.com>
|
||||
Peter Baumgarten <me@peterbaumgarten.com>
|
||||
Philip Olson <philip.olson@protonmail.ch>
|
||||
Rick Baker <rick@ricktbaker.com>
|
||||
runarberg <runar@greenqloud.com>
|
||||
Saad Malik <simfox3@gmail.com>
|
||||
Samuel Williams <samuel.williams@oriontransfer.co.nz>
|
||||
Saswat Das <saswatds@users.noreply.github.com>
|
||||
Saul Costa <saul@codevolve.com>
|
||||
Shuanglei Tao <tsl0922@gmail.com>
|
||||
sitzmar <sitzmar@gmail.com>
|
||||
Steven Silvester <steven.silvester@ieee.org>
|
||||
stuicey <stuicey@users.noreply.github.com>
|
||||
t-amqi <umberos@gmail.com>
|
||||
@@ -109,4 +90,3 @@ Tyler Jewell <tjewell@codenvy.com>
|
||||
Vincent Woo <me@vincentwoo.com>
|
||||
yutaka <shigure@refy.net>
|
||||
YuviPanda <yuvipanda@gmail.com>
|
||||
ZHAO Xudong <zxdong@gmail.com>
|
||||
|
||||
@@ -50,7 +50,3 @@ By contributing code to xterm.js you
|
||||
holder has explicitly granted the right to use it like this,
|
||||
through a compatible open source license or through a direct
|
||||
agreement with you.)
|
||||
|
||||
### Third party dependencies
|
||||
|
||||
We prefer to not include any non-dev third party dependencies in order to keep our code minimal, performant and secure. If you plan on adding a dependency on a third party library it's a good idea to discuss the need in an issue with the maintainers first.
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
FROM node:8
|
||||
FROM node:6
|
||||
MAINTAINER Paris Kasidiaris <paris@sourcelair.com>
|
||||
|
||||
# Set the working directory
|
||||
@@ -16,4 +16,4 @@ RUN npm install
|
||||
COPY . /usr/src/app
|
||||
|
||||
# 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
|
||||
|
||||
@@ -24,7 +24,7 @@ It enables applications to provide fully featured terminals to their users and c
|
||||
First you need to install the module, we ship exclusively through [npm](https://www.npmjs.com/) so you need that installed and then add xterm.js as a dependency by running:
|
||||
|
||||
```
|
||||
npm install 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.
|
||||
@@ -110,7 +110,7 @@ Xterm.js is used in several world-class applications to provide great terminal e
|
||||
- [**Selenoid UI**](https://github.com/aerokube/selenoid-ui): Simple UI for the scallable golang implementation of Selenium Hub named Selenoid. We use XTerm for streaming logs over websockets from docker containers.
|
||||
- [**Portainer**](https://portainer.io): Simple management UI for Docker.
|
||||
- [**SSHy**](https://github.com/stuicey/SSHy): HTML5 Based SSHv2 Web Client with E2E encryption utilising `xterm.js`, SJCL & websockets.
|
||||
- [**JupyterLab**](https://github.com/jupyterlab/jupyterlab): An extensible
|
||||
- [**JupyterLab**](https://github.com/jupyterlab/jupyterlab): An extensible
|
||||
computational environment for Jupyter, supporting interactive data science and scientific computing across all programming languages.
|
||||
- [**Theia**](https://github.com/theia-ide/theia): Theia is a cloud & desktop IDE framework implemented in TypeScript.
|
||||
- [**Opshell**](https://github.com/ricktbaker/opshell) Ops Helper tool to make life easier working with AWS instances across multiple organizations.
|
||||
@@ -118,87 +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.
|
||||
- [**Whack Whack Terminal**](https://github.com/Microsoft/WhackWhackTerminal): Terminal emulator for Visual Studio 2017.
|
||||
- [**VTerm**](https://github.com/vterm/vterm): Extensible terminal emulator based on Electron and React.
|
||||
- [**electerm**](http://electerm.html5beta.com): electerm is a terminal/ssh/sftp client(mac, win, linux) based on electron/node-pty/xterm.
|
||||
- [**Kubebox**](https://github.com/astefanutti/kubebox): Terminal console for Kubernetes clusters.
|
||||
- [**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
|
||||
- [**Fluent Terminal**](https://github.com/felixse/FluentTerminal): A terminal emulator based on UWP and web technologies.
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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 start # Run this in its own terminal
|
||||
npm run watch # Run this in its own terminal
|
||||
npm start
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
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 start # Run this in its own terminal
|
||||
npm run watch # Run this in its own terminal
|
||||
npm start
|
||||
```
|
||||
|
||||
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.*
|
||||
|
||||
@@ -214,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.
|
||||
|
||||
### 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
|
||||
|
||||
If you contribute code to this project, you are implicitly allowing your code to be distributed under the MIT license. You are also implicitly verifying that all code is your original work.
|
||||
|
||||
Copyright (c) 2017-2018, [The xterm.js authors](https://github.com/xtermjs/xterm.js/graphs/contributors) (MIT License)
|
||||
Copyright (c) 2014-2017, SourceLair, Private Company ([www.sourcelair.com](https://www.sourcelair.com/home)) (MIT License)
|
||||
|
||||
Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
|
||||
|
||||
+1
-13
@@ -23,9 +23,6 @@
|
||||
<p>
|
||||
<label><input type="checkbox" id="option-cursor-blink"> cursorBlink</label>
|
||||
</p>
|
||||
<p>
|
||||
<label><input type="checkbox" id="option-mac-option-is-meta"> macOptionIsMeta</label>
|
||||
</p>
|
||||
<p>
|
||||
<label>
|
||||
cursorStyle
|
||||
@@ -64,19 +61,10 @@
|
||||
<label for="rows">Rows</label>
|
||||
<input type="number" id="rows" />
|
||||
</div>
|
||||
<div style="display: inline-block; margin-right: 16px;">
|
||||
<label for="padding">Padding</label>
|
||||
<input type="number" id="padding" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Accessibility</h3>
|
||||
<p>
|
||||
<label><input type="checkbox" id="option-screen-reader-mode"> screenReaderMode</label>
|
||||
</p>
|
||||
</div>
|
||||
<p><strong>Attention:</strong> The demo is a barebones implementation and is designed for 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>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+8
-26
@@ -3,7 +3,6 @@ import * as attach from '../build/addons/attach/attach';
|
||||
import * as fit from '../build/addons/fit/fit';
|
||||
import * as fullscreen from '../build/addons/fullscreen/fullscreen';
|
||||
import * as search from '../build/addons/search/search';
|
||||
import * as webLinks from '../build/addons/webLinks/webLinks';
|
||||
import * as winptyCompat from '../build/addons/winptyCompat/winptyCompat';
|
||||
|
||||
|
||||
@@ -11,7 +10,6 @@ Terminal.applyAddon(attach);
|
||||
Terminal.applyAddon(fit);
|
||||
Terminal.applyAddon(fullscreen);
|
||||
Terminal.applyAddon(search);
|
||||
Terminal.applyAddon(webLinks);
|
||||
Terminal.applyAddon(winptyCompat);
|
||||
|
||||
|
||||
@@ -29,34 +27,28 @@ var terminalContainer = document.getElementById('terminal-container'),
|
||||
optionElements = {
|
||||
cursorBlink: document.querySelector('#option-cursor-blink'),
|
||||
cursorStyle: document.querySelector('#option-cursor-style'),
|
||||
macOptionIsMeta: document.querySelector('#option-mac-option-is-meta'),
|
||||
scrollback: document.querySelector('#option-scrollback'),
|
||||
tabstopwidth: document.querySelector('#option-tabstopwidth'),
|
||||
bellStyle: document.querySelector('#option-bell-style'),
|
||||
screenReaderMode: document.querySelector('#option-screen-reader-mode')
|
||||
bellStyle: document.querySelector('#option-bell-style')
|
||||
},
|
||||
colsElement = document.getElementById('cols'),
|
||||
rowsElement = document.getElementById('rows'),
|
||||
paddingElement = document.getElementById('padding');
|
||||
rowsElement = document.getElementById('rows');
|
||||
|
||||
function setTerminalSize() {
|
||||
var cols = parseInt(colsElement.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';
|
||||
|
||||
terminalContainer.style.width = width;
|
||||
terminalContainer.style.height = height;
|
||||
term.fit();
|
||||
}
|
||||
|
||||
function setPadding() {
|
||||
term.element.style.padding = parseInt(paddingElement.value, 10).toString() + 'px';
|
||||
term.fit();
|
||||
term.resize(cols, rows);
|
||||
}
|
||||
|
||||
colsElement.addEventListener('change', setTerminalSize);
|
||||
rowsElement.addEventListener('change', setTerminalSize);
|
||||
paddingElement.addEventListener('change', setPadding);
|
||||
|
||||
actionElements.findNext.addEventListener('keypress', function (e) {
|
||||
if (e.key === "Enter") {
|
||||
@@ -80,18 +72,12 @@ optionElements.cursorStyle.addEventListener('change', function () {
|
||||
optionElements.bellStyle.addEventListener('change', function () {
|
||||
term.setOption('bellStyle', optionElements.bellStyle.value);
|
||||
});
|
||||
optionElements.macOptionIsMeta.addEventListener('change', function () {
|
||||
term.setOption('macOptionIsMeta', optionElements.macOptionIsMeta.checked);
|
||||
});
|
||||
optionElements.scrollback.addEventListener('change', function () {
|
||||
term.setOption('scrollback', parseInt(optionElements.scrollback.value, 10));
|
||||
});
|
||||
optionElements.tabstopwidth.addEventListener('change', function () {
|
||||
term.setOption('tabStopWidth', parseInt(optionElements.tabstopwidth.value, 10));
|
||||
});
|
||||
optionElements.screenReaderMode.addEventListener('change', function () {
|
||||
term.setOption('screenReaderMode', optionElements.screenReaderMode.checked);
|
||||
});
|
||||
|
||||
createTerminal();
|
||||
|
||||
@@ -101,11 +87,9 @@ function createTerminal() {
|
||||
terminalContainer.removeChild(terminalContainer.children[0]);
|
||||
}
|
||||
term = new Terminal({
|
||||
macOptionIsMeta: optionElements.macOptionIsMeta.enabled,
|
||||
cursorBlink: optionElements.cursorBlink.checked,
|
||||
scrollback: parseInt(optionElements.scrollback.value, 10),
|
||||
tabStopWidth: parseInt(optionElements.tabstopwidth.value, 10),
|
||||
screenReaderMode: optionElements.screenReaderMode.checked
|
||||
tabStopWidth: parseInt(optionElements.tabstopwidth.value, 10)
|
||||
});
|
||||
window.term = term; // Expose `term` to window for debugging purposes
|
||||
term.on('resize', function (size) {
|
||||
@@ -123,7 +107,6 @@ function createTerminal() {
|
||||
|
||||
term.open(terminalContainer);
|
||||
term.winptyCompatInit();
|
||||
term.webLinksInit();
|
||||
term.fit();
|
||||
term.focus();
|
||||
|
||||
@@ -131,7 +114,6 @@ function createTerminal() {
|
||||
setTimeout(function () {
|
||||
colsElement.value = term.cols;
|
||||
rowsElement.value = term.rows;
|
||||
paddingElement.value = 0;
|
||||
|
||||
// Set terminal size again to set the specific dimensions on the demo
|
||||
setTerminalSize();
|
||||
|
||||
@@ -2,16 +2,8 @@ version: "3"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: xtermjs/xterm.js:latest
|
||||
build: .
|
||||
volumes:
|
||||
- ./:/usr/src/app
|
||||
ports:
|
||||
- ${XTERMJS_PORT:3000}:3000
|
||||
command: ["npm", "start"]
|
||||
|
||||
watch:
|
||||
build: .
|
||||
volumes:
|
||||
- ./:/usr/src/app
|
||||
command: ["npm", "run", "watch"]
|
||||
|
||||
@@ -144,11 +144,6 @@ namespace methods_core {
|
||||
const r20: string = t.getOption('bellStyle');
|
||||
const r21: boolean = t.getOption('enableBold');
|
||||
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();
|
||||
@@ -162,14 +157,9 @@ namespace methods_core {
|
||||
t.setOption('debug', true);
|
||||
t.setOption('disableStdin', 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('screenKeys', true);
|
||||
t.setOption('useFlowControl', true);
|
||||
t.setOption('allowTransparency', true);
|
||||
t.setOption('visualBell', true);
|
||||
t.setOption('colors', ['a', 'b']);
|
||||
t.setOption('letterSpacing', 1);
|
||||
@@ -187,8 +177,6 @@ namespace methods_core {
|
||||
t.setOption('lineHeight', 1);
|
||||
t.setOption('fontFamily', 'foo');
|
||||
t.setOption('theme', {background: '#ff0000'});
|
||||
t.setOption('macOptionIsMeta', true);
|
||||
t.setOption('rightClickSelectsWord', false);
|
||||
}
|
||||
}
|
||||
namespace scrolling {
|
||||
|
||||
+46
-42
@@ -18,13 +18,11 @@ const ts = require('gulp-typescript');
|
||||
const util = require('gulp-util');
|
||||
const webpack = require('webpack-stream');
|
||||
|
||||
const buildDir = process.env.BUILD_DIR || 'build';
|
||||
const tsProject = ts.createProject('tsconfig.json');
|
||||
const srcDir = tsProject.config.compilerOptions.rootDir;
|
||||
let buildDir = process.env.BUILD_DIR || 'build';
|
||||
let tsProject = ts.createProject('tsconfig.json');
|
||||
let srcDir = tsProject.config.compilerOptions.rootDir;
|
||||
let outDir = tsProject.config.compilerOptions.outDir;
|
||||
|
||||
const addons = fs.readdirSync(`${__dirname}/src/addons`);
|
||||
|
||||
// Under some environments like TravisCI, this comes out at absolute which can
|
||||
// break the build. This ensures that the outDir is absolute.
|
||||
if (path.normalize(outDir).indexOf(__dirname) !== 0) {
|
||||
@@ -47,19 +45,15 @@ gulp.task('tsc', function () {
|
||||
tsResult.dts.pipe(sourcemaps.write('.', {includeContent: false, sourceRoot: ''})).pipe(gulp.dest(outDir))
|
||||
);
|
||||
|
||||
let addons = ['attach', 'fit', 'fullscreen', 'search', 'terminado', 'winptyCompat', 'zmodem'];
|
||||
let addonStreams = addons.map(function(addon) {
|
||||
fs.emptyDirSync(`${outDir}/addons/${addon}`);
|
||||
|
||||
let tsProjectAddon = ts.createProject(`./src/addons/${addon}/tsconfig.json`);
|
||||
let tsResultAddon = tsProjectAddon.src().pipe(sourcemaps.init()).pipe(tsProjectAddon());
|
||||
let tscAddon = merge(
|
||||
tsResultAddon.js
|
||||
.pipe(sourcemaps.write('.', {includeContent: false, sourceRoot: ''}))
|
||||
.pipe(gulp.dest(`${outDir}/addons/${addon}`)),
|
||||
tsResultAddon.dts
|
||||
.pipe(sourcemaps.write('.', {includeContent: false, sourceRoot: ''}))
|
||||
.pipe(gulp.dest(`${outDir}/addons/${addon}`))
|
||||
)
|
||||
let tscAddon = tsResultAddon.js
|
||||
.pipe(sourcemaps.write('.', {includeContent: false, sourceRoot: ''}))
|
||||
.pipe(gulp.dest(`${outDir}/addons/${addon}`));
|
||||
|
||||
return tscAddon;
|
||||
});
|
||||
@@ -109,29 +103,45 @@ gulp.task('browserify', ['tsc'], function() {
|
||||
});
|
||||
|
||||
gulp.task('browserify-addons', ['tsc'], function() {
|
||||
const bundles = addons.map((addon) => {
|
||||
const addonOptions = {
|
||||
basedir: `${buildDir}/addons/${addon}`,
|
||||
debug: true,
|
||||
entries: [`${outDir}/addons/${addon}/${addon}.js`],
|
||||
standalone: addon,
|
||||
cache: {},
|
||||
packageCache: {}
|
||||
};
|
||||
let searchOptions = {
|
||||
basedir: `${buildDir}/addons/search`,
|
||||
debug: true,
|
||||
entries: [`${outDir}/addons/search/search.js`],
|
||||
cache: {},
|
||||
packageCache: {}
|
||||
};
|
||||
let searchBundle = browserify(searchOptions)
|
||||
.external(path.join(outDir, 'Terminal.js'))
|
||||
.bundle()
|
||||
.pipe(source('./addons/search/search.js'))
|
||||
.pipe(buffer())
|
||||
.pipe(sourcemaps.init({loadMaps: true, sourceRoot: ''}))
|
||||
.pipe(sourcemaps.write('./'))
|
||||
.pipe(gulp.dest(buildDir));
|
||||
|
||||
const addonBundle = browserify(addonOptions)
|
||||
.external(path.join(outDir, 'Terminal.js'))
|
||||
.bundle()
|
||||
.pipe(source(`./addons/${addon}/${addon}.js`))
|
||||
.pipe(buffer())
|
||||
.pipe(sourcemaps.init({loadMaps: true, sourceRoot: ''}))
|
||||
.pipe(sourcemaps.write('./'))
|
||||
.pipe(gulp.dest(buildDir));
|
||||
let winptyCompatOptions = {
|
||||
basedir: `${buildDir}/addons/winptyCompat`,
|
||||
debug: true,
|
||||
entries: [`${outDir}/addons/winptyCompat/winptyCompat.js`],
|
||||
cache: {},
|
||||
packageCache: {}
|
||||
};
|
||||
let winptyCompatBundle = browserify(winptyCompatOptions)
|
||||
.external(path.join(outDir, 'Terminal.js'))
|
||||
.bundle()
|
||||
.pipe(source('./addons/winptyCompat/winptyCompat.js'))
|
||||
.pipe(buffer())
|
||||
.pipe(sourcemaps.init({loadMaps: true, sourceRoot: ''}))
|
||||
.pipe(sourcemaps.write('./'))
|
||||
.pipe(gulp.dest(buildDir));
|
||||
|
||||
return addonBundle;
|
||||
});
|
||||
// Copy all add-ons from outDir to buildDir
|
||||
let copyAddons = gulp.src([
|
||||
// Copy JS addons
|
||||
`${outDir}/addons/**/*`
|
||||
]).pipe(gulp.dest(`${buildDir}/addons`));
|
||||
|
||||
return merge(...bundles);
|
||||
return merge(searchBundle, winptyCompatBundle, copyAddons);
|
||||
});
|
||||
|
||||
gulp.task('instrument-test', function () {
|
||||
@@ -179,11 +189,9 @@ gulp.task('sorcery', ['browserify'], function () {
|
||||
});
|
||||
|
||||
gulp.task('sorcery-addons', ['browserify-addons'], function () {
|
||||
addons.forEach((addon) => {
|
||||
const chain = sorcery.loadSync(`${buildDir}/addons/${addon}/${addon}.js`);
|
||||
chain.apply();
|
||||
chain.writeSync();
|
||||
})
|
||||
var chain = sorcery.loadSync(`${buildDir}/addons/search/search.js`);
|
||||
chain.apply();
|
||||
chain.writeSync();
|
||||
});
|
||||
|
||||
gulp.task('webpack', ['build'], function() {
|
||||
@@ -192,10 +200,6 @@ gulp.task('webpack', ['build'], function() {
|
||||
.pipe(gulp.dest('demo/dist/'));
|
||||
});
|
||||
|
||||
gulp.task('watch', ['webpack'], () => {
|
||||
gulp.watch(['./src/*', './src/**/*'], ['webpack']);
|
||||
});
|
||||
|
||||
/**
|
||||
* Submit coverage results to coveralls.io
|
||||
*/
|
||||
|
||||
Generated
+709
-3719
File diff suppressed because it is too large
Load Diff
+6
-7
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "xterm",
|
||||
"description": "Full xterm terminal, in your browser",
|
||||
"version": "3.3.0",
|
||||
"version": "3.0.1",
|
||||
"ignore": [
|
||||
"demo",
|
||||
"test",
|
||||
@@ -9,7 +9,7 @@
|
||||
],
|
||||
"main": "lib/Terminal.js",
|
||||
"types": "typings/xterm.d.ts",
|
||||
"repository": "https://github.com/xtermjs/xterm.js",
|
||||
"repository": "https://github.com/sourcelair/xterm.js",
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"*.js",
|
||||
@@ -66,8 +66,8 @@
|
||||
"node-pty": "^0.7.2",
|
||||
"nodemon": "1.10.2",
|
||||
"sorcery": "^0.10.0",
|
||||
"tslint": "^5.9.1",
|
||||
"typescript": "~2.7.1",
|
||||
"tslint": "^4.0.2",
|
||||
"typescript": "~2.4.0",
|
||||
"vinyl-buffer": "^1.0.0",
|
||||
"vinyl-source-stream": "^1.1.0",
|
||||
"webpack": "^3.10.0",
|
||||
@@ -75,18 +75,17 @@
|
||||
"zmodem.js": "^0.1.5"
|
||||
},
|
||||
"scripts": {
|
||||
"prestart": "gulp webpack",
|
||||
"start": "node demo/app",
|
||||
"prestart-zmodem": "npm run build",
|
||||
"start-zmodem": "node build/addons/zmodem/demo/app",
|
||||
"lint": "tslint src/*.ts src/**/*.ts",
|
||||
"test": "gulp test",
|
||||
"build:docs": "jsdoc -c jsdoc.json",
|
||||
"tsc": "tsc",
|
||||
"build": "gulp build",
|
||||
"prepublish": "npm run build",
|
||||
"coveralls": "gulp coveralls",
|
||||
"webpack": "gulp webpack",
|
||||
"watch": "gulp watch"
|
||||
"webpack": "gulp webpack"
|
||||
},
|
||||
"dependencies": {}
|
||||
}
|
||||
|
||||
@@ -1,282 +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;
|
||||
|
||||
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');
|
||||
this._rowElements = [];
|
||||
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._disposables.forEach(d => d.dispose());
|
||||
this._disposables.length = 0;
|
||||
this._terminal.element.removeChild(this._accessibilityTreeRoot);
|
||||
this._rowElements.length = 0;
|
||||
}
|
||||
|
||||
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 && 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;
|
||||
}
|
||||
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 += ' ';
|
||||
} else {
|
||||
this._liveRegion.textContent += char;
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-25
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { ITerminal } from './Types';
|
||||
import { ITerminal } from './Interfaces';
|
||||
import { Buffer } from './Buffer';
|
||||
import { CircularList } from './utils/CircularList';
|
||||
import { MockTerminal } from './utils/TestUtils.test';
|
||||
@@ -188,28 +188,4 @@ describe('Buffer', () => {
|
||||
assert.equal(buffer.lines.maxLength, INIT_ROWS / 2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addMarker', () => {
|
||||
it('should adjust a marker line when the buffer is trimmed', () => {
|
||||
terminal.options.scrollback = 0;
|
||||
buffer = new Buffer(terminal, true);
|
||||
buffer.fillViewportRows();
|
||||
const marker = buffer.addMarker(buffer.lines.length - 1);
|
||||
assert.equal(marker.line, buffer.lines.length - 1);
|
||||
buffer.lines.emit('trim', 1);
|
||||
assert.equal(marker.line, buffer.lines.length - 2);
|
||||
});
|
||||
it('should dispose of a marker if it is trimmed off the buffer', () => {
|
||||
terminal.options.scrollback = 0;
|
||||
buffer = new Buffer(terminal, true);
|
||||
buffer.fillViewportRows();
|
||||
assert.equal(buffer.markers.length, 0);
|
||||
const marker = buffer.addMarker(0);
|
||||
assert.equal(marker.isDisposed, false);
|
||||
assert.equal(buffer.markers.length, 1);
|
||||
buffer.lines.emit('trim', 1);
|
||||
assert.equal(marker.isDisposed, true);
|
||||
assert.equal(buffer.markers.length, 0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+15
-57
@@ -3,10 +3,9 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ITerminal, IBuffer } from './Interfaces';
|
||||
import { CircularList } from './utils/CircularList';
|
||||
import { LineData, CharData, ITerminal, IBuffer } from './Types';
|
||||
import { EventEmitter } from './EventEmitter';
|
||||
import { IDisposable, IMarker } from 'xterm';
|
||||
import { LineData, CharData } from './Types';
|
||||
|
||||
export const CHAR_DATA_ATTR_INDEX = 0;
|
||||
export const CHAR_DATA_CHAR_INDEX = 1;
|
||||
@@ -33,7 +32,6 @@ export class Buffer implements IBuffer {
|
||||
public tabs: any;
|
||||
public savedY: number;
|
||||
public savedX: number;
|
||||
public markers: Marker[] = [];
|
||||
|
||||
/**
|
||||
* Create a new Buffer.
|
||||
@@ -123,6 +121,11 @@ export class Buffer implements IBuffer {
|
||||
if (this._terminal.cols < newCols) {
|
||||
const ch: CharData = [this._terminal.defAttr, ' ', 1, 32]; // does xterm use the default attr?
|
||||
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) {
|
||||
this._lines.get(i).push(ch);
|
||||
}
|
||||
@@ -179,13 +182,16 @@ export class Buffer implements IBuffer {
|
||||
}
|
||||
|
||||
// Make sure that the cursor stays on screen
|
||||
this.x = Math.min(this.x, newCols - 1);
|
||||
this.y = Math.min(this.y, newRows - 1);
|
||||
if (this.y >= newRows) {
|
||||
this.y = newRows - 1;
|
||||
}
|
||||
if (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;
|
||||
}
|
||||
@@ -216,10 +222,7 @@ export class Buffer implements IBuffer {
|
||||
// needed here because some chars are 0 characters long (eg. after wide
|
||||
// chars) and some chars are longer than 1 characters long (eg. emojis).
|
||||
let startIndex = startCol;
|
||||
// Only set endCol to the line length when it is null. 0 is a valid column.
|
||||
if (endCol === null) {
|
||||
endCol = line.length;
|
||||
}
|
||||
endCol = endCol || line.length;
|
||||
let endIndex = endCol;
|
||||
|
||||
for (let i = 0; i < line.length; i++) {
|
||||
@@ -306,49 +309,4 @@ export class Buffer implements IBuffer {
|
||||
while (!this.tabs[++x] && x < this._terminal.cols);
|
||||
return x >= this._terminal.cols ? this._terminal.cols - 1 : x < 0 ? 0 : x;
|
||||
}
|
||||
|
||||
public addMarker(y: number): Marker {
|
||||
const marker = new Marker(y);
|
||||
this.markers.push(marker);
|
||||
marker.disposables.push(this._lines.addDisposableListener('trim', amount => {
|
||||
marker.line -= amount;
|
||||
// The marker should be disposed when the line is trimmed from the buffer
|
||||
if (marker.line < 0) {
|
||||
marker.dispose();
|
||||
}
|
||||
}));
|
||||
marker.on('dispose', () => this._removeMarker(marker));
|
||||
return marker;
|
||||
}
|
||||
|
||||
private _removeMarker(marker: Marker): void {
|
||||
// TODO: This could probably be optimized by relying on sort order and trimming the array using .length
|
||||
this.markers.splice(this.markers.indexOf(marker), 1);
|
||||
}
|
||||
}
|
||||
|
||||
export class Marker extends EventEmitter implements IMarker {
|
||||
private static NEXT_ID = 1;
|
||||
|
||||
private _id: number = Marker.NEXT_ID++;
|
||||
public isDisposed: boolean = false;
|
||||
public disposables: IDisposable[] = [];
|
||||
|
||||
public get id(): number { return this._id; }
|
||||
|
||||
constructor(
|
||||
public line: number
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
if (this.isDisposed) {
|
||||
return;
|
||||
}
|
||||
this.isDisposed = true;
|
||||
this.disposables.forEach(d => d.dispose());
|
||||
this.disposables.length = 0;
|
||||
this.emit('dispose');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { ITerminal } from './Types';
|
||||
import { ITerminal } from './Interfaces';
|
||||
import { BufferSet } from './BufferSet';
|
||||
import { Buffer } from './Buffer';
|
||||
import { MockTerminal } from './utils/TestUtils.test';
|
||||
|
||||
+4
-15
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ITerminal, IBufferSet } from './Types';
|
||||
import { ITerminal, IBufferSet } from './Interfaces';
|
||||
import { Buffer } from './Buffer';
|
||||
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
|
||||
*/
|
||||
public activateNormalBuffer(): void {
|
||||
if (this._activeBuffer === this._normal) {
|
||||
return;
|
||||
}
|
||||
// The alt buffer should always be cleared when we switch to the normal
|
||||
// buffer. This frees up memory since the alt buffer should always be new
|
||||
// when activated.
|
||||
this._alt.clear();
|
||||
|
||||
this._activeBuffer = this._normal;
|
||||
this.emit('activate', {
|
||||
activeBuffer: this._normal,
|
||||
inactiveBuffer: this._alt
|
||||
});
|
||||
this.emit('activate', this._normal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the alt Buffer of the BufferSet as its currently active Buffer
|
||||
*/
|
||||
public activateAltBuffer(): void {
|
||||
if (this._activeBuffer === this._alt) {
|
||||
return;
|
||||
}
|
||||
// Since the alt buffer is always cleared when the normal buffer is
|
||||
// activated, we want to fill it when switching to it.
|
||||
this._alt.fillViewportRows();
|
||||
this._activeBuffer = this._alt;
|
||||
this.emit('activate', {
|
||||
activeBuffer: this._alt,
|
||||
inactiveBuffer: this._normal
|
||||
});
|
||||
this.emit('activate', this._alt);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+13
-23
@@ -49,7 +49,7 @@ export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: nu
|
||||
[0x206A, 0x206F], [0x20D0, 0x20EF], [0x302A, 0x302F],
|
||||
[0x3099, 0x309A], [0xA806, 0xA806], [0xA80B, 0xA80B],
|
||||
[0xA825, 0xA826], [0xFB1E, 0xFB1E], [0xFE00, 0xFE0F],
|
||||
[0xFE20, 0xFE23], [0xFEFF, 0xFEFF], [0xFFF9, 0xFFFB]
|
||||
[0xFE20, 0xFE23], [0xFEFF, 0xFEFF], [0xFFF9, 0xFFFB],
|
||||
];
|
||||
const COMBINING_HIGH = [
|
||||
[0x10A01, 0x10A03], [0x10A05, 0x10A06], [0x10A0C, 0x10A0F],
|
||||
@@ -63,33 +63,28 @@ export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: nu
|
||||
let min = 0;
|
||||
let max = data.length - 1;
|
||||
let mid;
|
||||
if (ucs < data[0][0] || ucs > data[max][1]) {
|
||||
if (ucs < data[0][0] || ucs > data[max][1])
|
||||
return false;
|
||||
}
|
||||
while (max >= min) {
|
||||
mid = (min + max) >> 1;
|
||||
if (ucs > data[mid][1]) {
|
||||
if (ucs > data[mid][1])
|
||||
min = mid + 1;
|
||||
} else if (ucs < data[mid][0]) {
|
||||
else if (ucs < data[mid][0])
|
||||
max = mid - 1;
|
||||
} else {
|
||||
else
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function wcwidthBMP(ucs: number): number {
|
||||
// test for 8-bit control characters
|
||||
if (ucs === 0) {
|
||||
if (ucs === 0)
|
||||
return opts.nul;
|
||||
}
|
||||
if (ucs < 32 || (ucs >= 0x7f && ucs < 0xa0)) {
|
||||
if (ucs < 32 || (ucs >= 0x7f && ucs < 0xa0))
|
||||
return opts.control;
|
||||
}
|
||||
// binary search in table of non-spacing characters
|
||||
if (bisearch(ucs, COMBINING_BMP)) {
|
||||
if (bisearch(ucs, COMBINING_BMP))
|
||||
return 0;
|
||||
}
|
||||
// if we arrive here, ucs is not a combining or C0/C1 control character
|
||||
if (isWideBMP(ucs)) {
|
||||
return 2;
|
||||
@@ -111,9 +106,8 @@ export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: nu
|
||||
(ucs >= 0xffe0 && ucs <= 0xffe6)));
|
||||
}
|
||||
function wcwidthHigh(ucs: number): 0 | 1 | 2 {
|
||||
if (bisearch(ucs, COMBINING_HIGH)) {
|
||||
if (bisearch(ucs, COMBINING_HIGH))
|
||||
return 0;
|
||||
}
|
||||
if ((ucs >= 0x20000 && ucs <= 0x2fffd) || (ucs >= 0x30000 && ucs <= 0x3fffd)) {
|
||||
return 2;
|
||||
}
|
||||
@@ -134,9 +128,8 @@ export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: nu
|
||||
for (let i = 0; i < CONTAINERSIZE; ++i) {
|
||||
let num = 0;
|
||||
let pos = CODEPOINTS_PER_ITEM;
|
||||
while (pos--) {
|
||||
while (pos--)
|
||||
num = (num << 2) | wcwidthBMP(CODEPOINTS_PER_ITEM * i + pos);
|
||||
}
|
||||
table[i] = num;
|
||||
}
|
||||
return table;
|
||||
@@ -155,16 +148,13 @@ export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: nu
|
||||
// ==> n = n & 3 e.g. 000000000000000000000000000000XX
|
||||
return function (num: number): number {
|
||||
num = num | 0; // get asm.js like optimization under V8
|
||||
if (num < 32) {
|
||||
if (num < 32)
|
||||
return control | 0;
|
||||
}
|
||||
if (num < 127) {
|
||||
if (num < 127)
|
||||
return 1;
|
||||
}
|
||||
let t = table || init_table();
|
||||
if (num < 65536) {
|
||||
if (num < 65536)
|
||||
return t[num >> 4] >> ((num & 15) << 1) & 3;
|
||||
}
|
||||
// do a full search for high codepoints
|
||||
return wcwidthHigh(num);
|
||||
};
|
||||
|
||||
+3
-3
@@ -3,19 +3,19 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ICharset } from './Types';
|
||||
import { Charset } from './Types';
|
||||
|
||||
/**
|
||||
* The character sets supported by the terminal. These enable several languages
|
||||
* to be represented within the terminal with only 8-bit encoding. See ISO 2022
|
||||
* for a discussion on character sets. Only VT100 character sets are supported.
|
||||
*/
|
||||
export const CHARSETS: { [key: string]: ICharset } = {};
|
||||
export const CHARSETS: { [key: string]: Charset } = {};
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
||||
@@ -17,7 +17,7 @@ describe('CompositionHelper', () => {
|
||||
compositionView = {
|
||||
classList: {
|
||||
add: () => {},
|
||||
remove: () => {}
|
||||
remove: () => {},
|
||||
},
|
||||
getBoundingClientRect: () => {
|
||||
return { width: 0 };
|
||||
|
||||
+64
-64
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ITerminal } from './Types';
|
||||
import { ITerminal } from './Interfaces';
|
||||
|
||||
interface IPosition {
|
||||
start: number;
|
||||
@@ -20,43 +20,43 @@ export class CompositionHelper {
|
||||
* Whether input composition is currently happening, eg. via a mobile keyboard, speech input or
|
||||
* IME. This variable determines whether the compositionText should be displayed on the UI.
|
||||
*/
|
||||
private _isComposing: boolean;
|
||||
private isComposing: boolean;
|
||||
|
||||
/**
|
||||
* The position within the input textarea's value of the current composition.
|
||||
*/
|
||||
private _compositionPosition: IPosition;
|
||||
private compositionPosition: IPosition;
|
||||
|
||||
/**
|
||||
* Whether a composition is in the process of being sent, setting this to false will cancel any
|
||||
* in-progress composition.
|
||||
*/
|
||||
private _isSendingComposition: boolean;
|
||||
private isSendingComposition: boolean;
|
||||
|
||||
/**
|
||||
* Creates a new CompositionHelper.
|
||||
* @param _textarea The textarea that xterm uses for input.
|
||||
* @param _compositionView The element to display the in-progress composition in.
|
||||
* @param _terminal The Terminal to forward the finished composition to.
|
||||
* @param textarea The textarea that xterm uses for input.
|
||||
* @param compositionView The element to display the in-progress composition in.
|
||||
* @param terminal The Terminal to forward the finished composition to.
|
||||
*/
|
||||
constructor(
|
||||
private _textarea: HTMLTextAreaElement,
|
||||
private _compositionView: HTMLElement,
|
||||
private _terminal: ITerminal
|
||||
private textarea: HTMLTextAreaElement,
|
||||
private compositionView: HTMLElement,
|
||||
private terminal: ITerminal
|
||||
) {
|
||||
this._isComposing = false;
|
||||
this._isSendingComposition = false;
|
||||
this._compositionPosition = { start: null, end: null };
|
||||
this.isComposing = false;
|
||||
this.isSendingComposition = false;
|
||||
this.compositionPosition = { start: null, end: null };
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the compositionstart event, activating the composition view.
|
||||
*/
|
||||
public compositionstart(): void {
|
||||
this._isComposing = true;
|
||||
this._compositionPosition.start = this._textarea.value.length;
|
||||
this._compositionView.textContent = '';
|
||||
this._compositionView.classList.add('active');
|
||||
this.isComposing = true;
|
||||
this.compositionPosition.start = this.textarea.value.length;
|
||||
this.compositionView.textContent = '';
|
||||
this.compositionView.classList.add('active');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -64,10 +64,10 @@ export class CompositionHelper {
|
||||
* @param {CompositionEvent} ev The event.
|
||||
*/
|
||||
public compositionupdate(ev: CompositionEvent): void {
|
||||
this._compositionView.textContent = ev.data;
|
||||
this.compositionView.textContent = ev.data;
|
||||
this.updateCompositionElements();
|
||||
setTimeout(() => {
|
||||
this._compositionPosition.end = this._textarea.value.length;
|
||||
this.compositionPosition.end = this.textarea.value.length;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ export class CompositionHelper {
|
||||
* the handler.
|
||||
*/
|
||||
public compositionend(): void {
|
||||
this._finalizeComposition(true);
|
||||
this.finalizeComposition(true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -85,7 +85,7 @@ export class CompositionHelper {
|
||||
* @return Whether the Terminal should continue processing the keydown event.
|
||||
*/
|
||||
public keydown(ev: KeyboardEvent): boolean {
|
||||
if (this._isComposing || this._isSendingComposition) {
|
||||
if (this.isComposing || this.isSendingComposition) {
|
||||
if (ev.keyCode === 229) {
|
||||
// Continue composing if the keyCode is the "composition character"
|
||||
return false;
|
||||
@@ -95,14 +95,14 @@ export class CompositionHelper {
|
||||
} else {
|
||||
// Finish composition immediately. This is mainly here for the case where enter is
|
||||
// pressed and the handler needs to be triggered before the command is executed.
|
||||
this._finalizeComposition(false);
|
||||
this.finalizeComposition(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (ev.keyCode === 229) {
|
||||
// If the "composition character" is used but gets to this point it means a non-composition
|
||||
// character (eg. numbers and punctuation) was pressed when the IME was active.
|
||||
this._handleAnyTextareaChanges();
|
||||
this.handleAnyTextareaChanges();
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -117,22 +117,22 @@ export class CompositionHelper {
|
||||
* compositionend event is triggered, such as enter, so that the composition is send before
|
||||
* the command is executed.
|
||||
*/
|
||||
private _finalizeComposition(waitForPropogation: boolean): void {
|
||||
this._compositionView.classList.remove('active');
|
||||
this._isComposing = false;
|
||||
this._clearTextareaPosition();
|
||||
private finalizeComposition(waitForPropogation: boolean): void {
|
||||
this.compositionView.classList.remove('active');
|
||||
this.isComposing = false;
|
||||
this.clearTextareaPosition();
|
||||
|
||||
if (!waitForPropogation) {
|
||||
// Cancel any delayed composition send requests and send the input immediately.
|
||||
this._isSendingComposition = false;
|
||||
const input = this._textarea.value.substring(this._compositionPosition.start, this._compositionPosition.end);
|
||||
this._terminal.handler(input);
|
||||
this.isSendingComposition = false;
|
||||
const input = this.textarea.value.substring(this.compositionPosition.start, this.compositionPosition.end);
|
||||
this.terminal.handler(input);
|
||||
} else {
|
||||
// Make a deep copy of the composition position here as a new compositionstart event may
|
||||
// fire before the setTimeout executes.
|
||||
const currentCompositionPosition = {
|
||||
start: this._compositionPosition.start,
|
||||
end: this._compositionPosition.end
|
||||
start: this.compositionPosition.start,
|
||||
end: this.compositionPosition.end,
|
||||
};
|
||||
|
||||
// Since composition* events happen before the changes take place in the textarea on most
|
||||
@@ -143,22 +143,22 @@ export class CompositionHelper {
|
||||
// - The last compositionupdate event's data property does not always accurately describe
|
||||
// the character, a counter example being Korean where an ending consonsant can move to
|
||||
// the following character if the following input is a vowel.
|
||||
this._isSendingComposition = true;
|
||||
this.isSendingComposition = true;
|
||||
setTimeout(() => {
|
||||
// Ensure that the input has not already been sent
|
||||
if (this._isSendingComposition) {
|
||||
this._isSendingComposition = false;
|
||||
if (this.isSendingComposition) {
|
||||
this.isSendingComposition = false;
|
||||
let input;
|
||||
if (this._isComposing) {
|
||||
if (this.isComposing) {
|
||||
// Use the end position to get the string if a new composition has started.
|
||||
input = this._textarea.value.substring(currentCompositionPosition.start, currentCompositionPosition.end);
|
||||
input = this.textarea.value.substring(currentCompositionPosition.start, currentCompositionPosition.end);
|
||||
} else {
|
||||
// Don't use the end position here in order to pick up any characters after the
|
||||
// composition has finished, for example when typing a non-composition character
|
||||
// (eg. 2) after a composition character.
|
||||
input = this._textarea.value.substring(currentCompositionPosition.start);
|
||||
input = this.textarea.value.substring(currentCompositionPosition.start);
|
||||
}
|
||||
this._terminal.handler(input);
|
||||
this.terminal.handler(input);
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
@@ -170,15 +170,15 @@ export class CompositionHelper {
|
||||
* character" (229) is triggered, in order to allow non-composition text to be entered when an
|
||||
* IME is active.
|
||||
*/
|
||||
private _handleAnyTextareaChanges(): void {
|
||||
const oldValue = this._textarea.value;
|
||||
private handleAnyTextareaChanges(): void {
|
||||
const oldValue = this.textarea.value;
|
||||
setTimeout(() => {
|
||||
// Ignore if a composition has started since the timeout
|
||||
if (!this._isComposing) {
|
||||
const newValue = this._textarea.value;
|
||||
if (!this.isComposing) {
|
||||
const newValue = this.textarea.value;
|
||||
const diff = newValue.replace(oldValue, '');
|
||||
if (diff.length > 0) {
|
||||
this._terminal.handler(diff);
|
||||
this.terminal.handler(diff);
|
||||
}
|
||||
}
|
||||
}, 0);
|
||||
@@ -191,40 +191,40 @@ export class CompositionHelper {
|
||||
* necessary as the IME events across browsers are not consistently triggered.
|
||||
*/
|
||||
public updateCompositionElements(dontRecurse?: boolean): void {
|
||||
if (!this._isComposing) {
|
||||
if (!this.isComposing) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._terminal.buffer.isCursorInViewport) {
|
||||
const cellHeight = Math.ceil(this._terminal.charMeasure.height * this._terminal.options.lineHeight);
|
||||
const cursorTop = this._terminal.buffer.y * cellHeight;
|
||||
const cursorLeft = this._terminal.buffer.x * this._terminal.charMeasure.width;
|
||||
if (this.terminal.buffer.isCursorInViewport) {
|
||||
const cellHeight = Math.ceil(this.terminal.charMeasure.height * this.terminal.options.lineHeight);
|
||||
const cursorTop = this.terminal.buffer.y * cellHeight;
|
||||
const cursorLeft = this.terminal.buffer.x * this.terminal.charMeasure.width;
|
||||
|
||||
this._compositionView.style.left = cursorLeft + 'px';
|
||||
this._compositionView.style.top = cursorTop + 'px';
|
||||
this._compositionView.style.height = cellHeight + 'px';
|
||||
this._compositionView.style.lineHeight = cellHeight + 'px';
|
||||
this.compositionView.style.left = cursorLeft + 'px';
|
||||
this.compositionView.style.top = cursorTop + 'px';
|
||||
this.compositionView.style.height = cellHeight + 'px';
|
||||
this.compositionView.style.lineHeight = cellHeight + 'px';
|
||||
// Sync the textarea to the exact position of the composition view so the IME knows where the
|
||||
// text is.
|
||||
const compositionViewBounds = this._compositionView.getBoundingClientRect();
|
||||
this._textarea.style.left = cursorLeft + 'px';
|
||||
this._textarea.style.top = cursorTop + 'px';
|
||||
this._textarea.style.width = compositionViewBounds.width + 'px';
|
||||
this._textarea.style.height = compositionViewBounds.height + 'px';
|
||||
this._textarea.style.lineHeight = compositionViewBounds.height + 'px';
|
||||
const compositionViewBounds = this.compositionView.getBoundingClientRect();
|
||||
this.textarea.style.left = cursorLeft + 'px';
|
||||
this.textarea.style.top = cursorTop + 'px';
|
||||
this.textarea.style.width = compositionViewBounds.width + 'px';
|
||||
this.textarea.style.height = compositionViewBounds.height + 'px';
|
||||
this.textarea.style.lineHeight = compositionViewBounds.height + 'px';
|
||||
}
|
||||
|
||||
if (!dontRecurse) {
|
||||
setTimeout(() => this.updateCompositionElements(true), 0);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Clears the textarea's position so that the cursor does not blink on IE.
|
||||
* @private
|
||||
*/
|
||||
private _clearTextareaPosition(): void {
|
||||
this._textarea.style.left = '';
|
||||
this._textarea.style.top = '';
|
||||
}
|
||||
private clearTextareaPosition(): void {
|
||||
this.textarea.style.left = '';
|
||||
this.textarea.style.top = '';
|
||||
};
|
||||
}
|
||||
|
||||
@@ -76,4 +76,4 @@ export namespace C0 {
|
||||
export const SP = '\x20';
|
||||
/** Delete (Caret = ^?) */
|
||||
export const DEL = '\x7f';
|
||||
}
|
||||
};
|
||||
|
||||
@@ -13,6 +13,18 @@ describe('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', () => {
|
||||
it('should emit events to listeners', () => {
|
||||
let count1 = 0;
|
||||
|
||||
+16
-26
@@ -3,11 +3,10 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { XtermListener } from './Types';
|
||||
import { IEventEmitter, IDisposable } from 'xterm';
|
||||
import { IEventEmitter, IListenerType } from './Interfaces';
|
||||
|
||||
export class EventEmitter implements IEventEmitter {
|
||||
private _events: {[type: string]: XtermListener[]};
|
||||
private _events: {[type: string]: IListenerType[]};
|
||||
|
||||
constructor() {
|
||||
// Restore the previous events if available, this will happen if the
|
||||
@@ -15,31 +14,12 @@ export class EventEmitter implements IEventEmitter {
|
||||
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].push(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
public off(type: string, listener: IListenerType): void {
|
||||
if (!this._events[type]) {
|
||||
return;
|
||||
}
|
||||
@@ -48,7 +28,7 @@ export class EventEmitter implements IEventEmitter {
|
||||
let i = obj.length;
|
||||
|
||||
while (i--) {
|
||||
if (obj[i] === listener) {
|
||||
if (obj[i] === listener || obj[i].listener === listener) {
|
||||
obj.splice(i, 1);
|
||||
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 {
|
||||
if (!this._events[type]) {
|
||||
return;
|
||||
@@ -71,7 +61,7 @@ export class EventEmitter implements IEventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
public listeners(type: string): XtermListener[] {
|
||||
public listeners(type: string): IListenerType[] {
|
||||
return this._events[type] || [];
|
||||
}
|
||||
|
||||
|
||||
+11
-17
@@ -4,9 +4,10 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { CharData, IInputHandler, IInputHandlingTerminal } from './Types';
|
||||
import { IInputHandler, ITerminal, IInputHandlingTerminal } from './Interfaces';
|
||||
import { C0 } from './EscapeSequences';
|
||||
import { DEFAULT_CHARSET } from './Charsets';
|
||||
import { CharData } from './Types';
|
||||
import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX } from './Buffer';
|
||||
import { FLAGS } from './renderer/Types';
|
||||
import { wcwidth } from './CharWidth';
|
||||
@@ -25,21 +26,17 @@ export class InputHandler implements IInputHandler {
|
||||
if (char >= ' ') {
|
||||
// calculate print space
|
||||
// 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]) {
|
||||
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;
|
||||
|
||||
// insert combining char in last cell
|
||||
// FIXME: needs handling after cursor jumps
|
||||
if (!chWidth && this._terminal.buffer.x) {
|
||||
if (!ch_width && this._terminal.buffer.x) {
|
||||
// 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][CHAR_DATA_WIDTH_INDEX]) {
|
||||
@@ -59,7 +56,7 @@ export class InputHandler implements IInputHandler {
|
||||
|
||||
// goto next line if ch would overflow
|
||||
// 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
|
||||
if (this._terminal.wraparoundMode) {
|
||||
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;
|
||||
}
|
||||
} else {
|
||||
if (chWidth === 2) { // FIXME: check for xterm behavior
|
||||
if (ch_width === 2) // FIXME: check for xterm behavior
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
row = this._terminal.buffer.y + this._terminal.buffer.ybase;
|
||||
@@ -83,7 +79,7 @@ export class InputHandler implements IInputHandler {
|
||||
// insert mode: move characters to right
|
||||
if (this._terminal.insertMode) {
|
||||
// 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
|
||||
// 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();
|
||||
@@ -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.updateRange(this._terminal.buffer.y);
|
||||
|
||||
// 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.x++;
|
||||
}
|
||||
@@ -166,11 +162,7 @@ export class InputHandler implements IInputHandler {
|
||||
* Horizontal Tab (HT) (Ctrl-I).
|
||||
*/
|
||||
public tab(): void {
|
||||
const originalX = this._terminal.buffer.x;
|
||||
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 1047: // alt screen buffer
|
||||
this._terminal.buffers.activateAltBuffer();
|
||||
this._terminal.selectionManager.setBuffer(this._terminal.buffer);
|
||||
this._terminal.viewport.syncScrollArea();
|
||||
this._terminal.showCursor();
|
||||
break;
|
||||
@@ -1107,6 +1100,7 @@ export class InputHandler implements IInputHandler {
|
||||
// if (params[0] === 1049) {
|
||||
// this.restoreCursor(params);
|
||||
// }
|
||||
this._terminal.selectionManager.setBuffer(this._terminal.buffer);
|
||||
this._terminal.refresh(0, this._terminal.rows - 1);
|
||||
this._terminal.viewport.syncScrollArea();
|
||||
this._terminal.showCursor();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
+33
-51
@@ -4,19 +4,20 @@
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { IMouseZoneManager, IMouseZone } from './input/Types';
|
||||
import { ILinkMatcher, LineData, ITerminal } from './Types';
|
||||
import { ITerminal, ILinkifier, IBuffer, IBufferAccessor, IElementAccessor } from './Interfaces';
|
||||
import { Linkifier } from './Linkifier';
|
||||
import { MockBuffer, MockTerminal } from './utils/TestUtils.test';
|
||||
import { LinkMatcher, LineData } from './Types';
|
||||
import { IMouseZoneManager, IMouseZone } from './input/Interfaces';
|
||||
import { MockBuffer } from './utils/TestUtils.test';
|
||||
import { CircularList } from './utils/CircularList';
|
||||
|
||||
class TestLinkifier extends Linkifier {
|
||||
constructor(_terminal: ITerminal) {
|
||||
constructor(_terminal: IBufferAccessor & IElementAccessor) {
|
||||
super(_terminal);
|
||||
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); }
|
||||
}
|
||||
|
||||
@@ -32,14 +33,15 @@ class TestMouseZoneManager implements IMouseZoneManager {
|
||||
}
|
||||
|
||||
describe('Linkifier', () => {
|
||||
let terminal: ITerminal;
|
||||
let terminal: IBufferAccessor & IElementAccessor;
|
||||
let linkifier: TestLinkifier;
|
||||
let mouseZoneManager: TestMouseZoneManager;
|
||||
|
||||
beforeEach(() => {
|
||||
terminal = new MockTerminal();
|
||||
terminal.cols = 100;
|
||||
terminal.buffer = new MockBuffer();
|
||||
terminal = {
|
||||
buffer: new MockBuffer(),
|
||||
element: <HTMLElement>{}
|
||||
};
|
||||
terminal.buffer.lines = new CircularList<LineData>(20);
|
||||
terminal.buffer.ydisp = 0;
|
||||
linkifier = new TestLinkifier(terminal);
|
||||
@@ -58,6 +60,17 @@ describe('Linkifier', () => {
|
||||
terminal.buffer.lines.push(stringToRow(text));
|
||||
}
|
||||
|
||||
function assertLinkifiesEntireRow(uri: string, done: MochaDone): void {
|
||||
addRow(uri);
|
||||
linkifier.linkifyRows();
|
||||
setTimeout(() => {
|
||||
assert.equal(mouseZoneManager.zones[0].x1, 1);
|
||||
assert.equal(mouseZoneManager.zones[0].x2, uri.length + 1);
|
||||
assert.equal(mouseZoneManager.zones[0].y, terminal.buffer.lines.length);
|
||||
done();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function assertLinkifiesRow(rowText: string, linkMatcherRegex: RegExp, links: {x: number, length: number}[], done: MochaDone): void {
|
||||
addRow(rowText);
|
||||
linkifier.registerLinkMatcher(linkMatcherRegex, () => {});
|
||||
@@ -68,25 +81,7 @@ describe('Linkifier', () => {
|
||||
links.forEach((l, i) => {
|
||||
assert.equal(mouseZoneManager.zones[i].x1, l.x + 1);
|
||||
assert.equal(mouseZoneManager.zones[i].x2, l.x + l.length + 1);
|
||||
assert.equal(mouseZoneManager.zones[i].y1, terminal.buffer.lines.length);
|
||||
assert.equal(mouseZoneManager.zones[i].y2, terminal.buffer.lines.length);
|
||||
});
|
||||
done();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function assertLinkifiesMultiLineLink(rowText: string, linkMatcherRegex: RegExp, links: {x1: number, y1: number, x2: number, y2: number}[], done: MochaDone): void {
|
||||
addRow(rowText);
|
||||
linkifier.registerLinkMatcher(linkMatcherRegex, () => {});
|
||||
linkifier.linkifyRows();
|
||||
// Allow linkify to happen
|
||||
setTimeout(() => {
|
||||
assert.equal(mouseZoneManager.zones.length, links.length);
|
||||
links.forEach((l, i) => {
|
||||
assert.equal(mouseZoneManager.zones[i].x1, l.x1 + 1);
|
||||
assert.equal(mouseZoneManager.zones[i].x2, l.x2 + 1);
|
||||
assert.equal(mouseZoneManager.zones[i].y1, l.y1 + 1);
|
||||
assert.equal(mouseZoneManager.zones[i].y2, l.y2 + 1);
|
||||
assert.equal(mouseZoneManager.zones[i].y, terminal.buffer.lines.length);
|
||||
});
|
||||
done();
|
||||
}, 0);
|
||||
@@ -107,6 +102,12 @@ describe('Linkifier', () => {
|
||||
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', () => {
|
||||
it('should match a single link', done => {
|
||||
assertLinkifiesRow('foo', /foo/, [{x: 0, length: 3}], done);
|
||||
@@ -135,24 +136,6 @@ describe('Linkifier', () => {
|
||||
// character (U+1F537) which caused the path to be duplicated. See #642.
|
||||
assertLinkifiesRow('echo \'🔷foo\'', /foo/, [{x: 8, length: 3}], done);
|
||||
});
|
||||
describe('multi-line links', () => {
|
||||
it('should match links that start on line 1/2 of a wrapped line and end on the last character of line 1/2', done => {
|
||||
terminal.cols = 4;
|
||||
assertLinkifiesMultiLineLink('12345', /1234/, [{x1: 0, x2: 4, y1: 0, y2: 0}], done);
|
||||
});
|
||||
it('should match links that start on line 1/2 of a wrapped line and wrap to line 2/2', done => {
|
||||
terminal.cols = 4;
|
||||
assertLinkifiesMultiLineLink('12345', /12345/, [{x1: 0, x2: 1, y1: 0, y2: 1}], done);
|
||||
});
|
||||
it('should match links that start and end on line 2/2 of a wrapped line', done => {
|
||||
terminal.cols = 4;
|
||||
assertLinkifiesMultiLineLink('12345678', /5678/, [{x1: 0, x2: 4, y1: 1, y2: 1}], done);
|
||||
});
|
||||
it('should match links that start on line 2/3 of a wrapped line and wrap to line 3/3', done => {
|
||||
terminal.cols = 4;
|
||||
assertLinkifiesMultiLineLink('123456789', /56789/, [{x1: 0, x2: 1, y1: 1, y2: 2}], done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('validationCallback', () => {
|
||||
@@ -165,8 +148,7 @@ describe('Linkifier', () => {
|
||||
assert.equal(mouseZoneManager.zones.length, 1);
|
||||
assert.equal(mouseZoneManager.zones[0].x1, 1);
|
||||
assert.equal(mouseZoneManager.zones[0].x2, 5);
|
||||
assert.equal(mouseZoneManager.zones[0].y1, 1);
|
||||
assert.equal(mouseZoneManager.zones[0].y2, 1);
|
||||
assert.equal(mouseZoneManager.zones[0].y, 1);
|
||||
// Fires done()
|
||||
mouseZoneManager.zones[0].clickCallback(<any>{});
|
||||
}
|
||||
@@ -219,19 +201,19 @@ describe('Linkifier', () => {
|
||||
it('should order the list from highest priority to lowest #1', () => {
|
||||
const aId = linkifier.registerLinkMatcher(/a/, () => {}, { 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', () => {
|
||||
const aId = linkifier.registerLinkMatcher(/a/, () => {}, { 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', () => {
|
||||
const aId = linkifier.registerLinkMatcher(/a/, () => {}, { 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]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+72
-66
@@ -3,11 +3,36 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IMouseZoneManager } from './input/Types';
|
||||
import { ILinkHoverEvent, ILinkMatcher, LinkMatcherHandler, LinkHoverEventTypes, ILinkMatcherOptions, ILinkifier, ITerminal } from './Types';
|
||||
import { ILinkMatcherOptions, ITerminal, IBufferAccessor, ILinkifier, IElementAccessor } from './Interfaces';
|
||||
import { LinkMatcher, LinkMatcherHandler, LinkMatcherValidationCallback, LineData, LinkHoverEvent, LinkHoverEventTypes } from './Types';
|
||||
import { IMouseZoneManager } from './input/Interfaces';
|
||||
import { MouseZone } from './input/MouseZoneManager';
|
||||
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.
|
||||
*/
|
||||
@@ -19,21 +44,22 @@ export class Linkifier extends EventEmitter implements ILinkifier {
|
||||
*/
|
||||
protected static TIME_BEFORE_LINKIFY = 200;
|
||||
|
||||
protected _linkMatchers: ILinkMatcher[] = [];
|
||||
protected _linkMatchers: LinkMatcher[] = [];
|
||||
|
||||
private _mouseZoneManager: IMouseZoneManager;
|
||||
private _rowsTimeoutId: number;
|
||||
private _nextLinkMatcherId = 0;
|
||||
private _nextLinkMatcherId = HYPERTEXT_LINK_MATCHER_ID;
|
||||
private _rowsToLinkify: {start: number, end: number};
|
||||
|
||||
constructor(
|
||||
protected _terminal: ITerminal
|
||||
protected _terminal: IBufferAccessor & IElementAccessor
|
||||
) {
|
||||
super();
|
||||
this._rowsToLinkify = {
|
||||
start: null,
|
||||
end: null
|
||||
};
|
||||
this.registerLinkMatcher(strictUrlRegex, null, { matchIndex: 1 });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,12 +82,12 @@ export class Linkifier extends EventEmitter implements ILinkifier {
|
||||
}
|
||||
|
||||
// Increase range to linkify
|
||||
if (this._rowsToLinkify.start === null) {
|
||||
if (!this._rowsToLinkify.start) {
|
||||
this._rowsToLinkify.start = start;
|
||||
this._rowsToLinkify.end = end;
|
||||
} else {
|
||||
this._rowsToLinkify.start = Math.min(this._rowsToLinkify.start, start);
|
||||
this._rowsToLinkify.end = Math.max(this._rowsToLinkify.end, end);
|
||||
this._rowsToLinkify.start = this._rowsToLinkify.start < start ? this._rowsToLinkify.start : start;
|
||||
this._rowsToLinkify.end = this._rowsToLinkify.end > end ? this._rowsToLinkify.end : end;
|
||||
}
|
||||
|
||||
// Clear out any existing links on this row range
|
||||
@@ -86,6 +112,23 @@ export class Linkifier extends EventEmitter implements ILinkifier {
|
||||
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
|
||||
* 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.
|
||||
*/
|
||||
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');
|
||||
}
|
||||
const matcher: ILinkMatcher = {
|
||||
const matcher: LinkMatcher = {
|
||||
id: this._nextLinkMatcherId++,
|
||||
regex,
|
||||
handler,
|
||||
@@ -108,7 +151,6 @@ export class Linkifier extends EventEmitter implements ILinkifier {
|
||||
validationCallback: options.validationCallback,
|
||||
hoverTooltipCallback: options.tooltipCallback,
|
||||
hoverLeaveCallback: options.leaveCallback,
|
||||
willLinkActivate: options.willLinkActivate,
|
||||
priority: options.priority || 0
|
||||
};
|
||||
this._addLinkMatcherToList(matcher);
|
||||
@@ -121,7 +163,7 @@ export class Linkifier extends EventEmitter implements ILinkifier {
|
||||
* considered after older link matchers.
|
||||
* @param matcher The link matcher to be added.
|
||||
*/
|
||||
private _addLinkMatcherToList(matcher: ILinkMatcher): void {
|
||||
private _addLinkMatcherToList(matcher: LinkMatcher): void {
|
||||
if (this._linkMatchers.length === 0) {
|
||||
this._linkMatchers.push(matcher);
|
||||
return;
|
||||
@@ -143,7 +185,8 @@ export class Linkifier extends EventEmitter implements ILinkifier {
|
||||
* @return Whether a link matcher was found and deregistered.
|
||||
*/
|
||||
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) {
|
||||
this._linkMatchers.splice(i, 1);
|
||||
return true;
|
||||
@@ -157,32 +200,11 @@ export class Linkifier extends EventEmitter implements ILinkifier {
|
||||
* @param rowIndex The index of the row to linkify.
|
||||
*/
|
||||
private _linkifyRow(rowIndex: number): void {
|
||||
// Ensure the row exists
|
||||
let absoluteRowIndex = this._terminal.buffer.ydisp + rowIndex;
|
||||
const absoluteRowIndex = this._terminal.buffer.ydisp + rowIndex;
|
||||
if (absoluteRowIndex >= this._terminal.buffer.lines.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((<any>this._terminal.buffer.lines.get(absoluteRowIndex)).isWrapped) {
|
||||
// Only attempt to linkify rows that start in the viewport
|
||||
if (rowIndex !== 0) {
|
||||
return;
|
||||
}
|
||||
// If the first row is wrapped, backtrack to find the origin row and linkify that
|
||||
do {
|
||||
rowIndex--;
|
||||
absoluteRowIndex--;
|
||||
} while ((<any>this._terminal.buffer.lines.get(absoluteRowIndex)).isWrapped);
|
||||
}
|
||||
|
||||
// Construct full unwrapped line text
|
||||
let text = this._terminal.buffer.translateBufferLineToString(absoluteRowIndex, false);
|
||||
let currentIndex = absoluteRowIndex + 1;
|
||||
while (currentIndex < this._terminal.buffer.lines.length &&
|
||||
(<any>this._terminal.buffer.lines.get(currentIndex)).isWrapped) {
|
||||
text += this._terminal.buffer.translateBufferLineToString(currentIndex++, false);
|
||||
}
|
||||
|
||||
const text = this._terminal.buffer.translateBufferLineToString(absoluteRowIndex, false);
|
||||
for (let i = 0; i < this._linkMatchers.length; i++) {
|
||||
this._doLinkifyRow(rowIndex, text, this._linkMatchers[i]);
|
||||
}
|
||||
@@ -197,7 +219,11 @@ export class Linkifier extends EventEmitter implements ILinkifier {
|
||||
* @param offset The how much of the row has already been linkified.
|
||||
* @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
|
||||
let result = [];
|
||||
const isHttpLinkMatcher = matcher.id === HYPERTEXT_LINK_MATCHER_ID;
|
||||
|
||||
// Find the first match
|
||||
let match = text.match(matcher.regex);
|
||||
if (!match || match.length === 0) {
|
||||
@@ -238,21 +264,11 @@ export class Linkifier extends EventEmitter implements ILinkifier {
|
||||
* @param uri The URI of the link.
|
||||
* @param matcher The link matcher for the link.
|
||||
*/
|
||||
private _addLink(x: number, y: number, uri: string, matcher: ILinkMatcher): void {
|
||||
const x1 = x % this._terminal.cols;
|
||||
const y1 = y + Math.floor(x / this._terminal.cols);
|
||||
let x2 = (x1 + uri.length) % this._terminal.cols;
|
||||
let y2 = y1 + Math.floor((x1 + uri.length) / this._terminal.cols);
|
||||
if (x2 === 0) {
|
||||
x2 = this._terminal.cols;
|
||||
y2--;
|
||||
}
|
||||
|
||||
private _addLink(x: number, y: number, uri: string, matcher: LinkMatcher): void {
|
||||
this._mouseZoneManager.add(new MouseZone(
|
||||
x1 + 1,
|
||||
y1 + 1,
|
||||
x2 + 1,
|
||||
y2 + 1,
|
||||
x + 1,
|
||||
x + 1 + uri.length,
|
||||
y + 1,
|
||||
e => {
|
||||
if (matcher.handler) {
|
||||
return matcher.handler(e, uri);
|
||||
@@ -260,32 +276,22 @@ export class Linkifier extends EventEmitter implements ILinkifier {
|
||||
window.open(uri, '_blank');
|
||||
},
|
||||
e => {
|
||||
this.emit(LinkHoverEventTypes.HOVER, this._createLinkHoverEvent(x1, y1, x2, y2));
|
||||
this._terminal.element.classList.add('xterm-cursor-pointer');
|
||||
this.emit(LinkHoverEventTypes.HOVER, <LinkHoverEvent>{ x, y, length: uri.length});
|
||||
this._terminal.element.style.cursor = 'pointer';
|
||||
},
|
||||
e => {
|
||||
this.emit(LinkHoverEventTypes.TOOLTIP, this._createLinkHoverEvent(x1, y1, x2, y2));
|
||||
this.emit(LinkHoverEventTypes.TOOLTIP, <LinkHoverEvent>{ x, y, length: uri.length});
|
||||
if (matcher.hoverTooltipCallback) {
|
||||
matcher.hoverTooltipCallback(e, uri);
|
||||
}
|
||||
},
|
||||
() => {
|
||||
this.emit(LinkHoverEventTypes.LEAVE, this._createLinkHoverEvent(x1, y1, x2, y2));
|
||||
this._terminal.element.classList.remove('xterm-cursor-pointer');
|
||||
this.emit(LinkHoverEventTypes.LEAVE, <LinkHoverEvent>{ x, y, length: uri.length});
|
||||
this._terminal.element.style.cursor = '';
|
||||
if (matcher.hoverLeaveCallback) {
|
||||
matcher.hoverLeaveCallback();
|
||||
}
|
||||
},
|
||||
e => {
|
||||
if (matcher.willLinkActivate) {
|
||||
return matcher.willLinkActivate(e, uri);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
private _createLinkHoverEvent(x1: number, y1: number, x2: number, y2: number): ILinkHoverEvent {
|
||||
return { x1, y1, x2, y2, cols: this._terminal.cols };
|
||||
}
|
||||
}
|
||||
|
||||
+4
-11
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import { C0 } from './EscapeSequences';
|
||||
import { IInputHandler, IInputHandlingTerminal } from './Types';
|
||||
import { IInputHandler } from './Interfaces';
|
||||
import { CHARSETS, DEFAULT_CHARSET } from './Charsets';
|
||||
|
||||
const normalStateHandler: {[key: string]: (parser: Parser, handler: IInputHandler) => void} = {};
|
||||
@@ -185,6 +185,7 @@ export class Parser {
|
||||
*/
|
||||
public parse(data: string): ParserState {
|
||||
const l = data.length;
|
||||
let j;
|
||||
let cs;
|
||||
let ch;
|
||||
let code;
|
||||
@@ -223,9 +224,8 @@ export class Parser {
|
||||
ch += data.charAt(this._position + 1);
|
||||
}
|
||||
// surrogate low - already handled above
|
||||
if (0xDC00 <= code && code <= 0xDFFF) {
|
||||
if (0xDC00 <= code && code <= 0xDFFF)
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (this._state) {
|
||||
case ParserState.NORMAL:
|
||||
@@ -286,43 +286,36 @@ export class Parser {
|
||||
// Single Shift Select of G2 Character Set
|
||||
// ( SS2 is 0x8e). This affects next character only.
|
||||
case 'N':
|
||||
this._state = ParserState.NORMAL;
|
||||
break;
|
||||
// ESC O
|
||||
// Single Shift Select of G3 Character Set
|
||||
// ( SS3 is 0x8f). This affects next character only.
|
||||
case 'O':
|
||||
this._state = ParserState.NORMAL;
|
||||
break;
|
||||
// ESC n
|
||||
// Invoke the G2 Character Set as GL (LS2).
|
||||
case 'n':
|
||||
this._terminal.setgLevel(2);
|
||||
this._state = ParserState.NORMAL;
|
||||
break;
|
||||
// ESC o
|
||||
// Invoke the G3 Character Set as GL (LS3).
|
||||
case 'o':
|
||||
this._terminal.setgLevel(3);
|
||||
this._state = ParserState.NORMAL;
|
||||
break;
|
||||
// ESC |
|
||||
// Invoke the G3 Character Set as GR (LS3R).
|
||||
case '|':
|
||||
this._terminal.setgLevel(3);
|
||||
this._state = ParserState.NORMAL;
|
||||
break;
|
||||
// ESC }
|
||||
// Invoke the G2 Character Set as GR (LS2R).
|
||||
case '}':
|
||||
this._terminal.setgLevel(2);
|
||||
this._state = ParserState.NORMAL;
|
||||
break;
|
||||
// ESC ~
|
||||
// Invoke the G1 Character Set as GR (LS1R).
|
||||
case '~':
|
||||
this._terminal.setgLevel(1);
|
||||
this._state = ParserState.NORMAL;
|
||||
break;
|
||||
|
||||
// ESC 7 Save Cursor (DECSC).
|
||||
@@ -345,7 +338,7 @@ export class Parser {
|
||||
|
||||
// ESC H Tab Set (HTS is 0x88).
|
||||
case 'H':
|
||||
(<IInputHandlingTerminal>this._terminal).tabSet();
|
||||
this._terminal.tabSet();
|
||||
this._state = ParserState.NORMAL;
|
||||
break;
|
||||
|
||||
|
||||
@@ -3,13 +3,16 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import jsdom = require('jsdom');
|
||||
import { assert } from 'chai';
|
||||
import { ITerminal, ICircularList, IBuffer } from './Interfaces';
|
||||
import { CharMeasure } from './utils/CharMeasure';
|
||||
import { CircularList } from './utils/CircularList';
|
||||
import { SelectionManager } from './SelectionManager';
|
||||
import { SelectionModel } from './SelectionModel';
|
||||
import { BufferSet } from './BufferSet';
|
||||
import { LineData, CharData, ITerminal, IBuffer } from './Types';
|
||||
import { MockTerminal } from './utils/TestUtils.test';
|
||||
import { LineData, CharData } from './Types';
|
||||
|
||||
class TestMockTerminal extends MockTerminal {
|
||||
emit(event: string, data: any): void {}
|
||||
@@ -18,15 +21,16 @@ class TestMockTerminal extends MockTerminal {
|
||||
class TestSelectionManager extends SelectionManager {
|
||||
constructor(
|
||||
terminal: ITerminal,
|
||||
buffer: IBuffer,
|
||||
charMeasure: CharMeasure
|
||||
) {
|
||||
super(terminal, charMeasure);
|
||||
super(terminal, buffer, charMeasure);
|
||||
}
|
||||
|
||||
public get model(): SelectionModel { return this._model; }
|
||||
|
||||
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
|
||||
public enable(): void {}
|
||||
@@ -35,11 +39,19 @@ class TestSelectionManager extends SelectionManager {
|
||||
}
|
||||
|
||||
describe('SelectionManager', () => {
|
||||
let dom: jsdom.JSDOM;
|
||||
let window: Window;
|
||||
let document: Document;
|
||||
|
||||
let terminal: ITerminal;
|
||||
let buffer: IBuffer;
|
||||
let rowContainer: HTMLElement;
|
||||
let selectionManager: TestSelectionManager;
|
||||
|
||||
beforeEach(() => {
|
||||
dom = new jsdom.JSDOM('');
|
||||
window = dom.window;
|
||||
document = window.document;
|
||||
terminal = new TestMockTerminal();
|
||||
terminal.cols = 80;
|
||||
terminal.rows = 2;
|
||||
@@ -47,7 +59,7 @@ describe('SelectionManager', () => {
|
||||
terminal.buffers = new BufferSet(terminal);
|
||||
terminal.buffer = terminal.buffers.active;
|
||||
buffer = terminal.buffer;
|
||||
selectionManager = new TestSelectionManager(terminal, null);
|
||||
selectionManager = new TestSelectionManager(terminal, buffer, null);
|
||||
});
|
||||
|
||||
function stringToRow(text: string): LineData {
|
||||
@@ -314,45 +326,6 @@ describe('SelectionManager', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('selectLines', () => {
|
||||
it('should select a single line', () => {
|
||||
buffer.lines.length = 3;
|
||||
buffer.lines.set(0, stringToRow('1'));
|
||||
buffer.lines.set(1, stringToRow('2'));
|
||||
buffer.lines.set(2, stringToRow('3'));
|
||||
selectionManager.selectLines(1, 1);
|
||||
assert.deepEqual(selectionManager.model.finalSelectionStart, [0, 1]);
|
||||
assert.deepEqual(selectionManager.model.finalSelectionEnd, [terminal.cols, 1]);
|
||||
});
|
||||
it('should select multiple lines', () => {
|
||||
buffer.lines.length = 5;
|
||||
buffer.lines.set(0, stringToRow('1'));
|
||||
buffer.lines.set(1, stringToRow('2'));
|
||||
buffer.lines.set(2, stringToRow('3'));
|
||||
buffer.lines.set(3, stringToRow('4'));
|
||||
buffer.lines.set(4, stringToRow('5'));
|
||||
selectionManager.selectLines(1, 3);
|
||||
assert.deepEqual(selectionManager.model.finalSelectionStart, [0, 1]);
|
||||
assert.deepEqual(selectionManager.model.finalSelectionEnd, [terminal.cols, 3]);
|
||||
});
|
||||
it('should select the to the start when requesting a negative row', () => {
|
||||
buffer.lines.length = 2;
|
||||
buffer.lines.set(0, stringToRow('1'));
|
||||
buffer.lines.set(1, stringToRow('2'));
|
||||
selectionManager.selectLines(-1, 0);
|
||||
assert.deepEqual(selectionManager.model.finalSelectionStart, [0, 0]);
|
||||
assert.deepEqual(selectionManager.model.finalSelectionEnd, [terminal.cols, 0]);
|
||||
});
|
||||
it('should select the to the end when requesting beyond the final row', () => {
|
||||
buffer.lines.length = 2;
|
||||
buffer.lines.set(0, stringToRow('1'));
|
||||
buffer.lines.set(1, stringToRow('2'));
|
||||
selectionManager.selectLines(1, 2);
|
||||
assert.deepEqual(selectionManager.model.finalSelectionStart, [0, 1]);
|
||||
assert.deepEqual(selectionManager.model.finalSelectionEnd, [terminal.cols, 1]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasSelection', () => {
|
||||
it('should return whether there is a selection', () => {
|
||||
selectionManager.model.selectionStart = [0, 0];
|
||||
|
||||
+29
-99
@@ -3,14 +3,15 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ITerminal, ISelectionManager, IBuffer, CharData, XtermListener } from './Types';
|
||||
import { MouseHelper } from './utils/MouseHelper';
|
||||
import * as Browser from './shared/utils/Browser';
|
||||
import * as Browser from './utils/Browser';
|
||||
import { CharMeasure } from './utils/CharMeasure';
|
||||
import { CircularList } from './utils/CircularList';
|
||||
import { EventEmitter } from './EventEmitter';
|
||||
import { ITerminal, ICircularList, ISelectionManager, IBuffer } from './Interfaces';
|
||||
import { SelectionModel } from './SelectionModel';
|
||||
import { LineData, CharData } from './Types';
|
||||
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from './Buffer';
|
||||
import { AltClickHandler } from './handlers/AltClickHandler';
|
||||
|
||||
/**
|
||||
* The number of pixels the mouse needs to be above or below the viewport in
|
||||
@@ -28,12 +29,6 @@ const DRAG_SCROLL_MAX_SPEED = 15;
|
||||
*/
|
||||
const DRAG_SCROLL_INTERVAL = 50;
|
||||
|
||||
/**
|
||||
* The maximum amount of time that can have elapsed for an alt click to move the
|
||||
* cursor.
|
||||
*/
|
||||
const ALT_CLICK_MOVE_CURSOR_TIME = 500;
|
||||
|
||||
/**
|
||||
* A string containing all characters that are considered word separated by the
|
||||
* double click to select work logic.
|
||||
@@ -100,12 +95,10 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
|
||||
private _mouseMoveListener: EventListener;
|
||||
private _mouseUpListener: EventListener;
|
||||
private _trimListener: XtermListener;
|
||||
|
||||
private _mouseDownTimeStamp: number;
|
||||
|
||||
constructor(
|
||||
private _terminal: ITerminal,
|
||||
private _buffer: IBuffer,
|
||||
private _charMeasure: CharMeasure
|
||||
) {
|
||||
super();
|
||||
@@ -116,24 +109,18 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
this._activeSelectionMode = SelectionMode.NORMAL;
|
||||
}
|
||||
|
||||
private get _buffer(): IBuffer {
|
||||
return this._terminal.buffers.active;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes listener variables.
|
||||
*/
|
||||
private _initListeners(): void {
|
||||
this._mouseMoveListener = event => this._onMouseMove(<MouseEvent>event);
|
||||
this._mouseUpListener = event => this._onMouseUp(<MouseEvent>event);
|
||||
this._trimListener = (amount: number) => this._onTrim(amount);
|
||||
|
||||
this.initBuffersListeners();
|
||||
}
|
||||
|
||||
public initBuffersListeners(): void {
|
||||
this._terminal.buffer.lines.on('trim', this._trimListener);
|
||||
this._terminal.buffers.on('activate', e => this._onBufferActivate(e));
|
||||
// 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.
|
||||
this._buffer.lines.on('trim', (amount: number) => this._onTrim(amount));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -152,6 +139,16 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
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 selectionEnd(): [number, number] { return this._model.finalSelectionEnd; }
|
||||
|
||||
@@ -252,37 +249,6 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
this.emit('refresh', { start: this._model.finalSelectionStart, end: this._model.finalSelectionEnd });
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current click was inside the current selection
|
||||
* @param event The mouse event
|
||||
*/
|
||||
public isClickInSelection(event: MouseEvent): boolean {
|
||||
const coords = this._getMouseBufferCoords(event);
|
||||
const start = this._model.finalSelectionStart;
|
||||
const end = this._model.finalSelectionEnd;
|
||||
|
||||
if (!start || !end) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (coords[1] > start[1] && coords[1] < end[1]) ||
|
||||
(start[1] === end[1] && coords[1] === start[1] && coords[0] > start[0] && coords[0] < end[0]) ||
|
||||
(start[1] < end[1] && coords[1] === end[1] && coords[0] < end[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects word at the current mouse event coordinates.
|
||||
* @param event The mouse event.
|
||||
*/
|
||||
public selectWordAtCursor(event: MouseEvent): void {
|
||||
const coords = this._getMouseBufferCoords(event);
|
||||
if (coords) {
|
||||
this._selectWordAt(coords, false);
|
||||
this._model.selectionEnd = null;
|
||||
this.refresh(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects all text within the terminal.
|
||||
*/
|
||||
@@ -292,16 +258,6 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
this._terminal.emit('selection');
|
||||
}
|
||||
|
||||
public selectLines(start: number, end: number): void {
|
||||
this._model.clearSelection();
|
||||
start = Math.max(start, 0);
|
||||
end = Math.min(end, this._terminal.buffer.lines.length - 1);
|
||||
this._model.selectionStart = [0, start];
|
||||
this._model.selectionEnd = [this._terminal.cols, end];
|
||||
this.refresh();
|
||||
this._terminal.emit('selection');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the buffer being trimmed, adjust the selection position.
|
||||
* @param amount The amount the buffer is being trimmed.
|
||||
@@ -318,7 +274,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
* @param event The mouse event.
|
||||
*/
|
||||
private _getMouseBufferCoords(event: MouseEvent): [number, number] {
|
||||
const coords = this._terminal.mouseHelper.getCoords(event, this._terminal.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) {
|
||||
return null;
|
||||
}
|
||||
@@ -338,7 +294,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
* @param event The mouse event.
|
||||
*/
|
||||
private _getMouseEventScrollAmount(event: MouseEvent): number {
|
||||
let offset = MouseHelper.getCoordsRelativeToElement(event, this._terminal.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);
|
||||
if (offset >= 0 && offset <= terminalHeight) {
|
||||
return 0;
|
||||
@@ -366,7 +322,6 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
* @param event The mousedown event.
|
||||
*/
|
||||
public onMouseDown(event: MouseEvent): void {
|
||||
this._mouseDownTimeStamp = event.timeStamp;
|
||||
// If we have selection, we want the context menu on right click even if the
|
||||
// terminal is in mouse mode.
|
||||
if (event.button === 2 && this.hasSelection) {
|
||||
@@ -485,7 +440,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
const coords = this._getMouseBufferCoords(event);
|
||||
if (coords) {
|
||||
this._activeSelectionMode = SelectionMode.WORD;
|
||||
this._selectWordAt(coords, true);
|
||||
this._selectWordAt(coords);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -573,7 +528,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
this._terminal.scrollLines(this._dragScrollAmount, false);
|
||||
// Re-evaluate selection
|
||||
if (this._dragScrollAmount > 0) {
|
||||
this._model.selectionEnd = [this._terminal.cols - 1, 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 {
|
||||
this._model.selectionEnd = [0, this._terminal.buffer.ydisp];
|
||||
}
|
||||
@@ -586,25 +541,10 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
* @param event The mouseup event.
|
||||
*/
|
||||
private _onMouseUp(event: MouseEvent): void {
|
||||
let timeElapsed = event.timeStamp - this._mouseDownTimeStamp;
|
||||
|
||||
this._removeMouseDownListeners();
|
||||
|
||||
if (this.selectionText.length <= 1 && timeElapsed < ALT_CLICK_MOVE_CURSOR_TIME) {
|
||||
(new AltClickHandler(event, this._terminal)).move();
|
||||
} else if (this.hasSelection) {
|
||||
if (this.hasSelection)
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -642,12 +582,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
* Gets positional information for the word at the coordinated specified.
|
||||
* @param coords The coordinates to get the word at.
|
||||
*/
|
||||
private _getWordAt(coords: [number, number], allowWhitespaceOnlySelection: boolean): IWordPosition {
|
||||
// Ensure coords are within viewport (eg. not within scroll bar)
|
||||
if (coords[0] >= this._terminal.cols) {
|
||||
return null;
|
||||
}
|
||||
|
||||
private _getWordAt(coords: [number, number]): IWordPosition {
|
||||
const bufferLine = this._buffer.lines.get(coords[1]);
|
||||
if (!bufferLine) {
|
||||
return null;
|
||||
@@ -753,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)
|
||||
- rightLongCharOffset); // The number of additional chars right of the initial char (inclusive) added by columns with strings longer than 1 (emojis)
|
||||
|
||||
if (!allowWhitespaceOnlySelection && line.slice(startIndex, endIndex).trim() === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { start, length };
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects the word at the coordinates specified.
|
||||
* @param coords The coordinates to get the word at.
|
||||
* @param allowWhitespaceOnlySelection If whitespace should be selected
|
||||
*/
|
||||
protected _selectWordAt(coords: [number, number], allowWhitespaceOnlySelection: boolean): void {
|
||||
const wordPosition = this._getWordAt(coords, allowWhitespaceOnlySelection);
|
||||
protected _selectWordAt(coords: [number, number]): void {
|
||||
const wordPosition = this._getWordAt(coords);
|
||||
if (wordPosition) {
|
||||
this._model.selectionStart = [wordPosition.start, coords[1]];
|
||||
this._model.selectionStartLength = wordPosition.length;
|
||||
@@ -778,7 +708,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
* @param coords The coordinates to get the word at.
|
||||
*/
|
||||
private _selectToWordAt(coords: [number, number]): void {
|
||||
const wordPosition = this._getWordAt(coords, true);
|
||||
const wordPosition = this._getWordAt(coords);
|
||||
if (wordPosition) {
|
||||
this._model.selectionEnd = [this._model.areSelectionValuesReversed() ? wordPosition.start : (wordPosition.start + wordPosition.length), coords[1]];
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { ITerminal } from './Types';
|
||||
import { ITerminal } from './Interfaces';
|
||||
import { SelectionModel } from './SelectionModel';
|
||||
import { BufferSet } from './BufferSet';
|
||||
import {BufferSet} from './BufferSet';
|
||||
import { MockTerminal } from './utils/TestUtils.test';
|
||||
|
||||
class TestSelectionModel extends SelectionModel {
|
||||
@@ -18,6 +18,9 @@ class TestSelectionModel extends SelectionModel {
|
||||
}
|
||||
|
||||
describe('SelectionManager', () => {
|
||||
let window: Window;
|
||||
let document: Document;
|
||||
|
||||
let terminal: ITerminal;
|
||||
let model: TestSelectionModel;
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ITerminal } from './Types';
|
||||
import { ITerminal } from './Interfaces';
|
||||
|
||||
/**
|
||||
* Represents a selection within the buffer. This model only cares about column
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
+19
-19
@@ -15,17 +15,17 @@ import { assert } from 'chai';
|
||||
import { Terminal } from './Terminal';
|
||||
import { CHAR_DATA_CHAR_INDEX } from './Buffer';
|
||||
|
||||
let primitivePty: any;
|
||||
let primitive_pty: any;
|
||||
|
||||
// fake sychronous pty write - read
|
||||
// we just pipe the data from slave to master as a child program would do
|
||||
// pty.js opens pipe fds with O_NONBLOCK
|
||||
// just wait 10ms instead of setting fds to blocking mode
|
||||
function ptyWriteRead(data: string, cb: (result: string) => void): void {
|
||||
fs.writeSync(primitivePty.slave, data);
|
||||
fs.writeSync(primitive_pty.slave, data);
|
||||
setTimeout(() => {
|
||||
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));
|
||||
});
|
||||
}
|
||||
@@ -37,7 +37,7 @@ function ptyReset(cb: (result: string) => void): void {
|
||||
|
||||
/* debug helpers */
|
||||
// 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 {
|
||||
let counter = start || 0;
|
||||
return function(s: string): string {
|
||||
@@ -47,9 +47,9 @@ function formatError(input: string, output: string, expected: string): string {
|
||||
}
|
||||
let line80 = '12345678901234567890123456789012345678901234567890123456789012345678901234567890';
|
||||
let s = '';
|
||||
s += '\n\x1b[34m' + JSON.stringify(input);
|
||||
s += '\n\x1b[34m' + JSON.stringify(in_);
|
||||
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 += expected.split('\n').map(addLineNumber(0, '\x1b[32m')).join('\n');
|
||||
return s;
|
||||
@@ -58,15 +58,15 @@ function formatError(input: string, output: string, expected: string): string {
|
||||
// simple debug output of terminal cells
|
||||
function terminalToString(term: Terminal): string {
|
||||
let result = '';
|
||||
let lineText = '';
|
||||
let line_s = '';
|
||||
for (let line = term.buffer.ybase; line < term.buffer.ybase + term.rows; line++) {
|
||||
lineText = '';
|
||||
line_s = '';
|
||||
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
|
||||
lineText = lineText.replace(/\s+$/, '');
|
||||
result += lineText;
|
||||
line_s = line_s.replace(/\s+$/, '');
|
||||
result += line_s;
|
||||
result += '\n';
|
||||
}
|
||||
return result;
|
||||
@@ -83,7 +83,7 @@ if (os.platform() !== 'win32') {
|
||||
/** some helpers for pty interaction */
|
||||
// we need a pty in between to get the termios decorations
|
||||
// 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 */
|
||||
describe('xterm output comparison', () => {
|
||||
@@ -118,24 +118,24 @@ if (os.platform() !== 'win32') {
|
||||
((filename: string) => {
|
||||
it(filename.split('/').slice(-1)[0], done => {
|
||||
ptyReset(() => {
|
||||
let inFile = fs.readFileSync(filename, 'utf8');
|
||||
ptyWriteRead(inFile, fromPty => {
|
||||
let in_file = fs.readFileSync(filename, 'utf8');
|
||||
ptyWriteRead(in_file, from_pty => {
|
||||
// uncomment this to get log from terminal
|
||||
// console.log = function(){};
|
||||
|
||||
// Perform a synchronous .write(data)
|
||||
xterm.writeBuffer.push(fromPty);
|
||||
xterm._innerWrite();
|
||||
xterm.writeBuffer.push(from_pty);
|
||||
xterm.innerWrite();
|
||||
|
||||
let fromEmulator = terminalToString(xterm);
|
||||
let from_emulator = terminalToString(xterm);
|
||||
console.log = CONSOLE_LOG;
|
||||
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
|
||||
// from xterm.js so ignore this for now at least.
|
||||
let expectedRightTrimmed = expected.split('\n').map(l => l.replace(/\s+$/, '')).join('\n');
|
||||
if (fromEmulator !== expectedRightTrimmed) {
|
||||
if (from_emulator !== expectedRightTrimmed) {
|
||||
// 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');
|
||||
}
|
||||
done();
|
||||
|
||||
+6
-169
@@ -20,21 +20,20 @@ class TestTerminal extends Terminal {
|
||||
|
||||
describe('term.js addons', () => {
|
||||
let term: TestTerminal;
|
||||
const termOptions = {
|
||||
cols: INIT_COLS,
|
||||
rows: INIT_ROWS
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
term = new TestTerminal(termOptions);
|
||||
term = new TestTerminal({
|
||||
cols: INIT_COLS,
|
||||
rows: INIT_ROWS
|
||||
});
|
||||
term.refresh = () => {};
|
||||
(<any>term).renderer = new MockRenderer();
|
||||
term.viewport = new MockViewport();
|
||||
(<any>term)._compositionHelper = new MockCompositionHelper();
|
||||
(<any>term).compositionHelper = new MockCompositionHelper();
|
||||
// Force synchronous writes
|
||||
term.write = (data) => {
|
||||
term.writeBuffer.push(data);
|
||||
(<any>term)._innerWrite();
|
||||
(<any>term).innerWrite();
|
||||
};
|
||||
(<any>term).element = {
|
||||
classList: {
|
||||
@@ -44,15 +43,6 @@ describe('term.js addons', () => {
|
||||
};
|
||||
});
|
||||
|
||||
it('should not mutate the options parameter', () => {
|
||||
term.setOption('cols', 1000);
|
||||
|
||||
assert.deepEqual(termOptions, {
|
||||
cols: INIT_COLS,
|
||||
rows: INIT_ROWS
|
||||
});
|
||||
});
|
||||
|
||||
it('should apply addons with Terminal.applyAddon', () => {
|
||||
Terminal.applyAddon(attach);
|
||||
// Test that addon was applied successfully, adding attach to Terminal's
|
||||
@@ -268,34 +258,6 @@ describe('term.js addons', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('scrollToLine', () => {
|
||||
let startYDisp;
|
||||
beforeEach(() => {
|
||||
for (let i = 0; i < term.rows * 3; i++) {
|
||||
term.writeln('test');
|
||||
}
|
||||
startYDisp = (term.rows * 2) + 1;
|
||||
});
|
||||
it('should scroll to requested line', () => {
|
||||
assert.equal(term.buffer.ydisp, startYDisp);
|
||||
term.scrollToLine(0);
|
||||
assert.equal(term.buffer.ydisp, 0);
|
||||
term.scrollToLine(10);
|
||||
assert.equal(term.buffer.ydisp, 10);
|
||||
term.scrollToLine(startYDisp);
|
||||
assert.equal(term.buffer.ydisp, startYDisp);
|
||||
term.scrollToLine(20);
|
||||
assert.equal(term.buffer.ydisp, 20);
|
||||
});
|
||||
it('should not scroll beyond boundary lines', () => {
|
||||
assert.equal(term.buffer.ydisp, startYDisp);
|
||||
term.scrollToLine(-1);
|
||||
assert.equal(term.buffer.ydisp, 0);
|
||||
term.scrollToLine(startYDisp + 1);
|
||||
assert.equal(term.buffer.ydisp, startYDisp);
|
||||
});
|
||||
});
|
||||
|
||||
describe('keyDown', () => {
|
||||
it('should scroll down, when a key is pressed and terminal is scrolled up', () => {
|
||||
// Override _evaluateKeyEscapeSequence to return cancel code
|
||||
@@ -541,9 +503,6 @@ describe('term.js addons', () => {
|
||||
it('should return \\x1b[5C for alt+right', () => {
|
||||
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', () => {
|
||||
@@ -556,19 +515,6 @@ describe('term.js addons', () => {
|
||||
it('should return \\x1bf for alt+right', () => {
|
||||
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', () => {
|
||||
@@ -616,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: 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', () => {
|
||||
@@ -744,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', () => {
|
||||
beforeEach(() => {
|
||||
term.browser.isMac = true;
|
||||
|
||||
+337
-369
File diff suppressed because it is too large
Load Diff
+19
-341
@@ -3,355 +3,33 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { Terminal as PublicTerminal, ITerminalOptions as IPublicTerminalOptions, IEventEmitter } from 'xterm';
|
||||
import { IColorSet, IRenderer } from './renderer/Types';
|
||||
import { IMouseZoneManager } from './input/Types';
|
||||
export type LinkMatcher = {
|
||||
id: number,
|
||||
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 XtermListener = (...args: any[]) => void;
|
||||
export type Charset = {[key: string]: string};
|
||||
|
||||
export type CharData = [number, string, number, number];
|
||||
export type LineData = CharData[];
|
||||
|
||||
export type LinkMatcherHandler = (event: MouseEvent, uri: string) => void;
|
||||
export type LinkMatcherValidationCallback = (uri: string, callback: (isValid: boolean) => void) => void;
|
||||
export type LinkHoverEvent = {
|
||||
x: number,
|
||||
y: number,
|
||||
length: number
|
||||
};
|
||||
|
||||
export enum LinkHoverEventTypes {
|
||||
HOVER = 'linkhover',
|
||||
TOOLTIP = 'linktooltip',
|
||||
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;
|
||||
tabSet(): void;
|
||||
}
|
||||
|
||||
export interface IViewport {
|
||||
scrollBarWidth: number;
|
||||
syncScrollArea(): void;
|
||||
getLinesScrolled(ev: WheelEvent): number;
|
||||
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 {
|
||||
x1: number;
|
||||
y1: number;
|
||||
x2: number;
|
||||
y2: number;
|
||||
cols: 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;
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
+61
-107
@@ -3,68 +3,61 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IColorSet } from './renderer/Types';
|
||||
import { ITerminal, IViewport } from './Types';
|
||||
import { ITerminal, IViewport } from './Interfaces';
|
||||
import { CharMeasure } from './utils/CharMeasure';
|
||||
|
||||
const FALLBACK_SCROLL_BAR_WIDTH = 15;
|
||||
import { IColorSet } from './renderer/Interfaces';
|
||||
|
||||
/**
|
||||
* Represents the viewport of a terminal, the visible area within the larger buffer of output.
|
||||
* Logic for the virtual scroll bar is included in this object.
|
||||
*/
|
||||
export class Viewport implements IViewport {
|
||||
public scrollBarWidth: number = 0;
|
||||
private _currentRowHeight: number = 0;
|
||||
private _lastRecordedBufferLength: number = 0;
|
||||
private _lastRecordedViewportHeight: number = 0;
|
||||
private _lastRecordedBufferHeight: number = 0;
|
||||
private _lastTouchY: number;
|
||||
|
||||
// Stores a partial line amount when scrolling, this is used to keep track of how much of a line
|
||||
// is scrolled so we can "scroll" over partial lines and feel natural on touchpads. This is a
|
||||
// quick fix and could have a more robust solution in place that reset the value when needed.
|
||||
private _wheelPartialScroll: number = 0;
|
||||
private currentRowHeight: number = 0;
|
||||
private lastRecordedBufferLength: number = 0;
|
||||
private lastRecordedViewportHeight: number = 0;
|
||||
private lastRecordedBufferHeight: number = 0;
|
||||
private lastTouchY: number;
|
||||
|
||||
/**
|
||||
* Creates a new Viewport.
|
||||
* @param _terminal The terminal this viewport belongs to.
|
||||
* @param _viewportElement The DOM element acting as the viewport.
|
||||
* @param _scrollArea The DOM element acting as the scroll area.
|
||||
* @param _charMeasure A DOM element used to measure the character size of. the terminal.
|
||||
* @param terminal The terminal this viewport belongs to.
|
||||
* @param viewportElement The DOM element acting as the viewport.
|
||||
* @param scrollArea The DOM element acting as the scroll area.
|
||||
* @param charMeasure A DOM element used to measure the character size of. the terminal.
|
||||
*/
|
||||
constructor(
|
||||
private _terminal: ITerminal,
|
||||
private _viewportElement: HTMLElement,
|
||||
private _scrollArea: HTMLElement,
|
||||
private _charMeasure: CharMeasure
|
||||
private terminal: ITerminal,
|
||||
private viewportElement: HTMLElement,
|
||||
private scrollArea: HTMLElement,
|
||||
private charMeasure: CharMeasure
|
||||
) {
|
||||
// Measure the width of the scrollbar. If it is 0 we can assume it's an OSX overlay scrollbar.
|
||||
// Unfortunately the overlay scrollbar would be hidden underneath the screen element in that case,
|
||||
// therefore we account for a standard amount to make it visible
|
||||
this.scrollBarWidth = (this._viewportElement.offsetWidth - this._scrollArea.offsetWidth) || FALLBACK_SCROLL_BAR_WIDTH;
|
||||
this._viewportElement.addEventListener('scroll', this._onScroll.bind(this));
|
||||
this.viewportElement.addEventListener('scroll', this.onScroll.bind(this));
|
||||
|
||||
// Perform this async to ensure the CharMeasure is ready.
|
||||
setTimeout(() => this.syncScrollArea(), 0);
|
||||
}
|
||||
|
||||
public onThemeChanged(colors: IColorSet): void {
|
||||
this._viewportElement.style.backgroundColor = colors.background.css;
|
||||
this.viewportElement.style.backgroundColor = colors.background;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes row height, setting line-height, viewport height and scroll area height if
|
||||
* necessary.
|
||||
*/
|
||||
private _refresh(): void {
|
||||
if (this._charMeasure.height > 0) {
|
||||
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._lastRecordedBufferHeight !== newBufferHeight) {
|
||||
this._lastRecordedBufferHeight = newBufferHeight;
|
||||
this._scrollArea.style.height = this._lastRecordedBufferHeight + 'px';
|
||||
private refresh(): void {
|
||||
if (this.charMeasure.height > 0) {
|
||||
this.currentRowHeight = this.terminal.renderer.dimensions.scaledCellHeight / window.devicePixelRatio;
|
||||
|
||||
if (this.lastRecordedViewportHeight !== this.terminal.renderer.dimensions.canvasHeight) {
|
||||
this.lastRecordedViewportHeight = this.terminal.renderer.dimensions.canvasHeight;
|
||||
this.viewportElement.style.height = this.lastRecordedViewportHeight + 'px';
|
||||
}
|
||||
|
||||
const newBufferHeight = Math.round(this.currentRowHeight * this.lastRecordedBufferLength);
|
||||
if (this.lastRecordedBufferHeight !== newBufferHeight) {
|
||||
this.lastRecordedBufferHeight = newBufferHeight;
|
||||
this.scrollArea.style.height = this.lastRecordedBufferHeight + 'px';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,24 +66,24 @@ export class Viewport implements IViewport {
|
||||
* Updates dimensions and synchronizes the scroll area if necessary.
|
||||
*/
|
||||
public syncScrollArea(): void {
|
||||
if (this._lastRecordedBufferLength !== this._terminal.buffer.lines.length) {
|
||||
if (this.lastRecordedBufferLength !== this.terminal.buffer.lines.length) {
|
||||
// If buffer height changed
|
||||
this._lastRecordedBufferLength = this._terminal.buffer.lines.length;
|
||||
this._refresh();
|
||||
} else if (this._lastRecordedViewportHeight !== (<any>this._terminal).renderer.dimensions.canvasHeight) {
|
||||
this.lastRecordedBufferLength = this.terminal.buffer.lines.length;
|
||||
this.refresh();
|
||||
} else if (this.lastRecordedViewportHeight !== (<any>this.terminal).renderer.dimensions.canvasHeight) {
|
||||
// If viewport height changed
|
||||
this._refresh();
|
||||
this.refresh();
|
||||
} else {
|
||||
// If size has changed, refresh viewport
|
||||
if (this._terminal.renderer.dimensions.scaledCellHeight / window.devicePixelRatio !== this._currentRowHeight) {
|
||||
this._refresh();
|
||||
if (this.terminal.renderer.dimensions.scaledCellHeight / window.devicePixelRatio !== this.currentRowHeight) {
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
// Sync scrollTop
|
||||
const scrollTop = this._terminal.buffer.ydisp * this._currentRowHeight;
|
||||
if (this._viewportElement.scrollTop !== scrollTop) {
|
||||
this._viewportElement.scrollTop = scrollTop;
|
||||
const scrollTop = this.terminal.buffer.ydisp * this.currentRowHeight;
|
||||
if (this.viewportElement.scrollTop !== scrollTop) {
|
||||
this.viewportElement.scrollTop = scrollTop;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,16 +92,10 @@ export class Viewport implements IViewport {
|
||||
* terminal to scroll to it.
|
||||
* @param ev The scroll event.
|
||||
*/
|
||||
private _onScroll(ev: Event): void {
|
||||
// Don't attempt to scroll if the element is not visible, otherwise scrollTop will be corrupt
|
||||
// which causes the terminal to scroll the buffer to the top
|
||||
if (!this._viewportElement.offsetParent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newRow = Math.round(this._viewportElement.scrollTop / this._currentRowHeight);
|
||||
const diff = newRow - this._terminal.buffer.ydisp;
|
||||
this._terminal.scrollLines(diff, true);
|
||||
private onScroll(ev: Event): void {
|
||||
const newRow = Math.round(this.viewportElement.scrollTop / this.currentRowHeight);
|
||||
const diff = newRow - this.terminal.buffer.ydisp;
|
||||
this.terminal.scrollLines(diff, true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -118,74 +105,41 @@ export class Viewport implements IViewport {
|
||||
* @param ev The mouse wheel event.
|
||||
*/
|
||||
public onWheel(ev: WheelEvent): void {
|
||||
const amount = this._getPixelsScrolled(ev);
|
||||
if (amount === 0) {
|
||||
if (ev.deltaY === 0) {
|
||||
// Do nothing if it's not a vertical scroll event
|
||||
return;
|
||||
}
|
||||
this._viewportElement.scrollTop += amount;
|
||||
// Fallback to WheelEvent.DOM_DELTA_PIXEL
|
||||
let multiplier = 1;
|
||||
if (ev.deltaMode === WheelEvent.DOM_DELTA_LINE) {
|
||||
multiplier = this.currentRowHeight;
|
||||
} else if (ev.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
|
||||
multiplier = this.currentRowHeight * this.terminal.rows;
|
||||
}
|
||||
this.viewportElement.scrollTop += ev.deltaY * multiplier;
|
||||
// Prevent the page from scrolling when the terminal scrolls
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
private _getPixelsScrolled(ev: WheelEvent): number {
|
||||
// Do nothing if it's not a vertical scroll event
|
||||
if (ev.deltaY === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Fallback to WheelEvent.DOM_DELTA_PIXEL
|
||||
let amount = ev.deltaY;
|
||||
if (ev.deltaMode === WheelEvent.DOM_DELTA_LINE) {
|
||||
amount *= this._currentRowHeight;
|
||||
} else if (ev.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
|
||||
amount *= this._currentRowHeight * this._terminal.rows;
|
||||
}
|
||||
return amount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of pixels scrolled by the mouse event taking into account what type of delta
|
||||
* is being used.
|
||||
* @param ev The mouse wheel event.
|
||||
*/
|
||||
public getLinesScrolled(ev: WheelEvent): number {
|
||||
// Do nothing if it's not a vertical scroll event
|
||||
if (ev.deltaY === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Fallback to WheelEvent.DOM_DELTA_LINE
|
||||
let amount = ev.deltaY;
|
||||
if (ev.deltaMode === WheelEvent.DOM_DELTA_PIXEL) {
|
||||
amount /= this._currentRowHeight + 0.0; // Prevent integer division
|
||||
this._wheelPartialScroll += amount;
|
||||
amount = Math.floor(Math.abs(this._wheelPartialScroll)) * (this._wheelPartialScroll > 0 ? 1 : -1);
|
||||
this._wheelPartialScroll %= 1;
|
||||
} else if (ev.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
|
||||
amount *= this._terminal.rows;
|
||||
}
|
||||
return amount;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles the touchstart event, recording the touch occurred.
|
||||
* @param ev The touch event.
|
||||
*/
|
||||
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.
|
||||
* @param ev The touch event.
|
||||
*/
|
||||
public onTouchMove(ev: TouchEvent): void {
|
||||
let deltaY = this._lastTouchY - ev.touches[0].pageY;
|
||||
this._lastTouchY = ev.touches[0].pageY;
|
||||
let deltaY = this.lastTouchY - ev.touches[0].pageY;
|
||||
this.lastTouchY = ev.touches[0].pageY;
|
||||
if (deltaY === 0) {
|
||||
return;
|
||||
}
|
||||
this._viewportElement.scrollTop += deltaY;
|
||||
this.viewportElement.scrollTop += deltaY;
|
||||
ev.preventDefault();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -5,14 +5,14 @@
|
||||
|
||||
import { assert, expect } from 'chai';
|
||||
|
||||
import * as attach from './attach';
|
||||
import * as attach from './attach'
|
||||
|
||||
class MockTerminal {}
|
||||
|
||||
describe('attach addon', () => {
|
||||
describe('apply', () => {
|
||||
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.detach, 'function');
|
||||
});
|
||||
|
||||
+62
-82
@@ -5,138 +5,118 @@
|
||||
* 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.
|
||||
*
|
||||
* @param term The terminal to be attached to the given socket.
|
||||
* @param socket The socket to attach the current terminal.
|
||||
* @param bidirectional Whether the terminal should send data to the socket as well.
|
||||
* @param buffered Whether the rendering of incoming data should happen instantly or at a maximum
|
||||
* frequency of 1 rendering per 10ms.
|
||||
* @param {Terminal} term - The terminal to be attached to the given socket.
|
||||
* @param {WebSocket} socket - The socket to attach the current terminal.
|
||||
* @param {boolean} bidirectional - Whether the terminal should send data
|
||||
* to the socket as well.
|
||||
* @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 {
|
||||
const addonTerminal = <IAttachAddonTerminal>term;
|
||||
bidirectional = (typeof bidirectional === 'undefined') ? true : bidirectional;
|
||||
addonTerminal.__socket = socket;
|
||||
export function attach(term, socket, bidirectional, buffered) {
|
||||
bidirectional = (typeof bidirectional == 'undefined') ? true : bidirectional;
|
||||
term.socket = socket;
|
||||
|
||||
addonTerminal.__flushBuffer = () => {
|
||||
addonTerminal.write(addonTerminal.__attachSocketBuffer);
|
||||
addonTerminal.__attachSocketBuffer = null;
|
||||
term._flushBuffer = function() {
|
||||
term.write(term._attachSocketBuffer);
|
||||
term._attachSocketBuffer = null;
|
||||
};
|
||||
|
||||
addonTerminal.__pushToBuffer = (data: string) => {
|
||||
if (addonTerminal.__attachSocketBuffer) {
|
||||
addonTerminal.__attachSocketBuffer += data;
|
||||
term._pushToBuffer = function(data) {
|
||||
if (term._attachSocketBuffer) {
|
||||
term._attachSocketBuffer += data;
|
||||
} else {
|
||||
addonTerminal.__attachSocketBuffer = data;
|
||||
setTimeout(addonTerminal.__flushBuffer, 10);
|
||||
term._attachSocketBuffer = data;
|
||||
setTimeout(term._flushBuffer, 10);
|
||||
}
|
||||
};
|
||||
|
||||
let myTextDecoder;
|
||||
var myTextDecoder;
|
||||
|
||||
addonTerminal.__getMessage = function(ev: MessageEvent): void {
|
||||
let str;
|
||||
|
||||
if (typeof ev.data == 'object') {
|
||||
if (!myTextDecoder) {
|
||||
myTextDecoder = new TextDecoder();
|
||||
}
|
||||
term._getMessage = function(ev) {
|
||||
var str;
|
||||
if (typeof ev.data === "object") {
|
||||
if (ev.data instanceof ArrayBuffer) {
|
||||
str = myTextDecoder.decode(ev.data);
|
||||
displayData(str);
|
||||
} else {
|
||||
let fileReader = new FileReader();
|
||||
if (!myTextDecoder) {
|
||||
myTextDecoder = new TextDecoder();
|
||||
}
|
||||
|
||||
fileReader.addEventListener('load', function() {
|
||||
str = myTextDecoder.decode(this.result);
|
||||
displayData(str);
|
||||
});
|
||||
fileReader.readAsArrayBuffer(ev.data);
|
||||
str = myTextDecoder.decode( ev.data );
|
||||
}
|
||||
} else if (typeof ev.data == 'string') {
|
||||
displayData(ev.data)
|
||||
else {
|
||||
throw "TODO: handle Blob?";
|
||||
}
|
||||
}
|
||||
|
||||
if (buffered) {
|
||||
term._pushToBuffer(str || ev.data);
|
||||
} else {
|
||||
throw Error(`Cannot handle "${typeof ev.data}" websocket message.`);
|
||||
term.write(str || ev.data);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Push data to buffer or write it in the terminal.
|
||||
* This is used as a callback for FileReader.onload.
|
||||
*
|
||||
* @param str String decoded by FileReader.
|
||||
* @param data The data of the EventMessage.
|
||||
*/
|
||||
function displayData(str?: string, data?: string) {
|
||||
if (buffered) {
|
||||
addonTerminal.__pushToBuffer(str || data);
|
||||
} else {
|
||||
addonTerminal.write(str || data);
|
||||
}
|
||||
}
|
||||
|
||||
addonTerminal.__sendData = (data: string) => {
|
||||
term._sendData = function(data) {
|
||||
if (socket.readyState !== 1) {
|
||||
return;
|
||||
}
|
||||
socket.send(data);
|
||||
};
|
||||
|
||||
socket.addEventListener('message', addonTerminal.__getMessage);
|
||||
socket.addEventListener('message', term._getMessage);
|
||||
|
||||
if (bidirectional) {
|
||||
addonTerminal.on('data', addonTerminal.__sendData);
|
||||
term.on('data', term._sendData);
|
||||
}
|
||||
|
||||
socket.addEventListener('close', () => detach(addonTerminal, socket));
|
||||
socket.addEventListener('error', () => detach(addonTerminal, socket));
|
||||
}
|
||||
socket.addEventListener('close', term.detach.bind(term, socket));
|
||||
socket.addEventListener('error', term.detach.bind(term, socket));
|
||||
};
|
||||
|
||||
/**
|
||||
* Detaches the given terminal from the given socket
|
||||
*
|
||||
* @param term The terminal to be detached from the given socket.
|
||||
* @param socket The socket from which to detach the current terminal.
|
||||
* @param {Terminal} term - The terminal to be detached from the given socket.
|
||||
* @param {WebSocket} socket - The socket from which to detach the current
|
||||
* terminal.
|
||||
*/
|
||||
export function detach(term: Terminal, socket: WebSocket): void {
|
||||
const addonTerminal = <IAttachAddonTerminal>term;
|
||||
addonTerminal.off('data', addonTerminal.__sendData);
|
||||
export function detach(term, socket) {
|
||||
term.off('data', term._sendData);
|
||||
|
||||
socket = (typeof socket === 'undefined') ? addonTerminal.__socket : socket;
|
||||
socket = (typeof socket == 'undefined') ? term.socket : 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
|
||||
*
|
||||
* @param socket The socket to attach the current terminal.
|
||||
* @param bidirectional Whether the terminal should send data to the socket as well.
|
||||
* @param buffered Whether the rendering of incoming data should happen instantly or at a maximum
|
||||
* frequency of 1 rendering per 10ms.
|
||||
* @param {WebSocket} socket - The socket to attach the current terminal.
|
||||
* @param {boolean} bidirectional - Whether the terminal should send data
|
||||
* to the socket as well.
|
||||
* @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 {
|
||||
attach(this, socket, bidirectional, buffered);
|
||||
terminalConstructor.prototype.attach = function(socket, bidirectional, buffered) {
|
||||
return attach(this, socket, bidirectional, buffered);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
detach(this, socket);
|
||||
terminalConstructor.prototype.detach = function(socket) {
|
||||
return detach(this, socket);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"rootDir": ".",
|
||||
"outDir": "../../../lib/addons/attach/",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"declaration": true
|
||||
"removeComments": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,14 +5,14 @@
|
||||
|
||||
import { assert, expect } from 'chai';
|
||||
|
||||
import * as fit from './fit';
|
||||
import * as fit from './fit'
|
||||
|
||||
class MockTerminal {}
|
||||
|
||||
describe('fit addon', () => {
|
||||
describe('apply', () => {
|
||||
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.fit, 'function');
|
||||
});
|
||||
|
||||
+25
-39
@@ -13,57 +13,43 @@
|
||||
* row and truncate its width with the current number of columns).
|
||||
*/
|
||||
|
||||
/// <reference path="../../../typings/xterm.d.ts"/>
|
||||
|
||||
import { Terminal } from 'xterm';
|
||||
|
||||
export interface IGeometry {
|
||||
rows: number;
|
||||
cols: number;
|
||||
}
|
||||
|
||||
export function proposeGeometry(term: Terminal): IGeometry {
|
||||
export function proposeGeometry(term) {
|
||||
if (!term.element.parentElement) {
|
||||
return null;
|
||||
}
|
||||
const parentElementStyle = window.getComputedStyle(term.element.parentElement);
|
||||
const parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height'));
|
||||
const parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')));
|
||||
const elementStyle = window.getComputedStyle(term.element);
|
||||
const elementPadding = {
|
||||
top: parseInt(elementStyle.getPropertyValue('padding-top')),
|
||||
bottom: parseInt(elementStyle.getPropertyValue('padding-bottom')),
|
||||
right: parseInt(elementStyle.getPropertyValue('padding-right')),
|
||||
left: parseInt(elementStyle.getPropertyValue('padding-left'))
|
||||
var parentElementStyle = window.getComputedStyle(term.element.parentElement);
|
||||
var parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height'));
|
||||
var parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')) - 17);
|
||||
var elementStyle = window.getComputedStyle(term.element);
|
||||
var elementPaddingVer = parseInt(elementStyle.getPropertyValue('padding-top')) + parseInt(elementStyle.getPropertyValue('padding-bottom'));
|
||||
var elementPaddingHor = parseInt(elementStyle.getPropertyValue('padding-right')) + parseInt(elementStyle.getPropertyValue('padding-left'));
|
||||
var availableHeight = parentElementHeight - elementPaddingVer;
|
||||
var availableWidth = parentElementWidth - elementPaddingHor;
|
||||
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 {
|
||||
const geometry = proposeGeometry(term);
|
||||
return geometry;
|
||||
};
|
||||
|
||||
export function fit(term) {
|
||||
var geometry = proposeGeometry(term);
|
||||
if (geometry) {
|
||||
// Force a full render
|
||||
if (term.rows !== geometry.rows || term.cols !== geometry.cols) {
|
||||
(<any>term).renderer.clear();
|
||||
term.renderer.clear();
|
||||
term.resize(geometry.cols, geometry.rows);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export function apply(terminalConstructor: typeof Terminal): void {
|
||||
(<any>terminalConstructor.prototype).proposeGeometry = function (): IGeometry {
|
||||
export function apply(terminalConstructor) {
|
||||
terminalConstructor.prototype.proposeGeometry = function() {
|
||||
return proposeGeometry(this);
|
||||
};
|
||||
}
|
||||
|
||||
(<any>terminalConstructor.prototype).fit = function (): void {
|
||||
fit(this);
|
||||
};
|
||||
terminalConstructor.prototype.fit = function() {
|
||||
return fit(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"rootDir": ".",
|
||||
"outDir": "../../../lib/addons/fit/",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"declaration": true
|
||||
"removeComments": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,14 +5,14 @@
|
||||
|
||||
import { assert, expect } from 'chai';
|
||||
|
||||
import * as fullscreen from './fullscreen';
|
||||
import * as fullscreen from './fullscreen'
|
||||
|
||||
class MockTerminal {}
|
||||
|
||||
describe('fullscreen addon', () => {
|
||||
describe('apply', () => {
|
||||
it('should do register the `toggleFullscreen` method', () => {
|
||||
fullscreen.apply(<any>MockTerminal);
|
||||
fullscreen.apply(MockTerminal);
|
||||
assert.equal(typeof (<any>MockTerminal).prototype.toggleFullScreen, 'function');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,19 +3,15 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/// <reference path="../../../typings/xterm.d.ts"/>
|
||||
|
||||
import { Terminal } from 'xterm';
|
||||
|
||||
/**
|
||||
* Toggle the given terminal's fullscreen mode.
|
||||
* @param term The terminal to toggle full screen mode
|
||||
* @param fullscreen Toggle fullscreen on (true) or off (false)
|
||||
* @param {Terminal} term - The terminal to toggle full screen mode
|
||||
* @param {boolean} fullscreen - Toggle fullscreen on (true) or off (false)
|
||||
*/
|
||||
export function toggleFullScreen(term: Terminal, fullscreen: boolean): void {
|
||||
let fn: string;
|
||||
export function toggleFullScreen(term, fullscreen) {
|
||||
var fn;
|
||||
|
||||
if (typeof fullscreen === 'undefined') {
|
||||
if (typeof fullscreen == 'undefined') {
|
||||
fn = (term.element.classList.contains('fullscreen')) ? 'remove' : 'add';
|
||||
} else if (!fullscreen) {
|
||||
fn = 'remove';
|
||||
@@ -24,10 +20,10 @@ export function toggleFullScreen(term: Terminal, fullscreen: boolean): void {
|
||||
}
|
||||
|
||||
term.element.classList[fn]('fullscreen');
|
||||
}
|
||||
};
|
||||
|
||||
export function apply(terminalConstructor: typeof Terminal): void {
|
||||
(<any>terminalConstructor.prototype).toggleFullScreen = function (fullscreen: boolean): void {
|
||||
toggleFullScreen(this, fullscreen);
|
||||
export function apply(terminalConstructor) {
|
||||
terminalConstructor.prototype.toggleFullScreen = function (fullscreen) {
|
||||
return toggleFullScreen(this, fullscreen);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"rootDir": ".",
|
||||
"outDir": "../../../lib/addons/fullscreen/",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"declaration": true
|
||||
"removeComments": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -3,7 +3,8 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ISearchHelper, ISearchAddonTerminal } from './Interfaces';
|
||||
// import { ITerminal } from '../../Interfaces';
|
||||
// import { translateBufferLineToString } from '../../utils/BufferLine';
|
||||
|
||||
interface ISearchResult {
|
||||
term: string;
|
||||
@@ -14,8 +15,8 @@ interface ISearchResult {
|
||||
/**
|
||||
* A class that knows how to search the terminal and how to display the results.
|
||||
*/
|
||||
export class SearchHelper implements ISearchHelper {
|
||||
constructor(private _terminal: ISearchAddonTerminal) {
|
||||
export class SearchHelper {
|
||||
constructor(private _terminal: any) {
|
||||
// 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: Highlight other instances in the viewport
|
||||
@@ -113,23 +114,8 @@ export class SearchHelper implements ISearchHelper {
|
||||
private _findInLine(term: string, y: number): ISearchResult {
|
||||
const lowerStringLine = this._terminal.buffer.translateBufferLineToString(y, true).toLowerCase();
|
||||
const lowerTerm = term.toLowerCase();
|
||||
let searchIndex = lowerStringLine.indexOf(lowerTerm);
|
||||
const searchIndex = lowerStringLine.indexOf(lowerTerm);
|
||||
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 {
|
||||
term,
|
||||
col: searchIndex,
|
||||
@@ -148,7 +134,7 @@ export class SearchHelper implements ISearchHelper {
|
||||
return false;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
+16
-21
@@ -3,11 +3,8 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/// <reference path="../../../typings/xterm.d.ts"/>
|
||||
|
||||
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
|
||||
@@ -15,13 +12,12 @@ import { ISearchAddonTerminal } from './Interfaces';
|
||||
* @param term Tne search term.
|
||||
* @return Whether a result was found.
|
||||
*/
|
||||
export function findNext(terminal: Terminal, term: string): boolean {
|
||||
const addonTerminal = <ISearchAddonTerminal>terminal;
|
||||
if (!addonTerminal.__searchHelper) {
|
||||
addonTerminal.__searchHelper = new SearchHelper(addonTerminal);
|
||||
export function findNext(terminal: any, term: string): boolean {
|
||||
if (!terminal._searchHelper) {
|
||||
terminal.searchHelper = new SearchHelper(terminal);
|
||||
}
|
||||
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
|
||||
@@ -29,20 +25,19 @@ export function findNext(terminal: Terminal, term: string): boolean {
|
||||
* @param term Tne search term.
|
||||
* @return Whether a result was found.
|
||||
*/
|
||||
export function findPrevious(terminal: Terminal, term: string): boolean {
|
||||
const addonTerminal = <ISearchAddonTerminal>terminal;
|
||||
if (!addonTerminal.__searchHelper) {
|
||||
addonTerminal.__searchHelper = new SearchHelper(addonTerminal);
|
||||
export function findPrevious(terminal: any, term: string): boolean {
|
||||
if (!terminal._searchHelper) {
|
||||
terminal.searchHelper = new SearchHelper(terminal);
|
||||
}
|
||||
return addonTerminal.__searchHelper.findPrevious(term);
|
||||
}
|
||||
return (<SearchHelper>terminal.searchHelper).findPrevious(term);
|
||||
};
|
||||
|
||||
export function apply(terminalConstructor: typeof Terminal): void {
|
||||
(<any>terminalConstructor.prototype).findNext = function(term: string): boolean {
|
||||
export function apply(terminalConstructor) {
|
||||
terminalConstructor.prototype.findNext = function(term) {
|
||||
return findNext(this, term);
|
||||
};
|
||||
}
|
||||
|
||||
(<any>terminalConstructor.prototype).findPrevious = function(term: string): boolean {
|
||||
terminalConstructor.prototype.findPrevious = function(term) {
|
||||
return findPrevious(this, term);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"rootDir": ".",
|
||||
"outDir": "../../../lib/addons/search/",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"declaration": true
|
||||
"removeComments": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -5,14 +5,14 @@
|
||||
|
||||
import { assert, expect } from 'chai';
|
||||
|
||||
import * as terminado from './terminado';
|
||||
import * as terminado from './terminado'
|
||||
|
||||
class MockTerminal {}
|
||||
|
||||
describe('terminado addon', () => {
|
||||
describe('apply', () => {
|
||||
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.terminadoDetach, 'function');
|
||||
});
|
||||
|
||||
@@ -6,107 +6,106 @@
|
||||
* WebSocket stream.
|
||||
*/
|
||||
|
||||
/// <reference path="../../../typings/xterm.d.ts"/>
|
||||
|
||||
import { Terminal } from 'xterm';
|
||||
import { ITerminadoAddonTerminal } from './Interfaces';
|
||||
|
||||
/**
|
||||
* Attaches the given terminal to the given socket.
|
||||
*
|
||||
* @param term The terminal to be attached to the given socket.
|
||||
* @param socket The socket to attach the current terminal.
|
||||
* @param bidirectional Whether the terminal should send data to the socket as well.
|
||||
* @param buffered Whether the rendering of incoming data should happen instantly or at a maximum
|
||||
* frequency of 1 rendering per 10ms.
|
||||
* @param {Terminal} term - The terminal to be attached to the given socket.
|
||||
* @param {WebSocket} socket - The socket to attach the current terminal.
|
||||
* @param {boolean} bidirectional - Whether the terminal should send data
|
||||
* to the socket as well.
|
||||
* @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 {
|
||||
const addonTerminal = <ITerminadoAddonTerminal>term;
|
||||
bidirectional = (typeof bidirectional === 'undefined') ? true : bidirectional;
|
||||
addonTerminal.__socket = socket;
|
||||
export function terminadoAttach(term, socket, bidirectional, buffered) {
|
||||
bidirectional = (typeof bidirectional == 'undefined') ? true : bidirectional;
|
||||
term.socket = socket;
|
||||
|
||||
addonTerminal.__flushBuffer = () => {
|
||||
addonTerminal.write(addonTerminal.__attachSocketBuffer);
|
||||
addonTerminal.__attachSocketBuffer = null;
|
||||
term._flushBuffer = function() {
|
||||
term.write(term._attachSocketBuffer);
|
||||
term._attachSocketBuffer = null;
|
||||
};
|
||||
|
||||
addonTerminal.__pushToBuffer = (data: string) => {
|
||||
if (addonTerminal.__attachSocketBuffer) {
|
||||
addonTerminal.__attachSocketBuffer += data;
|
||||
term._pushToBuffer = function(data) {
|
||||
if (term._attachSocketBuffer) {
|
||||
term._attachSocketBuffer += data;
|
||||
} else {
|
||||
addonTerminal.__attachSocketBuffer = data;
|
||||
setTimeout(addonTerminal.__flushBuffer, 10);
|
||||
term._attachSocketBuffer = data;
|
||||
setTimeout(term._flushBuffer, 10);
|
||||
}
|
||||
};
|
||||
|
||||
addonTerminal.__getMessage = (ev: MessageEvent) => {
|
||||
const data = JSON.parse(ev.data);
|
||||
if (data[0] === 'stdout') {
|
||||
term._getMessage = function(ev) {
|
||||
var data = JSON.parse(ev.data)
|
||||
if( data[0] == "stdout" ) {
|
||||
if (buffered) {
|
||||
addonTerminal.__pushToBuffer(data[1]);
|
||||
term._pushToBuffer(data[1]);
|
||||
} else {
|
||||
addonTerminal.write(data[1]);
|
||||
term.write(data[1]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
addonTerminal.__sendData = (data: string) => {
|
||||
term._sendData = function(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.addEventListener('message', addonTerminal.__getMessage);
|
||||
socket.addEventListener('message', term._getMessage);
|
||||
|
||||
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('error', () => terminadoDetach(addonTerminal, socket));
|
||||
}
|
||||
socket.addEventListener('close', term.terminadoDetach.bind(term, socket));
|
||||
socket.addEventListener('error', term.terminadoDetach.bind(term, socket));
|
||||
};
|
||||
|
||||
/**
|
||||
* Detaches the given terminal from the given socket
|
||||
*
|
||||
* @param term The terminal to be detached from the given socket.
|
||||
* @param socket The socket from which to detach the current terminal.
|
||||
* @param {Xterm} term - The terminal to be detached from the given socket.
|
||||
* @param {WebSocket} socket - The socket from which to detach the current
|
||||
* terminal.
|
||||
*/
|
||||
export function terminadoDetach(term: Terminal, socket: WebSocket): void {
|
||||
const addonTerminal = <ITerminadoAddonTerminal>term;
|
||||
addonTerminal.off('data', addonTerminal.__sendData);
|
||||
export function terminadoDetach(term, socket) {
|
||||
term.off('data', term._sendData);
|
||||
|
||||
socket = (typeof socket === 'undefined') ? addonTerminal.__socket : socket;
|
||||
socket = (typeof socket == 'undefined') ? term.socket : 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
|
||||
*
|
||||
* @param socket - The socket to attach the current terminal.
|
||||
* @param bidirectional - Whether the terminal should send data to the socket as well.
|
||||
* @param buffered - Whether the rendering of incoming data should happen instantly or at a
|
||||
* maximum frequency of 1 rendering per 10ms.
|
||||
* @param {WebSocket} socket - The socket to attach the current terminal.
|
||||
* @param {boolean} bidirectional - Whether the terminal should send data
|
||||
* to the socket as well.
|
||||
* @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);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"rootDir": ".",
|
||||
"outDir": "../../../lib/addons/terminado/",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"declaration": true
|
||||
"removeComments": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"name": "xterm.weblinks",
|
||||
"main": "weblinks.js",
|
||||
"private": true
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"rootDir": ".",
|
||||
"outDir": "../../../lib/addons/webLinks/",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"declaration": true
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
@@ -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,5 +1,5 @@
|
||||
{
|
||||
"name": "xterm.winptycompat",
|
||||
"name": "xterm.winptyCompat",
|
||||
"main": "winptyCompat.js",
|
||||
"private": true
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"rootDir": ".",
|
||||
"outDir": "../../../lib/addons/winptyCompat/",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"declaration": true
|
||||
"removeComments": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,14 +5,14 @@
|
||||
|
||||
import { assert, expect } from 'chai';
|
||||
|
||||
import * as winptyCompat from './winptyCompat';
|
||||
import * as winptyCompat from './winptyCompat'
|
||||
|
||||
class MockTerminal {}
|
||||
|
||||
describe('winptyCompat addon', () => {
|
||||
describe('apply', () => {
|
||||
it('should do register the `winptyCompatInit` method', () => {
|
||||
winptyCompat.apply(<any>MockTerminal);
|
||||
winptyCompat.apply(MockTerminal);
|
||||
assert.equal(typeof (<any>MockTerminal).prototype.winptyCompatInit, 'function');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,43 +3,36 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/// <reference path="../../../typings/xterm.d.ts"/>
|
||||
|
||||
import { Terminal } from 'xterm';
|
||||
import { IWinptyCompatAddonTerminal } from './Interfaces';
|
||||
|
||||
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;
|
||||
export function winptyCompatInit(terminal): void {
|
||||
// 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.
|
||||
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 {
|
||||
(<any>terminalConstructor.prototype).winptyCompatInit = function (): void {
|
||||
export function apply(terminalConstructor) {
|
||||
terminalConstructor.prototype.winptyCompatInit = function(): void {
|
||||
winptyCompatInit(this);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"rootDir": ".",
|
||||
"outDir": "../../../lib/addons/zmodem/",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"declaration": true
|
||||
"removeComments": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,14 +5,14 @@
|
||||
|
||||
import { assert, expect } from 'chai';
|
||||
|
||||
import * as zmodem from './zmodem';
|
||||
import * as zmodem from './zmodem'
|
||||
|
||||
class MockTerminal {}
|
||||
|
||||
describe('zmodem addon', () => {
|
||||
describe('apply', () => {
|
||||
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.zmodemBrowser, 'object');
|
||||
});
|
||||
|
||||
+29
-31
@@ -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.
|
||||
@@ -36,40 +27,47 @@ import { Terminal } from 'xterm';
|
||||
* via `detach()` and a re-`attach()`.)
|
||||
*/
|
||||
|
||||
let zmodem;
|
||||
let Zmodem;
|
||||
|
||||
export interface IZModemOptions {
|
||||
noTerminalWriteOutsideSession?: boolean;
|
||||
}
|
||||
export function zmodemAttach(term, ws, opts) {
|
||||
if (!opts) opts = {};
|
||||
|
||||
export function zmodemAttach(term: Terminal, ws: WebSocket, opts: IZModemOptions = {}): void {
|
||||
const senderFunc = (octets: ArrayLike<number>) => ws.send(new Uint8Array(octets));
|
||||
var senderFunc = function _ws_sender_func(octets) {
|
||||
ws.send(new Uint8Array(octets));
|
||||
};
|
||||
|
||||
let zsentry;
|
||||
var zsentry;
|
||||
|
||||
function _shouldWrite(): boolean {
|
||||
function _shouldWrite() {
|
||||
return !!zsentry.get_confirmed_session() || !opts.noTerminalWriteOutsideSession;
|
||||
}
|
||||
|
||||
zsentry = new zmodem.Sentry({
|
||||
to_terminal: (octets: ArrayLike<number>) => {
|
||||
zsentry = new Zmodem.Sentry( {
|
||||
to_terminal: function _to_terminal(octets) {
|
||||
if (_shouldWrite()) {
|
||||
term.write(
|
||||
String.fromCharCode.apply(String, octets)
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
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
|
||||
// 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.
|
||||
on_detect: function _on_detect(detection) {
|
||||
term.emit('zmodemDetect', detection);
|
||||
},
|
||||
} );
|
||||
|
||||
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 (_shouldWrite()) {
|
||||
term.write(evt.data);
|
||||
@@ -84,9 +82,9 @@ export function zmodemAttach(term: Terminal, ws: WebSocket, opts: IZModemOptions
|
||||
ws.addEventListener('message', handleWSMessage);
|
||||
}
|
||||
|
||||
export function apply(terminalConstructor: typeof Terminal): void {
|
||||
zmodem = (typeof window === 'object') ? (<any>window).ZModem : {Browser: null}; // Nullify browser for tests
|
||||
export function apply(terminalConstructor) {
|
||||
Zmodem = (typeof window == 'object') ? (<any>window).ZModem : {Browser: null}; // Nullify browser for tests
|
||||
|
||||
(<any>terminalConstructor.prototype).zmodemAttach = zmodemAttach.bind(this, this);
|
||||
(<any>terminalConstructor.prototype).zmodemBrowser = zmodem.Browser;
|
||||
terminalConstructor.prototype.zmodemAttach = zmodemAttach.bind(this, this);
|
||||
terminalConstructor.prototype.zmodemBrowser = Zmodem.Browser
|
||||
}
|
||||
|
||||
@@ -1,267 +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 {
|
||||
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;
|
||||
}
|
||||
@@ -4,22 +4,17 @@
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import * as Terminal from '../Terminal';
|
||||
import * as Clipboard from './Clipboard';
|
||||
|
||||
describe('evaluatePastedTextProcessing', () => {
|
||||
it('should replace carriage return and/or line feed with carriage return', () => {
|
||||
const pastedText = {
|
||||
unix: 'foo\nbar\n',
|
||||
windows: 'foo\r\nbar\r\n'
|
||||
};
|
||||
it('should replace carriage return + line feed with line feed on windows', () => {
|
||||
const pastedText = 'foo\r\nbar\r\n';
|
||||
const processedText = Clipboard.prepareTextForTerminal(pastedText, false);
|
||||
const windowsProcessedText = Clipboard.prepareTextForTerminal(pastedText, true);
|
||||
|
||||
const processedText = {
|
||||
unix: Clipboard.prepareTextForTerminal(pastedText.unix),
|
||||
windows: Clipboard.prepareTextForTerminal(pastedText.windows)
|
||||
};
|
||||
|
||||
assert.equal(processedText.unix, 'foo\rbar\r');
|
||||
assert.equal(processedText.windows, 'foo\rbar\r');
|
||||
assert.equal(processedText, 'foo\r\nbar\r\n');
|
||||
assert.equal(windowsProcessedText, 'foo\rbar\r');
|
||||
});
|
||||
it('should bracket pasted text in bracketedPasteMode', () => {
|
||||
const pastedText = 'foo bar';
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ITerminal, ISelectionManager } from '../Types';
|
||||
import { ITerminal, ISelectionManager } from '../Interfaces';
|
||||
|
||||
interface IWindow extends Window {
|
||||
clipboardData?: {
|
||||
@@ -18,8 +18,11 @@ declare var window: IWindow;
|
||||
* Prepares text to be pasted into the terminal by normalizing the line endings
|
||||
* @param text The pasted text that needs processing before inserting into the terminal
|
||||
*/
|
||||
export function prepareTextForTerminal(text: string): string {
|
||||
return text.replace(/\r?\n/g, '\r');
|
||||
export function prepareTextForTerminal(text: string, isMSWindows: boolean): string {
|
||||
if (isMSWindows) {
|
||||
return text.replace(/\r?\n/g, '\r');
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -59,7 +62,7 @@ export function pasteHandler(ev: ClipboardEvent, term: ITerminal): void {
|
||||
let text: string;
|
||||
|
||||
let dispatchPaste = function(text: string): void {
|
||||
text = prepareTextForTerminal(text);
|
||||
text = prepareTextForTerminal(text, term.browser.isMSWindows);
|
||||
text = bracketTextForPaste(text, term.bracketedPasteMode);
|
||||
term.handler(text);
|
||||
term.textarea.value = '';
|
||||
@@ -97,7 +100,6 @@ export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextA
|
||||
textarea.focus();
|
||||
|
||||
// Reset the terminal textarea's styling
|
||||
// Timeout needs to be long enough for click event to be handled.
|
||||
setTimeout(() => {
|
||||
textarea.style.position = null;
|
||||
textarea.style.width = null;
|
||||
@@ -105,7 +107,7 @@ export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextA
|
||||
textarea.style.left = null;
|
||||
textarea.style.top = null;
|
||||
textarea.style.zIndex = null;
|
||||
}, 200);
|
||||
}, 4);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -113,15 +115,10 @@ export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextA
|
||||
* @param ev The original right click event to be handled.
|
||||
* @param textarea The terminal's textarea.
|
||||
* @param selectionManager The terminal's selection manager.
|
||||
* @param shouldSelectWord If true and there is no selection the current word will be selected
|
||||
*/
|
||||
export function rightClickHandler(ev: MouseEvent, textarea: HTMLTextAreaElement, selectionManager: ISelectionManager, shouldSelectWord: boolean): void {
|
||||
export function rightClickHandler(ev: MouseEvent, textarea: HTMLTextAreaElement, selectionManager: ISelectionManager): void {
|
||||
moveTextAreaUnderMouseCursor(ev, textarea);
|
||||
|
||||
if (shouldSelectWord && !selectionManager.isClickInSelection(ev)) {
|
||||
selectionManager.selectWordAtCursor(ev);
|
||||
}
|
||||
|
||||
// Get textarea ready to copy from the context menu
|
||||
textarea.value = selectionManager.selectionText;
|
||||
textarea.select();
|
||||
|
||||
@@ -11,11 +11,9 @@ export interface IMouseZoneManager {
|
||||
export interface IMouseZone {
|
||||
x1: number;
|
||||
x2: number;
|
||||
y1: number;
|
||||
y2: number;
|
||||
y: number;
|
||||
clickCallback: (e: MouseEvent) => any;
|
||||
hoverCallback: (e: MouseEvent) => any | undefined;
|
||||
tooltipCallback: (e: MouseEvent) => any | undefined;
|
||||
leaveCallback: () => any | undefined;
|
||||
willLinkActivate: (e: MouseEvent) => boolean;
|
||||
hoverCallback?: (e: MouseEvent) => any;
|
||||
tooltipCallback?: (e: MouseEvent) => any;
|
||||
leaveCallback?: () => any;
|
||||
}
|
||||
@@ -3,8 +3,8 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ITerminal } from '../Types';
|
||||
import { IMouseZoneManager, IMouseZone } from './Types';
|
||||
import { IMouseZoneManager, IMouseZone } from './Interfaces';
|
||||
import { ITerminal } from '../Interfaces';
|
||||
|
||||
const HOVER_DURATION = 500;
|
||||
|
||||
@@ -59,9 +59,7 @@ export class MouseZoneManager implements IMouseZoneManager {
|
||||
// Iterate through zones and clear them out if they're within the range
|
||||
for (let i = 0; i < this._zones.length; i++) {
|
||||
const zone = this._zones[i];
|
||||
if ((zone.y1 > start && zone.y1 <= end + 1) ||
|
||||
(zone.y2 > start && zone.y2 <= end + 1) ||
|
||||
(zone.y1 < start && zone.y2 > end + 1)) {
|
||||
if (zone.y > start && zone.y <= end + 1) {
|
||||
if (this._currentZone && this._currentZone === zone) {
|
||||
this._currentZone.leaveCallback();
|
||||
this._currentZone = null;
|
||||
@@ -153,10 +151,10 @@ export class MouseZoneManager implements IMouseZoneManager {
|
||||
// components from handling the mouse event.
|
||||
const zone = this._findZoneEventAt(e);
|
||||
if (zone) {
|
||||
if (zone.willLinkActivate(e)) {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
}
|
||||
// TODO: When link modifier support is added, the event should only be
|
||||
// cancelled when the modifier is held (see #1021)
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,28 +169,16 @@ export class MouseZoneManager implements IMouseZoneManager {
|
||||
}
|
||||
|
||||
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) {
|
||||
return null;
|
||||
}
|
||||
const x = coords[0];
|
||||
const y = coords[1];
|
||||
for (let i = 0; i < this._zones.length; i++) {
|
||||
const zone = this._zones[i];
|
||||
if (zone.y1 === zone.y2) {
|
||||
// Single line link
|
||||
if (y === zone.y1 && x >= zone.x1 && x < zone.x2) {
|
||||
return zone;
|
||||
}
|
||||
} else {
|
||||
// Multi-line link
|
||||
if ((y === zone.y1 && x >= zone.x1) ||
|
||||
(y === zone.y2 && x < zone.x2) ||
|
||||
(y > zone.y1 && y < zone.y2)) {
|
||||
return zone;
|
||||
}
|
||||
if (zone.y === coords[1] && zone.x1 <= coords[0] && zone.x2 > coords[0]) {
|
||||
return zone;
|
||||
}
|
||||
}
|
||||
};
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -200,14 +186,12 @@ export class MouseZoneManager implements IMouseZoneManager {
|
||||
export class MouseZone implements IMouseZone {
|
||||
constructor(
|
||||
public x1: number,
|
||||
public y1: number,
|
||||
public x2: number,
|
||||
public y2: number,
|
||||
public y: number,
|
||||
public clickCallback: (e: MouseEvent) => any,
|
||||
public hoverCallback: (e: MouseEvent) => any,
|
||||
public tooltipCallback: (e: MouseEvent) => any,
|
||||
public leaveCallback: () => void,
|
||||
public willLinkActivate: (e: MouseEvent) => boolean
|
||||
public hoverCallback?: (e: MouseEvent) => any,
|
||||
public tooltipCallback?: (e: MouseEvent) => any,
|
||||
public leaveCallback?: () => void
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IRenderLayer, IColorSet, IRenderDimensions } from './Types';
|
||||
import { CharData, ITerminal } from '../Types';
|
||||
import { DIM_OPACITY, INVERTED_DEFAULT_COLOR } from './atlas/Types';
|
||||
import { CHAR_ATLAS_CELL_SPACING } from '../shared/atlas/Types';
|
||||
import { acquireCharAtlas } from './atlas/CharAtlas';
|
||||
import { CHAR_DATA_CHAR_INDEX } from '../Buffer';
|
||||
import { IRenderLayer, IColorSet, IRenderDimensions } from './Interfaces';
|
||||
import { ITerminal, ITerminalOptions } from '../Interfaces';
|
||||
import { acquireCharAtlas, CHAR_ATLAS_CELL_SPACING } from './CharAtlas';
|
||||
import { CharData } from '../Types';
|
||||
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer';
|
||||
|
||||
export const INVERTED_DEFAULT_COLOR = -1;
|
||||
const DIM_OPACITY = 0.5;
|
||||
|
||||
export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
private _canvas: HTMLCanvasElement;
|
||||
@@ -23,25 +25,22 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
private _charAtlas: HTMLCanvasElement | ImageBitmap;
|
||||
|
||||
constructor(
|
||||
private _container: HTMLElement,
|
||||
container: HTMLElement,
|
||||
id: string,
|
||||
zIndex: number,
|
||||
private _alpha: boolean,
|
||||
protected _colors: IColorSet
|
||||
) {
|
||||
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._initCanvas();
|
||||
this._container.appendChild(this._canvas);
|
||||
}
|
||||
|
||||
private _initCanvas(): void {
|
||||
this._ctx = this._canvas.getContext('2d', {alpha: this._alpha});
|
||||
this._ctx = this._canvas.getContext('2d', {alpha: _alpha});
|
||||
this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
|
||||
// Draw the background if this is an opaque layer
|
||||
if (!this._alpha) {
|
||||
if (!_alpha) {
|
||||
this.clearAll();
|
||||
}
|
||||
container.appendChild(this._canvas);
|
||||
}
|
||||
|
||||
public onOptionsChanged(terminal: ITerminal): void {}
|
||||
@@ -55,25 +54,6 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
this._refreshCharAtlas(terminal, colorSet);
|
||||
}
|
||||
|
||||
protected setTransparency(terminal: ITerminal, alpha: boolean): void {
|
||||
// Do nothing when alpha doesn't change
|
||||
if (alpha === this._alpha) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create new canvas and replace old one
|
||||
const oldCanvas = this._canvas;
|
||||
this._alpha = alpha;
|
||||
// Cloning preserves properties
|
||||
this._canvas = <HTMLCanvasElement>this._canvas.cloneNode();
|
||||
this._initCanvas();
|
||||
this._container.replaceChild(this._canvas, oldCanvas);
|
||||
|
||||
// Regenerate char atlas and force a full redraw
|
||||
this._refreshCharAtlas(terminal, this._colors);
|
||||
this.onGridChanged(terminal, 0, terminal.rows - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the char atlas, aquiring a new one if necessary.
|
||||
* @param terminal The terminal.
|
||||
@@ -84,7 +64,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
return;
|
||||
}
|
||||
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) {
|
||||
this._charAtlas = result;
|
||||
} else {
|
||||
@@ -92,7 +72,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
}
|
||||
}
|
||||
|
||||
public resize(terminal: ITerminal, dim: IRenderDimensions): void {
|
||||
public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void {
|
||||
this._scaledCellWidth = dim.scaledCellWidth;
|
||||
this._scaledCellHeight = dim.scaledCellHeight;
|
||||
this._scaledCharWidth = dim.scaledCharWidth;
|
||||
@@ -109,7 +89,9 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
this.clearAll();
|
||||
}
|
||||
|
||||
this._refreshCharAtlas(terminal, this._colors);
|
||||
if (charSizeChanged) {
|
||||
this._refreshCharAtlas(terminal, this._colors);
|
||||
}
|
||||
}
|
||||
|
||||
public abstract reset(terminal: ITerminal): void;
|
||||
@@ -179,7 +161,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
if (this._alpha) {
|
||||
this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
|
||||
} else {
|
||||
this._ctx.fillStyle = this._colors.background.css;
|
||||
this._ctx.fillStyle = this._colors.background;
|
||||
this._ctx.fillRect(0, 0, this._canvas.width, this._canvas.height);
|
||||
}
|
||||
}
|
||||
@@ -199,7 +181,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
width * this._scaledCellWidth,
|
||||
height * this._scaledCellHeight);
|
||||
} else {
|
||||
this._ctx.fillStyle = this._colors.background.css;
|
||||
this._ctx.fillStyle = this._colors.background;
|
||||
this._ctx.fillRect(
|
||||
x * this._scaledCellWidth,
|
||||
y * this._scaledCellHeight,
|
||||
@@ -219,7 +201,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
* @param color The color of the character.
|
||||
*/
|
||||
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._clipRow(terminal, y);
|
||||
this._ctx.fillText(
|
||||
@@ -287,7 +269,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
charAtlasCellWidth,
|
||||
this._scaledCharHeight);
|
||||
} 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._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
|
||||
@@ -307,16 +289,19 @@ 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 {
|
||||
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';
|
||||
|
||||
if (fg === INVERTED_DEFAULT_COLOR) {
|
||||
this._ctx.fillStyle = this._colors.background.css;
|
||||
this._ctx.fillStyle = this._colors.background;
|
||||
} else if (fg < 256) {
|
||||
// 256 color support
|
||||
this._ctx.fillStyle = this._colors.ansi[fg].css;
|
||||
this._ctx.fillStyle = this._colors.ansi[fg];
|
||||
} else {
|
||||
this._ctx.fillStyle = this._colors.foreground.css;
|
||||
this._ctx.fillStyle = this._colors.foreground;
|
||||
}
|
||||
|
||||
this._clipRow(terminal, y);
|
||||
@@ -347,16 +332,5 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
this._scaledCellHeight);
|
||||
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}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ITerminal, ITheme } from '../Interfaces';
|
||||
import { IColorSet } from '../renderer/Interfaces';
|
||||
import { isFirefox } from '../utils/Browser';
|
||||
|
||||
export const CHAR_ATLAS_CELL_SPACING = 1;
|
||||
|
||||
interface ICharAtlasConfig {
|
||||
fontSize: number;
|
||||
fontFamily: string;
|
||||
scaledCharWidth: number;
|
||||
scaledCharHeight: number;
|
||||
colors: IColorSet;
|
||||
}
|
||||
|
||||
interface ICharAtlasCacheEntry {
|
||||
bitmap: HTMLCanvasElement | Promise<ImageBitmap>;
|
||||
config: ICharAtlasConfig;
|
||||
ownedBy: ITerminal[];
|
||||
}
|
||||
|
||||
let charAtlasCache: ICharAtlasCacheEntry[] = [];
|
||||
|
||||
/**
|
||||
* Acquires a char atlas, either generating a new one or returning an existing
|
||||
* one that is in use by another terminal.
|
||||
* @param terminal The terminal.
|
||||
* @param colors The colors to use.
|
||||
*/
|
||||
export function acquireCharAtlas(terminal: ITerminal, colors: IColorSet, scaledCharWidth: number, scaledCharHeight: number): HTMLCanvasElement | Promise<ImageBitmap> {
|
||||
const newConfig = generateConfig(scaledCharWidth, scaledCharHeight, terminal, colors);
|
||||
|
||||
// Check to see if the terminal already owns this config
|
||||
for (let i = 0; i < charAtlasCache.length; i++) {
|
||||
const entry = charAtlasCache[i];
|
||||
const ownedByIndex = entry.ownedBy.indexOf(terminal);
|
||||
if (ownedByIndex >= 0) {
|
||||
if (configEquals(entry.config, newConfig)) {
|
||||
return entry.bitmap;
|
||||
} else {
|
||||
// The configs differ, release the terminal from the entry
|
||||
if (entry.ownedBy.length === 1) {
|
||||
charAtlasCache.splice(i, 1);
|
||||
} else {
|
||||
entry.ownedBy.splice(ownedByIndex, 1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try match a char atlas from the cache
|
||||
for (let i = 0; i < charAtlasCache.length; i++) {
|
||||
const entry = charAtlasCache[i];
|
||||
if (configEquals(entry.config, newConfig)) {
|
||||
// Add the terminal to the cache entry and return
|
||||
entry.ownedBy.push(terminal);
|
||||
return entry.bitmap;
|
||||
}
|
||||
}
|
||||
|
||||
const newEntry: ICharAtlasCacheEntry = {
|
||||
bitmap: generator.generate(scaledCharWidth, scaledCharHeight, terminal.options.fontSize, terminal.options.fontFamily, colors.background, colors.foreground, colors.ansi),
|
||||
config: newConfig,
|
||||
ownedBy: [terminal]
|
||||
};
|
||||
charAtlasCache.push(newEntry);
|
||||
return newEntry.bitmap;
|
||||
}
|
||||
|
||||
function generateConfig(scaledCharWidth: number, scaledCharHeight: number, terminal: ITerminal, colors: IColorSet): ICharAtlasConfig {
|
||||
const clonedColors = {
|
||||
foreground: colors.foreground,
|
||||
background: colors.background,
|
||||
cursor: null,
|
||||
cursorAccent: null,
|
||||
selection: null,
|
||||
ansi: colors.ansi.slice(0, 16)
|
||||
};
|
||||
return {
|
||||
scaledCharWidth,
|
||||
scaledCharHeight,
|
||||
fontFamily: terminal.options.fontFamily,
|
||||
fontSize: terminal.options.fontSize,
|
||||
colors: clonedColors
|
||||
};
|
||||
}
|
||||
|
||||
function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean {
|
||||
for (let i = 0; i < a.colors.ansi.length; i++) {
|
||||
if (a.colors.ansi[i] !== b.colors.ansi[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return a.fontFamily === b.fontFamily &&
|
||||
a.fontSize === b.fontSize &&
|
||||
a.scaledCharWidth === b.scaledCharWidth &&
|
||||
a.scaledCharHeight === b.scaledCharHeight &&
|
||||
a.colors.foreground === b.colors.foreground &&
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+250
-268
@@ -3,286 +3,268 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import jsdom = require('jsdom');
|
||||
import { assert } from 'chai';
|
||||
import { ColorManager } from './ColorManager';
|
||||
|
||||
describe('ColorManager', () => {
|
||||
let cm: ColorManager;
|
||||
let dom: jsdom.JSDOM;
|
||||
let document: Document;
|
||||
let window: Window;
|
||||
|
||||
beforeEach(() => {
|
||||
dom = new jsdom.JSDOM('');
|
||||
window = dom.window;
|
||||
document = window.document;
|
||||
(<any>window).HTMLCanvasElement.prototype.getContext = () => ({
|
||||
createLinearGradient(): any {
|
||||
return null;
|
||||
},
|
||||
|
||||
fillRect(): void { },
|
||||
|
||||
getImageData(): any {
|
||||
return {data: [0, 0, 0, 0xFF]};
|
||||
}
|
||||
});
|
||||
cm = new ColorManager(document, false);
|
||||
cm = new ColorManager();
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should fill all colors with values', () => {
|
||||
for (let key of Object.keys(cm.colors)) {
|
||||
if (key !== 'ansi') {
|
||||
for (let key in cm.colors) {
|
||||
if (typeof key === 'string') {
|
||||
// A #rrggbb or rgba(...)
|
||||
assert.ok(cm.colors[key].css.length >= 7);
|
||||
assert.ok(cm.colors[key].length >= 7);
|
||||
}
|
||||
}
|
||||
assert.equal(cm.colors.ansi.length, 256);
|
||||
});
|
||||
|
||||
it('should fill 240 colors with expected values', () => {
|
||||
assert.equal(cm.colors.ansi[16].css, '#000000');
|
||||
assert.equal(cm.colors.ansi[17].css, '#00005f');
|
||||
assert.equal(cm.colors.ansi[18].css, '#000087');
|
||||
assert.equal(cm.colors.ansi[19].css, '#0000af');
|
||||
assert.equal(cm.colors.ansi[20].css, '#0000d7');
|
||||
assert.equal(cm.colors.ansi[21].css, '#0000ff');
|
||||
assert.equal(cm.colors.ansi[22].css, '#005f00');
|
||||
assert.equal(cm.colors.ansi[23].css, '#005f5f');
|
||||
assert.equal(cm.colors.ansi[24].css, '#005f87');
|
||||
assert.equal(cm.colors.ansi[25].css, '#005faf');
|
||||
assert.equal(cm.colors.ansi[26].css, '#005fd7');
|
||||
assert.equal(cm.colors.ansi[27].css, '#005fff');
|
||||
assert.equal(cm.colors.ansi[28].css, '#008700');
|
||||
assert.equal(cm.colors.ansi[29].css, '#00875f');
|
||||
assert.equal(cm.colors.ansi[30].css, '#008787');
|
||||
assert.equal(cm.colors.ansi[31].css, '#0087af');
|
||||
assert.equal(cm.colors.ansi[32].css, '#0087d7');
|
||||
assert.equal(cm.colors.ansi[33].css, '#0087ff');
|
||||
assert.equal(cm.colors.ansi[34].css, '#00af00');
|
||||
assert.equal(cm.colors.ansi[35].css, '#00af5f');
|
||||
assert.equal(cm.colors.ansi[36].css, '#00af87');
|
||||
assert.equal(cm.colors.ansi[37].css, '#00afaf');
|
||||
assert.equal(cm.colors.ansi[38].css, '#00afd7');
|
||||
assert.equal(cm.colors.ansi[39].css, '#00afff');
|
||||
assert.equal(cm.colors.ansi[40].css, '#00d700');
|
||||
assert.equal(cm.colors.ansi[41].css, '#00d75f');
|
||||
assert.equal(cm.colors.ansi[42].css, '#00d787');
|
||||
assert.equal(cm.colors.ansi[43].css, '#00d7af');
|
||||
assert.equal(cm.colors.ansi[44].css, '#00d7d7');
|
||||
assert.equal(cm.colors.ansi[45].css, '#00d7ff');
|
||||
assert.equal(cm.colors.ansi[46].css, '#00ff00');
|
||||
assert.equal(cm.colors.ansi[47].css, '#00ff5f');
|
||||
assert.equal(cm.colors.ansi[48].css, '#00ff87');
|
||||
assert.equal(cm.colors.ansi[49].css, '#00ffaf');
|
||||
assert.equal(cm.colors.ansi[50].css, '#00ffd7');
|
||||
assert.equal(cm.colors.ansi[51].css, '#00ffff');
|
||||
assert.equal(cm.colors.ansi[52].css, '#5f0000');
|
||||
assert.equal(cm.colors.ansi[53].css, '#5f005f');
|
||||
assert.equal(cm.colors.ansi[54].css, '#5f0087');
|
||||
assert.equal(cm.colors.ansi[55].css, '#5f00af');
|
||||
assert.equal(cm.colors.ansi[56].css, '#5f00d7');
|
||||
assert.equal(cm.colors.ansi[57].css, '#5f00ff');
|
||||
assert.equal(cm.colors.ansi[58].css, '#5f5f00');
|
||||
assert.equal(cm.colors.ansi[59].css, '#5f5f5f');
|
||||
assert.equal(cm.colors.ansi[60].css, '#5f5f87');
|
||||
assert.equal(cm.colors.ansi[61].css, '#5f5faf');
|
||||
assert.equal(cm.colors.ansi[62].css, '#5f5fd7');
|
||||
assert.equal(cm.colors.ansi[63].css, '#5f5fff');
|
||||
assert.equal(cm.colors.ansi[64].css, '#5f8700');
|
||||
assert.equal(cm.colors.ansi[65].css, '#5f875f');
|
||||
assert.equal(cm.colors.ansi[66].css, '#5f8787');
|
||||
assert.equal(cm.colors.ansi[67].css, '#5f87af');
|
||||
assert.equal(cm.colors.ansi[68].css, '#5f87d7');
|
||||
assert.equal(cm.colors.ansi[69].css, '#5f87ff');
|
||||
assert.equal(cm.colors.ansi[70].css, '#5faf00');
|
||||
assert.equal(cm.colors.ansi[71].css, '#5faf5f');
|
||||
assert.equal(cm.colors.ansi[72].css, '#5faf87');
|
||||
assert.equal(cm.colors.ansi[73].css, '#5fafaf');
|
||||
assert.equal(cm.colors.ansi[74].css, '#5fafd7');
|
||||
assert.equal(cm.colors.ansi[75].css, '#5fafff');
|
||||
assert.equal(cm.colors.ansi[76].css, '#5fd700');
|
||||
assert.equal(cm.colors.ansi[77].css, '#5fd75f');
|
||||
assert.equal(cm.colors.ansi[78].css, '#5fd787');
|
||||
assert.equal(cm.colors.ansi[79].css, '#5fd7af');
|
||||
assert.equal(cm.colors.ansi[80].css, '#5fd7d7');
|
||||
assert.equal(cm.colors.ansi[81].css, '#5fd7ff');
|
||||
assert.equal(cm.colors.ansi[82].css, '#5fff00');
|
||||
assert.equal(cm.colors.ansi[83].css, '#5fff5f');
|
||||
assert.equal(cm.colors.ansi[84].css, '#5fff87');
|
||||
assert.equal(cm.colors.ansi[85].css, '#5fffaf');
|
||||
assert.equal(cm.colors.ansi[86].css, '#5fffd7');
|
||||
assert.equal(cm.colors.ansi[87].css, '#5fffff');
|
||||
assert.equal(cm.colors.ansi[88].css, '#870000');
|
||||
assert.equal(cm.colors.ansi[89].css, '#87005f');
|
||||
assert.equal(cm.colors.ansi[90].css, '#870087');
|
||||
assert.equal(cm.colors.ansi[91].css, '#8700af');
|
||||
assert.equal(cm.colors.ansi[92].css, '#8700d7');
|
||||
assert.equal(cm.colors.ansi[93].css, '#8700ff');
|
||||
assert.equal(cm.colors.ansi[94].css, '#875f00');
|
||||
assert.equal(cm.colors.ansi[95].css, '#875f5f');
|
||||
assert.equal(cm.colors.ansi[96].css, '#875f87');
|
||||
assert.equal(cm.colors.ansi[97].css, '#875faf');
|
||||
assert.equal(cm.colors.ansi[98].css, '#875fd7');
|
||||
assert.equal(cm.colors.ansi[99].css, '#875fff');
|
||||
assert.equal(cm.colors.ansi[100].css, '#878700');
|
||||
assert.equal(cm.colors.ansi[101].css, '#87875f');
|
||||
assert.equal(cm.colors.ansi[102].css, '#878787');
|
||||
assert.equal(cm.colors.ansi[103].css, '#8787af');
|
||||
assert.equal(cm.colors.ansi[104].css, '#8787d7');
|
||||
assert.equal(cm.colors.ansi[105].css, '#8787ff');
|
||||
assert.equal(cm.colors.ansi[106].css, '#87af00');
|
||||
assert.equal(cm.colors.ansi[107].css, '#87af5f');
|
||||
assert.equal(cm.colors.ansi[108].css, '#87af87');
|
||||
assert.equal(cm.colors.ansi[109].css, '#87afaf');
|
||||
assert.equal(cm.colors.ansi[110].css, '#87afd7');
|
||||
assert.equal(cm.colors.ansi[111].css, '#87afff');
|
||||
assert.equal(cm.colors.ansi[112].css, '#87d700');
|
||||
assert.equal(cm.colors.ansi[113].css, '#87d75f');
|
||||
assert.equal(cm.colors.ansi[114].css, '#87d787');
|
||||
assert.equal(cm.colors.ansi[115].css, '#87d7af');
|
||||
assert.equal(cm.colors.ansi[116].css, '#87d7d7');
|
||||
assert.equal(cm.colors.ansi[117].css, '#87d7ff');
|
||||
assert.equal(cm.colors.ansi[118].css, '#87ff00');
|
||||
assert.equal(cm.colors.ansi[119].css, '#87ff5f');
|
||||
assert.equal(cm.colors.ansi[120].css, '#87ff87');
|
||||
assert.equal(cm.colors.ansi[121].css, '#87ffaf');
|
||||
assert.equal(cm.colors.ansi[122].css, '#87ffd7');
|
||||
assert.equal(cm.colors.ansi[123].css, '#87ffff');
|
||||
assert.equal(cm.colors.ansi[124].css, '#af0000');
|
||||
assert.equal(cm.colors.ansi[125].css, '#af005f');
|
||||
assert.equal(cm.colors.ansi[126].css, '#af0087');
|
||||
assert.equal(cm.colors.ansi[127].css, '#af00af');
|
||||
assert.equal(cm.colors.ansi[128].css, '#af00d7');
|
||||
assert.equal(cm.colors.ansi[129].css, '#af00ff');
|
||||
assert.equal(cm.colors.ansi[130].css, '#af5f00');
|
||||
assert.equal(cm.colors.ansi[131].css, '#af5f5f');
|
||||
assert.equal(cm.colors.ansi[132].css, '#af5f87');
|
||||
assert.equal(cm.colors.ansi[133].css, '#af5faf');
|
||||
assert.equal(cm.colors.ansi[134].css, '#af5fd7');
|
||||
assert.equal(cm.colors.ansi[135].css, '#af5fff');
|
||||
assert.equal(cm.colors.ansi[136].css, '#af8700');
|
||||
assert.equal(cm.colors.ansi[137].css, '#af875f');
|
||||
assert.equal(cm.colors.ansi[138].css, '#af8787');
|
||||
assert.equal(cm.colors.ansi[139].css, '#af87af');
|
||||
assert.equal(cm.colors.ansi[140].css, '#af87d7');
|
||||
assert.equal(cm.colors.ansi[141].css, '#af87ff');
|
||||
assert.equal(cm.colors.ansi[142].css, '#afaf00');
|
||||
assert.equal(cm.colors.ansi[143].css, '#afaf5f');
|
||||
assert.equal(cm.colors.ansi[144].css, '#afaf87');
|
||||
assert.equal(cm.colors.ansi[145].css, '#afafaf');
|
||||
assert.equal(cm.colors.ansi[146].css, '#afafd7');
|
||||
assert.equal(cm.colors.ansi[147].css, '#afafff');
|
||||
assert.equal(cm.colors.ansi[148].css, '#afd700');
|
||||
assert.equal(cm.colors.ansi[149].css, '#afd75f');
|
||||
assert.equal(cm.colors.ansi[150].css, '#afd787');
|
||||
assert.equal(cm.colors.ansi[151].css, '#afd7af');
|
||||
assert.equal(cm.colors.ansi[152].css, '#afd7d7');
|
||||
assert.equal(cm.colors.ansi[153].css, '#afd7ff');
|
||||
assert.equal(cm.colors.ansi[154].css, '#afff00');
|
||||
assert.equal(cm.colors.ansi[155].css, '#afff5f');
|
||||
assert.equal(cm.colors.ansi[156].css, '#afff87');
|
||||
assert.equal(cm.colors.ansi[157].css, '#afffaf');
|
||||
assert.equal(cm.colors.ansi[158].css, '#afffd7');
|
||||
assert.equal(cm.colors.ansi[159].css, '#afffff');
|
||||
assert.equal(cm.colors.ansi[160].css, '#d70000');
|
||||
assert.equal(cm.colors.ansi[161].css, '#d7005f');
|
||||
assert.equal(cm.colors.ansi[162].css, '#d70087');
|
||||
assert.equal(cm.colors.ansi[163].css, '#d700af');
|
||||
assert.equal(cm.colors.ansi[164].css, '#d700d7');
|
||||
assert.equal(cm.colors.ansi[165].css, '#d700ff');
|
||||
assert.equal(cm.colors.ansi[166].css, '#d75f00');
|
||||
assert.equal(cm.colors.ansi[167].css, '#d75f5f');
|
||||
assert.equal(cm.colors.ansi[168].css, '#d75f87');
|
||||
assert.equal(cm.colors.ansi[169].css, '#d75faf');
|
||||
assert.equal(cm.colors.ansi[170].css, '#d75fd7');
|
||||
assert.equal(cm.colors.ansi[171].css, '#d75fff');
|
||||
assert.equal(cm.colors.ansi[172].css, '#d78700');
|
||||
assert.equal(cm.colors.ansi[173].css, '#d7875f');
|
||||
assert.equal(cm.colors.ansi[174].css, '#d78787');
|
||||
assert.equal(cm.colors.ansi[175].css, '#d787af');
|
||||
assert.equal(cm.colors.ansi[176].css, '#d787d7');
|
||||
assert.equal(cm.colors.ansi[177].css, '#d787ff');
|
||||
assert.equal(cm.colors.ansi[178].css, '#d7af00');
|
||||
assert.equal(cm.colors.ansi[179].css, '#d7af5f');
|
||||
assert.equal(cm.colors.ansi[180].css, '#d7af87');
|
||||
assert.equal(cm.colors.ansi[181].css, '#d7afaf');
|
||||
assert.equal(cm.colors.ansi[182].css, '#d7afd7');
|
||||
assert.equal(cm.colors.ansi[183].css, '#d7afff');
|
||||
assert.equal(cm.colors.ansi[184].css, '#d7d700');
|
||||
assert.equal(cm.colors.ansi[185].css, '#d7d75f');
|
||||
assert.equal(cm.colors.ansi[186].css, '#d7d787');
|
||||
assert.equal(cm.colors.ansi[187].css, '#d7d7af');
|
||||
assert.equal(cm.colors.ansi[188].css, '#d7d7d7');
|
||||
assert.equal(cm.colors.ansi[189].css, '#d7d7ff');
|
||||
assert.equal(cm.colors.ansi[190].css, '#d7ff00');
|
||||
assert.equal(cm.colors.ansi[191].css, '#d7ff5f');
|
||||
assert.equal(cm.colors.ansi[192].css, '#d7ff87');
|
||||
assert.equal(cm.colors.ansi[193].css, '#d7ffaf');
|
||||
assert.equal(cm.colors.ansi[194].css, '#d7ffd7');
|
||||
assert.equal(cm.colors.ansi[195].css, '#d7ffff');
|
||||
assert.equal(cm.colors.ansi[196].css, '#ff0000');
|
||||
assert.equal(cm.colors.ansi[197].css, '#ff005f');
|
||||
assert.equal(cm.colors.ansi[198].css, '#ff0087');
|
||||
assert.equal(cm.colors.ansi[199].css, '#ff00af');
|
||||
assert.equal(cm.colors.ansi[200].css, '#ff00d7');
|
||||
assert.equal(cm.colors.ansi[201].css, '#ff00ff');
|
||||
assert.equal(cm.colors.ansi[202].css, '#ff5f00');
|
||||
assert.equal(cm.colors.ansi[203].css, '#ff5f5f');
|
||||
assert.equal(cm.colors.ansi[204].css, '#ff5f87');
|
||||
assert.equal(cm.colors.ansi[205].css, '#ff5faf');
|
||||
assert.equal(cm.colors.ansi[206].css, '#ff5fd7');
|
||||
assert.equal(cm.colors.ansi[207].css, '#ff5fff');
|
||||
assert.equal(cm.colors.ansi[208].css, '#ff8700');
|
||||
assert.equal(cm.colors.ansi[209].css, '#ff875f');
|
||||
assert.equal(cm.colors.ansi[210].css, '#ff8787');
|
||||
assert.equal(cm.colors.ansi[211].css, '#ff87af');
|
||||
assert.equal(cm.colors.ansi[212].css, '#ff87d7');
|
||||
assert.equal(cm.colors.ansi[213].css, '#ff87ff');
|
||||
assert.equal(cm.colors.ansi[214].css, '#ffaf00');
|
||||
assert.equal(cm.colors.ansi[215].css, '#ffaf5f');
|
||||
assert.equal(cm.colors.ansi[216].css, '#ffaf87');
|
||||
assert.equal(cm.colors.ansi[217].css, '#ffafaf');
|
||||
assert.equal(cm.colors.ansi[218].css, '#ffafd7');
|
||||
assert.equal(cm.colors.ansi[219].css, '#ffafff');
|
||||
assert.equal(cm.colors.ansi[220].css, '#ffd700');
|
||||
assert.equal(cm.colors.ansi[221].css, '#ffd75f');
|
||||
assert.equal(cm.colors.ansi[222].css, '#ffd787');
|
||||
assert.equal(cm.colors.ansi[223].css, '#ffd7af');
|
||||
assert.equal(cm.colors.ansi[224].css, '#ffd7d7');
|
||||
assert.equal(cm.colors.ansi[225].css, '#ffd7ff');
|
||||
assert.equal(cm.colors.ansi[226].css, '#ffff00');
|
||||
assert.equal(cm.colors.ansi[227].css, '#ffff5f');
|
||||
assert.equal(cm.colors.ansi[228].css, '#ffff87');
|
||||
assert.equal(cm.colors.ansi[229].css, '#ffffaf');
|
||||
assert.equal(cm.colors.ansi[230].css, '#ffffd7');
|
||||
assert.equal(cm.colors.ansi[231].css, '#ffffff');
|
||||
assert.equal(cm.colors.ansi[232].css, '#080808');
|
||||
assert.equal(cm.colors.ansi[233].css, '#121212');
|
||||
assert.equal(cm.colors.ansi[234].css, '#1c1c1c');
|
||||
assert.equal(cm.colors.ansi[235].css, '#262626');
|
||||
assert.equal(cm.colors.ansi[236].css, '#303030');
|
||||
assert.equal(cm.colors.ansi[237].css, '#3a3a3a');
|
||||
assert.equal(cm.colors.ansi[238].css, '#444444');
|
||||
assert.equal(cm.colors.ansi[239].css, '#4e4e4e');
|
||||
assert.equal(cm.colors.ansi[240].css, '#585858');
|
||||
assert.equal(cm.colors.ansi[241].css, '#626262');
|
||||
assert.equal(cm.colors.ansi[242].css, '#6c6c6c');
|
||||
assert.equal(cm.colors.ansi[243].css, '#767676');
|
||||
assert.equal(cm.colors.ansi[244].css, '#808080');
|
||||
assert.equal(cm.colors.ansi[245].css, '#8a8a8a');
|
||||
assert.equal(cm.colors.ansi[246].css, '#949494');
|
||||
assert.equal(cm.colors.ansi[247].css, '#9e9e9e');
|
||||
assert.equal(cm.colors.ansi[248].css, '#a8a8a8');
|
||||
assert.equal(cm.colors.ansi[249].css, '#b2b2b2');
|
||||
assert.equal(cm.colors.ansi[250].css, '#bcbcbc');
|
||||
assert.equal(cm.colors.ansi[251].css, '#c6c6c6');
|
||||
assert.equal(cm.colors.ansi[252].css, '#d0d0d0');
|
||||
assert.equal(cm.colors.ansi[253].css, '#dadada');
|
||||
assert.equal(cm.colors.ansi[254].css, '#e4e4e4');
|
||||
assert.equal(cm.colors.ansi[255].css, '#eeeeee');
|
||||
assert.equal(cm.colors.ansi[16], '#000000');
|
||||
assert.equal(cm.colors.ansi[17], '#00005f');
|
||||
assert.equal(cm.colors.ansi[18], '#000087');
|
||||
assert.equal(cm.colors.ansi[19], '#0000af');
|
||||
assert.equal(cm.colors.ansi[20], '#0000d7');
|
||||
assert.equal(cm.colors.ansi[21], '#0000ff');
|
||||
assert.equal(cm.colors.ansi[22], '#005f00');
|
||||
assert.equal(cm.colors.ansi[23], '#005f5f');
|
||||
assert.equal(cm.colors.ansi[24], '#005f87');
|
||||
assert.equal(cm.colors.ansi[25], '#005faf');
|
||||
assert.equal(cm.colors.ansi[26], '#005fd7');
|
||||
assert.equal(cm.colors.ansi[27], '#005fff');
|
||||
assert.equal(cm.colors.ansi[28], '#008700');
|
||||
assert.equal(cm.colors.ansi[29], '#00875f');
|
||||
assert.equal(cm.colors.ansi[30], '#008787');
|
||||
assert.equal(cm.colors.ansi[31], '#0087af');
|
||||
assert.equal(cm.colors.ansi[32], '#0087d7');
|
||||
assert.equal(cm.colors.ansi[33], '#0087ff');
|
||||
assert.equal(cm.colors.ansi[34], '#00af00');
|
||||
assert.equal(cm.colors.ansi[35], '#00af5f');
|
||||
assert.equal(cm.colors.ansi[36], '#00af87');
|
||||
assert.equal(cm.colors.ansi[37], '#00afaf');
|
||||
assert.equal(cm.colors.ansi[38], '#00afd7');
|
||||
assert.equal(cm.colors.ansi[39], '#00afff');
|
||||
assert.equal(cm.colors.ansi[40], '#00d700');
|
||||
assert.equal(cm.colors.ansi[41], '#00d75f');
|
||||
assert.equal(cm.colors.ansi[42], '#00d787');
|
||||
assert.equal(cm.colors.ansi[43], '#00d7af');
|
||||
assert.equal(cm.colors.ansi[44], '#00d7d7');
|
||||
assert.equal(cm.colors.ansi[45], '#00d7ff');
|
||||
assert.equal(cm.colors.ansi[46], '#00ff00');
|
||||
assert.equal(cm.colors.ansi[47], '#00ff5f');
|
||||
assert.equal(cm.colors.ansi[48], '#00ff87');
|
||||
assert.equal(cm.colors.ansi[49], '#00ffaf');
|
||||
assert.equal(cm.colors.ansi[50], '#00ffd7');
|
||||
assert.equal(cm.colors.ansi[51], '#00ffff');
|
||||
assert.equal(cm.colors.ansi[52], '#5f0000');
|
||||
assert.equal(cm.colors.ansi[53], '#5f005f');
|
||||
assert.equal(cm.colors.ansi[54], '#5f0087');
|
||||
assert.equal(cm.colors.ansi[55], '#5f00af');
|
||||
assert.equal(cm.colors.ansi[56], '#5f00d7');
|
||||
assert.equal(cm.colors.ansi[57], '#5f00ff');
|
||||
assert.equal(cm.colors.ansi[58], '#5f5f00');
|
||||
assert.equal(cm.colors.ansi[59], '#5f5f5f');
|
||||
assert.equal(cm.colors.ansi[60], '#5f5f87');
|
||||
assert.equal(cm.colors.ansi[61], '#5f5faf');
|
||||
assert.equal(cm.colors.ansi[62], '#5f5fd7');
|
||||
assert.equal(cm.colors.ansi[63], '#5f5fff');
|
||||
assert.equal(cm.colors.ansi[64], '#5f8700');
|
||||
assert.equal(cm.colors.ansi[65], '#5f875f');
|
||||
assert.equal(cm.colors.ansi[66], '#5f8787');
|
||||
assert.equal(cm.colors.ansi[67], '#5f87af');
|
||||
assert.equal(cm.colors.ansi[68], '#5f87d7');
|
||||
assert.equal(cm.colors.ansi[69], '#5f87ff');
|
||||
assert.equal(cm.colors.ansi[70], '#5faf00');
|
||||
assert.equal(cm.colors.ansi[71], '#5faf5f');
|
||||
assert.equal(cm.colors.ansi[72], '#5faf87');
|
||||
assert.equal(cm.colors.ansi[73], '#5fafaf');
|
||||
assert.equal(cm.colors.ansi[74], '#5fafd7');
|
||||
assert.equal(cm.colors.ansi[75], '#5fafff');
|
||||
assert.equal(cm.colors.ansi[76], '#5fd700');
|
||||
assert.equal(cm.colors.ansi[77], '#5fd75f');
|
||||
assert.equal(cm.colors.ansi[78], '#5fd787');
|
||||
assert.equal(cm.colors.ansi[79], '#5fd7af');
|
||||
assert.equal(cm.colors.ansi[80], '#5fd7d7');
|
||||
assert.equal(cm.colors.ansi[81], '#5fd7ff');
|
||||
assert.equal(cm.colors.ansi[82], '#5fff00');
|
||||
assert.equal(cm.colors.ansi[83], '#5fff5f');
|
||||
assert.equal(cm.colors.ansi[84], '#5fff87');
|
||||
assert.equal(cm.colors.ansi[85], '#5fffaf');
|
||||
assert.equal(cm.colors.ansi[86], '#5fffd7');
|
||||
assert.equal(cm.colors.ansi[87], '#5fffff');
|
||||
assert.equal(cm.colors.ansi[88], '#870000');
|
||||
assert.equal(cm.colors.ansi[89], '#87005f');
|
||||
assert.equal(cm.colors.ansi[90], '#870087');
|
||||
assert.equal(cm.colors.ansi[91], '#8700af');
|
||||
assert.equal(cm.colors.ansi[92], '#8700d7');
|
||||
assert.equal(cm.colors.ansi[93], '#8700ff');
|
||||
assert.equal(cm.colors.ansi[94], '#875f00');
|
||||
assert.equal(cm.colors.ansi[95], '#875f5f');
|
||||
assert.equal(cm.colors.ansi[96], '#875f87');
|
||||
assert.equal(cm.colors.ansi[97], '#875faf');
|
||||
assert.equal(cm.colors.ansi[98], '#875fd7');
|
||||
assert.equal(cm.colors.ansi[99], '#875fff');
|
||||
assert.equal(cm.colors.ansi[100], '#878700');
|
||||
assert.equal(cm.colors.ansi[101], '#87875f');
|
||||
assert.equal(cm.colors.ansi[102], '#878787');
|
||||
assert.equal(cm.colors.ansi[103], '#8787af');
|
||||
assert.equal(cm.colors.ansi[104], '#8787d7');
|
||||
assert.equal(cm.colors.ansi[105], '#8787ff');
|
||||
assert.equal(cm.colors.ansi[106], '#87af00');
|
||||
assert.equal(cm.colors.ansi[107], '#87af5f');
|
||||
assert.equal(cm.colors.ansi[108], '#87af87');
|
||||
assert.equal(cm.colors.ansi[109], '#87afaf');
|
||||
assert.equal(cm.colors.ansi[110], '#87afd7');
|
||||
assert.equal(cm.colors.ansi[111], '#87afff');
|
||||
assert.equal(cm.colors.ansi[112], '#87d700');
|
||||
assert.equal(cm.colors.ansi[113], '#87d75f');
|
||||
assert.equal(cm.colors.ansi[114], '#87d787');
|
||||
assert.equal(cm.colors.ansi[115], '#87d7af');
|
||||
assert.equal(cm.colors.ansi[116], '#87d7d7');
|
||||
assert.equal(cm.colors.ansi[117], '#87d7ff');
|
||||
assert.equal(cm.colors.ansi[118], '#87ff00');
|
||||
assert.equal(cm.colors.ansi[119], '#87ff5f');
|
||||
assert.equal(cm.colors.ansi[120], '#87ff87');
|
||||
assert.equal(cm.colors.ansi[121], '#87ffaf');
|
||||
assert.equal(cm.colors.ansi[122], '#87ffd7');
|
||||
assert.equal(cm.colors.ansi[123], '#87ffff');
|
||||
assert.equal(cm.colors.ansi[124], '#af0000');
|
||||
assert.equal(cm.colors.ansi[125], '#af005f');
|
||||
assert.equal(cm.colors.ansi[126], '#af0087');
|
||||
assert.equal(cm.colors.ansi[127], '#af00af');
|
||||
assert.equal(cm.colors.ansi[128], '#af00d7');
|
||||
assert.equal(cm.colors.ansi[129], '#af00ff');
|
||||
assert.equal(cm.colors.ansi[130], '#af5f00');
|
||||
assert.equal(cm.colors.ansi[131], '#af5f5f');
|
||||
assert.equal(cm.colors.ansi[132], '#af5f87');
|
||||
assert.equal(cm.colors.ansi[133], '#af5faf');
|
||||
assert.equal(cm.colors.ansi[134], '#af5fd7');
|
||||
assert.equal(cm.colors.ansi[135], '#af5fff');
|
||||
assert.equal(cm.colors.ansi[136], '#af8700');
|
||||
assert.equal(cm.colors.ansi[137], '#af875f');
|
||||
assert.equal(cm.colors.ansi[138], '#af8787');
|
||||
assert.equal(cm.colors.ansi[139], '#af87af');
|
||||
assert.equal(cm.colors.ansi[140], '#af87d7');
|
||||
assert.equal(cm.colors.ansi[141], '#af87ff');
|
||||
assert.equal(cm.colors.ansi[142], '#afaf00');
|
||||
assert.equal(cm.colors.ansi[143], '#afaf5f');
|
||||
assert.equal(cm.colors.ansi[144], '#afaf87');
|
||||
assert.equal(cm.colors.ansi[145], '#afafaf');
|
||||
assert.equal(cm.colors.ansi[146], '#afafd7');
|
||||
assert.equal(cm.colors.ansi[147], '#afafff');
|
||||
assert.equal(cm.colors.ansi[148], '#afd700');
|
||||
assert.equal(cm.colors.ansi[149], '#afd75f');
|
||||
assert.equal(cm.colors.ansi[150], '#afd787');
|
||||
assert.equal(cm.colors.ansi[151], '#afd7af');
|
||||
assert.equal(cm.colors.ansi[152], '#afd7d7');
|
||||
assert.equal(cm.colors.ansi[153], '#afd7ff');
|
||||
assert.equal(cm.colors.ansi[154], '#afff00');
|
||||
assert.equal(cm.colors.ansi[155], '#afff5f');
|
||||
assert.equal(cm.colors.ansi[156], '#afff87');
|
||||
assert.equal(cm.colors.ansi[157], '#afffaf');
|
||||
assert.equal(cm.colors.ansi[158], '#afffd7');
|
||||
assert.equal(cm.colors.ansi[159], '#afffff');
|
||||
assert.equal(cm.colors.ansi[160], '#d70000');
|
||||
assert.equal(cm.colors.ansi[161], '#d7005f');
|
||||
assert.equal(cm.colors.ansi[162], '#d70087');
|
||||
assert.equal(cm.colors.ansi[163], '#d700af');
|
||||
assert.equal(cm.colors.ansi[164], '#d700d7');
|
||||
assert.equal(cm.colors.ansi[165], '#d700ff');
|
||||
assert.equal(cm.colors.ansi[166], '#d75f00');
|
||||
assert.equal(cm.colors.ansi[167], '#d75f5f');
|
||||
assert.equal(cm.colors.ansi[168], '#d75f87');
|
||||
assert.equal(cm.colors.ansi[169], '#d75faf');
|
||||
assert.equal(cm.colors.ansi[170], '#d75fd7');
|
||||
assert.equal(cm.colors.ansi[171], '#d75fff');
|
||||
assert.equal(cm.colors.ansi[172], '#d78700');
|
||||
assert.equal(cm.colors.ansi[173], '#d7875f');
|
||||
assert.equal(cm.colors.ansi[174], '#d78787');
|
||||
assert.equal(cm.colors.ansi[175], '#d787af');
|
||||
assert.equal(cm.colors.ansi[176], '#d787d7');
|
||||
assert.equal(cm.colors.ansi[177], '#d787ff');
|
||||
assert.equal(cm.colors.ansi[178], '#d7af00');
|
||||
assert.equal(cm.colors.ansi[179], '#d7af5f');
|
||||
assert.equal(cm.colors.ansi[180], '#d7af87');
|
||||
assert.equal(cm.colors.ansi[181], '#d7afaf');
|
||||
assert.equal(cm.colors.ansi[182], '#d7afd7');
|
||||
assert.equal(cm.colors.ansi[183], '#d7afff');
|
||||
assert.equal(cm.colors.ansi[184], '#d7d700');
|
||||
assert.equal(cm.colors.ansi[185], '#d7d75f');
|
||||
assert.equal(cm.colors.ansi[186], '#d7d787');
|
||||
assert.equal(cm.colors.ansi[187], '#d7d7af');
|
||||
assert.equal(cm.colors.ansi[188], '#d7d7d7');
|
||||
assert.equal(cm.colors.ansi[189], '#d7d7ff');
|
||||
assert.equal(cm.colors.ansi[190], '#d7ff00');
|
||||
assert.equal(cm.colors.ansi[191], '#d7ff5f');
|
||||
assert.equal(cm.colors.ansi[192], '#d7ff87');
|
||||
assert.equal(cm.colors.ansi[193], '#d7ffaf');
|
||||
assert.equal(cm.colors.ansi[194], '#d7ffd7');
|
||||
assert.equal(cm.colors.ansi[195], '#d7ffff');
|
||||
assert.equal(cm.colors.ansi[196], '#ff0000');
|
||||
assert.equal(cm.colors.ansi[197], '#ff005f');
|
||||
assert.equal(cm.colors.ansi[198], '#ff0087');
|
||||
assert.equal(cm.colors.ansi[199], '#ff00af');
|
||||
assert.equal(cm.colors.ansi[200], '#ff00d7');
|
||||
assert.equal(cm.colors.ansi[201], '#ff00ff');
|
||||
assert.equal(cm.colors.ansi[202], '#ff5f00');
|
||||
assert.equal(cm.colors.ansi[203], '#ff5f5f');
|
||||
assert.equal(cm.colors.ansi[204], '#ff5f87');
|
||||
assert.equal(cm.colors.ansi[205], '#ff5faf');
|
||||
assert.equal(cm.colors.ansi[206], '#ff5fd7');
|
||||
assert.equal(cm.colors.ansi[207], '#ff5fff');
|
||||
assert.equal(cm.colors.ansi[208], '#ff8700');
|
||||
assert.equal(cm.colors.ansi[209], '#ff875f');
|
||||
assert.equal(cm.colors.ansi[210], '#ff8787');
|
||||
assert.equal(cm.colors.ansi[211], '#ff87af');
|
||||
assert.equal(cm.colors.ansi[212], '#ff87d7');
|
||||
assert.equal(cm.colors.ansi[213], '#ff87ff');
|
||||
assert.equal(cm.colors.ansi[214], '#ffaf00');
|
||||
assert.equal(cm.colors.ansi[215], '#ffaf5f');
|
||||
assert.equal(cm.colors.ansi[216], '#ffaf87');
|
||||
assert.equal(cm.colors.ansi[217], '#ffafaf');
|
||||
assert.equal(cm.colors.ansi[218], '#ffafd7');
|
||||
assert.equal(cm.colors.ansi[219], '#ffafff');
|
||||
assert.equal(cm.colors.ansi[220], '#ffd700');
|
||||
assert.equal(cm.colors.ansi[221], '#ffd75f');
|
||||
assert.equal(cm.colors.ansi[222], '#ffd787');
|
||||
assert.equal(cm.colors.ansi[223], '#ffd7af');
|
||||
assert.equal(cm.colors.ansi[224], '#ffd7d7');
|
||||
assert.equal(cm.colors.ansi[225], '#ffd7ff');
|
||||
assert.equal(cm.colors.ansi[226], '#ffff00');
|
||||
assert.equal(cm.colors.ansi[227], '#ffff5f');
|
||||
assert.equal(cm.colors.ansi[228], '#ffff87');
|
||||
assert.equal(cm.colors.ansi[229], '#ffffaf');
|
||||
assert.equal(cm.colors.ansi[230], '#ffffd7');
|
||||
assert.equal(cm.colors.ansi[231], '#ffffff');
|
||||
assert.equal(cm.colors.ansi[232], '#080808');
|
||||
assert.equal(cm.colors.ansi[233], '#121212');
|
||||
assert.equal(cm.colors.ansi[234], '#1c1c1c');
|
||||
assert.equal(cm.colors.ansi[235], '#262626');
|
||||
assert.equal(cm.colors.ansi[236], '#303030');
|
||||
assert.equal(cm.colors.ansi[237], '#3a3a3a');
|
||||
assert.equal(cm.colors.ansi[238], '#444444');
|
||||
assert.equal(cm.colors.ansi[239], '#4e4e4e');
|
||||
assert.equal(cm.colors.ansi[240], '#585858');
|
||||
assert.equal(cm.colors.ansi[241], '#626262');
|
||||
assert.equal(cm.colors.ansi[242], '#6c6c6c');
|
||||
assert.equal(cm.colors.ansi[243], '#767676');
|
||||
assert.equal(cm.colors.ansi[244], '#808080');
|
||||
assert.equal(cm.colors.ansi[245], '#8a8a8a');
|
||||
assert.equal(cm.colors.ansi[246], '#949494');
|
||||
assert.equal(cm.colors.ansi[247], '#9e9e9e');
|
||||
assert.equal(cm.colors.ansi[248], '#a8a8a8');
|
||||
assert.equal(cm.colors.ansi[249], '#b2b2b2');
|
||||
assert.equal(cm.colors.ansi[250], '#bcbcbc');
|
||||
assert.equal(cm.colors.ansi[251], '#c6c6c6');
|
||||
assert.equal(cm.colors.ansi[252], '#d0d0d0');
|
||||
assert.equal(cm.colors.ansi[253], '#dadada');
|
||||
assert.equal(cm.colors.ansi[254], '#e4e4e4');
|
||||
assert.equal(cm.colors.ansi[255], '#eeeeee');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -294,20 +276,20 @@ describe('ColorManager', () => {
|
||||
});
|
||||
|
||||
it('should set a partial set of colors, using the default if not present', () => {
|
||||
assert.equal(cm.colors.background.css, '#000000');
|
||||
assert.equal(cm.colors.foreground.css, '#ffffff');
|
||||
assert.equal(cm.colors.background, '#000000');
|
||||
assert.equal(cm.colors.foreground, '#ffffff');
|
||||
cm.setTheme({
|
||||
background: '#FF0000',
|
||||
foreground: '#00FF00'
|
||||
});
|
||||
assert.equal(cm.colors.background.css, '#FF0000');
|
||||
assert.equal(cm.colors.foreground.css, '#00FF00');
|
||||
assert.equal(cm.colors.background, '#FF0000');
|
||||
assert.equal(cm.colors.foreground, '#00FF00');
|
||||
cm.setTheme({
|
||||
background: '#0000FF'
|
||||
});
|
||||
assert.equal(cm.colors.background.css, '#0000FF');
|
||||
assert.equal(cm.colors.background, '#0000FF');
|
||||
// FG reverts back to default
|
||||
assert.equal(cm.colors.foreground.css, '#ffffff');
|
||||
assert.equal(cm.colors.foreground, '#ffffff');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+72
-133
@@ -3,75 +3,58 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IColorManager } from './Types';
|
||||
import { IColor, IColorSet } from '../shared/Types';
|
||||
import { ITheme } from 'xterm';
|
||||
import { IColorSet, IColorManager } from './Interfaces';
|
||||
import { ITheme } from '../Interfaces';
|
||||
|
||||
const DEFAULT_FOREGROUND = fromHex('#ffffff');
|
||||
const DEFAULT_BACKGROUND = fromHex('#000000');
|
||||
const DEFAULT_CURSOR = fromHex('#ffffff');
|
||||
const DEFAULT_CURSOR_ACCENT = fromHex('#000000');
|
||||
const DEFAULT_SELECTION = {
|
||||
css: 'rgba(255, 255, 255, 0.3)',
|
||||
rgba: 0xFFFFFF77
|
||||
};
|
||||
const DEFAULT_FOREGROUND = '#ffffff';
|
||||
const DEFAULT_BACKGROUND = '#000000';
|
||||
const DEFAULT_CURSOR = '#ffffff';
|
||||
const DEFAULT_CURSOR_ACCENT = '#000000';
|
||||
const DEFAULT_SELECTION = 'rgba(255, 255, 255, 0.3)';
|
||||
export const DEFAULT_ANSI_COLORS = [
|
||||
// dark:
|
||||
'#2e3436',
|
||||
'#cc0000',
|
||||
'#4e9a06',
|
||||
'#c4a000',
|
||||
'#3465a4',
|
||||
'#75507b',
|
||||
'#06989a',
|
||||
'#d3d7cf',
|
||||
// bright:
|
||||
'#555753',
|
||||
'#ef2929',
|
||||
'#8ae234',
|
||||
'#fce94f',
|
||||
'#729fcf',
|
||||
'#ad7fa8',
|
||||
'#34e2e2',
|
||||
'#eeeeec'
|
||||
];
|
||||
|
||||
// An IIFE to generate DEFAULT_ANSI_COLORS. Do not mutate DEFAULT_ANSI_COLORS, instead make a copy
|
||||
// and mutate that.
|
||||
export const DEFAULT_ANSI_COLORS = (() => {
|
||||
const colors = [
|
||||
// dark:
|
||||
fromHex('#2e3436'),
|
||||
fromHex('#cc0000'),
|
||||
fromHex('#4e9a06'),
|
||||
fromHex('#c4a000'),
|
||||
fromHex('#3465a4'),
|
||||
fromHex('#75507b'),
|
||||
fromHex('#06989a'),
|
||||
fromHex('#d3d7cf'),
|
||||
// bright:
|
||||
fromHex('#555753'),
|
||||
fromHex('#ef2929'),
|
||||
fromHex('#8ae234'),
|
||||
fromHex('#fce94f'),
|
||||
fromHex('#729fcf'),
|
||||
fromHex('#ad7fa8'),
|
||||
fromHex('#34e2e2'),
|
||||
fromHex('#eeeeec')
|
||||
];
|
||||
/**
|
||||
* Fills an existing 16 length string with the remaining 240 ANSI colors.
|
||||
* @param first16Colors The first 16 ANSI colors.
|
||||
*/
|
||||
function generate256Colors(first16Colors: string[]): string[] {
|
||||
let colors = first16Colors.slice();
|
||||
|
||||
// Fill in the remaining 240 ANSI colors.
|
||||
// Generate colors (16-231)
|
||||
let v = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff];
|
||||
for (let i = 0; i < 216; i++) {
|
||||
const r = v[(i / 36) % 6 | 0];
|
||||
const g = v[(i / 6) % 6 | 0];
|
||||
const b = v[i % 6];
|
||||
colors.push({
|
||||
css: `#${toPaddedHex(r)}${toPaddedHex(g)}${toPaddedHex(b)}`,
|
||||
// Use >>> 0 to force a conversion to an unsigned int
|
||||
rgba: ((r << 24) | (g << 16) | (b << 8) | 0xFF) >>> 0
|
||||
});
|
||||
const r = toPaddedHex(v[(i / 36) % 6 | 0]);
|
||||
const g = toPaddedHex(v[(i / 6) % 6 | 0]);
|
||||
const b = toPaddedHex(v[i % 6]);
|
||||
colors.push(`#${r}${g}${b}`);
|
||||
}
|
||||
|
||||
// Generate greys (232-255)
|
||||
for (let i = 0; i < 24; i++) {
|
||||
const c = 8 + i * 10;
|
||||
const ch = toPaddedHex(c);
|
||||
colors.push({
|
||||
css: `#${ch}${ch}${ch}`,
|
||||
rgba: ((c << 24) | (c << 16) | (c << 8) | 0xFF) >>> 0
|
||||
});
|
||||
const c = toPaddedHex(8 + i * 10);
|
||||
colors.push(`#${c}${c}${c}`);
|
||||
}
|
||||
|
||||
return colors;
|
||||
})();
|
||||
|
||||
function fromHex(css: string): IColor {
|
||||
return {
|
||||
css,
|
||||
rgba: parseInt(css.slice(1), 16) << 8 | 0xFF
|
||||
};
|
||||
}
|
||||
|
||||
function toPaddedHex(c: number): string {
|
||||
@@ -84,23 +67,15 @@ function toPaddedHex(c: number): string {
|
||||
*/
|
||||
export class ColorManager implements IColorManager {
|
||||
public colors: IColorSet;
|
||||
private _ctx: CanvasRenderingContext2D;
|
||||
private _litmusColor: CanvasGradient;
|
||||
|
||||
constructor(document: Document, public allowTransparency: boolean) {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 1;
|
||||
canvas.height = 1;
|
||||
this._ctx = canvas.getContext('2d');
|
||||
this._ctx.globalCompositeOperation = 'copy';
|
||||
this._litmusColor = this._ctx.createLinearGradient(0, 0, 1, 1);
|
||||
constructor() {
|
||||
this.colors = {
|
||||
foreground: DEFAULT_FOREGROUND,
|
||||
background: DEFAULT_BACKGROUND,
|
||||
cursor: DEFAULT_CURSOR,
|
||||
cursorAccent: DEFAULT_CURSOR_ACCENT,
|
||||
selection: DEFAULT_SELECTION,
|
||||
ansi: DEFAULT_ANSI_COLORS.slice()
|
||||
ansi: generate256Colors(DEFAULT_ANSI_COLORS)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -110,78 +85,42 @@ export class ColorManager implements IColorManager {
|
||||
* colors will be used where colors are not defined.
|
||||
*/
|
||||
public setTheme(theme: ITheme): void {
|
||||
this.colors.foreground = this._parseColor(theme.foreground, DEFAULT_FOREGROUND);
|
||||
this.colors.background = this._parseColor(theme.background, DEFAULT_BACKGROUND);
|
||||
this.colors.cursor = this._parseColor(theme.cursor, DEFAULT_CURSOR, true);
|
||||
this.colors.cursorAccent = this._parseColor(theme.cursorAccent, DEFAULT_CURSOR_ACCENT, true);
|
||||
this.colors.selection = this._parseColor(theme.selection, DEFAULT_SELECTION, true);
|
||||
this.colors.ansi[0] = this._parseColor(theme.black, DEFAULT_ANSI_COLORS[0]);
|
||||
this.colors.ansi[1] = this._parseColor(theme.red, DEFAULT_ANSI_COLORS[1]);
|
||||
this.colors.ansi[2] = this._parseColor(theme.green, DEFAULT_ANSI_COLORS[2]);
|
||||
this.colors.ansi[3] = this._parseColor(theme.yellow, DEFAULT_ANSI_COLORS[3]);
|
||||
this.colors.ansi[4] = this._parseColor(theme.blue, DEFAULT_ANSI_COLORS[4]);
|
||||
this.colors.ansi[5] = this._parseColor(theme.magenta, DEFAULT_ANSI_COLORS[5]);
|
||||
this.colors.ansi[6] = this._parseColor(theme.cyan, DEFAULT_ANSI_COLORS[6]);
|
||||
this.colors.ansi[7] = this._parseColor(theme.white, DEFAULT_ANSI_COLORS[7]);
|
||||
this.colors.ansi[8] = this._parseColor(theme.brightBlack, DEFAULT_ANSI_COLORS[8]);
|
||||
this.colors.ansi[9] = this._parseColor(theme.brightRed, DEFAULT_ANSI_COLORS[9]);
|
||||
this.colors.ansi[10] = this._parseColor(theme.brightGreen, DEFAULT_ANSI_COLORS[10]);
|
||||
this.colors.ansi[11] = this._parseColor(theme.brightYellow, DEFAULT_ANSI_COLORS[11]);
|
||||
this.colors.ansi[12] = this._parseColor(theme.brightBlue, DEFAULT_ANSI_COLORS[12]);
|
||||
this.colors.ansi[13] = this._parseColor(theme.brightMagenta, DEFAULT_ANSI_COLORS[13]);
|
||||
this.colors.ansi[14] = this._parseColor(theme.brightCyan, DEFAULT_ANSI_COLORS[14]);
|
||||
this.colors.ansi[15] = this._parseColor(theme.brightWhite, DEFAULT_ANSI_COLORS[15]);
|
||||
this.colors.foreground = theme.foreground || DEFAULT_FOREGROUND;
|
||||
this.colors.background = this._validateColor(theme.background, DEFAULT_BACKGROUND);
|
||||
this.colors.cursor = theme.cursor || DEFAULT_CURSOR;
|
||||
this.colors.cursorAccent = theme.cursorAccent || DEFAULT_CURSOR_ACCENT;
|
||||
this.colors.selection = theme.selection || DEFAULT_SELECTION;
|
||||
this.colors.ansi[0] = theme.black || DEFAULT_ANSI_COLORS[0];
|
||||
this.colors.ansi[1] = theme.red || DEFAULT_ANSI_COLORS[1];
|
||||
this.colors.ansi[2] = theme.green || DEFAULT_ANSI_COLORS[2];
|
||||
this.colors.ansi[3] = theme.yellow || DEFAULT_ANSI_COLORS[3];
|
||||
this.colors.ansi[4] = theme.blue || DEFAULT_ANSI_COLORS[4];
|
||||
this.colors.ansi[5] = theme.magenta || DEFAULT_ANSI_COLORS[5];
|
||||
this.colors.ansi[6] = theme.cyan || DEFAULT_ANSI_COLORS[6];
|
||||
this.colors.ansi[7] = theme.white || DEFAULT_ANSI_COLORS[7];
|
||||
this.colors.ansi[8] = theme.brightBlack || DEFAULT_ANSI_COLORS[8];
|
||||
this.colors.ansi[9] = theme.brightRed || DEFAULT_ANSI_COLORS[9];
|
||||
this.colors.ansi[10] = theme.brightGreen || DEFAULT_ANSI_COLORS[10];
|
||||
this.colors.ansi[11] = theme.brightYellow || DEFAULT_ANSI_COLORS[11];
|
||||
this.colors.ansi[12] = theme.brightBlue || DEFAULT_ANSI_COLORS[12];
|
||||
this.colors.ansi[13] = theme.brightMagenta || DEFAULT_ANSI_COLORS[13];
|
||||
this.colors.ansi[14] = theme.brightCyan || DEFAULT_ANSI_COLORS[14];
|
||||
this.colors.ansi[15] = theme.brightWhite || DEFAULT_ANSI_COLORS[15];
|
||||
}
|
||||
|
||||
private _parseColor(
|
||||
css: string,
|
||||
fallback: IColor,
|
||||
allowTransparency: boolean = this.allowTransparency
|
||||
): IColor {
|
||||
if (!css) {
|
||||
private _validateColor(color: string, fallback: string): string {
|
||||
if (!color) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
// If parsing the value results in failure, then it must be ignored, and the attribute must
|
||||
// retain its previous value.
|
||||
// -- https://html.spec.whatwg.org/multipage/canvas.html#fill-and-stroke-styles
|
||||
this._ctx.fillStyle = this._litmusColor;
|
||||
this._ctx.fillStyle = css;
|
||||
if (typeof this._ctx.fillStyle !== 'string') {
|
||||
console.warn(`Color: ${css} is invalid using fallback ${fallback.css}`);
|
||||
return fallback;
|
||||
if (color.length === 7 && color.charAt(0) === '#') {
|
||||
return color;
|
||||
}
|
||||
|
||||
this._ctx.fillRect(0, 0, 1, 1);
|
||||
const data = this._ctx.getImageData(0, 0, 1, 1).data;
|
||||
|
||||
if (!allowTransparency && data[3] !== 0xFF) {
|
||||
// Ideally we'd just ignore the alpha channel, but...
|
||||
//
|
||||
// Browsers may not give back exactly the same RGB values we put in, because most/all
|
||||
// convert the color to a pre-multiplied representation. getImageData converts that back to
|
||||
// a un-premultipled representation, but the precision loss may make the RGB channels unuable
|
||||
// on their own.
|
||||
//
|
||||
// E.g. In Chrome #12345610 turns into #10305010, and in the extreme case, 0xFFFFFF00 turns
|
||||
// into 0x00000000.
|
||||
//
|
||||
// "Note: Due to the lossy nature of converting to and from premultiplied alpha color values,
|
||||
// pixels that have just been set using putImageData() might be returned to an equivalent
|
||||
// getImageData() as different values."
|
||||
// -- https://html.spec.whatwg.org/multipage/canvas.html#pixel-manipulation
|
||||
//
|
||||
// So let's just use the fallback color in this case instead.
|
||||
console.warn(
|
||||
`Color: ${css} is using transparency, but allowTransparency is false. ` +
|
||||
`Using fallback ${fallback.css}.`
|
||||
);
|
||||
return fallback;
|
||||
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 {
|
||||
css,
|
||||
rgba: (data[0] << 24 | data[1] << 16 | data[2] << 8 | data[3]) >>> 0
|
||||
};
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,15 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { CHAR_DATA_WIDTH_INDEX } from '../Buffer';
|
||||
import { IColorSet, IRenderDimensions } from './Types';
|
||||
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 { GridCache } from './GridCache';
|
||||
import { FLAGS } from './Types';
|
||||
import { BaseRenderLayer } from './BaseRenderLayer';
|
||||
import { CharData, ITerminal } from '../Types';
|
||||
import { CharData } from '../Types';
|
||||
|
||||
interface ICursorState {
|
||||
interface CursorState {
|
||||
x: number;
|
||||
y: number;
|
||||
isFocused: boolean;
|
||||
@@ -22,9 +25,10 @@ interface ICursorState {
|
||||
const BLINK_INTERVAL = 600;
|
||||
|
||||
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 _cursorBlinkStateManager: CursorBlinkStateManager;
|
||||
private _isFocused: boolean;
|
||||
|
||||
constructor(container: HTMLElement, zIndex: number, colors: IColorSet) {
|
||||
super(container, 'cursor', zIndex, true, colors);
|
||||
@@ -33,7 +37,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
y: null,
|
||||
isFocused: null,
|
||||
style: null,
|
||||
width: null
|
||||
width: null,
|
||||
};
|
||||
this._cursorRenderers = {
|
||||
'bar': this._renderBarCursor.bind(this),
|
||||
@@ -43,15 +47,15 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
// TODO: Consider initial options? Maybe onOptionsChanged should be called at the end of open?
|
||||
}
|
||||
|
||||
public resize(terminal: ITerminal, dim: IRenderDimensions): void {
|
||||
super.resize(terminal, dim);
|
||||
public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void {
|
||||
super.resize(terminal, dim, charSizeChanged);
|
||||
// Resizing the canvas discards the contents of the canvas so clear state
|
||||
this._state = {
|
||||
x: null,
|
||||
y: null,
|
||||
isFocused: null,
|
||||
style: null,
|
||||
width: null
|
||||
width: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -104,10 +108,9 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
}
|
||||
|
||||
public onGridChanged(terminal: ITerminal, startRow: number, endRow: number): void {
|
||||
// Only render if the animation frame is not active
|
||||
if (!this._cursorBlinkStateManager || this._cursorBlinkStateManager.isPaused) {
|
||||
this._render(terminal, false);
|
||||
} else {
|
||||
this._cursorBlinkStateManager.restartBlinkAnimation(terminal);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,7 +138,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
if (!terminal.isFocused) {
|
||||
this._clearCursor();
|
||||
this._ctx.save();
|
||||
this._ctx.fillStyle = this._colors.cursor.css;
|
||||
this._ctx.fillStyle = this._colors.cursor;
|
||||
this._renderBlurCursor(terminal, terminal.buffer.x, viewportRelativeCursorY, charData);
|
||||
this._ctx.restore();
|
||||
this._state.x = terminal.buffer.x;
|
||||
@@ -183,37 +186,37 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
y: null,
|
||||
isFocused: null,
|
||||
style: null,
|
||||
width: null
|
||||
width: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private _renderBarCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void {
|
||||
this._ctx.save();
|
||||
this._ctx.fillStyle = this._colors.cursor.css;
|
||||
this._ctx.fillStyle = this._colors.cursor;
|
||||
this.fillLeftLineAtCell(x, y);
|
||||
this._ctx.restore();
|
||||
}
|
||||
|
||||
private _renderBlockCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void {
|
||||
this._ctx.save();
|
||||
this._ctx.fillStyle = this._colors.cursor.css;
|
||||
this._ctx.fillStyle = this._colors.cursor;
|
||||
this.fillCells(x, y, charData[CHAR_DATA_WIDTH_INDEX], 1);
|
||||
this._ctx.fillStyle = this._colors.cursorAccent.css;
|
||||
this._ctx.fillStyle = this._colors.cursorAccent;
|
||||
this.fillCharTrueColor(terminal, charData, x, y);
|
||||
this._ctx.restore();
|
||||
}
|
||||
|
||||
private _renderUnderlineCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void {
|
||||
this._ctx.save();
|
||||
this._ctx.fillStyle = this._colors.cursor.css;
|
||||
this._ctx.fillStyle = this._colors.cursor;
|
||||
this.fillBottomLineAtCells(x, y);
|
||||
this._ctx.restore();
|
||||
}
|
||||
|
||||
private _renderBlurCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void {
|
||||
this._ctx.save();
|
||||
this._ctx.strokeStyle = this._colors.cursor.css;
|
||||
this._ctx.strokeStyle = this._colors.cursor;
|
||||
this.strokeRectAtCell(x, y, charData[CHAR_DATA_WIDTH_INDEX], 1);
|
||||
this._ctx.restore();
|
||||
}
|
||||
@@ -235,7 +238,7 @@ class CursorBlinkStateManager {
|
||||
|
||||
constructor(
|
||||
terminal: ITerminal,
|
||||
private _renderCallback: () => void
|
||||
private renderCallback: () => void
|
||||
) {
|
||||
this.isCursorVisible = true;
|
||||
if (terminal.isFocused) {
|
||||
@@ -270,7 +273,7 @@ class CursorBlinkStateManager {
|
||||
this.isCursorVisible = true;
|
||||
if (!this._animationFrame) {
|
||||
this._animationFrame = window.requestAnimationFrame(() => {
|
||||
this._renderCallback();
|
||||
this.renderCallback();
|
||||
this._animationFrame = null;
|
||||
});
|
||||
}
|
||||
@@ -301,7 +304,7 @@ class CursorBlinkStateManager {
|
||||
// Hide the cursor
|
||||
this.isCursorVisible = false;
|
||||
this._animationFrame = window.requestAnimationFrame(() => {
|
||||
this._renderCallback();
|
||||
this.renderCallback();
|
||||
this._animationFrame = null;
|
||||
});
|
||||
|
||||
@@ -320,7 +323,7 @@ class CursorBlinkStateManager {
|
||||
// Invert visibility and render
|
||||
this.isCursorVisible = !this.isCursorVisible;
|
||||
this._animationFrame = window.requestAnimationFrame(() => {
|
||||
this._renderCallback();
|
||||
this.renderCallback();
|
||||
this._animationFrame = null;
|
||||
});
|
||||
}, BLINK_INTERVAL);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -3,21 +3,25 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ILinkHoverEvent, ITerminal, ILinkifierAccessor, LinkHoverEventTypes } from '../Types';
|
||||
import { IColorSet, IRenderDimensions } from './Types';
|
||||
import { BaseRenderLayer } from './BaseRenderLayer';
|
||||
import { IColorSet, IRenderDimensions } from './Interfaces';
|
||||
import { IBuffer, ICharMeasure, ITerminal, ILinkifierAccessor } from '../Interfaces';
|
||||
import { CHAR_DATA_ATTR_INDEX } from '../Buffer';
|
||||
import { GridCache } from './GridCache';
|
||||
import { FLAGS } from './Types';
|
||||
import { BaseRenderLayer, INVERTED_DEFAULT_COLOR } from './BaseRenderLayer';
|
||||
import { LinkHoverEvent, LinkHoverEventTypes } from '../Types';
|
||||
|
||||
export class LinkRenderLayer extends BaseRenderLayer {
|
||||
private _state: ILinkHoverEvent = null;
|
||||
private _state: LinkHoverEvent = null;
|
||||
|
||||
constructor(container: HTMLElement, zIndex: number, colors: IColorSet, terminal: ILinkifierAccessor) {
|
||||
super(container, 'link', zIndex, true, colors);
|
||||
terminal.linkifier.on(LinkHoverEventTypes.HOVER, (e: ILinkHoverEvent) => this._onLinkHover(e));
|
||||
terminal.linkifier.on(LinkHoverEventTypes.LEAVE, (e: ILinkHoverEvent) => this._onLinkLeave(e));
|
||||
terminal.linkifier.on(LinkHoverEventTypes.HOVER, (e: LinkHoverEvent) => this._onLinkHover(e));
|
||||
terminal.linkifier.on(LinkHoverEventTypes.LEAVE, (e: LinkHoverEvent) => this._onLinkLeave(e));
|
||||
}
|
||||
|
||||
public resize(terminal: ITerminal, dim: IRenderDimensions): void {
|
||||
super.resize(terminal, dim);
|
||||
public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void {
|
||||
super.resize(terminal, dim, charSizeChanged);
|
||||
// Resizing the canvas discards the contents of the canvas so clear state
|
||||
this._state = null;
|
||||
}
|
||||
@@ -28,33 +32,18 @@ export class LinkRenderLayer extends BaseRenderLayer {
|
||||
|
||||
private _clearCurrentLink(): void {
|
||||
if (this._state) {
|
||||
this.clearCells(this._state.x1, this._state.y1, this._state.cols - this._state.x1, 1);
|
||||
const middleRowCount = this._state.y2 - this._state.y1 - 1;
|
||||
if (middleRowCount > 0) {
|
||||
this.clearCells(0, this._state.y1 + 1, this._state.cols, middleRowCount);
|
||||
}
|
||||
this.clearCells(0, this._state.y2, this._state.x2, 1);
|
||||
this.clearCells(this._state.x, this._state.y, this._state.length, 1);
|
||||
this._state = null;
|
||||
}
|
||||
}
|
||||
|
||||
private _onLinkHover(e: ILinkHoverEvent): void {
|
||||
this._ctx.fillStyle = this._colors.foreground.css;
|
||||
if (e.y1 === e.y2) {
|
||||
// Single line link
|
||||
this.fillBottomLineAtCells(e.x1, e.y1, e.x2 - e.x1);
|
||||
} else {
|
||||
// Multi-line link
|
||||
this.fillBottomLineAtCells(e.x1, e.y1, e.cols - e.x1);
|
||||
for (let y = e.y1 + 1; y < e.y2; y++) {
|
||||
this.fillBottomLineAtCells(0, y, e.cols);
|
||||
}
|
||||
this.fillBottomLineAtCells(0, e.y2, e.x2);
|
||||
}
|
||||
private _onLinkHover(e: LinkHoverEvent): void {
|
||||
this._ctx.fillStyle = this._colors.foreground;
|
||||
this.fillBottomLineAtCells(e.x, e.y, e.length);
|
||||
this._state = e;
|
||||
}
|
||||
|
||||
private _onLinkLeave(e: ILinkHoverEvent): void {
|
||||
private _onLinkLeave(e: LinkHoverEvent): void {
|
||||
this._clearCurrentLink();
|
||||
}
|
||||
}
|
||||
|
||||
+56
-73
@@ -3,43 +3,39 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ITerminal, ITheme } from '../Interfaces';
|
||||
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer';
|
||||
import { TextRenderLayer } from './TextRenderLayer';
|
||||
import { SelectionRenderLayer } from './SelectionRenderLayer';
|
||||
import { CursorRenderLayer } from './CursorRenderLayer';
|
||||
import { ColorManager } from './ColorManager';
|
||||
import { IRenderLayer, IColorSet, IRenderer, IRenderDimensions } from './Types';
|
||||
import { ITerminal } from '../Types';
|
||||
import { BaseRenderLayer } from './BaseRenderLayer';
|
||||
import { IRenderLayer, IColorSet, IRenderer, IRenderDimensions } from './Interfaces';
|
||||
import { LinkRenderLayer } from './LinkRenderLayer';
|
||||
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 {
|
||||
private _renderDebouncer: RenderDebouncer;
|
||||
/** A queue of the rows to be refreshed */
|
||||
private _refreshRowsQueue: {start: number, end: number}[] = [];
|
||||
private _refreshAnimationFrame = null;
|
||||
|
||||
private _renderLayers: IRenderLayer[];
|
||||
private _devicePixelRatio: number;
|
||||
private _screenDprMonitor: ScreenDprMonitor;
|
||||
private _isPaused: boolean = false;
|
||||
private _needsFullRefresh: boolean = false;
|
||||
|
||||
public colorManager: ColorManager;
|
||||
public dimensions: IRenderDimensions;
|
||||
|
||||
constructor(private _terminal: ITerminal, theme: ITheme) {
|
||||
super();
|
||||
const allowTransparency = this._terminal.options.allowTransparency;
|
||||
this.colorManager = new ColorManager(document, allowTransparency);
|
||||
this.colorManager = new ColorManager();
|
||||
if (theme) {
|
||||
this.colorManager.setTheme(theme);
|
||||
}
|
||||
|
||||
this._renderLayers = [
|
||||
new TextRenderLayer(this._terminal.screenElement, 0, this.colorManager.colors, allowTransparency),
|
||||
new SelectionRenderLayer(this._terminal.screenElement, 1, this.colorManager.colors),
|
||||
new LinkRenderLayer(this._terminal.screenElement, 2, this.colorManager.colors, this._terminal),
|
||||
new CursorRenderLayer(this._terminal.screenElement, 3, this.colorManager.colors)
|
||||
new TextRenderLayer(this._terminal.element, 0, this.colorManager.colors),
|
||||
new SelectionRenderLayer(this._terminal.element, 1, this.colorManager.colors),
|
||||
new LinkRenderLayer(this._terminal.element, 2, this.colorManager.colors, this._terminal),
|
||||
new CursorRenderLayer(this._terminal.element, 3, this.colorManager.colors)
|
||||
];
|
||||
this.dimensions = {
|
||||
scaledCharWidth: null,
|
||||
@@ -57,25 +53,6 @@ export class Renderer extends EventEmitter implements IRenderer {
|
||||
};
|
||||
this._devicePixelRatio = window.devicePixelRatio;
|
||||
this._updateDimensions();
|
||||
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 {
|
||||
@@ -83,7 +60,7 @@ export class Renderer extends EventEmitter implements IRenderer {
|
||||
// and the terminal needs to refreshed
|
||||
if (this._devicePixelRatio !== devicePixelRatio) {
|
||||
this._devicePixelRatio = devicePixelRatio;
|
||||
this.onResize(this._terminal.cols, this._terminal.rows);
|
||||
this.onResize(this._terminal.cols, this._terminal.rows, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,32 +73,20 @@ export class Renderer extends EventEmitter implements IRenderer {
|
||||
l.reset(this._terminal);
|
||||
});
|
||||
|
||||
if (this._isPaused) {
|
||||
this._needsFullRefresh = true;
|
||||
} else {
|
||||
this._terminal.refresh(0, this._terminal.rows - 1);
|
||||
}
|
||||
this._terminal.refresh(0, this._terminal.rows - 1);
|
||||
|
||||
return this.colorManager.colors;
|
||||
}
|
||||
|
||||
public onResize(cols: number, rows: number): void {
|
||||
public onResize(cols: number, rows: number, didCharSizeChange: boolean): void {
|
||||
// Update character and canvas dimensions
|
||||
this._updateDimensions();
|
||||
|
||||
// Resize all render layers
|
||||
this._renderLayers.forEach(l => l.resize(this._terminal, this.dimensions));
|
||||
this._renderLayers.forEach(l => l.resize(this._terminal, this.dimensions, didCharSizeChange));
|
||||
|
||||
// Force a refresh
|
||||
if (this._isPaused) {
|
||||
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._terminal.refresh(0, this._terminal.rows - 1);
|
||||
|
||||
this.emit('resize', {
|
||||
width: this.dimensions.canvasWidth,
|
||||
@@ -130,39 +95,31 @@ export class Renderer extends EventEmitter implements IRenderer {
|
||||
}
|
||||
|
||||
public onCharSizeChanged(): void {
|
||||
this.onResize(this._terminal.cols, this._terminal.rows);
|
||||
this.onResize(this._terminal.cols, this._terminal.rows, true);
|
||||
}
|
||||
|
||||
public onBlur(): void {
|
||||
this._runOperation(l => l.onBlur(this._terminal));
|
||||
this._renderLayers.forEach(l => l.onBlur(this._terminal));
|
||||
}
|
||||
|
||||
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 {
|
||||
this._runOperation(l => l.onSelectionChanged(this._terminal, start, end));
|
||||
this._renderLayers.forEach(l => l.onSelectionChanged(this._terminal, start, end));
|
||||
}
|
||||
|
||||
public onCursorMove(): void {
|
||||
this._runOperation(l => l.onCursorMove(this._terminal));
|
||||
this._renderLayers.forEach(l => l.onCursorMove(this._terminal));
|
||||
}
|
||||
|
||||
public onOptionsChanged(): void {
|
||||
this._runOperation(l => l.onOptionsChanged(this._terminal));
|
||||
this._renderLayers.forEach(l => l.onOptionsChanged(this._terminal));
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this._runOperation(l => l.reset(this._terminal));
|
||||
}
|
||||
|
||||
private _runOperation(operation: (layer: IRenderLayer) => void): void {
|
||||
if (this._isPaused) {
|
||||
this._needsFullRefresh = true;
|
||||
} else {
|
||||
this._renderLayers.forEach(l => operation(l));
|
||||
}
|
||||
this._renderLayers.forEach(l => l.reset(this._terminal));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -171,19 +128,43 @@ export class Renderer extends EventEmitter implements IRenderer {
|
||||
* @param {number} start The start row.
|
||||
* @param {number} end The end row.
|
||||
*/
|
||||
public refreshRows(start: number, end: number): void {
|
||||
if (this._isPaused) {
|
||||
this._needsFullRefresh = true;
|
||||
return;
|
||||
public queueRefresh(start: number, end: number): void {
|
||||
this._refreshRowsQueue.push({ start: start, end: end });
|
||||
if (!this._refreshAnimationFrame) {
|
||||
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
|
||||
* 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._terminal.emit('refresh', {start, end});
|
||||
}
|
||||
@@ -246,5 +227,7 @@ export class Renderer extends EventEmitter implements IRenderer {
|
||||
// differ.
|
||||
this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._terminal.rows;
|
||||
this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._terminal.cols;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,8 +3,11 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ITerminal } from '../Types';
|
||||
import { IColorSet, IRenderDimensions } from './Types';
|
||||
import { IColorSet, IRenderDimensions } from './Interfaces';
|
||||
import { IBuffer, ICharMeasure, ITerminal } from '../Interfaces';
|
||||
import { CHAR_DATA_ATTR_INDEX } from '../Buffer';
|
||||
import { GridCache } from './GridCache';
|
||||
import { FLAGS } from './Types';
|
||||
import { BaseRenderLayer } from './BaseRenderLayer';
|
||||
|
||||
export class SelectionRenderLayer extends BaseRenderLayer {
|
||||
@@ -18,8 +21,8 @@ export class SelectionRenderLayer extends BaseRenderLayer {
|
||||
};
|
||||
}
|
||||
|
||||
public resize(terminal: ITerminal, dim: IRenderDimensions): void {
|
||||
super.resize(terminal, dim);
|
||||
public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void {
|
||||
super.resize(terminal, dim, charSizeChanged);
|
||||
// Resizing the canvas discards the contents of the canvas so clear state
|
||||
this._state = {
|
||||
start: null,
|
||||
@@ -65,7 +68,7 @@ export class SelectionRenderLayer extends BaseRenderLayer {
|
||||
// Draw first row
|
||||
const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0;
|
||||
const startRowEndCol = viewportCappedStartRow === viewportCappedEndRow ? end[0] : terminal.cols;
|
||||
this._ctx.fillStyle = this._colors.selection.css;
|
||||
this._ctx.fillStyle = this._colors.selection;
|
||||
this.fillCells(startCol, viewportCappedStartRow, startRowEndCol - startCol, 1);
|
||||
|
||||
// Draw middle rows
|
||||
|
||||
@@ -3,19 +3,20 @@
|
||||
* @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 { FLAGS, IColorSet, IRenderDimensions } from './Types';
|
||||
import { CharData, ITerminal } from '../Types';
|
||||
import { INVERTED_DEFAULT_COLOR } from './atlas/Types';
|
||||
import { FLAGS } from './Types';
|
||||
import { GridCache } from './GridCache';
|
||||
import { BaseRenderLayer } from './BaseRenderLayer';
|
||||
import { CharData } from '../Types';
|
||||
import { BaseRenderLayer, INVERTED_DEFAULT_COLOR } from './BaseRenderLayer';
|
||||
|
||||
/**
|
||||
* This CharData looks like a null character, which will forc a clear and render
|
||||
* when the character changes (a regular space ' ' character may not as it's
|
||||
* drawn state is a cleared cell).
|
||||
*/
|
||||
// const OVERLAP_OWNED_CHAR_DATA: CharData = [null, '', 0, -1];
|
||||
const OVERLAP_OWNED_CHAR_DATA: CharData = [null, '', 0, -1];
|
||||
|
||||
export class TextRenderLayer extends BaseRenderLayer {
|
||||
private _state: GridCache<CharData>;
|
||||
@@ -23,16 +24,16 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
private _characterFont: string;
|
||||
private _characterOverlapCache: { [key: string]: boolean } = {};
|
||||
|
||||
constructor(container: HTMLElement, zIndex: number, colors: IColorSet, alpha: boolean) {
|
||||
super(container, 'text', zIndex, alpha, colors);
|
||||
constructor(container: HTMLElement, zIndex: number, colors: IColorSet) {
|
||||
super(container, 'text', zIndex, false, colors);
|
||||
this._state = new GridCache<CharData>();
|
||||
}
|
||||
|
||||
public resize(terminal: ITerminal, dim: IRenderDimensions): void {
|
||||
super.resize(terminal, dim);
|
||||
public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void {
|
||||
super.resize(terminal, dim, charSizeChanged);
|
||||
|
||||
// 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) {
|
||||
this._characterWidth = dim.scaledCharWidth;
|
||||
this._characterFont = terminalFont;
|
||||
@@ -158,14 +159,14 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
// Draw background
|
||||
if (bg < 256) {
|
||||
this._ctx.save();
|
||||
this._ctx.fillStyle = (bg === INVERTED_DEFAULT_COLOR ? this._colors.foreground.css : this._colors.ansi[bg].css);
|
||||
this._ctx.fillStyle = (bg === INVERTED_DEFAULT_COLOR ? this._colors.foreground : this._colors.ansi[bg]);
|
||||
this.fillCells(x, y, width, 1);
|
||||
this._ctx.restore();
|
||||
}
|
||||
|
||||
this._ctx.save();
|
||||
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
|
||||
if (fg < 8) {
|
||||
fg += 8;
|
||||
@@ -174,12 +175,12 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
|
||||
if (flags & FLAGS.UNDERLINE) {
|
||||
if (fg === INVERTED_DEFAULT_COLOR) {
|
||||
this._ctx.fillStyle = this._colors.background.css;
|
||||
this._ctx.fillStyle = this._colors.background;
|
||||
} else if (fg < 256) {
|
||||
// 256 color support
|
||||
this._ctx.fillStyle = this._colors.ansi[fg].css;
|
||||
this._ctx.fillStyle = this._colors.ansi[fg];
|
||||
} else {
|
||||
this._ctx.fillStyle = this._colors.foreground.css;
|
||||
this._ctx.fillStyle = this._colors.foreground;
|
||||
}
|
||||
this.fillBottomLineAtCells(x, y);
|
||||
}
|
||||
@@ -191,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 {
|
||||
// Only single cell characters can be overlapping, rendering issues can
|
||||
// occur without this check
|
||||
@@ -239,13 +236,13 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
* @param x The column of the char.
|
||||
* @param y The row of the char.
|
||||
*/
|
||||
// private _clearChar(x: number, y: number): void {
|
||||
// let colsToClear = 1;
|
||||
// // Clear the adjacent character if it was wide
|
||||
// const state = this._state.cache[x][y];
|
||||
// if (state && state[CHAR_DATA_WIDTH_INDEX] === 2) {
|
||||
// colsToClear = 2;
|
||||
// }
|
||||
// this.clearCells(x, y, colsToClear, 1);
|
||||
// }
|
||||
private _clearChar(x: number, y: number): void {
|
||||
let colsToClear = 1;
|
||||
// Clear the adjacent character if it was wide
|
||||
const state = this._state.cache[x][y];
|
||||
if (state && state[CHAR_DATA_WIDTH_INDEX] === 2) {
|
||||
colsToClear = 2;
|
||||
}
|
||||
this.clearCells(x, y, colsToClear, 1);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-93
@@ -3,11 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ITerminal } from '../Types';
|
||||
import { IEventEmitter, ITheme } from 'xterm';
|
||||
import { IColorSet } from '../shared/Types';
|
||||
|
||||
/**
|
||||
/**
|
||||
* Flags used to render terminal text properly.
|
||||
*/
|
||||
export enum FLAGS {
|
||||
@@ -17,91 +13,4 @@ export enum FLAGS {
|
||||
INVERSE = 8,
|
||||
INVISIBLE = 16,
|
||||
DIM = 32
|
||||
}
|
||||
|
||||
export interface IRenderer extends IEventEmitter {
|
||||
dimensions: IRenderDimensions;
|
||||
colorManager: IColorManager;
|
||||
|
||||
setTheme(theme: ITheme): IColorSet;
|
||||
onWindowResize(devicePixelRatio: number): void;
|
||||
onResize(cols: number, rows: number): 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;
|
||||
}
|
||||
|
||||
// TODO: We should probably rewrite the imports for IColorSet, but there's a lot of them
|
||||
export { IColorSet };
|
||||
|
||||
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): void;
|
||||
|
||||
/**
|
||||
* Clear the state of the render layer.
|
||||
*/
|
||||
reset(terminal: ITerminal): void;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ITerminal } from '../../Types';
|
||||
import { IColorSet } from '../Types';
|
||||
import { ICharAtlasConfig } from '../../shared/atlas/Types';
|
||||
import { generateCharAtlas } from '../../shared/atlas/CharAtlasGenerator';
|
||||
import { generateConfig, configEquals } from './CharAtlasUtils';
|
||||
|
||||
interface ICharAtlasCacheEntry {
|
||||
bitmap: HTMLCanvasElement | Promise<ImageBitmap>;
|
||||
config: ICharAtlasConfig;
|
||||
ownedBy: ITerminal[];
|
||||
}
|
||||
|
||||
let charAtlasCache: ICharAtlasCacheEntry[] = [];
|
||||
|
||||
/**
|
||||
* Acquires a char atlas, either generating a new one or returning an existing
|
||||
* one that is in use by another terminal.
|
||||
* @param terminal The terminal.
|
||||
* @param colors The colors to use.
|
||||
*/
|
||||
export function acquireCharAtlas(terminal: ITerminal, colors: IColorSet, scaledCharWidth: number, scaledCharHeight: number): HTMLCanvasElement | Promise<ImageBitmap> {
|
||||
const newConfig = generateConfig(scaledCharWidth, scaledCharHeight, terminal, colors);
|
||||
|
||||
// Check to see if the terminal already owns this config
|
||||
for (let i = 0; i < charAtlasCache.length; i++) {
|
||||
const entry = charAtlasCache[i];
|
||||
const ownedByIndex = entry.ownedBy.indexOf(terminal);
|
||||
if (ownedByIndex >= 0) {
|
||||
if (configEquals(entry.config, newConfig)) {
|
||||
return entry.bitmap;
|
||||
} else {
|
||||
// The configs differ, release the terminal from the entry
|
||||
if (entry.ownedBy.length === 1) {
|
||||
charAtlasCache.splice(i, 1);
|
||||
} else {
|
||||
entry.ownedBy.splice(ownedByIndex, 1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try match a char atlas from the cache
|
||||
for (let i = 0; i < charAtlasCache.length; i++) {
|
||||
const entry = charAtlasCache[i];
|
||||
if (configEquals(entry.config, newConfig)) {
|
||||
// Add the terminal to the cache entry and return
|
||||
entry.ownedBy.push(terminal);
|
||||
return entry.bitmap;
|
||||
}
|
||||
}
|
||||
|
||||
const canvasFactory = (width: number, height: number) => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
return canvas;
|
||||
};
|
||||
|
||||
const newEntry: ICharAtlasCacheEntry = {
|
||||
bitmap: generateCharAtlas(window, canvasFactory, newConfig),
|
||||
config: newConfig,
|
||||
ownedBy: [terminal]
|
||||
};
|
||||
charAtlasCache.push(newEntry);
|
||||
return newEntry.bitmap;
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ITerminal } from '../../Types';
|
||||
import { IColorSet } from '../Types';
|
||||
import { ICharAtlasConfig } from '../../shared/atlas/Types';
|
||||
|
||||
export function generateConfig(scaledCharWidth: number, scaledCharHeight: number, terminal: ITerminal, colors: IColorSet): ICharAtlasConfig {
|
||||
const clonedColors = {
|
||||
foreground: colors.foreground,
|
||||
background: colors.background,
|
||||
cursor: null,
|
||||
cursorAccent: null,
|
||||
selection: null,
|
||||
ansi: colors.ansi.slice(0, 16)
|
||||
};
|
||||
return {
|
||||
devicePixelRatio: window.devicePixelRatio,
|
||||
scaledCharWidth,
|
||||
scaledCharHeight,
|
||||
fontFamily: terminal.options.fontFamily,
|
||||
fontSize: terminal.options.fontSize,
|
||||
fontWeight: terminal.options.fontWeight,
|
||||
fontWeightBold: terminal.options.fontWeightBold,
|
||||
allowTransparency: terminal.options.allowTransparency,
|
||||
colors: clonedColors
|
||||
};
|
||||
}
|
||||
|
||||
export function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean {
|
||||
for (let i = 0; i < a.colors.ansi.length; i++) {
|
||||
if (a.colors.ansi[i].rgba !== b.colors.ansi[i].rgba) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return a.devicePixelRatio === b.devicePixelRatio &&
|
||||
a.fontFamily === b.fontFamily &&
|
||||
a.fontSize === b.fontSize &&
|
||||
a.fontWeight === b.fontWeight &&
|
||||
a.fontWeightBold === b.fontWeightBold &&
|
||||
a.allowTransparency === b.allowTransparency &&
|
||||
a.scaledCharWidth === b.scaledCharWidth &&
|
||||
a.scaledCharHeight === b.scaledCharHeight &&
|
||||
a.colors.foreground === b.colors.foreground &&
|
||||
a.colors.background === b.colors.background;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
export const INVERTED_DEFAULT_COLOR = -1;
|
||||
export const DIM_OPACITY = 0.5;
|
||||
@@ -1,18 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
export interface IColor {
|
||||
css: string;
|
||||
rgba: number; // 32-bit int with rgba in each byte
|
||||
}
|
||||
|
||||
export interface IColorSet {
|
||||
foreground: IColor;
|
||||
background: IColor;
|
||||
cursor: IColor;
|
||||
cursorAccent: IColor;
|
||||
selection: IColor;
|
||||
ansi: IColor[];
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { FontWeight } from 'xterm';
|
||||
import { CHAR_ATLAS_CELL_SPACING, ICharAtlasConfig } from './Types';
|
||||
import { isFirefox } from '../utils/Browser';
|
||||
|
||||
declare const Promise: any;
|
||||
|
||||
export interface IOffscreenCanvas {
|
||||
width: number;
|
||||
height: number;
|
||||
getContext(type: '2d', config?: Canvas2DContextAttributes): CanvasRenderingContext2D;
|
||||
transferToImageBitmap(): ImageBitmap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, config: ICharAtlasConfig): HTMLCanvasElement | Promise<ImageBitmap> {
|
||||
const cellWidth = config.scaledCharWidth + CHAR_ATLAS_CELL_SPACING;
|
||||
const cellHeight = config.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: config.allowTransparency});
|
||||
|
||||
ctx.fillStyle = config.colors.background.css;
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
ctx.save();
|
||||
ctx.fillStyle = config.colors.foreground.css;
|
||||
ctx.font = getFont(config.fontWeight, config);
|
||||
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(config.fontWeightBold, config);
|
||||
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(config.fontWeight, config);
|
||||
for (let colorIndex = 0; colorIndex < 16; colorIndex++) {
|
||||
// colors 8-15 are bold
|
||||
if (colorIndex === 8) {
|
||||
ctx.font = getFont(config.fontWeightBold, config);
|
||||
}
|
||||
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 = config.colors.ansi[colorIndex].css;
|
||||
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 = config.colors.background.rgba >>> 24;
|
||||
const g = config.colors.background.rgba >>> 16 & 0xFF;
|
||||
const b = config.colors.background.rgba >>> 8 & 0xFF;
|
||||
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, config: ICharAtlasConfig): string {
|
||||
return `${fontWeight} ${config.fontSize * config.devicePixelRatio}px ${config.fontFamily}`;
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { FontWeight } from 'xterm';
|
||||
import { IColorSet } from '../Types';
|
||||
|
||||
export const CHAR_ATLAS_CELL_SPACING = 1;
|
||||
|
||||
export interface ICharAtlasConfig {
|
||||
devicePixelRatio: number;
|
||||
fontSize: number;
|
||||
fontFamily: string;
|
||||
fontWeight: FontWeight;
|
||||
fontWeightBold: FontWeight;
|
||||
scaledCharWidth: number;
|
||||
scaledCharHeight: number;
|
||||
allowTransparency: boolean;
|
||||
colors: IColorSet;
|
||||
}
|
||||
@@ -3,6 +3,8 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { contains } from './Generic';
|
||||
|
||||
const isNode = (typeof navigator === 'undefined') ? true : false;
|
||||
const userAgent = (isNode) ? 'node' : navigator.userAgent;
|
||||
const platform = (isNode) ? 'node' : navigator.platform;
|
||||
@@ -18,12 +20,3 @@ export const isIpad = platform === 'iPad';
|
||||
export const isIphone = platform === 'iPhone';
|
||||
export const isMSWindows = contains(['Windows', 'Win16', 'Win32', 'WinCE'], platform);
|
||||
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;
|
||||
}
|
||||
@@ -4,8 +4,8 @@
|
||||
*/
|
||||
|
||||
import jsdom = require('jsdom');
|
||||
import { ICharMeasure } from '../Types';
|
||||
import { assert } from 'chai';
|
||||
import { ICharMeasure, ITerminal } from '../Interfaces';
|
||||
import { CharMeasure } from './CharMeasure';
|
||||
|
||||
describe('CharMeasure', () => {
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ICharMeasure, ITerminalOptions } from '../Types';
|
||||
import { EventEmitter } from '../EventEmitter';
|
||||
import { ICharMeasure, ITerminal, ITerminalOptions } from '../Interfaces';
|
||||
|
||||
/**
|
||||
* Utility class that measures the size of a character. Measurements are done in
|
||||
@@ -23,7 +23,10 @@ export class CharMeasure extends EventEmitter implements ICharMeasure {
|
||||
this._document = document;
|
||||
this._parentElement = parentElement;
|
||||
this._measureElement = this._document.createElement('span');
|
||||
this._measureElement.classList.add('xterm-char-measure-element');
|
||||
this._measureElement.style.position = 'absolute';
|
||||
this._measureElement.style.top = '0';
|
||||
this._measureElement.style.left = '-9999em';
|
||||
this._measureElement.style.lineHeight = 'normal';
|
||||
this._measureElement.textContent = 'W';
|
||||
this._measureElement.setAttribute('aria-hidden', 'true');
|
||||
this._parentElement.appendChild(this._measureElement);
|
||||
@@ -38,7 +41,7 @@ export class CharMeasure extends EventEmitter implements ICharMeasure {
|
||||
}
|
||||
|
||||
public measure(options: ITerminalOptions): void {
|
||||
this._measureElement.style.fontFamily = options.fontFamily;
|
||||
this._measureElement.style.fontFamily = options.fontFamily;
|
||||
this._measureElement.style.fontSize = `${options.fontSize}px`;
|
||||
const geometry = this._measureElement.getBoundingClientRect();
|
||||
// The element is likely currently display:none, we should retain the
|
||||
@@ -52,4 +55,5 @@ export class CharMeasure extends EventEmitter implements ICharMeasure {
|
||||
this.emit('charsizechanged');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
import { assert } from 'chai';
|
||||
import { CircularList } from './CircularList';
|
||||
|
||||
class TestCircularList<T> extends CircularList<T> {
|
||||
public get array(): T[] { return this._array; }
|
||||
}
|
||||
|
||||
describe('CircularList', () => {
|
||||
describe('push', () => {
|
||||
it('should push values onto the array', () => {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
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,
|
||||
@@ -58,6 +58,16 @@ export class CircularList<T> extends EventEmitter implements ICircularList<T> {
|
||||
this._length = newLength;
|
||||
}
|
||||
|
||||
public get forEach(): (callbackfn: (value: T, index: number) => void) => void {
|
||||
return (callbackfn: (value: T, index: number) => void) => {
|
||||
let i = 0;
|
||||
let length = this.length;
|
||||
for (let i = 0; i < length; i++) {
|
||||
callbackfn(this.get(i), i);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the value at an index.
|
||||
*
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2016 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { assert, expect } from 'chai';
|
||||
import { clone } from './Clone';
|
||||
|
||||
describe('clone', () => {
|
||||
it('should clone simple objects', () => {
|
||||
const test = {
|
||||
a: 1,
|
||||
b: 2
|
||||
};
|
||||
|
||||
assert.deepEqual(clone(test), { a: 1, b: 2 });
|
||||
});
|
||||
|
||||
it('should clone nested objects', () => {
|
||||
const test = {
|
||||
bar: {
|
||||
a: 1,
|
||||
b: 2,
|
||||
c: {
|
||||
foo: 'bar'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
assert.deepEqual(clone(test), {
|
||||
bar: {
|
||||
a: 1,
|
||||
b: 2,
|
||||
c: {
|
||||
foo: 'bar'
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should clone null values', () => {
|
||||
const test = {
|
||||
a: null
|
||||
};
|
||||
|
||||
assert.deepEqual(clone(test), { a: null });
|
||||
});
|
||||
|
||||
it('should clone array values', () => {
|
||||
const test = {
|
||||
a: [1, 2, 3],
|
||||
b: [1, null, 'test', { foo: 'bar' }]
|
||||
};
|
||||
|
||||
assert.deepEqual(clone(test), {
|
||||
a: [1, 2, 3],
|
||||
b: [1, null, 'test', { foo: 'bar' }]
|
||||
});
|
||||
});
|
||||
|
||||
it('should stop mutation from occuring on the original object', () => {
|
||||
const test = {
|
||||
a: 1,
|
||||
b: 2,
|
||||
c: {
|
||||
foo: 'bar'
|
||||
}
|
||||
};
|
||||
|
||||
const cloned = clone(test);
|
||||
|
||||
test.a = 5;
|
||||
test.c.foo = 'barbaz';
|
||||
|
||||
assert.deepEqual(cloned, {
|
||||
a: 1,
|
||||
b: 2,
|
||||
c: {
|
||||
foo: 'bar'
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should clone to a maximum depth of 5 by default', () => {
|
||||
const test = {
|
||||
a: {
|
||||
b: {
|
||||
c: {
|
||||
d: {
|
||||
e: {
|
||||
f: 'foo'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const cloned = clone(test);
|
||||
|
||||
test.a.b.c.d.e.f = 'bar';
|
||||
|
||||
// The values at a greater depth then 5 should not be cloned
|
||||
assert.equal(cloned.a.b.c.d.e.f, 'bar');
|
||||
});
|
||||
|
||||
it('should allow an optional maximum depth to be set', () => {
|
||||
const test = {
|
||||
a: {
|
||||
b: {
|
||||
c: 'foo'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const cloned = clone(test, 2);
|
||||
|
||||
test.a.b.c = 'bar';
|
||||
|
||||
// The values at a greater depth then 2 should not be cloned
|
||||
assert.equal(cloned.a.b.c, 'bar');
|
||||
});
|
||||
|
||||
it('should not throw when cloning a recursive reference', () => {
|
||||
const test = {
|
||||
a: {
|
||||
b: {
|
||||
c: {}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
test.a.b.c = test;
|
||||
|
||||
expect(() => clone(test)).to.not.throw();
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user