Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86800d2d71 | ||
|
|
41fdc9071a | ||
|
|
ff63d3eb23 | ||
|
|
05b8ef1727 | ||
|
|
458f2d8bf1 | ||
|
|
87a84b608c | ||
|
|
dd7a9c3b7a | ||
|
|
d1ca0d7839 | ||
|
|
3419480b98 | ||
|
|
a003a0c585 | ||
|
|
e3dfe4bc32 | ||
|
|
c9ec7aa215 | ||
|
|
7675be7da3 | ||
|
|
1a03b703f1 | ||
|
|
9165581266 | ||
|
|
6a770296f9 | ||
|
|
cbfb99a5cf | ||
|
|
bae3f64c10 | ||
|
|
7c7102d8a6 | ||
|
|
8e9ba23227 | ||
|
|
1d91e3c5bf | ||
|
|
45d4622920 | ||
|
|
b7da4b6bfe | ||
|
|
fdf51c119b | ||
|
|
6de8c91059 | ||
|
|
490f0ad329 | ||
|
|
882aea357f | ||
|
|
d8eedfa1c7 | ||
|
|
90e57c9b3c | ||
|
|
a269e7f5d9 | ||
|
|
4a3c1a4cc6 | ||
|
|
804cb36adc | ||
|
|
5aaf9ef11b | ||
|
|
c65dd1f476 | ||
|
|
05f136b3da | ||
|
|
1c4cb8c620 | ||
|
|
fec66c7dac | ||
|
|
e82f78fc84 | ||
|
|
67d6ad57c3 | ||
|
|
48f1c4667a | ||
|
|
6a9da97163 | ||
|
|
7b68cb9fe7 | ||
|
|
6691f80906 | ||
|
|
bdfe877c8f | ||
|
|
ee3668eb03 | ||
|
|
4a443c4a45 | ||
|
|
d964c23c43 | ||
|
|
20d4c64ffe | ||
|
|
bf3709f4c5 | ||
|
|
8f22d8dda4 | ||
|
|
0f6a8d3991 | ||
|
|
5b8da8c89a | ||
|
|
f2e5ea5783 | ||
|
|
cb888c343e | ||
|
|
18f37440dc | ||
|
|
3582f8733d | ||
|
|
b84dba2e8d | ||
|
|
61fc4ca8a6 | ||
|
|
bbd234b951 | ||
|
|
0545cfa121 | ||
|
|
26969b725e | ||
|
|
eb31732af0 | ||
|
|
9c08f78017 | ||
|
|
54dcbeb6cf | ||
|
|
a52aab4618 | ||
|
|
5f5a49995a | ||
|
|
ac7239732d | ||
|
|
68d537fb28 | ||
|
|
7ce071c575 | ||
|
|
42f1aba00c | ||
|
|
dfb1b4db95 | ||
|
|
6900bbc07a | ||
|
|
d4b507ddb1 | ||
|
|
1bad71f13a | ||
|
|
63b3a6f323 | ||
|
|
ba7976f3b1 | ||
|
|
b5b350fdcf | ||
|
|
87e292414d | ||
|
|
5e3ebbde5e | ||
|
|
6d9d2a3304 | ||
|
|
eabb25c796 |
-19
@@ -1,19 +0,0 @@
|
|||||||
language: node_js
|
|
||||||
os:
|
|
||||||
- linux
|
|
||||||
node_js:
|
|
||||||
- 6
|
|
||||||
before_install:
|
|
||||||
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test ; fi
|
|
||||||
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get -qq update ; fi
|
|
||||||
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get -qq install g++-4.8 ; fi
|
|
||||||
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then export CXX=g++-4.8 ; fi
|
|
||||||
- npm install -g npm@5.1.0
|
|
||||||
env:
|
|
||||||
matrix:
|
|
||||||
- NPM_COMMAND=tsc
|
|
||||||
- NPM_COMMAND=lint
|
|
||||||
- NPM_COMMAND=test-coverage
|
|
||||||
notifications:
|
|
||||||
email: false
|
|
||||||
script: npm run $NPM_COMMAND
|
|
||||||
@@ -61,6 +61,7 @@ irokas <akasidiari@inf.uth.gr>
|
|||||||
Jakob Gillich <jakob@gillich.me>
|
Jakob Gillich <jakob@gillich.me>
|
||||||
Jan Kuri <jkuri88@gmail.com>
|
Jan Kuri <jkuri88@gmail.com>
|
||||||
Jean Bruenn <himself@jeanbruenn.info>
|
Jean Bruenn <himself@jeanbruenn.info>
|
||||||
|
Jeff Principe <princjef@gmail.com>
|
||||||
Jeremy Danyow <jedanyow@microsoft.com>
|
Jeremy Danyow <jedanyow@microsoft.com>
|
||||||
Jianhui Zhao <jianhuizhao329@gmail.com>
|
Jianhui Zhao <jianhuizhao329@gmail.com>
|
||||||
Joao Moreno <jomo@microsoft.com>
|
Joao Moreno <jomo@microsoft.com>
|
||||||
@@ -70,6 +71,7 @@ Jon Austin <jon.i.austin@gmail.com>
|
|||||||
Jon Kohler <jon@nutanix.com>
|
Jon Kohler <jon@nutanix.com>
|
||||||
Jon Masters <jon.masters@sky.com>
|
Jon Masters <jon.masters@sky.com>
|
||||||
Jörg Breitbart <jerch@rockborn.de>
|
Jörg Breitbart <jerch@rockborn.de>
|
||||||
|
Jose Anton <roke22@gmail.com>
|
||||||
jpoth <poth.john@gmail.com>
|
jpoth <poth.john@gmail.com>
|
||||||
Justin Luk <jluk@users.noreply.github.com>
|
Justin Luk <jluk@users.noreply.github.com>
|
||||||
Justin Mecham <justin@mecham.me>
|
Justin Mecham <justin@mecham.me>
|
||||||
@@ -103,6 +105,7 @@ Peng Xiao <pengxiao@outlook.com>
|
|||||||
Peter Baumgarten <me@peterbaumgarten.com>
|
Peter Baumgarten <me@peterbaumgarten.com>
|
||||||
Philip Olson <philip.olson@protonmail.ch>
|
Philip Olson <philip.olson@protonmail.ch>
|
||||||
PowerHat <31401273+7PH@users.noreply.github.com>
|
PowerHat <31401273+7PH@users.noreply.github.com>
|
||||||
|
PowerHat <b.raymond@protonmail.com>
|
||||||
pro-src <34285059+pro-src@users.noreply.github.com>
|
pro-src <34285059+pro-src@users.noreply.github.com>
|
||||||
pro-src <rodneyd.teal@gmail.com>
|
pro-src <rodneyd.teal@gmail.com>
|
||||||
Rick Baker <rick@ricktbaker.com>
|
Rick Baker <rick@ricktbaker.com>
|
||||||
@@ -111,6 +114,7 @@ Saad Malik <simfox3@gmail.com>
|
|||||||
Samuel Williams <samuel.williams@oriontransfer.co.nz>
|
Samuel Williams <samuel.williams@oriontransfer.co.nz>
|
||||||
Saswat Das <saswatds@users.noreply.github.com>
|
Saswat Das <saswatds@users.noreply.github.com>
|
||||||
Saul Costa <saul@codevolve.com>
|
Saul Costa <saul@codevolve.com>
|
||||||
|
Segev Finer <segev208@gmail.com>
|
||||||
Shuanglei Tao <tsl0922@gmail.com>
|
Shuanglei Tao <tsl0922@gmail.com>
|
||||||
sitzmar <sitzmar@gmail.com>
|
sitzmar <sitzmar@gmail.com>
|
||||||
Steven Silvester <steven.silvester@ieee.org>
|
Steven Silvester <steven.silvester@ieee.org>
|
||||||
|
|||||||
@@ -50,7 +50,3 @@ By contributing code to xterm.js you
|
|||||||
holder has explicitly granted the right to use it like this,
|
holder has explicitly granted the right to use it like this,
|
||||||
through a compatible open source license or through a direct
|
through a compatible open source license or through a direct
|
||||||
agreement with you.)
|
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.
|
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
# [](https://xtermjs.org)
|
# [](https://xtermjs.org)
|
||||||
|
|
||||||
[](https://travis-ci.org/xtermjs/xterm.js) [](https://xtermjs.visualstudio.com/xterm.js/_build/index?definitionId=1) [](https://coveralls.io/github/xtermjs/xterm.js?branch=master) [](https://gitter.im/sourcelair/xterm.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [](https://www.jsdelivr.com/package/npm/xterm)
|
[](https://xtermjs.visualstudio.com/xterm.js/_build/index?definitionId=1)
|
||||||
|
[](https://coveralls.io/github/xtermjs/xterm.js?branch=master)
|
||||||
|
[](https://gitter.im/sourcelair/xterm.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
||||||
|
[](https://www.jsdelivr.com/package/npm/xterm)
|
||||||
|
|
||||||
Xterm.js is a terminal front-end component written in JavaScript that works in the browser.
|
Xterm.js is a terminal front-end component written in JavaScript that works in the browser.
|
||||||
|
|
||||||
@@ -57,6 +60,12 @@ The proposed way to load xterm.js is via the ES6 module syntax.
|
|||||||
import { Terminal } from 'xterm';
|
import { Terminal } from 'xterm';
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### API
|
||||||
|
|
||||||
|
The full API for xterm.js is contained within the [TypeScript declaration file](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts), use the branch/tag picker in GitHub (`w`) to navigate to the correct version of the API.
|
||||||
|
|
||||||
|
Note that some APIs are marked *experimental*, these are added so we can experiment with new ideas without committing to support it like a normal semver API. Note that these APIs can change radically between versions so be sure to read release notes if you plan on using experimental APIs.
|
||||||
|
|
||||||
### Addons
|
### Addons
|
||||||
|
|
||||||
Addons are JavaScript modules that extend the `Terminal` prototype with new methods and attributes to provide additional functionality. There are a handful available in the main repository in the `src/addons` directory and you can even write your own, by using xterm.js' public API.
|
Addons are JavaScript modules that extend the `Terminal` prototype with new methods and attributes to provide additional functionality. There are a handful available in the main repository in the `src/addons` directory and you can even write your own, by using xterm.js' public API.
|
||||||
@@ -74,6 +83,8 @@ var xterm = new Terminal(); // Instantiate the terminal
|
|||||||
xterm.fit(); // Use the `fit` method, provided by the `fit` addon
|
xterm.fit(); // Use the `fit` method, provided by the `fit` addon
|
||||||
```
|
```
|
||||||
|
|
||||||
|
You will also need to include the addon's CSS file if it has one in the folder.
|
||||||
|
|
||||||
#### Importing Addons in TypeScript
|
#### Importing Addons in TypeScript
|
||||||
|
|
||||||
There are currently no typings for addons if they are accessed via extending Terminal prototype, so you will need to upcast if using TypeScript, eg. `(<any>xterm).fit()`.
|
There are currently no typings for addons if they are accessed via extending Terminal prototype, so you will need to upcast if using TypeScript, eg. `(<any>xterm).fit()`.
|
||||||
@@ -107,6 +118,10 @@ Since xterm.js is typically implemented as a developer tool, only modern browser
|
|||||||
|
|
||||||
Xterm.js works seamlessly in Electron apps and may even work on earlier versions of the browsers but these are the browsers we strive to keep working.
|
Xterm.js works seamlessly in Electron apps and may even work on earlier versions of the browsers but these are the browsers we strive to keep working.
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
The current full API documentation is available in the [TypeScript declaration file on the repository](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts), switch the tag (press `w` when viewing the file) to point at the specific version tag you're using.
|
||||||
|
|
||||||
## Real-world uses
|
## Real-world uses
|
||||||
Xterm.js is used in several world-class applications to provide great terminal experiences.
|
Xterm.js is used in several world-class applications to provide great terminal experiences.
|
||||||
|
|
||||||
@@ -153,105 +168,21 @@ computational environment for Jupyter, supporting interactive data science and s
|
|||||||
- [**genact**](https://github.com/svenstaro/genact): A nonsense activity generator.
|
- [**genact**](https://github.com/svenstaro/genact): A nonsense activity generator.
|
||||||
- [**cPanel & WHM**](https://cpanel.com): The hosting platform of choice.
|
- [**cPanel & WHM**](https://cpanel.com): The hosting platform of choice.
|
||||||
- [**Nutanix**](https://github.com/nutanix): Nutanix Enterprise Cloud uses xterm in the webssh functionality within Nutanix Calm, and is also looking to move our old noserial (termjs) functionality to xterm.js
|
- [**Nutanix**](https://github.com/nutanix): Nutanix Enterprise Cloud uses xterm in the webssh functionality within Nutanix Calm, and is also looking to move our old noserial (termjs) functionality to xterm.js
|
||||||
|
- [**SSH Web Client**](https://github.com/roke22/PHP-SSH2-Web-Client): SSH Web Client with PHP.
|
||||||
|
|
||||||
|
[And much more...](https://github.com/xtermjs/xterm.js/network/dependents)
|
||||||
|
|
||||||
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. Note: Please add any new contributions to the end of the list only.
|
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. Note: Please add any new contributions to the end of the list only.
|
||||||
|
|
||||||
## 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:
|
|
||||||
|
|
||||||
```
|
|
||||||
npm install
|
|
||||||
npm start # Run this in its own terminal
|
|
||||||
npm run watch # Run this in its own terminal
|
|
||||||
```
|
|
||||||
|
|
||||||
Then open http://0.0.0.0:3000 in a web browser to access the demo.
|
|
||||||
|
|
||||||
### Windows
|
|
||||||
|
|
||||||
First, ensure [node-gyp](https://github.com/nodejs/node-gyp) is installed and configured correctly, then run the following commands in your terminal:
|
|
||||||
|
|
||||||
```
|
|
||||||
npm install
|
|
||||||
npm start # Run this in its own terminal
|
|
||||||
npm run watch # Run this in its own terminal
|
|
||||||
```
|
|
||||||
|
|
||||||
Then open http://127.0.0.1:3000 in a web browser to access the demo.
|
|
||||||
|
|
||||||
*Note: Do not use ConEmu, as it seems to break the demo for some reason.*
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
Tests are run using the following npm scripts:
|
|
||||||
|
|
||||||
- `npm test`: This will run both unit tests and the linter
|
|
||||||
- `npm run test-suite <file>`: This will run all tests within a particular file, <file> is the test file name excluding the extension (eg. "Linkifier.test")
|
|
||||||
- `npm run test-debug`: This will run unit tests with `--inspect-brk`, this can then be automatically debugged using [VS Code auto attach](https://code.visualstudio.com/docs/nodejs/nodejs-debugging#_auto-attach-feature) or manually attached to by a debugger
|
|
||||||
- `npm run test-coverage`: This will run all unit tests and produce a coverage report
|
|
||||||
- `npm run lint`: This will run the linter only
|
|
||||||
|
|
||||||
## Releases
|
## Releases
|
||||||
|
|
||||||
Xterm.js follows a monthly release cycle roughly.
|
Xterm.js follows a monthly release cycle roughly.
|
||||||
|
|
||||||
The existing releases are available at this GitHub repo's [Releases](https://github.com/sourcelair/xterm.js/releases), while the roadmap is available as [Milestones](https://github.com/sourcelair/xterm.js/milestones).
|
The existing releases are available at this GitHub repo's [Releases](https://github.com/sourcelair/xterm.js/releases), while the roadmap is available as [Milestones](https://github.com/sourcelair/xterm.js/milestones).
|
||||||
|
|
||||||
## Development and Contribution
|
## Contributing
|
||||||
|
|
||||||
Xterm.js is maintained by [SourceLair](https://www.sourcelair.com/) and a few external contributors, but we would love to receive contributions from everyone!
|
You can read the [guide on the wiki](https://github.com/xtermjs/xterm.js/wiki/Contributing) to learn how to contribute and setup xterm.js for development.
|
||||||
|
|
||||||
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 ran when launching a web worker.
|
|
||||||
|
|
||||||
## License Agreement
|
## License Agreement
|
||||||
|
|
||||||
|
|||||||
+3
-4
@@ -174,9 +174,7 @@ function initOptions(term) {
|
|||||||
'termName',
|
'termName',
|
||||||
'useFlowControl',
|
'useFlowControl',
|
||||||
// Complex option
|
// Complex option
|
||||||
'theme',
|
'theme'
|
||||||
// Only in constructor
|
|
||||||
'rendererType'
|
|
||||||
];
|
];
|
||||||
var stringOptions = {
|
var stringOptions = {
|
||||||
bellSound: null,
|
bellSound: null,
|
||||||
@@ -185,7 +183,8 @@ function initOptions(term) {
|
|||||||
experimentalCharAtlas: ['none', 'static', 'dynamic'],
|
experimentalCharAtlas: ['none', 'static', 'dynamic'],
|
||||||
fontFamily: null,
|
fontFamily: null,
|
||||||
fontWeight: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'],
|
fontWeight: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'],
|
||||||
fontWeightBold: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900']
|
fontWeightBold: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'],
|
||||||
|
rendererType: ['dom', 'canvas']
|
||||||
};
|
};
|
||||||
var options = Object.keys(term._core.options);
|
var options = Object.keys(term._core.options);
|
||||||
var booleanOptions = [];
|
var booleanOptions = [];
|
||||||
|
|||||||
+2
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "xterm",
|
"name": "xterm",
|
||||||
"description": "Full xterm terminal, in your browser",
|
"description": "Full xterm terminal, in your browser",
|
||||||
"version": "3.5.0",
|
"version": "3.6.0",
|
||||||
"main": "lib/public/Terminal.js",
|
"main": "lib/public/Terminal.js",
|
||||||
"types": "typings/xterm.d.ts",
|
"types": "typings/xterm.d.ts",
|
||||||
"repository": "https://github.com/xtermjs/xterm.js",
|
"repository": "https://github.com/xtermjs/xterm.js",
|
||||||
@@ -35,6 +35,7 @@
|
|||||||
"npm-run-all": "^4.1.2",
|
"npm-run-all": "^4.1.2",
|
||||||
"nyc": "^11.8.0",
|
"nyc": "^11.8.0",
|
||||||
"sorcery": "^0.10.0",
|
"sorcery": "^0.10.0",
|
||||||
|
"source-map-loader": "^0.2.3",
|
||||||
"tslint": "^5.9.1",
|
"tslint": "^5.9.1",
|
||||||
"tslint-consistent-codestyle": "^1.13.0",
|
"tslint-consistent-codestyle": "^1.13.0",
|
||||||
"typescript": "2.8.3",
|
"typescript": "2.8.3",
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export class AccessibilityManager extends Disposable {
|
|||||||
this._terminal.element.insertAdjacentElement('afterbegin', this._accessibilityTreeRoot);
|
this._terminal.element.insertAdjacentElement('afterbegin', this._accessibilityTreeRoot);
|
||||||
|
|
||||||
this.register(this._renderRowsDebouncer);
|
this.register(this._renderRowsDebouncer);
|
||||||
this.register(this._terminal.addDisposableListener('resize', data => this._onResize(data.cols, data.rows)));
|
this.register(this._terminal.addDisposableListener('resize', data => this._onResize(data.rows)));
|
||||||
this.register(this._terminal.addDisposableListener('refresh', data => this._refreshRows(data.start, data.end)));
|
this.register(this._terminal.addDisposableListener('refresh', data => this._refreshRows(data.start, data.end)));
|
||||||
this.register(this._terminal.addDisposableListener('scroll', data => this._refreshRows()));
|
this.register(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
|
// Line feed is an issue as the prompt won't be read out after a command is run
|
||||||
@@ -157,7 +157,7 @@ export class AccessibilityManager extends Disposable {
|
|||||||
e.stopImmediatePropagation();
|
e.stopImmediatePropagation();
|
||||||
}
|
}
|
||||||
|
|
||||||
private _onResize(cols: number, rows: number): void {
|
private _onResize(rows: number): void {
|
||||||
// Remove bottom boundary listener
|
// Remove bottom boundary listener
|
||||||
this._rowElements[this._rowElements.length - 1].removeEventListener('focus', this._bottomBoundaryFocusListener);
|
this._rowElements[this._rowElements.length - 1].removeEventListener('focus', this._bottomBoundaryFocusListener);
|
||||||
|
|
||||||
@@ -259,6 +259,9 @@ export class AccessibilityManager extends Disposable {
|
|||||||
if (!this._terminal.renderer.dimensions.actualCellHeight) {
|
if (!this._terminal.renderer.dimensions.actualCellHeight) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (this._rowElements.length !== this._terminal.rows) {
|
||||||
|
this._onResize(this._terminal.rows);
|
||||||
|
}
|
||||||
for (let i = 0; i < this._terminal.rows; i++) {
|
for (let i = 0; i < this._terminal.rows; i++) {
|
||||||
this._refreshRowDimensions(this._rowElements[i]);
|
this._refreshRowDimensions(this._rowElements[i]);
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-1
@@ -15,6 +15,10 @@ export const CHAR_DATA_WIDTH_INDEX = 2;
|
|||||||
export const CHAR_DATA_CODE_INDEX = 3;
|
export const CHAR_DATA_CODE_INDEX = 3;
|
||||||
export const MAX_BUFFER_SIZE = 4294967295; // 2^32 - 1
|
export const MAX_BUFFER_SIZE = 4294967295; // 2^32 - 1
|
||||||
|
|
||||||
|
export const NULL_CELL_CHAR = ' ';
|
||||||
|
export const NULL_CELL_WIDTH = 1;
|
||||||
|
export const NULL_CELL_CODE = 32;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class represents a terminal buffer (an internal state of the terminal), where the
|
* This class represents a terminal buffer (an internal state of the terminal), where the
|
||||||
* following information is stored (in high-level):
|
* following information is stored (in high-level):
|
||||||
@@ -117,7 +121,7 @@ export class Buffer implements IBuffer {
|
|||||||
if (this.lines.length > 0) {
|
if (this.lines.length > 0) {
|
||||||
// Deal with columns increasing (we don't do anything when columns reduce)
|
// Deal with columns increasing (we don't do anything when columns reduce)
|
||||||
if (this._terminal.cols < newCols) {
|
if (this._terminal.cols < newCols) {
|
||||||
const ch: CharData = [DEFAULT_ATTR, ' ', 1, 32]; // does xterm use the default attr?
|
const ch: CharData = [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; // does xterm use the default attr?
|
||||||
for (let i = 0; i < this.lines.length; i++) {
|
for (let i = 0; i < this.lines.length; i++) {
|
||||||
while (this.lines.get(i).length < newCols) {
|
while (this.lines.get(i).length < newCols) {
|
||||||
this.lines.get(i).push(ch);
|
this.lines.get(i).push(ch);
|
||||||
|
|||||||
+1
-4
@@ -18,7 +18,7 @@ export class BufferSet extends EventEmitter implements IBufferSet {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new BufferSet for the given terminal.
|
* Create a new BufferSet for the given terminal.
|
||||||
* @param {Terminal} terminal - The terminal the BufferSet will belong to
|
* @param _terminal - The terminal the BufferSet will belong to
|
||||||
*/
|
*/
|
||||||
constructor(private _terminal: ITerminal) {
|
constructor(private _terminal: ITerminal) {
|
||||||
super();
|
super();
|
||||||
@@ -35,7 +35,6 @@ export class BufferSet extends EventEmitter implements IBufferSet {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the alt Buffer of the BufferSet
|
* Returns the alt Buffer of the BufferSet
|
||||||
* @returns {Buffer}
|
|
||||||
*/
|
*/
|
||||||
public get alt(): Buffer {
|
public get alt(): Buffer {
|
||||||
return this._alt;
|
return this._alt;
|
||||||
@@ -43,7 +42,6 @@ export class BufferSet extends EventEmitter implements IBufferSet {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the normal Buffer of the BufferSet
|
* Returns the normal Buffer of the BufferSet
|
||||||
* @returns {Buffer}
|
|
||||||
*/
|
*/
|
||||||
public get active(): Buffer {
|
public get active(): Buffer {
|
||||||
return this._activeBuffer;
|
return this._activeBuffer;
|
||||||
@@ -51,7 +49,6 @@ export class BufferSet extends EventEmitter implements IBufferSet {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the currently active Buffer of the BufferSet
|
* Returns the currently active Buffer of the BufferSet
|
||||||
* @returns {Buffer}
|
|
||||||
*/
|
*/
|
||||||
public get normal(): Buffer {
|
public get normal(): Buffer {
|
||||||
return this._normal;
|
return this._normal;
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export class CompositionHelper {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the compositionupdate event, updating the composition view.
|
* Handles the compositionupdate event, updating the composition view.
|
||||||
* @param {CompositionEvent} ev The event.
|
* @param ev The event.
|
||||||
*/
|
*/
|
||||||
public compositionupdate(ev: CompositionEvent): void {
|
public compositionupdate(ev: CompositionEvent): void {
|
||||||
this._compositionView.textContent = ev.data;
|
this._compositionView.textContent = ev.data;
|
||||||
|
|||||||
@@ -375,8 +375,8 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
|||||||
// shortcut for most chars (print action)
|
// shortcut for most chars (print action)
|
||||||
if (currentState === ParserState.GROUND && code > 0x1f && code < 0x80) {
|
if (currentState === ParserState.GROUND && code > 0x1f && code < 0x80) {
|
||||||
print = (~print) ? print : i;
|
print = (~print) ? print : i;
|
||||||
do code = data.charCodeAt(++i);
|
do i++;
|
||||||
while (i < l && code > 0x1f && code < 0x80);
|
while (i < l && data.charCodeAt(i) > 0x1f && data.charCodeAt(i) < 0x80);
|
||||||
i--;
|
i--;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
+46
-43
@@ -4,10 +4,10 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { CharData, IInputHandler, IDcsHandler, IEscapeSequenceParser, IBuffer } from './Types';
|
import { CharData, IInputHandler, IDcsHandler, IEscapeSequenceParser, IBuffer, IInputHandlingTerminal } from './Types';
|
||||||
import { C0, C1 } from './common/data/EscapeSequences';
|
import { C0, C1 } from './common/data/EscapeSequences';
|
||||||
import { CHARSETS, DEFAULT_CHARSET } from './core/data/Charsets';
|
import { CHARSETS, DEFAULT_CHARSET } from './core/data/Charsets';
|
||||||
import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, DEFAULT_ATTR } from './Buffer';
|
import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE } from './Buffer';
|
||||||
import { FLAGS } from './renderer/Types';
|
import { FLAGS } from './renderer/Types';
|
||||||
import { wcwidth } from './CharWidth';
|
import { wcwidth } from './CharWidth';
|
||||||
import { EscapeSequenceParser } from './EscapeSequenceParser';
|
import { EscapeSequenceParser } from './EscapeSequenceParser';
|
||||||
@@ -40,7 +40,7 @@ class RequestTerminfo implements IDcsHandler {
|
|||||||
}
|
}
|
||||||
unhook(): void {
|
unhook(): void {
|
||||||
// invalid: DCS 0 + r Pt ST
|
// invalid: DCS 0 + r Pt ST
|
||||||
this._terminal.send(`${C0.ESC}P0+r${this._data}${C0.ESC}\\`);
|
this._terminal.handler(`${C0.ESC}P0+r${this._data}${C0.ESC}\\`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,25 +68,25 @@ class DECRQSS implements IDcsHandler {
|
|||||||
switch (this._data) {
|
switch (this._data) {
|
||||||
// valid: DCS 1 $ r Pt ST (xterm)
|
// valid: DCS 1 $ r Pt ST (xterm)
|
||||||
case '"q': // DECSCA
|
case '"q': // DECSCA
|
||||||
return this._terminal.send(`${C0.ESC}P1$r0"q${C0.ESC}\\`);
|
return this._terminal.handler(`${C0.ESC}P1$r0"q${C0.ESC}\\`);
|
||||||
case '"p': // DECSCL
|
case '"p': // DECSCL
|
||||||
return this._terminal.send(`${C0.ESC}P1$r61"p${C0.ESC}\\`);
|
return this._terminal.handler(`${C0.ESC}P1$r61"p${C0.ESC}\\`);
|
||||||
case 'r': // DECSTBM
|
case 'r': // DECSTBM
|
||||||
const pt = '' + (this._terminal.buffer.scrollTop + 1) +
|
const pt = '' + (this._terminal.buffer.scrollTop + 1) +
|
||||||
';' + (this._terminal.buffer.scrollBottom + 1) + 'r';
|
';' + (this._terminal.buffer.scrollBottom + 1) + 'r';
|
||||||
return this._terminal.send(`${C0.ESC}P1$r${pt}${C0.ESC}\\`);
|
return this._terminal.handler(`${C0.ESC}P1$r${pt}${C0.ESC}\\`);
|
||||||
case 'm': // SGR
|
case 'm': // SGR
|
||||||
// TODO: report real settings instead of 0m
|
// TODO: report real settings instead of 0m
|
||||||
return this._terminal.send(`${C0.ESC}P1$r0m${C0.ESC}\\`);
|
return this._terminal.handler(`${C0.ESC}P1$r0m${C0.ESC}\\`);
|
||||||
case ' q': // DECSCUSR
|
case ' q': // DECSCUSR
|
||||||
const STYLES: {[key: string]: number} = {'block': 2, 'underline': 4, 'bar': 6};
|
const STYLES: {[key: string]: number} = {'block': 2, 'underline': 4, 'bar': 6};
|
||||||
let style = STYLES[this._terminal.getOption('cursorStyle')];
|
let style = STYLES[this._terminal.getOption('cursorStyle')];
|
||||||
style -= this._terminal.getOption('cursorBlink');
|
style -= this._terminal.getOption('cursorBlink');
|
||||||
return this._terminal.send(`${C0.ESC}P1$r${style} q${C0.ESC}\\`);
|
return this._terminal.handler(`${C0.ESC}P1$r${style} q${C0.ESC}\\`);
|
||||||
default:
|
default:
|
||||||
// invalid: DCS 0 $ r Pt ST (xterm)
|
// invalid: DCS 0 $ r Pt ST (xterm)
|
||||||
this._terminal.error('Unknown DCS $q %s', this._data);
|
this._terminal.error('Unknown DCS $q %s', this._data);
|
||||||
this._terminal.send(`${C0.ESC}P0$r${this._data}${C0.ESC}\\`);
|
this._terminal.handler(`${C0.ESC}P0$r${this._data}${C0.ESC}\\`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,7 +116,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
|||||||
private _surrogateHigh: string;
|
private _surrogateHigh: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private _terminal: any, // TODO: reestablish IInputHandlingTerminal here
|
private _terminal: IInputHandlingTerminal,
|
||||||
private _parser: IEscapeSequenceParser = new EscapeSequenceParser())
|
private _parser: IEscapeSequenceParser = new EscapeSequenceParser())
|
||||||
{
|
{
|
||||||
super();
|
super();
|
||||||
@@ -129,16 +129,16 @@ export class InputHandler extends Disposable implements IInputHandler {
|
|||||||
* custom fallback handlers
|
* custom fallback handlers
|
||||||
*/
|
*/
|
||||||
this._parser.setCsiHandlerFallback((collect: string, params: number[], flag: number) => {
|
this._parser.setCsiHandlerFallback((collect: string, params: number[], flag: number) => {
|
||||||
this._terminal.error('Unknown CSI code: ', collect, params, String.fromCharCode(flag));
|
this._terminal.error('Unknown CSI code: ', { collect, params, flag: String.fromCharCode(flag) });
|
||||||
});
|
});
|
||||||
this._parser.setEscHandlerFallback((collect: string, flag: number) => {
|
this._parser.setEscHandlerFallback((collect: string, flag: number) => {
|
||||||
this._terminal.error('Unknown ESC code: ', collect, String.fromCharCode(flag));
|
this._terminal.error('Unknown ESC code: ', { collect, flag: String.fromCharCode(flag) });
|
||||||
});
|
});
|
||||||
this._parser.setExecuteHandlerFallback((code: number) => {
|
this._parser.setExecuteHandlerFallback((code: number) => {
|
||||||
this._terminal.error('Unknown EXECUTE code: ', code);
|
this._terminal.error('Unknown EXECUTE code: ', { code });
|
||||||
});
|
});
|
||||||
this._parser.setOscHandlerFallback((identifier: number, data: string) => {
|
this._parser.setOscHandlerFallback((identifier: number, data: string) => {
|
||||||
this._terminal.error('Unknown OSC code: ', identifier, data);
|
this._terminal.error('Unknown OSC code: ', { identifier, data });
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -296,10 +296,17 @@ export class InputHandler extends Disposable implements IInputHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public parse(data: string): void {
|
public parse(data: string): void {
|
||||||
|
// Ensure the terminal is not disposed
|
||||||
|
if (!this._terminal) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let buffer = this._terminal.buffer;
|
let buffer = this._terminal.buffer;
|
||||||
const cursorStartX = buffer.x;
|
const cursorStartX = buffer.x;
|
||||||
const cursorStartY = buffer.y;
|
const cursorStartY = buffer.y;
|
||||||
if (this._terminal.debug) {
|
|
||||||
|
// TODO: Consolidate debug/logging #1560
|
||||||
|
if ((<any>this._terminal).debug) {
|
||||||
this._terminal.log('data: ' + data);
|
this._terminal.log('data: ' + data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -359,9 +366,9 @@ export class InputHandler extends Disposable implements IInputHandler {
|
|||||||
chWidth = wcwidth(code);
|
chWidth = wcwidth(code);
|
||||||
|
|
||||||
// get charset replacement character
|
// get charset replacement character
|
||||||
// FIXME: Should code be replaced as well?
|
|
||||||
if (charset) {
|
if (charset) {
|
||||||
char = charset[char] || char;
|
char = charset[char] || char;
|
||||||
|
code = char.charCodeAt(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (screenReaderMode) {
|
if (screenReaderMode) {
|
||||||
@@ -431,11 +438,11 @@ export class InputHandler extends Disposable implements IInputHandler {
|
|||||||
if (removed[CHAR_DATA_WIDTH_INDEX] === 0
|
if (removed[CHAR_DATA_WIDTH_INDEX] === 0
|
||||||
&& bufferRow[this._terminal.cols - 2]
|
&& bufferRow[this._terminal.cols - 2]
|
||||||
&& bufferRow[this._terminal.cols - 2][CHAR_DATA_WIDTH_INDEX] === 2) {
|
&& bufferRow[this._terminal.cols - 2][CHAR_DATA_WIDTH_INDEX] === 2) {
|
||||||
bufferRow[this._terminal.cols - 2] = [curAttr, ' ', 1, 32 /* ' '.charCodeAt(0) */ ];
|
bufferRow[this._terminal.cols - 2] = [curAttr, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE];
|
||||||
}
|
}
|
||||||
|
|
||||||
// insert empty cell at cursor
|
// insert empty cell at cursor
|
||||||
bufferRow.splice(buffer.x, 0, [curAttr, ' ', 1, 32 /* ' '.charCodeAt(0) */ ]);
|
bufferRow.splice(buffer.x, 0, [curAttr, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -547,7 +554,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
|||||||
|
|
||||||
const row = buffer.y + buffer.ybase;
|
const row = buffer.y + buffer.ybase;
|
||||||
let j = buffer.x;
|
let j = buffer.x;
|
||||||
const ch: CharData = [this._terminal.eraseAttr(), ' ', 1, 32]; // xterm
|
const ch: CharData = [this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; // xterm
|
||||||
|
|
||||||
while (param-- && j < this._terminal.cols) {
|
while (param-- && j < this._terminal.cols) {
|
||||||
buffer.lines.get(row).splice(j++, 0, ch);
|
buffer.lines.get(row).splice(j++, 0, ch);
|
||||||
@@ -857,7 +864,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
|||||||
const buffer = this._terminal.buffer;
|
const buffer = this._terminal.buffer;
|
||||||
|
|
||||||
const row = buffer.y + buffer.ybase;
|
const row = buffer.y + buffer.ybase;
|
||||||
const ch: CharData = [this._terminal.eraseAttr(), ' ', 1, 32]; // xterm
|
const ch: CharData = [this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; // xterm
|
||||||
|
|
||||||
while (param--) {
|
while (param--) {
|
||||||
buffer.lines.get(row).splice(buffer.x, 1);
|
buffer.lines.get(row).splice(buffer.x, 1);
|
||||||
@@ -919,7 +926,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
|||||||
|
|
||||||
const row = buffer.y + buffer.ybase;
|
const row = buffer.y + buffer.ybase;
|
||||||
let j = buffer.x;
|
let j = buffer.x;
|
||||||
const ch: CharData = [this._terminal.eraseAttr(), ' ', 1, 32]; // xterm
|
const ch: CharData = [this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; // xterm
|
||||||
|
|
||||||
while (param-- && j < this._terminal.cols) {
|
while (param-- && j < this._terminal.cols) {
|
||||||
buffer.lines.get(row)[j++] = ch;
|
buffer.lines.get(row)[j++] = ch;
|
||||||
@@ -981,7 +988,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
|||||||
const buffer = this._terminal.buffer;
|
const buffer = this._terminal.buffer;
|
||||||
|
|
||||||
const line = buffer.lines.get(buffer.ybase + buffer.y);
|
const line = buffer.lines.get(buffer.ybase + buffer.y);
|
||||||
const ch = line[buffer.x - 1] || [DEFAULT_ATTR, ' ', 1, 32];
|
const ch = line[buffer.x - 1] || [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE];
|
||||||
|
|
||||||
while (param--) {
|
while (param--) {
|
||||||
line[buffer.x++] = ch;
|
line[buffer.x++] = ch;
|
||||||
@@ -1032,24 +1039,24 @@ export class InputHandler extends Disposable implements IInputHandler {
|
|||||||
|
|
||||||
if (!collect) {
|
if (!collect) {
|
||||||
if (this._terminal.is('xterm') || this._terminal.is('rxvt-unicode') || this._terminal.is('screen')) {
|
if (this._terminal.is('xterm') || this._terminal.is('rxvt-unicode') || this._terminal.is('screen')) {
|
||||||
this._terminal.send(C0.ESC + '[?1;2c');
|
this._terminal.handler(C0.ESC + '[?1;2c');
|
||||||
} else if (this._terminal.is('linux')) {
|
} else if (this._terminal.is('linux')) {
|
||||||
this._terminal.send(C0.ESC + '[?6c');
|
this._terminal.handler(C0.ESC + '[?6c');
|
||||||
}
|
}
|
||||||
} else if (collect === '>') {
|
} else if (collect === '>') {
|
||||||
// xterm and urxvt
|
// xterm and urxvt
|
||||||
// seem to spit this
|
// seem to spit this
|
||||||
// out around ~370 times (?).
|
// out around ~370 times (?).
|
||||||
if (this._terminal.is('xterm')) {
|
if (this._terminal.is('xterm')) {
|
||||||
this._terminal.send(C0.ESC + '[>0;276;0c');
|
this._terminal.handler(C0.ESC + '[>0;276;0c');
|
||||||
} else if (this._terminal.is('rxvt-unicode')) {
|
} else if (this._terminal.is('rxvt-unicode')) {
|
||||||
this._terminal.send(C0.ESC + '[>85;95;0c');
|
this._terminal.handler(C0.ESC + '[>85;95;0c');
|
||||||
} else if (this._terminal.is('linux')) {
|
} else if (this._terminal.is('linux')) {
|
||||||
// not supported by linux console.
|
// not supported by linux console.
|
||||||
// linux console echoes parameters.
|
// linux console echoes parameters.
|
||||||
this._terminal.send(params[0] + 'c');
|
this._terminal.handler(params[0] + 'c');
|
||||||
} else if (this._terminal.is('screen')) {
|
} else if (this._terminal.is('screen')) {
|
||||||
this._terminal.send(C0.ESC + '[>83;40003;0c');
|
this._terminal.handler(C0.ESC + '[>83;40003;0c');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1712,15 +1719,13 @@ export class InputHandler extends Disposable implements IInputHandler {
|
|||||||
switch (params[0]) {
|
switch (params[0]) {
|
||||||
case 5:
|
case 5:
|
||||||
// status report
|
// status report
|
||||||
this._terminal.send(C0.ESC + '[0n');
|
this._terminal.emit('data', `${C0.ESC}[0n`);
|
||||||
break;
|
break;
|
||||||
case 6:
|
case 6:
|
||||||
// cursor position
|
// cursor position
|
||||||
this._terminal.send(C0.ESC + '['
|
const y = this._terminal.buffer.y + 1;
|
||||||
+ (this._terminal.buffer.y + 1)
|
const x = this._terminal.buffer.x + 1;
|
||||||
+ ';'
|
this._terminal.emit('data', `${C0.ESC}[${y};${x}R`);
|
||||||
+ (this._terminal.buffer.x + 1)
|
|
||||||
+ 'R');
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} else if (collect === '?') {
|
} else if (collect === '?') {
|
||||||
@@ -1729,27 +1734,25 @@ export class InputHandler extends Disposable implements IInputHandler {
|
|||||||
switch (params[0]) {
|
switch (params[0]) {
|
||||||
case 6:
|
case 6:
|
||||||
// cursor position
|
// cursor position
|
||||||
this._terminal.send(C0.ESC + '[?'
|
const y = this._terminal.buffer.y + 1;
|
||||||
+ (this._terminal.buffer.y + 1)
|
const x = this._terminal.buffer.x + 1;
|
||||||
+ ';'
|
this._terminal.emit('data', `${C0.ESC}[?${y};${x}R`);
|
||||||
+ (this._terminal.buffer.x + 1)
|
|
||||||
+ 'R');
|
|
||||||
break;
|
break;
|
||||||
case 15:
|
case 15:
|
||||||
// no printer
|
// no printer
|
||||||
// this.send(C0.ESC + '[?11n');
|
// this.handler(C0.ESC + '[?11n');
|
||||||
break;
|
break;
|
||||||
case 25:
|
case 25:
|
||||||
// dont support user defined keys
|
// dont support user defined keys
|
||||||
// this.send(C0.ESC + '[?21n');
|
// this.handler(C0.ESC + '[?21n');
|
||||||
break;
|
break;
|
||||||
case 26:
|
case 26:
|
||||||
// north american keyboard
|
// north american keyboard
|
||||||
// this.send(C0.ESC + '[?27;1;0;0n');
|
// this.handler(C0.ESC + '[?27;1;0;0n');
|
||||||
break;
|
break;
|
||||||
case 53:
|
case 53:
|
||||||
// no dec locator/mouse
|
// no dec locator/mouse
|
||||||
// this.send(C0.ESC + '[?50n');
|
// this.handler(C0.ESC + '[?50n');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-2
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { IMouseZoneManager } from './ui/Types';
|
import { IMouseZoneManager } from './ui/Types';
|
||||||
import { ILinkHoverEvent, ILinkMatcher, LinkMatcherHandler, LinkHoverEventTypes, ILinkMatcherOptions, ILinkifier, ITerminal } from './Types';
|
import { ILinkHoverEvent, ILinkMatcher, LinkMatcherHandler, LinkHoverEventTypes, ILinkMatcherOptions, ILinkifier, ITerminal, LineData } from './Types';
|
||||||
import { MouseZone } from './ui/MouseZoneManager';
|
import { MouseZone } from './ui/MouseZoneManager';
|
||||||
import { EventEmitter } from './EventEmitter';
|
import { EventEmitter } from './EventEmitter';
|
||||||
|
|
||||||
@@ -169,10 +169,18 @@ export class Linkifier extends EventEmitter implements ILinkifier {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// If the first row is wrapped, backtrack to find the origin row and linkify that
|
// If the first row is wrapped, backtrack to find the origin row and linkify that
|
||||||
|
let line: LineData;
|
||||||
|
|
||||||
do {
|
do {
|
||||||
rowIndex--;
|
rowIndex--;
|
||||||
absoluteRowIndex--;
|
absoluteRowIndex--;
|
||||||
} while ((<any>this._terminal.buffer.lines.get(absoluteRowIndex)).isWrapped);
|
line = this._terminal.buffer.lines.get(absoluteRowIndex);
|
||||||
|
|
||||||
|
if (!line) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
} while ((<any>line).isWrapped);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct full unwrapped line text
|
// Construct full unwrapped line text
|
||||||
|
|||||||
@@ -183,6 +183,42 @@ describe('SelectionManager', () => {
|
|||||||
selectionManager.selectWordAt([15, 0]);
|
selectionManager.selectWordAt([15, 0]);
|
||||||
assert.equal(selectionManager.selectionText, 'ij"');
|
assert.equal(selectionManager.selectionText, 'ij"');
|
||||||
});
|
});
|
||||||
|
it('should expand upwards or downards for wrapped lines', () => {
|
||||||
|
buffer.lines.set(0, stringToRow(' foo'));
|
||||||
|
buffer.lines.set(1, stringToRow('bar '));
|
||||||
|
(<any>buffer.lines.get(1)).isWrapped = true;
|
||||||
|
selectionManager.selectWordAt([1, 1]);
|
||||||
|
assert.equal(selectionManager.selectionText, 'foobar');
|
||||||
|
selectionManager.model.clearSelection();
|
||||||
|
selectionManager.selectWordAt([78, 0]);
|
||||||
|
assert.equal(selectionManager.selectionText, 'foobar');
|
||||||
|
});
|
||||||
|
it('should expand both upwards and downwards for word wrapped over many lines', () => {
|
||||||
|
const expectedText = 'fooaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccbar';
|
||||||
|
buffer.lines.set(0, stringToRow(' foo'));
|
||||||
|
buffer.lines.set(1, stringToRow('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'));
|
||||||
|
buffer.lines.set(2, stringToRow('bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'));
|
||||||
|
buffer.lines.set(3, stringToRow('cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc'));
|
||||||
|
buffer.lines.set(4, stringToRow('bar '));
|
||||||
|
(<any>buffer.lines.get(1)).isWrapped = true;
|
||||||
|
(<any>buffer.lines.get(2)).isWrapped = true;
|
||||||
|
(<any>buffer.lines.get(3)).isWrapped = true;
|
||||||
|
(<any>buffer.lines.get(4)).isWrapped = true;
|
||||||
|
selectionManager.selectWordAt([78, 0]);
|
||||||
|
assert.equal(selectionManager.selectionText, expectedText);
|
||||||
|
selectionManager.model.clearSelection();
|
||||||
|
selectionManager.selectWordAt([40, 1]);
|
||||||
|
assert.equal(selectionManager.selectionText, expectedText);
|
||||||
|
selectionManager.model.clearSelection();
|
||||||
|
selectionManager.selectWordAt([40, 2]);
|
||||||
|
assert.equal(selectionManager.selectionText, expectedText);
|
||||||
|
selectionManager.model.clearSelection();
|
||||||
|
selectionManager.selectWordAt([40, 3]);
|
||||||
|
assert.equal(selectionManager.selectionText, expectedText);
|
||||||
|
selectionManager.model.clearSelection();
|
||||||
|
selectionManager.selectWordAt([1, 4]);
|
||||||
|
assert.equal(selectionManager.selectionText, expectedText);
|
||||||
|
});
|
||||||
describe('emoji', () => {
|
describe('emoji', () => {
|
||||||
it('should treat a single emoji as a word when wrapped in spaces', () => {
|
it('should treat a single emoji as a word when wrapped in spaces', () => {
|
||||||
buffer.lines.set(0, stringToRow(' ⚽ a')); // The a is here to prevent the space being trimmed in selectionText
|
buffer.lines.set(0, stringToRow(' ⚽ a')); // The a is here to prevent the space being trimmed in selectionText
|
||||||
|
|||||||
+78
-14
@@ -9,7 +9,7 @@ import * as Browser from './shared/utils/Browser';
|
|||||||
import { CharMeasure } from './ui/CharMeasure';
|
import { CharMeasure } from './ui/CharMeasure';
|
||||||
import { EventEmitter } from './EventEmitter';
|
import { EventEmitter } from './EventEmitter';
|
||||||
import { SelectionModel } from './SelectionModel';
|
import { SelectionModel } from './SelectionModel';
|
||||||
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from './Buffer';
|
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_CODE_INDEX } from './Buffer';
|
||||||
import { AltClickHandler } from './handlers/AltClickHandler';
|
import { AltClickHandler } from './handlers/AltClickHandler';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -451,8 +451,10 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
|||||||
* Removes the listeners that are registered when mousedown is triggered.
|
* Removes the listeners that are registered when mousedown is triggered.
|
||||||
*/
|
*/
|
||||||
private _removeMouseDownListeners(): void {
|
private _removeMouseDownListeners(): void {
|
||||||
this._terminal.element.ownerDocument.removeEventListener('mousemove', this._mouseMoveListener);
|
if (this._terminal.element.ownerDocument) {
|
||||||
this._terminal.element.ownerDocument.removeEventListener('mouseup', this._mouseUpListener);
|
this._terminal.element.ownerDocument.removeEventListener('mousemove', this._mouseMoveListener);
|
||||||
|
this._terminal.element.ownerDocument.removeEventListener('mouseup', this._mouseUpListener);
|
||||||
|
}
|
||||||
clearInterval(this._dragScrollIntervalTimer);
|
clearInterval(this._dragScrollIntervalTimer);
|
||||||
this._dragScrollIntervalTimer = null;
|
this._dragScrollIntervalTimer = null;
|
||||||
}
|
}
|
||||||
@@ -574,11 +576,14 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
|||||||
this._dragScrollAmount = this._getMouseEventScrollAmount(event);
|
this._dragScrollAmount = this._getMouseEventScrollAmount(event);
|
||||||
|
|
||||||
// If the cursor was above or below the viewport, make sure it's at the
|
// If the cursor was above or below the viewport, make sure it's at the
|
||||||
// start or end of the viewport respectively.
|
// start or end of the viewport respectively. This should only happen when
|
||||||
if (this._dragScrollAmount > 0) {
|
// NOT in column select mode.
|
||||||
this._model.selectionEnd[0] = this._terminal.cols;
|
if (this._activeSelectionMode !== SelectionMode.COLUMN) {
|
||||||
} else if (this._dragScrollAmount < 0) {
|
if (this._dragScrollAmount > 0) {
|
||||||
this._model.selectionEnd[0] = 0;
|
this._model.selectionEnd[0] = this._terminal.cols;
|
||||||
|
} else if (this._dragScrollAmount < 0) {
|
||||||
|
this._model.selectionEnd[0] = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the character is a wide character include the cell to the right in the
|
// If the character is a wide character include the cell to the right in the
|
||||||
@@ -607,10 +612,19 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
|||||||
if (this._dragScrollAmount) {
|
if (this._dragScrollAmount) {
|
||||||
this._terminal.scrollLines(this._dragScrollAmount, false);
|
this._terminal.scrollLines(this._dragScrollAmount, false);
|
||||||
// Re-evaluate selection
|
// Re-evaluate selection
|
||||||
|
// If the cursor was above or below the viewport, make sure it's at the
|
||||||
|
// start or end of the viewport respectively. This should only happen when
|
||||||
|
// NOT in column select mode.
|
||||||
if (this._dragScrollAmount > 0) {
|
if (this._dragScrollAmount > 0) {
|
||||||
this._model.selectionEnd = [this._terminal.cols - 1, Math.min(this._terminal.buffer.ydisp + this._terminal.rows, this._terminal.buffer.lines.length - 1)];
|
if (this._activeSelectionMode !== SelectionMode.COLUMN) {
|
||||||
|
this._model.selectionEnd[0] = this._terminal.cols;
|
||||||
|
}
|
||||||
|
this._model.selectionEnd[1] = Math.min(this._terminal.buffer.ydisp + this._terminal.rows, this._terminal.buffer.lines.length - 1);
|
||||||
} else {
|
} else {
|
||||||
this._model.selectionEnd = [0, this._terminal.buffer.ydisp];
|
if (this._activeSelectionMode !== SelectionMode.COLUMN) {
|
||||||
|
this._model.selectionEnd[0] = 0;
|
||||||
|
}
|
||||||
|
this._model.selectionEnd[1] = this._terminal.buffer.ydisp;
|
||||||
}
|
}
|
||||||
this.refresh();
|
this.refresh();
|
||||||
}
|
}
|
||||||
@@ -677,7 +691,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
|||||||
* Gets positional information for the word at the coordinated specified.
|
* Gets positional information for the word at the coordinated specified.
|
||||||
* @param coords The coordinates to get the word at.
|
* @param coords The coordinates to get the word at.
|
||||||
*/
|
*/
|
||||||
private _getWordAt(coords: [number, number], allowWhitespaceOnlySelection: boolean): IWordPosition {
|
private _getWordAt(coords: [number, number], allowWhitespaceOnlySelection: boolean, followWrappedLinesAbove: boolean = true, followWrappedLinesBelow: boolean = true): IWordPosition {
|
||||||
// Ensure coords are within viewport (eg. not within scroll bar)
|
// Ensure coords are within viewport (eg. not within scroll bar)
|
||||||
if (coords[0] >= this._terminal.cols) {
|
if (coords[0] >= this._terminal.cols) {
|
||||||
return null;
|
return null;
|
||||||
@@ -772,7 +786,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
|||||||
|
|
||||||
// Calculate the start _column_, converting the the string indexes back to
|
// Calculate the start _column_, converting the the string indexes back to
|
||||||
// column coordinates.
|
// column coordinates.
|
||||||
const start =
|
let start =
|
||||||
startIndex // The index of the selection's start char in the line string
|
startIndex // The index of the selection's start char in the line string
|
||||||
+ charOffset // The difference between the initial char's column and index
|
+ charOffset // The difference between the initial char's column and index
|
||||||
- leftWideCharCount // The number of wide chars left of the initial char
|
- leftWideCharCount // The number of wide chars left of the initial char
|
||||||
@@ -780,7 +794,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
|||||||
|
|
||||||
// Calculate the length in _columns_, converting the the string indexes back
|
// Calculate the length in _columns_, converting the the string indexes back
|
||||||
// to column coordinates.
|
// to column coordinates.
|
||||||
const length = Math.min(this._terminal.cols, // Disallow lengths larger than the terminal cols
|
let length = Math.min(this._terminal.cols, // Disallow lengths larger than the terminal cols
|
||||||
endIndex // The index of the selection's end char in the line string
|
endIndex // The index of the selection's end char in the line string
|
||||||
- startIndex // The index of the selection's start char in the line string
|
- startIndex // The index of the selection's start char in the line string
|
||||||
+ leftWideCharCount // The number of wide chars left of the initial char
|
+ leftWideCharCount // The number of wide chars left of the initial char
|
||||||
@@ -792,6 +806,34 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Recurse upwards if the line is wrapped and the word wraps to the above line
|
||||||
|
if (followWrappedLinesAbove) {
|
||||||
|
if (start === 0 && bufferLine[0][CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) {
|
||||||
|
const previousBufferLine = this._buffer.lines.get(coords[1] - 1);
|
||||||
|
if (previousBufferLine && (<any>bufferLine).isWrapped && previousBufferLine[this._terminal.cols - 1][CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) {
|
||||||
|
const previousLineWordPosition = this._getWordAt([this._terminal.cols - 1, coords[1] - 1], false, true, false);
|
||||||
|
if (previousLineWordPosition) {
|
||||||
|
const offset = this._terminal.cols - previousLineWordPosition.start;
|
||||||
|
start -= offset;
|
||||||
|
length += offset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recurse downwards if the line is wrapped and the word wraps to the next line
|
||||||
|
if (followWrappedLinesBelow) {
|
||||||
|
if (start + length === this._terminal.cols && bufferLine[this._terminal.cols - 1][CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) {
|
||||||
|
const nextBufferLine = this._buffer.lines.get(coords[1] + 1);
|
||||||
|
if (nextBufferLine && (<any>nextBufferLine).isWrapped && nextBufferLine[0][CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) {
|
||||||
|
const nextLineWordPosition = this._getWordAt([0, coords[1] + 1], false, false, true);
|
||||||
|
if (nextLineWordPosition) {
|
||||||
|
length += nextLineWordPosition.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return { start, length };
|
return { start, length };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -803,6 +845,11 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
|||||||
protected _selectWordAt(coords: [number, number], allowWhitespaceOnlySelection: boolean): void {
|
protected _selectWordAt(coords: [number, number], allowWhitespaceOnlySelection: boolean): void {
|
||||||
const wordPosition = this._getWordAt(coords, allowWhitespaceOnlySelection);
|
const wordPosition = this._getWordAt(coords, allowWhitespaceOnlySelection);
|
||||||
if (wordPosition) {
|
if (wordPosition) {
|
||||||
|
// Adjust negative start value
|
||||||
|
while (wordPosition.start < 0) {
|
||||||
|
wordPosition.start += this._terminal.cols;
|
||||||
|
coords[1]--;
|
||||||
|
}
|
||||||
this._model.selectionStart = [wordPosition.start, coords[1]];
|
this._model.selectionStart = [wordPosition.start, coords[1]];
|
||||||
this._model.selectionStartLength = wordPosition.length;
|
this._model.selectionStartLength = wordPosition.length;
|
||||||
}
|
}
|
||||||
@@ -815,7 +862,24 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
|||||||
private _selectToWordAt(coords: [number, number]): void {
|
private _selectToWordAt(coords: [number, number]): void {
|
||||||
const wordPosition = this._getWordAt(coords, true);
|
const wordPosition = this._getWordAt(coords, true);
|
||||||
if (wordPosition) {
|
if (wordPosition) {
|
||||||
this._model.selectionEnd = [this._model.areSelectionValuesReversed() ? wordPosition.start : (wordPosition.start + wordPosition.length), coords[1]];
|
let endRow = coords[1];
|
||||||
|
|
||||||
|
// Adjust negative start value
|
||||||
|
while (wordPosition.start < 0) {
|
||||||
|
wordPosition.start += this._terminal.cols;
|
||||||
|
endRow--;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust wrapped length value, this only needs to happen when values are reversed as in that
|
||||||
|
// case we're interested in the start of the word, not the end
|
||||||
|
if (!this._model.areSelectionValuesReversed()) {
|
||||||
|
while (wordPosition.start + wordPosition.length > this._terminal.cols) {
|
||||||
|
wordPosition.length -= this._terminal.cols;
|
||||||
|
endRow++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._model.selectionEnd = [this._model.areSelectionValuesReversed() ? wordPosition.start : wordPosition.start + wordPosition.length, endRow];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -128,6 +128,11 @@ describe('SelectionManager', () => {
|
|||||||
model.selectionEnd = [3, 2];
|
model.selectionEnd = [3, 2];
|
||||||
assert.deepEqual(model.finalSelectionEnd, [4, 2]);
|
assert.deepEqual(model.finalSelectionEnd, [4, 2]);
|
||||||
});
|
});
|
||||||
|
it('should return the end on a different row when start + length overflows onto a following row', () => {
|
||||||
|
model.selectionStart = [78, 2];
|
||||||
|
model.selectionStartLength = 4;
|
||||||
|
assert.deepEqual(model.finalSelectionEnd, [2, 3]);
|
||||||
|
});
|
||||||
it('should return selection end if selection end is after selection start + length', () => {
|
it('should return selection end if selection end is after selection start + length', () => {
|
||||||
model.selectionStart = [2, 2];
|
model.selectionStart = [2, 2];
|
||||||
model.selectionStartLength = 2;
|
model.selectionStartLength = 2;
|
||||||
|
|||||||
@@ -76,9 +76,13 @@ export class SelectionModel {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the selection start if the end doesn't exist or they're reversed
|
// Use the selection start + length if the end doesn't exist or they're reversed
|
||||||
if (!this.selectionEnd || this.areSelectionValuesReversed()) {
|
if (!this.selectionEnd || this.areSelectionValuesReversed()) {
|
||||||
return [this.selectionStart[0] + this.selectionStartLength, this.selectionStart[1]];
|
const startPlusLength = this.selectionStart[0] + this.selectionStartLength;
|
||||||
|
if (startPlusLength > this._terminal.cols) {
|
||||||
|
return [startPlusLength % this._terminal.cols, this.selectionStart[1] + Math.floor(startPlusLength / this._terminal.cols)];
|
||||||
|
}
|
||||||
|
return [startPlusLength, this.selectionStart[1]];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure the the word/line is selected after a double/triple click
|
// Ensure the the word/line is selected after a double/triple click
|
||||||
|
|||||||
+103
-71
@@ -21,11 +21,11 @@
|
|||||||
* http://linux.die.net/man/7/urxvt
|
* http://linux.die.net/man/7/urxvt
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharData, LineData } from './Types';
|
import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharData, LineData, CharacterJoinerHandler } from './Types';
|
||||||
import { IMouseZoneManager } from './ui/Types';
|
import { IMouseZoneManager } from './ui/Types';
|
||||||
import { IRenderer } from './renderer/Types';
|
import { IRenderer } from './renderer/Types';
|
||||||
import { BufferSet } from './BufferSet';
|
import { BufferSet } from './BufferSet';
|
||||||
import { Buffer, MAX_BUFFER_SIZE, DEFAULT_ATTR } from './Buffer';
|
import { Buffer, MAX_BUFFER_SIZE, DEFAULT_ATTR, NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from './Buffer';
|
||||||
import { CompositionHelper } from './CompositionHelper';
|
import { CompositionHelper } from './CompositionHelper';
|
||||||
import { EventEmitter } from './EventEmitter';
|
import { EventEmitter } from './EventEmitter';
|
||||||
import { Viewport } from './Viewport';
|
import { Viewport } from './Viewport';
|
||||||
@@ -72,7 +72,7 @@ const WRITE_BATCH_SIZE = 300;
|
|||||||
/**
|
/**
|
||||||
* The set of options that only have an effect when set in the Terminal constructor.
|
* The set of options that only have an effect when set in the Terminal constructor.
|
||||||
*/
|
*/
|
||||||
const CONSTRUCTOR_ONLY_OPTIONS = ['cols', 'rows', 'rendererType'];
|
const CONSTRUCTOR_ONLY_OPTIONS = ['cols', 'rows'];
|
||||||
|
|
||||||
const DEFAULT_OPTIONS: ITerminalOptions = {
|
const DEFAULT_OPTIONS: ITerminalOptions = {
|
||||||
cols: 80,
|
cols: 80,
|
||||||
@@ -135,7 +135,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
|||||||
public cursorHidden: boolean;
|
public cursorHidden: boolean;
|
||||||
public convertEol: boolean;
|
public convertEol: boolean;
|
||||||
|
|
||||||
private _sendDataQueue: string;
|
|
||||||
private _customKeyEventHandler: CustomKeyEventHandler;
|
private _customKeyEventHandler: CustomKeyEventHandler;
|
||||||
|
|
||||||
// modes
|
// modes
|
||||||
@@ -207,6 +206,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
|||||||
public mouseHelper: MouseHelper;
|
public mouseHelper: MouseHelper;
|
||||||
private _accessibilityManager: AccessibilityManager;
|
private _accessibilityManager: AccessibilityManager;
|
||||||
private _screenDprMonitor: ScreenDprMonitor;
|
private _screenDprMonitor: ScreenDprMonitor;
|
||||||
|
private _theme: ITheme;
|
||||||
|
|
||||||
public cols: number;
|
public cols: number;
|
||||||
public rows: number;
|
public rows: number;
|
||||||
@@ -214,7 +214,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
|||||||
/**
|
/**
|
||||||
* Creates a new `Terminal` object.
|
* Creates a new `Terminal` object.
|
||||||
*
|
*
|
||||||
* @param {object} options An object containing a set of options, the available options are:
|
* @param options An object containing a set of options, the available options are:
|
||||||
* - `cursorBlink` (boolean): Whether the terminal cursor blinks
|
* - `cursorBlink` (boolean): Whether the terminal cursor blinks
|
||||||
* - `cols` (number): The number of columns of the terminal (horizontal size)
|
* - `cols` (number): The number of columns of the terminal (horizontal size)
|
||||||
* - `rows` (number): The number of rows of the terminal (vertical size)
|
* - `rows` (number): The number of rows of the terminal (vertical size)
|
||||||
@@ -270,7 +270,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
|||||||
|
|
||||||
this.cursorState = 0;
|
this.cursorState = 0;
|
||||||
this.cursorHidden = false;
|
this.cursorHidden = false;
|
||||||
this._sendDataQueue = '';
|
|
||||||
this._customKeyEventHandler = null;
|
this._customKeyEventHandler = null;
|
||||||
|
|
||||||
// modes
|
// modes
|
||||||
@@ -348,7 +347,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves an option's value from the terminal.
|
* Retrieves an option's value from the terminal.
|
||||||
* @param {string} key The option key.
|
* @param key The option key.
|
||||||
*/
|
*/
|
||||||
public getOption(key: string): any {
|
public getOption(key: string): any {
|
||||||
if (!(key in DEFAULT_OPTIONS)) {
|
if (!(key in DEFAULT_OPTIONS)) {
|
||||||
@@ -360,8 +359,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets an option on the terminal.
|
* Sets an option on the terminal.
|
||||||
* @param {string} key The option key.
|
* @param key The option key.
|
||||||
* @param {any} value The option value.
|
* @param value The option value.
|
||||||
*/
|
*/
|
||||||
public setOption(key: string, value: any): void {
|
public setOption(key: string, value: any): void {
|
||||||
if (!(key in DEFAULT_OPTIONS)) {
|
if (!(key in DEFAULT_OPTIONS)) {
|
||||||
@@ -370,6 +369,9 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
|||||||
if (CONSTRUCTOR_ONLY_OPTIONS.indexOf(key) !== -1) {
|
if (CONSTRUCTOR_ONLY_OPTIONS.indexOf(key) !== -1) {
|
||||||
console.error(`Option "${key}" can only be set in the constructor`);
|
console.error(`Option "${key}" can only be set in the constructor`);
|
||||||
}
|
}
|
||||||
|
if (this.options[key] === value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case 'bellStyle':
|
case 'bellStyle':
|
||||||
if (!value) {
|
if (!value) {
|
||||||
@@ -396,6 +398,11 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
|||||||
console.warn(`${key} cannot be less than 1, value: ${value}`);
|
console.warn(`${key} cannot be less than 1, value: ${value}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
case 'rendererType':
|
||||||
|
if (!value) {
|
||||||
|
value = 'canvas';
|
||||||
|
}
|
||||||
|
break;
|
||||||
case 'tabStopWidth':
|
case 'tabStopWidth':
|
||||||
if (value < 1) {
|
if (value < 1) {
|
||||||
console.warn(`${key} cannot be less than 1, value: ${value}`);
|
console.warn(`${key} cannot be less than 1, value: ${value}`);
|
||||||
@@ -455,6 +462,18 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
|||||||
this.renderer.onResize(this.cols, this.rows);
|
this.renderer.onResize(this.cols, this.rows);
|
||||||
this.refresh(0, this.rows - 1);
|
this.refresh(0, this.rows - 1);
|
||||||
}
|
}
|
||||||
|
case 'rendererType':
|
||||||
|
if (this.renderer) {
|
||||||
|
this.unregister(this.renderer);
|
||||||
|
this.renderer.dispose();
|
||||||
|
this.renderer = null;
|
||||||
|
}
|
||||||
|
this._setupRenderer();
|
||||||
|
this.renderer.onCharSizeChanged();
|
||||||
|
if (this._theme) {
|
||||||
|
this.renderer.setTheme(this._theme);
|
||||||
|
}
|
||||||
|
break;
|
||||||
case 'scrollback':
|
case 'scrollback':
|
||||||
this.buffers.resize(this.cols, this.rows);
|
this.buffers.resize(this.cols, this.rows);
|
||||||
if (this.viewport) {
|
if (this.viewport) {
|
||||||
@@ -486,7 +505,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
|||||||
*/
|
*/
|
||||||
private _onTextAreaFocus(): void {
|
private _onTextAreaFocus(): void {
|
||||||
if (this.sendFocus) {
|
if (this.sendFocus) {
|
||||||
this.send(C0.ESC + '[I');
|
this.handler(C0.ESC + '[I');
|
||||||
}
|
}
|
||||||
this.element.classList.add('focus');
|
this.element.classList.add('focus');
|
||||||
this.showCursor();
|
this.showCursor();
|
||||||
@@ -510,7 +529,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
|||||||
this.textarea.value = '';
|
this.textarea.value = '';
|
||||||
this.refresh(this.buffer.y, this.buffer.y);
|
this.refresh(this.buffer.y, this.buffer.y);
|
||||||
if (this.sendFocus) {
|
if (this.sendFocus) {
|
||||||
this.send(C0.ESC + '[O');
|
this.handler(C0.ESC + '[O');
|
||||||
}
|
}
|
||||||
this.element.classList.remove('focus');
|
this.element.classList.remove('focus');
|
||||||
this.emit('blur');
|
this.emit('blur');
|
||||||
@@ -602,7 +621,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
|||||||
/**
|
/**
|
||||||
* Opens the terminal within an element.
|
* Opens the terminal within an element.
|
||||||
*
|
*
|
||||||
* @param {HTMLElement} parent The element to create the terminal within.
|
* @param parent The element to create the terminal within.
|
||||||
*/
|
*/
|
||||||
public open(parent: HTMLElement): void {
|
public open(parent: HTMLElement): void {
|
||||||
this._parent = parent || this._parent;
|
this._parent = parent || this._parent;
|
||||||
@@ -674,12 +693,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
|||||||
// Performance: Add viewport and helper elements from the fragment
|
// Performance: Add viewport and helper elements from the fragment
|
||||||
this.element.appendChild(fragment);
|
this.element.appendChild(fragment);
|
||||||
|
|
||||||
switch (this.options.rendererType) {
|
this._setupRenderer();
|
||||||
case 'canvas': this.renderer = new Renderer(this, this.options.theme); break;
|
this._theme = this.options.theme;
|
||||||
case 'dom': this.renderer = new DomRenderer(this, this.options.theme); break;
|
|
||||||
default: throw new Error(`Unrecognized rendererType "${this.options.rendererType}"`);
|
|
||||||
}
|
|
||||||
this.register(this.renderer);
|
|
||||||
this.options.theme = null;
|
this.options.theme = null;
|
||||||
this.viewport = new Viewport(this, this._viewportElement, this._viewportScrollArea, this.charMeasure);
|
this.viewport = new Viewport(this, this._viewportElement, this._viewportScrollArea, this.charMeasure);
|
||||||
this.viewport.onThemeChanged(this.renderer.colorManager.colors);
|
this.viewport.onThemeChanged(this.renderer.colorManager.colors);
|
||||||
@@ -736,11 +751,21 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _setupRenderer(): void {
|
||||||
|
switch (this.options.rendererType) {
|
||||||
|
case 'canvas': this.renderer = new Renderer(this, this.options.theme); break;
|
||||||
|
case 'dom': this.renderer = new DomRenderer(this, this.options.theme); break;
|
||||||
|
default: throw new Error(`Unrecognized rendererType "${this.options.rendererType}"`);
|
||||||
|
}
|
||||||
|
this.register(this.renderer);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the theme on the renderer. The renderer must have been initialized.
|
* Sets the theme on the renderer. The renderer must have been initialized.
|
||||||
* @param theme The theme to ste.
|
* @param theme The theme to set.
|
||||||
*/
|
*/
|
||||||
private _setTheme(theme: ITheme): void {
|
private _setTheme(theme: ITheme): void {
|
||||||
|
this._theme = theme;
|
||||||
const colors = this.renderer.setTheme(theme);
|
const colors = this.renderer.setTheme(theme);
|
||||||
if (this.viewport) {
|
if (this.viewport) {
|
||||||
this.viewport.onThemeChanged(colors);
|
this.viewport.onThemeChanged(colors);
|
||||||
@@ -860,7 +885,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
|||||||
else if (button === 3) return;
|
else if (button === 3) return;
|
||||||
else data += '0';
|
else data += '0';
|
||||||
data += '~[' + pos.x + ',' + pos.y + ']\r';
|
data += '~[' + pos.x + ',' + pos.y + ']\r';
|
||||||
self.send(data);
|
self.handler(data);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -873,7 +898,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
|||||||
else if (button === 1) button = 4;
|
else if (button === 1) button = 4;
|
||||||
else if (button === 2) button = 6;
|
else if (button === 2) button = 6;
|
||||||
else if (button === 3) button = 3;
|
else if (button === 3) button = 3;
|
||||||
self.send(C0.ESC + '['
|
self.handler(C0.ESC + '['
|
||||||
+ button
|
+ button
|
||||||
+ ';'
|
+ ';'
|
||||||
+ (button === 3 ? 4 : 0)
|
+ (button === 3 ? 4 : 0)
|
||||||
@@ -893,14 +918,14 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
|||||||
pos.y -= 32;
|
pos.y -= 32;
|
||||||
pos.x++;
|
pos.x++;
|
||||||
pos.y++;
|
pos.y++;
|
||||||
self.send(C0.ESC + '[' + button + ';' + pos.x + ';' + pos.y + 'M');
|
self.handler(C0.ESC + '[' + button + ';' + pos.x + ';' + pos.y + 'M');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (self.sgrMouse) {
|
if (self.sgrMouse) {
|
||||||
pos.x -= 32;
|
pos.x -= 32;
|
||||||
pos.y -= 32;
|
pos.y -= 32;
|
||||||
self.send(C0.ESC + '[<'
|
self.handler(C0.ESC + '[<'
|
||||||
+ (((button & 3) === 3 ? button & ~3 : button) - 32)
|
+ (((button & 3) === 3 ? button & ~3 : button) - 32)
|
||||||
+ ';'
|
+ ';'
|
||||||
+ pos.x
|
+ pos.x
|
||||||
@@ -916,7 +941,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
|||||||
encode(data, pos.x);
|
encode(data, pos.x);
|
||||||
encode(data, pos.y);
|
encode(data, pos.y);
|
||||||
|
|
||||||
self.send(C0.ESC + '[M' + String.fromCharCode.apply(String, data));
|
self.handler(C0.ESC + '[M' + String.fromCharCode.apply(String, data));
|
||||||
}
|
}
|
||||||
|
|
||||||
function getButton(ev: MouseEvent): number {
|
function getButton(ev: MouseEvent): number {
|
||||||
@@ -1065,7 +1090,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
|||||||
for (let i = 0; i < Math.abs(amount); i++) {
|
for (let i = 0; i < Math.abs(amount); i++) {
|
||||||
data += sequence;
|
data += sequence;
|
||||||
}
|
}
|
||||||
this.send(data);
|
this.handler(data);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1098,8 +1123,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
|||||||
/**
|
/**
|
||||||
* Tells the renderer to refresh terminal content between two rows (inclusive) at the next
|
* Tells the renderer to refresh terminal content between two rows (inclusive) at the next
|
||||||
* opportunity.
|
* opportunity.
|
||||||
* @param {number} start The row to start from (between 0 and this.rows - 1).
|
* @param start The row to start from (between 0 and this.rows - 1).
|
||||||
* @param {number} end The row to end at (between start and this.rows - 1).
|
* @param end The row to end at (between start and this.rows - 1).
|
||||||
*/
|
*/
|
||||||
public refresh(start: number, end: number): void {
|
public refresh(start: number, end: number): void {
|
||||||
if (this.renderer) {
|
if (this.renderer) {
|
||||||
@@ -1109,8 +1134,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Queues linkification for the specified rows.
|
* Queues linkification for the specified rows.
|
||||||
* @param {number} start The row to start from (between 0 and this.rows - 1).
|
* @param start The row to start from (between 0 and this.rows - 1).
|
||||||
* @param {number} end The row to end at (between start and this.rows - 1).
|
* @param end The row to end at (between start and this.rows - 1).
|
||||||
*/
|
*/
|
||||||
private _queueLinkification(start: number, end: number): void {
|
private _queueLinkification(start: number, end: number): void {
|
||||||
if (this.linkifier) {
|
if (this.linkifier) {
|
||||||
@@ -1202,8 +1227,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Scroll the display of the terminal
|
* Scroll the display of the terminal
|
||||||
* @param {number} disp The number of lines to scroll down (negative scroll up).
|
* @param disp The number of lines to scroll down (negative scroll up).
|
||||||
* @param {boolean} suppressScrollEvent Don't emit the scroll event as scrollLines. This is used
|
* @param suppressScrollEvent Don't emit the scroll event as scrollLines. This is used
|
||||||
* to avoid unwanted events being handled by the viewport when the event was triggered from the
|
* to avoid unwanted events being handled by the viewport when the event was triggered from the
|
||||||
* viewport originally.
|
* viewport originally.
|
||||||
*/
|
*/
|
||||||
@@ -1234,7 +1259,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Scroll the display of the terminal by a number of pages.
|
* Scroll the display of the terminal by a number of pages.
|
||||||
* @param {number} pageCount The number of pages to scroll (negative scrolls up).
|
* @param pageCount The number of pages to scroll (negative scrolls up).
|
||||||
*/
|
*/
|
||||||
public scrollPages(pageCount: number): void {
|
public scrollPages(pageCount: number): void {
|
||||||
this.scrollLines(pageCount * (this.rows - 1));
|
this.scrollLines(pageCount * (this.rows - 1));
|
||||||
@@ -1263,9 +1288,14 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Writes text to the terminal.
|
* Writes text to the terminal.
|
||||||
* @param {string} data The text to write to the terminal.
|
* @param data The text to write to the terminal.
|
||||||
*/
|
*/
|
||||||
public write(data: string): void {
|
public write(data: string): void {
|
||||||
|
// Ensure the terminal isn't disposed
|
||||||
|
if (this._isDisposed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Ignore falsy data values (including the empty string)
|
// Ignore falsy data values (including the empty string)
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return;
|
return;
|
||||||
@@ -1279,7 +1309,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
|||||||
if (this.options.useFlowControl && !this._xoffSentToCatchUp && this.writeBuffer.length >= WRITE_BUFFER_PAUSE_THRESHOLD) {
|
if (this.options.useFlowControl && !this._xoffSentToCatchUp && this.writeBuffer.length >= WRITE_BUFFER_PAUSE_THRESHOLD) {
|
||||||
// XOFF - stop pty pipe
|
// XOFF - stop pty pipe
|
||||||
// XON will be triggered by emulator before processing data chunk
|
// XON will be triggered by emulator before processing data chunk
|
||||||
this.send(C0.DC3);
|
this.handler(C0.DC3);
|
||||||
this._xoffSentToCatchUp = true;
|
this._xoffSentToCatchUp = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1294,6 +1324,11 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected _innerWrite(): void {
|
protected _innerWrite(): void {
|
||||||
|
// Ensure the terminal isn't disposed
|
||||||
|
if (this._isDisposed) {
|
||||||
|
this.writeBuffer = [];
|
||||||
|
}
|
||||||
|
|
||||||
const writeBatch = this.writeBuffer.splice(0, WRITE_BATCH_SIZE);
|
const writeBatch = this.writeBuffer.splice(0, WRITE_BATCH_SIZE);
|
||||||
while (writeBatch.length > 0) {
|
while (writeBatch.length > 0) {
|
||||||
const data = writeBatch.shift();
|
const data = writeBatch.shift();
|
||||||
@@ -1301,7 +1336,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
|||||||
// If XOFF was sent in order to catch up with the pty process, resume it if
|
// If XOFF was sent in order to catch up with the pty process, resume it if
|
||||||
// the writeBuffer is empty to allow more data to come in.
|
// the writeBuffer is empty to allow more data to come in.
|
||||||
if (this._xoffSentToCatchUp && writeBatch.length === 0 && this.writeBuffer.length === 0) {
|
if (this._xoffSentToCatchUp && writeBatch.length === 0 && this.writeBuffer.length === 0) {
|
||||||
this.send(C0.DC1);
|
this.handler(C0.DC1);
|
||||||
this._xoffSentToCatchUp = false;
|
this._xoffSentToCatchUp = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1329,7 +1364,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Writes text to the terminal, followed by a break line character (\n).
|
* Writes text to the terminal, followed by a break line character (\n).
|
||||||
* @param {string} data The text to write to the terminal.
|
* @param data The text to write to the terminal.
|
||||||
*/
|
*/
|
||||||
public writeln(data: string): void {
|
public writeln(data: string): void {
|
||||||
this.write(data + '\r\n');
|
this.write(data + '\r\n');
|
||||||
@@ -1374,6 +1409,18 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public registerCharacterJoiner(handler: CharacterJoinerHandler): number {
|
||||||
|
const joinerId = this.renderer.registerCharacterJoiner(handler);
|
||||||
|
this.refresh(0, this.rows - 1);
|
||||||
|
return joinerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public deregisterCharacterJoiner(joinerId: number): void {
|
||||||
|
if (this.renderer.deregisterCharacterJoiner(joinerId)) {
|
||||||
|
this.refresh(0, this.rows - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public get markers(): IMarker[] {
|
public get markers(): IMarker[] {
|
||||||
return this.buffer.markers;
|
return this.buffer.markers;
|
||||||
}
|
}
|
||||||
@@ -1430,7 +1477,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
|||||||
* Handle a keydown event
|
* Handle a keydown event
|
||||||
* Key Resources:
|
* Key Resources:
|
||||||
* - https://developer.mozilla.org/en-US/docs/DOM/KeyboardEvent
|
* - https://developer.mozilla.org/en-US/docs/DOM/KeyboardEvent
|
||||||
* @param {KeyboardEvent} ev The keydown event to be handled.
|
* @param ev The keydown event to be handled.
|
||||||
*/
|
*/
|
||||||
protected _keyDown(event: KeyboardEvent): boolean {
|
protected _keyDown(event: KeyboardEvent): boolean {
|
||||||
if (this._customKeyEventHandler && this._customKeyEventHandler(event) === false) {
|
if (this._customKeyEventHandler && this._customKeyEventHandler(event) === false) {
|
||||||
@@ -1527,7 +1574,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
|||||||
* Handle a keypress event.
|
* Handle a keypress event.
|
||||||
* Key Resources:
|
* Key Resources:
|
||||||
* - https://developer.mozilla.org/en-US/docs/DOM/KeyboardEvent
|
* - https://developer.mozilla.org/en-US/docs/DOM/KeyboardEvent
|
||||||
* @param {KeyboardEvent} ev The keypress event to be handled.
|
* @param ev The keypress event to be handled.
|
||||||
*/
|
*/
|
||||||
protected _keyPress(ev: KeyboardEvent): boolean {
|
protected _keyPress(ev: KeyboardEvent): boolean {
|
||||||
let key;
|
let key;
|
||||||
@@ -1564,21 +1611,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Send data for handling to the terminal
|
|
||||||
* @param {string} data
|
|
||||||
*/
|
|
||||||
public send(data: string): void {
|
|
||||||
if (!this._sendDataQueue) {
|
|
||||||
setTimeout(() => {
|
|
||||||
this.handler(this._sendDataQueue);
|
|
||||||
this._sendDataQueue = '';
|
|
||||||
}, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
this._sendDataQueue += data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ring the bell.
|
* Ring the bell.
|
||||||
* Note: We could do sweet things with webaudio here
|
* Note: We could do sweet things with webaudio here
|
||||||
@@ -1619,8 +1651,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
|||||||
/**
|
/**
|
||||||
* Resizes the terminal.
|
* Resizes the terminal.
|
||||||
*
|
*
|
||||||
* @param {number} x The number of columns to resize to.
|
* @param x The number of columns to resize to.
|
||||||
* @param {number} y The number of rows to resize to.
|
* @param y The number of rows to resize to.
|
||||||
*/
|
*/
|
||||||
public resize(x: number, y: number): void {
|
public resize(x: number, y: number): void {
|
||||||
if (isNaN(x) || isNaN(y)) {
|
if (isNaN(x) || isNaN(y)) {
|
||||||
@@ -1654,7 +1686,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the range of rows to refresh
|
* Updates the range of rows to refresh
|
||||||
* @param {number} y The number of rows to refresh next.
|
* @param y The number of rows to refresh next.
|
||||||
*/
|
*/
|
||||||
public updateRange(y: number): void {
|
public updateRange(y: number): void {
|
||||||
if (y < this._refreshStart) this._refreshStart = y;
|
if (y < this._refreshStart) this._refreshStart = y;
|
||||||
@@ -1677,15 +1709,15 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Erase in the identified line everything from "x" to the end of the line (right).
|
* Erase in the identified line everything from "x" to the end of the line (right).
|
||||||
* @param {number} x The column from which to start erasing to the end of the line.
|
* @param x The column from which to start erasing to the end of the line.
|
||||||
* @param {number} y The line in which to operate.
|
* @param y The line in which to operate.
|
||||||
*/
|
*/
|
||||||
public eraseRight(x: number, y: number): void {
|
public eraseRight(x: number, y: number): void {
|
||||||
const line = this.buffer.lines.get(this.buffer.ybase + y);
|
const line = this.buffer.lines.get(this.buffer.ybase + y);
|
||||||
if (!line) {
|
if (!line) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const ch: CharData = [this.eraseAttr(), ' ', 1, 32 /* ' '.charCodeAt(0) */]; // xterm
|
const ch: CharData = [this.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; // xterm
|
||||||
for (; x < this.cols; x++) {
|
for (; x < this.cols; x++) {
|
||||||
line[x] = ch;
|
line[x] = ch;
|
||||||
}
|
}
|
||||||
@@ -1694,15 +1726,15 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Erase in the identified line everything from "x" to the start of the line (left).
|
* Erase in the identified line everything from "x" to the start of the line (left).
|
||||||
* @param {number} x The column from which to start erasing to the start of the line.
|
* @param x The column from which to start erasing to the start of the line.
|
||||||
* @param {number} y The line in which to operate.
|
* @param y The line in which to operate.
|
||||||
*/
|
*/
|
||||||
public eraseLeft(x: number, y: number): void {
|
public eraseLeft(x: number, y: number): void {
|
||||||
const line = this.buffer.lines.get(this.buffer.ybase + y);
|
const line = this.buffer.lines.get(this.buffer.ybase + y);
|
||||||
if (!line) {
|
if (!line) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const ch: CharData = [this.eraseAttr(), ' ', 1, 32 /* ' '.charCodeAt(0) */]; // xterm
|
const ch: CharData = [this.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; // xterm
|
||||||
x++;
|
x++;
|
||||||
while (x--) {
|
while (x--) {
|
||||||
line[x] = ch;
|
line[x] = ch;
|
||||||
@@ -1732,7 +1764,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Erase all content in the given line
|
* Erase all content in the given line
|
||||||
* @param {number} y The line to erase all of its contents.
|
* @param y The line to erase all of its contents.
|
||||||
*/
|
*/
|
||||||
public eraseLine(y: number): void {
|
public eraseLine(y: number): void {
|
||||||
this.eraseRight(0, y);
|
this.eraseRight(0, y);
|
||||||
@@ -1740,15 +1772,15 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the data array of a blank line
|
* Return the data array of a blank line
|
||||||
* @param {boolean} cur First bunch of data for each "blank" character.
|
* @param cur First bunch of data for each "blank" character.
|
||||||
* @param {boolean} isWrapped Whether the new line is wrapped from the previous line.
|
* @param isWrapped Whether the new line is wrapped from the previous line.
|
||||||
* @param {boolean} cols The number of columns in the terminal, if this is not
|
* @param cols The number of columns in the terminal, if this is not
|
||||||
* set, the terminal's current column count would be used.
|
* set, the terminal's current column count would be used.
|
||||||
*/
|
*/
|
||||||
public blankLine(cur?: boolean, isWrapped?: boolean, cols?: number): LineData {
|
public blankLine(cur?: boolean, isWrapped?: boolean, cols?: number): LineData {
|
||||||
const attr = cur ? this.eraseAttr() : DEFAULT_ATTR;
|
const attr = cur ? this.eraseAttr() : DEFAULT_ATTR;
|
||||||
|
|
||||||
const ch: CharData = [attr, ' ', 1, 32 /* ' '.charCodeAt(0) */]; // width defaults to 1 halfwidth character
|
const ch: CharData = [attr, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; // width defaults to 1 halfwidth character
|
||||||
const line: LineData = [];
|
const line: LineData = [];
|
||||||
|
|
||||||
// TODO: It is not ideal that this is a property on an array, a buffer line
|
// TODO: It is not ideal that this is a property on an array, a buffer line
|
||||||
@@ -1771,9 +1803,9 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
|||||||
*/
|
*/
|
||||||
public ch(cur?: boolean): CharData {
|
public ch(cur?: boolean): CharData {
|
||||||
if (cur) {
|
if (cur) {
|
||||||
return [this.eraseAttr(), ' ', 1, 32 /* ' '.charCodeAt(0) */];
|
return [this.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE];
|
||||||
}
|
}
|
||||||
return [DEFAULT_ATTR, ' ', 1, 32 /* ' '.charCodeAt(0) */];
|
return [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1786,7 +1818,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Emit the 'data' event and populate the given data.
|
* Emit the 'data' event and populate the given data.
|
||||||
* @param {string} data The data to populate in the event.
|
* @param data The data to populate in the event.
|
||||||
*/
|
*/
|
||||||
public handler(data: string): void {
|
public handler(data: string): void {
|
||||||
// Prevents all events to pty process if stdin is disabled
|
// Prevents all events to pty process if stdin is disabled
|
||||||
@@ -1808,7 +1840,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Emit the 'title' event and populate the given title.
|
* Emit the 'title' event and populate the given title.
|
||||||
* @param {string} title The title to populate in the event.
|
* @param title The title to populate in the event.
|
||||||
*/
|
*/
|
||||||
public handleTitle(title: string): void {
|
public handleTitle(title: string): void {
|
||||||
/**
|
/**
|
||||||
|
|||||||
+7
-2
@@ -18,6 +18,8 @@ export type LineData = CharData[];
|
|||||||
export type LinkMatcherHandler = (event: MouseEvent, uri: string) => void;
|
export type LinkMatcherHandler = (event: MouseEvent, uri: string) => void;
|
||||||
export type LinkMatcherValidationCallback = (uri: string, callback: (isValid: boolean) => void) => void;
|
export type LinkMatcherValidationCallback = (uri: string, callback: (isValid: boolean) => void) => void;
|
||||||
|
|
||||||
|
export type CharacterJoinerHandler = (text: string) => [number, number][];
|
||||||
|
|
||||||
export const enum LinkHoverEventTypes {
|
export const enum LinkHoverEventTypes {
|
||||||
HOVER = 'linkhover',
|
HOVER = 'linkhover',
|
||||||
TOOLTIP = 'linktooltip',
|
TOOLTIP = 'linktooltip',
|
||||||
@@ -45,6 +47,7 @@ export interface IInputHandlingTerminal extends IEventEmitter {
|
|||||||
wraparoundMode: boolean;
|
wraparoundMode: boolean;
|
||||||
bracketedPasteMode: boolean;
|
bracketedPasteMode: boolean;
|
||||||
curAttr: number;
|
curAttr: number;
|
||||||
|
savedCurAttr: number;
|
||||||
savedCols: number;
|
savedCols: number;
|
||||||
x10Mouse: boolean;
|
x10Mouse: boolean;
|
||||||
vt200Mouse: boolean;
|
vt200Mouse: boolean;
|
||||||
@@ -73,7 +76,6 @@ export interface IInputHandlingTerminal extends IEventEmitter {
|
|||||||
eraseLeft(x: number, y: number): void;
|
eraseLeft(x: number, y: number): void;
|
||||||
blankLine(cur?: boolean, isWrapped?: boolean): LineData;
|
blankLine(cur?: boolean, isWrapped?: boolean): LineData;
|
||||||
is(term: string): boolean;
|
is(term: string): boolean;
|
||||||
send(data: string): void;
|
|
||||||
setgCharset(g: number, charset: ICharset): void;
|
setgCharset(g: number, charset: ICharset): void;
|
||||||
resize(x: number, y: number): void;
|
resize(x: number, y: number): void;
|
||||||
log(text: string, data?: any): void;
|
log(text: string, data?: any): void;
|
||||||
@@ -84,6 +86,10 @@ export interface IInputHandlingTerminal extends IEventEmitter {
|
|||||||
error(text: string, data?: any): void;
|
error(text: string, data?: any): void;
|
||||||
setOption(key: string, value: any): void;
|
setOption(key: string, value: any): void;
|
||||||
tabSet(): void;
|
tabSet(): void;
|
||||||
|
handler(data: string): void;
|
||||||
|
handleTitle(title: string): void;
|
||||||
|
index(): void;
|
||||||
|
reverseIndex(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IViewport extends IDisposable {
|
export interface IViewport extends IDisposable {
|
||||||
@@ -223,7 +229,6 @@ export interface ITerminal extends PublicTerminal, IElementAccessor, IBufferAcce
|
|||||||
* @param data The data to populate in the event.
|
* @param data The data to populate in the event.
|
||||||
*/
|
*/
|
||||||
handler(data: string): void;
|
handler(data: string): void;
|
||||||
send(data: string): void;
|
|
||||||
scrollLines(disp: number, suppressScrollEvent?: boolean): void;
|
scrollLines(disp: number, suppressScrollEvent?: boolean): void;
|
||||||
cancel(ev: Event, force?: boolean): boolean | void;
|
cancel(ev: Event, force?: boolean): boolean | void;
|
||||||
log(text: string): void;
|
log(text: string): void;
|
||||||
|
|||||||
@@ -6,6 +6,9 @@
|
|||||||
import { Terminal } from 'xterm';
|
import { Terminal } from 'xterm';
|
||||||
import { IWinptyCompatAddonTerminal } from './Interfaces';
|
import { IWinptyCompatAddonTerminal } from './Interfaces';
|
||||||
|
|
||||||
|
const CHAR_DATA_CODE_INDEX = 3;
|
||||||
|
const NULL_CELL_CODE = 32;
|
||||||
|
|
||||||
export function winptyCompatInit(terminal: Terminal): void {
|
export function winptyCompatInit(terminal: Terminal): void {
|
||||||
const addonTerminal = <IWinptyCompatAddonTerminal>terminal;
|
const addonTerminal = <IWinptyCompatAddonTerminal>terminal;
|
||||||
|
|
||||||
@@ -29,7 +32,7 @@ export function winptyCompatInit(terminal: Terminal): void {
|
|||||||
const line = addonTerminal._core.buffer.lines.get(addonTerminal._core.buffer.ybase + addonTerminal._core.buffer.y - 1);
|
const line = addonTerminal._core.buffer.lines.get(addonTerminal._core.buffer.ybase + addonTerminal._core.buffer.y - 1);
|
||||||
const lastChar = line[addonTerminal.cols - 1];
|
const lastChar = line[addonTerminal.cols - 1];
|
||||||
|
|
||||||
if (lastChar[3] !== 32 /* ' ' */) {
|
if (lastChar[CHAR_DATA_CODE_INDEX] !== NULL_CELL_CODE) {
|
||||||
const nextLine = addonTerminal._core.buffer.lines.get(addonTerminal._core.buffer.ybase + addonTerminal._core.buffer.y);
|
const nextLine = addonTerminal._core.buffer.lines.get(addonTerminal._core.buffer.ybase + addonTerminal._core.buffer.y);
|
||||||
(<any>nextLine).isWrapped = true;
|
(<any>nextLine).isWrapped = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { assert } from 'chai';
|
||||||
|
import { Disposable } from './Lifecycle';
|
||||||
|
|
||||||
|
class TestDisposable extends Disposable {
|
||||||
|
public get isDisposed(): boolean {
|
||||||
|
return this._isDisposed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Disposable', () => {
|
||||||
|
describe('register', () => {
|
||||||
|
it('should register disposables', () => {
|
||||||
|
const d = new TestDisposable();
|
||||||
|
const d2 = {
|
||||||
|
dispose: () => { throw new Error(); }
|
||||||
|
};
|
||||||
|
d.register(d2);
|
||||||
|
assert.throws(() => d.dispose());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('unregister', () => {
|
||||||
|
it('should unregister disposables', () => {
|
||||||
|
const d = new TestDisposable();
|
||||||
|
const d2 = {
|
||||||
|
dispose: () => { throw new Error(); }
|
||||||
|
};
|
||||||
|
d.register(d2);
|
||||||
|
d.unregister(d2);
|
||||||
|
assert.doesNotThrow(() => d.dispose());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('dispose', () => {
|
||||||
|
it('should set is disposed flag', () => {
|
||||||
|
const d = new TestDisposable();
|
||||||
|
assert.isFalse(d.isDisposed);
|
||||||
|
d.dispose();
|
||||||
|
assert.isTrue(d.isDisposed);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -11,6 +11,7 @@ import { IDisposable } from 'xterm';
|
|||||||
*/
|
*/
|
||||||
export abstract class Disposable implements IDisposable {
|
export abstract class Disposable implements IDisposable {
|
||||||
protected _disposables: IDisposable[] = [];
|
protected _disposables: IDisposable[] = [];
|
||||||
|
protected _isDisposed: boolean = false;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
}
|
}
|
||||||
@@ -19,6 +20,7 @@ export abstract class Disposable implements IDisposable {
|
|||||||
* Disposes the object, triggering the `dispose` method on all registered IDisposables.
|
* Disposes the object, triggering the `dispose` method on all registered IDisposables.
|
||||||
*/
|
*/
|
||||||
public dispose(): void {
|
public dispose(): void {
|
||||||
|
this._isDisposed = true;
|
||||||
this._disposables.forEach(d => d.dispose());
|
this._disposables.forEach(d => d.dispose());
|
||||||
this._disposables.length = 0;
|
this._disposables.length = 0;
|
||||||
}
|
}
|
||||||
@@ -30,4 +32,16 @@ export abstract class Disposable implements IDisposable {
|
|||||||
public register<T extends IDisposable>(d: T): void {
|
public register<T extends IDisposable>(d: T): void {
|
||||||
this._disposables.push(d);
|
this._disposables.push(d);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregisters a disposable object if it has been registered, if not do
|
||||||
|
* nothing.
|
||||||
|
* @param d The disposable to unregister.
|
||||||
|
*/
|
||||||
|
public unregister<T extends IDisposable>(d: T): void {
|
||||||
|
const index = this._disposables.indexOf(d);
|
||||||
|
if (index !== -1) {
|
||||||
|
this._disposables.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export class AltClickHandler {
|
|||||||
*/
|
*/
|
||||||
public move(): void {
|
public move(): void {
|
||||||
if (this._mouseEvent.altKey && this._endCol !== undefined && this._endRow !== undefined) {
|
if (this._mouseEvent.altKey && this._endCol !== undefined && this._endRow !== undefined) {
|
||||||
this._terminal.send(this._arrowSequences());
|
this._terminal.handler(this._arrowSequences());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export function bracketTextForPaste(text: string, bracketedPasteMode: boolean):
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Binds copy functionality to the given terminal.
|
* Binds copy functionality to the given terminal.
|
||||||
* @param {ClipboardEvent} ev The original copy event to be handled
|
* @param ev The original copy event to be handled
|
||||||
*/
|
*/
|
||||||
export function copyHandler(ev: ClipboardEvent, term: ITerminal, selectionManager: ISelectionManager): void {
|
export function copyHandler(ev: ClipboardEvent, term: ITerminal, selectionManager: ISelectionManager): void {
|
||||||
if (term.browser.isMSIE) {
|
if (term.browser.isMSIE) {
|
||||||
@@ -50,8 +50,8 @@ export function copyHandler(ev: ClipboardEvent, term: ITerminal, selectionManage
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Redirect the clipboard's data to the terminal's input handler.
|
* Redirect the clipboard's data to the terminal's input handler.
|
||||||
* @param {ClipboardEvent} ev The original paste event to be handled
|
* @param ev The original paste event to be handled
|
||||||
* @param {Terminal} term The terminal on which to apply the handled paste event
|
* @param term The terminal on which to apply the handled paste event
|
||||||
*/
|
*/
|
||||||
export function pasteHandler(ev: ClipboardEvent, term: ITerminal): void {
|
export function pasteHandler(ev: ClipboardEvent, term: ITerminal): void {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
|
|||||||
@@ -65,6 +65,12 @@ export class Terminal implements ITerminalApi {
|
|||||||
public deregisterLinkMatcher(matcherId: number): void {
|
public deregisterLinkMatcher(matcherId: number): void {
|
||||||
this._core.deregisterLinkMatcher(matcherId);
|
this._core.deregisterLinkMatcher(matcherId);
|
||||||
}
|
}
|
||||||
|
public registerCharacterJoiner(handler: (text: string) => [number, number][]): number {
|
||||||
|
return this._core.registerCharacterJoiner(handler);
|
||||||
|
}
|
||||||
|
public deregisterCharacterJoiner(joinerId: number): void {
|
||||||
|
this._core.deregisterCharacterJoiner(joinerId);
|
||||||
|
}
|
||||||
public addMarker(cursorYOffset: number): IMarker {
|
public addMarker(cursorYOffset: number): IMarker {
|
||||||
return this._core.addMarker(cursorYOffset);
|
return this._core.addMarker(cursorYOffset);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,10 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
|||||||
this._container.appendChild(this._canvas);
|
this._container.appendChild(this._canvas);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public dispose(): void {
|
||||||
|
this._container.removeChild(this._canvas);
|
||||||
|
}
|
||||||
|
|
||||||
private _initCanvas(): void {
|
private _initCanvas(): void {
|
||||||
this._ctx = this._canvas.getContext('2d', {alpha: this._alpha});
|
this._ctx = this._canvas.getContext('2d', {alpha: this._alpha});
|
||||||
// Draw the background if this is an opaque layer
|
// Draw the background if this is an opaque layer
|
||||||
@@ -224,12 +228,12 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Draws a character at a cell. If possible this will draw using the character
|
* Draws one or more characters at a cell. If possible this will draw using
|
||||||
* atlas to reduce draw time.
|
* the character atlas to reduce draw time.
|
||||||
* @param terminal The terminal.
|
* @param terminal The terminal.
|
||||||
* @param char The character.
|
* @param chars The character or characters.
|
||||||
* @param code The character code.
|
* @param code The character code.
|
||||||
* @param width The width of the character.
|
* @param width The width of the characters.
|
||||||
* @param x The column to draw at.
|
* @param x The column to draw at.
|
||||||
* @param y The row to draw at.
|
* @param y The row to draw at.
|
||||||
* @param fg The foreground color, in the format stored within the attributes.
|
* @param fg The foreground color, in the format stored within the attributes.
|
||||||
@@ -237,33 +241,34 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
|||||||
* This is used to validate whether a cached image can be used.
|
* This is used to validate whether a cached image can be used.
|
||||||
* @param bold Whether the text is bold.
|
* @param bold Whether the text is bold.
|
||||||
*/
|
*/
|
||||||
protected drawChar(terminal: ITerminal, char: string, code: number, width: number, x: number, y: number, fg: number, bg: number, bold: boolean, dim: boolean, italic: boolean): void {
|
protected drawChars(terminal: ITerminal, chars: string, code: number, width: number, x: number, y: number, fg: number, bg: number, bold: boolean, dim: boolean, italic: boolean): void {
|
||||||
const drawInBrightColor = terminal.options.drawBoldTextInBrightColors && bold && fg < 8;
|
const drawInBrightColor = terminal.options.drawBoldTextInBrightColors && bold && fg < 8 && fg !== INVERTED_DEFAULT_COLOR;
|
||||||
|
|
||||||
fg += drawInBrightColor ? 8 : 0;
|
fg += drawInBrightColor ? 8 : 0;
|
||||||
const atlasDidDraw = this._charAtlas && this._charAtlas.draw(
|
const atlasDidDraw = this._charAtlas && this._charAtlas.draw(
|
||||||
this._ctx,
|
this._ctx,
|
||||||
{char, code, bg, fg, bold: bold && terminal.options.enableBold, dim, italic},
|
{chars, code, bg, fg, bold: bold && terminal.options.enableBold, dim, italic},
|
||||||
x * this._scaledCellWidth + this._scaledCharLeft,
|
x * this._scaledCellWidth + this._scaledCharLeft,
|
||||||
y * this._scaledCellHeight + this._scaledCharTop
|
y * this._scaledCellHeight + this._scaledCharTop
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!atlasDidDraw) {
|
if (!atlasDidDraw) {
|
||||||
this._drawUncachedChar(terminal, char, width, fg, x, y, bold && terminal.options.enableBold, dim, italic);
|
this._drawUncachedChars(terminal, chars, width, fg, x, y, bold && terminal.options.enableBold, dim, italic);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Draws a character at a cell. The character will be clipped to
|
* Draws one or more characters at one or more cells. The character(s) will be
|
||||||
* ensure that it fits with the cell, including the cell to the right if it's
|
* clipped to ensure that they fit with the cell(s), including the cell to the
|
||||||
* a wide character.
|
* right if the last character is a wide character.
|
||||||
* @param terminal The terminal.
|
* @param terminal The terminal.
|
||||||
* @param char The character.
|
* @param chars The character.
|
||||||
* @param width The width of the character.
|
* @param width The width of the character.
|
||||||
* @param fg The foreground color, in the format stored within the attributes.
|
* @param fg The foreground color, in the format stored within the attributes.
|
||||||
* @param x The column to draw at.
|
* @param x The column to draw at.
|
||||||
* @param y The row to draw at.
|
* @param y The row to draw at.
|
||||||
*/
|
*/
|
||||||
private _drawUncachedChar(terminal: ITerminal, char: string, width: number, fg: number, x: number, y: number, bold: boolean, dim: boolean, italic: boolean): void {
|
private _drawUncachedChars(terminal: ITerminal, chars: string, width: number, fg: number, x: number, y: number, bold: boolean, dim: boolean, italic: boolean): void {
|
||||||
this._ctx.save();
|
this._ctx.save();
|
||||||
this._ctx.font = this._getFont(terminal, bold, italic);
|
this._ctx.font = this._getFont(terminal, bold, italic);
|
||||||
this._ctx.textBaseline = 'top';
|
this._ctx.textBaseline = 'top';
|
||||||
@@ -285,7 +290,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
|||||||
}
|
}
|
||||||
// Draw the character
|
// Draw the character
|
||||||
this._ctx.fillText(
|
this._ctx.fillText(
|
||||||
char,
|
chars,
|
||||||
x * this._scaledCellWidth + this._scaledCharLeft,
|
x * this._scaledCellWidth + this._scaledCharLeft,
|
||||||
y * this._scaledCellHeight + this._scaledCharTop);
|
y * this._scaledCellHeight + this._scaledCharTop);
|
||||||
this._ctx.restore();
|
this._ctx.restore();
|
||||||
|
|||||||
@@ -0,0 +1,278 @@
|
|||||||
|
import { assert } from 'chai';
|
||||||
|
|
||||||
|
import { LineData, CharData } from '../Types';
|
||||||
|
import { MockTerminal, MockBuffer } from '../utils/TestUtils.test';
|
||||||
|
import { CircularList } from '../common/CircularList';
|
||||||
|
|
||||||
|
import { ICharacterJoinerRegistry } from './Types';
|
||||||
|
import { CharacterJoinerRegistry } from './CharacterJoinerRegistry';
|
||||||
|
|
||||||
|
describe('CharacterJoinerRegistry', () => {
|
||||||
|
let registry: ICharacterJoinerRegistry;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const terminal = new MockTerminal();
|
||||||
|
terminal.cols = 16;
|
||||||
|
terminal.buffer = new MockBuffer();
|
||||||
|
const lines = new CircularList<LineData>(7);
|
||||||
|
lines.set(0, lineData('a -> b -> c -> d'));
|
||||||
|
lines.set(1, lineData('a -> b => c -> d'));
|
||||||
|
lines.set(2, [...lineData('a -> b -', 0xFFFFFFFF), ...lineData('> c -> d', 0)]);
|
||||||
|
lines.set(3, lineData('no joined ranges'));
|
||||||
|
lines.set(4, []);
|
||||||
|
lines.set(5, [...lineData('a', 0x11111111), ...lineData(' -> b -> c -> '), ...lineData('d', 0x22222222)]);
|
||||||
|
lines.set(6, [
|
||||||
|
...lineData('wi'),
|
||||||
|
[0, '¥', 2, '¥'.charCodeAt(0)],
|
||||||
|
[0, '', 0, null],
|
||||||
|
...lineData('deemo'),
|
||||||
|
[0, '\xf0\x9f\x98\x81', 1, 128513],
|
||||||
|
[0, ' ', 1, ' '.charCodeAt(0)],
|
||||||
|
...lineData('jiabc')
|
||||||
|
]);
|
||||||
|
(<MockBuffer>terminal.buffer).setLines(lines);
|
||||||
|
terminal.buffer.ydisp = 0;
|
||||||
|
registry = new CharacterJoinerRegistry(terminal);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has no joiners upon creation', () => {
|
||||||
|
assert.deepEqual(registry.getJoinedCharacters(0), []);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns ranges matched by the registered joiners', () => {
|
||||||
|
registry.registerCharacterJoiner(substringJoiner('->'));
|
||||||
|
assert.deepEqual(
|
||||||
|
registry.getJoinedCharacters(0),
|
||||||
|
[[2, 4], [7, 9], [12, 14]]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('processes the input using all provided joiners', () => {
|
||||||
|
registry.registerCharacterJoiner(substringJoiner('->'));
|
||||||
|
assert.deepEqual(
|
||||||
|
registry.getJoinedCharacters(1),
|
||||||
|
[[2, 4], [12, 14]]
|
||||||
|
);
|
||||||
|
|
||||||
|
registry.registerCharacterJoiner(substringJoiner('=>'));
|
||||||
|
assert.deepEqual(
|
||||||
|
registry.getJoinedCharacters(1),
|
||||||
|
[[2, 4], [7, 9], [12, 14]]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes deregistered joiners from future calls', () => {
|
||||||
|
const joiner1 = registry.registerCharacterJoiner(substringJoiner('->'));
|
||||||
|
const joiner2 = registry.registerCharacterJoiner(substringJoiner('=>'));
|
||||||
|
assert.deepEqual(
|
||||||
|
registry.getJoinedCharacters(1),
|
||||||
|
[[2, 4], [7, 9], [12, 14]]
|
||||||
|
);
|
||||||
|
|
||||||
|
registry.deregisterCharacterJoiner(joiner1);
|
||||||
|
assert.deepEqual(
|
||||||
|
registry.getJoinedCharacters(1),
|
||||||
|
[[7, 9]]
|
||||||
|
);
|
||||||
|
|
||||||
|
registry.deregisterCharacterJoiner(joiner2);
|
||||||
|
assert.deepEqual(
|
||||||
|
registry.getJoinedCharacters(1),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('doesn\'t process joins on differently-styled characters', () => {
|
||||||
|
registry.registerCharacterJoiner(substringJoiner('->'));
|
||||||
|
assert.deepEqual(
|
||||||
|
registry.getJoinedCharacters(2),
|
||||||
|
[[2, 4], [12, 14]]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an empty list of ranges if there is nothing to be joined', () => {
|
||||||
|
registry.registerCharacterJoiner(substringJoiner('->'));
|
||||||
|
assert.deepEqual(
|
||||||
|
registry.getJoinedCharacters(3),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an empty list of ranges if the line is empty', () => {
|
||||||
|
registry.registerCharacterJoiner(substringJoiner('->'));
|
||||||
|
assert.deepEqual(
|
||||||
|
registry.getJoinedCharacters(4),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false when trying to deregister a joiner that does not exist', () => {
|
||||||
|
registry.registerCharacterJoiner(substringJoiner('->'));
|
||||||
|
assert.deepEqual(registry.deregisterCharacterJoiner(123), false);
|
||||||
|
assert.deepEqual(
|
||||||
|
registry.getJoinedCharacters(0),
|
||||||
|
[[2, 4], [7, 9], [12, 14]]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('doesn\'t process same-styled ranges that only have one character', () => {
|
||||||
|
registry.registerCharacterJoiner(substringJoiner('a'));
|
||||||
|
registry.registerCharacterJoiner(substringJoiner('b'));
|
||||||
|
registry.registerCharacterJoiner(substringJoiner('d'));
|
||||||
|
assert.deepEqual(
|
||||||
|
registry.getJoinedCharacters(5),
|
||||||
|
[[5, 6]]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles ranges that extend all the way to the end of the line', () => {
|
||||||
|
registry.registerCharacterJoiner(substringJoiner('-> d'));
|
||||||
|
assert.deepEqual(
|
||||||
|
registry.getJoinedCharacters(2),
|
||||||
|
[[12, 16]]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles adjacent ranges', () => {
|
||||||
|
registry.registerCharacterJoiner(substringJoiner('->'));
|
||||||
|
registry.registerCharacterJoiner(substringJoiner('> c '));
|
||||||
|
assert.deepEqual(
|
||||||
|
registry.getJoinedCharacters(2),
|
||||||
|
[[2, 4], [8, 12], [12, 14]]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles fullwidth characters in the middle of ranges', () => {
|
||||||
|
registry.registerCharacterJoiner(substringJoiner('wi¥de'));
|
||||||
|
assert.deepEqual(
|
||||||
|
registry.getJoinedCharacters(6),
|
||||||
|
[[0, 6]]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles fullwidth characters at the end of ranges', () => {
|
||||||
|
registry.registerCharacterJoiner(substringJoiner('wi¥'));
|
||||||
|
assert.deepEqual(
|
||||||
|
registry.getJoinedCharacters(6),
|
||||||
|
[[0, 4]]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles emojis in the middle of ranges', () => {
|
||||||
|
registry.registerCharacterJoiner(substringJoiner('emo\xf0\x9f\x98\x81 ji'));
|
||||||
|
assert.deepEqual(
|
||||||
|
registry.getJoinedCharacters(6),
|
||||||
|
[[6, 13]]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles emojis at the end of ranges', () => {
|
||||||
|
registry.registerCharacterJoiner(substringJoiner('emo\xf0\x9f\x98\x81 '));
|
||||||
|
assert.deepEqual(
|
||||||
|
registry.getJoinedCharacters(6),
|
||||||
|
[[6, 11]]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles ranges after wide and emoji characters', () => {
|
||||||
|
registry.registerCharacterJoiner(substringJoiner('abc'));
|
||||||
|
assert.deepEqual(
|
||||||
|
registry.getJoinedCharacters(6),
|
||||||
|
[[13, 16]]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('range merging', () => {
|
||||||
|
it('inserts a new range before the existing ones', () => {
|
||||||
|
registry.registerCharacterJoiner(() => [[1, 2], [2, 3]]);
|
||||||
|
registry.registerCharacterJoiner(() => [[0, 1]]);
|
||||||
|
assert.deepEqual(
|
||||||
|
registry.getJoinedCharacters(0),
|
||||||
|
[[0, 1], [1, 2], [2, 3]]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('inserts in between two ranges', () => {
|
||||||
|
registry.registerCharacterJoiner(() => [[0, 2], [4, 6]]);
|
||||||
|
registry.registerCharacterJoiner(() => [[2, 4]]);
|
||||||
|
assert.deepEqual(
|
||||||
|
registry.getJoinedCharacters(0),
|
||||||
|
[[0, 2], [2, 4], [4, 6]]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('inserts after the last range', () => {
|
||||||
|
registry.registerCharacterJoiner(() => [[0, 2], [4, 6]]);
|
||||||
|
registry.registerCharacterJoiner(() => [[6, 8]]);
|
||||||
|
assert.deepEqual(
|
||||||
|
registry.getJoinedCharacters(0),
|
||||||
|
[[0, 2], [4, 6], [6, 8]]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('extends the beginning of a range', () => {
|
||||||
|
registry.registerCharacterJoiner(() => [[0, 2], [4, 6]]);
|
||||||
|
registry.registerCharacterJoiner(() => [[3, 5]]);
|
||||||
|
assert.deepEqual(
|
||||||
|
registry.getJoinedCharacters(0),
|
||||||
|
[[0, 2], [3, 6]]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('extends the end of a range', () => {
|
||||||
|
registry.registerCharacterJoiner(() => [[0, 2], [4, 6]]);
|
||||||
|
registry.registerCharacterJoiner(() => [[1, 4]]);
|
||||||
|
assert.deepEqual(
|
||||||
|
registry.getJoinedCharacters(0),
|
||||||
|
[[0, 4], [4, 6]]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('extends the last range', () => {
|
||||||
|
registry.registerCharacterJoiner(() => [[0, 2], [4, 6]]);
|
||||||
|
registry.registerCharacterJoiner(() => [[5, 7]]);
|
||||||
|
assert.deepEqual(
|
||||||
|
registry.getJoinedCharacters(0),
|
||||||
|
[[0, 2], [4, 7]]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('connects two ranges', () => {
|
||||||
|
registry.registerCharacterJoiner(() => [[0, 2], [4, 6]]);
|
||||||
|
registry.registerCharacterJoiner(() => [[1, 5]]);
|
||||||
|
assert.deepEqual(
|
||||||
|
registry.getJoinedCharacters(0),
|
||||||
|
[[0, 6]]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('connects more than two ranges', () => {
|
||||||
|
registry.registerCharacterJoiner(() => [[0, 2], [4, 6], [8, 10], [12, 14]]);
|
||||||
|
registry.registerCharacterJoiner(() => [[1, 10]]);
|
||||||
|
assert.deepEqual(
|
||||||
|
registry.getJoinedCharacters(0),
|
||||||
|
[[0, 10], [12, 14]]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function lineData(line: string, attr: number = 0): LineData {
|
||||||
|
return line.split('').map<CharData>(char => [attr, char, 1, char.charCodeAt(0)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function substringJoiner(substring: string): (sequence: string) => [number, number][] {
|
||||||
|
return (sequence: string): [number, number][] => {
|
||||||
|
const ranges: [number, number][] = [];
|
||||||
|
let searchIndex = 0;
|
||||||
|
let matchIndex = -1;
|
||||||
|
|
||||||
|
while ((matchIndex = sequence.indexOf(substring, searchIndex)) !== -1) {
|
||||||
|
const matchEndIndex = matchIndex + substring.length;
|
||||||
|
searchIndex = matchEndIndex;
|
||||||
|
ranges.push([matchIndex, matchEndIndex]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ranges;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,276 @@
|
|||||||
|
import { CHAR_DATA_ATTR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer';
|
||||||
|
import { ITerminal, LineData } from '../Types';
|
||||||
|
import { ICharacterJoinerRegistry, ICharacterJoiner } from './Types';
|
||||||
|
|
||||||
|
export class CharacterJoinerRegistry implements ICharacterJoinerRegistry {
|
||||||
|
|
||||||
|
private _characterJoiners: ICharacterJoiner[] = [];
|
||||||
|
private _nextCharacterJoinerId: number = 0;
|
||||||
|
|
||||||
|
constructor(private _terminal: ITerminal) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public registerCharacterJoiner(handler: (text: string) => [number, number][]): number {
|
||||||
|
const joiner: ICharacterJoiner = {
|
||||||
|
id: this._nextCharacterJoinerId++,
|
||||||
|
handler
|
||||||
|
};
|
||||||
|
|
||||||
|
this._characterJoiners.push(joiner);
|
||||||
|
return joiner.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public deregisterCharacterJoiner(joinerId: number): boolean {
|
||||||
|
for (let i = 0; i < this._characterJoiners.length; i++) {
|
||||||
|
if (this._characterJoiners[i].id === joinerId) {
|
||||||
|
this._characterJoiners.splice(i, 1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getJoinedCharacters(row: number): [number, number][] {
|
||||||
|
if (this._characterJoiners.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const line = this._terminal.buffer.lines.get(row);
|
||||||
|
if (line.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const ranges: [number, number][] = [];
|
||||||
|
const lineStr = this._terminal.buffer.translateBufferLineToString(row, true);
|
||||||
|
|
||||||
|
// Because some cells can be represented by multiple javascript characters,
|
||||||
|
// we track the cell and the string indexes separately. This allows us to
|
||||||
|
// translate the string ranges we get from the joiners back into cell ranges
|
||||||
|
// for use when rendering
|
||||||
|
let rangeStartColumn = 0;
|
||||||
|
let currentStringIndex = 0;
|
||||||
|
let rangeStartStringIndex = 0;
|
||||||
|
let rangeAttr = line[0][CHAR_DATA_ATTR_INDEX] >> 9;
|
||||||
|
|
||||||
|
for (let x = 0; x < this._terminal.cols; x++) {
|
||||||
|
const charData = line[x];
|
||||||
|
const chars = charData[CHAR_DATA_CHAR_INDEX];
|
||||||
|
const width = charData[CHAR_DATA_WIDTH_INDEX];
|
||||||
|
const attr = charData[CHAR_DATA_ATTR_INDEX] >> 9;
|
||||||
|
|
||||||
|
if (width === 0) {
|
||||||
|
// If this character is of width 0, skip it.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// End of range
|
||||||
|
if (attr !== rangeAttr) {
|
||||||
|
// If we ended up with a sequence of more than one character,
|
||||||
|
// look for ranges to join.
|
||||||
|
if (x - rangeStartColumn > 1) {
|
||||||
|
const joinedRanges = this._getJoinedRanges(
|
||||||
|
lineStr,
|
||||||
|
rangeStartStringIndex,
|
||||||
|
currentStringIndex,
|
||||||
|
line,
|
||||||
|
rangeStartColumn
|
||||||
|
);
|
||||||
|
for (let i = 0; i < joinedRanges.length; i++) {
|
||||||
|
ranges.push(joinedRanges[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset our markers for a new range.
|
||||||
|
rangeStartColumn = x;
|
||||||
|
rangeStartStringIndex = currentStringIndex;
|
||||||
|
rangeAttr = attr;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentStringIndex += chars.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process any trailing ranges.
|
||||||
|
if (this._terminal.cols - rangeStartColumn > 1) {
|
||||||
|
const joinedRanges = this._getJoinedRanges(
|
||||||
|
lineStr,
|
||||||
|
rangeStartStringIndex,
|
||||||
|
currentStringIndex,
|
||||||
|
line,
|
||||||
|
rangeStartColumn
|
||||||
|
);
|
||||||
|
for (let i = 0; i < joinedRanges.length; i++) {
|
||||||
|
ranges.push(joinedRanges[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ranges;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a segment of a line of text, find all ranges of text that should be
|
||||||
|
* joined in a single rendering unit. Ranges are internally converted to
|
||||||
|
* column ranges, rather than string ranges.
|
||||||
|
* @param line String representation of the full line of text
|
||||||
|
* @param startIndex Start position of the range to search in the string (inclusive)
|
||||||
|
* @param endIndex End position of the range to search in the string (exclusive)
|
||||||
|
*/
|
||||||
|
private _getJoinedRanges(line: string, startIndex: number, endIndex: number, lineData: LineData, startCol: number): [number, number][] {
|
||||||
|
const text = line.substring(startIndex, endIndex);
|
||||||
|
// At this point we already know that there is at least one joiner so
|
||||||
|
// we can just pull its value and assign it directly rather than
|
||||||
|
// merging it into an empty array, which incurs unnecessary writes.
|
||||||
|
const joinedRanges: [number, number][] = this._characterJoiners[0].handler(text);
|
||||||
|
for (let i = 1; i < this._characterJoiners.length; i++) {
|
||||||
|
// We merge any overlapping ranges across the different joiners
|
||||||
|
const joinerRanges = this._characterJoiners[i].handler(text);
|
||||||
|
for (let j = 0; j < joinerRanges.length; j++) {
|
||||||
|
CharacterJoinerRegistry._mergeRanges(joinedRanges, joinerRanges[j]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._stringRangesToCellRanges(joinedRanges, lineData, startCol);
|
||||||
|
return joinedRanges;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modifies the provided ranges in-place to adjust for variations between
|
||||||
|
* string length and cell width so that the range represents a cell range,
|
||||||
|
* rather than the string range the joiner provides.
|
||||||
|
* @param ranges String ranges containing start (inclusive) and end (exclusive) index
|
||||||
|
* @param line Cell data for the relevant line in the terminal
|
||||||
|
* @param startCol Offset within the line to start from
|
||||||
|
*/
|
||||||
|
private _stringRangesToCellRanges(ranges: [number, number][], line: LineData, startCol: number): void {
|
||||||
|
let currentRangeIndex = 0;
|
||||||
|
let currentRangeStarted = false;
|
||||||
|
let currentStringIndex = 0;
|
||||||
|
let currentRange = ranges[currentRangeIndex];
|
||||||
|
|
||||||
|
// If we got through all of the ranges, stop searching
|
||||||
|
if (!currentRange) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let x = startCol; x < this._terminal.cols; x++) {
|
||||||
|
const charData = line[x];
|
||||||
|
const width = charData[CHAR_DATA_WIDTH_INDEX];
|
||||||
|
const length = charData[CHAR_DATA_CHAR_INDEX].length;
|
||||||
|
|
||||||
|
// We skip zero-width characters when creating the string to join the text
|
||||||
|
// so we do the same here
|
||||||
|
if (width === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust the start of the range
|
||||||
|
if (!currentRangeStarted && currentRange[0] <= currentStringIndex) {
|
||||||
|
currentRange[0] = x;
|
||||||
|
currentRangeStarted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust the end of the range
|
||||||
|
if (currentRange[1] <= currentStringIndex) {
|
||||||
|
currentRange[1] = x;
|
||||||
|
|
||||||
|
// We're finished with this range, so we move to the next one
|
||||||
|
currentRange = ranges[++currentRangeIndex];
|
||||||
|
|
||||||
|
// If there are no more ranges left, stop searching
|
||||||
|
if (!currentRange) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ranges can be on adjacent characters. Because the end index of the
|
||||||
|
// ranges are exclusive, this means that the index for the start of a
|
||||||
|
// range can be the same as the end index of the previous range. To
|
||||||
|
// account for the start of the next range, we check here just in case.
|
||||||
|
if (currentRange[0] <= currentStringIndex) {
|
||||||
|
currentRange[0] = x;
|
||||||
|
currentRangeStarted = true;
|
||||||
|
} else {
|
||||||
|
currentRangeStarted = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust the string index based on the character length to line up with
|
||||||
|
// the column adjustment
|
||||||
|
currentStringIndex += length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there is still a range left at the end, it must extend all the way to
|
||||||
|
// the end of the line.
|
||||||
|
if (currentRange) {
|
||||||
|
currentRange[1] = this._terminal.cols;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merges the range defined by the provided start and end into the list of
|
||||||
|
* existing ranges. The merge is done in place on the existing range for
|
||||||
|
* performance and is also returned.
|
||||||
|
* @param ranges Existing range list
|
||||||
|
* @param newRange Tuple of two numbers representing the new range to merge in.
|
||||||
|
* @returns The ranges input with the new range merged in place
|
||||||
|
*/
|
||||||
|
private static _mergeRanges(ranges: [number, number][], newRange: [number, number]): [number, number][] {
|
||||||
|
let inRange = false;
|
||||||
|
for (let i = 0; i < ranges.length; i++) {
|
||||||
|
const range = ranges[i];
|
||||||
|
if (!inRange) {
|
||||||
|
if (newRange[1] <= range[0]) {
|
||||||
|
// Case 1: New range is before the search range
|
||||||
|
ranges.splice(i, 0, newRange);
|
||||||
|
return ranges;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newRange[1] <= range[1]) {
|
||||||
|
// Case 2: New range is either wholly contained within the
|
||||||
|
// search range or overlaps with the front of it
|
||||||
|
range[0] = Math.min(newRange[0], range[0]);
|
||||||
|
return ranges;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newRange[0] < range[1]) {
|
||||||
|
// Case 3: New range either wholly contains the search range
|
||||||
|
// or overlaps with the end of it
|
||||||
|
range[0] = Math.min(newRange[0], range[0]);
|
||||||
|
inRange = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case 4: New range starts after the search range
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
if (newRange[1] <= range[0]) {
|
||||||
|
// Case 5: New range extends from previous range but doesn't
|
||||||
|
// reach the current one
|
||||||
|
ranges[i - 1][1] = newRange[1];
|
||||||
|
return ranges;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newRange[1] <= range[1]) {
|
||||||
|
// Case 6: New range extends from prvious range into the
|
||||||
|
// current range
|
||||||
|
ranges[i - 1][1] = Math.max(newRange[1], range[1]);
|
||||||
|
ranges.splice(i, 1);
|
||||||
|
inRange = false;
|
||||||
|
return ranges;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case 7: New range extends from previous range past the
|
||||||
|
// end of the current range
|
||||||
|
ranges.splice(i, 1);
|
||||||
|
i--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inRange) {
|
||||||
|
// Case 8: New range extends past the last existing range
|
||||||
|
ranges[ranges.length - 1][1] = newRange[1];
|
||||||
|
} else {
|
||||||
|
// Case 9: New range starts after the last existing range
|
||||||
|
ranges.push(newRange);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ranges;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,13 +7,14 @@ import { TextRenderLayer } from './TextRenderLayer';
|
|||||||
import { SelectionRenderLayer } from './SelectionRenderLayer';
|
import { SelectionRenderLayer } from './SelectionRenderLayer';
|
||||||
import { CursorRenderLayer } from './CursorRenderLayer';
|
import { CursorRenderLayer } from './CursorRenderLayer';
|
||||||
import { ColorManager } from './ColorManager';
|
import { ColorManager } from './ColorManager';
|
||||||
import { IRenderLayer, IColorSet, IRenderer, IRenderDimensions } from './Types';
|
import { IRenderLayer, IColorSet, IRenderer, IRenderDimensions, ICharacterJoinerRegistry } from './Types';
|
||||||
import { ITerminal } from '../Types';
|
import { ITerminal, CharacterJoinerHandler } from '../Types';
|
||||||
import { LinkRenderLayer } from './LinkRenderLayer';
|
import { LinkRenderLayer } from './LinkRenderLayer';
|
||||||
import { EventEmitter } from '../EventEmitter';
|
import { EventEmitter } from '../EventEmitter';
|
||||||
import { RenderDebouncer } from '../ui/RenderDebouncer';
|
import { RenderDebouncer } from '../ui/RenderDebouncer';
|
||||||
import { ScreenDprMonitor } from '../ui/ScreenDprMonitor';
|
import { ScreenDprMonitor } from '../ui/ScreenDprMonitor';
|
||||||
import { ITheme } from 'xterm';
|
import { ITheme } from 'xterm';
|
||||||
|
import { CharacterJoinerRegistry } from '../renderer/CharacterJoinerRegistry';
|
||||||
|
|
||||||
export class Renderer extends EventEmitter implements IRenderer {
|
export class Renderer extends EventEmitter implements IRenderer {
|
||||||
private _renderDebouncer: RenderDebouncer;
|
private _renderDebouncer: RenderDebouncer;
|
||||||
@@ -23,6 +24,7 @@ export class Renderer extends EventEmitter implements IRenderer {
|
|||||||
private _screenDprMonitor: ScreenDprMonitor;
|
private _screenDprMonitor: ScreenDprMonitor;
|
||||||
private _isPaused: boolean = false;
|
private _isPaused: boolean = false;
|
||||||
private _needsFullRefresh: boolean = false;
|
private _needsFullRefresh: boolean = false;
|
||||||
|
private _characterJoinerRegistry: ICharacterJoinerRegistry;
|
||||||
|
|
||||||
public colorManager: ColorManager;
|
public colorManager: ColorManager;
|
||||||
public dimensions: IRenderDimensions;
|
public dimensions: IRenderDimensions;
|
||||||
@@ -31,12 +33,13 @@ export class Renderer extends EventEmitter implements IRenderer {
|
|||||||
super();
|
super();
|
||||||
const allowTransparency = this._terminal.options.allowTransparency;
|
const allowTransparency = this._terminal.options.allowTransparency;
|
||||||
this.colorManager = new ColorManager(document, allowTransparency);
|
this.colorManager = new ColorManager(document, allowTransparency);
|
||||||
|
this._characterJoinerRegistry = new CharacterJoinerRegistry(_terminal);
|
||||||
if (theme) {
|
if (theme) {
|
||||||
this.colorManager.setTheme(theme);
|
this.colorManager.setTheme(theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._renderLayers = [
|
this._renderLayers = [
|
||||||
new TextRenderLayer(this._terminal.screenElement, 0, this.colorManager.colors, allowTransparency),
|
new TextRenderLayer(this._terminal.screenElement, 0, this.colorManager.colors, this._characterJoinerRegistry, allowTransparency),
|
||||||
new SelectionRenderLayer(this._terminal.screenElement, 1, this.colorManager.colors),
|
new SelectionRenderLayer(this._terminal.screenElement, 1, this.colorManager.colors),
|
||||||
new LinkRenderLayer(this._terminal.screenElement, 2, this.colorManager.colors, this._terminal),
|
new LinkRenderLayer(this._terminal.screenElement, 2, this.colorManager.colors, this._terminal),
|
||||||
new CursorRenderLayer(this._terminal.screenElement, 3, this.colorManager.colors)
|
new CursorRenderLayer(this._terminal.screenElement, 3, this.colorManager.colors)
|
||||||
@@ -67,12 +70,17 @@ export class Renderer extends EventEmitter implements IRenderer {
|
|||||||
// Detect whether IntersectionObserver is detected and enable renderer pause
|
// Detect whether IntersectionObserver is detected and enable renderer pause
|
||||||
// and resume based on terminal visibility if so
|
// and resume based on terminal visibility if so
|
||||||
if ('IntersectionObserver' in window) {
|
if ('IntersectionObserver' in window) {
|
||||||
const observer = new IntersectionObserver(e => this.onIntersectionChange(e[0]), {threshold: 0});
|
const observer = new IntersectionObserver(e => this.onIntersectionChange(e[0]), { threshold: 0 });
|
||||||
observer.observe(this._terminal.element);
|
observer.observe(this._terminal.element);
|
||||||
this.register({ dispose: () => observer.disconnect() });
|
this.register({ dispose: () => observer.disconnect() });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public dispose(): void {
|
||||||
|
super.dispose();
|
||||||
|
this._renderLayers.forEach(l => l.dispose());
|
||||||
|
}
|
||||||
|
|
||||||
public onIntersectionChange(entry: IntersectionObserverEntry): void {
|
public onIntersectionChange(entry: IntersectionObserverEntry): void {
|
||||||
this._isPaused = entry.intersectionRatio === 0;
|
this._isPaused = entry.intersectionRatio === 0;
|
||||||
if (!this._isPaused && this._needsFullRefresh) {
|
if (!this._isPaused && this._needsFullRefresh) {
|
||||||
@@ -171,8 +179,8 @@ export class Renderer extends EventEmitter implements IRenderer {
|
|||||||
/**
|
/**
|
||||||
* Queues a refresh between two rows (inclusive), to be done on next animation
|
* Queues a refresh between two rows (inclusive), to be done on next animation
|
||||||
* frame.
|
* frame.
|
||||||
* @param {number} start The start row.
|
* @param start The start row.
|
||||||
* @param {number} end The end row.
|
* @param end The end row.
|
||||||
*/
|
*/
|
||||||
public refreshRows(start: number, end: number): void {
|
public refreshRows(start: number, end: number): void {
|
||||||
if (this._isPaused) {
|
if (this._isPaused) {
|
||||||
@@ -188,7 +196,7 @@ export class Renderer extends EventEmitter implements IRenderer {
|
|||||||
*/
|
*/
|
||||||
private _renderRows(start: number, end: number): void {
|
private _renderRows(start: number, end: number): void {
|
||||||
this._renderLayers.forEach(l => l.onGridChanged(this._terminal, start, end));
|
this._renderLayers.forEach(l => l.onGridChanged(this._terminal, start, end));
|
||||||
this._terminal.emit('refresh', {start, end});
|
this._terminal.emit('refresh', { start, end });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -250,4 +258,12 @@ export class Renderer extends EventEmitter implements IRenderer {
|
|||||||
this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._terminal.rows;
|
this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._terminal.rows;
|
||||||
this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._terminal.cols;
|
this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._terminal.cols;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public registerCharacterJoiner(handler: CharacterJoinerHandler): number {
|
||||||
|
return this._characterJoinerRegistry.registerCharacterJoiner(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
public deregisterCharacterJoiner(joinerId: number): boolean {
|
||||||
|
return this._characterJoinerRegistry.deregisterCharacterJoiner(joinerId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export class SelectionRenderLayer extends BaseRenderLayer {
|
|||||||
this._ctx.fillStyle = this._colors.selection.css;
|
this._ctx.fillStyle = this._colors.selection.css;
|
||||||
|
|
||||||
if (columnSelectMode) {
|
if (columnSelectMode) {
|
||||||
const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0;
|
const startCol = start[0];
|
||||||
const width = end[0] - startCol;
|
const width = end[0] - startCol;
|
||||||
const height = viewportCappedEndRow - viewportCappedStartRow + 1;
|
const height = viewportCappedEndRow - viewportCappedStartRow + 1;
|
||||||
this.fillCells(startCol, viewportCappedStartRow, width, height);
|
this.fillCells(startCol, viewportCappedStartRow, width, height);
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { CHAR_DATA_ATTR_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX } from '../Buffer';
|
import { CHAR_DATA_ATTR_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, NULL_CELL_CODE } from '../Buffer';
|
||||||
import { FLAGS, IColorSet, IRenderDimensions } from './Types';
|
import { FLAGS, IColorSet, IRenderDimensions, ICharacterJoinerRegistry } from './Types';
|
||||||
import { CharData, ITerminal } from '../Types';
|
import { CharData, ITerminal } from '../Types';
|
||||||
import { INVERTED_DEFAULT_COLOR } from './atlas/Types';
|
import { INVERTED_DEFAULT_COLOR } from './atlas/Types';
|
||||||
import { GridCache } from './GridCache';
|
import { GridCache } from './GridCache';
|
||||||
@@ -22,10 +22,12 @@ export class TextRenderLayer extends BaseRenderLayer {
|
|||||||
private _characterWidth: number;
|
private _characterWidth: number;
|
||||||
private _characterFont: string;
|
private _characterFont: string;
|
||||||
private _characterOverlapCache: { [key: string]: boolean } = {};
|
private _characterOverlapCache: { [key: string]: boolean } = {};
|
||||||
|
private _characterJoinerRegistry: ICharacterJoinerRegistry;
|
||||||
|
|
||||||
constructor(container: HTMLElement, zIndex: number, colors: IColorSet, alpha: boolean) {
|
constructor(container: HTMLElement, zIndex: number, colors: IColorSet, characterJoinerRegistry: ICharacterJoinerRegistry, alpha: boolean) {
|
||||||
super(container, 'text', zIndex, alpha, colors);
|
super(container, 'text', zIndex, alpha, colors);
|
||||||
this._state = new GridCache<CharData>();
|
this._state = new GridCache<CharData>();
|
||||||
|
this._characterJoinerRegistry = characterJoinerRegistry;
|
||||||
}
|
}
|
||||||
|
|
||||||
public resize(terminal: ITerminal, dim: IRenderDimensions): void {
|
public resize(terminal: ITerminal, dim: IRenderDimensions): void {
|
||||||
@@ -52,9 +54,10 @@ export class TextRenderLayer extends BaseRenderLayer {
|
|||||||
terminal: ITerminal,
|
terminal: ITerminal,
|
||||||
firstRow: number,
|
firstRow: number,
|
||||||
lastRow: number,
|
lastRow: number,
|
||||||
|
joinerRegistry: ICharacterJoinerRegistry | null,
|
||||||
callback: (
|
callback: (
|
||||||
code: number,
|
code: number,
|
||||||
char: string,
|
chars: string,
|
||||||
width: number,
|
width: number,
|
||||||
x: number,
|
x: number,
|
||||||
y: number,
|
y: number,
|
||||||
@@ -66,22 +69,54 @@ export class TextRenderLayer extends BaseRenderLayer {
|
|||||||
for (let y = firstRow; y <= lastRow; y++) {
|
for (let y = firstRow; y <= lastRow; y++) {
|
||||||
const row = y + terminal.buffer.ydisp;
|
const row = y + terminal.buffer.ydisp;
|
||||||
const line = terminal.buffer.lines.get(row);
|
const line = terminal.buffer.lines.get(row);
|
||||||
|
const joinedRanges = joinerRegistry ? joinerRegistry.getJoinedCharacters(row) : [];
|
||||||
for (let x = 0; x < terminal.cols; x++) {
|
for (let x = 0; x < terminal.cols; x++) {
|
||||||
const charData = line[x];
|
const charData = line[x];
|
||||||
const code: number = <number>charData[CHAR_DATA_CODE_INDEX];
|
let code: number = <number>charData[CHAR_DATA_CODE_INDEX];
|
||||||
const char: string = charData[CHAR_DATA_CHAR_INDEX];
|
|
||||||
|
// Can either represent character(s) for a single cell or multiple cells
|
||||||
|
// if indicated by a character joiner.
|
||||||
|
let chars: string = charData[CHAR_DATA_CHAR_INDEX];
|
||||||
const attr: number = charData[CHAR_DATA_ATTR_INDEX];
|
const attr: number = charData[CHAR_DATA_ATTR_INDEX];
|
||||||
let width: number = charData[CHAR_DATA_WIDTH_INDEX];
|
let width: number = charData[CHAR_DATA_WIDTH_INDEX];
|
||||||
|
|
||||||
|
// If true, indicates that the current character(s) to draw were joined.
|
||||||
|
let isJoined = false;
|
||||||
|
let lastCharX = x;
|
||||||
|
|
||||||
// The character to the left is a wide character, drawing is owned by
|
// The character to the left is a wide character, drawing is owned by
|
||||||
// the char at x-1
|
// the char at x-1
|
||||||
if (width === 0) {
|
if (width === 0) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the character is an overlapping char and the character to the right is a
|
// Process any joined character ranges as needed. Because of how the
|
||||||
// space, take ownership of the cell to the right.
|
// ranges are produced, we know that they are valid for the characters
|
||||||
if (this._isOverlapping(charData)) {
|
// and attributes of our input.
|
||||||
|
if (joinedRanges.length > 0 && x === joinedRanges[0][0]) {
|
||||||
|
isJoined = true;
|
||||||
|
const range = joinedRanges.shift();
|
||||||
|
|
||||||
|
// We already know the exact start and end column of the joined range,
|
||||||
|
// so we get the string and width representing it directly
|
||||||
|
chars = terminal.buffer.translateBufferLineToString(
|
||||||
|
row,
|
||||||
|
true,
|
||||||
|
range[0],
|
||||||
|
range[1]
|
||||||
|
);
|
||||||
|
width = range[1] - range[0];
|
||||||
|
code = Infinity;
|
||||||
|
|
||||||
|
// Skip over the cells occupied by this range in the loop
|
||||||
|
lastCharX = range[1] - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the character is an overlapping char and the character to the
|
||||||
|
// right is a space, take ownership of the cell to the right. We skip
|
||||||
|
// this check for joined characters because their rendering likely won't
|
||||||
|
// yield the same result as rendering the last character individually.
|
||||||
|
if (!isJoined && this._isOverlapping(charData)) {
|
||||||
// If the character is overlapping, we want to force a re-render on every
|
// If the character is overlapping, we want to force a re-render on every
|
||||||
// frame. This is specifically to work around the case where two
|
// frame. This is specifically to work around the case where two
|
||||||
// overlaping chars `a` and `b` are adjacent, the cursor is moved to b and a
|
// overlaping chars `a` and `b` are adjacent, the cursor is moved to b and a
|
||||||
@@ -89,7 +124,7 @@ export class TextRenderLayer extends BaseRenderLayer {
|
|||||||
// get removed, and `a` would not re-render because it thinks it's
|
// get removed, and `a` would not re-render because it thinks it's
|
||||||
// already in the correct state.
|
// already in the correct state.
|
||||||
// this._state.cache[x][y] = OVERLAP_OWNED_CHAR_DATA;
|
// this._state.cache[x][y] = OVERLAP_OWNED_CHAR_DATA;
|
||||||
if (x < line.length - 1 && line[x + 1][CHAR_DATA_CODE_INDEX] === 32 /*' '*/) {
|
if (lastCharX < line.length - 1 && line[lastCharX + 1][CHAR_DATA_CODE_INDEX] === NULL_CELL_CODE) {
|
||||||
width = 2;
|
width = 2;
|
||||||
// this._clearChar(x + 1, y);
|
// this._clearChar(x + 1, y);
|
||||||
// The overlapping char's char data will force a clear and render when the
|
// The overlapping char's char data will force a clear and render when the
|
||||||
@@ -116,7 +151,18 @@ export class TextRenderLayer extends BaseRenderLayer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
callback(code, char, width, x, y, fg, bg, flags);
|
callback(
|
||||||
|
code,
|
||||||
|
chars,
|
||||||
|
width,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
fg,
|
||||||
|
bg,
|
||||||
|
flags
|
||||||
|
);
|
||||||
|
|
||||||
|
x = lastCharX;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -134,7 +180,7 @@ export class TextRenderLayer extends BaseRenderLayer {
|
|||||||
|
|
||||||
ctx.save();
|
ctx.save();
|
||||||
|
|
||||||
this._forEachCell(terminal, firstRow, lastRow, (code, char, width, x, y, fg, bg, flags) => {
|
this._forEachCell(terminal, firstRow, lastRow, null, (code, chars, width, x, y, fg, bg, flags) => {
|
||||||
// libvte and xterm both draw the background (but not foreground) of invisible characters,
|
// libvte and xterm both draw the background (but not foreground) of invisible characters,
|
||||||
// so we should too.
|
// so we should too.
|
||||||
let nextFillStyle = null; // null represents default background color
|
let nextFillStyle = null; // null represents default background color
|
||||||
@@ -176,7 +222,7 @@ export class TextRenderLayer extends BaseRenderLayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _drawForeground(terminal: ITerminal, firstRow: number, lastRow: number): void {
|
private _drawForeground(terminal: ITerminal, firstRow: number, lastRow: number): void {
|
||||||
this._forEachCell(terminal, firstRow, lastRow, (code, char, width, x, y, fg, bg, flags) => {
|
this._forEachCell(terminal, firstRow, lastRow, this._characterJoinerRegistry, (code, chars, width, x, y, fg, bg, flags) => {
|
||||||
if (flags & FLAGS.INVISIBLE) {
|
if (flags & FLAGS.INVISIBLE) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -190,11 +236,11 @@ export class TextRenderLayer extends BaseRenderLayer {
|
|||||||
} else {
|
} else {
|
||||||
this._ctx.fillStyle = this._colors.foreground.css;
|
this._ctx.fillStyle = this._colors.foreground.css;
|
||||||
}
|
}
|
||||||
this.fillBottomLineAtCells(x, y);
|
this.fillBottomLineAtCells(x, y, width);
|
||||||
this._ctx.restore();
|
this._ctx.restore();
|
||||||
}
|
}
|
||||||
this.drawChar(
|
this.drawChars(
|
||||||
terminal, char, code,
|
terminal, chars, code,
|
||||||
width, x, y,
|
width, x, y,
|
||||||
fg, bg,
|
fg, bg,
|
||||||
!!(flags & FLAGS.BOLD), !!(flags & FLAGS.DIM), !!(flags & FLAGS.ITALIC)
|
!!(flags & FLAGS.BOLD), !!(flags & FLAGS.DIM), !!(flags & FLAGS.ITALIC)
|
||||||
|
|||||||
+25
-2
@@ -3,7 +3,7 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ITerminal } from '../Types';
|
import { ITerminal, CharacterJoinerHandler } from '../Types';
|
||||||
import { IEventEmitter, ITheme, IDisposable } from 'xterm';
|
import { IEventEmitter, ITheme, IDisposable } from 'xterm';
|
||||||
import { IColorSet } from '../shared/Types';
|
import { IColorSet } from '../shared/Types';
|
||||||
|
|
||||||
@@ -40,6 +40,8 @@ export interface IRenderer extends IEventEmitter, IDisposable {
|
|||||||
onOptionsChanged(): void;
|
onOptionsChanged(): void;
|
||||||
clear(): void;
|
clear(): void;
|
||||||
refreshRows(start: number, end: number): void;
|
refreshRows(start: number, end: number): void;
|
||||||
|
registerCharacterJoiner(handler: CharacterJoinerHandler): number;
|
||||||
|
deregisterCharacterJoiner(joinerId: number): boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IColorManager {
|
export interface IColorManager {
|
||||||
@@ -64,7 +66,7 @@ export interface IRenderDimensions {
|
|||||||
actualCellHeight: number;
|
actualCellHeight: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IRenderLayer {
|
export interface IRenderLayer extends IDisposable {
|
||||||
/**
|
/**
|
||||||
* Called when the terminal loses focus.
|
* Called when the terminal loses focus.
|
||||||
*/
|
*/
|
||||||
@@ -101,6 +103,16 @@ export interface IRenderLayer {
|
|||||||
*/
|
*/
|
||||||
onSelectionChanged(terminal: ITerminal, start: [number, number], end: [number, number], columnSelectMode: boolean): void;
|
onSelectionChanged(terminal: ITerminal, start: [number, number], end: [number, number], columnSelectMode: boolean): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a handler to join characters to render as a group
|
||||||
|
*/
|
||||||
|
registerCharacterJoiner?(joiner: ICharacterJoiner): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deregisters the specified character joiner handler
|
||||||
|
*/
|
||||||
|
deregisterCharacterJoiner?(joinerId: number): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resize the render layer.
|
* Resize the render layer.
|
||||||
*/
|
*/
|
||||||
@@ -111,3 +123,14 @@ export interface IRenderLayer {
|
|||||||
*/
|
*/
|
||||||
reset(terminal: ITerminal): void;
|
reset(terminal: ITerminal): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ICharacterJoiner {
|
||||||
|
id: number;
|
||||||
|
handler: CharacterJoinerHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ICharacterJoinerRegistry {
|
||||||
|
registerCharacterJoiner(handler: (text: string) => [number, number][]): number;
|
||||||
|
deregisterCharacterJoiner(joinerId: number): boolean;
|
||||||
|
getJoinedCharacters(row: number): [number, number][];
|
||||||
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ interface IGlyphCacheValue {
|
|||||||
|
|
||||||
function getGlyphCacheKey(glyph: IGlyphIdentifier): string {
|
function getGlyphCacheKey(glyph: IGlyphIdentifier): string {
|
||||||
const styleFlags = (glyph.bold ? 0 : 4) + (glyph.dim ? 0 : 2) + (glyph.italic ? 0 : 1);
|
const styleFlags = (glyph.bold ? 0 : 4) + (glyph.dim ? 0 : 2) + (glyph.italic ? 0 : 1);
|
||||||
return `${glyph.bg}_${glyph.fg}_${styleFlags}${glyph.char}`;
|
return `${glyph.bg}_${glyph.fg}_${styleFlags}${glyph.chars}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class DynamicCharAtlas extends BaseCharAtlas {
|
export default class DynamicCharAtlas extends BaseCharAtlas {
|
||||||
@@ -216,7 +216,7 @@ export default class DynamicCharAtlas extends BaseCharAtlas {
|
|||||||
this._tmpCtx.globalAlpha = DIM_OPACITY;
|
this._tmpCtx.globalAlpha = DIM_OPACITY;
|
||||||
}
|
}
|
||||||
// Draw the character
|
// Draw the character
|
||||||
this._tmpCtx.fillText(glyph.char, 0, 0);
|
this._tmpCtx.fillText(glyph.chars, 0, 0);
|
||||||
this._tmpCtx.restore();
|
this._tmpCtx.restore();
|
||||||
|
|
||||||
// clear the background from the character to avoid issues with drawing over the previous
|
// clear the background from the character to avoid issues with drawing over the previous
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export const INVERTED_DEFAULT_COLOR = -1;
|
|||||||
export const DIM_OPACITY = 0.5;
|
export const DIM_OPACITY = 0.5;
|
||||||
|
|
||||||
export interface IGlyphIdentifier {
|
export interface IGlyphIdentifier {
|
||||||
char: string;
|
chars: string;
|
||||||
code: number;
|
code: number;
|
||||||
bg: number;
|
bg: number;
|
||||||
fg: number;
|
fg: number;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { IRenderer, IRenderDimensions, IColorSet } from '../Types';
|
import { IRenderer, IRenderDimensions, IColorSet } from '../Types';
|
||||||
import { ITerminal } from '../../Types';
|
import { ITerminal, CharacterJoinerHandler } from '../../Types';
|
||||||
import { ITheme } from 'xterm';
|
import { ITheme } from 'xterm';
|
||||||
import { EventEmitter } from '../../EventEmitter';
|
import { EventEmitter } from '../../EventEmitter';
|
||||||
import { ColorManager } from '../ColorManager';
|
import { ColorManager } from '../ColorManager';
|
||||||
@@ -52,7 +52,7 @@ export class DomRenderer extends EventEmitter implements IRenderer {
|
|||||||
this._rowContainer.classList.add(ROW_CONTAINER_CLASS);
|
this._rowContainer.classList.add(ROW_CONTAINER_CLASS);
|
||||||
this._rowContainer.style.lineHeight = 'normal';
|
this._rowContainer.style.lineHeight = 'normal';
|
||||||
this._rowContainer.setAttribute('aria-hidden', 'true');
|
this._rowContainer.setAttribute('aria-hidden', 'true');
|
||||||
this._refreshRowElements(this._terminal.rows, this._terminal.cols);
|
this._refreshRowElements(this._terminal.cols, this._terminal.rows);
|
||||||
this._selectionContainer = document.createElement('div');
|
this._selectionContainer = document.createElement('div');
|
||||||
this._selectionContainer.classList.add(SELECTION_CLASS);
|
this._selectionContainer.classList.add(SELECTION_CLASS);
|
||||||
this._selectionContainer.setAttribute('aria-hidden', 'true');
|
this._selectionContainer.setAttribute('aria-hidden', 'true');
|
||||||
@@ -81,6 +81,15 @@ export class DomRenderer extends EventEmitter implements IRenderer {
|
|||||||
this._terminal.screenElement.appendChild(this._selectionContainer);
|
this._terminal.screenElement.appendChild(this._selectionContainer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public dispose(): void {
|
||||||
|
this._terminal.element.classList.remove(TERMINAL_CLASS_PREFIX + this._terminalClass);
|
||||||
|
this._terminal.screenElement.removeChild(this._rowContainer);
|
||||||
|
this._terminal.screenElement.removeChild(this._selectionContainer);
|
||||||
|
this._terminal.screenElement.removeChild(this._themeStyleElement);
|
||||||
|
this._terminal.screenElement.removeChild(this._dimensionsStyleElement);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
private _updateDimensions(): void {
|
private _updateDimensions(): void {
|
||||||
this.dimensions.scaledCharWidth = this._terminal.charMeasure.width * window.devicePixelRatio;
|
this.dimensions.scaledCharWidth = this._terminal.charMeasure.width * window.devicePixelRatio;
|
||||||
this.dimensions.scaledCharHeight = this._terminal.charMeasure.height * window.devicePixelRatio;
|
this.dimensions.scaledCharHeight = this._terminal.charMeasure.height * window.devicePixelRatio;
|
||||||
@@ -241,14 +250,14 @@ export class DomRenderer extends EventEmitter implements IRenderer {
|
|||||||
|
|
||||||
// Create the selections
|
// Create the selections
|
||||||
const documentFragment = document.createDocumentFragment();
|
const documentFragment = document.createDocumentFragment();
|
||||||
const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0;
|
|
||||||
|
|
||||||
if (columnSelectMode) {
|
if (columnSelectMode) {
|
||||||
documentFragment.appendChild(
|
documentFragment.appendChild(
|
||||||
this._createSelectionElement(viewportCappedStartRow, startCol, end[0], viewportCappedEndRow - viewportStartRow + 1)
|
this._createSelectionElement(viewportCappedStartRow, start[0], end[0], viewportCappedEndRow - viewportCappedStartRow + 1)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Draw first row
|
// Draw first row
|
||||||
|
const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0;
|
||||||
const endCol = viewportCappedStartRow === viewportCappedEndRow ? end[0] : this._terminal.cols;
|
const endCol = viewportCappedStartRow === viewportCappedEndRow ? end[0] : this._terminal.cols;
|
||||||
documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow, startCol, endCol));
|
documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow, startCol, endCol));
|
||||||
// Draw middle rows
|
// Draw middle rows
|
||||||
@@ -319,4 +328,7 @@ export class DomRenderer extends EventEmitter implements IRenderer {
|
|||||||
private get _terminalSelector(): string {
|
private get _terminalSelector(): string {
|
||||||
return `.${TERMINAL_CLASS_PREFIX}${this._terminalClass}`;
|
return `.${TERMINAL_CLASS_PREFIX}${this._terminalClass}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public registerCharacterJoiner(handler: CharacterJoinerHandler): number { return -1; }
|
||||||
|
public deregisterCharacterJoiner(joinerId: number): boolean { return false; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import jsdom = require('jsdom');
|
|||||||
import { assert } from 'chai';
|
import { assert } from 'chai';
|
||||||
import { DomRendererRowFactory } from './DomRendererRowFactory';
|
import { DomRendererRowFactory } from './DomRendererRowFactory';
|
||||||
import { LineData } from '../../Types';
|
import { LineData } from '../../Types';
|
||||||
import { DEFAULT_ATTR } from '../../Buffer';
|
import { DEFAULT_ATTR, NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from '../../Buffer';
|
||||||
import { FLAGS } from '../Types';
|
import { FLAGS } from '../Types';
|
||||||
|
|
||||||
describe('DomRendererRowFactory', () => {
|
describe('DomRendererRowFactory', () => {
|
||||||
@@ -149,7 +149,7 @@ describe('DomRendererRowFactory', () => {
|
|||||||
function createEmptyLineData(cols: number): LineData {
|
function createEmptyLineData(cols: number): LineData {
|
||||||
const lineData: LineData = [];
|
const lineData: LineData = [];
|
||||||
for (let i = 0; i < cols; i++) {
|
for (let i = 0; i < cols; i++) {
|
||||||
lineData.push([DEFAULT_ATTR, ' ', 1, 32 /* ' '.charCodeAt(0) */]);
|
lineData.push([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
|
||||||
}
|
}
|
||||||
return lineData;
|
return lineData;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ export const isLinux = platform.indexOf('Linux') >= 0;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Return if the given array contains the given element
|
* Return if the given array contains the given element
|
||||||
* @param {Array} array The array to search for the given element.
|
* @param arr The array to search for the given element.
|
||||||
* @param {Object} el The element to look for into the array
|
* @param el The element to look for into the array
|
||||||
*/
|
*/
|
||||||
function contains(arr: any[], el: any): boolean {
|
function contains(arr: any[], el: any): boolean {
|
||||||
return arr.indexOf(el) >= 0;
|
return arr.indexOf(el) >= 0;
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { IColorSet, IRenderer, IRenderDimensions, IColorManager } from '../renderer/Types';
|
import { IColorSet, IRenderer, IRenderDimensions, IColorManager } from '../renderer/Types';
|
||||||
import { LineData, IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, ICircularList, ILinkifier, IMouseHelper, ILinkMatcherOptions, XtermListener } from '../Types';
|
import { LineData, IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, ICircularList, ILinkifier, IMouseHelper, ILinkMatcherOptions, XtermListener, CharacterJoinerHandler } from '../Types';
|
||||||
import { Buffer } from '../Buffer';
|
import { Buffer, NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from '../Buffer';
|
||||||
import * as Browser from '../shared/utils/Browser';
|
import * as Browser from '../shared/utils/Browser';
|
||||||
import { ITheme, IDisposable, IMarker } from 'xterm';
|
import { ITheme, IDisposable, IMarker } from 'xterm';
|
||||||
|
|
||||||
@@ -84,9 +84,6 @@ export class MockTerminal implements ITerminal {
|
|||||||
write(data: string): void {
|
write(data: string): void {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
send(data: string): void {
|
|
||||||
throw new Error('Method not implemented.');
|
|
||||||
}
|
|
||||||
bracketedPasteMode: boolean;
|
bracketedPasteMode: boolean;
|
||||||
mouseHelper: IMouseHelper;
|
mouseHelper: IMouseHelper;
|
||||||
renderer: IRenderer;
|
renderer: IRenderer;
|
||||||
@@ -152,10 +149,12 @@ export class MockTerminal implements ITerminal {
|
|||||||
const line: LineData = [];
|
const line: LineData = [];
|
||||||
cols = cols || this.cols;
|
cols = cols || this.cols;
|
||||||
for (let i = 0; i < cols; i++) {
|
for (let i = 0; i < cols; i++) {
|
||||||
line.push([0, ' ', 1, 32]);
|
line.push([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
|
||||||
}
|
}
|
||||||
return line;
|
return line;
|
||||||
}
|
}
|
||||||
|
registerCharacterJoiner(handler: CharacterJoinerHandler): number { return 0; }
|
||||||
|
deregisterCharacterJoiner(joinerId: number): void { }
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MockCharMeasure implements ICharMeasure {
|
export class MockCharMeasure implements ICharMeasure {
|
||||||
@@ -238,9 +237,6 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal {
|
|||||||
is(term: string): boolean {
|
is(term: string): boolean {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
send(data: string): void {
|
|
||||||
throw new Error('Method not implemented.');
|
|
||||||
}
|
|
||||||
setgCharset(g: number, charset: { [key: string]: string; }): void {
|
setgCharset(g: number, charset: { [key: string]: string; }): void {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
@@ -283,6 +279,18 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal {
|
|||||||
tabSet(): void {
|
tabSet(): void {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
|
handler(data: string): void {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
handleTitle(title: string): void {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
index(): void {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
reverseIndex(): void {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MockBuffer implements IBuffer {
|
export class MockBuffer implements IBuffer {
|
||||||
@@ -344,6 +352,8 @@ export class MockRenderer implements IRenderer {
|
|||||||
onWindowResize(devicePixelRatio: number): void {}
|
onWindowResize(devicePixelRatio: number): void {}
|
||||||
clear(): void {}
|
clear(): void {}
|
||||||
refreshRows(start: number, end: number): void {}
|
refreshRows(start: number, end: number): void {}
|
||||||
|
registerCharacterJoiner(handler: CharacterJoinerHandler): number { return 0; }
|
||||||
|
deregisterCharacterJoiner(): boolean { return true; }
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MockViewport implements IViewport {
|
export class MockViewport implements IViewport {
|
||||||
|
|||||||
Vendored
+45
@@ -416,6 +416,13 @@ declare module 'xterm' {
|
|||||||
*/
|
*/
|
||||||
off(type: 'blur' | 'focus' | 'linefeed' | 'selection' | 'data' | 'key' | 'keypress' | 'keydown' | 'refresh' | 'resize' | 'scroll' | 'title' | string, listener: (...args: any[]) => void): void;
|
off(type: 'blur' | 'focus' | 'linefeed' | 'selection' | 'data' | 'key' | 'keypress' | 'keydown' | 'refresh' | 'resize' | 'scroll' | 'title' | string, listener: (...args: any[]) => void): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits an event on the terminal.
|
||||||
|
* @param type The type of event
|
||||||
|
* @param data data associated with the event.
|
||||||
|
* @deprecated This is being removed from the API with no replacement, see
|
||||||
|
* issue #1505.
|
||||||
|
*/
|
||||||
emit(type: string, data?: any): void;
|
emit(type: string, data?: any): void;
|
||||||
|
|
||||||
addDisposableListener(type: string, handler: (...args: any[]) => void): IDisposable;
|
addDisposableListener(type: string, handler: (...args: any[]) => void): IDisposable;
|
||||||
@@ -470,6 +477,44 @@ declare module 'xterm' {
|
|||||||
*/
|
*/
|
||||||
deregisterLinkMatcher(matcherId: number): void;
|
deregisterLinkMatcher(matcherId: number): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (EXPERIMENTAL) Registers a character joiner, allowing custom sequences of
|
||||||
|
* characters to be rendered as a single unit. This is useful in particular
|
||||||
|
* for rendering ligatures and graphemes, among other things.
|
||||||
|
*
|
||||||
|
* Each registered character joiner is called with a string of text
|
||||||
|
* representing a portion of a line in the terminal that can be rendered as
|
||||||
|
* a single unit. The joiner must return a sorted array, where each entry is
|
||||||
|
* itself an array of length two, containing the start (inclusive) and end
|
||||||
|
* (exclusive) index of a substring of the input that should be rendered as
|
||||||
|
* a single unit. When multiple joiners are provided, the results of each
|
||||||
|
* are collected. If there are any overlapping substrings between them, they
|
||||||
|
* are combined into one larger unit that is drawn together.
|
||||||
|
*
|
||||||
|
* All character joiners that are registered get called every time a line is
|
||||||
|
* rendered in the terminal, so it is essential for the handler function to
|
||||||
|
* run as quickly as possible to avoid slowdowns when rendering. Similarly,
|
||||||
|
* joiners should strive to return the smallest possible substrings to
|
||||||
|
* render together, since they aren't drawn as optimally as individual
|
||||||
|
* characters.
|
||||||
|
*
|
||||||
|
* NOTE: character joiners are only used by the canvas renderer.
|
||||||
|
*
|
||||||
|
* @param handler The function that determines character joins. It is called
|
||||||
|
* with a string of text that is eligible for joining and returns an array
|
||||||
|
* where each entry is an array containing the start (inclusive) and end
|
||||||
|
* (exclusive) indexes of ranges that should be rendered as a single unit.
|
||||||
|
* @return The ID of the new joiner, this can be used to deregister
|
||||||
|
*/
|
||||||
|
registerCharacterJoiner(handler: (text: string) => [number, number][]): number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (EXPERIMENTAL) Deregisters the character joiner if one was registered.
|
||||||
|
* NOTE: character joiners are only used by the canvas renderer.
|
||||||
|
* @param joinerId The character joiner's ID (returned after register)
|
||||||
|
*/
|
||||||
|
deregisterCharacterJoiner(joinerId: number): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* (EXPERIMENTAL) Adds a marker to the normal buffer and returns it. If the
|
* (EXPERIMENTAL) Adds a marker to the normal buffer and returns it. If the
|
||||||
* alt buffer is active, undefined is returned.
|
* alt buffer is active, undefined is returned.
|
||||||
|
|||||||
+10
-1
@@ -6,5 +6,14 @@ module.exports = {
|
|||||||
path: path.resolve(__dirname, 'demo/dist'),
|
path: path.resolve(__dirname, 'demo/dist'),
|
||||||
filename: 'bundle.js'
|
filename: 'bundle.js'
|
||||||
},
|
},
|
||||||
devtool: 'source-map'
|
devtool: 'source-map',
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.js$/,
|
||||||
|
use: ["source-map-loader"],
|
||||||
|
enforce: "pre"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user