Compare commits

..
104 Commits
Author SHA1 Message Date
Paris Kasidiaris 606c68dd0c Bump version to 3.2.0
Signed-off-by: Paris Kasidiaris <paris@sourcelair.com>
2018-03-08 08:59:46 +00:00
Paris Kasidiaris 6741e92cc6 Merge pull request #1313 from xtermjs/docker-image
Add xterm.js Docker image to documentation
2018-03-07 23:00:04 +02:00
Paris Kasidiaris 31ce5c1826 Run the webpack build in the docker image too 2018-03-07 12:38:29 +02:00
Paris Kasidiaris 1f113b1d29 Add xterm.js Docker image to documentation
Closes #1295
2018-03-07 12:28:44 +02:00
Daniel Imms 6e546c0f30 Merge pull request #1298 from Tyriar/1297_webLinks_addon
Pull web link support into an addon
2018-03-06 09:29:33 -08:00
Daniel Imms 7cae8e51ca Merge pull request #1290 from Tyriar/1287_double_click_exception
Check for valid coords before getting word at coords
2018-03-06 09:28:55 -08:00
Daniel Imms bf4dd7de60 Merge branch 'master' into 1287_double_click_exception 2018-03-06 09:28:47 -08:00
Daniel Imms f2d8709dd8 Merge pull request #1309 from Tyriar/1305_alt_horizontal_only
Only move horizontally on alt click in normal buffer
2018-03-05 14:25:45 -08:00
Daniel Imms 7c471f5389 Fix test compile 2018-03-05 11:32:05 -08:00
Daniel Imms 9e62c8692f Only move horizontally on alt click in normal buffer
Fixes #1305
2018-03-05 11:11:54 -08:00
Daniel Imms 131e6ff949 Merge pull request #1302 from nikonso/Enforce-option-value-via-types
Enforce option value via types
2018-02-28 07:47:25 -08:00
Bruno Ribeito c51fa238ff Readd any overloads 2018-02-27 23:18:33 +00:00
Bruno Ribeito 76a3dbf883 Enforce option value via types 2018-02-26 23:27:53 +00:00
Daniel Imms c8017345f1 Merge pull request #1288 from dgriffen/dev/dgriffen/fix-scrollbar
fix terminal width calculation
2018-02-24 11:56:35 -08:00
Daniel Imms 0d2cc7489e Fix tests, add webLinks addon test 2018-02-24 10:36:34 -08:00
Daniel Imms 6a71717751 Allow customization of the web links handler and options 2018-02-23 11:28:14 -08:00
Daniel Imms dc822787d0 Move http links to the new webLinks addon
Fixes #1297
2018-02-23 11:11:23 -08:00
Daniel Imms 5011e629b1 Build addons by scanning addon dir 2018-02-23 11:08:11 -08:00
Daniel Imms 94bbc4e09d Merge pull request #1200 from nikonso/1181
WIP: Use web audio api
2018-02-21 15:36:19 -08:00
Daniel Imms ad7d2262ec Undo package-lock changes 2018-02-21 15:35:53 -08:00
Daniel Imms a91e0e234a Use const everywhere 2018-02-21 14:44:48 -08:00
Daniel Imms af5b3f704e Fix lint 2018-02-21 14:42:04 -08:00
Daniel Imms 45c9763092 Fix indentation 2018-02-21 14:40:22 -08:00
Daniel Imms db8ba17147 Check audio context ctor before constructing 2018-02-21 14:39:43 -08:00
Daniel Imms 1972bf4bb3 Merge remote-tracking branch 'ups/master' into 1181 2018-02-21 14:36:11 -08:00
Daniel Imms 62cf0936dc Merge pull request #1231 from saamalik/ctrl-alt-sequences
Additional alt and control sequences
2018-02-21 14:08:31 -08:00
Daniel Imms ae28814041 Check for valid coords before getting word at coords
Fixes #1287
2018-02-16 21:34:57 -08:00
Daniel Griffen 183fd2fe79 fix terminal width calculation 2018-02-16 10:18:03 -08:00
Saad Malik 8324462b32 Merge branch 'master' into ctrl-alt-sequences 2018-02-15 09:49:16 -08:00
Daniel Imms 93e5996422 Merge pull request #1182 from Tyriar/731_screen_reader
Screen reader support
2018-02-15 06:20:14 -08:00
Daniel Imms f089b997c0 Merge branch 'master' into 731_screen_reader 2018-02-14 09:03:31 -08:00
Saad Malik 51d2064061 Merge branch 'master' into ctrl-alt-sequences 2018-02-13 14:59:46 -08:00
Daniel Imms 712b91a971 Merge pull request #1282 from matthew-matvei/add-freeman
Added FreeMAN to list of real-world uses
2018-02-13 06:37:55 -08:00
Daniel Imms cbe6c64a92 Merge pull request #1280 from AndrienkoAleksandr/fixHeadlessMode
Fix xterm.js in headless mode yields TypeError on resize
2018-02-12 12:42:14 -08:00
Matthew James 64ed8309a1 Added FreeMAN to list of real-world uses 2018-02-12 20:26:09 +00:00
Oleksandr Andriienko b5ba9ae771 Fix xterm.js in headless mode yields TypeError on resize
Signed-off-by: Oleksandr Andriienko <oandriie@redhat.com>
2018-02-12 12:30:26 +02:00
Saad Malik 18f6556061 Merge branch 'master' into ctrl-alt-sequences 2018-02-09 14:00:24 -08:00
Saad Malik 6dc49b7d01 Merge branch 'master' into ctrl-alt-sequences 2018-02-08 12:03:38 -08:00
Saad Malik 73e0db1912 Merge branch 'master' into ctrl-alt-sequences 2018-01-29 10:21:36 -08:00
Saad Malik 9ade1f0a5d Add mapping for backward slash
* Add mapping for backward slash
* Fix mapping for single/double quote
2018-01-28 17:11:13 -08:00
Saad Malik f8fbdf31fa Merge branch 'master' into ctrl-alt-sequences 2018-01-28 16:54:55 -08:00
Daniel Imms a0f390ef54 Expose strings through the API 2018-01-28 13:10:11 -08:00
Daniel Imms ec156f62e1 Only update a11y manager dimensions after a renderer resize 2018-01-27 18:38:28 -08:00
Daniel Imms a3958a1cba Merge branch 'master' into 731_screen_reader 2018-01-27 18:15:31 -08:00
Daniel Imms 7c8b64aaf1 Revert "Hook up scroll APIs to scroll and focus when navigating"
This reverts commit b13826441c.
2018-01-27 18:14:58 -08:00
Daniel Imms b13826441c Hook up scroll APIs to scroll and focus when navigating 2018-01-27 18:14:52 -08:00
Daniel Imms 0bdf1e8265 Remove unused function 2018-01-27 17:51:43 -08:00
Daniel Imms 5506af5322 Clean up a11y manager 2018-01-27 16:47:23 -08:00
Daniel Imms 6439aca1d8 Refresh dimensions of only a single row 2018-01-27 15:00:19 -08:00
Daniel Imms 65850c1c20 Reduce duplication 2018-01-27 14:53:46 -08:00
Daniel Imms 963db4d2fd Remove navigation mode/more rows element 2018-01-27 14:48:45 -08:00
Daniel Imms 5c061195c0 Get navigation right/down working with nav mode 2018-01-27 14:45:22 -08:00
Saad Malik 0104e4214e Reimplement using map
* Reimplemented reg + shift alt digits and characters using a map
* Add test cases for digits
2018-01-26 18:33:37 -08:00
Saad Malik baaf61b3a3 Merge branch 'master' into ctrl-alt-sequences 2018-01-26 13:38:48 -08:00
Daniel Imms b9976c3d17 Merge remote-tracking branch 'origin/master' into 731_screen_reader 2018-01-26 10:24:09 -08:00
Daniel Imms 1cceda6052 Change navigation mode to work using focus instead of activedescendant 2018-01-24 16:34:39 -08:00
Daniel Imms 3e734fab79 Support i18n 2018-01-24 11:15:56 -08:00
Daniel Imms c41b35ae9c Add message about entering navigation mode 2018-01-24 11:04:41 -08:00
Saad Malik e478918ea7 Merge branch 'master' into ctrl-alt-sequences 2018-01-24 09:00:26 -08:00
Saad Malik e1c99b28f0 Merge branch 'master' into ctrl-alt-sequences 2018-01-23 11:16:47 -08:00
Daniel Imms 45c6008b27 Rotate terminal rows after removing focus 2018-01-23 11:11:29 -08:00
Daniel Imms fba4f5f261 Ensure active item is removed before scroll 2018-01-23 10:39:21 -08:00
Daniel Imms bce0cce74d Ensure a11y tree is located above the prompt 2018-01-23 09:50:47 -08:00
Daniel Imms c2d78e57d0 Fix screenReadeMode disable in demo 2018-01-23 09:49:43 -08:00
Saad Malik bb8e06592c Merge branch 'master' into ctrl-alt-sequences 2018-01-22 12:53:45 -08:00
Daniel Imms 421a885acd Merge remote-tracking branch 'origin/master' into 731_screen_reader 2018-01-22 06:32:38 -08:00
Saad Malik d62e4e25b7 Cleaup whitespace 2018-01-21 16:56:58 -08:00
Saad Malik ec9f2702eb Additional alt and control sequences
* Add punctuation alt sequences
* Add support for ctrl+alt sequences
2018-01-21 16:40:38 -08:00
Daniel Imms b97cedcf5b Merge remote-tracking branch 'origin/master' into 731_screen_reader 2018-01-19 14:01:37 -08:00
Daniel Imms 39fcbe61c0 Improve announcement on focus 2018-01-19 10:52:03 -08:00
Daniel Imms 0f04f9fbb4 Rotate rows to ensure an item will be read in nav mode when scrolling 2018-01-17 14:10:03 -08:00
Daniel Imms abfa58fca9 Keep track of nav mode focused element 2018-01-17 10:52:12 -08:00
Daniel Imms 78d734117b Support aria posinset and setsize 2018-01-17 09:56:57 -08:00
Daniel Imms f48da12c16 Prevent event propagation when handled by nav mode 2018-01-16 11:24:19 -08:00
Daniel Imms 3fa4b4941f Support nav mode pgup/pgdown/end/home, check boundaries 2018-01-16 11:01:45 -08:00
Daniel Imms 222c42f7ea Expose screen reader and navigation mode on demo/API 2018-01-16 10:29:40 -08:00
Daniel Imms 34eb08952c Fix RenderDebouncer not refreshing whole viewport when asked 2018-01-16 10:26:09 -08:00
Daniel Imms e9b55930d8 Fix test mocks 2018-01-16 10:12:37 -08:00
Daniel Imms 1e9fb8696a Merge remote-tracking branch 'origin/master' into 731_screen_reader 2018-01-16 09:56:19 -08:00
Daniel Imms 81b4d8fe9c Dispose of NavigationMode when AccessibilityManager is 2018-01-14 13:42:36 -08:00
Daniel Imms 23bd60a733 Properly dispose of disposables 2018-01-14 13:28:43 -08:00
Daniel Imms 85c02f10bf Improve dispoable listener functions 2018-01-14 13:18:00 -08:00
Daniel Imms 70489e5035 Move navigation mode stuff into its own class 2018-01-14 12:59:57 -08:00
Daniel Imms 1e41b11319 Remove logs 2018-01-12 21:15:53 -08:00
Daniel Imms 2a187a26e0 Update a11y rows on scroll 2018-01-11 23:15:02 -08:00
Daniel Imms 2311d7d66b Add basic navigation mode support for current viewport 2018-01-11 22:56:19 -08:00
Daniel Imms bbc9a010b9 Listen to dpr change in a11y manager 2018-01-11 09:27:37 -08:00
Bruno Ribeito b75f52734d Use Web audio API 2018-01-08 20:53:26 +00:00
Daniel Imms a19b05f813 Fix bad type check that caused rows to not refresh 2018-01-08 09:53:06 -08:00
Daniel Imms c46304c716 Merge remote-tracking branch 'origin/master' into 731_screen_reader 2018-01-05 14:28:08 -08:00
Daniel Imms e20d2caa0e Use consistent refresh/render method names 2018-01-05 08:19:30 -08:00
Daniel Imms a846e8c08d Pull render animation frame logic into a helper class 2018-01-05 08:15:21 -08:00
Daniel Imms c332aeb51f Remove obsolete code and resolved TODOs 2018-01-05 07:51:22 -08:00
Daniel Imms bb830eb1ae Rate limit row refresh, tweak live region 2018-01-05 07:39:45 -08:00
Daniel Imms 6d1064fc01 Set height of a11y rows on creation 2018-01-03 10:08:24 -08:00
Daniel Imms 5ca15cfe3b Fix linefeed event
This was causing new lines to join words together
2018-01-03 09:54:06 -08:00
Daniel Imms 17964a517c Add screenReaderMode to typings 2018-01-03 09:28:24 -08:00
Daniel Imms 8cb7a9dabe Merge remote-tracking branch 'origin/v3' into 731_screen_reader 2018-01-03 09:09:42 -08:00
Daniel Imms 5dc57a1305 Add detach logic back for mac only 2017-12-20 11:06:32 -08:00
Daniel Imms 0d65cb3f69 Add max rows to read to handle spam case 2017-12-19 16:27:03 -08:00
Daniel Imms ae957ecb3e Get output reading working on Windows, refresh on window resize 2017-12-19 15:48:58 -08:00
Daniel Imms d03f5b0c2a Range of improvements for macOS VoiceOver 2017-12-19 15:16:28 -08:00
Daniel Imms e5d57b08c8 Merge remote-tracking branch 'ups/v3' into 731_screen_reader 2017-12-19 13:28:31 -08:00
Daniel Imms 399779692d Initial prototype 2017-12-19 12:57:58 -08:00
33 changed files with 925 additions and 222 deletions
+2
View File
@@ -32,6 +32,7 @@ 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>
@@ -74,6 +75,7 @@ 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>
+1 -1
View File
@@ -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 test
RUN npm run build && npm run webpack && npm run test
+15 -2
View File
@@ -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.
@@ -126,6 +126,7 @@ computational environment for Jupyter, supporting interactive data science and s
- [**Pisth**](https://github.com/ColdGrub1384/Pisth): An SFTP and SSH client for iOS
- [**abstruse**](https://github.com/bleenco/abstruse): Abstruse CI is a continuous integration platform based on Node.JS and Docker.
- [**Microsoft SQL Operations Studio**](https://github.com/Microsoft/sqlopsstudio): A data management tool that enables working with SQL Server, Azure SQL DB and SQL DW from Windows, macOS and Linux
- [**FreeMAN**](https://github.com/matthew-matvei/freeman): A free, cross-platform file manager for power users
Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it in our list.
@@ -143,7 +144,19 @@ Then open your project's [Public URL](https://help.sourcelair.com/projects/the-p
### Docker
First, make sure you have Docker Engine 1.13.0 (or newer) and Docker Compose 1.10.0 (or newer). To run the demo and builder in parallel, run the following command in your terminal:
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
+5
View File
@@ -70,6 +70,11 @@
</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>
<script src="dist/bundle.js" defer ></script>
+10 -2
View File
@@ -3,6 +3,7 @@ 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';
@@ -10,6 +11,7 @@ Terminal.applyAddon(attach);
Terminal.applyAddon(fit);
Terminal.applyAddon(fullscreen);
Terminal.applyAddon(search);
Terminal.applyAddon(webLinks);
Terminal.applyAddon(winptyCompat);
@@ -30,7 +32,8 @@ var terminalContainer = document.getElementById('terminal-container'),
macOptionIsMeta: document.querySelector('#option-mac-option-is-meta'),
scrollback: document.querySelector('#option-scrollback'),
tabstopwidth: document.querySelector('#option-tabstopwidth'),
bellStyle: document.querySelector('#option-bell-style')
bellStyle: document.querySelector('#option-bell-style'),
screenReaderMode: document.querySelector('#option-screen-reader-mode')
},
colsElement = document.getElementById('cols'),
rowsElement = document.getElementById('rows'),
@@ -86,6 +89,9 @@ optionElements.scrollback.addEventListener('change', function () {
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();
@@ -98,7 +104,8 @@ function createTerminal() {
macOptionIsMeta: optionElements.macOptionIsMeta.enabled,
cursorBlink: optionElements.cursorBlink.checked,
scrollback: parseInt(optionElements.scrollback.value, 10),
tabStopWidth: parseInt(optionElements.tabstopwidth.value, 10)
tabStopWidth: parseInt(optionElements.tabstopwidth.value, 10),
screenReaderMode: optionElements.screenReaderMode.checked
});
window.term = term; // Expose `term` to window for debugging purposes
term.on('resize', function (size) {
@@ -116,6 +123,7 @@ function createTerminal() {
term.open(terminalContainer);
term.winptyCompatInit();
term.webLinksInit();
term.fit();
term.focus();
+1
View File
@@ -2,6 +2,7 @@ version: "3"
services:
web:
image: xtermjs/xterm.js:latest
build: .
volumes:
- ./:/usr/src/app
+1 -1
View File
@@ -23,7 +23,7 @@ const tsProject = ts.createProject('tsconfig.json');
const srcDir = tsProject.config.compilerOptions.rootDir;
let outDir = tsProject.config.compilerOptions.outDir;
const addons = ['attach', 'fit', 'fullscreen', 'search', 'terminado', 'winptyCompat', 'zmodem'];
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.
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "xterm",
"description": "Full xterm terminal, in your browser",
"version": "3.1.0",
"version": "3.2.0",
"ignore": [
"demo",
"test",
+287
View File
@@ -0,0 +1,287 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import * as Strings from './Strings';
import { ITerminal, IBuffer } from './Types';
import { isMac } from './shared/utils/Browser';
import { RenderDebouncer } from './utils/RenderDebouncer';
import { addDisposableListener } from './utils/Dom';
import { IDisposable } from 'xterm';
const MAX_ROWS_TO_READ = 20;
const ACTIVE_ITEM_ID_PREFIX = 'xterm-active-item-';
enum BoundaryPosition {
Top,
Bottom
}
export class AccessibilityManager implements IDisposable {
private _accessibilityTreeRoot: HTMLElement;
private _rowContainer: HTMLElement;
private _rowElements: HTMLElement[] = [];
private _liveRegion: HTMLElement;
private _liveRegionLineCount: number = 0;
private _renderRowsDebouncer: RenderDebouncer;
private _topBoundaryFocusListener: (e: FocusEvent) => void;
private _bottomBoundaryFocusListener: (e: FocusEvent) => void;
private _disposables: IDisposable[] = [];
/**
* This queue has a character pushed to it for keys that are pressed, if the
* next character added to the terminal is equal to the key char then it is
* not announced (added to live region) because it has already been announced
* by the textarea event (which cannot be canceled). There are some race
* condition cases if there is typing while data is streaming, but this covers
* the main case of typing into the prompt and inputting the answer to a
* question (Y/N, etc.).
*/
private _charsToConsume: string[] = [];
constructor(private _terminal: ITerminal) {
this._accessibilityTreeRoot = document.createElement('div');
this._accessibilityTreeRoot.classList.add('xterm-accessibility');
this._rowContainer = document.createElement('div');
this._rowContainer.classList.add('xterm-accessibility-tree');
for (let i = 0; i < this._terminal.rows; i++) {
this._rowElements[i] = this._createAccessibilityTreeNode();
this._rowContainer.appendChild(this._rowElements[i]);
}
this._topBoundaryFocusListener = e => this._onBoundaryFocus(e, BoundaryPosition.Top);
this._bottomBoundaryFocusListener = e => this._onBoundaryFocus(e, BoundaryPosition.Bottom);
this._rowElements[0].addEventListener('focus', this._topBoundaryFocusListener);
this._rowElements[this._rowElements.length - 1].addEventListener('focus', this._bottomBoundaryFocusListener);
this._refreshRowsDimensions();
this._accessibilityTreeRoot.appendChild(this._rowContainer);
this._renderRowsDebouncer = new RenderDebouncer(this._terminal, this._renderRows.bind(this));
this._refreshRows();
this._liveRegion = document.createElement('div');
this._liveRegion.classList.add('live-region');
this._liveRegion.setAttribute('aria-live', 'assertive');
this._accessibilityTreeRoot.appendChild(this._liveRegion);
this._terminal.element.insertAdjacentElement('afterbegin', this._accessibilityTreeRoot);
this._disposables.push(this._renderRowsDebouncer);
this._disposables.push(this._terminal.addDisposableListener('resize', data => this._onResize(data.cols, data.rows)));
this._disposables.push(this._terminal.addDisposableListener('refresh', data => this._refreshRows(data.start, data.end)));
this._disposables.push(this._terminal.addDisposableListener('scroll', data => this._refreshRows()));
// Line feed is an issue as the prompt won't be read out after a command is run
this._disposables.push(this._terminal.addDisposableListener('a11y.char', (char) => this._onChar(char)));
this._disposables.push(this._terminal.addDisposableListener('linefeed', () => this._onChar('\n')));
this._disposables.push(this._terminal.addDisposableListener('a11y.tab', spaceCount => this._onTab(spaceCount)));
this._disposables.push(this._terminal.addDisposableListener('key', keyChar => this._onKey(keyChar)));
this._disposables.push(this._terminal.addDisposableListener('blur', () => this._clearLiveRegion()));
// TODO: Maybe renderer should fire an event on terminal when the characters change and that
// should be listened to instead? That would mean that the order of events are always
// guarenteed
this._disposables.push(this._terminal.addDisposableListener('dprchange', () => this._refreshRowsDimensions()));
this._disposables.push(this._terminal.renderer.addDisposableListener('resize', () => this._refreshRowsDimensions()));
// This shouldn't be needed on modern browsers but is present in case the
// media query that drives the dprchange event isn't supported
this._disposables.push(addDisposableListener(window, 'resize', () => this._refreshRowsDimensions()));
}
public dispose(): void {
this._terminal.element.removeChild(this._accessibilityTreeRoot);
this._disposables.forEach(d => d.dispose());
this._disposables = null;
this._accessibilityTreeRoot = null;
this._rowContainer = null;
this._liveRegion = null;
this._rowContainer = null;
this._rowElements = null;
}
private _onBoundaryFocus(e: FocusEvent, position: BoundaryPosition): void {
const boundaryElement = <HTMLElement>e.target;
const beforeBoundaryElement = this._rowElements[position === BoundaryPosition.Top ? 1 : this._rowElements.length - 2];
// Don't scroll if the buffer top has reached the end in that direction
const posInSet = boundaryElement.getAttribute('aria-posinset');
const lastRowPos = position === BoundaryPosition.Top ? '1' : `${this._terminal.buffer.lines.length}`;
if (posInSet === lastRowPos) {
return;
}
// Don't scroll when the last focused item was not the second row (focus is going the other
// direction)
if (e.relatedTarget !== beforeBoundaryElement) {
return;
}
// Remove old boundary element from array
let topBoundaryElement: HTMLElement;
let bottomBoundaryElement: HTMLElement;
if (position === BoundaryPosition.Top) {
topBoundaryElement = boundaryElement;
bottomBoundaryElement = this._rowElements.pop();
this._rowContainer.removeChild(bottomBoundaryElement);
} else {
topBoundaryElement = this._rowElements.shift();
bottomBoundaryElement = boundaryElement;
this._rowContainer.removeChild(topBoundaryElement);
}
// Remove listeners from old boundary elements
topBoundaryElement.removeEventListener('focus', this._topBoundaryFocusListener);
bottomBoundaryElement.removeEventListener('focus', this._bottomBoundaryFocusListener);
// Add new element to array/DOM
if (position === BoundaryPosition.Top) {
const newElement = this._createAccessibilityTreeNode();
this._rowElements.unshift(newElement);
this._rowContainer.insertAdjacentElement('afterbegin', newElement);
} else {
const newElement = this._createAccessibilityTreeNode();
this._rowElements.push(newElement);
this._rowContainer.appendChild(newElement);
}
// Add listeners to new boundary elements
this._rowElements[0].addEventListener('focus', this._topBoundaryFocusListener);
this._rowElements[this._rowElements.length - 1].addEventListener('focus', this._bottomBoundaryFocusListener);
// Scroll up
this._terminal.scrollLines(position === BoundaryPosition.Top ? -1 : 1);
// Focus new boundary before element
this._rowElements[position === BoundaryPosition.Top ? 1 : this._rowElements.length - 2].focus();
// Prevent the standard behavior
e.preventDefault();
e.stopImmediatePropagation();
}
private _onResize(cols: number, rows: number): void {
// Remove bottom boundary listener
this._rowElements[this._rowElements.length - 1].removeEventListener('focus', this._bottomBoundaryFocusListener);
// Grow rows as required
for (let i = this._rowContainer.children.length; i < this._terminal.rows; i++) {
this._rowElements[i] = this._createAccessibilityTreeNode();
this._rowContainer.appendChild(this._rowElements[i]);
}
// Shrink rows as required
while (this._rowElements.length > rows) {
this._rowContainer.removeChild(this._rowElements.pop());
}
// Add bottom boundary listener
this._rowElements[this._rowElements.length - 1].addEventListener('focus', this._bottomBoundaryFocusListener);
this._refreshRowsDimensions();
}
public _createAccessibilityTreeNode(): HTMLElement {
const element = document.createElement('div');
element.setAttribute('role', 'listitem');
element.tabIndex = -1;
this._refreshRowDimensions(element);
return element;
}
private _onTab(spaceCount: number): void {
for (let i = 0; i < spaceCount; i++) {
this._onChar(' ');
}
}
private _onChar(char: string): void {
if (this._liveRegionLineCount < MAX_ROWS_TO_READ + 1) {
if (this._charsToConsume.length > 0) {
// Have the screen reader ignore the char if it was just input
const shiftedChar = this._charsToConsume.shift();
if (shiftedChar !== char) {
this._announceCharacter(char);
}
} else {
this._announceCharacter(char);
}
if (char === '\n') {
this._liveRegionLineCount++;
if (this._liveRegionLineCount === MAX_ROWS_TO_READ + 1) {
this._liveRegion.textContent += Strings.tooMuchOutput;
}
}
// Only detach/attach on mac as otherwise messages can go unaccounced
if (isMac) {
if (this._liveRegion.textContent.length > 0 && !this._liveRegion.parentNode) {
setTimeout(() => {
this._accessibilityTreeRoot.appendChild(this._liveRegion);
}, 0);
}
}
}
}
private _clearLiveRegion(): void {
this._liveRegion.textContent = '';
this._liveRegionLineCount = 0;
// Only detach/attach on mac as otherwise messages can go unaccounced
if (isMac) {
if (this._liveRegion.parentNode) {
this._accessibilityTreeRoot.removeChild(this._liveRegion);
}
}
}
private _onKey(keyChar: string): void {
this._clearLiveRegion();
this._charsToConsume.push(keyChar);
}
private _refreshRows(start?: number, end?: number): void {
this._renderRowsDebouncer.refresh(start, end);
}
private _renderRows(start: number, end: number): void {
const buffer: IBuffer = this._terminal.buffer;
const setSize = buffer.lines.length.toString();
for (let i = start; i <= end; i++) {
const lineData = buffer.translateBufferLineToString(buffer.ydisp + i, true);
const posInSet = (buffer.ydisp + i + 1).toString();
const element = this._rowElements[i];
element.textContent = lineData.length === 0 ? Strings.blankLine : lineData;
element.setAttribute('aria-posinset', posInSet);
element.setAttribute('aria-setsize', setSize);
}
}
private _refreshRowsDimensions(): void {
if (!this._terminal.renderer.dimensions.actualCellHeight) {
return;
}
const buffer: IBuffer = this._terminal.buffer;
for (let i = 0; i < this._terminal.rows; i++) {
this._refreshRowDimensions(this._rowElements[i]);
}
}
private _refreshRowDimensions(element: HTMLElement): void {
element.style.height = `${this._terminal.renderer.dimensions.actualCellHeight}px`;
}
private _announceCharacter(char: string): void {
if (char === ' ') {
// Always use nbsp for spaces in order to preserve the space between characters in
// voiceover's caption window
this._liveRegion.innerHTML += '&nbsp;';
} else {
this._liveRegion.textContent += char;
}
}
}
+25 -5
View File
@@ -3,10 +3,11 @@
* @license MIT
*/
import { IEventEmitter } from 'xterm';
import { XtermListener } from './Types';
import { IEventEmitter, IDisposable } from 'xterm';
export class EventEmitter implements IEventEmitter {
private _events: {[type: string]: ((...args: any[]) => void)[]};
private _events: {[type: string]: XtermListener[]};
constructor() {
// Restore the previous events if available, this will happen if the
@@ -14,12 +15,31 @@ export class EventEmitter implements IEventEmitter {
this._events = this._events || {};
}
public on(type: string, listener: ((...args: any[]) => void)): void {
public on(type: string, listener: XtermListener): void {
this._events[type] = this._events[type] || [];
this._events[type].push(listener);
}
public off(type: string, listener: ((...args: any[]) => void)): void {
/**
* Adds a disposabe listener to the EventEmitter, returning the disposable.
* @param type The event type.
* @param handler The handler for the listener.
*/
public addDisposableListener(type: string, handler: XtermListener): IDisposable {
this.on(type, handler);
return {
dispose: () => {
if (!handler) {
// Already disposed
return;
}
this.off(type, handler);
handler = null;
}
};
}
public off(type: string, listener: XtermListener): void {
if (!this._events[type]) {
return;
}
@@ -51,7 +71,7 @@ export class EventEmitter implements IEventEmitter {
}
}
public listeners(type: string): ((...args: any[]) => void)[] {
public listeners(type: string): XtermListener[] {
return this._events[type] || [];
}
+8
View File
@@ -31,6 +31,10 @@ export class InputHandler implements IInputHandler {
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
@@ -162,7 +166,11 @@ 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);
}
}
/**
+3 -9
View File
@@ -101,12 +101,6 @@ 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);
@@ -200,19 +194,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, 0, bId]);
assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [aId, 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, 0, aId]);
assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [bId, 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), [0, aId, bId]);
assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [aId, bId]);
});
});
});
+3 -47
View File
@@ -8,30 +8,6 @@ import { ILinkHoverEvent, ILinkMatcher, LinkMatcherHandler, LinkMatcherValidatio
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.
*/
@@ -47,7 +23,7 @@ export class Linkifier extends EventEmitter implements ILinkifier {
private _mouseZoneManager: IMouseZoneManager;
private _rowsTimeoutId: number;
private _nextLinkMatcherId = HYPERTEXT_LINK_MATCHER_ID;
private _nextLinkMatcherId = 0;
private _rowsToLinkify: {start: number, end: number};
constructor(
@@ -58,7 +34,6 @@ export class Linkifier extends EventEmitter implements ILinkifier {
start: null,
end: null
};
this.registerLinkMatcher(strictUrlRegex, null, { matchIndex: 1 });
}
/**
@@ -111,23 +86,6 @@ 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.
@@ -139,7 +97,7 @@ 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 (this._nextLinkMatcherId !== HYPERTEXT_LINK_MATCHER_ID && !handler) {
if (!handler) {
throw new Error('handler must be defined');
}
const matcher: ILinkMatcher = {
@@ -185,8 +143,7 @@ export class Linkifier extends EventEmitter implements ILinkifier {
* @return Whether a link matcher was found and deregistered.
*/
public deregisterLinkMatcher(matcherId: number): boolean {
// ID 0 is the hypertext link matcher which cannot be deregistered
for (let i = 1; i < this._linkMatchers.length; i++) {
for (let i = 0; i < this._linkMatchers.length; i++) {
if (this._linkMatchers[i].id === matcherId) {
this._linkMatchers.splice(i, 1);
return true;
@@ -222,7 +179,6 @@ export class Linkifier extends EventEmitter implements ILinkifier {
private _doLinkifyRow(rowIndex: number, text: string, matcher: ILinkMatcher, 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);
+7 -2
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { ITerminal, ICircularList, ISelectionManager, IBuffer, LineData, CharData } from './Types';
import { ITerminal, ICircularList, ISelectionManager, IBuffer, LineData, CharData, XtermListener } from './Types';
import { MouseHelper } from './utils/MouseHelper';
import * as Browser from './shared/utils/Browser';
import { CharMeasure } from './utils/CharMeasure';
@@ -101,7 +101,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
private _mouseMoveListener: EventListener;
private _mouseUpListener: EventListener;
private _trimListener: (...args: any[]) => void;
private _trimListener: XtermListener;
private _mouseDownTimeStamp: number;
@@ -634,6 +634,11 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
* @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;
}
const bufferLine = this._buffer.lines.get(coords[1]);
if (!bufferLine) {
return null;
+60
View File
@@ -0,0 +1,60 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { ITerminal, ISoundManager } from './Types';
// Source: https://freesound.org/people/altemark/sounds/45759/
// This sound is released under the Creative Commons Attribution 3.0 Unported
// (CC BY 3.0) license. It was created by 'altemark'. No modifications have been
// made, apart from the conversion to base64.
export const DEFAULT_BELL_SOUND = 'data:audio/wav;base64,UklGRigBAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQQBAADpAFgCwAMlBZoG/wdmCcoKRAypDQ8PbRDBEQQTOxRtFYcWlBePGIUZXhoiG88bcBz7HHIdzh0WHlMeZx51HmkeUx4WHs8dah0AHXwc3hs9G4saxRnyGBIYGBcQFv8U4RPAEoYRQBACD70NWwwHC6gJOwjWBloF7gOBAhABkf8b/qv8R/ve+Xf4Ife79W/0JfPZ8Z/wde9N7ijtE+wU6xvqM+lb6H7nw+YX5mrlxuQz5Mzje+Ma49fioeKD4nXiYeJy4pHitOL04j/jn+MN5IPkFOWs5U3mDefM55/ogOl36m7rdOyE7abuyu8D8Unyj/Pg9D/2qfcb+Yn6/vuK/Qj/lAAlAg==';
export class SoundManager implements ISoundManager {
private _audioContext: AudioContext;
constructor(
private _terminal: ITerminal
) {
}
public playBellSound(): void {
const audioContextCtor: typeof AudioContext = (<any>window).AudioContext || (<any>window).webkitAudioContext;
if (!this._audioContext && audioContextCtor) {
this._audioContext = new audioContextCtor();
}
if (this._audioContext) {
const bellAudioSource = this._audioContext.createBufferSource();
const context = this._audioContext;
this._audioContext.decodeAudioData(this.base64ToArrayBuffer(this.removeMimeType(this._terminal.options.bellSound)), (buffer) => {
bellAudioSource.buffer = buffer;
bellAudioSource.connect(context.destination);
bellAudioSource.start(0);
});
} else {
console.warn('Sorry, but the Web Audio API is not supported by your browser. Please, consider upgrading to the latest version');
}
}
private base64ToArrayBuffer(base64: string): ArrayBuffer {
const binaryString = window.atob(base64);
const len = binaryString.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes.buffer;
}
private removeMimeType(dataURI: string): string {
// Split the input to get the mime-type and the data itself
const splitUri = dataURI.split(',');
// Return only the data
return splitUri[1];
}
}
+8
View File
@@ -0,0 +1,8 @@
/**
* 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';
+93
View File
@@ -578,6 +578,99 @@ 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', () => {
+97 -87
View File
@@ -39,12 +39,43 @@ import { Linkifier } from './Linkifier';
import { SelectionManager } from './SelectionManager';
import { CharMeasure } from './utils/CharMeasure';
import * as Browser from './shared/utils/Browser';
import * as Strings from './Strings';
import { MouseHelper } from './utils/MouseHelper';
import { CHARSETS } from './Charsets';
import { BELL_SOUND } from './utils/Sounds';
import { DEFAULT_BELL_SOUND, SoundManager } from './SoundManager';
import { DEFAULT_ANSI_COLORS } from './renderer/ColorManager';
import { MouseZoneManager } from './input/MouseZoneManager';
import { ITheme } from 'xterm';
import { AccessibilityManager } from './AccessibilityManager';
import { ScreenDprMonitor } from './utils/ScreenDprMonitor';
import { ITheme, ILocalizableStrings } from 'xterm';
// reg + shift key mappings for digits and special chars
const KEYCODE_KEY_MAPPINGS = {
// digits 0-9
48: ['0', ')'],
49: ['1', '!'],
50: ['2', '@'],
51: ['3', '#'],
52: ['4', '$'],
53: ['5', '%'],
54: ['6', '^'],
55: ['7', '&'],
56: ['8', '*'],
57: ['9', '('],
// special chars
186: [';', ':'],
187: ['=', '+'],
188: [',', '<'],
189: ['-', '_'],
190: ['.', '>'],
191: ['/', '?'],
192: ['`', '~'],
219: ['[', '{'],
220: ['\\', '|'],
221: [']', '}'],
222: ['\'', '"']
};
// Let it work inside Node.js for automated testing purposes.
const document = (typeof window !== 'undefined') ? window.document : null;
@@ -69,7 +100,7 @@ const DEFAULT_OPTIONS: ITerminalOptions = {
termName: 'xterm',
cursorBlink: false,
cursorStyle: 'block',
bellSound: BELL_SOUND,
bellSound: DEFAULT_BELL_SOUND,
bellStyle: 'none',
enableBold: true,
fontFamily: 'courier-new, courier, monospace',
@@ -80,6 +111,7 @@ const DEFAULT_OPTIONS: ITerminalOptions = {
letterSpacing: 0,
scrollback: 1000,
screenKeys: false,
screenReaderMode: false,
debug: false,
macOptionIsMeta: false,
cancelEvents: false,
@@ -110,7 +142,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
private helperContainer: HTMLElement;
private compositionView: HTMLElement;
private charSizeStyleElement: HTMLStyleElement;
private bellAudioElement: HTMLAudioElement;
private visualBellTimer: number;
public browser: IBrowser = <any>Browser;
@@ -192,6 +224,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
private userScrolling: boolean;
private inputHandler: InputHandler;
public soundManager: SoundManager;
private parser: Parser;
public renderer: IRenderer;
public selectionManager: SelectionManager;
@@ -202,6 +235,8 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
public charMeasure: CharMeasure;
private _mouseZoneManager: IMouseZoneManager;
public mouseHelper: MouseHelper;
private _accessibilityManager: AccessibilityManager;
private _screenDprMonitor: ScreenDprMonitor;
public cols: number;
public rows: number;
@@ -294,6 +329,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
this.selectionManager = this.selectionManager || null;
this.linkifier = this.linkifier || new Linkifier(this);
this._mouseZoneManager = this._mouseZoneManager || null;
this.soundManager = this.soundManager || new SoundManager(this);
// Create the terminal's buffers and set the current buffer
this.buffers = new BufferSet(this);
@@ -310,6 +346,10 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
return this.buffers.active;
}
public static get strings(): ILocalizableStrings {
return Strings;
}
/**
* back_color_erase feature for xterm.
*/
@@ -442,9 +482,19 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
this.buffers.resize(this.cols, this.rows);
this.viewport.syncScrollArea();
break;
case 'screenReaderMode':
if (value) {
if (!this._accessibilityManager) {
this._accessibilityManager = new AccessibilityManager(this);
}
} else {
if (this._accessibilityManager) {
this._accessibilityManager.dispose();
this._accessibilityManager = null;
}
}
break;
case 'tabStopWidth': this.buffers.setupTabStops(); break;
case 'bellSound':
case 'bellStyle': this.syncBellSound(); break;
}
// Inform renderer of changes
if (this.renderer) {
@@ -476,6 +526,9 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
* Binds the desired blur behavior on a given terminal object.
*/
private _onTextAreaBlur(): void {
// Text can safely be removed on blur. Doing it earlier could interfere with
// screen readers reading it out.
this.textarea.value = '';
this.refresh(this.buffer.y, this.buffer.y);
if (this.sendFocus) {
this.send(C0.ESC + '[O');
@@ -556,16 +609,8 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
}
}, true);
on(this.textarea, 'keydown', (ev: KeyboardEvent) => {
this._keyDown(ev);
}, true);
on(this.textarea, 'keypress', (ev: KeyboardEvent) => {
this._keyPress(ev);
// Truncate the textarea's value, since it is not needed
this.textarea.value = '';
}, true);
on(this.textarea, 'keydown', (ev: KeyboardEvent) => this._keyDown(ev), true);
on(this.textarea, 'keypress', (ev: KeyboardEvent) => this._keyPress(ev), true);
on(this.textarea, 'compositionstart', () => this.compositionHelper.compositionstart());
on(this.textarea, 'compositionupdate', (e: CompositionEvent) => this.compositionHelper.compositionupdate(e));
on(this.textarea, 'compositionend', () => this.compositionHelper.compositionend());
@@ -593,6 +638,9 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
this.document = this.parent.ownerDocument;
this.body = <HTMLBodyElement>this.document.body;
this._screenDprMonitor = new ScreenDprMonitor();
this._screenDprMonitor.setListener(() => this.emit('dprchange', window.devicePixelRatio));
// Create main element container
this.element = this.document.createElement('div');
this.element.classList.add('terminal');
@@ -625,6 +673,9 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
this.textarea = document.createElement('textarea');
this.textarea.classList.add('xterm-helper-textarea');
// TODO: New API to set title? This could say "Terminal bash input", etc.
this.textarea.setAttribute('aria-label', Strings.promptLabel);
this.textarea.setAttribute('aria-multiline', 'false');
this.textarea.setAttribute('autocorrect', 'off');
this.textarea.setAttribute('autocapitalize', 'off');
this.textarea.setAttribute('spellcheck', 'false');
@@ -642,9 +693,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
this.helperContainer.appendChild(this.charSizeStyleElement);
this.charMeasure = new CharMeasure(document, this.helperContainer);
// Preload audio, this relied on helperContainer
this.syncBellSound();
// Performance: Add viewport and helper elements from the fragment
this.element.appendChild(fragment);
@@ -657,6 +705,10 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
this.on('resize', () => this.renderer.onResize(this.cols, this.rows, false));
this.on('blur', () => this.renderer.onBlur());
this.on('focus', () => this.renderer.onFocus());
this.on('dprchange', () => this.renderer.onWindowResize(window.devicePixelRatio));
// dprchange should handle this case, we need this as well for browsers that don't support the
// matchMedia query.
window.addEventListener('resize', () => this.renderer.onWindowResize(window.devicePixelRatio));
this.charMeasure.on('charsizechanged', () => this.renderer.onResize(this.cols, this.rows, true));
this.renderer.on('resize', (dimensions) => this.viewport.syncScrollArea());
@@ -679,6 +731,12 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
this.mouseHelper = new MouseHelper(this.renderer);
if (this.options.screenReaderMode) {
// Note that this must be done *after* the renderer is created in order to
// ensure the correct order of the dprchange event
this._accessibilityManager = new AccessibilityManager(this);
}
// Measure the character size
this.charMeasure.measure(this.options);
@@ -1046,7 +1104,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
*/
public refresh(start: number, end: number): void {
if (this.renderer) {
this.renderer.queueRefresh(start, end);
this.renderer.refreshRows(start, end);
}
}
@@ -1268,36 +1326,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
this.customKeyEventHandler = customKeyEventHandler;
}
/**
* Attaches a http(s) link handler, forcing web links to behave differently to
* regular <a> tags. This will trigger a refresh as links potentially need to be
* reconstructed. Calling this with null will remove the handler.
* @param handler The handler callback function.
*/
public setHypertextLinkHandler(handler: LinkMatcherHandler): void {
if (!this.linkifier) {
throw new Error('Cannot attach a hypertext link handler before Terminal.open is called');
}
this.linkifier.setHypertextLinkHandler(handler);
// Refresh to force links to refresh
this.refresh(0, this.rows - 1);
}
/**
* Attaches a validation callback for hypertext links. This is useful to use
* validation logic or to do something with the link's element and url.
* @param callback The callback to use, this can
* be cleared with null.
*/
public setHypertextValidationCallback(callback: LinkMatcherValidationCallback): void {
if (!this.linkifier) {
throw new Error('Cannot attach a hypertext validation callback before Terminal.open is called');
}
this.linkifier.setHypertextValidationCallback(callback);
// // Refresh to force links to refresh
this.refresh(0, this.rows - 1);
}
/**
* Registers a link matcher, allowing custom link patterns to be matched and
* handled.
@@ -1309,12 +1337,9 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
* @return The ID of the new matcher, this can be used to deregister.
*/
public registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options?: ILinkMatcherOptions): number {
if (this.linkifier) {
const matcherId = this.linkifier.registerLinkMatcher(regex, handler, options);
this.refresh(0, this.rows - 1);
return matcherId;
}
return 0;
const matcherId = this.linkifier.registerLinkMatcher(regex, handler, options);
this.refresh(0, this.rows - 1);
return matcherId;
}
/**
@@ -1322,10 +1347,8 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
* @param matcherId The link matcher's ID (returned after register)
*/
public deregisterLinkMatcher(matcherId: number): void {
if (this.linkifier) {
if (this.linkifier.deregisterLinkMatcher(matcherId)) {
this.refresh(0, this.rows - 1);
}
if (this.linkifier.deregisterLinkMatcher(matcherId)) {
this.refresh(0, this.rows - 1);
}
}
@@ -1729,14 +1752,15 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
// ^] - Operating System Command (OSC)
result.key = String.fromCharCode(29);
}
} else if ((!this.browser.isMac || this.options.macOptionIsMeta) && ev.altKey && !ev.ctrlKey && !ev.metaKey) {
} else if ((!this.browser.isMac || this.options.macOptionIsMeta) && ev.altKey && !ev.metaKey) {
// On macOS this is a third level shift when !macOptionIsMeta. Use <Esc> instead.
if (ev.keyCode >= 65 && ev.keyCode <= 90) {
result.key = C0.ESC + String.fromCharCode(ev.keyCode + 32);
} else if (ev.keyCode === 192) {
result.key = C0.ESC + '`';
} else if (ev.keyCode >= 48 && ev.keyCode <= 57) {
result.key = C0.ESC + (ev.keyCode - 48);
const keyMapping = KEYCODE_KEY_MAPPINGS[ev.keyCode];
const key = keyMapping && keyMapping[!ev.shiftKey ? 0 : 1];
if (key) {
result.key = C0.ESC + key;
} else if (ev.keyCode >= 65 && ev.keyCode <= 90) {
const keyCode = ev.ctrlKey ? ev.keyCode - 64 : ev.keyCode + 32;
result.key = C0.ESC + String.fromCharCode(keyCode);
}
} else if (this.browser.isMac && !ev.altKey && !ev.ctrlKey && ev.metaKey) {
if (ev.keyCode === 65) { // cmd + a
@@ -1832,7 +1856,9 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
*/
public bell(): void {
this.emit('bell');
if (this.soundBell()) this.bellAudioElement.play();
if (this.soundBell()) {
this.soundManager.playBellSound();
}
if (this.visualBell()) {
this.element.classList.add('visual-bell-active');
@@ -1889,7 +1915,9 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
this.rows = y;
this.buffers.setupTabStops(this.cols);
this.charMeasure.measure(this.options);
if (this.charMeasure) {
this.charMeasure.measure(this.options);
}
this.refresh(0, this.rows - 1);
this.emit('resize', {cols: x, rows: y});
@@ -2151,24 +2179,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
// return this.options.bellStyle === 'sound' ||
// this.options.bellStyle === 'both';
}
private syncBellSound(): void {
// Don't update anything if the terminal has not been opened yet
if (!this.element) {
return;
}
if (this.soundBell() && this.bellAudioElement) {
this.bellAudioElement.setAttribute('src', this.options.bellSound);
} else if (this.soundBell()) {
this.bellAudioElement = document.createElement('audio');
this.bellAudioElement.setAttribute('preload', 'auto');
this.bellAudioElement.setAttribute('src', this.options.bellSound);
this.helperContainer.appendChild(this.bellAudioElement);
} else if (this.bellAudioElement) {
this.helperContainer.removeChild(this.bellAudioElement);
}
}
}
/**
+9 -4
View File
@@ -3,16 +3,18 @@
* @license MIT
*/
import { Terminal as PublicTerminal, ITerminalOptions as IPublicTerminalOptions, IEventEmitter } from 'xterm';
import { Terminal as PublicTerminal, ITerminalOptions as IPublicTerminalOptions, IEventEmitter as IPublicEventEmitter, IEventEmitter } from 'xterm';
import { IColorSet, IRenderer } from './renderer/Types';
import { IMouseZoneManager } from './input/Types';
export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean;
export type XtermListener = (...args: any[]) => void;
export type CharData = [number, string, number, number];
export type LineData = CharData[];
export type LinkMatcherHandler = (event: MouseEvent, uri: string) => boolean | void;
export type LinkMatcherHandler = (event: MouseEvent, uri: string) => void;
export type LinkMatcherValidationCallback = (uri: string, callback: (isValid: boolean) => void) => void;
export enum LinkHoverEventTypes {
@@ -250,6 +252,7 @@ export interface IBuffer {
tabs: any;
scrollBottom: number;
scrollTop: number;
hasScrollback: boolean;
savedY: number;
savedX: number;
isCursorInViewport: boolean;
@@ -296,8 +299,6 @@ export interface ISelectionManager {
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;
}
@@ -347,3 +348,7 @@ export interface IBrowser {
isIphone: boolean;
isMSWindows: boolean;
}
export interface ISoundManager {
playBellSound(): void;
}
+5
View File
@@ -0,0 +1,5 @@
{
"name": "xterm.weblinks",
"main": "weblinks.js",
"private": true
}
+11
View File
@@ -0,0 +1,11 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"rootDir": ".",
"outDir": "../../../lib/addons/webLinks/",
"sourceMap": true,
"removeComments": true,
"declaration": true
}
}
+42
View File
@@ -0,0 +1,42 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert, expect } from 'chai';
import * as webLinks from './webLinks';
class MockTerminal {
public regex: RegExp;
public handler: (event: MouseEvent, uri: string) => void;
public options?: any;
public registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: any): number {
this.regex = regex;
this.handler = handler;
this.options = options;
return 0;
}
}
describe('webLinks addon', () => {
describe('apply', () => {
it('should do register the `webLinksInit` method', () => {
webLinks.apply(<any>MockTerminal);
assert.equal(typeof (<any>MockTerminal).prototype.webLinksInit, 'function');
});
});
it('should allow ~ character in URI path', () => {
const term = new MockTerminal();
webLinks.webLinksInit(<any>term);
const row = ' http://foo.com/a~b#c~d?e~f ';
let match = row.match(term.regex);
let uri = match[term.options.matchIndex];
assert.equal(uri, 'http://foo.com/a~b#c~d?e~f');
});
});
+48
View File
@@ -0,0 +1,48 @@
/**
* 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 -1
View File
@@ -1,5 +1,5 @@
{
"name": "xterm.winptyCompat",
"name": "xterm.winptycompat",
"main": "winptyCompat.js",
"private": true
}
+12 -3
View File
@@ -56,9 +56,13 @@ export class AltClickHandler {
* then moves to requested col.
*/
private _arrowSequences(): string {
return this._resetStartingRow() +
this._moveToRequestedRow() +
this._moveToRequestedCol();
// 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();
}
/**
@@ -113,6 +117,11 @@ export class AltClickHandler {
).length, this._sequence(direction));
}
private _moveHorizontallyOnly(): string {
let direction = this._horizontalDirection();
return repeat(Math.abs(this._startCol - this._endCol), this._sequence(direction));
}
/**
* Utility functions
*/
+7 -37
View File
@@ -13,13 +13,12 @@ import { IRenderLayer, IColorSet, IRenderer, IRenderDimensions } from './Types';
import { ITerminal } from '../Types';
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 {
/** A queue of the rows to be refreshed */
private _refreshRowsQueue: {start: number, end: number}[] = [];
private _refreshAnimationFrame = null;
private _renderDebouncer: RenderDebouncer;
private _renderLayers: IRenderLayer[];
private _devicePixelRatio: number;
@@ -61,6 +60,7 @@ export class Renderer extends EventEmitter implements IRenderer {
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));
@@ -121,7 +121,7 @@ export class Renderer extends EventEmitter implements IRenderer {
}
// Resize the screen
this._terminal.screenElement.style.width = `${this.dimensions.canvasWidth + this._terminal.viewport.scrollBarWidth}px`;
this._terminal.screenElement.style.width = `${this.dimensions.canvasWidth}px`;
this._terminal.screenElement.style.height = `${this.dimensions.canvasHeight}px`;
this.emit('resize', {
@@ -172,47 +172,19 @@ export class Renderer extends EventEmitter implements IRenderer {
* @param {number} start The start row.
* @param {number} end The end row.
*/
public queueRefresh(start: number, end: number): void {
public refreshRows(start: number, end: number): void {
if (this._isPaused) {
this._needsFullRefresh = true;
return;
}
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 _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);
private _renderRows(start: number, end: number): void {
this._renderLayers.forEach(l => l.onGridChanged(this._terminal, start, end));
this._terminal.emit('refresh', {start, end});
}
@@ -275,7 +247,5 @@ 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;
}
}
+1 -1
View File
@@ -32,7 +32,7 @@ export interface IRenderer extends IEventEmitter {
onCursorMove(): void;
onOptionsChanged(): void;
clear(): void;
queueRefresh(start: number, end: number): void;
refreshRows(start: number, end: number): void;
}
export interface IColorManager {
+31
View File
@@ -0,0 +1,31 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IDisposable } from 'xterm';
/**
* Adds a disposabe listener to a node in the DOM, returning the disposable.
* @param type The event type.
* @param handler The handler for the listener.
*/
export function addDisposableListener(
node: Element | Window | Document,
type: string,
handler: (e: any) => void,
useCapture?: boolean
): IDisposable {
node.addEventListener(type, handler, useCapture);
return {
dispose: () => {
if (!handler) {
// Already disposed
return;
}
node.removeEventListener(type, handler, useCapture);
node = null;
handler = null;
}
};
}
+51
View File
@@ -0,0 +1,51 @@
import { ITerminal } from '../Types';
import { IDisposable } from 'xterm';
/**
* Debounces calls to render terminal rows using animation frames.
*/
export class RenderDebouncer implements IDisposable {
private _rowStart: number;
private _rowEnd: number;
private _animationFrame: number = null;
constructor(
private _terminal: ITerminal,
private _callback: (start: number, end: number) => void
) {
}
public dispose(): void {
if (this._animationFrame) {
window.cancelAnimationFrame(this._animationFrame);
this._animationFrame = null;
}
}
public refresh(rowStart?: number, rowEnd?: number): void {
rowStart = rowStart || 0;
rowEnd = rowEnd || this._terminal.rows - 1;
this._rowStart = this._rowStart !== undefined ? Math.min(this._rowStart, rowStart) : rowStart;
this._rowEnd = this._rowEnd !== undefined ? Math.max(this._rowEnd, rowEnd) : rowEnd;
if (this._animationFrame) {
return;
}
this._animationFrame = window.requestAnimationFrame(() => this._innerRefresh());
}
private _innerRefresh(): void {
// Clamp values
this._rowStart = Math.max(this._rowStart, 0);
this._rowEnd = Math.min(this._rowEnd, this._terminal.rows - 1);
// Run render callback
this._callback(this._rowStart, this._rowEnd);
// Reset debouncer
this._rowStart = null;
this._rowEnd = null;
this._animationFrame = null;
}
}
-10
View File
@@ -1,10 +0,0 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
// Source: https://freesound.org/people/altemark/sounds/45759/
// This sound is released under the Creative Commons Attribution 3.0 Unported
// (CC BY 3.0) license. It was created by 'altemark'. No modifications have been
// made, apart from the conversion to base64.
export const 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==';
+22 -8
View File
@@ -4,12 +4,13 @@
*/
import { IColorSet, IRenderer, IRenderDimensions, IColorManager } from '../renderer/Types';
import { LineData, IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, ICircularList, ILinkifier, IMouseHelper, ILinkMatcherOptions } from '../Types';
import { LineData, IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, ICircularList, ILinkifier, IMouseHelper, ILinkMatcherOptions, XtermListener } from '../Types';
import { Buffer } from '../Buffer';
import * as Browser from '../shared/utils/Browser';
import { ITheme } from 'xterm';
import { ITheme, IDisposable } from 'xterm';
export class MockTerminal implements ITerminal {
static string: any;
getOption(key: any): any {
throw new Error('Method not implemented.');
}
@@ -105,12 +106,18 @@ export class MockTerminal implements ITerminal {
on(event: string, callback: () => void): void {
throw new Error('Method not implemented.');
}
off(type: string, listener: (...args: any[]) => void): void {
off(type: string, listener: XtermListener): void {
throw new Error('Method not implemented.');
}
addDisposableListener(type: string, handler: XtermListener): IDisposable {
throw new Error('Method not implemented.');
}
scrollLines(disp: number, suppressScrollEvent: boolean): void {
throw new Error('Method not implemented.');
}
scrollToRow(absoluteRow: number): number {
throw new Error('Method not implemented.');
}
cancel(ev: Event, force?: boolean): void {
throw new Error('Method not implemented.');
}
@@ -250,15 +257,18 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal {
setOption(key: string, value: any): void {
this.options[key] = value;
}
on(type: string, listener: (...args: any[]) => void): void {
on(type: string, listener: XtermListener): void {
throw new Error('Method not implemented.');
}
off(type: string, listener: (...args: any[]) => void): void {
off(type: string, listener: XtermListener): void {
throw new Error('Method not implemented.');
}
emit(type: string, data?: any): void {
throw new Error('Method not implemented.');
}
addDisposableListener(type: string, handler: XtermListener): IDisposable {
throw new Error('Method not implemented.');
}
}
export class MockBuffer implements IBuffer {
@@ -266,6 +276,7 @@ export class MockBuffer implements IBuffer {
lines: ICircularList<[number, string, number, number][]>;
ydisp: number;
ybase: number;
hasScrollback: boolean;
y: number;
x: number;
tabs: any;
@@ -286,15 +297,18 @@ export class MockBuffer implements IBuffer {
export class MockRenderer implements IRenderer {
colorManager: IColorManager;
on(type: string, listener: (...args: any[]) => void): void {
on(type: string, listener: XtermListener): void {
throw new Error('Method not implemented.');
}
off(type: string, listener: (...args: any[]) => void): void {
off(type: string, listener: XtermListener): void {
throw new Error('Method not implemented.');
}
emit(type: string, data?: any): void {
throw new Error('Method not implemented.');
}
addDisposableListener(type: string, handler: XtermListener): IDisposable {
throw new Error('Method not implemented.');
}
dimensions: IRenderDimensions;
setTheme(theme: ITheme): IColorSet { return <IColorSet>{}; }
onResize(cols: number, rows: number, didCharSizeChange: boolean): void {}
@@ -306,7 +320,7 @@ export class MockRenderer implements IRenderer {
onOptionsChanged(): void {}
onWindowResize(devicePixelRatio: number): void {}
clear(): void {}
queueRefresh(start: number, end: number): void {}
refreshRows(start: number, end: number): void {}
}
export class MockViewport implements IViewport {
+23
View File
@@ -132,3 +132,26 @@
.xterm:not(.enable-mouse-events) {
cursor: text;
}
.xterm .xterm-accessibility,
.xterm .xterm-message {
position: absolute;
left: 0;
top: 0;
bottom: 0;
right: 0;
z-index: 100;
color: transparent;
}
.xterm .xterm-accessibility-tree:focus [id^="xterm-active-item-"] {
outline: 1px solid #F80;
}
.xterm .live-region {
position: absolute;
left: -9999px;
width: 1px;
height: 1px;
overflow: hidden;
}
+35 -1
View File
@@ -106,6 +106,13 @@ declare module 'xterm' {
*/
rows?: number;
/**
* Whether screen reader support is enabled. When on this will expose
* supporting elements in the DOM to support NVDA on Windows and VoiceOver
* on macOS.
*/
screenReaderMode?: boolean;
/**
* The amount of scrollback in the terminal. Scrollback is the amount of rows
* that are retained when lines are scrolled beyond the initial viewport.
@@ -218,6 +225,20 @@ declare module 'xterm' {
on(type: string, listener: (...args: any[]) => void): void;
off(type: string, listener: (...args: any[]) => void): void;
emit(type: string, data?: any): void;
addDisposableListener(type: string, handler: (...args: any[]) => void): IDisposable;
}
/**
* An object that can be disposed via a dispose function.
*/
export interface IDisposable {
dispose(): void;
}
export interface ILocalizableStrings {
blankLine: string;
promptLabel: string;
tooMuchOutput: string;
}
/**
@@ -244,6 +265,11 @@ declare module 'xterm' {
*/
cols: number;
/**
* Natural language strings that can be localized.
*/
static strings: ILocalizableStrings;
/**
* Creates a new `Terminal` object.
*
@@ -325,6 +351,8 @@ declare module 'xterm' {
emit(type: string, data?: any): void;
addDisposableListener(type: string, handler: (...args: any[]) => void): IDisposable;
/**
* Resizes the terminal.
* @param x The number of columns to resize to.
@@ -367,7 +395,7 @@ declare module 'xterm' {
* @param options Options for the link matcher.
* @return The ID of the new matcher, this can be used to deregister.
*/
registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => boolean | void, options?: ILinkMatcherOptions): number;
registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): number;
/**
* (EXPERIMENTAL) Deregisters a link matcher if it has been registered.
@@ -519,6 +547,12 @@ declare module 'xterm' {
* @param value The option value.
*/
setOption(key: 'theme', value: ITheme): void;
/**
* Sets an option on the terminal.
* @param key The option key.
* @param value The option value.
*/
setOption(key: 'cols' | 'rows', value: number): void;
/**
* Sets an option on the terminal.
* @param key The option key.