diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index e5736562..141f7711 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,4 +1,4 @@ -FROM node:10 +FROM node:14 # Configure apt ENV DEBIAN_FRONTEND=noninteractive diff --git a/.eslintrc.json b/.eslintrc.json index 8c416f15..427c7a16 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -9,6 +9,7 @@ "project": [ "src/browser/tsconfig.json", "src/common/tsconfig.json", + "src/headless/tsconfig.json", "test/api/tsconfig.json", "test/benchmark/tsconfig.json", "addons/xterm-addon-attach/src/tsconfig.json", @@ -43,10 +44,11 @@ "@typescript-eslint/array-type": [ "warn", { - "default": "array-simple", + "default": "array", "readonly": "generic" } ], + "@typescript-eslint/consistent-type-assertions": "warn", "@typescript-eslint/consistent-type-definitions": "warn", "@typescript-eslint/explicit-function-return-type": [ "warn", @@ -151,6 +153,10 @@ "warn", "never" ], + "object-curly-spacing": [ + "warn", + "always" + ], "prefer-const": "warn", "spaced-comment": [ "warn", @@ -160,5 +166,13 @@ "exceptions": ["-"] } ] - } + }, + "overrides": [ + { + "files": ["**/*.test.ts"], + "rules": { + "object-curly-spacing": "off" + } + } + ] } diff --git a/.mocha.env b/.mocha.env new file mode 100644 index 00000000..78a9f930 --- /dev/null +++ b/.mocha.env @@ -0,0 +1 @@ +NODE_PATH=./out diff --git a/.mocharc.yml b/.mocharc.yml new file mode 100644 index 00000000..c19fe15a --- /dev/null +++ b/.mocharc.yml @@ -0,0 +1,11 @@ +require: + - source-map-support/register +spec: + - out/**/*.test.js + - addons/**/out/*.test.js +watch-files: + - out/**/*.js + - addons/**/out/*.js +reporter: spec +color: true +check-leaks: true diff --git a/.vscode/settings.json b/.vscode/settings.json index f07ca545..9fa94d5f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,5 @@ { "typescript.preferences.importModuleSpecifier": "non-relative", - "typescript.preferences.quoteStyle": "single" + "typescript.preferences.quoteStyle": "single", + "mochaExplorer.envPath": ".mocha.env" } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 2f752faf..4d268f61 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,51 +1,70 @@ { - "version": "2.0.0", - "presentation": { - "echo": false, - "reveal": "always", - "focus": false, - "panel": "dedicated", - "showReuseMessage": true - }, - "tasks": [ - { - "type": "npm", - "script": "test", - "group": "test", - "problemMatcher": [] - }, - { - "type": "npm", - "script": "watch", - "group": "build", - "isBackground": true, - "problemMatcher": [], - "presentation": { - "group": "vscode" - } - }, - { - "type": "npm", - "script": "start", - "group": "build", - "isBackground": true, - "problemMatcher": [], - "presentation": { - "group": "vscode" - } - }, - { - "label": "Start demo", - "dependsOn": ["npm: watch", "npm: start"], - "group": { - "kind": "build", - "isDefault": true - }, - "isBackground": true, - "problemMatcher": [], - "presentation": { - "group": "vscode" - } - } - ] + "version": "2.0.0", + "presentation": { + "echo": false, + "reveal": "always", + "focus": false, + "panel": "dedicated", + "showReuseMessage": true + }, + "tasks": [ + { + "type": "npm", + "script": "test", + "group": "test", + "problemMatcher": [] + }, + { + "type": "npm", + "script": "watch", + "group": "build", + "isBackground": true, + "problemMatcher": "$tsc-watch", + "presentation": { + "group": "vscode" + } + }, + { + "type": "npm", + "script": "start", + "dependsOn": "npm: watch", + "group": "build", + "isBackground": true, + "problemMatcher": [], + "presentation": { + "group": "vscode" + } + }, + { + "label": "Start demo", + "dependsOn": "npm: start", + "group": { + "kind": "build", + "isDefault": true + }, + "isBackground": true, + "problemMatcher": { + "owner": "typescript", + "fileLocation": [ + "relative", + "${workspaceFolder}" + ], + "pattern": [ + { + "regexp": "^([^\\\\s].*)\\\\((\\\\d+,\\\\d+)\\\\):\\\\s*(.*)$", + "file": 1, + "location": 2, + "message": 3 + } + ], + "background": { + "beginsPattern": "assets by", + "endsPattern": "webpack \\d+\\.\\d+\\.\\d+ compiled successfully" + } + }, + "presentation": { + "group": "vscode" + } + } + ] } diff --git a/README.md b/README.md index ce687da9..6b713d45 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,11 @@ Xterm.js is a front-end component written in TypeScript that lets applications b ## Features -- **Terminal apps just work**: Xterm.js works with most terminal apps such as `bash`, `vim` and `tmux`, this includes support for curses-based apps and mouse event support. +- **Terminal apps just work**: Xterm.js works with most terminal apps such as `bash`, `vim`, and `tmux`, including support for curses-based apps and mouse events. - **Performant**: Xterm.js is *really* fast, it even includes a GPU-accelerated renderer. -- **Rich unicode support**: Supports CJK, emojis and IMEs. +- **Rich Unicode support**: Supports CJK, emojis, and IMEs. - **Self-contained**: Requires zero dependencies to work. -- **Accessible**: Screen reader and minimum contrast ratio support can be turned on +- **Accessible**: Screen reader and minimum contrast ratio support can be turned on. - **And much more**: Links, theming, addons, well documented API, etc. ## What xterm.js is not @@ -20,13 +20,13 @@ Xterm.js is a front-end component written in TypeScript that lets applications b ## Getting Started -First you need to install the module, we ship exclusively through [npm](https://www.npmjs.com/) so you need that installed and then add xterm.js as a dependency by running: +First, you need to install the module, we ship exclusively through [npm](https://www.npmjs.com/), so you need that installed and then add xterm.js as a dependency by running: -``` +```bash npm install xterm ``` -To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to the head of your html page. Then create a `
` onto which xterm can attach itself. Finally instantiate the `Terminal` object and then call the `open` function with the DOM object of the `div`. +To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to the head of your HTML page. Then create a `
` onto which xterm can attach itself. Finally, instantiate the `Terminal` object and then call the `open` function with the DOM object of the `div`. ```html @@ -58,7 +58,7 @@ import { Terminal } from 'xterm'; ⚠️ *This section describes the new addon format introduced in v3.14.0, see [here](https://github.com/xtermjs/xterm.js/blob/3.14.2/README.md#addons) for the instructions on the old format* -Addons are separate modules that extend the `Terminal` by building on the [xterm.js API](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts). To use an addon you first need to install it in your project: +Addons are separate modules that extend the `Terminal` by building on the [xterm.js API](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts). To use an addon, you first need to install it in your project: ```bash npm i -S xterm-addon-web-links @@ -76,7 +76,7 @@ const terminal = new Terminal(); terminal.loadAddon(new WebLinksAddon()); ``` -The xterm.js team maintains the following addons but they can be built by anyone: +The xterm.js team maintains the following addons, but anyone can build them: - [`xterm-addon-attach`](https://github.com/xtermjs/xterm.js/tree/master/addons/xterm-addon-attach): Attaches to a server running a process via a websocket - [`xterm-addon-fit`](https://github.com/xtermjs/xterm.js/tree/master/addons/xterm-addon-fit): Fits the terminal to the containing element @@ -85,23 +85,27 @@ The xterm.js team maintains the following addons but they can be built by anyone ## Browser Support -Since xterm.js is typically implemented as a developer tool, only modern browsers are supported officially. Specifically the latest versions of *Chrome*, *Edge*, *Firefox* and *Safari*. +Since xterm.js is typically implemented as a developer tool, only modern browsers are supported officially. Specifically the latest versions of *Chrome*, *Edge*, *Firefox*, and *Safari*. We also partially support *Internet Explorer 11*, meaning xterm.js should work for the most part, but we reserve the right to not provide workarounds specifically for it unless it's absolutely necessary to get the basic input/output flow working. -Xterm.js works seamlessly in [Electron](https://electronjs.org/) apps and may even work on earlier versions of the browsers, these are the versions we strive to keep working. +Xterm.js works seamlessly in [Electron](https://electronjs.org/) apps and may even work on earlier versions of the browsers. These are the versions we strive to keep working. + +### Node.js Support + +We also publish [`xterm-headless`](https://www.npmjs.com/package/xterm-headless) which is a stripped down version of xterm.js that runs in Node.js. An example use case for this is to keep track of a terminal's state where the process is running and using the serialize addon so it can get all state restored upon reconnection. ## 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 to enable experimentation with new ideas without committing to support it like a normal [semver](https://semver.org/) API. Note that these APIs can change radically between versions so be sure to read release notes if you plan on using experimental APIs. +Note that some APIs are marked *experimental*, these are added to enable experimentation with new ideas without committing to support it like a normal [semver](https://semver.org/) API. Note that these APIs can change radically between versions, so be sure to read release notes if you plan on using experimental APIs. ## Real-world uses Xterm.js is used in several world-class applications to provide great terminal experiences. - [**SourceLair**](https://www.sourcelair.com/): In-browser IDE that provides its users with fully-featured Linux terminals based on xterm.js. -- [**Microsoft Visual Studio Code**](http://code.visualstudio.com/): Modern, versatile and powerful open source code editor that provides an integrated terminal based on xterm.js. +- [**Microsoft Visual Studio Code**](http://code.visualstudio.com/): Modern, versatile, and powerful open source code editor that provides an integrated terminal based on xterm.js. - [**ttyd**](https://github.com/tsl0922/ttyd): A command-line tool for sharing terminal over the web, with fully-featured terminal emulation based on xterm.js. - [**Katacoda**](https://www.katacoda.com/): Katacoda is an Interactive Learning Platform for software developers, covering the latest Cloud Native technologies. - [**Eclipse Che**](http://www.eclipse.org/che): Developer workspace server, cloud IDE, and Eclipse next-generation IDE. @@ -113,10 +117,10 @@ Xterm.js is used in several world-class applications to provide great terminal e - [**Next Tech**](https://next.tech "Next Tech"): Online platform for interactive coding and web development courses. Live container-backed terminal uses xterm.js. - [**RStudio**](https://www.rstudio.com/products/RStudio "RStudio"): RStudio is an integrated development environment (IDE) for R. - [**Terminal for Atom**](https://github.com/jsmecham/atom-terminal-tab): A simple terminal for the Atom text editor. -- [**Eclipse Orion**](https://orionhub.org): A modern, open source software development environment that runs in the cloud. Code, deploy and run in the cloud. +- [**Eclipse Orion**](https://orionhub.org): A modern, open source software development environment that runs in the cloud. Code, deploy, and run in the cloud. - [**Gravitational Teleport**](https://github.com/gravitational/teleport): Gravitational Teleport is a modern SSH server for remotely accessing clusters of Linux servers via SSH or HTTPS. - [**Hexlet**](https://en.hexlet.io): Practical programming courses (JavaScript, PHP, Unix, databases, functional programming). A steady path from the first line of code to the first job. -- [**Selenoid UI**](https://github.com/aerokube/selenoid-ui): Simple UI for the scallable golang implementation of Selenium Hub named Selenoid. We use XTerm for streaming logs over websockets from docker containers. +- [**Selenoid UI**](https://github.com/aerokube/selenoid-ui): Simple UI for the scalable golang implementation of Selenium Hub named Selenoid. We use XTerm for streaming logs over websockets from docker containers. - [**Portainer**](https://portainer.io): Simple management UI for Docker. - [**SSHy**](https://github.com/stuicey/SSHy): HTML5 Based SSHv2 Web Client with E2E encryption utilising xterm.js, SJCL & websockets. - [**JupyterLab**](https://github.com/jupyterlab/jupyterlab): An extensible computational environment for Jupyter, supporting interactive data science and scientific computing across all programming languages. @@ -143,7 +147,6 @@ Xterm.js is used in several world-class applications to provide great terminal e - [**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. - [**SSH Web Client**](https://github.com/roke22/PHP-SSH2-Web-Client): SSH Web Client with PHP. -- [**Shellvault**](https://www.shellvault.io): The cloud-based SSH terminal you can access from anywhere. - [**Juno**](http://junolab.org/): A flexible Julia IDE, based on Atom. - [**webssh**](https://github.com/huashengdun/webssh): Web based ssh client. - [**info-beamer hosted**](https://info-beamer.com): Uses xterm.js to manage digital signage devices from the web dashboard. @@ -163,16 +166,27 @@ Xterm.js is used in several world-class applications to provide great terminal e - [**CoCalc**](https://cocalc.com/): Lots of free software pre-installed, to chat, collaborate, develop, program, publish, research, share, teach, in C++, HTML, Julia, Jupyter, LaTeX, Markdown, Python, R, SageMath, Scala, ... - [**Dank Domain**](https://www.DDgame.us/): Open source multiuser medieval game supporting old & new terminal emulation. - [**DockerStacks**](https://docker-stacks.com/): Local LAMP/LEMP development studio -- [**Codecademy**](https://codecademy.com/): Uses xterm.js in its courses on Bash. +- [**Codecademy**](https://codecademy.com/): Uses xterm.js in its courses on Bash. - [**Laravel Ssh Web Client**](https://github.com/roke22/Laravel-ssh-client): Laravel server inventory with ssh web client to connect at server using xterm.js - [**Repl.it**](https://repl.it): Collaborative browser based IDE with support for 50+ different languages. - [**TeleType**](https://github.com/akshaykmr/TeleType): cli tool that allows you to share your terminal online conveniently. Show off mad cli-fu, help a colleague, teach, or troubleshoot. -- [**Intervue**](https://www.intervue.io): Pair programming for interviews. Multiple programming languages supported, with results displayed by xterm.js. -- [**TRASA**](https://trasa.io): Zero trust access to Web, SSH, RDP and Database services. +- [**Intervue**](https://www.intervue.io): Pair programming for interviews. Multiple programming languages are supported, with results displayed by xterm.js. +- [**TRASA**](https://trasa.io): Zero trust access to Web, SSH, RDP, and Database services. - [**FleetDeck**](https://fleetdeck.io): Remote desktop & virtual terminal -[And much more...](https://github.com/xtermjs/xterm.js/network/dependents) +- [**Commas**](https://github.com/CyanSalt/commas): Commas is a hackable terminal and command runner. +- [**Devtron**](https://github.com/devtron-labs/devtron): Software Delivery Workflow For Kubernetes. +- [**NxShell**](https://github.com/nxshell/nxshell): An easy to use new terminal for SSH. +- [**gifcast**](https://dstein64.github.io/gifcast/): Converts an asciinema cast to an animated GIF. +- [**WizardWebssh**](https://gitlab.com/mikeramsey/wizardwebssh): A terminal with Pyqt5 Widget for embedding, which can be used as an ssh client to connect to your ssh servers. It is written in Python, based on tornado, paramiko, and xterm.js. +- [**Wizard Assistant**](https://wizardassistant.com/): Wizard Assistant comes with advanced automation tools, preloaded common and special time-saving commands, and a built-in SSH terminal. Now you can remotely administer, troubleshoot, and analyze any system with ease. +- [**ucli**](https://github.com/tsadarsh/ucli): Command Line for everyone :family_man_woman_girl_boy: at [www.ucli.tech](https://www.ucli.tech). +- [**Tess**](https://github.com/SquitchYT/Tess/): Simple Terminal Fully Customizable for Everyone. +- [**HashiCorp Nomad**](https://www.nomadproject.io/): A container orchestrator with the ability to connect to remote tasks via a web interface using websockets and xterm.js. +- [**TermPair**](https://github.com/cs01/termpair): View and control terminals from your browser with end-to-end encryption +- [**gdbgui**](https://github.com/cs01/gdbgui): Browser-based frontend to gdb (gnu debugger) +- [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 on our list. Note: Please add any new contributions to the end of the list only. ## Releases @@ -182,21 +196,21 @@ All current and past releases are available on this repo's [Releases page](https ### Beta builds -Our CI releases beta builds to npm for every change that goes into master, install the latest beta build with: +Our CI releases beta builds to npm for every change that goes into master. Install the latest beta build with: -``` +```bash npm install -S xterm@beta ``` -These should generally be stable but some bugs may slip in, we recommend using the beta build primarily to test out new features and for verifying bug fixes. +These should generally be stable, but some bugs may slip in. We recommend using the beta build primarily to test out new features and to verify bug fixes. ## Contributing -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. +You can read the [guide on the wiki](https://github.com/xtermjs/xterm.js/wiki/Contributing) to learn how to contribute and set up xterm.js for development. ## License Agreement -If you contribute code to this project, you are implicitly allowing your code to be distributed under the MIT license. You are also implicitly verifying that all code is your original work. +If you contribute code to this project, you implicitly allow your code to be distributed under the MIT license. You are also implicitly verifying that all code is your original work. Copyright (c) 2017-2019, [The xterm.js authors](https://github.com/xtermjs/xterm.js/graphs/contributors) (MIT License)
Copyright (c) 2014-2017, SourceLair, Private Company ([www.sourcelair.com](https://www.sourcelair.com/home)) (MIT License)
diff --git a/addons/xterm-addon-attach/src/AttachAddon.ts b/addons/xterm-addon-attach/src/AttachAddon.ts index 279d1b2e..9fbd796b 100644 --- a/addons/xterm-addon-attach/src/AttachAddon.ts +++ b/addons/xterm-addon-attach/src/AttachAddon.ts @@ -20,7 +20,7 @@ export class AttachAddon implements ITerminalAddon { this._socket = socket; // always set binary type to arraybuffer, we do not handle blobs this._socket.binaryType = 'arraybuffer'; - this._bidirectional = (options && options.bidirectional === false) ? false : true; + this._bidirectional = !(options && options.bidirectional === false); } public activate(terminal: Terminal): void { @@ -41,7 +41,9 @@ export class AttachAddon implements ITerminalAddon { } public dispose(): void { - this._disposables.forEach(d => d.dispose()); + for (const d of this._disposables) { + d.dispose(); + } } private _sendData(data: string): void { diff --git a/addons/xterm-addon-attach/test/AttachAddon.api.ts b/addons/xterm-addon-attach/test/AttachAddon.api.ts index 41e579f5..8335cf0f 100644 --- a/addons/xterm-addon-attach/test/AttachAddon.api.ts +++ b/addons/xterm-addon-attach/test/AttachAddon.api.ts @@ -4,10 +4,10 @@ */ import WebSocket = require('ws'); -import { openTerminal, pollFor, getBrowserType } from '../../../out-test/api/TestUtils'; +import { openTerminal, pollFor, launchBrowser } from '../../../out-test/api/TestUtils'; import { Browser, Page } from 'playwright'; -const APP = 'http://127.0.0.1:3000/test'; +const APP = 'http://127.0.0.1:3001/test'; let browser: Browser; let page: Page; @@ -16,10 +16,7 @@ const height = 600; describe('AttachAddon', () => { before(async function(): Promise { - const browserType = getBrowserType(); - browser = await browserType.launch({ - headless: process.argv.indexOf('--headless') !== -1 - }); + browser = await launchBrowser(); page = await (await browser.newContext()).newPage(); await page.setViewportSize({ width, height }); }); diff --git a/addons/xterm-addon-fit/package.json b/addons/xterm-addon-fit/package.json index 859d6ebf..f9d96c4a 100644 --- a/addons/xterm-addon-fit/package.json +++ b/addons/xterm-addon-fit/package.json @@ -1,6 +1,6 @@ { "name": "xterm-addon-fit", - "version": "0.4.0", + "version": "0.5.0", "author": { "name": "The xterm.js authors", "url": "https://xtermjs.org/" diff --git a/addons/xterm-addon-fit/test/FitAddon.api.ts b/addons/xterm-addon-fit/test/FitAddon.api.ts index 1111dff0..987092ad 100644 --- a/addons/xterm-addon-fit/test/FitAddon.api.ts +++ b/addons/xterm-addon-fit/test/FitAddon.api.ts @@ -4,10 +4,10 @@ */ import { assert } from 'chai'; -import { openTerminal, getBrowserType } from '../../../out-test/api/TestUtils'; +import { openTerminal, launchBrowser } from '../../../out-test/api/TestUtils'; import { Browser, Page } from 'playwright'; -const APP = 'http://127.0.0.1:3000/test'; +const APP = 'http://127.0.0.1:3001/test'; let browser: Browser; let page: Page; @@ -16,10 +16,7 @@ const height = 768; describe('FitAddon', () => { before(async function(): Promise { - const browserType = getBrowserType(); - browser = await browserType.launch({ - headless: process.argv.indexOf('--headless') !== -1 - }); + browser = await launchBrowser(); page = await (await browser.newContext()).newPage(); await page.setViewportSize({ width, height }); await page.goto(APP); @@ -45,7 +42,7 @@ describe('FitAddon', () => { describe('proposeDimensions', () => { afterEach(async () => { - return unloadFit(); + return await unloadFit(); }); it('default', async function(): Promise { @@ -84,7 +81,7 @@ describe('FitAddon', () => { describe('fit', () => { afterEach(async () => { - return unloadFit(); + return await unloadFit(); }); it('default', async function(): Promise { diff --git a/addons/xterm-addon-ligatures/README.md b/addons/xterm-addon-ligatures/README.md index 6336bf83..8f869d73 100644 --- a/addons/xterm-addon-ligatures/README.md +++ b/addons/xterm-addon-ligatures/README.md @@ -51,3 +51,4 @@ This package makes use of the following fonts for testing: [Fira Code License]: https://github.com/tonsky/FiraCode/blob/master/LICENSE [Iosevka]: https://github.com/be5invis/Iosevka [Iosevka License]: https://github.com/be5invis/Iosevka/blob/master/LICENSE.md + diff --git a/addons/xterm-addon-ligatures/package.json b/addons/xterm-addon-ligatures/package.json index df85c020..b1688772 100644 --- a/addons/xterm-addon-ligatures/package.json +++ b/addons/xterm-addon-ligatures/package.json @@ -1,6 +1,6 @@ { "name": "xterm-addon-ligatures", - "version": "0.3.0", + "version": "0.5.1", "description": "Add support for programming ligatures to xterm.js", "author": { "name": "The xterm.js authors", @@ -31,12 +31,12 @@ ], "license": "MIT", "dependencies": { - "font-finder": "^1.0.4", - "font-ligatures": "^1.3.3" + "font-finder": "^1.1.0", + "font-ligatures": "^1.4.0" }, "devDependencies": { "@types/sinon": "^5.0.1", - "axios": "^0.18.0", + "axios": "^0.21.2", "mkdirp": "0.5.5", "sinon": "6.3.5", "yauzl": "^2.10.0" diff --git a/addons/xterm-addon-ligatures/src/font.ts b/addons/xterm-addon-ligatures/src/font.ts index 825fc797..abdbbc04 100644 --- a/addons/xterm-addon-ligatures/src/font.ts +++ b/addons/xterm-addon-ligatures/src/font.ts @@ -3,12 +3,28 @@ * @license MIT */ -import * as fontFinder from 'font-finder'; -import * as fontLigatures from 'font-ligatures'; +import { FontList } from 'font-finder'; +import { Font, loadBuffer, loadFile } from 'font-ligatures'; import parse from './parse'; -let fontsPromise: Promise | undefined = undefined; +interface IFontMetadata { + family: string; + fullName: string; + postscriptName: string; + blob: () => Promise; +} + +interface IFontAccessNavigator { + fonts: { + query: () => Promise; + }; + permissions: { + request?: (permission: { name: string }) => Promise<{state: string}>; + }; +} + +let fontsPromise: Promise> | undefined = undefined; /** * Loads the font ligature wrapper for the specified font family if it could be @@ -16,9 +32,50 @@ let fontsPromise: Promise | undefined = undefined; * @param fontFamily The CSS font family definition to resolve * @param cacheSize The size of the ligature cache to maintain if the font is resolved */ -export default async function load(fontFamily: string, cacheSize: number): Promise { +export default async function load(fontFamily: string, cacheSize: number): Promise { if (!fontsPromise) { - fontsPromise = fontFinder.list(); + // Web environment that supports font access API + if (typeof navigator !== 'undefined' && 'fonts' in navigator) { + try { + const status = await (navigator as unknown as IFontAccessNavigator).permissions.request?.({ + name: 'local-fonts' + }); + if (status && status.state !== 'granted') { + throw new Error('Permission to access local fonts not granted.'); + } + } catch (err) { + // A `TypeError` indicates the 'local-fonts' + // permission is not yet implemented, so + // only `throw` if this is _not_ the problem. + if (err.name !== 'TypeError') { + throw err; + } + } + const fonts: Record = {}; + try { + const fontsIterator = await (navigator as unknown as IFontAccessNavigator).fonts.query(); + for (const metadata of fontsIterator) { + if (!fonts.hasOwnProperty(metadata.family)) { + fonts[metadata.family] = []; + } + fonts[metadata.family].push(metadata); + } + fontsPromise = Promise.resolve(fonts); + } catch (err) { + console.error(err.name, err.message); + } + } + // Node environment or no font access API + else { + try { + fontsPromise = (await import('font-finder')).list(); + } catch (err) { + // No-op + } + } + if (!fontsPromise) { + fontsPromise = Promise.resolve({}); + } } const fonts = await fontsPromise; @@ -31,7 +88,11 @@ export default async function load(fontFamily: string, cacheSize: number): Promi } if (fonts.hasOwnProperty(family) && fonts[family].length > 0) { - return await fontLigatures.loadFile(fonts[family][0].path, { cacheSize }); + const font = fonts[family][0]; + if ('blob' in font) { + return loadBuffer(await (await font.blob()).arrayBuffer(), { cacheSize }); + } + return await loadFile(font.path, { cacheSize }); } } diff --git a/addons/xterm-addon-ligatures/src/parse.ts b/addons/xterm-addon-ligatures/src/parse.ts index 5ad7747f..6289e468 100644 --- a/addons/xterm-addon-ligatures/src/parse.ts +++ b/addons/xterm-addon-ligatures/src/parse.ts @@ -68,7 +68,7 @@ function parseString(context: IParseContext, quoteChar: '\'' | '"'): string { while (context.offset < context.input.length) { const char = context.input[context.offset++]; if (escaped) { - if (/[0-9a-fA-F]/.test(char)) { + if (/[\dA-Fa-f]/.test(char)) { // Unicode escape context.offset--; str += parseUnicode(context); @@ -107,7 +107,7 @@ function parseIdentifier(context: IParseContext): string { while (context.offset < context.input.length) { const char = context.input[context.offset++]; if (escaped) { - if (/[0-9a-fA-F]/.test(char)) { + if (/[\dA-Fa-f]/.test(char)) { // Unicode escape context.offset--; str += parseUnicode(context); @@ -156,7 +156,7 @@ function parseUnicode(context: IParseContext): string { // of the escape and is swallowed. return unicodeToString(str); } - if (str.length >= 6 || !/[0-9a-fA-F]/.test(char)) { + if (str.length >= 6 || !/[\dA-Fa-f]/.test(char)) { // If the next character is not a valid hex digit or we have reached the // maximum of 6 digits in the escape, terminate the escape. context.offset--; diff --git a/addons/xterm-addon-ligatures/webpack.config.js b/addons/xterm-addon-ligatures/webpack.config.js index 253e1207..ea69841e 100644 --- a/addons/xterm-addon-ligatures/webpack.config.js +++ b/addons/xterm-addon-ligatures/webpack.config.js @@ -30,7 +30,18 @@ module.exports = { }, mode: 'production', externals: { - 'font-finder':'font-finder', - 'font-ligatures':'font-ligatures' + 'font-finder': 'font-finder', + 'stream': 'stream', + 'os': 'os', + 'util': 'util' + }, + resolve: { + // The ligature modules contains fallbacks for node environments, we never want to browserify them + fallback: { + stream: false, + util: false, + os: false, + path: false + } } }; diff --git a/addons/xterm-addon-ligatures/yarn.lock b/addons/xterm-addon-ligatures/yarn.lock index d8e64e49..f6ce913b 100644 --- a/addons/xterm-addon-ligatures/yarn.lock +++ b/addons/xterm-addon-ligatures/yarn.lock @@ -45,23 +45,17 @@ array-from@^2.1.1: resolved "https://registry.yarnpkg.com/array-from/-/array-from-2.1.1.tgz#cfe9d8c26628b9dc5aecc62a9f5d8f1f352c1195" integrity sha1-z+nYwmYoudxa7MYqn12PHzUsEZU= -axios@^0.18.0: - version "0.18.1" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.18.1.tgz#ff3f0de2e7b5d180e757ad98000f1081b87bcea3" +axios@^0.21.2: + version "0.21.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.2.tgz#21297d5084b2aeeb422f5d38e7be4fbb82239017" + integrity sha512-87otirqUw3e8CzHTMO+/9kh/FSgXt/eVDvipijwDtEuwbkySWZ9SBm6VEubmJ/kLKEoLQV/POhxXFb66bfekfg== dependencies: - follow-redirects "1.5.10" - is-buffer "^2.0.2" + follow-redirects "^1.14.0" buffer-crc32@~0.2.3: version "0.2.13" resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" -debug@=3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" - dependencies: - ms "2.0.0" - diff@^3.5.0: version "3.5.0" resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" @@ -72,26 +66,33 @@ fd-slicer@~1.1.0: dependencies: pend "~1.2.0" -follow-redirects@1.5.10: - version "1.5.10" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a" - dependencies: - debug "=3.1.0" +follow-redirects@^1.14.0: + version "1.14.3" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.3.tgz#6ada78118d8d24caee595595accdc0ac6abd022e" + integrity sha512-3MkHxknWMUtb23apkgz/83fDoe+y+qr0TdgacGIA7bew+QLBo3vdgEN2xEsuXNivpFy4CyDhBBZnNZOtalmenw== -font-finder@^1.0.3, font-finder@^1.0.4: +font-finder@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/font-finder/-/font-finder-1.0.4.tgz#2ca944954dd8d0e1b5bdc4c596cc08607761d89b" dependencies: get-system-fonts "^2.0.0" promise-stream-reader "^1.0.1" -font-ligatures@^1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/font-ligatures/-/font-ligatures-1.3.3.tgz#63fff18dc8adb3a11fe5eec1f4e8d7edfa8075b9" - integrity sha512-NSGpHgVNX81M7AWS1XylK1UZbN3QllfUIDAAuPv6TUcl5O2b781JcKS5L2RopAU0AqlTyX3ZuX/04eaMpbVrHA== +font-finder@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/font-finder/-/font-finder-1.1.0.tgz#2bff2b2762acba720239c8bec898a96daae90858" + integrity sha512-wpCL2uIbi6GurJbU7ZlQ3nGd61Ho+dSU6U83/xJT5UPFfN35EeCW/rOtS+5k+IuEZu2SYmHzDIPL9eA5tSYRAw== + dependencies: + get-system-fonts "^2.0.0" + promise-stream-reader "^1.0.1" + +font-ligatures@^1.4.0: + version "1.4.0" + resolved "https://registry.npmjs.org/font-ligatures/-/font-ligatures-1.4.0.tgz#6a7b370d96be1358dddfad67830e82fbfd59e6dc" + integrity sha512-n7DFnnEpJ0NrVoLqZIL4tMGVs+CnFwQc92m80LWyrbgAFO4x234+t2/H9o4eOYA1eh6ta9dZAEEsJAwsBdNezA== dependencies: font-finder "^1.0.3" - lru-cache "^4.1.3" + lru-cache "^6.0.0" opentype.js "^0.8.0" get-system-fonts@^2.0.0: @@ -102,10 +103,6 @@ has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" -is-buffer@^2.0.2: - version "2.0.3" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.3.tgz#4ecf3fcf749cbd1e472689e109ac66261a25e725" - isarray@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" @@ -120,9 +117,9 @@ lodash.get@^4.4.2: resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" lodash@^4.17.15: - version "4.17.19" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" - integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ== + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== lolex@^2.7.5: version "2.7.5" @@ -136,12 +133,12 @@ lolex@^5.0.1: dependencies: "@sinonjs/commons" "^1.7.0" -lru-cache@^4.1.3: - version "4.1.3" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.3.tgz#a1175cf3496dfc8436c156c334b4955992bce69c" +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== dependencies: - pseudomap "^1.0.2" - yallist "^2.1.2" + yallist "^4.0.0" minimist@^1.2.5: version "1.2.5" @@ -155,10 +152,6 @@ mkdirp@0.5.5: dependencies: minimist "^1.2.5" -ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - nise@^1.4.5: version "1.5.3" resolved "https://registry.yarnpkg.com/nise/-/nise-1.5.3.tgz#9d2cfe37d44f57317766c6e9408a359c5d3ac1f7" @@ -190,10 +183,6 @@ promise-stream-reader@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/promise-stream-reader/-/promise-stream-reader-1.0.1.tgz#4e793a79c9d49a73ccd947c6da9c127f12923649" -pseudomap@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" - sinon@6.3.5: version "6.3.5" resolved "https://registry.yarnpkg.com/sinon/-/sinon-6.3.5.tgz#0f6d6a5b4ebaad1f6e8e019395542d1d02c144a0" @@ -224,9 +213,10 @@ type-detect@4.0.8, type-detect@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" -yallist@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== yauzl@^2.10.0: version "2.10.0" diff --git a/addons/xterm-addon-search/package.json b/addons/xterm-addon-search/package.json index 711335cb..08fe195f 100644 --- a/addons/xterm-addon-search/package.json +++ b/addons/xterm-addon-search/package.json @@ -1,6 +1,6 @@ { "name": "xterm-addon-search", - "version": "0.7.0", + "version": "0.8.1", "author": { "name": "The xterm.js authors", "url": "https://xtermjs.org/" diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index f5505689..1409328d 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -23,7 +23,7 @@ export interface ISearchResult { row: number; } -const NON_WORD_CHARACTERS = ' ~!@#$%^&*()+`-=[]{}|\;:"\',./<>?'; +const NON_WORD_CHARACTERS = ' ~!@#$%^&*()+`-=[]{}|\\;:"\',./<>?'; const LINES_CACHE_TIME_TO_LIVE = 15 * 1000; // 15 secs export class SearchAddon implements ITerminalAddon { @@ -158,8 +158,15 @@ export class SearchAddon implements ITerminalAddon { }; if (incremental) { + // Try to expand selection to right first. result = this._findInLine(term, searchPosition, searchOptions, false); - if (!(result && result.row === startRow && result.col === startCol)) { + const isOldResultHighlighted = result && result.row === startRow && result.col === startCol; + if (!isOldResultHighlighted) { + // If selection was not able to be expanded to the right, then try reverse search + if (currentSelection) { + searchPosition.startRow = currentSelection.endRow; + searchPosition.startCol = currentSelection.endColumn; + } result = this._findInLine(term, searchPosition, searchOptions, true); } } else { @@ -233,8 +240,8 @@ export class SearchAddon implements ITerminalAddon { * @param term the substring that starts at searchIndex */ private _isWholeWord(searchIndex: number, line: string, term: string): boolean { - return (((searchIndex === 0) || (NON_WORD_CHARACTERS.indexOf(line[searchIndex - 1]) !== -1)) && - (((searchIndex + term.length) === line.length) || (NON_WORD_CHARACTERS.indexOf(line[searchIndex + term.length]) !== -1))); + return ((searchIndex === 0) || (NON_WORD_CHARACTERS.includes(line[searchIndex - 1]))) && + (((searchIndex + term.length) === line.length) || (NON_WORD_CHARACTERS.includes(line[searchIndex + term.length]))); } /** @@ -245,6 +252,7 @@ export class SearchAddon implements ITerminalAddon { * @param term The search term. * @param position The position to start the search. * @param searchOptions Search options. + * @param isReverseSearch Whether the search should start from the right side of the terminal and search to the left. * @return The search result if it was found. */ protected _findInLine(term: string, searchPosition: ISearchPosition, searchOptions: ISearchOptions = {}, isReverseSearch: boolean = false): ISearchResult | undefined { @@ -254,7 +262,7 @@ export class SearchAddon implements ITerminalAddon { // Ignore wrapped lines, only consider on unwrapped line (first row of command string). const firstLine = terminal.buffer.active.getLine(row); - if (firstLine && firstLine.isWrapped) { + if (firstLine?.isWrapped) { if (isReverseSearch) { searchPosition.startCol += terminal.cols; return; @@ -386,7 +394,7 @@ export class SearchAddon implements ITerminalAddon { // If it is not in the viewport then we scroll else it just gets selected if (result.row >= (terminal.buffer.active.viewportY + terminal.rows) || result.row < terminal.buffer.active.viewportY) { let scroll = result.row - terminal.buffer.active.viewportY; - scroll = scroll - Math.floor(terminal.rows / 2); + scroll -= Math.floor(terminal.rows / 2); terminal.scrollLines(scroll); } return true; diff --git a/addons/xterm-addon-search/test/SearchAddon.api.ts b/addons/xterm-addon-search/test/SearchAddon.api.ts index 3707aecd..8bdf61cf 100644 --- a/addons/xterm-addon-search/test/SearchAddon.api.ts +++ b/addons/xterm-addon-search/test/SearchAddon.api.ts @@ -6,10 +6,10 @@ import { assert } from 'chai'; import { readFile } from 'fs'; import { resolve } from 'path'; -import { openTerminal, writeSync, getBrowserType } from '../../../out-test/api/TestUtils'; +import { openTerminal, writeSync, launchBrowser } from '../../../out-test/api/TestUtils'; import { Browser, Page } from 'playwright'; -const APP = 'http://127.0.0.1:3000/test'; +const APP = 'http://127.0.0.1:3001/test'; let browser: Browser; let page: Page; @@ -18,10 +18,7 @@ const height = 600; describe('Search Tests', function(): void { before(async function(): Promise { - const browserType = getBrowserType(); - browser = await browserType.launch({ - headless: process.argv.indexOf('--headless') !== -1 - }); + browser = await launchBrowser(); page = await (await browser.newContext()).newPage(); await page.setViewportSize({ width, height }); await page.goto(APP); diff --git a/addons/xterm-addon-serialize/benchmark/SerializeAddon.benchmark.ts b/addons/xterm-addon-serialize/benchmark/SerializeAddon.benchmark.ts index 0147631d..87741b63 100644 --- a/addons/xterm-addon-serialize/benchmark/SerializeAddon.benchmark.ts +++ b/addons/xterm-addon-serialize/benchmark/SerializeAddon.benchmark.ts @@ -35,7 +35,7 @@ perfContext('Terminal: sh -c "dd if=/dev/urandom count=40 bs=1k | hexdump | lolc chunks.push(data as unknown as Buffer); length += data.length; }); - await new Promise(resolve => p.on('exit', () => resolve())); + await new Promise(resolve => p.on('exit', () => resolve())); contentUtf8 = Buffer.concat(chunks, length); // translate to content string const buffer = new Uint32Array(contentUtf8.length); diff --git a/addons/xterm-addon-serialize/package.json b/addons/xterm-addon-serialize/package.json index bb799aec..77a54dbf 100644 --- a/addons/xterm-addon-serialize/package.json +++ b/addons/xterm-addon-serialize/package.json @@ -1,12 +1,13 @@ { "name": "xterm-addon-serialize", - "version": "0.4.0", + "version": "0.6.0", "author": { "name": "The xterm.js authors", "url": "https://xtermjs.org/" }, "main": "lib/xterm-addon-serialize.js", "types": "typings/xterm-addon-serialize.d.ts", + "repository": "https://github.com/xtermjs/xterm.js", "license": "MIT", "scripts": { "build": "../../node_modules/.bin/tsc -p .", diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts index dad6aa4f..24020b74 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -7,13 +7,17 @@ import { Terminal, ITerminalAddon, IBuffer, IBufferCell } from 'xterm'; + function constrain(value: number, low: number, high: number): number { return Math.max(low, Math.min(value, high)); } // TODO: Refine this template class later abstract class BaseSerializeHandler { - constructor(private _buffer: IBuffer) { } + constructor( + protected readonly _buffer: IBuffer + ) { + } public serialize(startRow: number, endRow: number): string { // we need two of them to flip between old and new cell @@ -21,7 +25,7 @@ abstract class BaseSerializeHandler { const cell2 = this._buffer.getNullCell(); let oldCell = cell1; - this._beforeSerialize(endRow - startRow); + this._beforeSerialize(endRow - startRow, startRow, endRow); for (let row = startRow; row < endRow; row++) { const line = this._buffer.getLine(row); @@ -36,7 +40,7 @@ abstract class BaseSerializeHandler { oldCell = c; } } - this._rowEnd(row); + this._rowEnd(row, row === endRow - 1); } this._afterSerialize(); @@ -45,8 +49,8 @@ abstract class BaseSerializeHandler { } protected _nextCell(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { } - protected _rowEnd(row: number): void { } - protected _beforeSerialize(rows: number): void { } + protected _rowEnd(row: number, isLastRow: boolean): void { } + protected _beforeSerialize(rows: number, startRow: number, endRow: number): void { } protected _afterSerialize(): void { } protected _serializeString(): string { return ''; } } @@ -68,30 +72,157 @@ function equalFlags(cell1: IBufferCell, cell2: IBufferCell): boolean { && cell1.isBlink() === cell2.isBlink() && cell1.isInvisible() === cell2.isInvisible() && cell1.isItalic() === cell2.isItalic() - && cell1.isDim() === cell2.isDim(); + && cell1.isDim() === cell2.isDim() + && cell1.isStrikethrough() === cell2.isStrikethrough(); } class StringSerializeHandler extends BaseSerializeHandler { private _rowIndex: number = 0; private _allRows: string[] = new Array(); + private _allRowSeparators: string[] = new Array(); private _currentRow: string = ''; private _nullCellCount: number = 0; - constructor(buffer: IBuffer) { + // we can see a full colored cell and a null cell that only have background the same style + // but the information isn't preserved by null cell itself + // so wee need to record it when required. + private _cursorStyle: IBufferCell = this._buffer.getNullCell(); + + // where exact the cursor styles comes from + // because we can't copy the cell directly + // so we remember where the content comes from instead + private _cursorStyleRow: number = 0; + private _cursorStyleCol: number = 0; + + // this is a null cell for reference for checking whether background is empty or not + private _backgroundCell: IBufferCell = this._buffer.getNullCell(); + + private _firstRow: number = 0; + private _lastCursorRow: number = 0; + private _lastCursorCol: number = 0; + private _lastContentCursorRow: number = 0; + private _lastContentCursorCol: number = 0; + + constructor( + buffer: IBuffer, + private readonly _terminal: Terminal + ) { super(buffer); } - protected _beforeSerialize(rows: number): void { + protected _beforeSerialize(rows: number, start: number, end: number): void { this._allRows = new Array(rows); + this._lastContentCursorRow = start; + this._lastCursorRow = start; + this._firstRow = start; } - protected _rowEnd(row: number): void { - this._allRows[this._rowIndex++] = this._currentRow; + private _thisRowLastChar: IBufferCell = this._buffer.getNullCell(); + private _thisRowLastSecondChar: IBufferCell = this._buffer.getNullCell(); + private _nextRowFirstChar: IBufferCell = this._buffer.getNullCell(); + protected _rowEnd(row: number, isLastRow: boolean): void { + // if there is colorful empty cell at line end, whe must pad it back, or the the color block will missing + if (this._nullCellCount > 0 && !equalBg(this._cursorStyle, this._backgroundCell)) { + // use clear right to set background. + this._currentRow += `\u001b[${this._nullCellCount}X`; + } + + let rowSeparator = ''; + + // handle row separator + if (!isLastRow) { + // Enable BCE + if (row - this._firstRow >= this._terminal.rows) { + this._buffer.getLine(this._cursorStyleRow)?.getCell(this._cursorStyleCol, this._backgroundCell); + } + + // Fetch current line + const currentLine = this._buffer.getLine(row)!; + // Fetch next line + const nextLine = this._buffer.getLine(row + 1)!; + + if (!nextLine.isWrapped) { + // just insert the line break + rowSeparator = '\r\n'; + // we sended the enter + this._lastCursorRow = row + 1; + this._lastCursorCol = 0; + } else { + rowSeparator = ''; + const thisRowLastChar = currentLine.getCell(currentLine.length - 1, this._thisRowLastChar)!; + const thisRowLastSecondChar = currentLine.getCell(currentLine.length - 2, this._thisRowLastSecondChar)!; + const nextRowFirstChar = nextLine.getCell(0, this._nextRowFirstChar)!; + const isNextRowFirstCharDoubleWidth = nextRowFirstChar.getWidth() > 1; + + // validate whether this line wrap is ever possible + // which mean whether cursor can placed at a overflow position (x === row) naturally + let isValid = false; + + if ( + // you must output character to cause overflow, control sequence can't do this + nextRowFirstChar.getChars() && + isNextRowFirstCharDoubleWidth ? this._nullCellCount <= 1 : this._nullCellCount <= 0 + ) { + if ( + // the last character can't be null, + // you can't use control sequence to move cursor to (x === row) + (thisRowLastChar.getChars() || thisRowLastChar.getWidth() === 0) && + // change background of the first wrapped cell also affects BCE + // so we mark it as invalid to simply the process to determine line separator + equalBg(thisRowLastChar, nextRowFirstChar) + ) { + isValid = true; + } + + if ( + // the second to last character can't be null if the next line starts with CJK, + // you can't use control sequence to move cursor to (x === row) + isNextRowFirstCharDoubleWidth && + (thisRowLastSecondChar.getChars() || thisRowLastSecondChar.getWidth() === 0) && + // change background of the first wrapped cell also affects BCE + // so we mark it as invalid to simply the process to determine line separator + equalBg(thisRowLastChar, nextRowFirstChar) && + equalBg(thisRowLastSecondChar, nextRowFirstChar) + ) { + isValid = true; + } + } + + if (!isValid) { + // force the wrap with magic + // insert enough character to force the wrap + rowSeparator = '-'.repeat(this._nullCellCount + 1); + // move back and erase next line head + rowSeparator += '\u001b[1D\u001b[1X'; + + if (this._nullCellCount > 0) { + // do these because we filled the last several null slot, which we shouldn't + rowSeparator += '\u001b[A'; + rowSeparator += `\u001b[${currentLine.length - this._nullCellCount}C`; + rowSeparator += `\u001b[${this._nullCellCount}X`; + rowSeparator += `\u001b[${currentLine.length - this._nullCellCount}D`; + rowSeparator += '\u001b[B'; + } + + // This is content and need the be serialized even it is invisible. + // without this, wrap will be missing from outputs. + this._lastContentCursorRow = row + 1; + this._lastContentCursorCol = 0; + + // force commit the cursor position + this._lastCursorRow = row + 1; + this._lastCursorCol = 0; + } + } + } + + this._allRows[this._rowIndex] = this._currentRow; + this._allRowSeparators[this._rowIndex++] = rowSeparator; this._currentRow = ''; this._nullCellCount = 0; } - protected _nextCell(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { + private _diffStyle(cell: IBufferCell, oldCell: IBufferCell): number[] { const sgrSeq: number[] = []; const fgChanged = !equalFg(cell, oldCell); const bgChanged = !equalBg(cell, oldCell); @@ -99,7 +230,9 @@ class StringSerializeHandler extends BaseSerializeHandler { if (fgChanged || bgChanged || flagsChanged) { if (cell.isAttributeDefault()) { - this._currentRow += '\x1b[0m'; + if (!oldCell.isAttributeDefault()) { + sgrSeq.push(0); + } } else { if (fgChanged) { const color = cell.getFgColor(); @@ -127,34 +260,133 @@ class StringSerializeHandler extends BaseSerializeHandler { if (cell.isInvisible() !== oldCell.isInvisible()) { sgrSeq.push(cell.isInvisible() ? 8 : 28); } if (cell.isItalic() !== oldCell.isItalic()) { sgrSeq.push(cell.isItalic() ? 3 : 23); } if (cell.isDim() !== oldCell.isDim()) { sgrSeq.push(cell.isDim() ? 2 : 22); } + if (cell.isStrikethrough() !== oldCell.isStrikethrough()) { sgrSeq.push(cell.isStrikethrough() ? 9 : 29); } } } } - if (sgrSeq.length) { - this._currentRow += `\x1b[${sgrSeq.join(';')}m`; + return sgrSeq; + } + + protected _nextCell(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { + // a width 0 cell don't need to be count because it is just a placeholder after a CJK character; + const isPlaceHolderCell = cell.getWidth() === 0; + + if (isPlaceHolderCell) { + return; } - // Count number of null cells encountered after the last non-null cell and move the cursor - // if a non-null cell is found (eg. \t or cursor move) - if (cell.getChars() === '') { + // this cell don't have content + const isEmptyCell = cell.getChars() === ''; + + const sgrSeq = this._diffStyle(cell, this._cursorStyle); + + // the empty cell style is only assumed to be changed when background changed, because foreground is always 0. + const styleChanged = isEmptyCell ? !equalBg(this._cursorStyle, cell) : sgrSeq.length > 0; + + /** + * handles style change + */ + if (styleChanged) { + // before update the style, we need to fill empty cell back + if (this._nullCellCount > 0) { + // use clear right to set background. + if (!equalBg(this._cursorStyle, this._backgroundCell)) { + this._currentRow += `\u001b[${this._nullCellCount}X`; + } + // use move right to move cursor. + this._currentRow += `\u001b[${this._nullCellCount}C`; + this._nullCellCount = 0; + } + + this._lastContentCursorRow = this._lastCursorRow = row; + this._lastContentCursorCol = this._lastCursorCol = col; + + this._currentRow += `\u001b[${sgrSeq.join(';')}m`; + + // update the last cursor style + const line = this._buffer.getLine(row); + if (line !== undefined) { + line.getCell(col, this._cursorStyle); + this._cursorStyleRow = row; + this._cursorStyleCol = col; + } + } + + /** + * handles actual content + */ + if (isEmptyCell) { this._nullCellCount += cell.getWidth(); - } else if (this._nullCellCount > 0) { - this._currentRow += `\x1b[${this._nullCellCount}C`; - this._nullCellCount = 0; - } + } else { + if (this._nullCellCount > 0) { + // we can just assume we have same style with previous one here + // because style change is handled by previous stage + // use move right when background is empty, use clear right when there is background. + if (equalBg(this._cursorStyle, this._backgroundCell)) { + this._currentRow += `\u001b[${this._nullCellCount}C`; + } else { + this._currentRow += `\u001b[${this._nullCellCount}X`; + this._currentRow += `\u001b[${this._nullCellCount}C`; + } + this._nullCellCount = 0; + } - this._currentRow += cell.getChars(); + this._currentRow += cell.getChars(); + + // update cursor + this._lastContentCursorRow = this._lastCursorRow = row; + this._lastContentCursorCol = this._lastCursorCol = col + cell.getWidth(); + } } protected _serializeString(): string { let rowEnd = this._allRows.length; - for (; rowEnd > 0; rowEnd--) { - if (this._allRows[rowEnd - 1]) { - break; + + // the fixup is only required for data without scrollback + // because it will always be placed at last line otherwise + if (this._buffer.length - this._firstRow <= this._terminal.rows) { + rowEnd = this._lastContentCursorRow + 1 - this._firstRow; + this._lastCursorCol = this._lastContentCursorCol; + this._lastCursorRow = this._lastContentCursorRow; + } + + let content = ''; + + for (let i = 0; i < rowEnd; i++) { + content += this._allRows[i]; + if (i + 1 < rowEnd) { + content += this._allRowSeparators[i]; } } - return this._allRows.slice(0, rowEnd).join('\r\n'); + + // restore the cursor + const realCursorRow = this._buffer.baseY + this._buffer.cursorY; + const realCursorCol = this._buffer.cursorX; + + const cursorMoved = (realCursorRow !== this._lastCursorRow || realCursorCol !== this._lastCursorCol); + + const moveRight = (offset: number): void => { + if (offset > 0) { + content += `\u001b[${offset}C`; + } else if (offset < 0) { + content += `\u001b[${-offset}D`; + } + }; + const moveDown = (offset: number): void => { + if (offset > 0) { + content += `\u001b[${offset}B`; + } else if (offset < 0) { + content += `\u001b[${-offset}A`; + } + }; + + if (cursorMoved) { + moveDown(realCursorRow - this._lastCursorRow); + moveRight(realCursorCol - this._lastCursorCol); + } + + return content; } } @@ -167,21 +399,73 @@ export class SerializeAddon implements ITerminalAddon { this._terminal = terminal; } - public serialize(rows?: number): string { - // TODO: Add re-position cursor support - // TODO: Add word wrap mode support + private _serializeBuffer(terminal: Terminal, buffer: IBuffer, scrollback?: number): string { + const maxRows = buffer.length; + const handler = new StringSerializeHandler(buffer, terminal); + const correctRows = (scrollback === undefined) ? maxRows : constrain(scrollback + terminal.rows, 0, maxRows); + return handler.serialize(maxRows - correctRows, maxRows); + } + + private _serializeModes(terminal: Terminal): string { + let content = ''; + const modes = terminal.modes; + + // Default: false + if (modes.applicationCursorKeysMode) content += '\x1b[?1h'; + if (modes.applicationKeypadMode) content += '\x1b[?66h'; + if (modes.bracketedPasteMode) content += '\x1b[?2004h'; + if (modes.insertMode) content += '\x1b[4h'; + if (modes.originMode) content += '\x1b[?6h'; + if (modes.reverseWraparoundMode) content += '\x1b[?45h'; + if (modes.sendFocusMode) content += '\x1b[?1004h'; + + // Default: true + if (modes.wraparoundMode === false) content += '\x1b[?7l'; + + // Default: 'none' + if (modes.mouseTrackingMode !== 'none') { + switch (modes.mouseTrackingMode) { + case 'x10': content += '\x1b[?9h'; break; + case 'vt200': content += '\x1b[?1000h'; break; + case 'drag': content += '\x1b[?1002h'; break; + case 'any': content += '\x1b[?1003h'; break; + } + } + + return content; + } + + public serialize(options?: ISerializeOptions): string { // TODO: Add combinedData support if (!this._terminal) { throw new Error('Cannot use addon until it has been loaded'); } - const maxRows = this._terminal.buffer.active.length; - const handler = new StringSerializeHandler(this._terminal.buffer.active); + // Normal buffer + let content = this._serializeBuffer(this._terminal, this._terminal.buffer.normal, options?.scrollback); - rows = (rows === undefined) ? maxRows : constrain(rows, 0, maxRows); + // Alternate buffer + if (!options?.excludeAltBuffer) { + if (this._terminal.buffer.active.type === 'alternate') { + const alternativeScreenContent = this._serializeBuffer(this._terminal, this._terminal.buffer.alternate, undefined); + content += `\u001b[?1049h\u001b[H${alternativeScreenContent}`; + } + } - return handler.serialize(maxRows - rows, maxRows); + // Modes + if (!options?.excludeModes) { + content += this._serializeModes(this._terminal); + } + + return content; } public dispose(): void { } } + + +interface ISerializeOptions { + scrollback?: number; + excludeModes?: boolean; + excludeAltBuffer?: boolean; +} diff --git a/addons/xterm-addon-serialize/test/SerializeAddon.api.ts b/addons/xterm-addon-serialize/test/SerializeAddon.api.ts index 5b615db1..334016e6 100644 --- a/addons/xterm-addon-serialize/test/SerializeAddon.api.ts +++ b/addons/xterm-addon-serialize/test/SerializeAddon.api.ts @@ -4,22 +4,41 @@ */ import { assert } from 'chai'; -import { openTerminal, writeSync, getBrowserType } from '../../../out-test/api/TestUtils'; +import { openTerminal, writeSync, launchBrowser } from '../../../out-test/api/TestUtils'; import { Browser, Page } from 'playwright'; -const APP = 'http://127.0.0.1:3000/test'; +const APP = 'http://127.0.0.1:3001/test'; let browser: Browser; let page: Page; const width = 800; const height = 600; +const writeRawSync = (page: any, str: string): Promise => writeSync(page, '\' +' + JSON.stringify(str) + '+ \''); + +const testNormalScreenEqual = async (page: any, str: string): Promise => { + await writeRawSync(page, str); + const originalBuffer = await page.evaluate(`inspectBuffer(term.buffer.normal);`); + + const result = await page.evaluate(`serializeAddon.serialize();`) as string; + await page.evaluate(`term.reset();`); + await writeRawSync(page, result); + const newBuffer = await page.evaluate(`inspectBuffer(term.buffer.normal);`); + + // chai decides -0 and 0 are different number... + // and firefox have a bug that output -0 for unknown reason + assert.equal(JSON.stringify(originalBuffer), JSON.stringify(newBuffer)); +}; + +async function testSerializeEquals(writeContent: string, expectedSerialized: string): Promise { + await writeRawSync(page, writeContent); + const result = await page.evaluate(`serializeAddon.serialize();`) as string; + assert.strictEqual(result, expectedSerialized); +} + describe('SerializeAddon', () => { before(async function(): Promise { - const browserType = getBrowserType(); - browser = await browserType.launch({ - headless: process.argv.indexOf('--headless') !== -1 - }); + browser = await launchBrowser(); page = await (await browser.newContext()).newPage(); await page.setViewportSize({ width, height }); await page.goto(APP); @@ -27,19 +46,72 @@ describe('SerializeAddon', () => { await page.evaluate(` window.serializeAddon = new SerializeAddon(); window.term.loadAddon(window.serializeAddon); + window.inspectBuffer = (buffer) => { + const lines = []; + for (let i = 0; i < buffer.length; i++) { + // Do this intentionally to get content of underlining source + const bufferLine = buffer.getLine(i)._line; + lines.push(JSON.stringify(bufferLine)); + } + return { + x: buffer.cursorX, + y: buffer.cursorY, + data: lines + }; + } `); }); after(async () => await browser.close()); beforeEach(async () => await page.evaluate(`window.term.reset()`)); + it('produce different output when we call test util with different text', async function(): Promise { + await writeRawSync(page, '12345'); + const buffer1 = await page.evaluate(`inspectBuffer(term.buffer.normal);`); + + await page.evaluate(`term.reset();`); + await writeRawSync(page, '67890'); + const buffer2 = await page.evaluate(`inspectBuffer(term.buffer.normal);`); + + assert.throw(() => { + assert.equal(JSON.stringify(buffer1), JSON.stringify(buffer2)); + }); + }); + + it('produce different output when we call test util with different line wrap', async function(): Promise { + await writeRawSync(page, '1234567890\r\n12345'); + const buffer3 = await page.evaluate(`inspectBuffer(term.buffer.normal);`); + + await page.evaluate(`term.reset();`); + await writeRawSync(page, '1234567890n12345'); + const buffer4 = await page.evaluate(`inspectBuffer(term.buffer.normal);`); + + assert.throw(() => { + assert.equal(JSON.stringify(buffer3), JSON.stringify(buffer4)); + }); + }); + it('empty content', async function(): Promise { - const rows = 10; - const cols = 10; assert.equal(await page.evaluate(`serializeAddon.serialize();`), ''); }); - it('trim last empty lines', async function(): Promise { + it('unwrap wrapped line', async function(): Promise { + const lines = ['123456789123456789']; + await writeSync(page, lines.join('\\r\\n')); + assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); + }); + + it('does not unwrap non-wrapped line', async function(): Promise { + const lines = [ + '123456789', + '123456789' + ]; + await writeSync(page, lines.join('\\r\\n')); + assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); + }); + + + it('preserve last empty lines', async function(): Promise { const cols = 10; const lines = [ '', @@ -55,7 +127,7 @@ describe('SerializeAddon', () => { '' ]; await writeSync(page, lines.join('\\r\\n')); - assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.slice(0, 8).join('\r\n')); + assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); }); it('digits content', async function(): Promise { @@ -67,21 +139,34 @@ describe('SerializeAddon', () => { assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); }); - it('serialize half rows of content', async function(): Promise { - const rows = 10; - const halfRows = rows >> 1; + it('serialize with half of scrollback', async function(): Promise { + const rows = 20; + const scrollback = rows - 10; + const halfScrollback = scrollback / 2; const cols = 10; const lines = newArray((index: number) => digitsString(cols, index), rows); await writeSync(page, lines.join('\\r\\n')); - assert.equal(await page.evaluate(`serializeAddon.serialize(${halfRows});`), lines.slice(halfRows, 2 * halfRows).join('\r\n')); + assert.equal(await page.evaluate(`serializeAddon.serialize({ scrollback: ${halfScrollback} });`), lines.slice(halfScrollback, rows).join('\r\n')); }); - it('serialize 0 rows of content', async function(): Promise { - const rows = 10; + it('serialize 0 rows of scrollback', async function(): Promise { + const rows = 20; const cols = 10; const lines = newArray((index: number) => digitsString(cols, index), rows); await writeSync(page, lines.join('\\r\\n')); - assert.equal(await page.evaluate(`serializeAddon.serialize(0);`), ''); + assert.equal(await page.evaluate(`serializeAddon.serialize({ scrollback: 0 });`), lines.slice(rows - 10, rows).join('\r\n')); + }); + + it('serialize exclude modes', async () => { + await writeSync(page, 'before\\x1b[?1hafter'); + assert.equal(await page.evaluate(`serializeAddon.serialize();`), 'beforeafter\x1b[?1h'); + assert.equal(await page.evaluate(`serializeAddon.serialize({ excludeModes: true });`), 'beforeafter'); + }); + + it('serialize exclude alt buffer', async () => { + await writeSync(page, 'normal\\x1b[?1049h\\x1b[Halt'); + assert.equal(await page.evaluate(`serializeAddon.serialize();`), 'normal\x1b[?1049h\x1b[Halt'); + assert.equal(await page.evaluate(`serializeAddon.serialize({ excludeAltBuffer: true });`), 'normal'); }); it('serialize all rows of content with color16', async function(): Promise { @@ -105,17 +190,19 @@ describe('SerializeAddon', () => { const cols = 10; const line = '+'.repeat(cols); const lines: string[] = [ - mkSGR(FG_P16_GREEN) + line, // Workaround: If we clear all flags a the end, serialize will use \x1b[0m to clear instead of the sepcific disable sequence - mkSGR(INVERSE) + line, - mkSGR(BOLD) + line, - mkSGR(UNDERLINED) + line, - mkSGR(BLINK) + line, - mkSGR(INVISIBLE) + line, - mkSGR(NO_INVERSE) + line, - mkSGR(NO_BOLD) + line, - mkSGR(NO_UNDERLINED) + line, - mkSGR(NO_BLINK) + line, - mkSGR(NO_INVISIBLE) + line + sgr(FG_P16_GREEN) + line, // Workaround: If we clear all flags a the end, serialize will use \x1b[0m to clear instead of the sepcific disable sequence + sgr(INVERSE) + line, + sgr(BOLD) + line, + sgr(UNDERLINED) + line, + sgr(BLINK) + line, + sgr(INVISIBLE) + line, + sgr(STRIKETHROUGH) + line, + sgr(NO_INVERSE) + line, + sgr(NO_BOLD) + line, + sgr(NO_UNDERLINED) + line, + sgr(NO_BLINK) + line, + sgr(NO_INVISIBLE) + line, + sgr(NO_STRIKETHROUGH) + line ]; const rows = lines.length; await writeSync(page, lines.join('\\r\\n')); @@ -137,16 +224,16 @@ describe('SerializeAddon', () => { const cols = 10; const line = '+'.repeat(cols); const lines: string[] = [ - mkSGR(FG_P16_RED) + line, // fg Red, - mkSGR(UNDERLINED) + line, // fg Red, Underlined - mkSGR(FG_P16_GREEN) + line, // fg Green, Underlined - mkSGR(INVERSE) + line, // fg Green, Underlined, Inverse - mkSGR(NO_INVERSE) + line, // fg Green, Underlined - mkSGR(INVERSE) + line, // fg Green, Underlined, Inverse - mkSGR(BG_P16_YELLOW) + line, // fg Green, bg Yellow, Underlined, Inverse - mkSGR(FG_RESET) + line, // bg Yellow, Underlined, Inverse - mkSGR(BG_RESET) + line, // Underlined, Inverse - mkSGR(NORMAL) + line // Back to normal + sgr(FG_P16_RED) + line, // fg Red, + sgr(UNDERLINED) + line, // fg Red, Underlined + sgr(FG_P16_GREEN) + line, // fg Green, Underlined + sgr(INVERSE) + line, // fg Green, Underlined, Inverse + sgr(NO_INVERSE) + line, // fg Green, Underlined + sgr(INVERSE) + line, // fg Green, Underlined, Inverse + sgr(BG_P16_YELLOW) + line, // fg Green, bg Yellow, Underlined, Inverse + sgr(FG_RESET) + line, // bg Yellow, Underlined, Inverse + sgr(BG_RESET) + line, // Underlined, Inverse + sgr(NORMAL) + line // Back to normal ]; await writeSync(page, lines.join('\\r\\n')); assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); @@ -156,19 +243,19 @@ describe('SerializeAddon', () => { const cols = 10; const line = '+'.repeat(cols); const lines: string[] = [ - mkSGR(FG_P16_RED) + line, // fg Red - mkSGR(FG_P16_GREEN, BG_P16_YELLOW) + line, // fg Green, bg Yellow - mkSGR(UNDERLINED, ITALIC) + line, // fg Green, bg Yellow, Underlined, Italic - mkSGR(NO_UNDERLINED, NO_ITALIC) + line, // fg Green, bg Yellow - mkSGR(FG_RESET, ITALIC) + line, // bg Yellow, Italic - mkSGR(BG_RESET) + line, // Italic - mkSGR(NORMAL) + line, // Back to normal - mkSGR(FG_P16_RED) + line, // fg Red - mkSGR(FG_P16_GREEN, BG_P16_YELLOW) + line, // fg Green, bg Yellow - mkSGR(UNDERLINED, ITALIC) + line, // fg Green, bg Yellow, Underlined, Italic - mkSGR(NO_UNDERLINED, NO_ITALIC) + line, // fg Green, bg Yellow - mkSGR(FG_RESET, ITALIC) + line, // bg Yellow, Italic - mkSGR(BG_RESET) + line // Italic + sgr(FG_P16_RED) + line, // fg Red + sgr(FG_P16_GREEN, BG_P16_YELLOW) + line, // fg Green, bg Yellow + sgr(UNDERLINED, ITALIC) + line, // fg Green, bg Yellow, Underlined, Italic + sgr(NO_UNDERLINED, NO_ITALIC) + line, // fg Green, bg Yellow + sgr(FG_RESET, ITALIC) + line, // bg Yellow, Italic + sgr(BG_RESET) + line, // Italic + sgr(NORMAL) + line, // Back to normal + sgr(FG_P16_RED) + line, // fg Red + sgr(FG_P16_GREEN, BG_P16_YELLOW) + line, // fg Green, bg Yellow + sgr(UNDERLINED, ITALIC) + line, // fg Green, bg Yellow, Underlined, Italic + sgr(NO_UNDERLINED, NO_ITALIC) + line, // fg Green, bg Yellow + sgr(FG_RESET, ITALIC) + line, // bg Yellow, Italic + sgr(BG_RESET) + line // Italic ]; await writeSync(page, lines.join('\\r\\n')); assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); @@ -178,16 +265,16 @@ describe('SerializeAddon', () => { const cols = 10; const line = '+'.repeat(cols); const lines: string[] = [ - mkSGR(FG_P256_RED) + line, // fg Red 256, - mkSGR(UNDERLINED) + line, // fg Red 256, Underlined - mkSGR(FG_P256_GREEN) + line, // fg Green 256, Underlined - mkSGR(INVERSE) + line, // fg Green 256, Underlined, Inverse - mkSGR(NO_INVERSE) + line, // fg Green 256, Underlined - mkSGR(INVERSE) + line, // fg Green 256, Underlined, Inverse - mkSGR(BG_P256_YELLOW) + line, // fg Green 256, bg Yellow 256, Underlined, Inverse - mkSGR(FG_RESET) + line, // bg Yellow 256, Underlined, Inverse - mkSGR(BG_RESET) + line, // Underlined, Inverse - mkSGR(NORMAL) + line // Back to normal + sgr(FG_P256_RED) + line, // fg Red 256, + sgr(UNDERLINED) + line, // fg Red 256, Underlined + sgr(FG_P256_GREEN) + line, // fg Green 256, Underlined + sgr(INVERSE) + line, // fg Green 256, Underlined, Inverse + sgr(NO_INVERSE) + line, // fg Green 256, Underlined + sgr(INVERSE) + line, // fg Green 256, Underlined, Inverse + sgr(BG_P256_YELLOW) + line, // fg Green 256, bg Yellow 256, Underlined, Inverse + sgr(FG_RESET) + line, // bg Yellow 256, Underlined, Inverse + sgr(BG_RESET) + line, // Underlined, Inverse + sgr(NORMAL) + line // Back to normal ]; await writeSync(page, lines.join('\\r\\n')); assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); @@ -197,19 +284,19 @@ describe('SerializeAddon', () => { const cols = 10; const line = '+'.repeat(cols); const lines: string[] = [ - mkSGR(FG_P256_RED) + line, // fg Red 256 - mkSGR(FG_P256_GREEN, BG_P256_YELLOW) + line, // fg Green 256, bg Yellow 256 - mkSGR(UNDERLINED, ITALIC) + line, // fg Green 256, bg Yellow 256, Underlined, Italic - mkSGR(NO_UNDERLINED, NO_ITALIC) + line, // fg Green 256, bg Yellow 256 - mkSGR(FG_RESET, ITALIC) + line, // bg Yellow 256, Italic - mkSGR(BG_RESET) + line, // Italic - mkSGR(NORMAL) + line, // Back to normal - mkSGR(FG_P256_RED) + line, // fg Red 256 - mkSGR(FG_P256_GREEN, BG_P256_YELLOW) + line, // fg Green 256, bg Yellow 256 - mkSGR(UNDERLINED, ITALIC) + line, // fg Green 256, bg Yellow 256, Underlined, Italic - mkSGR(NO_UNDERLINED, NO_ITALIC) + line, // fg Green 256, bg Yellow 256 - mkSGR(FG_RESET, ITALIC) + line, // bg Yellow 256, Italic - mkSGR(BG_RESET) + line // Italic + sgr(FG_P256_RED) + line, // fg Red 256 + sgr(FG_P256_GREEN, BG_P256_YELLOW) + line, // fg Green 256, bg Yellow 256 + sgr(UNDERLINED, ITALIC) + line, // fg Green 256, bg Yellow 256, Underlined, Italic + sgr(NO_UNDERLINED, NO_ITALIC) + line, // fg Green 256, bg Yellow 256 + sgr(FG_RESET, ITALIC) + line, // bg Yellow 256, Italic + sgr(BG_RESET) + line, // Italic + sgr(NORMAL) + line, // Back to normal + sgr(FG_P256_RED) + line, // fg Red 256 + sgr(FG_P256_GREEN, BG_P256_YELLOW) + line, // fg Green 256, bg Yellow 256 + sgr(UNDERLINED, ITALIC) + line, // fg Green 256, bg Yellow 256, Underlined, Italic + sgr(NO_UNDERLINED, NO_ITALIC) + line, // fg Green 256, bg Yellow 256 + sgr(FG_RESET, ITALIC) + line, // bg Yellow 256, Italic + sgr(BG_RESET) + line // Italic ]; await writeSync(page, lines.join('\\r\\n')); assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); @@ -219,16 +306,16 @@ describe('SerializeAddon', () => { const cols = 10; const line = '+'.repeat(cols); const lines: string[] = [ - mkSGR(FG_RGB_RED) + line, // fg Red RGB, - mkSGR(UNDERLINED) + line, // fg Red RGB, Underlined - mkSGR(FG_RGB_GREEN) + line, // fg Green RGB, Underlined - mkSGR(INVERSE) + line, // fg Green RGB, Underlined, Inverse - mkSGR(NO_INVERSE) + line, // fg Green RGB, Underlined - mkSGR(INVERSE) + line, // fg Green RGB, Underlined, Inverse - mkSGR(BG_RGB_YELLOW) + line, // fg Green RGB, bg Yellow RGB, Underlined, Inverse - mkSGR(FG_RESET) + line, // bg Yellow RGB, Underlined, Inverse - mkSGR(BG_RESET) + line, // Underlined, Inverse - mkSGR(NORMAL) + line // Back to normal + sgr(FG_RGB_RED) + line, // fg Red RGB, + sgr(UNDERLINED) + line, // fg Red RGB, Underlined + sgr(FG_RGB_GREEN) + line, // fg Green RGB, Underlined + sgr(INVERSE) + line, // fg Green RGB, Underlined, Inverse + sgr(NO_INVERSE) + line, // fg Green RGB, Underlined + sgr(INVERSE) + line, // fg Green RGB, Underlined, Inverse + sgr(BG_RGB_YELLOW) + line, // fg Green RGB, bg Yellow RGB, Underlined, Inverse + sgr(FG_RESET) + line, // bg Yellow RGB, Underlined, Inverse + sgr(BG_RESET) + line, // Underlined, Inverse + sgr(NORMAL) + line // Back to normal ]; await writeSync(page, lines.join('\\r\\n')); assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); @@ -238,19 +325,19 @@ describe('SerializeAddon', () => { const cols = 10; const line = '+'.repeat(cols); const lines: string[] = [ - mkSGR(FG_RGB_RED) + line, // fg Red RGB - mkSGR(FG_RGB_GREEN, BG_RGB_YELLOW) + line, // fg Green RGB, bg Yellow RGB - mkSGR(UNDERLINED, ITALIC) + line, // fg Green RGB, bg Yellow RGB, Underlined, Italic - mkSGR(NO_UNDERLINED, NO_ITALIC) + line, // fg Green RGB, bg Yellow RGB - mkSGR(FG_RESET, ITALIC) + line, // bg Yellow RGB, Italic - mkSGR(BG_RESET) + line, // Italic - mkSGR(NORMAL) + line, // Back to normal - mkSGR(FG_RGB_RED) + line, // fg Red RGB - mkSGR(FG_RGB_GREEN, BG_RGB_YELLOW) + line, // fg Green RGB, bg Yellow RGB - mkSGR(UNDERLINED, ITALIC) + line, // fg Green RGB, bg Yellow RGB, Underlined, Italic - mkSGR(NO_UNDERLINED, NO_ITALIC) + line, // fg Green RGB, bg Yellow RGB - mkSGR(FG_RESET, ITALIC) + line, // bg Yellow RGB, Italic - mkSGR(BG_RESET) + line // Italic + sgr(FG_RGB_RED) + line, // fg Red RGB + sgr(FG_RGB_GREEN, BG_RGB_YELLOW) + line, // fg Green RGB, bg Yellow RGB + sgr(UNDERLINED, ITALIC) + line, // fg Green RGB, bg Yellow RGB, Underlined, Italic + sgr(NO_UNDERLINED, NO_ITALIC) + line, // fg Green RGB, bg Yellow RGB + sgr(FG_RESET, ITALIC) + line, // bg Yellow RGB, Italic + sgr(BG_RESET) + line, // Italic + sgr(NORMAL) + line, // Back to normal + sgr(FG_RGB_RED) + line, // fg Red RGB + sgr(FG_RGB_GREEN, BG_RGB_YELLOW) + line, // fg Green RGB, bg Yellow RGB + sgr(UNDERLINED, ITALIC) + line, // fg Green RGB, bg Yellow RGB, Underlined, Italic + sgr(NO_UNDERLINED, NO_ITALIC) + line, // fg Green RGB, bg Yellow RGB + sgr(FG_RESET, ITALIC) + line, // bg Yellow RGB, Italic + sgr(BG_RESET) + line // Italic ]; await writeSync(page, lines.join('\\r\\n')); assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); @@ -276,17 +363,15 @@ describe('SerializeAddon', () => { '中文中文', '12中文', '中文12', - '1中文中文中' // this line is going to be wrapped at last character because it has line length of 11 (1+2*5) - ]; - const expected = [ - '中文中文', - '12中文', - '中文12', - '1中文中文', - '中' + // This line is going to be wrapped at last character + // because it has line length of 11 (1+2*5). + // We concat it back without the null cell currently. + // But this may be incorrect. + // see also #3097 + '1中文中文中' ]; await writeSync(page, lines.join('\\r\\n')); - assert.equal(await page.evaluate(`serializeAddon.serialize();`), expected.join('\r\n')); + assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); }); it('serialize CJK Mixed with tab correctly', async () => { @@ -299,15 +384,173 @@ describe('SerializeAddon', () => { await writeSync(page, lines.join('\\r\\n')); assert.equal(await page.evaluate(`serializeAddon.serialize();`), expected.join('\r\n')); }); + + it('serialize with alt screen correctly', async () => { + const SMCUP = '\u001b[?1049h'; + const CUP = '\u001b[H'; + + const lines = [ + `1${SMCUP}${CUP}2` + ]; + const expected = [ + `1${SMCUP}${CUP}2` + ]; + + await writeSync(page, lines.join('\\r\\n')); + assert.equal(await page.evaluate(`window.term.buffer.active.type`), 'alternate'); + assert.equal(JSON.stringify(await page.evaluate(`serializeAddon.serialize();`)), JSON.stringify(expected.join('\r\n'))); + }); + + it('serialize without alt screen correctly', async () => { + const SMCUP = '\u001b[?1049h'; + const RMCUP = '\u001b[?1049l'; + + const lines = [ + `1${SMCUP}2${RMCUP}` + ]; + const expected = [ + `1` + ]; + + await writeSync(page, lines.join('\\r\\n')); + assert.equal(await page.evaluate(`window.term.buffer.active.type`), 'normal'); + assert.equal(JSON.stringify(await page.evaluate(`serializeAddon.serialize();`)), JSON.stringify(expected.join('\r\n'))); + }); + + it('serialize with background', async () => { + const CLEAR_RIGHT = (l: number): string => `\u001b[${l}X`; + + const lines = [ + `1\u001b[44m${CLEAR_RIGHT(5)}`, + `2${CLEAR_RIGHT(9)}` + ]; + + await testNormalScreenEqual(page, lines.join('\r\n')); + }); + + it('cause the BCE on scroll', async () => { + const CLEAR_RIGHT = (l: number): string => `\u001b[${l}X`; + + const padLines = newArray( + (index: number) => digitsString(10, index), + 10 + ); + + const lines = [ + ...padLines, + `\u001b[44m${CLEAR_RIGHT(5)}1111111111111111` + ]; + + await testNormalScreenEqual(page, lines.join('\r\n')); + }); + + it('handle invalid wrap before scroll', async () => { + const CLEAR_RIGHT = (l: number): string => `\u001b[${l}X`; + const MOVE_UP = (l: number): string => `\u001b[${l}A`; + const MOVE_DOWN = (l: number): string => `\u001b[${l}B`; + const MOVE_LEFT = (l: number): string => `\u001b[${l}D`; + + // A line wrap happened after current line. + // But there is no content. + // so wrap shouldn't even be able to happen. + const segments = [ + `123456789012345`, + MOVE_UP(1), + CLEAR_RIGHT(5), + MOVE_DOWN(1), + MOVE_LEFT(5), + CLEAR_RIGHT(5), + MOVE_UP(1), + '1' + ]; + + await testNormalScreenEqual(page, segments.join('')); + }); + + it('handle invalid wrap after scroll', async () => { + const CLEAR_RIGHT = (l: number): string => `\u001b[${l}X`; + const MOVE_UP = (l: number): string => `\u001b[${l}A`; + const MOVE_DOWN = (l: number): string => `\u001b[${l}B`; + const MOVE_LEFT = (l: number): string => `\u001b[${l}D`; + + const padLines = newArray( + (index: number) => digitsString(10, index), + 10 + ); + + // A line wrap happened after current line. + // But there is no content. + // so wrap shouldn't even be able to happen. + const lines = [ + padLines.join('\r\n'), + '\r\n', + `123456789012345`, + MOVE_UP(1), + CLEAR_RIGHT(5), + MOVE_DOWN(1), + MOVE_LEFT(5), + CLEAR_RIGHT(5), + MOVE_UP(1), + '1' + ]; + + await testNormalScreenEqual(page, lines.join('')); + }); + + describe('handle modes', () => { + it('applicationCursorKeysMode', async () => { + await testSerializeEquals('test\u001b[?1h', 'test\u001b[?1h'); + await testSerializeEquals('\u001b[?1l', 'test'); + }); + it('applicationKeypadMode', async () => { + await testSerializeEquals('test\u001b[?66h', 'test\u001b[?66h'); + await testSerializeEquals('\u001b[?66l', 'test'); + }); + it('bracketedPasteMode', async () => { + await testSerializeEquals('test\u001b[?2004h', 'test\u001b[?2004h'); + await testSerializeEquals('\u001b[?2004l', 'test'); + }); + it('insertMode', async () => { + await testSerializeEquals('test\u001b[4h', 'test\u001b[4h'); + await testSerializeEquals('\u001b[4l', 'test'); + }); + it('mouseTrackingMode', async () => { + await testSerializeEquals('test\u001b[?9h', 'test\u001b[?9h'); + await testSerializeEquals('\u001b[?9l', 'test'); + await testSerializeEquals('\u001b[?1000h', 'test\u001b[?1000h'); + await testSerializeEquals('\u001b[?1000l', 'test'); + await testSerializeEquals('\u001b[?1002h', 'test\u001b[?1002h'); + await testSerializeEquals('\u001b[?1002l', 'test'); + await testSerializeEquals('\u001b[?1003h', 'test\u001b[?1003h'); + await testSerializeEquals('\u001b[?1003l', 'test'); + }); + it('originMode', async () => { + // origin mode moves cursor to (0,0) + await testSerializeEquals('test\u001b[?6h', 'test\u001b[4D\u001b[?6h'); + await testSerializeEquals('\u001b[?6l', 'test\u001b[4D'); + }); + it('reverseWraparoundMode', async () => { + await testSerializeEquals('test\u001b[?45h', 'test\u001b[?45h'); + await testSerializeEquals('\u001b[?45l', 'test'); + }); + it('sendFocusMode', async () => { + await testSerializeEquals('test\u001b[?1004h', 'test\u001b[?1004h'); + await testSerializeEquals('\u001b[?1004l', 'test'); + }); + it('wraparoundMode', async () => { + await testSerializeEquals('test\u001b[?7l', 'test\u001b[?7l'); + await testSerializeEquals('\u001b[?7h', 'test'); + }); + }); }); function newArray(initial: T | ((index: number) => T), count: number): T[] { const array: T[] = new Array(count); for (let i = 0; i < array.length; i++) { if (typeof initial === 'function') { - array[i] = (<(index: number) => T>initial)(i); + array[i] = (initial as (index: number) => T)(i); } else { - array[i] = initial; + array[i] = initial as T; } } return array; @@ -321,7 +564,7 @@ function digitsString(length: number, from: number = 0, sgr: string = ''): strin return s; } -function mkSGR(...seq: string[]): string { +function sgr(...seq: string[]): string { return `\x1b[${seq.join(';')}m`; } @@ -350,20 +593,20 @@ const BG_RGB_GREEN = '48;2;0;255;0'; const BG_RGB_YELLOW = '48;2;255;255;0'; const BG_RESET = '49'; -const INVERSE = '7'; const BOLD = '1'; +const DIM = '2'; +const ITALIC = '3'; const UNDERLINED = '4'; const BLINK = '5'; +const INVERSE = '7'; const INVISIBLE = '8'; +const STRIKETHROUGH = '9'; -const NO_INVERSE = '27'; const NO_BOLD = '22'; +const NO_DIM = '22'; +const NO_ITALIC = '23'; const NO_UNDERLINED = '24'; const NO_BLINK = '25'; +const NO_INVERSE = '27'; const NO_INVISIBLE = '28'; - -const ITALIC = '3'; -const DIM = '2'; - -const NO_ITALIC = '23'; -const NO_DIM = '22'; +const NO_STRIKETHROUGH = '29'; diff --git a/addons/xterm-addon-serialize/typings/xterm-addon-serialize.d.ts b/addons/xterm-addon-serialize/typings/xterm-addon-serialize.d.ts index 9e7b500a..a29dbb28 100644 --- a/addons/xterm-addon-serialize/typings/xterm-addon-serialize.d.ts +++ b/addons/xterm-addon-serialize/typings/xterm-addon-serialize.d.ts @@ -3,36 +3,53 @@ * @license MIT */ - import { Terminal, ITerminalAddon } from 'xterm'; declare module 'xterm-addon-serialize' { /** - * An xterm.js addon that enables web links. + * An xterm.js addon that enables serialization of terminal contents. */ export class SerializeAddon implements ITerminalAddon { constructor(); /** - * Activates the addon + * Activates the addon. * @param terminal The terminal the addon is being loaded in. */ public activate(terminal: Terminal): void; /** - * Serializes terminal rows into a string that can be written back to the terminal - * to restore the state. The cursor will also be positioned to the correct cell. - * When restoring a terminal it is best to do before `Terminal.open` is called - * to avoid wasting CPU cycles rendering incomplete frames. - * @param rows The number of rows to serialize, starting from the bottom of the - * terminal. This defaults to the number of rows in the viewport. + * Serializes terminal rows into a string that can be written back to the terminal to restore + * the state. The cursor will also be positioned to the correct cell. When restoring a terminal + * it is best to do before `Terminal.open` is called to avoid wasting CPU cycles rendering + * incomplete frames. + * @param options Custom options to allow control over what gets serialized. */ - public serialize(rows?: number): string; + public serialize(options?: ISerializeOptions): string; /** * Disposes the addon. */ public dispose(): void; } + + export interface ISerializeOptions { + /** + * The number of rows in the scrollback buffer to serialize, starting from the bottom of the + * scrollback buffer. When not specified, all available rows in the scrollback buffer will be + * serialized. + */ + scrollback?: number; + + /** + * Whether to exclude the terminal modes from the serialization. False by default. + */ + excludeModes?: boolean; + + /** + * Whether to exclude the alt buffer from the serialization. False by default. + */ + excludeAltBuffer?: boolean; + } } diff --git a/addons/xterm-addon-serialize/webpack.config.js b/addons/xterm-addon-serialize/webpack.config.js index 4cabbad9..7c0ccd01 100644 --- a/addons/xterm-addon-serialize/webpack.config.js +++ b/addons/xterm-addon-serialize/webpack.config.js @@ -25,7 +25,8 @@ module.exports = { filename: mainFile, path: path.resolve('./lib'), library: addonName, - libraryTarget: 'umd' + libraryTarget: 'umd', + globalObject: 'this' }, mode: 'production' }; diff --git a/addons/xterm-addon-unicode11/package.json b/addons/xterm-addon-unicode11/package.json index 397bf2b9..9fc69416 100644 --- a/addons/xterm-addon-unicode11/package.json +++ b/addons/xterm-addon-unicode11/package.json @@ -1,6 +1,6 @@ { "name": "xterm-addon-unicode11", - "version": "0.2.0", + "version": "0.3.0", "author": { "name": "The xterm.js authors", "url": "https://xtermjs.org/" diff --git a/addons/xterm-addon-unicode11/src/Unicode11Addon.ts b/addons/xterm-addon-unicode11/src/Unicode11Addon.ts index cb8c9c56..d4ddf77e 100644 --- a/addons/xterm-addon-unicode11/src/Unicode11Addon.ts +++ b/addons/xterm-addon-unicode11/src/Unicode11Addon.ts @@ -8,6 +8,7 @@ import { Terminal, ITerminalAddon } from 'xterm'; import { UnicodeV11 } from './UnicodeV11'; + export class Unicode11Addon implements ITerminalAddon { public activate(terminal: Terminal): void { terminal.unicode.register(new UnicodeV11()); diff --git a/addons/xterm-addon-unicode11/test/Unicode11Addon.api.ts b/addons/xterm-addon-unicode11/test/Unicode11Addon.api.ts index d0e09da2..4c695b00 100644 --- a/addons/xterm-addon-unicode11/test/Unicode11Addon.api.ts +++ b/addons/xterm-addon-unicode11/test/Unicode11Addon.api.ts @@ -4,10 +4,10 @@ */ import { assert } from 'chai'; -import { openTerminal, getBrowserType } from '../../../out-test/api/TestUtils'; +import { openTerminal, launchBrowser } from '../../../out-test/api/TestUtils'; import { Browser, Page } from 'playwright'; -const APP = 'http://127.0.0.1:3000/test'; +const APP = 'http://127.0.0.1:3001/test'; let browser: Browser; let page: Page; @@ -16,10 +16,7 @@ const height = 600; describe('Unicode11Addon', () => { before(async function(): Promise { - const browserType = getBrowserType(); - browser = await browserType.launch({ - headless: process.argv.indexOf('--headless') !== -1 - }); + browser = await launchBrowser(); page = await (await browser.newContext()).newPage(); await page.setViewportSize({ width, height }); }); diff --git a/addons/xterm-addon-unicode11/webpack.config.js b/addons/xterm-addon-unicode11/webpack.config.js index 66a83a2f..10c0adc3 100644 --- a/addons/xterm-addon-unicode11/webpack.config.js +++ b/addons/xterm-addon-unicode11/webpack.config.js @@ -32,7 +32,8 @@ module.exports = { filename: mainFile, path: path.resolve('./lib'), library: addonName, - libraryTarget: 'umd' + libraryTarget: 'umd', + globalObject: 'this' }, mode: 'production' }; diff --git a/addons/xterm-addon-web-links/src/WebLinkProvider.ts b/addons/xterm-addon-web-links/src/WebLinkProvider.ts index af6ad7d3..f0caf974 100644 --- a/addons/xterm-addon-web-links/src/WebLinkProvider.ts +++ b/addons/xterm-addon-web-links/src/WebLinkProvider.ts @@ -3,25 +3,45 @@ * @license MIT */ -import { ILinkProvider, IBufferCellPosition, ILink, Terminal } from 'xterm'; +import { ILinkProvider, ILink, Terminal, IViewportRange } from 'xterm'; + +interface ILinkProviderOptions { + hover?(event: MouseEvent, text: string, location: IViewportRange): void; + leave?(event: MouseEvent, text: string): void; +} export class WebLinkProvider implements ILinkProvider { constructor( private readonly _terminal: Terminal, private readonly _regex: RegExp, - private readonly _handler: (event: MouseEvent, uri: string) => void + private readonly _handler: (event: MouseEvent, uri: string) => void, + private readonly _options: ILinkProviderOptions = {} ) { } public provideLinks(y: number, callback: (links: ILink[] | undefined) => void): void { - callback(LinkComputer.computeLink(y, this._regex, this._terminal, this._handler)); + const links = LinkComputer.computeLink(y, this._regex, this._terminal, this._handler); + callback(this._addCallbacks(links)); + } + + private _addCallbacks(links: ILink[]): ILink[] { + return links.map(link => { + link.leave = this._options.leave; + link.hover = (event: MouseEvent, uri: string): void => { + if (this._options.hover) { + const { range } = link; + this._options.hover(event, uri, range); + } + }; + return link; + }); } } export class LinkComputer { - public static computeLink(y: number, regex: RegExp, terminal: Terminal, handler: (event: MouseEvent, uri: string) => void): ILink[] { + public static computeLink(y: number, regex: RegExp, terminal: Terminal, activate: (event: MouseEvent, uri: string) => void): ILink[] { const rex = new RegExp(regex.source, (regex.flags || '') + 'g'); const [line, startLineIndex] = LinkComputer._translateBufferLineToStringWithWrap(y - 1, false, terminal); @@ -69,7 +89,7 @@ export class LinkComputer { } }; - result.push({ range, text, activate: handler }); + result.push({ range, text, activate }); } return result; diff --git a/addons/xterm-addon-web-links/src/WebLinksAddon.ts b/addons/xterm-addon-web-links/src/WebLinksAddon.ts index dce405a5..46ddcf7b 100644 --- a/addons/xterm-addon-web-links/src/WebLinksAddon.ts +++ b/addons/xterm-addon-web-links/src/WebLinksAddon.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { Terminal, ILinkMatcherOptions, ITerminalAddon, IDisposable } from 'xterm'; +import { Terminal, ILinkMatcherOptions, ITerminalAddon, IDisposable, IViewportRange } from 'xterm'; import { WebLinkProvider } from './WebLinkProvider'; const protocolClause = '(https?:\\/\\/)'; @@ -36,6 +36,11 @@ function handleLink(event: MouseEvent, uri: string): void { } } +interface ILinkProviderOptions { + hover?(event: MouseEvent, text: string, location: IViewportRange): void; + leave?(event: MouseEvent, text: string): void; +} + export class WebLinksAddon implements ITerminalAddon { private _linkMatcherId: number | undefined; private _terminal: Terminal | undefined; @@ -43,20 +48,22 @@ export class WebLinksAddon implements ITerminalAddon { constructor( private _handler: (event: MouseEvent, uri: string) => void = handleLink, - private _options: ILinkMatcherOptions = {}, + private _options: ILinkMatcherOptions | ILinkProviderOptions = {}, private _useLinkProvider: boolean = false ) { - this._options.matchIndex = 1; } public activate(terminal: Terminal): void { this._terminal = terminal; if (this._useLinkProvider && 'registerLinkProvider' in this._terminal) { - this._linkProvider = this._terminal.registerLinkProvider(new WebLinkProvider(this._terminal, strictUrlRegex, this._handler)); + const options = this._options as ILinkProviderOptions; + this._linkProvider = this._terminal.registerLinkProvider(new WebLinkProvider(this._terminal, strictUrlRegex, this._handler, options)); } else { // TODO: This should be removed eventually - this._linkMatcherId = (this._terminal as Terminal).registerLinkMatcher(strictUrlRegex, this._handler, this._options); + const options = this._options as ILinkMatcherOptions; + options.matchIndex = 1; + this._linkMatcherId = (this._terminal as Terminal).registerLinkMatcher(strictUrlRegex, this._handler, options); } } diff --git a/addons/xterm-addon-web-links/test/WebLinksAddon.api.ts b/addons/xterm-addon-web-links/test/WebLinksAddon.api.ts index 6e42ff96..fe44dc31 100644 --- a/addons/xterm-addon-web-links/test/WebLinksAddon.api.ts +++ b/addons/xterm-addon-web-links/test/WebLinksAddon.api.ts @@ -4,10 +4,10 @@ */ import { assert } from 'chai'; -import { openTerminal, pollFor, writeSync, getBrowserType } from '../../../out-test/api/TestUtils'; +import { openTerminal, pollFor, writeSync, launchBrowser } from '../../../out-test/api/TestUtils'; import { Browser, Page } from 'playwright'; -const APP = 'http://127.0.0.1:3000/test'; +const APP = 'http://127.0.0.1:3001/test'; let browser: Browser; let page: Page; @@ -16,10 +16,7 @@ const height = 600; describe('WebLinksAddon', () => { before(async function(): Promise { - const browserType = getBrowserType(); - browser = await browserType.launch({ - headless: process.argv.indexOf('--headless') !== -1 - }); + browser = await launchBrowser(); page = await (await browser.newContext()).newPage(); await page.setViewportSize({ width, height }); }); diff --git a/addons/xterm-addon-web-links/typings/xterm-addon-web-links.d.ts b/addons/xterm-addon-web-links/typings/xterm-addon-web-links.d.ts index f0564704..78a258e5 100644 --- a/addons/xterm-addon-web-links/typings/xterm-addon-web-links.d.ts +++ b/addons/xterm-addon-web-links/typings/xterm-addon-web-links.d.ts @@ -4,7 +4,7 @@ */ -import { Terminal, ILinkMatcherOptions, ITerminalAddon } from 'xterm'; +import { Terminal, ILinkMatcherOptions, ITerminalAddon, IViewportRange } from 'xterm'; declare module 'xterm-addon-web-links' { /** @@ -20,7 +20,7 @@ declare module 'xterm-addon-web-links' { * link provider (new) may cause issues. Link provider will eventually be * the default and only option. */ - constructor(handler?: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions, useLinkProvider?: boolean); + constructor(handler?: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions | ILinkProviderOptions, useLinkProvider?: boolean); /** * Activates the addon @@ -33,4 +33,21 @@ declare module 'xterm-addon-web-links' { */ public dispose(): void; } + + /** + * An object containing options for a link provider. + */ + export interface ILinkProviderOptions { + /** + * A callback that fires when the mouse hovers over a link for a period of + * time (defined by {@link ITerminalOptions.linkTooltipHoverDuration}). + */ + hover?(event: MouseEvent, text: string, location: IViewportRange): void; + + /** + * A callback that fires when the mouse leaves a link. Note that this can + * happen even when tooltipCallback hasn't fired for the link yet. + */ + leave?(event: MouseEvent, text: string): void; + } } diff --git a/addons/xterm-addon-webgl/README.md b/addons/xterm-addon-webgl/README.md index 026a738e..67fafe7b 100644 --- a/addons/xterm-addon-webgl/README.md +++ b/addons/xterm-addon-webgl/README.md @@ -1,8 +1,6 @@ ## xterm-addon-webgl -An addon for [xterm.js](https://github.com/xtermjs/xterm.js) that enables a WebGL-based renderer. This addon requires xterm.js v4+. - -⚠️ This is an experimental addon that is [missing some features and may be unstable](https://github.com/xtermjs/xterm.js/issues?q=is%3Aopen+is%3Aissue+label%3Aarea%2Faddon%2Fwebgl) ⚠️ +An addon for [xterm.js](https://github.com/xtermjs/xterm.js) that enables a WebGL2-based renderer. This addon requires xterm.js v4+. ### Install @@ -21,3 +19,18 @@ terminal.loadAddon(new WebglAddon()); ``` See the full [API](https://github.com/xtermjs/xterm.js/blob/master/addons/xterm-addon-webgl/typings/xterm-addon-webgl.d.ts) for more advanced usage. + +### Handling Context Loss + +The browser may drop WebGL contexts for various reasons like OOM or after the system has been suspended. There is an API exposed that fires the `webglcontextlost` event fired on the canvas so embedders can handle it however they wish. An easy, but suboptimal way, to handle this is by disposing of WebglAddon when the event fires: + +```ts +const terminal = new Terminal(); +const addon = new WebglAddon(); +addon.onContextLoss(e => { + addon.dispose(); +}); +terminal.loadAddon(addon); +``` + +Read more about handling WebGL context losses on the [Khronos wiki](https://www.khronos.org/webgl/wiki/HandlingContextLost). diff --git a/addons/xterm-addon-webgl/package.json b/addons/xterm-addon-webgl/package.json index c0e91b37..226f34ba 100644 --- a/addons/xterm-addon-webgl/package.json +++ b/addons/xterm-addon-webgl/package.json @@ -1,6 +1,6 @@ { "name": "xterm-addon-webgl", - "version": "0.9.0", + "version": "0.11.2", "author": { "name": "The xterm.js authors", "url": "https://xtermjs.org/" diff --git a/addons/xterm-addon-webgl/src/GlyphRenderer.ts b/addons/xterm-addon-webgl/src/GlyphRenderer.ts index 8be4c011..71b3659e 100644 --- a/addons/xterm-addon-webgl/src/GlyphRenderer.ts +++ b/addons/xterm-addon-webgl/src/GlyphRenderer.ts @@ -176,8 +176,9 @@ export class GlyphRenderer { const i = (y * terminal.cols + x) * INDICES_PER_CELL; - // Exit early if this is a null/space character - if (code === NULL_CELL_CODE || code === WHITESPACE_CELL_CODE || code === undefined/* This is used for the right side of wide chars */) { + // Exit early if this is a null character, allow space character to continue as it may have + // underline/strikethrough styles + if (code === NULL_CELL_CODE || code === undefined/* This is used for the right side of wide chars */) { fill(array, 0, i, i + INDICES_PER_CELL - 1 - CELL_POSITION_INDICES); return; } @@ -263,7 +264,7 @@ export class GlyphRenderer { // Get attributes from fg (excluding inverse) and resolve inverse by pullibng rgb colors // from bg. This is needed since the inverse fg color should be based on the original bg // color, not on the selection color - fg = (fg & ~(Attributes.CM_MASK | Attributes.RGB_MASK | FgFlags.INVERSE)); + fg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK | FgFlags.INVERSE); switch (workCell.getBgColorMode()) { case Attributes.CM_P16: case Attributes.CM_P256: diff --git a/addons/xterm-addon-webgl/src/WebglAddon.ts b/addons/xterm-addon-webgl/src/WebglAddon.ts index aef1301e..ad2393d8 100644 --- a/addons/xterm-addon-webgl/src/WebglAddon.ts +++ b/addons/xterm-addon-webgl/src/WebglAddon.ts @@ -3,14 +3,17 @@ * @license MIT */ -import { Terminal, ITerminalAddon } from 'xterm'; +import { Terminal, ITerminalAddon, IEvent } from 'xterm'; import { WebglRenderer } from './WebglRenderer'; -import { IRenderService } from 'browser/services/Services'; +import { ICharacterJoinerService, IRenderService } from 'browser/services/Services'; import { IColorSet } from 'browser/Types'; +import { EventEmitter } from 'common/EventEmitter'; export class WebglAddon implements ITerminalAddon { private _terminal?: Terminal; private _renderer?: WebglRenderer; + private _onContextLoss = new EventEmitter(); + public get onContextLoss(): IEvent { return this._onContextLoss.event; } constructor( private _preserveDrawingBuffer?: boolean @@ -21,9 +24,11 @@ export class WebglAddon implements ITerminalAddon { throw new Error('Cannot activate WebglAddon before Terminal.open'); } this._terminal = terminal; - const renderService: IRenderService = (terminal)._core._renderService; - const colors: IColorSet = (terminal)._core._colorManager.colors; - this._renderer = new WebglRenderer(terminal, colors, this._preserveDrawingBuffer); + const renderService: IRenderService = (terminal as any)._core._renderService; + const characterJoinerService: ICharacterJoinerService = (terminal as any)._core._characterJoinerService; + const colors: IColorSet = (terminal as any)._core._colorManager.colors; + this._renderer = new WebglRenderer(terminal, colors, characterJoinerService, this._preserveDrawingBuffer); + this._renderer.onContextLoss(() => this._onContextLoss.fire()); renderService.setRenderer(this._renderer); } diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index 9f982165..9d8bde79 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -12,13 +12,17 @@ import { RectangleRenderer } from './RectangleRenderer'; import { IWebGL2RenderingContext } from './Types'; import { RenderModel, COMBINED_CHAR_BIT_MASK, RENDER_MODEL_BG_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel'; import { Disposable } from 'common/Lifecycle'; -import { NULL_CELL_CODE } from 'common/buffer/Constants'; +import { Content, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants'; import { Terminal, IEvent } from 'xterm'; import { IRenderLayer } from './renderLayer/Types'; import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/Types'; import { ITerminal, IColorSet } from 'browser/Types'; import { EventEmitter } from 'common/EventEmitter'; import { CellData } from 'common/buffer/CellData'; +import { addDisposableDomListener } from 'browser/Lifecycle'; +import { ICharacterJoinerService } from 'browser/services/Services'; +import { CharData, ICellData } from 'common/Types'; +import { AttributeData } from 'common/buffer/AttributeData'; export class WebglRenderer extends Disposable implements IRenderer { private _renderLayers: IRenderLayer[]; @@ -41,9 +45,13 @@ export class WebglRenderer extends Disposable implements IRenderer { private _onRequestRedraw = new EventEmitter(); public get onRequestRedraw(): IEvent { return this._onRequestRedraw.event; } + private _onContextLoss = new EventEmitter(); + public get onContextLoss(): IEvent { return this._onContextLoss.event; } + constructor( private _terminal: Terminal, private _colors: IColorSet, + private readonly _characterJoinerService: ICharacterJoinerService, preserveDrawingBuffer?: boolean ) { super(); @@ -52,7 +60,7 @@ export class WebglRenderer extends Disposable implements IRenderer { this._renderLayers = [ new LinkRenderLayer(this._core.screenElement!, 2, this._colors, this._core), - new CursorRenderLayer(this._core.screenElement!, 3, this._colors, this._onRequestRedraw) + new CursorRenderLayer(this._core.screenElement!, 3, this._colors, this._core, this._onRequestRedraw) ]; this.dimensions = { scaledCharWidth: 0, @@ -82,6 +90,9 @@ export class WebglRenderer extends Disposable implements IRenderer { if (!this._gl) { throw new Error('WebGL2 not supported ' + this._gl); } + + this.register(addDisposableDomListener(this._canvas, 'webglcontextlost', (e) => { this._onContextLoss.fire(e); })); + this._core.screenElement!.appendChild(this._canvas); this._rectangleRenderer = new RectangleRenderer(this._terminal, this._colors, this._gl, this.dimensions); @@ -94,8 +105,10 @@ export class WebglRenderer extends Disposable implements IRenderer { } public dispose(): void { - this._renderLayers.forEach(l => l.dispose()); - this._core.screenElement!.removeChild(this._canvas); + for (const l of this._renderLayers) { + l.dispose(); + } + this._canvas.parentElement?.removeChild(this._canvas); super.dispose(); } @@ -106,22 +119,18 @@ export class WebglRenderer extends Disposable implements IRenderer { public setColors(colors: IColorSet): void { this._colors = colors; // Clear layers and force a full render - this._renderLayers.forEach(l => { + for (const l of this._renderLayers) { l.setColors(this._terminal, this._colors); l.reset(this._terminal); - }); + } this._rectangleRenderer.setColors(); this._glyphRenderer.setColors(); this._refreshCharAtlas(); - this._rectangleRenderer.updateSelection(this._model.selection); - this._glyphRenderer.updateSelection(this._model); - // Force a full refresh this._model.clear(); - this._model.clearSelection(); } public onDevicePixelRatioChange(): void { @@ -138,10 +147,11 @@ export class WebglRenderer extends Disposable implements IRenderer { this._updateDimensions(); this._model.resize(this._terminal.cols, this._terminal.rows); - this._rectangleRenderer.onResize(); // Resize all render layers - this._renderLayers.forEach(l => l.resize(this._terminal, this.dimensions)); + for (const l of this._renderLayers) { + l.resize(this._terminal, this.dimensions); + } // Resize the canvas this._canvas.width = this.dimensions.scaledCanvasWidth; @@ -152,6 +162,13 @@ export class WebglRenderer extends Disposable implements IRenderer { // Resize the screen this._core.screenElement!.style.width = `${this.dimensions.canvasWidth}px`; this._core.screenElement!.style.height = `${this.dimensions.canvasHeight}px`; + + this._rectangleRenderer.onResize(); + if (this._model.selection.hasSelection) { + // Update selection as dimensions have changed + this._rectangleRenderer.updateSelection(this._model.selection); + } + this._glyphRenderer.setDimensions(this.dimensions); this._glyphRenderer.onResize(); @@ -159,7 +176,6 @@ export class WebglRenderer extends Disposable implements IRenderer { // Force a full refresh this._model.clear(); - this._model.clearSelection(); } public onCharSizeChanged(): void { @@ -167,30 +183,37 @@ export class WebglRenderer extends Disposable implements IRenderer { } public onBlur(): void { - this._renderLayers.forEach(l => l.onBlur(this._terminal)); + for (const l of this._renderLayers) { + l.onBlur(this._terminal); + } } public onFocus(): void { - this._renderLayers.forEach(l => l.onFocus(this._terminal)); + for (const l of this._renderLayers) { + l.onFocus(this._terminal); + } } public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void { - this._renderLayers.forEach(l => l.onSelectionChanged(this._terminal, start, end, columnSelectMode)); + for (const l of this._renderLayers) { + l.onSelectionChanged(this._terminal, start, end, columnSelectMode); + } this._updateSelectionModel(start, end, columnSelectMode); - this._rectangleRenderer.updateSelection(this._model.selection); - this._glyphRenderer.updateSelection(this._model); - this._onRequestRedraw.fire({ start: 0, end: this._terminal.rows - 1 }); } public onCursorMove(): void { - this._renderLayers.forEach(l => l.onCursorMove(this._terminal)); + for (const l of this._renderLayers) { + l.onCursorMove(this._terminal); + } } public onOptionsChanged(): void { - this._renderLayers.forEach(l => l.onOptionsChanged(this._terminal)); + for (const l of this._renderLayers) { + l.onOptionsChanged(this._terminal); + } this._updateDimensions(); this._refreshCharAtlas(); } @@ -207,7 +230,7 @@ export class WebglRenderer extends Disposable implements IRenderer { return; } - const atlas = acquireCharAtlas(this._terminal, this._colors, this.dimensions.scaledCharWidth, this.dimensions.scaledCharHeight); + const atlas = acquireCharAtlas(this._terminal, this._colors, this.dimensions.scaledCellWidth, this.dimensions.scaledCellHeight, this.dimensions.scaledCharWidth, this.dimensions.scaledCharHeight); if (!('getRasterizedGlyph' in atlas)) { throw new Error('The webgl renderer only works with the webgl char atlas'); } @@ -220,12 +243,13 @@ export class WebglRenderer extends Disposable implements IRenderer { this._charAtlas?.clearTexture(); this._model.clear(); this._updateModel(0, this._terminal.rows - 1); - this._glyphRenderer.updateSelection(this._model); this._onRequestRedraw.fire({ start: 0, end: this._terminal.rows - 1 }); } public clear(): void { - this._renderLayers.forEach(l => l.reset(this._terminal)); + for (const l of this._renderLayers) { + l.reset(this._terminal); + } } public registerCharacterJoiner(handler: (text: string) => [number, number][]): number { @@ -248,12 +272,14 @@ export class WebglRenderer extends Disposable implements IRenderer { } // Update render layers - this._renderLayers.forEach(l => l.onGridChanged(this._terminal, start, end)); + for (const l of this._renderLayers) { + l.onGridChanged(this._terminal, start, end); + } // Tell renderer the frame is beginning if (this._glyphRenderer.beginFrame()) { this._model.clear(); - this._model.clearSelection(); + this._updateSelectionModel(undefined, undefined); } // Update model to reflect what's drawn @@ -266,16 +292,41 @@ export class WebglRenderer extends Disposable implements IRenderer { private _updateModel(start: number, end: number): void { const terminal = this._core; + let cell: ICellData = this._workCell; for (let y = start; y <= end; y++) { const row = y + terminal.buffer.ydisp; const line = terminal.buffer.lines.get(row)!; this._model.lineLengths[y] = 0; + const joinedRanges = this._characterJoinerService.getJoinedCharacters(row); for (let x = 0; x < terminal.cols; x++) { - line.loadCell(x, this._workCell); + line.loadCell(x, cell); - const chars = this._workCell.getChars(); - let code = this._workCell.getCode(); + // If true, indicates that the current character(s) to draw were joined. + let isJoined = false; + let lastCharX = x; + + // Process any joined character ranges as needed. Because of how the + // ranges are produced, we know that they are valid for the characters + // 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 + cell = new JoinedCellData( + cell, + line!.translateToString(true, range[0], range[1]), + range[1] - range[0] + ); + + // Skip over the cells occupied by this range in the loop + lastCharX = range[1] - 1; + } + + const chars = cell.getChars(); + let code = cell.getCode(); const i = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL; if (code !== NULL_CELL_CODE) { @@ -284,33 +335,52 @@ export class WebglRenderer extends Disposable implements IRenderer { // Nothing has changed, no updates needed if (this._model.cells[i] === code && - this._model.cells[i + RENDER_MODEL_BG_OFFSET] === this._workCell.bg && - this._model.cells[i + RENDER_MODEL_FG_OFFSET] === this._workCell.fg) { + this._model.cells[i + RENDER_MODEL_BG_OFFSET] === cell.bg && + this._model.cells[i + RENDER_MODEL_FG_OFFSET] === cell.fg) { continue; } // Flag combined chars with a bit mask so they're easily identifiable if (chars.length > 1) { - code = code | COMBINED_CHAR_BIT_MASK; + code |= COMBINED_CHAR_BIT_MASK; } // Cache the results in the model this._model.cells[i] = code; - this._model.cells[i + RENDER_MODEL_BG_OFFSET] = this._workCell.bg; - this._model.cells[i + RENDER_MODEL_FG_OFFSET] = this._workCell.fg; + this._model.cells[i + RENDER_MODEL_BG_OFFSET] = cell.bg; + this._model.cells[i + RENDER_MODEL_FG_OFFSET] = cell.fg; - this._glyphRenderer.updateCell(x, y, code, this._workCell.bg, this._workCell.fg, chars); + this._glyphRenderer.updateCell(x, y, code, cell.bg, cell.fg, chars); + + if (isJoined) { + // Restore work cell + cell = this._workCell; + + // Null out non-first cells + for (x++; x < lastCharX; x++) { + const j = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL; + this._glyphRenderer.updateCell(x, y, NULL_CELL_CODE, 0, 0, NULL_CELL_CHAR); + this._model.cells[j] = NULL_CELL_CODE; + this._model.cells[j + RENDER_MODEL_BG_OFFSET] = this._workCell.bg; + this._model.cells[j + RENDER_MODEL_FG_OFFSET] = this._workCell.fg; + } + } } } this._rectangleRenderer.updateBackgrounds(this._model); + if (this._model.selection.hasSelection) { + // Model could be updated but the selection is unchanged + this._glyphRenderer.updateSelection(this._model); + } } - private _updateSelectionModel(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void { + private _updateSelectionModel(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void { const terminal = this._terminal; // Selection does not exist if (!start || !end || (start[0] === end[0] && start[1] === end[1])) { this._model.clearSelection(); + this._rectangleRenderer.updateSelection(this._model.selection); return; } @@ -323,6 +393,7 @@ export class WebglRenderer extends Disposable implements IRenderer { // No need to draw the selection if (viewportCappedStartRow >= terminal.rows || viewportCappedEndRow < 0) { this._model.clearSelection(); + this._rectangleRenderer.updateSelection(this._model.selection); return; } @@ -334,6 +405,8 @@ export class WebglRenderer extends Disposable implements IRenderer { this._model.selection.viewportCappedEndRow = viewportCappedEndRow; this._model.selection.startCol = start[0]; this._model.selection.endCol = end[0]; + + this._rectangleRenderer.updateSelection(this._model.selection); } /** @@ -408,3 +481,49 @@ export class WebglRenderer extends Disposable implements IRenderer { this.dimensions.actualCellWidth = this.dimensions.scaledCellWidth / this._devicePixelRatio; } } + +// TODO: Share impl with core +export class JoinedCellData extends AttributeData implements ICellData { + private _width: number; + // .content carries no meaning for joined CellData, simply nullify it + // thus we have to overload all other .content accessors + public content: number = 0; + public fg: number; + public bg: number; + public combinedData: string = ''; + + constructor(firstCell: ICellData, chars: string, width: number) { + super(); + this.fg = firstCell.fg; + this.bg = firstCell.bg; + this.combinedData = chars; + this._width = width; + } + + public isCombined(): number { + // always mark joined cell data as combined + return Content.IS_COMBINED_MASK; + } + + public getWidth(): number { + return this._width; + } + + public getChars(): string { + return this.combinedData; + } + + public getCode(): number { + // code always gets the highest possible fake codepoint (read as -1) + // this is needed as code is used by caches as identifier + return 0x1FFFFF; + } + + public setFromCharData(value: CharData): void { + throw new Error('not implemented'); + } + + public getAsCharData(): CharData { + return [this.fg, this.getChars(), this.getWidth(), this.getCode()]; + } +} diff --git a/addons/xterm-addon-webgl/src/atlas/CharAtlasCache.ts b/addons/xterm-addon-webgl/src/atlas/CharAtlasCache.ts index 5046006f..41114de0 100644 --- a/addons/xterm-addon-webgl/src/atlas/CharAtlasCache.ts +++ b/addons/xterm-addon-webgl/src/atlas/CharAtlasCache.ts @@ -28,10 +28,12 @@ const charAtlasCache: ICharAtlasCacheEntry[] = []; export function acquireCharAtlas( terminal: Terminal, colors: IColorSet, + scaledCellWidth: number, + scaledCellHeight: number, scaledCharWidth: number, scaledCharHeight: number ): WebglCharAtlas { - const newConfig = generateConfig(scaledCharWidth, scaledCharHeight, terminal, colors); + const newConfig = generateConfig(scaledCellWidth, scaledCellHeight, scaledCharWidth, scaledCharHeight, terminal, colors); // Check to see if the terminal already owns this config for (let i = 0; i < charAtlasCache.length; i++) { diff --git a/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts b/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts index 5496a500..962eb7b3 100644 --- a/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts +++ b/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts @@ -13,7 +13,7 @@ const NULL_COLOR: IColor = { rgba: 0 }; -export function generateConfig(scaledCharWidth: number, scaledCharHeight: number, terminal: Terminal, colors: IColorSet): ICharAtlasConfig { +export function generateConfig(scaledCellWidth: number, scaledCellHeight: number, scaledCharWidth: number, scaledCharHeight: number, terminal: Terminal, colors: IColorSet): ICharAtlasConfig { // null out some fields that don't matter const clonedColors: IColorSet = { foreground: colors.foreground, @@ -28,7 +28,12 @@ export function generateConfig(scaledCharWidth: number, scaledCharHeight: number contrastCache: colors.contrastCache }; return { + customGlyphs: terminal.getOption('customGlyphs'), devicePixelRatio: window.devicePixelRatio, + letterSpacing: terminal.getOption('letterSpacing'), + lineHeight: terminal.getOption('lineHeight'), + scaledCellWidth, + scaledCellHeight, scaledCharWidth, scaledCharHeight, fontFamily: terminal.getOption('fontFamily'), @@ -49,6 +54,9 @@ export function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean } } return a.devicePixelRatio === b.devicePixelRatio && + a.customGlyphs === b.customGlyphs && + a.lineHeight === b.lineHeight && + a.letterSpacing === b.letterSpacing && a.fontFamily === b.fontFamily && a.fontSize === b.fontSize && a.fontWeight === b.fontWeight && diff --git a/addons/xterm-addon-webgl/src/atlas/Types.d.ts b/addons/xterm-addon-webgl/src/atlas/Types.d.ts index cd73393c..8d2870cd 100644 --- a/addons/xterm-addon-webgl/src/atlas/Types.d.ts +++ b/addons/xterm-addon-webgl/src/atlas/Types.d.ts @@ -17,11 +17,16 @@ export interface IGlyphIdentifier { } export interface ICharAtlasConfig { + customGlyphs: boolean; devicePixelRatio: number; + letterSpacing: number; + lineHeight: number; fontSize: number; fontFamily: string; fontWeight: FontWeight; fontWeightBold: FontWeight; + scaledCellWidth: number; + scaledCellHeight: number; scaledCharWidth: number; scaledCharHeight: number; allowTransparency: boolean; diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index 223ec16a..6128c836 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -12,6 +12,7 @@ import { IColor } from 'browser/Types'; import { IDisposable } from 'xterm'; import { AttributeData } from 'common/buffer/AttributeData'; import { channels, rgba } from 'browser/Color'; +import { tryDrawCustomChar } from 'browser/renderer/CustomGlyphs'; // In practice we're probably never going to exhaust a texture this large. For debugging purposes, // however, it can be useful to set this to a really tiny value, to verify that LRU eviction works. @@ -80,12 +81,12 @@ export class WebglCharAtlas implements IDisposable { // The canvas needs alpha because we use clearColor to convert the background color to alpha. // It might also contain some characters with transparent backgrounds if allowTransparency is // set. - this._cacheCtx = throwIfFalsy(this.cacheCanvas.getContext('2d', {alpha: true})); + this._cacheCtx = throwIfFalsy(this.cacheCanvas.getContext('2d', { alpha: true })); this._tmpCanvas = document.createElement('canvas'); - this._tmpCanvas.width = this._config.scaledCharWidth * 2 + TMP_CANVAS_GLYPH_PADDING * 2; - this._tmpCanvas.height = this._config.scaledCharHeight + TMP_CANVAS_GLYPH_PADDING * 2; - this._tmpCtx = throwIfFalsy(this._tmpCanvas.getContext('2d', {alpha: this._config.allowTransparency})); + this._tmpCanvas.width = this._config.scaledCellWidth * 4 + TMP_CANVAS_GLYPH_PADDING * 2; + this._tmpCanvas.height = this._config.scaledCellHeight + TMP_CANVAS_GLYPH_PADDING * 2; + this._tmpCtx = throwIfFalsy(this._tmpCanvas.getContext('2d', { alpha: this._config.allowTransparency })); } public dispose(): void { @@ -317,6 +318,13 @@ export class WebglCharAtlas implements IDisposable { this.hasCanvasChanged = true; + // Allow 1 cell width per character, with a minimum of 2 (CJK), plus some padding. This is used + // to draw the glyph to the canvas as well as to restrict the bounding box search to ensure + // giant ligatures (eg. =====>) don't impact overall performance. + const allowedWidth = this._config.scaledCharWidth * Math.max(chars.length, 2) + TMP_CANVAS_GLYPH_PADDING * 2; + if (this._tmpCanvas.width < allowedWidth) { + this._tmpCanvas.width = allowedWidth; + } this._tmpCtx.save(); this._workAttributeData.fg = fg; @@ -331,6 +339,8 @@ export class WebglCharAtlas implements IDisposable { const inverse = !!this._workAttributeData.isInverse(); const dim = !!this._workAttributeData.isDim(); const italic = !!this._workAttributeData.isItalic(); + const underline = !!this._workAttributeData.isUnderline(); + const strikethrough = !!this._workAttributeData.isStrikethrough(); let fgColor = this._workAttributeData.getFgColor(); let fgColorMode = this._workAttributeData.getFgColorMode(); let bgColor = this._workAttributeData.getBgColor(); @@ -358,7 +368,7 @@ export class WebglCharAtlas implements IDisposable { const fontStyle = italic ? 'italic' : ''; this._tmpCtx.font = `${fontStyle} ${fontWeight} ${this._config.fontSize * this._config.devicePixelRatio}px ${this._config.fontFamily}`; - this._tmpCtx.textBaseline = 'middle'; + this._tmpCtx.textBaseline = 'ideographic'; this._tmpCtx.fillStyle = this._getForegroundCss(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, bold); @@ -367,8 +377,66 @@ export class WebglCharAtlas implements IDisposable { this._tmpCtx.globalAlpha = DIM_OPACITY; } + // Check if the char is a powerline glyph, these will be restricted to a single cell glyph, no + // padding on either side that are allowed for other glyphs since they are designed to be pixel + // perfect but may render with "bad" anti-aliasing + let isPowerlineGlyph = false; + if (chars.length === 1) { + const code = chars.charCodeAt(0); + if (code >= 0xE0A0 && code <= 0xE0D6) { + isPowerlineGlyph = true; + } + } + + // For powerline glyphs left/top padding is excluded (https://github.com/microsoft/vscode/issues/120129) + const padding = isPowerlineGlyph ? 0 : TMP_CANVAS_GLYPH_PADDING; + + // Draw custom characters if applicable + let drawSuccess = false; + if (this._config.customGlyphs !== false) { + drawSuccess = tryDrawCustomChar(this._tmpCtx, chars, padding, padding, this._config.scaledCellWidth, this._config.scaledCellHeight); + } + // Draw the character - this._tmpCtx.fillText(chars, TMP_CANVAS_GLYPH_PADDING, TMP_CANVAS_GLYPH_PADDING + this._config.scaledCharHeight / 2); + if (!drawSuccess) { + this._tmpCtx.fillText(chars, padding, padding + this._config.scaledCharHeight); + } + + // If this charcater is underscore and beyond the cell bounds, shift it up until it is visible, + // try for a maximum of 5 pixels. + if (chars === '_' && !this._config.allowTransparency) { + let isBeyondCellBounds = clearColor(this._tmpCtx.getImageData(padding, padding, this._config.scaledCellWidth, this._config.scaledCellHeight), backgroundColor); + if (isBeyondCellBounds) { + for (let offset = 1; offset <= 5; offset++) { + this._tmpCtx.clearRect(0, 0, this._tmpCanvas.width, this._tmpCanvas.height); + this._tmpCtx.fillText(chars, padding, padding + this._config.scaledCharHeight - offset); + isBeyondCellBounds = clearColor(this._tmpCtx.getImageData(padding, padding, this._config.scaledCellWidth, this._config.scaledCellHeight), backgroundColor); + if (!isBeyondCellBounds) { + break; + } + } + } + } + + // Draw underline and strikethrough + if (underline || strikethrough) { + const lineWidth = Math.max(1, Math.floor(this._config.fontSize / 10)); + const yOffset = this._tmpCtx.lineWidth % 2 === 1 ? 0.5 : 0; // When the width is odd, draw at 0.5 position + this._tmpCtx.lineWidth = lineWidth; + this._tmpCtx.strokeStyle = this._tmpCtx.fillStyle; + this._tmpCtx.beginPath(); + if (underline) { + this._tmpCtx.moveTo(padding, padding + this._config.scaledCharHeight - yOffset); + this._tmpCtx.lineTo(padding + this._config.scaledCharWidth, padding + this._config.scaledCharHeight - yOffset); + } + if (strikethrough) { + this._tmpCtx.moveTo(padding, padding + Math.floor(this._config.scaledCharHeight / 2) - yOffset); + this._tmpCtx.lineTo(padding + this._config.scaledCharWidth, padding + Math.floor(this._config.scaledCharHeight / 2) - yOffset); + } + this._tmpCtx.stroke(); + this._tmpCtx.closePath(); + } + this._tmpCtx.restore(); // clear the background from the character to avoid issues with drawing over the previous @@ -391,7 +459,7 @@ export class WebglCharAtlas implements IDisposable { return NULL_RASTERIZED_GLYPH; } - const rasterizedGlyph = this._findGlyphBoundingBox(imageData, this._workBoundingBox); + const rasterizedGlyph = this._findGlyphBoundingBox(imageData, this._workBoundingBox, allowedWidth, isPowerlineGlyph, drawSuccess); const clippedImageData = this._clipImageData(imageData, this._workBoundingBox); // Check if there is enough room in the current row and go to next if needed @@ -424,11 +492,13 @@ export class WebglCharAtlas implements IDisposable { * @param imageData The image data to read. * @param boundingBox An IBoundingBox to put the clipped bounding box values. */ - private _findGlyphBoundingBox(imageData: ImageData, boundingBox: IBoundingBox): IRasterizedGlyph { + private _findGlyphBoundingBox(imageData: ImageData, boundingBox: IBoundingBox, allowedWidth: number, restrictedGlyph: boolean, customGlyph: boolean): IRasterizedGlyph { boundingBox.top = 0; + const height = restrictedGlyph ? this._config.scaledCharHeight : this._tmpCanvas.height; + const width = restrictedGlyph ? this._config.scaledCharWidth : allowedWidth; let found = false; - for (let y = 0; y < this._tmpCanvas.height; y++) { - for (let x = 0; x < this._tmpCanvas.width; x++) { + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3; if (imageData.data[alphaOffset] !== 0) { boundingBox.top = y; @@ -442,8 +512,8 @@ export class WebglCharAtlas implements IDisposable { } boundingBox.left = 0; found = false; - for (let x = 0; x < this._tmpCanvas.width; x++) { - for (let y = 0; y < this._tmpCanvas.height; y++) { + for (let x = 0; x < width; x++) { + for (let y = 0; y < height; y++) { const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3; if (imageData.data[alphaOffset] !== 0) { boundingBox.left = x; @@ -455,10 +525,10 @@ export class WebglCharAtlas implements IDisposable { break; } } - boundingBox.right = this._tmpCanvas.width; + boundingBox.right = width; found = false; - for (let x = this._tmpCanvas.width - 1; x >= 0; x--) { - for (let y = 0; y < this._tmpCanvas.height; y++) { + for (let x = width - 1; x >= 0; x--) { + for (let y = 0; y < height; y++) { const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3; if (imageData.data[alphaOffset] !== 0) { boundingBox.right = x; @@ -470,10 +540,10 @@ export class WebglCharAtlas implements IDisposable { break; } } - boundingBox.bottom = this._tmpCanvas.height; + boundingBox.bottom = height; found = false; - for (let y = this._tmpCanvas.height - 1; y >= 0; y--) { - for (let x = 0; x < this._tmpCanvas.width; x++) { + for (let y = height - 1; y >= 0; y--) { + for (let x = 0; x < width; x++) { const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3; if (imageData.data[alphaOffset] !== 0) { boundingBox.bottom = y; @@ -497,8 +567,8 @@ export class WebglCharAtlas implements IDisposable { y: (boundingBox.bottom - boundingBox.top + 1) / TEXTURE_HEIGHT }, offset: { - x: -boundingBox.left + TMP_CANVAS_GLYPH_PADDING, - y: -boundingBox.top + TMP_CANVAS_GLYPH_PADDING + x: -boundingBox.left + (restrictedGlyph ? 0 : TMP_CANVAS_GLYPH_PADDING) + (customGlyph ? Math.floor(this._config.letterSpacing / 2) : 0), + y: -boundingBox.top + (restrictedGlyph ? 0 : TMP_CANVAS_GLYPH_PADDING) + (customGlyph ? this._config.lineHeight === 1 ? 0 : Math.round((this._config.scaledCellHeight - this._config.scaledCharHeight) / 2) : 0) } }; } diff --git a/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts b/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts index da3b4d41..532836ee 100644 --- a/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts +++ b/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts @@ -46,7 +46,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { } private _initCanvas(): void { - this._ctx = throwIfFalsy(this._canvas.getContext('2d', {alpha: this._alpha})); + this._ctx = throwIfFalsy(this._canvas.getContext('2d', { alpha: this._alpha })); // Draw the background if this is an opaque layer if (!this._alpha) { this._clearAll(); @@ -92,7 +92,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { if (this._scaledCharWidth <= 0 && this._scaledCharHeight <= 0) { return; } - this._charAtlas = acquireCharAtlas(terminal, colorSet, this._scaledCharWidth, this._scaledCharHeight); + this._charAtlas = acquireCharAtlas(terminal, colorSet, this._scaledCellWidth, this._scaledCellHeight, this._scaledCharWidth, this._scaledCharHeight); this._charAtlas.warmUp(); } @@ -224,12 +224,12 @@ export abstract class BaseRenderLayer implements IRenderLayer { */ protected _fillCharTrueColor(terminal: Terminal, cell: CellData, x: number, y: number): void { this._ctx.font = this._getFont(terminal, false, false); - this._ctx.textBaseline = 'middle'; + this._ctx.textBaseline = 'ideographic'; this._clipRow(terminal, y); this._ctx.fillText( cell.getChars(), x * this._scaledCellWidth + this._scaledCharLeft, - y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight / 2); + y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight); } /** diff --git a/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts b/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts index b2e834d3..912c23c9 100644 --- a/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts +++ b/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts @@ -7,7 +7,7 @@ import { Terminal } from 'xterm'; import { BaseRenderLayer } from './BaseRenderLayer'; import { ICellData } from 'common/Types'; import { CellData } from 'common/buffer/CellData'; -import { IColorSet } from 'browser/Types'; +import { IColorSet, ITerminal } from 'browser/Types'; import { IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/Types'; import { IEventEmitter } from 'common/EventEmitter'; @@ -34,6 +34,7 @@ export class CursorRenderLayer extends BaseRenderLayer { container: HTMLElement, zIndex: number, colors: IColorSet, + private readonly _terminal: ITerminal, private _onRequestRefreshRowsEvent: IEventEmitter ) { super(container, 'cursor', zIndex, true, colors); @@ -120,7 +121,7 @@ export class CursorRenderLayer extends BaseRenderLayer { private _render(terminal: Terminal, triggeredByAnimationFrame: boolean): void { // Don't draw the cursor if it's hidden // TODO: Need to expose API for this - if (!(terminal as any)._core._coreService.isCursorInitialized || (terminal as any)._core._coreService.isCursorHidden) { + if (!this._terminal.coreService.isCursorInitialized || this._terminal.coreService.isCursorHidden) { this._clearCursor(); return; } @@ -193,7 +194,12 @@ export class CursorRenderLayer extends BaseRenderLayer { private _clearCursor(): void { if (this._state) { - this._clearCells(this._state.x, this._state.y, this._state.width, 1); + // Avoid potential rounding errors when device pixel ratio is less than 1 + if (window.devicePixelRatio < 1) { + this._clearAll(); + } else { + this._clearCells(this._state.x, this._state.y, this._state.width, 1); + } this._state = { x: 0, y: 0, diff --git a/addons/xterm-addon-webgl/test/WebglRenderer.api.ts b/addons/xterm-addon-webgl/test/WebglRenderer.api.ts index 558605e2..e6942d1c 100644 --- a/addons/xterm-addon-webgl/test/WebglRenderer.api.ts +++ b/addons/xterm-addon-webgl/test/WebglRenderer.api.ts @@ -3,13 +3,13 @@ * @license MIT */ -import { ITerminalOptions } from '../../../src/common/Types'; -import { ITheme } from 'xterm'; import { assert } from 'chai'; -import { openTerminal, pollFor, writeSync, getBrowserType } from '../../../out-test/api/TestUtils'; import { Browser, Page } from 'playwright'; +import { ITheme } from 'xterm'; +import { getBrowserType, launchBrowser, openTerminal, pollFor, writeSync } from '../../../out-test/api/TestUtils'; +import { ITerminalOptions } from '../../../src/common/Types'; -const APP = 'http://127.0.0.1:3000/test'; +const APP = 'http://127.0.0.1:3001/test'; let browser: Browser; let page: Page; @@ -18,7 +18,7 @@ const height = 600; describe('WebGL Renderer Integration Tests', async () => { const browserType = getBrowserType(); - const isHeadless = process.argv.indexOf('--headless') !== -1; + const isHeadless = process.argv.includes('--headless'); // Firefox works only in non-headless mode https://github.com/microsoft/playwright/issues/1032 const areTestsEnabled = browserType.name() === 'chromium' || (browserType.name() === 'firefox' && !isHeadless); const itWebgl = areTestsEnabled ? it : it.skip; @@ -890,11 +890,22 @@ async function getCellColor(col: number, row: number): Promise { return await page.evaluate(`Array.from(window.result)`); } +async function getCellPixels(col: number, row: number): Promise { + await page.evaluate(` + window.gl = window.term._core._renderService._renderer._gl; + window.result = new Uint8Array(window.d.scaledCellWidth * window.d.scaledCellHeight * 4); + window.d = window.term._core._renderService.dimensions; + window.gl.readPixels( + Math.floor(${col - 1} * window.d.scaledCellWidth), + Math.floor(window.gl.drawingBufferHeight - ${row} * window.d.scaledCellHeight), + window.d.scaledCellWidth, window.d.scaledCellHeight, window.gl.RGBA, window.gl.UNSIGNED_BYTE, window.result + ); + `); + return await page.evaluate(`Array.from(window.result)`); +} + async function setupBrowser(options: ITerminalOptions = { rendererType: 'dom' }): Promise { - const browserType = getBrowserType(); - browser = await browserType.launch({ - headless: process.argv.indexOf('--headless') !== -1 - }); + browser = await launchBrowser(); page = await (await browser.newContext()).newPage(); await page.setViewportSize({ width, height }); await page.goto(APP); diff --git a/addons/xterm-addon-webgl/typings/xterm-addon-webgl.d.ts b/addons/xterm-addon-webgl/typings/xterm-addon-webgl.d.ts index 5c15aa17..ee390d8a 100644 --- a/addons/xterm-addon-webgl/typings/xterm-addon-webgl.d.ts +++ b/addons/xterm-addon-webgl/typings/xterm-addon-webgl.d.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { Terminal, ITerminalAddon } from 'xterm'; +import { Terminal, ITerminalAddon, IEvent } from 'xterm'; declare module 'xterm-addon-webgl' { /** @@ -29,5 +29,10 @@ declare module 'xterm-addon-webgl' { * Clears the terminal's texture atlas and triggers a redraw. */ public clearTextureAtlas(): void; + + /** + * Fired when the WebglRenderer loses context + */ + public get onContextLoss(): IEvent; } } diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 4b2c5f73..5d4e9918 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -10,7 +10,7 @@ jobs: steps: - task: NodeTool@0 inputs: - versionSpec: '10.x' + versionSpec: '14.x' displayName: 'Install Node.js' - task: YarnInstaller@3 inputs: @@ -43,7 +43,7 @@ jobs: steps: - task: NodeTool@0 inputs: - versionSpec: '10.x' + versionSpec: '14.x' displayName: 'Install Node.js' - task: CacheBeta@1 inputs: @@ -63,7 +63,7 @@ jobs: steps: - task: NodeTool@0 inputs: - versionSpec: '10.x' + versionSpec: '14.x' displayName: 'Install Node.js' - task: CacheBeta@1 inputs: @@ -92,7 +92,7 @@ jobs: displayName: Install required packages - task: NodeTool@0 inputs: - versionSpec: '10.x' + versionSpec: '14.x' displayName: 'Install Node.js' - task: YarnInstaller@3 inputs: @@ -100,10 +100,6 @@ jobs: displayName: 'Install Yarn' - script: yarn --frozen-lockfile displayName: 'Install dependencies and build' - - script: | - yarn start & - sleep 10 - displayName: 'Start test server' - script: yarn test-api-chromium --headless --forbid-only displayName: 'Integration tests (Chromium)' - script: xvfb-run --auto-servernum -- bash -c "yarn test-api-firefox --headless --forbid-only" @@ -115,14 +111,10 @@ jobs: steps: - task: NodeTool@0 inputs: - versionSpec: '10.x' + versionSpec: '14.x' displayName: 'Install Node.js' - script: yarn --frozen-lockfile displayName: 'Install dependencies and build' - - script: | - yarn start & - sleep 10 - displayName: 'Start test server' - script: yarn test-api-chromium --headless --forbid-only displayName: 'Integration tests (Chromium)' - script: yarn test-api-firefox --headless --forbid-only @@ -130,6 +122,21 @@ jobs: - script: yarn test-api-webkit --headless --forbid-only displayName: 'Integration tests (Webkit)' +- job: Windows_IntegrationTests + pool: + vmImage: 'vs2017-win2016' + steps: + - task: NodeTool@0 + inputs: + versionSpec: '14.x' + displayName: 'Install Node.js' + - script: yarn --frozen-lockfile + displayName: 'Install dependencies and build' + - script: yarn test-api-chromium --headless --forbid-only + displayName: 'Integration tests (Chromium)' + - script: yarn test-api-firefox --headless --forbid-only + displayName: 'Integration tests (Firefox)' + - job: Release dependsOn: - Linux @@ -137,13 +144,14 @@ jobs: - Windows - Linux_IntegrationTests - macOS_IntegrationTests + - Windows_IntegrationTests condition: and(succeeded(), or(eq(variables['Build.SourceBranch'], 'refs/heads/master'), eq(variables['FORCE_RELEASE'], 'true'))) pool: vmImage: 'ubuntu-16.04' steps: - task: NodeTool@0 inputs: - versionSpec: '10.x' + versionSpec: '14.x' displayName: 'Install Node.js' - task: YarnInstaller@3 inputs: @@ -156,5 +164,9 @@ jobs: displayName: Cache node modules - script: yarn --frozen-lockfile displayName: 'Install dependencies and build' + - script: | + yarn package-headless + node ./bin/package_headless.js + displayName: 'Package xterm-headless' - script: NPM_AUTH_TOKEN="$(NPM_AUTH_TOKEN)" node ./bin/publish.js displayName: 'Package and publish to npm' diff --git a/bin/package_headless.js b/bin/package_headless.js new file mode 100644 index 00000000..d002a95e --- /dev/null +++ b/bin/package_headless.js @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2021 The xterm.js authors. All rights reserved. + * @license MIT + */ + +const { exec } = require('child_process'); +const fs = require('fs'); +const { join } = require('path'); + +const repoRoot = join(__dirname, '..'); +const headlessRoot = join(repoRoot, 'headless'); + +console.log('> headless/package.json'); +const xtermPackageJson = require('../package.json'); +const xtermHeadlessPackageJson = { + ...xtermPackageJson, + name: 'xterm-headless', + description: 'A headless terminal component that runs in Node.js', + main: 'lib-headless/xterm-headless.js', + types: 'typings/xterm-headless.d.ts', +}; +delete xtermHeadlessPackageJson['scripts']; +delete xtermHeadlessPackageJson['devDependencies']; +delete xtermHeadlessPackageJson['style']; +fs.writeFileSync(join(headlessRoot, 'package.json'), JSON.stringify(xtermHeadlessPackageJson, null, 1)); +console.log(fs.readFileSync(join(headlessRoot, 'package.json')).toString()); + +console.log('> headless/typings/'); +mkdirF(join(headlessRoot, 'typings')); +fs.copyFileSync( + join(repoRoot, 'typings/xterm-headless.d.ts'), + join(headlessRoot, 'typings/xterm-headless.d.ts') +); + +console.log('> headless/logo-full.png'); +fs.copyFileSync( + join(repoRoot, 'logo-full.png'), + join(headlessRoot, 'logo-full.png') +); + +function mkdirF(p) { + if (!fs.existsSync(p)) { + fs.mkdirSync(p); + } +} + +console.log('> Publish dry run'); +exec('npm publish --dry-run', { cwd: headlessRoot }, (error, stdout, stderr) => { + if (error) { + console.log(`error: ${error.message}`); + return; + } + if (stderr) { + console.error(`stderr:\n${stderr}`); + } + console.log(`stdout:\n${stdout}`); +}); diff --git a/bin/publish.js b/bin/publish.js index 64336fdc..3c729f6c 100644 --- a/bin/publish.js +++ b/bin/publish.js @@ -22,6 +22,7 @@ const changedFiles = getChangedFilesInCommit('HEAD'); let isStableRelease = false; if (changedFiles.some(e => e.search(/^addons\//) === -1)) { isStableRelease = checkAndPublishPackage(path.resolve(__dirname, '..')); + checkAndPublishPackage(path.resolve(__dirname, '../headless')); } // Publish addons if any files were changed inside of the addon diff --git a/bin/test_api.js b/bin/test_api.js index 7afb34bc..f173b417 100644 --- a/bin/test_api.js +++ b/bin/test_api.js @@ -34,19 +34,37 @@ if (process.argv.length > 2) { env.DEBUG = flagArgs.indexOf('--debug') >= 0 ? 'debug' : ''; +env.PORT = 3001; -const run = cp.spawnSync( - npmBinScript('mocha'), - [...testFiles, ...flagArgs], - { - cwd: path.resolve(__dirname, '..'), - env, - stdio: 'inherit' +const server = cp.spawn('node', ['demo/start'], { + cwd: path.resolve(__dirname, '..'), + env, + stdio: 'pipe' +}) + +server.stdout.on('data', (data) => { + // await for the server to fully start + if (data.indexOf("successfully") !== -1) { + const run = cp.spawnSync( + npmBinScript('mocha'), + [...testFiles, ...flagArgs], { + cwd: path.resolve(__dirname, '..'), + env, + stdio: 'inherit' + } + ); + + function npmBinScript(script) { + return path.resolve(__dirname, `../node_modules/.bin/` + (process.platform === 'win32' ? + `${script}.cmd` : script)); + } + + server.kill(); + + process.exit(run.status); } -); +}); -function npmBinScript(script) { - return path.resolve(__dirname, `../node_modules/.bin/` + (process.platform === 'win32' ? `${script}.cmd` : script)); -} - -process.exit(run.status); +server.stderr.on('data', (data) => { + console.error(data.toString()); +}); diff --git a/css/xterm.css b/css/xterm.css index 7ddcc2d0..3fab18bd 100644 --- a/css/xterm.css +++ b/css/xterm.css @@ -36,7 +36,6 @@ */ .xterm { - font-feature-settings: "liga" 0; position: relative; user-select: none; -ms-user-select: none; @@ -169,3 +168,7 @@ .xterm-underline { text-decoration: underline; } + +.xterm-strikethrough { + text-decoration: line-through; +} diff --git a/demo/client.ts b/demo/client.ts index 93b7c26c..a5ac8bb5 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -16,6 +16,7 @@ import { SerializeAddon } from '../addons/xterm-addon-serialize/out/SerializeAdd import { WebLinksAddon } from '../addons/xterm-addon-web-links/out/WebLinksAddon'; import { WebglAddon } from '../addons/xterm-addon-webgl/out/WebglAddon'; import { Unicode11Addon } from '../addons/xterm-addon-unicode11/out/Unicode11Addon'; +import { LigaturesAddon } from '../addons/xterm-addon-ligatures/out/LigaturesAddon'; // Use webpacked version (yarn package) // import { Terminal } from '../lib/xterm'; @@ -26,6 +27,7 @@ import { Unicode11Addon } from '../addons/xterm-addon-unicode11/out/Unicode11Add // import { WebLinksAddon } from 'xterm-addon-web-links'; // import { WebglAddon } from 'xterm-addon-webgl'; // import { Unicode11Addon } from 'xterm-addon-unicode11'; +// import { LigaturesAddon } from 'xterm-addon-ligatures'; // Pulling in the module's types relies on the above, it's looks a // little weird here as we're importing "this" module @@ -41,6 +43,7 @@ export interface IWindowWithTerminal extends Window { WebLinksAddon?: typeof WebLinksAddon; WebglAddon?: typeof WebglAddon; Unicode11Addon?: typeof Unicode11Addon; + LigaturesAddon?: typeof LigaturesAddon; } declare let window: IWindowWithTerminal; @@ -50,7 +53,7 @@ let socketURL; let socket; let pid; -type AddonType = 'attach' | 'fit' | 'search' | 'serialize' | 'unicode11' | 'web-links' | 'webgl'; +type AddonType = 'attach' | 'fit' | 'search' | 'serialize' | 'unicode11' | 'web-links' | 'webgl' | 'ligatures'; interface IDemoAddon { name: T; @@ -62,8 +65,9 @@ interface IDemoAddon { T extends 'serialize' ? typeof SerializeAddon : T extends 'web-links' ? typeof WebLinksAddon : T extends 'unicode11' ? typeof Unicode11Addon : + T extends 'ligatures' ? typeof LigaturesAddon : typeof WebglAddon; - instance?: + instance?: T extends 'attach' ? AttachAddon : T extends 'fit' ? FitAddon : T extends 'search' ? SearchAddon : @@ -71,6 +75,7 @@ interface IDemoAddon { T extends 'web-links' ? WebLinksAddon : T extends 'webgl' ? WebglAddon : T extends 'unicode11' ? typeof Unicode11Addon : + T extends 'ligatures' ? typeof LigaturesAddon : never; } @@ -81,7 +86,8 @@ const addons: { [T in AddonType]: IDemoAddon} = { serialize: { name: 'serialize', ctor: SerializeAddon, canChange: true }, 'web-links': { name: 'web-links', ctor: WebLinksAddon, canChange: true }, webgl: { name: 'webgl', ctor: WebglAddon, canChange: true }, - unicode11: { name: 'unicode11', ctor: Unicode11Addon, canChange: true } + unicode11: { name: 'unicode11', ctor: Unicode11Addon, canChange: true }, + ligatures: { name: 'ligatures', ctor: LigaturesAddon, canChange: true } }; const terminalContainer = document.getElementById('terminal-container'); @@ -117,6 +123,7 @@ const disposeRecreateButtonHandler = () => { addons.search.instance = undefined; addons.serialize.instance = undefined; addons.unicode11.instance = undefined; + addons.ligatures.instance = undefined; addons['web-links'].instance = undefined; addons.webgl.instance = undefined; document.getElementById('dispose').innerHTML = 'Recreate Terminal'; @@ -133,12 +140,15 @@ if (document.location.pathname === '/test') { window.SearchAddon = SearchAddon; window.SerializeAddon = SerializeAddon; window.Unicode11Addon = Unicode11Addon; + window.LigaturesAddon = LigaturesAddon; window.WebLinksAddon = WebLinksAddon; window.WebglAddon = WebglAddon; } else { createTerminal(); document.getElementById('dispose').addEventListener('click', disposeRecreateButtonHandler); document.getElementById('serialize').addEventListener('click', serializeButtonHandler); + document.getElementById('custom-glyph').addEventListener('click', writeCustomGlyphHandler); + document.getElementById('load-test').addEventListener('click', loadTest); } function createTerminal(): void { @@ -149,7 +159,8 @@ function createTerminal(): void { const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].indexOf(navigator.platform) >= 0; term = new Terminal({ - windowsMode: isWindows + windowsMode: isWindows, + fontFamily: 'Fira Code, courier-new, courier, monospace' } as ITerminalOptions); // Load addons @@ -339,6 +350,9 @@ function initOptions(term: TerminalType): void { } else if (o === 'lineHeight' || o === 'scrollSensitivity') { term.setOption(o, parseFloat(input.value)); updateTerminalSize(); + } else if(o === 'scrollback') { + term.setOption(o, parseInt(input.value)); + setTimeout(() => updateTerminalSize(), 5); } else { term.setOption(o, parseInt(input.value)); } @@ -363,6 +377,9 @@ function initAddons(term: TerminalType): void { if (!addon.canChange) { checkbox.disabled = true; } + if(name === 'unicode11' && checkbox.checked) { + term.unicode.activeVersion = '11'; + } addDomListener(checkbox, 'change', () => { if (checkbox.checked) { addon.instance = new addon.ctor(); @@ -371,10 +388,14 @@ function initAddons(term: TerminalType): void { setTimeout(() => { document.body.appendChild((addon.instance as WebglAddon).textureAtlas); }, 0); + } else if (name === 'unicode11') { + term.unicode.activeVersion = '11'; } } else { if (name === 'webgl') { document.body.removeChild((addon.instance as WebglAddon).textureAtlas); + } else if (name === 'unicode11') { + term.unicode.activeVersion = '6'; } addon.instance!.dispose(); addon.instance = undefined; @@ -422,3 +443,82 @@ function serializeButtonHandler(): void { term.write(output); } } + + +function writeCustomGlyphHandler() { + term.write('\n\r'); + term.write('\n\r'); + term.write('Box styles: ┎┰┒┍┯┑╓╥╖╒╤╕ ┏┳┓┌┲┓┌┬┐┏┱┐\n\r'); + term.write('┌─┬─┐ ┏━┳━┓ ╔═╦═╗ ┠╂┨┝┿┥╟╫╢╞╪╡ ┡╇┩├╊┫┢╈┪┣╉┤\n\r'); + term.write('│ │ │ ┃ ┃ ┃ ║ ║ ║ ┖┸┚┕┷┙╙╨╜╘╧╛ └┴┘└┺┛┗┻┛┗┹┘\n\r'); + term.write('├─┼─┤ ┣━╋━┫ ╠═╬═╣ ┏┱┐┌┲┓┌┬┐┌┬┐ ┏┳┓┌┮┓┌┬┐┏┭┐\n\r'); + term.write('│ │ │ ┃ ┃ ┃ ║ ║ ║ ┡╃┤├╄┩├╆┪┢╅┤ ┞╀┦├┾┫┟╁┧┣┽┤\n\r'); + term.write('└─┴─┘ ┗━┻━┛ ╚═╩═╝ └┴┘└┴┘└┺┛┗┹┘ └┴┘└┶┛┗┻┛┗┵┘\n\r'); + term.write('\n\r'); + term.write('Other:\n\r'); + term.write('╭─╮ ╲ ╱ ╷╻╎╏┆┇┊┋ ╺╾╴ ╌╌╌ ┄┄┄ ┈┈┈\n\r'); + term.write('│ │ ╳ ╽╿╎╏┆┇┊┋ ╶╼╸ ╍╍╍ ┅┅┅ ┉┉┉\n\r'); + term.write('╰─╯ ╱ ╲ ╹╵╎╏┆┇┊┋\n\r'); + term.write('\n\r'); + term.write('All box drawing characters:\n\r'); + term.write('─ ━ │ ┃ ┄ ┅ ┆ ┇ ┈ ┉ ┊ ┋ ┌ ┍ ┎ ┏\n\r'); + term.write('┐ ┑ ┒ ┓ └ ┕ ┖ ┗ ┘ ┙ ┚ ┛ ├ ┝ ┞ ┟\n\r'); + term.write('┠ ┡ ┢ ┣ ┤ ┥ ┦ ┧ ┨ ┩ ┪ ┫ ┬ ┭ ┮ ┯\n\r'); + term.write('┰ ┱ ┲ ┳ ┴ ┵ ┶ ┷ ┸ ┹ ┺ ┻ ┼ ┽ ┾ ┿\n\r'); + term.write('╀ ╁ ╂ ╃ ╄ ╅ ╆ ╇ ╈ ╉ ╊ ╋ ╌ ╍ ╎ ╏\n\r'); + term.write('═ ║ ╒ ╓ ╔ ╕ ╖ ╗ ╘ ╙ ╚ ╛ ╜ ╝ ╞ ╟\n\r'); + term.write('╠ ╡ ╢ ╣ ╤ ╥ ╦ ╧ ╨ ╩ ╪ ╫ ╬ ╭ ╮ ╯\n\r'); + term.write('╰ ╱ ╲ ╳ ╴ ╵ ╶ ╷ ╸ ╹ ╺ ╻ ╼ ╽ ╾ ╿\n\r'); + term.write('Box drawing alignment tests:\x1b[31m █\n\r'); + term.write(' ▉\n\r'); + term.write(' ╔══╦══╗ ┌──┬──┐ ╭──┬──╮ ╭──┬──╮ ┏━━┳━━┓ ┎┒┏┑ ╷ ╻ ┏┯┓ ┌┰┐ ▊ ╱╲╱╲╳╳╳\n\r'); + term.write(' ║┌─╨─┐║ │╔═╧═╗│ │╒═╪═╕│ │╓─╁─╖│ ┃┌─╂─┐┃ ┗╃╄┙ ╶┼╴╺╋╸┠┼┨ ┝╋┥ ▋ ╲╱╲╱╳╳╳\n\r'); + term.write(' ║│╲ ╱│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╿ │┃ ┍╅╆┓ ╵ ╹ ┗┷┛ └┸┘ ▌ ╱╲╱╲╳╳╳\n\r'); + term.write(' ╠╡ ╳ ╞╣ ├╢ ╟┤ ├┼─┼─┼┤ ├╫─╂─╫┤ ┣┿╾┼╼┿┫ ┕┛┖┚ ┌┄┄┐ ╎ ┏┅┅┓ ┋ ▍ ╲╱╲╱╳╳╳\n\r'); + term.write(' ║│╱ ╲│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╽ │┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▎\n\r'); + term.write(' ║└─╥─┘║ │╚═╤═╝│ │╘═╪═╛│ │╙─╀─╜│ ┃└─╂─┘┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▏\n\r'); + term.write(' ╚══╩══╝ └──┴──┘ ╰──┴──╯ ╰──┴──╯ ┗━━┻━━┛ └╌╌┘ ╎ ┗╍╍┛ ┋ ▁▂▃▄▅▆▇█\n\r'); + term.write('Box drawing alignment tests:\x1b[32m █\n\r'); + term.write(' ▉\n\r'); + term.write(' ╔══╦══╗ ┌──┬──┐ ╭──┬──╮ ╭──┬──╮ ┏━━┳━━┓ ┎┒┏┑ ╷ ╻ ┏┯┓ ┌┰┐ ▊ ╱╲╱╲╳╳╳\n\r'); + term.write(' ║┌─╨─┐║ │╔═╧═╗│ │╒═╪═╕│ │╓─╁─╖│ ┃┌─╂─┐┃ ┗╃╄┙ ╶┼╴╺╋╸┠┼┨ ┝╋┥ ▋ ╲╱╲╱╳╳╳\n\r'); + term.write(' ║│╲ ╱│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╿ │┃ ┍╅╆┓ ╵ ╹ ┗┷┛ └┸┘ ▌ ╱╲╱╲╳╳╳\n\r'); + term.write(' ╠╡ ╳ ╞╣ ├╢ ╟┤ ├┼─┼─┼┤ ├╫─╂─╫┤ ┣┿╾┼╼┿┫ ┕┛┖┚ ┌┄┄┐ ╎ ┏┅┅┓ ┋ ▍ ╲╱╲╱╳╳╳\n\r'); + term.write(' ║│╱ ╲│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╽ │┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▎\n\r'); + term.write(' ║└─╥─┘║ │╚═╤═╝│ │╘═╪═╛│ │╙─╀─╜│ ┃└─╂─┘┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▏\n\r'); + term.write(' ╚══╩══╝ └──┴──┘ ╰──┴──╯ ╰──┴──╯ ┗━━┻━━┛ └╌╌┘ ╎ ┗╍╍┛ ┋ ▁▂▃▄▅▆▇█\n\r'); + window.scrollTo(0, 0); +} + +function loadTest() { + const isWebglEnabled = !!addons.webgl.instance; + const testData = []; + let byteCount = 0; + for (let i = 0; i < 50; i++) { + const count = 1 + Math.floor(Math.random() * 79); + byteCount += count + 2; + const data = new Uint8Array(count + 2); + data[0] = 0x0A; // \n + for (let i = 1; i < count + 1; i++) { + data[i] = 0x61 + Math.floor(Math.random() * (0x7A - 0x61)); + } + // End each line with \r so the cursor remains constant, this is what ls/tree do and improves + // performance significantly due to the cursor DOM element not needing to change + data[data.length - 1] = 0x0D; // \r + testData.push(data); + } + const start = performance.now(); + for (let i = 0; i < 1024; i++) { + for (const d of testData) { + term.write(d); + } + } + // Wait for all data to be parsed before evaluating time + term.write('', () => { + const time = Math.round(performance.now() - start); + const mbs = ((byteCount / 1024) * (1 / (time / 1000))).toFixed(2); + term.write(`\n\r\nWrote ${byteCount}kB in ${time}ms (${mbs}MB/s) using the (${isWebglEnabled ? 'webgl' : 'canvas'} renderer)`); + // Send ^C to get a new prompt + term._core._onData.fire('\x03'); + }); +} diff --git a/demo/index.html b/demo/index.html index ab5b2040..9c86783b 100644 --- a/demo/index.html +++ b/demo/index.html @@ -15,41 +15,91 @@

xterm.js: A terminal for the web

-
-
-

Options

-

These options can be set in the Terminal constructor or by using the Terminal.setOption function.

-
-
-
-

Addons

-

Addons can be loaded and unloaded on a particular terminal to extend its functionality.

-
-

Addons Control

-

SearchAddon

-

- - - - - -

-

SerializeAddon

-

- - -

-

-
-
-

Style

-
- - +
+
+
+
+
+
+ + + + +
+
+

Options

+

These options can be set in the Terminal constructor or by using the Terminal.setOption function.

+
+
+
+

Addons

+

Addons can be loaded and unloaded on a particular terminal to extend its functionality.

+
+

Addons Control

+

SearchAddon

+
+ + + + + +
+

SerializeAddon

+
+ + +
+
+
+
+

Style

+
+ + +
+
+
+

Test

+
+ + + +
+
-
- - - + + + diff --git a/demo/server.js b/demo/server.js index c04c48ea..c0d5e1f6 100644 --- a/demo/server.js +++ b/demo/server.js @@ -43,15 +43,15 @@ function startServer() { const env = Object.assign({}, process.env); env['COLORTERM'] = 'truecolor'; var cols = parseInt(req.query.cols), - rows = parseInt(req.query.rows), - term = pty.spawn(process.platform === 'win32' ? 'cmd.exe' : 'bash', [], { - name: 'xterm-256color', - cols: cols || 80, - rows: rows || 24, - cwd: env.PWD, - env: env, - encoding: USE_BINARY ? null : 'utf8' - }); + rows = parseInt(req.query.rows), + term = pty.spawn(process.platform === 'win32' ? 'cmd.exe' : 'bash', [], { + name: 'xterm-256color', + cols: cols || 80, + rows: rows || 24, + cwd: process.platform === 'win32' ? undefined : env.PWD, + env: env, + encoding: USE_BINARY ? null : 'utf8' + }); console.log('Created terminal with PID: ' + term.pid); terminals[term.pid] = term; diff --git a/demo/start.js b/demo/start.js index a14627c1..b40b9bc3 100644 --- a/demo/start.js +++ b/demo/start.js @@ -49,6 +49,14 @@ const clientConfig = { alias: { common: path.resolve('./out/common'), browser: path.resolve('./out/browser') + }, + fallback: { + // The ligature modules contains fallbacks for node environments, we never want to browserify them + stream: false, + util: false, + os: false, + path: false, + fs: false } }, output: { diff --git a/demo/style.css b/demo/style.css index 9c5fd0bd..cebd08e0 100644 --- a/demo/style.css +++ b/demo/style.css @@ -41,3 +41,52 @@ pre { word-wrap: break-word; white-space: pre-wrap; } + + +#container { + display: flex; + height: 75vh; +} +#grid { + flex: 1; + /* max-height: 80vh; + overflow-y: auto; */ + width: 100%; +} +.tab { + overflow: hidden; + border: 1px solid #ccc; + background-color: #f1f1f1; +} + +/* Style the buttons inside the tab */ +.tab button { + background-color: inherit; + float: left; + border: none; + outline: none; + cursor: pointer; + padding: 14px 16px; + transition: 0.3s; + font-size: 17px; +} + +/* Change background color of buttons on hover */ +.tab button:hover { + background-color: #ddd; +} + +/* Create an active/current tablink class */ +.tab button.active { + background-color: #ccc; + } + +/* Style the tab content */ +.tabContent { + display: none; + padding: 6px 12px; + border: 1px solid #ccc; + border-top: none; + max-height: 100vh; + overflow-y: auto; +} diff --git a/headless/.gitignore b/headless/.gitignore new file mode 100644 index 00000000..f1cf6c8d --- /dev/null +++ b/headless/.gitignore @@ -0,0 +1,4 @@ +lib-headless/ +typings/ +logo-full.png +./package.json diff --git a/headless/.npmignore b/headless/.npmignore new file mode 100644 index 00000000..83e9f201 --- /dev/null +++ b/headless/.npmignore @@ -0,0 +1,6 @@ +# Include +!typings/*.d.ts +!lib-headless/ + +# Exclude +test/ diff --git a/headless/README.md b/headless/README.md new file mode 100644 index 00000000..1d2d6156 --- /dev/null +++ b/headless/README.md @@ -0,0 +1,31 @@ +# [![xterm.js logo](logo-full.png)](https://xtermjs.org) + +⚠ This package is experimental + +`xterm-headless` is a headless terminal that can be run in node.js. This is useful in combination with the frontend [`xterm`](https://www.npmjs.com/package/xterm) for example to keep track of a terminal's state on a remote server where the process is hosted. + +## Getting Started + +First, you need to install the module, we ship exclusively through npm, so you need that installed and then add xterm.js as a dependency by running: + +```sh +npm install xterm-headless +``` + +Then import as you would a regular node package. The recommended way to load `xterm-headless` is with TypeScript and the ES6 module syntax: + +```javascript +import { Terminal } from 'xterm-headless'; +``` + +## API + +The full API for `xterm-headless` is contained within the [TypeScript declaration file](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm-headless.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 to enable experimentation with new ideas without committing to support it like a normal [semver](https://semver.org/) 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 in `xterm-headless` work the [same as in `xterm`](https://github.com/xtermjs/xterm.js/blob/master/README.md#addons) with the one caveat being that the addon needs to be packaged for node.js and not use any DOM APIs. + +Currently no official addons are packaged on npm. diff --git a/package.json b/package.json index 59902931..ad288f49 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "xterm", "description": "Full xterm terminal, in your browser", - "version": "4.9.0", + "version": "4.14.1", "main": "lib/xterm.js", "style": "css/xterm.css", "types": "typings/xterm.d.ts", @@ -10,7 +10,10 @@ "scripts": { "prepackage": "npm run build", "package": "webpack", + "package-headless": "webpack --config ./webpack.config.headless.js", + "postpackage-headless": "node ./bin/package_headless.js", "start": "node demo/start", + "start-debug": "node --inspect-brk demo/start", "lint": "eslint -c .eslintrc.json --max-warnings 0 --ext .ts src/ addons/", "test": "npm run test-unit", "posttest": "npm run lint", @@ -20,6 +23,7 @@ "test-api-webkit": "node ./bin/test_api.js --browser=webkit --timeout=20000", "test-unit": "node ./bin/test.js", "test-unit-coverage": "node ./bin/test.js --coverage", + "test-unit-dev": "cross-env NODE_PATH='./out' mocha", "build": "tsc -b ./tsconfig.all.json", "prepare": "npm run setup", "setup": "npm run build", @@ -33,37 +37,39 @@ "vtfeatures": "node bin/extract_vtfeatures.js src/**/*.ts src/*.ts" }, "devDependencies": { - "@types/chai": "^4.2.14", + "@types/chai": "^4.2.18", "@types/debug": "^4.1.5", "@types/deep-equal": "^1.0.1", "@types/glob": "^7.1.3", - "@types/jsdom": "^16.2.5", - "@types/mocha": "^8.0.3", - "@types/node": "^10.17.17", + "@types/jsdom": "^16.2.10", + "@types/mocha": "^8.2.2", + "@types/node": "^14.14.44", "@types/utf8": "^2.1.6", - "@types/webpack": "^4.41.24", - "@types/ws": "^7.2.9", - "@typescript-eslint/eslint-plugin": "^4.0.0", - "@typescript-eslint/parser": "^3.10.1", - "chai": "^4.2.0", - "deep-equal": "^2.0.4", - "eslint": "^7.12.1", + "@types/webpack": "^5.28.0", + "@types/ws": "^7.4.4", + "@typescript-eslint/eslint-plugin": "^4.23.0", + "@typescript-eslint/parser": "^4.23.0", + "chai": "^4.3.4", + "cross-env": "^7.0.3", + "deep-equal": "^2.0.5", + "eslint": "^7.26.0", "express": "^4.17.1", "express-ws": "^4.0.0", - "glob": "^7.0.5", - "jsdom": "^16.4.0", - "mocha": "^8.2.1", - "mustache": "^4.0.1", - "node-pty": "^0.9.0", + "glob": "^7.1.7", + "jsdom": "^16.5.3", + "mocha": "^8.4.0", + "mustache": "^4.2.0", + "node-pty": "^0.10.1", "nyc": "^15.1.0", - "playwright": "^1.5.2", - "source-map-loader": "^1.1.2", - "ts-loader": "^8.0.8", - "typescript": "4.0", + "playwright": "^1.11.0", + "source-map-loader": "^2.0.1", + "source-map-support": "^0.5.19", + "ts-loader": "^9.1.2", + "typescript": "^4.2.4", "utf8": "^3.0.0", - "webpack": "^5.4.0", - "webpack-cli": "^4.2.0", - "ws": "^7.3.1", - "xterm-benchmark": "^0.1.3" + "webpack": "^5.37.0", + "webpack-cli": "^4.7.0", + "ws": "^7.4.5", + "xterm-benchmark": "^0.2.1" } } diff --git a/src/browser/AccessibilityManager.ts b/src/browser/AccessibilityManager.ts index c55aaad9..80092202 100644 --- a/src/browser/AccessibilityManager.ts +++ b/src/browser/AccessibilityManager.ts @@ -4,10 +4,10 @@ */ import * as Strings from 'browser/LocalizableStrings'; -import { ITerminal } from 'browser/Types'; +import { ITerminal, IRenderDebouncer } from 'browser/Types'; import { IBuffer } from 'common/buffer/Types'; import { isMac } from 'common/Platform'; -import { RenderDebouncer } from 'browser/RenderDebouncer'; +import { TimeBasedDebouncer } from 'browser/TimeBasedDebouncer'; import { addDisposableDomListener } from 'browser/Lifecycle'; import { Disposable } from 'common/Lifecycle'; import { ScreenDprMonitor } from 'browser/ScreenDprMonitor'; @@ -28,7 +28,7 @@ export class AccessibilityManager extends Disposable { private _liveRegion: HTMLElement; private _liveRegionLineCount: number = 0; - private _renderRowsDebouncer: RenderDebouncer; + private _renderRowsDebouncer: IRenderDebouncer; private _screenDprMonitor: ScreenDprMonitor; private _topBoundaryFocusListener: (e: FocusEvent) => void; @@ -53,6 +53,7 @@ export class AccessibilityManager extends Disposable { ) { super(); this._accessibilityTreeRoot = document.createElement('div'); + this._accessibilityTreeRoot.setAttribute('role', 'document'); this._accessibilityTreeRoot.classList.add('xterm-accessibility'); this._rowContainer = document.createElement('div'); @@ -72,7 +73,7 @@ export class AccessibilityManager extends Disposable { this._refreshRowsDimensions(); this._accessibilityTreeRoot.appendChild(this._rowContainer); - this._renderRowsDebouncer = new RenderDebouncer(this._renderRows.bind(this)); + this._renderRowsDebouncer = new TimeBasedDebouncer(this._renderRows.bind(this)); this._refreshRows(); this._liveRegion = document.createElement('div'); @@ -112,7 +113,7 @@ export class AccessibilityManager extends Disposable { } private _onBoundaryFocus(e: FocusEvent, position: BoundaryPosition): void { - const boundaryElement = e.target; + const boundaryElement = e.target as HTMLElement; const beforeBoundaryElement = this._rowElements[position === BoundaryPosition.TOP ? 1 : this._rowElements.length - 2]; // Don't scroll if the buffer top has reached the end in that direction @@ -263,7 +264,7 @@ export class AccessibilityManager extends Disposable { const element = this._rowElements[i]; if (element) { if (lineData.length === 0) { - element.innerHTML = ' '; + element.innerText = '\u00a0'; } else { element.textContent = lineData; } diff --git a/src/browser/Clipboard.ts b/src/browser/Clipboard.ts index b0b42022..29e865c8 100644 --- a/src/browser/Clipboard.ts +++ b/src/browser/Clipboard.ts @@ -89,8 +89,8 @@ export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextA export function rightClickHandler(ev: MouseEvent, textarea: HTMLTextAreaElement, screenElement: HTMLElement, selectionService: ISelectionService, shouldSelectWord: boolean): void { moveTextAreaUnderMouseCursor(ev, textarea, screenElement); - if (shouldSelectWord && !selectionService.isClickInSelection(ev)) { - selectionService.selectWordAtCursor(ev); + if (shouldSelectWord) { + selectionService.rightClickSelect(ev); } // Get textarea ready to copy from the context menu diff --git a/src/browser/ColorManager.test.ts b/src/browser/ColorManager.test.ts index 766f4c12..926a7df3 100644 --- a/src/browser/ColorManager.test.ts +++ b/src/browser/ColorManager.test.ts @@ -17,7 +17,7 @@ describe('ColorManager', () => { dom = new jsdom.JSDOM(''); window = dom.window; document = window.document; - (window).HTMLCanvasElement.prototype.getContext = () => ({ + (window as any).HTMLCanvasElement.prototype.getContext = () => ({ createLinearGradient(): any { return null; }, @@ -36,7 +36,7 @@ describe('ColorManager', () => { for (const key of Object.keys(cm.colors)) { if (key !== 'ansi' && key !== 'contrastCache') { // A #rrggbb or rgba(...) - assert.ok((cm.colors)[key].css.length >= 7); + assert.ok((cm.colors as any)[key].css.length >= 7); } } assert.equal(cm.colors.ansi.length, 256); diff --git a/src/browser/ColorManager.ts b/src/browser/ColorManager.ts index 9a0fd7ee..b6950d28 100644 --- a/src/browser/ColorManager.ts +++ b/src/browser/ColorManager.ts @@ -17,9 +17,8 @@ const DEFAULT_SELECTION = { rgba: 0xFFFFFF4D }; -// An IIFE to generate DEFAULT_ANSI_COLORS. Do not mutate DEFAULT_ANSI_COLORS, instead make a copy -// and mutate that. -export const DEFAULT_ANSI_COLORS = (() => { +// An IIFE to generate DEFAULT_ANSI_COLORS. +export const DEFAULT_ANSI_COLORS = Object.freeze((() => { const colors = [ // dark: css.toColor('#2e3436'), @@ -64,7 +63,7 @@ export const DEFAULT_ANSI_COLORS = (() => { } return colors; -})(); +})()); /** * Manages the source of truth for a terminal's colors. diff --git a/src/browser/Linkifier.test.ts b/src/browser/Linkifier.test.ts index 2567a669..a07f69ed 100644 --- a/src/browser/Linkifier.test.ts +++ b/src/browser/Linkifier.test.ts @@ -174,7 +174,7 @@ describe('Linkifier', () => { assert.equal(mouseZoneManager.zones[0].y1, 1); assert.equal(mouseZoneManager.zones[0].y2, 1); // Fires done() - mouseZoneManager.zones[0].clickCallback({}); + mouseZoneManager.zones[0].clickCallback({} as any); } }); linkifier.linkifyRows(); @@ -210,7 +210,7 @@ describe('Linkifier', () => { let count = 0; linkifier.registerLinkMatcher(/test/, () => assert.fail(), { validationCallback: (url, cb) => { - count += 1; + count++; if (count === 2) { done(); } diff --git a/src/browser/Linkifier.ts b/src/browser/Linkifier.ts index 6d25e730..b17d66a8 100644 --- a/src/browser/Linkifier.ts +++ b/src/browser/Linkifier.ts @@ -89,7 +89,9 @@ export class Linkifier implements ILinkifier { if (this._rowsTimeoutId) { clearTimeout(this._rowsTimeoutId); } - this._rowsTimeoutId = setTimeout(() => this._linkifyRows(), Linkifier._timeBeforeLatency); + + // Cannot use window.setTimeout since tests need to run in node + this._rowsTimeoutId = setTimeout(() => this._linkifyRows(), Linkifier._timeBeforeLatency) as any as number; } /** diff --git a/src/browser/Linkifier2.ts b/src/browser/Linkifier2.ts index 73ea0268..89542936 100644 --- a/src/browser/Linkifier2.ts +++ b/src/browser/Linkifier2.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { ILinkifier2, ILinkProvider, IBufferCellPosition, ILink, ILinkifierEvent, ILinkDecorations } from 'browser/Types'; +import { ILinkifier2, ILinkProvider, IBufferCellPosition, ILink, ILinkifierEvent, ILinkDecorations, ILinkWithState } from 'browser/Types'; import { IDisposable } from 'common/Types'; import { IMouseService, IRenderService } from './services/Services'; import { IBufferService } from 'common/services/Services'; @@ -11,21 +11,12 @@ import { EventEmitter, IEvent } from 'common/EventEmitter'; import { Disposable, getDisposeArrayDisposable, disposeArray } from 'common/Lifecycle'; import { addDisposableDomListener } from 'browser/Lifecycle'; -interface ILinkState { - decorations: ILinkDecorations; - isHovered: boolean; -} - -interface ILinkWithState { - link: ILink; - state?: ILinkState; -} - export class Linkifier2 extends Disposable implements ILinkifier2 { private _element: HTMLElement | undefined; private _mouseService: IMouseService | undefined; private _renderService: IRenderService | undefined; private _linkProviders: ILinkProvider[] = []; + public get currentLink(): ILinkWithState | undefined { return this._currentLink; } protected _currentLink: ILinkWithState | undefined; private _lastMouseEvent: MouseEvent | undefined; private _linkCacheDisposables: IDisposable[] = []; diff --git a/src/browser/MouseZoneManager.ts b/src/browser/MouseZoneManager.ts index 59b6b0f1..b6740157 100644 --- a/src/browser/MouseZoneManager.ts +++ b/src/browser/MouseZoneManager.ts @@ -156,9 +156,7 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager { private _onTooltip(e: MouseEvent): void { this._tooltipTimeout = undefined; const zone = this._findZoneEventAt(e); - if (zone && zone.tooltipCallback) { - zone.tooltipCallback(e); - } + zone?.tooltipCallback(e); } private _onMouseDown(e: MouseEvent): void { diff --git a/src/browser/RenderDebouncer.ts b/src/browser/RenderDebouncer.ts index 2a06fdd6..02521070 100644 --- a/src/browser/RenderDebouncer.ts +++ b/src/browser/RenderDebouncer.ts @@ -3,12 +3,12 @@ * @license MIT */ -import { IDisposable } from 'common/Types'; +import { IRenderDebouncer } from 'browser/Types'; /** * Debounces calls to render terminal rows using animation frames. */ -export class RenderDebouncer implements IDisposable { +export class RenderDebouncer implements IRenderDebouncer { private _rowStart: number | undefined; private _rowEnd: number | undefined; private _rowCount: number | undefined; diff --git a/src/browser/Terminal.test.ts b/src/browser/Terminal.test.ts index 8626be09..a102d93f 100644 --- a/src/browser/Terminal.test.ts +++ b/src/browser/Terminal.test.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { assert, expect } from 'chai'; +import { assert } from 'chai'; import { MockViewport, MockCompositionHelper, MockRenderer, TestTerminal } from 'browser/TestUtils.test'; import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; import { CellData } from 'common/buffer/CellData'; @@ -29,10 +29,10 @@ describe('Terminal', () => { beforeEach(() => { term = new TestTerminal(termOptions); term.refresh = () => { }; - (term).renderer = new MockRenderer(); + (term as any).renderer = new MockRenderer(); term.viewport = new MockViewport(); - (term)._compositionHelper = new MockCompositionHelper(); - (term).element = { + (term as any)._compositionHelper = new MockCompositionHelper(); + (term as any).element = { classList: { toggle: () => { }, remove: () => { } @@ -55,55 +55,62 @@ describe('Terminal', () => { // term.onData(() => done()); // term.handler('fake'); // }); - it('should fire the onCursorMove event', (done) => { - term.onCursorMove(() => done()); - term.writeSync('foo'); + it('should fire the onCursorMove event', () => { + return new Promise(async r => { + term.onCursorMove(() => r()); + await term.writeP('foo'); + }); }); - it('should fire the onLineFeed event', (done) => { - term.onLineFeed(() => done()); - term.writeSync('\n'); + it('should fire the onLineFeed event', () => { + return new Promise(async r => { + term.onLineFeed(() => r()); + await term.writeP('\n'); + }); }); - it('should fire a scroll event when scrollback is created', (done) => { - term.onScroll(() => done()); - term.writeSync('\n'.repeat(INIT_ROWS)); + it('should fire a scroll event when scrollback is created', () => { + return new Promise(async r => { + term.onScroll(() => r()); + await term.writeP('\n'.repeat(INIT_ROWS)); + }); }); - it('should fire a scroll event when scrollback is cleared', (done) => { - term.writeSync('\n'.repeat(INIT_ROWS)); - term.onScroll(() => done()); - term.clear(); + it('should fire a scroll event when scrollback is cleared', () => { + return new Promise(async r => { + await term.writeP('\n'.repeat(INIT_ROWS)); + term.onScroll(() => r()); + term.clear(); + }); }); it('should fire a key event after a keypress DOM event', (done) => { term.onKey(e => { assert.equal(typeof e.key, 'string'); - expect(e.domEvent).to.be.an.instanceof(Object); + assert.equal(e.domEvent instanceof Object, true); done(); }); - const evKeyPress = { + const evKeyPress = { preventDefault: () => { }, stopPropagation: () => { }, type: 'keypress', keyCode: 13 - }; + } as KeyboardEvent; term.keyPress(evKeyPress); }); it('should fire a key event after a keydown DOM event', (done) => { term.onKey(e => { assert.equal(typeof e.key, 'string'); - expect(e.domEvent).to.be.an.instanceof(Object); + assert.equal(e.domEvent instanceof Object, true); done(); }); - (term).textarea = { value: '' }; - const evKeyDown = { + (term as any).textarea = { value: '' }; + const evKeyDown = { preventDefault: () => { }, stopPropagation: () => { }, type: 'keydown', keyCode: 13 - }; + } as KeyboardEvent; term.keyDown(evKeyDown); }); it('should fire the onResize event', (done) => { term.onResize(e => { - expect(e).to.have.keys(['cols', 'rows']); assert.equal(typeof e.cols, 'number'); assert.equal(typeof e.rows, 'number'); done(); @@ -124,21 +131,27 @@ describe('Terminal', () => { }); term.write('\x1b]2;title\x07'); }); + it('should fire the onBell event', (done) => { + term.onBell(e => { + done(); + }); + term.write('\x07'); + }); }); describe('attachCustomKeyEventHandler', () => { - const evKeyDown = { + const evKeyDown = { preventDefault: () => { }, stopPropagation: () => { }, type: 'keydown', keyCode: 77 - }; - const evKeyPress = { + } as KeyboardEvent; + const evKeyPress = { preventDefault: () => { }, stopPropagation: () => { }, type: 'keypress', keyCode: 77 - }; + } as KeyboardEvent; beforeEach(() => { term.clearSelection = () => { }; @@ -178,10 +191,10 @@ describe('Terminal', () => { assert.deepEqual(term.buffer.lines.get(i), term.buffer.getBlankLine(DEFAULT_ATTR_DATA)); } }); - it('should clear a buffer larger than rows', () => { + it('should clear a buffer larger than rows', async () => { // Fill the buffer with dummy rows for (let i = 0; i < term.rows * 2; i++) { - term.writeSync('test\n'); + await term.writeP('test\n'); } const promptLine = term.buffer.lines.get(term.buffer.ybase + term.buffer.y); @@ -225,22 +238,24 @@ describe('Terminal', () => { }); term.paste('\r\nfoo\nbar\r'); }); - it('should respect bracketed paste mode', done => { - term.onData(e => { - assert.equal(e, '\x1b[200~foo\x1b[201~'); - done(); + it('should respect bracketed paste mode', () => { + return new Promise(async r => { + term.onData(e => { + assert.equal(e, '\x1b[200~foo\x1b[201~'); + r(); + }); + await term.writeP('\x1b[?2004h'); + term.paste('foo'); }); - term.writeSync('\x1b[?2004h'); - term.paste('foo'); }); }); describe('scroll', () => { describe('scrollLines', () => { let startYDisp: number; - beforeEach(() => { + beforeEach(async () => { for (let i = 0; i < INIT_ROWS * 2; i++) { - term.writeSync('test\r\n'); + await term.writeP('test\r\n'); } startYDisp = INIT_ROWS + 1; }); @@ -273,9 +288,9 @@ describe('Terminal', () => { describe('scrollPages', () => { let startYDisp: number; - beforeEach(() => { + beforeEach(async () => { for (let i = 0; i < term.rows * 3; i++) { - term.writeSync('test\r\n'); + await term.writeP('test\r\n'); } startYDisp = (term.rows * 2) + 1; }); @@ -296,9 +311,9 @@ describe('Terminal', () => { }); describe('scrollToTop', () => { - beforeEach(() => { + beforeEach(async () => { for (let i = 0; i < term.rows * 3; i++) { - term.writeSync('test\r\n'); + await term.writeP('test\r\n'); } }); it('should scroll to the top', () => { @@ -310,9 +325,9 @@ describe('Terminal', () => { describe('scrollToBottom', () => { let startYDisp: number; - beforeEach(() => { + beforeEach(async () => { for (let i = 0; i < term.rows * 3; i++) { - term.writeSync('test\r\n'); + await term.writeP('test\r\n'); } startYDisp = (term.rows * 2) + 1; }); @@ -331,9 +346,9 @@ describe('Terminal', () => { describe('scrollToLine', () => { let startYDisp: number; - beforeEach(() => { + beforeEach(async () => { for (let i = 0; i < term.rows * 3; i++) { - term.writeSync('test\r\n'); + await term.writeP('test\r\n'); } startYDisp = (term.rows * 2) + 1; }); @@ -359,13 +374,13 @@ describe('Terminal', () => { describe('keyPress', () => { it('should scroll down, when a key is pressed and terminal is scrolled up', () => { - const event = { + const event = { type: 'keydown', key: 'a', keyCode: 65, preventDefault: () => { }, stopPropagation: () => { } - }; + } as KeyboardEvent; term.buffer.ydisp = 0; term.buffer.ybase = 40; @@ -375,10 +390,10 @@ describe('Terminal', () => { assert.equal(term.buffer.ydisp, term.buffer.ybase); }); - it('should not scroll down, when a custom keydown handler prevents the event', () => { + it('should not scroll down, when a custom keydown handler prevents the event', async () => { // Add some output to the terminal for (let i = 0; i < term.rows * 3; i++) { - term.writeSync('test\r\n'); + await term.writeP('test\r\n'); } const startYDisp = (term.rows * 2) + 1; term.attachCustomKeyEventHandler(() => { @@ -388,7 +403,7 @@ describe('Terminal', () => { assert.equal(term.buffer.ydisp, startYDisp); term.scrollLines(-1); assert.equal(term.buffer.ydisp, startYDisp - 1); - term.keyPress({ keyCode: 0 }); + term.keyPress({ keyCode: 0 }); assert.equal(term.buffer.ydisp, startYDisp - 1); }); }); @@ -717,315 +732,315 @@ describe('Terminal', () => { }); describe('unicode - surrogates', () => { - it('2 characters per cell', function (): void { - this.timeout(10000); // This is needed because istanbul patches code and slows it down - const high = String.fromCharCode(0xD800); - const cell = new CellData(); - for (let i = 0xDC00; i <= 0xDCFF; ++i) { - term.writeSync(high + String.fromCharCode(i)); - const tchar = term.buffer.lines.get(0)!.loadCell(0, cell); - expect(tchar.getChars()).eql(high + String.fromCharCode(i)); - expect(tchar.getChars().length).eql(2); - expect(tchar.getWidth()).eql(1); - expect(term.buffer.lines.get(0)!.loadCell(1, cell).getChars()).eql(''); - term.reset(); - } - }); - it('2 characters at last cell', () => { - const high = String.fromCharCode(0xD800); - const cell = new CellData(); - for (let i = 0xDC00; i <= 0xDCFF; ++i) { - term.buffer.x = term.cols - 1; - term.writeSync(high + String.fromCharCode(i)); - expect(term.buffer.lines.get(0)!.loadCell(term.buffer.x - 1, cell).getChars()).eql(high + String.fromCharCode(i)); - expect(term.buffer.lines.get(0)!.loadCell(term.buffer.x - 1, cell).getChars().length).eql(2); - expect(term.buffer.lines.get(1)!.loadCell(0, cell).getChars()).eql(''); - term.reset(); - } - }); - it('2 characters per cell over line end with autowrap', () => { - const high = String.fromCharCode(0xD800); - const cell = new CellData(); - for (let i = 0xDC00; i <= 0xDCFF; ++i) { - term.buffer.x = term.cols - 1; - - term.writeSync('a' + high + String.fromCharCode(i)); - expect(term.buffer.lines.get(0)!.loadCell(term.cols - 1, cell).getChars()).eql('a'); - expect(term.buffer.lines.get(1)!.loadCell(0, cell).getChars()).eql(high + String.fromCharCode(i)); - expect(term.buffer.lines.get(1)!.loadCell(0, cell).getChars().length).eql(2); - expect(term.buffer.lines.get(1)!.loadCell(1, cell).getChars()).eql(''); - term.reset(); - } - }); - it('2 characters per cell over line end without autowrap', () => { - const high = String.fromCharCode(0xD800); - const cell = new CellData(); - for (let i = 0xDC00; i <= 0xDCFF; ++i) { - term.buffer.x = term.cols - 1; - term.writeSync('\x1b[?7l'); // Disable wraparound mode - const width = wcwidth((0xD800 - 0xD800) * 0x400 + i - 0xDC00 + 0x10000); - if (width !== 1) { - continue; + for (let i = 0xDC00; i <= 0xDCF0; i += 0x10) { + const range = `0x${i.toString(16).toUpperCase()}-0x${(i + 0xF).toString(16).toUpperCase()}`; + it(`${range}: 2 characters per cell`, async function (): Promise { + const high = String.fromCharCode(0xD800); + const cell = new CellData(); + for (let j = i; j <= i + 0xF; j++) { + await term.writeP(high + String.fromCharCode(j)); + const tchar = term.buffer.lines.get(0)!.loadCell(0, cell); + assert.equal(tchar.getChars(), high + String.fromCharCode(j)); + assert.equal(tchar.getChars().length, 2); + assert.equal(tchar.getWidth(), 1); + assert.equal(term.buffer.lines.get(0)!.loadCell(1, cell).getChars(), ''); + term.reset(); } - term.writeSync('a' + high + String.fromCharCode(i)); - // auto wraparound mode should cut off the rest of the line - expect(term.buffer.lines.get(0)!.loadCell(term.cols - 1, cell).getChars()).eql(high + String.fromCharCode(i)); - expect(term.buffer.lines.get(0)!.loadCell(term.cols - 1, cell).getChars().length).eql(2); - expect(term.buffer.lines.get(1)!.loadCell(1, cell).getChars()).eql(''); - term.reset(); - } - }); - it('splitted surrogates', () => { - const high = String.fromCharCode(0xD800); - const cell = new CellData(); - for (let i = 0xDC00; i <= 0xDCFF; ++i) { - term.writeSync(high); - term.writeSync(String.fromCharCode(i)); - const tchar = term.buffer.lines.get(0)!.loadCell(0, cell); - expect(tchar.getChars()).eql(high + String.fromCharCode(i)); - expect(tchar.getChars().length).eql(2); - expect(tchar.getWidth()).eql(1); - expect(term.buffer.lines.get(0)!.loadCell(1, cell).getChars()).eql(''); - term.reset(); - } - }); + }); + it(`${range}: 2 characters at last cell`, async () => { + const high = String.fromCharCode(0xD800); + const cell = new CellData(); + term.buffer.x = term.cols - 1; + for (let j = i; j <= i + 0xF; j++) { + await term.writeP(high + String.fromCharCode(j)); + assert.equal(term.buffer.lines.get(0)!.loadCell(term.buffer.x - 1, cell).getChars(), high + String.fromCharCode(j)); + assert.equal(term.buffer.lines.get(0)!.loadCell(term.buffer.x - 1, cell).getChars().length, 2); + assert.equal(term.buffer.lines.get(1)!.loadCell(0, cell).getChars(), ''); + term.reset(); + } + }); + it(`${range}: 2 characters per cell over line end with autowrap`, async function (): Promise { + const high = String.fromCharCode(0xD800); + const cell = new CellData(); + for (let j = i; j <= i + 0xF; j++) { + term.buffer.x = term.cols - 1; + await term.writeP('a' + high + String.fromCharCode(j)); + assert.equal(term.buffer.lines.get(0)!.loadCell(term.cols - 1, cell).getChars(), 'a'); + assert.equal(term.buffer.lines.get(1)!.loadCell(0, cell).getChars(), high + String.fromCharCode(j)); + assert.equal(term.buffer.lines.get(1)!.loadCell(0, cell).getChars().length, 2); + assert.equal(term.buffer.lines.get(1)!.loadCell(1, cell).getChars(), ''); + term.reset(); + } + }); + it(`${range}: 2 characters per cell over line end without autowrap`, async function (): Promise { + const high = String.fromCharCode(0xD800); + const cell = new CellData(); + for (let j = i; j <= i + 0xF; j++) { + term.buffer.x = term.cols - 1; + await term.writeP('\x1b[?7l'); // Disable wraparound mode + const width = wcwidth((0xD800 - 0xD800) * 0x400 + j - 0xDC00 + 0x10000); + if (width !== 1) { + continue; + } + await term.writeP('a' + high + String.fromCharCode(j)); + // auto wraparound mode should cut off the rest of the line + assert.equal(term.buffer.lines.get(0)!.loadCell(term.cols - 1, cell).getChars(), high + String.fromCharCode(j)); + assert.equal(term.buffer.lines.get(0)!.loadCell(term.cols - 1, cell).getChars().length, 2); + assert.equal(term.buffer.lines.get(1)!.loadCell(1, cell).getChars(), ''); + term.reset(); + } + }); + it(`${range}: splitted surrogates`, async function (): Promise { + const high = String.fromCharCode(0xD800); + const cell = new CellData(); + for (let j = i; j <= i + 0xF; j++) { + await term.writeP(high + String.fromCharCode(j)); + const tchar = term.buffer.lines.get(0)!.loadCell(0, cell); + assert.equal(tchar.getChars(), high + String.fromCharCode(j)); + assert.equal(tchar.getChars().length, 2); + assert.equal(tchar.getWidth(), 1); + assert.equal(term.buffer.lines.get(0)!.loadCell(1, cell).getChars(), ''); + term.reset(); + } + }); + } }); describe('unicode - combining characters', () => { const cell = new CellData(); - it('café', () => { - term.writeSync('cafe\u0301'); + it('café', async () => { + await term.writeP('cafe\u0301'); term.buffer.lines.get(0)!.loadCell(3, cell); - expect(cell.getChars()).eql('e\u0301'); - expect(cell.getChars().length).eql(2); - expect(cell.getWidth()).eql(1); + assert.equal(cell.getChars(), 'e\u0301'); + assert.equal(cell.getChars().length, 2); + assert.equal(cell.getWidth(), 1); }); - it('café - end of line', () => { + it('café - end of line', async () => { term.buffer.x = term.cols - 1 - 3; - term.writeSync('cafe\u0301'); + await term.writeP('cafe\u0301'); term.buffer.lines.get(0)!.loadCell(term.cols - 1, cell); - expect(cell.getChars()).eql('e\u0301'); - expect(cell.getChars().length).eql(2); - expect(cell.getWidth()).eql(1); + assert.equal(cell.getChars(), 'e\u0301'); + assert.equal(cell.getChars().length, 2); + assert.equal(cell.getWidth(), 1); term.buffer.lines.get(0)!.loadCell(1, cell); - expect(cell.getChars()).eql(''); - expect(cell.getChars().length).eql(0); - expect(cell.getWidth()).eql(1); + assert.equal(cell.getChars(), ''); + assert.equal(cell.getChars().length, 0); + assert.equal(cell.getWidth(), 1); }); - it('multiple combined é', () => { - term.writeSync(Array(100).join('e\u0301')); + it('multiple combined é', async () => { + await term.writeP(Array(100).join('e\u0301')); for (let i = 0; i < term.cols; ++i) { term.buffer.lines.get(0)!.loadCell(i, cell); - expect(cell.getChars()).eql('e\u0301'); - expect(cell.getChars().length).eql(2); - expect(cell.getWidth()).eql(1); + assert.equal(cell.getChars(), 'e\u0301'); + assert.equal(cell.getChars().length, 2); + assert.equal(cell.getWidth(), 1); } term.buffer.lines.get(1)!.loadCell(0, cell); - expect(cell.getChars()).eql('e\u0301'); - expect(cell.getChars().length).eql(2); - expect(cell.getWidth()).eql(1); + assert.equal(cell.getChars(), 'e\u0301'); + assert.equal(cell.getChars().length, 2); + assert.equal(cell.getWidth(), 1); }); - it('multiple surrogate with combined', () => { - term.writeSync(Array(100).join('\uD800\uDC00\u0301')); + it('multiple surrogate with combined', async () => { + await term.writeP(Array(100).join('\uD800\uDC00\u0301')); for (let i = 0; i < term.cols; ++i) { term.buffer.lines.get(0)!.loadCell(i, cell); - expect(cell.getChars()).eql('\uD800\uDC00\u0301'); - expect(cell.getChars().length).eql(3); - expect(cell.getWidth()).eql(1); + assert.equal(cell.getChars(), '\uD800\uDC00\u0301'); + assert.equal(cell.getChars().length, 3); + assert.equal(cell.getWidth(), 1); } term.buffer.lines.get(1)!.loadCell(0, cell); - expect(cell.getChars()).eql('\uD800\uDC00\u0301'); - expect(cell.getChars().length).eql(3); - expect(cell.getWidth()).eql(1); + assert.equal(cell.getChars(), '\uD800\uDC00\u0301'); + assert.equal(cell.getChars().length, 3); + assert.equal(cell.getWidth(), 1); }); }); describe('unicode - fullwidth characters', () => { const cell = new CellData(); - it('cursor movement even', () => { - expect(term.buffer.x).eql(0); - term.writeSync('¥'); - expect(term.buffer.x).eql(2); + it('cursor movement even', async () => { + assert.equal(term.buffer.x, 0); + await term.writeP('¥'); + assert.equal(term.buffer.x, 2); }); - it('cursor movement odd', () => { + it('cursor movement odd', async () => { term.buffer.x = 1; - expect(term.buffer.x).eql(1); - term.writeSync('¥'); - expect(term.buffer.x).eql(3); + assert.equal(term.buffer.x, 1); + await term.writeP('¥'); + assert.equal(term.buffer.x, 3); }); - it('line of ¥ even', () => { - term.writeSync(Array(50).join('¥')); + it('line of ¥ even', async () => { + await term.writeP(Array(50).join('¥')); for (let i = 0; i < term.cols; ++i) { term.buffer.lines.get(0)!.loadCell(i, cell); if (i % 2) { - expect(cell.getChars()).eql(''); - expect(cell.getChars().length).eql(0); - expect(cell.getWidth()).eql(0); + assert.equal(cell.getChars(), ''); + assert.equal(cell.getChars().length, 0); + assert.equal(cell.getWidth(), 0); } else { - expect(cell.getChars()).eql('¥'); - expect(cell.getChars().length).eql(1); - expect(cell.getWidth()).eql(2); + assert.equal(cell.getChars(), '¥'); + assert.equal(cell.getChars().length, 1); + assert.equal(cell.getWidth(), 2); } } term.buffer.lines.get(1)!.loadCell(0, cell); - expect(cell.getChars()).eql('¥'); - expect(cell.getChars().length).eql(1); - expect(cell.getWidth()).eql(2); + assert.equal(cell.getChars(), '¥'); + assert.equal(cell.getChars().length, 1); + assert.equal(cell.getWidth(), 2); }); - it('line of ¥ odd', () => { + it('line of ¥ odd', async () => { term.buffer.x = 1; - term.writeSync(Array(50).join('¥')); + await term.writeP(Array(50).join('¥')); for (let i = 1; i < term.cols - 1; ++i) { term.buffer.lines.get(0)!.loadCell(i, cell); if (!(i % 2)) { - expect(cell.getChars()).eql(''); - expect(cell.getChars().length).eql(0); - expect(cell.getWidth()).eql(0); + assert.equal(cell.getChars(), ''); + assert.equal(cell.getChars().length, 0); + assert.equal(cell.getWidth(), 0); } else { - expect(cell.getChars()).eql('¥'); - expect(cell.getChars().length).eql(1); - expect(cell.getWidth()).eql(2); + assert.equal(cell.getChars(), '¥'); + assert.equal(cell.getChars().length, 1); + assert.equal(cell.getWidth(), 2); } } term.buffer.lines.get(0)!.loadCell(term.cols - 1, cell); - expect(cell.getChars()).eql(''); - expect(cell.getChars().length).eql(0); - expect(cell.getWidth()).eql(1); + assert.equal(cell.getChars(), ''); + assert.equal(cell.getChars().length, 0); + assert.equal(cell.getWidth(), 1); term.buffer.lines.get(1)!.loadCell(0, cell); - expect(cell.getChars()).eql('¥'); - expect(cell.getChars().length).eql(1); - expect(cell.getWidth()).eql(2); + assert.equal(cell.getChars(), '¥'); + assert.equal(cell.getChars().length, 1); + assert.equal(cell.getWidth(), 2); }); - it('line of ¥ with combining odd', () => { + it('line of ¥ with combining odd', async () => { term.buffer.x = 1; - term.writeSync(Array(50).join('¥\u0301')); + await term.writeP(Array(50).join('¥\u0301')); for (let i = 1; i < term.cols - 1; ++i) { term.buffer.lines.get(0)!.loadCell(i, cell); if (!(i % 2)) { - expect(cell.getChars()).eql(''); - expect(cell.getChars().length).eql(0); - expect(cell.getWidth()).eql(0); + assert.equal(cell.getChars(), ''); + assert.equal(cell.getChars().length, 0); + assert.equal(cell.getWidth(), 0); } else { - expect(cell.getChars()).eql('¥\u0301'); - expect(cell.getChars().length).eql(2); - expect(cell.getWidth()).eql(2); + assert.equal(cell.getChars(), '¥\u0301'); + assert.equal(cell.getChars().length, 2); + assert.equal(cell.getWidth(), 2); } } term.buffer.lines.get(0)!.loadCell(term.cols - 1, cell); - expect(cell.getChars()).eql(''); - expect(cell.getChars().length).eql(0); - expect(cell.getWidth()).eql(1); + assert.equal(cell.getChars(), ''); + assert.equal(cell.getChars().length, 0); + assert.equal(cell.getWidth(), 1); term.buffer.lines.get(1)!.loadCell(0, cell); - expect(cell.getChars()).eql('¥\u0301'); - expect(cell.getChars().length).eql(2); - expect(cell.getWidth()).eql(2); + assert.equal(cell.getChars(), '¥\u0301'); + assert.equal(cell.getChars().length, 2); + assert.equal(cell.getWidth(), 2); }); - it('line of ¥ with combining even', () => { - term.writeSync(Array(50).join('¥\u0301')); + it('line of ¥ with combining even', async () => { + await term.writeP(Array(50).join('¥\u0301')); for (let i = 0; i < term.cols; ++i) { term.buffer.lines.get(0)!.loadCell(i, cell); if (i % 2) { - expect(cell.getChars()).eql(''); - expect(cell.getChars().length).eql(0); - expect(cell.getWidth()).eql(0); + assert.equal(cell.getChars(), ''); + assert.equal(cell.getChars().length, 0); + assert.equal(cell.getWidth(), 0); } else { - expect(cell.getChars()).eql('¥\u0301'); - expect(cell.getChars().length).eql(2); - expect(cell.getWidth()).eql(2); + assert.equal(cell.getChars(), '¥\u0301'); + assert.equal(cell.getChars().length, 2); + assert.equal(cell.getWidth(), 2); } } term.buffer.lines.get(1)!.loadCell(0, cell); - expect(cell.getChars()).eql('¥\u0301'); - expect(cell.getChars().length).eql(2); - expect(cell.getWidth()).eql(2); + assert.equal(cell.getChars(), '¥\u0301'); + assert.equal(cell.getChars().length, 2); + assert.equal(cell.getWidth(), 2); }); - it('line of surrogate fullwidth with combining odd', () => { + it('line of surrogate fullwidth with combining odd', async () => { term.buffer.x = 1; - term.writeSync(Array(50).join('\ud843\ude6d\u0301')); + await term.writeP(Array(50).join('\ud843\ude6d\u0301')); for (let i = 1; i < term.cols - 1; ++i) { term.buffer.lines.get(0)!.loadCell(i, cell); if (!(i % 2)) { - expect(cell.getChars()).eql(''); - expect(cell.getChars().length).eql(0); - expect(cell.getWidth()).eql(0); + assert.equal(cell.getChars(), ''); + assert.equal(cell.getChars().length, 0); + assert.equal(cell.getWidth(), 0); } else { - expect(cell.getChars()).eql('\ud843\ude6d\u0301'); - expect(cell.getChars().length).eql(3); - expect(cell.getWidth()).eql(2); + assert.equal(cell.getChars(), '\ud843\ude6d\u0301'); + assert.equal(cell.getChars().length, 3); + assert.equal(cell.getWidth(), 2); } } term.buffer.lines.get(0)!.loadCell(term.cols - 1, cell); - expect(cell.getChars()).eql(''); - expect(cell.getChars().length).eql(0); - expect(cell.getWidth()).eql(1); + assert.equal(cell.getChars(), ''); + assert.equal(cell.getChars().length, 0); + assert.equal(cell.getWidth(), 1); term.buffer.lines.get(1)!.loadCell(0, cell); - expect(cell.getChars()).eql('\ud843\ude6d\u0301'); - expect(cell.getChars().length).eql(3); - expect(cell.getWidth()).eql(2); + assert.equal(cell.getChars(), '\ud843\ude6d\u0301'); + assert.equal(cell.getChars().length, 3); + assert.equal(cell.getWidth(), 2); }); - it('line of surrogate fullwidth with combining even', () => { - term.writeSync(Array(50).join('\ud843\ude6d\u0301')); + it('line of surrogate fullwidth with combining even', async () => { + await term.writeP(Array(50).join('\ud843\ude6d\u0301')); for (let i = 0; i < term.cols; ++i) { term.buffer.lines.get(0)!.loadCell(i, cell); if (i % 2) { - expect(cell.getChars()).eql(''); - expect(cell.getChars().length).eql(0); - expect(cell.getWidth()).eql(0); + assert.equal(cell.getChars(), ''); + assert.equal(cell.getChars().length, 0); + assert.equal(cell.getWidth(), 0); } else { - expect(cell.getChars()).eql('\ud843\ude6d\u0301'); - expect(cell.getChars().length).eql(3); - expect(cell.getWidth()).eql(2); + assert.equal(cell.getChars(), '\ud843\ude6d\u0301'); + assert.equal(cell.getChars().length, 3); + assert.equal(cell.getWidth(), 2); } } term.buffer.lines.get(1)!.loadCell(0, cell); - expect(cell.getChars()).eql('\ud843\ude6d\u0301'); - expect(cell.getChars().length).eql(3); - expect(cell.getWidth()).eql(2); + assert.equal(cell.getChars(), '\ud843\ude6d\u0301'); + assert.equal(cell.getChars().length, 3); + assert.equal(cell.getWidth(), 2); }); }); describe('insert mode', () => { const cell = new CellData(); - it('halfwidth - all', () => { - term.writeSync(Array(9).join('0123456789').slice(-80)); + it('halfwidth - all', async () => { + await term.writeP(Array(9).join('0123456789').slice(-80)); term.buffer.x = 10; term.buffer.y = 0; term.write('\x1b[4h'); - term.writeSync('abcde'); - expect(term.buffer.lines.get(0)!.length).eql(term.cols); - expect(term.buffer.lines.get(0)!.loadCell(10, cell).getChars()).eql('a'); - expect(term.buffer.lines.get(0)!.loadCell(14, cell).getChars()).eql('e'); - expect(term.buffer.lines.get(0)!.loadCell(15, cell).getChars()).eql('0'); - expect(term.buffer.lines.get(0)!.loadCell(79, cell).getChars()).eql('4'); + await term.writeP('abcde'); + assert.equal(term.buffer.lines.get(0)!.length, term.cols); + assert.equal(term.buffer.lines.get(0)!.loadCell(10, cell).getChars(), 'a'); + assert.equal(term.buffer.lines.get(0)!.loadCell(14, cell).getChars(), 'e'); + assert.equal(term.buffer.lines.get(0)!.loadCell(15, cell).getChars(), '0'); + assert.equal(term.buffer.lines.get(0)!.loadCell(79, cell).getChars(), '4'); }); - it('fullwidth - insert', () => { - term.writeSync(Array(9).join('0123456789').slice(-80)); + it('fullwidth - insert', async () => { + await term.writeP(Array(9).join('0123456789').slice(-80)); term.buffer.x = 10; term.buffer.y = 0; term.write('\x1b[4h'); - term.writeSync('¥¥¥'); - expect(term.buffer.lines.get(0)!.length).eql(term.cols); - expect(term.buffer.lines.get(0)!.loadCell(10, cell).getChars()).eql('¥'); - expect(term.buffer.lines.get(0)!.loadCell(11, cell).getChars()).eql(''); - expect(term.buffer.lines.get(0)!.loadCell(14, cell).getChars()).eql('¥'); - expect(term.buffer.lines.get(0)!.loadCell(15, cell).getChars()).eql(''); - expect(term.buffer.lines.get(0)!.loadCell(79, cell).getChars()).eql('3'); + await term.writeP('¥¥¥'); + assert.equal(term.buffer.lines.get(0)!.length, term.cols); + assert.equal(term.buffer.lines.get(0)!.loadCell(10, cell).getChars(), '¥'); + assert.equal(term.buffer.lines.get(0)!.loadCell(11, cell).getChars(), ''); + assert.equal(term.buffer.lines.get(0)!.loadCell(14, cell).getChars(), '¥'); + assert.equal(term.buffer.lines.get(0)!.loadCell(15, cell).getChars(), ''); + assert.equal(term.buffer.lines.get(0)!.loadCell(79, cell).getChars(), '3'); }); - it('fullwidth - right border', () => { - term.writeSync(Array(41).join('¥')); + it('fullwidth - right border', async () => { + await term.writeP(Array(41).join('¥')); term.buffer.x = 10; term.buffer.y = 0; term.write('\x1b[4h'); - term.writeSync('a'); - expect(term.buffer.lines.get(0)!.length).eql(term.cols); - expect(term.buffer.lines.get(0)!.loadCell(10, cell).getChars()).eql('a'); - expect(term.buffer.lines.get(0)!.loadCell(11, cell).getChars()).eql('¥'); - expect(term.buffer.lines.get(0)!.loadCell(79, cell).getChars()).eql(''); // fullwidth char got replaced - term.writeSync('b'); - expect(term.buffer.lines.get(0)!.length).eql(term.cols); - expect(term.buffer.lines.get(0)!.loadCell(11, cell).getChars()).eql('b'); - expect(term.buffer.lines.get(0)!.loadCell(12, cell).getChars()).eql('¥'); - expect(term.buffer.lines.get(0)!.loadCell(79, cell).getChars()).eql(''); // empty cell after fullwidth + await term.writeP('a'); + assert.equal(term.buffer.lines.get(0)!.length, term.cols); + assert.equal(term.buffer.lines.get(0)!.loadCell(10, cell).getChars(), 'a'); + assert.equal(term.buffer.lines.get(0)!.loadCell(11, cell).getChars(), '¥'); + assert.equal(term.buffer.lines.get(0)!.loadCell(79, cell).getChars(), ''); // fullwidth char got replaced + await term.writeP('b'); + assert.equal(term.buffer.lines.get(0)!.length, term.cols); + assert.equal(term.buffer.lines.get(0)!.loadCell(11, cell).getChars(), 'b'); + assert.equal(term.buffer.lines.get(0)!.loadCell(12, cell).getChars(), '¥'); + assert.equal(term.buffer.lines.get(0)!.loadCell(79, cell).getChars(), ''); // empty cell after fullwidth }); }); @@ -1043,85 +1058,87 @@ describe('Terminal', () => { linkifier.attachToDom({} as any, mouseZoneManager); }); - function assertLinkifiesInTerminal(rowText: string, linkMatcherRegex: RegExp, links: {x1: number, y1: number, x2: number, y2: number}[], done: Mocha.Done): void { - terminal.writeSync(rowText); - linkifier.registerLinkMatcher(linkMatcherRegex, () => {}); - linkifier.linkifyRows(); - // Allow linkify to happen - setTimeout(() => { - assert.equal(mouseZoneManager.zones.length, links.length); - links.forEach((l, i) => { - assert.equal(mouseZoneManager.zones[i].x1, l.x1 + 1); - assert.equal(mouseZoneManager.zones[i].x2, l.x2 + 1); - assert.equal(mouseZoneManager.zones[i].y1, l.y1 + 1); - assert.equal(mouseZoneManager.zones[i].y2, l.y2 + 1); - }); - done(); - }, 0); + function assertLinkifiesInTerminal(rowText: string, linkMatcherRegex: RegExp, links: { x1: number, y1: number, x2: number, y2: number }[]): Promise { + return new Promise(async r => { + await terminal.writeP(rowText); + linkifier.registerLinkMatcher(linkMatcherRegex, () => { }); + linkifier.linkifyRows(); + // Allow linkify to happen + setTimeout(() => { + assert.equal(mouseZoneManager.zones.length, links.length); + links.forEach((l, i) => { + assert.equal(mouseZoneManager.zones[i].x1, l.x1 + 1); + assert.equal(mouseZoneManager.zones[i].x2, l.x2 + 1); + assert.equal(mouseZoneManager.zones[i].y1, l.y1 + 1); + assert.equal(mouseZoneManager.zones[i].y2, l.y2 + 1); + }); + r(); + }, 0); + }); } describe('unicode before the match', () => { - it('combining - match within one line', function(done: () => void): void { - assertLinkifiesInTerminal('e\u0301e\u0301e\u0301 foo', /foo/, [{x1: 4, x2: 7, y1: 0, y2: 0}], done); + it('combining - match within one line', () => { + return assertLinkifiesInTerminal('e\u0301e\u0301e\u0301 foo', /foo/, [{ x1: 4, x2: 7, y1: 0, y2: 0 }]); }); - it('combining - match over two lines', function(done: () => void): void { - assertLinkifiesInTerminal('e\u0301e\u0301e\u0301 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done); + it('combining - match over two lines', () => { + return assertLinkifiesInTerminal('e\u0301e\u0301e\u0301 foo', /foo/, [{ x1: 8, x2: 1, y1: 0, y2: 1 }]); }); - it('surrogate - match within one line', function(done: () => void): void { - assertLinkifiesInTerminal('𝄞𝄞𝄞 foo', /foo/, [{x1: 4, x2: 7, y1: 0, y2: 0}], done); + it('surrogate - match within one line', () => { + return assertLinkifiesInTerminal('𝄞𝄞𝄞 foo', /foo/, [{ x1: 4, x2: 7, y1: 0, y2: 0 }]); }); - it('surrogate - match over two lines', function(done: () => void): void { - assertLinkifiesInTerminal('𝄞𝄞𝄞 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done); + it('surrogate - match over two lines', () => { + return assertLinkifiesInTerminal('𝄞𝄞𝄞 foo', /foo/, [{ x1: 8, x2: 1, y1: 0, y2: 1 }]); }); - it('combining surrogate - match within one line', function(done: () => void): void { - assertLinkifiesInTerminal('𓂀\u0301𓂀\u0301𓂀\u0301 foo', /foo/, [{x1: 4, x2: 7, y1: 0, y2: 0}], done); + it('combining surrogate - match within one line', () => { + return assertLinkifiesInTerminal('𓂀\u0301𓂀\u0301𓂀\u0301 foo', /foo/, [{ x1: 4, x2: 7, y1: 0, y2: 0 }]); }); - it('combining surrogate - match over two lines', function(done: () => void): void { - assertLinkifiesInTerminal('𓂀\u0301𓂀\u0301𓂀\u0301 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done); + it('combining surrogate - match over two lines', () => { + return assertLinkifiesInTerminal('𓂀\u0301𓂀\u0301𓂀\u0301 foo', /foo/, [{ x1: 8, x2: 1, y1: 0, y2: 1 }]); }); - it('fullwidth - match within one line', function(done: () => void): void { - assertLinkifiesInTerminal('12 foo', /foo/, [{x1: 5, x2: 8, y1: 0, y2: 0}], done); + it('fullwidth - match within one line', () => { + return assertLinkifiesInTerminal('12 foo', /foo/, [{ x1: 5, x2: 8, y1: 0, y2: 0 }]); }); - it('fullwidth - match over two lines', function(done: () => void): void { - assertLinkifiesInTerminal('12 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done); + it('fullwidth - match over two lines', () => { + return assertLinkifiesInTerminal('12 foo', /foo/, [{ x1: 8, x2: 1, y1: 0, y2: 1 }]); }); - it('combining fullwidth - match within one line', function(done: () => void): void { - assertLinkifiesInTerminal('¥\u0301¥\u0301 foo', /foo/, [{x1: 5, x2: 8, y1: 0, y2: 0}], done); + it('combining fullwidth - match within one line', () => { + return assertLinkifiesInTerminal('¥\u0301¥\u0301 foo', /foo/, [{ x1: 5, x2: 8, y1: 0, y2: 0 }]); }); - it('combining fullwidth - match over two lines', function(done: () => void): void { - assertLinkifiesInTerminal('¥\u0301¥\u0301 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done); + it('combining fullwidth - match over two lines', () => { + return assertLinkifiesInTerminal('¥\u0301¥\u0301 foo', /foo/, [{ x1: 8, x2: 1, y1: 0, y2: 1 }]); }); }); describe('unicode within the match', () => { - it('combining - match within one line', function(done: () => void): void { - assertLinkifiesInTerminal('test cafe\u0301', /cafe\u0301/, [{x1: 5, x2: 9, y1: 0, y2: 0}], done); + it('combining - match within one line', () => { + return assertLinkifiesInTerminal('test cafe\u0301', /cafe\u0301/, [{ x1: 5, x2: 9, y1: 0, y2: 0 }]); }); - it('combining - match over two lines', function(done: () => void): void { - assertLinkifiesInTerminal('testtest cafe\u0301', /cafe\u0301/, [{x1: 9, x2: 3, y1: 0, y2: 1}], done); + it('combining - match over two lines', () => { + return assertLinkifiesInTerminal('testtest cafe\u0301', /cafe\u0301/, [{ x1: 9, x2: 3, y1: 0, y2: 1 }]); }); - it('surrogate - match within one line', function(done: () => void): void { - assertLinkifiesInTerminal('test a𝄞b', /a𝄞b/, [{x1: 5, x2: 8, y1: 0, y2: 0}], done); + it('surrogate - match within one line', () => { + return assertLinkifiesInTerminal('test a𝄞b', /a𝄞b/, [{ x1: 5, x2: 8, y1: 0, y2: 0 }]); }); - it('surrogate - match over two lines', function(done: () => void): void { - assertLinkifiesInTerminal('testtest a𝄞b', /a𝄞b/, [{x1: 9, x2: 2, y1: 0, y2: 1}], done); + it('surrogate - match over two lines', () => { + return assertLinkifiesInTerminal('testtest a𝄞b', /a𝄞b/, [{ x1: 9, x2: 2, y1: 0, y2: 1 }]); }); - it('combining surrogate - match within one line', function(done: () => void): void { - assertLinkifiesInTerminal('test a𓂀\u0301b', /a𓂀\u0301b/, [{x1: 5, x2: 8, y1: 0, y2: 0}], done); + it('combining surrogate - match within one line', () => { + return assertLinkifiesInTerminal('test a𓂀\u0301b', /a𓂀\u0301b/, [{ x1: 5, x2: 8, y1: 0, y2: 0 }]); }); - it('combining surrogate - match over two lines', function(done: () => void): void { - assertLinkifiesInTerminal('testtest a𓂀\u0301b', /a𓂀\u0301b/, [{x1: 9, x2: 2, y1: 0, y2: 1}], done); + it('combining surrogate - match over two lines', () => { + return assertLinkifiesInTerminal('testtest a𓂀\u0301b', /a𓂀\u0301b/, [{ x1: 9, x2: 2, y1: 0, y2: 1 }]); }); - it('fullwidth - match within one line', function(done: () => void): void { - assertLinkifiesInTerminal('test a1b', /a1b/, [{x1: 5, x2: 9, y1: 0, y2: 0}], done); + it('fullwidth - match within one line', () => { + return assertLinkifiesInTerminal('test a1b', /a1b/, [{ x1: 5, x2: 9, y1: 0, y2: 0 }]); }); - it('fullwidth - match over two lines', function(done: () => void): void { - assertLinkifiesInTerminal('testtest a1b', /a1b/, [{x1: 9, x2: 3, y1: 0, y2: 1}], done); + it('fullwidth - match over two lines', () => { + return assertLinkifiesInTerminal('testtest a1b', /a1b/, [{ x1: 9, x2: 3, y1: 0, y2: 1 }]); }); - it('combining fullwidth - match within one line', function(done: () => void): void { - assertLinkifiesInTerminal('test a¥\u0301b', /a¥\u0301b/, [{x1: 5, x2: 9, y1: 0, y2: 0}], done); + it('combining fullwidth - match within one line', () => { + return assertLinkifiesInTerminal('test a¥\u0301b', /a¥\u0301b/, [{ x1: 5, x2: 9, y1: 0, y2: 0 }]); }); - it('combining fullwidth - match over two lines', function(done: () => void): void { - assertLinkifiesInTerminal('testtest a¥\u0301b', /a¥\u0301b/, [{x1: 9, x2: 3, y1: 0, y2: 1}], done); + it('combining fullwidth - match over two lines', () => { + return assertLinkifiesInTerminal('testtest a¥\u0301b', /a¥\u0301b/, [{ x1: 9, x2: 3, y1: 0, y2: 1 }]); }); }); }); @@ -1130,12 +1147,12 @@ describe('Terminal', () => { let terminal: TestTerminal; beforeEach(() => { - terminal = new TestTerminal({rows: 5, cols: 10, scrollback: 5}); + terminal = new TestTerminal({ rows: 5, cols: 10, scrollback: 5 }); }); - it('multiline ascii', () => { + it('multiline ascii', async () => { const input = 'This is ASCII text spanning multiple lines.'; - terminal.writeSync(input); + await terminal.writeP(input); const s = terminal.buffer.iterator(true).next().content; assert.equal(input, s); for (let i = 0; i < input.length; ++i) { @@ -1144,9 +1161,9 @@ describe('Terminal', () => { } }); - it('combining e\u0301 in a sentence', () => { + it('combining e\u0301 in a sentence', async () => { const input = 'Sitting in the cafe\u0301 drinking coffee.'; - terminal.writeSync(input); + await terminal.writeP(input); const s = terminal.buffer.iterator(true).next().content; assert.equal(input, s); for (let i = 0; i < 19; ++i) { @@ -1164,9 +1181,9 @@ describe('Terminal', () => { } }); - it('multiline combining e\u0301', () => { + it('multiline combining e\u0301', async () => { const input = 'e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301'; - terminal.writeSync(input); + await terminal.writeP(input); const s = terminal.buffer.iterator(true).next().content; assert.equal(input, s); // every buffer cell index contains 2 string indices @@ -1176,9 +1193,9 @@ describe('Terminal', () => { } }); - it('surrogate char in a sentence', () => { + it('surrogate char in a sentence', async () => { const input = 'The 𝄞 is a clef widely used in modern notation.'; - terminal.writeSync(input); + await terminal.writeP(input); const s = terminal.buffer.iterator(true).next().content; assert.equal(input, s); for (let i = 0; i < 5; ++i) { @@ -1196,9 +1213,9 @@ describe('Terminal', () => { } }); - it('multiline surrogate char', () => { + it('multiline surrogate char', async () => { const input = '𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞'; - terminal.writeSync(input); + await terminal.writeP(input); const s = terminal.buffer.iterator(true).next().content; assert.equal(input, s); // every buffer cell index contains 2 string indices @@ -1208,10 +1225,10 @@ describe('Terminal', () => { } }); - it('surrogate char with combining', () => { + it('surrogate char with combining', async () => { // eye of Ra with acute accent - string length of 3 const input = '𓂀\u0301 - the eye hiroglyph with an acute accent.'; - terminal.writeSync(input); + await terminal.writeP(input); const s = terminal.buffer.iterator(true).next().content; assert.equal(input, s); // index 0..2 should map to 0 @@ -1223,9 +1240,9 @@ describe('Terminal', () => { } }); - it('multiline surrogate with combining', () => { + it('multiline surrogate with combining', async () => { const input = '𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301'; - terminal.writeSync(input); + await terminal.writeP(input); const s = terminal.buffer.iterator(true).next().content; assert.equal(input, s); // every buffer cell index contains 3 string indices @@ -1235,9 +1252,9 @@ describe('Terminal', () => { } }); - it('fullwidth chars', () => { + it('fullwidth chars', async () => { const input = 'These 123 are some fat numbers.'; - terminal.writeSync(input); + await terminal.writeP(input); const s = terminal.buffer.iterator(true).next().content; assert.equal(input, s); for (let i = 0; i < 6; ++i) { @@ -1254,9 +1271,9 @@ describe('Terminal', () => { } }); - it('multiline fullwidth chars', () => { + it('multiline fullwidth chars', async () => { const input = '12345678901234567890'; - terminal.writeSync(input); + await terminal.writeP(input); const s = terminal.buffer.iterator(true).next().content; assert.equal(input, s); for (let i = 9; i < input.length; ++i) { @@ -1265,9 +1282,9 @@ describe('Terminal', () => { } }); - it('fullwidth combining with emoji - match emoji cell', () => { + it('fullwidth combining with emoji - match emoji cell', async () => { const input = 'Lots of ¥\u0301 make me 😃.'; - terminal.writeSync(input); + await terminal.writeP(input); const s = terminal.buffer.iterator(true).next().content; assert.equal(input, s); const stringIndex = s.match(/😃/)!.index!; @@ -1275,14 +1292,14 @@ describe('Terminal', () => { assert(terminal.buffer.lines.get(bufferIndex[0])!.loadCell(bufferIndex[1], new CellData()).getChars(), '😃'); }); - it('multiline fullwidth chars with offset 1 (currently tests for broken behavior)', () => { + it('multiline fullwidth chars with offset 1 (currently tests for broken behavior)', async () => { const input = 'a12345678901234567890'; // the 'a' at the beginning moves all fullwidth chars one to the right // now the end of the line contains a dangling empty cell since // the next fullwidth char has to wrap early // the dangling last cell is wrongly added in the string // --> fixable after resolving #1685 - terminal.writeSync(input); + await terminal.writeP(input); const s = terminal.buffer.iterator(true).next().content; assert.equal(input, s); for (let i = 10; i < input.length; ++i) { @@ -1292,9 +1309,9 @@ describe('Terminal', () => { } }); - it('test fully wrapped buffer up to last char', () => { + it('test fully wrapped buffer up to last char', async () => { const input = Array(6).join('1234567890'); - terminal.writeSync(input); + await terminal.writeP(input); const s = terminal.buffer.iterator(true).next().content; assert.equal(input, s); for (let i = 0; i < input.length; ++i) { @@ -1303,10 +1320,10 @@ describe('Terminal', () => { } }); - it('test fully wrapped buffer up to last char with full width odd', () => { + it('test fully wrapped buffer up to last char with full width odd', async () => { const input = 'a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301' - + 'a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301'; - terminal.writeSync(input); + + 'a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301'; + await terminal.writeP(input); const s = terminal.buffer.iterator(true).next().content; assert.equal(input, s); for (let i = 0; i < input.length; ++i) { @@ -1321,17 +1338,17 @@ describe('Terminal', () => { } }); - it('should handle \t in lines correctly', () => { + it('should handle \t in lines correctly', async () => { const input = '\thttps://google.de'; - terminal.writeSync(input); + await terminal.writeP(input); const s = terminal.buffer.iterator(true).next().content; assert.equal(s, Array(terminal.optionsService.options.tabStopWidth + 1).join(' ') + 'https://google.de'); }); }); - describe('BufferStringIterator', function(): void { - it('iterator does not overflow buffer limits', function(): void { - const terminal = new TestTerminal({rows: 5, cols: 10, scrollback: 5}); + describe('BufferStringIterator', function (): void { + it('iterator does not overflow buffer limits', async () => { + const terminal = new TestTerminal({ rows: 5, cols: 10, scrollback: 5 }); const data = [ 'aaaaaaaaaa', 'aaaaaaaaa\n', @@ -1344,9 +1361,9 @@ describe('Terminal', () => { 'aaaaaaaaaa', 'aaaaaaaaaa' ]; - terminal.writeSync(data.join('')); + await terminal.writeP(data.join('')); // brute force test with insane values - expect(() => { + assert.doesNotThrow(() => { for (let overscan = 0; overscan < 20; ++overscan) { for (let start = -10; start < 20; ++start) { for (let end = -10; end < 20; ++end) { @@ -1357,182 +1374,67 @@ describe('Terminal', () => { } } } - }).to.not.throw(); + }); }); }); describe('Windows Mode', () => { - it('should mark lines as wrapped when the line ends in a non-null character after a LF', () => { + it('should mark lines as wrapped when the line ends in a non-null character after a LF', async () => { const data = [ 'aaaaaaaaaa\n\r', // cannot wrap as it's the first 'aaaaaaaaa\n\r', // wrapped (windows mode only) 'aaaaaaaaa' // not wrapped ]; - const normalTerminal = new TestTerminal({rows: 5, cols: 10, windowsMode: false}); - normalTerminal.writeSync(data.join('')); + const normalTerminal = new TestTerminal({ rows: 5, cols: 10, windowsMode: false }); + await normalTerminal.writeP(data.join('')); assert.equal(normalTerminal.buffer.lines.get(0)!.isWrapped, false); assert.equal(normalTerminal.buffer.lines.get(1)!.isWrapped, false); assert.equal(normalTerminal.buffer.lines.get(2)!.isWrapped, false); - const windowsModeTerminal = new TestTerminal({rows: 5, cols: 10, windowsMode: true}); - windowsModeTerminal.writeSync(data.join('')); + const windowsModeTerminal = new TestTerminal({ rows: 5, cols: 10, windowsMode: true }); + await windowsModeTerminal.writeP(data.join('')); assert.equal(windowsModeTerminal.buffer.lines.get(0)!.isWrapped, false); assert.equal(windowsModeTerminal.buffer.lines.get(1)!.isWrapped, true, 'This line should wrap in Windows mode as the previous line ends in a non-null character'); assert.equal(windowsModeTerminal.buffer.lines.get(2)!.isWrapped, false); }); - it('should mark lines as wrapped when the line ends in a non-null character after a CUP', () => { + it('should mark lines as wrapped when the line ends in a non-null character after a CUP', async () => { const data = [ 'aaaaaaaaaa\x1b[2;1H', // cannot wrap as it's the first 'aaaaaaaaa\x1b[3;1H', // wrapped (windows mode only) 'aaaaaaaaa' // not wrapped ]; - const normalTerminal = new TestTerminal({rows: 5, cols: 10, windowsMode: false}); - normalTerminal.writeSync(data.join('')); + const normalTerminal = new TestTerminal({ rows: 5, cols: 10, windowsMode: false }); + await normalTerminal.writeP(data.join('')); assert.equal(normalTerminal.buffer.lines.get(0)!.isWrapped, false); assert.equal(normalTerminal.buffer.lines.get(1)!.isWrapped, false); assert.equal(normalTerminal.buffer.lines.get(2)!.isWrapped, false); - const windowsModeTerminal = new TestTerminal({rows: 5, cols: 10, windowsMode: true}); - windowsModeTerminal.writeSync(data.join('')); + const windowsModeTerminal = new TestTerminal({ rows: 5, cols: 10, windowsMode: true }); + await windowsModeTerminal.writeP(data.join('')); assert.equal(windowsModeTerminal.buffer.lines.get(0)!.isWrapped, false); assert.equal(windowsModeTerminal.buffer.lines.get(1)!.isWrapped, true, 'This line should wrap in Windows mode as the previous line ends in a non-null character'); assert.equal(windowsModeTerminal.buffer.lines.get(2)!.isWrapped, false); }); }); - it('convertEol setting', function(): void { + it('convertEol setting', async () => { // not converting - const termNotConverting = new TestTerminal({cols: 15, rows: 10}); - termNotConverting.writeSync('Hello\nWorld'); - expect(termNotConverting.buffer.lines.get(0)!.translateToString(false)).equals('Hello '); - expect(termNotConverting.buffer.lines.get(1)!.translateToString(false)).equals(' World '); - expect(termNotConverting.buffer.lines.get(0)!.translateToString(true)).equals('Hello'); - expect(termNotConverting.buffer.lines.get(1)!.translateToString(true)).equals(' World'); + const termNotConverting = new TestTerminal({ cols: 15, rows: 10 }); + await termNotConverting.writeP('Hello\nWorld'); + assert.equal(termNotConverting.buffer.lines.get(0)!.translateToString(false), 'Hello '); + assert.equal(termNotConverting.buffer.lines.get(1)!.translateToString(false), ' World '); + assert.equal(termNotConverting.buffer.lines.get(0)!.translateToString(true), 'Hello'); + assert.equal(termNotConverting.buffer.lines.get(1)!.translateToString(true), ' World'); // converting - const termConverting = new TestTerminal({cols: 15, rows: 10, convertEol: true}); - termConverting.writeSync('Hello\nWorld'); - expect(termConverting.buffer.lines.get(0)!.translateToString(false)).equals('Hello '); - expect(termConverting.buffer.lines.get(1)!.translateToString(false)).equals('World '); - expect(termConverting.buffer.lines.get(0)!.translateToString(true)).equals('Hello'); - expect(termConverting.buffer.lines.get(1)!.translateToString(true)).equals('World'); - }); - describe('Terminal InputHandler integration', () => { - function getLines(term: TestTerminal, limit: number = term.rows): string[] { - const res: string[] = []; - for (let i = 0; i < limit; ++i) { - res.push(term.buffer.lines.get(i)!.translateToString(true)); - } - return res; - } - - // This suite cannot live in InputHandler unless Terminal.scroll moved into IBufferService - describe('SL/SR/DECIC/DECDC', () => { - let term: TestTerminal; - beforeEach(() => { - term = new TestTerminal({cols: 5, rows: 5, scrollback: 1}); - }); - it('SL (scrollLeft)', () => { - term.writeSync('12345'.repeat(6)); - term.writeSync('\x1b[ @'); - assert.deepEqual(getLines(term, term.rows + 1), ['12345', '2345', '2345', '2345', '2345', '2345']); - term.writeSync('\x1b[0 @'); - assert.deepEqual(getLines(term, term.rows + 1), ['12345', '345', '345', '345', '345', '345']); - term.writeSync('\x1b[2 @'); - assert.deepEqual(getLines(term, term.rows + 1), ['12345', '5', '5', '5', '5', '5']); - }); - it('SR (scrollRight)', () => { - term.writeSync('12345'.repeat(6)); - term.writeSync('\x1b[ A'); - assert.deepEqual(getLines(term, term.rows + 1), ['12345', ' 1234', ' 1234', ' 1234', ' 1234', ' 1234']); - term.writeSync('\x1b[0 A'); - assert.deepEqual(getLines(term, term.rows + 1), ['12345', ' 123', ' 123', ' 123', ' 123', ' 123']); - term.writeSync('\x1b[2 A'); - assert.deepEqual(getLines(term, term.rows + 1), ['12345', ' 1', ' 1', ' 1', ' 1', ' 1']); - }); - it('insertColumns (DECIC)', () => { - term.writeSync('12345'.repeat(6)); - term.writeSync('\x1b[3;3H'); - term.writeSync('\x1b[\'}'); - assert.deepEqual(getLines(term, term.rows + 1), ['12345', '12 34', '12 34', '12 34', '12 34', '12 34']); - term.reset(); - term.writeSync('12345'.repeat(6)); - term.writeSync('\x1b[3;3H'); - term.writeSync('\x1b[1\'}'); - assert.deepEqual(getLines(term, term.rows + 1), ['12345', '12 34', '12 34', '12 34', '12 34', '12 34']); - term.reset(); - term.writeSync('12345'.repeat(6)); - term.writeSync('\x1b[3;3H'); - term.writeSync('\x1b[2\'}'); - assert.deepEqual(getLines(term, term.rows + 1), ['12345', '12 3', '12 3', '12 3', '12 3', '12 3']); - }); - it('deleteColumns (DECDC)', () => { - term.writeSync('12345'.repeat(6)); - term.writeSync('\x1b[3;3H'); - term.writeSync('\x1b[\'~'); - assert.deepEqual(getLines(term, term.rows + 1), ['12345', '1245', '1245', '1245', '1245', '1245']); - term.reset(); - term.writeSync('12345'.repeat(6)); - term.writeSync('\x1b[3;3H'); - term.writeSync('\x1b[1\'~'); - assert.deepEqual(getLines(term, term.rows + 1), ['12345', '1245', '1245', '1245', '1245', '1245']); - term.reset(); - term.writeSync('12345'.repeat(6)); - term.writeSync('\x1b[3;3H'); - term.writeSync('\x1b[2\'~'); - assert.deepEqual(getLines(term, term.rows + 1), ['12345', '125', '125', '125', '125', '125']); - }); - }); - - describe('BS with reverseWraparound set/unset', () => { - const ttyBS = '\x08 \x08'; // tty ICANON sends on pressing BS - - beforeEach(() => { - term = new TestTerminal({cols: 5, rows: 5, scrollback: 1}); - }); - - describe('reverseWraparound set', () => { - it('should not reverse outside of scroll margins', () => { - // prepare buffer content - term.writeSync('#####abcdefghijklmnopqrstuvwxy'); - assert.deepEqual(getLines(term, 6), ['#####', 'abcde', 'fghij', 'klmno', 'pqrst', 'uvwxy']); - assert.equal(term.buffer.ydisp, 1); - assert.equal(term.buffer.x, 5); - assert.equal(term.buffer.y, 4); - term.writeSync(ttyBS.repeat(100)); - assert.deepEqual(getLines(term, 6), ['#####', 'abcde', 'fghij', 'klmno', 'pqrst', ' y']); - - term.writeSync('\x1b[?45h'); - term.writeSync('uvwxy'); - - // set top/bottom to 1/3 (0-based) - term.writeSync('\x1b[2;4r'); - // place cursor below scroll bottom - term.buffer.x = 5; - term.buffer.y = 4; - term.writeSync(ttyBS.repeat(100)); - assert.deepEqual(getLines(term, 6), ['#####', 'abcde', 'fghij', 'klmno', 'pqrst', ' ']); - - term.writeSync('uvwxy'); - // place cursor within scroll margins - term.buffer.x = 5; - term.buffer.y = 3; - term.writeSync(ttyBS.repeat(100)); - assert.deepEqual(getLines(term, 6), ['#####', 'abcde', ' ', ' ', ' ', 'uvwxy']); - assert.equal(term.buffer.x, 0); - assert.equal(term.buffer.y, term.buffer.scrollTop); // stops at 0, scrollTop - - term.writeSync('fghijklmnopqrst'); - // place cursor above scroll top - term.buffer.x = 5; - term.buffer.y = 0; - term.writeSync(ttyBS.repeat(100)); - assert.deepEqual(getLines(term, 6), ['#####', ' ', 'fghij', 'klmno', 'pqrst', 'uvwxy']); - }); - }); - }); + const termConverting = new TestTerminal({ cols: 15, rows: 10, convertEol: true }); + await termConverting.writeP('Hello\nWorld'); + assert.equal(termConverting.buffer.lines.get(0)!.translateToString(false), 'Hello '); + assert.equal(termConverting.buffer.lines.get(1)!.translateToString(false), 'World '); + assert.equal(termConverting.buffer.lines.get(0)!.translateToString(true), 'Hello'); + assert.equal(termConverting.buffer.lines.get(1)!.translateToString(true), 'World'); }); // FIXME: move to common/CoreTerminal.test once the trimming is moved over @@ -1542,22 +1444,22 @@ describe('Terminal', () => { let markers: IMarker[]; let disposeStack: IMarker[]; let term: TestTerminal; - beforeEach(() => { + beforeEach(async () => { term = new TestTerminal({}); markers = []; disposeStack = []; term.optionsService.setOption('scrollback', 1); term.resize(10, 5); markers.push(term.buffers.active.addMarker(term.buffers.active.y)); - term.writeSync('\x1b[r0\r\n'); + await term.writeP('\x1b[r0\r\n'); markers.push(term.buffers.active.addMarker(term.buffers.active.y)); - term.writeSync('1\r\n'); + await term.writeP('1\r\n'); markers.push(term.buffers.active.addMarker(term.buffers.active.y)); - term.writeSync('2\r\n'); + await term.writeP('2\r\n'); markers.push(term.buffers.active.addMarker(term.buffers.active.y)); - term.writeSync('3\r\n'); + await term.writeP('3\r\n'); markers.push(term.buffers.active.addMarker(term.buffers.active.y)); - term.writeSync('4'); + await term.writeP('4'); for (let i = 0; i < markers.length; ++i) { const marker = markers[i]; marker.onDispose(() => disposeStack.push(marker)); @@ -1566,15 +1468,15 @@ describe('Terminal', () => { it('initial', () => { assert.deepEqual(markers.map(m => m.line), [0, 1, 2, 3, 4]); }); - it('should dispose on normal trim off the top', () => { + it('should dispose on normal trim off the top', async () => { // moves top line into scrollback - term.writeSync('\n'); + await term.writeP('\n'); assert.deepEqual(disposeStack, []); // trims first marker - term.writeSync('\n'); + await term.writeP('\n'); assert.deepEqual(disposeStack, [markers[0]]); // trims second marker - term.writeSync('\n'); + await term.writeP('\n'); assert.deepEqual(disposeStack, [markers[0], markers[1]]); // trimmed marker objs should be disposed assert.deepEqual(disposeStack.map(el => el.isDisposed), [true, true]); @@ -1582,14 +1484,14 @@ describe('Terminal', () => { // trimmed markers should contain line -1 assert.deepEqual(disposeStack.map(el => el.line), [-1, -1]); }); - it('should dispose on DL', () => { - term.writeSync('\x1b[3;1H'); // move cursor to 0, 2 - term.writeSync('\x1b[2M'); // delete 2 lines + it('should dispose on DL', async () => { + await term.writeP('\x1b[3;1H'); // move cursor to 0, 2 + await term.writeP('\x1b[2M'); // delete 2 lines assert.deepEqual(disposeStack, [markers[2], markers[3]]); }); - it('should dispose on IL', () => { - term.writeSync('\x1b[3;1H'); // move cursor to 0, 2 - term.writeSync('\x1b[2L'); // insert 2 lines + it('should dispose on IL', async () => { + await term.writeP('\x1b[3;1H'); // move cursor to 0, 2 + await term.writeP('\x1b[2L'); // insert 2 lines assert.deepEqual(disposeStack, [markers[4], markers[3]]); assert.deepEqual(markers.map(el => el.line), [0, 1, 4, -1, -1]); }); diff --git a/src/browser/Terminal.ts b/src/browser/Terminal.ts index f8b8b3e4..edbeccf2 100644 --- a/src/browser/Terminal.ts +++ b/src/browser/Terminal.ts @@ -21,8 +21,8 @@ * http://linux.die.net/man/7/urxvt */ -import { ICompositionHelper, ITerminal, IBrowser, CustomKeyEventHandler, ILinkifier, IMouseZoneManager, LinkMatcherHandler, ILinkMatcherOptions, IViewport, ILinkifier2 } from 'browser/Types'; -import { IRenderer, CharacterJoinerHandler } from 'browser/renderer/Types'; +import { ICompositionHelper, ITerminal, IBrowser, CustomKeyEventHandler, ILinkifier, IMouseZoneManager, LinkMatcherHandler, ILinkMatcherOptions, IViewport, ILinkifier2, CharacterJoinerHandler } from 'browser/Types'; +import { IRenderer } from 'browser/renderer/Types'; import { CompositionHelper } from 'browser/input/CompositionHelper'; import { Viewport } from 'browser/Viewport'; import { rightClickHandler, moveTextAreaUnderMouseCursor, handlePasteEvent, copyHandler, paste } from 'browser/Clipboard'; @@ -39,13 +39,13 @@ import { MouseZoneManager } from 'browser/MouseZoneManager'; import { AccessibilityManager } from './AccessibilityManager'; import { ITheme, IMarker, IDisposable, ISelectionPosition, ILinkProvider } from 'xterm'; import { DomRenderer } from 'browser/renderer/dom/DomRenderer'; -import { IKeyboardEvent, KeyboardResultType, CoreMouseEventType, CoreMouseButton, CoreMouseAction, ITerminalOptions } from 'common/Types'; +import { IKeyboardEvent, KeyboardResultType, CoreMouseEventType, CoreMouseButton, CoreMouseAction, ITerminalOptions, ScrollSource, IAnsiColorChangeEvent } from 'common/Types'; import { evaluateKeyboardEvent } from 'common/input/Keyboard'; import { EventEmitter, IEvent, forwardEvent } from 'common/EventEmitter'; import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; import { ColorManager } from 'browser/ColorManager'; import { RenderService } from 'browser/services/RenderService'; -import { ICharSizeService, IRenderService, IMouseService, ISelectionService, ISoundService, ICoreBrowserService } from 'browser/services/Services'; +import { ICharSizeService, IRenderService, IMouseService, ISelectionService, ISoundService, ICoreBrowserService, ICharacterJoinerService } from 'browser/services/Services'; import { CharSizeService } from 'browser/services/CharSizeService'; import { IBuffer } from 'common/buffer/Types'; import { MouseService } from 'browser/services/MouseService'; @@ -53,6 +53,8 @@ import { Linkifier2 } from 'browser/Linkifier2'; import { CoreBrowserService } from 'browser/services/CoreBrowserService'; import { CoreTerminal } from 'common/CoreTerminal'; import { ITerminalOptions as IInitializedTerminalOptions } from 'common/services/Services'; +import { rgba } from 'browser/Color'; +import { CharacterJoinerService } from 'browser/services/CharacterJoinerService'; // Let it work inside Node.js for automated testing purposes. const document: Document = (typeof window !== 'undefined') ? window.document : null as any; @@ -70,7 +72,7 @@ export class Terminal extends CoreTerminal implements ITerminal { // private _visualBellTimer: number; - public browser: IBrowser = Browser; + public browser: IBrowser = Browser as any; // TODO: We should remove options once components adopt optionsService public get options(): IInitializedTerminalOptions { return this.optionsService.options; } @@ -81,6 +83,7 @@ export class Terminal extends CoreTerminal implements ITerminal { private _charSizeService: ICharSizeService | undefined; private _mouseService: IMouseService | undefined; private _renderService: IRenderService | undefined; + private _characterJoinerService: ICharacterJoinerService | undefined; private _selectionService: ISelectionService | undefined; private _soundService: ISoundService | undefined; @@ -91,6 +94,20 @@ export class Terminal extends CoreTerminal implements ITerminal { */ private _keyDownHandled: boolean = false; + /** + * Records whether the keypress event has already been handled and triggered a data event, if so + * the input event should not trigger a data event but should still print to the textarea so + * screen readers will announce it. + */ + private _keyPressHandled: boolean = false; + + /** + * Records whether there has been a keydown event for a dead key without a corresponding keydown + * event for the composed/alternative character. If we cancel the keydown event for the dead key, + * no events will be emitted for the final character. + */ + private _unprocessedDeadKey: boolean = false; + public linkifier: ILinkifier; public linkifier2: ILinkifier2; public viewport: IViewport | undefined; @@ -110,6 +127,8 @@ export class Terminal extends CoreTerminal implements ITerminal { public get onSelectionChange(): IEvent { return this._onSelectionChange.event; } private _onTitleChange = new EventEmitter(); public get onTitleChange(): IEvent { return this._onTitleChange.event; } + private _onBell = new EventEmitter(); + public get onBell(): IEvent { return this._onBell.event; } private _onFocus = new EventEmitter(); public get onFocus(): IEvent { return this._onFocus.event; } @@ -146,8 +165,8 @@ export class Terminal extends CoreTerminal implements ITerminal { this.register(this._inputHandler.onRequestBell(() => this.bell())); this.register(this._inputHandler.onRequestRefreshRows((start, end) => this.refresh(start, end))); this.register(this._inputHandler.onRequestReset(() => this.reset())); - this.register(this._inputHandler.onRequestScroll((eraseAttr, isWrapped) => this.scroll(eraseAttr, isWrapped || undefined))); this.register(this._inputHandler.onRequestWindowsOptionsReport(type => this._reportWindowsOptions(type))); + this.register(this._inputHandler.onAnsiColorChange((event) => this._changeAnsiColor(event))); this.register(forwardEvent(this._inputHandler.onCursorMove, this._onCursorMove)); this.register(forwardEvent(this._inputHandler.onTitleChange, this._onTitleChange)); this.register(forwardEvent(this._inputHandler.onA11yChar, this._onA11yCharEmitter)); @@ -157,6 +176,19 @@ export class Terminal extends CoreTerminal implements ITerminal { this.register(this._bufferService.onResize(e => this._afterResize(e.cols, e.rows))); } + private _changeAnsiColor(event: IAnsiColorChangeEvent): void { + if (!this._colorManager) { return; } + + for (const ansiColor of event.colors) { + const color = rgba.toColor(ansiColor.red, ansiColor.green, ansiColor.blue); + + this._colorManager!.colors.ansi[ansiColor.colorIndex] = color; + } + + this._renderService?.setColors(this._colorManager!.colors); + this.viewport?.onThemeChange(this._colorManager!.colors); + } + public dispose(): void { if (this._isDisposed) { return; @@ -206,6 +238,7 @@ export class Terminal extends CoreTerminal implements ITerminal { // The DOM renderer needs a row refresh to update the cursor styles this.refresh(this.buffer.y, this.buffer.y); break; + case 'customGlyphs': case 'drawBoldTextInBrightColors': case 'letterSpacing': case 'lineHeight': @@ -249,8 +282,8 @@ export class Terminal extends CoreTerminal implements ITerminal { * Binds the desired focus behavior on a given terminal object. */ private _onTextAreaFocus(ev: KeyboardEvent): void { - if (this._coreService.decPrivateModes.sendFocus) { - this._coreService.triggerDataEvent(C0.ESC + '[I'); + if (this.coreService.decPrivateModes.sendFocus) { + this.coreService.triggerDataEvent(C0.ESC + '[I'); } this.updateCursorStyle(ev); this.element!.classList.add('focus'); @@ -274,27 +307,34 @@ export class Terminal extends CoreTerminal implements ITerminal { // screen readers reading it out. this.textarea!.value = ''; this.refresh(this.buffer.y, this.buffer.y); - if (this._coreService.decPrivateModes.sendFocus) { - this._coreService.triggerDataEvent(C0.ESC + '[O'); + if (this.coreService.decPrivateModes.sendFocus) { + this.coreService.triggerDataEvent(C0.ESC + '[O'); } this.element!.classList.remove('focus'); this._onBlur.fire(); } private _syncTextArea(): void { - if (!this.textarea || !this.buffer.isCursorInViewport || this._compositionHelper!.isComposing) { + if (!this.textarea || !this.buffer.isCursorInViewport || this._compositionHelper!.isComposing || !this._renderService) { return; } - - const cellHeight = Math.ceil(this._charSizeService!.height * this.optionsService.options.lineHeight); - const cursorTop = this._bufferService.buffer.y * cellHeight; - const cursorLeft = this._bufferService.buffer.x * this._charSizeService!.width; + const cursorY = this.buffer.ybase + this.buffer.y; + const bufferLine = this.buffer.lines.get(cursorY); + if (!bufferLine) { + return; + } + const cursorX = Math.min(this.buffer.x, this.cols - 1); + const cellHeight = this._renderService.dimensions.actualCellHeight; + const width = bufferLine.getWidth(cursorX); + const cellWidth = this._renderService.dimensions.actualCellWidth * width; + const cursorTop = this.buffer.y * this._renderService.dimensions.actualCellHeight; + const cursorLeft = cursorX * this._renderService.dimensions.actualCellWidth; // Sync the textarea to the exact position of the composition view so the IME knows where the // text is. this.textarea.style.left = cursorLeft + 'px'; this.textarea.style.top = cursorTop + 'px'; - this.textarea.style.width = this._charSizeService!.width + 'px'; + this.textarea.style.width = cellWidth + 'px'; this.textarea.style.height = cellHeight + 'px'; this.textarea.style.lineHeight = cellHeight + 'px'; this.textarea.style.zIndex = '-5'; @@ -315,7 +355,7 @@ export class Terminal extends CoreTerminal implements ITerminal { } copyHandler(event, this._selectionService!); })); - const pasteHandlerWrapper = (event: ClipboardEvent): void => handlePasteEvent(event, this.textarea!, this._coreService); + const pasteHandlerWrapper = (event: ClipboardEvent): void => handlePasteEvent(event, this.textarea!, this.coreService); this.register(addDisposableDomListener(this.textarea!, 'paste', pasteHandlerWrapper)); this.register(addDisposableDomListener(this.element!, 'paste', pasteHandlerWrapper)); @@ -357,6 +397,7 @@ export class Terminal extends CoreTerminal implements ITerminal { this.register(addDisposableDomListener(this.textarea!, 'compositionstart', () => this._compositionHelper!.compositionstart())); this.register(addDisposableDomListener(this.textarea!, 'compositionupdate', (e: CompositionEvent) => this._compositionHelper!.compositionupdate(e))); this.register(addDisposableDomListener(this.textarea!, 'compositionend', () => this._compositionHelper!.compositionend())); + this.register(addDisposableDomListener(this.textarea!, 'input', (ev: InputEvent) => this._inputEvent(ev), true)); this.register(this.onRender(() => this._compositionHelper!.updateCompositionElements())); this.register(this.onRender(e => this._queueLinkification(e.start, e.end))); } @@ -371,7 +412,7 @@ export class Terminal extends CoreTerminal implements ITerminal { throw new Error('Terminal requires a parent element.'); } - if (!document.body.contains(parent)) { + if (!parent.isConnected) { this._logService.debug('Terminal.open was called on an element that was not attached to the DOM'); } @@ -383,7 +424,6 @@ export class Terminal extends CoreTerminal implements ITerminal { this.element.classList.add('terminal'); this.element.classList.add('xterm'); this.element.setAttribute('tabindex', '0'); - this.element.setAttribute('role', 'document'); parent.appendChild(this.element); // Performance: Use a document fragment to build the terminal @@ -423,6 +463,20 @@ export class Terminal extends CoreTerminal implements ITerminal { this._charSizeService = this._instantiationService.createInstance(CharSizeService, this._document, this._helperContainer); this._instantiationService.setService(ICharSizeService, this._charSizeService); + this._theme = this.options.theme || this._theme; + this._colorManager = new ColorManager(document, this.options.allowTransparency); + this.register(this.optionsService.onOptionChange(e => this._colorManager!.onOptionsChange(e))); + this._colorManager.setTheme(this._theme); + + this._characterJoinerService = this._instantiationService.createInstance(CharacterJoinerService); + this._instantiationService.setService(ICharacterJoinerService, this._characterJoinerService); + + const renderer = this._createRenderer(); + this._renderService = this.register(this._instantiationService.createInstance(RenderService, renderer, this.rows, this.screenElement)); + this._instantiationService.setService(IRenderService, this._renderService); + this.register(this._renderService.onRenderedBufferChange(e => this._onRender.fire(e))); + this.onResize(e => this._renderService!.resize(e.cols, e.rows)); + this._compositionView = document.createElement('div'); this._compositionView.classList.add('composition-view'); this._compositionHelper = this._instantiationService.createInstance(CompositionHelper, this.textarea, this._compositionView); @@ -431,24 +485,13 @@ export class Terminal extends CoreTerminal implements ITerminal { // Performance: Add viewport and helper elements from the fragment this.element.appendChild(fragment); - this._theme = this.options.theme || this._theme; - this._colorManager = new ColorManager(document, this.options.allowTransparency); - this.register(this.optionsService.onOptionChange(e => this._colorManager!.onOptionsChange(e))); - this._colorManager.setTheme(this._theme); - - const renderer = this._createRenderer(); - this._renderService = this.register(this._instantiationService.createInstance(RenderService, renderer, this.rows, this.screenElement)); - this._instantiationService.setService(IRenderService, this._renderService); - this.register(this._renderService.onRenderedBufferChange(e => this._onRender.fire(e))); - this.onResize(e => this._renderService!.resize(e.cols, e.rows)); - this._soundService = this._instantiationService.createInstance(SoundService); this._instantiationService.setService(ISoundService, this._soundService); this._mouseService = this._instantiationService.createInstance(MouseService); this._instantiationService.setService(IMouseService, this._mouseService); this.viewport = this._instantiationService.createInstance(Viewport, - (amount: number, suppressEvent: boolean) => this.scrollLines(amount, suppressEvent), + (amount: number) => this.scrollLines(amount, true, ScrollSource.VIEWPORT), this._viewportElement, this._viewportScrollArea ); @@ -467,7 +510,9 @@ export class Terminal extends CoreTerminal implements ITerminal { this._selectionService = this.register(this._instantiationService.createInstance(SelectionService, this.element, - this.screenElement)); + this.screenElement, + this.linkifier2 + )); this._instantiationService.setService(ISelectionService, this._selectionService); this.register(this._selectionService.onRequestScrollLines(e => this.scrollLines(e.amount, e.suppressScrollEvent))); this.register(this._selectionService.onSelectionChange(() => this._onSelectionChange.fire())); @@ -480,7 +525,7 @@ export class Terminal extends CoreTerminal implements ITerminal { this.textarea!.focus(); this.textarea!.select(); })); - this.register(this.onScroll(() => { + this.register(this._onScroll.event(ev => { this.viewport!.syncScrollArea(); this._selectionService!.refresh(); })); @@ -496,7 +541,7 @@ export class Terminal extends CoreTerminal implements ITerminal { this.register(addDisposableDomListener(this.element, 'mousedown', (e: MouseEvent) => this._selectionService!.onMouseDown(e))); // apply mouse event classes set by escape codes before terminal was attached - if (this._coreMouseService.areMouseEventsActive) { + if (this.coreMouseService.areMouseEventsActive) { this._selectionService.disable(); this.element.classList.add('enable-mouse-events'); } else { @@ -571,7 +616,7 @@ export class Terminal extends CoreTerminal implements ITerminal { let but: CoreMouseButton; let action: CoreMouseAction | undefined; - switch ((ev).overrideType || ev.type) { + switch ((ev as any).overrideType || ev.type) { case 'mousemove': action = CoreMouseAction.MOVE; if (ev.buttons === undefined) { @@ -614,7 +659,7 @@ export class Terminal extends CoreTerminal implements ITerminal { return false; } - return self._coreMouseService.triggerMouseEvent({ + return self.coreMouseService.triggerMouseEvent({ col: pos.x - 33, // FIXME: why -33 here? row: pos.y - 33, button: but, @@ -669,11 +714,11 @@ export class Terminal extends CoreTerminal implements ITerminal { } } }; - this.register(this._coreMouseService.onProtocolChange(events => { + this.register(this.coreMouseService.onProtocolChange(events => { // apply global changes on events if (events) { if (this.optionsService.options.logLevel === 'debug') { - this._logService.debug('Binding to mouse events:', this._coreMouseService.explainEvents(events)); + this._logService.debug('Binding to mouse events:', this.coreMouseService.explainEvents(events)); } this.element!.classList.add('enable-mouse-events'); this._selectionService!.disable(); @@ -716,7 +761,7 @@ export class Terminal extends CoreTerminal implements ITerminal { } })); // force initial onProtocolChange so we dont miss early mouse requests - this._coreMouseService.activeProtocol = this._coreMouseService.activeProtocol; + this.coreMouseService.activeProtocol = this.coreMouseService.activeProtocol; /** * "Always on" event listeners. @@ -728,7 +773,7 @@ export class Terminal extends CoreTerminal implements ITerminal { // Don't send the mouse button to the pty if mouse events are disabled or // if the selection manager is having selection forced (ie. a modifier is // held). - if (!this._coreMouseService.areMouseEventsActive || this._selectionService!.shouldForceSelection(ev)) { + if (!this.coreMouseService.areMouseEventsActive || this._selectionService!.shouldForceSelection(ev)) { return; } @@ -761,12 +806,12 @@ export class Terminal extends CoreTerminal implements ITerminal { } // Construct and send sequences - const sequence = C0.ESC + (this._coreService.decPrivateModes.applicationCursorKeys ? 'O' : '[') + (ev.deltaY < 0 ? 'A' : 'B'); + const sequence = C0.ESC + (this.coreService.decPrivateModes.applicationCursorKeys ? 'O' : '[') + (ev.deltaY < 0 ? 'A' : 'B'); let data = ''; for (let i = 0; i < Math.abs(amount); i++) { data += sequence; } - this._coreService.triggerDataEvent(data, true); + this.coreService.triggerDataEvent(data, true); } return; } @@ -782,13 +827,13 @@ export class Terminal extends CoreTerminal implements ITerminal { }, { passive: false })); this.register(addDisposableDomListener(el, 'touchstart', (ev: TouchEvent) => { - if (this._coreMouseService.areMouseEventsActive) return; + if (this.coreMouseService.areMouseEventsActive) return; this.viewport!.onTouchStart(ev); return this.cancel(ev); }, { passive: true })); this.register(addDisposableDomListener(el, 'touchmove', (ev: TouchEvent) => { - if (this._coreMouseService.areMouseEventsActive) return; + if (this.coreMouseService.areMouseEventsActive) return; if (!this.viewport!.onTouchMove(ev)) { return this.cancel(ev); } @@ -819,7 +864,7 @@ export class Terminal extends CoreTerminal implements ITerminal { * Change the cursor style for different selection modes */ public updateCursorStyle(ev: KeyboardEvent): void { - if (this._selectionService && this._selectionService.shouldColumnSelect(ev)) { + if (this._selectionService?.shouldColumnSelect(ev)) { this.element!.classList.add('column-select'); } else { this.element!.classList.remove('column-select'); @@ -830,19 +875,19 @@ export class Terminal extends CoreTerminal implements ITerminal { * Display the cursor element */ private _showCursor(): void { - if (!this._coreService.isCursorInitialized) { - this._coreService.isCursorInitialized = true; + if (!this.coreService.isCursorInitialized) { + this.coreService.isCursorInitialized = true; this.refresh(this.buffer.y, this.buffer.y); } } - public scrollLines(disp: number, suppressScrollEvent?: boolean): void { - super.scrollLines(disp, suppressScrollEvent); + public scrollLines(disp: number, suppressScrollEvent?: boolean, source = ScrollSource.TERMINAL): void { + super.scrollLines(disp, suppressScrollEvent, source); this.refresh(0, this.rows - 1); } public paste(data: string): void { - paste(data, this.textarea!, this._coreService); + paste(data, this.textarea!, this.coreService); } /** @@ -889,13 +934,19 @@ export class Terminal extends CoreTerminal implements ITerminal { } public registerCharacterJoiner(handler: CharacterJoinerHandler): number { - const joinerId = this._renderService!.registerCharacterJoiner(handler); + if (!this._characterJoinerService) { + throw new Error('Terminal must be opened first'); + } + const joinerId = this._characterJoinerService.register(handler); this.refresh(0, this.rows - 1); return joinerId; } public deregisterCharacterJoiner(joinerId: number): void { - if (this._renderService!.deregisterCharacterJoiner(joinerId)) { + if (!this._characterJoinerService) { + throw new Error('Terminal must be opened first'); + } + if (this._characterJoinerService.deregister(joinerId)) { this.refresh(0, this.rows - 1); } } @@ -984,12 +1035,16 @@ export class Terminal extends CoreTerminal implements ITerminal { if (!this._compositionHelper!.keydown(event)) { if (this.buffer.ybase !== this.buffer.ydisp) { - this.scrollToBottom(); + this._bufferService.scrollToBottom(); } return false; } - const result = evaluateKeyboardEvent(event, this._coreService.decPrivateModes.applicationCursorKeys, this.browser.isMac, this.options.macOptionIsMeta); + if (event.key === 'Dead' || event.key === 'AltGraph') { + this._unprocessedDeadKey = true; + } + + const result = evaluateKeyboardEvent(event, this.coreService.decPrivateModes.applicationCursorKeys, this.browser.isMac, this.options.macOptionIsMeta); this.updateCursorStyle(event); @@ -1016,6 +1071,11 @@ export class Terminal extends CoreTerminal implements ITerminal { return true; } + if (this._unprocessedDeadKey) { + this._unprocessedDeadKey = false; + return true; + } + // If ctrl+c or enter is being sent, clear out the textarea. This is done so that screen readers // will announce deleted characters. This will not work 100% of the time but it should cover // most scenarios. @@ -1025,7 +1085,7 @@ export class Terminal extends CoreTerminal implements ITerminal { this._onKey.fire({ key: result.key, domEvent: event }); this._showCursor(); - this._coreService.triggerDataEvent(result.key, true); + this.coreService.triggerDataEvent(result.key, true); // Cancel events when not in screen reader mode so events don't get bubbled up and handled by // other listeners. When screen reader mode is enabled, this could cause issues if the event @@ -1038,10 +1098,11 @@ export class Terminal extends CoreTerminal implements ITerminal { this._keyDownHandled = true; } - private _isThirdLevelShift(browser: IBrowser, ev: IKeyboardEvent): boolean { + private _isThirdLevelShift(browser: IBrowser, ev: KeyboardEvent): boolean { const thirdLevelKey = (browser.isMac && !this.options.macOptionIsMeta && ev.altKey && !ev.ctrlKey && !ev.metaKey) || - (browser.isWindows && ev.altKey && ev.ctrlKey && !ev.metaKey); + (browser.isWindows && ev.altKey && ev.ctrlKey && !ev.metaKey) || + (browser.isWindows && ev.getModifierState('AltGraph')); if (ev.type === 'keypress') { return thirdLevelKey; @@ -1061,6 +1122,7 @@ export class Terminal extends CoreTerminal implements ITerminal { } this.updateCursorStyle(ev); + this._keyPressHandled = false; } /** @@ -1072,6 +1134,8 @@ export class Terminal extends CoreTerminal implements ITerminal { protected _keyPress(ev: KeyboardEvent): boolean { let key; + this._keyPressHandled = false; + if (this._keyDownHandled) { return false; } @@ -1102,20 +1166,48 @@ export class Terminal extends CoreTerminal implements ITerminal { this._onKey.fire({ key, domEvent: ev }); this._showCursor(); - this._coreService.triggerDataEvent(key, true); + this.coreService.triggerDataEvent(key, true); + + this._keyPressHandled = true; return true; } + /** + * Handle an input event. + * Key Resources: + * - https://developer.mozilla.org/en-US/docs/Web/API/InputEvent + * @param ev The input event to be handled. + */ + protected _inputEvent(ev: InputEvent): boolean { + // Only support emoji IMEs when screen reader mode is disabled as the event must bubble up to + // support reading out character input which can doubling up input characters + if (ev.data && ev.inputType === 'insertText' && !this.optionsService.options.screenReaderMode) { + if (this._keyPressHandled) { + return false; + } + + const text = ev.data; + this.coreService.triggerDataEvent(text, true); + + this.cancel(ev); + return true; + } + + return false; + } + /** * Ring the bell. * Note: We could do sweet things with webaudio here */ public bell(): void { if (this._soundBell()) { - this._soundService!.playBellSound(); + this._soundService?.playBellSound(); } + this._onBell.fire(); + // if (this._visualBell()) { // this.element.classList.add('visual-bell-active'); // clearTimeout(this._visualBellTimer); @@ -1168,7 +1260,7 @@ export class Terminal extends CoreTerminal implements ITerminal { this.buffer.lines.push(this.buffer.getBlankLine(DEFAULT_ATTR_DATA)); } this.refresh(0, this.rows - 1); - this._onScroll.fire(this.buffer.ydisp); + this._onScroll.fire({ position: this.buffer.ydisp, source: ScrollSource.TERMINAL }); } /** @@ -1200,6 +1292,10 @@ export class Terminal extends CoreTerminal implements ITerminal { this.viewport?.syncScrollArea(); } + public clearTextureAtlas(): void { + this._renderService?.clearTextureAtlas(); + } + private _reportWindowsOptions(type: WindowsOptionsReportType): void { if (!this._renderService) { return; @@ -1209,12 +1305,12 @@ export class Terminal extends CoreTerminal implements ITerminal { case WindowsOptionsReportType.GET_WIN_SIZE_PIXELS: const canvasWidth = this._renderService.dimensions.scaledCanvasWidth.toFixed(0); const canvasHeight = this._renderService.dimensions.scaledCanvasHeight.toFixed(0); - this._coreService.triggerDataEvent(`${C0.ESC}[4;${canvasHeight};${canvasWidth}t`); + this.coreService.triggerDataEvent(`${C0.ESC}[4;${canvasHeight};${canvasWidth}t`); break; case WindowsOptionsReportType.GET_CELL_SIZE_PIXELS: const cellWidth = this._renderService.dimensions.scaledCellWidth.toFixed(0); const cellHeight = this._renderService.dimensions.scaledCellHeight.toFixed(0); - this._coreService.triggerDataEvent(`${C0.ESC}[6;${cellHeight};${cellWidth}t`); + this.coreService.triggerDataEvent(`${C0.ESC}[6;${cellHeight};${cellWidth}t`); break; } } diff --git a/src/browser/Terminal2.test.ts b/src/browser/Terminal2.test.ts index 7d3fe851..4965e8f3 100644 --- a/src/browser/Terminal2.test.ts +++ b/src/browser/Terminal2.test.ts @@ -17,6 +17,7 @@ const ROWS = 25; const TESTFILES = glob.sync('**/escape_sequence_files/*.in', { cwd: path.join(__dirname, '../..')}); const SKIP_FILES = [ + 't0055-EL.in', // EL/ED handle cursor at cols differently (see #3362) 't0084-CBT.in', 't0101-NLM.in', 't0103-reverse_wrap.in', // not comparable, we deviate from xterm reverse wrap on purpose @@ -73,8 +74,8 @@ describe('Escape Sequence Files', function(): void { // register handler to trigger viewport scraping, wait for it to finish let content = ''; const OSC_CODE = 12345; - await new Promise(resolve => { - customHandler = term.addOscHandler(OSC_CODE, () => { + await new Promise(resolve => { + customHandler = term.registerOscHandler(OSC_CODE, () => { // grab terminal viewport content content = terminalToString(term); resolve(); @@ -105,7 +106,7 @@ function formatError(input: string, output: string, expected: string): string { function addLineNumber(start: number, color: string): (s: string) => string { let counter = start || 0; return (s: string): string => { - counter += 1; + counter++; return '\x1b[33m' + (' ' + counter).slice(-2) + color + s; }; } diff --git a/src/browser/TestUtils.test.ts b/src/browser/TestUtils.test.ts index 6a4b4a0d..8fdf458e 100644 --- a/src/browser/TestUtils.test.ts +++ b/src/browser/TestUtils.test.ts @@ -5,15 +5,15 @@ import { IDisposable, IMarker, ISelectionPosition, ILinkProvider } from 'xterm'; import { IEvent, EventEmitter } from 'common/EventEmitter'; -import { ICharSizeService, IMouseService, IRenderService, ISelectionService } from 'browser/services/Services'; -import { IRenderDimensions, IRenderer, CharacterJoinerHandler, IRequestRedrawEvent } from 'browser/renderer/Types'; -import { IColorSet, ILinkMatcherOptions, ITerminal, ILinkifier, ILinkifier2, IBrowser, IViewport, IColorManager, ICompositionHelper } from 'browser/Types'; +import { ICharacterJoinerService, ICharSizeService, IMouseService, IRenderService, ISelectionService } from 'browser/services/Services'; +import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/Types'; +import { IColorSet, ILinkMatcherOptions, ITerminal, ILinkifier, ILinkifier2, IBrowser, IViewport, IColorManager, ICompositionHelper, CharacterJoinerHandler } from 'browser/Types'; import { IBuffer, IBufferStringIterator, IBufferSet } from 'common/buffer/Types'; import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener, ICharset, ITerminalOptions } from 'common/Types'; import { Buffer } from 'common/buffer/Buffer'; import * as Browser from 'common/Platform'; import { Terminal } from 'browser/Terminal'; -import { IUnicodeService, IOptionsService } from 'common/services/Services'; +import { IUnicodeService, IOptionsService, ICoreService, ICoreMouseService } from 'common/services/Services'; import { IFunctionIdentifier, IParams } from 'common/parser/Types'; import { AttributeData } from 'common/buffer/AttributeData'; @@ -21,6 +21,9 @@ export class TestTerminal extends Terminal { public get curAttrData(): IAttributeData { return (this as any)._inputHandler._curAttrData; } public keyDown(ev: any): boolean | undefined { return this._keyDown(ev); } public keyPress(ev: any): boolean { return this._keyPress(ev); } + public writeP(data: string | Uint8Array): Promise { + return new Promise(r => this.write(data, r)); + } } export class MockTerminal implements ITerminal { @@ -34,11 +37,14 @@ export class MockTerminal implements ITerminal { public onData!: IEvent; public onBinary!: IEvent; public onTitleChange!: IEvent; + public onBell!: IEvent; public onScroll!: IEvent; public onKey!: IEvent<{ key: string, domEvent: KeyboardEvent }>; public onRender!: IEvent<{ start: number, end: number }>; public onResize!: IEvent<{ cols: number, rows: number }>; public markers!: IMarker[]; + public coreMouseService!: ICoreMouseService; + public coreService!: ICoreService; public optionsService!: IOptionsService; public unicodeService!: IUnicodeService; public addMarker(cursorYOffset: number): IMarker { @@ -75,16 +81,16 @@ export class MockTerminal implements ITerminal { public attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void { throw new Error('Method not implemented.'); } - public addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable { + public registerCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean | Promise): IDisposable { throw new Error('Method not implemented.'); } - public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean): IDisposable { + public registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean | Promise): IDisposable { throw new Error('Method not implemented.'); } - public addEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable { + public registerEscHandler(id: IFunctionIdentifier, handler: () => boolean | Promise): IDisposable { throw new Error('Method not implemented.'); } - public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { + public registerOscHandler(ident: number, callback: (data: string) => boolean | Promise): IDisposable { throw new Error('Method not implemented.'); } public registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => boolean | void, options?: ILinkMatcherOptions): number { @@ -149,7 +155,7 @@ export class MockTerminal implements ITerminal { public textarea!: HTMLTextAreaElement; public rows!: number; public cols!: number; - public browser: IBrowser = Browser; + public browser: IBrowser = Browser as any; public writeBuffer!: string[]; public children!: HTMLElement[]; public cursorHidden!: boolean; @@ -189,6 +195,9 @@ export class MockTerminal implements ITerminal { public reset(): void { throw new Error('Method not implemented.'); } + public clearTextureAtlas(): void { + throw new Error('Method not implemented.'); + } public refresh(start: number, end: number): void { throw new Error('Method not implemented.'); } @@ -281,8 +290,6 @@ export class MockRenderer implements IRenderer { public onDevicePixelRatioChange(): void { } public clear(): void { } public renderRows(start: number, end: number): void { } - public registerCharacterJoiner(handler: CharacterJoinerHandler): number { return 0; } - public deregisterCharacterJoiner(): boolean { return true; } } export class MockViewport implements IViewport { @@ -370,6 +377,9 @@ export class MockRenderService implements IRenderService { public refreshRows(start: number, end: number): void { throw new Error('Method not implemented.'); } + public clearTextureAtlas(): void { + throw new Error('Method not implemented.'); + } public resize(cols: number, rows: number): void { throw new Error('Method not implemented.'); } @@ -406,13 +416,20 @@ export class MockRenderService implements IRenderService { public clear(): void { throw new Error('Method not implemented.'); } - public registerCharacterJoiner(handler: CharacterJoinerHandler): number { - throw new Error('Method not implemented.'); - } - public deregisterCharacterJoiner(joinerId: number): boolean { - throw new Error('Method not implemented.'); - } public dispose(): void { throw new Error('Method not implemented.'); } } + +export class MockCharacterJoinerService implements ICharacterJoinerService { + public serviceBrand: undefined; + public register(handler: (text: string) => [number, number][]): number { + return 0; + } + public deregister(joinerId: number): boolean { + return true; + } + public getJoinedCharacters(row: number): [number, number][] { + return []; + } +} diff --git a/src/browser/TimeBasedDebouncer.ts b/src/browser/TimeBasedDebouncer.ts new file mode 100644 index 00000000..707e25cb --- /dev/null +++ b/src/browser/TimeBasedDebouncer.ts @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +const RENDER_DEBOUNCE_THRESHOLD_MS = 1000; // 1 Second + +import { IRenderDebouncer } from 'browser/Types'; + +/** + * Debounces calls to update screen readers to update at most once configurable interval of time. + */ +export class TimeBasedDebouncer implements IRenderDebouncer { + private _rowStart: number | undefined; + private _rowEnd: number | undefined; + private _rowCount: number | undefined; + + // The last moment that the Terminal was refreshed at + private _lastRefreshMs = 0; + // Whether a trailing refresh should be triggered due to a refresh request that was throttled + private _additionalRefreshRequested = false; + + private _refreshTimeoutID: number | undefined; + + constructor( + private _renderCallback: (start: number, end: number) => void, + private readonly _debounceThresholdMS = RENDER_DEBOUNCE_THRESHOLD_MS + ) { + } + + public dispose(): void { + if (this._refreshTimeoutID) { + clearTimeout(this._refreshTimeoutID); + } + } + + public refresh(rowStart: number | undefined, rowEnd: number | undefined, rowCount: number): void { + this._rowCount = rowCount; + // Get the min/max row start/end for the arg values + rowStart = rowStart !== undefined ? rowStart : 0; + rowEnd = rowEnd !== undefined ? rowEnd : this._rowCount - 1; + // Set the properties to the updated values + this._rowStart = this._rowStart !== undefined ? Math.min(this._rowStart, rowStart) : rowStart; + this._rowEnd = this._rowEnd !== undefined ? Math.max(this._rowEnd, rowEnd) : rowEnd; + + // Only refresh if the time since last refresh is above a threshold, otherwise wait for + // enough time to pass before refreshing again. + const refreshRequestTime: number = Date.now(); + if (refreshRequestTime - this._lastRefreshMs >= this._debounceThresholdMS) { + // Enough time has lapsed since the last refresh; refresh immediately + this._lastRefreshMs = refreshRequestTime; + this._innerRefresh(); + } else if (!this._additionalRefreshRequested) { + // This is the first additional request throttled; set up trailing refresh + const elapsed = refreshRequestTime - this._lastRefreshMs; + const waitPeriodBeforeTrailingRefresh = this._debounceThresholdMS - elapsed; + this._additionalRefreshRequested = true; + + this._refreshTimeoutID = window.setTimeout(() => { + this._lastRefreshMs = Date.now(); + this._innerRefresh(); + this._additionalRefreshRequested = false; + this._refreshTimeoutID = undefined; // No longer need to clear the timeout + }, waitPeriodBeforeTrailingRefresh); + } + } + + private _innerRefresh(): void { + // Make sure values are set + if (this._rowStart === undefined || this._rowEnd === undefined || this._rowCount === undefined) { + return; + } + + // Clamp values + const start = Math.max(this._rowStart, 0); + const end = Math.min(this._rowEnd, this._rowCount - 1); + + // Reset debouncer (this happens before render callback as the render could trigger it again) + this._rowStart = undefined; + this._rowEnd = undefined; + + // Run render callback + this._renderCallback(start, end); + } +} + diff --git a/src/browser/Types.d.ts b/src/browser/Types.d.ts index e8e0cabd..bafeff77 100644 --- a/src/browser/Types.d.ts +++ b/src/browser/Types.d.ts @@ -15,7 +15,6 @@ export interface ITerminal extends IPublicTerminal, ICoreTerminal { screenElement: HTMLElement | undefined; browser: IBrowser; buffer: IBuffer; - buffers: IBufferSet; viewport: IViewport | undefined; // TODO: We should remove options once components adopt optionsService options: ITerminalOptions; @@ -47,15 +46,16 @@ export interface IPublicTerminal extends IDisposable { onRender: IEvent<{ start: number, end: number }>; onResize: IEvent<{ cols: number, rows: number }>; onTitleChange: IEvent; + onBell: IEvent; blur(): void; focus(): void; resize(columns: number, rows: number): void; open(parent: HTMLElement): void; attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void; - addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable; - addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean): IDisposable; - addEscHandler(id: IFunctionIdentifier, callback: () => boolean): IDisposable; - addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable; + registerCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean | Promise): IDisposable; + registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean | Promise): IDisposable; + registerEscHandler(id: IFunctionIdentifier, callback: () => boolean | Promise): IDisposable; + registerOscHandler(ident: number, callback: (data: string) => boolean | Promise): IDisposable; registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): number; deregisterLinkMatcher(matcherId: number): void; registerLinkProvider(linkProvider: ILinkProvider): IDisposable; @@ -79,6 +79,7 @@ export interface IPublicTerminal extends IDisposable { write(data: string | Uint8Array, callback?: () => void): void; paste(data: string): void; refresh(start: number, end: number): void; + clearTextureAtlas(): void; reset(): void; } @@ -205,9 +206,19 @@ export interface ILinkifier { deregisterLinkMatcher(matcherId: number): boolean; } +interface ILinkState { + decorations: ILinkDecorations; + isHovered: boolean; +} +export interface ILinkWithState { + link: ILink; + state?: ILinkState; +} + export interface ILinkifier2 { onShowLinkUnderline: IEvent; onHideLinkUnderline: IEvent; + readonly currentLink: ILinkWithState | undefined; attachToDom(element: HTMLElement, mouseService: IMouseService, renderService: IRenderService): void; registerLinkProvider(linkProvider: ILinkProvider): IDisposable; @@ -292,3 +303,14 @@ interface IBufferCellPosition { x: number; y: number; } + +export type CharacterJoinerHandler = (text: string) => [number, number][]; + +export interface ICharacterJoiner { + id: number; + handler: CharacterJoinerHandler; +} + +export interface IRenderDebouncer extends IDisposable { + refresh(rowStart: number | undefined, rowEnd: number | undefined, rowCount: number): void; +} diff --git a/src/browser/Viewport.ts b/src/browser/Viewport.ts index 29edce6f..3c9bea4c 100644 --- a/src/browser/Viewport.ts +++ b/src/browser/Viewport.ts @@ -8,6 +8,8 @@ import { addDisposableDomListener } from 'browser/Lifecycle'; import { IColorSet, IViewport } from 'browser/Types'; import { ICharSizeService, IRenderService } from 'browser/services/Services'; import { IBufferService, IOptionsService } from 'common/services/Services'; +import { IBuffer } from 'common/buffer/Types'; +import { IRenderDimensions } from 'browser/renderer/Types'; const FALLBACK_SCROLL_BAR_WIDTH = 15; @@ -18,11 +20,15 @@ const FALLBACK_SCROLL_BAR_WIDTH = 15; export class Viewport extends Disposable implements IViewport { public scrollBarWidth: number = 0; private _currentRowHeight: number = 0; + private _currentScaledCellHeight: number = 0; private _lastRecordedBufferLength: number = 0; private _lastRecordedViewportHeight: number = 0; private _lastRecordedBufferHeight: number = 0; private _lastTouchY: number = 0; private _lastScrollTop: number = 0; + private _lastHadScrollBar: boolean = false; + private _activeBuffer: IBuffer; + private _renderDimensions: IRenderDimensions; // Stores a partial line amount when scrolling, this is used to keep track of how much of a line // is scrolled so we can "scroll" over partial lines and feel natural on touchpads. This is a @@ -33,7 +39,7 @@ export class Viewport extends Disposable implements IViewport { private _ignoreNextScrollEvent: boolean = false; constructor( - private readonly _scrollLines: (amount: number, suppressEvent: boolean) => void, + private readonly _scrollLines: (amount: number) => void, private readonly _viewportElement: HTMLElement, private readonly _scrollArea: HTMLElement, @IBufferService private readonly _bufferService: IBufferService, @@ -47,8 +53,15 @@ export class Viewport extends Disposable implements IViewport { // Unfortunately the overlay scrollbar would be hidden underneath the screen element in that case, // therefore we account for a standard amount to make it visible this.scrollBarWidth = (this._viewportElement.offsetWidth - this._scrollArea.offsetWidth) || FALLBACK_SCROLL_BAR_WIDTH; + this._lastHadScrollBar = true; this.register(addDisposableDomListener(this._viewportElement, 'scroll', this._onScroll.bind(this))); + // Track properties used in performance critical code manually to avoid using slow getters + this._activeBuffer = this._bufferService.buffer; + this.register(this._bufferService.buffers.onBufferActivate(e => this._activeBuffer = e.activeBuffer)); + this._renderDimensions = this._renderService.dimensions; + this.register(this._renderService.onDimensionsChange(e => this._renderDimensions = e)); + // Perform this async to ensure the ICharSizeService is ready. setTimeout(() => this.syncScrollArea(), 0); } @@ -77,6 +90,7 @@ export class Viewport extends Disposable implements IViewport { private _innerRefresh(): void { if (this._charSizeService.height > 0) { this._currentRowHeight = this._renderService.dimensions.scaledCellHeight / window.devicePixelRatio; + this._currentScaledCellHeight = this._renderService.dimensions.scaledCellHeight; this._lastRecordedViewportHeight = this._viewportElement.offsetHeight; const newBufferHeight = Math.round(this._currentRowHeight * this._lastRecordedBufferLength) + (this._lastRecordedViewportHeight - this._renderService.dimensions.canvasHeight); if (this._lastRecordedBufferHeight !== newBufferHeight) { @@ -94,8 +108,18 @@ export class Viewport extends Disposable implements IViewport { this._viewportElement.scrollTop = scrollTop; } + // Update scroll bar width + if (this._optionsService.options.scrollback === 0) { + this.scrollBarWidth = 0; + } else { + this.scrollBarWidth = (this._viewportElement.offsetWidth - this._scrollArea.offsetWidth) || FALLBACK_SCROLL_BAR_WIDTH; + } + this._lastHadScrollBar = this.scrollBarWidth > 0; + + this._viewportElement.style.width = (this._renderService.dimensions.actualCellWidth * (this._bufferService.cols) + this.scrollBarWidth).toString() + 'px'; this._refreshAnimationFrame = null; } + /** * Updates dimensions and synchronizes the scroll area if necessary. */ @@ -114,23 +138,21 @@ export class Viewport extends Disposable implements IViewport { } // If the buffer position doesn't match last scroll top - const newScrollTop = this._bufferService.buffer.ydisp * this._currentRowHeight; - if (this._lastScrollTop !== newScrollTop) { - this._refresh(immediate); - return; - } - - // If element's scroll top changed, this can happen when hiding the element - if (this._lastScrollTop !== this._viewportElement.scrollTop) { + if (this._lastScrollTop !== this._activeBuffer.ydisp * this._currentRowHeight) { this._refresh(immediate); return; } // If row height changed - if (this._renderService.dimensions.scaledCellHeight / window.devicePixelRatio !== this._currentRowHeight) { + if (this._renderDimensions.scaledCellHeight !== this._currentScaledCellHeight) { this._refresh(immediate); return; } + + // If the scroll bar visibility changed + if (this._lastHadScrollBar !== (this._optionsService.options.scrollback > 0)) { + this._refresh(immediate); + } } /** @@ -151,12 +173,14 @@ export class Viewport extends Disposable implements IViewport { // Ignore the event if it was flagged to ignore (when the source of the event is from Viewport) if (this._ignoreNextScrollEvent) { this._ignoreNextScrollEvent = false; + // Still trigger the scroll so lines get refreshed + this._scrollLines(0); return; } const newRow = Math.round(this._lastScrollTop / this._currentRowHeight); const diff = newRow - this._bufferService.buffer.ydisp; - this._scrollLines(diff, true); + this._scrollLines(diff); } /** diff --git a/src/browser/input/CompositionHelper.test.ts b/src/browser/input/CompositionHelper.test.ts index b9a4f668..e29111e1 100644 --- a/src/browser/input/CompositionHelper.test.ts +++ b/src/browser/input/CompositionHelper.test.ts @@ -5,7 +5,7 @@ import { assert } from 'chai'; import { CompositionHelper } from 'browser/input/CompositionHelper'; -import { MockCharSizeService } from 'browser/TestUtils.test'; +import { MockRenderService } from 'browser/TestUtils.test'; import { MockCoreService, MockBufferService, MockOptionsService } from 'common/TestUtils.test'; describe('CompositionHelper', () => { @@ -42,14 +42,14 @@ describe('CompositionHelper', () => { }; handledText = ''; const bufferService = new MockBufferService(10, 5); - compositionHelper = new CompositionHelper(textarea, compositionView, bufferService, new MockOptionsService(), new MockCharSizeService(10, 10), coreService); + compositionHelper = new CompositionHelper(textarea, compositionView, bufferService, new MockOptionsService(), coreService, new MockRenderService()); }); describe('Input', () => { it('Should insert simple characters', (done) => { // First character 'ㅇ' compositionHelper.compositionstart(); - compositionHelper.compositionupdate({ data: 'ㅇ' }); + compositionHelper.compositionupdate({ data: 'ㅇ' }); textarea.value = 'ㅇ'; setTimeout(() => { // wait for any textarea updates compositionHelper.compositionend(); @@ -57,7 +57,7 @@ describe('CompositionHelper', () => { assert.equal(handledText, 'ㅇ'); // Second character 'ㅇ' compositionHelper.compositionstart(); - compositionHelper.compositionupdate({ data: 'ㅇ' }); + compositionHelper.compositionupdate({ data: 'ㅇ' }); textarea.value = 'ㅇㅇ'; setTimeout(() => { // wait for any textarea updates compositionHelper.compositionend(); @@ -73,13 +73,13 @@ describe('CompositionHelper', () => { it('Should insert complex characters', (done) => { // First character '앙' compositionHelper.compositionstart(); - compositionHelper.compositionupdate({ data: 'ㅇ' }); + compositionHelper.compositionupdate({ data: 'ㅇ' }); textarea.value = 'ㅇ'; setTimeout(() => { // wait for any textarea updates - compositionHelper.compositionupdate({ data: '아' }); + compositionHelper.compositionupdate({ data: '아' }); textarea.value = '아'; setTimeout(() => { // wait for any textarea updates - compositionHelper.compositionupdate({ data: '앙' }); + compositionHelper.compositionupdate({ data: '앙' }); textarea.value = '앙'; setTimeout(() => { // wait for any textarea updates compositionHelper.compositionend(); @@ -87,13 +87,13 @@ describe('CompositionHelper', () => { assert.equal(handledText, '앙'); // Second character '앙' compositionHelper.compositionstart(); - compositionHelper.compositionupdate({ data: 'ㅇ' }); + compositionHelper.compositionupdate({ data: 'ㅇ' }); textarea.value = '앙ㅇ'; setTimeout(() => { // wait for any textarea updates - compositionHelper.compositionupdate({ data: '아' }); + compositionHelper.compositionupdate({ data: '아' }); textarea.value = '앙아'; setTimeout(() => { // wait for any textarea updates - compositionHelper.compositionupdate({ data: '앙' }); + compositionHelper.compositionupdate({ data: '앙' }); textarea.value = '앙앙'; setTimeout(() => { // wait for any textarea updates compositionHelper.compositionend(); @@ -113,19 +113,19 @@ describe('CompositionHelper', () => { it('Should insert complex characters that change with following character', (done) => { // First character '아' compositionHelper.compositionstart(); - compositionHelper.compositionupdate({ data: 'ㅇ' }); + compositionHelper.compositionupdate({ data: 'ㅇ' }); textarea.value = 'ㅇ'; setTimeout(() => { // wait for any textarea updates - compositionHelper.compositionupdate({ data: '아' }); + compositionHelper.compositionupdate({ data: '아' }); textarea.value = '아'; setTimeout(() => { // wait for any textarea updates // Start second character '아' in first character - compositionHelper.compositionupdate({ data: '앙' }); + compositionHelper.compositionupdate({ data: '앙' }); textarea.value = '앙'; setTimeout(() => { // wait for any textarea updates compositionHelper.compositionend(); compositionHelper.compositionstart(); - compositionHelper.compositionupdate({ data: '아' }); + compositionHelper.compositionupdate({ data: '아' }); textarea.value = '아아'; setTimeout(() => { // wait for any textarea updates compositionHelper.compositionend(); @@ -142,14 +142,14 @@ describe('CompositionHelper', () => { it('Should insert multi-characters compositions', (done) => { // First character 'だ' compositionHelper.compositionstart(); - compositionHelper.compositionupdate({ data: 'd' }); + compositionHelper.compositionupdate({ data: 'd' }); textarea.value = 'd'; setTimeout(() => { // wait for any textarea updates - compositionHelper.compositionupdate({ data: 'だ' }); + compositionHelper.compositionupdate({ data: 'だ' }); textarea.value = 'だ'; setTimeout(() => { // wait for any textarea updates // Second character 'あ' - compositionHelper.compositionupdate({ data: 'だあ' }); + compositionHelper.compositionupdate({ data: 'だあ' }); textarea.value = 'だあ'; setTimeout(() => { // wait for any textarea updates compositionHelper.compositionend(); @@ -165,18 +165,18 @@ describe('CompositionHelper', () => { it('Should insert multi-character compositions that are converted to other characters with the same length', (done) => { // First character 'だ' compositionHelper.compositionstart(); - compositionHelper.compositionupdate({ data: 'd' }); + compositionHelper.compositionupdate({ data: 'd' }); textarea.value = 'd'; setTimeout(() => { // wait for any textarea updates - compositionHelper.compositionupdate({ data: 'だ' }); + compositionHelper.compositionupdate({ data: 'だ' }); textarea.value = 'だ'; setTimeout(() => { // wait for any textarea updates // Second character 'ー' - compositionHelper.compositionupdate({ data: 'だー' }); + compositionHelper.compositionupdate({ data: 'だー' }); textarea.value = 'だー'; setTimeout(() => { // wait for any textarea updates // Convert to katakana 'ダー' - compositionHelper.compositionupdate({ data: 'ダー' }); + compositionHelper.compositionupdate({ data: 'ダー' }); textarea.value = 'ダー'; setTimeout(() => { // wait for any textarea updates compositionHelper.compositionend(); @@ -193,18 +193,18 @@ describe('CompositionHelper', () => { it('Should insert multi-character compositions that are converted to other characters with different lengths', (done) => { // First character 'い' compositionHelper.compositionstart(); - compositionHelper.compositionupdate({ data: 'い' }); + compositionHelper.compositionupdate({ data: 'い' }); textarea.value = 'い'; setTimeout(() => { // wait for any textarea updates // Second character 'ま' - compositionHelper.compositionupdate({ data: 'いm' }); + compositionHelper.compositionupdate({ data: 'いm' }); textarea.value = 'いm'; setTimeout(() => { // wait for any textarea updates - compositionHelper.compositionupdate({ data: 'いま' }); + compositionHelper.compositionupdate({ data: 'いま' }); textarea.value = 'いま'; setTimeout(() => { // wait for any textarea updates // Convert to kanji '今' - compositionHelper.compositionupdate({ data: '今' }); + compositionHelper.compositionupdate({ data: '今' }); textarea.value = '今'; setTimeout(() => { // wait for any textarea updates compositionHelper.compositionend(); @@ -221,7 +221,7 @@ describe('CompositionHelper', () => { it('Should insert non-composition characters input immediately after composition characters', (done) => { // First character 'ㅇ' compositionHelper.compositionstart(); - compositionHelper.compositionupdate({ data: 'ㅇ' }); + compositionHelper.compositionupdate({ data: 'ㅇ' }); textarea.value = 'ㅇ'; setTimeout(() => { // wait for any textarea updates compositionHelper.compositionend(); diff --git a/src/browser/input/CompositionHelper.ts b/src/browser/input/CompositionHelper.ts index 8d393e33..4e176725 100644 --- a/src/browser/input/CompositionHelper.ts +++ b/src/browser/input/CompositionHelper.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { ICharSizeService } from 'browser/services/Services'; +import { IRenderService } from 'browser/services/Services'; import { IBufferService, ICoreService, IOptionsService } from 'common/services/Services'; interface IPosition { @@ -35,17 +35,23 @@ export class CompositionHelper { */ private _isSendingComposition: boolean; + /** + * Data already sent due to keydown event. + */ + private _dataAlreadySent: string; + constructor( private readonly _textarea: HTMLTextAreaElement, private readonly _compositionView: HTMLElement, @IBufferService private readonly _bufferService: IBufferService, @IOptionsService private readonly _optionsService: IOptionsService, - @ICharSizeService private readonly _charSizeService: ICharSizeService, - @ICoreService private readonly _coreService: ICoreService + @ICoreService private readonly _coreService: ICoreService, + @IRenderService private readonly _renderService: IRenderService ) { this._isComposing = false; this._isSendingComposition = false; this._compositionPosition = { start: 0, end: 0 }; + this._dataAlreadySent = ''; } /** @@ -55,6 +61,7 @@ export class CompositionHelper { this._isComposing = true; this._compositionPosition.start = this._textarea.value.length; this._compositionView.textContent = ''; + this._dataAlreadySent = ''; this._compositionView.classList.add('active'); } @@ -62,7 +69,7 @@ export class CompositionHelper { * Handles the compositionupdate event, updating the composition view. * @param ev The event. */ - public compositionupdate(ev: CompositionEvent): void { + public compositionupdate(ev: Pick): void { this._compositionView.textContent = ev.data; this.updateCompositionElements(); setTimeout(() => { @@ -147,6 +154,9 @@ export class CompositionHelper { if (this._isSendingComposition) { this._isSendingComposition = false; let input; + // Add length of data already sent due to keydown event, + // otherwise input characters can be duplicated. (Issue #3191) + currentCompositionPosition.start += this._dataAlreadySent.length; if (this._isComposing) { // Use the end position to get the string if a new composition has started. input = this._textarea.value.substring(currentCompositionPosition.start, currentCompositionPosition.end); @@ -156,7 +166,9 @@ export class CompositionHelper { // (eg. 2) after a composition character. input = this._textarea.value.substring(currentCompositionPosition.start); } - this._coreService.triggerDataEvent(input, true); + if (input.length > 0) { + this._coreService.triggerDataEvent(input, true); + } } }, 0); } @@ -176,6 +188,7 @@ export class CompositionHelper { const newValue = this._textarea.value; const diff = newValue.replace(oldValue, ''); if (diff.length > 0) { + this._dataAlreadySent = diff; this._coreService.triggerDataEvent(diff, true); } } @@ -194,9 +207,11 @@ export class CompositionHelper { } if (this._bufferService.buffer.isCursorInViewport) { - const cellHeight = Math.ceil(this._charSizeService.height * this._optionsService.options.lineHeight); - const cursorTop = this._bufferService.buffer.y * cellHeight; - const cursorLeft = this._bufferService.buffer.x * this._charSizeService.width; + const cursorX = Math.min(this._bufferService.buffer.x, this._bufferService.cols - 1); + + const cellHeight = this._renderService.dimensions.actualCellHeight; + const cursorTop = this._bufferService.buffer.y * this._renderService.dimensions.actualCellHeight; + const cursorLeft = cursorX * this._renderService.dimensions.actualCellWidth; this._compositionView.style.left = cursorLeft + 'px'; this._compositionView.style.top = cursorTop + 'px'; @@ -209,8 +224,9 @@ export class CompositionHelper { const compositionViewBounds = this._compositionView.getBoundingClientRect(); this._textarea.style.left = cursorLeft + 'px'; this._textarea.style.top = cursorTop + 'px'; - this._textarea.style.width = compositionViewBounds.width + 'px'; - this._textarea.style.height = compositionViewBounds.height + 'px'; + // Ensure the text area is at least 1x1, otherwise certain IMEs may break + this._textarea.style.width = Math.max(compositionViewBounds.width, 1) + 'px'; + this._textarea.style.height = Math.max(compositionViewBounds.height, 1) + 'px'; this._textarea.style.lineHeight = compositionViewBounds.height + 'px'; } diff --git a/src/browser/public/Terminal.ts b/src/browser/public/Terminal.ts index 33aa9024..26cf2728 100644 --- a/src/browser/public/Terminal.ts +++ b/src/browser/public/Terminal.ts @@ -3,21 +3,21 @@ * @license MIT */ -import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings, ITerminalAddon, ISelectionPosition, IBuffer as IBufferApi, IBufferNamespace as IBufferNamespaceApi, IBufferLine as IBufferLineApi, IBufferCell as IBufferCellApi, IParser, IFunctionIdentifier, ILinkProvider, IUnicodeHandling, IUnicodeVersionProvider, FontWeight } from 'xterm'; +import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings, ITerminalAddon, ISelectionPosition, IBufferNamespace as IBufferNamespaceApi, IParser, ILinkProvider, IUnicodeHandling, FontWeight, IModes } from 'xterm'; import { ITerminal } from 'browser/Types'; -import { IBufferLine, ICellData } from 'common/Types'; -import { IBuffer, IBufferSet } from 'common/buffer/Types'; -import { CellData } from 'common/buffer/CellData'; -import { Terminal as TerminalCore } from '../Terminal'; -import * as Strings from '../LocalizableStrings'; -import { IEvent, EventEmitter } from 'common/EventEmitter'; -import { AddonManager } from './AddonManager'; -import { IParams } from 'common/parser/Types'; +import { Terminal as TerminalCore } from 'browser/Terminal'; +import * as Strings from 'browser/LocalizableStrings'; +import { IEvent } from 'common/EventEmitter'; +import { ParserApi } from 'common/public/ParserApi'; +import { UnicodeApi } from 'common/public/UnicodeApi'; +import { AddonManager } from 'common/public/AddonManager'; +import { BufferNamespaceApi } from 'common/public/BufferNamespaceApi'; export class Terminal implements ITerminalApi { private _core: ITerminal; private _addonManager: AddonManager; private _parser: IParser | undefined; + private _buffer: BufferNamespaceApi | undefined; constructor(options?: ITerminalOptions) { this._core = new TerminalCore(options); @@ -30,16 +30,17 @@ export class Terminal implements ITerminalApi { } } - public get onCursorMove(): IEvent { return this._core.onCursorMove; } - public get onLineFeed(): IEvent { return this._core.onLineFeed; } - public get onSelectionChange(): IEvent { return this._core.onSelectionChange; } - public get onData(): IEvent { return this._core.onData; } + public get onBell(): IEvent { return this._core.onBell; } public get onBinary(): IEvent { return this._core.onBinary; } - public get onTitleChange(): IEvent { return this._core.onTitleChange; } - public get onScroll(): IEvent { return this._core.onScroll; } + public get onCursorMove(): IEvent { return this._core.onCursorMove; } + public get onData(): IEvent { return this._core.onData; } public get onKey(): IEvent<{ key: string, domEvent: KeyboardEvent }> { return this._core.onKey; } + public get onLineFeed(): IEvent { return this._core.onLineFeed; } public get onRender(): IEvent<{ start: number, end: number }> { return this._core.onRender; } public get onResize(): IEvent<{ cols: number, rows: number }> { return this._core.onResize; } + public get onScroll(): IEvent { return this._core.onScroll; } + public get onSelectionChange(): IEvent { return this._core.onSelectionChange; } + public get onTitleChange(): IEvent { return this._core.onTitleChange; } public get element(): HTMLElement | undefined { return this._core.element; } public get parser(): IParser { @@ -58,12 +59,36 @@ export class Terminal implements ITerminalApi { public get cols(): number { return this._core.cols; } public get buffer(): IBufferNamespaceApi { this._checkProposedApi(); - return new BufferNamespaceApi(this._core.buffers); + if (!this._buffer) { + this._buffer = new BufferNamespaceApi(this._core); + } + return this._buffer; } public get markers(): ReadonlyArray { this._checkProposedApi(); return this._core.markers; } + public get modes(): IModes { + const m = this._core.coreService.decPrivateModes; + let mouseTrackingMode: 'none' | 'x10' | 'vt200' | 'drag' | 'any' = 'none'; + switch (this._core.coreMouseService.activeProtocol) { + case 'X10': mouseTrackingMode = 'x10'; break; + case 'VT200': mouseTrackingMode = 'vt200'; break; + case 'DRAG': mouseTrackingMode = 'drag'; break; + case 'ANY': mouseTrackingMode = 'any'; break; + } + return { + applicationCursorKeysMode: m.applicationCursorKeys, + applicationKeypadMode: m.applicationKeypad, + bracketedPasteMode: m.bracketedPasteMode, + insertMode: this._core.coreService.modes.insertMode, + mouseTrackingMode: mouseTrackingMode, + originMode: m.origin, + reverseWraparoundMode: m.reverseWraparound, + sendFocusMode: m.sendFocus, + wraparoundMode: m.wraparound + }; + } public blur(): void { this._core.blur(); } @@ -170,7 +195,7 @@ export class Terminal implements ITerminalApi { this._core.paste(data); } public getOption(key: 'bellSound' | 'bellStyle' | 'cursorStyle' | 'fontFamily' | 'logLevel' | 'rendererType' | 'termName' | 'wordSeparator'): string; - public getOption(key: 'allowTransparency' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'disableStdin' | 'macOptionIsMeta' | 'rightClickSelectsWord' | 'popOnBell' | 'visualBell'): boolean; + public getOption(key: 'allowTransparency' | 'altClickMovesCursor' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'disableStdin' | 'macOptionIsMeta' | 'rightClickSelectsWord' | 'popOnBell' | 'visualBell'): boolean; public getOption(key: 'cols' | 'fontSize' | 'letterSpacing' | 'lineHeight' | 'rows' | 'tabStopWidth' | 'scrollback'): number; public getOption(key: 'fontWeight' | 'fontWeightBold'): FontWeight; public getOption(key: string): any; @@ -182,7 +207,7 @@ export class Terminal implements ITerminalApi { public setOption(key: 'logLevel', value: 'debug' | 'info' | 'warn' | 'error' | 'off'): void; public setOption(key: 'bellStyle', value: 'none' | 'visual' | 'sound' | 'both'): void; public setOption(key: 'cursorStyle', value: 'block' | 'underline' | 'bar'): void; - public setOption(key: 'allowTransparency' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'disableStdin' | 'macOptionIsMeta' | 'rightClickSelectsWord' | 'popOnBell' | 'visualBell', value: boolean): void; + public setOption(key: 'allowTransparency' | 'altClickMovesCursor' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'disableStdin' | 'macOptionIsMeta' | 'rightClickSelectsWord' | 'popOnBell' | 'visualBell', value: boolean): void; public setOption(key: 'fontSize' | 'letterSpacing' | 'lineHeight' | 'tabStopWidth' | 'scrollback', value: number): void; public setOption(key: 'theme', value: ITheme): void; public setOption(key: 'cols' | 'rows', value: number): void; @@ -197,6 +222,9 @@ export class Terminal implements ITerminalApi { public reset(): void { this._core.reset(); } + public clearTextureAtlas(): void { + this._core.clearTextureAtlas(); + } public loadAddon(addon: ITerminalAddon): void { return this._addonManager.loadAddon(this, addon); } @@ -212,123 +240,3 @@ export class Terminal implements ITerminalApi { } } } - -class BufferApiView implements IBufferApi { - constructor( - private _buffer: IBuffer, - public readonly type: 'normal' | 'alternate' - ) { } - - public init(buffer: IBuffer): BufferApiView { - this._buffer = buffer; - return this; - } - - public get cursorY(): number { return this._buffer.y; } - public get cursorX(): number { return this._buffer.x; } - public get viewportY(): number { return this._buffer.ydisp; } - public get baseY(): number { return this._buffer.ybase; } - public get length(): number { return this._buffer.lines.length; } - public getLine(y: number): IBufferLineApi | undefined { - const line = this._buffer.lines.get(y); - if (!line) { - return undefined; - } - return new BufferLineApiView(line); - } - public getNullCell(): IBufferCellApi { return new CellData(); } -} - -class BufferNamespaceApi implements IBufferNamespaceApi { - private _normal: BufferApiView; - private _alternate: BufferApiView; - private _onBufferChange = new EventEmitter(); - public get onBufferChange(): IEvent { return this._onBufferChange.event; } - - constructor(private _buffers: IBufferSet) { - this._normal = new BufferApiView(this._buffers.normal, 'normal'); - this._alternate = new BufferApiView(this._buffers.alt, 'alternate'); - this._buffers.onBufferActivate(() => this._onBufferChange.fire(this.active)); - } - public get active(): IBufferApi { - if (this._buffers.active === this._buffers.normal) { return this.normal; } - if (this._buffers.active === this._buffers.alt) { return this.alternate; } - throw new Error('Active buffer is neither normal nor alternate'); - } - public get normal(): IBufferApi { - return this._normal.init(this._buffers.normal); - } - public get alternate(): IBufferApi { - return this._alternate.init(this._buffers.alt); - } -} - -class BufferLineApiView implements IBufferLineApi { - constructor(private _line: IBufferLine) { } - - public get isWrapped(): boolean { return this._line.isWrapped; } - public get length(): number { return this._line.length; } - public getCell(x: number, cell?: IBufferCellApi): IBufferCellApi | undefined { - if (x < 0 || x >= this._line.length) { - return undefined; - } - - if (cell) { - this._line.loadCell(x, cell); - return cell; - } - return this._line.loadCell(x, new CellData()); - } - public translateToString(trimRight?: boolean, startColumn?: number, endColumn?: number): string { - return this._line.translateToString(trimRight, startColumn, endColumn); - } -} - -class ParserApi implements IParser { - constructor(private _core: ITerminal) { } - - public registerCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean): IDisposable { - return this._core.addCsiHandler(id, (params: IParams) => callback(params.toArray())); - } - public addCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean): IDisposable { - return this.registerCsiHandler(id, callback); - } - public registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean): IDisposable { - return this._core.addDcsHandler(id, (data: string, params: IParams) => callback(data, params.toArray())); - } - public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean): IDisposable { - return this.registerDcsHandler(id, callback); - } - public registerEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable { - return this._core.addEscHandler(id, handler); - } - public addEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable { - return this.registerEscHandler(id, handler); - } - public registerOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { - return this._core.addOscHandler(ident, callback); - } - public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { - return this.registerOscHandler(ident, callback); - } -} - -class UnicodeApi implements IUnicodeHandling { - constructor(private _core: ITerminal) { } - - public register(provider: IUnicodeVersionProvider): void { - this._core.unicodeService.register(provider); - } - - public get versions(): string[] { - return this._core.unicodeService.versions; - } - - public get activeVersion(): string { - return this._core.unicodeService.activeVersion; - } - - public set activeVersion(version: string) { - this._core.unicodeService.activeVersion = version; - } -} diff --git a/src/browser/renderer/BaseRenderLayer.ts b/src/browser/renderer/BaseRenderLayer.ts index 8afec352..68f83a75 100644 --- a/src/browser/renderer/BaseRenderLayer.ts +++ b/src/browser/renderer/BaseRenderLayer.ts @@ -17,6 +17,7 @@ import { IBufferService, IOptionsService } from 'common/services/Services'; import { throwIfFalsy } from 'browser/renderer/RendererUtils'; import { channels, color, rgba } from 'browser/Color'; import { removeElementFromParent } from 'browser/Dom'; +import { tryDrawCustomChar } from 'browser/renderer/CustomGlyphs'; export abstract class BaseRenderLayer implements IRenderLayer { private _canvas: HTMLCanvasElement; @@ -66,7 +67,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { } private _initCanvas(): void { - this._ctx = throwIfFalsy(this._canvas.getContext('2d', {alpha: this._alpha})); + this._ctx = throwIfFalsy(this._canvas.getContext('2d', { alpha: this._alpha })); // Draw the background if this is an opaque layer if (!this._alpha) { this._clearAll(); @@ -137,6 +138,10 @@ export abstract class BaseRenderLayer implements IRenderLayer { public abstract reset(): void; + public clearTextureAtlas(): void { + this._charAtlas?.clear(); + } + /** * Fills 1+ cells completely. This uses the existing fillStyle on the context. * @param x The column to start at. @@ -152,6 +157,21 @@ export abstract class BaseRenderLayer implements IRenderLayer { height * this._scaledCellHeight); } + /** + * Fills a 1px line (2px on HDPI) at the middle of the cell. This uses the + * existing fillStyle on the context. + * @param x The column to fill. + * @param y The row to fill. + */ + protected _fillMiddleLineAtCells(x: number, y: number, width: number = 1): void { + const cellOffset = Math.ceil(this._scaledCellHeight * 0.5); + this._ctx.fillRect( + x * this._scaledCellWidth, + (y + 1) * this._scaledCellHeight - cellOffset - window.devicePixelRatio, + width * this._scaledCellWidth, + window.devicePixelRatio); + } + /** * Fills a 1px line (2px on HDPI) at the bottom of the cell. This uses the * existing fillStyle on the context. @@ -242,12 +262,22 @@ export abstract class BaseRenderLayer implements IRenderLayer { */ protected _fillCharTrueColor(cell: CellData, x: number, y: number): void { this._ctx.font = this._getFont(false, false); - this._ctx.textBaseline = 'middle'; + this._ctx.textBaseline = 'ideographic'; this._clipRow(y); - this._ctx.fillText( - cell.getChars(), - x * this._scaledCellWidth + this._scaledCharLeft, - y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight / 2); + + // Draw custom characters if applicable + let drawSuccess = false; + if (this._optionsService.options.customGlyphs !== false) { + drawSuccess = tryDrawCustomChar(this._ctx, cell.getChars(), x * this._scaledCellWidth, y * this._scaledCellHeight, this._scaledCellWidth, this._scaledCellHeight); + } + + // Draw the character + if (!drawSuccess) { + this._ctx.fillText( + cell.getChars(), + x * this._scaledCellWidth + this._scaledCharLeft, + y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight); + } } /** @@ -320,7 +350,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { private _drawUncachedChars(cell: ICellData, x: number, y: number, fgOverride?: IColor): void { this._ctx.save(); this._ctx.font = this._getFont(!!cell.isBold(), !!cell.isItalic()); - this._ctx.textBaseline = 'middle'; + this._ctx.textBaseline = 'ideographic'; if (cell.isInverse()) { if (fgOverride) { @@ -358,14 +388,25 @@ export abstract class BaseRenderLayer implements IRenderLayer { if (cell.isDim()) { this._ctx.globalAlpha = DIM_OPACITY; } + + // Draw custom characters if applicable + let drawSuccess = false; + if (this._optionsService.options.customGlyphs !== false) { + drawSuccess = tryDrawCustomChar(this._ctx, cell.getChars(), x * this._scaledCellWidth, y * this._scaledCellHeight, this._scaledCellWidth, this._scaledCellHeight); + } + // Draw the character - this._ctx.fillText( - cell.getChars(), - x * this._scaledCellWidth + this._scaledCharLeft, - y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight / 2); + if (!drawSuccess) { + this._ctx.fillText( + cell.getChars(), + x * this._scaledCellWidth + this._scaledCharLeft, + y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight); + } + this._ctx.restore(); } + /** * Clips a row to ensure no pixels will be drawn outside the cells in the row. * @param y The row to clip. diff --git a/src/browser/renderer/CursorRenderLayer.ts b/src/browser/renderer/CursorRenderLayer.ts index d358d580..65109a80 100644 --- a/src/browser/renderer/CursorRenderLayer.ts +++ b/src/browser/renderer/CursorRenderLayer.ts @@ -37,10 +37,10 @@ export class CursorRenderLayer extends BaseRenderLayer { colors: IColorSet, rendererId: number, private _onRequestRedraw: IEventEmitter, - bufferService: IBufferService, - optionsService: IOptionsService, - private readonly _coreService: ICoreService, - private readonly _coreBrowserService: ICoreBrowserService + @IBufferService bufferService: IBufferService, + @IOptionsService optionsService: IOptionsService, + @ICoreService private readonly _coreService: ICoreService, + @ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService ) { super(container, 'cursor', zIndex, true, colors, rendererId, bufferService, optionsService); this._state = { @@ -58,6 +58,14 @@ export class CursorRenderLayer extends BaseRenderLayer { // TODO: Consider initial options? Maybe onOptionsChanged should be called at the end of open? } + public dispose(): void { + if (this._cursorBlinkStateManager) { + this._cursorBlinkStateManager.dispose(); + this._cursorBlinkStateManager = undefined; + } + super.dispose(); + } + public resize(dim: IRenderDimensions): void { super.resize(dim); // Resizing the canvas discards the contents of the canvas so clear state @@ -197,7 +205,12 @@ export class CursorRenderLayer extends BaseRenderLayer { private _clearCursor(): void { if (this._state) { - this._clearCells(this._state.x, this._state.y, this._state.width, 1); + // Avoid potential rounding errors when device pixel ratio is less than 1 + if (window.devicePixelRatio < 1) { + this._clearAll(); + } else { + this._clearCells(this._state.x, this._state.y, this._state.width, 1); + } this._state = { x: 0, y: 0, diff --git a/src/browser/renderer/CustomGlyphs.ts b/src/browser/renderer/CustomGlyphs.ts new file mode 100644 index 00000000..77562790 --- /dev/null +++ b/src/browser/renderer/CustomGlyphs.ts @@ -0,0 +1,563 @@ +/** + * Copyright (c) 2021 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { throwIfFalsy } from 'browser/renderer/RendererUtils'; + +interface IBlockVector { + x: number; + y: number; + w: number; + h: number; +} + +export const blockElementDefinitions: { [index: string]: IBlockVector[] | undefined } = { + // Block elements (0x2580-0x2590) + '▀': [{ x: 0, y: 0, w: 8, h: 4 }], // UPPER HALF BLOCK + '▁': [{ x: 0, y: 7, w: 8, h: 1 }], // LOWER ONE EIGHTH BLOCK + '▂': [{ x: 0, y: 6, w: 8, h: 2 }], // LOWER ONE QUARTER BLOCK + '▃': [{ x: 0, y: 5, w: 8, h: 3 }], // LOWER THREE EIGHTHS BLOCK + '▄': [{ x: 0, y: 4, w: 8, h: 4 }], // LOWER HALF BLOCK + '▅': [{ x: 0, y: 3, w: 8, h: 5 }], // LOWER FIVE EIGHTHS BLOCK + '▆': [{ x: 0, y: 2, w: 8, h: 6 }], // LOWER THREE QUARTERS BLOCK + '▇': [{ x: 0, y: 1, w: 8, h: 7 }], // LOWER SEVEN EIGHTHS BLOCK + '█': [{ x: 0, y: 0, w: 8, h: 8 }], // FULL BLOCK + '▉': [{ x: 0, y: 0, w: 7, h: 8 }], // LEFT SEVEN EIGHTHS BLOCK + '▊': [{ x: 0, y: 0, w: 6, h: 8 }], // LEFT THREE QUARTERS BLOCK + '▋': [{ x: 0, y: 0, w: 5, h: 8 }], // LEFT FIVE EIGHTHS BLOCK + '▌': [{ x: 0, y: 0, w: 4, h: 8 }], // LEFT HALF BLOCK + '▍': [{ x: 0, y: 0, w: 3, h: 8 }], // LEFT THREE EIGHTHS BLOCK + '▎': [{ x: 0, y: 0, w: 2, h: 8 }], // LEFT ONE QUARTER BLOCK + '▏': [{ x: 0, y: 0, w: 1, h: 8 }], // LEFT ONE EIGHTH BLOCK + '▐': [{ x: 4, y: 0, w: 4, h: 8 }], // RIGHT HALF BLOCK + + // Block elements (0x2594-0x2595) + '▔': [{ x: 0, y: 0, w: 9, h: 1 }], // UPPER ONE EIGHTH BLOCK + '▕': [{ x: 7, y: 0, w: 1, h: 8 }], // RIGHT ONE EIGHTH BLOCK + + // Terminal graphic characters (0x2596-0x259F) + '▖': [{ x: 0, y: 4, w: 4, h: 4 }], // QUADRANT LOWER LEFT + '▗': [{ x: 4, y: 4, w: 4, h: 4 }], // QUADRANT LOWER RIGHT + '▘': [{ x: 0, y: 0, w: 4, h: 4 }], // QUADRANT UPPER LEFT + '▙': [{ x: 0, y: 0, w: 4, h: 8 }, { x: 0, y: 4, w: 8, h: 4 }], // QUADRANT UPPER LEFT AND LOWER LEFT AND LOWER RIGHT + '▚': [{ x: 0, y: 0, w: 4, h: 4 }, { x: 4, y: 4, w: 4, h: 4 }], // QUADRANT UPPER LEFT AND LOWER RIGHT + '▛': [{ x: 0, y: 0, w: 4, h: 8 }, { x: 0, y: 0, w: 4, h: 8 }], // QUADRANT UPPER LEFT AND UPPER RIGHT AND LOWER LEFT + '▜': [{ x: 0, y: 0, w: 8, h: 4 }, { x: 4, y: 0, w: 4, h: 8 }], // QUADRANT UPPER LEFT AND UPPER RIGHT AND LOWER RIGHT + '▝': [{ x: 4, y: 0, w: 4, h: 4 }], // QUADRANT UPPER RIGHT + '▞': [{ x: 4, y: 0, w: 4, h: 4 }, { x: 0, y: 4, w: 4, h: 4 }], // QUADRANT UPPER RIGHT AND LOWER LEFT + '▟': [{ x: 4, y: 0, w: 4, h: 8 }, { x: 0, y: 4, w: 8, h: 4 }], // QUADRANT UPPER RIGHT AND LOWER LEFT AND LOWER RIGHT + + // VERTICAL ONE EIGHTH BLOCK-2 through VERTICAL ONE EIGHTH BLOCK-7 + '\u{1FB70}': [{ x: 1, y: 0, w: 1, h: 8 }], + '\u{1FB71}': [{ x: 2, y: 0, w: 1, h: 8 }], + '\u{1FB72}': [{ x: 3, y: 0, w: 1, h: 8 }], + '\u{1FB73}': [{ x: 4, y: 0, w: 1, h: 8 }], + '\u{1FB74}': [{ x: 5, y: 0, w: 1, h: 8 }], + '\u{1FB75}': [{ x: 6, y: 0, w: 1, h: 8 }], + + // HORIZONTAL ONE EIGHTH BLOCK-2 through HORIZONTAL ONE EIGHTH BLOCK-7 + '\u{1FB76}': [{ x: 0, y: 1, w: 8, h: 1 }], + '\u{1FB77}': [{ x: 0, y: 2, w: 8, h: 1 }], + '\u{1FB78}': [{ x: 0, y: 3, w: 8, h: 1 }], + '\u{1FB79}': [{ x: 0, y: 4, w: 8, h: 1 }], + '\u{1FB7A}': [{ x: 0, y: 5, w: 8, h: 1 }], + '\u{1FB7B}': [{ x: 0, y: 6, w: 8, h: 1 }], + + // LEFT AND LOWER ONE EIGHTH BLOCK + '\u{1FB7C}': [{ x: 0, y: 0, w: 1, h: 8 }, { x: 0, y: 7, w: 8, h: 1 }], + // LEFT AND UPPER ONE EIGHTH BLOCK + '\u{1FB7D}': [{ x: 0, y: 0, w: 1, h: 8 }, { x: 0, y: 0, w: 8, h: 1 }], + // RIGHT AND UPPER ONE EIGHTH BLOCK + '\u{1FB7E}': [{ x: 7, y: 0, w: 1, h: 8 }, { x: 0, y: 0, w: 8, h: 1 }], + // RIGHT AND LOWER ONE EIGHTH BLOCK + '\u{1FB7F}': [{ x: 7, y: 0, w: 1, h: 8 }, { x: 0, y: 7, w: 8, h: 1 }], + // UPPER AND LOWER ONE EIGHTH BLOCK + '\u{1FB80}': [{ x: 0, y: 0, w: 8, h: 1 }, { x: 0, y: 7, w: 8, h: 1 }], + // HORIZONTAL ONE EIGHTH BLOCK-1358 + '\u{1FB81}': [{ x: 0, y: 0, w: 8, h: 1 }, { x: 0, y: 2, w: 8, h: 1 }, { x: 0, y: 4, w: 8, h: 1 }, { x: 0, y: 7, w: 8, h: 1 }], + + // UPPER ONE QUARTER BLOCK + '\u{1FB82}': [{ x: 0, y: 0, w: 8, h: 2 }], + // UPPER THREE EIGHTHS BLOCK + '\u{1FB83}': [{ x: 0, y: 0, w: 8, h: 3 }], + // UPPER FIVE EIGHTHS BLOCK + '\u{1FB84}': [{ x: 0, y: 0, w: 8, h: 5 }], + // UPPER THREE QUARTERS BLOCK + '\u{1FB85}': [{ x: 0, y: 0, w: 8, h: 6 }], + // UPPER SEVEN EIGHTHS BLOCK + '\u{1FB86}': [{ x: 0, y: 0, w: 8, h: 7 }], + + // RIGHT ONE QUARTER BLOCK + '\u{1FB87}': [{ x: 6, y: 0, w: 2, h: 8 }], + // RIGHT THREE EIGHTHS B0OCK + '\u{1FB88}': [{ x: 5, y: 0, w: 3, h: 8 }], + // RIGHT FIVE EIGHTHS BL0CK + '\u{1FB89}': [{ x: 3, y: 0, w: 5, h: 8 }], + // RIGHT THREE QUARTERS 0LOCK + '\u{1FB8A}': [{ x: 2, y: 0, w: 6, h: 8 }], + // RIGHT SEVEN EIGHTHS B0OCK + '\u{1FB8B}': [{ x: 1, y: 0, w: 7, h: 8 }], + + // CHECKER BOARD FILL + '\u{1FB95}': [ + { x: 0, y: 0, w: 2, h: 2 }, { x: 4, y: 0, w: 2, h: 2 }, + { x: 2, y: 2, w: 2, h: 2 }, { x: 6, y: 2, w: 2, h: 2 }, + { x: 0, y: 4, w: 2, h: 2 }, { x: 4, y: 4, w: 2, h: 2 }, + { x: 2, y: 6, w: 2, h: 2 }, { x: 6, y: 6, w: 2, h: 2 } + ], + // INVERSE CHECKER BOARD FILL + '\u{1FB96}': [ + { x: 2, y: 0, w: 2, h: 2 }, { x: 6, y: 0, w: 2, h: 2 }, + { x: 0, y: 2, w: 2, h: 2 }, { x: 4, y: 2, w: 2, h: 2 }, + { x: 2, y: 4, w: 2, h: 2 }, { x: 6, y: 4, w: 2, h: 2 }, + { x: 0, y: 6, w: 2, h: 2 }, { x: 4, y: 6, w: 2, h: 2 } + ], + // HEAVY HORIZONTAL FILL (upper middle and lower one quarter block) + '\u{1FB97}': [{ x: 0, y: 2, w: 8, h: 2 }, { x: 0, y: 6, w: 8, h: 2 }] +}; + +type PatternDefinition = number[][]; + +/** + * Defines the repeating pattern used by special characters, the pattern is made up of a 2d array of + * pixel values to be filled (1) or not filled (0). + */ +const patternCharacterDefinitions: { [key: string]: PatternDefinition | undefined } = { + // Shade characters (0x2591-0x2593) + '░': [ // LIGHT SHADE (25%) + [1, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 0] + ], + '▒': [ // MEDIUM SHADE (50%) + [1, 0], + [0, 0], + [0, 1], + [0, 0] + ], + '▓': [ // DARK SHADE (75%) + [0, 1], + [1, 1], + [1, 0], + [1, 1] + ] +}; + +const enum Shapes { + /** │ */ TOP_TO_BOTTOM = 'M.5,0 L.5,1', + /** ─ */ LEFT_TO_RIGHT = 'M0,.5 L1,.5', + + /** └ */ TOP_TO_RIGHT = 'M.5,0 L.5,.5 L1,.5', + /** ┘ */ TOP_TO_LEFT = 'M.5,0 L.5,.5 L0,.5', + /** ┐ */ LEFT_TO_BOTTOM = 'M0,.5 L.5,.5 L.5,1', + /** ┌ */ RIGHT_TO_BOTTOM = 'M0.5,1 L.5,.5 L1,.5', + + /** ╵ */ MIDDLE_TO_TOP = 'M.5,.5 L.5,0', + /** ╴ */ MIDDLE_TO_LEFT = 'M.5,.5 L0,.5', + /** ╶ */ MIDDLE_TO_RIGHT = 'M.5,.5 L1,.5', + /** ╷ */ MIDDLE_TO_BOTTOM = 'M.5,.5 L.5,1', + + /** ┴ */ T_TOP = 'M0,.5 L1,.5 M.5,.5 L.5,0', + /** ┤ */ T_LEFT = 'M.5,0 L.5,1 M.5,.5 L0,.5', + /** ├ */ T_RIGHT = 'M.5,0 L.5,1 M.5,.5 L1,.5', + /** ┬ */ T_BOTTOM = 'M0,.5 L1,.5 M.5,.5 L.5,1', + + /** ┼ */ CROSS = 'M0,.5 L1,.5 M.5,0 L.5,1', + + /** ╌ */ TWO_DASHES_HORIZONTAL = 'M.1,.5 L.4,.5 M.6,.5 L.9,.5', // .2 empty, .3 filled + /** ┄ */ THREE_DASHES_HORIZONTAL = 'M.0667,.5 L.2667,.5 M.4,.5 L.6,.5 M.7333,.5 L.9333,.5', // .1333 empty, .2 filled + /** ┉ */ FOUR_DASHES_HORIZONTAL = 'M.05,.5 L.2,.5 M.3,.5 L.45,.5 M.55,.5 L.7,.5 M.8,.5 L.95,.5', // .1 empty, .15 filled + /** ╎ */ TWO_DASHES_VERTICAL = 'M.5,.1 L.5,.4 M.5,.6 L.5,.9', + /** ┆ */ THREE_DASHES_VERTICAL = 'M.5,.0667 L.5,.2667 M.5,.4 L.5,.6 M.5,.7333 L.5,.9333', + /** ┊ */ FOUR_DASHES_VERTICAL = 'M.5,.05 L.5,.2 M.5,.3 L.5,.45 L.5,.55 M.5,.7 L.5,.95', +} + +const enum Style { + NORMAL = 1, + BOLD = 3 +} + +/** + * This contains the definitions of all box drawing characters in the format of SVG paths (ie. the + * svg d attribute). + */ +export const boxDrawingDefinitions: { [character: string]: { [fontWeight: number]: string | ((xp: number, yp: number) => string) } | undefined } = { + // Uniform normal and bold + '─': { [Style.NORMAL]: Shapes.LEFT_TO_RIGHT }, + '━': { [Style.BOLD]: Shapes.LEFT_TO_RIGHT }, + '│': { [Style.NORMAL]: Shapes.TOP_TO_BOTTOM }, + '┃': { [Style.BOLD]: Shapes.TOP_TO_BOTTOM }, + '┌': { [Style.NORMAL]: Shapes.RIGHT_TO_BOTTOM }, + '┏': { [Style.BOLD]: Shapes.RIGHT_TO_BOTTOM }, + '┐': { [Style.NORMAL]: Shapes.LEFT_TO_BOTTOM }, + '┓': { [Style.BOLD]: Shapes.LEFT_TO_BOTTOM }, + '└': { [Style.NORMAL]: Shapes.TOP_TO_RIGHT }, + '┗': { [Style.BOLD]: Shapes.TOP_TO_RIGHT }, + '┘': { [Style.NORMAL]: Shapes.TOP_TO_LEFT }, + '┛': { [Style.BOLD]: Shapes.TOP_TO_LEFT }, + '├': { [Style.NORMAL]: Shapes.T_RIGHT }, + '┣': { [Style.BOLD]: Shapes.T_RIGHT }, + '┤': { [Style.NORMAL]: Shapes.T_LEFT }, + '┫': { [Style.BOLD]: Shapes.T_LEFT }, + '┬': { [Style.NORMAL]: Shapes.T_BOTTOM }, + '┳': { [Style.BOLD]: Shapes.T_BOTTOM }, + '┴': { [Style.NORMAL]: Shapes.T_TOP }, + '┻': { [Style.BOLD]: Shapes.T_TOP }, + '┼': { [Style.NORMAL]: Shapes.CROSS }, + '╋': { [Style.BOLD]: Shapes.CROSS }, + '╴': { [Style.NORMAL]: Shapes.MIDDLE_TO_LEFT }, + '╸': { [Style.BOLD]: Shapes.MIDDLE_TO_LEFT }, + '╵': { [Style.NORMAL]: Shapes.MIDDLE_TO_TOP }, + '╹': { [Style.BOLD]: Shapes.MIDDLE_TO_TOP }, + '╶': { [Style.NORMAL]: Shapes.MIDDLE_TO_RIGHT }, + '╺': { [Style.BOLD]: Shapes.MIDDLE_TO_RIGHT }, + '╷': { [Style.NORMAL]: Shapes.MIDDLE_TO_BOTTOM }, + '╻': { [Style.BOLD]: Shapes.MIDDLE_TO_BOTTOM }, + + // Double border + '═': { [Style.NORMAL]: (xp, yp) => `M0,${.5 - yp} L1,${.5 - yp} M0,${.5 + yp} L1,${.5 + yp}` }, + '║': { [Style.NORMAL]: (xp, yp) => `M${.5 - xp},0 L${.5 - xp},1 M${.5 + xp},0 L${.5 + xp},1` }, + '╒': { [Style.NORMAL]: (xp, yp) => `M.5,1 L.5,${.5 - yp} L1,${.5 - yp} M.5,${.5 + yp} L1,${.5 + yp}` }, + '╓': { [Style.NORMAL]: (xp, yp) => `M${.5 - xp},1 L${.5 - xp},.5 L1,.5 M${.5 + xp},.5 L${.5 + xp},1` }, + '╔': { [Style.NORMAL]: (xp, yp) => `M1,${.5 - yp} L${.5 - xp},${.5 - yp} L${.5 - xp},1 M1,${.5 + yp} L${.5 + xp},${.5 + yp} L${.5 + xp},1` }, + '╕': { [Style.NORMAL]: (xp, yp) => `M0,${.5 - yp} L.5,${.5 - yp} L.5,1 M0,${.5 + yp} L.5,${.5 + yp}` }, + '╖': { [Style.NORMAL]: (xp, yp) => `M${.5 + xp},1 L${.5 + xp},.5 L0,.5 M${.5 - xp},.5 L${.5 - xp},1` }, + '╗': { [Style.NORMAL]: (xp, yp) => `M0,${.5 + yp} L${.5 - xp},${.5 + yp} L${.5 - xp},1 M0,${.5 - yp} L${.5 + xp},${.5 - yp} L${.5 + xp},1` }, + '╘': { [Style.NORMAL]: (xp, yp) => `M.5,0 L.5,${.5 + yp} L1,${.5 + yp} M.5,${.5 - yp} L1,${.5 - yp}` }, + '╙': { [Style.NORMAL]: (xp, yp) => `M1,.5 L${.5 - xp},.5 L${.5 - xp},0 M${.5 + xp},.5 L${.5 + xp},0` }, + '╚': { [Style.NORMAL]: (xp, yp) => `M1,${.5 - yp} L${.5 + xp},${.5 - yp} L${.5 + xp},0 M1,${.5 + yp} L${.5 - xp},${.5 + yp} L${.5 - xp},0` }, + '╛': { [Style.NORMAL]: (xp, yp) => `M0,${.5 + yp} L.5,${.5 + yp} L.5,0 M0,${.5 - yp} L.5,${.5 - yp}` }, + '╜': { [Style.NORMAL]: (xp, yp) => `M0,.5 L${.5 + xp},.5 L${.5 + xp},0 M${.5 - xp},.5 L${.5 - xp},0` }, + '╝': { [Style.NORMAL]: (xp, yp) => `M0,${.5 - yp} L${.5 - xp},${.5 - yp} L${.5 - xp},0 M0,${.5 + yp} L${.5 + xp},${.5 + yp} L${.5 + xp},0` }, + '╞': { [Style.NORMAL]: (xp, yp) => `${Shapes.TOP_TO_BOTTOM} M.5,${.5 - yp} L1,${.5 - yp} M.5,${.5 + yp} L1,${.5 + yp}` }, + '╟': { [Style.NORMAL]: (xp, yp) => `M${.5 - xp},0 L${.5 - xp},1 M${.5 + xp},0 L${.5 + xp},1 M${.5 + xp},.5 L1,.5` }, + '╠': { [Style.NORMAL]: (xp, yp) => `M${.5 - xp},0 L${.5 - xp},1 M1,${.5 + yp} L${.5 + xp},${.5 + yp} L${.5 + xp},1 M1,${.5 - yp} L${.5 + xp},${.5 - yp} L${.5 + xp},0` }, + '╡': { [Style.NORMAL]: (xp, yp) => `${Shapes.TOP_TO_BOTTOM} M0,${.5 - yp} L.5,${.5 - yp} M0,${.5 + yp} L.5,${.5 + yp}` }, + '╢': { [Style.NORMAL]: (xp, yp) => `M0,.5 L${.5 - xp},.5 M${.5 - xp},0 L${.5 - xp},1 M${.5 + xp},0 L${.5 + xp},1` }, + '╣': { [Style.NORMAL]: (xp, yp) => `M${.5 + xp},0 L${.5 + xp},1 M0,${.5 + yp} L${.5 - xp},${.5 + yp} L${.5 - xp},1 M0,${.5 - yp} L${.5 - xp},${.5 - yp} L${.5 - xp},0` }, + '╤': { [Style.NORMAL]: (xp, yp) => `M0,${.5 - yp} L1,${.5 - yp} M0,${.5 + yp} L1,${.5 + yp} M.5,${.5 + yp} L.5,1` }, + '╥': { [Style.NORMAL]: (xp, yp) => `${Shapes.LEFT_TO_RIGHT} M${.5 - xp},.5 L${.5 - xp},1 M${.5 + xp},.5 L${.5 + xp},1` }, + '╦': { [Style.NORMAL]: (xp, yp) => `M0,${.5 - yp} L1,${.5 - yp} M0,${.5 + yp} L${.5 - xp},${.5 + yp} L${.5 - xp},1 M1,${.5 + yp} L${.5 + xp},${.5 + yp} L${.5 + xp},1` }, + '╧': { [Style.NORMAL]: (xp, yp) => `M.5,0 L.5,${.5 - yp} M0,${.5 - yp} L1,${.5 - yp} M0,${.5 + yp} L1,${.5 + yp}` }, + '╨': { [Style.NORMAL]: (xp, yp) => `${Shapes.LEFT_TO_RIGHT} M${.5 - xp},.5 L${.5 - xp},0 M${.5 + xp},.5 L${.5 + xp},0` }, + '╩': { [Style.NORMAL]: (xp, yp) => `M0,${.5 + yp} L1,${.5 + yp} M0,${.5 - yp} L${.5 - xp},${.5 - yp} L${.5 - xp},0 M1,${.5 - yp} L${.5 + xp},${.5 - yp} L${.5 + xp},0` }, + '╪': { [Style.NORMAL]: (xp, yp) => `${Shapes.TOP_TO_BOTTOM} M0,${.5 - yp} L1,${.5 - yp} M0,${.5 + yp} L1,${.5 + yp}` }, + '╫': { [Style.NORMAL]: (xp, yp) => `${Shapes.LEFT_TO_RIGHT} M${.5 - xp},0 L${.5 - xp},1 M${.5 + xp},0 L${.5 + xp},1` }, + '╬': { [Style.NORMAL]: (xp, yp) => `M0,${.5 + yp} L${.5 - xp},${.5 + yp} L${.5 - xp},1 M1,${.5 + yp} L${.5 + xp},${.5 + yp} L${.5 + xp},1 M0,${.5 - yp} L${.5 - xp},${.5 - yp} L${.5 - xp},0 M1,${.5 - yp} L${.5 + xp},${.5 - yp} L${.5 + xp},0` }, + + // Diagonal + '╱': { [Style.NORMAL]: 'M1,0 L0,1' }, + '╲': { [Style.NORMAL]: 'M0,0 L1,1' }, + '╳': { [Style.NORMAL]: 'M1,0 L0,1 M0,0 L1,1' }, + + // Mixed weight + '╼': { [Style.NORMAL]: Shapes.MIDDLE_TO_LEFT, [Style.BOLD]: Shapes.MIDDLE_TO_RIGHT }, + '╽': { [Style.NORMAL]: Shapes.MIDDLE_TO_TOP, [Style.BOLD]: Shapes.MIDDLE_TO_BOTTOM }, + '╾': { [Style.NORMAL]: Shapes.MIDDLE_TO_RIGHT, [Style.BOLD]: Shapes.MIDDLE_TO_LEFT }, + '╿': { [Style.NORMAL]: Shapes.MIDDLE_TO_BOTTOM, [Style.BOLD]: Shapes.MIDDLE_TO_TOP }, + '┍': { [Style.NORMAL]: Shapes.MIDDLE_TO_BOTTOM, [Style.BOLD]: Shapes.MIDDLE_TO_RIGHT }, + '┎': { [Style.NORMAL]: Shapes.MIDDLE_TO_RIGHT, [Style.BOLD]: Shapes.MIDDLE_TO_BOTTOM }, + '┑': { [Style.NORMAL]: Shapes.MIDDLE_TO_BOTTOM, [Style.BOLD]: Shapes.MIDDLE_TO_LEFT }, + '┒': { [Style.NORMAL]: Shapes.MIDDLE_TO_LEFT, [Style.BOLD]: Shapes.MIDDLE_TO_BOTTOM }, + '┕': { [Style.NORMAL]: Shapes.MIDDLE_TO_TOP, [Style.BOLD]: Shapes.MIDDLE_TO_RIGHT }, + '┖': { [Style.NORMAL]: Shapes.MIDDLE_TO_RIGHT, [Style.BOLD]: Shapes.MIDDLE_TO_TOP }, + '┙': { [Style.NORMAL]: Shapes.MIDDLE_TO_TOP, [Style.BOLD]: Shapes.MIDDLE_TO_LEFT }, + '┚': { [Style.NORMAL]: Shapes.MIDDLE_TO_LEFT, [Style.BOLD]: Shapes.MIDDLE_TO_TOP }, + '┝': { [Style.NORMAL]: Shapes.TOP_TO_BOTTOM, [Style.BOLD]: Shapes.MIDDLE_TO_RIGHT }, + '┞': { [Style.NORMAL]: Shapes.RIGHT_TO_BOTTOM, [Style.BOLD]: Shapes.MIDDLE_TO_TOP }, + '┟': { [Style.NORMAL]: Shapes.TOP_TO_RIGHT, [Style.BOLD]: Shapes.MIDDLE_TO_BOTTOM }, + '┠': { [Style.NORMAL]: Shapes.MIDDLE_TO_RIGHT, [Style.BOLD]: Shapes.TOP_TO_BOTTOM }, + '┡': { [Style.NORMAL]: Shapes.MIDDLE_TO_BOTTOM, [Style.BOLD]: Shapes.TOP_TO_RIGHT }, + '┢': { [Style.NORMAL]: Shapes.MIDDLE_TO_TOP, [Style.BOLD]: Shapes.RIGHT_TO_BOTTOM }, + '┥': { [Style.NORMAL]: Shapes.TOP_TO_BOTTOM, [Style.BOLD]: Shapes.MIDDLE_TO_LEFT }, + '┦': { [Style.NORMAL]: Shapes.LEFT_TO_BOTTOM, [Style.BOLD]: Shapes.MIDDLE_TO_TOP }, + '┧': { [Style.NORMAL]: Shapes.TOP_TO_LEFT, [Style.BOLD]: Shapes.MIDDLE_TO_BOTTOM }, + '┨': { [Style.NORMAL]: Shapes.MIDDLE_TO_LEFT, [Style.BOLD]: Shapes.TOP_TO_BOTTOM }, + '┩': { [Style.NORMAL]: Shapes.MIDDLE_TO_BOTTOM, [Style.BOLD]: Shapes.TOP_TO_LEFT }, + '┪': { [Style.NORMAL]: Shapes.MIDDLE_TO_TOP, [Style.BOLD]: Shapes.LEFT_TO_BOTTOM }, + '┭': { [Style.NORMAL]: Shapes.RIGHT_TO_BOTTOM, [Style.BOLD]: Shapes.MIDDLE_TO_LEFT }, + '┮': { [Style.NORMAL]: Shapes.LEFT_TO_BOTTOM, [Style.BOLD]: Shapes.MIDDLE_TO_RIGHT }, + '┯': { [Style.NORMAL]: Shapes.MIDDLE_TO_BOTTOM, [Style.BOLD]: Shapes.LEFT_TO_RIGHT }, + '┰': { [Style.NORMAL]: Shapes.LEFT_TO_RIGHT, [Style.BOLD]: Shapes.MIDDLE_TO_BOTTOM }, + '┱': { [Style.NORMAL]: Shapes.MIDDLE_TO_RIGHT, [Style.BOLD]: Shapes.LEFT_TO_BOTTOM }, + '┲': { [Style.NORMAL]: Shapes.MIDDLE_TO_LEFT, [Style.BOLD]: Shapes.RIGHT_TO_BOTTOM }, + '┵': { [Style.NORMAL]: Shapes.TOP_TO_RIGHT, [Style.BOLD]: Shapes.MIDDLE_TO_LEFT }, + '┶': { [Style.NORMAL]: Shapes.TOP_TO_LEFT, [Style.BOLD]: Shapes.MIDDLE_TO_RIGHT }, + '┷': { [Style.NORMAL]: Shapes.MIDDLE_TO_TOP, [Style.BOLD]: Shapes.LEFT_TO_RIGHT }, + '┸': { [Style.NORMAL]: Shapes.LEFT_TO_RIGHT, [Style.BOLD]: Shapes.MIDDLE_TO_TOP }, + '┹': { [Style.NORMAL]: Shapes.MIDDLE_TO_RIGHT, [Style.BOLD]: Shapes.TOP_TO_LEFT }, + '┺': { [Style.NORMAL]: Shapes.MIDDLE_TO_LEFT, [Style.BOLD]: Shapes.TOP_TO_RIGHT }, + '┽': { [Style.NORMAL]: `${Shapes.TOP_TO_BOTTOM} ${Shapes.MIDDLE_TO_RIGHT}`, [Style.BOLD]: Shapes.MIDDLE_TO_LEFT }, + '┾': { [Style.NORMAL]: `${Shapes.TOP_TO_BOTTOM} ${Shapes.MIDDLE_TO_LEFT}`, [Style.BOLD]: Shapes.MIDDLE_TO_RIGHT }, + '┿': { [Style.NORMAL]: Shapes.TOP_TO_BOTTOM, [Style.BOLD]: Shapes.LEFT_TO_RIGHT }, + '╀': { [Style.NORMAL]: `${Shapes.LEFT_TO_RIGHT} ${Shapes.MIDDLE_TO_BOTTOM}`, [Style.BOLD]: Shapes.MIDDLE_TO_TOP }, + '╁': { [Style.NORMAL]: `${Shapes.MIDDLE_TO_TOP} ${Shapes.LEFT_TO_RIGHT}`, [Style.BOLD]: Shapes.MIDDLE_TO_BOTTOM }, + '╂': { [Style.NORMAL]: Shapes.LEFT_TO_RIGHT, [Style.BOLD]: Shapes.TOP_TO_BOTTOM }, + '╃': { [Style.NORMAL]: Shapes.RIGHT_TO_BOTTOM, [Style.BOLD]: Shapes.TOP_TO_LEFT }, + '╄': { [Style.NORMAL]: Shapes.LEFT_TO_BOTTOM, [Style.BOLD]: Shapes.TOP_TO_RIGHT }, + '╅': { [Style.NORMAL]: Shapes.TOP_TO_RIGHT, [Style.BOLD]: Shapes.LEFT_TO_BOTTOM }, + '╆': { [Style.NORMAL]: Shapes.TOP_TO_LEFT, [Style.BOLD]: Shapes.RIGHT_TO_BOTTOM }, + '╇': { [Style.NORMAL]: Shapes.MIDDLE_TO_BOTTOM, [Style.BOLD]: `${Shapes.MIDDLE_TO_TOP} ${Shapes.LEFT_TO_RIGHT}` }, + '╈': { [Style.NORMAL]: Shapes.MIDDLE_TO_TOP, [Style.BOLD]: `${Shapes.LEFT_TO_RIGHT} ${Shapes.MIDDLE_TO_BOTTOM}` }, + '╉': { [Style.NORMAL]: Shapes.MIDDLE_TO_RIGHT, [Style.BOLD]: `${Shapes.TOP_TO_BOTTOM} ${Shapes.MIDDLE_TO_LEFT}` }, + '╊': { [Style.NORMAL]: Shapes.MIDDLE_TO_LEFT, [Style.BOLD]: `${Shapes.TOP_TO_BOTTOM} ${Shapes.MIDDLE_TO_RIGHT}` }, + + // Dashed + '╌': { [Style.NORMAL]: Shapes.TWO_DASHES_HORIZONTAL }, + '╍': { [Style.BOLD]: Shapes.TWO_DASHES_HORIZONTAL }, + '┄': { [Style.NORMAL]: Shapes.THREE_DASHES_HORIZONTAL }, + '┅': { [Style.BOLD]: Shapes.THREE_DASHES_HORIZONTAL }, + '┈': { [Style.NORMAL]: Shapes.FOUR_DASHES_HORIZONTAL }, + '┉': { [Style.BOLD]: Shapes.FOUR_DASHES_HORIZONTAL }, + '╎': { [Style.NORMAL]: Shapes.TWO_DASHES_VERTICAL }, + '╏': { [Style.BOLD]: Shapes.TWO_DASHES_VERTICAL }, + '┆': { [Style.NORMAL]: Shapes.THREE_DASHES_VERTICAL }, + '┇': { [Style.BOLD]: Shapes.THREE_DASHES_VERTICAL }, + '┊': { [Style.NORMAL]: Shapes.FOUR_DASHES_VERTICAL }, + '┋': { [Style.BOLD]: Shapes.FOUR_DASHES_VERTICAL }, + + // Curved + '╭': { [Style.NORMAL]: 'C.5,1,.5,.5,1,.5' }, + '╮': { [Style.NORMAL]: 'C.5,1,.5,.5,0,.5' }, + '╯': { [Style.NORMAL]: 'C.5,0,.5,.5,0,.5' }, + '╰': { [Style.NORMAL]: 'C.5,0,.5,.5,1,.5' } +}; + +/** + * Try drawing a custom block element or box drawing character, returning whether it was + * successfully drawn. + */ +export function tryDrawCustomChar( + ctx: CanvasRenderingContext2D, + c: string, + xOffset: number, + yOffset: number, + scaledCellWidth: number, + scaledCellHeight: number +): boolean { + const blockElementDefinition = blockElementDefinitions[c]; + if (blockElementDefinition) { + drawBlockElementChar(ctx, blockElementDefinition, xOffset, yOffset, scaledCellWidth, scaledCellHeight); + return true; + } + + const patternDefinition = patternCharacterDefinitions[c]; + if (patternDefinition) { + drawPatternChar(ctx, patternDefinition, xOffset, yOffset, scaledCellWidth, scaledCellHeight); + return true; + } + + const boxDrawingDefinition = boxDrawingDefinitions[c]; + if (boxDrawingDefinition) { + drawBoxDrawingChar(ctx, boxDrawingDefinition, xOffset, yOffset, scaledCellWidth, scaledCellHeight); + return true; + } + + return false; +} + +function drawBlockElementChar( + ctx: CanvasRenderingContext2D, + charDefinition: IBlockVector[], + xOffset: number, + yOffset: number, + scaledCellWidth: number, + scaledCellHeight: number +): void { + for (let i = 0; i < charDefinition.length; i++) { + const box = charDefinition[i]; + const xEighth = scaledCellWidth / 8; + const yEighth = scaledCellHeight / 8; + ctx.fillRect( + xOffset + box.x * xEighth, + yOffset + box.y * yEighth, + box.w * xEighth, + box.h * yEighth + ); + } +} + +const cachedPatterns: Map> = new Map(); + +function drawPatternChar( + ctx: CanvasRenderingContext2D, + charDefinition: number[][], + xOffset: number, + yOffset: number, + scaledCellWidth: number, + scaledCellHeight: number +): void { + let patternSet = cachedPatterns.get(charDefinition); + if (!patternSet) { + patternSet = new Map(); + cachedPatterns.set(charDefinition, patternSet); + } + const fillStyle = ctx.fillStyle; + if (typeof fillStyle !== 'string') { + throw new Error(`Unexpected fillStyle type "${fillStyle}"`); + } + let pattern = patternSet.get(fillStyle); + if (!pattern) { + const width = charDefinition[0].length; + const height = charDefinition.length; + const tmpCanvas = document.createElement('canvas'); + tmpCanvas.width = width; + tmpCanvas.height = height; + const tmpCtx = throwIfFalsy(tmpCanvas.getContext('2d')); + const imageData = new ImageData(width, height); + + // Extract rgba from fillStyle + let r: number; + let g: number; + let b: number; + let a: number; + if (fillStyle.startsWith('#')) { + r = parseInt(fillStyle.substr(1, 2), 16); + g = parseInt(fillStyle.substr(3, 2), 16); + b = parseInt(fillStyle.substr(5, 2), 16); + a = fillStyle.length > 7 && parseInt(fillStyle.substr(7, 2), 16) || 1; + } else if (fillStyle.startsWith('rgba')) { + ([r, g, b, a] = fillStyle.substring(5, fillStyle.length - 1).split(',').map(e => parseFloat(e))); + } else { + throw new Error(`Unexpected fillStyle color format "${fillStyle}" when drawing pattern glyph`); + } + + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + imageData.data[(y * width + x) * 4 ] = r; + imageData.data[(y * width + x) * 4 + 1] = g; + imageData.data[(y * width + x) * 4 + 2] = b; + imageData.data[(y * width + x) * 4 + 3] = charDefinition[y][x] * (a * 255); + } + } + tmpCtx.putImageData(imageData, 0, 0); + pattern = throwIfFalsy(ctx.createPattern(tmpCanvas, null)); + patternSet.set(fillStyle, pattern); + } + ctx.fillStyle = pattern; + ctx.fillRect(xOffset, yOffset, scaledCellWidth, scaledCellHeight); +} + +/** + * Draws the following box drawing characters by mapping a subset of SVG d attribute instructions to + * canvas draw calls. + * + * Box styles: ┎┰┒┍┯┑╓╥╖╒╤╕ ┏┳┓┌┲┓┌┬┐┏┱┐ + * ┌─┬─┐ ┏━┳━┓ ╔═╦═╗ ┠╂┨┝┿┥╟╫╢╞╪╡ ┡╇┩├╊┫┢╈┪┣╉┤ + * │ │ │ ┃ ┃ ┃ ║ ║ ║ ┖┸┚┕┷┙╙╨╜╘╧╛ └┴┘└┺┛┗┻┛┗┹┘ + * ├─┼─┤ ┣━╋━┫ ╠═╬═╣ ┏┱┐┌┲┓┌┬┐┌┬┐ ┏┳┓┌┮┓┌┬┐┏┭┐ + * │ │ │ ┃ ┃ ┃ ║ ║ ║ ┡╃┤├╄┩├╆┪┢╅┤ ┞╀┦├┾┫┟╁┧┣┽┤ + * └─┴─┘ ┗━┻━┛ ╚═╩═╝ └┴┘└┴┘└┺┛┗┹┘ └┴┘└┶┛┗┻┛┗┵┘ + * + * Other: + * ╭─╮ ╲ ╱ ╷╻╎╏┆┇┊┋ ╺╾╴ ╌╌╌ ┄┄┄ ┈┈┈ + * │ │ ╳ ╽╿╎╏┆┇┊┋ ╶╼╸ ╍╍╍ ┅┅┅ ┉┉┉ + * ╰─╯ ╱ ╲ ╹╵╎╏┆┇┊┋ + * + * All box drawing characters: + * ─ ━ │ ┃ ┄ ┅ ┆ ┇ ┈ ┉ ┊ ┋ ┌ ┍ ┎ ┏ + * ┐ ┑ ┒ ┓ └ ┕ ┖ ┗ ┘ ┙ ┚ ┛ ├ ┝ ┞ ┟ + * ┠ ┡ ┢ ┣ ┤ ┥ ┦ ┧ ┨ ┩ ┪ ┫ ┬ ┭ ┮ ┯ + * ┰ ┱ ┲ ┳ ┴ ┵ ┶ ┷ ┸ ┹ ┺ ┻ ┼ ┽ ┾ ┿ + * ╀ ╁ ╂ ╃ ╄ ╅ ╆ ╇ ╈ ╉ ╊ ╋ ╌ ╍ ╎ ╏ + * ═ ║ ╒ ╓ ╔ ╕ ╖ ╗ ╘ ╙ ╚ ╛ ╜ ╝ ╞ ╟ + * ╠ ╡ ╢ ╣ ╤ ╥ ╦ ╧ ╨ ╩ ╪ ╫ ╬ ╭ ╮ ╯ + * ╰ ╱ ╲ ╳ ╴ ╵ ╶ ╷ ╸ ╹ ╺ ╻ ╼ ╽ ╾ ╿ + * + * --- + * + * Box drawing alignment tests: █ + * ▉ + * ╔══╦══╗ ┌──┬──┐ ╭──┬──╮ ╭──┬──╮ ┏━━┳━━┓ ┎┒┏┑ ╷ ╻ ┏┯┓ ┌┰┐ ▊ ╱╲╱╲╳╳╳ + * ║┌─╨─┐║ │╔═╧═╗│ │╒═╪═╕│ │╓─╁─╖│ ┃┌─╂─┐┃ ┗╃╄┙ ╶┼╴╺╋╸┠┼┨ ┝╋┥ ▋ ╲╱╲╱╳╳╳ + * ║│╲ ╱│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╿ │┃ ┍╅╆┓ ╵ ╹ ┗┷┛ └┸┘ ▌ ╱╲╱╲╳╳╳ + * ╠╡ ╳ ╞╣ ├╢ ╟┤ ├┼─┼─┼┤ ├╫─╂─╫┤ ┣┿╾┼╼┿┫ ┕┛┖┚ ┌┄┄┐ ╎ ┏┅┅┓ ┋ ▍ ╲╱╲╱╳╳╳ + * ║│╱ ╲│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╽ │┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▎ + * ║└─╥─┘║ │╚═╤═╝│ │╘═╪═╛│ │╙─╀─╜│ ┃└─╂─┘┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▏ + * ╚══╩══╝ └──┴──┘ ╰──┴──╯ ╰──┴──╯ ┗━━┻━━┛ └╌╌┘ ╎ ┗╍╍┛ ┋ ▁▂▃▄▅▆▇█ + * + * Source: https://www.w3.org/2001/06/utf-8-test/UTF-8-demo.html + */ +function drawBoxDrawingChar( + ctx: CanvasRenderingContext2D, + charDefinition: { [fontWeight: number]: string | ((xp: number, yp: number) => string) }, + xOffset: number, + yOffset: number, + scaledCellWidth: number, + scaledCellHeight: number +): void { + ctx.strokeStyle = ctx.fillStyle; + for (const [fontWeight, instructions] of Object.entries(charDefinition)) { + ctx.beginPath(); + ctx.lineWidth = window.devicePixelRatio * Number.parseInt(fontWeight); + let actualInstructions: string; + if (typeof instructions === 'function') { + const xp = .15; + const yp = .15 / scaledCellHeight * scaledCellWidth; + actualInstructions = instructions(xp, yp); + } else { + actualInstructions = instructions; + } + for (const instruction of actualInstructions.split(' ')) { + const type = instruction[0]; + const f = svgToCanvasInstructionMap[type]; + if (!f) { + console.error(`Could not find drawing instructions for "${type}"`); + continue; + } + const args: string[] = instruction.substring(1).split(','); + if (!args[0] || !args[1]) { + continue; + } + f(ctx, translateArgs(args, scaledCellWidth, scaledCellHeight, xOffset, yOffset)); + } + ctx.stroke(); + ctx.closePath(); + } +} + +function clamp(value: number, max: number, min: number = 0): number { + return Math.max(Math.min(value, max), min); +} + +const svgToCanvasInstructionMap: { [index: string]: any } = { + 'C': (ctx: CanvasRenderingContext2D, args: number[]) => ctx.bezierCurveTo(args[0], args[1], args[2], args[3], args[4], args[5]), + 'L': (ctx: CanvasRenderingContext2D, args: number[]) => ctx.lineTo(args[0], args[1]), + 'M': (ctx: CanvasRenderingContext2D, args: number[]) => ctx.moveTo(args[0], args[1]) +}; + +function translateArgs(args: string[], cellWidth: number, cellHeight: number, xOffset: number, yOffset: number): number[] { + const result = args.map(e => parseFloat(e) || parseInt(e)); + + if (result.length < 2) { + throw new Error('Too few arguments for instruction'); + } + + for (let x = 0; x < result.length; x += 2) { + // Translate from 0-1 to 0-cellWidth + result[x] *= cellWidth; + // Ensure coordinate doesn't escape cell bounds and round to the nearest 0.5 to ensure a crisp + // line at 100% devicePixelRatio + if (result[x] !== 0) { + result[x] = clamp(Math.round(result[x] + 0.5) - 0.5, cellWidth, 0); + } + // Apply the cell's offset (ie. x*cellWidth) + result[x] += xOffset; + } + + for (let y = 1; y < result.length; y += 2) { + // Translate from 0-1 to 0-cellHeight + result[y] *= cellHeight; + // Ensure coordinate doesn't escape cell bounds and round to the nearest 0.5 to ensure a crisp + // line at 100% devicePixelRatio + if (result[y] !== 0) { + result[y] = clamp(Math.round(result[y] + 0.5) - 0.5, cellHeight, 0); + } + // Apply the cell's offset (ie. x*cellHeight) + result[y] += yOffset; + } + + return result; +} diff --git a/src/browser/renderer/LinkRenderLayer.ts b/src/browser/renderer/LinkRenderLayer.ts index c41955d9..2492f921 100644 --- a/src/browser/renderer/LinkRenderLayer.ts +++ b/src/browser/renderer/LinkRenderLayer.ts @@ -20,8 +20,8 @@ export class LinkRenderLayer extends BaseRenderLayer { rendererId: number, linkifier: ILinkifier, linkifier2: ILinkifier2, - bufferService: IBufferService, - optionsService: IOptionsService + @IBufferService bufferService: IBufferService, + @IOptionsService optionsService: IOptionsService ) { super(container, 'link', zIndex, true, colors, rendererId, bufferService, optionsService); linkifier.onShowLinkUnderline(e => this._onShowLinkUnderline(e)); diff --git a/src/browser/renderer/Renderer.ts b/src/browser/renderer/Renderer.ts index b9d02ff8..162a7ed3 100644 --- a/src/browser/renderer/Renderer.ts +++ b/src/browser/renderer/Renderer.ts @@ -6,13 +6,12 @@ import { TextRenderLayer } from 'browser/renderer/TextRenderLayer'; import { SelectionRenderLayer } from 'browser/renderer/SelectionRenderLayer'; import { CursorRenderLayer } from 'browser/renderer/CursorRenderLayer'; -import { IRenderLayer, IRenderer, IRenderDimensions, CharacterJoinerHandler, ICharacterJoinerRegistry, IRequestRedrawEvent } from 'browser/renderer/Types'; +import { IRenderLayer, IRenderer, IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/Types'; import { LinkRenderLayer } from 'browser/renderer/LinkRenderLayer'; -import { CharacterJoinerRegistry } from 'browser/renderer/CharacterJoinerRegistry'; import { Disposable } from 'common/Lifecycle'; import { IColorSet, ILinkifier, ILinkifier2 } from 'browser/Types'; import { ICharSizeService, ICoreBrowserService } from 'browser/services/Services'; -import { IBufferService, IOptionsService, ICoreService } from 'common/services/Services'; +import { IBufferService, IOptionsService, ICoreService, IInstantiationService } from 'common/services/Services'; import { removeTerminalFromCache } from 'browser/renderer/atlas/CharAtlasCache'; import { EventEmitter, IEvent } from 'common/EventEmitter'; @@ -23,7 +22,6 @@ export class Renderer extends Disposable implements IRenderer { private _renderLayers: IRenderLayer[]; private _devicePixelRatio: number; - private _characterJoinerRegistry: ICharacterJoinerRegistry; public dimensions: IRenderDimensions; @@ -35,20 +33,18 @@ export class Renderer extends Disposable implements IRenderer { private readonly _screenElement: HTMLElement, linkifier: ILinkifier, linkifier2: ILinkifier2, + @IInstantiationService instantiationService: IInstantiationService, @IBufferService private readonly _bufferService: IBufferService, @ICharSizeService private readonly _charSizeService: ICharSizeService, - @IOptionsService private readonly _optionsService: IOptionsService, - @ICoreService coreService: ICoreService, - @ICoreBrowserService coreBrowserService: ICoreBrowserService + @IOptionsService private readonly _optionsService: IOptionsService ) { super(); const allowTransparency = this._optionsService.options.allowTransparency; - this._characterJoinerRegistry = new CharacterJoinerRegistry(this._bufferService); this._renderLayers = [ - new TextRenderLayer(this._screenElement, 0, this._colors, this._characterJoinerRegistry, allowTransparency, this._id, this._bufferService, _optionsService), - new SelectionRenderLayer(this._screenElement, 1, this._colors, this._id, this._bufferService, _optionsService), - new LinkRenderLayer(this._screenElement, 2, this._colors, this._id, linkifier, linkifier2, this._bufferService, _optionsService), - new CursorRenderLayer(this._screenElement, 3, this._colors, this._id, this._onRequestRedraw, this._bufferService, _optionsService, coreService, coreBrowserService) + instantiationService.createInstance(TextRenderLayer, this._screenElement, 0, this._colors, allowTransparency, this._id), + instantiationService.createInstance(SelectionRenderLayer, this._screenElement, 1, this._colors, this._id), + instantiationService.createInstance(LinkRenderLayer, this._screenElement, 2, this._colors, this._id, linkifier, linkifier2), + instantiationService.createInstance(CursorRenderLayer, this._screenElement, 3, this._colors, this._id, this._onRequestRedraw) ]; this.dimensions = { scaledCharWidth: 0, @@ -153,6 +149,12 @@ export class Renderer extends Disposable implements IRenderer { } } + public clearTextureAtlas(): void { + for (const layer of this._renderLayers) { + layer.clearTextureAtlas(); + } + } + /** * Recalculates the character and canvas dimensions. */ @@ -210,12 +212,4 @@ export class Renderer extends Disposable implements IRenderer { this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._bufferService.rows; this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._bufferService.cols; } - - public registerCharacterJoiner(handler: CharacterJoinerHandler): number { - return this._characterJoinerRegistry.registerCharacterJoiner(handler); - } - - public deregisterCharacterJoiner(joinerId: number): boolean { - return this._characterJoinerRegistry.deregisterCharacterJoiner(joinerId); - } } diff --git a/src/browser/renderer/SelectionRenderLayer.ts b/src/browser/renderer/SelectionRenderLayer.ts index 80022f01..9054e3ca 100644 --- a/src/browser/renderer/SelectionRenderLayer.ts +++ b/src/browser/renderer/SelectionRenderLayer.ts @@ -23,8 +23,8 @@ export class SelectionRenderLayer extends BaseRenderLayer { zIndex: number, colors: IColorSet, rendererId: number, - bufferService: IBufferService, - optionsService: IOptionsService + @IBufferService bufferService: IBufferService, + @IOptionsService optionsService: IOptionsService ) { super(container, 'selection', zIndex, true, colors, rendererId, bufferService, optionsService); this._clearState(); diff --git a/src/browser/renderer/TextRenderLayer.ts b/src/browser/renderer/TextRenderLayer.ts index 1f35fae1..59fbb7b1 100644 --- a/src/browser/renderer/TextRenderLayer.ts +++ b/src/browser/renderer/TextRenderLayer.ts @@ -3,16 +3,17 @@ * @license MIT */ -import { ICharacterJoinerRegistry, IRenderDimensions } from 'browser/renderer/Types'; +import { IRenderDimensions } from 'browser/renderer/Types'; import { CharData, ICellData } from 'common/Types'; import { GridCache } from 'browser/renderer/GridCache'; import { BaseRenderLayer } from 'browser/renderer/BaseRenderLayer'; import { AttributeData } from 'common/buffer/AttributeData'; import { NULL_CELL_CODE, Content } from 'common/buffer/Constants'; -import { JoinedCellData } from 'browser/renderer/CharacterJoinerRegistry'; import { IColorSet } from 'browser/Types'; import { CellData } from 'common/buffer/CellData'; import { IOptionsService, IBufferService } from 'common/services/Services'; +import { ICharacterJoinerService } from 'browser/services/Services'; +import { JoinedCellData } from 'browser/services/CharacterJoinerService'; /** * This CharData looks like a null character, which will forc a clear and render @@ -26,22 +27,20 @@ export class TextRenderLayer extends BaseRenderLayer { private _characterWidth: number = 0; private _characterFont: string = ''; private _characterOverlapCache: { [key: string]: boolean } = {}; - private _characterJoinerRegistry: ICharacterJoinerRegistry; private _workCell = new CellData(); constructor( container: HTMLElement, zIndex: number, colors: IColorSet, - characterJoinerRegistry: ICharacterJoinerRegistry, alpha: boolean, rendererId: number, - bufferService: IBufferService, - optionsService: IOptionsService + @IBufferService bufferService: IBufferService, + @IOptionsService optionsService: IOptionsService, + @ICharacterJoinerService private readonly _characterJoinerService: ICharacterJoinerService ) { super(container, 'text', zIndex, alpha, colors, rendererId, bufferService, optionsService); this._state = new GridCache(); - this._characterJoinerRegistry = characterJoinerRegistry; } public resize(dim: IRenderDimensions): void { @@ -67,7 +66,6 @@ export class TextRenderLayer extends BaseRenderLayer { private _forEachCell( firstRow: number, lastRow: number, - joinerRegistry: ICharacterJoinerRegistry | null, callback: ( cell: ICellData, x: number, @@ -77,7 +75,7 @@ export class TextRenderLayer extends BaseRenderLayer { for (let y = firstRow; y <= lastRow; y++) { const row = y + this._bufferService.buffer.ydisp; const line = this._bufferService.buffer.lines.get(row); - const joinedRanges = joinerRegistry ? joinerRegistry.getJoinedCharacters(row) : []; + const joinedRanges = this._characterJoinerService.getJoinedCharacters(row); for (let x = 0; x < this._bufferService.cols; x++) { line!.loadCell(x, this._workCell); let cell = this._workCell; @@ -101,7 +99,6 @@ export class TextRenderLayer extends BaseRenderLayer { // We already know the exact start and end column of the joined range, // so we get the string and width representing it directly - cell = new JoinedCellData( this._workCell, line!.translateToString(true, range[0], range[1]), @@ -160,7 +157,7 @@ export class TextRenderLayer extends BaseRenderLayer { ctx.save(); - this._forEachCell(firstRow, lastRow, null, (cell, x, y) => { + this._forEachCell(firstRow, lastRow, (cell, x, y) => { // libvte and xterm both draw the background (but not foreground) of invisible characters, // so we should too. let nextFillStyle = null; // null represents default background color @@ -213,12 +210,12 @@ export class TextRenderLayer extends BaseRenderLayer { } private _drawForeground(firstRow: number, lastRow: number): void { - this._forEachCell(firstRow, lastRow, this._characterJoinerRegistry, (cell, x, y) => { + this._forEachCell(firstRow, lastRow, (cell, x, y) => { if (cell.isInvisible()) { return; } this._drawChars(cell, x, y); - if (cell.isUnderline()) { + if (cell.isUnderline() || cell.isStrikethrough()) { this._ctx.save(); if (cell.isInverse()) { @@ -247,7 +244,12 @@ export class TextRenderLayer extends BaseRenderLayer { } } - this._fillBottomLineAtCells(x, y, cell.getWidth()); + if (cell.isStrikethrough()) { + this._fillMiddleLineAtCells(x, y, cell.getWidth()); + } + if (cell.isUnderline()) { + this._fillBottomLineAtCells(x, y, cell.getWidth()); + } this._ctx.restore(); } }); diff --git a/src/browser/renderer/Types.d.ts b/src/browser/renderer/Types.d.ts index cab14b88..6818a926 100644 --- a/src/browser/renderer/Types.d.ts +++ b/src/browser/renderer/Types.d.ts @@ -7,8 +7,6 @@ import { IDisposable } from 'common/Types'; import { IColorSet } from 'browser/Types'; import { IEvent } from 'common/EventEmitter'; -export type CharacterJoinerHandler = (text: string) => [number, number][]; - export interface IRenderDimensions { scaledCharWidth: number; scaledCharHeight: number; @@ -54,19 +52,7 @@ export interface IRenderer extends IDisposable { onOptionsChanged(): void; clear(): void; renderRows(start: number, end: number): void; - registerCharacterJoiner(handler: CharacterJoinerHandler): number; - deregisterCharacterJoiner(joinerId: number): boolean; -} - -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][]; + clearTextureAtlas?(): void; } export interface IRenderLayer extends IDisposable { @@ -106,16 +92,6 @@ export interface IRenderLayer extends IDisposable { */ onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, 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. */ @@ -125,4 +101,9 @@ export interface IRenderLayer extends IDisposable { * Clear the state of the render layer. */ reset(): void; + + /** + * Clears the texture atlas. + */ + clearTextureAtlas(): void; } diff --git a/src/browser/renderer/atlas/BaseCharAtlas.ts b/src/browser/renderer/atlas/BaseCharAtlas.ts index 4ebaaa47..83c30d2f 100644 --- a/src/browser/renderer/atlas/BaseCharAtlas.ts +++ b/src/browser/renderer/atlas/BaseCharAtlas.ts @@ -28,6 +28,8 @@ export abstract class BaseCharAtlas implements IDisposable { */ private _doWarmUp(): void { } + public clear(): void { } + /** * Called when we start drawing a new frame. * diff --git a/src/browser/renderer/atlas/CharAtlasUtils.ts b/src/browser/renderer/atlas/CharAtlasUtils.ts index 346b35f8..b196b373 100644 --- a/src/browser/renderer/atlas/CharAtlasUtils.ts +++ b/src/browser/renderer/atlas/CharAtlasUtils.ts @@ -10,15 +10,13 @@ import { ITerminalOptions } from 'common/services/Services'; export function generateConfig(scaledCharWidth: number, scaledCharHeight: number, options: ITerminalOptions, colors: IColorSet): ICharAtlasConfig { // null out some fields that don't matter - const clonedColors = { + const clonedColors: IPartialColorSet = { foreground: colors.foreground, background: colors.background, cursor: undefined, cursorAccent: undefined, selection: undefined, - // For the static char atlas, we only use the first 16 colors, but we need all 256 for the - // dynamic character atlas. - ansi: colors.ansi.slice(0, 16) + ansi: colors.ansi }; return { devicePixelRatio: window.devicePixelRatio, diff --git a/src/browser/renderer/atlas/DynamicCharAtlas.ts b/src/browser/renderer/atlas/DynamicCharAtlas.ts index 696fb63c..666324ad 100644 --- a/src/browser/renderer/atlas/DynamicCharAtlas.ts +++ b/src/browser/renderer/atlas/DynamicCharAtlas.ts @@ -91,12 +91,12 @@ export class DynamicCharAtlas extends BaseCharAtlas { // The canvas needs alpha because we use clearColor to convert the background color to alpha. // It might also contain some characters with transparent backgrounds if allowTransparency is // set. - this._cacheCtx = throwIfFalsy(this._cacheCanvas.getContext('2d', {alpha: true})); + this._cacheCtx = throwIfFalsy(this._cacheCanvas.getContext('2d', { alpha: true })); const tmpCanvas = document.createElement('canvas'); tmpCanvas.width = this._config.scaledCharWidth; tmpCanvas.height = this._config.scaledCharHeight; - this._tmpCtx = throwIfFalsy(tmpCanvas.getContext('2d', {alpha: this._config.allowTransparency})); + this._tmpCtx = throwIfFalsy(tmpCanvas.getContext('2d', { alpha: this._config.allowTransparency })); this._width = Math.floor(TEXTURE_WIDTH / this._config.scaledCharWidth); this._height = Math.floor(TEXTURE_HEIGHT / this._config.scaledCharHeight); @@ -119,6 +119,16 @@ export class DynamicCharAtlas extends BaseCharAtlas { this._drawToCacheCount = 0; } + public clear(): void { + if (this._cacheMap.size > 0) { + const capacity = this._width * this._height; + this._cacheMap = new LRUMap(capacity); + this._cacheMap.prealloc(capacity); + } + this._cacheCtx.clearRect(0, 0, TEXTURE_WIDTH, TEXTURE_HEIGHT); + this._tmpCtx.clearRect(0, 0, this._config.scaledCharWidth, this._config.scaledCharHeight); + } + public draw( ctx: CanvasRenderingContext2D, glyph: IGlyphIdentifier, @@ -256,7 +266,7 @@ export class DynamicCharAtlas extends BaseCharAtlas { const fontStyle = glyph.italic ? 'italic' : ''; this._tmpCtx.font = `${fontStyle} ${fontWeight} ${this._config.fontSize * this._config.devicePixelRatio}px ${this._config.fontFamily}`; - this._tmpCtx.textBaseline = 'middle'; + this._tmpCtx.textBaseline = 'ideographic'; this._tmpCtx.fillStyle = this._getForegroundColor(glyph).css; @@ -265,12 +275,11 @@ export class DynamicCharAtlas extends BaseCharAtlas { this._tmpCtx.globalAlpha = DIM_OPACITY; } // Draw the character - this._tmpCtx.fillText(glyph.chars, 0, this._config.scaledCharHeight / 2); - this._tmpCtx.restore(); + this._tmpCtx.fillText(glyph.chars, 0, this._config.scaledCharHeight); // clear the background from the character to avoid issues with drawing over the previous // character if it extends past it's bounds - const imageData = this._tmpCtx.getImageData( + let imageData = this._tmpCtx.getImageData( 0, 0, this._config.scaledCharWidth, this._config.scaledCharHeight ); let isEmpty = false; @@ -278,6 +287,27 @@ export class DynamicCharAtlas extends BaseCharAtlas { isEmpty = clearColor(imageData, backgroundColor); } + // If this charcater is underscore and empty, shift it up until it is visible, try for a maximum + // of 5 pixels. + if (isEmpty && glyph.chars === '_' && !this._config.allowTransparency) { + for (let offset = 1; offset <= 5; offset++) { + // Draw the character + this._tmpCtx.fillText(glyph.chars, 0, this._config.scaledCharHeight - offset); + + // clear the background from the character to avoid issues with drawing over the previous + // character if it extends past it's bounds + imageData = this._tmpCtx.getImageData( + 0, 0, this._config.scaledCharWidth, this._config.scaledCharHeight + ); + isEmpty = clearColor(imageData, backgroundColor); + if (!isEmpty) { + break; + } + } + } + + this._tmpCtx.restore(); + // copy the data from imageData to _cacheCanvas const x = this._toCoordinateX(index); const y = this._toCoordinateY(index); diff --git a/src/browser/renderer/dom/DomRenderer.ts b/src/browser/renderer/dom/DomRenderer.ts index ed2c340b..d08cf987 100644 --- a/src/browser/renderer/dom/DomRenderer.ts +++ b/src/browser/renderer/dom/DomRenderer.ts @@ -3,13 +3,13 @@ * @license MIT */ -import { IRenderer, IRenderDimensions, CharacterJoinerHandler, IRequestRedrawEvent } from 'browser/renderer/Types'; +import { IRenderer, IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/Types'; import { BOLD_CLASS, ITALIC_CLASS, CURSOR_CLASS, CURSOR_STYLE_BLOCK_CLASS, CURSOR_BLINK_CLASS, CURSOR_STYLE_BAR_CLASS, CURSOR_STYLE_UNDERLINE_CLASS, DomRendererRowFactory } from 'browser/renderer/dom/DomRendererRowFactory'; import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants'; import { Disposable } from 'common/Lifecycle'; import { IColorSet, ILinkifierEvent, ILinkifier, ILinkifier2 } from 'browser/Types'; import { ICharSizeService } from 'browser/services/Services'; -import { IOptionsService, IBufferService } from 'common/services/Services'; +import { IOptionsService, IBufferService, IInstantiationService } from 'common/services/Services'; import { EventEmitter, IEvent } from 'common/EventEmitter'; import { color } from 'browser/Color'; import { removeElementFromParent } from 'browser/Dom'; @@ -49,6 +49,7 @@ export class DomRenderer extends Disposable implements IRenderer { private readonly _viewportElement: HTMLElement, private readonly _linkifier: ILinkifier, private readonly _linkifier2: ILinkifier2, + @IInstantiationService instantiationService: IInstantiationService, @ICharSizeService private readonly _charSizeService: ICharSizeService, @IOptionsService private readonly _optionsService: IOptionsService, @IBufferService private readonly _bufferService: IBufferService @@ -80,7 +81,7 @@ export class DomRenderer extends Disposable implements IRenderer { this._updateDimensions(); this._injectCss(); - this._rowFactory = new DomRendererRowFactory(document, this._optionsService, this._colors); + this._rowFactory = instantiationService.createInstance(DomRendererRowFactory, document, this._colors); this._element.classList.add(TERMINAL_CLASS_PREFIX + this._terminalClass); this._screenElement.appendChild(this._rowContainer); @@ -138,7 +139,7 @@ export class DomRenderer extends Disposable implements IRenderer { ` width: ${this.dimensions.actualCellWidth}px` + `}`; - this._dimensionsStyleElement.innerHTML = styles; + this._dimensionsStyleElement.textContent = styles; this._selectionContainer.style.height = this._viewportElement.style.height; this._screenElement.style.width = `${this.dimensions.canvasWidth}px`; @@ -237,7 +238,7 @@ export class DomRenderer extends Disposable implements IRenderer { `${this._terminalSelector} .${FG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { color: ${color.opaque(this._colors.background).css}; }` + `${this._terminalSelector} .${BG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { background-color: ${this._colors.foreground.css}; }`; - this._themeStyleElement.innerHTML = styles; + this._themeStyleElement.textContent = styles; } public onDevicePixelRatioChange(): void { @@ -348,7 +349,7 @@ export class DomRenderer extends Disposable implements IRenderer { public clear(): void { for (const e of this._rowElements) { - e.innerHTML = ''; + e.innerText = ''; } } @@ -359,12 +360,12 @@ export class DomRenderer extends Disposable implements IRenderer { for (let y = start; y <= end; y++) { const rowElement = this._rowElements[y]; - rowElement.innerHTML = ''; + rowElement.innerText = ''; const row = y + this._bufferService.buffer.ydisp; const lineData = this._bufferService.buffer.lines.get(row); const cursorStyle = this._optionsService.options.cursorStyle; - rowElement.appendChild(this._rowFactory.createRow(lineData!, row === cursorAbsoluteY, cursorStyle, cursorX, cursorBlink, this.dimensions.actualCellWidth, this._bufferService.cols)); + rowElement.appendChild(this._rowFactory.createRow(lineData!, row, row === cursorAbsoluteY, cursorStyle, cursorX, cursorBlink, this.dimensions.actualCellWidth, this._bufferService.cols)); } } @@ -372,9 +373,6 @@ export class DomRenderer extends Disposable implements IRenderer { return `.${TERMINAL_CLASS_PREFIX}${this._terminalClass}`; } - public registerCharacterJoiner(handler: CharacterJoinerHandler): number { return -1; } - public deregisterCharacterJoiner(joinerId: number): boolean { return false; } - private _onLinkHover(e: ILinkifierEvent): void { this._setCellUnderline(e.x1, e.x2, e.y1, e.y2, e.cols, true); } @@ -389,7 +387,7 @@ export class DomRenderer extends Disposable implements IRenderer { if (!row) { return; } - const span = row.children[x]; + const span = row.children[x] as HTMLElement; if (span) { span.style.textDecoration = enabled ? 'underline' : 'none'; } diff --git a/src/browser/renderer/dom/DomRendererRowFactory.test.ts b/src/browser/renderer/dom/DomRendererRowFactory.test.ts index b6604b14..2f8d264a 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.test.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.test.ts @@ -12,6 +12,7 @@ import { IBufferLine } from 'common/Types'; import { CellData } from 'common/buffer/CellData'; import { MockOptionsService } from 'common/TestUtils.test'; import { css } from 'browser/Color'; +import { MockCharacterJoinerService } from 'browser/TestUtils.test'; describe('DomRendererRowFactory', () => { let dom: jsdom.JSDOM; @@ -20,7 +21,7 @@ describe('DomRendererRowFactory', () => { beforeEach(() => { dom = new jsdom.JSDOM(''); - rowFactory = new DomRendererRowFactory(dom.window.document, new MockOptionsService({ drawBoldTextInBrightColors: true }), { + rowFactory = new DomRendererRowFactory(dom.window.document, { background: css.toColor('#010101'), foreground: css.toColor('#020202'), ansi: [ @@ -43,13 +44,13 @@ describe('DomRendererRowFactory', () => { css.toColor('#34e2e2'), css.toColor('#eeeeec') ] - } as any); + } as any, new MockCharacterJoinerService(), new MockOptionsService({ drawBoldTextInBrightColors: true })); lineData = createEmptyLineData(2); }); describe('createRow', () => { it('should not create anything for an empty row', () => { - const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); + const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), '' ); @@ -59,7 +60,7 @@ describe('DomRendererRowFactory', () => { lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, '語', 2, '語'.charCodeAt(0)])); // There should be no element for the following "empty" cell lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, '', 0, 0])); - const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); + const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), '' ); @@ -67,7 +68,7 @@ describe('DomRendererRowFactory', () => { it('should add class for cursor and cursor style', () => { for (const style of ['block', 'bar', 'underline']) { - const fragment = rowFactory.createRow(lineData, true, style, 0, false, 5, 20); + const fragment = rowFactory.createRow(lineData, 0, true, style, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), ` ` ); @@ -75,7 +76,7 @@ describe('DomRendererRowFactory', () => { }); it('should add class for cursor blink', () => { - const fragment = rowFactory.createRow(lineData, true, 'block', 0, true, 5, 20); + const fragment = rowFactory.createRow(lineData, 0, true, 'block', 0, true, 5, 20); assert.equal(getFragmentHtml(fragment), ` ` ); @@ -84,7 +85,7 @@ describe('DomRendererRowFactory', () => { it('should not render cells that go beyond the terminal\'s columns', () => { lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)])); lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)])); - const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 1); + const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 1); assert.equal(getFragmentHtml(fragment), 'a' ); @@ -95,7 +96,7 @@ describe('DomRendererRowFactory', () => { const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]); cell.fg = DEFAULT_ATTR_DATA.fg | FgFlags.BOLD; lineData.setCell(0, cell); - const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); + const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), 'a' ); @@ -105,7 +106,7 @@ describe('DomRendererRowFactory', () => { const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]); cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.ITALIC; lineData.setCell(0, cell); - const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); + const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), 'a' ); @@ -115,7 +116,7 @@ describe('DomRendererRowFactory', () => { const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]); cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.DIM; lineData.setCell(0, cell); - const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); + const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), 'a' ); @@ -125,12 +126,22 @@ describe('DomRendererRowFactory', () => { const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]); cell.fg = DEFAULT_ATTR_DATA.fg | FgFlags.UNDERLINE; lineData.setCell(0, cell); - const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); + const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), 'a' ); }); + it('should add class for strikethrough', () => { + const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]); + cell.fg = DEFAULT_ATTR_DATA.fg | FgFlags.STRIKETHROUGH; + lineData.setCell(0, cell); + const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20); + assert.equal(getFragmentHtml(fragment), + 'a' + ); + }); + it('should add classes for 256 foreground colors', () => { const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]); cell.fg |= Attributes.CM_P256; @@ -138,7 +149,7 @@ describe('DomRendererRowFactory', () => { cell.fg &= ~Attributes.PCOLOR_MASK; cell.fg |= i; lineData.setCell(0, cell); - const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); + const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), `a` ); @@ -152,7 +163,7 @@ describe('DomRendererRowFactory', () => { cell.bg &= ~Attributes.PCOLOR_MASK; cell.bg |= i; lineData.setCell(0, cell); - const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); + const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), `a` ); @@ -164,7 +175,7 @@ describe('DomRendererRowFactory', () => { cell.fg |= Attributes.CM_P16 | 2 | FgFlags.INVERSE; cell.bg |= Attributes.CM_P16 | 1; lineData.setCell(0, cell); - const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); + const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), 'a' ); @@ -175,7 +186,7 @@ describe('DomRendererRowFactory', () => { cell.fg |= FgFlags.INVERSE; cell.bg |= Attributes.CM_P16 | 1; lineData.setCell(0, cell); - const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); + const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), 'a' ); @@ -185,7 +196,7 @@ describe('DomRendererRowFactory', () => { const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]); cell.fg |= Attributes.CM_P16 | 1 | FgFlags.INVERSE; lineData.setCell(0, cell); - const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); + const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), 'a' ); @@ -198,7 +209,7 @@ describe('DomRendererRowFactory', () => { cell.fg &= ~Attributes.PCOLOR_MASK; cell.fg |= i; lineData.setCell(0, cell); - const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); + const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), `a` ); @@ -210,7 +221,7 @@ describe('DomRendererRowFactory', () => { cell.fg |= Attributes.CM_RGB | 1 << 16 | 2 << 8 | 3; cell.bg |= Attributes.CM_RGB | 4 << 16 | 5 << 8 | 6; lineData.setCell(0, cell); - const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); + const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), 'a' ); @@ -221,7 +232,7 @@ describe('DomRendererRowFactory', () => { cell.fg |= Attributes.CM_RGB | 1 << 16 | 2 << 8 | 3 | FgFlags.INVERSE; cell.bg |= Attributes.CM_RGB | 4 << 16 | 5 << 8 | 6; lineData.setCell(0, cell); - const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); + const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), 'a' ); diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts index c7c87451..a61ebd73 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.ts @@ -10,11 +10,14 @@ import { CellData } from 'common/buffer/CellData'; import { IOptionsService } from 'common/services/Services'; import { color, rgba } from 'browser/Color'; import { IColorSet, IColor } from 'browser/Types'; +import { ICharacterJoinerService } from 'browser/services/Services'; +import { JoinedCellData } from 'browser/services/CharacterJoinerService'; export const BOLD_CLASS = 'xterm-bold'; export const DIM_CLASS = 'xterm-dim'; export const ITALIC_CLASS = 'xterm-italic'; export const UNDERLINE_CLASS = 'xterm-underline'; +export const STRIKETHROUGH_CLASS = 'xterm-strikethrough'; export const CURSOR_CLASS = 'xterm-cursor'; export const CURSOR_BLINK_CLASS = 'xterm-cursor-blink'; export const CURSOR_STYLE_BLOCK_CLASS = 'xterm-cursor-block'; @@ -26,8 +29,9 @@ export class DomRendererRowFactory { constructor( private readonly _document: Document, - private readonly _optionsService: IOptionsService, - private _colors: IColorSet + private _colors: IColorSet, + @ICharacterJoinerService private readonly _characterJoinerService: ICharacterJoinerService, + @IOptionsService private readonly _optionsService: IOptionsService ) { } @@ -35,9 +39,10 @@ export class DomRendererRowFactory { this._colors = colors; } - public createRow(lineData: IBufferLine, isCursorRow: boolean, cursorStyle: string | undefined, cursorX: number, cursorBlink: boolean, cellWidth: number, cols: number): DocumentFragment { + public createRow(lineData: IBufferLine, row: number, isCursorRow: boolean, cursorStyle: string | undefined, cursorX: number, cursorBlink: boolean, cellWidth: number, cols: number): DocumentFragment { const fragment = this._document.createDocumentFragment(); + const joinedRanges = this._characterJoinerService.getJoinedCharacters(row); // Find the line length first, this prevents the need to output a bunch of // empty cells at the end. This cannot easily be integrated into the main // loop below because of the colCount feature (which can be removed after we @@ -53,18 +58,58 @@ export class DomRendererRowFactory { for (let x = 0; x < lineLength; x++) { lineData.loadCell(x, this._workCell); - const width = this._workCell.getWidth(); + let width = this._workCell.getWidth(); // The character to the left is a wide character, drawing is owned by the char at x-1 if (width === 0) { continue; } + // If true, indicates that the current character(s) to draw were joined. + let isJoined = false; + let lastCharX = x; + + // Process any joined character ranges as needed. Because of how the + // ranges are produced, we know that they are valid for the characters + // and attributes of our input. + let cell = this._workCell; + 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 + cell = new JoinedCellData( + this._workCell, + lineData.translateToString(true, range[0], range[1]), + range[1] - range[0] + ); + + // Skip over the cells occupied by this range in the loop + lastCharX = range[1] - 1; + + // Recalculate width + width = cell.getWidth(); + } + const charElement = this._document.createElement('span'); if (width > 1) { charElement.style.width = `${cellWidth * width}px`; } + if (isJoined) { + // Ligatures in the DOM renderer must use display inline, as they may not show with + // inline-block if they are outside the bounds of the element + charElement.style.display = 'inline'; + + // The DOM renderer colors the background of the cursor but for ligatures all cells are + // joined. The workaround here is to show a cursor around the whole ligature so it shows up, + // the cursor looks the same when on any character of the ligature though + if (cursorX >= x && cursorX <= lastCharX) { + cursorX = x; + } + } + if (isCursorRow && x === cursorX) { charElement.classList.add(CURSOR_CLASS); @@ -85,33 +130,37 @@ export class DomRendererRowFactory { } } - if (this._workCell.isBold()) { + if (cell.isBold()) { charElement.classList.add(BOLD_CLASS); } - if (this._workCell.isItalic()) { + if (cell.isItalic()) { charElement.classList.add(ITALIC_CLASS); } - if (this._workCell.isDim()) { + if (cell.isDim()) { charElement.classList.add(DIM_CLASS); } - if (this._workCell.isUnderline()) { + if (cell.isUnderline()) { charElement.classList.add(UNDERLINE_CLASS); } - if (this._workCell.isInvisible()) { + if (cell.isInvisible()) { charElement.textContent = WHITESPACE_CELL_CHAR; } else { - charElement.textContent = this._workCell.getChars() || WHITESPACE_CELL_CHAR; + charElement.textContent = cell.getChars() || WHITESPACE_CELL_CHAR; } - let fg = this._workCell.getFgColor(); - let fgColorMode = this._workCell.getFgColorMode(); - let bg = this._workCell.getBgColor(); - let bgColorMode = this._workCell.getBgColorMode(); - const isInverse = !!this._workCell.isInverse(); + if (cell.isStrikethrough()) { + charElement.classList.add(STRIKETHROUGH_CLASS); + } + + let fg = cell.getFgColor(); + let fgColorMode = cell.getFgColorMode(); + let bg = cell.getBgColor(); + let bgColorMode = cell.getBgColorMode(); + const isInverse = !!cell.isInverse(); if (isInverse) { const temp = fg; fg = bg; @@ -125,7 +174,7 @@ export class DomRendererRowFactory { switch (fgColorMode) { case Attributes.CM_P16: case Attributes.CM_P256: - if (this._workCell.isBold() && fg < 8 && this._optionsService.options.drawBoldTextInBrightColors) { + if (cell.isBold() && fg < 8 && this._optionsService.options.drawBoldTextInBrightColors) { fg += 8; } if (!this._applyMinimumContrast(charElement, this._colors.background, this._colors.ansi[fg])) { @@ -168,6 +217,8 @@ export class DomRendererRowFactory { } fragment.appendChild(charElement); + + x = lastCharX; } return fragment; } diff --git a/src/browser/renderer/CharacterJoinerRegistry.test.ts b/src/browser/services/CharacterJoinerService.test.ts similarity index 59% rename from src/browser/renderer/CharacterJoinerRegistry.test.ts rename to src/browser/services/CharacterJoinerService.test.ts index bca12d6b..6b5326d9 100644 --- a/src/browser/renderer/CharacterJoinerRegistry.test.ts +++ b/src/browser/services/CharacterJoinerService.test.ts @@ -4,15 +4,15 @@ */ import { assert } from 'chai'; -import { ICharacterJoinerRegistry } from 'browser/renderer/Types'; -import { CharacterJoinerRegistry } from 'browser/renderer/CharacterJoinerRegistry'; +import { ICharacterJoinerService } from 'browser/services/Services'; +import { CharacterJoinerService } from 'browser/services/CharacterJoinerService'; import { BufferLine } from 'common/buffer/BufferLine'; import { IBufferLine } from 'common/Types'; import { CellData } from 'common/buffer/CellData'; import { MockBufferService } from 'common/TestUtils.test'; -describe('CharacterJoinerRegistry', () => { - let registry: ICharacterJoinerRegistry; +describe('CharacterJoinerService', () => { + let service: ICharacterJoinerService; beforeEach(() => { const bufferService = new MockBufferService(16, 10); @@ -39,225 +39,225 @@ describe('CharacterJoinerRegistry', () => { for (let i = 0; i < sub.length; ++i) line6.setCell(i + oldSize, sub.loadCell(i, new CellData())); lines.set(6, line6); - registry = new CharacterJoinerRegistry(bufferService); + service = new CharacterJoinerService(bufferService); }); it('has no joiners upon creation', () => { - assert.deepEqual(registry.getJoinedCharacters(0), []); + assert.deepEqual(service.getJoinedCharacters(0), []); }); it('returns ranges matched by the registered joiners', () => { - registry.registerCharacterJoiner(substringJoiner('->')); + service.register(substringJoiner('->')); assert.deepEqual( - registry.getJoinedCharacters(0), + service.getJoinedCharacters(0), [[2, 4], [7, 9], [12, 14]] ); }); it('processes the input using all provided joiners', () => { - registry.registerCharacterJoiner(substringJoiner('->')); + service.register(substringJoiner('->')); assert.deepEqual( - registry.getJoinedCharacters(1), + service.getJoinedCharacters(1), [[2, 4], [12, 14]] ); - registry.registerCharacterJoiner(substringJoiner('=>')); + service.register(substringJoiner('=>')); assert.deepEqual( - registry.getJoinedCharacters(1), + service.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('=>')); + const joiner1 = service.register(substringJoiner('->')); + const joiner2 = service.register(substringJoiner('=>')); assert.deepEqual( - registry.getJoinedCharacters(1), + service.getJoinedCharacters(1), [[2, 4], [7, 9], [12, 14]] ); - registry.deregisterCharacterJoiner(joiner1); + service.deregister(joiner1); assert.deepEqual( - registry.getJoinedCharacters(1), + service.getJoinedCharacters(1), [[7, 9]] ); - registry.deregisterCharacterJoiner(joiner2); + service.deregister(joiner2); assert.deepEqual( - registry.getJoinedCharacters(1), + service.getJoinedCharacters(1), [] ); }); it('doesn\'t process joins on differently-styled characters', () => { - registry.registerCharacterJoiner(substringJoiner('->')); + service.register(substringJoiner('->')); assert.deepEqual( - registry.getJoinedCharacters(2), + service.getJoinedCharacters(2), [[2, 4], [12, 14]] ); }); it('returns an empty list of ranges if there is nothing to be joined', () => { - registry.registerCharacterJoiner(substringJoiner('->')); + service.register(substringJoiner('->')); assert.deepEqual( - registry.getJoinedCharacters(3), + service.getJoinedCharacters(3), [] ); }); it('returns an empty list of ranges if the line is empty', () => { - registry.registerCharacterJoiner(substringJoiner('->')); + service.register(substringJoiner('->')); assert.deepEqual( - registry.getJoinedCharacters(4), + service.getJoinedCharacters(4), [] ); }); it('returns false when trying to deregister a joiner that does not exist', () => { - registry.registerCharacterJoiner(substringJoiner('->')); - assert.deepEqual(registry.deregisterCharacterJoiner(123), false); + service.register(substringJoiner('->')); + assert.deepEqual(service.deregister(123), false); assert.deepEqual( - registry.getJoinedCharacters(0), + service.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')); + service.register(substringJoiner('a')); + service.register(substringJoiner('b')); + service.register(substringJoiner('d')); assert.deepEqual( - registry.getJoinedCharacters(5), + service.getJoinedCharacters(5), [[5, 6]] ); }); it('handles ranges that extend all the way to the end of the line', () => { - registry.registerCharacterJoiner(substringJoiner('-> d')); + service.register(substringJoiner('-> d')); assert.deepEqual( - registry.getJoinedCharacters(2), + service.getJoinedCharacters(2), [[12, 16]] ); }); it('handles adjacent ranges', () => { - registry.registerCharacterJoiner(substringJoiner('->')); - registry.registerCharacterJoiner(substringJoiner('> c ')); + service.register(substringJoiner('->')); + service.register(substringJoiner('> c ')); assert.deepEqual( - registry.getJoinedCharacters(2), + service.getJoinedCharacters(2), [[2, 4], [8, 12], [12, 14]] ); }); it('handles fullwidth characters in the middle of ranges', () => { - registry.registerCharacterJoiner(substringJoiner('wi¥de')); + service.register(substringJoiner('wi¥de')); assert.deepEqual( - registry.getJoinedCharacters(6), + service.getJoinedCharacters(6), [[0, 6]] ); }); it('handles fullwidth characters at the end of ranges', () => { - registry.registerCharacterJoiner(substringJoiner('wi¥')); + service.register(substringJoiner('wi¥')); assert.deepEqual( - registry.getJoinedCharacters(6), + service.getJoinedCharacters(6), [[0, 4]] ); }); it('handles emojis in the middle of ranges', () => { - registry.registerCharacterJoiner(substringJoiner('emo\xf0\x9f\x98\x81 ji')); + service.register(substringJoiner('emo\xf0\x9f\x98\x81 ji')); assert.deepEqual( - registry.getJoinedCharacters(6), + service.getJoinedCharacters(6), [[6, 13]] ); }); it('handles emojis at the end of ranges', () => { - registry.registerCharacterJoiner(substringJoiner('emo\xf0\x9f\x98\x81 ')); + service.register(substringJoiner('emo\xf0\x9f\x98\x81 ')); assert.deepEqual( - registry.getJoinedCharacters(6), + service.getJoinedCharacters(6), [[6, 11]] ); }); it('handles ranges after wide and emoji characters', () => { - registry.registerCharacterJoiner(substringJoiner('abc')); + service.register(substringJoiner('abc')); assert.deepEqual( - registry.getJoinedCharacters(6), + service.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]]); + service.register(() => [[1, 2], [2, 3]]); + service.register(() => [[0, 1]]); assert.deepEqual( - registry.getJoinedCharacters(0), + service.getJoinedCharacters(0), [[0, 1], [1, 2], [2, 3]] ); }); it('inserts in between two ranges', () => { - registry.registerCharacterJoiner(() => [[0, 2], [4, 6]]); - registry.registerCharacterJoiner(() => [[2, 4]]); + service.register(() => [[0, 2], [4, 6]]); + service.register(() => [[2, 4]]); assert.deepEqual( - registry.getJoinedCharacters(0), + service.getJoinedCharacters(0), [[0, 2], [2, 4], [4, 6]] ); }); it('inserts after the last range', () => { - registry.registerCharacterJoiner(() => [[0, 2], [4, 6]]); - registry.registerCharacterJoiner(() => [[6, 8]]); + service.register(() => [[0, 2], [4, 6]]); + service.register(() => [[6, 8]]); assert.deepEqual( - registry.getJoinedCharacters(0), + service.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]]); + service.register(() => [[0, 2], [4, 6]]); + service.register(() => [[3, 5]]); assert.deepEqual( - registry.getJoinedCharacters(0), + service.getJoinedCharacters(0), [[0, 2], [3, 6]] ); }); it('extends the end of a range', () => { - registry.registerCharacterJoiner(() => [[0, 2], [4, 6]]); - registry.registerCharacterJoiner(() => [[1, 4]]); + service.register(() => [[0, 2], [4, 6]]); + service.register(() => [[1, 4]]); assert.deepEqual( - registry.getJoinedCharacters(0), + service.getJoinedCharacters(0), [[0, 4], [4, 6]] ); }); it('extends the last range', () => { - registry.registerCharacterJoiner(() => [[0, 2], [4, 6]]); - registry.registerCharacterJoiner(() => [[5, 7]]); + service.register(() => [[0, 2], [4, 6]]); + service.register(() => [[5, 7]]); assert.deepEqual( - registry.getJoinedCharacters(0), + service.getJoinedCharacters(0), [[0, 2], [4, 7]] ); }); it('connects two ranges', () => { - registry.registerCharacterJoiner(() => [[0, 2], [4, 6]]); - registry.registerCharacterJoiner(() => [[1, 5]]); + service.register(() => [[0, 2], [4, 6]]); + service.register(() => [[1, 5]]); assert.deepEqual( - registry.getJoinedCharacters(0), + service.getJoinedCharacters(0), [[0, 6]] ); }); it('connects more than two ranges', () => { - registry.registerCharacterJoiner(() => [[0, 2], [4, 6], [8, 10], [12, 14]]); - registry.registerCharacterJoiner(() => [[1, 10]]); + service.register(() => [[0, 2], [4, 6], [8, 10], [12, 14]]); + service.register(() => [[1, 10]]); assert.deepEqual( - registry.getJoinedCharacters(0), + service.getJoinedCharacters(0), [[0, 10], [12, 14]] ); }); @@ -270,7 +270,7 @@ function lineData(data: IPartialLineData[]): IBufferLine { const tline = new BufferLine(0); for (let i = 0; i < data.length; ++i) { const line = data[i][0]; - const attr = (data[i][1] || 0); + const attr = (data[i][1] || 0) as number; const offset = tline.length; tline.resize(tline.length + line.split('').length, CellData.fromCharData([0, '', 0, 0])); line.split('').map((char, idx) => tline.setCell(idx + offset, CellData.fromCharData([attr, char, 1, char.charCodeAt(0)]))); diff --git a/src/browser/renderer/CharacterJoinerRegistry.ts b/src/browser/services/CharacterJoinerService.ts similarity index 95% rename from src/browser/renderer/CharacterJoinerRegistry.ts rename to src/browser/services/CharacterJoinerService.ts index 5385b76b..ea65c29b 100644 --- a/src/browser/renderer/CharacterJoinerRegistry.ts +++ b/src/browser/services/CharacterJoinerService.ts @@ -4,11 +4,12 @@ */ import { IBufferLine, ICellData, CharData } from 'common/Types'; -import { ICharacterJoinerRegistry, ICharacterJoiner } from 'browser/renderer/Types'; +import { ICharacterJoiner } from 'browser/Types'; import { AttributeData } from 'common/buffer/AttributeData'; import { WHITESPACE_CELL_CHAR, Content } from 'common/buffer/Constants'; import { CellData } from 'common/buffer/CellData'; import { IBufferService } from 'common/services/Services'; +import { ICharacterJoinerService } from 'browser/services/Services'; export class JoinedCellData extends AttributeData implements ICellData { private _width: number; @@ -55,15 +56,18 @@ export class JoinedCellData extends AttributeData implements ICellData { } } -export class CharacterJoinerRegistry implements ICharacterJoinerRegistry { +export class CharacterJoinerService implements ICharacterJoinerService { + public serviceBrand: undefined; private _characterJoiners: ICharacterJoiner[] = []; private _nextCharacterJoinerId: number = 0; private _workCell: CellData = new CellData(); - constructor(private _bufferService: IBufferService) { } + constructor( + @IBufferService private _bufferService: IBufferService + ) { } - public registerCharacterJoiner(handler: (text: string) => [number, number][]): number { + public register(handler: (text: string) => [number, number][]): number { const joiner: ICharacterJoiner = { id: this._nextCharacterJoinerId++, handler @@ -73,7 +77,7 @@ export class CharacterJoinerRegistry implements ICharacterJoinerRegistry { return joiner.id; } - public deregisterCharacterJoiner(joinerId: number): boolean { + public deregister(joinerId: number): boolean { for (let i = 0; i < this._characterJoiners.length; i++) { if (this._characterJoiners[i].id === joinerId) { this._characterJoiners.splice(i, 1); @@ -177,7 +181,7 @@ export class CharacterJoinerRegistry implements ICharacterJoinerRegistry { // 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]); + CharacterJoinerService._mergeRanges(joinedRanges, joinerRanges[j]); } } this._stringRangesToCellRanges(joinedRanges, lineData, startCol); diff --git a/src/browser/services/RenderService.ts b/src/browser/services/RenderService.ts index 174a1272..b8283e0e 100644 --- a/src/browser/services/RenderService.ts +++ b/src/browser/services/RenderService.ts @@ -3,13 +3,13 @@ * @license MIT */ -import { IRenderer, IRenderDimensions, CharacterJoinerHandler } from 'browser/renderer/Types'; +import { IRenderer, IRenderDimensions } from 'browser/renderer/Types'; import { RenderDebouncer } from 'browser/RenderDebouncer'; import { EventEmitter, IEvent } from 'common/EventEmitter'; import { Disposable } from 'common/Lifecycle'; import { ScreenDprMonitor } from 'browser/ScreenDprMonitor'; import { addDisposableDomListener } from 'browser/Lifecycle'; -import { IColorSet } from 'browser/Types'; +import { IColorSet, IRenderDebouncer } from 'browser/Types'; import { IOptionsService, IBufferService } from 'common/services/Services'; import { ICharSizeService, IRenderService } from 'browser/services/Services'; @@ -22,7 +22,7 @@ interface ISelectionState { export class RenderService extends Disposable implements IRenderService { public serviceBrand: undefined; - private _renderDebouncer: RenderDebouncer; + private _renderDebouncer: IRenderDebouncer; private _screenDprMonitor: ScreenDprMonitor; private _isPaused: boolean = false; @@ -168,12 +168,21 @@ export class RenderService extends Disposable implements IRenderService { } } + public clearTextureAtlas(): void { + this._renderer?.clearTextureAtlas?.(); + this._fullRefresh(); + } + public setColors(colors: IColorSet): void { this._renderer.setColors(colors); this._fullRefresh(); } public onDevicePixelRatioChange(): void { + // Force char size measurement as DomMeasureStrategy(getBoundingClientRect) is not stable + // when devicePixelRatio changes + this._charSizeService.measure(); + this._renderer.onDevicePixelRatioChange(); this.refreshRows(0, this._rowCount - 1); } @@ -210,12 +219,4 @@ export class RenderService extends Disposable implements IRenderService { public clear(): void { this._renderer.clear(); } - - public registerCharacterJoiner(handler: CharacterJoinerHandler): number { - return this._renderer.registerCharacterJoiner(handler); - } - - public deregisterCharacterJoiner(joinerId: number): boolean { - return this._renderer.deregisterCharacterJoiner(joinerId); - } } diff --git a/src/browser/services/SelectionService.test.ts b/src/browser/services/SelectionService.test.ts index eb90ba44..514d5803 100644 --- a/src/browser/services/SelectionService.test.ts +++ b/src/browser/services/SelectionService.test.ts @@ -21,7 +21,7 @@ class TestSelectionService extends SelectionService { optionsService: IOptionsService, renderService: IRenderService ) { - super(null!, null!, bufferService, new MockCoreService(), new MockMouseService(), optionsService, renderService); + super(null!, null!, null!, bufferService, new MockCoreService(), new MockMouseService(), optionsService, renderService); } public get model(): SelectionModel { return this._model; } diff --git a/src/browser/services/SelectionService.ts b/src/browser/services/SelectionService.ts index 81e72087..c0547755 100644 --- a/src/browser/services/SelectionService.ts +++ b/src/browser/services/SelectionService.ts @@ -10,11 +10,13 @@ import * as Browser from 'common/Platform'; import { SelectionModel } from 'browser/selection/SelectionModel'; import { CellData } from 'common/buffer/CellData'; import { EventEmitter, IEvent } from 'common/EventEmitter'; -import { ICharSizeService, IMouseService, ISelectionService, IRenderService } from 'browser/services/Services'; +import { IMouseService, ISelectionService, IRenderService } from 'browser/services/Services'; +import { ILinkifier2 } from 'browser/Types'; import { IBufferService, IOptionsService, ICoreService } from 'common/services/Services'; import { getCoordsRelativeToElement } from 'browser/input/Mouse'; import { moveToCellSequence } from 'browser/input/MoveToCell'; import { Disposable } from 'common/Lifecycle'; +import { getRangeLength } from 'common/buffer/BufferRange'; /** * The number of pixels the mouse needs to be above or below the viewport in @@ -105,6 +107,9 @@ export class SelectionService extends Disposable implements ISelectionService { private _workCell: CellData = new CellData(); private _mouseDownTimeStamp: number = 0; + private _oldHasSelection: boolean = false; + private _oldSelectionStart: [number, number] | undefined = undefined; + private _oldSelectionEnd: [number, number] | undefined = undefined; private _onLinuxMouseSelection = this.register(new EventEmitter()); public get onLinuxMouseSelection(): IEvent { return this._onLinuxMouseSelection.event; } @@ -118,6 +123,7 @@ export class SelectionService extends Disposable implements ISelectionService { constructor( private readonly _element: HTMLElement, private readonly _screenElement: HTMLElement, + private readonly _linkifier: ILinkifier2, @IBufferService private readonly _bufferService: IBufferService, @ICoreService private readonly _coreService: ICoreService, @IMouseService private readonly _mouseService: IMouseService, @@ -127,8 +133,8 @@ export class SelectionService extends Disposable implements ISelectionService { super(); // Init listeners - this._mouseMoveListener = event => this._onMouseMove(event); - this._mouseUpListener = event => this._onMouseUp(event); + this._mouseMoveListener = event => this._onMouseMove(event as MouseEvent); + this._mouseUpListener = event => this._onMouseUp(event as MouseEvent); this._coreService.onUserInput(() => { if (this.hasSelection) { this.clearSelection(); @@ -290,7 +296,7 @@ export class SelectionService extends Disposable implements ISelectionService { * Checks if the current click was inside the current selection * @param event The mouse event */ - public isClickInSelection(event: MouseEvent): boolean { + private _isClickInSelection(event: MouseEvent): boolean { const coords = this._getMouseBufferCoords(event); const start = this._model.finalSelectionStart; const end = this._model.finalSelectionEnd; @@ -313,13 +319,23 @@ export class SelectionService extends Disposable implements ISelectionService { * Selects word at the current mouse event coordinates. * @param event The mouse event. */ - public selectWordAtCursor(event: MouseEvent): void { + private _selectWordAtCursor(event: MouseEvent, allowWhitespaceOnlySelection: boolean): boolean { + // Check if there is a link under the cursor first and select that if so + const range = this._linkifier.currentLink?.link?.range; + if (range) { + this._model.selectionStart = [range.start.x - 1, range.start.y - 1]; + this._model.selectionStartLength = getRangeLength(range, this._bufferService.cols); + this._model.selectionEnd = undefined; + return true; + } + const coords = this._getMouseBufferCoords(event); if (coords) { - this._selectWordAt(coords, false); + this._selectWordAt(coords, allowWhitespaceOnlySelection); this._model.selectionEnd = undefined; - this.refresh(true); + return true; } + return false; } /** @@ -524,14 +540,12 @@ export class SelectionService extends Disposable implements ISelectionService { } /** - * Performs a double click, selecting the current work. + * Performs a double click, selecting the current word. * @param event The mouse event. */ private _onDoubleClick(event: MouseEvent): void { - const coords = this._getMouseBufferCoords(event); - if (coords) { + if (this._selectWordAtCursor(event, true)) { this._activeSelectionMode = SelectionMode.WORD; - this._selectWordAt(coords, true); } } @@ -667,7 +681,7 @@ export class SelectionService extends Disposable implements ISelectionService { this._removeMouseDownListeners(); - if (this.selectionText.length <= 1 && timeElapsed < ALT_CLICK_MOVE_CURSOR_TIME && event.altKey) { + if (this.selectionText.length <= 1 && timeElapsed < ALT_CLICK_MOVE_CURSOR_TIME && event.altKey && this._optionsService.getOption('altClickMovesCursor')) { if (this._bufferService.buffer.ybase === this._bufferService.buffer.ydisp) { const coordinates = this._mouseService.getCoords( event, @@ -681,11 +695,43 @@ export class SelectionService extends Disposable implements ISelectionService { this._coreService.triggerDataEvent(sequence, true); } } - } else if (this.hasSelection) { - this._onSelectionChange.fire(); + } else { + this._fireEventIfSelectionChanged(); } } + private _fireEventIfSelectionChanged(): void { + const start = this._model.finalSelectionStart; + const end = this._model.finalSelectionEnd; + const hasSelection = !!start && !!end && (start[0] !== end[0] || start[1] !== end[1]); + + if (!hasSelection) { + if (this._oldHasSelection) { + this._fireOnSelectionChange(start, end, hasSelection); + } + return; + } + + // Sanity check, these should not be undefined as there is a selection + if (!start || !end) { + return; + } + + if (!this._oldSelectionStart || !this._oldSelectionEnd || ( + start[0] !== this._oldSelectionStart[0] || start[1] !== this._oldSelectionStart[1] || + end[0] !== this._oldSelectionEnd[0] || end[1] !== this._oldSelectionEnd[1])) { + + this._fireOnSelectionChange(start, end, hasSelection); + } + } + + private _fireOnSelectionChange(start: [number, number] | undefined, end: [number, number] | undefined, hasSelection: boolean): void { + this._oldSelectionStart = start; + this._oldSelectionEnd = end; + this._oldHasSelection = hasSelection; + this._onSelectionChange.fire(); + } + private _onBufferActivate(e: {activeBuffer: IBuffer, inactiveBuffer: IBuffer}): void { this.clearSelection(); // Only adjust the selection on trim, shiftElements is rarely used (only in @@ -727,6 +773,15 @@ export class SelectionService extends Disposable implements ISelectionService { this.refresh(); } + public rightClickSelect(ev: MouseEvent): void { + if (!this._isClickInSelection(ev)) { + if (this._selectWordAtCursor(ev, false)) { + this.refresh(true); + } + this._fireEventIfSelectionChanged(); + } + } + /** * Gets positional information for the word at the coordinated specified. * @param coords The coordinates to get the word at. diff --git a/src/browser/services/Services.ts b/src/browser/services/Services.ts index 1c71d387..4928fa28 100644 --- a/src/browser/services/Services.ts +++ b/src/browser/services/Services.ts @@ -4,7 +4,7 @@ */ import { IEvent } from 'common/EventEmitter'; -import { IRenderDimensions, IRenderer, CharacterJoinerHandler } from 'browser/renderer/Types'; +import { IRenderDimensions, IRenderer } from 'browser/renderer/Types'; import { IColorSet } from 'browser/Types'; import { ISelectionRedrawRequestEvent as ISelectionRequestRedrawEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types'; import { createDecorator } from 'common/services/ServiceRegistry'; @@ -53,6 +53,7 @@ export interface IRenderService extends IDisposable { dimensions: IRenderDimensions; refreshRows(start: number, end: number): void; + clearTextureAtlas(): void; resize(cols: number, rows: number): void; changeOptions(): void; setRenderer(renderer: IRenderer): void; @@ -66,8 +67,6 @@ export interface IRenderService extends IDisposable { onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void; onCursorMove(): void; clear(): void; - registerCharacterJoiner(handler: CharacterJoinerHandler): number; - deregisterCharacterJoiner(joinerId: number): boolean; } export const ISelectionService = createDecorator('SelectionService'); @@ -91,8 +90,7 @@ export interface ISelectionService { selectAll(): void; selectLines(start: number, end: number): void; clearSelection(): void; - isClickInSelection(event: MouseEvent): boolean; - selectWordAtCursor(event: MouseEvent): void; + rightClickSelect(event: MouseEvent): void; shouldColumnSelect(event: KeyboardEvent | MouseEvent): boolean; shouldForceSelection(event: MouseEvent): boolean; refresh(isLinuxMouseSelection?: boolean): void; @@ -105,3 +103,13 @@ export interface ISoundService { playBellSound(): void; } + + +export const ICharacterJoinerService = createDecorator('CharacterJoinerService'); +export interface ICharacterJoinerService { + serviceBrand: undefined; + + register(handler: (text: string) => [number, number][]): number; + deregister(joinerId: number): boolean; + getJoinedCharacters(row: number): [number, number][]; +} diff --git a/src/browser/services/SoundService.ts b/src/browser/services/SoundService.ts index 8d940c13..3880b42d 100644 --- a/src/browser/services/SoundService.ts +++ b/src/browser/services/SoundService.ts @@ -13,7 +13,7 @@ export class SoundService implements ISoundService { public static get audioContext(): AudioContext | null { if (!SoundService._audioContext) { - const audioContextCtor: typeof AudioContext = (window).AudioContext || (window).webkitAudioContext; + const audioContextCtor: typeof AudioContext = (window as any).AudioContext || (window as any).webkitAudioContext; if (!audioContextCtor) { console.warn('Web Audio API is not supported by this browser. Consider upgrading to the latest version'); return null; diff --git a/src/common/CircularList.ts b/src/common/CircularList.ts index ab00e681..4d2c04ec 100644 --- a/src/common/CircularList.ts +++ b/src/common/CircularList.ts @@ -158,7 +158,7 @@ export class CircularList implements ICircularList { this._array[this._getCyclicIndex(i)] = this._array[this._getCyclicIndex(i + deleteCount)]; } this._length -= deleteCount; - this.onDeleteEmitter.fire({index: start, amount: deleteCount}); + this.onDeleteEmitter.fire({ index: start, amount: deleteCount }); } // Add items @@ -169,7 +169,7 @@ export class CircularList implements ICircularList { this._array[this._getCyclicIndex(start + i)] = items[i]; } if (items.length) { - this.onInsertEmitter.fire({index: start, amount: items.length}); + this.onInsertEmitter.fire({ index: start, amount: items.length }); } // Adjust length as needed diff --git a/src/common/Clone.test.ts b/src/common/Clone.test.ts index 370538ea..5fbe4bf9 100644 --- a/src/common/Clone.test.ts +++ b/src/common/Clone.test.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { assert, expect } from 'chai'; +import { assert } from 'chai'; import { clone } from 'common/Clone'; describe('clone', () => { @@ -124,6 +124,6 @@ describe('clone', () => { test.a.b.c = test; - expect(() => clone(test)).to.not.throw(); + assert.doesNotThrow(() => clone(test)); }); }); diff --git a/src/common/Clone.ts b/src/common/Clone.ts index 51c5abaa..37821fe0 100644 --- a/src/common/Clone.ts +++ b/src/common/Clone.ts @@ -16,7 +16,7 @@ export function clone(val: T, depth: number = 5): T { for (const key in val) { // Recursively clone eack item unless we're at the maximum depth - clonedObject[key] = depth <= 1 ? val[key] : (val[key] ? clone(val[key], depth - 1) : val[key]); + clonedObject[key] = depth <= 1 ? val[key] : (val[key] && clone(val[key], depth - 1)); } return clonedObject as T; diff --git a/src/common/CoreTerminal.ts b/src/common/CoreTerminal.ts index 2f636349..96a4b53d 100644 --- a/src/common/CoreTerminal.ts +++ b/src/common/CoreTerminal.ts @@ -22,12 +22,12 @@ */ import { Disposable } from 'common/Lifecycle'; -import { IInstantiationService, IOptionsService, IBufferService, ILogService, ICharsetService, ICoreService, ICoreMouseService, IUnicodeService, IDirtyRowService } from 'common/services/Services'; +import { IInstantiationService, IOptionsService, IBufferService, ILogService, ICharsetService, ICoreService, ICoreMouseService, IUnicodeService, IDirtyRowService, LogLevelEnum } from 'common/services/Services'; import { InstantiationService } from 'common/services/InstantiationService'; import { LogService } from 'common/services/LogService'; import { BufferService, MINIMUM_COLS, MINIMUM_ROWS } from 'common/services/BufferService'; import { OptionsService } from 'common/services/OptionsService'; -import { ITerminalOptions, IDisposable, IBufferLine, IAttributeData, ICoreTerminal } from 'common/Types'; +import { ITerminalOptions, IDisposable, IBufferLine, IAttributeData, ICoreTerminal, IKeyboardEvent, IScrollEvent, ScrollSource } from 'common/Types'; import { CoreService } from 'common/services/CoreService'; import { EventEmitter, IEvent, forwardEvent } from 'common/EventEmitter'; import { CoreMouseService } from 'common/services/CoreMouseService'; @@ -40,23 +40,24 @@ import { IBufferSet } from 'common/buffer/Types'; import { InputHandler } from 'common/InputHandler'; import { WriteBuffer } from 'common/input/WriteBuffer'; +// Only trigger this warning a single time per session +let hasWriteSyncWarnHappened = false; + export abstract class CoreTerminal extends Disposable implements ICoreTerminal { protected readonly _instantiationService: IInstantiationService; protected readonly _bufferService: IBufferService; protected readonly _logService: ILogService; - protected readonly _coreService: ICoreService; protected readonly _charsetService: ICharsetService; - protected readonly _coreMouseService: ICoreMouseService; protected readonly _dirtyRowService: IDirtyRowService; + public readonly coreMouseService: ICoreMouseService; + public readonly coreService: ICoreService; public readonly unicodeService: IUnicodeService; public readonly optionsService: IOptionsService; protected _inputHandler: InputHandler; private _writeBuffer: WriteBuffer; private _windowsMode: IDisposable | undefined; - /** An IBufferline to clone/copy from for new blank lines */ - private _cachedBlankLine: IBufferLine | undefined; private _onBinary = new EventEmitter(); public get onBinary(): IEvent { return this._onBinary.event; } @@ -66,8 +67,21 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { public get onLineFeed(): IEvent { return this._onLineFeed.event; } private _onResize = new EventEmitter<{ cols: number, rows: number }>(); public get onResize(): IEvent<{ cols: number, rows: number }> { return this._onResize.event; } - protected _onScroll = new EventEmitter(); - public get onScroll(): IEvent { return this._onScroll.event; } + protected _onScroll = new EventEmitter(); + /** + * Internally we track the source of the scroll but this is meaningless outside the library so + * it's filtered out. + */ + protected _onScrollApi?: EventEmitter; + public get onScroll(): IEvent { + if (!this._onScrollApi) { + this._onScrollApi = new EventEmitter(); + this.register(this._onScroll.event(ev => { + this._onScrollApi?.fire(ev.position); + })); + } + return this._onScrollApi.event; + } public get cols(): number { return this._bufferService.cols; } public get rows(): number { return this._bufferService.rows; } @@ -86,10 +100,10 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { this._instantiationService.setService(IBufferService, this._bufferService); this._logService = this._instantiationService.createInstance(LogService); this._instantiationService.setService(ILogService, this._logService); - this._coreService = this.register(this._instantiationService.createInstance(CoreService, () => this.scrollToBottom())); - this._instantiationService.setService(ICoreService, this._coreService); - this._coreMouseService = this._instantiationService.createInstance(CoreMouseService); - this._instantiationService.setService(ICoreMouseService, this._coreMouseService); + this.coreService = this.register(this._instantiationService.createInstance(CoreService, () => this.scrollToBottom())); + this._instantiationService.setService(ICoreService, this.coreService); + this.coreMouseService = this._instantiationService.createInstance(CoreMouseService); + this._instantiationService.setService(ICoreMouseService, this.coreMouseService); this._dirtyRowService = this._instantiationService.createInstance(DirtyRowService); this._instantiationService.setService(IDirtyRowService, this._dirtyRowService); this.unicodeService = this._instantiationService.createInstance(UnicodeService); @@ -98,18 +112,26 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { this._instantiationService.setService(ICharsetService, this._charsetService); // Register input handler and handle/forward events - this._inputHandler = new InputHandler(this._bufferService, this._charsetService, this._coreService, this._dirtyRowService, this._logService, this.optionsService, this._coreMouseService, this.unicodeService); + this._inputHandler = new InputHandler(this._bufferService, this._charsetService, this.coreService, this._dirtyRowService, this._logService, this.optionsService, this.coreMouseService, this.unicodeService); this.register(forwardEvent(this._inputHandler.onLineFeed, this._onLineFeed)); this.register(this._inputHandler); // Setup listeners this.register(forwardEvent(this._bufferService.onResize, this._onResize)); - this.register(forwardEvent(this._coreService.onData, this._onData)); - this.register(forwardEvent(this._coreService.onBinary, this._onBinary)); + this.register(forwardEvent(this.coreService.onData, this._onData)); + this.register(forwardEvent(this.coreService.onBinary, this._onBinary)); this.register(this.optionsService.onOptionChange(key => this._updateOptions(key))); + this.register(this._bufferService.onScroll(event => { + this._onScroll.fire({ position: this._bufferService.buffer.ydisp, source: ScrollSource.TERMINAL }); + this._dirtyRowService.markRangeDirty(this._bufferService.buffer.scrollTop, this._bufferService.buffer.scrollBottom); + })); + this.register(this._inputHandler.onScroll(event => { + this._onScroll.fire({ position: this._bufferService.buffer.ydisp, source: ScrollSource.TERMINAL }); + this._dirtyRowService.markRangeDirty(this._bufferService.buffer.scrollTop, this._bufferService.buffer.scrollBottom); + })); // Setup WriteBuffer - this._writeBuffer = new WriteBuffer(data => this._inputHandler.parse(data)); + this._writeBuffer = new WriteBuffer((data, promiseResult) => this._inputHandler.parse(data, promiseResult)); } public dispose(): void { @@ -125,8 +147,21 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { this._writeBuffer.write(data, callback); } - public writeSync(data: string | Uint8Array): void { - this._writeBuffer.writeSync(data); + /** + * Write data to terminal synchonously. + * + * This method is unreliable with async parser handlers, thus should not + * be used anymore. If you need blocking semantics on data input consider + * `write` with a callback instead. + * + * @deprecated Unreliable, will be removed soon. + */ + public writeSync(data: string | Uint8Array, maxSubsequentCalls?: number): void { + if (this._logService.logLevel <= LogLevelEnum.WARN && !hasWriteSyncWarnHappened) { + this._logService.warn('writeSync is unreliable and will be removed soon.'); + hasWriteSyncWarnHappened = true; + } + this._writeBuffer.writeSync(data, maxSubsequentCalls); } public resize(x: number, y: number): void { @@ -145,66 +180,7 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { * @param isWrapped Whether the new line is wrapped from the previous line. */ public scroll(eraseAttr: IAttributeData, isWrapped: boolean = false): void { - const buffer = this._bufferService.buffer; - - let newLine: IBufferLine | undefined; - newLine = this._cachedBlankLine; - if (!newLine || newLine.length !== this.cols || newLine.getFg(0) !== eraseAttr.fg || newLine.getBg(0) !== eraseAttr.bg) { - newLine = buffer.getBlankLine(eraseAttr, isWrapped); - this._cachedBlankLine = newLine; - } - newLine.isWrapped = isWrapped; - - const topRow = buffer.ybase + buffer.scrollTop; - const bottomRow = buffer.ybase + buffer.scrollBottom; - - if (buffer.scrollTop === 0) { - // Determine whether the buffer is going to be trimmed after insertion. - const willBufferBeTrimmed = buffer.lines.isFull; - - // Insert the line using the fastest method - if (bottomRow === buffer.lines.length - 1) { - if (willBufferBeTrimmed) { - buffer.lines.recycle().copyFrom(newLine); - } else { - buffer.lines.push(newLine.clone()); - } - } else { - buffer.lines.splice(bottomRow + 1, 0, newLine.clone()); - } - - // Only adjust ybase and ydisp when the buffer is not trimmed - if (!willBufferBeTrimmed) { - buffer.ybase++; - // Only scroll the ydisp with ybase if the user has not scrolled up - if (!this._bufferService.isUserScrolling) { - buffer.ydisp++; - } - } else { - // When the buffer is full and the user has scrolled up, keep the text - // stable unless ydisp is right at the top - if (this._bufferService.isUserScrolling) { - buffer.ydisp = Math.max(buffer.ydisp - 1, 0); - } - } - } else { - // scrollTop is non-zero which means no line will be going to the - // scrollback, instead we can just shift them in-place. - const scrollRegionHeight = bottomRow - topRow + 1 /* as it's zero-based */; - buffer.lines.shiftElements(topRow + 1, scrollRegionHeight - 1, -1); - buffer.lines.set(bottomRow, newLine.clone()); - } - - // Move the viewport to the bottom of the buffer unless the user is - // scrolling. - if (!this._bufferService.isUserScrolling) { - buffer.ydisp = buffer.ybase; - } - - // Flag rows that need updating - this._dirtyRowService.markRangeDirty(buffer.scrollTop, buffer.scrollBottom); - - this._onScroll.fire(buffer.ydisp); + this._bufferService.scroll(eraseAttr, isWrapped); } /** @@ -214,28 +190,8 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { * to avoid unwanted events being handled by the viewport when the event was triggered from the * viewport originally. */ - public scrollLines(disp: number, suppressScrollEvent?: boolean): void { - const buffer = this._bufferService.buffer; - if (disp < 0) { - if (buffer.ydisp === 0) { - return; - } - this._bufferService.isUserScrolling = true; - } else if (disp + buffer.ydisp >= buffer.ybase) { - this._bufferService.isUserScrolling = false; - } - - const oldYdisp = buffer.ydisp; - buffer.ydisp = Math.max(Math.min(buffer.ydisp + disp, buffer.ybase), 0); - - // No change occurred, don't trigger scroll/refresh - if (oldYdisp === buffer.ydisp) { - return; - } - - if (!suppressScrollEvent) { - this._onScroll.fire(buffer.ydisp); - } + public scrollLines(disp: number, suppressScrollEvent?: boolean, source?: ScrollSource): void { + this._bufferService.scrollLines(disp, suppressScrollEvent, source); } /** @@ -243,48 +199,45 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { * @param pageCount The number of pages to scroll (negative scrolls up). */ public scrollPages(pageCount: number): void { - this.scrollLines(pageCount * (this.rows - 1)); + this._bufferService.scrollPages(pageCount); } /** * Scrolls the display of the terminal to the top. */ public scrollToTop(): void { - this.scrollLines(-this._bufferService.buffer.ydisp); + this._bufferService.scrollToTop(); } /** * Scrolls the display of the terminal to the bottom. */ public scrollToBottom(): void { - this.scrollLines(this._bufferService.buffer.ybase - this._bufferService.buffer.ydisp); + this._bufferService.scrollToBottom(); } public scrollToLine(line: number): void { - const scrollAmount = line - this._bufferService.buffer.ydisp; - if (scrollAmount !== 0) { - this.scrollLines(scrollAmount); - } + this._bufferService.scrollToLine(line); } /** Add handler for ESC escape sequence. See xterm.d.ts for details. */ - public addEscHandler(id: IFunctionIdentifier, callback: () => boolean): IDisposable { - return this._inputHandler.addEscHandler(id, callback); + public registerEscHandler(id: IFunctionIdentifier, callback: () => boolean | Promise): IDisposable { + return this._inputHandler.registerEscHandler(id, callback); } /** Add handler for DCS escape sequence. See xterm.d.ts for details. */ - public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean): IDisposable { - return this._inputHandler.addDcsHandler(id, callback); + public registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean | Promise): IDisposable { + return this._inputHandler.registerDcsHandler(id, callback); } /** Add handler for CSI escape sequence. See xterm.d.ts for details. */ - public addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable { - return this._inputHandler.addCsiHandler(id, callback); + public registerCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean | Promise): IDisposable { + return this._inputHandler.registerCsiHandler(id, callback); } /** Add handler for OSC escape sequence. See xterm.d.ts for details. */ - public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { - return this._inputHandler.addOscHandler(ident, callback); + public registerOscHandler(ident: number, callback: (data: string) => boolean | Promise): IDisposable { + return this._inputHandler.registerOscHandler(ident, callback); } protected _setup(): void { @@ -297,8 +250,8 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { this._inputHandler.reset(); this._bufferService.reset(); this._charsetService.reset(); - this._coreService.reset(); - this._coreMouseService.reset(); + this.coreService.reset(); + this.coreMouseService.reset(); } protected _updateOptions(key: string): void { @@ -322,7 +275,7 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { if (!this._windowsMode) { const disposables: IDisposable[] = []; disposables.push(this.onLineFeed(updateWindowsModeWrappedState.bind(null, this._bufferService))); - disposables.push(this.addCsiHandler({ final: 'H' }, () => { + disposables.push(this.registerCsiHandler({ final: 'H' }, () => { updateWindowsModeWrappedState(this._bufferService); return false; })); diff --git a/src/common/InputHandler.test.ts b/src/common/InputHandler.test.ts index 46f8eacc..92162d9a 100644 --- a/src/common/InputHandler.test.ts +++ b/src/common/InputHandler.test.ts @@ -3,9 +3,9 @@ * @license MIT */ -import { assert, expect } from 'chai'; +import { assert } from 'chai'; import { InputHandler } from 'common/InputHandler'; -import { IBufferLine, IAttributeData } from 'common/Types'; +import { IBufferLine, IAttributeData, IAnsiColorChangeEvent } from 'common/Types'; import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; import { CellData } from 'common/buffer/CellData'; import { Attributes, UnderlineStyle } from 'common/buffer/Constants'; @@ -17,6 +17,7 @@ import { DEFAULT_OPTIONS } from 'common/services/OptionsService'; import { clone } from 'common/Clone'; import { BufferService } from 'common/services/BufferService'; import { CoreService } from 'common/services/CoreService'; +import { OscHandler } from 'common/parser/OscParser'; function getCursor(bufferService: IBufferService): number[] { return [ @@ -40,6 +41,19 @@ class TestInputHandler extends InputHandler { public get curAttrData(): IAttributeData { return (this as any)._curAttrData; } public get windowTitleStack(): string[] { return this._windowTitleStack; } public get iconNameStack(): string[] { return this._iconNameStack; } + public parseAnsiColorChange(data: string): IAnsiColorChangeEvent | null { return this._parseAnsiColorChange(data); } + + /** + * Promise based parse call to await the full resolve of given input data. + * This is useful to test async handlers in inputhandler directly. + */ + public async parseP(data: string | Uint8Array): Promise { + let result: Promise | void; + let prev: boolean | undefined; + while (result = this.parse(data, prev)) { + prev = await result; + } + } } describe('InputHandler', () => { @@ -52,11 +66,117 @@ describe('InputHandler', () => { optionsService = new MockOptionsService(); bufferService = new BufferService(optionsService); bufferService.resize(80, 30); - coreService = new CoreService(() => {}, bufferService, new MockLogService(), optionsService); + coreService = new CoreService(() => { }, bufferService, new MockLogService(), optionsService); inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockDirtyRowService(), new MockLogService(), optionsService, new MockCoreMouseService(), new MockUnicodeService()); }); + describe('SL/SR/DECIC/DECDC', () => { + beforeEach(() => { + bufferService.resize(5, 5); + optionsService.options.scrollback = 1; + bufferService.reset(); + }); + it('SL (scrollLeft)', () => { + inputHandler.parseP('12345'.repeat(6)); + inputHandler.parseP('\x1b[ @'); + assert.deepEqual(getLines(bufferService, 6), ['12345', '2345', '2345', '2345', '2345', '2345']); + inputHandler.parseP('\x1b[0 @'); + assert.deepEqual(getLines(bufferService, 6), ['12345', '345', '345', '345', '345', '345']); + inputHandler.parseP('\x1b[2 @'); + assert.deepEqual(getLines(bufferService, 6), ['12345', '5', '5', '5', '5', '5']); + }); + it('SR (scrollRight)', () => { + inputHandler.parseP('12345'.repeat(6)); + inputHandler.parseP('\x1b[ A'); + assert.deepEqual(getLines(bufferService, 6), ['12345', ' 1234', ' 1234', ' 1234', ' 1234', ' 1234']); + inputHandler.parseP('\x1b[0 A'); + assert.deepEqual(getLines(bufferService, 6), ['12345', ' 123', ' 123', ' 123', ' 123', ' 123']); + inputHandler.parseP('\x1b[2 A'); + assert.deepEqual(getLines(bufferService, 6), ['12345', ' 1', ' 1', ' 1', ' 1', ' 1']); + }); + it('insertColumns (DECIC)', () => { + inputHandler.parseP('12345'.repeat(6)); + inputHandler.parseP('\x1b[3;3H'); + inputHandler.parseP('\x1b[\'}'); + assert.deepEqual(getLines(bufferService, 6), ['12345', '12 34', '12 34', '12 34', '12 34', '12 34']); + bufferService.reset(); + inputHandler.parseP('12345'.repeat(6)); + inputHandler.parseP('\x1b[3;3H'); + inputHandler.parseP('\x1b[1\'}'); + assert.deepEqual(getLines(bufferService, 6), ['12345', '12 34', '12 34', '12 34', '12 34', '12 34']); + bufferService.reset(); + inputHandler.parseP('12345'.repeat(6)); + inputHandler.parseP('\x1b[3;3H'); + inputHandler.parseP('\x1b[2\'}'); + assert.deepEqual(getLines(bufferService, 6), ['12345', '12 3', '12 3', '12 3', '12 3', '12 3']); + }); + it('deleteColumns (DECDC)', () => { + inputHandler.parseP('12345'.repeat(6)); + inputHandler.parseP('\x1b[3;3H'); + inputHandler.parseP('\x1b[\'~'); + assert.deepEqual(getLines(bufferService, 6), ['12345', '1245', '1245', '1245', '1245', '1245']); + bufferService.reset(); + inputHandler.parseP('12345'.repeat(6)); + inputHandler.parseP('\x1b[3;3H'); + inputHandler.parseP('\x1b[1\'~'); + assert.deepEqual(getLines(bufferService, 6), ['12345', '1245', '1245', '1245', '1245', '1245']); + bufferService.reset(); + inputHandler.parseP('12345'.repeat(6)); + inputHandler.parseP('\x1b[3;3H'); + inputHandler.parseP('\x1b[2\'~'); + assert.deepEqual(getLines(bufferService, 6), ['12345', '125', '125', '125', '125', '125']); + }); + }); + + describe('BS with reverseWraparound set/unset', () => { + const ttyBS = '\x08 \x08'; // tty ICANON sends on pressing BS + beforeEach(() => { + bufferService.resize(5, 5); + optionsService.options.scrollback = 1; + bufferService.reset(); + }); + describe('reverseWraparound set', () => { + it('should not reverse outside of scroll margins', () => { + // prepare buffer content + inputHandler.parseP('#####abcdefghijklmnopqrstuvwxy'); + assert.deepEqual(getLines(bufferService, 6), ['#####', 'abcde', 'fghij', 'klmno', 'pqrst', 'uvwxy']); + assert.equal(bufferService.buffers.active.ydisp, 1); + assert.equal(bufferService.buffers.active.x, 5); + assert.equal(bufferService.buffers.active.y, 4); + inputHandler.parseP(ttyBS.repeat(100)); + assert.deepEqual(getLines(bufferService, 6), ['#####', 'abcde', 'fghij', 'klmno', 'pqrst', ' y']); + + inputHandler.parseP('\x1b[?45h'); + inputHandler.parseP('uvwxy'); + + // set top/bottom to 1/3 (0-based) + inputHandler.parseP('\x1b[2;4r'); + // place cursor below scroll bottom + bufferService.buffers.active.x = 5; + bufferService.buffers.active.y = 4; + inputHandler.parseP(ttyBS.repeat(100)); + assert.deepEqual(getLines(bufferService, 6), ['#####', 'abcde', 'fghij', 'klmno', 'pqrst', ' ']); + + inputHandler.parseP('uvwxy'); + // place cursor within scroll margins + bufferService.buffers.active.x = 5; + bufferService.buffers.active.y = 3; + inputHandler.parseP(ttyBS.repeat(100)); + assert.deepEqual(getLines(bufferService, 6), ['#####', 'abcde', ' ', ' ', ' ', 'uvwxy']); + assert.equal(bufferService.buffers.active.x, 0); + assert.equal(bufferService.buffers.active.y, bufferService.buffers.active.scrollTop); // stops at 0, scrollTop + + inputHandler.parseP('fghijklmnopqrst'); + // place cursor above scroll top + bufferService.buffers.active.x = 5; + bufferService.buffers.active.y = 0; + inputHandler.parseP(ttyBS.repeat(100)); + assert.deepEqual(getLines(bufferService, 6), ['#####', ' ', 'fghij', 'klmno', 'pqrst', 'uvwxy']); + }); + }); + }); + it('save and restore cursor', () => { bufferService.buffer.x = 1; bufferService.buffer.y = 2; @@ -117,7 +237,7 @@ describe('InputHandler', () => { describe('setMode', () => { it('should toggle bracketedPasteMode', () => { const coreService = new MockCoreService(); - const inputHandler = new InputHandler(new MockBufferService(80, 30), new MockCharsetService(), coreService, new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService(), new MockUnicodeService()); + const inputHandler = new TestInputHandler(new MockBufferService(80, 30), new MockCharsetService(), coreService, new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService(), new MockUnicodeService()); // Set bracketed paste mode inputHandler.setModePrivate(Params.fromArray([2004])); assert.equal(coreService.decPrivateModes.bracketedPasteMode, true); @@ -126,130 +246,167 @@ describe('InputHandler', () => { assert.equal(coreService.decPrivateModes.bracketedPasteMode, false); }); }); - describe('regression tests', function(): void { + describe('regression tests', function (): void { function termContent(bufferService: IBufferService, trim: boolean): string[] { const result = []; for (let i = 0; i < bufferService.rows; ++i) result.push(bufferService.buffer.lines.get(i)!.translateToString(trim)); return result; } - it('insertChars', function(): void { + it('insertChars', async () => { const bufferService = new MockBufferService(80, 30); - const inputHandler = new InputHandler(bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService(), new MockUnicodeService()); + const inputHandler = new TestInputHandler( + bufferService, + new MockCharsetService(), + new MockCoreService(), + new MockDirtyRowService(), + new MockLogService(), + new MockOptionsService(), + new MockCoreMouseService(), + new MockUnicodeService() + ); // insert some data in first and second line - inputHandler.parse(Array(bufferService.cols - 9).join('a')); - inputHandler.parse('1234567890'); - inputHandler.parse(Array(bufferService.cols - 9).join('a')); - inputHandler.parse('1234567890'); + await inputHandler.parseP(Array(bufferService.cols - 9).join('a')); + await inputHandler.parseP('1234567890'); + await inputHandler.parseP(Array(bufferService.cols - 9).join('a')); + await inputHandler.parseP('1234567890'); const line1: IBufferLine = bufferService.buffer.lines.get(0)!; - expect(line1.translateToString(false)).equals(Array(bufferService.cols - 9).join('a') + '1234567890'); + assert.equal(line1.translateToString(false), Array(bufferService.cols - 9).join('a') + '1234567890'); // insert one char from params = [0] bufferService.buffer.y = 0; bufferService.buffer.x = 70; inputHandler.insertChars(Params.fromArray([0])); - expect(line1.translateToString(false)).equals(Array(bufferService.cols - 9).join('a') + ' 123456789'); + assert.equal(line1.translateToString(false), Array(bufferService.cols - 9).join('a') + ' 123456789'); // insert one char from params = [1] bufferService.buffer.y = 0; bufferService.buffer.x = 70; inputHandler.insertChars(Params.fromArray([1])); - expect(line1.translateToString(false)).equals(Array(bufferService.cols - 9).join('a') + ' 12345678'); + assert.equal(line1.translateToString(false), Array(bufferService.cols - 9).join('a') + ' 12345678'); // insert two chars from params = [2] bufferService.buffer.y = 0; bufferService.buffer.x = 70; inputHandler.insertChars(Params.fromArray([2])); - expect(line1.translateToString(false)).equals(Array(bufferService.cols - 9).join('a') + ' 123456'); + assert.equal(line1.translateToString(false), Array(bufferService.cols - 9).join('a') + ' 123456'); // insert 10 chars from params = [10] bufferService.buffer.y = 0; bufferService.buffer.x = 70; inputHandler.insertChars(Params.fromArray([10])); - expect(line1.translateToString(false)).equals(Array(bufferService.cols - 9).join('a') + ' '); - expect(line1.translateToString(true)).equals(Array(bufferService.cols - 9).join('a')); + assert.equal(line1.translateToString(false), Array(bufferService.cols - 9).join('a') + ' '); + assert.equal(line1.translateToString(true), Array(bufferService.cols - 9).join('a')); }); - it('deleteChars', function(): void { + it('deleteChars', async () => { const bufferService = new MockBufferService(80, 30); - const inputHandler = new InputHandler(bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService(), new MockUnicodeService()); + const inputHandler = new TestInputHandler( + bufferService, + new MockCharsetService(), + new MockCoreService(), + new MockDirtyRowService(), + new MockLogService(), + new MockOptionsService(), + new MockCoreMouseService(), + new MockUnicodeService() + ); // insert some data in first and second line - inputHandler.parse(Array(bufferService.cols - 9).join('a')); - inputHandler.parse('1234567890'); - inputHandler.parse(Array(bufferService.cols - 9).join('a')); - inputHandler.parse('1234567890'); + await inputHandler.parseP(Array(bufferService.cols - 9).join('a')); + await inputHandler.parseP('1234567890'); + await inputHandler.parseP(Array(bufferService.cols - 9).join('a')); + await inputHandler.parseP('1234567890'); const line1: IBufferLine = bufferService.buffer.lines.get(0)!; - expect(line1.translateToString(false)).equals(Array(bufferService.cols - 9).join('a') + '1234567890'); + assert.equal(line1.translateToString(false), Array(bufferService.cols - 9).join('a') + '1234567890'); // delete one char from params = [0] bufferService.buffer.y = 0; bufferService.buffer.x = 70; inputHandler.deleteChars(Params.fromArray([0])); - expect(line1.translateToString(false)).equals(Array(bufferService.cols - 9).join('a') + '234567890 '); - expect(line1.translateToString(true)).equals(Array(bufferService.cols - 9).join('a') + '234567890'); + assert.equal(line1.translateToString(false), Array(bufferService.cols - 9).join('a') + '234567890 '); + assert.equal(line1.translateToString(true), Array(bufferService.cols - 9).join('a') + '234567890'); // insert one char from params = [1] bufferService.buffer.y = 0; bufferService.buffer.x = 70; inputHandler.deleteChars(Params.fromArray([1])); - expect(line1.translateToString(false)).equals(Array(bufferService.cols - 9).join('a') + '34567890 '); - expect(line1.translateToString(true)).equals(Array(bufferService.cols - 9).join('a') + '34567890'); + assert.equal(line1.translateToString(false), Array(bufferService.cols - 9).join('a') + '34567890 '); + assert.equal(line1.translateToString(true), Array(bufferService.cols - 9).join('a') + '34567890'); // insert two chars from params = [2] bufferService.buffer.y = 0; bufferService.buffer.x = 70; inputHandler.deleteChars(Params.fromArray([2])); - expect(line1.translateToString(false)).equals(Array(bufferService.cols - 9).join('a') + '567890 '); - expect(line1.translateToString(true)).equals(Array(bufferService.cols - 9).join('a') + '567890'); + assert.equal(line1.translateToString(false), Array(bufferService.cols - 9).join('a') + '567890 '); + assert.equal(line1.translateToString(true), Array(bufferService.cols - 9).join('a') + '567890'); + // insert 10 chars from params = [10] bufferService.buffer.y = 0; bufferService.buffer.x = 70; inputHandler.deleteChars(Params.fromArray([10])); - expect(line1.translateToString(false)).equals(Array(bufferService.cols - 9).join('a') + ' '); - expect(line1.translateToString(true)).equals(Array(bufferService.cols - 9).join('a')); + assert.equal(line1.translateToString(false), Array(bufferService.cols - 9).join('a') + ' '); + assert.equal(line1.translateToString(true), Array(bufferService.cols - 9).join('a')); }); - it('eraseInLine', function(): void { + it('eraseInLine', async () => { const bufferService = new MockBufferService(80, 30); - const inputHandler = new InputHandler(bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService(), new MockUnicodeService()); + const inputHandler = new TestInputHandler( + bufferService, + new MockCharsetService(), + new MockCoreService(), + new MockDirtyRowService(), + new MockLogService(), + new MockOptionsService(), + new MockCoreMouseService(), + new MockUnicodeService() + ); // fill 6 lines to test 3 different states - inputHandler.parse(Array(bufferService.cols + 1).join('a')); - inputHandler.parse(Array(bufferService.cols + 1).join('a')); - inputHandler.parse(Array(bufferService.cols + 1).join('a')); + await inputHandler.parseP(Array(bufferService.cols + 1).join('a')); + await inputHandler.parseP(Array(bufferService.cols + 1).join('a')); + await inputHandler.parseP(Array(bufferService.cols + 1).join('a')); // params[0] - right erase bufferService.buffer.y = 0; bufferService.buffer.x = 70; inputHandler.eraseInLine(Params.fromArray([0])); - expect(bufferService.buffer.lines.get(0)!.translateToString(false)).equals(Array(71).join('a') + ' '); + assert.equal(bufferService.buffer.lines.get(0)!.translateToString(false), Array(71).join('a') + ' '); // params[1] - left erase bufferService.buffer.y = 1; bufferService.buffer.x = 70; inputHandler.eraseInLine(Params.fromArray([1])); - expect(bufferService.buffer.lines.get(1)!.translateToString(false)).equals(Array(71).join(' ') + ' aaaaaaaaa'); + assert.equal(bufferService.buffer.lines.get(1)!.translateToString(false), Array(71).join(' ') + ' aaaaaaaaa'); // params[1] - left erase bufferService.buffer.y = 2; bufferService.buffer.x = 70; inputHandler.eraseInLine(Params.fromArray([2])); - expect(bufferService.buffer.lines.get(2)!.translateToString(false)).equals(Array(bufferService.cols + 1).join(' ')); + assert.equal(bufferService.buffer.lines.get(2)!.translateToString(false), Array(bufferService.cols + 1).join(' ')); }); - it('eraseInDisplay', function(): void { + it('eraseInDisplay', async () => { const bufferService = new MockBufferService(80, 7); - const inputHandler = new InputHandler(bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService(), new MockUnicodeService()); + const inputHandler = new TestInputHandler( + bufferService, + new MockCharsetService(), + new MockCoreService(), + new MockDirtyRowService(), + new MockLogService(), + new MockOptionsService(), + new MockCoreMouseService(), + new MockUnicodeService() + ); // fill display with a's - for (let i = 0; i < bufferService.rows; ++i) inputHandler.parse(Array(bufferService.cols + 1).join('a')); + for (let i = 0; i < bufferService.rows; ++i) await inputHandler.parseP(Array(bufferService.cols + 1).join('a')); // params [0] - right and below erase bufferService.buffer.y = 5; bufferService.buffer.x = 40; inputHandler.eraseInDisplay(Params.fromArray([0])); - expect(termContent(bufferService, false)).eql([ + assert.deepEqual(termContent(bufferService, false), [ Array(bufferService.cols + 1).join('a'), Array(bufferService.cols + 1).join('a'), Array(bufferService.cols + 1).join('a'), @@ -258,7 +415,7 @@ describe('InputHandler', () => { Array(40 + 1).join('a') + Array(bufferService.cols - 40 + 1).join(' '), Array(bufferService.cols + 1).join(' ') ]); - expect(termContent(bufferService, true)).eql([ + assert.deepEqual(termContent(bufferService, true), [ Array(bufferService.cols + 1).join('a'), Array(bufferService.cols + 1).join('a'), Array(bufferService.cols + 1).join('a'), @@ -271,13 +428,13 @@ describe('InputHandler', () => { // reset bufferService.buffer.y = 0; bufferService.buffer.x = 0; - for (let i = 0; i < bufferService.rows; ++i) inputHandler.parse(Array(bufferService.cols + 1).join('a')); + for (let i = 0; i < bufferService.rows; ++i) await inputHandler.parseP(Array(bufferService.cols + 1).join('a')); // params [1] - left and above bufferService.buffer.y = 5; bufferService.buffer.x = 40; inputHandler.eraseInDisplay(Params.fromArray([1])); - expect(termContent(bufferService, false)).eql([ + assert.deepEqual(termContent(bufferService, false), [ Array(bufferService.cols + 1).join(' '), Array(bufferService.cols + 1).join(' '), Array(bufferService.cols + 1).join(' '), @@ -286,7 +443,7 @@ describe('InputHandler', () => { Array(41 + 1).join(' ') + Array(bufferService.cols - 41 + 1).join('a'), Array(bufferService.cols + 1).join('a') ]); - expect(termContent(bufferService, true)).eql([ + assert.deepEqual(termContent(bufferService, true), [ '', '', '', @@ -299,13 +456,13 @@ describe('InputHandler', () => { // reset bufferService.buffer.y = 0; bufferService.buffer.x = 0; - for (let i = 0; i < bufferService.rows; ++i) inputHandler.parse(Array(bufferService.cols + 1).join('a')); + for (let i = 0; i < bufferService.rows; ++i) await inputHandler.parseP(Array(bufferService.cols + 1).join('a')); // params [2] - whole screen bufferService.buffer.y = 5; bufferService.buffer.x = 40; inputHandler.eraseInDisplay(Params.fromArray([2])); - expect(termContent(bufferService, false)).eql([ + assert.deepEqual(termContent(bufferService, false), [ Array(bufferService.cols + 1).join(' '), Array(bufferService.cols + 1).join(' '), Array(bufferService.cols + 1).join(' '), @@ -314,7 +471,7 @@ describe('InputHandler', () => { Array(bufferService.cols + 1).join(' '), Array(bufferService.cols + 1).join(' ') ]); - expect(termContent(bufferService, true)).eql([ + assert.deepEqual(termContent(bufferService, true), [ '', '', '', @@ -327,242 +484,257 @@ describe('InputHandler', () => { // reset and add a wrapped line bufferService.buffer.y = 0; bufferService.buffer.x = 0; - inputHandler.parse(Array(bufferService.cols + 1).join('a')); // line 0 - inputHandler.parse(Array(bufferService.cols + 10).join('a')); // line 1 and 2 - for (let i = 3; i < bufferService.rows; ++i) inputHandler.parse(Array(bufferService.cols + 1).join('a')); + await inputHandler.parseP(Array(bufferService.cols + 1).join('a')); // line 0 + await inputHandler.parseP(Array(bufferService.cols + 10).join('a')); // line 1 and 2 + for (let i = 3; i < bufferService.rows; ++i) await inputHandler.parseP(Array(bufferService.cols + 1).join('a')); // params[1] left and above with wrap // confirm precondition that line 2 is wrapped - expect(bufferService.buffer.lines.get(2)!.isWrapped).true; + assert.equal(bufferService.buffer.lines.get(2)!.isWrapped, true); bufferService.buffer.y = 2; bufferService.buffer.x = 40; inputHandler.eraseInDisplay(Params.fromArray([1])); - expect(bufferService.buffer.lines.get(2)!.isWrapped).false; + assert.equal(bufferService.buffer.lines.get(2)!.isWrapped, false); // reset and add a wrapped line bufferService.buffer.y = 0; bufferService.buffer.x = 0; - inputHandler.parse(Array(bufferService.cols + 1).join('a')); // line 0 - inputHandler.parse(Array(bufferService.cols + 10).join('a')); // line 1 and 2 - for (let i = 3; i < bufferService.rows; ++i) inputHandler.parse(Array(bufferService.cols + 1).join('a')); + await inputHandler.parseP(Array(bufferService.cols + 1).join('a')); // line 0 + await inputHandler.parseP(Array(bufferService.cols + 10).join('a')); // line 1 and 2 + for (let i = 3; i < bufferService.rows; ++i) await inputHandler.parseP(Array(bufferService.cols + 1).join('a')); // params[1] left and above with wrap // confirm precondition that line 2 is wrapped - expect(bufferService.buffer.lines.get(2)!.isWrapped).true; + assert.equal(bufferService.buffer.lines.get(2)!.isWrapped, true); bufferService.buffer.y = 1; bufferService.buffer.x = 90; // Cursor is beyond last column inputHandler.eraseInDisplay(Params.fromArray([1])); - expect(bufferService.buffer.lines.get(2)!.isWrapped).false; + assert.equal(bufferService.buffer.lines.get(2)!.isWrapped, false); }); }); describe('print', () => { it('should not cause an infinite loop (regression test)', () => { - const inputHandler = new InputHandler(new MockBufferService(80, 30), new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService(), new MockUnicodeService()); + const inputHandler = new TestInputHandler( + new MockBufferService(80, 30), + new MockCharsetService(), + new MockCoreService(), + new MockDirtyRowService(), + new MockLogService(), + new MockOptionsService(), + new MockCoreMouseService(), + new MockUnicodeService() + ); const container = new Uint32Array(10); container[0] = 0x200B; inputHandler.print(container, 0, 1); }); - it('should clear cells to the right on early wrap-around', () => { + it('should clear cells to the right on early wrap-around', async () => { bufferService.resize(5, 5); optionsService.options.scrollback = 1; - inputHandler.parse('12345'); + await inputHandler.parseP('12345'); bufferService.buffer.x = 0; - inputHandler.parse('¥¥¥'); + await inputHandler.parseP('¥¥¥'); assert.deepEqual(getLines(bufferService, 2), ['¥¥', '¥']); }); }); describe('alt screen', () => { let bufferService: IBufferService; - let handler: InputHandler; + let handler: TestInputHandler; beforeEach(() => { bufferService = new MockBufferService(80, 30); - handler = new InputHandler(bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService(), new MockUnicodeService()); + handler = new TestInputHandler(bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService(), new MockUnicodeService()); }); - it('should handle DECSET/DECRST 47 (alt screen buffer)', () => { - handler.parse('\x1b[?47h\r\n\x1b[31mJUNK\x1b[?47lTEST'); - expect(bufferService.buffer.translateBufferLineToString(0, true)).to.equal(''); - expect(bufferService.buffer.translateBufferLineToString(1, true)).to.equal(' TEST'); + it('should handle DECSET/DECRST 47 (alt screen buffer)', async () => { + await handler.parseP('\x1b[?47h\r\n\x1b[31mJUNK\x1b[?47lTEST'); + assert.equal(bufferService.buffer.translateBufferLineToString(0, true), ''); + assert.equal(bufferService.buffer.translateBufferLineToString(1, true), ' TEST'); // Text color of 'TEST' should be red - expect((bufferService.buffer.lines.get(1)!.loadCell(4, new CellData()).getFgColor())).to.equal(1); + assert.equal((bufferService.buffer.lines.get(1)!.loadCell(4, new CellData()).getFgColor()), 1); }); - it('should handle DECSET/DECRST 1047 (alt screen buffer)', () => { - handler.parse('\x1b[?1047h\r\n\x1b[31mJUNK\x1b[?1047lTEST'); - expect(bufferService.buffer.translateBufferLineToString(0, true)).to.equal(''); - expect(bufferService.buffer.translateBufferLineToString(1, true)).to.equal(' TEST'); + it('should handle DECSET/DECRST 1047 (alt screen buffer)', async () => { + await handler.parseP('\x1b[?1047h\r\n\x1b[31mJUNK\x1b[?1047lTEST'); + assert.equal(bufferService.buffer.translateBufferLineToString(0, true), ''); + assert.equal(bufferService.buffer.translateBufferLineToString(1, true), ' TEST'); // Text color of 'TEST' should be red - expect((bufferService.buffer.lines.get(1)!.loadCell(4, new CellData()).getFgColor())).to.equal(1); + assert.equal((bufferService.buffer.lines.get(1)!.loadCell(4, new CellData()).getFgColor()), 1); }); - it('should handle DECSET/DECRST 1048 (alt screen cursor)', () => { - handler.parse('\x1b[?1048h\r\n\x1b[31mJUNK\x1b[?1048lTEST'); - expect(bufferService.buffer.translateBufferLineToString(0, true)).to.equal('TEST'); - expect(bufferService.buffer.translateBufferLineToString(1, true)).to.equal('JUNK'); + it('should handle DECSET/DECRST 1048 (alt screen cursor)', async () => { + await handler.parseP('\x1b[?1048h\r\n\x1b[31mJUNK\x1b[?1048lTEST'); + assert.equal(bufferService.buffer.translateBufferLineToString(0, true), 'TEST'); + assert.equal(bufferService.buffer.translateBufferLineToString(1, true), 'JUNK'); // Text color of 'TEST' should be default - expect(bufferService.buffer.lines.get(0)!.loadCell(0, new CellData()).fg).to.equal(DEFAULT_ATTR_DATA.fg); + assert.equal(bufferService.buffer.lines.get(0)!.loadCell(0, new CellData()).fg, DEFAULT_ATTR_DATA.fg); // Text color of 'JUNK' should be red - expect((bufferService.buffer.lines.get(1)!.loadCell(0, new CellData()).getFgColor())).to.equal(1); + assert.equal((bufferService.buffer.lines.get(1)!.loadCell(0, new CellData()).getFgColor()), 1); }); - it('should handle DECSET/DECRST 1049 (alt screen buffer+cursor)', () => { - handler.parse('\x1b[?1049h\r\n\x1b[31mJUNK\x1b[?1049lTEST'); - expect(bufferService.buffer.translateBufferLineToString(0, true)).to.equal('TEST'); - expect(bufferService.buffer.translateBufferLineToString(1, true)).to.equal(''); + it('should handle DECSET/DECRST 1049 (alt screen buffer+cursor)', async () => { + await handler.parseP('\x1b[?1049h\r\n\x1b[31mJUNK\x1b[?1049lTEST'); + assert.equal(bufferService.buffer.translateBufferLineToString(0, true), 'TEST'); + assert.equal(bufferService.buffer.translateBufferLineToString(1, true), ''); // Text color of 'TEST' should be default - expect(bufferService.buffer.lines.get(0)!.loadCell(0, new CellData()).fg).to.equal(DEFAULT_ATTR_DATA.fg); + assert.equal(bufferService.buffer.lines.get(0)!.loadCell(0, new CellData()).fg, DEFAULT_ATTR_DATA.fg); }); - it('should handle DECSET/DECRST 1049 - maintains saved cursor for alt buffer', () => { - handler.parse('\x1b[?1049h\r\n\x1b[31m\x1b[s\x1b[?1049lTEST'); - expect(bufferService.buffer.translateBufferLineToString(0, true)).to.equal('TEST'); + it('should handle DECSET/DECRST 1049 - maintains saved cursor for alt buffer', async () => { + await handler.parseP('\x1b[?1049h\r\n\x1b[31m\x1b[s\x1b[?1049lTEST'); + assert.equal(bufferService.buffer.translateBufferLineToString(0, true), 'TEST'); // Text color of 'TEST' should be default - expect(bufferService.buffer.lines.get(0)!.loadCell(0, new CellData()).fg).to.equal(DEFAULT_ATTR_DATA.fg); - handler.parse('\x1b[?1049h\x1b[uTEST'); - expect(bufferService.buffer.translateBufferLineToString(1, true)).to.equal('TEST'); + assert.equal(bufferService.buffer.lines.get(0)!.loadCell(0, new CellData()).fg, DEFAULT_ATTR_DATA.fg); + await handler.parseP('\x1b[?1049h\x1b[uTEST'); + assert.equal(bufferService.buffer.translateBufferLineToString(1, true), 'TEST'); // Text color of 'TEST' should be red - expect((bufferService.buffer.lines.get(1)!.loadCell(0, new CellData()).getFgColor())).to.equal(1); + assert.equal((bufferService.buffer.lines.get(1)!.loadCell(0, new CellData()).getFgColor()), 1); }); - it('should handle DECSET/DECRST 1049 - clears alt buffer with erase attributes', () => { - handler.parse('\x1b[42m\x1b[?1049h'); + it('should handle DECSET/DECRST 1049 - clears alt buffer with erase attributes', async () => { + await handler.parseP('\x1b[42m\x1b[?1049h'); // Buffer should be filled with green background - expect(bufferService.buffer.lines.get(20)!.loadCell(10, new CellData()).getBgColor()).to.equal(2); + assert.equal(bufferService.buffer.lines.get(20)!.loadCell(10, new CellData()).getBgColor(), 2); }); }); describe('text attributes', () => { - it('bold', () => { - inputHandler.parse('\x1b[1m'); + it('bold', async () => { + await inputHandler.parseP('\x1b[1m'); assert.equal(!!inputHandler.curAttrData.isBold(), true); - inputHandler.parse('\x1b[22m'); + await inputHandler.parseP('\x1b[22m'); assert.equal(!!inputHandler.curAttrData.isBold(), false); }); - it('dim', () => { - inputHandler.parse('\x1b[2m'); + it('dim', async () => { + await inputHandler.parseP('\x1b[2m'); assert.equal(!!inputHandler.curAttrData.isDim(), true); - inputHandler.parse('\x1b[22m'); + await inputHandler.parseP('\x1b[22m'); assert.equal(!!inputHandler.curAttrData.isDim(), false); }); - it('italic', () => { - inputHandler.parse('\x1b[3m'); + it('italic', async () => { + await inputHandler.parseP('\x1b[3m'); assert.equal(!!inputHandler.curAttrData.isItalic(), true); - inputHandler.parse('\x1b[23m'); + await inputHandler.parseP('\x1b[23m'); assert.equal(!!inputHandler.curAttrData.isItalic(), false); }); - it('underline', () => { - inputHandler.parse('\x1b[4m'); + it('underline', async () => { + await inputHandler.parseP('\x1b[4m'); assert.equal(!!inputHandler.curAttrData.isUnderline(), true); - inputHandler.parse('\x1b[24m'); + await inputHandler.parseP('\x1b[24m'); assert.equal(!!inputHandler.curAttrData.isUnderline(), false); }); - it('blink', () => { - inputHandler.parse('\x1b[5m'); + it('blink', async () => { + await inputHandler.parseP('\x1b[5m'); assert.equal(!!inputHandler.curAttrData.isBlink(), true); - inputHandler.parse('\x1b[25m'); + await inputHandler.parseP('\x1b[25m'); assert.equal(!!inputHandler.curAttrData.isBlink(), false); }); - it('inverse', () => { - inputHandler.parse('\x1b[7m'); + it('inverse', async () => { + await inputHandler.parseP('\x1b[7m'); assert.equal(!!inputHandler.curAttrData.isInverse(), true); - inputHandler.parse('\x1b[27m'); + await inputHandler.parseP('\x1b[27m'); assert.equal(!!inputHandler.curAttrData.isInverse(), false); }); - it('invisible', () => { - inputHandler.parse('\x1b[8m'); + it('invisible', async () => { + await inputHandler.parseP('\x1b[8m'); assert.equal(!!inputHandler.curAttrData.isInvisible(), true); - inputHandler.parse('\x1b[28m'); + await inputHandler.parseP('\x1b[28m'); assert.equal(!!inputHandler.curAttrData.isInvisible(), false); }); - it('colormode palette 16', () => { + it('strikethrough', async () => { + await inputHandler.parseP('\x1b[9m'); + assert.equal(!!inputHandler.curAttrData.isStrikethrough(), true); + await inputHandler.parseP('\x1b[29m'); + assert.equal(!!inputHandler.curAttrData.isStrikethrough(), false); + }); + it('colormode palette 16', async () => { assert.equal(inputHandler.curAttrData.getFgColorMode(), 0); // DEFAULT assert.equal(inputHandler.curAttrData.getBgColorMode(), 0); // DEFAULT // lower 8 colors for (let i = 0; i < 8; ++i) { - inputHandler.parse(`\x1b[${i + 30};${i + 40}m`); + await inputHandler.parseP(`\x1b[${i + 30};${i + 40}m`); assert.equal(inputHandler.curAttrData.getFgColorMode(), Attributes.CM_P16); assert.equal(inputHandler.curAttrData.getFgColor(), i); assert.equal(inputHandler.curAttrData.getBgColorMode(), Attributes.CM_P16); assert.equal(inputHandler.curAttrData.getBgColor(), i); } // reset to DEFAULT - inputHandler.parse(`\x1b[39;49m`); + await inputHandler.parseP(`\x1b[39;49m`); assert.equal(inputHandler.curAttrData.getFgColorMode(), 0); assert.equal(inputHandler.curAttrData.getBgColorMode(), 0); }); - it('colormode palette 256', () => { + it('colormode palette 256', async () => { assert.equal(inputHandler.curAttrData.getFgColorMode(), 0); // DEFAULT assert.equal(inputHandler.curAttrData.getBgColorMode(), 0); // DEFAULT // lower 8 colors for (let i = 0; i < 256; ++i) { - inputHandler.parse(`\x1b[38;5;${i};48;5;${i}m`); + await inputHandler.parseP(`\x1b[38;5;${i};48;5;${i}m`); assert.equal(inputHandler.curAttrData.getFgColorMode(), Attributes.CM_P256); assert.equal(inputHandler.curAttrData.getFgColor(), i); assert.equal(inputHandler.curAttrData.getBgColorMode(), Attributes.CM_P256); assert.equal(inputHandler.curAttrData.getBgColor(), i); } // reset to DEFAULT - inputHandler.parse(`\x1b[39;49m`); + await inputHandler.parseP(`\x1b[39;49m`); assert.equal(inputHandler.curAttrData.getFgColorMode(), 0); assert.equal(inputHandler.curAttrData.getFgColor(), -1); assert.equal(inputHandler.curAttrData.getBgColorMode(), 0); assert.equal(inputHandler.curAttrData.getBgColor(), -1); }); - it('colormode RGB', () => { + it('colormode RGB', async () => { assert.equal(inputHandler.curAttrData.getFgColorMode(), 0); // DEFAULT assert.equal(inputHandler.curAttrData.getBgColorMode(), 0); // DEFAULT - inputHandler.parse(`\x1b[38;2;1;2;3;48;2;4;5;6m`); + await inputHandler.parseP(`\x1b[38;2;1;2;3;48;2;4;5;6m`); assert.equal(inputHandler.curAttrData.getFgColorMode(), Attributes.CM_RGB); assert.equal(inputHandler.curAttrData.getFgColor(), 1 << 16 | 2 << 8 | 3); assert.deepEqual(AttributeData.toColorRGB(inputHandler.curAttrData.getFgColor()), [1, 2, 3]); assert.equal(inputHandler.curAttrData.getBgColorMode(), Attributes.CM_RGB); assert.deepEqual(AttributeData.toColorRGB(inputHandler.curAttrData.getBgColor()), [4, 5, 6]); // reset to DEFAULT - inputHandler.parse(`\x1b[39;49m`); + await inputHandler.parseP(`\x1b[39;49m`); assert.equal(inputHandler.curAttrData.getFgColorMode(), 0); assert.equal(inputHandler.curAttrData.getFgColor(), -1); assert.equal(inputHandler.curAttrData.getBgColorMode(), 0); assert.equal(inputHandler.curAttrData.getBgColor(), -1); }); - it('colormode transition RGB to 256', () => { + it('colormode transition RGB to 256', async () => { // enter RGB for FG and BG - inputHandler.parse(`\x1b[38;2;1;2;3;48;2;4;5;6m`); + await inputHandler.parseP(`\x1b[38;2;1;2;3;48;2;4;5;6m`); // enter 256 for FG and BG - inputHandler.parse(`\x1b[38;5;255;48;5;255m`); + await inputHandler.parseP(`\x1b[38;5;255;48;5;255m`); assert.equal(inputHandler.curAttrData.getFgColorMode(), Attributes.CM_P256); assert.equal(inputHandler.curAttrData.getFgColor(), 255); assert.equal(inputHandler.curAttrData.getBgColorMode(), Attributes.CM_P256); assert.equal(inputHandler.curAttrData.getBgColor(), 255); }); - it('colormode transition RGB to 16', () => { + it('colormode transition RGB to 16', async () => { // enter RGB for FG and BG - inputHandler.parse(`\x1b[38;2;1;2;3;48;2;4;5;6m`); + await inputHandler.parseP(`\x1b[38;2;1;2;3;48;2;4;5;6m`); // enter 16 for FG and BG - inputHandler.parse(`\x1b[37;47m`); + await inputHandler.parseP(`\x1b[37;47m`); assert.equal(inputHandler.curAttrData.getFgColorMode(), Attributes.CM_P16); assert.equal(inputHandler.curAttrData.getFgColor(), 7); assert.equal(inputHandler.curAttrData.getBgColorMode(), Attributes.CM_P16); assert.equal(inputHandler.curAttrData.getBgColor(), 7); }); - it('colormode transition 16 to 256', () => { + it('colormode transition 16 to 256', async () => { // enter 16 for FG and BG - inputHandler.parse(`\x1b[37;47m`); + await inputHandler.parseP(`\x1b[37;47m`); // enter 256 for FG and BG - inputHandler.parse(`\x1b[38;5;255;48;5;255m`); + await inputHandler.parseP(`\x1b[38;5;255;48;5;255m`); assert.equal(inputHandler.curAttrData.getFgColorMode(), Attributes.CM_P256); assert.equal(inputHandler.curAttrData.getFgColor(), 255); assert.equal(inputHandler.curAttrData.getBgColorMode(), Attributes.CM_P256); assert.equal(inputHandler.curAttrData.getBgColor(), 255); }); - it('colormode transition 256 to 16', () => { + it('colormode transition 256 to 16', async () => { // enter 256 for FG and BG - inputHandler.parse(`\x1b[38;5;255;48;5;255m`); + await inputHandler.parseP(`\x1b[38;5;255;48;5;255m`); // enter 16 for FG and BG - inputHandler.parse(`\x1b[37;47m`); + await inputHandler.parseP(`\x1b[37;47m`); assert.equal(inputHandler.curAttrData.getFgColorMode(), Attributes.CM_P16); assert.equal(inputHandler.curAttrData.getFgColor(), 7); assert.equal(inputHandler.curAttrData.getBgColorMode(), Attributes.CM_P16); assert.equal(inputHandler.curAttrData.getBgColor(), 7); }); - it('should zero missing RGB values', () => { - inputHandler.parse(`\x1b[38;2;1;2;3m`); - inputHandler.parse(`\x1b[38;2;5m`); + it('should zero missing RGB values', async () => { + await inputHandler.parseP(`\x1b[38;2;1;2;3m`); + await inputHandler.parseP(`\x1b[38;2;5m`); assert.deepEqual(AttributeData.toColorRGB(inputHandler.curAttrData.getFgColor()), [5, 0, 0]); }); }); @@ -572,141 +744,141 @@ describe('InputHandler', () => { inputHandler2 = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockDirtyRowService(), new MockLogService(), optionsService, new MockCoreMouseService(), new MockUnicodeService()); }); describe('should equal to semicolon', () => { - it('CSI 38:2::50:100:150 m', () => { + it('CSI 38:2::50:100:150 m', async () => { inputHandler.curAttrData.fg = 0xFFFFFFFF; inputHandler2.curAttrData.fg = 0xFFFFFFFF; - inputHandler2.parse('\x1b[38;2;50;100;150m'); - inputHandler.parse('\x1b[38:2::50:100:150m'); + await inputHandler2.parseP('\x1b[38;2;50;100;150m'); + await inputHandler.parseP('\x1b[38:2::50:100:150m'); assert.equal(inputHandler2.curAttrData.fg & 0xFFFFFF, 50 << 16 | 100 << 8 | 150); assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg); }); - it('CSI 38:2::50:100: m', () => { + it('CSI 38:2::50:100: m', async () => { inputHandler.curAttrData.fg = 0xFFFFFFFF; inputHandler2.curAttrData.fg = 0xFFFFFFFF; - inputHandler2.parse('\x1b[38;2;50;100;m'); - inputHandler.parse('\x1b[38:2::50:100:m'); + await inputHandler2.parseP('\x1b[38;2;50;100;m'); + await inputHandler.parseP('\x1b[38:2::50:100:m'); assert.equal(inputHandler2.curAttrData.fg & 0xFFFFFF, 50 << 16 | 100 << 8 | 0); assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg); }); - it('CSI 38:2::50:: m', () => { + it('CSI 38:2::50:: m', async () => { inputHandler.curAttrData.fg = 0xFFFFFFFF; inputHandler2.curAttrData.fg = 0xFFFFFFFF; - inputHandler2.parse('\x1b[38;2;50;;m'); - inputHandler.parse('\x1b[38:2::50::m'); + await inputHandler2.parseP('\x1b[38;2;50;;m'); + await inputHandler.parseP('\x1b[38:2::50::m'); assert.equal(inputHandler2.curAttrData.fg & 0xFFFFFF, 50 << 16 | 0 << 8 | 0); assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg); }); - it('CSI 38:2:::: m', () => { + it('CSI 38:2:::: m', async () => { inputHandler.curAttrData.fg = 0xFFFFFFFF; inputHandler2.curAttrData.fg = 0xFFFFFFFF; - inputHandler2.parse('\x1b[38;2;;;m'); - inputHandler.parse('\x1b[38:2::::m'); + await inputHandler2.parseP('\x1b[38;2;;;m'); + await inputHandler.parseP('\x1b[38:2::::m'); assert.equal(inputHandler2.curAttrData.fg & 0xFFFFFF, 0 << 16 | 0 << 8 | 0); assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg); }); - it('CSI 38;2::50:100:150 m', () => { + it('CSI 38;2::50:100:150 m', async () => { inputHandler.curAttrData.fg = 0xFFFFFFFF; inputHandler2.curAttrData.fg = 0xFFFFFFFF; - inputHandler2.parse('\x1b[38;2;50;100;150m'); - inputHandler.parse('\x1b[38;2::50:100:150m'); + await inputHandler2.parseP('\x1b[38;2;50;100;150m'); + await inputHandler.parseP('\x1b[38;2::50:100:150m'); assert.equal(inputHandler2.curAttrData.fg & 0xFFFFFF, 50 << 16 | 100 << 8 | 150); assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg); }); - it('CSI 38;2;50:100:150 m', () => { + it('CSI 38;2;50:100:150 m', async () => { inputHandler.curAttrData.fg = 0xFFFFFFFF; inputHandler2.curAttrData.fg = 0xFFFFFFFF; - inputHandler2.parse('\x1b[38;2;50;100;150m'); - inputHandler.parse('\x1b[38;2;50:100:150m'); + await inputHandler2.parseP('\x1b[38;2;50;100;150m'); + await inputHandler.parseP('\x1b[38;2;50:100:150m'); assert.equal(inputHandler2.curAttrData.fg & 0xFFFFFF, 50 << 16 | 100 << 8 | 150); assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg); }); - it('CSI 38;2;50;100:150 m', () => { + it('CSI 38;2;50;100:150 m', async () => { inputHandler.curAttrData.fg = 0xFFFFFFFF; inputHandler2.curAttrData.fg = 0xFFFFFFFF; - inputHandler2.parse('\x1b[38;2;50;100;150m'); - inputHandler.parse('\x1b[38;2;50;100:150m'); + await inputHandler2.parseP('\x1b[38;2;50;100;150m'); + await inputHandler.parseP('\x1b[38;2;50;100:150m'); assert.equal(inputHandler2.curAttrData.fg & 0xFFFFFF, 50 << 16 | 100 << 8 | 150); assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg); }); - it('CSI 38:5:50 m', () => { + it('CSI 38:5:50 m', async () => { inputHandler.curAttrData.fg = 0xFFFFFFFF; inputHandler2.curAttrData.fg = 0xFFFFFFFF; - inputHandler2.parse('\x1b[38;5;50m'); - inputHandler.parse('\x1b[38:5:50m'); + await inputHandler2.parseP('\x1b[38;5;50m'); + await inputHandler.parseP('\x1b[38:5:50m'); assert.equal(inputHandler2.curAttrData.fg & 0xFF, 50); assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg); }); - it('CSI 38:5: m', () => { + it('CSI 38:5: m', async () => { inputHandler.curAttrData.fg = 0xFFFFFFFF; inputHandler2.curAttrData.fg = 0xFFFFFFFF; - inputHandler2.parse('\x1b[38;5;m'); - inputHandler.parse('\x1b[38:5:m'); + await inputHandler2.parseP('\x1b[38;5;m'); + await inputHandler.parseP('\x1b[38:5:m'); assert.equal(inputHandler2.curAttrData.fg & 0xFF, 0); assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg); }); - it('CSI 38;5:50 m', () => { + it('CSI 38;5:50 m', async () => { inputHandler.curAttrData.fg = 0xFFFFFFFF; inputHandler2.curAttrData.fg = 0xFFFFFFFF; - inputHandler2.parse('\x1b[38;5;50m'); - inputHandler.parse('\x1b[38;5:50m'); + await inputHandler2.parseP('\x1b[38;5;50m'); + await inputHandler.parseP('\x1b[38;5:50m'); assert.equal(inputHandler2.curAttrData.fg & 0xFF, 50); assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg); }); }); describe('should fill early sequence end with default of 0', () => { - it('CSI 38:2 m', () => { + it('CSI 38:2 m', async () => { inputHandler.curAttrData.fg = 0xFFFFFFFF; inputHandler2.curAttrData.fg = 0xFFFFFFFF; - inputHandler2.parse('\x1b[38;2m'); - inputHandler.parse('\x1b[38:2m'); + await inputHandler2.parseP('\x1b[38;2m'); + await inputHandler.parseP('\x1b[38:2m'); assert.equal(inputHandler2.curAttrData.fg & 0xFFFFFF, 0 << 16 | 0 << 8 | 0); assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg); }); - it('CSI 38:5 m', () => { + it('CSI 38:5 m', async () => { inputHandler.curAttrData.fg = 0xFFFFFFFF; inputHandler2.curAttrData.fg = 0xFFFFFFFF; - inputHandler2.parse('\x1b[38;5m'); - inputHandler.parse('\x1b[38:5m'); + await inputHandler2.parseP('\x1b[38;5m'); + await inputHandler.parseP('\x1b[38:5m'); assert.equal(inputHandler2.curAttrData.fg & 0xFF, 0); assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg); }); }); describe('should not interfere with leading/following SGR attrs', () => { - it('CSI 1 ; 38:2::50:100:150 ; 4 m', () => { - inputHandler2.parse('\x1b[1;38;2;50;100;150;4m'); - inputHandler.parse('\x1b[1;38:2::50:100:150;4m'); + it('CSI 1 ; 38:2::50:100:150 ; 4 m', async () => { + await inputHandler2.parseP('\x1b[1;38;2;50;100;150;4m'); + await inputHandler.parseP('\x1b[1;38:2::50:100:150;4m'); assert.equal(!!inputHandler2.curAttrData.isBold(), true); assert.equal(!!inputHandler2.curAttrData.isUnderline(), true); assert.equal(inputHandler2.curAttrData.fg & 0xFFFFFF, 50 << 16 | 100 << 8 | 150); assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg); }); - it('CSI 1 ; 38:2::50:100: ; 4 m', () => { - inputHandler2.parse('\x1b[1;38;2;50;100;;4m'); - inputHandler.parse('\x1b[1;38:2::50:100:;4m'); + it('CSI 1 ; 38:2::50:100: ; 4 m', async () => { + await inputHandler2.parseP('\x1b[1;38;2;50;100;;4m'); + await inputHandler.parseP('\x1b[1;38:2::50:100:;4m'); assert.equal(!!inputHandler2.curAttrData.isBold(), true); assert.equal(!!inputHandler2.curAttrData.isUnderline(), true); assert.equal(inputHandler2.curAttrData.fg & 0xFFFFFF, 50 << 16 | 100 << 8 | 0); assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg); }); - it('CSI 1 ; 38:2::50:100 ; 4 m', () => { - inputHandler2.parse('\x1b[1;38;2;50;100;;4m'); - inputHandler.parse('\x1b[1;38:2::50:100;4m'); + it('CSI 1 ; 38:2::50:100 ; 4 m', async () => { + await inputHandler2.parseP('\x1b[1;38;2;50;100;;4m'); + await inputHandler.parseP('\x1b[1;38:2::50:100;4m'); assert.equal(!!inputHandler2.curAttrData.isBold(), true); assert.equal(!!inputHandler2.curAttrData.isUnderline(), true); assert.equal(inputHandler2.curAttrData.fg & 0xFFFFFF, 50 << 16 | 100 << 8 | 0); assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg); }); - it('CSI 1 ; 38:2:: ; 4 m', () => { - inputHandler2.parse('\x1b[1;38;2;;;;4m'); - inputHandler.parse('\x1b[1;38:2::;4m'); + it('CSI 1 ; 38:2:: ; 4 m', async () => { + await inputHandler2.parseP('\x1b[1;38;2;;;;4m'); + await inputHandler.parseP('\x1b[1;38:2::;4m'); assert.equal(!!inputHandler2.curAttrData.isBold(), true); assert.equal(!!inputHandler2.curAttrData.isUnderline(), true); assert.equal(inputHandler2.curAttrData.fg & 0xFFFFFF, 0); assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg); }); - it('CSI 1 ; 38;2:: ; 4 m', () => { - inputHandler2.parse('\x1b[1;38;2;;;;4m'); - inputHandler.parse('\x1b[1;38;2::;4m'); + it('CSI 1 ; 38;2:: ; 4 m', async () => { + await inputHandler2.parseP('\x1b[1;38;2;;;;4m'); + await inputHandler.parseP('\x1b[1;38;2::;4m'); assert.equal(!!inputHandler2.curAttrData.isBold(), true); assert.equal(!!inputHandler2.curAttrData.isUnderline(), true); assert.equal(inputHandler2.curAttrData.fg & 0xFFFFFF, 0); @@ -718,372 +890,372 @@ describe('InputHandler', () => { beforeEach(() => { bufferService.resize(10, 10); }); - it('cursor forward (CUF)', () => { - inputHandler.parse('\x1b[C'); + it('cursor forward (CUF)', async () => { + await inputHandler.parseP('\x1b[C'); assert.deepEqual(getCursor(bufferService), [1, 0]); - inputHandler.parse('\x1b[1C'); + await inputHandler.parseP('\x1b[1C'); assert.deepEqual(getCursor(bufferService), [2, 0]); - inputHandler.parse('\x1b[4C'); + await inputHandler.parseP('\x1b[4C'); assert.deepEqual(getCursor(bufferService), [6, 0]); - inputHandler.parse('\x1b[100C'); + await inputHandler.parseP('\x1b[100C'); assert.deepEqual(getCursor(bufferService), [9, 0]); // should not change y bufferService.buffer.x = 8; bufferService.buffer.y = 4; - inputHandler.parse('\x1b[C'); + await inputHandler.parseP('\x1b[C'); assert.deepEqual(getCursor(bufferService), [9, 4]); }); - it('cursor backward (CUB)', () => { - inputHandler.parse('\x1b[D'); + it('cursor backward (CUB)', async () => { + await inputHandler.parseP('\x1b[D'); assert.deepEqual(getCursor(bufferService), [0, 0]); - inputHandler.parse('\x1b[1D'); + await inputHandler.parseP('\x1b[1D'); assert.deepEqual(getCursor(bufferService), [0, 0]); // place cursor at end of first line - inputHandler.parse('\x1b[100C'); - inputHandler.parse('\x1b[D'); + await inputHandler.parseP('\x1b[100C'); + await inputHandler.parseP('\x1b[D'); assert.deepEqual(getCursor(bufferService), [8, 0]); - inputHandler.parse('\x1b[1D'); + await inputHandler.parseP('\x1b[1D'); assert.deepEqual(getCursor(bufferService), [7, 0]); - inputHandler.parse('\x1b[4D'); + await inputHandler.parseP('\x1b[4D'); assert.deepEqual(getCursor(bufferService), [3, 0]); - inputHandler.parse('\x1b[100D'); + await inputHandler.parseP('\x1b[100D'); assert.deepEqual(getCursor(bufferService), [0, 0]); // should not change y bufferService.buffer.x = 4; bufferService.buffer.y = 4; - inputHandler.parse('\x1b[D'); + await inputHandler.parseP('\x1b[D'); assert.deepEqual(getCursor(bufferService), [3, 4]); }); - it('cursor down (CUD)', () => { - inputHandler.parse('\x1b[B'); + it('cursor down (CUD)', async () => { + await inputHandler.parseP('\x1b[B'); assert.deepEqual(getCursor(bufferService), [0, 1]); - inputHandler.parse('\x1b[1B'); + await inputHandler.parseP('\x1b[1B'); assert.deepEqual(getCursor(bufferService), [0, 2]); - inputHandler.parse('\x1b[4B'); + await inputHandler.parseP('\x1b[4B'); assert.deepEqual(getCursor(bufferService), [0, 6]); - inputHandler.parse('\x1b[100B'); + await inputHandler.parseP('\x1b[100B'); assert.deepEqual(getCursor(bufferService), [0, 9]); // should not change x bufferService.buffer.x = 8; bufferService.buffer.y = 0; - inputHandler.parse('\x1b[B'); + await inputHandler.parseP('\x1b[B'); assert.deepEqual(getCursor(bufferService), [8, 1]); }); - it('cursor up (CUU)', () => { - inputHandler.parse('\x1b[A'); + it('cursor up (CUU)', async () => { + await inputHandler.parseP('\x1b[A'); assert.deepEqual(getCursor(bufferService), [0, 0]); - inputHandler.parse('\x1b[1A'); + await inputHandler.parseP('\x1b[1A'); assert.deepEqual(getCursor(bufferService), [0, 0]); // place cursor at beginning of last row - inputHandler.parse('\x1b[100B'); - inputHandler.parse('\x1b[A'); + await inputHandler.parseP('\x1b[100B'); + await inputHandler.parseP('\x1b[A'); assert.deepEqual(getCursor(bufferService), [0, 8]); - inputHandler.parse('\x1b[1A'); + await inputHandler.parseP('\x1b[1A'); assert.deepEqual(getCursor(bufferService), [0, 7]); - inputHandler.parse('\x1b[4A'); + await inputHandler.parseP('\x1b[4A'); assert.deepEqual(getCursor(bufferService), [0, 3]); - inputHandler.parse('\x1b[100A'); + await inputHandler.parseP('\x1b[100A'); assert.deepEqual(getCursor(bufferService), [0, 0]); // should not change x bufferService.buffer.x = 8; bufferService.buffer.y = 9; - inputHandler.parse('\x1b[A'); + await inputHandler.parseP('\x1b[A'); assert.deepEqual(getCursor(bufferService), [8, 8]); }); - it('cursor next line (CNL)', () => { - inputHandler.parse('\x1b[E'); + it('cursor next line (CNL)', async () => { + await inputHandler.parseP('\x1b[E'); assert.deepEqual(getCursor(bufferService), [0, 1]); - inputHandler.parse('\x1b[1E'); + await inputHandler.parseP('\x1b[1E'); assert.deepEqual(getCursor(bufferService), [0, 2]); - inputHandler.parse('\x1b[4E'); + await inputHandler.parseP('\x1b[4E'); assert.deepEqual(getCursor(bufferService), [0, 6]); - inputHandler.parse('\x1b[100E'); + await inputHandler.parseP('\x1b[100E'); assert.deepEqual(getCursor(bufferService), [0, 9]); // should reset x to zero bufferService.buffer.x = 8; bufferService.buffer.y = 0; - inputHandler.parse('\x1b[E'); + await inputHandler.parseP('\x1b[E'); assert.deepEqual(getCursor(bufferService), [0, 1]); }); - it('cursor previous line (CPL)', () => { - inputHandler.parse('\x1b[F'); + it('cursor previous line (CPL)', async () => { + await inputHandler.parseP('\x1b[F'); assert.deepEqual(getCursor(bufferService), [0, 0]); - inputHandler.parse('\x1b[1F'); + await inputHandler.parseP('\x1b[1F'); assert.deepEqual(getCursor(bufferService), [0, 0]); // place cursor at beginning of last row - inputHandler.parse('\x1b[100E'); - inputHandler.parse('\x1b[F'); + await inputHandler.parseP('\x1b[100E'); + await inputHandler.parseP('\x1b[F'); assert.deepEqual(getCursor(bufferService), [0, 8]); - inputHandler.parse('\x1b[1F'); + await inputHandler.parseP('\x1b[1F'); assert.deepEqual(getCursor(bufferService), [0, 7]); - inputHandler.parse('\x1b[4F'); + await inputHandler.parseP('\x1b[4F'); assert.deepEqual(getCursor(bufferService), [0, 3]); - inputHandler.parse('\x1b[100F'); + await inputHandler.parseP('\x1b[100F'); assert.deepEqual(getCursor(bufferService), [0, 0]); // should reset x to zero bufferService.buffer.x = 8; bufferService.buffer.y = 9; - inputHandler.parse('\x1b[F'); + await inputHandler.parseP('\x1b[F'); assert.deepEqual(getCursor(bufferService), [0, 8]); }); - it('cursor character absolute (CHA)', () => { - inputHandler.parse('\x1b[G'); + it('cursor character absolute (CHA)', async () => { + await inputHandler.parseP('\x1b[G'); assert.deepEqual(getCursor(bufferService), [0, 0]); - inputHandler.parse('\x1b[1G'); + await inputHandler.parseP('\x1b[1G'); assert.deepEqual(getCursor(bufferService), [0, 0]); - inputHandler.parse('\x1b[2G'); + await inputHandler.parseP('\x1b[2G'); assert.deepEqual(getCursor(bufferService), [1, 0]); - inputHandler.parse('\x1b[5G'); + await inputHandler.parseP('\x1b[5G'); assert.deepEqual(getCursor(bufferService), [4, 0]); - inputHandler.parse('\x1b[100G'); + await inputHandler.parseP('\x1b[100G'); assert.deepEqual(getCursor(bufferService), [9, 0]); }); - it('cursor position (CUP)', () => { + it('cursor position (CUP)', async () => { bufferService.buffer.x = 5; bufferService.buffer.y = 5; - inputHandler.parse('\x1b[H'); + await inputHandler.parseP('\x1b[H'); assert.deepEqual(getCursor(bufferService), [0, 0]); bufferService.buffer.x = 5; bufferService.buffer.y = 5; - inputHandler.parse('\x1b[1H'); + await inputHandler.parseP('\x1b[1H'); assert.deepEqual(getCursor(bufferService), [0, 0]); bufferService.buffer.x = 5; bufferService.buffer.y = 5; - inputHandler.parse('\x1b[1;1H'); + await inputHandler.parseP('\x1b[1;1H'); assert.deepEqual(getCursor(bufferService), [0, 0]); bufferService.buffer.x = 5; bufferService.buffer.y = 5; - inputHandler.parse('\x1b[8H'); + await inputHandler.parseP('\x1b[8H'); assert.deepEqual(getCursor(bufferService), [0, 7]); bufferService.buffer.x = 5; bufferService.buffer.y = 5; - inputHandler.parse('\x1b[;8H'); + await inputHandler.parseP('\x1b[;8H'); assert.deepEqual(getCursor(bufferService), [7, 0]); bufferService.buffer.x = 5; bufferService.buffer.y = 5; - inputHandler.parse('\x1b[100;100H'); + await inputHandler.parseP('\x1b[100;100H'); assert.deepEqual(getCursor(bufferService), [9, 9]); }); - it('horizontal position absolute (HPA)', () => { - inputHandler.parse('\x1b[`'); + it('horizontal position absolute (HPA)', async () => { + await inputHandler.parseP('\x1b[`'); assert.deepEqual(getCursor(bufferService), [0, 0]); - inputHandler.parse('\x1b[1`'); + await inputHandler.parseP('\x1b[1`'); assert.deepEqual(getCursor(bufferService), [0, 0]); - inputHandler.parse('\x1b[2`'); + await inputHandler.parseP('\x1b[2`'); assert.deepEqual(getCursor(bufferService), [1, 0]); - inputHandler.parse('\x1b[5`'); + await inputHandler.parseP('\x1b[5`'); assert.deepEqual(getCursor(bufferService), [4, 0]); - inputHandler.parse('\x1b[100`'); + await inputHandler.parseP('\x1b[100`'); assert.deepEqual(getCursor(bufferService), [9, 0]); }); - it('horizontal position relative (HPR)', () => { - inputHandler.parse('\x1b[a'); + it('horizontal position relative (HPR)', async () => { + await inputHandler.parseP('\x1b[a'); assert.deepEqual(getCursor(bufferService), [1, 0]); - inputHandler.parse('\x1b[1a'); + await inputHandler.parseP('\x1b[1a'); assert.deepEqual(getCursor(bufferService), [2, 0]); - inputHandler.parse('\x1b[4a'); + await inputHandler.parseP('\x1b[4a'); assert.deepEqual(getCursor(bufferService), [6, 0]); - inputHandler.parse('\x1b[100a'); + await inputHandler.parseP('\x1b[100a'); assert.deepEqual(getCursor(bufferService), [9, 0]); // should not change y bufferService.buffer.x = 8; bufferService.buffer.y = 4; - inputHandler.parse('\x1b[a'); + await inputHandler.parseP('\x1b[a'); assert.deepEqual(getCursor(bufferService), [9, 4]); }); - it('vertical position absolute (VPA)', () => { - inputHandler.parse('\x1b[d'); + it('vertical position absolute (VPA)', async () => { + await inputHandler.parseP('\x1b[d'); assert.deepEqual(getCursor(bufferService), [0, 0]); - inputHandler.parse('\x1b[1d'); + await inputHandler.parseP('\x1b[1d'); assert.deepEqual(getCursor(bufferService), [0, 0]); - inputHandler.parse('\x1b[2d'); + await inputHandler.parseP('\x1b[2d'); assert.deepEqual(getCursor(bufferService), [0, 1]); - inputHandler.parse('\x1b[5d'); + await inputHandler.parseP('\x1b[5d'); assert.deepEqual(getCursor(bufferService), [0, 4]); - inputHandler.parse('\x1b[100d'); + await inputHandler.parseP('\x1b[100d'); assert.deepEqual(getCursor(bufferService), [0, 9]); // should not change x bufferService.buffer.x = 8; bufferService.buffer.y = 4; - inputHandler.parse('\x1b[d'); + await inputHandler.parseP('\x1b[d'); assert.deepEqual(getCursor(bufferService), [8, 0]); }); - it('vertical position relative (VPR)', () => { - inputHandler.parse('\x1b[e'); + it('vertical position relative (VPR)', async () => { + await inputHandler.parseP('\x1b[e'); assert.deepEqual(getCursor(bufferService), [0, 1]); - inputHandler.parse('\x1b[1e'); + await inputHandler.parseP('\x1b[1e'); assert.deepEqual(getCursor(bufferService), [0, 2]); - inputHandler.parse('\x1b[4e'); + await inputHandler.parseP('\x1b[4e'); assert.deepEqual(getCursor(bufferService), [0, 6]); - inputHandler.parse('\x1b[100e'); + await inputHandler.parseP('\x1b[100e'); assert.deepEqual(getCursor(bufferService), [0, 9]); // should not change x bufferService.buffer.x = 8; bufferService.buffer.y = 4; - inputHandler.parse('\x1b[e'); + await inputHandler.parseP('\x1b[e'); assert.deepEqual(getCursor(bufferService), [8, 5]); }); describe('should clamp cursor into addressible range', () => { - it('CUF', () => { + it('CUF', async () => { bufferService.buffer.x = 10000; bufferService.buffer.y = 10000; - inputHandler.parse('\x1b[C'); + await inputHandler.parseP('\x1b[C'); assert.deepEqual(getCursor(bufferService), [9, 9]); bufferService.buffer.x = -10000; bufferService.buffer.y = -10000; - inputHandler.parse('\x1b[C'); + await inputHandler.parseP('\x1b[C'); assert.deepEqual(getCursor(bufferService), [1, 0]); }); - it('CUB', () => { + it('CUB', async () => { bufferService.buffer.x = 10000; bufferService.buffer.y = 10000; - inputHandler.parse('\x1b[D'); + await inputHandler.parseP('\x1b[D'); assert.deepEqual(getCursor(bufferService), [8, 9]); bufferService.buffer.x = -10000; bufferService.buffer.y = -10000; - inputHandler.parse('\x1b[D'); + await inputHandler.parseP('\x1b[D'); assert.deepEqual(getCursor(bufferService), [0, 0]); }); - it('CUD', () => { + it('CUD', async () => { bufferService.buffer.x = 10000; bufferService.buffer.y = 10000; - inputHandler.parse('\x1b[B'); + await inputHandler.parseP('\x1b[B'); assert.deepEqual(getCursor(bufferService), [9, 9]); bufferService.buffer.x = -10000; bufferService.buffer.y = -10000; - inputHandler.parse('\x1b[B'); + await inputHandler.parseP('\x1b[B'); assert.deepEqual(getCursor(bufferService), [0, 1]); }); - it('CUU', () => { + it('CUU', async () => { bufferService.buffer.x = 10000; bufferService.buffer.y = 10000; - inputHandler.parse('\x1b[A'); + await inputHandler.parseP('\x1b[A'); assert.deepEqual(getCursor(bufferService), [9, 8]); bufferService.buffer.x = -10000; bufferService.buffer.y = -10000; - inputHandler.parse('\x1b[A'); + await inputHandler.parseP('\x1b[A'); assert.deepEqual(getCursor(bufferService), [0, 0]); }); - it('CNL', () => { + it('CNL', async () => { bufferService.buffer.x = 10000; bufferService.buffer.y = 10000; - inputHandler.parse('\x1b[E'); + await inputHandler.parseP('\x1b[E'); assert.deepEqual(getCursor(bufferService), [0, 9]); bufferService.buffer.x = -10000; bufferService.buffer.y = -10000; - inputHandler.parse('\x1b[E'); + await inputHandler.parseP('\x1b[E'); assert.deepEqual(getCursor(bufferService), [0, 1]); }); - it('CPL', () => { + it('CPL', async () => { bufferService.buffer.x = 10000; bufferService.buffer.y = 10000; - inputHandler.parse('\x1b[F'); + await inputHandler.parseP('\x1b[F'); assert.deepEqual(getCursor(bufferService), [0, 8]); bufferService.buffer.x = -10000; bufferService.buffer.y = -10000; - inputHandler.parse('\x1b[F'); + await inputHandler.parseP('\x1b[F'); assert.deepEqual(getCursor(bufferService), [0, 0]); }); - it('CHA', () => { + it('CHA', async () => { bufferService.buffer.x = 10000; bufferService.buffer.y = 10000; - inputHandler.parse('\x1b[5G'); + await inputHandler.parseP('\x1b[5G'); assert.deepEqual(getCursor(bufferService), [4, 9]); bufferService.buffer.x = -10000; bufferService.buffer.y = -10000; - inputHandler.parse('\x1b[5G'); + await inputHandler.parseP('\x1b[5G'); assert.deepEqual(getCursor(bufferService), [4, 0]); }); - it('CUP', () => { + it('CUP', async () => { bufferService.buffer.x = 10000; bufferService.buffer.y = 10000; - inputHandler.parse('\x1b[5;5H'); + await inputHandler.parseP('\x1b[5;5H'); assert.deepEqual(getCursor(bufferService), [4, 4]); bufferService.buffer.x = -10000; bufferService.buffer.y = -10000; - inputHandler.parse('\x1b[5;5H'); + await inputHandler.parseP('\x1b[5;5H'); assert.deepEqual(getCursor(bufferService), [4, 4]); }); - it('HPA', () => { + it('HPA', async () => { bufferService.buffer.x = 10000; bufferService.buffer.y = 10000; - inputHandler.parse('\x1b[5`'); + await inputHandler.parseP('\x1b[5`'); assert.deepEqual(getCursor(bufferService), [4, 9]); bufferService.buffer.x = -10000; bufferService.buffer.y = -10000; - inputHandler.parse('\x1b[5`'); + await inputHandler.parseP('\x1b[5`'); assert.deepEqual(getCursor(bufferService), [4, 0]); }); - it('HPR', () => { + it('HPR', async () => { bufferService.buffer.x = 10000; bufferService.buffer.y = 10000; - inputHandler.parse('\x1b[a'); + await inputHandler.parseP('\x1b[a'); assert.deepEqual(getCursor(bufferService), [9, 9]); bufferService.buffer.x = -10000; bufferService.buffer.y = -10000; - inputHandler.parse('\x1b[a'); + await inputHandler.parseP('\x1b[a'); assert.deepEqual(getCursor(bufferService), [1, 0]); }); - it('VPA', () => { + it('VPA', async () => { bufferService.buffer.x = 10000; bufferService.buffer.y = 10000; - inputHandler.parse('\x1b[5d'); + await inputHandler.parseP('\x1b[5d'); assert.deepEqual(getCursor(bufferService), [9, 4]); bufferService.buffer.x = -10000; bufferService.buffer.y = -10000; - inputHandler.parse('\x1b[5d'); + await inputHandler.parseP('\x1b[5d'); assert.deepEqual(getCursor(bufferService), [0, 4]); }); - it('VPR', () => { + it('VPR', async () => { bufferService.buffer.x = 10000; bufferService.buffer.y = 10000; - inputHandler.parse('\x1b[e'); + await inputHandler.parseP('\x1b[e'); assert.deepEqual(getCursor(bufferService), [9, 9]); bufferService.buffer.x = -10000; bufferService.buffer.y = -10000; - inputHandler.parse('\x1b[e'); + await inputHandler.parseP('\x1b[e'); assert.deepEqual(getCursor(bufferService), [0, 1]); }); - it('DCH', () => { + it('DCH', async () => { bufferService.buffer.x = 10000; bufferService.buffer.y = 10000; - inputHandler.parse('\x1b[P'); + await inputHandler.parseP('\x1b[P'); assert.deepEqual(getCursor(bufferService), [9, 9]); bufferService.buffer.x = -10000; bufferService.buffer.y = -10000; - inputHandler.parse('\x1b[P'); + await inputHandler.parseP('\x1b[P'); assert.deepEqual(getCursor(bufferService), [0, 0]); }); - it('DCH - should delete last cell', () => { - inputHandler.parse('0123456789\x1b[P'); + it('DCH - should delete last cell', async () => { + await inputHandler.parseP('0123456789\x1b[P'); assert.equal(bufferService.buffer.lines.get(0)!.translateToString(false), '012345678 '); }); - it('ECH', () => { + it('ECH', async () => { bufferService.buffer.x = 10000; bufferService.buffer.y = 10000; - inputHandler.parse('\x1b[X'); + await inputHandler.parseP('\x1b[X'); assert.deepEqual(getCursor(bufferService), [9, 9]); bufferService.buffer.x = -10000; bufferService.buffer.y = -10000; - inputHandler.parse('\x1b[X'); + await inputHandler.parseP('\x1b[X'); assert.deepEqual(getCursor(bufferService), [0, 0]); }); - it('ECH - should delete last cell', () => { - inputHandler.parse('0123456789\x1b[X'); + it('ECH - should delete last cell', async () => { + await inputHandler.parseP('0123456789\x1b[X'); assert.equal(bufferService.buffer.lines.get(0)!.translateToString(false), '012345678 '); }); - it('ICH', () => { + it('ICH', async () => { bufferService.buffer.x = 10000; bufferService.buffer.y = 10000; - inputHandler.parse('\x1b[@'); + await inputHandler.parseP('\x1b[@'); assert.deepEqual(getCursor(bufferService), [9, 9]); bufferService.buffer.x = -10000; bufferService.buffer.y = -10000; - inputHandler.parse('\x1b[@'); + await inputHandler.parseP('\x1b[@'); assert.deepEqual(getCursor(bufferService), [0, 0]); }); - it('ICH - should delete last cell', () => { - inputHandler.parse('0123456789\x1b[@'); + it('ICH - should delete last cell', async () => { + await inputHandler.parseP('0123456789\x1b[@'); assert.equal(bufferService.buffer.lines.get(0)!.translateToString(false), '012345678 '); }); }); @@ -1092,31 +1264,31 @@ describe('InputHandler', () => { beforeEach(() => { bufferService.resize(10, 10); }); - it('should default to whole viewport', () => { - inputHandler.parse('\x1b[r'); + it('should default to whole viewport', async () => { + await inputHandler.parseP('\x1b[r'); assert.equal(bufferService.buffer.scrollTop, 0); assert.equal(bufferService.buffer.scrollBottom, 9); - inputHandler.parse('\x1b[3;7r'); + await inputHandler.parseP('\x1b[3;7r'); assert.equal(bufferService.buffer.scrollTop, 2); assert.equal(bufferService.buffer.scrollBottom, 6); - inputHandler.parse('\x1b[0;0r'); + await inputHandler.parseP('\x1b[0;0r'); assert.equal(bufferService.buffer.scrollTop, 0); assert.equal(bufferService.buffer.scrollBottom, 9); }); - it('should clamp bottom', () => { - inputHandler.parse('\x1b[3;1000r'); + it('should clamp bottom', async () => { + await inputHandler.parseP('\x1b[3;1000r'); assert.equal(bufferService.buffer.scrollTop, 2); assert.equal(bufferService.buffer.scrollBottom, 9); }); - it('should only apply for top < bottom', () => { - inputHandler.parse('\x1b[7;2r'); + it('should only apply for top < bottom', async () => { + await inputHandler.parseP('\x1b[7;2r'); assert.equal(bufferService.buffer.scrollTop, 0); assert.equal(bufferService.buffer.scrollBottom, 9); }); - it('should home cursor', () => { + it('should home cursor', async () => { bufferService.buffer.x = 10000; bufferService.buffer.y = 10000; - inputHandler.parse('\x1b[2;7r'); + await inputHandler.parseP('\x1b[2;7r'); assert.deepEqual(getCursor(bufferService), [0, 0]); }); }); @@ -1124,76 +1296,76 @@ describe('InputHandler', () => { beforeEach(() => { bufferService.resize(10, 10); }); - it('scrollUp', () => { - inputHandler.parse('0\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\x1b[2;4r\x1b[2Sm'); + it('scrollUp', async () => { + await inputHandler.parseP('0\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\x1b[2;4r\x1b[2Sm'); assert.deepEqual(getLines(bufferService), ['m', '3', '', '', '4', '5', '6', '7', '8', '9']); }); - it('scrollDown', () => { - inputHandler.parse('0\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\x1b[2;4r\x1b[2Tm'); + it('scrollDown', async () => { + await inputHandler.parseP('0\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\x1b[2;4r\x1b[2Tm'); assert.deepEqual(getLines(bufferService), ['m', '', '', '1', '4', '5', '6', '7', '8', '9']); }); - it('insertLines - out of margins', () => { - inputHandler.parse('0\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\x1b[3;6r'); + it('insertLines - out of margins', async () => { + await inputHandler.parseP('0\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\x1b[3;6r'); assert.equal(bufferService.buffer.scrollTop, 2); assert.equal(bufferService.buffer.scrollBottom, 5); - inputHandler.parse('\x1b[2Lm'); + await inputHandler.parseP('\x1b[2Lm'); assert.deepEqual(getLines(bufferService), ['m', '1', '2', '3', '4', '5', '6', '7', '8', '9']); - inputHandler.parse('\x1b[2H\x1b[2Ln'); + await inputHandler.parseP('\x1b[2H\x1b[2Ln'); assert.deepEqual(getLines(bufferService), ['m', 'n', '2', '3', '4', '5', '6', '7', '8', '9']); // skip below scrollbottom - inputHandler.parse('\x1b[7H\x1b[2Lo'); + await inputHandler.parseP('\x1b[7H\x1b[2Lo'); assert.deepEqual(getLines(bufferService), ['m', 'n', '2', '3', '4', '5', 'o', '7', '8', '9']); - inputHandler.parse('\x1b[8H\x1b[2Lp'); + await inputHandler.parseP('\x1b[8H\x1b[2Lp'); assert.deepEqual(getLines(bufferService), ['m', 'n', '2', '3', '4', '5', 'o', 'p', '8', '9']); - inputHandler.parse('\x1b[100H\x1b[2Lq'); + await inputHandler.parseP('\x1b[100H\x1b[2Lq'); assert.deepEqual(getLines(bufferService), ['m', 'n', '2', '3', '4', '5', 'o', 'p', '8', 'q']); }); - it('insertLines - within margins', () => { - inputHandler.parse('0\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\x1b[3;6r'); + it('insertLines - within margins', async () => { + await inputHandler.parseP('0\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\x1b[3;6r'); assert.equal(bufferService.buffer.scrollTop, 2); assert.equal(bufferService.buffer.scrollBottom, 5); - inputHandler.parse('\x1b[3H\x1b[2Lm'); + await inputHandler.parseP('\x1b[3H\x1b[2Lm'); assert.deepEqual(getLines(bufferService), ['0', '1', 'm', '', '2', '3', '6', '7', '8', '9']); - inputHandler.parse('\x1b[6H\x1b[2Ln'); + await inputHandler.parseP('\x1b[6H\x1b[2Ln'); assert.deepEqual(getLines(bufferService), ['0', '1', 'm', '', '2', 'n', '6', '7', '8', '9']); }); - it('deleteLines - out of margins', () => { - inputHandler.parse('0\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\x1b[3;6r'); + it('deleteLines - out of margins', async () => { + await inputHandler.parseP('0\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\x1b[3;6r'); assert.equal(bufferService.buffer.scrollTop, 2); assert.equal(bufferService.buffer.scrollBottom, 5); - inputHandler.parse('\x1b[2Mm'); + await inputHandler.parseP('\x1b[2Mm'); assert.deepEqual(getLines(bufferService), ['m', '1', '2', '3', '4', '5', '6', '7', '8', '9']); - inputHandler.parse('\x1b[2H\x1b[2Mn'); + await inputHandler.parseP('\x1b[2H\x1b[2Mn'); assert.deepEqual(getLines(bufferService), ['m', 'n', '2', '3', '4', '5', '6', '7', '8', '9']); // skip below scrollbottom - inputHandler.parse('\x1b[7H\x1b[2Mo'); + await inputHandler.parseP('\x1b[7H\x1b[2Mo'); assert.deepEqual(getLines(bufferService), ['m', 'n', '2', '3', '4', '5', 'o', '7', '8', '9']); - inputHandler.parse('\x1b[8H\x1b[2Mp'); + await inputHandler.parseP('\x1b[8H\x1b[2Mp'); assert.deepEqual(getLines(bufferService), ['m', 'n', '2', '3', '4', '5', 'o', 'p', '8', '9']); - inputHandler.parse('\x1b[100H\x1b[2Mq'); + await inputHandler.parseP('\x1b[100H\x1b[2Mq'); assert.deepEqual(getLines(bufferService), ['m', 'n', '2', '3', '4', '5', 'o', 'p', '8', 'q']); }); - it('deleteLines - within margins', () => { - inputHandler.parse('0\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\x1b[3;6r'); + it('deleteLines - within margins', async () => { + await inputHandler.parseP('0\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\x1b[3;6r'); assert.equal(bufferService.buffer.scrollTop, 2); assert.equal(bufferService.buffer.scrollBottom, 5); - inputHandler.parse('\x1b[6H\x1b[2Mm'); + await inputHandler.parseP('\x1b[6H\x1b[2Mm'); assert.deepEqual(getLines(bufferService), ['0', '1', '2', '3', '4', 'm', '6', '7', '8', '9']); - inputHandler.parse('\x1b[3H\x1b[2Mn'); - assert.deepEqual(getLines(bufferService), ['0', '1', 'n', 'm', '', '', '6', '7', '8', '9']); + await inputHandler.parseP('\x1b[3H\x1b[2Mn'); + assert.deepEqual(getLines(bufferService), ['0', '1', 'n', 'm', '', '', '6', '7', '8', '9']); }); }); - it('should parse big chunks in smaller subchunks', () => { + it('should parse big chunks in smaller subchunks', async () => { // max single chunk size is hardcoded as 131072 const calls: any[] = []; bufferService.resize(10, 10); (inputHandler as any)._parser.parse = (data: Uint32Array, length: number) => { calls.push([data.length, length]); }; - inputHandler.parse('12345'); - inputHandler.parse('a'.repeat(10000)); - inputHandler.parse('a'.repeat(200000)); - inputHandler.parse('a'.repeat(300000)); + await inputHandler.parseP('12345'); + await inputHandler.parseP('a'.repeat(10000)); + await inputHandler.parseP('a'.repeat(200000)); + await inputHandler.parseP('a'.repeat(300000)); assert.deepEqual(calls, [ [4096, 5], [10000, 10000], @@ -1202,187 +1374,187 @@ describe('InputHandler', () => { ]); }); describe('windowOptions', () => { - it('all should be disabled by default and not report', () => { + it('all should be disabled by default and not report', async () => { bufferService.resize(10, 10); const stack: string[] = []; coreService.onData(data => stack.push(data)); - inputHandler.parse('\x1b[14t'); - inputHandler.parse('\x1b[16t'); - inputHandler.parse('\x1b[18t'); - inputHandler.parse('\x1b[20t'); - inputHandler.parse('\x1b[21t'); + await inputHandler.parseP('\x1b[14t'); + await inputHandler.parseP('\x1b[16t'); + await inputHandler.parseP('\x1b[18t'); + await inputHandler.parseP('\x1b[20t'); + await inputHandler.parseP('\x1b[21t'); assert.deepEqual(stack, []); }); - it('14 - GetWinSizePixels', () => { + it('14 - GetWinSizePixels', async () => { bufferService.resize(10, 10); optionsService.options.windowOptions.getWinSizePixels = true; const stack: string[] = []; coreService.onData(data => stack.push(data)); - inputHandler.parse('\x1b[14t'); + await inputHandler.parseP('\x1b[14t'); // does not report in test terminal due to missing renderer assert.deepEqual(stack, []); }); - it('16 - GetCellSizePixels', () => { + it('16 - GetCellSizePixels', async () => { bufferService.resize(10, 10); optionsService.options.windowOptions.getCellSizePixels = true; const stack: string[] = []; coreService.onData(data => stack.push(data)); - inputHandler.parse('\x1b[16t'); + await inputHandler.parseP('\x1b[16t'); // does not report in test terminal due to missing renderer assert.deepEqual(stack, []); }); - it('18 - GetWinSizeChars', () => { + it('18 - GetWinSizeChars', async () => { bufferService.resize(10, 10); optionsService.options.windowOptions.getWinSizeChars = true; const stack: string[] = []; coreService.onData(data => stack.push(data)); - inputHandler.parse('\x1b[18t'); + await inputHandler.parseP('\x1b[18t'); assert.deepEqual(stack, ['\x1b[8;10;10t']); bufferService.resize(50, 20); - inputHandler.parse('\x1b[18t'); + await inputHandler.parseP('\x1b[18t'); assert.deepEqual(stack, ['\x1b[8;10;10t', '\x1b[8;20;50t']); }); - it('22/23 - PushTitle/PopTitle', () => { + it('22/23 - PushTitle/PopTitle', async () => { bufferService.resize(10, 10); optionsService.options.windowOptions.pushTitle = true; optionsService.options.windowOptions.popTitle = true; const stack: string[] = []; inputHandler.onTitleChange(data => stack.push(data)); - inputHandler.parse('\x1b]0;1\x07'); - inputHandler.parse('\x1b[22t'); - inputHandler.parse('\x1b]0;2\x07'); - inputHandler.parse('\x1b[22t'); - inputHandler.parse('\x1b]0;3\x07'); - inputHandler.parse('\x1b[22t'); + await inputHandler.parseP('\x1b]0;1\x07'); + await inputHandler.parseP('\x1b[22t'); + await inputHandler.parseP('\x1b]0;2\x07'); + await inputHandler.parseP('\x1b[22t'); + await inputHandler.parseP('\x1b]0;3\x07'); + await inputHandler.parseP('\x1b[22t'); assert.deepEqual(inputHandler.windowTitleStack, ['1', '2', '3']); assert.deepEqual(inputHandler.iconNameStack, ['1', '2', '3']); assert.deepEqual(stack, ['1', '2', '3']); - inputHandler.parse('\x1b[23t'); - inputHandler.parse('\x1b[23t'); - inputHandler.parse('\x1b[23t'); - inputHandler.parse('\x1b[23t'); // one more to test "overflow" + await inputHandler.parseP('\x1b[23t'); + await inputHandler.parseP('\x1b[23t'); + await inputHandler.parseP('\x1b[23t'); + await inputHandler.parseP('\x1b[23t'); // one more to test "overflow" assert.deepEqual(inputHandler.windowTitleStack, []); assert.deepEqual(inputHandler.iconNameStack, []); assert.deepEqual(stack, ['1', '2', '3', '3', '2', '1']); }); - it('22/23 - PushTitle/PopTitle with ;1', () => { + it('22/23 - PushTitle/PopTitle with ;1', async () => { bufferService.resize(10, 10); optionsService.options.windowOptions.pushTitle = true; optionsService.options.windowOptions.popTitle = true; const stack: string[] = []; inputHandler.onTitleChange(data => stack.push(data)); - inputHandler.parse('\x1b]0;1\x07'); - inputHandler.parse('\x1b[22;1t'); - inputHandler.parse('\x1b]0;2\x07'); - inputHandler.parse('\x1b[22;1t'); - inputHandler.parse('\x1b]0;3\x07'); - inputHandler.parse('\x1b[22;1t'); + await inputHandler.parseP('\x1b]0;1\x07'); + await inputHandler.parseP('\x1b[22;1t'); + await inputHandler.parseP('\x1b]0;2\x07'); + await inputHandler.parseP('\x1b[22;1t'); + await inputHandler.parseP('\x1b]0;3\x07'); + await inputHandler.parseP('\x1b[22;1t'); assert.deepEqual(inputHandler.windowTitleStack, []); assert.deepEqual(inputHandler.iconNameStack, ['1', '2', '3']); assert.deepEqual(stack, ['1', '2', '3']); - inputHandler.parse('\x1b[23;1t'); - inputHandler.parse('\x1b[23;1t'); - inputHandler.parse('\x1b[23;1t'); - inputHandler.parse('\x1b[23;1t'); // one more to test "overflow" + await inputHandler.parseP('\x1b[23;1t'); + await inputHandler.parseP('\x1b[23;1t'); + await inputHandler.parseP('\x1b[23;1t'); + await inputHandler.parseP('\x1b[23;1t'); // one more to test "overflow" assert.deepEqual(inputHandler.windowTitleStack, []); assert.deepEqual(inputHandler.iconNameStack, []); assert.deepEqual(stack, ['1', '2', '3']); }); - it('22/23 - PushTitle/PopTitle with ;2', () => { + it('22/23 - PushTitle/PopTitle with ;2', async () => { bufferService.resize(10, 10); optionsService.options.windowOptions.pushTitle = true; optionsService.options.windowOptions.popTitle = true; const stack: string[] = []; inputHandler.onTitleChange(data => stack.push(data)); - inputHandler.parse('\x1b]0;1\x07'); - inputHandler.parse('\x1b[22;2t'); - inputHandler.parse('\x1b]0;2\x07'); - inputHandler.parse('\x1b[22;2t'); - inputHandler.parse('\x1b]0;3\x07'); - inputHandler.parse('\x1b[22;2t'); + await inputHandler.parseP('\x1b]0;1\x07'); + await inputHandler.parseP('\x1b[22;2t'); + await inputHandler.parseP('\x1b]0;2\x07'); + await inputHandler.parseP('\x1b[22;2t'); + await inputHandler.parseP('\x1b]0;3\x07'); + await inputHandler.parseP('\x1b[22;2t'); assert.deepEqual(inputHandler.windowTitleStack, ['1', '2', '3']); assert.deepEqual(inputHandler.iconNameStack, []); assert.deepEqual(stack, ['1', '2', '3']); - inputHandler.parse('\x1b[23;2t'); - inputHandler.parse('\x1b[23;2t'); - inputHandler.parse('\x1b[23;2t'); - inputHandler.parse('\x1b[23;2t'); // one more to test "overflow" + await inputHandler.parseP('\x1b[23;2t'); + await inputHandler.parseP('\x1b[23;2t'); + await inputHandler.parseP('\x1b[23;2t'); + await inputHandler.parseP('\x1b[23;2t'); // one more to test "overflow" assert.deepEqual(inputHandler.windowTitleStack, []); assert.deepEqual(inputHandler.iconNameStack, []); assert.deepEqual(stack, ['1', '2', '3', '3', '2', '1']); }); - it('DECCOLM - should only work with "SetWinLines" (24) enabled', () => { + it('DECCOLM - should only work with "SetWinLines" (24) enabled', async () => { // disabled bufferService.resize(10, 10); - inputHandler.parse('\x1b[?3l'); + await inputHandler.parseP('\x1b[?3l'); assert.equal(bufferService.cols, 10); - inputHandler.parse('\x1b[?3h'); + await inputHandler.parseP('\x1b[?3h'); assert.equal(bufferService.cols, 10); // enabled inputHandler.reset(); optionsService.options.windowOptions.setWinLines = true; - inputHandler.parse('\x1b[?3l'); + await inputHandler.parseP('\x1b[?3l'); assert.equal(bufferService.cols, 80); - inputHandler.parse('\x1b[?3h'); + await inputHandler.parseP('\x1b[?3h'); assert.equal(bufferService.cols, 132); }); }); describe('should correctly reset cells taken by wide chars', () => { - beforeEach(() => { + beforeEach(async () => { bufferService.resize(10, 5); optionsService.options.scrollback = 1; - inputHandler.parse('¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥'); + await inputHandler.parseP('¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥'); }); - it('print', () => { - inputHandler.parse('\x1b[H#'); + it('print', async () => { + await inputHandler.parseP('\x1b[H#'); assert.deepEqual(getLines(bufferService), ['# ¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); - inputHandler.parse('\x1b[1;6H######'); + await inputHandler.parseP('\x1b[1;6H######'); assert.deepEqual(getLines(bufferService), ['# ¥ #####', '# ¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); - inputHandler.parse('#'); + await inputHandler.parseP('#'); assert.deepEqual(getLines(bufferService), ['# ¥ #####', '##¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); - inputHandler.parse('#'); + await inputHandler.parseP('#'); assert.deepEqual(getLines(bufferService), ['# ¥ #####', '### ¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); - inputHandler.parse('\x1b[3;9H#'); + await inputHandler.parseP('\x1b[3;9H#'); assert.deepEqual(getLines(bufferService), ['# ¥ #####', '### ¥¥¥', '¥¥¥¥#', '¥¥¥¥¥', '']); - inputHandler.parse('#'); + await inputHandler.parseP('#'); assert.deepEqual(getLines(bufferService), ['# ¥ #####', '### ¥¥¥', '¥¥¥¥##', '¥¥¥¥¥', '']); - inputHandler.parse('#'); + await inputHandler.parseP('#'); assert.deepEqual(getLines(bufferService), ['# ¥ #####', '### ¥¥¥', '¥¥¥¥##', '# ¥¥¥¥', '']); - inputHandler.parse('\x1b[4;10H#'); + await inputHandler.parseP('\x1b[4;10H#'); assert.deepEqual(getLines(bufferService), ['# ¥ #####', '### ¥¥¥', '¥¥¥¥##', '# ¥¥¥ #', '']); }); - it('EL', () => { - inputHandler.parse('\x1b[1;6H\x1b[K#'); + it('EL', async () => { + await inputHandler.parseP('\x1b[1;6H\x1b[K#'); assert.deepEqual(getLines(bufferService), ['¥¥ #', '¥¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); - inputHandler.parse('\x1b[2;5H\x1b[1K'); + await inputHandler.parseP('\x1b[2;5H\x1b[1K'); assert.deepEqual(getLines(bufferService), ['¥¥ #', ' ¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); - inputHandler.parse('\x1b[3;6H\x1b[1K'); + await inputHandler.parseP('\x1b[3;6H\x1b[1K'); assert.deepEqual(getLines(bufferService), ['¥¥ #', ' ¥¥', ' ¥¥', '¥¥¥¥¥', '']); }); - it('ICH', () => { - inputHandler.parse('\x1b[1;6H\x1b[@'); + it('ICH', async () => { + await inputHandler.parseP('\x1b[1;6H\x1b[@'); assert.deepEqual(getLines(bufferService), ['¥¥ ¥', '¥¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); - inputHandler.parse('\x1b[2;4H\x1b[2@'); + await inputHandler.parseP('\x1b[2;4H\x1b[2@'); assert.deepEqual(getLines(bufferService), ['¥¥ ¥', '¥ ¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); - inputHandler.parse('\x1b[3;4H\x1b[3@'); + await inputHandler.parseP('\x1b[3;4H\x1b[3@'); assert.deepEqual(getLines(bufferService), ['¥¥ ¥', '¥ ¥¥', '¥ ¥', '¥¥¥¥¥', '']); - inputHandler.parse('\x1b[4;4H\x1b[4@'); + await inputHandler.parseP('\x1b[4;4H\x1b[4@'); assert.deepEqual(getLines(bufferService), ['¥¥ ¥', '¥ ¥¥', '¥ ¥', '¥ ¥', '']); }); - it('DCH', () => { - inputHandler.parse('\x1b[1;6H\x1b[P'); + it('DCH', async () => { + await inputHandler.parseP('\x1b[1;6H\x1b[P'); assert.deepEqual(getLines(bufferService), ['¥¥ ¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); - inputHandler.parse('\x1b[2;6H\x1b[2P'); + await inputHandler.parseP('\x1b[2;6H\x1b[2P'); assert.deepEqual(getLines(bufferService), ['¥¥ ¥¥', '¥¥ ¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); - inputHandler.parse('\x1b[3;6H\x1b[3P'); + await inputHandler.parseP('\x1b[3;6H\x1b[3P'); assert.deepEqual(getLines(bufferService), ['¥¥ ¥¥', '¥¥ ¥', '¥¥ ¥', '¥¥¥¥¥', '']); }); - it('ECH', () => { - inputHandler.parse('\x1b[1;6H\x1b[X'); + it('ECH', async () => { + await inputHandler.parseP('\x1b[1;6H\x1b[X'); assert.deepEqual(getLines(bufferService), ['¥¥ ¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); - inputHandler.parse('\x1b[2;6H\x1b[2X'); + await inputHandler.parseP('\x1b[2;6H\x1b[2X'); assert.deepEqual(getLines(bufferService), ['¥¥ ¥¥', '¥¥ ¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); - inputHandler.parse('\x1b[3;6H\x1b[3X'); + await inputHandler.parseP('\x1b[3;6H\x1b[3X'); assert.deepEqual(getLines(bufferService), ['¥¥ ¥¥', '¥¥ ¥', '¥¥ ¥', '¥¥¥¥¥', '']); }); }); @@ -1394,74 +1566,74 @@ describe('InputHandler', () => { optionsService.options.scrollback = 1; }); describe('reverseWraparound unset (default)', () => { - it('cannot delete last cell', () => { - inputHandler.parse('12345'); - inputHandler.parse(ttyBS); + it('cannot delete last cell', async () => { + await inputHandler.parseP('12345'); + await inputHandler.parseP(ttyBS); assert.deepEqual(getLines(bufferService, 1), ['123 5']); - inputHandler.parse(ttyBS.repeat(10)); + await inputHandler.parseP(ttyBS.repeat(10)); assert.deepEqual(getLines(bufferService, 1), [' 5']); }); - it('cannot access prev line', () => { - inputHandler.parse('12345'.repeat(2)); - inputHandler.parse(ttyBS); + it('cannot access prev line', async () => { + await inputHandler.parseP('12345'.repeat(2)); + await inputHandler.parseP(ttyBS); assert.deepEqual(getLines(bufferService, 2), ['12345', '123 5']); - inputHandler.parse(ttyBS.repeat(10)); + await inputHandler.parseP(ttyBS.repeat(10)); assert.deepEqual(getLines(bufferService, 2), ['12345', ' 5']); }); }); describe('reverseWraparound set', () => { - it('can delete last cell', () => { - inputHandler.parse('\x1b[?45h'); - inputHandler.parse('12345'); - inputHandler.parse(ttyBS); + it('can delete last cell', async () => { + await inputHandler.parseP('\x1b[?45h'); + await inputHandler.parseP('12345'); + await inputHandler.parseP(ttyBS); assert.deepEqual(getLines(bufferService, 1), ['1234 ']); - inputHandler.parse(ttyBS.repeat(7)); + await inputHandler.parseP(ttyBS.repeat(7)); assert.deepEqual(getLines(bufferService, 1), [' ']); }); - it('can access prev line if wrapped', () => { - inputHandler.parse('\x1b[?45h'); - inputHandler.parse('12345'.repeat(2)); - inputHandler.parse(ttyBS); + it('can access prev line if wrapped', async () => { + await inputHandler.parseP('\x1b[?45h'); + await inputHandler.parseP('12345'.repeat(2)); + await inputHandler.parseP(ttyBS); assert.deepEqual(getLines(bufferService, 2), ['12345', '1234 ']); - inputHandler.parse(ttyBS.repeat(7)); + await inputHandler.parseP(ttyBS.repeat(7)); assert.deepEqual(getLines(bufferService, 2), ['12 ', ' ']); }); - it('should lift isWrapped', () => { - inputHandler.parse('\x1b[?45h'); - inputHandler.parse('12345'.repeat(2)); + it('should lift isWrapped', async () => { + await inputHandler.parseP('\x1b[?45h'); + await inputHandler.parseP('12345'.repeat(2)); assert.equal(bufferService.buffer.lines.get(1)?.isWrapped, true); - inputHandler.parse(ttyBS.repeat(7)); + await inputHandler.parseP(ttyBS.repeat(7)); assert.equal(bufferService.buffer.lines.get(1)?.isWrapped, false); }); - it('stops at hard NLs', () => { - inputHandler.parse('\x1b[?45h'); - inputHandler.parse('12345\r\n'); - inputHandler.parse('12345'.repeat(2)); - inputHandler.parse(ttyBS.repeat(50)); + it('stops at hard NLs', async () => { + await inputHandler.parseP('\x1b[?45h'); + await inputHandler.parseP('12345\r\n'); + await inputHandler.parseP('12345'.repeat(2)); + await inputHandler.parseP(ttyBS.repeat(50)); assert.deepEqual(getLines(bufferService, 3), ['12345', ' ', ' ']); assert.equal(bufferService.buffer.x, 0); assert.equal(bufferService.buffer.y, 1); }); - it('handles wide chars correctly', () => { - inputHandler.parse('\x1b[?45h'); - inputHandler.parse('¥¥¥'); + it('handles wide chars correctly', async () => { + await inputHandler.parseP('\x1b[?45h'); + await inputHandler.parseP('¥¥¥'); assert.deepEqual(getLines(bufferService, 2), ['¥¥', '¥']); - inputHandler.parse(ttyBS); + await inputHandler.parseP(ttyBS); assert.deepEqual(getLines(bufferService, 2), ['¥¥', ' ']); assert.equal(bufferService.buffer.x, 1); - inputHandler.parse(ttyBS); + await inputHandler.parseP(ttyBS); assert.deepEqual(getLines(bufferService, 2), ['¥¥', ' ']); assert.equal(bufferService.buffer.x, 0); - inputHandler.parse(ttyBS); + await inputHandler.parseP(ttyBS); assert.deepEqual(getLines(bufferService, 2), ['¥ ', ' ']); assert.equal(bufferService.buffer.x, 3); // x=4 skipped due to early wrap-around - inputHandler.parse(ttyBS); + await inputHandler.parseP(ttyBS); assert.deepEqual(getLines(bufferService, 2), ['¥ ', ' ']); assert.equal(bufferService.buffer.x, 2); - inputHandler.parse(ttyBS); + await inputHandler.parseP(ttyBS); assert.deepEqual(getLines(bufferService, 2), [' ', ' ']); assert.equal(bufferService.buffer.x, 1); - inputHandler.parse(ttyBS); + await inputHandler.parseP(ttyBS); assert.deepEqual(getLines(bufferService, 2), [' ', ' ']); assert.equal(bufferService.buffer.x, 0); }); @@ -1472,72 +1644,72 @@ describe('InputHandler', () => { beforeEach(() => { bufferService.resize(10, 5); }); - it('4 | 24', () => { - inputHandler.parse('\x1b[4m'); + it('4 | 24', async () => { + await inputHandler.parseP('\x1b[4m'); assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.SINGLE); - inputHandler.parse('\x1b[24m'); + await inputHandler.parseP('\x1b[24m'); assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.NONE); }); - it('21 | 24', () => { - inputHandler.parse('\x1b[21m'); + it('21 | 24', async () => { + await inputHandler.parseP('\x1b[21m'); assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.DOUBLE); - inputHandler.parse('\x1b[24m'); + await inputHandler.parseP('\x1b[24m'); assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.NONE); }); - it('4:1 | 4:0', () => { - inputHandler.parse('\x1b[4:1m'); + it('4:1 | 4:0', async () => { + await inputHandler.parseP('\x1b[4:1m'); assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.SINGLE); - inputHandler.parse('\x1b[4:0m'); + await inputHandler.parseP('\x1b[4:0m'); assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.NONE); - inputHandler.parse('\x1b[4:1m'); + await inputHandler.parseP('\x1b[4:1m'); assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.SINGLE); - inputHandler.parse('\x1b[24m'); + await inputHandler.parseP('\x1b[24m'); assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.NONE); }); - it('4:2 | 4:0', () => { - inputHandler.parse('\x1b[4:2m'); + it('4:2 | 4:0', async () => { + await inputHandler.parseP('\x1b[4:2m'); assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.DOUBLE); - inputHandler.parse('\x1b[4:0m'); + await inputHandler.parseP('\x1b[4:0m'); assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.NONE); - inputHandler.parse('\x1b[4:2m'); + await inputHandler.parseP('\x1b[4:2m'); assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.DOUBLE); - inputHandler.parse('\x1b[24m'); + await inputHandler.parseP('\x1b[24m'); assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.NONE); }); - it('4:3 | 4:0', () => { - inputHandler.parse('\x1b[4:3m'); + it('4:3 | 4:0', async () => { + await inputHandler.parseP('\x1b[4:3m'); assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.CURLY); - inputHandler.parse('\x1b[4:0m'); + await inputHandler.parseP('\x1b[4:0m'); assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.NONE); - inputHandler.parse('\x1b[4:3m'); + await inputHandler.parseP('\x1b[4:3m'); assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.CURLY); - inputHandler.parse('\x1b[24m'); + await inputHandler.parseP('\x1b[24m'); assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.NONE); }); - it('4:4 | 4:0', () => { - inputHandler.parse('\x1b[4:4m'); + it('4:4 | 4:0', async () => { + await inputHandler.parseP('\x1b[4:4m'); assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.DOTTED); - inputHandler.parse('\x1b[4:0m'); + await inputHandler.parseP('\x1b[4:0m'); assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.NONE); - inputHandler.parse('\x1b[4:4m'); + await inputHandler.parseP('\x1b[4:4m'); assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.DOTTED); - inputHandler.parse('\x1b[24m'); + await inputHandler.parseP('\x1b[24m'); assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.NONE); }); - it('4:5 | 4:0', () => { - inputHandler.parse('\x1b[4:5m'); + it('4:5 | 4:0', async () => { + await inputHandler.parseP('\x1b[4:5m'); assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.DASHED); - inputHandler.parse('\x1b[4:0m'); + await inputHandler.parseP('\x1b[4:0m'); assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.NONE); - inputHandler.parse('\x1b[4:5m'); + await inputHandler.parseP('\x1b[4:5m'); assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.DASHED); - inputHandler.parse('\x1b[24m'); + await inputHandler.parseP('\x1b[24m'); assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.NONE); }); - it('4:x --> 4 should revert to single underline', () => { - inputHandler.parse('\x1b[4:5m'); + it('4:x --> 4 should revert to single underline', async () => { + await inputHandler.parseP('\x1b[4:5m'); assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.DASHED); - inputHandler.parse('\x1b[4m'); + await inputHandler.parseP('\x1b[4m'); assert.equal(inputHandler.curAttrData.getUnderlineStyle(), UnderlineStyle.SINGLE); }); }); @@ -1545,9 +1717,9 @@ describe('InputHandler', () => { beforeEach(() => { bufferService.resize(10, 5); }); - it('defaults to FG color', () => { + it('defaults to FG color', async () => { for (const s of ['', '\x1b[30m', '\x1b[38;510m', '\x1b[38;2;1;2;3m']) { - inputHandler.parse(s); + await inputHandler.parseP(s); assert.equal(inputHandler.curAttrData.getUnderlineColor(), inputHandler.curAttrData.getFgColor()); assert.equal(inputHandler.curAttrData.getUnderlineColorMode(), inputHandler.curAttrData.getFgColorMode()); assert.equal(inputHandler.curAttrData.isUnderlineColorRGB(), inputHandler.curAttrData.isFgRGB()); @@ -1555,31 +1727,31 @@ describe('InputHandler', () => { assert.equal(inputHandler.curAttrData.isUnderlineColorDefault(), inputHandler.curAttrData.isFgDefault()); } }); - it('correctly sets P256/RGB colors', () => { - inputHandler.parse('\x1b[4m'); - inputHandler.parse('\x1b[58;5;123m'); + it('correctly sets P256/RGB colors', async () => { + await inputHandler.parseP('\x1b[4m'); + await inputHandler.parseP('\x1b[58;5;123m'); assert.equal(inputHandler.curAttrData.getUnderlineColor(), 123); assert.equal(inputHandler.curAttrData.getUnderlineColorMode(), Attributes.CM_P256); assert.equal(inputHandler.curAttrData.isUnderlineColorRGB(), false); assert.equal(inputHandler.curAttrData.isUnderlineColorPalette(), true); assert.equal(inputHandler.curAttrData.isUnderlineColorDefault(), false); - inputHandler.parse('\x1b[58;2::1:2:3m'); + await inputHandler.parseP('\x1b[58;2::1:2:3m'); assert.equal(inputHandler.curAttrData.getUnderlineColor(), (1 << 16) | (2 << 8) | 3); assert.equal(inputHandler.curAttrData.getUnderlineColorMode(), Attributes.CM_RGB); assert.equal(inputHandler.curAttrData.isUnderlineColorRGB(), true); assert.equal(inputHandler.curAttrData.isUnderlineColorPalette(), false); assert.equal(inputHandler.curAttrData.isUnderlineColorDefault(), false); }); - it('P256/RGB persistence', () => { + it('P256/RGB persistence', async () => { const cell = new CellData(); - inputHandler.parse('\x1b[4m'); - inputHandler.parse('\x1b[58;5;123m'); + await inputHandler.parseP('\x1b[4m'); + await inputHandler.parseP('\x1b[58;5;123m'); assert.equal(inputHandler.curAttrData.getUnderlineColor(), 123); assert.equal(inputHandler.curAttrData.getUnderlineColorMode(), Attributes.CM_P256); assert.equal(inputHandler.curAttrData.isUnderlineColorRGB(), false); assert.equal(inputHandler.curAttrData.isUnderlineColorPalette(), true); assert.equal(inputHandler.curAttrData.isUnderlineColorDefault(), false); - inputHandler.parse('ab'); + await inputHandler.parseP('ab'); bufferService.buffer!.lines.get(0)!.loadCell(1, cell); assert.equal(cell.getUnderlineColor(), 123); assert.equal(cell.getUnderlineColorMode(), Attributes.CM_P256); @@ -1587,13 +1759,13 @@ describe('InputHandler', () => { assert.equal(cell.isUnderlineColorPalette(), true); assert.equal(cell.isUnderlineColorDefault(), false); - inputHandler.parse('\x1b[4:0m'); + await inputHandler.parseP('\x1b[4:0m'); assert.equal(inputHandler.curAttrData.getUnderlineColor(), inputHandler.curAttrData.getFgColor()); assert.equal(inputHandler.curAttrData.getUnderlineColorMode(), inputHandler.curAttrData.getFgColorMode()); assert.equal(inputHandler.curAttrData.isUnderlineColorRGB(), inputHandler.curAttrData.isFgRGB()); assert.equal(inputHandler.curAttrData.isUnderlineColorPalette(), inputHandler.curAttrData.isFgPalette()); assert.equal(inputHandler.curAttrData.isUnderlineColorDefault(), inputHandler.curAttrData.isFgDefault()); - inputHandler.parse('a'); + await inputHandler.parseP('a'); bufferService.buffer!.lines.get(0)!.loadCell(1, cell); assert.equal(cell.getUnderlineColor(), 123); assert.equal(cell.getUnderlineColorMode(), Attributes.CM_P256); @@ -1607,15 +1779,15 @@ describe('InputHandler', () => { assert.equal(cell.isUnderlineColorPalette(), inputHandler.curAttrData.isFgPalette()); assert.equal(cell.isUnderlineColorDefault(), inputHandler.curAttrData.isFgDefault()); - inputHandler.parse('\x1b[4m'); - inputHandler.parse('\x1b[58;2::1:2:3m'); + await inputHandler.parseP('\x1b[4m'); + await inputHandler.parseP('\x1b[58;2::1:2:3m'); assert.equal(inputHandler.curAttrData.getUnderlineColor(), (1 << 16) | (2 << 8) | 3); assert.equal(inputHandler.curAttrData.getUnderlineColorMode(), Attributes.CM_RGB); assert.equal(inputHandler.curAttrData.isUnderlineColorRGB(), true); assert.equal(inputHandler.curAttrData.isUnderlineColorPalette(), false); assert.equal(inputHandler.curAttrData.isUnderlineColorDefault(), false); - inputHandler.parse('a'); - inputHandler.parse('\x1b[24m'); + await inputHandler.parseP('a'); + await inputHandler.parseP('\x1b[24m'); bufferService.buffer!.lines.get(0)!.loadCell(1, cell); assert.equal(cell.getUnderlineColor(), 123); assert.equal(cell.getUnderlineColorMode(), Attributes.CM_P256); @@ -1644,52 +1816,251 @@ describe('InputHandler', () => { }); }); describe('DECSTR', () => { - beforeEach(() => { + beforeEach(async () => { bufferService.resize(10, 5); optionsService.options.scrollback = 1; - inputHandler.parse('01234567890123'); + await inputHandler.parseP('01234567890123'); }); - it('should reset IRM', () => { - inputHandler.parse('\x1b[4h'); + it('should reset IRM', async () => { + await inputHandler.parseP('\x1b[4h'); assert.equal(coreService.modes.insertMode, true); - inputHandler.parse('\x1b[!p'); + await inputHandler.parseP('\x1b[!p'); assert.equal(coreService.modes.insertMode, false); }); - it('should reset cursor visibility', () => { - inputHandler.parse('\x1b[?25l'); + it('should reset cursor visibility', async () => { + await inputHandler.parseP('\x1b[?25l'); assert.equal(coreService.isCursorHidden, true); - inputHandler.parse('\x1b[!p'); + await inputHandler.parseP('\x1b[!p'); assert.equal(coreService.isCursorHidden, false); }); - it('should reset scroll margins', () => { - inputHandler.parse('\x1b[2;4r'); + it('should reset scroll margins', async () => { + await inputHandler.parseP('\x1b[2;4r'); assert.equal(bufferService.buffer.scrollTop, 1); assert.equal(bufferService.buffer.scrollBottom, 3); - inputHandler.parse('\x1b[!p'); + await inputHandler.parseP('\x1b[!p'); assert.equal(bufferService.buffer.scrollTop, 0); assert.equal(bufferService.buffer.scrollBottom, bufferService.rows - 1); }); - it('should reset text attributes', () => { - inputHandler.parse('\x1b[1;2;32;43m'); + it('should reset text attributes', async () => { + await inputHandler.parseP('\x1b[1;2;32;43m'); assert.equal(!!inputHandler.curAttrData.isBold(), true); - inputHandler.parse('\x1b[!p'); + await inputHandler.parseP('\x1b[!p'); assert.equal(!!inputHandler.curAttrData.isBold(), false); assert.equal(inputHandler.curAttrData.fg, 0); assert.equal(inputHandler.curAttrData.bg, 0); }); - it('should reset DECSC data', () => { - inputHandler.parse('\x1b7'); + it('should reset DECSC data', async () => { + await inputHandler.parseP('\x1b7'); assert.equal(bufferService.buffer.savedX, 4); assert.equal(bufferService.buffer.savedY, 1); - inputHandler.parse('\x1b[!p'); + await inputHandler.parseP('\x1b[!p'); assert.equal(bufferService.buffer.savedX, 0); assert.equal(bufferService.buffer.savedY, 0); }); - it('should reset DECOM', () => { - inputHandler.parse('\x1b[?6h'); + it('should reset DECOM', async () => { + await inputHandler.parseP('\x1b[?6h'); assert.equal(coreService.decPrivateModes.origin, true); - inputHandler.parse('\x1b[!p'); + await inputHandler.parseP('\x1b[!p'); assert.equal(coreService.decPrivateModes.origin, false); }); }); + describe('OSC', () => { + it('4: should parse correct Ansi color change data', () => { + // this is testing a private method + const event = inputHandler.parseAnsiColorChange('19;rgb:a1/b2/c3'); + + assert.isNotNull(event); + assert.deepEqual(event!.colors[0], { colorIndex: 19, red: 0xa1, green: 0xb2, blue: 0xc3 }); + }); + + it('4: should ignore incorrect Ansi color change data', () => { + // this is testing a private method + assert.isNull(inputHandler.parseAnsiColorChange('17;rgb:a/b/c')); + assert.isNull(inputHandler.parseAnsiColorChange('17;rgb:#aabbcc')); + assert.isNull(inputHandler.parseAnsiColorChange('17;rgba:aa/bb/cc')); + assert.isNull(inputHandler.parseAnsiColorChange('rgb:aa/bb/cc')); + }); + + it('4: should parse a list of Ansi color changes', () => { + // this is testing a private method + const event = inputHandler.parseAnsiColorChange('19;rgb:a1/b2/c3;17;rgb:00/11/22;255;rgb:01/ef/2d'); + + assert.isNotNull(event); + assert.equal(event!.colors.length, 3); + assert.deepEqual(event!.colors[0], { colorIndex: 19, red: 0xa1, green: 0xb2, blue: 0xc3 }); + assert.deepEqual(event!.colors[1], { colorIndex: 17, red: 0x00, green: 0x11, blue: 0x22 }); + assert.deepEqual(event!.colors[2], { colorIndex: 255, red: 0x01, green: 0xef, blue: 0x2d }); + }); + it('4: should ignore incorrect colors in a list of Ansi color changes', () => { + // this is testing a private method + const event = inputHandler.parseAnsiColorChange('19;rgb:a1/b2/c3;17;rgb:WR/ON/G;255;rgb:01/ef/2d'); + + assert.equal(event!.colors.length, 2); + assert.deepEqual(event!.colors[0], { colorIndex: 19, red: 0xa1, green: 0xb2, blue: 0xc3 }); + assert.deepEqual(event!.colors[1], { colorIndex: 255, red: 0x01, green: 0xef, blue: 0x2d }); + }); + it('4: should be case insensitive when parsing Ansi color changes', () => { + // this is testing a private method + const event = inputHandler.parseAnsiColorChange('19;rGb:A1/b2/C3'); + + assert.equal(event!.colors.length, 1); + assert.deepEqual(event!.colors[0], { colorIndex: 19, red: 0xa1, green: 0xb2, blue: 0xc3 }); + }); + it('4: should fire event on Ansi color change', async () => { + return new Promise(async r => { + inputHandler.onAnsiColorChange(e => { + assert.isNotNull(e); + assert.isNotNull(e!.colors); + assert.deepEqual(e!.colors[0], { colorIndex: 17, red: 0x1a, green: 0x2b, blue: 0x3c }); + assert.deepEqual(e!.colors[1], { colorIndex: 12, red: 0x11, green: 0x22, blue: 0x33 }); + r(); + }); + await inputHandler.parseP('\x1b]4;17;rgb:1a/2b/3c;12;rgb:11/22/33\x1b\\'); + }); + }); + }); + + // issue #3362 and #2979 + describe('EL/ED cursor at buffer.cols', () => { + beforeEach(() => { + bufferService.resize(10, 5); + }); + describe('cursor should stay at cols / does not overflow', () => { + it('EL0', async () => { + await inputHandler.parseP('##########\x1b[0K'); + assert.equal(bufferService.buffer.x, 10); + assert.deepEqual(getLines(bufferService), ['#'.repeat(10), '', '', '', '']); + }); + it('EL1', async () => { + await inputHandler.parseP('##########\x1b[1K'); + assert.equal(bufferService.buffer.x, 10); + assert.deepEqual(getLines(bufferService), ['', '', '', '', '']); + }); + it('EL2', async () => { + await inputHandler.parseP('##########\x1b[2K'); + assert.equal(bufferService.buffer.x, 10); + assert.deepEqual(getLines(bufferService), ['', '', '', '', '']); + }); + it('ED0', async () => { + await inputHandler.parseP('##########\x1b[0J'); + assert.equal(bufferService.buffer.x, 10); + assert.deepEqual(getLines(bufferService), ['#'.repeat(10), '', '', '', '']); + }); + it('ED1', async () => { + await inputHandler.parseP('##########\x1b[1J'); + assert.equal(bufferService.buffer.x, 10); + assert.deepEqual(getLines(bufferService), ['', '', '', '', '']); + }); + it('ED2', async () => { + await inputHandler.parseP('##########\x1b[2J'); + assert.equal(bufferService.buffer.x, 10); + assert.deepEqual(getLines(bufferService), ['', '', '', '', '']); + }); + it('ED3', async () => { + await inputHandler.parseP('##########\x1b[3J'); + assert.equal(bufferService.buffer.x, 10); + assert.deepEqual(getLines(bufferService), ['#'.repeat(10), '', '', '', '']); + }); + }); + describe('following sequence keeps working', () => { + // sequences to test (cursor related ones) + const SEQ = [ + /* ICH */ '\x1b[10@', + /* SL */ '\x1b[10 @', + /* CUU */ '\x1b[10A', + /* SR */ '\x1b[10 A', + /* CUD */ '\x1b[10B', + /* CUF */ '\x1b[10C', + /* CUB */ '\x1b[10D', + /* CNL */ '\x1b[10E', + /* CPL */ '\x1b[10F', + /* CHA */ '\x1b[10G', + /* CUP */ '\x1b[10;10H', + /* CHT */ '\x1b[10I', + /* IL */ '\x1b[10L', + /* DL */ '\x1b[10M', + /* DCH */ '\x1b[10P', + /* SU */ '\x1b[10S', + /* SD */ '\x1b[10T', + /* ECH */ '\x1b[10X', + /* CBT */ '\x1b[10Z', + /* HPA */ '\x1b[10`', + /* HPR */ '\x1b[10a', + /* REP */ '\x1b[10b', + /* VPA */ '\x1b[10d', + /* VPR */ '\x1b[10e', + /* HVP */ '\x1b[10;10f', + /* TBC */ '\x1b[0g', + /* SCOSC */ '\x1b[s', + /* DECIC */ '\x1b[10\'}', + /* DECDC */ '\x1b[10\'~' + ]; + it('cursor never advances beyond cols', async () => { + for (const seq of SEQ) { + await inputHandler.parseP('##########\x1b[2J' + seq); + assert.equal(bufferService.buffer.x <= bufferService.cols, true); + inputHandler.reset(); + bufferService.reset(); + } + }); + }); + }); +}); + + +describe('InputHandler - async handlers', () => { + let bufferService: IBufferService; + let coreService: ICoreService; + let optionsService: MockOptionsService; + let inputHandler: TestInputHandler; + + beforeEach(() => { + optionsService = new MockOptionsService(); + bufferService = new BufferService(optionsService); + bufferService.resize(80, 30); + coreService = new CoreService(() => { }, bufferService, new MockLogService(), optionsService); + coreService.onData(data => { console.log(data); }); + + inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockDirtyRowService(), new MockLogService(), optionsService, new MockCoreMouseService(), new MockUnicodeService()); + }); + + it('async CUP with CPR check', async () => { + const cup: number[][] = []; + const cpr: number[][] = []; + inputHandler.registerCsiHandler({ final: 'H' }, async params => { + cup.push(params.toArray() as number[]); + await new Promise(res => setTimeout(res, 50)); + // late call of real repositioning + return inputHandler.cursorPosition(params); + }); + coreService.onData(data => { + const m = data.match(/\x1b\[(.*?);(.*?)R/); + if (m) { + cpr.push([parseInt(m[1]), parseInt(m[2])]); + } + }); + await inputHandler.parseP('aaa\x1b[3;4H\x1b[6nbbb\x1b[6;8H\x1b[6n'); + assert.deepEqual(cup, cpr); + }); + it('async OSC between', async () => { + inputHandler.registerOscHandler(1000, async data => { + await new Promise(res => setTimeout(res, 50)); + assert.deepEqual(getLines(bufferService, 2), ['hello world!', '']); + assert.equal(data, 'some data'); + return true; + }); + await inputHandler.parseP('hello world!\r\n\x1b]1000;some data\x07second line'); + assert.deepEqual(getLines(bufferService, 2), ['hello world!', 'second line']); + }); + it('async DCS between', async () => { + inputHandler.registerDcsHandler({ final: 'a' }, async (data, params) => { + await new Promise(res => setTimeout(res, 50)); + assert.deepEqual(getLines(bufferService, 2), ['hello world!', '']); + assert.equal(data, 'some data'); + assert.deepEqual(params.toArray(), [1, 2]); + return true; + }); + await inputHandler.parseP('hello world!\r\n\x1bP1;2asome data\x1b\\second line'); + assert.deepEqual(getLines(bufferService, 2), ['hello world!', 'second line']); + }); }); diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index dbf695e6..f2f0d0ce 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -4,7 +4,7 @@ * @license MIT */ -import { IInputHandler, IAttributeData, IDisposable, IWindowOptions } from 'common/Types'; +import { IInputHandler, IAttributeData, IDisposable, IWindowOptions, IAnsiColorChangeEvent, IParseStack } from 'common/Types'; import { C0, C1 } from 'common/data/EscapeSequences'; import { CHARSETS, DEFAULT_CHARSET } from 'common/data/Charsets'; import { EscapeSequenceParser } from 'common/parser/EscapeSequenceParser'; @@ -17,14 +17,15 @@ import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IFunctionId import { NULL_CELL_CODE, NULL_CELL_WIDTH, Attributes, FgFlags, BgFlags, Content, UnderlineStyle } from 'common/buffer/Constants'; import { CellData } from 'common/buffer/CellData'; import { AttributeData } from 'common/buffer/AttributeData'; -import { ICoreService, IBufferService, IOptionsService, ILogService, IDirtyRowService, ICoreMouseService, ICharsetService, IUnicodeService } from 'common/services/Services'; +import { ICoreService, IBufferService, IOptionsService, ILogService, IDirtyRowService, ICoreMouseService, ICharsetService, IUnicodeService, LogLevelEnum } from 'common/services/Services'; import { OscHandler } from 'common/parser/OscParser'; import { DcsHandler } from 'common/parser/DcsParser'; +import { IBuffer } from 'common/buffer/Types'; /** * Map collect to glevel. Used in `selectCharset`. */ -const GLEVEL: {[key: string]: number} = {'(': 0, ')': 1, '*': 2, '+': 3, '-': 1, '.': 2}; +const GLEVEL: {[key: string]: number} = { '(': 0, ')': 1, '*': 2, '+': 3, '-': 1, '.': 2 }; /** * VT commands done by the parser - FIXME: move this to the parser? @@ -97,6 +98,9 @@ export enum WindowsOptionsReportType { GET_CELL_SIZE_PIXELS = 1 } +// create a warning log if an async handler takes longer than the limit (in ms) +const SLOW_ASYNC_LIMIT = 5000; + /** * DCS subparser implementations */ @@ -146,36 +150,42 @@ class DECRQSS implements IDcsHandler { this._data = concat(this._data, data.subarray(start, end)); } - public unhook(success: boolean): void { + public unhook(success: boolean): boolean { if (!success) { this._data = new Uint32Array(0); - return; + return true; } const data = utf32ToString(this._data); this._data = new Uint32Array(0); switch (data) { // valid: DCS 1 $ r Pt ST (xterm) case '"q': // DECSCA - return this._coreService.triggerDataEvent(`${C0.ESC}P1$r0"q${C0.ESC}\\`); + this._coreService.triggerDataEvent(`${C0.ESC}P1$r0"q${C0.ESC}\\`); + break; case '"p': // DECSCL - return this._coreService.triggerDataEvent(`${C0.ESC}P1$r61;1"p${C0.ESC}\\`); + this._coreService.triggerDataEvent(`${C0.ESC}P1$r61;1"p${C0.ESC}\\`); + break; case 'r': // DECSTBM const pt = '' + (this._bufferService.buffer.scrollTop + 1) + ';' + (this._bufferService.buffer.scrollBottom + 1) + 'r'; - return this._coreService.triggerDataEvent(`${C0.ESC}P1$r${pt}${C0.ESC}\\`); + this._coreService.triggerDataEvent(`${C0.ESC}P1$r${pt}${C0.ESC}\\`); + break; case 'm': // SGR // TODO: report real settings instead of 0m - return this._coreService.triggerDataEvent(`${C0.ESC}P1$r0m${C0.ESC}\\`); + this._coreService.triggerDataEvent(`${C0.ESC}P1$r0m${C0.ESC}\\`); + break; 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._optionsService.options.cursorStyle]; style -= this._optionsService.options.cursorBlink ? 1 : 0; - return this._coreService.triggerDataEvent(`${C0.ESC}P1$r${style} q${C0.ESC}\\`); + this._coreService.triggerDataEvent(`${C0.ESC}P1$r${style} q${C0.ESC}\\`); + break; default: // invalid: DCS 0 $ r Pt ST (xterm) this._logService.debug('Unknown DCS $q %s', data); this._coreService.triggerDataEvent(`${C0.ESC}P0$r${C0.ESC}\\`); } + return true; } } @@ -225,14 +235,14 @@ export class InputHandler extends Disposable implements IInputHandler { private _curAttrData: IAttributeData = DEFAULT_ATTR_DATA.clone(); private _eraseAttrDataInternal: IAttributeData = DEFAULT_ATTR_DATA.clone(); + private _activeBuffer: IBuffer; + private _onRequestBell = new EventEmitter(); public get onRequestBell(): IEvent { return this._onRequestBell.event; } private _onRequestRefreshRows = new EventEmitter(); public get onRequestRefreshRows(): IEvent { return this._onRequestRefreshRows.event; } private _onRequestReset = new EventEmitter(); public get onRequestReset(): IEvent { return this._onRequestReset.event; } - private _onRequestScroll = new EventEmitter(); - public get onRequestScroll(): IEvent { return this._onRequestScroll.event; } private _onRequestSyncScrollBar = new EventEmitter(); public get onRequestSyncScrollBar(): IEvent { return this._onRequestSyncScrollBar.event; } private _onRequestWindowsOptionsReport = new EventEmitter(); @@ -250,6 +260,16 @@ export class InputHandler extends Disposable implements IInputHandler { public get onScroll(): IEvent { return this._onScroll.event; } private _onTitleChange = new EventEmitter(); public get onTitleChange(): IEvent { return this._onTitleChange.event; } + private _onAnsiColorChange = new EventEmitter(); + public get onAnsiColorChange(): IEvent { return this._onAnsiColorChange.event; } + + private _parseStack: IParseStack = { + paused: false, + cursorStartX: 0, + cursorStartY: 0, + decodedLength: 0, + position: 0 + }; constructor( private readonly _bufferService: IBufferService, @@ -265,6 +285,10 @@ export class InputHandler extends Disposable implements IInputHandler { super(); this.register(this._parser); + // Track properties used in performance critical code manually to avoid using slow getters + this._activeBuffer = this._bufferService.buffer; + this.register(this._bufferService.buffers.onBufferActivate(e => this._activeBuffer = e.activeBuffer)); + /** * custom fallback handlers */ @@ -295,53 +319,53 @@ export class InputHandler extends Disposable implements IInputHandler { /** * CSI handler */ - this._parser.setCsiHandler({final: '@'}, params => this.insertChars(params)); - this._parser.setCsiHandler({intermediates: ' ', final: '@'}, params => this.scrollLeft(params)); - this._parser.setCsiHandler({final: 'A'}, params => this.cursorUp(params)); - this._parser.setCsiHandler({intermediates: ' ', final: 'A'}, params => this.scrollRight(params)); - this._parser.setCsiHandler({final: 'B'}, params => this.cursorDown(params)); - this._parser.setCsiHandler({final: 'C'}, params => this.cursorForward(params)); - this._parser.setCsiHandler({final: 'D'}, params => this.cursorBackward(params)); - this._parser.setCsiHandler({final: 'E'}, params => this.cursorNextLine(params)); - this._parser.setCsiHandler({final: 'F'}, params => this.cursorPrecedingLine(params)); - this._parser.setCsiHandler({final: 'G'}, params => this.cursorCharAbsolute(params)); - this._parser.setCsiHandler({final: 'H'}, params => this.cursorPosition(params)); - this._parser.setCsiHandler({final: 'I'}, params => this.cursorForwardTab(params)); - this._parser.setCsiHandler({final: 'J'}, params => this.eraseInDisplay(params)); - this._parser.setCsiHandler({prefix: '?', final: 'J'}, params => this.eraseInDisplay(params)); - this._parser.setCsiHandler({final: 'K'}, params => this.eraseInLine(params)); - this._parser.setCsiHandler({prefix: '?', final: 'K'}, params => this.eraseInLine(params)); - this._parser.setCsiHandler({final: 'L'}, params => this.insertLines(params)); - this._parser.setCsiHandler({final: 'M'}, params => this.deleteLines(params)); - this._parser.setCsiHandler({final: 'P'}, params => this.deleteChars(params)); - this._parser.setCsiHandler({final: 'S'}, params => this.scrollUp(params)); - this._parser.setCsiHandler({final: 'T'}, params => this.scrollDown(params)); - this._parser.setCsiHandler({final: 'X'}, params => this.eraseChars(params)); - this._parser.setCsiHandler({final: 'Z'}, params => this.cursorBackwardTab(params)); - this._parser.setCsiHandler({final: '`'}, params => this.charPosAbsolute(params)); - this._parser.setCsiHandler({final: 'a'}, params => this.hPositionRelative(params)); - this._parser.setCsiHandler({final: 'b'}, params => this.repeatPrecedingCharacter(params)); - this._parser.setCsiHandler({final: 'c'}, params => this.sendDeviceAttributesPrimary(params)); - this._parser.setCsiHandler({prefix: '>', final: 'c'}, params => this.sendDeviceAttributesSecondary(params)); - this._parser.setCsiHandler({final: 'd'}, params => this.linePosAbsolute(params)); - this._parser.setCsiHandler({final: 'e'}, params => this.vPositionRelative(params)); - this._parser.setCsiHandler({final: 'f'}, params => this.hVPosition(params)); - this._parser.setCsiHandler({final: 'g'}, params => this.tabClear(params)); - this._parser.setCsiHandler({final: 'h'}, params => this.setMode(params)); - this._parser.setCsiHandler({prefix: '?', final: 'h'}, params => this.setModePrivate(params)); - this._parser.setCsiHandler({final: 'l'}, params => this.resetMode(params)); - this._parser.setCsiHandler({prefix: '?', final: 'l'}, params => this.resetModePrivate(params)); - this._parser.setCsiHandler({final: 'm'}, params => this.charAttributes(params)); - this._parser.setCsiHandler({final: 'n'}, params => this.deviceStatus(params)); - this._parser.setCsiHandler({prefix: '?', final: 'n'}, params => this.deviceStatusPrivate(params)); - this._parser.setCsiHandler({intermediates: '!', final: 'p'}, params => this.softReset(params)); - this._parser.setCsiHandler({intermediates: ' ', final: 'q'}, params => this.setCursorStyle(params)); - this._parser.setCsiHandler({final: 'r'}, params => this.setScrollRegion(params)); - this._parser.setCsiHandler({final: 's'}, params => this.saveCursor(params)); - this._parser.setCsiHandler({final: 't'}, params => this.windowOptions(params)); - this._parser.setCsiHandler({final: 'u'}, params => this.restoreCursor(params)); - this._parser.setCsiHandler({intermediates: '\'', final: '}'}, params => this.insertColumns(params)); - this._parser.setCsiHandler({intermediates: '\'', final: '~'}, params => this.deleteColumns(params)); + this._parser.registerCsiHandler({ final: '@' }, params => this.insertChars(params)); + this._parser.registerCsiHandler({ intermediates: ' ', final: '@' }, params => this.scrollLeft(params)); + this._parser.registerCsiHandler({ final: 'A' }, params => this.cursorUp(params)); + this._parser.registerCsiHandler({ intermediates: ' ', final: 'A' }, params => this.scrollRight(params)); + this._parser.registerCsiHandler({ final: 'B' }, params => this.cursorDown(params)); + this._parser.registerCsiHandler({ final: 'C' }, params => this.cursorForward(params)); + this._parser.registerCsiHandler({ final: 'D' }, params => this.cursorBackward(params)); + this._parser.registerCsiHandler({ final: 'E' }, params => this.cursorNextLine(params)); + this._parser.registerCsiHandler({ final: 'F' }, params => this.cursorPrecedingLine(params)); + this._parser.registerCsiHandler({ final: 'G' }, params => this.cursorCharAbsolute(params)); + this._parser.registerCsiHandler({ final: 'H' }, params => this.cursorPosition(params)); + this._parser.registerCsiHandler({ final: 'I' }, params => this.cursorForwardTab(params)); + this._parser.registerCsiHandler({ final: 'J' }, params => this.eraseInDisplay(params)); + this._parser.registerCsiHandler({ prefix: '?', final: 'J' }, params => this.eraseInDisplay(params)); + this._parser.registerCsiHandler({ final: 'K' }, params => this.eraseInLine(params)); + this._parser.registerCsiHandler({ prefix: '?', final: 'K' }, params => this.eraseInLine(params)); + this._parser.registerCsiHandler({ final: 'L' }, params => this.insertLines(params)); + this._parser.registerCsiHandler({ final: 'M' }, params => this.deleteLines(params)); + this._parser.registerCsiHandler({ final: 'P' }, params => this.deleteChars(params)); + this._parser.registerCsiHandler({ final: 'S' }, params => this.scrollUp(params)); + this._parser.registerCsiHandler({ final: 'T' }, params => this.scrollDown(params)); + this._parser.registerCsiHandler({ final: 'X' }, params => this.eraseChars(params)); + this._parser.registerCsiHandler({ final: 'Z' }, params => this.cursorBackwardTab(params)); + this._parser.registerCsiHandler({ final: '`' }, params => this.charPosAbsolute(params)); + this._parser.registerCsiHandler({ final: 'a' }, params => this.hPositionRelative(params)); + this._parser.registerCsiHandler({ final: 'b' }, params => this.repeatPrecedingCharacter(params)); + this._parser.registerCsiHandler({ final: 'c' }, params => this.sendDeviceAttributesPrimary(params)); + this._parser.registerCsiHandler({ prefix: '>', final: 'c' }, params => this.sendDeviceAttributesSecondary(params)); + this._parser.registerCsiHandler({ final: 'd' }, params => this.linePosAbsolute(params)); + this._parser.registerCsiHandler({ final: 'e' }, params => this.vPositionRelative(params)); + this._parser.registerCsiHandler({ final: 'f' }, params => this.hVPosition(params)); + this._parser.registerCsiHandler({ final: 'g' }, params => this.tabClear(params)); + this._parser.registerCsiHandler({ final: 'h' }, params => this.setMode(params)); + this._parser.registerCsiHandler({ prefix: '?', final: 'h' }, params => this.setModePrivate(params)); + this._parser.registerCsiHandler({ final: 'l' }, params => this.resetMode(params)); + this._parser.registerCsiHandler({ prefix: '?', final: 'l' }, params => this.resetModePrivate(params)); + this._parser.registerCsiHandler({ final: 'm' }, params => this.charAttributes(params)); + this._parser.registerCsiHandler({ final: 'n' }, params => this.deviceStatus(params)); + this._parser.registerCsiHandler({ prefix: '?', final: 'n' }, params => this.deviceStatusPrivate(params)); + this._parser.registerCsiHandler({ intermediates: '!', final: 'p' }, params => this.softReset(params)); + this._parser.registerCsiHandler({ intermediates: ' ', final: 'q' }, params => this.setCursorStyle(params)); + this._parser.registerCsiHandler({ final: 'r' }, params => this.setScrollRegion(params)); + this._parser.registerCsiHandler({ final: 's' }, params => this.saveCursor(params)); + this._parser.registerCsiHandler({ final: 't' }, params => this.windowOptions(params)); + this._parser.registerCsiHandler({ final: 'u' }, params => this.restoreCursor(params)); + this._parser.registerCsiHandler({ intermediates: '\'', final: '}' }, params => this.insertColumns(params)); + this._parser.registerCsiHandler({ intermediates: '\'', final: '~' }, params => this.deleteColumns(params)); /** * execute handler @@ -365,13 +389,14 @@ export class InputHandler extends Disposable implements IInputHandler { * OSC handler */ // 0 - icon name + title - this._parser.setOscHandler(0, new OscHandler((data: string) => { this.setTitle(data); this.setIconName(data); })); + this._parser.registerOscHandler(0, new OscHandler(data => { this.setTitle(data); this.setIconName(data); return true; })); // 1 - icon name - this._parser.setOscHandler(1, new OscHandler((data: string) => this.setIconName(data))); + this._parser.registerOscHandler(1, new OscHandler(data => this.setIconName(data))); // 2 - title - this._parser.setOscHandler(2, new OscHandler((data: string) => this.setTitle(data))); + this._parser.registerOscHandler(2, new OscHandler(data => this.setTitle(data))); // 3 - set property X in the form "prop=value" // 4 - Change Color Number + this._parser.registerOscHandler(4, new OscHandler(data => this.setAnsiColor(data))); // 5 - Change Special Color Number // 6 - Enable/disable Special Color Number c // 7 - current directory? (not in xterm spec, see https://gitlab.com/gnachman/iterm2/issues/3939) @@ -406,32 +431,32 @@ export class InputHandler extends Disposable implements IInputHandler { /** * ESC handlers */ - this._parser.setEscHandler({final: '7'}, () => this.saveCursor()); - this._parser.setEscHandler({final: '8'}, () => this.restoreCursor()); - this._parser.setEscHandler({final: 'D'}, () => this.index()); - this._parser.setEscHandler({final: 'E'}, () => this.nextLine()); - this._parser.setEscHandler({final: 'H'}, () => this.tabSet()); - this._parser.setEscHandler({final: 'M'}, () => this.reverseIndex()); - this._parser.setEscHandler({final: '='}, () => this.keypadApplicationMode()); - this._parser.setEscHandler({final: '>'}, () => this.keypadNumericMode()); - this._parser.setEscHandler({final: 'c'}, () => this.fullReset()); - this._parser.setEscHandler({final: 'n'}, () => this.setgLevel(2)); - this._parser.setEscHandler({final: 'o'}, () => this.setgLevel(3)); - this._parser.setEscHandler({final: '|'}, () => this.setgLevel(3)); - this._parser.setEscHandler({final: '}'}, () => this.setgLevel(2)); - this._parser.setEscHandler({final: '~'}, () => this.setgLevel(1)); - this._parser.setEscHandler({intermediates: '%', final: '@'}, () => this.selectDefaultCharset()); - this._parser.setEscHandler({intermediates: '%', final: 'G'}, () => this.selectDefaultCharset()); + this._parser.registerEscHandler({ final: '7' }, () => this.saveCursor()); + this._parser.registerEscHandler({ final: '8' }, () => this.restoreCursor()); + this._parser.registerEscHandler({ final: 'D' }, () => this.index()); + this._parser.registerEscHandler({ final: 'E' }, () => this.nextLine()); + this._parser.registerEscHandler({ final: 'H' }, () => this.tabSet()); + this._parser.registerEscHandler({ final: 'M' }, () => this.reverseIndex()); + this._parser.registerEscHandler({ final: '=' }, () => this.keypadApplicationMode()); + this._parser.registerEscHandler({ final: '>' }, () => this.keypadNumericMode()); + this._parser.registerEscHandler({ final: 'c' }, () => this.fullReset()); + this._parser.registerEscHandler({ final: 'n' }, () => this.setgLevel(2)); + this._parser.registerEscHandler({ final: 'o' }, () => this.setgLevel(3)); + this._parser.registerEscHandler({ final: '|' }, () => this.setgLevel(3)); + this._parser.registerEscHandler({ final: '}' }, () => this.setgLevel(2)); + this._parser.registerEscHandler({ final: '~' }, () => this.setgLevel(1)); + this._parser.registerEscHandler({ intermediates: '%', final: '@' }, () => this.selectDefaultCharset()); + this._parser.registerEscHandler({ intermediates: '%', final: 'G' }, () => this.selectDefaultCharset()); for (const flag in CHARSETS) { - this._parser.setEscHandler({intermediates: '(', final: flag}, () => this.selectCharset('(' + flag)); - this._parser.setEscHandler({intermediates: ')', final: flag}, () => this.selectCharset(')' + flag)); - this._parser.setEscHandler({intermediates: '*', final: flag}, () => this.selectCharset('*' + flag)); - this._parser.setEscHandler({intermediates: '+', final: flag}, () => this.selectCharset('+' + flag)); - this._parser.setEscHandler({intermediates: '-', final: flag}, () => this.selectCharset('-' + flag)); - this._parser.setEscHandler({intermediates: '.', final: flag}, () => this.selectCharset('.' + flag)); - this._parser.setEscHandler({intermediates: '/', final: flag}, () => this.selectCharset('/' + flag)); // TODO: supported? + this._parser.registerEscHandler({ intermediates: '(', final: flag }, () => this.selectCharset('(' + flag)); + this._parser.registerEscHandler({ intermediates: ')', final: flag }, () => this.selectCharset(')' + flag)); + this._parser.registerEscHandler({ intermediates: '*', final: flag }, () => this.selectCharset('*' + flag)); + this._parser.registerEscHandler({ intermediates: '+', final: flag }, () => this.selectCharset('+' + flag)); + this._parser.registerEscHandler({ intermediates: '-', final: flag }, () => this.selectCharset('-' + flag)); + this._parser.registerEscHandler({ intermediates: '.', final: flag }, () => this.selectCharset('.' + flag)); + this._parser.registerEscHandler({ intermediates: '/', final: flag }, () => this.selectCharset('/' + flag)); // TODO: supported? } - this._parser.setEscHandler({intermediates: '#', final: '8'}, () => this.screenAlignmentPattern()); + this._parser.registerEscHandler({ intermediates: '#', final: '8' }, () => this.screenAlignmentPattern()); /** * error handler @@ -444,17 +469,70 @@ export class InputHandler extends Disposable implements IInputHandler { /** * DCS handler */ - this._parser.setDcsHandler({intermediates: '$', final: 'q'}, new DECRQSS(this._bufferService, this._coreService, this._logService, this._optionsService)); + this._parser.registerDcsHandler({ intermediates: '$', final: 'q' }, new DECRQSS(this._bufferService, this._coreService, this._logService, this._optionsService)); } public dispose(): void { super.dispose(); } - public parse(data: string | Uint8Array): void { - let buffer = this._bufferService.buffer; - const cursorStartX = buffer.x; - const cursorStartY = buffer.y; + /** + * Async parse support. + */ + private _preserveStack(cursorStartX: number, cursorStartY: number, decodedLength: number, position: number): void { + this._parseStack.paused = true; + this._parseStack.cursorStartX = cursorStartX; + this._parseStack.cursorStartY = cursorStartY; + this._parseStack.decodedLength = decodedLength; + this._parseStack.position = position; + } + + private _logSlowResolvingAsync(p: Promise): void { + // log a limited warning about an async handler taking too long + if (this._logService.logLevel <= LogLevelEnum.WARN) { + Promise.race([p, new Promise((res, rej) => setTimeout(() => rej('#SLOW_TIMEOUT'), SLOW_ASYNC_LIMIT))]) + .catch(err => { + if (err !== '#SLOW_TIMEOUT') { + throw err; + } + console.warn(`async parser handler taking longer than ${SLOW_ASYNC_LIMIT} ms`); + }); + } + } + + /** + * Parse call with async handler support. + * + * Whether the stack state got preserved for the next call, is indicated by the return value: + * - undefined (void): + * all handlers were sync, no stack save, continue normally with next chunk + * - Promise\: + * execution stopped at async handler, stack saved, continue with + * same chunk and the promise resolve value as `promiseResult` until the method returns `undefined` + * + * Note: This method should only be called by `Terminal.write` to ensure correct execution order and + * proper continuation of async parser handlers. + */ + public parse(data: string | Uint8Array, promiseResult?: boolean): void | Promise { + let result: void | Promise; + let cursorStartX = this._activeBuffer.x; + let cursorStartY = this._activeBuffer.y; + let start = 0; + const wasPaused = this._parseStack.paused; + + if (wasPaused) { + // assumption: _parseBuffer never mutates between async calls + if (result = this._parser.parse(this._parseBuffer, this._parseStack.decodedLength, promiseResult)) { + this._logSlowResolvingAsync(result); + return result; + } + cursorStartX = this._parseStack.cursorStartX; + cursorStartY = this._parseStack.cursorStartY; + this._parseStack.paused = false; + if (data.length > MAX_PARSEBUFFER_LENGTH) { + start = this._parseStack.position + MAX_PARSEBUFFER_LENGTH; + } + } this._logService.debug('parsing data', data); @@ -466,26 +544,38 @@ export class InputHandler extends Disposable implements IInputHandler { } // Clear the dirty row service so we know which lines changed as a result of parsing - this._dirtyRowService.clearRange(); + // Important: do not clear between async calls, otherwise we lost pending update information. + if (!wasPaused) { + this._dirtyRowService.clearRange(); + } // process big data in smaller chunks if (data.length > MAX_PARSEBUFFER_LENGTH) { - for (let i = 0; i < data.length; i += MAX_PARSEBUFFER_LENGTH) { + for (let i = start; i < data.length; i += MAX_PARSEBUFFER_LENGTH) { const end = i + MAX_PARSEBUFFER_LENGTH < data.length ? i + MAX_PARSEBUFFER_LENGTH : data.length; const len = (typeof data === 'string') ? this._stringDecoder.decode(data.substring(i, end), this._parseBuffer) : this._utf8Decoder.decode(data.subarray(i, end), this._parseBuffer); - this._parser.parse(this._parseBuffer, len); + if (result = this._parser.parse(this._parseBuffer, len)) { + this._preserveStack(cursorStartX, cursorStartY, len, i); + this._logSlowResolvingAsync(result); + return result; + } } } else { - const len = (typeof data === 'string') - ? this._stringDecoder.decode(data, this._parseBuffer) - : this._utf8Decoder.decode(data, this._parseBuffer); - this._parser.parse(this._parseBuffer, len); + if (!wasPaused) { + const len = (typeof data === 'string') + ? this._stringDecoder.decode(data, this._parseBuffer) + : this._utf8Decoder.decode(data, this._parseBuffer); + if (result = this._parser.parse(this._parseBuffer, len)) { + this._preserveStack(cursorStartX, cursorStartY, len, 0); + this._logSlowResolvingAsync(result); + return result; + } + } } - buffer = this._bufferService.buffer; - if (buffer.x !== cursorStartX || buffer.y !== cursorStartY) { + if (this._activeBuffer.x !== cursorStartX || this._activeBuffer.y !== cursorStartY) { this._onCursorMove.fire(); } @@ -496,20 +586,19 @@ export class InputHandler extends Disposable implements IInputHandler { public print(data: Uint32Array, start: number, end: number): void { let code: number; let chWidth: number; - const buffer = this._bufferService.buffer; const charset = this._charsetService.charset; const screenReaderMode = this._optionsService.options.screenReaderMode; const cols = this._bufferService.cols; const wraparoundMode = this._coreService.decPrivateModes.wraparound; const insertMode = this._coreService.modes.insertMode; const curAttr = this._curAttrData; - let bufferRow = buffer.lines.get(buffer.ybase + buffer.y)!; + let bufferRow = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!; - this._dirtyRowService.markDirty(buffer.y); + this._dirtyRowService.markDirty(this._activeBuffer.y); // handle wide chars: reset start_cell-1 if we would overwrite the second cell of a wide char - if (buffer.x && end - start > 0 && bufferRow.getWidth(buffer.x - 1) === 2) { - bufferRow.setCellFromCodePoint(buffer.x - 1, 0, 1, curAttr.fg, curAttr.bg, curAttr.extended); + if (this._activeBuffer.x && end - start > 0 && bufferRow.getWidth(this._activeBuffer.x - 1) === 2) { + bufferRow.setCellFromCodePoint(this._activeBuffer.x - 1, 0, 1, curAttr.fg, curAttr.bg, curAttr.extended); } for (let pos = start; pos < end; ++pos) { @@ -534,17 +623,17 @@ export class InputHandler extends Disposable implements IInputHandler { } // insert combining char at last cursor position - // buffer.x should never be 0 for a combining char + // this._activeBuffer.x should never be 0 for a combining char // since they always follow a cell consuming char - // therefore we can test for buffer.x to avoid overflow left - if (!chWidth && buffer.x) { - if (!bufferRow.getWidth(buffer.x - 1)) { + // therefore we can test for this._activeBuffer.x to avoid overflow left + if (!chWidth && this._activeBuffer.x) { + if (!bufferRow.getWidth(this._activeBuffer.x - 1)) { // found empty cell after fullwidth, need to go 2 cells back // it is save to step 2 cells back here // since an empty cell is only set by fullwidth chars - bufferRow.addCodepointToCell(buffer.x - 2, code); + bufferRow.addCodepointToCell(this._activeBuffer.x - 2, code); } else { - bufferRow.addCodepointToCell(buffer.x - 1, code); + bufferRow.addCodepointToCell(this._activeBuffer.x - 1, code); } continue; } @@ -552,31 +641,31 @@ export class InputHandler extends Disposable implements IInputHandler { // goto next line if ch would overflow // NOTE: To avoid costly width checks here, // the terminal does not allow a cols < 2. - if (buffer.x + chWidth - 1 >= cols) { + if (this._activeBuffer.x + chWidth - 1 >= cols) { // autowrap - DECAWM // automatically wraps to the beginning of the next line if (wraparoundMode) { // clear left over cells to the right - while (buffer.x < cols) { - bufferRow.setCellFromCodePoint(buffer.x++, 0, 1, curAttr.fg, curAttr.bg, curAttr.extended); + while (this._activeBuffer.x < cols) { + bufferRow.setCellFromCodePoint(this._activeBuffer.x++, 0, 1, curAttr.fg, curAttr.bg, curAttr.extended); } - buffer.x = 0; - buffer.y++; - if (buffer.y === buffer.scrollBottom + 1) { - buffer.y--; - this._onRequestScroll.fire(this._eraseAttrData(), true); + this._activeBuffer.x = 0; + this._activeBuffer.y++; + if (this._activeBuffer.y === this._activeBuffer.scrollBottom + 1) { + this._activeBuffer.y--; + this._bufferService.scroll(this._eraseAttrData(), true); } else { - if (buffer.y >= this._bufferService.rows) { - buffer.y = this._bufferService.rows - 1; + if (this._activeBuffer.y >= this._bufferService.rows) { + this._activeBuffer.y = this._bufferService.rows - 1; } // The line already exists (eg. the initial viewport), mark it as a // wrapped line - buffer.lines.get(buffer.ybase + buffer.y)!.isWrapped = true; + this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!.isWrapped = true; } // row changed, get it again - bufferRow = buffer.lines.get(buffer.ybase + buffer.y)!; + bufferRow = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!; } else { - buffer.x = cols - 1; + this._activeBuffer.x = cols - 1; if (chWidth === 2) { // FIXME: check for xterm behavior // What to do here? We got a wide char that does not fit into last cell @@ -588,7 +677,7 @@ export class InputHandler extends Disposable implements IInputHandler { // insert mode: move characters to right if (insertMode) { // right shift cells according to the width - bufferRow.insertCells(buffer.x, chWidth, buffer.getNullCell(curAttr), curAttr); + bufferRow.insertCells(this._activeBuffer.x, chWidth, this._activeBuffer.getNullCell(curAttr), curAttr); // test last cell - since the last cell has only room for // a halfwidth char any fullwidth shifted there is lost // and will be set to empty cell @@ -598,15 +687,15 @@ export class InputHandler extends Disposable implements IInputHandler { } // write current char to buffer and advance cursor - bufferRow.setCellFromCodePoint(buffer.x++, code, chWidth, curAttr.fg, curAttr.bg, curAttr.extended); + bufferRow.setCellFromCodePoint(this._activeBuffer.x++, code, chWidth, curAttr.fg, curAttr.bg, curAttr.extended); // fullwidth char - also set next cell to placeholder stub and advance cursor // for graphemes bigger than fullwidth we can simply loop to zero - // we already made sure above, that buffer.x + chWidth will not overflow right + // we already made sure above, that this._activeBuffer.x + chWidth will not overflow right if (chWidth > 0) { while (--chWidth) { // other than a regular empty cell a cell following a wide char has no width - bufferRow.setCellFromCodePoint(buffer.x++, 0, 0, curAttr.fg, curAttr.bg, curAttr.extended); + bufferRow.setCellFromCodePoint(this._activeBuffer.x++, 0, 0, curAttr.fg, curAttr.bg, curAttr.extended); } } } @@ -615,7 +704,7 @@ export class InputHandler extends Disposable implements IInputHandler { // - fullwidth + surrogates: reset // - combining: only base char gets carried on (bug in xterm?) if (end - start > 0) { - bufferRow.loadCell(buffer.x - 1, this._workCell); + bufferRow.loadCell(this._activeBuffer.x - 1, this._workCell); if (this._workCell.getWidth() === 2 || this._workCell.getCode() > 0xFFFF) { this._parser.precedingCodepoint = 0; } else if (this._workCell.isCombined()) { @@ -626,48 +715,48 @@ export class InputHandler extends Disposable implements IInputHandler { } // handle wide chars: reset cell to the right if it is second cell of a wide char - if (buffer.x < cols && end - start > 0 && bufferRow.getWidth(buffer.x) === 0 && !bufferRow.hasContent(buffer.x)) { - bufferRow.setCellFromCodePoint(buffer.x, 0, 1, curAttr.fg, curAttr.bg, curAttr.extended); + if (this._activeBuffer.x < cols && end - start > 0 && bufferRow.getWidth(this._activeBuffer.x) === 0 && !bufferRow.hasContent(this._activeBuffer.x)) { + bufferRow.setCellFromCodePoint(this._activeBuffer.x, 0, 1, curAttr.fg, curAttr.bg, curAttr.extended); } - this._dirtyRowService.markDirty(buffer.y); + this._dirtyRowService.markDirty(this._activeBuffer.y); } /** - * Forward addCsiHandler from parser. + * Forward registerCsiHandler from parser. */ - public addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable { + public registerCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean | Promise): IDisposable { if (id.final === 't' && !id.prefix && !id.intermediates) { // security: always check whether window option is allowed - return this._parser.addCsiHandler(id, params => { + return this._parser.registerCsiHandler(id, params => { if (!paramToWindowOption(params.params[0], this._optionsService.options.windowOptions)) { return true; } return callback(params); }); } - return this._parser.addCsiHandler(id, callback); + return this._parser.registerCsiHandler(id, callback); } /** - * Forward addDcsHandler from parser. + * Forward registerDcsHandler from parser. */ - public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean): IDisposable { - return this._parser.addDcsHandler(id, new DcsHandler(callback)); + public registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean | Promise): IDisposable { + return this._parser.registerDcsHandler(id, new DcsHandler(callback)); } /** - * Forward addEscHandler from parser. + * Forward registerEscHandler from parser. */ - public addEscHandler(id: IFunctionIdentifier, callback: () => boolean): IDisposable { - return this._parser.addEscHandler(id, callback); + public registerEscHandler(id: IFunctionIdentifier, callback: () => boolean | Promise): IDisposable { + return this._parser.registerEscHandler(id, callback); } /** - * Forward addOscHandler from parser. + * Forward registerOscHandler from parser. */ - public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { - return this._parser.addOscHandler(ident, new OscHandler(callback)); + public registerOscHandler(ident: number, callback: (data: string) => boolean | Promise): IDisposable { + return this._parser.registerOscHandler(ident, new OscHandler(callback)); } /** @@ -678,8 +767,9 @@ export class InputHandler extends Disposable implements IInputHandler { * The behavior of the bell is further customizable with `ITerminalOptions.bellStyle` * and `ITerminalOptions.bellSound`. */ - public bell(): void { + public bell(): boolean { this._onRequestBell.fire(); + return true; } /** @@ -692,28 +782,26 @@ export class InputHandler extends Disposable implements IInputHandler { * @vt: #Y C0 VT "Vertical Tabulation" "\v, \x0B" "Treated as LF." * @vt: #Y C0 FF "Form Feed" "\f, \x0C" "Treated as LF." */ - public lineFeed(): void { - // make buffer local for faster access - const buffer = this._bufferService.buffer; - - this._dirtyRowService.markDirty(buffer.y); + public lineFeed(): boolean { + this._dirtyRowService.markDirty(this._activeBuffer.y); if (this._optionsService.options.convertEol) { - buffer.x = 0; + this._activeBuffer.x = 0; } - buffer.y++; - if (buffer.y === buffer.scrollBottom + 1) { - buffer.y--; - this._onRequestScroll.fire(this._eraseAttrData()); - } else if (buffer.y >= this._bufferService.rows) { - buffer.y = this._bufferService.rows - 1; + this._activeBuffer.y++; + if (this._activeBuffer.y === this._activeBuffer.scrollBottom + 1) { + this._activeBuffer.y--; + this._bufferService.scroll(this._eraseAttrData()); + } else if (this._activeBuffer.y >= this._bufferService.rows) { + this._activeBuffer.y = this._bufferService.rows - 1; } // If the end of the line is hit, prevent this action from wrapping around to the next line. - if (buffer.x >= this._bufferService.cols) { - buffer.x--; + if (this._activeBuffer.x >= this._bufferService.cols) { + this._activeBuffer.x--; } - this._dirtyRowService.markDirty(buffer.y); + this._dirtyRowService.markDirty(this._activeBuffer.y); this._onLineFeed.fire(); + return true; } /** @@ -722,8 +810,9 @@ export class InputHandler extends Disposable implements IInputHandler { * * @vt: #Y C0 CR "Carriage Return" "\r, \x0D" "Move the cursor to the beginning of the row." */ - public carriageReturn(): void { - this._bufferService.buffer.x = 0; + public carriageReturn(): boolean { + this._activeBuffer.x = 0; + return true; } /** @@ -737,16 +826,14 @@ export class InputHandler extends Disposable implements IInputHandler { * to the end of the previous row. Note that it is not possible to peek back into the scrollbuffer * with the cursor, thus at the home position (top-leftmost cell) this has no effect. */ - public backspace(): void { - const buffer = this._bufferService.buffer; - + public backspace(): boolean { // reverse wrap-around is disabled if (!this._coreService.decPrivateModes.reverseWraparound) { this._restrictCursor(); - if (buffer.x > 0) { - buffer.x--; + if (this._activeBuffer.x > 0) { + this._activeBuffer.x--; } - return; + return true; } // reverse wrap-around is enabled @@ -754,8 +841,8 @@ export class InputHandler extends Disposable implements IInputHandler { // to be at x=cols to be able to address the last cell of a row by BS this._restrictCursor(this._bufferService.cols); - if (buffer.x > 0) { - buffer.x--; + if (this._activeBuffer.x > 0) { + this._activeBuffer.x--; } else { /** * reverse wrap-around handling: @@ -765,21 +852,21 @@ export class InputHandler extends Disposable implements IInputHandler { * - cannot peek into scrollbuffer * - any cursor movement sequence keeps working as expected */ - if (buffer.x === 0 - && buffer.y > buffer.scrollTop - && buffer.y <= buffer.scrollBottom - && buffer.lines.get(buffer.ybase + buffer.y)?.isWrapped) + if (this._activeBuffer.x === 0 + && this._activeBuffer.y > this._activeBuffer.scrollTop + && this._activeBuffer.y <= this._activeBuffer.scrollBottom + && this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)?.isWrapped) { - buffer.lines.get(buffer.ybase + buffer.y)!.isWrapped = false; - buffer.y--; - buffer.x = this._bufferService.cols - 1; + this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!.isWrapped = false; + this._activeBuffer.y--; + this._activeBuffer.x = this._bufferService.cols - 1; // find last taken cell - last cell can have 3 different states: // - hasContent(true) + hasWidth(1): narrow char - we are done // - hasWidth(0): second part of wide char - we are done // - hasContent(false) + hasWidth(1): empty cell due to early wrapping wide char, go one cell further back - const line = buffer.lines.get(buffer.ybase + buffer.y)!; - if (line.hasWidth(buffer.x) && !line.hasContent(buffer.x)) { - buffer.x--; + const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!; + if (line.hasWidth(this._activeBuffer.x) && !line.hasContent(this._activeBuffer.x)) { + this._activeBuffer.x--; // We do this only once, since width=1 + hasContent=false currently happens only once before // early wrapping of a wide char. // This needs to be fixed once we support graphemes taking more than 2 cells. @@ -787,6 +874,7 @@ export class InputHandler extends Disposable implements IInputHandler { } } this._restrictCursor(); + return true; } /** @@ -795,15 +883,16 @@ export class InputHandler extends Disposable implements IInputHandler { * * @vt: #Y C0 HT "Horizontal Tabulation" "\t, \x09" "Move the cursor to the next character tab stop." */ - public tab(): void { - if (this._bufferService.buffer.x >= this._bufferService.cols) { - return; + public tab(): boolean { + if (this._activeBuffer.x >= this._bufferService.cols) { + return true; } - const originalX = this._bufferService.buffer.x; - this._bufferService.buffer.x = this._bufferService.buffer.nextStop(); + const originalX = this._activeBuffer.x; + this._activeBuffer.x = this._activeBuffer.nextStop(); if (this._optionsService.options.screenReaderMode) { - this._onA11yTab.fire(this._bufferService.buffer.x - originalX); + this._onA11yTab.fire(this._activeBuffer.x - originalX); } + return true; } /** @@ -813,8 +902,9 @@ export class InputHandler extends Disposable implements IInputHandler { * * @vt: #P[Only limited ISO-2022 charset support.] C0 SO "Shift Out" "\x0E" "Switch to an alternative character set." */ - public shiftOut(): void { + public shiftOut(): boolean { this._charsetService.setgLevel(1); + return true; } /** @@ -824,35 +914,36 @@ export class InputHandler extends Disposable implements IInputHandler { * * @vt: #Y C0 SI "Shift In" "\x0F" "Return to regular character set after Shift Out." */ - public shiftIn(): void { + public shiftIn(): boolean { this._charsetService.setgLevel(0); + return true; } /** * Restrict cursor to viewport size / scroll margin (origin mode). */ private _restrictCursor(maxCol: number = this._bufferService.cols - 1): void { - this._bufferService.buffer.x = Math.min(maxCol, Math.max(0, this._bufferService.buffer.x)); - this._bufferService.buffer.y = this._coreService.decPrivateModes.origin - ? Math.min(this._bufferService.buffer.scrollBottom, Math.max(this._bufferService.buffer.scrollTop, this._bufferService.buffer.y)) - : Math.min(this._bufferService.rows - 1, Math.max(0, this._bufferService.buffer.y)); - this._dirtyRowService.markDirty(this._bufferService.buffer.y); + this._activeBuffer.x = Math.min(maxCol, Math.max(0, this._activeBuffer.x)); + this._activeBuffer.y = this._coreService.decPrivateModes.origin + ? Math.min(this._activeBuffer.scrollBottom, Math.max(this._activeBuffer.scrollTop, this._activeBuffer.y)) + : Math.min(this._bufferService.rows - 1, Math.max(0, this._activeBuffer.y)); + this._dirtyRowService.markDirty(this._activeBuffer.y); } /** * Set absolute cursor position. */ private _setCursor(x: number, y: number): void { - this._dirtyRowService.markDirty(this._bufferService.buffer.y); + this._dirtyRowService.markDirty(this._activeBuffer.y); if (this._coreService.decPrivateModes.origin) { - this._bufferService.buffer.x = x; - this._bufferService.buffer.y = this._bufferService.buffer.scrollTop + y; + this._activeBuffer.x = x; + this._activeBuffer.y = this._activeBuffer.scrollTop + y; } else { - this._bufferService.buffer.x = x; - this._bufferService.buffer.y = y; + this._activeBuffer.x = x; + this._activeBuffer.y = y; } this._restrictCursor(); - this._dirtyRowService.markDirty(this._bufferService.buffer.y); + this._dirtyRowService.markDirty(this._activeBuffer.y); } /** @@ -862,7 +953,7 @@ export class InputHandler extends Disposable implements IInputHandler { // for relative changes we have to make sure we are within 0 .. cols/rows - 1 // before calculating the new position this._restrictCursor(); - this._setCursor(this._bufferService.buffer.x + x, this._bufferService.buffer.y + y); + this._setCursor(this._activeBuffer.x + x, this._activeBuffer.y + y); } /** @@ -872,14 +963,15 @@ export class InputHandler extends Disposable implements IInputHandler { * @vt: #Y CSI CUU "Cursor Up" "CSI Ps A" "Move cursor `Ps` times up (default=1)." * If the cursor would pass the top scroll margin, it will stop there. */ - public cursorUp(params: IParams): void { + public cursorUp(params: IParams): boolean { // stop at scrollTop - const diffToTop = this._bufferService.buffer.y - this._bufferService.buffer.scrollTop; + const diffToTop = this._activeBuffer.y - this._activeBuffer.scrollTop; if (diffToTop >= 0) { this._moveCursor(0, -Math.min(diffToTop, params.params[0] || 1)); } else { this._moveCursor(0, -(params.params[0] || 1)); } + return true; } /** @@ -889,14 +981,15 @@ export class InputHandler extends Disposable implements IInputHandler { * @vt: #Y CSI CUD "Cursor Down" "CSI Ps B" "Move cursor `Ps` times down (default=1)." * If the cursor would pass the bottom scroll margin, it will stop there. */ - public cursorDown(params: IParams): void { + public cursorDown(params: IParams): boolean { // stop at scrollBottom - const diffToBottom = this._bufferService.buffer.scrollBottom - this._bufferService.buffer.y; + const diffToBottom = this._activeBuffer.scrollBottom - this._activeBuffer.y; if (diffToBottom >= 0) { this._moveCursor(0, Math.min(diffToBottom, params.params[0] || 1)); } else { this._moveCursor(0, params.params[0] || 1); } + return true; } /** @@ -905,8 +998,9 @@ export class InputHandler extends Disposable implements IInputHandler { * * @vt: #Y CSI CUF "Cursor Forward" "CSI Ps C" "Move cursor `Ps` times forward (default=1)." */ - public cursorForward(params: IParams): void { + public cursorForward(params: IParams): boolean { this._moveCursor(params.params[0] || 1, 0); + return true; } /** @@ -915,8 +1009,9 @@ export class InputHandler extends Disposable implements IInputHandler { * * @vt: #Y CSI CUB "Cursor Backward" "CSI Ps D" "Move cursor `Ps` times backward (default=1)." */ - public cursorBackward(params: IParams): void { + public cursorBackward(params: IParams): boolean { this._moveCursor(-(params.params[0] || 1), 0); + return true; } /** @@ -927,9 +1022,10 @@ export class InputHandler extends Disposable implements IInputHandler { * @vt: #Y CSI CNL "Cursor Next Line" "CSI Ps E" "Move cursor `Ps` times down (default=1) and to the first column." * Same as CUD, additionally places the cursor at the first column. */ - public cursorNextLine(params: IParams): void { + public cursorNextLine(params: IParams): boolean { this.cursorDown(params); - this._bufferService.buffer.x = 0; + this._activeBuffer.x = 0; + return true; } /** @@ -940,9 +1036,10 @@ export class InputHandler extends Disposable implements IInputHandler { * @vt: #Y CSI CPL "Cursor Backward" "CSI Ps F" "Move cursor `Ps` times up (default=1) and to the first column." * Same as CUU, additionally places the cursor at the first column. */ - public cursorPrecedingLine(params: IParams): void { + public cursorPrecedingLine(params: IParams): boolean { this.cursorUp(params); - this._bufferService.buffer.x = 0; + this._activeBuffer.x = 0; + return true; } /** @@ -951,8 +1048,9 @@ export class InputHandler extends Disposable implements IInputHandler { * * @vt: #Y CSI CHA "Cursor Horizontal Absolute" "CSI Ps G" "Move cursor to `Ps`-th column of the active row (default=1)." */ - public cursorCharAbsolute(params: IParams): void { - this._setCursor((params.params[0] || 1) - 1, this._bufferService.buffer.y); + public cursorCharAbsolute(params: IParams): boolean { + this._setCursor((params.params[0] || 1) - 1, this._activeBuffer.y); + return true; } /** @@ -964,12 +1062,14 @@ export class InputHandler extends Disposable implements IInputHandler { * If ORIGIN mode is not set, places the cursor to the absolute position within the viewport. * Note that the coordinates are 1-based, thus the top left position starts at `1 ; 1`. */ - public cursorPosition(params: IParams): void { + public cursorPosition(params: IParams): boolean { this._setCursor( // col (params.length >= 2) ? (params.params[1] || 1) - 1 : 0, // row - (params.params[0] || 1) - 1); + (params.params[0] || 1) - 1 + ); + return true; } /** @@ -979,8 +1079,9 @@ export class InputHandler extends Disposable implements IInputHandler { * * @vt: #Y CSI HPA "Horizontal Position Absolute" "CSI Ps ` " "Same as CHA." */ - public charPosAbsolute(params: IParams): void { - this._setCursor((params.params[0] || 1) - 1, this._bufferService.buffer.y); + public charPosAbsolute(params: IParams): boolean { + this._setCursor((params.params[0] || 1) - 1, this._activeBuffer.y); + return true; } /** @@ -989,8 +1090,9 @@ export class InputHandler extends Disposable implements IInputHandler { * * @vt: #Y CSI HPR "Horizontal Position Relative" "CSI Ps a" "Same as CUF." */ - public hPositionRelative(params: IParams): void { + public hPositionRelative(params: IParams): boolean { this._moveCursor(params.params[0] || 1, 0); + return true; } /** @@ -999,8 +1101,9 @@ export class InputHandler extends Disposable implements IInputHandler { * * @vt: #Y CSI VPA "Vertical Position Absolute" "CSI Ps d" "Move cursor to `Ps`-th row (default=1)." */ - public linePosAbsolute(params: IParams): void { - this._setCursor(this._bufferService.buffer.x, (params.params[0] || 1) - 1); + public linePosAbsolute(params: IParams): boolean { + this._setCursor(this._activeBuffer.x, (params.params[0] || 1) - 1); + return true; } /** @@ -1010,8 +1113,9 @@ export class InputHandler extends Disposable implements IInputHandler { * * @vt: #Y CSI VPR "Vertical Position Relative" "CSI Ps e" "Move cursor `Ps` times down (default=1)." */ - public vPositionRelative(params: IParams): void { + public vPositionRelative(params: IParams): boolean { this._moveCursor(0, params.params[0] || 1); + return true; } /** @@ -1022,8 +1126,9 @@ export class InputHandler extends Disposable implements IInputHandler { * * @vt: #Y CSI HVP "Horizontal and Vertical Position" "CSI Ps ; Ps f" "Same as CUP." */ - public hVPosition(params: IParams): void { + public hVPosition(params: IParams): boolean { this.cursorPosition(params); + return true; } /** @@ -1037,13 +1142,14 @@ export class InputHandler extends Disposable implements IInputHandler { * @vt: #Y CSI TBC "Tab Clear" "CSI Ps g" "Clear tab stops at current position (0) or all (3) (default=0)." * Clearing tabstops off the active row (Ps = 2, VT100) is currently not supported. */ - public tabClear(params: IParams): void { + public tabClear(params: IParams): boolean { const param = params.params[0]; if (param === 0) { - delete this._bufferService.buffer.tabs[this._bufferService.buffer.x]; + delete this._activeBuffer.tabs[this._activeBuffer.x]; } else if (param === 3) { - this._bufferService.buffer.tabs = {}; + this._activeBuffer.tabs = {}; } + return true; } /** @@ -1052,14 +1158,15 @@ export class InputHandler extends Disposable implements IInputHandler { * * @vt: #Y CSI CHT "Cursor Horizontal Tabulation" "CSI Ps I" "Move cursor `Ps` times tabs forward (default=1)." */ - public cursorForwardTab(params: IParams): void { - if (this._bufferService.buffer.x >= this._bufferService.cols) { - return; + public cursorForwardTab(params: IParams): boolean { + if (this._activeBuffer.x >= this._bufferService.cols) { + return true; } let param = params.params[0] || 1; while (param--) { - this._bufferService.buffer.x = this._bufferService.buffer.nextStop(); + this._activeBuffer.x = this._activeBuffer.nextStop(); } + return true; } /** @@ -1067,18 +1174,16 @@ export class InputHandler extends Disposable implements IInputHandler { * * @vt: #Y CSI CBT "Cursor Backward Tabulation" "CSI Ps Z" "Move cursor `Ps` tabs backward (default=1)." */ - public cursorBackwardTab(params: IParams): void { - if (this._bufferService.buffer.x >= this._bufferService.cols) { - return; + public cursorBackwardTab(params: IParams): boolean { + if (this._activeBuffer.x >= this._bufferService.cols) { + return true; } let param = params.params[0] || 1; - // make buffer local for faster access - const buffer = this._bufferService.buffer; - while (param--) { - buffer.x = buffer.prevStop(); + this._activeBuffer.x = this._activeBuffer.prevStop(); } + return true; } @@ -1090,11 +1195,11 @@ export class InputHandler extends Disposable implements IInputHandler { * @param end end - 1 is last erased cell */ private _eraseInBufferLine(y: number, start: number, end: number, clearWrap: boolean = false): void { - const line = this._bufferService.buffer.lines.get(this._bufferService.buffer.ybase + y)!; + const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y)!; line.replaceCells( start, end, - this._bufferService.buffer.getNullCell(this._eraseAttrData()), + this._activeBuffer.getNullCell(this._eraseAttrData()), this._eraseAttrData() ); if (clearWrap) { @@ -1108,8 +1213,8 @@ export class InputHandler extends Disposable implements IInputHandler { * @param y row index */ private _resetBufferLine(y: number): void { - const line = this._bufferService.buffer.lines.get(this._bufferService.buffer.ybase + y)!; - line.fill(this._bufferService.buffer.getNullCell(this._eraseAttrData())); + const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y)!; + line.fill(this._activeBuffer.getNullCell(this._eraseAttrData())); line.isWrapped = false; } @@ -1137,27 +1242,27 @@ export class InputHandler extends Disposable implements IInputHandler { * * @vt: #P[Protection attributes are not supported.] CSI DECSED "Selective Erase In Display" "CSI ? Ps J" "Currently the same as ED." */ - public eraseInDisplay(params: IParams): void { - this._restrictCursor(); + public eraseInDisplay(params: IParams): boolean { + this._restrictCursor(this._bufferService.cols); let j; switch (params.params[0]) { case 0: - j = this._bufferService.buffer.y; + j = this._activeBuffer.y; this._dirtyRowService.markDirty(j); - this._eraseInBufferLine(j++, this._bufferService.buffer.x, this._bufferService.cols, this._bufferService.buffer.x === 0); + this._eraseInBufferLine(j++, this._activeBuffer.x, this._bufferService.cols, this._activeBuffer.x === 0); for (; j < this._bufferService.rows; j++) { this._resetBufferLine(j); } this._dirtyRowService.markDirty(j); break; case 1: - j = this._bufferService.buffer.y; + j = this._activeBuffer.y; this._dirtyRowService.markDirty(j); // Deleted front part of line and everything before. This line will no longer be wrapped. - this._eraseInBufferLine(j, 0, this._bufferService.buffer.x + 1, true); - if (this._bufferService.buffer.x + 1 >= this._bufferService.cols) { + this._eraseInBufferLine(j, 0, this._activeBuffer.x + 1, true); + if (this._activeBuffer.x + 1 >= this._bufferService.cols) { // Deleted entire previous line. This next line can no longer be wrapped. - this._bufferService.buffer.lines.get(j + 1)!.isWrapped = false; + this._activeBuffer.lines.get(j + 1)!.isWrapped = false; } while (j--) { this._resetBufferLine(j); @@ -1174,16 +1279,17 @@ export class InputHandler extends Disposable implements IInputHandler { break; case 3: // Clear scrollback (everything not in viewport) - const scrollBackSize = this._bufferService.buffer.lines.length - this._bufferService.rows; + const scrollBackSize = this._activeBuffer.lines.length - this._bufferService.rows; if (scrollBackSize > 0) { - this._bufferService.buffer.lines.trimStart(scrollBackSize); - this._bufferService.buffer.ybase = Math.max(this._bufferService.buffer.ybase - scrollBackSize, 0); - this._bufferService.buffer.ydisp = Math.max(this._bufferService.buffer.ydisp - scrollBackSize, 0); + this._activeBuffer.lines.trimStart(scrollBackSize); + this._activeBuffer.ybase = Math.max(this._activeBuffer.ybase - scrollBackSize, 0); + this._activeBuffer.ydisp = Math.max(this._activeBuffer.ydisp - scrollBackSize, 0); // Force a scroll event to refresh viewport this._onScroll.fire(0); } break; } + return true; } /** @@ -1208,20 +1314,21 @@ export class InputHandler extends Disposable implements IInputHandler { * * @vt: #P[Protection attributes are not supported.] CSI DECSEL "Selective Erase In Line" "CSI ? Ps K" "Currently the same as EL." */ - public eraseInLine(params: IParams): void { - this._restrictCursor(); + public eraseInLine(params: IParams): boolean { + this._restrictCursor(this._bufferService.cols); switch (params.params[0]) { case 0: - this._eraseInBufferLine(this._bufferService.buffer.y, this._bufferService.buffer.x, this._bufferService.cols); + this._eraseInBufferLine(this._activeBuffer.y, this._activeBuffer.x, this._bufferService.cols); break; case 1: - this._eraseInBufferLine(this._bufferService.buffer.y, 0, this._bufferService.buffer.x + 1); + this._eraseInBufferLine(this._activeBuffer.y, 0, this._activeBuffer.x + 1); break; case 2: - this._eraseInBufferLine(this._bufferService.buffer.y, 0, this._bufferService.cols); + this._eraseInBufferLine(this._activeBuffer.y, 0, this._bufferService.cols); break; } - this._dirtyRowService.markDirty(this._bufferService.buffer.y); + this._dirtyRowService.markDirty(this._activeBuffer.y); + return true; } /** @@ -1233,30 +1340,28 @@ export class InputHandler extends Disposable implements IInputHandler { * The cursor is set to the first column. * IL has no effect if the cursor is outside the scroll margins. */ - public insertLines(params: IParams): void { + public insertLines(params: IParams): boolean { this._restrictCursor(); let param = params.params[0] || 1; - // make buffer local for faster access - const buffer = this._bufferService.buffer; - - if (buffer.y > buffer.scrollBottom || buffer.y < buffer.scrollTop) { - return; + if (this._activeBuffer.y > this._activeBuffer.scrollBottom || this._activeBuffer.y < this._activeBuffer.scrollTop) { + return true; } - const row: number = buffer.ybase + buffer.y; + const row: number = this._activeBuffer.ybase + this._activeBuffer.y; - const scrollBottomRowsOffset = this._bufferService.rows - 1 - buffer.scrollBottom; - const scrollBottomAbsolute = this._bufferService.rows - 1 + buffer.ybase - scrollBottomRowsOffset + 1; + const scrollBottomRowsOffset = this._bufferService.rows - 1 - this._activeBuffer.scrollBottom; + const scrollBottomAbsolute = this._bufferService.rows - 1 + this._activeBuffer.ybase - scrollBottomRowsOffset + 1; while (param--) { // test: echo -e '\e[44m\e[1L\e[0m' // blankLine(true) - xterm/linux behavior - buffer.lines.splice(scrollBottomAbsolute - 1, 1); - buffer.lines.splice(row, 0, buffer.getBlankLine(this._eraseAttrData())); + this._activeBuffer.lines.splice(scrollBottomAbsolute - 1, 1); + this._activeBuffer.lines.splice(row, 0, this._activeBuffer.getBlankLine(this._eraseAttrData())); } - this._dirtyRowService.markRangeDirty(buffer.y, buffer.scrollBottom); - buffer.x = 0; // see https://vt100.net/docs/vt220-rm/chapter4.html - vt220 only? + this._dirtyRowService.markRangeDirty(this._activeBuffer.y, this._activeBuffer.scrollBottom); + this._activeBuffer.x = 0; // see https://vt100.net/docs/vt220-rm/chapter4.html - vt220 only? + return true; } /** @@ -1268,31 +1373,29 @@ export class InputHandler extends Disposable implements IInputHandler { * The cursor is set to the first column. * DL has no effect if the cursor is outside the scroll margins. */ - public deleteLines(params: IParams): void { + public deleteLines(params: IParams): boolean { this._restrictCursor(); let param = params.params[0] || 1; - // make buffer local for faster access - const buffer = this._bufferService.buffer; - - if (buffer.y > buffer.scrollBottom || buffer.y < buffer.scrollTop) { - return; + if (this._activeBuffer.y > this._activeBuffer.scrollBottom || this._activeBuffer.y < this._activeBuffer.scrollTop) { + return true; } - const row: number = buffer.ybase + buffer.y; + const row: number = this._activeBuffer.ybase + this._activeBuffer.y; let j: number; - j = this._bufferService.rows - 1 - buffer.scrollBottom; - j = this._bufferService.rows - 1 + buffer.ybase - j; + j = this._bufferService.rows - 1 - this._activeBuffer.scrollBottom; + j = this._bufferService.rows - 1 + this._activeBuffer.ybase - j; while (param--) { // test: echo -e '\e[44m\e[1M\e[0m' // blankLine(true) - xterm/linux behavior - buffer.lines.splice(row, 1); - buffer.lines.splice(j, 0, buffer.getBlankLine(this._eraseAttrData())); + this._activeBuffer.lines.splice(row, 1); + this._activeBuffer.lines.splice(j, 0, this._activeBuffer.getBlankLine(this._eraseAttrData())); } - this._dirtyRowService.markRangeDirty(buffer.y, buffer.scrollBottom); - buffer.x = 0; // see https://vt100.net/docs/vt220-rm/chapter4.html - vt220 only? + this._dirtyRowService.markRangeDirty(this._activeBuffer.y, this._activeBuffer.scrollBottom); + this._activeBuffer.x = 0; // see https://vt100.net/docs/vt220-rm/chapter4.html - vt220 only? + return true; } /** @@ -1306,18 +1409,19 @@ export class InputHandler extends Disposable implements IInputHandler { * * FIXME: check against xterm - should not work outside of scroll margins (see VT520 manual) */ - public insertChars(params: IParams): void { + public insertChars(params: IParams): boolean { this._restrictCursor(); - const line = this._bufferService.buffer.lines.get(this._bufferService.buffer.ybase + this._bufferService.buffer.y); + const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y); if (line) { line.insertCells( - this._bufferService.buffer.x, + this._activeBuffer.x, params.params[0] || 1, - this._bufferService.buffer.getNullCell(this._eraseAttrData()), + this._activeBuffer.getNullCell(this._eraseAttrData()), this._eraseAttrData() ); - this._dirtyRowService.markDirty(this._bufferService.buffer.y); + this._dirtyRowService.markDirty(this._activeBuffer.y); } + return true; } /** @@ -1331,18 +1435,19 @@ export class InputHandler extends Disposable implements IInputHandler { * * FIXME: check against xterm - should not work outside of scroll margins (see VT520 manual) */ - public deleteChars(params: IParams): void { + public deleteChars(params: IParams): boolean { this._restrictCursor(); - const line = this._bufferService.buffer.lines.get(this._bufferService.buffer.ybase + this._bufferService.buffer.y); + const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y); if (line) { line.deleteCells( - this._bufferService.buffer.x, + this._activeBuffer.x, params.params[0] || 1, - this._bufferService.buffer.getNullCell(this._eraseAttrData()), + this._activeBuffer.getNullCell(this._eraseAttrData()), this._eraseAttrData() ); - this._dirtyRowService.markDirty(this._bufferService.buffer.y); + this._dirtyRowService.markDirty(this._activeBuffer.y); } + return true; } /** @@ -1353,17 +1458,15 @@ export class InputHandler extends Disposable implements IInputHandler { * * FIXME: scrolled out lines at top = 1 should add to scrollback (xterm) */ - public scrollUp(params: IParams): void { + public scrollUp(params: IParams): boolean { let param = params.params[0] || 1; - // make buffer local for faster access - const buffer = this._bufferService.buffer; - while (param--) { - buffer.lines.splice(buffer.ybase + buffer.scrollTop, 1); - buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 0, buffer.getBlankLine(this._eraseAttrData())); + this._activeBuffer.lines.splice(this._activeBuffer.ybase + this._activeBuffer.scrollTop, 1); + this._activeBuffer.lines.splice(this._activeBuffer.ybase + this._activeBuffer.scrollBottom, 0, this._activeBuffer.getBlankLine(this._eraseAttrData())); } - this._dirtyRowService.markRangeDirty(buffer.scrollTop, buffer.scrollBottom); + this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom); + return true; } /** @@ -1371,17 +1474,15 @@ export class InputHandler extends Disposable implements IInputHandler { * * @vt: #Y CSI SD "Scroll Down" "CSI Ps T" "Scroll `Ps` lines down (default=1)." */ - public scrollDown(params: IParams): void { + public scrollDown(params: IParams): boolean { let param = params.params[0] || 1; - // make buffer local for faster access - const buffer = this._bufferService.buffer; - while (param--) { - buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 1); - buffer.lines.splice(buffer.ybase + buffer.scrollTop, 0, buffer.getBlankLine(DEFAULT_ATTR_DATA)); + this._activeBuffer.lines.splice(this._activeBuffer.ybase + this._activeBuffer.scrollBottom, 1); + this._activeBuffer.lines.splice(this._activeBuffer.ybase + this._activeBuffer.scrollTop, 0, this._activeBuffer.getBlankLine(DEFAULT_ATTR_DATA)); } - this._dirtyRowService.markRangeDirty(buffer.scrollTop, buffer.scrollBottom); + this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom); + return true; } /** @@ -1402,18 +1503,18 @@ export class InputHandler extends Disposable implements IInputHandler { * SL moves the content of all lines within the scroll margins `Ps` times to the left. * SL has no effect outside of the scroll margins. */ - public scrollLeft(params: IParams): void { - const buffer = this._bufferService.buffer; - if (buffer.y > buffer.scrollBottom || buffer.y < buffer.scrollTop) { - return; + public scrollLeft(params: IParams): boolean { + if (this._activeBuffer.y > this._activeBuffer.scrollBottom || this._activeBuffer.y < this._activeBuffer.scrollTop) { + return true; } const param = params.params[0] || 1; - for (let y = buffer.scrollTop; y <= buffer.scrollBottom; ++y) { - const line = buffer.lines.get(buffer.ybase + y)!; - line.deleteCells(0, param, buffer.getNullCell(this._eraseAttrData()), this._eraseAttrData()); + for (let y = this._activeBuffer.scrollTop; y <= this._activeBuffer.scrollBottom; ++y) { + const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y)!; + line.deleteCells(0, param, this._activeBuffer.getNullCell(this._eraseAttrData()), this._eraseAttrData()); line.isWrapped = false; } - this._dirtyRowService.markRangeDirty(buffer.scrollTop, buffer.scrollBottom); + this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom); + return true; } /** @@ -1435,18 +1536,18 @@ export class InputHandler extends Disposable implements IInputHandler { * Content at the right margin is lost. * SL has no effect outside of the scroll margins. */ - public scrollRight(params: IParams): void { - const buffer = this._bufferService.buffer; - if (buffer.y > buffer.scrollBottom || buffer.y < buffer.scrollTop) { - return; + public scrollRight(params: IParams): boolean { + if (this._activeBuffer.y > this._activeBuffer.scrollBottom || this._activeBuffer.y < this._activeBuffer.scrollTop) { + return true; } const param = params.params[0] || 1; - for (let y = buffer.scrollTop; y <= buffer.scrollBottom; ++y) { - const line = buffer.lines.get(buffer.ybase + y)!; - line.insertCells(0, param, buffer.getNullCell(this._eraseAttrData()), this._eraseAttrData()); + for (let y = this._activeBuffer.scrollTop; y <= this._activeBuffer.scrollBottom; ++y) { + const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y)!; + line.insertCells(0, param, this._activeBuffer.getNullCell(this._eraseAttrData()), this._eraseAttrData()); line.isWrapped = false; } - this._dirtyRowService.markRangeDirty(buffer.scrollTop, buffer.scrollBottom); + this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom); + return true; } /** @@ -1458,18 +1559,18 @@ export class InputHandler extends Disposable implements IInputHandler { * moving content to the right. Content at the right margin is lost. * DECIC has no effect outside the scrolling margins. */ - public insertColumns(params: IParams): void { - const buffer = this._bufferService.buffer; - if (buffer.y > buffer.scrollBottom || buffer.y < buffer.scrollTop) { - return; + public insertColumns(params: IParams): boolean { + if (this._activeBuffer.y > this._activeBuffer.scrollBottom || this._activeBuffer.y < this._activeBuffer.scrollTop) { + return true; } const param = params.params[0] || 1; - for (let y = buffer.scrollTop; y <= buffer.scrollBottom; ++y) { - const line = this._bufferService.buffer.lines.get(buffer.ybase + y)!; - line.insertCells(buffer.x, param, buffer.getNullCell(this._eraseAttrData()), this._eraseAttrData()); + for (let y = this._activeBuffer.scrollTop; y <= this._activeBuffer.scrollBottom; ++y) { + const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y)!; + line.insertCells(this._activeBuffer.x, param, this._activeBuffer.getNullCell(this._eraseAttrData()), this._eraseAttrData()); line.isWrapped = false; } - this._dirtyRowService.markRangeDirty(buffer.scrollTop, buffer.scrollBottom); + this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom); + return true; } /** @@ -1481,18 +1582,18 @@ export class InputHandler extends Disposable implements IInputHandler { * moving content to the left. Blank columns are added at the right margin. * DECDC has no effect outside the scrolling margins. */ - public deleteColumns(params: IParams): void { - const buffer = this._bufferService.buffer; - if (buffer.y > buffer.scrollBottom || buffer.y < buffer.scrollTop) { - return; + public deleteColumns(params: IParams): boolean { + if (this._activeBuffer.y > this._activeBuffer.scrollBottom || this._activeBuffer.y < this._activeBuffer.scrollTop) { + return true; } const param = params.params[0] || 1; - for (let y = buffer.scrollTop; y <= buffer.scrollBottom; ++y) { - const line = buffer.lines.get(buffer.ybase + y)!; - line.deleteCells(buffer.x, param, buffer.getNullCell(this._eraseAttrData()), this._eraseAttrData()); + for (let y = this._activeBuffer.scrollTop; y <= this._activeBuffer.scrollBottom; ++y) { + const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y)!; + line.deleteCells(this._activeBuffer.x, param, this._activeBuffer.getNullCell(this._eraseAttrData()), this._eraseAttrData()); line.isWrapped = false; } - this._dirtyRowService.markRangeDirty(buffer.scrollTop, buffer.scrollBottom); + this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom); + return true; } /** @@ -1503,18 +1604,19 @@ export class InputHandler extends Disposable implements IInputHandler { * ED erases `Ps` characters from current cursor position to the right. * ED works inside or outside the scrolling margins. */ - public eraseChars(params: IParams): void { + public eraseChars(params: IParams): boolean { this._restrictCursor(); - const line = this._bufferService.buffer.lines.get(this._bufferService.buffer.ybase + this._bufferService.buffer.y); + const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y); if (line) { line.replaceCells( - this._bufferService.buffer.x, - this._bufferService.buffer.x + (params.params[0] || 1), - this._bufferService.buffer.getNullCell(this._eraseAttrData()), + this._activeBuffer.x, + this._activeBuffer.x + (params.params[0] || 1), + this._activeBuffer.getNullCell(this._eraseAttrData()), this._eraseAttrData() ); - this._dirtyRowService.markDirty(this._bufferService.buffer.y); + this._dirtyRowService.markDirty(this._activeBuffer.y); } + return true; } /** @@ -1544,9 +1646,9 @@ export class InputHandler extends Disposable implements IInputHandler { * REP has no effect if the sequence does not follow a printable ASCII character * (NOOP for any other sequence in between or NON ASCII characters). */ - public repeatPrecedingCharacter(params: IParams): void { + public repeatPrecedingCharacter(params: IParams): boolean { if (!this._parser.precedingCodepoint) { - return; + return true; } // call print to insert the chars and handle correct wrapping const length = params.params[0] || 1; @@ -1555,6 +1657,7 @@ export class InputHandler extends Disposable implements IInputHandler { data[i] = this._parser.precedingCodepoint; } this.print(data, 0, data.length); + return true; } /** @@ -1582,15 +1685,16 @@ export class InputHandler extends Disposable implements IInputHandler { * * TODO: fix and cleanup response */ - public sendDeviceAttributesPrimary(params: IParams): void { + public sendDeviceAttributesPrimary(params: IParams): boolean { if (params.params[0] > 0) { - return; + return true; } if (this._is('xterm') || this._is('rxvt-unicode') || this._is('screen')) { this._coreService.triggerDataEvent(C0.ESC + '[?1;2c'); } else if (this._is('linux')) { this._coreService.triggerDataEvent(C0.ESC + '[?6c'); } + return true; } /** @@ -1617,9 +1721,9 @@ export class InputHandler extends Disposable implements IInputHandler { * * TODO: fix and cleanup response */ - public sendDeviceAttributesSecondary(params: IParams): void { + public sendDeviceAttributesSecondary(params: IParams): boolean { if (params.params[0] > 0) { - return; + return true; } // xterm and urxvt // seem to spit this @@ -1635,6 +1739,7 @@ export class InputHandler extends Disposable implements IInputHandler { } else if (this._is('screen')) { this._coreService.triggerDataEvent(C0.ESC + '[>83;40003;0c'); } + return true; } /** @@ -1662,7 +1767,7 @@ export class InputHandler extends Disposable implements IInputHandler { * | 12 | Send/receive (SRM). Always off. | #N | * | 20 | Automatic Newline (LNM). Always off. | #N | */ - public setMode(params: IParams): void { + public setMode(params: IParams): boolean { for (let i = 0; i < params.length; i++) { switch (params.params[i]) { case 4: @@ -1673,6 +1778,7 @@ export class InputHandler extends Disposable implements IInputHandler { break; } } + return true; } /** @@ -1788,7 +1894,7 @@ export class InputHandler extends Disposable implements IInputHandler { * * FIXME: implement DECSCNM, 1049 should clear altbuffer */ - public setModePrivate(params: IParams): void { + public setModePrivate(params: IParams): boolean { for (let i = 0; i < params.length; i++) { switch (params.params[i]) { case 1: @@ -1881,6 +1987,7 @@ export class InputHandler extends Disposable implements IInputHandler { break; } } + return true; } @@ -1904,7 +2011,7 @@ export class InputHandler extends Disposable implements IInputHandler { * * FIXME: why is LNM commented out? */ - public resetMode(params: IParams): void { + public resetMode(params: IParams): boolean { for (let i = 0; i < params.length; i++) { switch (params.params[i]) { case 4: @@ -1915,6 +2022,7 @@ export class InputHandler extends Disposable implements IInputHandler { break; } } + return true; } /** @@ -2026,7 +2134,7 @@ export class InputHandler extends Disposable implements IInputHandler { * * FIXME: DECCOLM is currently broken (already fixed in window options PR) */ - public resetModePrivate(params: IParams): void { + public resetModePrivate(params: IParams): boolean { for (let i = 0; i < params.length; i++) { switch (params.params[i]) { case 1: @@ -2103,6 +2211,7 @@ export class InputHandler extends Disposable implements IInputHandler { break; } } + return true; } /** @@ -2236,7 +2345,7 @@ export class InputHandler extends Disposable implements IInputHandler { * | 6 | Rapidly blinking. | #N | * | 7 | Inverse. Flips foreground and background color. | #Y | * | 8 | Invisible (hidden). | #Y | - * | 9 | Crossed-out characters. | #N | + * | 9 | Crossed-out characters (strikethrough). | #Y | * | 21 | Doubly underlined. | #P[Currently outputs a single underline.] | * | 22 | Normal (neither bold nor faint). | #Y | * | 23 | No italic. | #Y | @@ -2244,7 +2353,7 @@ export class InputHandler extends Disposable implements IInputHandler { * | 25 | Steady (not blinking). | #Y | * | 27 | Positive (not inverse). | #Y | * | 28 | Visible (not hidden). | #Y | - * | 29 | Not Crossed-out. | #N | + * | 29 | Not Crossed-out (strikethrough). | #Y | * | 30 | Foreground color: Black. | #Y | * | 31 | Foreground color: Red. | #Y | * | 32 | Foreground color: Green. | #Y | @@ -2295,12 +2404,12 @@ export class InputHandler extends Disposable implements IInputHandler { * FIXME: blinking is implemented in attrs, but not working in renderers? * FIXME: remove dead branch for p=100 */ - public charAttributes(params: IParams): void { + public charAttributes(params: IParams): boolean { // Optimize a single SGR0. if (params.length === 1 && params.params[0] === 0) { this._curAttrData.fg = DEFAULT_ATTR_DATA.fg; this._curAttrData.bg = DEFAULT_ATTR_DATA.bg; - return; + return true; } const l = params.length; @@ -2349,6 +2458,9 @@ export class InputHandler extends Disposable implements IInputHandler { } else if (p === 8) { // invisible attr.fg |= FgFlags.INVISIBLE; + } else if (p === 9) { + // strikethrough + attr.fg |= FgFlags.STRIKETHROUGH; } else if (p === 2) { // dimmed text attr.bg |= BgFlags.DIM; @@ -2374,6 +2486,9 @@ export class InputHandler extends Disposable implements IInputHandler { } else if (p === 28) { // not invisible attr.fg &= ~FgFlags.INVISIBLE; + } else if (p === 29) { + // not strikethrough + attr.fg &= ~FgFlags.STRIKETHROUGH; } else if (p === 39) { // reset fg attr.fg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK); @@ -2399,6 +2514,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._logService.debug('Unknown SGR attribute: %d.', p); } } + return true; } /** @@ -2426,7 +2542,7 @@ export class InputHandler extends Disposable implements IInputHandler { * * @vt: #Y CSI DSR "Device Status Report" "CSI Ps n" "Request cursor position (CPR) with `Ps` = 6." */ - public deviceStatus(params: IParams): void { + public deviceStatus(params: IParams): boolean { switch (params.params[0]) { case 5: // status report @@ -2434,22 +2550,23 @@ export class InputHandler extends Disposable implements IInputHandler { break; case 6: // cursor position - const y = this._bufferService.buffer.y + 1; - const x = this._bufferService.buffer.x + 1; + const y = this._activeBuffer.y + 1; + const x = this._activeBuffer.x + 1; this._coreService.triggerDataEvent(`${C0.ESC}[${y};${x}R`); break; } + return true; } // @vt: #P[Only CPR is supported.] CSI DECDSR "DEC Device Status Report" "CSI ? Ps n" "Only CPR is supported (same as DSR)." - public deviceStatusPrivate(params: IParams): void { + public deviceStatusPrivate(params: IParams): boolean { // modern xterm doesnt seem to // respond to any of these except ?6, 6, and 5 switch (params.params[0]) { case 6: // cursor position - const y = this._bufferService.buffer.y + 1; - const x = this._bufferService.buffer.x + 1; + const y = this._activeBuffer.y + 1; + const x = this._activeBuffer.x + 1; this._coreService.triggerDataEvent(`${C0.ESC}[?${y};${x}R`); break; case 15: @@ -2469,6 +2586,7 @@ export class InputHandler extends Disposable implements IInputHandler { // this.handler(C0.ESC + '[?50n'); break; } + return true; } /** @@ -2490,24 +2608,25 @@ export class InputHandler extends Disposable implements IInputHandler { * * FIXME: there are several more attributes missing (see VT520 manual) */ - public softReset(params: IParams): void { + public softReset(params: IParams): boolean { this._coreService.isCursorHidden = false; this._onRequestSyncScrollBar.fire(); - this._bufferService.buffer.scrollTop = 0; - this._bufferService.buffer.scrollBottom = this._bufferService.rows - 1; + this._activeBuffer.scrollTop = 0; + this._activeBuffer.scrollBottom = this._bufferService.rows - 1; this._curAttrData = DEFAULT_ATTR_DATA.clone(); this._coreService.reset(); this._charsetService.reset(); // reset DECSC data - this._bufferService.buffer.savedX = 0; - this._bufferService.buffer.savedY = this._bufferService.buffer.ybase; - this._bufferService.buffer.savedCurAttrData.fg = this._curAttrData.fg; - this._bufferService.buffer.savedCurAttrData.bg = this._curAttrData.bg; - this._bufferService.buffer.savedCharset = this._charsetService.charset; + this._activeBuffer.savedX = 0; + this._activeBuffer.savedY = this._activeBuffer.ybase; + this._activeBuffer.savedCurAttrData.fg = this._curAttrData.fg; + this._activeBuffer.savedCurAttrData.bg = this._curAttrData.bg; + this._activeBuffer.savedCharset = this._charsetService.charset; // reset DECOM this._coreService.decPrivateModes.origin = false; + return true; } /** @@ -2529,7 +2648,7 @@ export class InputHandler extends Disposable implements IInputHandler { * - 5: steady bar * - 6: blink bar */ - public setCursorStyle(params: IParams): void { + public setCursorStyle(params: IParams): boolean { const param = params.params[0] || 1; switch (param) { case 1: @@ -2547,6 +2666,7 @@ export class InputHandler extends Disposable implements IInputHandler { } const isBlinking = param % 2 === 1; this._optionsService.options.cursorBlink = isBlinking; + return true; } /** @@ -2556,7 +2676,7 @@ export class InputHandler extends Disposable implements IInputHandler { * * @vt: #Y CSI DECSTBM "Set Top and Bottom Margin" "CSI Ps ; Ps r" "Set top and bottom margins of the viewport [top;bottom] (default = viewport size)." */ - public setScrollRegion(params: IParams): void { + public setScrollRegion(params: IParams): boolean { const top = params.params[0] || 1; let bottom: number; @@ -2565,10 +2685,11 @@ export class InputHandler extends Disposable implements IInputHandler { } if (bottom > top) { - this._bufferService.buffer.scrollTop = top - 1; - this._bufferService.buffer.scrollBottom = bottom - 1; + this._activeBuffer.scrollTop = top - 1; + this._activeBuffer.scrollBottom = bottom - 1; this._setCursor(0, 0); } + return true; } /** @@ -2601,9 +2722,9 @@ export class InputHandler extends Disposable implements IInputHandler { * Ps = 23 ; 2 -> Restore xterm window title from stack. supported * Ps >= 24 not implemented */ - public windowOptions(params: IParams): void { + public windowOptions(params: IParams): boolean { if (!paramToWindowOption(params.params[0], this._optionsService.options.windowOptions)) { - return; + return true; } const second = (params.length > 1) ? params.params[1] : 0; switch (params.params[0]) { @@ -2647,6 +2768,7 @@ export class InputHandler extends Disposable implements IInputHandler { } break; } + return true; } @@ -2658,12 +2780,13 @@ export class InputHandler extends Disposable implements IInputHandler { * @vt: #P[TODO...] CSI SCOSC "Save Cursor" "CSI s" "Save cursor position, charmap and text attributes." * @vt: #Y ESC SC "Save Cursor" "ESC 7" "Save cursor position, charmap and text attributes." */ - public saveCursor(params?: IParams): void { - this._bufferService.buffer.savedX = this._bufferService.buffer.x; - this._bufferService.buffer.savedY = this._bufferService.buffer.ybase + this._bufferService.buffer.y; - this._bufferService.buffer.savedCurAttrData.fg = this._curAttrData.fg; - this._bufferService.buffer.savedCurAttrData.bg = this._curAttrData.bg; - this._bufferService.buffer.savedCharset = this._charsetService.charset; + public saveCursor(params?: IParams): boolean { + this._activeBuffer.savedX = this._activeBuffer.x; + this._activeBuffer.savedY = this._activeBuffer.ybase + this._activeBuffer.y; + this._activeBuffer.savedCurAttrData.fg = this._curAttrData.fg; + this._activeBuffer.savedCurAttrData.bg = this._curAttrData.bg; + this._activeBuffer.savedCharset = this._charsetService.charset; + return true; } @@ -2675,16 +2798,17 @@ export class InputHandler extends Disposable implements IInputHandler { * @vt: #P[TODO...] CSI SCORC "Restore Cursor" "CSI u" "Restore cursor position, charmap and text attributes." * @vt: #Y ESC RC "Restore Cursor" "ESC 8" "Restore cursor position, charmap and text attributes." */ - public restoreCursor(params?: IParams): void { - this._bufferService.buffer.x = this._bufferService.buffer.savedX || 0; - this._bufferService.buffer.y = Math.max(this._bufferService.buffer.savedY - this._bufferService.buffer.ybase, 0); - this._curAttrData.fg = this._bufferService.buffer.savedCurAttrData.fg; - this._curAttrData.bg = this._bufferService.buffer.savedCurAttrData.bg; + public restoreCursor(params?: IParams): boolean { + this._activeBuffer.x = this._activeBuffer.savedX || 0; + this._activeBuffer.y = Math.max(this._activeBuffer.savedY - this._activeBuffer.ybase, 0); + this._curAttrData.fg = this._activeBuffer.savedCurAttrData.fg; + this._curAttrData.bg = this._activeBuffer.savedCurAttrData.bg; this._charsetService.charset = (this as any)._savedCharset; - if (this._bufferService.buffer.savedCharset) { - this._charsetService.charset = this._bufferService.buffer.savedCharset; + if (this._activeBuffer.savedCharset) { + this._charsetService.charset = this._activeBuffer.savedCharset; } this._restrictCursor(); + return true; } @@ -2698,17 +2822,59 @@ export class InputHandler extends Disposable implements IInputHandler { * @vt: #Y OSC 2 "Set Windows Title" "OSC 2 ; Pt BEL" "Set window title." * xterm.js does not manipulate the title directly, instead exposes changes via the event `Terminal.onTitleChange`. */ - public setTitle(data: string): void { + public setTitle(data: string): boolean { this._windowTitle = data; this._onTitleChange.fire(data); + return true; } /** * OSC 1; ST * Note: Icon name is not exposed. */ - public setIconName(data: string): void { + public setIconName(data: string): boolean { this._iconName = data; + return true; + } + + protected _parseAnsiColorChange(data: string): IAnsiColorChangeEvent | null { + const result: IAnsiColorChangeEvent = { colors: [] }; + // example data: 5;rgb:aa/bb/cc + const regex = /(\d+);rgb:([\da-f]{2})\/([\da-f]{2})\/([\da-f]{2})/gi; + let match; + + while ((match = regex.exec(data)) !== null) { + result.colors.push({ + colorIndex: parseInt(match[1]), + red: parseInt(match[2], 16), + green: parseInt(match[3], 16), + blue: parseInt(match[4], 16) + }); + } + + if (result.colors.length === 0) { + return null; + } + + return result; + } + + /** + * OSC 4; ; ST (set ANSI color to ) + * + * @vt: #Y OSC 4 "Set ANSI color" "OSC 4 ; c ; spec BEL" "Change color number `c` to the color specified by `spec`." + * `c` is the color index between 0 and 255. `spec` color format is 'rgb:hh/hh/hh' where `h` are hexadecimal digits. + * There may be multipe c ; spec elements present in the same instruction, e.g. 1;rgb:10/20/30;2;rgb:a0/b0/c0. + */ + public setAnsiColor(data: string): boolean { + const event = this._parseAnsiColorChange(data); + if (event) { + this._onAnsiColorChange.fire(event); + } + else { + this._logService.warn(`Expected format ;rgb:// but got data: ${data}`); + } + return true; } /** @@ -2720,9 +2886,10 @@ export class InputHandler extends Disposable implements IInputHandler { * @vt: #Y C1 NEL "Next Line" "\x85" "Move the cursor to the beginning of the next row." * @vt: #Y ESC NEL "Next Line" "ESC E" "Move the cursor to the beginning of the next row." */ - public nextLine(): void { - this._bufferService.buffer.x = 0; + public nextLine(): boolean { + this._activeBuffer.x = 0; this.index(); + return true; } /** @@ -2730,10 +2897,11 @@ export class InputHandler extends Disposable implements IInputHandler { * DEC mnemonic: DECKPAM (https://vt100.net/docs/vt510-rm/DECKPAM.html) * Enables the numeric keypad to send application sequences to the host. */ - public keypadApplicationMode(): void { + public keypadApplicationMode(): boolean { this._logService.debug('Serial port requested application keypad.'); this._coreService.decPrivateModes.applicationKeypad = true; this._onRequestSyncScrollBar.fire(); + return true; } /** @@ -2741,10 +2909,11 @@ export class InputHandler extends Disposable implements IInputHandler { * DEC mnemonic: DECKPNM (https://vt100.net/docs/vt510-rm/DECKPNM.html) * Enables the keypad to send numeric characters to the host. */ - public keypadNumericMode(): void { + public keypadNumericMode(): boolean { this._logService.debug('Switching back to normal keypad.'); this._coreService.decPrivateModes.applicationKeypad = false; this._onRequestSyncScrollBar.fire(); + return true; } /** @@ -2753,9 +2922,10 @@ export class InputHandler extends Disposable implements IInputHandler { * Select default character set. UTF-8 is not supported (string are unicode anyways) * therefore ESC % G does the same. */ - public selectDefaultCharset(): void { + public selectDefaultCharset(): boolean { this._charsetService.setgLevel(0); this._charsetService.setgCharset(0, DEFAULT_CHARSET); // US (default) + return true; } /** @@ -2774,16 +2944,16 @@ export class InputHandler extends Disposable implements IInputHandler { * ESC / C * Designate G3 Character Set (VT300). C = A -> ISO Latin-1 Supplemental. - Supported? */ - public selectCharset(collectAndFlag: string): void { + public selectCharset(collectAndFlag: string): boolean { if (collectAndFlag.length !== 2) { this.selectDefaultCharset(); - return; + return true; } if (collectAndFlag[0] === '/') { - return; // TODO: Is this supported? + return true; // TODO: Is this supported? } this._charsetService.setgCharset(GLEVEL[collectAndFlag[0]], CHARSETS[collectAndFlag[1]] || DEFAULT_CHARSET); - return; + return true; } /** @@ -2795,17 +2965,17 @@ export class InputHandler extends Disposable implements IInputHandler { * @vt: #Y C1 IND "Index" "\x84" "Move the cursor one line down scrolling if needed." * @vt: #Y ESC IND "Index" "ESC D" "Move the cursor one line down scrolling if needed." */ - public index(): void { + public index(): boolean { this._restrictCursor(); - const buffer = this._bufferService.buffer; - this._bufferService.buffer.y++; - if (buffer.y === buffer.scrollBottom + 1) { - buffer.y--; - this._onRequestScroll.fire(this._eraseAttrData()); - } else if (buffer.y >= this._bufferService.rows) { - buffer.y = this._bufferService.rows - 1; + this._activeBuffer.y++; + if (this._activeBuffer.y === this._activeBuffer.scrollBottom + 1) { + this._activeBuffer.y--; + this._bufferService.scroll(this._eraseAttrData()); + } else if (this._activeBuffer.y >= this._bufferService.rows) { + this._activeBuffer.y = this._bufferService.rows - 1; } this._restrictCursor(); + return true; } /** @@ -2818,8 +2988,9 @@ export class InputHandler extends Disposable implements IInputHandler { * @vt: #Y C1 HTS "Horizontal Tabulation Set" "\x88" "Places a tab stop at the current cursor position." * @vt: #Y ESC HTS "Horizontal Tabulation Set" "ESC H" "Places a tab stop at the current cursor position." */ - public tabSet(): void { - this._bufferService.buffer.tabs[this._bufferService.buffer.x] = true; + public tabSet(): boolean { + this._activeBuffer.tabs[this._activeBuffer.x] = true; + return true; } /** @@ -2831,21 +3002,21 @@ export class InputHandler extends Disposable implements IInputHandler { * * @vt: #Y ESC IR "Reverse Index" "ESC M" "Move the cursor one line up scrolling if needed." */ - public reverseIndex(): void { + public reverseIndex(): boolean { this._restrictCursor(); - const buffer = this._bufferService.buffer; - if (buffer.y === buffer.scrollTop) { + if (this._activeBuffer.y === this._activeBuffer.scrollTop) { // possibly move the code below to term.reverseScroll(); // test: echo -ne '\e[1;1H\e[44m\eM\e[0m' // blankLine(true) is xterm/linux behavior - const scrollRegionHeight = buffer.scrollBottom - buffer.scrollTop; - buffer.lines.shiftElements(buffer.ybase + buffer.y, scrollRegionHeight, 1); - buffer.lines.set(buffer.ybase + buffer.y, buffer.getBlankLine(this._eraseAttrData())); - this._dirtyRowService.markRangeDirty(buffer.scrollTop, buffer.scrollBottom); + const scrollRegionHeight = this._activeBuffer.scrollBottom - this._activeBuffer.scrollTop; + this._activeBuffer.lines.shiftElements(this._activeBuffer.ybase + this._activeBuffer.y, scrollRegionHeight, 1); + this._activeBuffer.lines.set(this._activeBuffer.ybase + this._activeBuffer.y, this._activeBuffer.getBlankLine(this._eraseAttrData())); + this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom); } else { - buffer.y--; + this._activeBuffer.y--; this._restrictCursor(); // quickfix to not run out of bounds } + return true; } /** @@ -2853,9 +3024,10 @@ export class InputHandler extends Disposable implements IInputHandler { * DEC mnemonic: RIS (https://vt100.net/docs/vt510-rm/RIS.html) * Reset to initial state. */ - public fullReset(): void { + public fullReset(): boolean { this._parser.reset(); this._onRequestReset.fire(); + return true; } public reset(): void { @@ -2882,8 +3054,9 @@ export class InputHandler extends Disposable implements IInputHandler { * When you use a locking shift, the character set remains in GL or GR until * you use another locking shift. (partly supported) */ - public setgLevel(level: number): void { + public setgLevel(level: number): boolean { this._charsetService.setgLevel(level); + return true; } /** @@ -2894,19 +3067,18 @@ export class InputHandler extends Disposable implements IInputHandler { * * @vt: #Y ESC DECALN "Screen Alignment Pattern" "ESC # 8" "Fill viewport with a test pattern (E)." */ - public screenAlignmentPattern(): void { + public screenAlignmentPattern(): boolean { // prepare cell data const cell = new CellData(); cell.content = 1 << Content.WIDTH_SHIFT | 'E'.charCodeAt(0); cell.fg = this._curAttrData.fg; cell.bg = this._curAttrData.bg; - const buffer = this._bufferService.buffer; this._setCursor(0, 0); for (let yOffset = 0; yOffset < this._bufferService.rows; ++yOffset) { - const row = buffer.ybase + buffer.y + yOffset; - const line = buffer.lines.get(row); + const row = this._activeBuffer.ybase + this._activeBuffer.y + yOffset; + const line = this._activeBuffer.lines.get(row); if (line) { line.fill(cell); line.isWrapped = false; @@ -2914,5 +3086,6 @@ export class InputHandler extends Disposable implements IInputHandler { } this._dirtyRowService.markAllDirty(); this._setCursor(0, 0); + return true; } } diff --git a/src/common/TestUtils.test.ts b/src/common/TestUtils.test.ts index 35357744..52fe00d0 100644 --- a/src/common/TestUtils.test.ts +++ b/src/common/TestUtils.test.ts @@ -3,13 +3,13 @@ * @license MIT */ -import { IBufferService, ICoreService, ILogService, IOptionsService, ITerminalOptions, IPartialTerminalOptions, IDirtyRowService, ICoreMouseService, ICharsetService, IUnicodeService, IUnicodeVersionProvider } from 'common/services/Services'; +import { IBufferService, ICoreService, ILogService, IOptionsService, ITerminalOptions, IPartialTerminalOptions, IDirtyRowService, ICoreMouseService, ICharsetService, IUnicodeService, IUnicodeVersionProvider, LogLevelEnum } from 'common/services/Services'; import { IEvent, EventEmitter } from 'common/EventEmitter'; import { clone } from 'common/Clone'; import { DEFAULT_OPTIONS } from 'common/services/OptionsService'; import { IBufferSet, IBuffer } from 'common/buffer/Types'; import { BufferSet } from 'common/buffer/BufferSet'; -import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEventType, ICharset, IModes } from 'common/Types'; +import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEventType, ICharset, IModes, IAttributeData } from 'common/Types'; import { UnicodeV6 } from 'common/input/UnicodeV6'; export class MockBufferService implements IBufferService { @@ -17,6 +17,7 @@ export class MockBufferService implements IBufferService { public get buffer(): IBuffer { return this.buffers.active; } public buffers: IBufferSet = {} as any; public onResize: IEvent<{ cols: number, rows: number }> = new EventEmitter<{ cols: number, rows: number }>().event; + public onScroll: IEvent = new EventEmitter().event; public isUserScrolling: boolean = false; constructor( public cols: number, @@ -25,23 +26,41 @@ export class MockBufferService implements IBufferService { ) { this.buffers = new BufferSet(optionsService, this); } + public scrollPages(pageCount: number): void { + throw new Error('Method not implemented.'); + } + public scrollToTop(): void { + throw new Error('Method not implemented.'); + } + public scrollToLine(line: number): void { + throw new Error('Method not implemented.'); + } + public scroll(eraseAttr: IAttributeData, isWrapped: boolean): void { + throw new Error('Method not implemented.'); + } + public scrollToBottom(): void { + throw new Error('Method not implemented.'); + } + public scrollLines(disp: number, suppressScrollEvent?: boolean): void { + throw new Error('Method not implemented.'); + } public resize(cols: number, rows: number): void { this.cols = cols; this.rows = rows; } - public reset(): void {} + public reset(): void { } } export class MockCoreMouseService implements ICoreMouseService { public areMouseEventsActive: boolean = false; public activeEncoding: string = ''; public activeProtocol: string = ''; - public addEncoding(name: string): void {} - public addProtocol(name: string): void {} - public reset(): void {} + public addEncoding(name: string): void { } + public addProtocol(name: string): void { } + public reset(): void { } public triggerMouseEvent(event: ICoreMouseEvent): boolean { return false; } public onProtocolChange: IEvent = new EventEmitter().event; - public explainEvents(events: CoreMouseEventType): {[event: string]: boolean} { + public explainEvents(events: CoreMouseEventType): { [event: string]: boolean } { throw new Error('Method not implemented.'); } } @@ -50,9 +69,9 @@ export class MockCharsetService implements ICharsetService { public serviceBrand: any; public charset: ICharset | undefined; public glevel: number = 0; - public reset(): void {} - public setgLevel(g: number): void {} - public setgCharset(g: number, charset: ICharset): void {} + public reset(): void { } + public setgLevel(g: number): void { } + public setgCharset(g: number, charset: ICharset): void { } } export class MockCoreService implements ICoreService { @@ -75,27 +94,28 @@ export class MockCoreService implements ICoreService { public onData: IEvent = new EventEmitter().event; public onUserInput: IEvent = new EventEmitter().event; public onBinary: IEvent = new EventEmitter().event; - public reset(): void {} - public triggerDataEvent(data: string, wasUserInput?: boolean): void {} - public triggerBinaryEvent(data: string): void {} + public reset(): void { } + public triggerDataEvent(data: string, wasUserInput?: boolean): void { } + public triggerBinaryEvent(data: string): void { } } export class MockDirtyRowService implements IDirtyRowService { public serviceBrand: any; public start: number = 0; public end: number = 0; - public clearRange(): void {} - public markDirty(y: number): void {} - public markRangeDirty(y1: number, y2: number): void {} - public markAllDirty(): void {} + public clearRange(): void { } + public markDirty(y: number): void { } + public markRangeDirty(y1: number, y2: number): void { } + public markAllDirty(): void { } } export class MockLogService implements ILogService { public serviceBrand: any; - public debug(message: any, ...optionalParams: any[]): void {} - public info(message: any, ...optionalParams: any[]): void {} - public warn(message: any, ...optionalParams: any[]): void {} - public error(message: any, ...optionalParams: any[]): void {} + public logLevel = LogLevelEnum.DEBUG; + public debug(message: any, ...optionalParams: any[]): void { } + public info(message: any, ...optionalParams: any[]): void { } + public warn(message: any, ...optionalParams: any[]): void { } + public error(message: any, ...optionalParams: any[]): void { } } export class MockOptionsService implements IOptionsService { @@ -105,7 +125,7 @@ export class MockOptionsService implements IOptionsService { constructor(testOptions?: IPartialTerminalOptions) { if (testOptions) { for (const key of Object.keys(testOptions)) { - this.options[key] = (testOptions)[key]; + this.options[key] = (testOptions as any)[key]; } } } diff --git a/src/common/Types.d.ts b/src/common/Types.d.ts index bd0d11c6..78e2e62d 100644 --- a/src/common/Types.d.ts +++ b/src/common/Types.d.ts @@ -3,15 +3,23 @@ * @license MIT */ -import { ITerminalOptions as IPublicTerminalOptions } from 'xterm'; +import { IFunctionIdentifier, ITerminalOptions as IPublicTerminalOptions } from 'xterm'; import { IEvent, IEventEmitter } from 'common/EventEmitter'; import { IDeleteEvent, IInsertEvent } from 'common/CircularList'; import { IParams } from 'common/parser/Types'; -import { IOptionsService, IUnicodeService } from 'common/services/Services'; +import { ICoreMouseService, ICoreService, IOptionsService, IUnicodeService } from 'common/services/Services'; +import { IBufferSet } from 'common/buffer/Types'; export interface ICoreTerminal { + coreMouseService: ICoreMouseService; + coreService: ICoreService; optionsService: IOptionsService; unicodeService: IUnicodeService; + buffers: IBufferSet; + registerCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean | Promise): IDisposable; + registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean | Promise): IDisposable; + registerEscHandler(id: IFunctionIdentifier, callback: () => boolean | Promise): IDisposable; + registerOscHandler(ident: number, callback: (data: string) => boolean | Promise): IDisposable; } export interface IDisposable { @@ -42,6 +50,16 @@ export interface IKeyboardEvent { type: string; } +export interface IScrollEvent { + position: number; + source: ScrollSource; +} + +export const enum ScrollSource { + TERMINAL, + VIEWPORT, +} + export interface ICircularList { length: number; maxLength: number; @@ -107,6 +125,7 @@ export interface IAttributeData { isInvisible(): number; isItalic(): number; isDim(): number; + isStrikethrough(): number; // color modes getFgColorMode(): number; @@ -328,15 +347,32 @@ export interface IWindowOptions { setWinLines?: boolean; } +export interface IAnsiColorChangeEventColor { + colorIndex: number; + red: number; + green: number; + blue: number; +} + +/** + * Event fired for OSC 4 command - to change ANSI color based on its index. + */ +export interface IAnsiColorChangeEvent { + colors: IAnsiColorChangeEventColor[]; +} + /** * Calls the parser and handles actions generated by the parser. */ export interface IInputHandler { onTitleChange: IEvent; - onRequestScroll: IEvent; - parse(data: string | Uint8Array): void; + parse(data: string | Uint8Array, promiseResult?: boolean): void | Promise; print(data: Uint32Array, start: number, end: number): void; + registerCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean | Promise): IDisposable; + registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean | Promise): IDisposable; + registerEscHandler(id: IFunctionIdentifier, callback: () => boolean | Promise): IDisposable; + registerOscHandler(ident: number, callback: (data: string) => boolean | Promise): IDisposable; /** C0 BEL */ bell(): void; /** C0 LF */ lineFeed(): void; @@ -389,6 +425,7 @@ export interface IInputHandler { /** CSI ' ~ */ deleteColumns(params: IParams): void; /** OSC 0 OSC 2 */ setTitle(data: string): void; + /** OSC 4 */ setAnsiColor(data: string): void; /** ESC E */ nextLine(): void; /** ESC = */ keypadApplicationMode(): void; /** ESC > */ keypadNumericMode(): void; @@ -412,3 +449,11 @@ export interface IInputHandler { ESC ~ */ setgLevel(level: number): void; /** ESC # 8 */ screenAlignmentPattern(): void; } + +interface IParseStack { + paused: boolean; + cursorStartX: number; + cursorStartY: number; + decodedLength: number; + position: number; +} diff --git a/src/common/buffer/AttributeData.ts b/src/common/buffer/AttributeData.ts index c7217a2a..43d378ea 100644 --- a/src/common/buffer/AttributeData.ts +++ b/src/common/buffer/AttributeData.ts @@ -33,13 +33,14 @@ export class AttributeData implements IAttributeData { public extended = new ExtendedAttrs(); // flags - public isInverse(): number { return this.fg & FgFlags.INVERSE; } - public isBold(): number { return this.fg & FgFlags.BOLD; } - public isUnderline(): number { return this.fg & FgFlags.UNDERLINE; } - public isBlink(): number { return this.fg & FgFlags.BLINK; } - public isInvisible(): number { return this.fg & FgFlags.INVISIBLE; } - public isItalic(): number { return this.bg & BgFlags.ITALIC; } - public isDim(): number { return this.bg & BgFlags.DIM; } + public isInverse(): number { return this.fg & FgFlags.INVERSE; } + public isBold(): number { return this.fg & FgFlags.BOLD; } + public isUnderline(): number { return this.fg & FgFlags.UNDERLINE; } + public isBlink(): number { return this.fg & FgFlags.BLINK; } + public isInvisible(): number { return this.fg & FgFlags.INVISIBLE; } + public isItalic(): number { return this.bg & BgFlags.ITALIC; } + public isDim(): number { return this.bg & BgFlags.DIM; } + public isStrikethrough(): number { return this.fg & FgFlags.STRIKETHROUGH; } // color modes public getFgColorMode(): number { return this.fg & Attributes.CM_MASK; } diff --git a/src/common/buffer/Buffer.ts b/src/common/buffer/Buffer.ts index e06ffb01..c788bf36 100644 --- a/src/common/buffer/Buffer.ts +++ b/src/common/buffer/Buffer.ts @@ -671,6 +671,6 @@ export class BufferStringIterator implements IBufferStringIterator { content += this._buffer.translateBufferLineToString(i, this._trimRight); } this._current = range.last + 1; - return {range, content}; + return { range, content }; } } diff --git a/src/common/buffer/BufferLine.test.ts b/src/common/buffer/BufferLine.test.ts index 4c5fc3a4..fa15a854 100644 --- a/src/common/buffer/BufferLine.test.ts +++ b/src/common/buffer/BufferLine.test.ts @@ -6,7 +6,7 @@ import { NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, DEFAULT_ATTR, Content, import { BufferLine } from 'common/buffer//BufferLine'; import { CellData } from 'common/buffer/CellData'; import { CharData, IBufferLine } from '../Types'; -import { assert, expect } from 'chai'; +import { assert } from 'chai'; import { AttributeData } from 'common/buffer/AttributeData'; @@ -151,20 +151,20 @@ describe('CellData', () => { describe('BufferLine', function(): void { it('ctor', function(): void { let line: IBufferLine = new TestBufferLine(0); - expect(line.length).equals(0); - expect(line.isWrapped).equals(false); + assert.equal(line.length, 0); + assert.equal(line.isWrapped, false); line = new TestBufferLine(10); - expect(line.length).equals(10); - expect(line.loadCell(0, new CellData()).getAsCharData()).eql([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); - expect(line.isWrapped).equals(false); + assert.equal(line.length, 10); + assert.deepEqual(line.loadCell(0, new CellData()).getAsCharData(), [0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); + assert.equal(line.isWrapped, false); line = new TestBufferLine(10, undefined, true); - expect(line.length).equals(10); - expect(line.loadCell(0, new CellData()).getAsCharData()).eql([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); - expect(line.isWrapped).equals(true); + assert.equal(line.length, 10); + assert.deepEqual(line.loadCell(0, new CellData()).getAsCharData(), [0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); + assert.equal(line.isWrapped, true); line = new TestBufferLine(10, CellData.fromCharData([123, 'a', 456, 'a'.charCodeAt(0)]), true); - expect(line.length).equals(10); - expect(line.loadCell(0, new CellData()).getAsCharData()).eql([123, 'a', 456, 'a'.charCodeAt(0)]); - expect(line.isWrapped).equals(true); + assert.equal(line.length, 10); + assert.deepEqual(line.loadCell(0, new CellData()).getAsCharData(), [123, 'a', 456, 'a'.charCodeAt(0)]); + assert.equal(line.isWrapped, true); }); it('insertCells', function(): void { const line = new TestBufferLine(3); @@ -172,7 +172,7 @@ describe('BufferLine', function(): void { line.setCell(1, CellData.fromCharData([2, 'b', 0, 'b'.charCodeAt(0)])); line.setCell(2, CellData.fromCharData([3, 'c', 0, 'c'.charCodeAt(0)])); line.insertCells(1, 3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)])); - expect(line.toArray()).eql([ + assert.deepEqual(line.toArray(), [ [1, 'a', 0, 'a'.charCodeAt(0)], [4, 'd', 0, 'd'.charCodeAt(0)], [4, 'd', 0, 'd'.charCodeAt(0)] @@ -186,7 +186,7 @@ describe('BufferLine', function(): void { line.setCell(3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)])); line.setCell(4, CellData.fromCharData([5, 'e', 0, 'e'.charCodeAt(0)])); line.deleteCells(1, 2, CellData.fromCharData([6, 'f', 0, 'f'.charCodeAt(0)])); - expect(line.toArray()).eql([ + assert.deepEqual(line.toArray(), [ [1, 'a', 0, 'a'.charCodeAt(0)], [4, 'd', 0, 'd'.charCodeAt(0)], [5, 'e', 0, 'e'.charCodeAt(0)], @@ -202,7 +202,7 @@ describe('BufferLine', function(): void { line.setCell(3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)])); line.setCell(4, CellData.fromCharData([5, 'e', 0, 'e'.charCodeAt(0)])); line.replaceCells(2, 4, CellData.fromCharData([6, 'f', 0, 'f'.charCodeAt(0)])); - expect(line.toArray()).eql([ + assert.deepEqual(line.toArray(), [ [1, 'a', 0, 'a'.charCodeAt(0)], [2, 'b', 0, 'b'.charCodeAt(0)], [6, 'f', 0, 'f'.charCodeAt(0)], @@ -218,7 +218,7 @@ describe('BufferLine', function(): void { line.setCell(3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)])); line.setCell(4, CellData.fromCharData([5, 'e', 0, 'e'.charCodeAt(0)])); line.fill(CellData.fromCharData([123, 'z', 0, 'z'.charCodeAt(0)])); - expect(line.toArray()).eql([ + assert.deepEqual(line.toArray(), [ [123, 'z', 0, 'z'.charCodeAt(0)], [123, 'z', 0, 'z'.charCodeAt(0)], [123, 'z', 0, 'z'.charCodeAt(0)], @@ -234,9 +234,9 @@ describe('BufferLine', function(): void { line.setCell(3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)])); line.setCell(4, CellData.fromCharData([5, 'e', 0, 'e'.charCodeAt(0)])); const line2 = line.clone(); - expect(TestBufferLine.prototype.toArray.apply(line2)).eql(line.toArray()); - expect(line2.length).equals(line.length); - expect(line2.isWrapped).equals(line.isWrapped); + assert.deepEqual(TestBufferLine.prototype.toArray.apply(line2), line.toArray()); + assert.equal(line2.length, line.length); + assert.equal(line2.isWrapped, line.isWrapped); }); it('copyFrom', function(): void { const line = new TestBufferLine(5); @@ -247,92 +247,92 @@ describe('BufferLine', function(): void { line.setCell(4, CellData.fromCharData([5, 'e', 0, 'e'.charCodeAt(0)])); const line2 = new TestBufferLine(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), true); line2.copyFrom(line); - expect(line2.toArray()).eql(line.toArray()); - expect(line2.length).equals(line.length); - expect(line2.isWrapped).equals(line.isWrapped); + assert.deepEqual(line2.toArray(), line.toArray()); + assert.equal(line2.length, line.length); + assert.equal(line2.isWrapped, line.isWrapped); }); it('should support combining chars', function(): void { // CHAR_DATA_CODE_INDEX resembles current behavior in InputHandler.print // --> set code to the last charCodeAt value of the string // Note: needs to be fixed once the string pointer is in place const line = new TestBufferLine(2, CellData.fromCharData([1, 'e\u0301', 0, '\u0301'.charCodeAt(0)])); - expect(line.toArray()).eql([[1, 'e\u0301', 0, '\u0301'.charCodeAt(0)], [1, 'e\u0301', 0, '\u0301'.charCodeAt(0)]]); + assert.deepEqual(line.toArray(), [[1, 'e\u0301', 0, '\u0301'.charCodeAt(0)], [1, 'e\u0301', 0, '\u0301'.charCodeAt(0)]]); const line2 = new TestBufferLine(5, CellData.fromCharData([1, 'a', 0, '\u0301'.charCodeAt(0)]), true); line2.copyFrom(line); - expect(line2.toArray()).eql(line.toArray()); + assert.deepEqual(line2.toArray(), line.toArray()); const line3 = line.clone(); - expect(TestBufferLine.prototype.toArray.apply(line3)).eql(line.toArray()); + assert.deepEqual(TestBufferLine.prototype.toArray.apply(line3), line.toArray()); }); describe('resize', function(): void { it('enlarge(false)', function(): void { const line = new TestBufferLine(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false); line.resize(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)])); - expect(line.toArray()).eql((Array(10) as any).fill([1, 'a', 0, 'a'.charCodeAt(0)])); + assert.deepEqual(line.toArray(), (Array(10) as any).fill([1, 'a', 0, 'a'.charCodeAt(0)])); }); it('enlarge(true)', function(): void { const line = new TestBufferLine(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false); line.resize(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)])); - expect(line.toArray()).eql((Array(10) as any).fill([1, 'a', 0, 'a'.charCodeAt(0)])); + assert.deepEqual(line.toArray(), (Array(10) as any).fill([1, 'a', 0, 'a'.charCodeAt(0)])); }); it('shrink(true) - should apply new size', function(): void { const line = new TestBufferLine(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false); line.resize(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)])); - expect(line.toArray()).eql((Array(5) as any).fill([1, 'a', 0, 'a'.charCodeAt(0)])); + assert.deepEqual(line.toArray(), (Array(5) as any).fill([1, 'a', 0, 'a'.charCodeAt(0)])); }); it('shrink to 0 length', function(): void { const line = new TestBufferLine(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false); line.resize(0, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)])); - expect(line.toArray()).eql((Array(0) as any).fill([1, 'a', 0, 'a'.charCodeAt(0)])); + assert.deepEqual(line.toArray(), (Array(0) as any).fill([1, 'a', 0, 'a'.charCodeAt(0)])); }); it('should remove combining data on replaced cells after shrinking then enlarging', () => { const line = new TestBufferLine(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false); line.set(2, [ 0, '😁', 1, '😁'.charCodeAt(0) ]); line.set(9, [ 0, '😁', 1, '😁'.charCodeAt(0) ]); - expect(line.translateToString()).eql('aa😁aaaaaa😁'); - expect(Object.keys(line.combined).length).eql(2); + assert.equal(line.translateToString(), 'aa😁aaaaaa😁'); + assert.equal(Object.keys(line.combined).length, 2); line.resize(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)])); - expect(line.translateToString()).eql('aa😁aa'); + assert.equal(line.translateToString(), 'aa😁aa'); line.resize(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)])); - expect(line.translateToString()).eql('aa😁aaaaaaa'); - expect(Object.keys(line.combined).length).eql(1); + assert.equal(line.translateToString(), 'aa😁aaaaaaa'); + assert.equal(Object.keys(line.combined).length, 1); }); }); describe('getTrimLength', function(): void { it('empty line', function(): void { const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); - expect(line.getTrimmedLength()).equal(0); + assert.equal(line.getTrimmedLength(), 0); }); it('ASCII', function(): void { const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); line.setCell(2, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); - expect(line.getTrimmedLength()).equal(3); + assert.equal(line.getTrimmedLength(), 3); }); it('surrogate', function(): void { const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); line.setCell(2, CellData.fromCharData([1, '𝄞', 1, '𝄞'.charCodeAt(0)])); - expect(line.getTrimmedLength()).equal(3); + assert.equal(line.getTrimmedLength(), 3); }); it('combining', function(): void { const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); line.setCell(2, CellData.fromCharData([1, 'e\u0301', 1, '\u0301'.charCodeAt(0)])); - expect(line.getTrimmedLength()).equal(3); + assert.equal(line.getTrimmedLength(), 3); }); it('fullwidth', function(): void { const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); line.setCell(2, CellData.fromCharData([1, '1', 2, '1'.charCodeAt(0)])); line.setCell(3, CellData.fromCharData([0, '', 0, 0])); - expect(line.getTrimmedLength()).equal(4); // also counts null cell after fullwidth + assert.equal(line.getTrimmedLength(), 4); // also counts null cell after fullwidth }); }); describe('translateToString with and w\'o trimming', function(): void { it('empty line', function(): void { const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); - expect(line.translateToString(false)).equal(' '); - expect(line.translateToString(true)).equal(''); + assert.equal(line.translateToString(false), ' '); + assert.equal(line.translateToString(true), ''); }); it('ASCII', function(): void { const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); @@ -340,14 +340,14 @@ describe('BufferLine', function(): void { line.setCell(2, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); line.setCell(4, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); line.setCell(5, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); - expect(line.translateToString(false)).equal('a a aa '); - expect(line.translateToString(true)).equal('a a aa'); - expect(line.translateToString(false, 0, 5)).equal('a a a'); - expect(line.translateToString(false, 0, 4)).equal('a a '); - expect(line.translateToString(false, 0, 3)).equal('a a'); - expect(line.translateToString(true, 0, 5)).equal('a a a'); - expect(line.translateToString(true, 0, 4)).equal('a a '); - expect(line.translateToString(true, 0, 3)).equal('a a'); + assert.equal(line.translateToString(false), 'a a aa '); + assert.equal(line.translateToString(true), 'a a aa'); + assert.equal(line.translateToString(false, 0, 5), 'a a a'); + assert.equal(line.translateToString(false, 0, 4), 'a a '); + assert.equal(line.translateToString(false, 0, 3), 'a a'); + assert.equal(line.translateToString(true, 0, 5), 'a a a'); + assert.equal(line.translateToString(true, 0, 4), 'a a '); + assert.equal(line.translateToString(true, 0, 3), 'a a'); }); it('surrogate', function(): void { @@ -356,14 +356,14 @@ describe('BufferLine', function(): void { line.setCell(2, CellData.fromCharData([1, '𝄞', 1, '𝄞'.charCodeAt(0)])); line.setCell(4, CellData.fromCharData([1, '𝄞', 1, '𝄞'.charCodeAt(0)])); line.setCell(5, CellData.fromCharData([1, '𝄞', 1, '𝄞'.charCodeAt(0)])); - expect(line.translateToString(false)).equal('a 𝄞 𝄞𝄞 '); - expect(line.translateToString(true)).equal('a 𝄞 𝄞𝄞'); - expect(line.translateToString(false, 0, 5)).equal('a 𝄞 𝄞'); - expect(line.translateToString(false, 0, 4)).equal('a 𝄞 '); - expect(line.translateToString(false, 0, 3)).equal('a 𝄞'); - expect(line.translateToString(true, 0, 5)).equal('a 𝄞 𝄞'); - expect(line.translateToString(true, 0, 4)).equal('a 𝄞 '); - expect(line.translateToString(true, 0, 3)).equal('a 𝄞'); + assert.equal(line.translateToString(false), 'a 𝄞 𝄞𝄞 '); + assert.equal(line.translateToString(true), 'a 𝄞 𝄞𝄞'); + assert.equal(line.translateToString(false, 0, 5), 'a 𝄞 𝄞'); + assert.equal(line.translateToString(false, 0, 4), 'a 𝄞 '); + assert.equal(line.translateToString(false, 0, 3), 'a 𝄞'); + assert.equal(line.translateToString(true, 0, 5), 'a 𝄞 𝄞'); + assert.equal(line.translateToString(true, 0, 4), 'a 𝄞 '); + assert.equal(line.translateToString(true, 0, 3), 'a 𝄞'); }); it('combining', function(): void { const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); @@ -371,14 +371,14 @@ describe('BufferLine', function(): void { line.setCell(2, CellData.fromCharData([1, 'e\u0301', 1, '\u0301'.charCodeAt(0)])); line.setCell(4, CellData.fromCharData([1, 'e\u0301', 1, '\u0301'.charCodeAt(0)])); line.setCell(5, CellData.fromCharData([1, 'e\u0301', 1, '\u0301'.charCodeAt(0)])); - expect(line.translateToString(false)).equal('a e\u0301 e\u0301e\u0301 '); - expect(line.translateToString(true)).equal('a e\u0301 e\u0301e\u0301'); - expect(line.translateToString(false, 0, 5)).equal('a e\u0301 e\u0301'); - expect(line.translateToString(false, 0, 4)).equal('a e\u0301 '); - expect(line.translateToString(false, 0, 3)).equal('a e\u0301'); - expect(line.translateToString(true, 0, 5)).equal('a e\u0301 e\u0301'); - expect(line.translateToString(true, 0, 4)).equal('a e\u0301 '); - expect(line.translateToString(true, 0, 3)).equal('a e\u0301'); + assert.equal(line.translateToString(false), 'a e\u0301 e\u0301e\u0301 '); + assert.equal(line.translateToString(true), 'a e\u0301 e\u0301e\u0301'); + assert.equal(line.translateToString(false, 0, 5), 'a e\u0301 e\u0301'); + assert.equal(line.translateToString(false, 0, 4), 'a e\u0301 '); + assert.equal(line.translateToString(false, 0, 3), 'a e\u0301'); + assert.equal(line.translateToString(true, 0, 5), 'a e\u0301 e\u0301'); + assert.equal(line.translateToString(true, 0, 4), 'a e\u0301 '); + assert.equal(line.translateToString(true, 0, 3), 'a e\u0301'); }); it('fullwidth', function(): void { const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); @@ -389,20 +389,20 @@ describe('BufferLine', function(): void { line.setCell(6, CellData.fromCharData([0, '', 0, 0])); line.setCell(7, CellData.fromCharData([1, '1', 2, '1'.charCodeAt(0)])); line.setCell(8, CellData.fromCharData([0, '', 0, 0])); - expect(line.translateToString(false)).equal('a 1 11 '); - expect(line.translateToString(true)).equal('a 1 11'); - expect(line.translateToString(false, 0, 7)).equal('a 1 1'); - expect(line.translateToString(false, 0, 6)).equal('a 1 1'); - expect(line.translateToString(false, 0, 5)).equal('a 1 '); - expect(line.translateToString(false, 0, 4)).equal('a 1'); - expect(line.translateToString(false, 0, 3)).equal('a 1'); - expect(line.translateToString(false, 0, 2)).equal('a '); - expect(line.translateToString(true, 0, 7)).equal('a 1 1'); - expect(line.translateToString(true, 0, 6)).equal('a 1 1'); - expect(line.translateToString(true, 0, 5)).equal('a 1 '); - expect(line.translateToString(true, 0, 4)).equal('a 1'); - expect(line.translateToString(true, 0, 3)).equal('a 1'); - expect(line.translateToString(true, 0, 2)).equal('a '); + assert.equal(line.translateToString(false), 'a 1 11 '); + assert.equal(line.translateToString(true), 'a 1 11'); + assert.equal(line.translateToString(false, 0, 7), 'a 1 1'); + assert.equal(line.translateToString(false, 0, 6), 'a 1 1'); + assert.equal(line.translateToString(false, 0, 5), 'a 1 '); + assert.equal(line.translateToString(false, 0, 4), 'a 1'); + assert.equal(line.translateToString(false, 0, 3), 'a 1'); + assert.equal(line.translateToString(false, 0, 2), 'a '); + assert.equal(line.translateToString(true, 0, 7), 'a 1 1'); + assert.equal(line.translateToString(true, 0, 6), 'a 1 1'); + assert.equal(line.translateToString(true, 0, 5), 'a 1 '); + assert.equal(line.translateToString(true, 0, 4), 'a 1'); + assert.equal(line.translateToString(true, 0, 3), 'a 1'); + assert.equal(line.translateToString(true, 0, 2), 'a '); }); it('space at end', function(): void { const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); @@ -411,21 +411,21 @@ describe('BufferLine', function(): void { line.setCell(4, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); line.setCell(5, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); line.setCell(6, CellData.fromCharData([1, ' ', 1, ' '.charCodeAt(0)])); - expect(line.translateToString(false)).equal('a a aa '); - expect(line.translateToString(true)).equal('a a aa '); + assert.equal(line.translateToString(false), 'a a aa '); + assert.equal(line.translateToString(true), 'a a aa '); }); it('should always return some sane value', function(): void { // sanity check - broken line with invalid out of bound null width cells // this can atm happen with deleting/inserting chars in inputhandler by "breaking" // fullwidth pairs --> needs to be fixed after settling BufferLine impl const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); - expect(line.translateToString(false)).equal(' '); - expect(line.translateToString(true)).equal(''); + assert.equal(line.translateToString(false), ' '); + assert.equal(line.translateToString(true), ''); }); it('should work with endCol=0', () => { const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); - expect(line.translateToString(true, 0, 0)).equal(''); + assert.equal(line.translateToString(true, 0, 0), ''); }); }); describe('addCharToCell', () => { diff --git a/src/common/buffer/BufferRange.test.ts b/src/common/buffer/BufferRange.test.ts new file mode 100644 index 00000000..69aacd99 --- /dev/null +++ b/src/common/buffer/BufferRange.test.ts @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2021 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { assert } from 'chai'; +import { getRangeLength } from 'common/buffer/BufferRange'; +import { IBufferRange } from 'xterm'; + +describe('BufferRange', () => { + describe('getRangeLength', () => { + it('should get range for single line', () => { + assert.equal(getRangeLength(createRange(1, 1, 4, 1), 0), 4); + }); + it('should throw for invalid range', () => { + assert.throws(() => getRangeLength(createRange(1, 3, 1, 1), 0)); + }); + it('should get range multiple lines', () => { + assert.equal(getRangeLength(createRange(1, 1, 4, 5), 5), 24); + }); + it('should get range for end line right after start line', () => { + assert.equal(getRangeLength(createRange(1, 1, 7, 2), 5), 12); + }); + }); +}); + +function createRange(x1: number, y1: number, x2: number, y2: number): IBufferRange { + return { + start: { x: x1, y: y1 }, + end: { x: x2, y: y2 } + }; +} diff --git a/src/common/buffer/BufferRange.ts b/src/common/buffer/BufferRange.ts new file mode 100644 index 00000000..a49cf481 --- /dev/null +++ b/src/common/buffer/BufferRange.ts @@ -0,0 +1,13 @@ +/** + * Copyright (c) 2021 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IBufferRange } from 'xterm'; + +export function getRangeLength(range: IBufferRange, bufferCols: number): number { + if (range.start.y > range.end.y) { + throw new Error(`Buffer range end (${range.end.x}, ${range.end.y}) cannot be before start (${range.start.x}, ${range.start.y})`); + } + return bufferCols * (range.end.y - range.start.y) + (range.end.x - range.start.x + 1); +} diff --git a/src/common/buffer/BufferSet.ts b/src/common/buffer/BufferSet.ts index b9dc7995..de220e8f 100644 --- a/src/common/buffer/BufferSet.ts +++ b/src/common/buffer/BufferSet.ts @@ -15,10 +15,9 @@ import { Disposable } from 'common/Lifecycle'; * provides also utilities for working with them. */ export class BufferSet extends Disposable implements IBufferSet { - private _normal: Buffer; - private _alt: Buffer; - private _activeBuffer: Buffer; - + private _normal!: Buffer; + private _alt!: Buffer; + private _activeBuffer!: Buffer; private _onBufferActivate = this.register(new EventEmitter<{activeBuffer: IBuffer, inactiveBuffer: IBuffer}>()); public get onBufferActivate(): IEvent<{activeBuffer: IBuffer, inactiveBuffer: IBuffer}> { return this._onBufferActivate.event; } @@ -28,18 +27,25 @@ export class BufferSet extends Disposable implements IBufferSet { * @param _terminal - The terminal the BufferSet will belong to */ constructor( - optionsService: IOptionsService, - bufferService: IBufferService + private readonly _optionsService: IOptionsService, + private readonly _bufferService: IBufferService ) { super(); + this.reset(); + } - this._normal = new Buffer(true, optionsService, bufferService); + public reset(): void { + this._normal = new Buffer(true, this._optionsService, this._bufferService); this._normal.fillViewportRows(); // The alt buffer should never have scrollback. // See http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-The-Alternate-Screen-Buffer - this._alt = new Buffer(false, optionsService, bufferService); + this._alt = new Buffer(false, this._optionsService, this._bufferService); this._activeBuffer = this._normal; + this._onBufferActivate.fire({ + activeBuffer: this._normal, + inactiveBuffer: this._alt + }); this.setupTabStops(); } diff --git a/src/common/buffer/Constants.ts b/src/common/buffer/Constants.ts index ee86a804..a2c1b884 100644 --- a/src/common/buffer/Constants.ts +++ b/src/common/buffer/Constants.ts @@ -110,13 +110,14 @@ export const enum Attributes { export const enum FgFlags { /** - * bit 27..31 (32th bit unused) + * bit 27..32 */ INVERSE = 0x4000000, BOLD = 0x8000000, UNDERLINE = 0x10000000, BLINK = 0x20000000, - INVISIBLE = 0x40000000 + INVISIBLE = 0x40000000, + STRIKETHROUGH = 0x80000000, } export const enum BgFlags { diff --git a/src/common/buffer/Types.d.ts b/src/common/buffer/Types.d.ts index 752b1a26..cbf40a03 100644 --- a/src/common/buffer/Types.d.ts +++ b/src/common/buffer/Types.d.ts @@ -56,6 +56,7 @@ export interface IBufferSet extends IDisposable { activateNormalBuffer(): void; activateAltBuffer(fillAttr?: IAttributeData): void; + reset(): void; resize(newCols: number, newRows: number): void; setupTabStops(i?: number): void; } diff --git a/src/common/input/Keyboard.ts b/src/common/input/Keyboard.ts index fdb777b4..20a28f61 100644 --- a/src/common/input/Keyboard.ts +++ b/src/common/input/Keyboard.ts @@ -356,7 +356,7 @@ export function evaluateKeyboardEvent( const keyCode = ev.ctrlKey ? ev.keyCode - 64 : ev.keyCode + 32; result.key = C0.ESC + String.fromCharCode(keyCode); } - } else if (isMac && !ev.altKey && !ev.ctrlKey && ev.metaKey) { + } else if (isMac && !ev.altKey && !ev.ctrlKey && !ev.shiftKey && ev.metaKey) { if (ev.keyCode === 65) { // cmd + a result.type = KeyboardResultType.SELECT_ALL; } diff --git a/src/common/input/TextDecoder.test.ts b/src/common/input/TextDecoder.test.ts index 92b0e03a..da1760a2 100644 --- a/src/common/input/TextDecoder.test.ts +++ b/src/common/input/TextDecoder.test.ts @@ -58,8 +58,8 @@ describe('text encodings', () => { const decoder = new StringToUtf32(); const target = new Uint32Array(5); for (let i = 0; i < 65536; ++i) { - // skip surrogate pairs - if (i >= 0xD800 && i <= 0xDFFF) { + // skip surrogate pairs and a BOM + if ((i >= 0xD800 && i <= 0xDFFF) || i === 0xFEFF) { continue; } const length = decoder.decode(String.fromCharCode(i), target); @@ -84,6 +84,14 @@ describe('text encodings', () => { decoder.clear(); } }); + + it('0xFEFF(BOM)', () => { + const decoder = new StringToUtf32(); + const target = new Uint32Array(5); + const length = decoder.decode(String.fromCharCode(0xFEFF), target); + assert.equal(length, 0); + decoder.clear(); + }); }); it('test strings', () => { @@ -118,8 +126,8 @@ describe('text encodings', () => { const decoder = new Utf8ToUtf32(); const target = new Uint32Array(5); for (let i = 0; i < 65536; ++i) { - // skip surrogate pairs - if (i >= 0xD800 && i <= 0xDFFF) { + // skip surrogate pairs and a BOM + if ((i >= 0xD800 && i <= 0xDFFF) || i === 0xFEFF) { continue; } const utf8Data = fromByteString(encode(String.fromCharCode(i))); @@ -142,6 +150,15 @@ describe('text encodings', () => { decoder.clear(); } }); + + it('0xFEFF(BOM)', () => { + const decoder = new Utf8ToUtf32(); + const target = new Uint32Array(5); + const utf8Data = fromByteString(encode(String.fromCharCode(0xFEFF))); + const length = decoder.decode(utf8Data, target); + assert.equal(length, 0); + decoder.clear(); + }); }); it('test strings', () => { @@ -215,6 +232,19 @@ describe('text encodings', () => { } assert(decoded, 'Ä€𝄞Ö𝄞€Ü𝄞€'); }); + + it('BOMs (3 byte sequences) - advance by 2', () => { + const decoder = new Utf8ToUtf32(); + const target = new Uint32Array(5); + const utf8Data = fromByteString('\xef\xbb\xbf\xef\xbb\xbf'); + let decoded = ''; + for (let i = 0; i < utf8Data.length; i += 2) { + const written = decoder.decode(utf8Data.slice(i, i + 2), target); + decoded += toString(target, written); + } + assert.equal(decoded, ''); + }); + it('test break after 3 bytes - issue #2495', () => { const decoder = new Utf8ToUtf32(); const target = new Uint32Array(5); diff --git a/src/common/input/TextDecoder.ts b/src/common/input/TextDecoder.ts index 6ecab011..715e9197 100644 --- a/src/common/input/TextDecoder.ts +++ b/src/common/input/TextDecoder.ts @@ -105,6 +105,10 @@ export class StringToUtf32 { } continue; } + if (code === 0xFEFF) { + // BOM + continue; + } target[size++] = code; } return size; @@ -188,8 +192,8 @@ export class Utf8ToUtf32 { target[size++] = cp; } } else if (type === 3) { - if (cp < 0x0800 || (cp >= 0xD800 && cp <= 0xDFFF)) { - // illegal codepoint + if (cp < 0x0800 || (cp >= 0xD800 && cp <= 0xDFFF) || cp === 0xFEFF) { + // illegal codepoint or BOM } else { target[size++] = cp; } @@ -286,8 +290,8 @@ export class Utf8ToUtf32 { continue; } codepoint = (byte1 & 0x0F) << 12 | (byte2 & 0x3F) << 6 | (byte3 & 0x3F); - if (codepoint < 0x0800 || (codepoint >= 0xD800 && codepoint <= 0xDFFF)) { - // illegal codepoint, no i-- here + if (codepoint < 0x0800 || (codepoint >= 0xD800 && codepoint <= 0xDFFF) || codepoint === 0xFEFF) { + // illegal codepoint or BOM, no i-- here continue; } target[size++] = codepoint; diff --git a/src/common/input/WriteBuffer.test.ts b/src/common/input/WriteBuffer.test.ts index d3dfebd0..89106423 100644 --- a/src/common/input/WriteBuffer.test.ts +++ b/src/common/input/WriteBuffer.test.ts @@ -85,5 +85,26 @@ describe('WriteBuffer', () => { done(); }); }); + it('writeSync called from action does not overflow callstack - issue #3265', () => { + wb = new WriteBuffer(data => { + const num = parseInt(data as string); + if (num < 1000000) { + wb.writeSync('' + (num + 1)); + } + }); + wb.writeSync('1'); + }); + it('writeSync maxSubsequentCalls argument', () => { + let last: string = ''; + wb = new WriteBuffer(data => { + last = data as string; + const num = parseInt(data as string); + if (num < 1000000) { + wb.writeSync('' + (num + 1), 10); + } + }); + wb.writeSync('1', 10); + assert.equal(last, '11'); // 1 + 10 sub calls = 11 + }); }); }); diff --git a/src/common/input/WriteBuffer.ts b/src/common/input/WriteBuffer.ts index c7d82458..cc84c9ab 100644 --- a/src/common/input/WriteBuffer.ts +++ b/src/common/input/WriteBuffer.ts @@ -31,33 +31,66 @@ const WRITE_TIMEOUT_MS = 12; */ const WRITE_BUFFER_LENGTH_THRESHOLD = 50; +// queueMicrotask polyfill for nodejs < v11 +const qmt: (cb: () => void) => void = (typeof queueMicrotask === 'undefined') + ? (cb: () => void) => { Promise.resolve().then(cb); } + : queueMicrotask; + + export class WriteBuffer { private _writeBuffer: (string | Uint8Array)[] = []; private _callbacks: ((() => void) | undefined)[] = []; private _pendingData = 0; private _bufferOffset = 0; + private _isSyncWriting = false; + private _syncCalls = 0; - constructor(private _action: (data: string | Uint8Array) => void) { } + constructor(private _action: (data: string | Uint8Array, promiseResult?: boolean) => void | Promise) { } + + /** + * @deprecated Unreliable, to be removed soon. + */ + public writeSync(data: string | Uint8Array, maxSubsequentCalls?: number): void { + // stop writeSync recursions with maxSubsequentCalls argument + // This is dangerous to use as it will lose the current data chunk + // and return immediately. + if (maxSubsequentCalls !== undefined && this._syncCalls > maxSubsequentCalls) { + // comment next line if a whole loop block should only contain x `writeSync` calls + // (total flat vs. deep nested limit) + this._syncCalls = 0; + return; + } + // append chunk to buffer + this._pendingData += data.length; + this._writeBuffer.push(data); + this._callbacks.push(undefined); + + // increase recursion counter + this._syncCalls++; + // exit early if another writeSync loop is active + if (this._isSyncWriting) { + return; + } + this._isSyncWriting = true; - public writeSync(data: string | Uint8Array): void { // force sync processing on pending data chunks to avoid in-band data scrambling // does the same as innerWrite but without event loop - if (this._writeBuffer.length) { - for (let i = this._bufferOffset; i < this._writeBuffer.length; ++i) { - const data = this._writeBuffer[i]; - const cb = this._callbacks[i]; - this._action(data); - if (cb) cb(); - } - // reset all to avoid reprocessing of chunks with scheduled innerWrite call - this._writeBuffer = []; - this._callbacks = []; - this._pendingData = 0; - // stop scheduled innerWrite by offset > length condition - this._bufferOffset = 0x7FFFFFFF; + // we have to do it here as single loop steps to not corrupt loop subject + // by another writeSync call triggered from _action + let chunk: string | Uint8Array | undefined; + while (chunk = this._writeBuffer.shift()) { + this._action(chunk); + const cb = this._callbacks.shift(); + if (cb) cb(); } - // handle current data chunk - this._action(data); + // reset to avoid reprocessing of chunks with scheduled innerWrite call + // stopping scheduled innerWrite by offset > length condition + this._pendingData = 0; + this._bufferOffset = 0x7FFFFFFF; + + // allow another writeSync to loop + this._isSyncWriting = false; + this._syncCalls = 0; } public write(data: string | Uint8Array, callback?: () => void): void { @@ -76,16 +109,97 @@ export class WriteBuffer { this._callbacks.push(callback); } - protected _innerWrite(): void { - const startTime = Date.now(); + /** + * Inner write call, that enters the sliced chunk processing by timing. + * + * `lastTime` indicates, when the last _innerWrite call had started. + * It is used to aggregate async handler execution under a timeout constraint + * effectively lowering the redrawing needs, schematically: + * + * macroTask _innerWrite: + * if (Date.now() - (lastTime | 0) < WRITE_TIMEOUT_MS): + * schedule microTask _innerWrite(lastTime) + * else: + * schedule macroTask _innerWrite(0) + * + * overall execution order on task queues: + * + * macrotasks: [...] --> _innerWrite(0) --> [...] --> screenUpdate --> [...] + * m t: | + * i a: [...] + * c s: | + * r k: while < timeout: + * o s: _innerWrite(timeout) + * + * `promiseResult` depicts the promise resolve value of an async handler. + * This value gets carried forward through all saved stack states of the + * paused parser for proper continuation. + * + * Note, for pure sync code `lastTime` and `promiseResult` have no meaning. + */ + protected _innerWrite(lastTime: number = 0, promiseResult: boolean = true): void { + const startTime = lastTime || Date.now(); while (this._writeBuffer.length > this._bufferOffset) { const data = this._writeBuffer[this._bufferOffset]; - const cb = this._callbacks[this._bufferOffset]; - this._bufferOffset++; + const result = this._action(data, promiseResult); + if (result) { + /** + * If we get a promise as return value, we re-schedule the continuation + * as thenable on the promise and exit right away. + * + * The exit here means, that we block input processing at the current active chunk, + * the exact execution position within the chunk is preserved by the saved + * stack content in InputHandler and EscapeSequenceParser. + * + * Resuming happens automatically from that saved stack state. + * Also the resolved promise value is passed along the callstack to + * `EscapeSequenceParser.parse` to correctly resume the stopped handler loop. + * + * Exceptions on async handlers will be logged to console async, but do not interrupt + * the input processing (continues with next handler at the current input position). + */ - this._action(data); - this._pendingData -= data.length; + /** + * If a promise takes long to resolve, we should schedule continuation behind setTimeout. + * This might already be too late, if our .then enters really late (executor + prev thens took very long). + * This cannot be solved here for the handler itself (it is the handlers responsibility to slice hard work), + * but we can at least schedule a screen update as we gain control. + */ + const continuation: (r: boolean) => void = (r: boolean) => Date.now() - startTime >= WRITE_TIMEOUT_MS + ? setTimeout(() => this._innerWrite(0, r)) + : this._innerWrite(startTime, r); + + /** + * Optimization considerations: + * The continuation above favors FPS over throughput by eval'ing `startTime` on resolve. + * This might schedule too many screen updates with bad throughput drops (in case a slow + * resolving handler sliced its work properly behind setTimeout calls). We cannot spot + * this condition here, also the renderer has no way to spot nonsense updates either. + * FIXME: A proper fix for this would track the FPS at the renderer entry level separately. + * + * If favoring of FPS shows bad throughtput impact, use the following instead. It favors + * throughput by eval'ing `startTime` upfront pulling at least one more chunk into the + * current microtask queue (executed before setTimeout). + */ + // const continuation: (r: boolean) => void = Date.now() - startTime >= WRITE_TIMEOUT_MS + // ? r => setTimeout(() => this._innerWrite(0, r)) + // : r => this._innerWrite(startTime, r); + + // Handle exceptions synchronously to current band position, idea: + // 1. spawn a single microtask which we allow to throw hard + // 2. spawn a promise immediately resolving to `true` + // (executed on the same queue, thus properly aligned before continuation happens) + result.catch(err => { + qmt(() => {throw err;}); + return Promise.resolve(false); + }).then(continuation); + return; + } + + const cb = this._callbacks[this._bufferOffset]; if (cb) cb(); + this._bufferOffset++; + this._pendingData -= data.length; if (Date.now() - startTime >= WRITE_TIMEOUT_MS) { break; @@ -99,10 +213,10 @@ export class WriteBuffer { this._callbacks = this._callbacks.slice(this._bufferOffset); this._bufferOffset = 0; } - setTimeout(() => this._innerWrite(), 0); + setTimeout(() => this._innerWrite()); } else { - this._writeBuffer = []; - this._callbacks = []; + this._writeBuffer.length = 0; + this._callbacks.length = 0; this._pendingData = 0; this._bufferOffset = 0; } diff --git a/src/common/parser/DcsParser.test.ts b/src/common/parser/DcsParser.test.ts index dbabb4fa..096ebb58 100644 --- a/src/common/parser/DcsParser.test.ts +++ b/src/common/parser/DcsParser.test.ts @@ -61,11 +61,12 @@ class TestHandler implements IDcsHandler { public put(data: Uint32Array, start: number, end: number): void { this.output.push([this.msg, 'PUT', utf32ToString(data, start, end)]); } - public unhook(success: boolean): void | boolean { + public unhook(success: boolean): boolean { this.output.push([this.msg, 'UNHOOK', success]); if (this.returnFalse) { return false; } + return true; } } @@ -84,7 +85,7 @@ describe('DcsParser', () => { }); describe('handler registration', () => { it('setDcsHandler', () => { - parser.setHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th')); + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th')); parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); let data = toUtf32('Here comes'); parser.put(data, 0, data.length); @@ -100,7 +101,7 @@ describe('DcsParser', () => { ]); }); it('clearDcsHandler', () => { - parser.setHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th')); + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th')); parser.clearHandler(identifier({intermediates: '+', final: 'p'})); parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); let data = toUtf32('Here comes'); @@ -117,8 +118,8 @@ describe('DcsParser', () => { ]); }); it('addDcsHandler', () => { - parser.setHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th1')); - parser.addHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th2')); + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th1')); + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th2')); parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); let data = toUtf32('Here comes'); parser.put(data, 0, data.length); @@ -137,8 +138,8 @@ describe('DcsParser', () => { ]); }); it('addDcsHandler with return false', () => { - parser.setHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th1')); - parser.addHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th2', true)); + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th1')); + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th2', true)); parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); let data = toUtf32('Here comes'); parser.put(data, 0, data.length); @@ -157,8 +158,8 @@ describe('DcsParser', () => { ]); }); it('dispose handlers', () => { - parser.setHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th1')); - const dispo = parser.addHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th2', true)); + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th1')); + const dispo = parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th2', true)); dispo.dispose(); parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); let data = toUtf32('Here comes'); @@ -176,7 +177,7 @@ describe('DcsParser', () => { }); describe('DcsHandlerFactory', () => { it('should be called once on end(true)', () => { - parser.setHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push([params.toArray(), data]))); + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => { reports.push([params.toArray(), data]); return true; })); parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); let data = toUtf32('Here comes'); parser.put(data, 0, data.length); @@ -186,7 +187,7 @@ describe('DcsParser', () => { assert.deepEqual(reports, [[[1, 2, 3], 'Here comes the mouse!']]); }); it('should not be called on end(false)', () => { - parser.setHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push([params.toArray(), data]))); + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => { reports.push([params.toArray(), data]); return true; })); parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); let data = toUtf32('Here comes'); parser.put(data, 0, data.length); @@ -196,8 +197,8 @@ describe('DcsParser', () => { assert.deepEqual(reports, []); }); it('should be disposable', () => { - parser.setHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push(['one', params.toArray(), data]))); - const dispo = parser.addHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push(['two', params.toArray(), data]))); + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => { reports.push(['one', params.toArray(), data]); return true; })); + const dispo = parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => { reports.push(['two', params.toArray(), data]); return true; })); parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); let data = toUtf32('Here comes'); parser.put(data, 0, data.length); @@ -215,8 +216,8 @@ describe('DcsParser', () => { assert.deepEqual(reports, [['two', [1, 2, 3], 'Here comes the mouse!'], ['one', [1, 2, 3], 'some other data']]); }); it('should respect return false', () => { - parser.setHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push(['one', params.toArray(), data]))); - parser.addHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => { reports.push(['two', params.toArray(), data]); return false; })); + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => { reports.push(['one', params.toArray(), data]); return true; })); + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => { reports.push(['two', params.toArray(), data]); return false; })); parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); let data = toUtf32('Here comes'); parser.put(data, 0, data.length); @@ -227,7 +228,7 @@ describe('DcsParser', () => { }); it('should work up to payload limit', function(): void { this.timeout(10000); - parser.setHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push([params.toArray(), data]))); + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => { reports.push([params.toArray(), data]); return true; })); parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); const data = toUtf32('A'.repeat(1000)); for (let i = 0; i < PAYLOAD_LIMIT; i += 1000) { @@ -238,7 +239,7 @@ describe('DcsParser', () => { }); it('should abort for payload limit +1', function(): void { this.timeout(10000); - parser.setHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push([params.toArray(), data]))); + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => { reports.push([params.toArray(), data]); return true; })); parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); let data = toUtf32('A'.repeat(1000)); for (let i = 0; i < PAYLOAD_LIMIT; i += 1000) { @@ -251,3 +252,208 @@ describe('DcsParser', () => { }); }); }); + + +class TestHandlerAsync implements IDcsHandler { + constructor(public output: any[], public msg: string, public returnFalse: boolean = false) {} + public hook(params: IParams): void { + this.output.push([this.msg, 'HOOK', params.toArray()]); + } + public put(data: Uint32Array, start: number, end: number): void { + this.output.push([this.msg, 'PUT', utf32ToString(data, start, end)]); + } + public async unhook(success: boolean): Promise { + // simple sleep to check in tests whether ordering gets messed up + await new Promise(res => setTimeout(res, 20)); + this.output.push([this.msg, 'UNHOOK', success]); + if (this.returnFalse) { + return false; + } + return true; + } +} +async function unhookP(parser: DcsParser, success: boolean): Promise { + let result: void | Promise; + let prev: boolean | undefined; + while (result = parser.unhook(success, prev)) { + prev = await result; + } +} + + +describe('DcsParser - async tests', () => { + let parser: DcsParser; + let reports: any[] = []; + beforeEach(() => { + reports = []; + parser = new DcsParser(); + parser.setHandlerFallback((id, action, data) => { + if (action === 'HOOK') { + data = data.toArray(); + } + reports.push([id, action, data]); + }); + }); + describe('sync and async mixed', () => { + describe('sync | async | sync', () => { + it('first should run, cleanup action for others', async () => { + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 's1', false)); + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandlerAsync(reports, 'a1', false)); + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 's2', false)); + parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); + let data = toUtf32('Here comes'); + parser.put(data, 0, data.length); + data = toUtf32('the mouse!'); + parser.put(data, 0, data.length); + await unhookP(parser, true); + assert.deepEqual(reports, [ + // messages from TestHandler + ['s2', 'HOOK', [1, 2, 3]], + ['a1', 'HOOK', [1, 2, 3]], + ['s1', 'HOOK', [1, 2, 3]], + ['s2', 'PUT', 'Here comes'], + ['a1', 'PUT', 'Here comes'], + ['s1', 'PUT', 'Here comes'], + ['s2', 'PUT', 'the mouse!'], + ['a1', 'PUT', 'the mouse!'], + ['s1', 'PUT', 'the mouse!'], + ['s2', 'UNHOOK', true], + ['a1', 'UNHOOK', false], // important: a1 before s1 + ['s1', 'UNHOOK', false] + ]); + }); + it('all should run', async () => { + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 's1', true)); + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandlerAsync(reports, 'a1', true)); + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 's2', true)); + parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); + let data = toUtf32('Here comes'); + parser.put(data, 0, data.length); + data = toUtf32('the mouse!'); + parser.put(data, 0, data.length); + await unhookP(parser, true); + assert.deepEqual(reports, [ + // messages from TestHandler + ['s2', 'HOOK', [1, 2, 3]], + ['a1', 'HOOK', [1, 2, 3]], + ['s1', 'HOOK', [1, 2, 3]], + ['s2', 'PUT', 'Here comes'], + ['a1', 'PUT', 'Here comes'], + ['s1', 'PUT', 'Here comes'], + ['s2', 'PUT', 'the mouse!'], + ['a1', 'PUT', 'the mouse!'], + ['s1', 'PUT', 'the mouse!'], + ['s2', 'UNHOOK', true], + ['a1', 'UNHOOK', true], // important: a1 before s1 + ['s1', 'UNHOOK', true] + ]); + }); + }); + describe('async | sync | async', () => { + it('first should run, cleanup action for others', async () => { + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandlerAsync(reports, 'a1', false)); + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 's1', false)); + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandlerAsync(reports, 'a2', false)); + parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); + let data = toUtf32('Here comes'); + parser.put(data, 0, data.length); + data = toUtf32('the mouse!'); + parser.put(data, 0, data.length); + await unhookP(parser, true); + assert.deepEqual(reports, [ + // messages from TestHandler + ['a2', 'HOOK', [1, 2, 3]], + ['s1', 'HOOK', [1, 2, 3]], + ['a1', 'HOOK', [1, 2, 3]], + ['a2', 'PUT', 'Here comes'], + ['s1', 'PUT', 'Here comes'], + ['a1', 'PUT', 'Here comes'], + ['a2', 'PUT', 'the mouse!'], + ['s1', 'PUT', 'the mouse!'], + ['a1', 'PUT', 'the mouse!'], + ['a2', 'UNHOOK', true], + ['s1', 'UNHOOK', false], // important: s1 between a2 .. a1 + ['a1', 'UNHOOK', false] + ]); + }); + it('all should run', async () => { + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandlerAsync(reports, 'a1', true)); + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 's1', true)); + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandlerAsync(reports, 'a2', true)); + parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); + let data = toUtf32('Here comes'); + parser.put(data, 0, data.length); + data = toUtf32('the mouse!'); + parser.put(data, 0, data.length); + await unhookP(parser, true); + assert.deepEqual(reports, [ + // messages from TestHandler + ['a2', 'HOOK', [1, 2, 3]], + ['s1', 'HOOK', [1, 2, 3]], + ['a1', 'HOOK', [1, 2, 3]], + ['a2', 'PUT', 'Here comes'], + ['s1', 'PUT', 'Here comes'], + ['a1', 'PUT', 'Here comes'], + ['a2', 'PUT', 'the mouse!'], + ['s1', 'PUT', 'the mouse!'], + ['a1', 'PUT', 'the mouse!'], + ['a2', 'UNHOOK', true], + ['s1', 'UNHOOK', true], // important: s1 between a2 .. a1 + ['a1', 'UNHOOK', true] + ]); + }); + }); + describe('DcsHandlerFactory', () => { + it('should be called once on end(true)', async () => { + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler(async (data, params) => { reports.push([params.toArray(), data]); return true; })); + parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); + let data = toUtf32('Here comes'); + parser.put(data, 0, data.length); + data = toUtf32(' the mouse!'); + parser.put(data, 0, data.length); + await unhookP(parser, true); + assert.deepEqual(reports, [[[1, 2, 3], 'Here comes the mouse!']]); + }); + it('should not be called on end(false)', async () => { + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler(async (data, params) => { reports.push([params.toArray(), data]); return true; })); + parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); + let data = toUtf32('Here comes'); + parser.put(data, 0, data.length); + data = toUtf32(' the mouse!'); + parser.put(data, 0, data.length); + await unhookP(parser, false); + assert.deepEqual(reports, []); + }); + it('should be disposable', async () => { + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler(async (data, params) => { reports.push(['one', params.toArray(), data]); return true; })); + const dispo = parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler(async (data, params) => { reports.push(['two', params.toArray(), data]); return true; })); + parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); + let data = toUtf32('Here comes'); + parser.put(data, 0, data.length); + data = toUtf32(' the mouse!'); + parser.put(data, 0, data.length); + await unhookP(parser, true); + assert.deepEqual(reports, [['two', [1, 2, 3], 'Here comes the mouse!']]); + dispo.dispose(); + parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); + data = toUtf32('some other'); + parser.put(data, 0, data.length); + data = toUtf32(' data'); + parser.put(data, 0, data.length); + await unhookP(parser, true); + assert.deepEqual(reports, [['two', [1, 2, 3], 'Here comes the mouse!'], ['one', [1, 2, 3], 'some other data']]); + }); + it('should respect return false', async () => { + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler(async (data, params) => { reports.push(['one', params.toArray(), data]); return true; })); + parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler(async (data, params) => { reports.push(['two', params.toArray(), data]); return false; })); + parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3])); + let data = toUtf32('Here comes'); + parser.put(data, 0, data.length); + data = toUtf32(' the mouse!'); + parser.put(data, 0, data.length); + await unhookP(parser, true); + assert.deepEqual(reports, [['two', [1, 2, 3], 'Here comes the mouse!'], ['one', [1, 2, 3], 'Here comes the mouse!']]); + }); + }); + }); +}); diff --git a/src/common/parser/DcsParser.ts b/src/common/parser/DcsParser.ts index a09fd68c..b66524ba 100644 --- a/src/common/parser/DcsParser.ts +++ b/src/common/parser/DcsParser.ts @@ -4,7 +4,7 @@ */ import { IDisposable } from 'common/Types'; -import { IDcsHandler, IParams, IHandlerCollection, IDcsParser, DcsFallbackHandlerType } from 'common/parser/Types'; +import { IDcsHandler, IParams, IHandlerCollection, IDcsParser, DcsFallbackHandlerType, ISubParserStackState } from 'common/parser/Types'; import { utf32ToString } from 'common/input/TextDecoder'; import { Params } from 'common/parser/Params'; import { PAYLOAD_LIMIT } from 'common/parser/Constants'; @@ -15,14 +15,20 @@ export class DcsParser implements IDcsParser { private _handlers: IHandlerCollection = Object.create(null); private _active: IDcsHandler[] = EMPTY_HANDLERS; private _ident: number = 0; - private _handlerFb: DcsFallbackHandlerType = () => {}; + private _handlerFb: DcsFallbackHandlerType = () => { }; + private _stack: ISubParserStackState = { + paused: false, + loopPosition: 0, + fallThrough: false + }; public dispose(): void { this._handlers = Object.create(null); - this._handlerFb = () => {}; + this._handlerFb = () => { }; + this._active = EMPTY_HANDLERS; } - public addHandler(ident: number, handler: IDcsHandler): IDisposable { + public registerHandler(ident: number, handler: IDcsHandler): IDisposable { if (this._handlers[ident] === undefined) { this._handlers[ident] = []; } @@ -38,10 +44,6 @@ export class DcsParser implements IDcsParser { }; } - public setHandler(ident: number, handler: IDcsHandler): void { - this._handlers[ident] = [handler]; - } - public clearHandler(ident: number): void { if (this._handlers[ident]) delete this._handlers[ident]; } @@ -51,9 +53,13 @@ export class DcsParser implements IDcsParser { } public reset(): void { + // force cleanup leftover handlers if (this._active.length) { - this.unhook(false); + for (let j = this._stack.paused ? this._stack.loopPosition - 1 : this._active.length - 1; j >= 0; --j) { + this._active[j].unhook(false); + } } + this._stack.paused = false; this._active = EMPTY_HANDLERS; this._ident = 0; } @@ -82,20 +88,42 @@ export class DcsParser implements IDcsParser { } } - public unhook(success: boolean): void { + public unhook(success: boolean, promiseResult: boolean = true): void | Promise { if (!this._active.length) { this._handlerFb(this._ident, 'UNHOOK', success); } else { + let handlerResult: boolean | Promise = false; let j = this._active.length - 1; - for (; j >= 0; j--) { - if (this._active[j].unhook(success) !== false) { - break; - } + let fallThrough = false; + if (this._stack.paused) { + j = this._stack.loopPosition - 1; + handlerResult = promiseResult; + fallThrough = this._stack.fallThrough; + this._stack.paused = false; } - j--; - // cleanup left over handlers + if (!fallThrough && handlerResult === false) { + for (; j >= 0; j--) { + handlerResult = this._active[j].unhook(success); + if (handlerResult === true) { + break; + } else if (handlerResult instanceof Promise) { + this._stack.paused = true; + this._stack.loopPosition = j; + this._stack.fallThrough = false; + return handlerResult; + } + } + j--; + } + // cleanup left over handlers (fallThrough for async) for (; j >= 0; j--) { - this._active[j].unhook(false); + handlerResult = this._active[j].unhook(false); + if (handlerResult instanceof Promise) { + this._stack.paused = true; + this._stack.loopPosition = j; + this._stack.fallThrough = true; + return handlerResult; + } } } this._active = EMPTY_HANDLERS; @@ -103,19 +131,27 @@ export class DcsParser implements IDcsParser { } } +// predefine empty params as [0] (ZDM) +const EMPTY_PARAMS = new Params(); +EMPTY_PARAMS.addParam(0); + /** * Convenient class to create a DCS handler from a single callback function. * Note: The payload is currently limited to 50 MB (hardcoded). */ export class DcsHandler implements IDcsHandler { private _data = ''; - private _params: IParams | undefined; + private _params: IParams = EMPTY_PARAMS; private _hitLimit: boolean = false; - constructor(private _handler: (data: string, params: IParams) => any) {} + constructor(private _handler: (data: string, params: IParams) => boolean | Promise) { } public hook(params: IParams): void { - this._params = params.clone(); + // since we need to preserve params until `unhook`, we have to clone it + // (only borrowed from parser and spans multiple parser states) + // perf optimization: + // clone only, if we have non empty params, otherwise stick with default + this._params = (params.length > 1 || params.params[0]) ? params.clone() : EMPTY_PARAMS; this._data = ''; this._hitLimit = false; } @@ -131,14 +167,24 @@ export class DcsHandler implements IDcsHandler { } } - public unhook(success: boolean): any { - let ret; + public unhook(success: boolean): boolean | Promise { + let ret: boolean | Promise = false; if (this._hitLimit) { ret = false; } else if (success) { - ret = this._handler(this._data, this._params || new Params()); + ret = this._handler(this._data, this._params); + if (ret instanceof Promise) { + // need to hold data and params until `ret` got resolved + // dont care for errors, data will be freed anyway on next start + return ret.then(res => { + this._params = EMPTY_PARAMS; + this._data = ''; + this._hitLimit = false; + return res; + }); + } } - this._params = undefined; + this._params = EMPTY_PARAMS; this._data = ''; this._hitLimit = false; return ret; diff --git a/src/common/parser/EscapeSequenceParser.test.ts b/src/common/parser/EscapeSequenceParser.test.ts index 4934d346..4b23fa00 100644 --- a/src/common/parser/EscapeSequenceParser.test.ts +++ b/src/common/parser/EscapeSequenceParser.test.ts @@ -3,9 +3,9 @@ * @license MIT */ -import { IParsingState, IParams, ParamsArray, IOscParser, IOscHandler, OscFallbackHandlerType, IFunctionIdentifier } from 'common/parser/Types'; +import { IParsingState, IParams, ParamsArray, IOscParser, IOscHandler, OscFallbackHandlerType, IFunctionIdentifier, IParserStackState, ParserStackType, ResumableHandlersType } from 'common/parser/Types'; import { EscapeSequenceParser, TransitionTable, VT500_TRANSITION_TABLE } from 'common/parser/EscapeSequenceParser'; -import * as chai from 'chai'; +import { assert } from 'chai'; import { StringToUtf32, stringFromCodePoint, utf32ToString } from 'common/input/TextDecoder'; import { ParserState } from 'common/parser/Constants'; import { Params } from 'common/parser/Params'; @@ -24,7 +24,7 @@ function r(a: number, b: number): string[] { } class MockOscPutParser implements IOscParser { - private _fallback: OscFallbackHandlerType = () => {}; + private _fallback: OscFallbackHandlerType = () => { }; public data = ''; public reset(): void { this.data = ''; @@ -41,7 +41,7 @@ class MockOscPutParser implements IOscParser { this._fallback(id, 'END', this.data.slice(this.data.indexOf(';') + 1)); } } - public addHandler(ident: number, handler: IOscHandler): IDisposable { + public registerHandler(ident: number, handler: IOscHandler): IDisposable { throw new Error('not implemented'); } public setHandler(ident: number, handler: IOscHandler): void { @@ -92,43 +92,58 @@ class TestEscapeSequenceParser extends EscapeSequenceParser { public identifier(id: IFunctionIdentifier): number { return this._identifier(id); } + public get parseStack(): IParserStackState { + return this._parseStack; + } + private _trackStack = false; + public trackStackSavesOnPause(): void { + this._trackStack = true; + } + public trackedStack: IParserStackState[] = []; + public parse(data: Uint32Array, length: number, promiseResult?: boolean): void | Promise { + const result = super.parse(data, length, promiseResult); + if (result instanceof Promise && this._trackStack) { + this.trackedStack.push({ ...this.parseStack }); + } + return result; + } } // test object to collect parser actions and compare them with expected values const testTerminal: any = { calls: [], - clear: function (): void { + clear(): void { this.calls = []; }, - compare: function (value: any): void { - chai.expect(this.calls.slice()).eql(value); // weird bug w'o slicing here + compare(value: any): void { + assert.deepEqual(this.calls, value); }, - print: function (data: Uint32Array, start: number, end: number): void { + print(data: Uint32Array, start: number, end: number): void { let s = ''; for (let i = start; i < end; ++i) { s += stringFromCodePoint(data[i]); } this.calls.push(['print', s]); }, - actionOSC: function (s: string): void { + actionOSC(s: string): void { this.calls.push(['osc', s]); }, - actionExecute: function (flag: string): void { + actionExecute(flag: string): void { this.calls.push(['exe', flag]); }, - actionCSI: function (collect: string, params: IParams, flag: string): void { + actionCSI(collect: string, params: IParams, flag: string): void { this.calls.push(['csi', collect, params.toArray(), flag]); }, - actionESC: function (collect: string, flag: string): void { + actionESC(collect: string, flag: string): void { this.calls.push(['esc', collect, flag]); }, - actionDCSHook: function (params: IParams): void { + actionDCSHook(params: IParams): void { this.calls.push(['dcs hook', params.toArray()]); }, - actionDCSPrint: function (s: string): void { + actionDCSPrint(s: string): void { this.calls.push(['dcs put', s]); }, - actionDCSUnhook: function (success: boolean): void { + actionDCSUnhook(success: boolean): void { this.calls.push(['dcs unhook', success]); } }; @@ -191,40 +206,40 @@ function parse(parser: TestEscapeSequenceParser, data: string): void { parser.parse(container, decoder.decode(data, container)); } -describe('EscapeSequenceParser', function (): void { +describe('EscapeSequenceParser', () => { const parser = testParser; - describe('Parser init and methods', function (): void { - it('constructor', function (): void { + describe('Parser init and methods', () => { + it('constructor', () => { let p = new TestEscapeSequenceParser(); - chai.expect(p.transitions).equal(VT500_TRANSITION_TABLE); + assert.deepEqual(p.transitions, VT500_TRANSITION_TABLE); p = new TestEscapeSequenceParser(VT500_TRANSITION_TABLE); - chai.expect(p.transitions).equal(VT500_TRANSITION_TABLE); + assert.deepEqual(p.transitions, VT500_TRANSITION_TABLE); const tansitions: TransitionTable = new TransitionTable(10); p = new TestEscapeSequenceParser(tansitions); - chai.expect(p.transitions).equal(tansitions); + assert.deepEqual(p.transitions, tansitions); }); - it('inital states', function (): void { - chai.expect(parser.initialState).equal(ParserState.GROUND); - chai.expect(parser.currentState).equal(ParserState.GROUND); - chai.expect(parser.osc).equal(''); - chai.expect(parser.params).eql([0]); - chai.expect(parser.collect).equal(''); + it('inital states', () => { + assert.equal(parser.initialState, ParserState.GROUND); + assert.equal(parser.currentState, ParserState.GROUND); + assert.equal(parser.osc, ''); + assert.deepEqual(parser.params, [0]); + assert.equal(parser.collect, ''); }); - it('reset states', function (): void { + it('reset states', () => { parser.currentState = 124; parser.osc = '#'; parser.params = [123]; parser.collect = '#'; parser.reset(); - chai.expect(parser.currentState).equal(ParserState.GROUND); - chai.expect(parser.osc).equal(''); - chai.expect(parser.params).eql([0]); - chai.expect(parser.collect).equal(''); + assert.equal(parser.currentState, ParserState.GROUND); + assert.equal(parser.osc, ''); + assert.deepEqual(parser.params, [0]); + assert.equal(parser.collect, ''); }); }); - describe('state transitions and actions', function (): void { - it('state GROUND execute action', function (): void { + describe('state transitions and actions', () => { + it('state GROUND execute action', () => { parser.reset(); testTerminal.clear(); let exes = r(0x00, 0x18); @@ -233,26 +248,26 @@ describe('EscapeSequenceParser', function (): void { for (let i = 0; i < exes.length; ++i) { parser.currentState = ParserState.GROUND; parse(parser, exes[i]); - chai.expect(parser.currentState).equal(ParserState.GROUND); + assert.equal(parser.currentState, ParserState.GROUND); testTerminal.compare([['exe', exes[i]]]); parser.reset(); testTerminal.clear(); } }); - it('state GROUND print action', function (): void { + it('state GROUND print action', () => { parser.reset(); testTerminal.clear(); const printables = r(0x20, 0x7f); // NOTE: DEL excluded for (let i = 0; i < printables.length; ++i) { parser.currentState = ParserState.GROUND; parse(parser, printables[i]); - chai.expect(parser.currentState).equal(ParserState.GROUND); + assert.equal(parser.currentState, ParserState.GROUND); testTerminal.compare([['print', printables[i]]]); parser.reset(); testTerminal.clear(); } }); - it('trans ANYWHERE --> GROUND with actions', function (): void { + it('trans ANYWHERE --> GROUND with actions', () => { const exes = [ '\x18', '\x1a', '\x80', '\x81', '\x82', '\x83', '\x84', '\x85', '\x86', '\x87', '\x88', @@ -269,32 +284,32 @@ describe('EscapeSequenceParser', function (): void { for (let i = 0; i < exes.length; ++i) { parser.currentState = state; parse(parser, exes[i]); - chai.expect(parser.currentState).equal(ParserState.GROUND); + assert.equal(parser.currentState, ParserState.GROUND); testTerminal.compare((state in exceptions ? exceptions[state][exes[i]] : 0) || [['exe', exes[i]]]); parser.reset(); testTerminal.clear(); } parse(parser, '\x9c'); - chai.expect(parser.currentState).equal(ParserState.GROUND); + assert.equal(parser.currentState, ParserState.GROUND); testTerminal.compare([]); parser.reset(); testTerminal.clear(); } }); - it('trans ANYWHERE --> ESCAPE with clear', function (): void { + it('trans ANYWHERE --> ESCAPE with clear', () => { parser.reset(); for (state in states) { parser.currentState = state; parser.params = [23]; parser.collect = '#'; parse(parser, '\x1b'); - chai.expect(parser.currentState).equal(ParserState.ESCAPE); - chai.expect(parser.params).eql([0]); - chai.expect(parser.collect).equal(''); + assert.equal(parser.currentState, ParserState.ESCAPE); + assert.deepEqual(parser.params, [0]); + assert.equal(parser.collect, ''); parser.reset(); } }); - it('state ESCAPE execute rules', function (): void { + it('state ESCAPE execute rules', () => { parser.reset(); testTerminal.clear(); let exes = r(0x00, 0x18); @@ -303,23 +318,23 @@ describe('EscapeSequenceParser', function (): void { for (let i = 0; i < exes.length; ++i) { parser.currentState = ParserState.ESCAPE; parse(parser, exes[i]); - chai.expect(parser.currentState).equal(ParserState.ESCAPE); + assert.equal(parser.currentState, ParserState.ESCAPE); testTerminal.compare([['exe', exes[i]]]); parser.reset(); testTerminal.clear(); } }); - it('state ESCAPE ignore', function (): void { + it('state ESCAPE ignore', () => { parser.reset(); testTerminal.clear(); parser.currentState = ParserState.ESCAPE; parse(parser, '\x7f'); - chai.expect(parser.currentState).equal(ParserState.ESCAPE); + assert.equal(parser.currentState, ParserState.ESCAPE); testTerminal.compare([]); parser.reset(); testTerminal.clear(); }); - it('trans ESCAPE --> GROUND with ecs_dispatch action', function (): void { + it('trans ESCAPE --> GROUND with ecs_dispatch action', () => { parser.reset(); testTerminal.clear(); let dispatches = r(0x30, 0x50); @@ -329,24 +344,24 @@ describe('EscapeSequenceParser', function (): void { for (let i = 0; i < dispatches.length; ++i) { parser.currentState = ParserState.ESCAPE; parse(parser, dispatches[i]); - chai.expect(parser.currentState).equal(ParserState.GROUND); + assert.equal(parser.currentState, ParserState.GROUND); testTerminal.compare([['esc', '', dispatches[i]]]); parser.reset(); testTerminal.clear(); } }); - it('trans ESCAPE --> ESCAPE_INTERMEDIATE with collect action', function (): void { + it('trans ESCAPE --> ESCAPE_INTERMEDIATE with collect action', () => { parser.reset(); const collect = r(0x20, 0x30); for (let i = 0; i < collect.length; ++i) { parser.currentState = ParserState.ESCAPE; parse(parser, collect[i]); - chai.expect(parser.currentState).equal(ParserState.ESCAPE_INTERMEDIATE); - chai.expect(parser.collect).equal(collect[i]); + assert.equal(parser.currentState, ParserState.ESCAPE_INTERMEDIATE); + assert.equal(parser.collect, collect[i]); parser.reset(); } }); - it('state ESCAPE_INTERMEDIATE execute rules', function (): void { + it('state ESCAPE_INTERMEDIATE execute rules', () => { parser.reset(); testTerminal.clear(); let exes = r(0x00, 0x18); @@ -355,57 +370,57 @@ describe('EscapeSequenceParser', function (): void { for (let i = 0; i < exes.length; ++i) { parser.currentState = ParserState.ESCAPE_INTERMEDIATE; parse(parser, exes[i]); - chai.expect(parser.currentState).equal(ParserState.ESCAPE_INTERMEDIATE); + assert.equal(parser.currentState, ParserState.ESCAPE_INTERMEDIATE); testTerminal.compare([['exe', exes[i]]]); parser.reset(); testTerminal.clear(); } }); - it('state ESCAPE_INTERMEDIATE ignore', function (): void { + it('state ESCAPE_INTERMEDIATE ignore', () => { parser.reset(); testTerminal.clear(); parser.currentState = ParserState.ESCAPE_INTERMEDIATE; parse(parser, '\x7f'); - chai.expect(parser.currentState).equal(ParserState.ESCAPE_INTERMEDIATE); + assert.equal(parser.currentState, ParserState.ESCAPE_INTERMEDIATE); testTerminal.compare([]); parser.reset(); testTerminal.clear(); }); - it('state ESCAPE_INTERMEDIATE collect action', function (): void { + it('state ESCAPE_INTERMEDIATE collect action', () => { parser.reset(); const collect = r(0x20, 0x30); for (let i = 0; i < collect.length; ++i) { parser.currentState = ParserState.ESCAPE_INTERMEDIATE; parse(parser, collect[i]); - chai.expect(parser.currentState).equal(ParserState.ESCAPE_INTERMEDIATE); - chai.expect(parser.collect).equal(collect[i]); + assert.equal(parser.currentState, ParserState.ESCAPE_INTERMEDIATE); + assert.equal(parser.collect, collect[i]); parser.reset(); } }); - it('trans ESCAPE_INTERMEDIATE --> GROUND with esc_dispatch action', function (): void { + it('trans ESCAPE_INTERMEDIATE --> GROUND with esc_dispatch action', () => { parser.reset(); testTerminal.clear(); const collect = r(0x30, 0x7f); for (let i = 0; i < collect.length; ++i) { parser.currentState = ParserState.ESCAPE_INTERMEDIATE; parse(parser, collect[i]); - chai.expect(parser.currentState).equal(ParserState.GROUND); + assert.equal(parser.currentState, ParserState.GROUND); // '\x5c' --> ESC + \ (7bit ST) parser does not expose this as it already got handled testTerminal.compare((collect[i] === '\x5c') ? [] : [['esc', '', collect[i]]]); parser.reset(); testTerminal.clear(); } }); - it('trans ANYWHERE/ESCAPE --> CSI_ENTRY with clear', function (): void { + it('trans ANYWHERE/ESCAPE --> CSI_ENTRY with clear', () => { parser.reset(); // C0 parser.currentState = ParserState.ESCAPE; parser.params = [123]; parser.collect = '#'; parse(parser, '['); - chai.expect(parser.currentState).equal(ParserState.CSI_ENTRY); - chai.expect(parser.params).eql([0]); - chai.expect(parser.collect).equal(''); + assert.equal(parser.currentState, ParserState.CSI_ENTRY); + assert.deepEqual(parser.params, [0]); + assert.equal(parser.collect, ''); parser.reset(); // C1 for (state in states) { @@ -413,13 +428,13 @@ describe('EscapeSequenceParser', function (): void { parser.params = [123]; parser.collect = '#'; parse(parser, '\x9b'); - chai.expect(parser.currentState).equal(ParserState.CSI_ENTRY); - chai.expect(parser.params).eql([0]); - chai.expect(parser.collect).equal(''); + assert.equal(parser.currentState, ParserState.CSI_ENTRY); + assert.deepEqual(parser.params, [0]); + assert.equal(parser.collect, ''); parser.reset(); } }); - it('state CSI_ENTRY execute rules', function (): void { + it('state CSI_ENTRY execute rules', () => { parser.reset(); testTerminal.clear(); let exes = r(0x00, 0x18); @@ -428,59 +443,59 @@ describe('EscapeSequenceParser', function (): void { for (let i = 0; i < exes.length; ++i) { parser.currentState = ParserState.CSI_ENTRY; parse(parser, exes[i]); - chai.expect(parser.currentState).equal(ParserState.CSI_ENTRY); + assert.equal(parser.currentState, ParserState.CSI_ENTRY); testTerminal.compare([['exe', exes[i]]]); parser.reset(); testTerminal.clear(); } }); - it('state CSI_ENTRY ignore', function (): void { + it('state CSI_ENTRY ignore', () => { parser.reset(); testTerminal.clear(); parser.currentState = ParserState.CSI_ENTRY; parse(parser, '\x7f'); - chai.expect(parser.currentState).equal(ParserState.CSI_ENTRY); + assert.equal(parser.currentState, ParserState.CSI_ENTRY); testTerminal.compare([]); parser.reset(); testTerminal.clear(); }); - it('trans CSI_ENTRY --> GROUND with csi_dispatch action', function (): void { + it('trans CSI_ENTRY --> GROUND with csi_dispatch action', () => { parser.reset(); const dispatches = r(0x40, 0x7f); for (let i = 0; i < dispatches.length; ++i) { parser.currentState = ParserState.CSI_ENTRY; parse(parser, dispatches[i]); - chai.expect(parser.currentState).equal(ParserState.GROUND); + assert.equal(parser.currentState, ParserState.GROUND); testTerminal.compare([['csi', '', [0], dispatches[i]]]); parser.reset(); testTerminal.clear(); } }); - it('trans CSI_ENTRY --> CSI_PARAM with param/collect actions', function (): void { + it('trans CSI_ENTRY --> CSI_PARAM with param/collect actions', () => { parser.reset(); const params = ['\x30', '\x31', '\x32', '\x33', '\x34', '\x35', '\x36', '\x37', '\x38', '\x39']; const collect = ['\x3c', '\x3d', '\x3e', '\x3f']; for (let i = 0; i < params.length; ++i) { parser.currentState = ParserState.CSI_ENTRY; parse(parser, params[i]); - chai.expect(parser.currentState).equal(ParserState.CSI_PARAM); - chai.expect(parser.params).eql([params[i].charCodeAt(0) - 48]); + assert.equal(parser.currentState, ParserState.CSI_PARAM); + assert.deepEqual(parser.params, [params[i].charCodeAt(0) - 48]); parser.reset(); } parser.currentState = ParserState.CSI_ENTRY; parse(parser, '\x3b'); - chai.expect(parser.currentState).equal(ParserState.CSI_PARAM); - chai.expect(parser.params).eql([0, 0]); + assert.equal(parser.currentState, ParserState.CSI_PARAM); + assert.deepEqual(parser.params, [0, 0]); parser.reset(); for (let i = 0; i < collect.length; ++i) { parser.currentState = ParserState.CSI_ENTRY; parse(parser, collect[i]); - chai.expect(parser.currentState).equal(ParserState.CSI_PARAM); - chai.expect(parser.collect).equal(collect[i]); + assert.equal(parser.currentState, ParserState.CSI_PARAM); + assert.equal(parser.collect, collect[i]); parser.reset(); } }); - it('state CSI_PARAM execute rules', function (): void { + it('state CSI_PARAM execute rules', () => { parser.reset(); testTerminal.clear(); let exes = r(0x00, 0x18); @@ -489,74 +504,74 @@ describe('EscapeSequenceParser', function (): void { for (let i = 0; i < exes.length; ++i) { parser.currentState = ParserState.CSI_PARAM; parse(parser, exes[i]); - chai.expect(parser.currentState).equal(ParserState.CSI_PARAM); + assert.equal(parser.currentState, ParserState.CSI_PARAM); testTerminal.compare([['exe', exes[i]]]); parser.reset(); testTerminal.clear(); } }); - it('state CSI_PARAM param action', function (): void { + it('state CSI_PARAM param action', () => { parser.reset(); const params = ['\x30', '\x31', '\x32', '\x33', '\x34', '\x35', '\x36', '\x37', '\x38', '\x39']; for (let i = 0; i < params.length; ++i) { parser.currentState = ParserState.CSI_PARAM; parse(parser, params[i]); - chai.expect(parser.currentState).equal(ParserState.CSI_PARAM); - chai.expect(parser.params).eql([params[i].charCodeAt(0) - 48]); + assert.equal(parser.currentState, ParserState.CSI_PARAM); + assert.deepEqual(parser.params, [params[i].charCodeAt(0) - 48]); parser.reset(); } parser.currentState = ParserState.CSI_PARAM; parse(parser, '\x3b'); - chai.expect(parser.currentState).equal(ParserState.CSI_PARAM); - chai.expect(parser.params).eql([0, 0]); + assert.equal(parser.currentState, ParserState.CSI_PARAM); + assert.deepEqual(parser.params, [0, 0]); parser.reset(); }); - it('state CSI_PARAM ignore', function (): void { + it('state CSI_PARAM ignore', () => { parser.reset(); testTerminal.clear(); parser.currentState = ParserState.CSI_PARAM; parse(parser, '\x7f'); - chai.expect(parser.currentState).equal(ParserState.CSI_PARAM); + assert.equal(parser.currentState, ParserState.CSI_PARAM); testTerminal.compare([]); parser.reset(); testTerminal.clear(); }); - it('trans CSI_PARAM --> GROUND with csi_dispatch action', function (): void { + it('trans CSI_PARAM --> GROUND with csi_dispatch action', () => { parser.reset(); const dispatches = r(0x40, 0x7f); for (let i = 0; i < dispatches.length; ++i) { parser.currentState = ParserState.CSI_PARAM; parser.params = [0, 1]; parse(parser, dispatches[i]); - chai.expect(parser.currentState).equal(ParserState.GROUND); + assert.equal(parser.currentState, ParserState.GROUND); testTerminal.compare([['csi', '', [0, 1], dispatches[i]]]); parser.reset(); testTerminal.clear(); } }); - it('trans CSI_ENTRY --> CSI_INTERMEDIATE with collect action', function (): void { + it('trans CSI_ENTRY --> CSI_INTERMEDIATE with collect action', () => { parser.reset(); const collect = r(0x20, 0x30); for (let i = 0; i < collect.length; ++i) { parser.currentState = ParserState.CSI_ENTRY; parse(parser, collect[i]); - chai.expect(parser.currentState).equal(ParserState.CSI_INTERMEDIATE); - chai.expect(parser.collect).equal(collect[i]); + assert.equal(parser.currentState, ParserState.CSI_INTERMEDIATE); + assert.equal(parser.collect, collect[i]); parser.reset(); } }); - it('trans CSI_PARAM --> CSI_INTERMEDIATE with collect action', function (): void { + it('trans CSI_PARAM --> CSI_INTERMEDIATE with collect action', () => { parser.reset(); const collect = r(0x20, 0x30); for (let i = 0; i < collect.length; ++i) { parser.currentState = ParserState.CSI_PARAM; parse(parser, collect[i]); - chai.expect(parser.currentState).equal(ParserState.CSI_INTERMEDIATE); - chai.expect(parser.collect).equal(collect[i]); + assert.equal(parser.currentState, ParserState.CSI_INTERMEDIATE); + assert.equal(parser.collect, collect[i]); parser.reset(); } }); - it('state CSI_INTERMEDIATE execute rules', function (): void { + it('state CSI_INTERMEDIATE execute rules', () => { parser.reset(); testTerminal.clear(); let exes = r(0x00, 0x18); @@ -565,88 +580,88 @@ describe('EscapeSequenceParser', function (): void { for (let i = 0; i < exes.length; ++i) { parser.currentState = ParserState.CSI_INTERMEDIATE; parse(parser, exes[i]); - chai.expect(parser.currentState).equal(ParserState.CSI_INTERMEDIATE); + assert.equal(parser.currentState, ParserState.CSI_INTERMEDIATE); testTerminal.compare([['exe', exes[i]]]); parser.reset(); testTerminal.clear(); } }); - it('state CSI_INTERMEDIATE collect', function (): void { + it('state CSI_INTERMEDIATE collect', () => { parser.reset(); const collect = r(0x20, 0x30); for (let i = 0; i < collect.length; ++i) { parser.currentState = ParserState.CSI_INTERMEDIATE; parse(parser, collect[i]); - chai.expect(parser.currentState).equal(ParserState.CSI_INTERMEDIATE); - chai.expect(parser.collect).equal(collect[i]); + assert.equal(parser.currentState, ParserState.CSI_INTERMEDIATE); + assert.equal(parser.collect, collect[i]); parser.reset(); } }); - it('state CSI_INTERMEDIATE ignore', function (): void { + it('state CSI_INTERMEDIATE ignore', () => { parser.reset(); testTerminal.clear(); parser.currentState = ParserState.CSI_INTERMEDIATE; parse(parser, '\x7f'); - chai.expect(parser.currentState).equal(ParserState.CSI_INTERMEDIATE); + assert.equal(parser.currentState, ParserState.CSI_INTERMEDIATE); testTerminal.compare([]); parser.reset(); testTerminal.clear(); }); - it('trans CSI_INTERMEDIATE --> GROUND with csi_dispatch action', function (): void { + it('trans CSI_INTERMEDIATE --> GROUND with csi_dispatch action', () => { parser.reset(); const dispatches = r(0x40, 0x7f); for (let i = 0; i < dispatches.length; ++i) { parser.currentState = ParserState.CSI_INTERMEDIATE; parser.params = [0, 1]; parse(parser, dispatches[i]); - chai.expect(parser.currentState).equal(ParserState.GROUND); + assert.equal(parser.currentState, ParserState.GROUND); testTerminal.compare([['csi', '', [0, 1], dispatches[i]]]); parser.reset(); testTerminal.clear(); } }); - it('trans CSI_ENTRY --> CSI_PARAM for ":" (0x3a)', function (): void { + it('trans CSI_ENTRY --> CSI_PARAM for ":" (0x3a)', () => { parser.reset(); parser.currentState = ParserState.CSI_ENTRY; parse(parser, '\x3a'); - chai.expect(parser.currentState).equal(ParserState.CSI_PARAM); + assert.equal(parser.currentState, ParserState.CSI_PARAM); parser.reset(); }); - it('trans CSI_PARAM --> CSI_IGNORE', function (): void { + it('trans CSI_PARAM --> CSI_IGNORE', () => { parser.reset(); const chars = ['\x3c', '\x3d', '\x3e', '\x3f']; for (let i = 0; i < chars.length; ++i) { parser.currentState = ParserState.CSI_PARAM; parse(parser, '\x3b' + chars[i]); - chai.expect(parser.currentState).equal(ParserState.CSI_IGNORE); - chai.expect(parser.params).eql([0, 0]); + assert.equal(parser.currentState, ParserState.CSI_IGNORE); + assert.deepEqual(parser.params, [0, 0]); parser.reset(); } }); - it('trans CSI_PARAM --> CSI_IGNORE', function (): void { + it('trans CSI_PARAM --> CSI_IGNORE', () => { parser.reset(); const chars = ['\x3c', '\x3d', '\x3e', '\x3f']; for (let i = 0; i < chars.length; ++i) { - chai.expect(parser.params).eql([0]); + assert.deepEqual(parser.params, [0]); parser.currentState = ParserState.CSI_PARAM; parse(parser, '\x3b' + chars[i]); - chai.expect(parser.currentState).equal(ParserState.CSI_IGNORE); - chai.expect(parser.params).eql([0, 0]); + assert.equal(parser.currentState, ParserState.CSI_IGNORE); + assert.deepEqual(parser.params, [0, 0]); parser.reset(); } }); - it('trans CSI_INTERMEDIATE --> CSI_IGNORE', function (): void { + it('trans CSI_INTERMEDIATE --> CSI_IGNORE', () => { parser.reset(); const chars = r(0x30, 0x40); for (let i = 0; i < chars.length; ++i) { parser.currentState = ParserState.CSI_INTERMEDIATE; parse(parser, chars[i]); - chai.expect(parser.currentState).equal(ParserState.CSI_IGNORE); - chai.expect(parser.params).eql([0]); + assert.equal(parser.currentState, ParserState.CSI_IGNORE); + assert.deepEqual(parser.params, [0]); parser.reset(); } }); - it('state CSI_IGNORE execute rules', function (): void { + it('state CSI_IGNORE execute rules', () => { parser.reset(); testTerminal.clear(); let exes = r(0x00, 0x18); @@ -655,13 +670,13 @@ describe('EscapeSequenceParser', function (): void { for (let i = 0; i < exes.length; ++i) { parser.currentState = ParserState.CSI_IGNORE; parse(parser, exes[i]); - chai.expect(parser.currentState).equal(ParserState.CSI_IGNORE); + assert.equal(parser.currentState, ParserState.CSI_IGNORE); testTerminal.compare([['exe', exes[i]]]); parser.reset(); testTerminal.clear(); } }); - it('state CSI_IGNORE ignore', function (): void { + it('state CSI_IGNORE ignore', () => { parser.reset(); testTerminal.clear(); let ignored = r(0x20, 0x40); @@ -669,32 +684,32 @@ describe('EscapeSequenceParser', function (): void { for (let i = 0; i < ignored.length; ++i) { parser.currentState = ParserState.CSI_IGNORE; parse(parser, ignored[i]); - chai.expect(parser.currentState).equal(ParserState.CSI_IGNORE); + assert.equal(parser.currentState, ParserState.CSI_IGNORE); testTerminal.compare([]); parser.reset(); testTerminal.clear(); } }); - it('trans CSI_IGNORE --> GROUND', function (): void { + it('trans CSI_IGNORE --> GROUND', () => { parser.reset(); const dispatches = r(0x40, 0x7f); for (let i = 0; i < dispatches.length; ++i) { parser.currentState = ParserState.CSI_IGNORE; parser.params = [0, 1]; parse(parser, dispatches[i]); - chai.expect(parser.currentState).equal(ParserState.GROUND); + assert.equal(parser.currentState, ParserState.GROUND); testTerminal.compare([]); parser.reset(); testTerminal.clear(); } }); - it('trans ANYWHERE/ESCAPE --> SOS_PM_APC_STRING', function (): void { + it('trans ANYWHERE/ESCAPE --> SOS_PM_APC_STRING', () => { parser.reset(); // C0 let initializers = ['\x58', '\x5e', '\x5f']; for (let i = 0; i < initializers.length; ++i) { parse(parser, '\x1b' + initializers[i]); - chai.expect(parser.currentState).equal(ParserState.SOS_PM_APC_STRING); + assert.equal(parser.currentState, ParserState.SOS_PM_APC_STRING); parser.reset(); } // C1 @@ -703,12 +718,12 @@ describe('EscapeSequenceParser', function (): void { initializers = ['\x98', '\x9e', '\x9f']; for (let i = 0; i < initializers.length; ++i) { parse(parser, initializers[i]); - chai.expect(parser.currentState).equal(ParserState.SOS_PM_APC_STRING); + assert.equal(parser.currentState, ParserState.SOS_PM_APC_STRING); parser.reset(); } } }); - it('state SOS_PM_APC_STRING ignore rules', function (): void { + it('state SOS_PM_APC_STRING ignore rules', () => { parser.reset(); let ignored = r(0x00, 0x18); ignored = ignored.concat(['\x19']); @@ -717,25 +732,25 @@ describe('EscapeSequenceParser', function (): void { for (let i = 0; i < ignored.length; ++i) { parser.currentState = ParserState.SOS_PM_APC_STRING; parse(parser, ignored[i]); - chai.expect(parser.currentState).equal(ParserState.SOS_PM_APC_STRING); + assert.equal(parser.currentState, ParserState.SOS_PM_APC_STRING); parser.reset(); } }); - it('trans ANYWHERE/ESCAPE --> OSC_STRING', function (): void { + it('trans ANYWHERE/ESCAPE --> OSC_STRING', () => { parser.reset(); // C0 parse(parser, '\x1b]'); - chai.expect(parser.currentState).equal(ParserState.OSC_STRING); + assert.equal(parser.currentState, ParserState.OSC_STRING); parser.reset(); // C1 for (state in states) { parser.currentState = state; parse(parser, '\x9d'); - chai.expect(parser.currentState).equal(ParserState.OSC_STRING); + assert.equal(parser.currentState, ParserState.OSC_STRING); parser.reset(); } }); - it('state OSC_STRING ignore rules', function (): void { + it('state OSC_STRING ignore rules', () => { parser.reset(); const ignored = [ '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', /* '\x07', */ '\x08', @@ -744,37 +759,37 @@ describe('EscapeSequenceParser', function (): void { for (let i = 0; i < ignored.length; ++i) { parser.currentState = ParserState.OSC_STRING; parse(parser, ignored[i]); - chai.expect(parser.currentState).equal(ParserState.OSC_STRING); - chai.expect(parser.osc).equal(''); + assert.equal(parser.currentState, ParserState.OSC_STRING); + assert.equal(parser.osc, ''); parser.reset(); } }); - it('state OSC_STRING put action', function (): void { + it('state OSC_STRING put action', () => { parser.reset(); const puts = r(0x20, 0x80); for (let i = 0; i < puts.length; ++i) { parser.currentState = ParserState.OSC_STRING; parse(parser, puts[i]); - chai.expect(parser.currentState).equal(ParserState.OSC_STRING); - chai.expect(parser.osc).equal(puts[i]); + assert.equal(parser.currentState, ParserState.OSC_STRING); + assert.equal(parser.osc, puts[i]); parser.reset(); } }); - it('state DCS_ENTRY', function (): void { + it('state DCS_ENTRY', () => { parser.reset(); // C0 parse(parser, '\x1bP'); - chai.expect(parser.currentState).equal(ParserState.DCS_ENTRY); + assert.equal(parser.currentState, ParserState.DCS_ENTRY); parser.reset(); // C1 for (state in states) { parser.currentState = state; parse(parser, '\x90'); - chai.expect(parser.currentState).equal(ParserState.DCS_ENTRY); + assert.equal(parser.currentState, ParserState.DCS_ENTRY); parser.reset(); } }); - it('state DCS_ENTRY ignore rules', function (): void { + it('state DCS_ENTRY ignore rules', () => { parser.reset(); const ignored = [ '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', '\x08', @@ -783,35 +798,35 @@ describe('EscapeSequenceParser', function (): void { for (let i = 0; i < ignored.length; ++i) { parser.currentState = ParserState.DCS_ENTRY; parse(parser, ignored[i]); - chai.expect(parser.currentState).equal(ParserState.DCS_ENTRY); + assert.equal(parser.currentState, ParserState.DCS_ENTRY); parser.reset(); } }); - it('state DCS_ENTRY --> DCS_PARAM with param/collect actions', function (): void { + it('state DCS_ENTRY --> DCS_PARAM with param/collect actions', () => { parser.reset(); const params = ['\x30', '\x31', '\x32', '\x33', '\x34', '\x35', '\x36', '\x37', '\x38', '\x39']; const collect = ['\x3c', '\x3d', '\x3e', '\x3f']; for (let i = 0; i < params.length; ++i) { parser.currentState = ParserState.DCS_ENTRY; parse(parser, params[i]); - chai.expect(parser.currentState).equal(ParserState.DCS_PARAM); - chai.expect(parser.params).eql([params[i].charCodeAt(0) - 48]); + assert.equal(parser.currentState, ParserState.DCS_PARAM); + assert.deepEqual(parser.params, [params[i].charCodeAt(0) - 48]); parser.reset(); } parser.currentState = ParserState.DCS_ENTRY; parse(parser, '\x3b'); - chai.expect(parser.currentState).equal(ParserState.DCS_PARAM); - chai.expect(parser.params).eql([0, 0]); + assert.equal(parser.currentState, ParserState.DCS_PARAM); + assert.deepEqual(parser.params, [0, 0]); parser.reset(); for (let i = 0; i < collect.length; ++i) { parser.currentState = ParserState.DCS_ENTRY; parse(parser, collect[i]); - chai.expect(parser.currentState).equal(ParserState.DCS_PARAM); - chai.expect(parser.collect).equal(collect[i]); + assert.equal(parser.currentState, ParserState.DCS_PARAM); + assert.equal(parser.collect, collect[i]); parser.reset(); } }); - it('state DCS_PARAM ignore rules', function (): void { + it('state DCS_PARAM ignore rules', () => { parser.reset(); const ignored = [ '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', '\x08', @@ -820,55 +835,55 @@ describe('EscapeSequenceParser', function (): void { for (let i = 0; i < ignored.length; ++i) { parser.currentState = ParserState.DCS_PARAM; parse(parser, ignored[i]); - chai.expect(parser.currentState).equal(ParserState.DCS_PARAM); + assert.equal(parser.currentState, ParserState.DCS_PARAM); parser.reset(); } }); - it('state DCS_PARAM param action', function (): void { + it('state DCS_PARAM param action', () => { parser.reset(); const params = ['\x30', '\x31', '\x32', '\x33', '\x34', '\x35', '\x36', '\x37', '\x38', '\x39']; for (let i = 0; i < params.length; ++i) { parser.currentState = ParserState.DCS_PARAM; parse(parser, params[i]); - chai.expect(parser.currentState).equal(ParserState.DCS_PARAM); - chai.expect(parser.params).eql([params[i].charCodeAt(0) - 48]); + assert.equal(parser.currentState, ParserState.DCS_PARAM); + assert.deepEqual(parser.params, [params[i].charCodeAt(0) - 48]); parser.reset(); } parser.currentState = ParserState.DCS_PARAM; parse(parser, '\x3b'); - chai.expect(parser.currentState).equal(ParserState.DCS_PARAM); - chai.expect(parser.params).eql([0, 0]); + assert.equal(parser.currentState, ParserState.DCS_PARAM); + assert.deepEqual(parser.params, [0, 0]); parser.reset(); }); - it('trans DCS_ENTRY --> DCS_PARAM for ":" (0x3a)', function (): void { + it('trans DCS_ENTRY --> DCS_PARAM for ":" (0x3a)', () => { parser.reset(); parser.currentState = ParserState.DCS_ENTRY; parse(parser, '\x3a'); - chai.expect(parser.currentState).equal(ParserState.DCS_PARAM); + assert.equal(parser.currentState, ParserState.DCS_PARAM); parser.reset(); }); - it('trans DCS_PARAM --> DCS_IGNORE', function (): void { + it('trans DCS_PARAM --> DCS_IGNORE', () => { parser.reset(); const chars = ['\x3c', '\x3d', '\x3e', '\x3f']; for (let i = 0; i < chars.length; ++i) { parser.currentState = ParserState.DCS_PARAM; parse(parser, '\x3b' + chars[i]); - chai.expect(parser.currentState).equal(ParserState.DCS_IGNORE); - chai.expect(parser.params).eql([0, 0]); + assert.equal(parser.currentState, ParserState.DCS_IGNORE); + assert.deepEqual(parser.params, [0, 0]); parser.reset(); } }); - it('trans DCS_INTERMEDIATE --> DCS_IGNORE', function (): void { + it('trans DCS_INTERMEDIATE --> DCS_IGNORE', () => { parser.reset(); const chars = r(0x30, 0x40); for (let i = 0; i < chars.length; ++i) { parser.currentState = ParserState.DCS_INTERMEDIATE; parse(parser, chars[i]); - chai.expect(parser.currentState).equal(ParserState.DCS_IGNORE); + assert.equal(parser.currentState, ParserState.DCS_IGNORE); parser.reset(); } }); - it('state DCS_IGNORE ignore rules', function (): void { + it('state DCS_IGNORE ignore rules', () => { parser.reset(); let ignored = [ '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', '\x08', @@ -878,33 +893,33 @@ describe('EscapeSequenceParser', function (): void { for (let i = 0; i < ignored.length; ++i) { parser.currentState = ParserState.DCS_IGNORE; parse(parser, ignored[i]); - chai.expect(parser.currentState).equal(ParserState.DCS_IGNORE); + assert.equal(parser.currentState, ParserState.DCS_IGNORE); parser.reset(); } }); - it('trans DCS_ENTRY --> DCS_INTERMEDIATE with collect action', function (): void { + it('trans DCS_ENTRY --> DCS_INTERMEDIATE with collect action', () => { parser.reset(); const collect = r(0x20, 0x30); for (let i = 0; i < collect.length; ++i) { parser.currentState = ParserState.DCS_ENTRY; parse(parser, collect[i]); - chai.expect(parser.currentState).equal(ParserState.DCS_INTERMEDIATE); - chai.expect(parser.collect).equal(collect[i]); + assert.equal(parser.currentState, ParserState.DCS_INTERMEDIATE); + assert.equal(parser.collect, collect[i]); parser.reset(); } }); - it('trans DCS_PARAM --> DCS_INTERMEDIATE with collect action', function (): void { + it('trans DCS_PARAM --> DCS_INTERMEDIATE with collect action', () => { parser.reset(); const collect = r(0x20, 0x30); for (let i = 0; i < collect.length; ++i) { parser.currentState = ParserState.DCS_PARAM; parse(parser, collect[i]); - chai.expect(parser.currentState).equal(ParserState.DCS_INTERMEDIATE); - chai.expect(parser.collect).equal(collect[i]); + assert.equal(parser.currentState, ParserState.DCS_INTERMEDIATE); + assert.equal(parser.collect, collect[i]); parser.reset(); } }); - it('state DCS_INTERMEDIATE ignore rules', function (): void { + it('state DCS_INTERMEDIATE ignore rules', () => { parser.reset(); const ignored = [ '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', '\x08', @@ -913,72 +928,72 @@ describe('EscapeSequenceParser', function (): void { for (let i = 0; i < ignored.length; ++i) { parser.currentState = ParserState.DCS_INTERMEDIATE; parse(parser, ignored[i]); - chai.expect(parser.currentState).equal(ParserState.DCS_INTERMEDIATE); + assert.equal(parser.currentState, ParserState.DCS_INTERMEDIATE); parser.reset(); } }); - it('state DCS_INTERMEDIATE collect action', function (): void { + it('state DCS_INTERMEDIATE collect action', () => { parser.reset(); const collect = r(0x20, 0x30); for (let i = 0; i < collect.length; ++i) { parser.currentState = ParserState.DCS_INTERMEDIATE; parse(parser, collect[i]); - chai.expect(parser.currentState).equal(ParserState.DCS_INTERMEDIATE); - chai.expect(parser.collect).equal(collect[i]); + assert.equal(parser.currentState, ParserState.DCS_INTERMEDIATE); + assert.equal(parser.collect, collect[i]); parser.reset(); } }); - it('trans DCS_INTERMEDIATE --> DCS_IGNORE', function (): void { + it('trans DCS_INTERMEDIATE --> DCS_IGNORE', () => { parser.reset(); const chars = r(0x30, 0x40); for (let i = 0; i < chars.length; ++i) { parser.currentState = ParserState.DCS_INTERMEDIATE; parse(parser, '\x20' + chars[i]); - chai.expect(parser.currentState).equal(ParserState.DCS_IGNORE); - chai.expect(parser.collect).equal('\x20'); + assert.equal(parser.currentState, ParserState.DCS_IGNORE); + assert.equal(parser.collect, '\x20'); parser.reset(); } }); - it('trans DCS_ENTRY --> DCS_PASSTHROUGH with hook', function (): void { + it('trans DCS_ENTRY --> DCS_PASSTHROUGH with hook', () => { parser.reset(); testTerminal.clear(); const collect = r(0x40, 0x7f); for (let i = 0; i < collect.length; ++i) { parser.currentState = ParserState.DCS_ENTRY; parse(parser, collect[i]); - chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH); + assert.equal(parser.currentState, ParserState.DCS_PASSTHROUGH); testTerminal.compare([['dcs hook', [0]]]); parser.reset(); testTerminal.clear(); } }); - it('trans DCS_PARAM --> DCS_PASSTHROUGH with hook', function (): void { + it('trans DCS_PARAM --> DCS_PASSTHROUGH with hook', () => { parser.reset(); testTerminal.clear(); const collect = r(0x40, 0x7f); for (let i = 0; i < collect.length; ++i) { parser.currentState = ParserState.DCS_PARAM; parse(parser, collect[i]); - chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH); + assert.equal(parser.currentState, ParserState.DCS_PASSTHROUGH); testTerminal.compare([['dcs hook', [0]]]); parser.reset(); testTerminal.clear(); } }); - it('trans DCS_INTERMEDIATE --> DCS_PASSTHROUGH with hook', function (): void { + it('trans DCS_INTERMEDIATE --> DCS_PASSTHROUGH with hook', () => { parser.reset(); testTerminal.clear(); const collect = r(0x40, 0x7f); for (let i = 0; i < collect.length; ++i) { parser.currentState = ParserState.DCS_INTERMEDIATE; parse(parser, collect[i]); - chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH); + assert.equal(parser.currentState, ParserState.DCS_PASSTHROUGH); testTerminal.compare([['dcs hook', [0]]]); parser.reset(); testTerminal.clear(); } }); - it('state DCS_PASSTHROUGH put action', function (): void { + it('state DCS_PASSTHROUGH put action', () => { parser.reset(); testTerminal.clear(); let puts = r(0x00, 0x18); @@ -988,18 +1003,18 @@ describe('EscapeSequenceParser', function (): void { for (let i = 0; i < puts.length; ++i) { parser.currentState = ParserState.DCS_PASSTHROUGH; parse(parser, puts[i]); - chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH); + assert.equal(parser.currentState, ParserState.DCS_PASSTHROUGH); testTerminal.compare([['dcs put', puts[i]]]); parser.reset(); testTerminal.clear(); } }); - it('state DCS_PASSTHROUGH ignore', function (): void { + it('state DCS_PASSTHROUGH ignore', () => { parser.reset(); testTerminal.clear(); parser.currentState = ParserState.DCS_PASSTHROUGH; parse(parser, '\x7f'); - chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH); + assert.equal(parser.currentState, ParserState.DCS_PASSTHROUGH); testTerminal.compare([]); parser.reset(); testTerminal.clear(); @@ -1015,8 +1030,8 @@ describe('EscapeSequenceParser', function (): void { testTerminal.compare(value); } - describe('escape sequence examples', function (): void { - it('CSI with print and execute', function (): void { + describe('escape sequence examples', () => { + it('CSI with print and execute', () => { test('\x1b[<31;5mHello World! öäü€\nabc', [ ['csi', '<', [31, 5], 'm'], @@ -1025,19 +1040,19 @@ describe('EscapeSequenceParser', function (): void { ['print', 'abc'] ], null); }); - it('OSC', function (): void { + it('OSC', () => { test('\x1b]0;abc123€öäü\x07', [ ['osc', '0;abc123€öäü, success: true'] ], null); }); - it('single DCS', function (): void { + it('single DCS', () => { test('\x1bP1;2;3+$aäbc;däe\x9c', [ ['dcs hook', [1, 2, 3]], ['dcs put', 'äbc;däe'], ['dcs unhook', true] ], null); }); - it('multi DCS', function (): void { + it('multi DCS', () => { test('\x1bP1;2;3+$abc;de', [ ['dcs hook', [1, 2, 3]], ['dcs put', 'bc;de'] @@ -1048,7 +1063,7 @@ describe('EscapeSequenceParser', function (): void { ['dcs unhook', true] ], true); }); - it('print + DCS(C1)', function (): void { + it('print + DCS(C1)', () => { test('abc\x901;2;3+$abc;de\x9c', [ ['print', 'abc'], ['dcs hook', [1, 2, 3]], @@ -1056,26 +1071,26 @@ describe('EscapeSequenceParser', function (): void { ['dcs unhook', true] ], null); }); - it('print + PM(C1) + print', function (): void { + it('print + PM(C1) + print', () => { test('abc\x98123tzf\x9cdefg', [ ['print', 'abc'], ['print', 'defg'] ], null); }); - it('print + OSC(C1) + print', function (): void { + it('print + OSC(C1) + print', () => { test('abc\x9d123;tzf\x9cdefg', [ ['print', 'abc'], ['osc', '123;tzf, success: true'], ['print', 'defg'] ], null); }); - it('error recovery', function (): void { + it('error recovery', () => { test('\x1b[1€abcdefg\x9b<;c', [ ['print', 'abcdefg'], ['csi', '<', [0, 0], 'c'] ], null); }); - it('7bit ST should be swallowed', function (): void { + it('7bit ST should be swallowed', () => { test('abc\x9d123;tzf\x1b\\defg', [ ['print', 'abc'], ['osc', '123;tzf, success: true'], @@ -1091,7 +1106,7 @@ describe('EscapeSequenceParser', function (): void { ['print', 'abc'] ], null); }); - it('colon notation in DCS params', function (): void { + it('colon notation in DCS params', () => { test('abc\x901;2::55;3+$abc;de\x9c', [ ['print', 'abc'], ['dcs hook', [1, 2, [-1, 55], 3]], @@ -1127,50 +1142,50 @@ describe('EscapeSequenceParser', function (): void { }); }); - describe('coverage tests', function (): void { - it('CSI_IGNORE error', function (): void { + describe('coverage tests', () => { + it('CSI_IGNORE error', () => { parser.reset(); testTerminal.clear(); parser.currentState = ParserState.CSI_IGNORE; parse(parser, '€öäü'); - chai.expect(parser.currentState).equal(ParserState.CSI_IGNORE); + assert.equal(parser.currentState, ParserState.CSI_IGNORE); testTerminal.compare([]); parser.reset(); testTerminal.clear(); }); - it('DCS_IGNORE error', function (): void { + it('DCS_IGNORE error', () => { parser.reset(); testTerminal.clear(); parser.currentState = ParserState.DCS_IGNORE; parse(parser, '€öäü'); - chai.expect(parser.currentState).equal(ParserState.DCS_IGNORE); + assert.equal(parser.currentState, ParserState.DCS_IGNORE); testTerminal.compare([]); parser.reset(); testTerminal.clear(); }); - it('DCS_PASSTHROUGH error', function (): void { + it('DCS_PASSTHROUGH error', () => { parser.reset(); testTerminal.clear(); parser.currentState = ParserState.DCS_PASSTHROUGH; parse(parser, '\x901;2;3+$a€öäü'); - chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH); + assert.equal(parser.currentState, ParserState.DCS_PASSTHROUGH); testTerminal.compare([['dcs hook', [1, 2, 3]], ['dcs put', '€öäü']]); parser.reset(); testTerminal.clear(); }); - it('error else of if (code > 159)', function (): void { + it('error else of if (code > 159)', () => { parser.reset(); testTerminal.clear(); parser.currentState = ParserState.GROUND; parse(parser, '\x9c'); - chai.expect(parser.currentState).equal(ParserState.GROUND); + assert.equal(parser.currentState, ParserState.GROUND); testTerminal.compare([]); parser.reset(); testTerminal.clear(); }); }); - describe('set/clear handler', function (): void { + describe('set/clear handler', () => { const INPUT = '\x1b[1;31mhello \x1b%Gwor\x1bEld!\x1b[0m\r\n$>\x1b]1;foo=bar\x1b\\'; let parser2: TestEscapeSequenceParser; let print = ''; @@ -1187,269 +1202,275 @@ describe('EscapeSequenceParser', function (): void { osc.length = 0; dcs.length = 0; } - beforeEach(function (): void { + beforeEach(() => { parser2 = new TestEscapeSequenceParser(); clearAccu(); }); - it('print handler', function (): void { + it('print handler', () => { parser2.setPrintHandler(function (data: Uint32Array, start: number, end: number): void { for (let i = start; i < end; ++i) { print += stringFromCodePoint(data[i]); } }); parse(parser2, INPUT); - chai.expect(print).equal('hello world!$>'); + assert.equal(print, 'hello world!$>'); parser2.clearPrintHandler(); parser2.clearPrintHandler(); // should not throw clearAccu(); parse(parser2, INPUT); - chai.expect(print).equal(''); + assert.equal(print, ''); }); - it('ESC handler', function (): void { - parser2.setEscHandler({intermediates: '%', final: 'G'}, function (): void { + it('ESC handler', () => { + parser2.registerEscHandler({ intermediates: '%', final: 'G' }, function (): boolean { esc.push('%G'); + return true; }); - parser2.setEscHandler({final: 'E'}, function (): void { + parser2.registerEscHandler({ final: 'E' }, function (): boolean { esc.push('E'); + return true; }); parse(parser2, INPUT); - chai.expect(esc).eql(['%G', 'E']); - parser2.clearEscHandler({intermediates: '%', final: 'G'}); - parser2.clearEscHandler({intermediates: '%', final: 'G'}); // should not throw + assert.deepEqual(esc, ['%G', 'E']); + parser2.clearEscHandler({ intermediates: '%', final: 'G' }); + parser2.clearEscHandler({ intermediates: '%', final: 'G' }); // should not throw clearAccu(); parse(parser2, INPUT); - chai.expect(esc).eql(['E']); - parser2.clearEscHandler({final: 'E'}); + assert.deepEqual(esc, ['E']); + parser2.clearEscHandler({ final: 'E' }); clearAccu(); parse(parser2, INPUT); - chai.expect(esc).eql([]); + assert.deepEqual(esc, []); }); describe('ESC custom handlers', () => { it('prevent fallback', () => { - parser2.setEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('default - %G'); }); - parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return true; }); + parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('default - %G'); return true; }); + parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('custom - %G'); return true; }); parse(parser2, INPUT); - chai.expect(esc).eql(['custom - %G']); + assert.deepEqual(esc, ['custom - %G']); }); it('allow fallback', () => { - parser2.setEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('default - %G'); }); - parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return false; }); + parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('default - %G'); return true; }); + parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('custom - %G'); return false; }); parse(parser2, INPUT); - chai.expect(esc).eql(['custom - %G', 'default - %G']); + assert.deepEqual(esc, ['custom - %G', 'default - %G']); }); it('Multiple custom handlers fallback once', () => { - parser2.setEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('default - %G'); }); - parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return true; }); - parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom2 - %G'); return false; }); + parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('default - %G'); return true; }); + parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('custom - %G'); return true; }); + parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('custom2 - %G'); return false; }); parse(parser2, INPUT); - chai.expect(esc).eql(['custom2 - %G', 'custom - %G']); + assert.deepEqual(esc, ['custom2 - %G', 'custom - %G']); }); it('Multiple custom handlers no fallback', () => { - parser2.setEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('default - %G'); }); - parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return true; }); - parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom2 - %G'); return true; }); + parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('default - %G'); return true; }); + parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('custom - %G'); return true; }); + parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('custom2 - %G'); return true; }); parse(parser2, INPUT); - chai.expect(esc).eql(['custom2 - %G']); + assert.deepEqual(esc, ['custom2 - %G']); }); it('Execution order should go from latest handler down to the original', () => { const order: number[] = []; - parser2.setEscHandler({intermediates: '%', final: 'G'}, () => { order.push(1); }); - parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { order.push(2); return false; }); - parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { order.push(3); return false; }); + parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { order.push(1); return true; }); + parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { order.push(2); return false; }); + parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { order.push(3); return false; }); parse(parser2, '\x1b%G'); - chai.expect(order).eql([3, 2, 1]); + assert.deepEqual(order, [3, 2, 1]); }); it('Dispose should work', () => { - parser2.setEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('default - %G'); }); - const dispo = parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return true; }); + parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('default - %G'); return true; }); + const dispo = parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('custom - %G'); return true; }); dispo.dispose(); parse(parser2, INPUT); - chai.expect(esc).eql(['default - %G']); + assert.deepEqual(esc, ['default - %G']); }); it('Should not corrupt the parser when dispose is called twice', () => { - parser2.setEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('default - %G'); }); - const dispo = parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return true; }); + parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('default - %G'); return true; }); + const dispo = parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('custom - %G'); return true; }); dispo.dispose(); dispo.dispose(); parse(parser2, INPUT); - chai.expect(esc).eql(['default - %G']); + assert.deepEqual(esc, ['default - %G']); }); }); - it('CSI handler', function (): void { - parser2.setCsiHandler({final: 'm'}, function (params: IParams): void { + it('CSI handler', () => { + parser2.registerCsiHandler({ final: 'm' }, function (params: IParams): boolean { csi.push(['m', params.toArray(), '']); + return true; }); parse(parser2, INPUT); - chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']]); - parser2.clearCsiHandler({final: 'm'}); - parser2.clearCsiHandler({final: 'm'}); // should not throw + assert.deepEqual(csi, [['m', [1, 31], ''], ['m', [0], '']]); + parser2.clearCsiHandler({ final: 'm' }); + parser2.clearCsiHandler({ final: 'm' }); // should not throw clearAccu(); parse(parser2, INPUT); - chai.expect(csi).eql([]); + assert.deepEqual(csi, []); }); describe('CSI custom handlers', () => { it('Prevent fallback', () => { const csiCustom: [string, ParamsArray, string][] = []; - parser2.setCsiHandler({final: 'm'}, params => { csi.push(['m', params.toArray(), '']); }); - parser2.addCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return true; }); + parser2.registerCsiHandler({ final: 'm' }, params => { csi.push(['m', params.toArray(), '']); return true; }); + parser2.registerCsiHandler({ final: 'm' }, params => { csiCustom.push(['m', params.toArray(), '']); return true; }); parse(parser2, INPUT); - chai.expect(csi).eql([], 'Should not fallback to original handler'); - chai.expect(csiCustom).eql([['m', [1, 31], ''], ['m', [0], '']]); + assert.deepEqual(csi, [], 'Should not fallback to original handler'); + assert.deepEqual(csiCustom, [['m', [1, 31], ''], ['m', [0], '']]); }); it('Allow fallback', () => { const csiCustom: [string, ParamsArray, string][] = []; - parser2.setCsiHandler({final: 'm'}, params => { csi.push(['m', params.toArray(), '']); }); - parser2.addCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return false; }); + parser2.registerCsiHandler({ final: 'm' }, params => { csi.push(['m', params.toArray(), '']); return true; }); + parser2.registerCsiHandler({ final: 'm' }, params => { csiCustom.push(['m', params.toArray(), '']); return false; }); parse(parser2, INPUT); - chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']], 'Should fallback to original handler'); - chai.expect(csiCustom).eql([['m', [1, 31], ''], ['m', [0], '']]); + assert.deepEqual(csi, [['m', [1, 31], ''], ['m', [0], '']], 'Should fallback to original handler'); + assert.deepEqual(csiCustom, [['m', [1, 31], ''], ['m', [0], '']]); }); it('Multiple custom handlers fallback once', () => { const csiCustom: [string, ParamsArray, string][] = []; const csiCustom2: [string, ParamsArray, string][] = []; - parser2.setCsiHandler({final: 'm'}, params => { csi.push(['m', params.toArray(), '']); }); - parser2.addCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return true; }); - parser2.addCsiHandler({final: 'm'}, params => { csiCustom2.push(['m', params.toArray(), '']); return false; }); + parser2.registerCsiHandler({ final: 'm' }, params => { csi.push(['m', params.toArray(), '']); return true; }); + parser2.registerCsiHandler({ final: 'm' }, params => { csiCustom.push(['m', params.toArray(), '']); return true; }); + parser2.registerCsiHandler({ final: 'm' }, params => { csiCustom2.push(['m', params.toArray(), '']); return false; }); parse(parser2, INPUT); - chai.expect(csi).eql([], 'Should not fallback to original handler'); - chai.expect(csiCustom).eql([['m', [1, 31], ''], ['m', [0], '']]); - chai.expect(csiCustom2).eql([['m', [1, 31], ''], ['m', [0], '']]); + assert.deepEqual(csi, [], 'Should not fallback to original handler'); + assert.deepEqual(csiCustom, [['m', [1, 31], ''], ['m', [0], '']]); + assert.deepEqual(csiCustom2, [['m', [1, 31], ''], ['m', [0], '']]); }); it('Multiple custom handlers no fallback', () => { const csiCustom: [string, ParamsArray, string][] = []; const csiCustom2: [string, ParamsArray, string][] = []; - parser2.setCsiHandler({final: 'm'}, params => { csi.push(['m', params.toArray(), '']); }); - parser2.addCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return true; }); - parser2.addCsiHandler({final: 'm'}, params => { csiCustom2.push(['m', params.toArray(), '']); return true; }); + parser2.registerCsiHandler({ final: 'm' }, params => { csi.push(['m', params.toArray(), '']); return true; }); + parser2.registerCsiHandler({ final: 'm' }, params => { csiCustom.push(['m', params.toArray(), '']); return true; }); + parser2.registerCsiHandler({ final: 'm' }, params => { csiCustom2.push(['m', params.toArray(), '']); return true; }); parse(parser2, INPUT); - chai.expect(csi).eql([], 'Should not fallback to original handler'); - chai.expect(csiCustom).eql([], 'Should not fallback once'); - chai.expect(csiCustom2).eql([['m', [1, 31], ''], ['m', [0], '']]); + assert.deepEqual(csi, [], 'Should not fallback to original handler'); + assert.deepEqual(csiCustom, [], 'Should not fallback once'); + assert.deepEqual(csiCustom2, [['m', [1, 31], ''], ['m', [0], '']]); }); it('Execution order should go from latest handler down to the original', () => { const order: number[] = []; - parser2.setCsiHandler({final: 'm'}, () => { order.push(1); }); - parser2.addCsiHandler({final: 'm'}, () => { order.push(2); return false; }); - parser2.addCsiHandler({final: 'm'}, () => { order.push(3); return false; }); + parser2.registerCsiHandler({ final: 'm' }, () => { order.push(1); return true; }); + parser2.registerCsiHandler({ final: 'm' }, () => { order.push(2); return false; }); + parser2.registerCsiHandler({ final: 'm' }, () => { order.push(3); return false; }); parse(parser2, '\x1b[0m'); - chai.expect(order).eql([3, 2, 1]); + assert.deepEqual(order, [3, 2, 1]); }); it('Dispose should work', () => { const csiCustom: [string, ParamsArray, string][] = []; - parser2.setCsiHandler({final: 'm'}, params => { csi.push(['m', params.toArray(), '']); }); - const customHandler = parser2.addCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return true; }); + parser2.registerCsiHandler({ final: 'm' }, params => { csi.push(['m', params.toArray(), '']); return true; }); + const customHandler = parser2.registerCsiHandler({ final: 'm' }, params => { csiCustom.push(['m', params.toArray(), '']); return true; }); customHandler.dispose(); parse(parser2, INPUT); - chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']]); - chai.expect(csiCustom).eql([], 'Should not use custom handler as it was disposed'); + assert.deepEqual(csi, [['m', [1, 31], ''], ['m', [0], '']]); + assert.deepEqual(csiCustom, [], 'Should not use custom handler as it was disposed'); }); it('Should not corrupt the parser when dispose is called twice', () => { const csiCustom: [string, ParamsArray, string][] = []; - parser2.setCsiHandler({final: 'm'}, params => { csi.push(['m', params.toArray(), '']); }); - const customHandler = parser2.addCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return true; }); + parser2.registerCsiHandler({ final: 'm' }, params => { csi.push(['m', params.toArray(), '']); return true; }); + const customHandler = parser2.registerCsiHandler({ final: 'm' }, params => { csiCustom.push(['m', params.toArray(), '']); return true; }); customHandler.dispose(); customHandler.dispose(); parse(parser2, INPUT); - chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']]); - chai.expect(csiCustom).eql([], 'Should not use custom handler as it was disposed'); + assert.deepEqual(csi, [['m', [1, 31], ''], ['m', [0], '']]); + assert.deepEqual(csiCustom, [], 'Should not use custom handler as it was disposed'); }); }); - it('EXECUTE handler', function (): void { - parser2.setExecuteHandler('\n', function (): void { + it('EXECUTE handler', () => { + parser2.setExecuteHandler('\n', function (): boolean { exe.push('\n'); + return true; }); - parser2.setExecuteHandler('\r', function (): void { + parser2.setExecuteHandler('\r', function (): boolean { exe.push('\r'); + return true; }); parse(parser2, INPUT); - chai.expect(exe).eql(['\r', '\n']); + assert.deepEqual(exe, ['\r', '\n']); parser2.clearExecuteHandler('\r'); parser2.clearExecuteHandler('\r'); // should not throw clearAccu(); parse(parser2, INPUT); - chai.expect(exe).eql(['\n']); + assert.deepEqual(exe, ['\n']); }); - it('OSC handler', function (): void { - parser2.setOscHandler(1, new OscHandler(function (data: string): void { + it('OSC handler', () => { + parser2.registerOscHandler(1, new OscHandler(function (data: string): boolean { osc.push([1, data]); + return true; })); parse(parser2, INPUT); - chai.expect(osc).eql([[1, 'foo=bar']]); + assert.deepEqual(osc, [[1, 'foo=bar']]); parser2.clearOscHandler(1); parser2.clearOscHandler(1); // should not throw clearAccu(); parse(parser2, INPUT); - chai.expect(osc).eql([]); + assert.deepEqual(osc, []); }); describe('OSC custom handlers', () => { it('Prevent fallback', () => { const oscCustom: [number, string][] = []; - parser2.setOscHandler(1, new OscHandler(data => osc.push([1, data]))); - parser2.addOscHandler(1, new OscHandler(data => { oscCustom.push([1, data]); return true; })); + parser2.registerOscHandler(1, new OscHandler(data => { osc.push([1, data]); return true; })); + parser2.registerOscHandler(1, new OscHandler(data => { oscCustom.push([1, data]); return true; })); parse(parser2, INPUT); - chai.expect(osc).eql([], 'Should not fallback to original handler'); - chai.expect(oscCustom).eql([[1, 'foo=bar']]); + assert.deepEqual(osc, [], 'Should not fallback to original handler'); + assert.deepEqual(oscCustom, [[1, 'foo=bar']]); }); it('Allow fallback', () => { const oscCustom: [number, string][] = []; - parser2.setOscHandler(1, new OscHandler(data => osc.push([1, data]))); - parser2.addOscHandler(1, new OscHandler(data => { oscCustom.push([1, data]); return false; })); + parser2.registerOscHandler(1, new OscHandler(data => { osc.push([1, data]); return true; })); + parser2.registerOscHandler(1, new OscHandler(data => { oscCustom.push([1, data]); return false; })); parse(parser2, INPUT); - chai.expect(osc).eql([[1, 'foo=bar']], 'Should fallback to original handler'); - chai.expect(oscCustom).eql([[1, 'foo=bar']]); + assert.deepEqual(osc, [[1, 'foo=bar']], 'Should fallback to original handler'); + assert.deepEqual(oscCustom, [[1, 'foo=bar']]); }); it('Multiple custom handlers fallback once', () => { const oscCustom: [number, string][] = []; const oscCustom2: [number, string][] = []; - parser2.setOscHandler(1, new OscHandler(data => osc.push([1, data]))); - parser2.addOscHandler(1, new OscHandler(data => { oscCustom.push([1, data]); return true; })); - parser2.addOscHandler(1, new OscHandler(data => { oscCustom2.push([1, data]); return false; })); + parser2.registerOscHandler(1, new OscHandler(data => { osc.push([1, data]); return true; })); + parser2.registerOscHandler(1, new OscHandler(data => { oscCustom.push([1, data]); return true; })); + parser2.registerOscHandler(1, new OscHandler(data => { oscCustom2.push([1, data]); return false; })); parse(parser2, INPUT); - chai.expect(osc).eql([], 'Should not fallback to original handler'); - chai.expect(oscCustom).eql([[1, 'foo=bar']]); - chai.expect(oscCustom2).eql([[1, 'foo=bar']]); + assert.deepEqual(osc, [], 'Should not fallback to original handler'); + assert.deepEqual(oscCustom, [[1, 'foo=bar']]); + assert.deepEqual(oscCustom2, [[1, 'foo=bar']]); }); it('Multiple custom handlers no fallback', () => { const oscCustom: [number, string][] = []; const oscCustom2: [number, string][] = []; - parser2.setOscHandler(1, new OscHandler(data => osc.push([1, data]))); - parser2.addOscHandler(1, new OscHandler(data => { oscCustom.push([1, data]); return true; })); - parser2.addOscHandler(1, new OscHandler(data => { oscCustom2.push([1, data]); return true; })); + parser2.registerOscHandler(1, new OscHandler(data => { osc.push([1, data]); return true; })); + parser2.registerOscHandler(1, new OscHandler(data => { oscCustom.push([1, data]); return true; })); + parser2.registerOscHandler(1, new OscHandler(data => { oscCustom2.push([1, data]); return true; })); parse(parser2, INPUT); - chai.expect(osc).eql([], 'Should not fallback to original handler'); - chai.expect(oscCustom).eql([], 'Should not fallback once'); - chai.expect(oscCustom2).eql([[1, 'foo=bar']]); + assert.deepEqual(osc, [], 'Should not fallback to original handler'); + assert.deepEqual(oscCustom, [], 'Should not fallback once'); + assert.deepEqual(oscCustom2, [[1, 'foo=bar']]); }); it('Execution order should go from latest handler down to the original', () => { const order: number[] = []; - parser2.setOscHandler(1, new OscHandler(() => order.push(1))); - parser2.addOscHandler(1, new OscHandler(() => { order.push(2); return false; })); - parser2.addOscHandler(1, new OscHandler(() => { order.push(3); return false; })); + parser2.registerOscHandler(1, new OscHandler(() => { order.push(1); return true; })); + parser2.registerOscHandler(1, new OscHandler(() => { order.push(2); return false; })); + parser2.registerOscHandler(1, new OscHandler(() => { order.push(3); return false; })); parse(parser2, '\x1b]1;foo=bar\x1b\\'); - chai.expect(order).eql([3, 2, 1]); + assert.deepEqual(order, [3, 2, 1]); }); it('Dispose should work', () => { const oscCustom: [number, string][] = []; - parser2.setOscHandler(1, new OscHandler(data => osc.push([1, data]))); - const customHandler = parser2.addOscHandler(1, new OscHandler(data => { oscCustom.push([1, data]); return true; })); + parser2.registerOscHandler(1, new OscHandler(data => { osc.push([1, data]); return true; })); + const customHandler = parser2.registerOscHandler(1, new OscHandler(data => { oscCustom.push([1, data]); return true; })); customHandler.dispose(); parse(parser2, INPUT); - chai.expect(osc).eql([[1, 'foo=bar']]); - chai.expect(oscCustom).eql([], 'Should not use custom handler as it was disposed'); + assert.deepEqual(osc, [[1, 'foo=bar']]); + assert.deepEqual(oscCustom, [], 'Should not use custom handler as it was disposed'); }); it('Should not corrupt the parser when dispose is called twice', () => { const oscCustom: [number, string][] = []; - parser2.setOscHandler(1, new OscHandler(data => osc.push([1, data]))); - const customHandler = parser2.addOscHandler(1, new OscHandler(data => { oscCustom.push([1, data]); return true; })); + parser2.registerOscHandler(1, new OscHandler(data => { osc.push([1, data]); return true; })); + const customHandler = parser2.registerOscHandler(1, new OscHandler(data => { oscCustom.push([1, data]); return true; })); customHandler.dispose(); customHandler.dispose(); parse(parser2, INPUT); - chai.expect(osc).eql([[1, 'foo=bar']]); - chai.expect(oscCustom).eql([], 'Should not use custom handler as it was disposed'); + assert.deepEqual(osc, [[1, 'foo=bar']]); + assert.deepEqual(oscCustom, [], 'Should not use custom handler as it was disposed'); }); }); - it('DCS handler', function (): void { - parser2.setDcsHandler({intermediates: '+', final: 'p'}, { + it('DCS handler', () => { + parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, { hook: function (params: IParams): void { dcs.push(['hook', '', params.toArray(), 0]); }, @@ -1460,90 +1481,91 @@ describe('EscapeSequenceParser', function (): void { } dcs.push(['put', s]); }, - unhook: function (): void { + unhook: function (): boolean { dcs.push(['unhook']); + return true; } }); parse(parser2, '\x1bP1;2;3+pabc'); parse(parser2, ';de\x9c'); - chai.expect(dcs).eql([ + assert.deepEqual(dcs, [ ['hook', '', [1, 2, 3], 0], ['put', 'abc'], ['put', ';de'], ['unhook'] ]); - parser2.clearDcsHandler({intermediates: '+', final: 'p'}); - parser2.clearDcsHandler({intermediates: '+', final: 'p'}); // should not throw + parser2.clearDcsHandler({ intermediates: '+', final: 'p' }); + parser2.clearDcsHandler({ intermediates: '+', final: 'p' }); // should not throw clearAccu(); parse(parser2, '\x1bP1;2;3+pabc'); parse(parser2, ';de\x9c'); - chai.expect(dcs).eql([]); + assert.deepEqual(dcs, []); }); describe('DCS custom handlers', () => { const DCS_INPUT = '\x1bP1;2;3+pabc\x1b\\'; it('Prevent fallback', () => { const dcsCustom: [string, (number | number[])[], string][] = []; - parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => dcsCustom.push(['A', params.toArray(), data]))); - parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); + parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['A', params.toArray(), data]); return true; })); + parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); parse(parser2, DCS_INPUT); - chai.expect(dcsCustom).eql([['B', [1, 2, 3], 'abc']]); + assert.deepEqual(dcsCustom, [['B', [1, 2, 3], 'abc']]); }); it('Allow fallback', () => { const dcsCustom: [string, (number | number[])[], string][] = []; - parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => dcsCustom.push(['A', params.toArray(), data]))); - parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return false; })); + parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['A', params.toArray(), data]); return true; })); + parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return false; })); parse(parser2, DCS_INPUT); - chai.expect(dcsCustom).eql([['B', [1, 2, 3], 'abc'], ['A', [1, 2, 3], 'abc']]); + assert.deepEqual(dcsCustom, [['B', [1, 2, 3], 'abc'], ['A', [1, 2, 3], 'abc']]); }); it('Multiple custom handlers fallback once', () => { const dcsCustom: [string, (number | number[])[], string][] = []; - parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => dcsCustom.push(['A', params.toArray(), data]))); - parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); - parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['C', params.toArray(), data]); return false; })); + parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['A', params.toArray(), data]); return true; })); + parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); + parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['C', params.toArray(), data]); return false; })); parse(parser2, DCS_INPUT); - chai.expect(dcsCustom).eql([['C', [1, 2, 3], 'abc'], ['B', [1, 2, 3], 'abc']]); + assert.deepEqual(dcsCustom, [['C', [1, 2, 3], 'abc'], ['B', [1, 2, 3], 'abc']]); }); it('Multiple custom handlers no fallback', () => { const dcsCustom: [string, (number | number[])[], string][] = []; - parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => dcsCustom.push(['A', params.toArray(), data]))); - parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); - parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['C', params.toArray(), data]); return true; })); + parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['A', params.toArray(), data]); return true; })); + parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); + parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['C', params.toArray(), data]); return true; })); parse(parser2, DCS_INPUT); - chai.expect(dcsCustom).eql([['C', [1, 2, 3], 'abc']]); + assert.deepEqual(dcsCustom, [['C', [1, 2, 3], 'abc']]); }); it('Execution order should go from latest handler down to the original', () => { const order: number[] = []; - parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler(() => order.push(1))); - parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler(() => { order.push(2); return false; })); - parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler(() => { order.push(3); return false; })); + parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler(() => { order.push(1); return true; })); + parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler(() => { order.push(2); return false; })); + parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler(() => { order.push(3); return false; })); parse(parser2, DCS_INPUT); - chai.expect(order).eql([3, 2, 1]); + assert.deepEqual(order, [3, 2, 1]); }); it('Dispose should work', () => { const dcsCustom: [string, (number | number[])[], string][] = []; - parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => dcsCustom.push(['A', params.toArray(), data]))); - const dispo = parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); + parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['A', params.toArray(), data]); return true; })); + const dispo = parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); dispo.dispose(); parse(parser2, DCS_INPUT); - chai.expect(dcsCustom).eql([['A', [1, 2, 3], 'abc']]); + assert.deepEqual(dcsCustom, [['A', [1, 2, 3], 'abc']]); }); it('Should not corrupt the parser when dispose is called twice', () => { const dcsCustom: [string, (number | number[])[], string][] = []; - parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => dcsCustom.push(['A', params.toArray(), data]))); - const dispo = parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); + parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['A', params.toArray(), data]); return true; })); + const dispo = parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; })); dispo.dispose(); dispo.dispose(); parse(parser2, DCS_INPUT); - chai.expect(dcsCustom).eql([['A', [1, 2, 3], 'abc']]); + assert.deepEqual(dcsCustom, [['A', [1, 2, 3], 'abc']]); }); }); - it('ERROR handler', function (): void { + it('ERROR handler', () => { let errorState: IParsingState | null = null; parser2.setErrorHandler(function (state: IParsingState): IParsingState { errorState = state; return state; }); parse(parser2, '\x1b[1;2;€;3m'); // faulty escape sequence - chai.expect(errorState).eql({ + assert.deepEqual(errorState, { position: 6, code: '€'.charCodeAt(0), currentState: ParserState.CSI_PARAM, @@ -1555,7 +1577,7 @@ describe('EscapeSequenceParser', function (): void { parser2.clearErrorHandler(); // should not throw errorState = null; parse(parser2, '\x1b[1;2;a;3m'); - chai.expect(errorState).eql(null); + assert.equal(errorState, null); }); }); describe('function identifiers', () => { @@ -1563,69 +1585,69 @@ describe('EscapeSequenceParser', function (): void { it('prefix range 0x3c .. 0x3f, one byte', () => { for (let i = 0x3c; i <= 0x3f; ++i) { const c = String.fromCharCode(i); - chai.expect(parser.identToString(parser.identifier({prefix: c, final: 'z'}))).eql(c + 'z'); + assert.equal(parser.identToString(parser.identifier({ prefix: c, final: 'z' })), c + 'z'); } - chai.assert.throws(() => { parser.identifier({prefix: '\x3b', final: 'z'}); }, 'prefix must be in range 0x3c .. 0x3f'); - chai.assert.throws(() => { parser.identifier({prefix: '\x40', final: 'z'}); }, 'prefix must be in range 0x3c .. 0x3f'); - chai.assert.throws(() => { parser.identifier({prefix: '??', final: 'z'}); }, 'only one byte as prefix supported'); + assert.throws(() => { parser.identifier({ prefix: '\x3b', final: 'z' }); }, 'prefix must be in range 0x3c .. 0x3f'); + assert.throws(() => { parser.identifier({ prefix: '\x40', final: 'z' }); }, 'prefix must be in range 0x3c .. 0x3f'); + assert.throws(() => { parser.identifier({ prefix: '??', final: 'z' }); }, 'only one byte as prefix supported'); }); it('intermediates range 0x20 .. 0x2f, up to two bytes', () => { for (let i = 0x20; i <= 0x2f; ++i) { const c = String.fromCharCode(i); - chai.expect(parser.identToString(parser.identifier({intermediates: c + c, final: 'z'}))).eql(c + c + 'z'); + assert.equal(parser.identToString(parser.identifier({ intermediates: c + c, final: 'z' })), c + c + 'z'); } - chai.assert.throws(() => { parser.identifier({intermediates: '\x1f', final: 'z'}); }, 'intermediate must be in range 0x20 .. 0x2f'); - chai.assert.throws(() => { parser.identifier({intermediates: '\x30', final: 'z'}); }, 'intermediate must be in range 0x20 .. 0x2f'); - chai.assert.throws(() => { parser.identifier({intermediates: '!!!', final: 'z'}); }, 'only two bytes as intermediates are supported'); + assert.throws(() => { parser.identifier({ intermediates: '\x1f', final: 'z' }); }, 'intermediate must be in range 0x20 .. 0x2f'); + assert.throws(() => { parser.identifier({ intermediates: '\x30', final: 'z' }); }, 'intermediate must be in range 0x20 .. 0x2f'); + assert.throws(() => { parser.identifier({ intermediates: '!!!', final: 'z' }); }, 'only two bytes as intermediates are supported'); }); it('final CSI/DCS range 0x40 .. 0x7e (default), one byte', () => { for (let i = 0x40; i <= 0x7e; ++i) { const c = String.fromCharCode(i); - chai.expect(parser.identToString(parser.identifier({final: c}))).eql(c); + assert.equal(parser.identToString(parser.identifier({ final: c })), c); } - chai.assert.throws(() => { parser.identifier({final: '\x3f'}); }, 'final must be in range 64 .. 126'); - chai.assert.throws(() => { parser.identifier({final: '\x7f'}); }, 'final must be in range 64 .. 126'); - chai.assert.throws(() => { parser.identifier({final: 'zz'}); }, 'final must be a single byte'); + assert.throws(() => { parser.identifier({ final: '\x3f' }); }, 'final must be in range 64 .. 126'); + assert.throws(() => { parser.identifier({ final: '\x7f' }); }, 'final must be in range 64 .. 126'); + assert.throws(() => { parser.identifier({ final: 'zz' }); }, 'final must be a single byte'); }); it('final ESC range 0x30 .. 0x7e, one byte', () => { for (let i = 0x30; i <= 0x7e; ++i) { const final = String.fromCharCode(i); let handler: IDisposable | undefined; - chai.assert.doesNotThrow(() => { handler = parser.addEscHandler({final}, () => {}); }, 'final must be in range 48 .. 126'); + assert.doesNotThrow(() => { handler = parser.registerEscHandler({ final }, () => true); }, 'final must be in range 48 .. 126'); if (handler) handler.dispose(); } - chai.assert.throws(() => { parser.addEscHandler({final: '\x2f'}, () => {}); }, 'final must be in range 48 .. 126'); - chai.assert.throws(() => { parser.addEscHandler({final: '\x7f'}, () => {}); }, 'final must be in range 48 .. 126'); + assert.throws(() => { parser.registerEscHandler({ final: '\x2f' }, () => true); }, 'final must be in range 48 .. 126'); + assert.throws(() => { parser.registerEscHandler({ final: '\x7f' }, () => true); }, 'final must be in range 48 .. 126'); }); it('id calculation - should stacking prefix -> intermediate -> final', () => { - chai.expect(parser.identToString(parser.identifier({final: 'z'}))).eql('z'); - chai.expect(parser.identToString(parser.identifier({prefix: '?', final: 'z'}))).eql('?z'); - chai.expect(parser.identToString(parser.identifier({intermediates: '!', final: 'z'}))).eql('!z'); - chai.expect(parser.identToString(parser.identifier({prefix: '?', intermediates: '!', final: 'z'}))).eql('?!z'); - chai.expect(parser.identToString(parser.identifier({prefix: '?', intermediates: '!!', final: 'z'}))).eql('?!!z'); + assert.equal(parser.identToString(parser.identifier({ final: 'z' })), 'z'); + assert.equal(parser.identToString(parser.identifier({ prefix: '?', final: 'z' })), '?z'); + assert.equal(parser.identToString(parser.identifier({ intermediates: '!', final: 'z' })), '!z'); + assert.equal(parser.identToString(parser.identifier({ prefix: '?', intermediates: '!', final: 'z' })), '?!z'); + assert.equal(parser.identToString(parser.identifier({ prefix: '?', intermediates: '!!', final: 'z' })), '?!!z'); }); }); describe('identifier invocation', () => { it('ESC', () => { const callstack: string[] = []; - const h1 = parser.addEscHandler({final: 'z'}, () => { callstack.push('z'); }); - const h2 = parser.addEscHandler({intermediates: '!', final: 'z'}, () => { callstack.push('!z'); }); - const h3 = parser.addEscHandler({intermediates: '!!', final: 'z'}, () => { callstack.push('!!z'); }); + const h1 = parser.registerEscHandler({ final: 'z' }, () => { callstack.push('z'); return true; }); + const h2 = parser.registerEscHandler({ intermediates: '!', final: 'z' }, () => { callstack.push('!z'); return true; }); + const h3 = parser.registerEscHandler({ intermediates: '!!', final: 'z' }, () => { callstack.push('!!z'); return true; }); parse(parser, '\x1bz\x1b!z\x1b!!z'); h1.dispose(); h2.dispose(); h3.dispose(); parse(parser, '\x1bz\x1b!z\x1b!!z'); - chai.expect(callstack).eql(['z', '!z', '!!z']); + assert.deepEqual(callstack, ['z', '!z', '!!z']); }); it('CSI', () => { const callstack: any[] = []; - const h1 = parser.addCsiHandler({final: 'z'}, params => { callstack.push(['z', params.toArray()]); }); - const h2 = parser.addCsiHandler({intermediates: '!', final: 'z'}, params => { callstack.push(['!z', params.toArray()]); }); - const h3 = parser.addCsiHandler({intermediates: '!!', final: 'z'}, params => { callstack.push(['!!z', params.toArray()]); }); - const h4 = parser.addCsiHandler({prefix: '?', final: 'z'}, params => { callstack.push(['?z', params.toArray()]); }); - const h5 = parser.addCsiHandler({prefix: '?', intermediates: '!', final: 'z'}, params => { callstack.push(['?!z', params.toArray()]); }); - const h6 = parser.addCsiHandler({prefix: '?', intermediates: '!!', final: 'z'}, params => { callstack.push(['?!!z', params.toArray()]); }); + const h1 = parser.registerCsiHandler({ final: 'z' }, params => { callstack.push(['z', params.toArray()]); return true; }); + const h2 = parser.registerCsiHandler({ intermediates: '!', final: 'z' }, params => { callstack.push(['!z', params.toArray()]); return true; }); + const h3 = parser.registerCsiHandler({ intermediates: '!!', final: 'z' }, params => { callstack.push(['!!z', params.toArray()]); return true; }); + const h4 = parser.registerCsiHandler({ prefix: '?', final: 'z' }, params => { callstack.push(['?z', params.toArray()]); return true; }); + const h5 = parser.registerCsiHandler({ prefix: '?', intermediates: '!', final: 'z' }, params => { callstack.push(['?!z', params.toArray()]); return true; }); + const h6 = parser.registerCsiHandler({ prefix: '?', intermediates: '!!', final: 'z' }, params => { callstack.push(['?!!z', params.toArray()]); return true; }); parse(parser, '\x1b[1;z\x1b[1;!z\x1b[1;!!z\x1b[?1;z\x1b[?1;!z\x1b[?1;!!z'); h1.dispose(); h2.dispose(); @@ -1634,16 +1656,19 @@ describe('EscapeSequenceParser', function (): void { h5.dispose(); h6.dispose(); parse(parser, '\x1b[1;z\x1b[1;!z\x1b[1;!!z\x1b[?1;z\x1b[?1;!z\x1b[?1;!!z'); - chai.expect(callstack).eql([['z', [1, 0]], ['!z', [1, 0]], ['!!z', [1, 0]], ['?z', [1, 0]], ['?!z', [1, 0]], ['?!!z', [1, 0]]]); + assert.deepEqual( + callstack, + [['z', [1, 0]], ['!z', [1, 0]], ['!!z', [1, 0]], ['?z', [1, 0]], ['?!z', [1, 0]], ['?!!z', [1, 0]]] + ); }); it('DCS', () => { const callstack: any[] = []; - const h1 = parser.addDcsHandler({final: 'z'}, new DcsHandler((data, params) => { callstack.push(['z', params.toArray(), data]); })); - const h2 = parser.addDcsHandler({intermediates: '!', final: 'z'}, new DcsHandler((data, params) => { callstack.push(['!z', params.toArray(), data]); })); - const h3 = parser.addDcsHandler({intermediates: '!!', final: 'z'}, new DcsHandler((data, params) => { callstack.push(['!!z', params.toArray(), data]); })); - const h4 = parser.addDcsHandler({prefix: '?', final: 'z'}, new DcsHandler((data, params) => { callstack.push(['?z', params.toArray(), data]); })); - const h5 = parser.addDcsHandler({prefix: '?', intermediates: '!', final: 'z'}, new DcsHandler((data, params) => { callstack.push(['?!z', params.toArray(), data]); })); - const h6 = parser.addDcsHandler({prefix: '?', intermediates: '!!', final: 'z'}, new DcsHandler((data, params) => { callstack.push(['?!!z', params.toArray(), data]); })); + const h1 = parser.registerDcsHandler({ final: 'z' }, new DcsHandler((data, params) => { callstack.push(['z', params.toArray(), data]); return true; })); + const h2 = parser.registerDcsHandler({ intermediates: '!', final: 'z' }, new DcsHandler((data, params) => { callstack.push(['!z', params.toArray(), data]); return true; })); + const h3 = parser.registerDcsHandler({ intermediates: '!!', final: 'z' }, new DcsHandler((data, params) => { callstack.push(['!!z', params.toArray(), data]); return true; })); + const h4 = parser.registerDcsHandler({ prefix: '?', final: 'z' }, new DcsHandler((data, params) => { callstack.push(['?z', params.toArray(), data]); return true; })); + const h5 = parser.registerDcsHandler({ prefix: '?', intermediates: '!', final: 'z' }, new DcsHandler((data, params) => { callstack.push(['?!z', params.toArray(), data]); return true; })); + const h6 = parser.registerDcsHandler({ prefix: '?', intermediates: '!!', final: 'z' }, new DcsHandler((data, params) => { callstack.push(['?!!z', params.toArray(), data]); return true; })); parse(parser, '\x1bP1;zAB\x1b\\\x1bP1;!zAB\x1b\\\x1bP1;!!zAB\x1b\\\x1bP?1;zAB\x1b\\\x1bP?1;!zAB\x1b\\\x1bP?1;!!zAB\x1b\\'); h1.dispose(); h2.dispose(); @@ -1652,16 +1677,544 @@ describe('EscapeSequenceParser', function (): void { h5.dispose(); h6.dispose(); parse(parser, '\x1bP1;zAB\x1b\\\x1bP1;!zAB\x1b\\\x1bP1;!!zAB\x1b\\\x1bP?1;zAB\x1b\\\x1bP?1;!zAB\x1b\\\x1bP?1;!!zAB\x1b\\'); - chai.expect(callstack).eql([ - ['z', [1, 0], 'AB'], - ['!z', [1, 0], 'AB'], - ['!!z', [1, 0], 'AB'], - ['?z', [1, 0], 'AB'], - ['?!z', [1, 0], 'AB'], - ['?!!z', [1, 0], 'AB'] - ]); + assert.deepEqual( + callstack, + [ + ['z', [1, 0], 'AB'], + ['!z', [1, 0], 'AB'], + ['!!z', [1, 0], 'AB'], + ['?z', [1, 0], 'AB'], + ['?!z', [1, 0], 'AB'], + ['?!!z', [1, 0], 'AB'] + ] + ); }); }); }); // TODO: error conditions and error recovery (not implemented yet in parser) }); + + +/** + * async handler tests. + */ + +function parseSync(parser: TestEscapeSequenceParser, data: string): void | Promise { + const container = new Uint32Array(data.length); + const decoder = new StringToUtf32(); + return parser.parse(container, decoder.decode(data, container)); +} +async function parseP(parser: TestEscapeSequenceParser, data: string): Promise { + const container = new Uint32Array(data.length); + const decoder = new StringToUtf32(); + const len = decoder.decode(data, container); + let result: void | Promise; + let prev: boolean | undefined; + while (result = parser.parse(container, len, prev)) { + prev = await result; + } +} +function evalStackSaves(stackSaves: IParserStackState[], data: [number, ParserStackType, number][]): void { + assert.equal(stackSaves.length, data.length); + for (let i = 0; i < data.length; ++i) { + assert.equal(stackSaves[i].chunkPos, data[i][0]); + assert.equal(stackSaves[i].state, data[i][1]); + assert.equal(stackSaves[i].handlerPos, data[i][2]); + } +} +// helper similiar to assert.throws for async functions +async function throwsAsync(fn: () => Promise, message?: string | undefined): Promise { + let msg: string | undefined; + try { + await fn(); + } catch (e) { + if (e instanceof Error) { + msg = e.message; + } else if (typeof e === 'string') { + msg = e; + } + if (typeof message === 'string') { + assert.equal(msg, message); + } + return; + } + assert.throws(fn, message); +} + +describe('EscapeSequenceParser - async', () => { + // sequences: SGR 1;31 | hello SP | ESC %G | wor | ESC E | ld! | SGR 0 | EXE \r\n | $> | DCS 1;2 a [xyz] ST | OSC 1;foo=bar ST | FIN + // needed handlers: CSI m, PRINT, ESC %G, ESC E, EXE \r, EXE \n, OSC 1 + const INPUT = '\x1b[1;31mhello \x1b%Gwor\x1bEld!\x1b[0m\r\n$>\x1bP1;2axyz\x1b\\\x1b]1;foo=bar\x1b\\FIN'; + let RESULT: any[]; + let parser: TestEscapeSequenceParser; + const callstack: any[] = []; + function clearAccu(): void { + callstack.length = 0; + parser.trackedStack.length = 0; + } + beforeEach(() => { + RESULT = [ + ['SGR', [1, 31]], + ['PRINT', 'hello '], + ['ESC %G'], + ['PRINT', 'wor'], + ['ESC E'], + ['PRINT', 'ld!'], + ['SGR', [0]], + ['EXE \r'], + ['EXE \n'], + ['PRINT', '$>'], + ['DCS a', ['xyz', [1, 2]]], + ['OSC 1', 'foo=bar'], + ['PRINT', 'FIN'] + ]; + parser = new TestEscapeSequenceParser(); + parser.reset(); + parser.trackStackSavesOnPause(); + clearAccu(); + }); + describe('sync handlers should behave as before', () => { + beforeEach(() => { + parser.setPrintHandler((data, start, end) => { + let result = ''; + for (let i = start; i < end; ++i) { + result += stringFromCodePoint(data[i]); + } + callstack.push(['PRINT', result]); + }); + parser.registerCsiHandler({ final: 'm' }, params => { callstack.push(['SGR', params.toArray()]); return true; }); + parser.registerEscHandler({ intermediates: '%', final: 'G' }, () => { callstack.push(['ESC %G']); return true; }); + parser.registerEscHandler({ final: 'E' }, () => { callstack.push(['ESC E']); return true; }); + parser.setExecuteHandler('\r', () => { callstack.push(['EXE \r']); return true; }); + parser.setExecuteHandler('\n', () => { callstack.push(['EXE \n']); return true; }); + parser.registerOscHandler(1, new OscHandler(data => { callstack.push(['OSC 1', data]); return true; })); + parser.registerDcsHandler({final: 'a'}, new DcsHandler((data, params) => { callstack.push(['DCS a', [data, params.toArray()]]); return true;})); + }); + + it('sync handlers keep being parsed in sync mode', () => { + // note: if we have only sync handlers, a parse call should never return anything + assert.equal(!parseSync(parser, INPUT), true); + assert.equal(parser.parseStack.state, ParserStackType.NONE); // not paused + assert.equal(parser.trackedStack.length, 0); // never got paused + }); + it('correct result on sync parse call', () => { + parseSync(parser, INPUT); + assert.deepEqual(callstack, RESULT); + assert.equal(parser.trackedStack.length, 0); + }); + it('correct result on async parse call', async () => { + await parseP(parser, INPUT); + assert.deepEqual(callstack, RESULT); + assert.equal(parser.trackedStack.length, 0); + }); + }); + describe('async handlers', () => { + beforeEach(() => { + parser.setPrintHandler((data, start, end) => { + let result = ''; + for (let i = start; i < end; ++i) { + result += stringFromCodePoint(data[i]); + } + callstack.push(['PRINT', result]); + }); + parser.registerCsiHandler({ final: 'm' }, async params => { callstack.push(['SGR', params.toArray()]); return true; }); + parser.registerEscHandler({ intermediates: '%', final: 'G' }, async () => { callstack.push(['ESC %G']); return true; }); + parser.registerEscHandler({ final: 'E' }, async () => { callstack.push(['ESC E']); return true; }); + parser.setExecuteHandler('\r', () => { callstack.push(['EXE \r']); return true; }); + parser.setExecuteHandler('\n', () => { callstack.push(['EXE \n']); return true; }); + parser.registerOscHandler(1, new OscHandler(async data => { callstack.push(['OSC 1', data]); return true; })); + parser.registerDcsHandler({final: 'a'}, new DcsHandler(async (data, params) => { callstack.push(['DCS a', [data, params.toArray()]]); return true;})); + }); + + it('sync parse call does not work anymore', () => { + assert.notEqual(!parseSync(parser, INPUT), true); + assert.notDeepEqual(callstack, RESULT); + // due to sync calling we should save exactly one saved stack + // proper continuation is not possible anymore, as we lost the promise resolve value + assert.equal(parser.trackedStack.length, 1); + }); + it('improper continuation should throw', async () => { + /** + * Explanation: + * The first sync call will stop at the first promise returned, + * but does not await its resolve value. + * The second sync call to parse will fail due to missing `promiseResult`, + * which is needed for correct continuation. + */ + assert.notEqual(!parseSync(parser, INPUT), true); + assert.notDeepEqual(callstack, RESULT); + assert.throws(() => parseSync(parser, INPUT), 'improper continuation due to previous async handler, giving up parsing'); + // keeps being broken for further parse calls (sync and async) + assert.throws(() => parseSync(parser, 'random'), 'improper continuation due to previous async handler, giving up parsing'); + await throwsAsync(() => parseP(parser, 'foobar'), 'improper continuation due to previous async handler, giving up parsing'); + // reset should lift the error condition + parser.reset(); + await parseP(parser, INPUT); // does not throw anymore + }); + it('correct result on awaited parse call', async () => { + await parseP(parser, INPUT); + assert.deepEqual(callstack, RESULT); + evalStackSaves(parser.trackedStack, [ + [6, ParserStackType.CSI, 0], + [15, ParserStackType.ESC, 0], + [20, ParserStackType.ESC, 0], + [27, ParserStackType.CSI, 0], + [41, ParserStackType.DCS, 0], + [54, ParserStackType.OSC, 0] + ]); + }); + it('correct result on chunked awaited parse calls', async () => { + RESULT = [ + ['SGR', [1, 31]], + ['PRINT', 'h'], // due to single char input PRINT is split + ['PRINT', 'e'], + ['PRINT', 'l'], + ['PRINT', 'l'], + ['PRINT', 'o'], + ['PRINT', ' '], + ['ESC %G'], + ['PRINT', 'w'], + ['PRINT', 'o'], + ['PRINT', 'r'], + ['ESC E'], + ['PRINT', 'l'], + ['PRINT', 'd'], + ['PRINT', '!'], + ['SGR', [0]], + ['EXE \r'], + ['EXE \n'], + ['PRINT', '$'], + ['PRINT', '>'], + ['DCS a', ['xyz', [1, 2]]], + ['OSC 1', 'foo=bar'], + ['PRINT', 'F'], + ['PRINT', 'I'], + ['PRINT', 'N'] + ]; + + // split to single char input + for (let i = 0; i < INPUT.length; ++i) { + // Note: a single fully awaited parse call always ends in sync mode, + // which re-enables faster sync processing in the higher up callstack + await parseP(parser, INPUT[i]); + } + assert.deepEqual(callstack, RESULT); + evalStackSaves(parser.trackedStack, [ + [0, ParserStackType.CSI, 0], + [0, ParserStackType.ESC, 0], + [0, ParserStackType.ESC, 0], + [0, ParserStackType.CSI, 0], + [0, ParserStackType.DCS, 0], + [0, ParserStackType.OSC, 0] + ]); + }); + it('multiple async SGR handlers', async () => { + // register with fallback + const SGR2 = parser.registerCsiHandler({ final: 'm' }, async params => { callstack.push(['2# SGR', params.toArray()]); return false; }); + await parseP(parser, INPUT); + // should contain [2# SGR, SGR] call pairs + for (let i = 0; i < callstack.length; ++i) { + const entry = callstack[i]; + if (entry[0] === '2# SGR') assert.equal(callstack[i + 1][0], 'SGR', 'Should fallback to original handler'); + } + evalStackSaves(parser.trackedStack, [ + [6, ParserStackType.CSI, 1], + [6, ParserStackType.CSI, 0], + [15, ParserStackType.ESC, 0], + [20, ParserStackType.ESC, 0], + [27, ParserStackType.CSI, 1], + [27, ParserStackType.CSI, 0], + [41, ParserStackType.DCS, 0], + [54, ParserStackType.OSC, 0] + ]); + clearAccu(); + // after dispose we should be back to RESULT + SGR2.dispose(); + await parseP(parser, INPUT); + assert.deepEqual(callstack, RESULT, 'Should not call custom handler'); + evalStackSaves(parser.trackedStack, [ + [6, ParserStackType.CSI, 0], + [15, ParserStackType.ESC, 0], + [20, ParserStackType.ESC, 0], + [27, ParserStackType.CSI, 0], + [41, ParserStackType.DCS, 0], + [54, ParserStackType.OSC, 0] + ]); + clearAccu(); + + // register without fallback + const SGR22 = parser.registerCsiHandler({ final: 'm' }, async params => { callstack.push(['2# SGR', params.toArray()]); return true; }); + await parseP(parser, INPUT); + // should only contain 2# SGR + for (let i = 0; i < callstack.length; ++i) { + const entry = callstack[i]; + if (entry[0] === '2# SGR') assert.notEqual(callstack[i + 1][0], 'SGR', 'Should not fallback to original handler'); + } + evalStackSaves(parser.trackedStack, [ + [6, ParserStackType.CSI, 1], + [15, ParserStackType.ESC, 0], + [20, ParserStackType.ESC, 0], + [27, ParserStackType.CSI, 1], + [41, ParserStackType.DCS, 0], + [54, ParserStackType.OSC, 0] + ]); + clearAccu(); + // after dispose we should be back to RESULT + SGR22.dispose(); + await parseP(parser, INPUT); + assert.deepEqual(callstack, RESULT, 'Should not call custom handler'); + evalStackSaves(parser.trackedStack, [ + [6, ParserStackType.CSI, 0], + [15, ParserStackType.ESC, 0], + [20, ParserStackType.ESC, 0], + [27, ParserStackType.CSI, 0], + [41, ParserStackType.DCS, 0], + [54, ParserStackType.OSC, 0] + ]); + }); + it('multiple async ESC handlers', async () => { + // register with fallback + const ESC2 = parser.registerEscHandler({ final: 'E' }, async () => { callstack.push(['2# ESC E']); return false; }); + await parseP(parser, INPUT); + for (let i = 0; i < callstack.length; ++i) { + const entry = callstack[i]; + if (entry[0] === '2# ESC E') assert.equal(callstack[i + 1][0], 'ESC E', 'Should fallback to original handler'); + } + evalStackSaves(parser.trackedStack, [ + [6, ParserStackType.CSI, 0], + [15, ParserStackType.ESC, 0], + [20, ParserStackType.ESC, 1], + [20, ParserStackType.ESC, 0], + [27, ParserStackType.CSI, 0], + [41, ParserStackType.DCS, 0], + [54, ParserStackType.OSC, 0] + ]); + clearAccu(); + // after dispose we should be back to RESULT + ESC2.dispose(); + await parseP(parser, INPUT); + assert.deepEqual(callstack, RESULT, 'Should not call custom handler'); + evalStackSaves(parser.trackedStack, [ + [6, ParserStackType.CSI, 0], + [15, ParserStackType.ESC, 0], + [20, ParserStackType.ESC, 0], + [27, ParserStackType.CSI, 0], + [41, ParserStackType.DCS, 0], + [54, ParserStackType.OSC, 0] + ]); + clearAccu(); + + // register without fallback + const ESC22 = parser.registerEscHandler({ final: 'E' }, async () => { callstack.push(['2# ESC E']); return true; }); + await parseP(parser, INPUT); + for (let i = 0; i < callstack.length; ++i) { + const entry = callstack[i]; + if (entry[0] === '2# ESC E') assert.notEqual(callstack[i + 1][0], 'ESC E', 'Should not fallback to original handler'); + } + evalStackSaves(parser.trackedStack, [ + [6, ParserStackType.CSI, 0], + [15, ParserStackType.ESC, 0], + [20, ParserStackType.ESC, 1], + [27, ParserStackType.CSI, 0], + [41, ParserStackType.DCS, 0], + [54, ParserStackType.OSC, 0] + ]); + clearAccu(); + // after dispose we should be back to RESULT + ESC22.dispose(); + await parseP(parser, INPUT); + assert.deepEqual(callstack, RESULT, 'Should not call custom handler'); + evalStackSaves(parser.trackedStack, [ + [6, ParserStackType.CSI, 0], + [15, ParserStackType.ESC, 0], + [20, ParserStackType.ESC, 0], + [27, ParserStackType.CSI, 0], + [41, ParserStackType.DCS, 0], + [54, ParserStackType.OSC, 0] + ]); + }); + it('sync/async SGR mixed', async () => { + // sync with fallback + const SGR2 = parser.registerCsiHandler({ final: 'm' }, params => { callstack.push(['2# SGR', params.toArray()]); return false; }); + // async with fallback + const SGR3 = parser.registerCsiHandler({ final: 'm' }, async params => { callstack.push(['3# SGR', params.toArray()]); return false; }); + await parseP(parser, INPUT); + // should contain [3# SGR, 2# SGR, SGR] call triples + for (let i = 0; i < callstack.length; ++i) { + const entry = callstack[i]; + if (entry[0] === '3# SGR') { + assert.equal(callstack[i + 1][0], '2# SGR', 'Should fallback to next handler'); + assert.equal(callstack[i + 2][0], 'SGR', 'Should fallback to original handler'); + } + } + evalStackSaves(parser.trackedStack, [ + [6, ParserStackType.CSI, 2], + [6, ParserStackType.CSI, 0], + [15, ParserStackType.ESC, 0], + [20, ParserStackType.ESC, 0], + [27, ParserStackType.CSI, 2], + [27, ParserStackType.CSI, 0], + [41, ParserStackType.DCS, 0], + [54, ParserStackType.OSC, 0] + ]); + clearAccu(); + // dispose SGR2 (sync one) + SGR2.dispose(); + await parseP(parser, INPUT); + // should contain [3# SGR, SGR] call pairs + for (let i = 0; i < callstack.length; ++i) { + const entry = callstack[i]; + if (entry[0] === '3# SGR') { + assert.equal(callstack[i + 1][0], 'SGR', 'Should fallback to original handler'); + } + } + evalStackSaves(parser.trackedStack, [ + [6, ParserStackType.CSI, 1], + [6, ParserStackType.CSI, 0], + [15, ParserStackType.ESC, 0], + [20, ParserStackType.ESC, 0], + [27, ParserStackType.CSI, 1], + [27, ParserStackType.CSI, 0], + [41, ParserStackType.DCS, 0], + [54, ParserStackType.OSC, 0] + ]); + clearAccu(); + // dispose SGR3 (async one) + SGR3.dispose(); + await parseP(parser, INPUT); + assert.deepEqual(callstack, RESULT, 'Should not call custom handler'); + evalStackSaves(parser.trackedStack, [ + [6, ParserStackType.CSI, 0], + [15, ParserStackType.ESC, 0], + [20, ParserStackType.ESC, 0], + [27, ParserStackType.CSI, 0], + [41, ParserStackType.DCS, 0], + [54, ParserStackType.OSC, 0] + ]); + }); + it('multiple async OSC handlers', async () => { + // register with fallback + const OSC2 = parser.registerOscHandler(1, new OscHandler(async data => { callstack.push(['2# OSC 1', data]); return false; })); + await parseP(parser, INPUT); + for (let i = 0; i < callstack.length; ++i) { + const entry = callstack[i]; + if (entry[0] === '2# OSC 1') assert.equal(callstack[i + 1][0], 'OSC 1', 'Should fallback to original handler'); + } + evalStackSaves(parser.trackedStack, [ + [6, ParserStackType.CSI, 0], + [15, ParserStackType.ESC, 0], + [20, ParserStackType.ESC, 0], + [27, ParserStackType.CSI, 0], + [41, ParserStackType.DCS, 0], + [54, ParserStackType.OSC, 0], + [54, ParserStackType.OSC, 0] + ]); + clearAccu(); + // after dispose we should be back to RESULT + OSC2.dispose(); + await parseP(parser, INPUT); + assert.deepEqual(callstack, RESULT, 'Should not call custom handler'); + evalStackSaves(parser.trackedStack, [ + [6, ParserStackType.CSI, 0], + [15, ParserStackType.ESC, 0], + [20, ParserStackType.ESC, 0], + [27, ParserStackType.CSI, 0], + [41, ParserStackType.DCS, 0], + [54, ParserStackType.OSC, 0] + ]); + clearAccu(); + + // register without fallback + const OSC22 = parser.registerOscHandler(1, new OscHandler(async data => { callstack.push(['2# OSC 1', data]); return true; })); + await parseP(parser, INPUT); + for (let i = 0; i < callstack.length; ++i) { + const entry = callstack[i]; + if (entry[0] === '2# OSC 1') assert.notEqual(callstack[i + 1][0], 'OSC 1', 'Should fallback to original handler'); + } + evalStackSaves(parser.trackedStack, [ + [6, ParserStackType.CSI, 0], + [15, ParserStackType.ESC, 0], + [20, ParserStackType.ESC, 0], + [27, ParserStackType.CSI, 0], + [41, ParserStackType.DCS, 0], + [54, ParserStackType.OSC, 0] + ]); + clearAccu(); + // after dispose we should be back to RESULT + OSC22.dispose(); + await parseP(parser, INPUT); + assert.deepEqual(callstack, RESULT, 'Should not call custom handler'); + evalStackSaves(parser.trackedStack, [ + [6, ParserStackType.CSI, 0], + [15, ParserStackType.ESC, 0], + [20, ParserStackType.ESC, 0], + [27, ParserStackType.CSI, 0], + [41, ParserStackType.DCS, 0], + [54, ParserStackType.OSC, 0] + ]); + clearAccu(); + }); + it('multiple async DCS handlers', async () => { + // register with fallback + const DCS2 = parser.registerDcsHandler({final: 'a'}, new DcsHandler(async (data, params) => { callstack.push(['#2 DCS a', [data, params.toArray()]]); return false;})); + await parseP(parser, INPUT); + for (let i = 0; i < callstack.length; ++i) { + const entry = callstack[i]; + if (entry[0] === '2# DCS a') assert.equal(callstack[i + 1][0], 'DCS a', 'Should fallback to original handler'); + } + evalStackSaves(parser.trackedStack, [ + [6, ParserStackType.CSI, 0], + [15, ParserStackType.ESC, 0], + [20, ParserStackType.ESC, 0], + [27, ParserStackType.CSI, 0], + [41, ParserStackType.DCS, 0], + [41, ParserStackType.DCS, 0], + [54, ParserStackType.OSC, 0] + ]); + clearAccu(); + // after dispose we should be back to RESULT + DCS2.dispose(); + await parseP(parser, INPUT); + assert.deepEqual(callstack, RESULT, 'Should not call custom handler'); + evalStackSaves(parser.trackedStack, [ + [6, ParserStackType.CSI, 0], + [15, ParserStackType.ESC, 0], + [20, ParserStackType.ESC, 0], + [27, ParserStackType.CSI, 0], + [41, ParserStackType.DCS, 0], + [54, ParserStackType.OSC, 0] + ]); + clearAccu(); + + // register without fallback + const DCS22 = parser.registerDcsHandler({final: 'a'}, new DcsHandler(async (data, params) => { callstack.push(['#2 DCS a', [data, params.toArray()]]); return true;})); + await parseP(parser, INPUT); + for (let i = 0; i < callstack.length; ++i) { + const entry = callstack[i]; + if (entry[0] === '2# DCS a') assert.notEqual(callstack[i + 1][0], 'DCS a', 'Should fallback to original handler'); + } + evalStackSaves(parser.trackedStack, [ + [6, ParserStackType.CSI, 0], + [15, ParserStackType.ESC, 0], + [20, ParserStackType.ESC, 0], + [27, ParserStackType.CSI, 0], + [41, ParserStackType.DCS, 0], + [54, ParserStackType.OSC, 0] + ]); + clearAccu(); + // after dispose we should be back to RESULT + DCS22.dispose(); + await parseP(parser, INPUT); + assert.deepEqual(callstack, RESULT, 'Should not call custom handler'); + evalStackSaves(parser.trackedStack, [ + [6, ParserStackType.CSI, 0], + [15, ParserStackType.ESC, 0], + [20, ParserStackType.ESC, 0], + [27, ParserStackType.CSI, 0], + [41, ParserStackType.DCS, 0], + [54, ParserStackType.OSC, 0] + ]); + clearAccu(); + }); + }); +}); diff --git a/src/common/parser/EscapeSequenceParser.ts b/src/common/parser/EscapeSequenceParser.ts index 96d206b9..f20a7e91 100644 --- a/src/common/parser/EscapeSequenceParser.ts +++ b/src/common/parser/EscapeSequenceParser.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IOscHandler, IHandlerCollection, CsiHandlerType, OscFallbackHandlerType, IOscParser, EscHandlerType, IDcsParser, DcsFallbackHandlerType, IFunctionIdentifier, ExecuteFallbackHandlerType, CsiFallbackHandlerType, EscFallbackHandlerType, PrintHandlerType, PrintFallbackHandlerType, ExecuteHandlerType } from 'common/parser/Types'; +import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IOscHandler, IHandlerCollection, CsiHandlerType, OscFallbackHandlerType, IOscParser, EscHandlerType, IDcsParser, DcsFallbackHandlerType, IFunctionIdentifier, ExecuteFallbackHandlerType, CsiFallbackHandlerType, EscFallbackHandlerType, PrintHandlerType, PrintFallbackHandlerType, ExecuteHandlerType, IParserStackState, ParserStackType, ResumableHandlersType } from 'common/parser/Types'; import { ParserState, ParserAction } from 'common/parser/Constants'; import { Disposable } from 'common/Lifecycle'; import { IDisposable } from 'common/Types'; @@ -239,7 +239,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP // handler lookup containers protected _printHandler: PrintHandlerType; - protected _executeHandlers: {[flag: number]: ExecuteHandlerType}; + protected _executeHandlers: { [flag: number]: ExecuteHandlerType }; protected _csiHandlers: IHandlerCollection; protected _escHandlers: IHandlerCollection; protected _oscParser: IOscParser; @@ -253,6 +253,15 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP protected _escHandlerFb: EscFallbackHandlerType; protected _errorHandlerFb: (state: IParsingState) => IParsingState; + // parser stack save for async handler support + protected _parseStack: IParserStackState = { + state: ParserStackType.NONE, + handlers: [], + handlerPos: 0, + transition: 0, + chunkPos: 0 + }; + constructor( protected readonly _transitions: TransitionTable = VT500_TRANSITION_TABLE ) { @@ -280,7 +289,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._errorHandler = this._errorHandlerFb; // swallow 7bit ST (ESC+\) - this.setEscHandler({final: '\\'}, () => {}); + this.registerEscHandler({ final: '\\' }, () => true); } protected _identifier(id: IFunctionIdentifier, finalRange: number[] = [0x40, 0x7e]): number { @@ -344,7 +353,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._printHandler = this._printHandlerFb; } - public addEscHandler(id: IFunctionIdentifier, handler: EscHandlerType): IDisposable { + public registerEscHandler(id: IFunctionIdentifier, handler: EscHandlerType): IDisposable { const ident = this._identifier(id, [0x30, 0x7e]); if (this._escHandlers[ident] === undefined) { this._escHandlers[ident] = []; @@ -360,9 +369,6 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP } }; } - public setEscHandler(id: IFunctionIdentifier, handler: EscHandlerType): void { - this._escHandlers[this._identifier(id, [0x30, 0x7e])] = [handler]; - } public clearEscHandler(id: IFunctionIdentifier): void { if (this._escHandlers[this._identifier(id, [0x30, 0x7e])]) delete this._escHandlers[this._identifier(id, [0x30, 0x7e])]; } @@ -380,7 +386,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._executeHandlerFb = handler; } - public addCsiHandler(id: IFunctionIdentifier, handler: CsiHandlerType): IDisposable { + public registerCsiHandler(id: IFunctionIdentifier, handler: CsiHandlerType): IDisposable { const ident = this._identifier(id); if (this._csiHandlers[ident] === undefined) { this._csiHandlers[ident] = []; @@ -396,9 +402,6 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP } }; } - public setCsiHandler(id: IFunctionIdentifier, handler: CsiHandlerType): void { - this._csiHandlers[this._identifier(id)] = [handler]; - } public clearCsiHandler(id: IFunctionIdentifier): void { if (this._csiHandlers[this._identifier(id)]) delete this._csiHandlers[this._identifier(id)]; } @@ -406,11 +409,8 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._csiHandlerFb = callback; } - public addDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): IDisposable { - return this._dcsParser.addHandler(this._identifier(id), handler); - } - public setDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): void { - this._dcsParser.setHandler(this._identifier(id), handler); + public registerDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): IDisposable { + return this._dcsParser.registerHandler(this._identifier(id), handler); } public clearDcsHandler(id: IFunctionIdentifier): void { this._dcsParser.clearHandler(this._identifier(id)); @@ -419,11 +419,8 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._dcsParser.setHandlerFallback(handler); } - public addOscHandler(ident: number, handler: IOscHandler): IDisposable { - return this._oscParser.addHandler(ident, handler); - } - public setOscHandler(ident: number, handler: IOscHandler): void { - this._oscParser.setHandler(ident, handler); + public registerOscHandler(ident: number, handler: IOscHandler): IDisposable { + return this._oscParser.registerHandler(ident, handler); } public clearOscHandler(ident: number): void { this._oscParser.clearHandler(ident); @@ -439,6 +436,15 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._errorHandler = this._errorHandlerFb; } + /** + * Reset parser to initial values. + * + * This can also be used to lift the improper continuation error condition + * when dealing with async handlers. Use this only as a last resort to silence + * that error when the terminal has no pending data to be processed. Note that + * the interrupted async handler might continue its work in the future messing + * up the terminal state even further. + */ public reset(): void { this.currentState = this.initialState; this._oscParser.reset(); @@ -447,9 +453,31 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._params.addParam(0); // ZDM this._collect = 0; this.precedingCodepoint = 0; + // abort pending continuation from async handler + // Here the RESET type indicates, that the next parse call will + // ignore any saved stack, instead continues sync with next codepoint from GROUND + if (this._parseStack.state !== ParserStackType.NONE) { + this._parseStack.state = ParserStackType.RESET; + this._parseStack.handlers = []; // also release handlers ref + } } - + /** + * Async parse support. + */ + protected _preserveStack( + state: ParserStackType, + handlers: ResumableHandlersType, + handlerPos: number, + transition: number, + chunkPos: number + ): void { + this._parseStack.state = state; + this._parseStack.handlers = handlers; + this._parseStack.handlerPos = handlerPos; + this._parseStack.transition = transition; + this._parseStack.chunkPos = chunkPos; + } /** * Parse UTF32 codepoints in `data` up to `length`. @@ -464,23 +492,141 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP * - DCS_PARAM:PARAM * - OSC_STRING:OSC_PUT * - DCS_PASSTHROUGH:DCS_PUT + * + * Note on asynchronous handler support: + * Any handler returning a promise will be treated as asynchronous. + * To keep the in-band blocking working for async handlers, `parse` pauses execution, + * creates a stack save and returns the promise to the caller. + * For proper continuation of the paused state it is important + * to await the promise resolving. On resolve the parse must be repeated + * with the same chunk of data and the resolved value in `promiseResult` + * until no promise is returned. + * + * Important: With only sync handlers defined, parsing is completely synchronous as well. + * As soon as an async handler is involved, synchronous parsing is not possible anymore. + * + * Boilerplate for proper parsing of multiple chunks with async handlers: + * + * ```typescript + * async function parseMultipleChunks(chunks: Uint32Array[]): Promise { + * for (const chunk of chunks) { + * let result: void | Promise; + * let prev: boolean | undefined; + * while (result = parser.parse(chunk, chunk.length, prev)) { + * prev = await result; + * } + * } + * // finished parsing all chunks... + * } + * ``` */ - public parse(data: Uint32Array, length: number): void { + public parse(data: Uint32Array, length: number, promiseResult?: boolean): void | Promise { let code = 0; let transition = 0; - let currentState = this.currentState; - const osc = this._oscParser; - const dcs = this._dcsParser; - let collect = this._collect; - const params = this._params; - const table: Uint8Array = this._transitions.table; + let start = 0; + let handlerResult: void | boolean | Promise; + + // resume from async handler + if (this._parseStack.state) { + // allow sync parser reset even in continuation mode + // Note: can be used to recover parser from improper continuation error below + if (this._parseStack.state === ParserStackType.RESET) { + this._parseStack.state = ParserStackType.NONE; + start = this._parseStack.chunkPos + 1; // continue with next codepoint in GROUND + } else { + if (promiseResult === undefined || this._parseStack.state === ParserStackType.FAIL) { + /** + * Reject further parsing on improper continuation after pausing. + * This is a really bad condition with screwed up execution order and prolly messed up + * terminal state, therefore we exit hard with an exception and reject any further parsing. + * + * Note: With `Terminal.write` usage this exception should never occur, as the top level + * calls are guaranteed to handle async conditions properly. If you ever encounter this + * exception in your terminal integration it indicates, that you injected data chunks to + * `InputHandler.parse` or `EscapeSequenceParser.parse` synchronously without waiting for + * continuation of a running async handler. + * + * It is possible to get rid of this error by calling `reset`. But dont rely on that, + * as the pending async handler still might mess up the terminal later. Instead fix the faulty + * async handling, so this error will not be thrown anymore. + */ + this._parseStack.state = ParserStackType.FAIL; + throw new Error('improper continuation due to previous async handler, giving up parsing'); + } + + // we have to resume the old handler loop if: + // - return value of the promise was `false` + // - handlers are not exhausted yet + const handlers = this._parseStack.handlers; + let handlerPos = this._parseStack.handlerPos - 1; + switch (this._parseStack.state) { + case ParserStackType.CSI: + if (promiseResult === false && handlerPos > -1) { + for (; handlerPos >= 0; handlerPos--) { + handlerResult = (handlers as CsiHandlerType[])[handlerPos](this._params); + if (handlerResult === true) { + break; + } else if (handlerResult instanceof Promise) { + this._parseStack.handlerPos = handlerPos; + return handlerResult; + } + } + } + this._parseStack.handlers = []; + break; + case ParserStackType.ESC: + if (promiseResult === false && handlerPos > -1) { + for (; handlerPos >= 0; handlerPos--) { + handlerResult = (handlers as EscHandlerType[])[handlerPos](); + if (handlerResult === true) { + break; + } else if (handlerResult instanceof Promise) { + this._parseStack.handlerPos = handlerPos; + return handlerResult; + } + } + } + this._parseStack.handlers = []; + break; + case ParserStackType.DCS: + code = data[this._parseStack.chunkPos]; + handlerResult = this._dcsParser.unhook(code !== 0x18 && code !== 0x1a, promiseResult); + if (handlerResult) { + return handlerResult; + } + if (code === 0x1b) this._parseStack.transition |= ParserState.ESCAPE; + this._params.reset(); + this._params.addParam(0); // ZDM + this._collect = 0; + break; + case ParserStackType.OSC: + code = data[this._parseStack.chunkPos]; + handlerResult = this._oscParser.end(code !== 0x18 && code !== 0x1a, promiseResult); + if (handlerResult) { + return handlerResult; + } + if (code === 0x1b) this._parseStack.transition |= ParserState.ESCAPE; + this._params.reset(); + this._params.addParam(0); // ZDM + this._collect = 0; + break; + } + // cleanup before continuing with the main sync loop + this._parseStack.state = ParserStackType.NONE; + start = this._parseStack.chunkPos + 1; + this.precedingCodepoint = 0; + this.currentState = this._parseStack.transition & TableAccess.TRANSITION_STATE_MASK; + } + } + + // continue with main sync loop // process input string - for (let i = 0; i < length; ++i) { + for (let i = start; i < length; ++i) { code = data[i]; // normal transition & action lookup - transition = table[currentState << TableAccess.INDEX_STATE_SHIFT | (code < 0xa0 ? code : NON_ASCII_PRINTABLE)]; + transition = this._transitions.table[this.currentState << TableAccess.INDEX_STATE_SHIFT | (code < 0xa0 ? code : NON_ASCII_PRINTABLE)]; switch (transition >> TableAccess.TRANSITION_ACTION_SHIFT) { case ParserAction.PRINT: // read ahead with loop unrolling @@ -520,9 +666,9 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP { position: i, code, - currentState, - collect, - params, + currentState: this.currentState, + collect: this._collect, + params: this._params, abort: false }); if (inject.abort) return; @@ -530,16 +676,21 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP break; case ParserAction.CSI_DISPATCH: // Trigger CSI Handler - const handlers = this._csiHandlers[collect << 8 | code]; + const handlers = this._csiHandlers[this._collect << 8 | code]; let j = handlers ? handlers.length - 1 : -1; for (; j >= 0; j--) { - // undefined or true means success and to stop bubbling - if (handlers[j](params) !== false) { + // true means success and to stop bubbling + // a promise indicates an async handler that needs to finish before progressing + handlerResult = handlers[j](this._params); + if (handlerResult === true) { break; + } else if (handlerResult instanceof Promise) { + this._preserveStack(ParserStackType.CSI, handlers, j, transition, i); + return handlerResult; } } if (j < 0) { - this._csiHandlerFb(collect << 8 | code, params); + this._csiHandlerFb(this._collect << 8 | code, this._params); } this.precedingCodepoint = 0; break; @@ -548,91 +699,98 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP do { switch (code) { case 0x3b: - params.addParam(0); // ZDM + this._params.addParam(0); // ZDM break; case 0x3a: - params.addSubParam(-1); + this._params.addSubParam(-1); break; default: // 0x30 - 0x39 - params.addDigit(code - 48); + this._params.addDigit(code - 48); } } while (++i < length && (code = data[i]) > 0x2f && code < 0x3c); i--; break; case ParserAction.COLLECT: - collect <<= 8; - collect |= code; + this._collect <<= 8; + this._collect |= code; break; case ParserAction.ESC_DISPATCH: - const handlersEsc = this._escHandlers[collect << 8 | code]; + const handlersEsc = this._escHandlers[this._collect << 8 | code]; let jj = handlersEsc ? handlersEsc.length - 1 : -1; for (; jj >= 0; jj--) { - // undefined or true means success and to stop bubbling - if (handlersEsc[jj]() !== false) { + // true means success and to stop bubbling + // a promise indicates an async handler that needs to finish before progressing + handlerResult = handlersEsc[jj](); + if (handlerResult === true) { break; + } else if (handlerResult instanceof Promise) { + this._preserveStack(ParserStackType.ESC, handlersEsc, jj, transition, i); + return handlerResult; } } if (jj < 0) { - this._escHandlerFb(collect << 8 | code); + this._escHandlerFb(this._collect << 8 | code); } this.precedingCodepoint = 0; break; case ParserAction.CLEAR: - params.reset(); - params.addParam(0); // ZDM - collect = 0; + this._params.reset(); + this._params.addParam(0); // ZDM + this._collect = 0; break; case ParserAction.DCS_HOOK: - dcs.hook(collect << 8 | code, params); + this._dcsParser.hook(this._collect << 8 | code, this._params); break; case ParserAction.DCS_PUT: // inner loop - exit DCS_PUT: 0x18, 0x1a, 0x1b, 0x7f, 0x80 - 0x9f // unhook triggered by: 0x1b, 0x9c (success) and 0x18, 0x1a (abort) for (let j = i + 1; ; ++j) { if (j >= length || (code = data[j]) === 0x18 || code === 0x1a || code === 0x1b || (code > 0x7f && code < NON_ASCII_PRINTABLE)) { - dcs.put(data, i, j); + this._dcsParser.put(data, i, j); i = j - 1; break; } } break; case ParserAction.DCS_UNHOOK: - dcs.unhook(code !== 0x18 && code !== 0x1a); + handlerResult = this._dcsParser.unhook(code !== 0x18 && code !== 0x1a); + if (handlerResult) { + this._preserveStack(ParserStackType.DCS, [], 0, transition, i); + return handlerResult; + } if (code === 0x1b) transition |= ParserState.ESCAPE; - params.reset(); - params.addParam(0); // ZDM - collect = 0; + this._params.reset(); + this._params.addParam(0); // ZDM + this._collect = 0; this.precedingCodepoint = 0; break; case ParserAction.OSC_START: - osc.start(); + this._oscParser.start(); break; case ParserAction.OSC_PUT: // inner loop: 0x20 (SP) included, 0x7F (DEL) included for (let j = i + 1; ; j++) { - if (j >= length || (code = data[j]) < 0x20 || (code > 0x7f && code <= 0x9f)) { - osc.put(data, i, j); + if (j >= length || (code = data[j]) < 0x20 || (code > 0x7f && code < NON_ASCII_PRINTABLE)) { + this._oscParser.put(data, i, j); i = j - 1; break; } } break; case ParserAction.OSC_END: - osc.end(code !== 0x18 && code !== 0x1a); + handlerResult = this._oscParser.end(code !== 0x18 && code !== 0x1a); + if (handlerResult) { + this._preserveStack(ParserStackType.OSC, [], 0, transition, i); + return handlerResult; + } if (code === 0x1b) transition |= ParserState.ESCAPE; - params.reset(); - params.addParam(0); // ZDM - collect = 0; + this._params.reset(); + this._params.addParam(0); // ZDM + this._collect = 0; this.precedingCodepoint = 0; break; } - currentState = transition & TableAccess.TRANSITION_STATE_MASK; + this.currentState = transition & TableAccess.TRANSITION_STATE_MASK; } - - // save collected intermediates - this._collect = collect; - - // save state - this.currentState = currentState; } } diff --git a/src/common/parser/OscParser.test.ts b/src/common/parser/OscParser.test.ts index 127bd83a..5c7f5777 100644 --- a/src/common/parser/OscParser.test.ts +++ b/src/common/parser/OscParser.test.ts @@ -23,11 +23,12 @@ class TestHandler implements IOscHandler { public put(data: Uint32Array, start: number, end: number): void { this.output.push([this.msg, this.id, 'PUT', utf32ToString(data, start, end)]); } - public end(success: boolean): void | boolean { + public end(success: boolean): boolean { this.output.push([this.msg, this.id, 'END', success]); if (this.returnFalse) { return false; } + return true; } } @@ -78,7 +79,7 @@ describe('OscParser', () => { }); describe('handler registration', () => { it('setOscHandler', () => { - parser.setHandler(1234, new TestHandler(1234, reports, 'th')); + parser.registerHandler(1234, new TestHandler(1234, reports, 'th')); parser.start(); let data = toUtf32('1234;Here comes'); parser.put(data, 0, data.length); @@ -94,7 +95,7 @@ describe('OscParser', () => { ]); }); it('clearOscHandler', () => { - parser.setHandler(1234, new TestHandler(1234, reports, 'th')); + parser.registerHandler(1234, new TestHandler(1234, reports, 'th')); parser.clearHandler(1234); parser.start(); let data = toUtf32('1234;Here comes'); @@ -111,8 +112,8 @@ describe('OscParser', () => { ]); }); it('addOscHandler', () => { - parser.setHandler(1234, new TestHandler(1234, reports, 'th1')); - parser.addHandler(1234, new TestHandler(1234, reports, 'th2')); + parser.registerHandler(1234, new TestHandler(1234, reports, 'th1')); + parser.registerHandler(1234, new TestHandler(1234, reports, 'th2')); parser.start(); let data = toUtf32('1234;Here comes'); parser.put(data, 0, data.length); @@ -131,8 +132,8 @@ describe('OscParser', () => { ]); }); it('addOscHandler with return false', () => { - parser.setHandler(1234, new TestHandler(1234, reports, 'th1')); - parser.addHandler(1234, new TestHandler(1234, reports, 'th2', true)); + parser.registerHandler(1234, new TestHandler(1234, reports, 'th1')); + parser.registerHandler(1234, new TestHandler(1234, reports, 'th2', true)); parser.start(); let data = toUtf32('1234;Here comes'); parser.put(data, 0, data.length); @@ -151,8 +152,8 @@ describe('OscParser', () => { ]); }); it('dispose handlers', () => { - parser.setHandler(1234, new TestHandler(1234, reports, 'th1')); - const dispo = parser.addHandler(1234, new TestHandler(1234, reports, 'th2', true)); + parser.registerHandler(1234, new TestHandler(1234, reports, 'th1')); + const dispo = parser.registerHandler(1234, new TestHandler(1234, reports, 'th2', true)); dispo.dispose(); parser.start(); let data = toUtf32('1234;Here comes'); @@ -170,7 +171,7 @@ describe('OscParser', () => { }); describe('OscHandlerFactory', () => { it('should be called once on end(true)', () => { - parser.setHandler(1234, new OscHandler(data => reports.push([1234, data]))); + parser.registerHandler(1234, new OscHandler(data => { reports.push([1234, data]); return true; })); parser.start(); let data = toUtf32('1234;Here comes'); parser.put(data, 0, data.length); @@ -180,7 +181,7 @@ describe('OscParser', () => { assert.deepEqual(reports, [[1234, 'Here comes the mouse!']]); }); it('should not be called on end(false)', () => { - parser.setHandler(1234, new OscHandler(data => reports.push([1234, data]))); + parser.registerHandler(1234, new OscHandler(data => { reports.push([1234, data]); return true; })); parser.start(); let data = toUtf32('1234;Here comes'); parser.put(data, 0, data.length); @@ -190,8 +191,8 @@ describe('OscParser', () => { assert.deepEqual(reports, []); }); it('should be disposable', () => { - parser.setHandler(1234, new OscHandler(data => reports.push(['one', data]))); - const dispo = parser.addHandler(1234, new OscHandler(data => reports.push(['two', data]))); + parser.registerHandler(1234, new OscHandler(data => { reports.push(['one', data]); return true; })); + const dispo = parser.registerHandler(1234, new OscHandler(data => { reports.push(['two', data]); return true; })); parser.start(); let data = toUtf32('1234;Here comes'); parser.put(data, 0, data.length); @@ -209,8 +210,8 @@ describe('OscParser', () => { assert.deepEqual(reports, [['two', 'Here comes the mouse!'], ['one', 'some other data']]); }); it('should respect return false', () => { - parser.setHandler(1234, new OscHandler(data => reports.push(['one', data]))); - parser.addHandler(1234, new OscHandler(data => { reports.push(['two', data]); return false; })); + parser.registerHandler(1234, new OscHandler(data => { reports.push(['one', data]); return true; })); + parser.registerHandler(1234, new OscHandler(data => { reports.push(['two', data]); return false; })); parser.start(); let data = toUtf32('1234;Here comes'); parser.put(data, 0, data.length); @@ -221,7 +222,7 @@ describe('OscParser', () => { }); it('should work up to payload limit', function(): void { this.timeout(10000); - parser.setHandler(1234, new OscHandler(data => reports.push([1234, data]))); + parser.registerHandler(1234, new OscHandler(data => { reports.push([1234, data]); return true; })); parser.start(); let data = toUtf32('1234;'); parser.put(data, 0, data.length); @@ -234,7 +235,7 @@ describe('OscParser', () => { }); it('should abort for payload limit +1', function(): void { this.timeout(10000); - parser.setHandler(1234, new OscHandler(data => reports.push([1234, data]))); + parser.registerHandler(1234, new OscHandler(data => { reports.push([1234, data]); return true; })); parser.start(); let data = toUtf32('1234;'); parser.put(data, 0, data.length); @@ -249,3 +250,204 @@ describe('OscParser', () => { }); }); }); + + +class TestHandlerAsync implements IOscHandler { + constructor(public id: number, public output: any[], public msg: string, public returnFalse: boolean = false) {} + public start(): void { + this.output.push([this.msg, this.id, 'START']); + } + public put(data: Uint32Array, start: number, end: number): void { + this.output.push([this.msg, this.id, 'PUT', utf32ToString(data, start, end)]); + } + public async end(success: boolean): Promise { + await new Promise(res => setTimeout(res, 20)); + this.output.push([this.msg, this.id, 'END', success]); + if (this.returnFalse) { + return false; + } + return true; + } +} +async function endP(parser: OscParser, success: boolean): Promise { + let result: void | Promise; + let prev: boolean | undefined; + while (result = parser.end(success, prev)) { + prev = await result; + } +} + +describe('OscParser - async tests', () => { + let parser: OscParser; + let reports: any[] = []; + beforeEach(() => { + reports = []; + parser = new OscParser(); + parser.setHandlerFallback((id, action, data) => { + reports.push([id, action, data]); + }); + }); + describe('sync and async mixed', () => { + describe('sync | async | sync', () => { + it('first should run, cleanup action for others', async () => { + parser.registerHandler(1234, new TestHandler(1234, reports, 's1')); + parser.registerHandler(1234, new TestHandlerAsync(1234, reports, 'a1')); + parser.registerHandler(1234, new TestHandler(1234, reports, 's2')); + parser.start(); + let data = toUtf32('1234;Here comes'); + parser.put(data, 0, data.length); + data = toUtf32('the mouse!'); + parser.put(data, 0, data.length); + await endP(parser, true); + assert.deepEqual(reports, [ + // messages from TestHandler + ['s2', 1234, 'START'], + ['a1', 1234, 'START'], + ['s1', 1234, 'START'], + ['s2', 1234, 'PUT', 'Here comes'], + ['a1', 1234, 'PUT', 'Here comes'], + ['s1', 1234, 'PUT', 'Here comes'], + ['s2', 1234, 'PUT', 'the mouse!'], + ['a1', 1234, 'PUT', 'the mouse!'], + ['s1', 1234, 'PUT', 'the mouse!'], + ['s2', 1234, 'END', true], + ['a1', 1234, 'END', false], + ['s1', 1234, 'END', false] + ]); + }); + it('all should run', async () => { + parser.registerHandler(1234, new TestHandler(1234, reports, 's1', true)); + parser.registerHandler(1234, new TestHandlerAsync(1234, reports, 'a1', true)); + parser.registerHandler(1234, new TestHandler(1234, reports, 's2', true)); + parser.start(); + let data = toUtf32('1234;Here comes'); + parser.put(data, 0, data.length); + data = toUtf32('the mouse!'); + parser.put(data, 0, data.length); + await endP(parser, true); + assert.deepEqual(reports, [ + // messages from TestHandler + ['s2', 1234, 'START'], + ['a1', 1234, 'START'], + ['s1', 1234, 'START'], + ['s2', 1234, 'PUT', 'Here comes'], + ['a1', 1234, 'PUT', 'Here comes'], + ['s1', 1234, 'PUT', 'Here comes'], + ['s2', 1234, 'PUT', 'the mouse!'], + ['a1', 1234, 'PUT', 'the mouse!'], + ['s1', 1234, 'PUT', 'the mouse!'], + ['s2', 1234, 'END', true], + ['a1', 1234, 'END', true], + ['s1', 1234, 'END', true] + ]); + }); + }); + describe('async | sync | async', () => { + it('first should run, cleanup action for others', async () => { + parser.registerHandler(1234, new TestHandlerAsync(1234, reports, 's1')); + parser.registerHandler(1234, new TestHandler(1234, reports, 'a1')); + parser.registerHandler(1234, new TestHandlerAsync(1234, reports, 's2')); + parser.start(); + let data = toUtf32('1234;Here comes'); + parser.put(data, 0, data.length); + data = toUtf32('the mouse!'); + parser.put(data, 0, data.length); + await endP(parser, true); + assert.deepEqual(reports, [ + // messages from TestHandler + ['s2', 1234, 'START'], + ['a1', 1234, 'START'], + ['s1', 1234, 'START'], + ['s2', 1234, 'PUT', 'Here comes'], + ['a1', 1234, 'PUT', 'Here comes'], + ['s1', 1234, 'PUT', 'Here comes'], + ['s2', 1234, 'PUT', 'the mouse!'], + ['a1', 1234, 'PUT', 'the mouse!'], + ['s1', 1234, 'PUT', 'the mouse!'], + ['s2', 1234, 'END', true], + ['a1', 1234, 'END', false], + ['s1', 1234, 'END', false] + ]); + }); + it('all should run', async () => { + parser.registerHandler(1234, new TestHandlerAsync(1234, reports, 's1', true)); + parser.registerHandler(1234, new TestHandler(1234, reports, 'a1', true)); + parser.registerHandler(1234, new TestHandlerAsync(1234, reports, 's2', true)); + parser.start(); + let data = toUtf32('1234;Here comes'); + parser.put(data, 0, data.length); + data = toUtf32('the mouse!'); + parser.put(data, 0, data.length); + await endP(parser, true); + assert.deepEqual(reports, [ + // messages from TestHandler + ['s2', 1234, 'START'], + ['a1', 1234, 'START'], + ['s1', 1234, 'START'], + ['s2', 1234, 'PUT', 'Here comes'], + ['a1', 1234, 'PUT', 'Here comes'], + ['s1', 1234, 'PUT', 'Here comes'], + ['s2', 1234, 'PUT', 'the mouse!'], + ['a1', 1234, 'PUT', 'the mouse!'], + ['s1', 1234, 'PUT', 'the mouse!'], + ['s2', 1234, 'END', true], + ['a1', 1234, 'END', true], + ['s1', 1234, 'END', true] + ]); + }); + }); + describe('OscHandlerFactory', () => { + it('should be called once on end(true)', async () => { + parser.registerHandler(1234, new OscHandler(async data => { reports.push([1234, data]); return true; })); + parser.start(); + let data = toUtf32('1234;Here comes'); + parser.put(data, 0, data.length); + data = toUtf32(' the mouse!'); + parser.put(data, 0, data.length); + parser.end(true); + await endP(parser, true); + assert.deepEqual(reports, [[1234, 'Here comes the mouse!']]); + }); + it('should not be called on end(false)', async () => { + parser.registerHandler(1234, new OscHandler(async data => { reports.push([1234, data]); return true; })); + parser.start(); + let data = toUtf32('1234;Here comes'); + parser.put(data, 0, data.length); + data = toUtf32(' the mouse!'); + parser.put(data, 0, data.length); + await endP(parser, false); + assert.deepEqual(reports, []); + }); + it('should be disposable', async () => { + parser.registerHandler(1234, new OscHandler(async data => { reports.push(['one', data]); return true; })); + const dispo = parser.registerHandler(1234, new OscHandler(async data => { reports.push(['two', data]); return true; })); + parser.start(); + let data = toUtf32('1234;Here comes'); + parser.put(data, 0, data.length); + data = toUtf32(' the mouse!'); + parser.put(data, 0, data.length); + await endP(parser, true); + assert.deepEqual(reports, [['two', 'Here comes the mouse!']]); + dispo.dispose(); + parser.start(); + data = toUtf32('1234;some other'); + parser.put(data, 0, data.length); + data = toUtf32(' data'); + parser.put(data, 0, data.length); + await endP(parser, true); + assert.deepEqual(reports, [['two', 'Here comes the mouse!'], ['one', 'some other data']]); + }); + it('should respect return false', async () => { + parser.registerHandler(1234, new OscHandler(async data => { reports.push(['one', data]); return true; })); + parser.registerHandler(1234, new OscHandler(async data => { reports.push(['two', data]); return false; })); + parser.start(); + let data = toUtf32('1234;Here comes'); + parser.put(data, 0, data.length); + data = toUtf32(' the mouse!'); + parser.put(data, 0, data.length); + await endP(parser, true); + assert.deepEqual(reports, [['two', 'Here comes the mouse!'], ['one', 'Here comes the mouse!']]); + }); + }); + }); +}); diff --git a/src/common/parser/OscParser.ts b/src/common/parser/OscParser.ts index e8c5a801..32710aed 100644 --- a/src/common/parser/OscParser.ts +++ b/src/common/parser/OscParser.ts @@ -3,19 +3,26 @@ * @license MIT */ -import { IOscHandler, IHandlerCollection, OscFallbackHandlerType, IOscParser } from 'common/parser/Types'; +import { IOscHandler, IHandlerCollection, OscFallbackHandlerType, IOscParser, ISubParserStackState } from 'common/parser/Types'; import { OscState, PAYLOAD_LIMIT } from 'common/parser/Constants'; import { utf32ToString } from 'common/input/TextDecoder'; import { IDisposable } from 'common/Types'; +const EMPTY_HANDLERS: IOscHandler[] = []; export class OscParser implements IOscParser { private _state = OscState.START; + private _active = EMPTY_HANDLERS; private _id = -1; private _handlers: IHandlerCollection = Object.create(null); private _handlerFb: OscFallbackHandlerType = () => { }; + private _stack: ISubParserStackState = { + paused: false, + loopPosition: 0, + fallThrough: false + }; - public addHandler(ident: number, handler: IOscHandler): IDisposable { + public registerHandler(ident: number, handler: IOscHandler): IDisposable { if (this._handlers[ident] === undefined) { this._handlers[ident] = []; } @@ -30,9 +37,6 @@ export class OscParser implements IOscParser { } }; } - public setHandler(ident: number, handler: IOscHandler): void { - this._handlers[ident] = [handler]; - } public clearHandler(ident: number): void { if (this._handlers[ident]) delete this._handlers[ident]; } @@ -42,58 +46,40 @@ export class OscParser implements IOscParser { public dispose(): void { this._handlers = Object.create(null); - this._handlerFb = () => {}; + this._handlerFb = () => { }; + this._active = EMPTY_HANDLERS; } public reset(): void { - // cleanup handlers if payload was already sent + // force cleanup handlers if payload was already sent if (this._state === OscState.PAYLOAD) { - this.end(false); + for (let j = this._stack.paused ? this._stack.loopPosition - 1 : this._active.length - 1; j >= 0; --j) { + this._active[j].end(false); + } } + this._stack.paused = false; + this._active = EMPTY_HANDLERS; this._id = -1; this._state = OscState.START; } private _start(): void { - const handlers = this._handlers[this._id]; - if (!handlers) { + this._active = this._handlers[this._id] || EMPTY_HANDLERS; + if (!this._active.length) { this._handlerFb(this._id, 'START'); } else { - for (let j = handlers.length - 1; j >= 0; j--) { - handlers[j].start(); + for (let j = this._active.length - 1; j >= 0; j--) { + this._active[j].start(); } } } private _put(data: Uint32Array, start: number, end: number): void { - const handlers = this._handlers[this._id]; - if (!handlers) { + if (!this._active.length) { this._handlerFb(this._id, 'PUT', utf32ToString(data, start, end)); } else { - for (let j = handlers.length - 1; j >= 0; j--) { - handlers[j].put(data, start, end); - } - } - } - - private _end(success: boolean): void { - // other than the old code we always have to call .end - // to keep the bubbling we use `success` to indicate - // whether a handler should execute - const handlers = this._handlers[this._id]; - if (!handlers) { - this._handlerFb(this._id, 'END', success); - } else { - let j = handlers.length - 1; - for (; j >= 0; j--) { - if (handlers[j].end(success) !== false) { - break; - } - } - j--; - // cleanup left over handlers - for (; j >= 0; j--) { - handlers[j].end(false); + for (let j = this._active.length - 1; j >= 0; j--) { + this._active[j].put(data, start, end); } } } @@ -101,7 +87,6 @@ export class OscParser implements IOscParser { public start(): void { // always reset leftover handlers this.reset(); - this._id = -1; this._state = OscState.ID; } @@ -144,7 +129,7 @@ export class OscParser implements IOscParser { * Whether the OSC got aborted or finished normally * is indicated by `success`. */ - public end(success: boolean): void { + public end(success: boolean, promiseResult: boolean = true): void | Promise { if (this._state === OscState.START) { return; } @@ -156,8 +141,49 @@ export class OscParser implements IOscParser { if (this._state === OscState.ID) { this._start(); } - this._end(success); + + if (!this._active.length) { + this._handlerFb(this._id, 'END', success); + } else { + let handlerResult: boolean | Promise = false; + let j = this._active.length - 1; + let fallThrough = false; + if (this._stack.paused) { + j = this._stack.loopPosition - 1; + handlerResult = promiseResult; + fallThrough = this._stack.fallThrough; + this._stack.paused = false; + } + if (!fallThrough && handlerResult === false) { + for (; j >= 0; j--) { + handlerResult = this._active[j].end(success); + if (handlerResult === true) { + break; + } else if (handlerResult instanceof Promise) { + this._stack.paused = true; + this._stack.loopPosition = j; + this._stack.fallThrough = false; + return handlerResult; + } + } + j--; + } + // cleanup left over handlers + // we always have to call .end for proper cleanup, + // here we use `success` to indicate whether a handler should execute + for (; j >= 0; j--) { + handlerResult = this._active[j].end(false); + if (handlerResult instanceof Promise) { + this._stack.paused = true; + this._stack.loopPosition = j; + this._stack.fallThrough = true; + return handlerResult; + } + } + } + } + this._active = EMPTY_HANDLERS; this._id = -1; this._state = OscState.START; } @@ -171,7 +197,7 @@ export class OscHandler implements IOscHandler { private _data = ''; private _hitLimit: boolean = false; - constructor(private _handler: (data: string) => any) {} + constructor(private _handler: (data: string) => boolean | Promise) { } public start(): void { this._data = ''; @@ -189,12 +215,21 @@ export class OscHandler implements IOscHandler { } } - public end(success: boolean): any { - let ret; + public end(success: boolean): boolean | Promise { + let ret: boolean | Promise = false; if (this._hitLimit) { ret = false; } else if (success) { ret = this._handler(this._data); + if (ret instanceof Promise) { + // need to hold data until `ret` got resolved + // dont care for errors, data will be freed anyway on next start + return ret.then(res => { + this._data = ''; + this._hitLimit = false; + return res; + }); + } } this._data = ''; this._hitLimit = false; diff --git a/src/common/parser/Types.d.ts b/src/common/parser/Types.d.ts index e2fac89f..3a621eab 100644 --- a/src/common/parser/Types.d.ts +++ b/src/common/parser/Types.d.ts @@ -6,6 +6,7 @@ import { IDisposable } from 'common/Types'; import { ParserState } from 'common/parser/Constants'; + /** sequence params serialized to js arrays */ export type ParamsArray = (number | number[])[]; @@ -69,7 +70,7 @@ export interface IParsingState { * CSI handler types. * Note: `params` is borrowed. */ -export type CsiHandlerType = (params: IParams) => boolean | void; +export type CsiHandlerType = (params: IParams) => boolean | Promise; export type CsiFallbackHandlerType = (ident: number, params: IParams) => void; /** @@ -93,20 +94,20 @@ export interface IDcsHandler { * execution of the command should depend on `success`. * To save memory also cleanup data structures here. */ - unhook(success: boolean): void | boolean; + unhook(success: boolean): boolean | Promise; } export type DcsFallbackHandlerType = (ident: number, action: 'HOOK' | 'PUT' | 'UNHOOK', payload?: any) => void; /** * ESC handler types. */ -export type EscHandlerType = () => boolean | void; +export type EscHandlerType = () => boolean | Promise; export type EscFallbackHandlerType = (identifier: number) => void; /** * EXECUTE handler types. */ -export type ExecuteHandlerType = () => boolean | void; +export type ExecuteHandlerType = () => boolean; export type ExecuteFallbackHandlerType = (ident: number) => void; /** @@ -129,7 +130,7 @@ export interface IOscHandler { * execution of the command should depend on `success`. * To save memory also cleanup data structures here. */ - end(success: boolean): void | boolean; + end(success: boolean): boolean | Promise; } export type OscFallbackHandlerType = (ident: number, action: 'START' | 'PUT' | 'END', payload?: any) => void; @@ -160,7 +161,7 @@ export interface IEscapeSequenceParser extends IDisposable { * Parse UTF32 codepoints in `data` up to `length`. * @param data The data to parse. */ - parse(data: Uint32Array, length: number): void; + parse(data: Uint32Array, length: number, promiseResult?: boolean): void | Promise; /** * Get string from numercial function identifier `ident`. @@ -174,29 +175,25 @@ export interface IEscapeSequenceParser extends IDisposable { setPrintHandler(handler: PrintHandlerType): void; clearPrintHandler(): void; - setEscHandler(id: IFunctionIdentifier, handler: EscHandlerType): void; + registerEscHandler(id: IFunctionIdentifier, handler: EscHandlerType): IDisposable; clearEscHandler(id: IFunctionIdentifier): void; setEscHandlerFallback(handler: EscFallbackHandlerType): void; - addEscHandler(id: IFunctionIdentifier, handler: EscHandlerType): IDisposable; setExecuteHandler(flag: string, handler: ExecuteHandlerType): void; clearExecuteHandler(flag: string): void; setExecuteHandlerFallback(handler: ExecuteFallbackHandlerType): void; - setCsiHandler(id: IFunctionIdentifier, handler: CsiHandlerType): void; + registerCsiHandler(id: IFunctionIdentifier, handler: CsiHandlerType): IDisposable; clearCsiHandler(id: IFunctionIdentifier): void; setCsiHandlerFallback(callback: CsiFallbackHandlerType): void; - addCsiHandler(id: IFunctionIdentifier, handler: CsiHandlerType): IDisposable; - setDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): void; + registerDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): IDisposable; clearDcsHandler(id: IFunctionIdentifier): void; setDcsHandlerFallback(handler: DcsFallbackHandlerType): void; - addDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): IDisposable; - setOscHandler(ident: number, handler: IOscHandler): void; + registerOscHandler(ident: number, handler: IOscHandler): IDisposable; clearOscHandler(ident: number): void; setOscHandlerFallback(handler: OscFallbackHandlerType): void; - addOscHandler(ident: number, handler: IOscHandler): IDisposable; setErrorHandler(handler: (state: IParsingState) => IParsingState): void; clearErrorHandler(): void; @@ -209,8 +206,7 @@ export interface IEscapeSequenceParser extends IDisposable { */ export interface ISubParser extends IDisposable { reset(): void; - addHandler(ident: number, handler: T): IDisposable; - setHandler(ident: number, handler: T): void; + registerHandler(ident: number, handler: T): IDisposable; clearHandler(ident: number): void; setHandlerFallback(handler: U): void; put(data: Uint32Array, start: number, end: number): void; @@ -218,12 +214,12 @@ export interface ISubParser extends IDisposable { export interface IOscParser extends ISubParser { start(): void; - end(success: boolean): void; + end(success: boolean, promiseResult?: boolean): void | Promise; } export interface IDcsParser extends ISubParser { hook(ident: number, params: IParams): void; - unhook(success: boolean): void; + unhook(success: boolean, promiseResult?: boolean): void | Promise; } /** @@ -242,3 +238,37 @@ export interface IFunctionIdentifier { export interface IHandlerCollection { [key: string]: T[]; } + +/** + * Types for async parser support. + */ + +// type of saved stack state in parser +export const enum ParserStackType { + NONE = 0, + FAIL, + RESET, + CSI, + ESC, + OSC, + DCS +} + +// aggregate of resumable handler lists +export type ResumableHandlersType = CsiHandlerType[] | EscHandlerType[]; + +// saved stack state of the parser +export interface IParserStackState { + state: ParserStackType; + handlers: ResumableHandlersType; + handlerPos: number; + transition: number; + chunkPos: number; +} + +// saved stack state of subparser (OSC and DCS) +export interface ISubParserStackState { + paused: boolean; + loopPosition: number; + fallThrough: boolean; +} diff --git a/src/browser/public/AddonManager.test.ts b/src/common/public/AddonManager.test.ts similarity index 100% rename from src/browser/public/AddonManager.test.ts rename to src/common/public/AddonManager.test.ts diff --git a/src/browser/public/AddonManager.ts b/src/common/public/AddonManager.ts similarity index 97% rename from src/browser/public/AddonManager.ts rename to src/common/public/AddonManager.ts index 0261fd68..06c78121 100644 --- a/src/browser/public/AddonManager.ts +++ b/src/common/public/AddonManager.ts @@ -31,7 +31,7 @@ export class AddonManager implements IDisposable { }; this._addons.push(loadedAddon); instance.dispose = () => this._wrappedAddonDispose(loadedAddon); - instance.activate(terminal); + instance.activate(terminal as any); } private _wrappedAddonDispose(loadedAddon: ILoadedAddon): void { diff --git a/src/common/public/BufferApiView.ts b/src/common/public/BufferApiView.ts new file mode 100644 index 00000000..ca9ef2d8 --- /dev/null +++ b/src/common/public/BufferApiView.ts @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2021 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IBuffer as IBufferApi, IBufferLine as IBufferLineApi, IBufferCell as IBufferCellApi } from 'xterm'; +import { IBuffer } from 'common/buffer/Types'; +import { BufferLineApiView } from 'common/public/BufferLineApiView'; +import { CellData } from 'common/buffer/CellData'; + +export class BufferApiView implements IBufferApi { + constructor( + private _buffer: IBuffer, + public readonly type: 'normal' | 'alternate' + ) { } + + public init(buffer: IBuffer): BufferApiView { + this._buffer = buffer; + return this; + } + + public get cursorY(): number { return this._buffer.y; } + public get cursorX(): number { return this._buffer.x; } + public get viewportY(): number { return this._buffer.ydisp; } + public get baseY(): number { return this._buffer.ybase; } + public get length(): number { return this._buffer.lines.length; } + public getLine(y: number): IBufferLineApi | undefined { + const line = this._buffer.lines.get(y); + if (!line) { + return undefined; + } + return new BufferLineApiView(line); + } + public getNullCell(): IBufferCellApi { return new CellData(); } +} diff --git a/src/common/public/BufferLineApiView.ts b/src/common/public/BufferLineApiView.ts new file mode 100644 index 00000000..60375015 --- /dev/null +++ b/src/common/public/BufferLineApiView.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2021 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { CellData } from 'common/buffer/CellData'; +import { IBufferLine, ICellData } from 'common/Types'; +import { IBufferCell as IBufferCellApi, IBufferLine as IBufferLineApi } from 'xterm'; + +export class BufferLineApiView implements IBufferLineApi { + constructor(private _line: IBufferLine) { } + + public get isWrapped(): boolean { return this._line.isWrapped; } + public get length(): number { return this._line.length; } + public getCell(x: number, cell?: IBufferCellApi): IBufferCellApi | undefined { + if (x < 0 || x >= this._line.length) { + return undefined; + } + + if (cell) { + this._line.loadCell(x, cell as ICellData); + return cell; + } + return this._line.loadCell(x, new CellData()); + } + public translateToString(trimRight?: boolean, startColumn?: number, endColumn?: number): string { + return this._line.translateToString(trimRight, startColumn, endColumn); + } +} diff --git a/src/common/public/BufferNamespaceApi.ts b/src/common/public/BufferNamespaceApi.ts new file mode 100644 index 00000000..d86f6bf5 --- /dev/null +++ b/src/common/public/BufferNamespaceApi.ts @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2021 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IBuffer as IBufferApi, IBufferNamespace as IBufferNamespaceApi } from 'xterm'; +import { BufferApiView } from 'common/public/BufferApiView'; +import { IEvent, EventEmitter } from 'common/EventEmitter'; +import { ICoreTerminal } from 'common/Types'; + +export class BufferNamespaceApi implements IBufferNamespaceApi { + private _normal: BufferApiView; + private _alternate: BufferApiView; + private _onBufferChange = new EventEmitter(); + public get onBufferChange(): IEvent { return this._onBufferChange.event; } + + constructor(private _core: ICoreTerminal) { + this._normal = new BufferApiView(this._core.buffers.normal, 'normal'); + this._alternate = new BufferApiView(this._core.buffers.alt, 'alternate'); + this._core.buffers.onBufferActivate(() => this._onBufferChange.fire(this.active)); + } + public get active(): IBufferApi { + if (this._core.buffers.active === this._core.buffers.normal) { return this.normal; } + if (this._core.buffers.active === this._core.buffers.alt) { return this.alternate; } + throw new Error('Active buffer is neither normal nor alternate'); + } + public get normal(): IBufferApi { + return this._normal.init(this._core.buffers.normal); + } + public get alternate(): IBufferApi { + return this._alternate.init(this._core.buffers.alt); + } +} diff --git a/src/common/public/ParserApi.ts b/src/common/public/ParserApi.ts new file mode 100644 index 00000000..67df4be5 --- /dev/null +++ b/src/common/public/ParserApi.ts @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2021 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IParams } from 'common/parser/Types'; +import { IDisposable, IFunctionIdentifier, IParser } from 'xterm'; +import { ICoreTerminal } from 'common/Types'; + +export class ParserApi implements IParser { + constructor(private _core: ICoreTerminal) { } + + public registerCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean | Promise): IDisposable { + return this._core.registerCsiHandler(id, (params: IParams) => callback(params.toArray())); + } + public addCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean | Promise): IDisposable { + return this.registerCsiHandler(id, callback); + } + public registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean | Promise): IDisposable { + return this._core.registerDcsHandler(id, (data: string, params: IParams) => callback(data, params.toArray())); + } + public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean | Promise): IDisposable { + return this.registerDcsHandler(id, callback); + } + public registerEscHandler(id: IFunctionIdentifier, handler: () => boolean | Promise): IDisposable { + return this._core.registerEscHandler(id, handler); + } + public addEscHandler(id: IFunctionIdentifier, handler: () => boolean | Promise): IDisposable { + return this.registerEscHandler(id, handler); + } + public registerOscHandler(ident: number, callback: (data: string) => boolean | Promise): IDisposable { + return this._core.registerOscHandler(ident, callback); + } + public addOscHandler(ident: number, callback: (data: string) => boolean | Promise): IDisposable { + return this.registerOscHandler(ident, callback); + } +} diff --git a/src/common/public/UnicodeApi.ts b/src/common/public/UnicodeApi.ts new file mode 100644 index 00000000..8a669a05 --- /dev/null +++ b/src/common/public/UnicodeApi.ts @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2021 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { ICoreTerminal } from 'common/Types'; +import { IUnicodeHandling, IUnicodeVersionProvider } from 'xterm'; + +export class UnicodeApi implements IUnicodeHandling { + constructor(private _core: ICoreTerminal) { } + + public register(provider: IUnicodeVersionProvider): void { + this._core.unicodeService.register(provider); + } + + public get versions(): string[] { + return this._core.unicodeService.versions; + } + + public get activeVersion(): string { + return this._core.unicodeService.activeVersion; + } + + public set activeVersion(version: string) { + this._core.unicodeService.activeVersion = version; + } +} diff --git a/src/common/services/BufferService.ts b/src/common/services/BufferService.ts index 301146e1..c8c5f273 100644 --- a/src/common/services/BufferService.ts +++ b/src/common/services/BufferService.ts @@ -8,6 +8,7 @@ import { BufferSet } from 'common/buffer/BufferSet'; import { IBufferSet, IBuffer } from 'common/buffer/Types'; import { EventEmitter, IEvent } from 'common/EventEmitter'; import { Disposable } from 'common/Lifecycle'; +import { IAttributeData, IBufferLine, ScrollSource } from 'common/Types'; export const MINIMUM_COLS = 2; // Less than 2 can mess with wide chars export const MINIMUM_ROWS = 1; @@ -23,15 +24,20 @@ export class BufferService extends Disposable implements IBufferService { private _onResize = new EventEmitter<{ cols: number, rows: number }>(); public get onResize(): IEvent<{ cols: number, rows: number }> { return this._onResize.event; } + private _onScroll = new EventEmitter(); + public get onScroll(): IEvent { return this._onScroll.event; } public get buffer(): IBuffer { return this.buffers.active; } + /** An IBufferline to clone/copy from for new blank lines */ + private _cachedBlankLine: IBufferLine | undefined; + constructor( @IOptionsService private _optionsService: IOptionsService ) { super(); - this.cols = Math.max(_optionsService.options.cols, MINIMUM_COLS); - this.rows = Math.max(_optionsService.options.rows, MINIMUM_ROWS); + this.cols = Math.max(_optionsService.options.cols || 0, MINIMUM_COLS); + this.rows = Math.max(_optionsService.options.rows || 0, MINIMUM_ROWS); this.buffers = new BufferSet(_optionsService, this); } @@ -49,8 +55,131 @@ export class BufferService extends Disposable implements IBufferService { } public reset(): void { - this.buffers.dispose(); - this.buffers = new BufferSet(this._optionsService, this); + this.buffers.reset(); this.isUserScrolling = false; } + + /** + * Scroll the terminal down 1 row, creating a blank line. + * @param isWrapped Whether the new line is wrapped from the previous line. + */ + public scroll(eraseAttr: IAttributeData, isWrapped: boolean = false): void { + const buffer = this.buffer; + + let newLine: IBufferLine | undefined; + newLine = this._cachedBlankLine; + if (!newLine || newLine.length !== this.cols || newLine.getFg(0) !== eraseAttr.fg || newLine.getBg(0) !== eraseAttr.bg) { + newLine = buffer.getBlankLine(eraseAttr, isWrapped); + this._cachedBlankLine = newLine; + } + newLine.isWrapped = isWrapped; + + const topRow = buffer.ybase + buffer.scrollTop; + const bottomRow = buffer.ybase + buffer.scrollBottom; + + if (buffer.scrollTop === 0) { + // Determine whether the buffer is going to be trimmed after insertion. + const willBufferBeTrimmed = buffer.lines.isFull; + + // Insert the line using the fastest method + if (bottomRow === buffer.lines.length - 1) { + if (willBufferBeTrimmed) { + buffer.lines.recycle().copyFrom(newLine); + } else { + buffer.lines.push(newLine.clone()); + } + } else { + buffer.lines.splice(bottomRow + 1, 0, newLine.clone()); + } + + // Only adjust ybase and ydisp when the buffer is not trimmed + if (!willBufferBeTrimmed) { + buffer.ybase++; + // Only scroll the ydisp with ybase if the user has not scrolled up + if (!this.isUserScrolling) { + buffer.ydisp++; + } + } else { + // When the buffer is full and the user has scrolled up, keep the text + // stable unless ydisp is right at the top + if (this.isUserScrolling) { + buffer.ydisp = Math.max(buffer.ydisp - 1, 0); + } + } + } else { + // scrollTop is non-zero which means no line will be going to the + // scrollback, instead we can just shift them in-place. + const scrollRegionHeight = bottomRow - topRow + 1 /* as it's zero-based */; + buffer.lines.shiftElements(topRow + 1, scrollRegionHeight - 1, -1); + buffer.lines.set(bottomRow, newLine.clone()); + } + + // Move the viewport to the bottom of the buffer unless the user is + // scrolling. + if (!this.isUserScrolling) { + buffer.ydisp = buffer.ybase; + } + + this._onScroll.fire(buffer.ydisp); + } + + /** + * Scroll the display of the terminal + * @param disp The number of lines to scroll down (negative scroll up). + * @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 + * viewport originally. + */ + public scrollLines(disp: number, suppressScrollEvent?: boolean, source?: ScrollSource): void { + const buffer = this.buffer; + if (disp < 0) { + if (buffer.ydisp === 0) { + return; + } + this.isUserScrolling = true; + } else if (disp + buffer.ydisp >= buffer.ybase) { + this.isUserScrolling = false; + } + + const oldYdisp = buffer.ydisp; + buffer.ydisp = Math.max(Math.min(buffer.ydisp + disp, buffer.ybase), 0); + + // No change occurred, don't trigger scroll/refresh + if (oldYdisp === buffer.ydisp) { + return; + } + + if (!suppressScrollEvent) { + this._onScroll.fire(buffer.ydisp); + } + } + + /** + * Scroll the display of the terminal by a number of pages. + * @param pageCount The number of pages to scroll (negative scrolls up). + */ + public scrollPages(pageCount: number): void { + this.scrollLines(pageCount * (this.rows - 1)); + } + + /** + * Scrolls the display of the terminal to the top. + */ + public scrollToTop(): void { + this.scrollLines(-this.buffer.ydisp); + } + + /** + * Scrolls the display of the terminal to the bottom. + */ + public scrollToBottom(): void { + this.scrollLines(this.buffer.ybase - this.buffer.ydisp); + } + + public scrollToLine(line: number): void { + const scrollAmount = line - this.buffer.ydisp; + if (scrollAmount !== 0) { + this.scrollLines(scrollAmount); + } + } } diff --git a/src/common/services/InstantiationService.ts b/src/common/services/InstantiationService.ts index e5727fa6..8280948a 100644 --- a/src/common/services/InstantiationService.ts +++ b/src/common/services/InstantiationService.ts @@ -42,6 +42,8 @@ export class ServiceCollection { } export class InstantiationService implements IInstantiationService { + public serviceBrand: undefined; + private readonly _services: ServiceCollection = new ServiceCollection(); constructor() { diff --git a/src/common/services/LogService.ts b/src/common/services/LogService.ts index 3c0ccaf1..de4e18f8 100644 --- a/src/common/services/LogService.ts +++ b/src/common/services/LogService.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { ILogService, IOptionsService } from 'common/services/Services'; +import { ILogService, IOptionsService, LogLevelEnum } from 'common/services/Services'; type LogType = (message?: any, ...optionalParams: any[]) => void; @@ -19,21 +19,12 @@ interface IConsole { // module doesn't depend on them so we need to explicitly declare it. declare const console: IConsole; - -export enum LogLevel { - DEBUG = 0, - INFO = 1, - WARN = 2, - ERROR = 3, - OFF = 4 -} - -const optionsKeyToLogLevel: { [key: string]: LogLevel } = { - debug: LogLevel.DEBUG, - info: LogLevel.INFO, - warn: LogLevel.WARN, - error: LogLevel.ERROR, - off: LogLevel.OFF +const optionsKeyToLogLevel: { [key: string]: LogLevelEnum } = { + debug: LogLevelEnum.DEBUG, + info: LogLevelEnum.INFO, + warn: LogLevelEnum.WARN, + error: LogLevelEnum.ERROR, + off: LogLevelEnum.OFF }; const LOG_PREFIX = 'xterm.js: '; @@ -41,7 +32,7 @@ const LOG_PREFIX = 'xterm.js: '; export class LogService implements ILogService { public serviceBrand: any; - private _logLevel!: LogLevel; + public logLevel: LogLevelEnum = LogLevelEnum.OFF; constructor( @IOptionsService private readonly _optionsService: IOptionsService @@ -55,7 +46,7 @@ export class LogService implements ILogService { } private _updateLogLevel(): void { - this._logLevel = optionsKeyToLogLevel[this._optionsService.options.logLevel]; + this.logLevel = optionsKeyToLogLevel[this._optionsService.options.logLevel]; } private _evalLazyOptionalParams(optionalParams: any[]): void { @@ -72,25 +63,25 @@ export class LogService implements ILogService { } public debug(message: string, ...optionalParams: any[]): void { - if (this._logLevel <= LogLevel.DEBUG) { + if (this.logLevel <= LogLevelEnum.DEBUG) { this._log(console.log, message, optionalParams); } } public info(message: string, ...optionalParams: any[]): void { - if (this._logLevel <= LogLevel.INFO) { + if (this.logLevel <= LogLevelEnum.INFO) { this._log(console.info, message, optionalParams); } } public warn(message: string, ...optionalParams: any[]): void { - if (this._logLevel <= LogLevel.WARN) { + if (this.logLevel <= LogLevelEnum.WARN) { this._log(console.warn, message, optionalParams); } } public error(message: string, ...optionalParams: any[]): void { - if (this._logLevel <= LogLevel.ERROR) { + if (this.logLevel <= LogLevelEnum.ERROR) { this._log(console.error, message, optionalParams); } } diff --git a/src/common/services/OptionsService.test.ts b/src/common/services/OptionsService.test.ts index c289b5be..8675b6b9 100644 --- a/src/common/services/OptionsService.test.ts +++ b/src/common/services/OptionsService.test.ts @@ -15,6 +15,16 @@ describe('OptionsService', () => { afterEach(() => { console.error = originalError; }); + it('uses default value if invalid constructor option values passed for cols/rows', () => { + const optionsService = new OptionsService({ cols: undefined, rows: undefined }); + assert.equal(optionsService.getOption('rows'), DEFAULT_OPTIONS.rows); + assert.equal(optionsService.getOption('cols'), DEFAULT_OPTIONS.cols); + }); + it('uses values from constructor option values if correctly passed', () => { + const optionsService = new OptionsService({ cols: 80, rows: 25 }); + assert.equal(optionsService.getOption('rows'), 25); + assert.equal(optionsService.getOption('cols'), 80); + }); it('uses default value if invalid constructor option value passed', () => { assert.equal(new OptionsService({tabStopWidth: 0}).getOption('tabStopWidth'), DEFAULT_OPTIONS.tabStopWidth); }); diff --git a/src/common/services/OptionsService.ts b/src/common/services/OptionsService.ts index 6c43c303..e9dcaa6a 100644 --- a/src/common/services/OptionsService.ts +++ b/src/common/services/OptionsService.ts @@ -21,6 +21,7 @@ export const DEFAULT_OPTIONS: ITerminalOptions = Object.freeze({ cursorBlink: false, cursorStyle: 'block', cursorWidth: 1, + customGlyphs: true, bellSound: DEFAULT_BELL_SOUND, bellStyle: 'none', drawBoldTextInBrightColors: true, @@ -50,7 +51,7 @@ export const DEFAULT_OPTIONS: ITerminalOptions = Object.freeze({ windowOptions: {}, windowsMode: false, wordSeparator: ' ()[]{}\',"`', - + altClickMovesCursor: true, convertEol: false, termName: 'xterm', cancelEvents: false @@ -148,6 +149,11 @@ export class OptionsService implements IOptionsService { if (value <= 0) { throw new Error(`${key} cannot be less than or equal to 0, value: ${value}`); } + case 'rows': + case 'cols': + if (!value && value !== 0) { + throw new Error(`${key} must be numeric, value: ${value}`); + } break; } return value; diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index 0ea7a308..2190a0f8 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -5,7 +5,7 @@ import { IEvent } from 'common/EventEmitter'; import { IBuffer, IBufferSet } from 'common/buffer/Types'; -import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEncoding, ICoreMouseProtocol, CoreMouseEventType, ICharset, IWindowOptions, IModes } from 'common/Types'; +import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEncoding, ICoreMouseProtocol, CoreMouseEventType, ICharset, IWindowOptions, IModes, IAttributeData, ScrollSource } from 'common/Types'; import { createDecorator } from 'common/services/ServiceRegistry'; export const IBufferService = createDecorator('BufferService'); @@ -17,9 +17,14 @@ export interface IBufferService { readonly buffer: IBuffer; readonly buffers: IBufferSet; isUserScrolling: boolean; - onResize: IEvent<{ cols: number, rows: number }>; - + onScroll: IEvent; + scroll(eraseAttr: IAttributeData, isWrapped?: boolean): void; + scrollToBottom(): void; + scrollToTop(): void; + scrollToLine(line: number): void; + scrollLines(disp: number, suppressScrollEvent?: boolean, source?: ScrollSource): void; + scrollPages(pageCount: number): void; resize(cols: number, rows: number): void; reset(): void; } @@ -152,6 +157,8 @@ type GetLeadingNonServiceArgs = export const IInstantiationService = createDecorator('InstantiationService'); export interface IInstantiationService { + serviceBrand: undefined; + setService(id: IServiceIdentifier, instance: T): void; getService(id: IServiceIdentifier): T | undefined; createInstance any, R extends InstanceType>(t: Ctor, ...args: GetLeadingNonServiceArgs>): R; @@ -161,6 +168,8 @@ export const ILogService = createDecorator('LogService'); export interface ILogService { serviceBrand: undefined; + logLevel: LogLevelEnum; + debug(message: any, ...optionalParams: any[]): void; info(message: any, ...optionalParams: any[]): void; warn(message: any, ...optionalParams: any[]): void; @@ -181,9 +190,17 @@ export interface IOptionsService { export type FontWeight = 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900' | number; export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'off'; +export enum LogLevelEnum { + DEBUG = 0, + INFO = 1, + WARN = 2, + ERROR = 3, + OFF = 4 +} export type RendererType = 'dom' | 'canvas'; export interface IPartialTerminalOptions { + altClickMovesCursor?: boolean; allowTransparency?: boolean; bellSound?: string; bellStyle?: 'none' | 'sound' /* | 'visual' | 'both' */; @@ -220,12 +237,14 @@ export interface IPartialTerminalOptions { export interface ITerminalOptions { allowProposedApi: boolean; allowTransparency: boolean; + altClickMovesCursor: boolean; bellSound: string; bellStyle: 'none' | 'sound' /* | 'visual' | 'both' */; cols: number; cursorBlink: boolean; cursorStyle: 'block' | 'underline' | 'bar'; cursorWidth: number; + customGlyphs: boolean; disableStdin: boolean; drawBoldTextInBrightColors: boolean; fastScrollModifier: 'alt' | 'ctrl' | 'shift' | undefined; diff --git a/src/headless/Terminal.ts b/src/headless/Terminal.ts new file mode 100644 index 00000000..7f138ce1 --- /dev/null +++ b/src/headless/Terminal.ts @@ -0,0 +1,170 @@ +/** + * Copyright (c) 2014 The xterm.js authors. All rights reserved. + * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License) + * @license MIT + * + * Originally forked from (with the author's permission): + * Fabrice Bellard's javascript vt100 for jslinux: + * http://bellard.org/jslinux/ + * Copyright (c) 2011 Fabrice Bellard + * The original design remains. The terminal itself + * has been extended to include xterm CSI codes, among + * other features. + * + * Terminal Emulation References: + * http://vt100.net/ + * http://invisible-island.net/xterm/ctlseqs/ctlseqs.txt + * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html + * http://invisible-island.net/vttest/ + * http://www.inwap.com/pdp10/ansicode.txt + * http://linux.die.net/man/4/console_codes + * http://linux.die.net/man/7/urxvt + */ + +import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; +import { IBuffer } from 'common/buffer/Types'; +import { CoreTerminal } from 'common/CoreTerminal'; +import { EventEmitter, forwardEvent, IEvent } from 'common/EventEmitter'; +import { ITerminalOptions as IInitializedTerminalOptions } from 'common/services/Services'; +import { IMarker, ITerminalOptions, ScrollSource } from 'common/Types'; + +export class Terminal extends CoreTerminal { + // TODO: We should remove options once components adopt optionsService + public get options(): IInitializedTerminalOptions { return this.optionsService.options; } + + private _onBell = new EventEmitter(); + public get onBell(): IEvent { return this._onBell.event; } + private _onCursorMove = new EventEmitter(); + public get onCursorMove(): IEvent { return this._onCursorMove.event; } + private _onTitleChange = new EventEmitter(); + public get onTitleChange(): IEvent { return this._onTitleChange.event; } + + private _onA11yCharEmitter = new EventEmitter(); + public get onA11yChar(): IEvent { return this._onA11yCharEmitter.event; } + private _onA11yTabEmitter = new EventEmitter(); + public get onA11yTab(): IEvent { return this._onA11yTabEmitter.event; } + + /** + * Creates a new `Terminal` object. + * + * @param options An object containing a set of options, the available options are: + * - `cursorBlink` (boolean): Whether the terminal cursor blinks + * - `cols` (number): The number of columns of the terminal (horizontal size) + * - `rows` (number): The number of rows of the terminal (vertical size) + * + * @public + * @class Xterm Xterm + * @alias module:xterm/src/xterm + */ + constructor( + options: ITerminalOptions = {} + ) { + super(options); + + this._setup(); + + // Setup InputHandler listeners + this.register(this._inputHandler.onRequestBell(() => this.bell())); + this.register(this._inputHandler.onRequestReset(() => this.reset())); + this.register(forwardEvent(this._inputHandler.onCursorMove, this._onCursorMove)); + this.register(forwardEvent(this._inputHandler.onTitleChange, this._onTitleChange)); + this.register(forwardEvent(this._inputHandler.onA11yChar, this._onA11yCharEmitter)); + this.register(forwardEvent(this._inputHandler.onA11yTab, this._onA11yTabEmitter)); + } + + public dispose(): void { + if (this._isDisposed) { + return; + } + super.dispose(); + this.write = () => { }; + } + + /** + * Convenience property to active buffer. + */ + public get buffer(): IBuffer { + return this.buffers.active; + } + + protected _updateOptions(key: string): void { + super._updateOptions(key); + + // TODO: These listeners should be owned by individual components + switch (key) { + case 'tabStopWidth': this.buffers.setupTabStops(); break; + } + } + + // TODO: Support paste here? + + public get markers(): IMarker[] { + return this.buffer.markers; + } + + public addMarker(cursorYOffset: number): IMarker | undefined { + // Disallow markers on the alt buffer + if (this.buffer !== this.buffers.normal) { + return; + } + + return this.buffer.addMarker(this.buffer.ybase + this.buffer.y + cursorYOffset); + } + + public bell(): void { + this._onBell.fire(); + } + + /** + * Resizes the terminal. + * + * @param x The number of columns to resize to. + * @param y The number of rows to resize to. + */ + public resize(x: number, y: number): void { + if (x === this.cols && y === this.rows) { + return; + } + + super.resize(x, y); + } + + /** + * Clear the entire buffer, making the prompt line the new first line. + */ + public clear(): void { + if (this.buffer.ybase === 0 && this.buffer.y === 0) { + // Don't clear if it's already clear + return; + } + this.buffer.lines.set(0, this.buffer.lines.get(this.buffer.ybase + this.buffer.y)!); + this.buffer.lines.length = 1; + this.buffer.ydisp = 0; + this.buffer.ybase = 0; + this.buffer.y = 0; + for (let i = 1; i < this.rows; i++) { + this.buffer.lines.push(this.buffer.getBlankLine(DEFAULT_ATTR_DATA)); + } + this._onScroll.fire({ position: this.buffer.ydisp, source: ScrollSource.TERMINAL }); + } + + /** + * Reset terminal. + * Note: Calling this directly from JS is synchronous but does not clear + * input buffers and does not reset the parser, thus the terminal will + * continue to apply pending input data. + * If you need in band reset (synchronous with input data) consider + * using DECSTR (soft reset, CSI ! p) or RIS instead (hard reset, ESC c). + */ + public reset(): void { + /** + * Since _setup handles a full terminal creation, we have to carry forward + * a few things that should not reset. + */ + this.options.rows = this.rows; + this.options.cols = this.cols; + + this._setup(); + super.reset(); + } +} diff --git a/src/headless/Types.d.ts b/src/headless/Types.d.ts new file mode 100644 index 00000000..868e5c17 --- /dev/null +++ b/src/headless/Types.d.ts @@ -0,0 +1,31 @@ +import { IBuffer, IBufferSet } from 'common/buffer/Types'; +import { IEvent } from 'common/EventEmitter'; +import { IFunctionIdentifier, IParams } from 'common/parser/Types'; +import { ICoreTerminal, IDisposable, IMarker, ITerminalOptions } from 'common/Types'; + +export interface ITerminal extends ICoreTerminal { + rows: number; + cols: number; + buffer: IBuffer; + buffers: IBufferSet; + markers: IMarker[]; + // TODO: We should remove options once components adopt optionsService + options: ITerminalOptions; + + onCursorMove: IEvent; + onData: IEvent; + onBinary: IEvent; + onLineFeed: IEvent; + onResize: IEvent<{ cols: number, rows: number }>; + onTitleChange: IEvent; + resize(columns: number, rows: number): void; + addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable; + addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean): IDisposable; + addEscHandler(id: IFunctionIdentifier, callback: () => boolean): IDisposable; + addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable; + addMarker(cursorYOffset: number): IMarker | undefined; + dispose(): void; + clear(): void; + write(data: string | Uint8Array, callback?: () => void): void; + reset(): void; +} diff --git a/src/headless/public/Terminal.test.ts b/src/headless/public/Terminal.test.ts new file mode 100644 index 00000000..4247aae9 --- /dev/null +++ b/src/headless/public/Terminal.test.ts @@ -0,0 +1,468 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { deepStrictEqual, strictEqual, throws } from 'assert'; +import { Terminal } from 'headless/public/Terminal'; + +let term: Terminal; + +describe('Headless API Tests', function(): void { + beforeEach(() => { + // Create default terminal to be used by most tests + term = new Terminal(); + }); + + it('Default options', async () => { + strictEqual(term.cols, 80); + strictEqual(term.rows, 24); + }); + + it('Proposed API check', async () => { + term = new Terminal({ allowProposedApi: false }); + throws(() => term.buffer, (error) => error.message === 'You must set the allowProposedApi option to true to use proposed API'); + }); + + it('write', async () => { + await writeSync('foo'); + await writeSync('bar'); + await writeSync('文'); + lineEquals(0, 'foobar文'); + }); + + it('write with callback', async () => { + let result: string | undefined; + await new Promise(r => { + term.write('foo', () => { result = 'a'; }); + term.write('bar', () => { result += 'b'; }); + term.write('文', () => { + result += 'c'; + r(); + }); + }); + lineEquals(0, 'foobar文'); + strictEqual(result, 'abc'); + }); + + it('write - bytes (UTF8)', async () => { + await writeSync(new Uint8Array([102, 111, 111])); // foo + await writeSync(new Uint8Array([98, 97, 114])); // bar + await writeSync(new Uint8Array([230, 150, 135])); // 文 + lineEquals(0, 'foobar文'); + }); + + it('write - bytes (UTF8) with callback', async () => { + let result: string | undefined; + await new Promise(r => { + term.write(new Uint8Array([102, 111, 111]), () => { result = 'A'; }); // foo + term.write(new Uint8Array([98, 97, 114]), () => { result += 'B'; }); // bar + term.write(new Uint8Array([230, 150, 135]), () => { // 文 + result += 'C'; + r(); + }); + }); + lineEquals(0, 'foobar文'); + strictEqual(result, 'ABC'); + }); + + it('writeln', async () => { + await writelnSync('foo'); + await writelnSync('bar'); + await writelnSync('文'); + lineEquals(0, 'foo'); + lineEquals(1, 'bar'); + lineEquals(2, '文'); + }); + + it('writeln with callback', async () => { + let result: string | undefined; + await new Promise(r => { + term.writeln('foo', () => { result = '1'; }); + term.writeln('bar', () => { result += '2'; }); + term.writeln('文', () => { + result += '3'; + r(); + }); + }); + lineEquals(0, 'foo'); + lineEquals(1, 'bar'); + lineEquals(2, '文'); + strictEqual(result, '123'); + }); + + it('writeln - bytes (UTF8)', async () => { + await writelnSync(new Uint8Array([102, 111, 111])); + await writelnSync(new Uint8Array([98, 97, 114])); + await writelnSync(new Uint8Array([230, 150, 135])); + lineEquals(0, 'foo'); + lineEquals(1, 'bar'); + lineEquals(2, '文'); + }); + + it('clear', async () => { + term = new Terminal({ rows: 5 }); + for (let i = 0; i < 10; i++) { + await writeSync('\n\rtest' + i); + } + term.clear(); + strictEqual(term.buffer.active.length, 5); + lineEquals(0, 'test9'); + for (let i = 1; i < 5; i++) { + lineEquals(i, ''); + } + }); + + it('getOption, setOption', async () => { + strictEqual(term.getOption('scrollback'), 1000); + term.setOption('scrollback', 50); + strictEqual(term.getOption('scrollback'), 50); + }); + + describe('loadAddon', () => { + it('constructor', async () => { + term = new Terminal({ cols: 5 }); + let cols = 0; + term.loadAddon({ + activate: (t) => cols = t.cols, + dispose: () => {} + }); + strictEqual(cols, 5); + }); + + it('dispose (addon)', async () => { + let disposeCalled = false; + const addon = { + activate: () => {}, + dispose: () => disposeCalled = true + }; + term.loadAddon(addon); + strictEqual(disposeCalled, false); + addon.dispose(); + strictEqual(disposeCalled, true); + }); + + it('dispose (terminal)', async () => { + let disposeCalled = false; + term.loadAddon({ + activate: () => {}, + dispose: () => disposeCalled = true + }); + strictEqual(disposeCalled, false); + term.dispose(); + strictEqual(disposeCalled, true); + }); + }); + + describe('Events', () => { + it('onCursorMove', async () => { + let callCount = 0; + term.onCursorMove(e => callCount++); + await writeSync('foo'); + strictEqual(callCount, 1); + await writeSync('bar'); + strictEqual(callCount, 2); + }); + + it('onData', async () => { + const calls: string[] = []; + term.onData(e => calls.push(e)); + await writeSync('\x1b[5n'); // DSR Status Report + deepStrictEqual(calls, ['\x1b[0n']); + }); + + it('onLineFeed', async () => { + let callCount = 0; + term.onLineFeed(() => callCount++); + await writelnSync('foo'); + strictEqual(callCount, 1); + await writelnSync('bar'); + strictEqual(callCount, 2); + }); + + it('onScroll', async () => { + term = new Terminal({ rows: 5 }); + const calls: number[] = []; + term.onScroll(e => calls.push(e)); + for (let i = 0; i < 4; i++) { + await writelnSync('foo'); + } + deepStrictEqual(calls, []); + await writelnSync('bar'); + deepStrictEqual(calls, [1]); + await writelnSync('baz'); + deepStrictEqual(calls, [1, 2]); + }); + + it('onResize', async () => { + const calls: [number, number][] = []; + term.onResize(e => calls.push([e.cols, e.rows])); + deepStrictEqual(calls, []); + term.resize(10, 5); + deepStrictEqual(calls, [[10, 5]]); + term.resize(20, 15); + deepStrictEqual(calls, [[10, 5], [20, 15]]); + }); + + it('onTitleChange', async () => { + const calls: string[] = []; + term.onTitleChange(e => calls.push(e)); + deepStrictEqual(calls, []); + await writeSync('\x1b]2;foo\x9c'); + deepStrictEqual(calls, ['foo']); + }); + + it('onBell', async () => { + const calls: boolean[] = []; + term.onBell(() => calls.push(true)); + deepStrictEqual(calls, []); + await writeSync('\x07'); + deepStrictEqual(calls, [true]); + }); + }); + + describe('buffer', () => { + it('cursorX, cursorY', async () => { + term = new Terminal({ rows: 5, cols: 5 }); + strictEqual(term.buffer.active.cursorX, 0); + strictEqual(term.buffer.active.cursorY, 0); + await writeSync('foo'); + strictEqual(term.buffer.active.cursorX, 3); + strictEqual(term.buffer.active.cursorY, 0); + await writeSync('\n'); + strictEqual(term.buffer.active.cursorX, 3); + strictEqual(term.buffer.active.cursorY, 1); + await writeSync('\r'); + strictEqual(term.buffer.active.cursorX, 0); + strictEqual(term.buffer.active.cursorY, 1); + await writeSync('abcde'); + strictEqual(term.buffer.active.cursorX, 5); + strictEqual(term.buffer.active.cursorY, 1); + await writeSync('\n\r\n\n\n\n\n'); + strictEqual(term.buffer.active.cursorX, 0); + strictEqual(term.buffer.active.cursorY, 4); + }); + + it('viewportY', async () => { + term = new Terminal({ rows: 5 }); + strictEqual(term.buffer.active.viewportY, 0); + await writeSync('\n\n\n\n'); + strictEqual(term.buffer.active.viewportY, 0); + await writeSync('\n'); + strictEqual(term.buffer.active.viewportY, 1); + await writeSync('\n\n\n\n'); + strictEqual(term.buffer.active.viewportY, 5); + term.scrollLines(-1); + strictEqual(term.buffer.active.viewportY, 4); + term.scrollToTop(); + strictEqual(term.buffer.active.viewportY, 0); + }); + + it('baseY', async () => { + term = new Terminal({ rows: 5 }); + strictEqual(term.buffer.active.baseY, 0); + await writeSync('\n\n\n\n'); + strictEqual(term.buffer.active.baseY, 0); + await writeSync('\n'); + strictEqual(term.buffer.active.baseY, 1); + await writeSync('\n\n\n\n'); + strictEqual(term.buffer.active.baseY, 5); + term.scrollLines(-1); + strictEqual(term.buffer.active.baseY, 5); + term.scrollToTop(); + strictEqual(term.buffer.active.baseY, 5); + }); + + it('length', async () => { + term = new Terminal({ rows: 5 }); + strictEqual(term.buffer.active.length, 5); + await writeSync('\n\n\n\n'); + strictEqual(term.buffer.active.length, 5); + await writeSync('\n'); + strictEqual(term.buffer.active.length, 6); + await writeSync('\n\n\n\n'); + strictEqual(term.buffer.active.length, 10); + }); + + describe('getLine', () => { + it('invalid index', async () => { + term = new Terminal({ rows: 5 }); + strictEqual(term.buffer.active.getLine(-1), undefined); + strictEqual(term.buffer.active.getLine(5), undefined); + }); + + it('isWrapped', async () => { + term = new Terminal({ cols: 5 }); + strictEqual(term.buffer.active.getLine(0)!.isWrapped, false); + strictEqual(term.buffer.active.getLine(1)!.isWrapped, false); + await writeSync('abcde'); + strictEqual(term.buffer.active.getLine(0)!.isWrapped, false); + strictEqual(term.buffer.active.getLine(1)!.isWrapped, false); + await writeSync('f'); + strictEqual(term.buffer.active.getLine(0)!.isWrapped, false); + strictEqual(term.buffer.active.getLine(1)!.isWrapped, true); + }); + + it('translateToString', async () => { + term = new Terminal({ cols: 5 }); + strictEqual(term.buffer.active.getLine(0)!.translateToString(), ' '); + strictEqual(term.buffer.active.getLine(0)!.translateToString(true), ''); + await writeSync('foo'); + strictEqual(term.buffer.active.getLine(0)!.translateToString(), 'foo '); + strictEqual(term.buffer.active.getLine(0)!.translateToString(true), 'foo'); + await writeSync('bar'); + strictEqual(term.buffer.active.getLine(0)!.translateToString(), 'fooba'); + strictEqual(term.buffer.active.getLine(0)!.translateToString(true), 'fooba'); + strictEqual(term.buffer.active.getLine(1)!.translateToString(true), 'r'); + strictEqual(term.buffer.active.getLine(0)!.translateToString(false, 1), 'ooba'); + strictEqual(term.buffer.active.getLine(0)!.translateToString(false, 1, 3), 'oo'); + }); + + it('getCell', async () => { + term = new Terminal({ cols: 5 }); + strictEqual(term.buffer.active.getLine(0)!.getCell(-1), undefined); + strictEqual(term.buffer.active.getLine(0)!.getCell(5), undefined); + strictEqual(term.buffer.active.getLine(0)!.getCell(0)!.getChars(), ''); + strictEqual(term.buffer.active.getLine(0)!.getCell(0)!.getWidth(), 1); + await writeSync('a文'); + strictEqual(term.buffer.active.getLine(0)!.getCell(0)!.getChars(), 'a'); + strictEqual(term.buffer.active.getLine(0)!.getCell(0)!.getWidth(), 1); + strictEqual(term.buffer.active.getLine(0)!.getCell(1)!.getChars(), '文'); + strictEqual(term.buffer.active.getLine(0)!.getCell(1)!.getWidth(), 2); + strictEqual(term.buffer.active.getLine(0)!.getCell(2)!.getChars(), ''); + strictEqual(term.buffer.active.getLine(0)!.getCell(2)!.getWidth(), 0); + }); + }); + + it('active, normal, alternate', async () => { + term = new Terminal({ cols: 5 }); + strictEqual(term.buffer.active.type, 'normal'); + strictEqual(term.buffer.normal.type, 'normal'); + strictEqual(term.buffer.alternate.type, 'alternate'); + + await writeSync('norm '); + strictEqual(term.buffer.active.getLine(0)!.translateToString(), 'norm '); + strictEqual(term.buffer.normal.getLine(0)!.translateToString(), 'norm '); + strictEqual(term.buffer.alternate.getLine(0), undefined); + + await writeSync('\x1b[?47h\r'); // use alternate screen buffer + strictEqual(term.buffer.active.type, 'alternate'); + strictEqual(term.buffer.normal.type, 'normal'); + strictEqual(term.buffer.alternate.type, 'alternate'); + + strictEqual(term.buffer.active.getLine(0)!.translateToString(), ' '); + await writeSync('alt '); + strictEqual(term.buffer.active.getLine(0)!.translateToString(), 'alt '); + strictEqual(term.buffer.normal.getLine(0)!.translateToString(), 'norm '); + strictEqual(term.buffer.alternate.getLine(0)!.translateToString(), 'alt '); + + await writeSync('\x1b[?47l\r'); // use normal screen buffer + strictEqual(term.buffer.active.type, 'normal'); + strictEqual(term.buffer.normal.type, 'normal'); + strictEqual(term.buffer.alternate.type, 'alternate'); + + strictEqual(term.buffer.active.getLine(0)!.translateToString(), 'norm '); + strictEqual(term.buffer.normal.getLine(0)!.translateToString(), 'norm '); + strictEqual(term.buffer.alternate.getLine(0), undefined); + }); + }); + + describe('modes', () => { + it('defaults', () => { + deepStrictEqual(term.modes, { + applicationCursorKeysMode: false, + applicationKeypadMode: false, + bracketedPasteMode: false, + insertMode: false, + mouseTrackingMode: 'none', + originMode: false, + reverseWraparoundMode: false, + sendFocusMode: false, + wraparoundMode: true + }); + }); + it('applicationCursorKeysMode', async () => { + await writeSync('\x1b[?1h'); + strictEqual(term.modes.applicationCursorKeysMode, true); + await writeSync('\x1b[?1l'); + strictEqual(term.modes.applicationCursorKeysMode, false); + }); + it('applicationKeypadMode', async () => { + await writeSync('\x1b[?66h'); + strictEqual(term.modes.applicationKeypadMode, true); + await writeSync('\x1b[?66l'); + strictEqual(term.modes.applicationKeypadMode, false); + }); + it('bracketedPasteMode', async () => { + await writeSync('\x1b[?2004h'); + strictEqual(term.modes.bracketedPasteMode, true); + await writeSync('\x1b[?2004l'); + strictEqual(term.modes.bracketedPasteMode, false); + }); + it('insertMode', async () => { + await writeSync('\x1b[4h'); + strictEqual(term.modes.insertMode, true); + await writeSync('\x1b[4l'); + strictEqual(term.modes.insertMode, false); + }); + it('mouseTrackingMode', async () => { + await writeSync('\x1b[?9h'); + strictEqual(term.modes.mouseTrackingMode, 'x10'); + await writeSync('\x1b[?9l'); + strictEqual(term.modes.mouseTrackingMode, 'none'); + await writeSync('\x1b[?1000h'); + strictEqual(term.modes.mouseTrackingMode, 'vt200'); + await writeSync('\x1b[?1000l'); + strictEqual(term.modes.mouseTrackingMode, 'none'); + await writeSync('\x1b[?1002h'); + strictEqual(term.modes.mouseTrackingMode, 'drag'); + await writeSync('\x1b[?1002l'); + strictEqual(term.modes.mouseTrackingMode, 'none'); + await writeSync('\x1b[?1003h'); + strictEqual(term.modes.mouseTrackingMode, 'any'); + await writeSync('\x1b[?1003l'); + strictEqual(term.modes.mouseTrackingMode, 'none'); + }); + it('originMode', async () => { + await writeSync('\x1b[?6h'); + strictEqual(term.modes.originMode, true); + await writeSync('\x1b[?6l'); + strictEqual(term.modes.originMode, false); + }); + it('reverseWraparoundMode', async () => { + await writeSync('\x1b[?45h'); + strictEqual(term.modes.reverseWraparoundMode, true); + await writeSync('\x1b[?45l'); + strictEqual(term.modes.reverseWraparoundMode, false); + }); + it('sendFocusMode', async () => { + await writeSync('\x1b[?1004h'); + strictEqual(term.modes.sendFocusMode, true); + await writeSync('\x1b[?1004l'); + strictEqual(term.modes.sendFocusMode, false); + }); + it('wraparoundMode', async () => { + await writeSync('\x1b[?7h'); + strictEqual(term.modes.wraparoundMode, true); + await writeSync('\x1b[?7l'); + strictEqual(term.modes.wraparoundMode, false); + }); + }); + + it('dispose', async () => { + term.dispose(); + strictEqual((term as any)._core._isDisposed, true); + }); +}); + +function writeSync(text: string | Uint8Array): Promise { + return new Promise(r => term.write(text, r)); +} + +function writelnSync(text: string | Uint8Array): Promise { + return new Promise(r => term.writeln(text, r)); +} + +function lineEquals(index: number, text: string): void { + strictEqual(term.buffer.active.getLine(index)!.translateToString(true), text); +} diff --git a/src/headless/public/Terminal.ts b/src/headless/public/Terminal.ts new file mode 100644 index 00000000..29e83581 --- /dev/null +++ b/src/headless/public/Terminal.ts @@ -0,0 +1,166 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IEvent } from 'common/EventEmitter'; +import { BufferNamespaceApi } from 'common/public/BufferNamespaceApi'; +import { ParserApi } from 'common/public/ParserApi'; +import { UnicodeApi } from 'common/public/UnicodeApi'; +import { IBufferNamespace as IBufferNamespaceApi, IMarker, IModes, IParser, ITerminalAddon, ITerminalOptions, IUnicodeHandling, Terminal as ITerminalApi } from 'xterm-headless'; +import { Terminal as TerminalCore } from 'headless/Terminal'; +import { AddonManager } from 'common/public/AddonManager'; + +export class Terminal implements ITerminalApi { + private _core: TerminalCore; + private _addonManager: AddonManager; + private _parser: IParser | undefined; + private _buffer: BufferNamespaceApi | undefined; + + constructor(options?: ITerminalOptions) { + this._core = new TerminalCore(options); + this._addonManager = new AddonManager(); + } + + private _checkProposedApi(): void { + if (!this._core.optionsService.options.allowProposedApi) { + throw new Error('You must set the allowProposedApi option to true to use proposed API'); + } + } + + public get onBell(): IEvent { return this._core.onBell; } + public get onBinary(): IEvent { return this._core.onBinary; } + public get onCursorMove(): IEvent { return this._core.onCursorMove; } + public get onData(): IEvent { return this._core.onData; } + public get onLineFeed(): IEvent { return this._core.onLineFeed; } + public get onResize(): IEvent<{ cols: number, rows: number }> { return this._core.onResize; } + public get onScroll(): IEvent { return this._core.onScroll; } + public get onTitleChange(): IEvent { return this._core.onTitleChange; } + + public get parser(): IParser { + this._checkProposedApi(); + if (!this._parser) { + this._parser = new ParserApi(this._core); + } + return this._parser; + } + public get unicode(): IUnicodeHandling { + this._checkProposedApi(); + return new UnicodeApi(this._core); + } + public get rows(): number { return this._core.rows; } + public get cols(): number { return this._core.cols; } + public get buffer(): IBufferNamespaceApi { + this._checkProposedApi(); + if (!this._buffer) { + this._buffer = new BufferNamespaceApi(this._core); + } + return this._buffer; + } + public get markers(): ReadonlyArray { + this._checkProposedApi(); + return this._core.markers; + } + public get modes(): IModes { + const m = this._core.coreService.decPrivateModes; + let mouseTrackingMode: 'none' | 'x10' | 'vt200' | 'drag' | 'any' = 'none'; + switch (this._core.coreMouseService.activeProtocol) { + case 'X10': mouseTrackingMode = 'x10'; break; + case 'VT200': mouseTrackingMode = 'vt200'; break; + case 'DRAG': mouseTrackingMode = 'drag'; break; + case 'ANY': mouseTrackingMode = 'any'; break; + } + return { + applicationCursorKeysMode: m.applicationCursorKeys, + applicationKeypadMode: m.applicationKeypad, + bracketedPasteMode: m.bracketedPasteMode, + insertMode: this._core.coreService.modes.insertMode, + mouseTrackingMode: mouseTrackingMode, + originMode: m.origin, + reverseWraparoundMode: m.reverseWraparound, + sendFocusMode: m.sendFocus, + wraparoundMode: m.wraparound + }; + } + public resize(columns: number, rows: number): void { + this._verifyIntegers(columns, rows); + this._core.resize(columns, rows); + } + public registerMarker(cursorYOffset: number): IMarker | undefined { + this._checkProposedApi(); + this._verifyIntegers(cursorYOffset); + return this._core.addMarker(cursorYOffset); + } + public addMarker(cursorYOffset: number): IMarker | undefined { + return this.registerMarker(cursorYOffset); + } + public dispose(): void { + this._addonManager.dispose(); + this._core.dispose(); + } + public scrollLines(amount: number): void { + this._verifyIntegers(amount); + this._core.scrollLines(amount); + } + public scrollPages(pageCount: number): void { + this._verifyIntegers(pageCount); + this._core.scrollPages(pageCount); + } + public scrollToTop(): void { + this._core.scrollToTop(); + } + public scrollToBottom(): void { + this._core.scrollToBottom(); + } + public scrollToLine(line: number): void { + this._verifyIntegers(line); + this._core.scrollToLine(line); + } + public clear(): void { + this._core.clear(); + } + public write(data: string | Uint8Array, callback?: () => void): void { + this._core.write(data, callback); + } + public writeUtf8(data: Uint8Array, callback?: () => void): void { + this._core.write(data, callback); + } + public writeln(data: string | Uint8Array, callback?: () => void): void { + this._core.write(data); + this._core.write('\r\n', callback); + } + public getOption(key: 'bellSound' | 'bellStyle' | 'cursorStyle' | 'fontFamily' | 'logLevel' | 'rendererType' | 'termName' | 'wordSeparator'): string; + public getOption(key: 'allowTransparency' | 'altClickMovesCursor' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'disableStdin' | 'macOptionIsMeta' | 'rightClickSelectsWord' | 'popOnBell' | 'visualBell'): boolean; + public getOption(key: 'cols' | 'fontSize' | 'letterSpacing' | 'lineHeight' | 'rows' | 'tabStopWidth' | 'scrollback'): number; + public getOption(key: string): any; + public getOption(key: any): any { + return this._core.optionsService.getOption(key); + } + public setOption(key: 'bellSound' | 'fontFamily' | 'termName' | 'wordSeparator', value: string): void; + public setOption(key: 'fontWeight' | 'fontWeightBold', value: 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900' | number): void; + public setOption(key: 'logLevel', value: 'debug' | 'info' | 'warn' | 'error' | 'off'): void; + public setOption(key: 'bellStyle', value: 'none' | 'visual' | 'sound' | 'both'): void; + public setOption(key: 'cursorStyle', value: 'block' | 'underline' | 'bar'): void; + public setOption(key: 'allowTransparency' | 'altClickMovesCursor' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'disableStdin' | 'macOptionIsMeta' | 'rightClickSelectsWord' | 'popOnBell' | 'visualBell', value: boolean): void; + public setOption(key: 'fontSize' | 'letterSpacing' | 'lineHeight' | 'tabStopWidth' | 'scrollback', value: number): void; + public setOption(key: 'cols' | 'rows', value: number): void; + public setOption(key: string, value: any): void; + public setOption(key: any, value: any): void { + this._core.optionsService.setOption(key, value); + } + public reset(): void { + this._core.reset(); + } + public loadAddon(addon: ITerminalAddon): void { + // TODO: This could cause issues if the addon calls renderer apis + return this._addonManager.loadAddon(this as any, addon); + } + + private _verifyIntegers(...values: number[]): void { + for (const value of values) { + if (value === Infinity || isNaN(value) || value % 1 !== 0) { + throw new Error('This API only accepts integers'); + } + } + } +} diff --git a/src/headless/tsconfig.json b/src/headless/tsconfig.json new file mode 100644 index 00000000..b579af97 --- /dev/null +++ b/src/headless/tsconfig.json @@ -0,0 +1,26 @@ +{ + "extends": "../tsconfig-library-base", + "compilerOptions": { + "lib": [ + "es2015", + "es2016.Array.Include" + ], + "outDir": "../../out", + "types": [ + "../../node_modules/@types/mocha", + "../../node_modules/@types/node" + ], + "baseUrl": "../", + "paths": { + "common/*": [ "./common/*" ] + } + }, + "include": [ + "./**/*", + "../../typings/xterm.d.ts", // common/Types.d.ts imports from 'xterm' + "../../typings/xterm-headless.d.ts" + ], + "references": [ + { "path": "../common" } + ] +} diff --git a/test/api/CharWidth.api.ts b/test/api/CharWidth.api.ts index 01d69861..83067149 100644 --- a/test/api/CharWidth.api.ts +++ b/test/api/CharWidth.api.ts @@ -3,10 +3,10 @@ * @license MIT */ -import { pollFor, openTerminal, getBrowserType } from './TestUtils'; +import { pollFor, openTerminal, launchBrowser } from './TestUtils'; import { Browser, Page } from 'playwright'; -const APP = 'http://127.0.0.1:3000/test'; +const APP = 'http://127.0.0.1:3001/test'; let browser: Browser; let page: Page; @@ -15,10 +15,7 @@ const height = 600; describe('CharWidth Integration Tests', function(): void { before(async function(): Promise { - const browserType = getBrowserType(); - browser = await browserType.launch({ - headless: process.argv.indexOf('--headless') !== -1 - }); + browser = await launchBrowser(); page = await (await browser.newContext()).newPage(); await page.setViewportSize({ width, height }); await page.goto(APP); diff --git a/test/api/InputHandler.api.ts b/test/api/InputHandler.api.ts index 16ab2301..54ee6957 100644 --- a/test/api/InputHandler.api.ts +++ b/test/api/InputHandler.api.ts @@ -4,11 +4,11 @@ */ import { assert } from 'chai'; -import { pollFor, openTerminal, getBrowserType } from './TestUtils'; +import { pollFor, openTerminal, getBrowserType, launchBrowser } from './TestUtils'; import { Browser, Page } from 'playwright'; import { IRenderDimensions } from 'browser/renderer/Types'; -const APP = 'http://127.0.0.1:3000/test'; +const APP = 'http://127.0.0.1:3001/test'; let browser: Browser; let page: Page; @@ -21,9 +21,7 @@ describe('InputHandler Integration Tests', function(): void { before(async function(): Promise { const browserType = getBrowserType(); isChromium = browserType.name() === 'chromium'; - browser = await browserType.launch({ - headless: process.argv.indexOf('--headless') !== -1 - }); + browser = await launchBrowser(); page = await (await browser.newContext()).newPage(); await page.setViewportSize({ width, height }); await page.goto(APP); diff --git a/test/api/MouseTracking.api.ts b/test/api/MouseTracking.api.ts index 0bd0cc18..ebeb680f 100644 --- a/test/api/MouseTracking.api.ts +++ b/test/api/MouseTracking.api.ts @@ -3,10 +3,10 @@ * @license MIT */ -import { pollFor, writeSync, openTerminal, getBrowserType } from './TestUtils'; +import { pollFor, writeSync, openTerminal, getBrowserType, launchBrowser } from './TestUtils'; import { Browser, Page } from 'playwright'; -const APP = 'http://127.0.0.1:3000/test'; +const APP = 'http://127.0.0.1:3001/test'; let browser: Browser; let page: Page; @@ -211,9 +211,7 @@ describe('Mouse Tracking Tests', async () => { const itMouse = isChromium ? it : it.skip; before(async function(): Promise { - browser = await browserType.launch({ - headless: process.argv.indexOf('--headless') !== -1 - }); + browser = await launchBrowser(); page = await (await browser.newContext()).newPage(); await page.setViewportSize({ width, height }); }); diff --git a/test/api/Parser.api.ts b/test/api/Parser.api.ts index 77101e23..ada28adf 100644 --- a/test/api/Parser.api.ts +++ b/test/api/Parser.api.ts @@ -4,22 +4,19 @@ */ import { assert } from 'chai'; -import { writeSync, openTerminal, getBrowserType } from './TestUtils'; +import { writeSync, openTerminal, launchBrowser } from './TestUtils'; import { Browser, Page } from 'playwright'; -const APP = 'http://127.0.0.1:3000/test'; +const APP = 'http://127.0.0.1:3001/test'; let browser: Browser; let page: Page; const width = 800; const height = 600; -describe('Parser Integration Tests', function(): void { - before(async function(): Promise { - const browserType = getBrowserType(); - browser = await browserType.launch({ - headless: process.argv.indexOf('--headless') !== -1 - }); +describe('Parser Integration Tests', function (): void { + before(async function (): Promise { + browser = await launchBrowser(); page = await (await browser.newContext()).newPage(); await page.setViewportSize({ width, height }); await page.goto(APP); @@ -28,84 +25,189 @@ describe('Parser Integration Tests', function(): void { after(async () => browser.close()); - describe('addCsiHandler', () => { + describe('registerCsiHandler', () => { it('should call custom CSI handler with js array params', async () => { await page.evaluate(` window.term.reset(); window._customCsiHandlerParams = []; - const _customCsiHandler = window.term.parser.addCsiHandler({final: 'm'}, (params, collect) => { + const _customCsiHandler = window.term.parser.registerCsiHandler({final: 'm'}, (params, collect) => { window._customCsiHandlerParams.push(params); return false; }, ''); `); await writeSync(page, '\x1b[38;5;123mparams\x1b[38:2::50:100:150msubparams'); - assert.deepEqual(await page.evaluate(`(() => window._customCsiHandlerParams)();`), [[38, 5, 123], [38, [2, -1, 50, 100, 150]]]); + assert.deepEqual(await page.evaluate(`window._customCsiHandlerParams`), [ + [38, 5, 123], + [38, [2, -1, 50, 100, 150]] + ]); + }); + it('async', async () => { + await page.evaluate(` + window.term.reset(); + window._customCsiHandlerCallStack = []; + const _customCsiHandlerA = window.term.parser.registerCsiHandler({intermediates:'+', final: 'Z'}, params => { + window._customCsiHandlerCallStack.push(['A', params]); + return false; + }); + const _customCsiHandlerB = window.term.parser.registerCsiHandler({intermediates:'+', final: 'Z'}, async params => { + await new Promise(res => setTimeout(res, 50)); + window._customCsiHandlerCallStack.push(['B', params]); + return false; + }); + const _customCsiHandlerC = window.term.parser.registerCsiHandler({intermediates:'+', final: 'Z'}, params => { + window._customCsiHandlerCallStack.push(['C', params]); + return false; + }); + `); + await writeSync(page, '\x1b[1;2+Z'); + assert.deepEqual(await page.evaluate(`window._customCsiHandlerCallStack`), [ + ['C', [1, 2]], + ['B', [1, 2]], + ['A', [1, 2]] + ]); }); }); - describe('addDcsHandler', () => { + describe('registerDcsHandler', () => { it('should respects return value', async () => { await page.evaluate(` window.term.reset(); window._customDcsHandlerCallStack = []; - const _customDcsHandlerA = window.term.parser.addDcsHandler({intermediates:'+', final: 'p'}, (data, params) => { + const _customDcsHandlerA = window.term.parser.registerDcsHandler({intermediates:'+', final: 'p'}, (data, params) => { window._customDcsHandlerCallStack.push(['A', params, data]); return false; }); - const _customDcsHandlerB = window.term.parser.addDcsHandler({intermediates:'+', final: 'p'}, (data, params) => { + const _customDcsHandlerB = window.term.parser.registerDcsHandler({intermediates:'+', final: 'p'}, (data, params) => { window._customDcsHandlerCallStack.push(['B', params, data]); return true; }); - const _customDcsHandlerC = window.term.parser.addDcsHandler({intermediates:'+', final: 'p'}, (data, params) => { + const _customDcsHandlerC = window.term.parser.registerDcsHandler({intermediates:'+', final: 'p'}, (data, params) => { window._customDcsHandlerCallStack.push(['C', params, data]); return false; }); `); await writeSync(page, '\x1bP1;2+psome data\x1b\\\\'); - assert.deepEqual(await page.evaluate(`(() => window._customDcsHandlerCallStack)();`), [['C', [1, 2], 'some data'], ['B', [1, 2], 'some data']]); + assert.deepEqual(await page.evaluate(`window._customDcsHandlerCallStack`), [ + ['C', [1, 2], 'some data'], + ['B', [1, 2], 'some data'] + ]); + }); + it('async', async () => { + await page.evaluate(` + window.term.reset(); + window._customDcsHandlerCallStack = []; + const _customDcsHandlerA = window.term.parser.registerDcsHandler({intermediates:'+', final: 'q'}, (data, params) => { + window._customDcsHandlerCallStack.push(['A', params, data]); + return false; + }); + const _customDcsHandlerB = window.term.parser.registerDcsHandler({intermediates:'+', final: 'q'}, async (data, params) => { + await new Promise(res => setTimeout(res, 50)); + window._customDcsHandlerCallStack.push(['B', params, data]); + return false; + }); + const _customDcsHandlerC = window.term.parser.registerDcsHandler({intermediates:'+', final: 'q'}, (data, params) => { + window._customDcsHandlerCallStack.push(['C', params, data]); + return false; + }); + `); + await writeSync(page, '\x1bP1;2+qsome data\x1b\\\\'); + assert.deepEqual(await page.evaluate(`window._customDcsHandlerCallStack`), [ + ['C', [1, 2], 'some data'], + ['B', [1, 2], 'some data'], + ['A', [1, 2], 'some data'] + ]); }); }); - describe('addEscHandler', () => { + describe('registerEscHandler', () => { it('should respects return value', async () => { await page.evaluate(` window.term.reset(); window._customEscHandlerCallStack = []; - const _customEscHandlerA = window.term.parser.addEscHandler({intermediates:'(', final: 'B'}, () => { + const _customEscHandlerA = window.term.parser.registerEscHandler({intermediates:'(', final: 'B'}, () => { window._customEscHandlerCallStack.push('A'); return false; }); - const _customEscHandlerB = window.term.parser.addEscHandler({intermediates:'(', final: 'B'}, () => { + const _customEscHandlerB = window.term.parser.registerEscHandler({intermediates:'(', final: 'B'}, () => { window._customEscHandlerCallStack.push('B'); return true; }); - const _customEscHandlerC = window.term.parser.addEscHandler({intermediates:'(', final: 'B'}, () => { + const _customEscHandlerC = window.term.parser.registerEscHandler({intermediates:'(', final: 'B'}, () => { window._customEscHandlerCallStack.push('C'); return false; }); `); await writeSync(page, '\x1b(B'); - assert.deepEqual(await page.evaluate(`(() => window._customEscHandlerCallStack)();`), ['C', 'B']); + assert.deepEqual(await page.evaluate(`window._customEscHandlerCallStack`), ['C', 'B']); + }); + it('async', async () => { + await page.evaluate(` + window.term.reset(); + window._customEscHandlerCallStack = []; + const _customEscHandlerA = window.term.parser.registerEscHandler({intermediates:'(', final: 'Z'}, () => { + window._customEscHandlerCallStack.push('A'); + return false; + }); + const _customEscHandlerB = window.term.parser.registerEscHandler({intermediates:'(', final: 'Z'}, async () => { + await new Promise(res => setTimeout(res, 50)); + window._customEscHandlerCallStack.push('B'); + return false; + }); + const _customEscHandlerC = window.term.parser.registerEscHandler({intermediates:'(', final: 'Z'}, () => { + window._customEscHandlerCallStack.push('C'); + return false; + }); + `); + await writeSync(page, '\x1b(Z'); + assert.deepEqual(await page.evaluate(`window._customEscHandlerCallStack`), ['C', 'B', 'A']); }); }); - describe('addOscHandler', () => { + describe('registerOscHandler', () => { it('should respects return value', async () => { await page.evaluate(` window.term.reset(); window._customOscHandlerCallStack = []; - const _customOscHandlerA = window.term.parser.addOscHandler(1234, data => { + const _customOscHandlerA = window.term.parser.registerOscHandler(1234, data => { window._customOscHandlerCallStack.push(['A', data]); return false; }); - const _customOscHandlerB = window.term.parser.addOscHandler(1234, data => { + const _customOscHandlerB = window.term.parser.registerOscHandler(1234, data => { window._customOscHandlerCallStack.push(['B', data]); return true; }); - const _customOscHandlerC = window.term.parser.addOscHandler(1234, data => { + const _customOscHandlerC = window.term.parser.registerOscHandler(1234, data => { window._customOscHandlerCallStack.push(['C', data]); return false; }); `); await writeSync(page, '\x1b]1234;some data\x07'); - assert.deepEqual(await page.evaluate(`(() => window._customOscHandlerCallStack)();`), [['C', 'some data'], ['B', 'some data']]); + assert.deepEqual(await page.evaluate(`window._customOscHandlerCallStack`), [ + ['C', 'some data'], + ['B', 'some data'] + ]); + }); + it('async', async () => { + await page.evaluate(` + window.term.reset(); + window._customOscHandlerCallStack = []; + const _customOscHandlerA = window.term.parser.registerOscHandler(666, data => { + window._customOscHandlerCallStack.push(['A', data]); + return false; + }); + const _customOscHandlerB = window.term.parser.registerOscHandler(666, async data => { + await new Promise(res => setTimeout(res, 50)); + window._customOscHandlerCallStack.push(['B', data]); + return false; + }); + const _customOscHandlerC = window.term.parser.registerOscHandler(666, data => { + window._customOscHandlerCallStack.push(['C', data]); + return false; + }); + `); + await writeSync(page, '\x1b]666;some data\x07'); + assert.deepEqual(await page.evaluate(`window._customOscHandlerCallStack`), [ + ['C', 'some data'], + ['B', 'some data'], + ['A', 'some data'] + ]); }); }); }); diff --git a/test/api/Terminal.api.ts b/test/api/Terminal.api.ts index c5ce8f92..00598e47 100644 --- a/test/api/Terminal.api.ts +++ b/test/api/Terminal.api.ts @@ -4,10 +4,10 @@ */ import { assert } from 'chai'; -import { pollFor, timeout, writeSync, openTerminal, getBrowserType } from './TestUtils'; +import { pollFor, timeout, writeSync, openTerminal, launchBrowser } from './TestUtils'; import { Browser, Page } from 'playwright'; -const APP = 'http://127.0.0.1:3000/test'; +const APP = 'http://127.0.0.1:3001/test'; let browser: Browser; let page: Page; @@ -16,10 +16,7 @@ const height = 600; describe('API Integration Tests', function(): void { before(async () => { - const browserType = getBrowserType(); - browser = await browserType.launch({ - headless: process.argv.indexOf('--headless') !== -1 - }); + browser = await launchBrowser(); page = await (await browser.newContext()).newPage(); await page.setViewportSize({ width, height }); }); @@ -69,12 +66,9 @@ describe('API Integration Tests', function(): void { it('write - bytes (UTF8)', async () => { await openTerminal(page); await page.evaluate(` - // foo - window.term.write(new Uint8Array([102, 111, 111])); - // bar - window.term.write(new Uint8Array([98, 97, 114])); - // 文 - window.term.write(new Uint8Array([230, 150, 135])); + window.term.write(new Uint8Array([102, 111, 111])); // foo + window.term.write(new Uint8Array([98, 97, 114])); // bar + window.term.write(new Uint8Array([230, 150, 135])); // 文 `); await pollFor(page, `window.term.buffer.active.getLine(0).translateToString(true)`, 'foobar文'); }); @@ -82,12 +76,9 @@ describe('API Integration Tests', function(): void { it('write - bytes (UTF8) with callback', async () => { await openTerminal(page); await page.evaluate(` - // foo - window.term.write(new Uint8Array([102, 111, 111]), () => { window.__x = 'A'; }); - // bar - window.term.write(new Uint8Array([98, 97, 114]), () => { window.__x += 'B'; }); - // 文 - window.term.write(new Uint8Array([230, 150, 135]), () => { window.__x += 'C'; }); + window.term.write(new Uint8Array([102, 111, 111]), () => { window.__x = 'A'; }); // foo + window.term.write(new Uint8Array([98, 97, 114]), () => { window.__x += 'B'; }); // bar + window.term.write(new Uint8Array([230, 150, 135]), () => { window.__x += 'C'; }); // 文 `); await pollFor(page, `window.term.buffer.active.getLine(0).translateToString(true)`, 'foobar文'); await pollFor(page, `window.__x`, 'ABC'); @@ -410,6 +401,16 @@ describe('API Integration Tests', function(): void { await page.evaluate(`window.term.write('\\x1b]2;foo\\x9c')`); await pollFor(page, `window.calls`, ['foo']); }); + it('onBell', async () => { + await openTerminal(page); + await page.evaluate(` + window.calls = []; + window.term.onBell(() => window.calls.push(true)); + `); + await pollFor(page, `window.calls`, []); + await page.evaluate(`window.term.write('\\x07')`); + await pollFor(page, `window.calls`, [true]); + }); }); describe('buffer', () => { @@ -558,6 +559,98 @@ describe('API Integration Tests', function(): void { }); }); + describe('modes', () => { + it('defaults', async () => { + await openTerminal(page); + assert.deepStrictEqual(await page.evaluate(`window.term.modes`), { + applicationCursorKeysMode: false, + applicationKeypadMode: false, + bracketedPasteMode: false, + insertMode: false, + mouseTrackingMode: 'none', + originMode: false, + reverseWraparoundMode: false, + sendFocusMode: false, + wraparoundMode: true + }); + }); + it('applicationCursorKeysMode', async () => { + await openTerminal(page); + await writeSync(page, '\\x1b[?1h'); + assert.strictEqual(await page.evaluate(`window.term.modes.applicationCursorKeysMode`), true); + await writeSync(page, '\\x1b[?1l'); + assert.strictEqual(await page.evaluate(`window.term.modes.applicationCursorKeysMode`), false); + }); + it('applicationKeypadMode', async () => { + await openTerminal(page); + await writeSync(page, '\\x1b[?66h'); + assert.strictEqual(await page.evaluate(`window.term.modes.applicationKeypadMode`), true); + await writeSync(page, '\\x1b[?66l'); + assert.strictEqual(await page.evaluate(`window.term.modes.applicationKeypadMode`), false); + }); + it('bracketedPasteMode', async () => { + await openTerminal(page); + await writeSync(page, '\\x1b[?2004h'); + assert.strictEqual(await page.evaluate(`window.term.modes.bracketedPasteMode`), true); + await writeSync(page, '\\x1b[?2004l'); + assert.strictEqual(await page.evaluate(`window.term.modes.bracketedPasteMode`), false); + }); + it('insertMode', async () => { + await openTerminal(page); + await writeSync(page, '\\x1b[4h'); + assert.strictEqual(await page.evaluate(`window.term.modes.insertMode`), true); + await writeSync(page, '\\x1b[4l'); + assert.strictEqual(await page.evaluate(`window.term.modes.insertMode`), false); + }); + it('mouseTrackingMode', async () => { + await openTerminal(page); + await writeSync(page, '\\x1b[?9h'); + assert.strictEqual(await page.evaluate(`window.term.modes.mouseTrackingMode`), 'x10'); + await writeSync(page, '\\x1b[?9l'); + assert.strictEqual(await page.evaluate(`window.term.modes.mouseTrackingMode`), 'none'); + await writeSync(page, '\\x1b[?1000h'); + assert.strictEqual(await page.evaluate(`window.term.modes.mouseTrackingMode`), 'vt200'); + await writeSync(page, '\\x1b[?1000l'); + assert.strictEqual(await page.evaluate(`window.term.modes.mouseTrackingMode`), 'none'); + await writeSync(page, '\\x1b[?1002h'); + assert.strictEqual(await page.evaluate(`window.term.modes.mouseTrackingMode`), 'drag'); + await writeSync(page, '\\x1b[?1002l'); + assert.strictEqual(await page.evaluate(`window.term.modes.mouseTrackingMode`), 'none'); + await writeSync(page, '\\x1b[?1003h'); + assert.strictEqual(await page.evaluate(`window.term.modes.mouseTrackingMode`), 'any'); + await writeSync(page, '\\x1b[?1003l'); + assert.strictEqual(await page.evaluate(`window.term.modes.mouseTrackingMode`), 'none'); + }); + it('originMode', async () => { + await openTerminal(page); + await writeSync(page, '\\x1b[?6h'); + assert.strictEqual(await page.evaluate(`window.term.modes.originMode`), true); + await writeSync(page, '\\x1b[?6l'); + assert.strictEqual(await page.evaluate(`window.term.modes.originMode`), false); + }); + it('reverseWraparoundMode', async () => { + await openTerminal(page); + await writeSync(page, '\\x1b[?45h'); + assert.strictEqual(await page.evaluate(`window.term.modes.reverseWraparoundMode`), true); + await writeSync(page, '\\x1b[?45l'); + assert.strictEqual(await page.evaluate(`window.term.modes.reverseWraparoundMode`), false); + }); + it('sendFocusMode', async () => { + await openTerminal(page); + await writeSync(page, '\\x1b[?1004h'); + assert.strictEqual(await page.evaluate(`window.term.modes.sendFocusMode`), true); + await writeSync(page, '\\x1b[?1004l'); + assert.strictEqual(await page.evaluate(`window.term.modes.sendFocusMode`), false); + }); + it('wraparoundMode', async () => { + await openTerminal(page); + await writeSync(page, '\\x1b[?7h'); + assert.strictEqual(await page.evaluate(`window.term.modes.wraparoundMode`), true); + await writeSync(page, '\\x1b[?7l'); + assert.strictEqual(await page.evaluate(`window.term.modes.wraparoundMode`), false); + }); + }); + it('dispose', async () => { await page.evaluate(` window.term = new Terminal(); @@ -625,11 +718,11 @@ describe('API Integration Tests', function(): void { `); const dims = await getDimensions(); await moveMouseCell(page, dims, 5, 1); - await pollFor(page, `window.calls`, ['provide 1', 'match', 'hover']); + await timeout(100); await moveMouseCell(page, dims, 4, 1); await pollFor(page, `window.calls`, ['provide 1', 'match', 'hover', 'leave' ]); await moveMouseCell(page, dims, 7, 1); - await pollFor(page, `window.calls`, ['provide 1', 'match', 'hover', 'leave', 'hover']); + await timeout(100); await moveMouseCell(page, dims, 8, 1); await pollFor(page, `window.calls`, ['provide 1', 'match', 'hover', 'leave', 'hover', 'leave']); await page.evaluate(`window.disposable.dispose()`); diff --git a/test/api/TestUtils.ts b/test/api/TestUtils.ts index f36d2830..4fa98d43 100644 --- a/test/api/TestUtils.ts +++ b/test/api/TestUtils.ts @@ -6,8 +6,9 @@ import * as playwright from 'playwright'; import deepEqual = require('deep-equal'); import { ITerminalOptions } from 'xterm'; +import { deepStrictEqual, fail } from 'assert'; -export async function pollFor(page: playwright.Page, evalOrFn: string | (() => Promise), val: T, preFn?: () => Promise): Promise { +export async function pollFor(page: playwright.Page, evalOrFn: string | (() => Promise), val: T, preFn?: () => Promise, maxDuration?: number): Promise { if (preFn) { await preFn(); } @@ -18,8 +19,14 @@ export async function pollFor(page: playwright.Page, evalOrFn: string | (() = } if (!deepEqual(result, val)) { + if (maxDuration === undefined) { + maxDuration = 2000; + } + if (maxDuration <= 0) { + deepStrictEqual(result, val, 'pollFor max duration exceeded'); + } return new Promise(r => { - setTimeout(() => r(pollFor(page, evalOrFn, val, preFn)), 1); + setTimeout(() => r(pollFor(page, evalOrFn, val, preFn, maxDuration! - 10)), 10); }); } } @@ -60,3 +67,17 @@ export function getBrowserType(): playwright.BrowserType = { + headless: process.argv.includes('--headless'), + } + + const index = process.argv.indexOf('--executablePath'); + if(index > 0 && process.argv.length > index + 1 && typeof process.argv[index + 1] === 'string') { + options.executablePath = process.argv[index + 1]; + } + + return browserType.launch(options); +} diff --git a/test/benchmark/EscapeSequenceParser.benchmark.ts b/test/benchmark/EscapeSequenceParser.benchmark.ts index e4591dde..10c44797 100644 --- a/test/benchmark/EscapeSequenceParser.benchmark.ts +++ b/test/benchmark/EscapeSequenceParser.benchmark.ts @@ -6,9 +6,11 @@ import { perfContext, before, beforeEach, ThroughputRuntimeCase } from 'xterm-be import { EscapeSequenceParser } from 'common/parser/EscapeSequenceParser'; import { C0, C1 } from 'common/data/EscapeSequences'; -import { IDcsHandler, IParams } from 'common/parser/Types'; +import { IDcsHandler, IOscHandler, IParams } from 'common/parser/Types'; import { OscHandler } from 'common/parser/OscParser'; +import { DcsHandler } from '../../out/common/parser/DcsParser'; +const SIZE = 5000000; function toUtf32(s: string): Uint32Array { const result = new Uint32Array(s.length); @@ -18,10 +20,16 @@ function toUtf32(s: string): Uint32Array { return result; } -class DcsHandler implements IDcsHandler { +class FastDcsHandler implements IDcsHandler { public hook(params: IParams): void {} public put(data: Uint32Array, start: number, end: number): void {} - public unhook(): void {} + public unhook(success: boolean): boolean { return true; } +} + +class FastOscHandler implements IOscHandler { + public start(): void {} + public put(data: Uint32Array, start: number, end: number): void {} + public end(success: boolean): boolean { return true; } } @@ -32,252 +40,313 @@ perfContext('Parser throughput - 50MB data', () => { beforeEach(() => { parser = new EscapeSequenceParser(); parser.setPrintHandler((data, start, end) => {}); - parser.setCsiHandler({final: '@'}, params => {}); - parser.setCsiHandler({final: 'A'}, params => {}); - parser.setCsiHandler({final: 'B'}, params => {}); - parser.setCsiHandler({final: 'C'}, params => {}); - parser.setCsiHandler({final: 'D'}, params => {}); - parser.setCsiHandler({final: 'E'}, params => {}); - parser.setCsiHandler({final: 'F'}, params => {}); - parser.setCsiHandler({final: 'G'}, params => {}); - parser.setCsiHandler({final: 'H'}, params => {}); - parser.setCsiHandler({final: 'I'}, params => {}); - parser.setCsiHandler({final: 'J'}, params => {}); - parser.setCsiHandler({final: 'K'}, params => {}); - parser.setCsiHandler({final: 'L'}, params => {}); - parser.setCsiHandler({final: 'M'}, params => {}); - parser.setCsiHandler({final: 'P'}, params => {}); - parser.setCsiHandler({final: 'S'}, params => {}); - parser.setCsiHandler({final: 'T'}, params => {}); - parser.setCsiHandler({final: 'X'}, params => {}); - parser.setCsiHandler({final: 'Z'}, params => {}); - parser.setCsiHandler({final: '`'}, params => {}); - parser.setCsiHandler({final: 'a'}, params => {}); - parser.setCsiHandler({final: 'b'}, params => {}); - parser.setCsiHandler({final: 'c'}, params => {}); - parser.setCsiHandler({final: 'd'}, params => {}); - parser.setCsiHandler({final: 'e'}, params => {}); - parser.setCsiHandler({final: 'f'}, params => {}); - parser.setCsiHandler({final: 'g'}, params => {}); - parser.setCsiHandler({final: 'h'}, params => {}); - parser.setCsiHandler({final: 'l'}, params => {}); - parser.setCsiHandler({final: 'm'}, params => {}); - parser.setCsiHandler({final: 'n'}, params => {}); - parser.setCsiHandler({final: 'p'}, params => {}); - parser.setCsiHandler({final: 'q'}, params => {}); - parser.setCsiHandler({final: 'r'}, params => {}); - parser.setCsiHandler({final: 's'}, params => {}); - parser.setCsiHandler({final: 'u'}, params => {}); - parser.setExecuteHandler(C0.BEL, () => {}); - parser.setExecuteHandler(C0.LF, () => {}); - parser.setExecuteHandler(C0.VT, () => {}); - parser.setExecuteHandler(C0.FF, () => {}); - parser.setExecuteHandler(C0.CR, () => {}); - parser.setExecuteHandler(C0.BS, () => {}); - parser.setExecuteHandler(C0.HT, () => {}); - parser.setExecuteHandler(C0.SO, () => {}); - parser.setExecuteHandler(C0.SI, () => {}); - parser.setExecuteHandler(C1.IND, () => {}); - parser.setExecuteHandler(C1.NEL, () => {}); - parser.setExecuteHandler(C1.HTS, () => {}); - parser.setOscHandler(0, new OscHandler((data) => {})); - parser.setOscHandler(2, new OscHandler((data) => {})); - parser.setEscHandler({final: '7'}, () => {}); - parser.setEscHandler({final: '8'}, () => {}); - parser.setEscHandler({final: 'D'}, () => {}); - parser.setEscHandler({final: 'E'}, () => {}); - parser.setEscHandler({final: 'H'}, () => {}); - parser.setEscHandler({final: 'M'}, () => {}); - parser.setEscHandler({final: '='}, () => {}); - parser.setEscHandler({final: '>'}, () => {}); - parser.setEscHandler({final: 'c'}, () => {}); - parser.setEscHandler({final: 'n'}, () => {}); - parser.setEscHandler({final: 'o'}, () => {}); - parser.setEscHandler({final: '|'}, () => {}); - parser.setEscHandler({final: '}'}, () => {}); - parser.setEscHandler({final: '~'}, () => {}); - parser.setEscHandler({intermediates: '%', final: '@'}, () => {}); - parser.setEscHandler({intermediates: '%', final: 'G'}, () => {}); - parser.setDcsHandler({final: 'q'}, new DcsHandler()); + parser.registerCsiHandler({ final: '@' }, params => true); + parser.registerCsiHandler({ final: 'A' }, params => true); + parser.registerCsiHandler({ final: 'B' }, params => true); + parser.registerCsiHandler({ final: 'C' }, params => true); + parser.registerCsiHandler({ final: 'D' }, params => true); + parser.registerCsiHandler({ final: 'E' }, params => true); + parser.registerCsiHandler({ final: 'F' }, params => true); + parser.registerCsiHandler({ final: 'G' }, params => true); + parser.registerCsiHandler({ final: 'H' }, params => true); + parser.registerCsiHandler({ final: 'I' }, params => true); + parser.registerCsiHandler({ final: 'J' }, params => true); + parser.registerCsiHandler({ final: 'K' }, params => true); + parser.registerCsiHandler({ final: 'L' }, params => true); + parser.registerCsiHandler({ final: 'M' }, params => true); + parser.registerCsiHandler({ final: 'P' }, params => true); + parser.registerCsiHandler({ final: 'S' }, params => true); + parser.registerCsiHandler({ final: 'T' }, params => true); + parser.registerCsiHandler({ final: 'X' }, params => true); + parser.registerCsiHandler({ final: 'Z' }, params => true); + parser.registerCsiHandler({ final: '`' }, params => true); + parser.registerCsiHandler({ final: 'a' }, params => true); + parser.registerCsiHandler({ final: 'b' }, params => true); + parser.registerCsiHandler({ final: 'c' }, params => true); + parser.registerCsiHandler({ final: 'd' }, params => true); + parser.registerCsiHandler({ final: 'e' }, params => true); + parser.registerCsiHandler({ final: 'f' }, params => true); + parser.registerCsiHandler({ final: 'g' }, params => true); + parser.registerCsiHandler({ final: 'h' }, params => true); + parser.registerCsiHandler({ final: 'l' }, params => true); + parser.registerCsiHandler({ final: 'm' }, params => true); + parser.registerCsiHandler({ final: 'n' }, params => true); + parser.registerCsiHandler({ final: 'p' }, params => true); + parser.registerCsiHandler({ final: 'q' }, params => true); + parser.registerCsiHandler({ final: 'r' }, params => true); + parser.registerCsiHandler({ final: 's' }, params => true); + parser.registerCsiHandler({ final: 'u' }, params => true); + parser.setExecuteHandler(C0.BEL, () => true); + parser.setExecuteHandler(C0.LF, () => true); + parser.setExecuteHandler(C0.VT, () => true); + parser.setExecuteHandler(C0.FF, () => true); + parser.setExecuteHandler(C0.CR, () => true); + parser.setExecuteHandler(C0.BS, () => true); + parser.setExecuteHandler(C0.HT, () => true); + parser.setExecuteHandler(C0.SO, () => true); + parser.setExecuteHandler(C0.SI, () => true); + parser.setExecuteHandler(C1.IND, () => true); + parser.setExecuteHandler(C1.NEL, () => true); + parser.setExecuteHandler(C1.HTS, () => true); + parser.registerOscHandler(0, new OscHandler(data => true)); + parser.registerOscHandler(1, new FastOscHandler()); + parser.registerEscHandler({ final: '7' }, () => true); + parser.registerEscHandler({ final: '8' }, () => true); + parser.registerEscHandler({ final: 'D' }, () => true); + parser.registerEscHandler({ final: 'E' }, () => true); + parser.registerEscHandler({ final: 'H' }, () => true); + parser.registerEscHandler({ final: 'M' }, () => true); + parser.registerEscHandler({ final: '=' }, () => true); + parser.registerEscHandler({ final: '>' }, () => true); + parser.registerEscHandler({ final: 'c' }, () => true); + parser.registerEscHandler({ final: 'n' }, () => true); + parser.registerEscHandler({ final: 'o' }, () => true); + parser.registerEscHandler({ final: '|' }, () => true); + parser.registerEscHandler({ final: '}' }, () => true); + parser.registerEscHandler({ final: '~' }, () => true); + parser.registerEscHandler({ intermediates: '%', final: '@' }, () => true); + parser.registerEscHandler({ intermediates: '%', final: 'G' }, () => true); + parser.registerDcsHandler({ final: 'p' }, new DcsHandler(data => true)); + parser.registerDcsHandler({ final: 'q' }, new FastDcsHandler()); }); perfContext('PRINT - a', () => { before(() => { const data = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; let content = ''; - while (content.length < 50000000) { + while (content.length < SIZE) { content += data; } parsed = toUtf32(content); }); new ThroughputRuntimeCase('', async () => { parser.parse(parsed, parsed.length); - return {payloadSize: parsed.length}; - }, {fork: true}).showAverageThroughput(); + return { payloadSize: parsed.length }; + }, { fork: true }).showAverageThroughput(); }); perfContext('EXECUTE - \\n', () => { before(() => { const data = '\n\n\n\n\n\n\n'; let content = ''; - while (content.length < 50000000) { + while (content.length < SIZE) { content += data; } parsed = toUtf32(content); }); new ThroughputRuntimeCase('', () => { parser.parse(parsed, parsed.length); - return {payloadSize: parsed.length}; - }, {fork: true}).showAverageThroughput(); + return { payloadSize: parsed.length }; + }, { fork: true }).showAverageThroughput(); }); perfContext('ESCAPE - ESC E', () => { before(() => { const data = '\x1bE\x1bE\x1bE\x1bE\x1bE\x1bE\x1bE\x1bE\x1bE\x1bE'; let content = ''; - while (content.length < 50000000) { + while (content.length < SIZE) { content += data; } parsed = toUtf32(content); }); new ThroughputRuntimeCase('', () => { parser.parse(parsed, parsed.length); - return {payloadSize: parsed.length}; - }, {fork: true}).showAverageThroughput(); + return { payloadSize: parsed.length }; + }, { fork: true }).showAverageThroughput(); }); perfContext('ESCAPE with collect - ESC % G', () => { before(() => { const data = '\x1b%G\x1b%G\x1b%G\x1b%G\x1b%G\x1b%G\x1b%G\x1b%G\x1b%G\x1b%G'; let content = ''; - while (content.length < 50000000) { + while (content.length < SIZE) { content += data; } parsed = toUtf32(content); }); new ThroughputRuntimeCase('', () => { parser.parse(parsed, parsed.length); - return {payloadSize: parsed.length}; - }, {fork: true}).showAverageThroughput(); + return { payloadSize: parsed.length }; + }, { fork: true }).showAverageThroughput(); }); perfContext('CSI - CSI A', () => { before(() => { const data = '\x1b[A\x1b[A\x1b[A\x1b[A\x1b[A\x1b[A\x1b[A\x1b[A\x1b[A\x1b[A'; let content = ''; - while (content.length < 50000000) { + while (content.length < SIZE) { content += data; } parsed = toUtf32(content); }); new ThroughputRuntimeCase('', () => { parser.parse(parsed, parsed.length); - return {payloadSize: parsed.length}; - }, {fork: true}).showAverageThroughput(); + return { payloadSize: parsed.length }; + }, { fork: true }).showAverageThroughput(); }); perfContext('CSI with collect - CSI ? p', () => { before(() => { const data = '\x1b[?p\x1b[?p\x1b[?p\x1b[?p\x1b[?p\x1b[?p\x1b[?p\x1b[?p\x1b[?p\x1b[?p'; let content = ''; - while (content.length < 50000000) { + while (content.length < SIZE) { content += data; } parsed = toUtf32(content); }); new ThroughputRuntimeCase('', () => { parser.parse(parsed, parsed.length); - return {payloadSize: parsed.length}; - }, {fork: true}).showAverageThroughput(); + return { payloadSize: parsed.length }; + }, { fork: true }).showAverageThroughput(); }); perfContext('CSI with params (short) - CSI 1;2 m', () => { before(() => { const data = '\x1b[1;2m\x1b[1;2m\x1b[1;2m\x1b[1;2m\x1b[1;2m\x1b[1;2m\x1b[1;2m\x1b[1;2m\x1b[1;2m\x1b[1;2m'; let content = ''; - while (content.length < 50000000) { + while (content.length < SIZE) { content += data; } parsed = toUtf32(content); }); new ThroughputRuntimeCase('', () => { parser.parse(parsed, parsed.length); - return {payloadSize: parsed.length}; - }, {fork: true}).showAverageThroughput(); + return { payloadSize: parsed.length }; + }, { fork: true }).showAverageThroughput(); }); perfContext('CSI with params (long) - CSI 1;2;3;4;5;6;7;8;9;0 m', () => { before(() => { const data = '\x1b[1;2;3;4;5;6;7;8;9;0m\x1b[1;2;3;4;5;6;7;8;9;0m\x1b[1;2;3;4;5;6;7;8;9;0m'; let content = ''; - while (content.length < 50000000) { + while (content.length < SIZE) { content += data; } parsed = toUtf32(content); }); new ThroughputRuntimeCase('', () => { parser.parse(parsed, parsed.length); - return {payloadSize: parsed.length}; - }, {fork: true}).showAverageThroughput(); + return { payloadSize: parsed.length }; + }, { fork: true }).showAverageThroughput(); }); - perfContext('OSC (short) - OSC 0;hi ST', () => { + perfContext('OSC string interface (short seq) - OSC 0;hi ST', () => { before(() => { const data = '\x1b]0;hi\x1b\\\x1b]0;hi\x1b\\\x1b]0;hi\x1b\\\x1b]0;hi\x1b\\x1b]0;hi\x1b\\'; let content = ''; - while (content.length < 50000000) { + while (content.length < SIZE) { content += data; } parsed = toUtf32(content); }); new ThroughputRuntimeCase('', () => { parser.parse(parsed, parsed.length); - return {payloadSize: parsed.length}; - }, {fork: true}).showAverageThroughput(); + return { payloadSize: parsed.length }; + }, { fork: true }).showAverageThroughput(); }); - perfContext('OSC (long) - OSC 0; ST', () => { + perfContext('OSC string interface (long seq) - OSC 0; ST', () => { before(() => { const data = '\x1b]0;Lorem ipsum dolor sit amet, consetetur sadipscing elitr.\x1b\\'; let content = ''; - while (content.length < 50000000) { + while (content.length < SIZE) { content += data; } parsed = toUtf32(content); }); new ThroughputRuntimeCase('', () => { parser.parse(parsed, parsed.length); - return {payloadSize: parsed.length}; - }, {fork: true}).showAverageThroughput(); + return { payloadSize: parsed.length }; + }, { fork: true }).showAverageThroughput(); }); - perfContext('DCS (short)', () => { + perfContext('OSC class interface (short seq) - OSC 0;hi ST', () => { before(() => { - const data = '\x1bPq~~\x1b\\'; + const data = '\x1b]1;hi\x1b\\\x1b]1;hi\x1b\\\x1b]1;hi\x1b\\\x1b]1;hi\x1b\\x1b]1;hi\x1b\\'; let content = ''; - while (content.length < 50000000) { + while (content.length < SIZE) { + content += data; + } + parsed = toUtf32(content); + }); + new ThroughputRuntimeCase('', () => { + parser.parse(parsed, parsed.length); + return { payloadSize: parsed.length }; + }, { fork: true }).showAverageThroughput(); + }); + + perfContext('OSC class interface (long seq) - OSC 0; ST', () => { + before(() => { + const data = '\x1b]1;Lorem ipsum dolor sit amet, consetetur sadipscing elitr.\x1b\\'; + let content = ''; + while (content.length < SIZE) { + content += data; + } + parsed = toUtf32(content); + }); + new ThroughputRuntimeCase('', () => { + parser.parse(parsed, parsed.length); + return { payloadSize: parsed.length }; + }, { fork: true }).showAverageThroughput(); + }); + + perfContext('DCS string interface (short seq)', () => { + before(() => { + const data = '\x1bPphi\x1b\\\x1bPphi\x1b\\\x1bPphi\x1b\\\x1bPphi\x1b\\\x1bPphi\x1b\\'; + let content = ''; + while (content.length < SIZE) { content += data; } parsed = toUtf32(content); }); new ThroughputRuntimeCase('', async () => { parser.parse(parsed, parsed.length); - return {payloadSize: parsed.length}; - }, {fork: true}).showAverageThroughput(); + return { payloadSize: parsed.length }; + }, { fork: true }).showAverageThroughput(); }); - perfContext('DCS (long)', () => { + perfContext('DCS string interface (long seq)', () => { before(() => { - const data = '\x1bPq#0;2;0;0;0#1;2;100;100;0#2;2;0;100;0#1~~@@vv@@~~@@~~$#2??}}GG}}??}}??-#1!14@\x1b\\'; + const data = '\x1bPpLorem ipsum dolor sit amet, consetetur sadipscing elitr.\x1b\\'; let content = ''; - while (content.length < 50000000) { + while (content.length < SIZE) { content += data; } parsed = toUtf32(content); }); new ThroughputRuntimeCase('', async () => { parser.parse(parsed, parsed.length); - return {payloadSize: parsed.length}; - }, {fork: true}).showAverageThroughput(); + return { payloadSize: parsed.length }; + }, { fork: true }).showAverageThroughput(); + }); + + perfContext('DCS class interface (short seq)', () => { + before(() => { + const data = '\x1bPqhi\x1b\\\x1bPqhi\x1b\\\x1bPqhi\x1b\\\x1bPqhi\x1b\\\x1bPqhi\x1b\\'; + let content = ''; + while (content.length < SIZE) { + content += data; + } + parsed = toUtf32(content); + }); + new ThroughputRuntimeCase('', async () => { + parser.parse(parsed, parsed.length); + return { payloadSize: parsed.length }; + }, { fork: true }).showAverageThroughput(); + }); + + perfContext('DCS class interface (long seq)', () => { + before(() => { + const data = '\x1bPqLorem ipsum dolor sit amet, consetetur sadipscing elitr.\x1b\\'; + let content = ''; + while (content.length < SIZE) { + content += data; + } + parsed = toUtf32(content); + }); + new ThroughputRuntimeCase('', async () => { + parser.parse(parsed, parsed.length); + return { payloadSize: parsed.length }; + }, { fork: true }).showAverageThroughput(); }); }); diff --git a/test/benchmark/Terminal.benchmark.ts b/test/benchmark/Terminal.benchmark.ts index 76139cb6..578a83d0 100644 --- a/test/benchmark/Terminal.benchmark.ts +++ b/test/benchmark/Terminal.benchmark.ts @@ -29,7 +29,7 @@ perfContext('Terminal: ls -lR /usr/lib', () => { chunks.push(data as unknown as Buffer); length += data.length; }); - await new Promise(resolve => p.on('exit', () => resolve())); + await new Promise(resolve => p.on('exit', () => resolve())); contentUtf8 = Buffer.concat(chunks, length); // translate to content string const buffer = new Uint32Array(contentUtf8.length); @@ -44,25 +44,25 @@ perfContext('Terminal: ls -lR /usr/lib', () => { } }); - perfContext('write', () => { + perfContext('write/string/async', () => { let terminal: Terminal; before(() => { - terminal = new Terminal({cols: 80, rows: 25, scrollback: 1000}); + terminal = new Terminal({ cols: 80, rows: 25, scrollback: 1000 }); }); - new ThroughputRuntimeCase('', () => { - terminal.writeSync(content); - return {payloadSize: contentUtf8.length}; - }, {fork: false}).showAverageThroughput(); + new ThroughputRuntimeCase('', async () => { + await new Promise(res => terminal.write(content, res)); + return { payloadSize: contentUtf8.length }; + }, { fork: false }).showAverageThroughput(); }); - perfContext('writeUtf8', () => { + perfContext('write/Utf8/async', () => { let terminal: Terminal; before(() => { - terminal = new Terminal({cols: 80, rows: 25, scrollback: 1000}); + terminal = new Terminal({ cols: 80, rows: 25, scrollback: 1000 }); }); - new ThroughputRuntimeCase('', () => { - terminal.writeSync(content); - return {payloadSize: contentUtf8.length}; - }, {fork: false}).showAverageThroughput(); + new ThroughputRuntimeCase('', async () => { + await new Promise(res => terminal.write(content, res)); + return { payloadSize: contentUtf8.length }; + }, { fork: false }).showAverageThroughput(); }); }); diff --git a/tsconfig.all.json b/tsconfig.all.json index 009cd498..6fa02446 100644 --- a/tsconfig.all.json +++ b/tsconfig.all.json @@ -3,6 +3,7 @@ "include": [], "references": [ { "path": "./src/browser" }, + { "path": "./src/headless" }, { "path": "./test/api" }, { "path": "./test/benchmark" }, { "path": "./addons/xterm-addon-attach" }, diff --git a/typings/xterm-headless.d.ts b/typings/xterm-headless.d.ts new file mode 100644 index 00000000..84b715e8 --- /dev/null +++ b/typings/xterm-headless.d.ts @@ -0,0 +1,1315 @@ +/** + * @license MIT + * + * This contains the type declarations for the xterm.js library. Note that + * some interfaces differ between this file and the actual implementation in + * src/, that's because this file declares the *public* API which is intended + * to be stable and consumed by external programs. + */ + +declare module 'xterm-headless' { + /** + * A string representing log level. + */ + export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'off'; + + /** + * An object containing start up options for the terminal. + */ + export interface ITerminalOptions { + /** + * Whether to allow the use of proposed API. When false, any usage of APIs + * marked as experimental/proposed will throw an error. This defaults to + * true currently, but will change to false in v5.0. + */ + allowProposedApi?: boolean; + + /** + * Whether background should support non-opaque color. It must be set before + * executing the `Terminal.open()` method and can't be changed later without + * executing it again. Note that enabling this can negatively impact + * performance. + */ + allowTransparency?: boolean; + + /** + * If enabled, alt + click will move the prompt cursor to position + * underneath the mouse. The default is true. + */ + altClickMovesCursor?: boolean; + + /** + * A data uri of the sound to use for the bell when `bellStyle = 'sound'`. + */ + bellSound?: string; + + /** + * The type of the bell notification the terminal will use. + */ + bellStyle?: 'none' | 'sound'; + + /** + * When enabled the cursor will be set to the beginning of the next line + * with every new line. This is equivalent to sending '\r\n' for each '\n'. + * Normally the termios settings of the underlying PTY deals with the + * translation of '\n' to '\r\n' and this setting should not be used. If you + * deal with data from a non-PTY related source, this settings might be + * useful. + */ + convertEol?: boolean; + + /** + * The number of columns in the terminal. + */ + cols?: number; + + /** + * Whether the cursor blinks. + */ + cursorBlink?: boolean; + + /** + * The style of the cursor. + */ + cursorStyle?: 'block' | 'underline' | 'bar'; + + /** + * The width of the cursor in CSS pixels when `cursorStyle` is set to 'bar'. + */ + cursorWidth?: number; + + /** + * Whether to draw custom glyphs for block element and box drawing characters instead of using + * the font. This should typically result in better rendering with continuous lines, even when + * line height and letter spacing is used. Note that this doesn't work with the DOM renderer + * which renders all characters using the font. The default is true. + */ + customGlyphs?: boolean; + + /** + * Whether input should be disabled. + */ + disableStdin?: boolean; + + /** + * Whether to draw bold text in bright colors. The default is true. + */ + drawBoldTextInBrightColors?: boolean; + + /** + * The modifier key hold to multiply scroll speed. + */ + fastScrollModifier?: 'alt' | 'ctrl' | 'shift' | undefined; + + /** + * The spacing in whole pixels between characters. + */ + letterSpacing?: number; + + /** + * The line height used to render text. + */ + lineHeight?: number; + + /** + * The duration in milliseconds before link tooltip events fire when + * hovering on a link. + * @deprecated This will be removed when the link matcher API is removed. + */ + linkTooltipHoverDuration?: number; + + /** + * What log level to use, this will log for all levels below and including + * what is set: + * + * 1. debug + * 2. info (default) + * 3. warn + * 4. error + * 5. off + */ + logLevel?: LogLevel; + + /** + * Whether to treat option as the meta key. + */ + macOptionIsMeta?: boolean; + + /** + * Whether holding a modifier key will force normal selection behavior, + * regardless of whether the terminal is in mouse events mode. This will + * also prevent mouse events from being emitted by the terminal. For + * example, this allows you to use xterm.js' regular selection inside tmux + * with mouse mode enabled. + */ + macOptionClickForcesSelection?: boolean; + + /** + * The minimum contrast ratio for text in the terminal, setting this will + * change the foreground color dynamically depending on whether the contrast + * ratio is met. Example values: + * + * - 1: The default, do nothing. + * - 4.5: Minimum for WCAG AA compliance. + * - 7: Minimum for WCAG AAA compliance. + * - 21: White on black or black on white. + */ + minimumContrastRatio?: number; + + /** + * Whether to select the word under the cursor on right click, this is + * standard behavior in a lot of macOS applications. + */ + rightClickSelectsWord?: boolean; + + /** + * The number of rows in the terminal. + */ + rows?: number; + + /** + * Whether screen reader support is enabled. When on this will expose + * supporting elements in the DOM to support NVDA on Windows and VoiceOver + * on macOS. + */ + screenReaderMode?: boolean; + + /** + * The amount of scrollback in the terminal. Scrollback is the amount of + * rows that are retained when lines are scrolled beyond the initial + * viewport. + */ + scrollback?: number; + + /** + * The scrolling speed multiplier used for adjusting normal scrolling speed. + */ + scrollSensitivity?: number; + + /** + * The size of tab stops in the terminal. + */ + tabStopWidth?: number; + + /** + * The color theme of the terminal. + */ + theme?: ITheme; + + /** + * Whether "Windows mode" is enabled. Because Windows backends winpty and + * conpty operate by doing line wrapping on their side, xterm.js does not + * have access to wrapped lines. When Windows mode is enabled the following + * changes will be in effect: + * + * - Reflow is disabled. + * - Lines are assumed to be wrapped if the last character of the line is + * not whitespace. + */ + windowsMode?: boolean; + + /** + * A string containing all characters that are considered word separated by the + * double click to select work logic. + */ + wordSeparator?: string; + + /** + * Enable various window manipulation and report features. + * All features are disabled by default for security reasons. + */ + windowOptions?: IWindowOptions; + } + + /** + * Contains colors to theme the terminal with. + */ + export interface ITheme { + /** The default foreground color */ + foreground?: string; + /** The default background color */ + background?: string; + /** The cursor color */ + cursor?: string; + /** The accent color of the cursor (fg color for a block cursor) */ + cursorAccent?: string; + /** The selection background color (can be transparent) */ + selection?: string; + /** ANSI black (eg. `\x1b[30m`) */ + black?: string; + /** ANSI red (eg. `\x1b[31m`) */ + red?: string; + /** ANSI green (eg. `\x1b[32m`) */ + green?: string; + /** ANSI yellow (eg. `\x1b[33m`) */ + yellow?: string; + /** ANSI blue (eg. `\x1b[34m`) */ + blue?: string; + /** ANSI magenta (eg. `\x1b[35m`) */ + magenta?: string; + /** ANSI cyan (eg. `\x1b[36m`) */ + cyan?: string; + /** ANSI white (eg. `\x1b[37m`) */ + white?: string; + /** ANSI bright black (eg. `\x1b[1;30m`) */ + brightBlack?: string; + /** ANSI bright red (eg. `\x1b[1;31m`) */ + brightRed?: string; + /** ANSI bright green (eg. `\x1b[1;32m`) */ + brightGreen?: string; + /** ANSI bright yellow (eg. `\x1b[1;33m`) */ + brightYellow?: string; + /** ANSI bright blue (eg. `\x1b[1;34m`) */ + brightBlue?: string; + /** ANSI bright magenta (eg. `\x1b[1;35m`) */ + brightMagenta?: string; + /** ANSI bright cyan (eg. `\x1b[1;36m`) */ + brightCyan?: string; + /** ANSI bright white (eg. `\x1b[1;37m`) */ + brightWhite?: string; + } + + /** + * An object that can be disposed via a dispose function. + */ + export interface IDisposable { + dispose(): void; + } + + /** + * An event that can be listened to. + * @returns an `IDisposable` to stop listening. + */ + export interface IEvent { + (listener: (arg1: T, arg2: U) => any): IDisposable; + } + + /** + * Represents a specific line in the terminal that is tracked when scrollback + * is trimmed and lines are added or removed. This is a single line that may + * be part of a larger wrapped line. + */ + export interface IMarker extends IDisposable { + /** + * A unique identifier for this marker. + */ + readonly id: number; + + /** + * Whether this marker is disposed. + */ + readonly isDisposed: boolean; + + /** + * The actual line index in the buffer at this point in time. This is set to + * -1 if the marker has been disposed. + */ + readonly line: number; + + /** + * Event listener to get notified when the marker gets disposed. Automatic disposal + * might happen for a marker, that got invalidated by scrolling out or removal of + * a line from the buffer. + */ + onDispose: IEvent; + } + + /** + * The set of localizable strings. + */ + export interface ILocalizableStrings { + /** + * The aria label for the underlying input textarea for the terminal. + */ + promptLabel: string; + + /** + * Announcement for when line reading is suppressed due to too many lines + * being printed to the terminal when `screenReaderMode` is enabled. + */ + tooMuchOutput: string; + } + + /** + * Enable various window manipulation and report features (CSI Ps ; Ps ; Ps t). + * + * Most settings have no default implementation, as they heavily rely on + * the embedding environment. + * + * To implement a feature, create a custom CSI hook like this: + * ```ts + * term.parser.addCsiHandler({final: 't'}, params => { + * const ps = params[0]; + * switch (ps) { + * case XY: + * ... // your implementation for option XY + * return true; // signal Ps=XY was handled + * } + * return false; // any Ps that was not handled + * }); + * ``` + * + * Note on security: + * Most features are meant to deal with some information of the host machine + * where the terminal runs on. This is seen as a security risk possibly leaking + * sensitive data of the host to the program in the terminal. Therefore all options + * (even those without a default implementation) are guarded by the boolean flag + * and disabled by default. + */ + export interface IWindowOptions { + /** + * Ps=1 De-iconify window. + * No default implementation. + */ + restoreWin?: boolean; + /** + * Ps=2 Iconify window. + * No default implementation. + */ + minimizeWin?: boolean; + /** + * Ps=3 ; x ; y + * Move window to [x, y]. + * No default implementation. + */ + setWinPosition?: boolean; + /** + * Ps = 4 ; height ; width + * Resize the window to given `height` and `width` in pixels. + * Omitted parameters should reuse the current height or width. + * Zero parameters should use the display's height or width. + * No default implementation. + */ + setWinSizePixels?: boolean; + /** + * Ps=5 Raise the window to the front of the stacking order. + * No default implementation. + */ + raiseWin?: boolean; + /** + * Ps=6 Lower the xterm window to the bottom of the stacking order. + * No default implementation. + */ + lowerWin?: boolean; + /** Ps=7 Refresh the window. */ + refreshWin?: boolean; + /** + * Ps = 8 ; height ; width + * Resize the text area to given height and width in characters. + * Omitted parameters should reuse the current height or width. + * Zero parameters use the display's height or width. + * No default implementation. + */ + setWinSizeChars?: boolean; + /** + * Ps=9 ; 0 Restore maximized window. + * Ps=9 ; 1 Maximize window (i.e., resize to screen size). + * Ps=9 ; 2 Maximize window vertically. + * Ps=9 ; 3 Maximize window horizontally. + * No default implementation. + */ + maximizeWin?: boolean; + /** + * Ps=10 ; 0 Undo full-screen mode. + * Ps=10 ; 1 Change to full-screen. + * Ps=10 ; 2 Toggle full-screen. + * No default implementation. + */ + fullscreenWin?: boolean; + /** Ps=11 Report xterm window state. + * If the xterm window is non-iconified, it returns "CSI 1 t". + * If the xterm window is iconified, it returns "CSI 2 t". + * No default implementation. + */ + getWinState?: boolean; + /** + * Ps=13 Report xterm window position. Result is "CSI 3 ; x ; y t". + * Ps=13 ; 2 Report xterm text-area position. Result is "CSI 3 ; x ; y t". + * No default implementation. + */ + getWinPosition?: boolean; + /** + * Ps=14 Report xterm text area size in pixels. Result is "CSI 4 ; height ; width t". + * Ps=14 ; 2 Report xterm window size in pixels. Result is "CSI 4 ; height ; width t". + * Has a default implementation. + */ + getWinSizePixels?: boolean; + /** + * Ps=15 Report size of the screen in pixels. Result is "CSI 5 ; height ; width t". + * No default implementation. + */ + getScreenSizePixels?: boolean; + /** + * Ps=16 Report xterm character cell size in pixels. Result is "CSI 6 ; height ; width t". + * Has a default implementation. + */ + getCellSizePixels?: boolean; + /** + * Ps=18 Report the size of the text area in characters. Result is "CSI 8 ; height ; width t". + * Has a default implementation. + */ + getWinSizeChars?: boolean; + /** + * Ps=19 Report the size of the screen in characters. Result is "CSI 9 ; height ; width t". + * No default implementation. + */ + getScreenSizeChars?: boolean; + /** + * Ps=20 Report xterm window's icon label. Result is "OSC L label ST". + * No default implementation. + */ + getIconTitle?: boolean; + /** + * Ps=21 Report xterm window's title. Result is "OSC l label ST". + * No default implementation. + */ + getWinTitle?: boolean; + /** + * Ps=22 ; 0 Save xterm icon and window title on stack. + * Ps=22 ; 1 Save xterm icon title on stack. + * Ps=22 ; 2 Save xterm window title on stack. + * All variants have a default implementation. + */ + pushTitle?: boolean; + /** + * Ps=23 ; 0 Restore xterm icon and window title from stack. + * Ps=23 ; 1 Restore xterm icon title from stack. + * Ps=23 ; 2 Restore xterm window title from stack. + * All variants have a default implementation. + */ + popTitle?: boolean; + /** + * Ps>=24 Resize to Ps lines (DECSLPP). + * DECSLPP is not implemented. This settings is also used to + * enable / disable DECCOLM (earlier variant of DECSLPP). + */ + setWinLines?: boolean; + } + + /** + * The class that represents an xterm.js terminal. + */ + export class Terminal implements IDisposable { + /** + * The number of rows in the terminal's viewport. Use + * `ITerminalOptions.rows` to set this in the constructor and + * `Terminal.resize` for when the terminal exists. + */ + readonly rows: number; + + /** + * The number of columns in the terminal's viewport. Use + * `ITerminalOptions.cols` to set this in the constructor and + * `Terminal.resize` for when the terminal exists. + */ + readonly cols: number; + + /** + * (EXPERIMENTAL) The terminal's current buffer, this might be either the + * normal buffer or the alt buffer depending on what's running in the + * terminal. + */ + readonly buffer: IBufferNamespace; + + /** + * (EXPERIMENTAL) Get all markers registered against the buffer. If the alt + * buffer is active this will always return []. + */ + readonly markers: ReadonlyArray; + + /** + * (EXPERIMENTAL) Get the parser interface to register + * custom escape sequence handlers. + */ + readonly parser: IParser; + + /** + * (EXPERIMENTAL) Get the Unicode handling interface + * to register and switch Unicode version. + */ + readonly unicode: IUnicodeHandling; + + /** + * Gets the terminal modes as set by SM/DECSET. + */ + readonly modes: IModes; + + /** + * Natural language strings that can be localized. + */ + static strings: ILocalizableStrings; + + /** + * Creates a new `Terminal` object. + * + * @param options An object containing a set of options. + */ + constructor(options?: ITerminalOptions); + + /** + * Adds an event listener for when the bell is triggered. + * @returns an `IDisposable` to stop listening. + */ + onBell: IEvent; + + /** + * Adds an event listener for when a binary event fires. This is used to + * enable non UTF-8 conformant binary messages to be sent to the backend. + * Currently this is only used for a certain type of mouse reports that + * happen to be not UTF-8 compatible. + * The event value is a JS string, pass it to the underlying pty as + * binary data, e.g. `pty.write(Buffer.from(data, 'binary'))`. + * @returns an `IDisposable` to stop listening. + */ + onBinary: IEvent; + + /** + * Adds an event listener for the cursor moves. + * @returns an `IDisposable` to stop listening. + */ + onCursorMove: IEvent; + + /** + * Adds an event listener for when a data event fires. This happens for + * example when the user types or pastes into the terminal. The event value + * is whatever `string` results, in a typical setup, this should be passed + * on to the backing pty. + * @returns an `IDisposable` to stop listening. + */ + onData: IEvent; + + /** + * Adds an event listener for when a line feed is added. + * @returns an `IDisposable` to stop listening. + */ + onLineFeed: IEvent; + + /** + * Adds an event listener for when the terminal is resized. The event value + * contains the new size. + * @returns an `IDisposable` to stop listening. + */ + onResize: IEvent<{ cols: number, rows: number }>; + + /** + * Adds an event listener for when a scroll occurs. The event value is the + * new position of the viewport. + * @returns an `IDisposable` to stop listening. + */ + onScroll: IEvent; + + /** + * Adds an event listener for when an OSC 0 or OSC 2 title change occurs. + * The event value is the new title. + * @returns an `IDisposable` to stop listening. + */ + onTitleChange: IEvent; + + /** + * Resizes the terminal. It's best practice to debounce calls to resize, + * this will help ensure that the pty can respond to the resize event + * before another one occurs. + * @param x The number of columns to resize to. + * @param y The number of rows to resize to. + */ + resize(columns: number, rows: number): void; + + /** + * (EXPERIMENTAL) Adds a marker to the normal buffer and returns it. If the + * alt buffer is active, undefined is returned. + * @param cursorYOffset The y position offset of the marker from the cursor. + * @returns The new marker or undefined. + */ + registerMarker(cursorYOffset: number): IMarker | undefined; + + /** + * @deprecated use `registerMarker` instead. + */ + addMarker(cursorYOffset: number): IMarker | undefined; + + /* + * Disposes of the terminal, detaching it from the DOM and removing any + * active listeners. + */ + dispose(): void; + + /** + * Scroll the display of the terminal + * @param amount The number of lines to scroll down (negative scroll up). + */ + scrollLines(amount: number): void; + + /** + * Scroll the display of the terminal by a number of pages. + * @param pageCount The number of pages to scroll (negative scrolls up). + */ + scrollPages(pageCount: number): void; + + /** + * Scrolls the display of the terminal to the top. + */ + scrollToTop(): void; + + /** + * Scrolls the display of the terminal to the bottom. + */ + scrollToBottom(): void; + + /** + * Scrolls to a line within the buffer. + * @param line The 0-based line index to scroll to. + */ + scrollToLine(line: number): void; + + /** + * Clear the entire buffer, making the prompt line the new first line. + */ + clear(): void; + + /** + * Write data to the terminal. + * @param data The data to write to the terminal. This can either be raw + * bytes given as Uint8Array from the pty or a string. Raw bytes will always + * be treated as UTF-8 encoded, string data as UTF-16. + * @param callback Optional callback that fires when the data was processed + * by the parser. + */ + write(data: string | Uint8Array, callback?: () => void): void; + + /** + * Writes data to the terminal, followed by a break line character (\n). + * @param data The data to write to the terminal. This can either be raw + * bytes given as Uint8Array from the pty or a string. Raw bytes will always + * be treated as UTF-8 encoded, string data as UTF-16. + * @param callback Optional callback that fires when the data was processed + * by the parser. + */ + writeln(data: string | Uint8Array, callback?: () => void): void; + + /** + * Write UTF8 data to the terminal. + * @param data The data to write to the terminal. + * @param callback Optional callback when data was processed. + * @deprecated use `write` instead + */ + writeUtf8(data: Uint8Array, callback?: () => void): void; + + /** + * Retrieves an option's value from the terminal. + * @param key The option key. + */ + getOption(key: 'bellSound' | 'bellStyle' | 'cursorStyle' | 'fontFamily' | 'logLevel' | 'rendererType' | 'termName' | 'wordSeparator'): string; + /** + * Retrieves an option's value from the terminal. + * @param key The option key. + */ + getOption(key: 'allowTransparency' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'disableStdin' | 'macOptionIsMeta' | 'rightClickSelectsWord' | 'popOnBell' | 'visualBell' | 'windowsMode'): boolean; + /** + * Retrieves an option's value from the terminal. + * @param key The option key. + */ + getOption(key: 'cols' | 'fontSize' | 'letterSpacing' | 'lineHeight' | 'rows' | 'tabStopWidth' | 'scrollback'): number; + /** + * Retrieves an option's value from the terminal. + * @param key The option key. + */ + getOption(key: string): any; + + /** + * Sets an option on the terminal. + * @param key The option key. + * @param value The option value. + */ + setOption(key: 'fontFamily' | 'termName' | 'bellSound' | 'wordSeparator', value: string): void; + /** + * Sets an option on the terminal. + * @param key The option key. + * @param value The option value. + */ + setOption(key: 'fontWeight' | 'fontWeightBold', value: null | 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900' | number): void; + /** + * Sets an option on the terminal. + * @param key The option key. + * @param value The option value. + */ + setOption(key: 'logLevel', value: LogLevel): void; + /** + * Sets an option on the terminal. + * @param key The option key. + * @param value The option value. + */ + setOption(key: 'bellStyle', value: null | 'none' | 'visual' | 'sound' | 'both'): void; + /** + * Sets an option on the terminal. + * @param key The option key. + * @param value The option value. + */ + setOption(key: 'cursorStyle', value: null | 'block' | 'underline' | 'bar'): void; + /** + * Sets an option on the terminal. + * @param key The option key. + * @param value The option value. + */ + setOption(key: 'allowTransparency' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'disableStdin' | 'macOptionIsMeta' | 'popOnBell' | 'rightClickSelectsWord' | 'visualBell' | 'windowsMode', value: boolean): void; + /** + * Sets an option on the terminal. + * @param key The option key. + * @param value The option value. + */ + setOption(key: 'fontSize' | 'letterSpacing' | 'lineHeight' | 'tabStopWidth' | 'scrollback', value: number): void; + /** + * Sets an option on the terminal. + * @param key The option key. + * @param value The option value. + */ + setOption(key: 'theme', value: ITheme): void; + /** + * Sets an option on the terminal. + * @param key The option key. + * @param value The option value. + */ + setOption(key: 'cols' | 'rows', value: number): void; + /** + * Sets an option on the terminal. + * @param key The option key. + * @param value The option value. + */ + setOption(key: string, value: any): void; + + /** + * Perform a full reset (RIS, aka '\x1bc'). + */ + reset(): void; + + /** + * Loads an addon into this instance of xterm.js. + * @param addon The addon to load. + */ + loadAddon(addon: ITerminalAddon): void; + } + + /** + * An addon that can provide additional functionality to the terminal. + */ + export interface ITerminalAddon extends IDisposable { + /** + * This is called when the addon is activated. + */ + activate(terminal: Terminal): void; + } + + /** + * An object representing a selection within the terminal. + */ + interface ISelectionPosition { + /** + * The start column of the selection. + */ + startColumn: number; + + /** + * The start row of the selection. + */ + startRow: number; + + /** + * The end column of the selection. + */ + endColumn: number; + + /** + * The end row of the selection. + */ + endRow: number; + } + + /** + * An object representing a range within the viewport of the terminal. + */ + export interface IViewportRange { + /** + * The start of the range. + */ + start: IViewportRangePosition; + + /** + * The end of the range. + */ + end: IViewportRangePosition; + } + + /** + * An object representing a cell position within the viewport of the terminal. + */ + interface IViewportRangePosition { + /** + * The x position of the cell. This is a 0-based index that refers to the + * space in between columns, not the column itself. Index 0 refers to the + * left side of the viewport, index `Terminal.cols` refers to the right side + * of the viewport. This can be thought of as how a cursor is positioned in + * a text editor. + */ + x: number; + + /** + * The y position of the cell. This is a 0-based index that refers to a + * specific row. + */ + y: number; + } + + /** + * A range within a buffer. + */ + interface IBufferRange { + /** + * The start position of the range. + */ + start: IBufferCellPosition; + + /** + * The end position of the range. + */ + end: IBufferCellPosition; + } + + /** + * A position within a buffer. + */ + interface IBufferCellPosition { + /** + * The x position within the buffer. + */ + x: number; + + /** + * The y position within the buffer. + */ + y: number; + } + + /** + * Represents a terminal buffer. + */ + interface IBuffer { + /** + * The type of the buffer. + */ + readonly type: 'normal' | 'alternate'; + + /** + * The y position of the cursor. This ranges between `0` (when the + * cursor is at baseY) and `Terminal.rows - 1` (when the cursor is on the + * last row). + */ + readonly cursorY: number; + + /** + * The x position of the cursor. This ranges between `0` (left side) and + * `Terminal.cols` (after last cell of the row). + */ + readonly cursorX: number; + + /** + * The line within the buffer where the top of the viewport is. + */ + readonly viewportY: number; + + /** + * The line within the buffer where the top of the bottom page is (when + * fully scrolled down). + */ + readonly baseY: number; + + /** + * The amount of lines in the buffer. + */ + readonly length: number; + + /** + * Gets a line from the buffer, or undefined if the line index does not + * exist. + * + * Note that the result of this function should be used immediately after + * calling as when the terminal updates it could lead to unexpected + * behavior. + * + * @param y The line index to get. + */ + getLine(y: number): IBufferLine | undefined; + + /** + * Creates an empty cell object suitable as a cell reference in + * `line.getCell(x, cell)`. Use this to avoid costly recreation of + * cell objects when dealing with tons of cells. + */ + getNullCell(): IBufferCell; + } + + /** + * Represents the terminal's set of buffers. + */ + interface IBufferNamespace { + /** + * The active buffer, this will either be the normal or alternate buffers. + */ + readonly active: IBuffer; + + /** + * The normal buffer. + */ + readonly normal: IBuffer; + + /** + * The alternate buffer, this becomes the active buffer when an application + * enters this mode via DECSET (`CSI ? 4 7 h`) + */ + readonly alternate: IBuffer; + + /** + * Adds an event listener for when the active buffer changes. + * @returns an `IDisposable` to stop listening. + */ + onBufferChange: IEvent; + } + + /** + * Represents a line in the terminal's buffer. + */ + interface IBufferLine { + /** + * Whether the line is wrapped from the previous line. + */ + readonly isWrapped: boolean; + + /** + * The length of the line, all call to getCell beyond the length will result + * in `undefined`. + */ + readonly length: number; + + /** + * Gets a cell from the line, or undefined if the line index does not exist. + * + * Note that the result of this function should be used immediately after + * calling as when the terminal updates it could lead to unexpected + * behavior. + * + * @param x The character index to get. + * @param cell Optional cell object to load data into for performance + * reasons. This is mainly useful when every cell in the buffer is being + * looped over to avoid creating new objects for every cell. + */ + getCell(x: number, cell?: IBufferCell): IBufferCell | undefined; + + /** + * Gets the line as a string. Note that this is gets only the string for the + * line, not taking isWrapped into account. + * + * @param trimRight Whether to trim any whitespace at the right of the line. + * @param startColumn The column to start from (inclusive). + * @param endColumn The column to end at (exclusive). + */ + translateToString(trimRight?: boolean, startColumn?: number, endColumn?: number): string; + } + + /** + * Represents a single cell in the terminal's buffer. + */ + interface IBufferCell { + /** + * The width of the character. Some examples: + * + * - `1` for most cells. + * - `2` for wide character like CJK glyphs. + * - `0` for cells immediately following cells with a width of `2`. + */ + getWidth(): number; + + /** + * The character(s) within the cell. Examples of what this can contain: + * + * - A normal width character + * - A wide character (eg. CJK) + * - An emoji + */ + getChars(): string; + + /** + * Gets the UTF32 codepoint of single characters, if content is a combined + * string it returns the codepoint of the last character in the string. + */ + getCode(): number; + + /** + * Gets the number representation of the foreground color mode, this can be + * used to perform quick comparisons of 2 cells to see if they're the same. + * Use `isFgRGB`, `isFgPalette` and `isFgDefault` to check what color mode + * a cell is. + */ + getFgColorMode(): number; + + /** + * Gets the number representation of the background color mode, this can be + * used to perform quick comparisons of 2 cells to see if they're the same. + * Use `isBgRGB`, `isBgPalette` and `isBgDefault` to check what color mode + * a cell is. + */ + getBgColorMode(): number; + + /** + * Gets a cell's foreground color number, this differs depending on what the + * color mode of the cell is: + * + * - Default: This should be 0, representing the default foreground color + * (CSI 39 m). + * - Palette: This is a number from 0 to 255 of ANSI colors (CSI 3(0-7) m, + * CSI 9(0-7) m, CSI 38 ; 5 ; 0-255 m). + * - RGB: A hex value representing a 'true color': 0xRRGGBB. + * (CSI 3 8 ; 2 ; Pi ; Pr ; Pg ; Pb) + */ + getFgColor(): number; + + /** + * Gets a cell's background color number, this differs depending on what the + * color mode of the cell is: + * + * - Default: This should be 0, representing the default background color + * (CSI 49 m). + * - Palette: This is a number from 0 to 255 of ANSI colors + * (CSI 4(0-7) m, CSI 10(0-7) m, CSI 48 ; 5 ; 0-255 m). + * - RGB: A hex value representing a 'true color': 0xRRGGBB + * (CSI 4 8 ; 2 ; Pi ; Pr ; Pg ; Pb) + */ + getBgColor(): number; + + /** Whether the cell has the bold attribute (CSI 1 m). */ + isBold(): number; + /** Whether the cell has the italic attribute (CSI 3 m). */ + isItalic(): number; + /** Whether the cell has the dim attribute (CSI 2 m). */ + isDim(): number; + /** Whether the cell has the underline attribute (CSI 4 m). */ + isUnderline(): number; + /** Whether the cell has the blink attribute (CSI 5 m). */ + isBlink(): number; + /** Whether the cell has the inverse attribute (CSI 7 m). */ + isInverse(): number; + /** Whether the cell has the invisible attribute (CSI 8 m). */ + isInvisible(): number; + /** Whether the cell has the strikethrough attribute (CSI 9 m). */ + isStrikethrough(): number; + + /** Whether the cell is using the RGB foreground color mode. */ + isFgRGB(): boolean; + /** Whether the cell is using the RGB background color mode. */ + isBgRGB(): boolean; + /** Whether the cell is using the palette foreground color mode. */ + isFgPalette(): boolean; + /** Whether the cell is using the palette background color mode. */ + isBgPalette(): boolean; + /** Whether the cell is using the default foreground color mode. */ + isFgDefault(): boolean; + /** Whether the cell is using the default background color mode. */ + isBgDefault(): boolean; + + /** Whether the cell has the default attribute (no color or style). */ + isAttributeDefault(): boolean; + } + + /** + * Data type to register a CSI, DCS or ESC callback in the parser + * in the form: + * ESC I..I F + * CSI Prefix P..P I..I F + * DCS Prefix P..P I..I F data_bytes ST + * + * with these rules/restrictions: + * - prefix can only be used with CSI and DCS + * - only one leading prefix byte is recognized by the parser + * before any other parameter bytes (P..P) + * - intermediate bytes are recognized up to 2 + * + * For custom sequences make sure to read ECMA-48 and the resources at + * vt100.net to not clash with existing sequences or reserved address space. + * General recommendations: + * - use private address space (see ECMA-48) + * - use max one intermediate byte (technically not limited by the spec, + * in practice there are no sequences with more than one intermediate byte, + * thus parsers might get confused with more intermediates) + * - test against other common emulators to check whether they escape/ignore + * the sequence correctly + * + * Notes: OSC command registration is handled differently (see addOscHandler) + * APC, PM or SOS is currently not supported. + */ + export interface IFunctionIdentifier { + /** + * Optional prefix byte, must be in range \x3c .. \x3f. + * Usable in CSI and DCS. + */ + prefix?: string; + /** + * Optional intermediate bytes, must be in range \x20 .. \x2f. + * Usable in CSI, DCS and ESC. + */ + intermediates?: string; + /** + * Final byte, must be in range \x40 .. \x7e for CSI and DCS, + * \x30 .. \x7e for ESC. + */ + final: string; + } + + /** + * Allows hooking into the parser for custom handling of escape sequences. + */ + export interface IParser { + /** + * Adds a handler for CSI escape sequences. + * @param id Specifies the function identifier under which the callback + * gets registered, e.g. {final: 'm'} for SGR. + * @param callback The function to handle the sequence. The callback is + * called with the numerical params. If the sequence has subparams the + * array will contain subarrays with their numercial values. + * Return true if the sequence was handled; false if we should try + * a previous handler (set by addCsiHandler or setCsiHandler). + * The most recently added handler is tried first. + * @return An IDisposable you can call to remove this handler. + */ + registerCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean): IDisposable; + + /** + * Adds a handler for DCS escape sequences. + * @param id Specifies the function identifier under which the callback + * gets registered, e.g. {intermediates: '$' final: 'q'} for DECRQSS. + * @param callback The function to handle the sequence. Note that the + * function will only be called once if the sequence finished sucessfully. + * There is currently no way to intercept smaller data chunks, data chunks + * will be stored up until the sequence is finished. Since DCS sequences + * are not limited by the amount of data this might impose a problem for + * big payloads. Currently xterm.js limits DCS payload to 10 MB + * which should give enough room for most use cases. + * The function gets the payload and numerical parameters as arguments. + * Return true if the sequence was handled; false if we should try + * a previous handler (set by addDcsHandler or setDcsHandler). + * The most recently added handler is tried first. + * @return An IDisposable you can call to remove this handler. + */ + registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean): IDisposable; + + /** + * Adds a handler for ESC escape sequences. + * @param id Specifies the function identifier under which the callback + * gets registered, e.g. {intermediates: '%' final: 'G'} for + * default charset selection. + * @param callback The function to handle the sequence. + * Return true if the sequence was handled; false if we should try + * a previous handler (set by addEscHandler or setEscHandler). + * The most recently added handler is tried first. + * @return An IDisposable you can call to remove this handler. + */ + registerEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable; + + /** + * Adds a handler for OSC escape sequences. + * @param ident The number (first parameter) of the sequence. + * @param callback The function to handle the sequence. Note that the + * function will only be called once if the sequence finished sucessfully. + * There is currently no way to intercept smaller data chunks, data chunks + * will be stored up until the sequence is finished. Since OSC sequences + * are not limited by the amount of data this might impose a problem for + * big payloads. Currently xterm.js limits OSC payload to 10 MB + * which should give enough room for most use cases. + * The callback is called with OSC data string. + * Return true if the sequence was handled; false if we should try + * a previous handler (set by addOscHandler or setOscHandler). + * The most recently added handler is tried first. + * @return An IDisposable you can call to remove this handler. + */ + registerOscHandler(ident: number, callback: (data: string) => boolean): IDisposable; + } + + /** + * (EXPERIMENTAL) Unicode version provider. + * Used to register custom Unicode versions with `Terminal.unicode.register`. + */ + export interface IUnicodeVersionProvider { + /** + * String indicating the Unicode version provided. + */ + readonly version: string; + + /** + * Unicode version dependent wcwidth implementation. + */ + wcwidth(codepoint: number): 0 | 1 | 2; + } + + /** + * (EXPERIMENTAL) Unicode handling interface. + */ + export interface IUnicodeHandling { + /** + * Register a custom Unicode version provider. + */ + register(provider: IUnicodeVersionProvider): void; + + /** + * Registered Unicode versions. + */ + readonly versions: ReadonlyArray; + + /** + * Getter/setter for active Unicode version. + */ + activeVersion: string; + } + + /** + * Terminal modes as set by SM/DECSET. + */ + export interface IModes { + /** + * Application Cursor Keys (DECCKM): `CSI ? 1 h` + */ + readonly applicationCursorKeysMode: boolean; + /** + * Application Keypad Mode (DECNKM): `CSI ? 6 6 h` + */ + readonly applicationKeypadMode: boolean; + /** + * Bracketed Paste Mode: `CSI ? 2 0 0 4 h` + */ + readonly bracketedPasteMode: boolean; + /** + * Insert Mode (IRM): `CSI 4 h` + */ + readonly insertMode: boolean; + /** + * Mouse Tracking, this can be one of the following: + * - none: This is the default value and can be reset with DECRST + * - x10: Send Mouse X & Y on button press `CSI ? 9 h` + * - vt200: Send Mouse X & Y on button press and release `CSI ? 1 0 0 0 h` + * - drag: Use Cell Motion Mouse Tracking `CSI ? 1 0 0 2 h` + * - any: Use All Motion Mouse Tracking `CSI ? 1 0 0 3 h` + */ + readonly mouseTrackingMode: 'none' | 'x10' | 'vt200' | 'drag' | 'any'; + /** + * Origin Mode (DECOM): `CSI ? 6 h` + */ + readonly originMode: boolean; + /** + * Reverse-wraparound Mode: `CSI ? 4 5 h` + */ + readonly reverseWraparoundMode: boolean; + /** + * Send FocusIn/FocusOut events: `CSI ? 1 0 0 4 h` + */ + readonly sendFocusMode: boolean; + /** + * Auto-Wrap Mode (DECAWM): `CSI ? 7 h` + */ + readonly wraparoundMode: boolean + } +} diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index b5a736a7..d7c2effc 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -44,6 +44,12 @@ declare module 'xterm' { */ allowTransparency?: boolean; + /** + * If enabled, alt + click will move the prompt cursor to position + * underneath the mouse. The default is true. + */ + altClickMovesCursor?: boolean; + /** * A data uri of the sound to use for the bell when `bellStyle = 'sound'`. */ @@ -84,6 +90,14 @@ declare module 'xterm' { */ cursorWidth?: number; + /** + * Whether to draw custom glyphs for block element and box drawing characters instead of using + * the font. This should typically result in better rendering with continuous lines, even when + * line height and letter spacing is used. Note that this doesn't work with the DOM renderer + * which renders all characters using the font. The default is true. + */ + customGlyphs?: boolean; + /** * Whether input should be disabled. */ @@ -616,6 +630,11 @@ declare module 'xterm' { */ readonly unicode: IUnicodeHandling; + /** + * Gets the terminal modes as set by SM/DECSET. + */ + readonly modes: IModes; + /** * Natural language strings that can be localized. */ @@ -628,6 +647,12 @@ declare module 'xterm' { */ constructor(options?: ITerminalOptions); + /** + * Adds an event listener for when the bell is triggered. + * @returns an `IDisposable` to stop listening. + */ + onBell: IEvent; + /** * Adds an event listener for when a binary event fires. This is used to * enable non UTF-8 conformant binary messages to be sent to the backend. @@ -668,19 +693,6 @@ declare module 'xterm' { */ onLineFeed: IEvent; - /** - * Adds an event listener for when a scroll occurs. The event value is the - * new position of the viewport. - * @returns an `IDisposable` to stop listening. - */ - onScroll: IEvent; - - /** - * Adds an event listener for when a selection change occurs. - * @returns an `IDisposable` to stop listening. - */ - onSelectionChange: IEvent; - /** * Adds an event listener for when rows are rendered. The event value * contains the start row and end rows of the rendered area (ranges from `0` @@ -696,6 +708,19 @@ declare module 'xterm' { */ onResize: IEvent<{ cols: number, rows: number }>; + /** + * Adds an event listener for when a scroll occurs. The event value is the + * new position of the viewport. + * @returns an `IDisposable` to stop listening. + */ + onScroll: IEvent; + + /** + * Adds an event listener for when a selection change occurs. + * @returns an `IDisposable` to stop listening. + */ + onSelectionChange: IEvent; + /** * Adds an event listener for when an OSC 0 or OSC 2 title change occurs. * The event value is the new title. @@ -1031,6 +1056,14 @@ declare module 'xterm' { */ refresh(start: number, end: number): void; + /** + * Clears the texture atlas of the canvas renderer if it's active. Doing this will force a + * redraw of all glyphs which can workaround issues causing the texture to become corrupt, for + * example Chromium/Nvidia has an issue where the texture gets messed up when resuming the OS + * from sleep. + */ + clearTextureAtlas(): void; + /** * Perform a full reset (RIS, aka '\x1bc'). */ @@ -1419,18 +1452,20 @@ declare module 'xterm' { /** Whether the cell has the bold attribute (CSI 1 m). */ isBold(): number; - /** Whether the cell has the inverse attribute (CSI 3 m). */ + /** Whether the cell has the italic attribute (CSI 3 m). */ isItalic(): number; - /** Whether the cell has the inverse attribute (CSI 2 m). */ + /** Whether the cell has the dim attribute (CSI 2 m). */ isDim(): number; /** Whether the cell has the underline attribute (CSI 4 m). */ isUnderline(): number; - /** Whether the cell has the inverse attribute (CSI 5 m). */ + /** Whether the cell has the blink attribute (CSI 5 m). */ isBlink(): number; /** Whether the cell has the inverse attribute (CSI 7 m). */ isInverse(): number; - /** Whether the cell has the inverse attribute (CSI 8 m). */ + /** Whether the cell has the invisible attribute (CSI 8 m). */ isInvisible(): number; + /** Whether the cell has the strikethrough attribute (CSI 9 m). */ + isStrikethrough(): number; /** Whether the cell is using the RGB foreground color mode. */ isFgRGB(): boolean; @@ -1495,6 +1530,23 @@ declare module 'xterm' { /** * Allows hooking into the parser for custom handling of escape sequences. + * + * Note on sync vs. async handlers: + * xterm.js implements all parser actions with synchronous handlers. + * In general custom handlers should also operate in sync mode wherever + * possible to keep the parser fast. + * Still the exposed interfaces allow to register async handlers by returning + * a `Promise`. Here the parser will pause input processing until + * the promise got resolved or rejected (in-band blocking). This "full stop" + * on the input chain allows to implement backpressure from a certain async + * action while the terminal state will not progress any further from input. + * It does not mean that the terminal state will not change at all in between, + * as user actions like resize or reset are still processed immediately. + * It is an error to assume a stable terminal state while giving back control + * in between, e.g. by multiple chained `then` calls. + * Downside of an async handler is a rather bad throughput performance, + * thus use async handlers only as a last resort or for actions that have + * to rely on async interfaces itself. */ export interface IParser { /** @@ -1504,12 +1556,11 @@ declare module 'xterm' { * @param callback The function to handle the sequence. The callback is * called with the numerical params. If the sequence has subparams the * array will contain subarrays with their numercial values. - * Return true if the sequence was handled; false if we should try - * a previous handler (set by addCsiHandler or setCsiHandler). - * The most recently added handler is tried first. + * Return `true` if the sequence was handled, `false` if the parser should try + * a previous handler. The most recently added handler is tried first. * @return An IDisposable you can call to remove this handler. */ - registerCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean): IDisposable; + registerCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean | Promise): IDisposable; /** * Adds a handler for DCS escape sequences. @@ -1523,12 +1574,11 @@ declare module 'xterm' { * big payloads. Currently xterm.js limits DCS payload to 10 MB * which should give enough room for most use cases. * The function gets the payload and numerical parameters as arguments. - * Return true if the sequence was handled; false if we should try - * a previous handler (set by addDcsHandler or setDcsHandler). - * The most recently added handler is tried first. + * Return `true` if the sequence was handled, `false` if the parser should try + * a previous handler. The most recently added handler is tried first. * @return An IDisposable you can call to remove this handler. */ - registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean): IDisposable; + registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean | Promise): IDisposable; /** * Adds a handler for ESC escape sequences. @@ -1536,12 +1586,11 @@ declare module 'xterm' { * gets registered, e.g. {intermediates: '%' final: 'G'} for * default charset selection. * @param callback The function to handle the sequence. - * Return true if the sequence was handled; false if we should try - * a previous handler (set by addEscHandler or setEscHandler). - * The most recently added handler is tried first. + * Return `true` if the sequence was handled, `false` if the parser should try + * a previous handler. The most recently added handler is tried first. * @return An IDisposable you can call to remove this handler. */ - registerEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable; + registerEscHandler(id: IFunctionIdentifier, handler: () => boolean | Promise): IDisposable; /** * Adds a handler for OSC escape sequences. @@ -1554,12 +1603,11 @@ declare module 'xterm' { * big payloads. Currently xterm.js limits OSC payload to 10 MB * which should give enough room for most use cases. * The callback is called with OSC data string. - * Return true if the sequence was handled; false if we should try - * a previous handler (set by addOscHandler or setOscHandler). - * The most recently added handler is tried first. + * Return `true` if the sequence was handled, `false` if the parser should try + * a previous handler. The most recently added handler is tried first. * @return An IDisposable you can call to remove this handler. */ - registerOscHandler(ident: number, callback: (data: string) => boolean): IDisposable; + registerOscHandler(ident: number, callback: (data: string) => boolean | Promise): IDisposable; } /** @@ -1597,4 +1645,51 @@ declare module 'xterm' { */ activeVersion: string; } + + /** + * Terminal modes as set by SM/DECSET. + */ + export interface IModes { + /** + * Application Cursor Keys (DECCKM): `CSI ? 1 h` + */ + readonly applicationCursorKeysMode: boolean; + /** + * Application Keypad Mode (DECNKM): `CSI ? 6 6 h` + */ + readonly applicationKeypadMode: boolean; + /** + * Bracketed Paste Mode: `CSI ? 2 0 0 4 h` + */ + readonly bracketedPasteMode: boolean; + /** + * Insert Mode (IRM): `CSI 4 h` + */ + readonly insertMode: boolean; + /** + * Mouse Tracking, this can be one of the following: + * - none: This is the default value and can be reset with DECRST + * - x10: Send Mouse X & Y on button press `CSI ? 9 h` + * - vt200: Send Mouse X & Y on button press and release `CSI ? 1 0 0 0 h` + * - drag: Use Cell Motion Mouse Tracking `CSI ? 1 0 0 2 h` + * - any: Use All Motion Mouse Tracking `CSI ? 1 0 0 3 h` + */ + readonly mouseTrackingMode: 'none' | 'x10' | 'vt200' | 'drag' | 'any'; + /** + * Origin Mode (DECOM): `CSI ? 6 h` + */ + readonly originMode: boolean; + /** + * Reverse-wraparound Mode: `CSI ? 4 5 h` + */ + readonly reverseWraparoundMode: boolean; + /** + * Send FocusIn/FocusOut events: `CSI ? 1 0 0 4 h` + */ + readonly sendFocusMode: boolean; + /** + * Auto-Wrap Mode (DECAWM): `CSI ? 7 h` + */ + readonly wraparoundMode: boolean + } } diff --git a/webpack.config.headless.js b/webpack.config.headless.js new file mode 100644 index 00000000..d5bb97b7 --- /dev/null +++ b/webpack.config.headless.js @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +const path = require('path'); + +/** + * This webpack config does a production build for xterm.js headless. It works by taking the output + * from tsc (via `yarn watch` or `yarn prebuild`) which are put into `out/` and webpacks them into a + * production mode umd library module in `lib-headless/`. The aliases are used fix up the absolute + * paths output by tsc (because of `baseUrl` and `paths` in `tsconfig.json`. + */ +module.exports = { + entry: './out/headless/public/Terminal.js', + devtool: 'source-map', + module: { + rules: [ + { + test: /\.js$/, + use: ["source-map-loader"], + enforce: "pre", + exclude: /node_modules/ + } + ] + }, + resolve: { + modules: ['./node_modules'], + extensions: [ '.js' ], + alias: { + common: path.resolve('./out/common'), + headless: path.resolve('./out/headless') + } + }, + output: { + filename: 'xterm-headless.js', + path: path.resolve('./headless/lib-headless'), + library: { + type: 'commonjs' + } + }, + mode: 'production' +}; diff --git a/yarn.lock b/yarn.lock index 8d8c9dd3..cf92d8f7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,13 @@ # yarn lockfile v1 +"@babel/code-frame@7.12.11": + version "7.12.11" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f" + integrity sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw== + dependencies: + "@babel/highlight" "^7.10.4" + "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.8.3.tgz#33e25903d7481181534e12ec0a25f16b6fcf419e" @@ -116,6 +123,11 @@ dependencies: "@babel/types" "^7.8.3" +"@babel/helper-validator-identifier@^7.14.0": + version "7.14.0" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz#d26cad8a47c65286b15df1547319a5d0bcf27288" + integrity sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A== + "@babel/helper-validator-identifier@^7.9.0": version "7.9.0" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.0.tgz#ad53562a7fc29b3b9a91bbf7d10397fd146346ed" @@ -130,6 +142,15 @@ "@babel/traverse" "^7.9.0" "@babel/types" "^7.9.0" +"@babel/highlight@^7.10.4": + version "7.14.0" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.14.0.tgz#3197e375711ef6bf834e67d0daec88e4f46113cf" + integrity sha512-YSCOwxvTYEIMSGaBQb5kDDsCopDdiUGsqpatp3fOlI4+2HQSkTmEVWnVuySdAC5EWCqSWWTv0ib63RjR7dTBdg== + dependencies: + "@babel/helper-validator-identifier" "^7.14.0" + chalk "^2.0.0" + js-tokens "^4.0.0" + "@babel/highlight@^7.8.3": version "7.9.0" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.9.0.tgz#4e9b45ccb82b79607271b2979ad82c7b68163079" @@ -177,10 +198,24 @@ lodash "^4.17.13" to-fast-properties "^2.0.0" -"@eslint/eslintrc@^0.2.1": - version "0.2.1" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.2.1.tgz#f72069c330461a06684d119384435e12a5d76e3c" - integrity sha512-XRUeBZ5zBWLYgSANMpThFddrZZkEbGHgUdt5UJjZfnlN9BGCiUBrf+nvbRupSjMvqzwnQN0qwCmOxITt1cfywA== +"@dabh/diagnostics@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@dabh/diagnostics/-/diagnostics-2.0.2.tgz#290d08f7b381b8f94607dc8f471a12c675f9db31" + integrity sha512-+A1YivoVDNNVCdfozHSR8v/jyuuLTMXwjWuxPFlFlUapXoGc+Gj9mDlTDDfrwl7rXCl2tNZ0kE8sIBO6YOn96Q== + dependencies: + colorspace "1.1.x" + enabled "2.0.x" + kuler "^2.0.0" + +"@discoveryjs/json-ext@^0.5.0": + version "0.5.2" + resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.2.tgz#8f03a22a04de437254e8ce8cc84ba39689288752" + integrity sha512-HyYEUDeIj5rRQU2Hk5HTB2uHsbRQpF70nvMhVzi+VJR0X+xNEhjPui4/kBf3VeH/wqD28PT4sVOm8qqLjBrSZg== + +"@eslint/eslintrc@^0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.1.tgz#442763b88cecbe3ee0ec7ca6d6dd6168550cbf14" + integrity sha512-5v7TDE9plVhvxQeWLXDTvFvJBdH6pEsdnl2g/dAptmuFEPedQ4Erq5rsDsX+mvAM610IhNaO2W5V1dOOnDKxkQ== dependencies: ajv "^6.12.4" debug "^4.1.1" @@ -189,7 +224,6 @@ ignore "^4.0.6" import-fresh "^3.2.1" js-yaml "^3.13.1" - lodash "^4.17.19" minimatch "^3.0.4" strip-json-comments "^3.1.1" @@ -208,41 +242,48 @@ resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.2.tgz#26520bf09abe4a5644cd5414e37125a8954241dd" integrity sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw== -"@nodelib/fs.scandir@2.1.3": - version "2.1.3" - resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz#3a582bdb53804c6ba6d146579c46e52130cf4a3b" - integrity sha512-eGmwYQn3gxo4r7jdQnkrrN6bY478C3P+a/y72IJukF8LjB6ZHeB3c+Ehacj3sYeSmUXGlnA67/PmbM9CVwL7Dw== +"@kwsites/file-exists@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@kwsites/file-exists/-/file-exists-1.1.1.tgz#ad1efcac13e1987d8dbaf235ef3be5b0d96faa99" + integrity sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw== dependencies: - "@nodelib/fs.stat" "2.0.3" + debug "^4.1.1" + +"@kwsites/promise-deferred@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz#8ace5259254426ccef57f3175bc64ed7095ed919" + integrity sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw== + +"@nodelib/fs.scandir@2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz#d4b3549a5db5de2683e0c1071ab4f140904bbf69" + integrity sha512-33g3pMJk3bg5nXbL/+CY6I2eJDzZAni49PfJnL5fghPTggPvBd/pFNSgJsdAgWptuFu7qq/ERvOYFlhvsLTCKA== + dependencies: + "@nodelib/fs.stat" "2.0.4" run-parallel "^1.1.9" -"@nodelib/fs.stat@2.0.3", "@nodelib/fs.stat@^2.0.2": - version "2.0.3" - resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz#34dc5f4cabbc720f4e60f75a747e7ecd6c175bd3" - integrity sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA== +"@nodelib/fs.stat@2.0.4", "@nodelib/fs.stat@^2.0.2": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.4.tgz#a3f2dd61bab43b8db8fa108a121cfffe4c676655" + integrity sha512-IYlHJA0clt2+Vg7bccq+TzRdJvv19c2INqBSsoOLp1je7xjtr7J26+WXR72MCdvU9q1qTzIWDfhMf+DRvQJK4Q== "@nodelib/fs.walk@^1.2.3": - version "1.2.4" - resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz#011b9202a70a6366e436ca5c065844528ab04976" - integrity sha512-1V9XOY4rDW0rehzbrcqAmHnz8e7SKvX27gh8Gt2WgB0+pdzdiLV83p72kZPU+jvMbS1qU5mauP2iOvO8rhmurQ== + version "1.2.6" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.6.tgz#cce9396b30aa5afe9e3756608f5831adcb53d063" + integrity sha512-8Broas6vTtW4GIXTAHDoE32hnN2M5ykgCpWGbuXHQ15vEMqr23pB76e/GZcYsZCHALv50ktd24qhEyKr6wBtow== dependencies: - "@nodelib/fs.scandir" "2.1.3" + "@nodelib/fs.scandir" "2.1.4" fastq "^1.6.0" -"@types/anymatch@*": - version "1.3.1" - resolved "https://registry.yarnpkg.com/@types/anymatch/-/anymatch-1.3.1.tgz#336badc1beecb9dacc38bea2cf32adf627a8421a" - integrity sha512-/+CRPXpBDpo2RK9C68N3b2cOvO0Cf5B9aPijHsoDQTHivnGSObdOF2BRQOYjojWTDy6nQvMjmqRXIxH55VjxxA== - "@types/app-root-path@^1.2.4": version "1.2.4" resolved "https://registry.yarnpkg.com/@types/app-root-path/-/app-root-path-1.2.4.tgz#a78b703282b32ac54de768f5512ecc3569919dc7" integrity sha1-p4twMoKzKsVN52j1US7MNWmRncc= -"@types/chai@^4.2.14": - version "4.2.14" - resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.14.tgz#44d2dd0b5de6185089375d976b4ec5caf6861193" - integrity sha512-G+ITQPXkwTrslfG5L/BksmbLUA0M1iybEsmCWPqzSxsRRhJZimBKJkoMi8fr/CPygPTj4zO5pJH7I2/cm9M7SQ== +"@types/chai@^4.2.18": + version "4.2.18" + resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.18.tgz#0c8e298dbff8205e2266606c1ea5fbdba29b46e4" + integrity sha512-rS27+EkB/RE1Iz3u0XtVL5q36MGDWbgYe7zWiodyKNUnthxY0rukK5V36eiUCtCisB7NN8zKYH6DO2M37qxFEQ== "@types/cli-table@^0.3.0": version "0.3.0" @@ -272,28 +313,23 @@ "@types/eslint" "*" "@types/estree" "*" -"@types/eslint-visitor-keys@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d" - integrity sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag== - "@types/eslint@*": - version "7.2.4" - resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-7.2.4.tgz#d12eeed7741d2491b69808576ac2d20c14f74c41" - integrity sha512-YCY4kzHMsHoyKspQH+nwSe+70Kep7Vjt2X+dZe5Vs2vkRudqtoFoUIv1RlJmZB8Hbp7McneupoZij4PadxsK5Q== + version "7.2.10" + resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-7.2.10.tgz#4b7a9368d46c0f8cd5408c23288a59aa2394d917" + integrity sha512-kUEPnMKrqbtpCq/KTaGFFKAcz6Ethm2EjCoKIDaCmfRBWLbFuTcOJfTlorwbnboXBzahqWLgUp1BQeKHiJzPUQ== dependencies: "@types/estree" "*" "@types/json-schema" "*" -"@types/estree@*", "@types/estree@^0.0.45": - version "0.0.45" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.45.tgz#e9387572998e5ecdac221950dab3e8c3b16af884" - integrity sha512-jnqIUKDUqJbDIUxm0Uj7bnlMnRm1T/eZ9N+AVMqhPgzrba2GhGG5o/jCTwmdPK709nEZsGoMzXEDUjcXHa3W0g== +"@types/estree@*", "@types/estree@^0.0.47": + version "0.0.47" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.47.tgz#d7a51db20f0650efec24cd04994f523d93172ed4" + integrity sha512-c5ciR06jK8u9BstrmJyO97m+klJrrhCf9u3rLu3DEAJBirxRqSCvDQoYKmxuYwQI5SZChAWu+tq9oVlGRuzPAg== -"@types/fs-extra@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-7.0.0.tgz#9c4ad9e1339e7448a76698829def1f159c1b636c" - integrity sha512-ndoMMbGyuToTy4qB6Lex/inR98nPiNHacsgMPvy+zqMLgSxbt8VtWpDArpGp69h1fEDQHn1KB+9DWD++wgbwYA== +"@types/fs-extra@9.0.1": + version "9.0.1" + resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-9.0.1.tgz#91c8fc4c51f6d5dbe44c2ca9ab09310bd00c7918" + integrity sha512-B42Sxuaz09MhC3DDeW5kubRcQ5by4iuVQ0cRRWM2lggLzAa/KVom0Aft/208NgMvNQQZ86s5rVcqDdn/SH0/mg== dependencies: "@types/node" "*" @@ -305,29 +341,29 @@ "@types/minimatch" "*" "@types/node" "*" -"@types/jsdom@^16.2.5": - version "16.2.5" - resolved "https://registry.yarnpkg.com/@types/jsdom/-/jsdom-16.2.5.tgz#74ebad438741d249ecb416c5486dcde4217eb66c" - integrity sha512-k/ZaTXtReAjwWu0clU0KLS53dyqZnA8mm+jwKFeFrvufXgICp+VNbskETFxKKAguv0pkaEKTax5MaRmvalM+TA== +"@types/jsdom@^16.2.10": + version "16.2.10" + resolved "https://registry.yarnpkg.com/@types/jsdom/-/jsdom-16.2.10.tgz#c05ea94682d035943ae2453b79d56178496b6653" + integrity sha512-q3aIjp3ehhVSXSbvNyuireAfvU2umRiZ2aLumyeZewCnoNaokrRDdTu5IvaeE9pzNtWHXrUnM9lb22Vl3W08EA== dependencies: "@types/node" "*" "@types/parse5" "*" "@types/tough-cookie" "*" -"@types/json-schema@*", "@types/json-schema@^7.0.6": +"@types/json-schema@*", "@types/json-schema@^7.0.3": + version "7.0.7" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.7.tgz#98a993516c859eb0d5c4c8f098317a9ea68db9ad" + integrity sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA== + +"@types/json-schema@^7.0.6": version "7.0.6" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.6.tgz#f4c7ec43e81b319a9815115031709f26987891f0" integrity sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw== -"@types/json-schema@^7.0.3": - version "7.0.5" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.5.tgz#dcce4430e64b443ba8945f0290fb564ad5bac6dd" - integrity sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ== - -"@types/mathjs@^5.0.1": - version "5.0.1" - resolved "https://registry.yarnpkg.com/@types/mathjs/-/mathjs-5.0.1.tgz#b98e163ea396b4f27bec20ee25ffb8fe9e656af8" - integrity sha512-EFBuueI+BRed9bnUO6/9my55b4FH+VQIvqMm58h9JGbtaGCkqr3YSDhnmVbM1SJjF//8SURERSypzNwejOk7lA== +"@types/mathjs@^6.0.11": + version "6.0.12" + resolved "https://registry.yarnpkg.com/@types/mathjs/-/mathjs-6.0.12.tgz#1c2a60352852676e10936ce150b9500d36555973" + integrity sha512-bpKs8CDJ0aOiiJguywryE/U6Wre/uftJ89xhp4aCgF4oRb3Yug2VyZ87958gmSeq4WMsvWPMs2Q5TtFv+dJtaA== dependencies: decimal.js "^10.0.0" @@ -336,95 +372,61 @@ resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== -"@types/mocha@^5.2.7": - version "5.2.7" - resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-5.2.7.tgz#315d570ccb56c53452ff8638738df60726d5b6ea" - integrity sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ== - -"@types/mocha@^8.0.3": - version "8.0.3" - resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-8.0.3.tgz#51b21b6acb6d1b923bbdc7725c38f9f455166402" - integrity sha512-vyxR57nv8NfcU0GZu8EUXZLTbCMupIUwy95LJ6lllN+JRPG25CwMHoB1q5xKh8YKhQnHYRAn4yW2yuHbf/5xgg== +"@types/mocha@^8.2.1", "@types/mocha@^8.2.2": + version "8.2.2" + resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-8.2.2.tgz#91daa226eb8c2ff261e6a8cbf8c7304641e095e0" + integrity sha512-Lwh0lzzqT5Pqh6z61P3c3P5nm6fzQK/MMHl9UKeneAeInVflBSz1O2EkX6gM6xfJd7FBXBY5purtLx7fUiZ7Hw== "@types/node@*": version "13.9.5" resolved "https://registry.yarnpkg.com/@types/node/-/node-13.9.5.tgz#59738bf30b31aea1faa2df7f4a5f55613750cf00" integrity sha512-hkzMMD3xu6BrJpGVLeQ3htQQNAcOrJjX7WFmtK8zWQpz2UJf13LCFF2ALA7c9OVdvc2vQJeDdjfR35M0sBCxvw== -"@types/node@^10.17.17": - version "10.17.18" - resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.18.tgz#ae364d97382aacdebf583fa4e7132af2dfe56a0c" - integrity sha512-DQ2hl/Jl3g33KuAUOcMrcAOtsbzb+y/ufakzAdeK9z/H/xsvkpbETZZbPNMIiQuk24f5ZRMCcZIViAwyFIiKmg== +"@types/node@^12.12.37": + version "12.20.12" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.12.tgz#fd9c1c2cfab536a2383ed1ef70f94adea743a226" + integrity sha512-KQZ1al2hKOONAs2MFv+yTQP1LkDWMrRJ9YCVRalXltOfXsBmH5IownLxQaiq0lnAHwAViLnh2aTYqrPcRGEbgg== -"@types/node@^12.0.4": - version "12.12.32" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.32.tgz#0ccc836d273e8a3cddf568daf22729cfa57c1925" - integrity sha512-44/reuCrwiQEsXud3I5X3sqI5jIXAmHB5xoiyKUw965olNHF3IWKjBLKK3F9LOSUZmK+oDt8jmyO637iX+hMgA== +"@types/node@^14.14.44": + version "14.14.44" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.44.tgz#df7503e6002847b834371c004b372529f3f85215" + integrity sha512-+gaugz6Oce6ZInfI/tK4Pq5wIIkJMEJUu92RB3Eu93mtj4wjjjz9EB5mLp5s1pSsLXdC/CPut/xF20ZzAQJbTA== "@types/parse5@*": - version "5.0.2" - resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-5.0.2.tgz#a877a4658f8238c8266faef300ae41c84d72ec8a" - integrity sha512-BOl+6KDs4ItndUWUFchy3aEqGdHhw0BC4Uu+qoDonN/f0rbUnJbm71Ulj8Tt9jLFRaAxPLKvdS1bBLfx1qXR9g== + version "6.0.0" + resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-6.0.0.tgz#38590dc2c3cf5717154064e3ee9b6947ee21b299" + integrity sha512-oPwPSj4a1wu9rsXTEGIJz91ISU725t0BmSnUhb57sI+M8XEmvUop84lzuiYdq0Y5M6xLY8DBPg0C2xEQKLyvBA== -"@types/puppeteer@^1.12.4": - version "1.20.4" - resolved "https://registry.yarnpkg.com/@types/puppeteer/-/puppeteer-1.20.4.tgz#30cb0a4ee5394c420119cbdf9f079d6595a07f67" - integrity sha512-T/kFgyLnYWk0H94hxI0HbOLnqHvzBRpfS0F0oo9ESGI24oiC2fEjDcMbBjuK3wH7VLsaIsp740vVXVzR1dsMNg== +"@types/puppeteer@^5.4.3": + version "5.4.3" + resolved "https://registry.yarnpkg.com/@types/puppeteer/-/puppeteer-5.4.3.tgz#cdca84aa7751d77448d8a477dbfa0af1f11485f2" + integrity sha512-3nE8YgR9DIsgttLW+eJf6mnXxq8Ge+27m5SU3knWmrlfl6+KOG0Bf9f7Ua7K+C4BnaTMAh3/UpySqdAYvrsvjg== dependencies: "@types/node" "*" -"@types/source-list-map@*": - version "0.1.2" - resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9" - integrity sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA== - -"@types/tapable@*": - version "1.0.5" - resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.5.tgz#9adbc12950582aa65ead76bffdf39fe0c27a3c02" - integrity sha512-/gG2M/Imw7cQFp8PGvz/SwocNrmKFjFsm5Pb8HdbHkZ1K8pmuPzOX4VeVoiEecFCVf4CsN1r3/BRvx+6sNqwtQ== - "@types/tough-cookie@*": - version "2.3.6" - resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-2.3.6.tgz#c880579e087d7a0db13777ff8af689f4ffc7b0d5" - integrity sha512-wHNBMnkoEBiRAd3s8KTKwIuO9biFtTf0LehITzBhSco+HQI0xkXZbLOD55SW3Aqw3oUkHstkm5SPv58yaAdFPQ== - -"@types/uglify-js@*": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.0.4.tgz#96beae23df6f561862a830b4288a49e86baac082" - integrity sha512-SudIN9TRJ+v8g5pTG8RRCqfqTMNqgWCKKd3vtynhGzkIIjxaicNAMuY5TRadJ6tzDu3Dotf3ngaMILtmOdmWEQ== - dependencies: - source-map "^0.6.1" + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.0.tgz#fef1904e4668b6e5ecee60c52cc6a078ffa6697d" + integrity sha512-I99sngh224D0M7XgW1s120zxCt3VYQ3IQsuw3P3jbq5GG4yc79+ZjyKznyOGIQrflfylLgcfekeZW/vk0yng6A== "@types/utf8@^2.1.6": version "2.1.6" resolved "https://registry.yarnpkg.com/@types/utf8/-/utf8-2.1.6.tgz#430cabb71a42d0a3613cce5621324fe4f5a25753" integrity sha512-pRs2gYF5yoKYrgSaira0DJqVg2tFuF+Qjp838xS7K+mJyY2jJzjsrl6y17GbIa4uMRogMbxs+ghNCvKg6XyNrA== -"@types/webpack-sources@*": - version "0.1.7" - resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-0.1.7.tgz#0a330a9456113410c74a5d64180af0cbca007141" - integrity sha512-XyaHrJILjK1VHVC4aVlKsdNN5KBTwufMb43cQs+flGxtPAf/1Qwl8+Q0tp5BwEGaI8D6XT1L+9bSWXckgkjTLw== +"@types/webpack@^5.28.0": + version "5.28.0" + resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-5.28.0.tgz#78dde06212f038d77e54116cfe69e88ae9ed2c03" + integrity sha512-8cP0CzcxUiFuA9xGJkfeVpqmWTk9nx6CWwamRGCj95ph1SmlRRk9KlCZ6avhCbZd4L68LvYT6l1kpdEnQXrF8w== dependencies: "@types/node" "*" - "@types/source-list-map" "*" - source-map "^0.6.1" + tapable "^2.2.0" + webpack "^5" -"@types/webpack@^4.41.24": - version "4.41.24" - resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.41.24.tgz#75b664abe3d5bcfe54e64313ca3b43e498550422" - integrity sha512-1A0MXPwZiMOD3DPMuOKUKcpkdPo8Lq33UGggZ7xio6wJ/jV1dAu5cXDrOfGDnldUroPIRLsr/DT43/GqOA4RFQ== - dependencies: - "@types/anymatch" "*" - "@types/node" "*" - "@types/tapable" "*" - "@types/uglify-js" "*" - "@types/webpack-sources" "*" - source-map "^0.6.0" - -"@types/ws@^7.2.9": - version "7.2.9" - resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.2.9.tgz#cadfac473acfab192678a487c3ecbb13a503547f" - integrity sha512-gmXYAXr7G4BrRMnkGQGkGonc3ArVro9VZd//C1uns/qqsJyl2dxaJdlPMhZbcq5MTxFFC+ttFWtHSfVW5+hlRA== +"@types/ws@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.4.4.tgz#93e1e00824c1de2608c30e6de4303ab3b4c0c9bc" + integrity sha512-d/7W23JAXPodQNbOZNXvl2K+bqAQrCMwlh/nuQsPSQk6Fq0opHoPrUw43aHsvSbIiQPr8Of2hkFbnz1XBFVyZQ== dependencies: "@types/node" "*" @@ -435,112 +437,74 @@ dependencies: "@types/node" "*" -"@typescript-eslint/eslint-plugin@^4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.0.0.tgz#99349a501447fed91de18346705c0c65cf603bee" - integrity sha512-5e6q1TR7gS2P+8W2xndCu7gBh3BzmYEo70OyIdsmCmknHha/yNbz2vdevl+tP1uoaMOcrzg4gyrAijuV3DDBHA== +"@typescript-eslint/eslint-plugin@^4.23.0": + version "4.23.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.23.0.tgz#29d3c9c81f6200b1fd6d8454cfb007ba176cde80" + integrity sha512-tGK1y3KIvdsQEEgq6xNn1DjiFJtl+wn8JJQiETtCbdQxw1vzjXyAaIkEmO2l6Nq24iy3uZBMFQjZ6ECf1QdgGw== dependencies: - "@typescript-eslint/experimental-utils" "4.0.0" - "@typescript-eslint/scope-manager" "4.0.0" + "@typescript-eslint/experimental-utils" "4.23.0" + "@typescript-eslint/scope-manager" "4.23.0" debug "^4.1.1" functional-red-black-tree "^1.0.1" + lodash "^4.17.15" regexpp "^3.0.0" semver "^7.3.2" tsutils "^3.17.1" -"@typescript-eslint/experimental-utils@3.10.1": - version "3.10.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-3.10.1.tgz#e179ffc81a80ebcae2ea04e0332f8b251345a686" - integrity sha512-DewqIgscDzmAfd5nOGe4zm6Bl7PKtMG2Ad0KG8CUZAHlXfAKTF9Ol5PXhiMh39yRL2ChRH1cuuUGOcVyyrhQIw== +"@typescript-eslint/experimental-utils@4.23.0": + version "4.23.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.23.0.tgz#f2059434cd6e5672bfeab2fb03b7c0a20622266f" + integrity sha512-WAFNiTDnQfrF3Z2fQ05nmCgPsO5o790vOhmWKXbbYQTO9erE1/YsFot5/LnOUizLzU2eeuz6+U/81KV5/hFTGA== dependencies: "@types/json-schema" "^7.0.3" - "@typescript-eslint/types" "3.10.1" - "@typescript-eslint/typescript-estree" "3.10.1" + "@typescript-eslint/scope-manager" "4.23.0" + "@typescript-eslint/types" "4.23.0" + "@typescript-eslint/typescript-estree" "4.23.0" eslint-scope "^5.0.0" eslint-utils "^2.0.0" -"@typescript-eslint/experimental-utils@4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.0.0.tgz#fbec21a3b5ab59127edb6ce2e139ed378cc50eb5" - integrity sha512-hbX6zR+a/vcpFVNJYN/Nbd7gmaMosDTxHEKcvmhWeWcq/0UDifrqmCfkkodbAKL46Fn4ekSBMTyq2zlNDzcQxw== +"@typescript-eslint/parser@^4.23.0": + version "4.23.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.23.0.tgz#239315d38e42e852bef43a4b0b01bef78f78911c" + integrity sha512-wsvjksHBMOqySy/Pi2Q6UuIuHYbgAMwLczRl4YanEPKW5KVxI9ZzDYh3B5DtcZPQTGRWFJrfcbJ6L01Leybwug== dependencies: - "@types/json-schema" "^7.0.3" - "@typescript-eslint/scope-manager" "4.0.0" - "@typescript-eslint/types" "4.0.0" - "@typescript-eslint/typescript-estree" "4.0.0" - eslint-scope "^5.0.0" - eslint-utils "^2.0.0" - -"@typescript-eslint/parser@^3.10.1": - version "3.10.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-3.10.1.tgz#1883858e83e8b442627e1ac6f408925211155467" - integrity sha512-Ug1RcWcrJP02hmtaXVS3axPPTTPnZjupqhgj+NnZ6BCkwSImWk/283347+x9wN+lqOdK9Eo3vsyiyDHgsmiEJw== - dependencies: - "@types/eslint-visitor-keys" "^1.0.0" - "@typescript-eslint/experimental-utils" "3.10.1" - "@typescript-eslint/types" "3.10.1" - "@typescript-eslint/typescript-estree" "3.10.1" - eslint-visitor-keys "^1.1.0" - -"@typescript-eslint/scope-manager@4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.0.0.tgz#8c9e3b3b8cdf5a1fbe671d9fad73ff67bc027ea8" - integrity sha512-9gcWUPoWo7gk/+ZQPg7L1ySRmR5HLIy3Vu6/LfhQbuzIkGm6v2CGIjpVRISoDLFRovNRDImd4aP/sa8O4yIEBg== - dependencies: - "@typescript-eslint/types" "4.0.0" - "@typescript-eslint/visitor-keys" "4.0.0" - -"@typescript-eslint/types@3.10.1": - version "3.10.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-3.10.1.tgz#1d7463fa7c32d8a23ab508a803ca2fe26e758727" - integrity sha512-+3+FCUJIahE9q0lDi1WleYzjCwJs5hIsbugIgnbB+dSCYUxl8L6PwmsyOPFZde2hc1DlTo/xnkOgiTLSyAbHiQ== - -"@typescript-eslint/types@4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.0.0.tgz#ec1f9fc06b8558a1d5afa6e337182d08beece7f5" - integrity sha512-bK+c2VLzznX2fUWLK6pFDv3cXGTp7nHIuBMq1B9klA+QCsqLHOOqe5TQReAQDl7DN2RfH+neweo0oC5hYlG7Rg== - -"@typescript-eslint/typescript-estree@3.10.1": - version "3.10.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-3.10.1.tgz#fd0061cc38add4fad45136d654408569f365b853" - integrity sha512-QbcXOuq6WYvnB3XPsZpIwztBoquEYLXh2MtwVU+kO8jgYCiv4G5xrSP/1wg4tkvrEE+esZVquIPX/dxPlePk1w== - dependencies: - "@typescript-eslint/types" "3.10.1" - "@typescript-eslint/visitor-keys" "3.10.1" + "@typescript-eslint/scope-manager" "4.23.0" + "@typescript-eslint/types" "4.23.0" + "@typescript-eslint/typescript-estree" "4.23.0" debug "^4.1.1" - glob "^7.1.6" - is-glob "^4.0.1" - lodash "^4.17.15" - semver "^7.3.2" - tsutils "^3.17.1" -"@typescript-eslint/typescript-estree@4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.0.0.tgz#2244c63de2f2190bc5718eb0fb3fd2c437d42097" - integrity sha512-ewFMPi2pMLDNIXGMPdf8r7El2oPSZw9PEYB0j+WcpKd7AX2ARmajGa7RUHTukllWX2bj4vWX6JLE1Oih2BMokA== +"@typescript-eslint/scope-manager@4.23.0": + version "4.23.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.23.0.tgz#8792ef7eacac122e2ec8fa2d30a59b8d9a1f1ce4" + integrity sha512-ZZ21PCFxPhI3n0wuqEJK9omkw51wi2bmeKJvlRZPH5YFkcawKOuRMQMnI8mH6Vo0/DoHSeZJnHiIx84LmVQY+w== dependencies: - "@typescript-eslint/types" "4.0.0" - "@typescript-eslint/visitor-keys" "4.0.0" + "@typescript-eslint/types" "4.23.0" + "@typescript-eslint/visitor-keys" "4.23.0" + +"@typescript-eslint/types@4.23.0": + version "4.23.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.23.0.tgz#da1654c8a5332f4d1645b2d9a1c64193cae3aa3b" + integrity sha512-oqkNWyG2SLS7uTWLZf6Sr7Dm02gA5yxiz1RP87tvsmDsguVATdpVguHr4HoGOcFOpCvx9vtCSCyQUGfzq28YCw== + +"@typescript-eslint/typescript-estree@4.23.0": + version "4.23.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.23.0.tgz#0753b292097523852428a6f5a1aa8ccc1aae6cd9" + integrity sha512-5Sty6zPEVZF5fbvrZczfmLCOcby3sfrSPu30qKoY1U3mca5/jvU5cwsPb/CO6Q3ByRjixTMIVsDkqwIxCf/dMw== + dependencies: + "@typescript-eslint/types" "4.23.0" + "@typescript-eslint/visitor-keys" "4.23.0" debug "^4.1.1" globby "^11.0.1" is-glob "^4.0.1" - lodash "^4.17.15" semver "^7.3.2" tsutils "^3.17.1" -"@typescript-eslint/visitor-keys@3.10.1": - version "3.10.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-3.10.1.tgz#cd4274773e3eb63b2e870ac602274487ecd1e931" - integrity sha512-9JgC82AaQeglebjZMgYR5wgmfUdUc+EitGUUMW8u2nDckaeimzW+VsoLV6FoimPv2id3VQzfjwBxEMVz08ameQ== +"@typescript-eslint/visitor-keys@4.23.0": + version "4.23.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.23.0.tgz#7215cc977bd3b4ef22467b9023594e32f9e4e455" + integrity sha512-5PNe5cmX9pSifit0H+nPoQBXdbNzi5tOEec+3riK+ku4e3er37pKxMKDH5Ct5Y4fhWxcD4spnlYjxi9vXbSpwg== dependencies: - eslint-visitor-keys "^1.1.0" - -"@typescript-eslint/visitor-keys@4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.0.0.tgz#e2bbb69d98076d6a3f06abcb2048225a74362c33" - integrity sha512-sTouJbv6rjVJeTE4lpSBVYXq/u5K3gbB6LKt7ccFEZPTZB/VeQ0ssUz9q5Hx++sCqBbdF8PzrrgvEnicXAR6NQ== - dependencies: - "@typescript-eslint/types" "4.0.0" + "@typescript-eslint/types" "4.23.0" eslint-visitor-keys "^2.0.0" "@ungap/promise-all-settled@1.1.2": @@ -548,162 +512,143 @@ resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44" integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q== -"@webassemblyjs/ast@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.9.0.tgz#bd850604b4042459a5a41cd7d338cbed695ed964" - integrity sha512-C6wW5L+b7ogSDVqymbkkvuW9kruN//YisMED04xzeBBqjHa2FYnmvOlS6Xj68xWQRgWvI9cIglsjFowH/RJyEA== +"@webassemblyjs/ast@1.11.0": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.0.tgz#a5aa679efdc9e51707a4207139da57920555961f" + integrity sha512-kX2W49LWsbthrmIRMbQZuQDhGtjyqXfEmmHyEi4XWnSZtPmxY0+3anPIzsnRb45VH/J55zlOfWvZuY47aJZTJg== dependencies: - "@webassemblyjs/helper-module-context" "1.9.0" - "@webassemblyjs/helper-wasm-bytecode" "1.9.0" - "@webassemblyjs/wast-parser" "1.9.0" + "@webassemblyjs/helper-numbers" "1.11.0" + "@webassemblyjs/helper-wasm-bytecode" "1.11.0" -"@webassemblyjs/floating-point-hex-parser@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.9.0.tgz#3c3d3b271bddfc84deb00f71344438311d52ffb4" - integrity sha512-TG5qcFsS8QB4g4MhrxK5TqfdNe7Ey/7YL/xN+36rRjl/BlGE/NcBvJcqsRgCP6Z92mRE+7N50pRIi8SmKUbcQA== +"@webassemblyjs/floating-point-hex-parser@1.11.0": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.0.tgz#34d62052f453cd43101d72eab4966a022587947c" + integrity sha512-Q/aVYs/VnPDVYvsCBL/gSgwmfjeCb4LW8+TMrO3cSzJImgv8lxxEPM2JA5jMrivE7LSz3V+PFqtMbls3m1exDA== -"@webassemblyjs/helper-api-error@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.9.0.tgz#203f676e333b96c9da2eeab3ccef33c45928b6a2" - integrity sha512-NcMLjoFMXpsASZFxJ5h2HZRcEhDkvnNFOAKneP5RbKRzaWJN36NC4jqQHKwStIhGXu5mUWlUUk7ygdtrO8lbmw== +"@webassemblyjs/helper-api-error@1.11.0": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.0.tgz#aaea8fb3b923f4aaa9b512ff541b013ffb68d2d4" + integrity sha512-baT/va95eXiXb2QflSx95QGT5ClzWpGaa8L7JnJbgzoYeaA27FCvuBXU758l+KXWRndEmUXjP0Q5fibhavIn8w== -"@webassemblyjs/helper-buffer@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.9.0.tgz#a1442d269c5feb23fcbc9ef759dac3547f29de00" - integrity sha512-qZol43oqhq6yBPx7YM3m9Bv7WMV9Eevj6kMi6InKOuZxhw+q9hOkvq5e/PpKSiLfyetpaBnogSbNCfBwyB00CA== +"@webassemblyjs/helper-buffer@1.11.0": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.0.tgz#d026c25d175e388a7dbda9694e91e743cbe9b642" + integrity sha512-u9HPBEl4DS+vA8qLQdEQ6N/eJQ7gT7aNvMIo8AAWvAl/xMrcOSiI2M0MAnMCy3jIFke7bEee/JwdX1nUpCtdyA== -"@webassemblyjs/helper-code-frame@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.9.0.tgz#647f8892cd2043a82ac0c8c5e75c36f1d9159f27" - integrity sha512-ERCYdJBkD9Vu4vtjUYe8LZruWuNIToYq/ME22igL+2vj2dQ2OOujIZr3MEFvfEaqKoVqpsFKAGsRdBSBjrIvZA== +"@webassemblyjs/helper-numbers@1.11.0": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.0.tgz#7ab04172d54e312cc6ea4286d7d9fa27c88cd4f9" + integrity sha512-DhRQKelIj01s5IgdsOJMKLppI+4zpmcMQ3XboFPLwCpSNH6Hqo1ritgHgD0nqHeSYqofA6aBN/NmXuGjM1jEfQ== dependencies: - "@webassemblyjs/wast-printer" "1.9.0" + "@webassemblyjs/floating-point-hex-parser" "1.11.0" + "@webassemblyjs/helper-api-error" "1.11.0" + "@xtuc/long" "4.2.2" -"@webassemblyjs/helper-fsm@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-fsm/-/helper-fsm-1.9.0.tgz#c05256b71244214671f4b08ec108ad63b70eddb8" - integrity sha512-OPRowhGbshCb5PxJ8LocpdX9Kl0uB4XsAjl6jH/dWKlk/mzsANvhwbiULsaiqT5GZGT9qinTICdj6PLuM5gslw== +"@webassemblyjs/helper-wasm-bytecode@1.11.0": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.0.tgz#85fdcda4129902fe86f81abf7e7236953ec5a4e1" + integrity sha512-MbmhvxXExm542tWREgSFnOVo07fDpsBJg3sIl6fSp9xuu75eGz5lz31q7wTLffwL3Za7XNRCMZy210+tnsUSEA== -"@webassemblyjs/helper-module-context@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-module-context/-/helper-module-context-1.9.0.tgz#25d8884b76839871a08a6c6f806c3979ef712f07" - integrity sha512-MJCW8iGC08tMk2enck1aPW+BE5Cw8/7ph/VGZxwyvGbJwjktKkDK7vy7gAmMDx88D7mhDTCNKAW5tED+gZ0W8g== +"@webassemblyjs/helper-wasm-section@1.11.0": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.0.tgz#9ce2cc89300262509c801b4af113d1ca25c1a75b" + integrity sha512-3Eb88hcbfY/FCukrg6i3EH8H2UsD7x8Vy47iVJrP967A9JGqgBVL9aH71SETPx1JrGsOUVLo0c7vMCN22ytJew== dependencies: - "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/ast" "1.11.0" + "@webassemblyjs/helper-buffer" "1.11.0" + "@webassemblyjs/helper-wasm-bytecode" "1.11.0" + "@webassemblyjs/wasm-gen" "1.11.0" -"@webassemblyjs/helper-wasm-bytecode@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.9.0.tgz#4fed8beac9b8c14f8c58b70d124d549dd1fe5790" - integrity sha512-R7FStIzyNcd7xKxCZH5lE0Bqy+hGTwS3LJjuv1ZVxd9O7eHCedSdrId/hMOd20I+v8wDXEn+bjfKDLzTepoaUw== - -"@webassemblyjs/helper-wasm-section@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.9.0.tgz#5a4138d5a6292ba18b04c5ae49717e4167965346" - integrity sha512-XnMB8l3ek4tvrKUUku+IVaXNHz2YsJyOOmz+MMkZvh8h1uSJpSen6vYnw3IoQ7WwEuAhL8Efjms1ZWjqh2agvw== - dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/helper-buffer" "1.9.0" - "@webassemblyjs/helper-wasm-bytecode" "1.9.0" - "@webassemblyjs/wasm-gen" "1.9.0" - -"@webassemblyjs/ieee754@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.9.0.tgz#15c7a0fbaae83fb26143bbacf6d6df1702ad39e4" - integrity sha512-dcX8JuYU/gvymzIHc9DgxTzUUTLexWwt8uCTWP3otys596io0L5aW02Gb1RjYpx2+0Jus1h4ZFqjla7umFniTg== +"@webassemblyjs/ieee754@1.11.0": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.0.tgz#46975d583f9828f5d094ac210e219441c4e6f5cf" + integrity sha512-KXzOqpcYQwAfeQ6WbF6HXo+0udBNmw0iXDmEK5sFlmQdmND+tr773Ti8/5T/M6Tl/413ArSJErATd8In3B+WBA== dependencies: "@xtuc/ieee754" "^1.2.0" -"@webassemblyjs/leb128@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.9.0.tgz#f19ca0b76a6dc55623a09cffa769e838fa1e1c95" - integrity sha512-ENVzM5VwV1ojs9jam6vPys97B/S65YQtv/aanqnU7D8aSoHFX8GyhGg0CMfyKNIHBuAVjy3tlzd5QMMINa7wpw== +"@webassemblyjs/leb128@1.11.0": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.0.tgz#f7353de1df38aa201cba9fb88b43f41f75ff403b" + integrity sha512-aqbsHa1mSQAbeeNcl38un6qVY++hh8OpCOzxhixSYgbRfNWcxJNJQwe2rezK9XEcssJbbWIkblaJRwGMS9zp+g== dependencies: "@xtuc/long" "4.2.2" -"@webassemblyjs/utf8@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.9.0.tgz#04d33b636f78e6a6813227e82402f7637b6229ab" - integrity sha512-GZbQlWtopBTP0u7cHrEx+73yZKrQoBMpwkGEIqlacljhXCkVM1kMQge/Mf+csMJAjEdSwhOyLAS0AoR3AG5P8w== +"@webassemblyjs/utf8@1.11.0": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.0.tgz#86e48f959cf49e0e5091f069a709b862f5a2cadf" + integrity sha512-A/lclGxH6SpSLSyFowMzO/+aDEPU4hvEiooCMXQPcQFPPJaYcPQNKGOCLUySJsYJ4trbpr+Fs08n4jelkVTGVw== -"@webassemblyjs/wasm-edit@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.9.0.tgz#3fe6d79d3f0f922183aa86002c42dd256cfee9cf" - integrity sha512-FgHzBm80uwz5M8WKnMTn6j/sVbqilPdQXTWraSjBwFXSYGirpkSWE2R9Qvz9tNiTKQvoKILpCuTjBKzOIm0nxw== +"@webassemblyjs/wasm-edit@1.11.0": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.0.tgz#ee4a5c9f677046a210542ae63897094c2027cb78" + integrity sha512-JHQ0damXy0G6J9ucyKVXO2j08JVJ2ntkdJlq1UTiUrIgfGMmA7Ik5VdC/L8hBK46kVJgujkBIoMtT8yVr+yVOQ== dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/helper-buffer" "1.9.0" - "@webassemblyjs/helper-wasm-bytecode" "1.9.0" - "@webassemblyjs/helper-wasm-section" "1.9.0" - "@webassemblyjs/wasm-gen" "1.9.0" - "@webassemblyjs/wasm-opt" "1.9.0" - "@webassemblyjs/wasm-parser" "1.9.0" - "@webassemblyjs/wast-printer" "1.9.0" + "@webassemblyjs/ast" "1.11.0" + "@webassemblyjs/helper-buffer" "1.11.0" + "@webassemblyjs/helper-wasm-bytecode" "1.11.0" + "@webassemblyjs/helper-wasm-section" "1.11.0" + "@webassemblyjs/wasm-gen" "1.11.0" + "@webassemblyjs/wasm-opt" "1.11.0" + "@webassemblyjs/wasm-parser" "1.11.0" + "@webassemblyjs/wast-printer" "1.11.0" -"@webassemblyjs/wasm-gen@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.9.0.tgz#50bc70ec68ded8e2763b01a1418bf43491a7a49c" - integrity sha512-cPE3o44YzOOHvlsb4+E9qSqjc9Qf9Na1OO/BHFy4OI91XDE14MjFN4lTMezzaIWdPqHnsTodGGNP+iRSYfGkjA== +"@webassemblyjs/wasm-gen@1.11.0": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.0.tgz#3cdb35e70082d42a35166988dda64f24ceb97abe" + integrity sha512-BEUv1aj0WptCZ9kIS30th5ILASUnAPEvE3tVMTrItnZRT9tXCLW2LEXT8ezLw59rqPP9klh9LPmpU+WmRQmCPQ== dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/helper-wasm-bytecode" "1.9.0" - "@webassemblyjs/ieee754" "1.9.0" - "@webassemblyjs/leb128" "1.9.0" - "@webassemblyjs/utf8" "1.9.0" + "@webassemblyjs/ast" "1.11.0" + "@webassemblyjs/helper-wasm-bytecode" "1.11.0" + "@webassemblyjs/ieee754" "1.11.0" + "@webassemblyjs/leb128" "1.11.0" + "@webassemblyjs/utf8" "1.11.0" -"@webassemblyjs/wasm-opt@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.9.0.tgz#2211181e5b31326443cc8112eb9f0b9028721a61" - integrity sha512-Qkjgm6Anhm+OMbIL0iokO7meajkzQD71ioelnfPEj6r4eOFuqm4YC3VBPqXjFyyNwowzbMD+hizmprP/Fwkl2A== +"@webassemblyjs/wasm-opt@1.11.0": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.0.tgz#1638ae188137f4bb031f568a413cd24d32f92978" + integrity sha512-tHUSP5F4ywyh3hZ0+fDQuWxKx3mJiPeFufg+9gwTpYp324mPCQgnuVKwzLTZVqj0duRDovnPaZqDwoyhIO8kYg== dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/helper-buffer" "1.9.0" - "@webassemblyjs/wasm-gen" "1.9.0" - "@webassemblyjs/wasm-parser" "1.9.0" + "@webassemblyjs/ast" "1.11.0" + "@webassemblyjs/helper-buffer" "1.11.0" + "@webassemblyjs/wasm-gen" "1.11.0" + "@webassemblyjs/wasm-parser" "1.11.0" -"@webassemblyjs/wasm-parser@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.9.0.tgz#9d48e44826df4a6598294aa6c87469d642fff65e" - integrity sha512-9+wkMowR2AmdSWQzsPEjFU7njh8HTO5MqO8vjwEHuM+AMHioNqSBONRdr0NQQ3dVQrzp0s8lTcYqzUdb7YgELA== +"@webassemblyjs/wasm-parser@1.11.0": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.0.tgz#3e680b8830d5b13d1ec86cc42f38f3d4a7700754" + integrity sha512-6L285Sgu9gphrcpDXINvm0M9BskznnzJTE7gYkjDbxET28shDqp27wpruyx3C2S/dvEwiigBwLA1cz7lNUi0kw== dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/helper-api-error" "1.9.0" - "@webassemblyjs/helper-wasm-bytecode" "1.9.0" - "@webassemblyjs/ieee754" "1.9.0" - "@webassemblyjs/leb128" "1.9.0" - "@webassemblyjs/utf8" "1.9.0" + "@webassemblyjs/ast" "1.11.0" + "@webassemblyjs/helper-api-error" "1.11.0" + "@webassemblyjs/helper-wasm-bytecode" "1.11.0" + "@webassemblyjs/ieee754" "1.11.0" + "@webassemblyjs/leb128" "1.11.0" + "@webassemblyjs/utf8" "1.11.0" -"@webassemblyjs/wast-parser@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-parser/-/wast-parser-1.9.0.tgz#3031115d79ac5bd261556cecc3fa90a3ef451914" - integrity sha512-qsqSAP3QQ3LyZjNC/0jBJ/ToSxfYJ8kYyuiGvtn/8MK89VrNEfwj7BPQzJVHi0jGTRK2dGdJ5PRqhtjzoww+bw== +"@webassemblyjs/wast-printer@1.11.0": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.0.tgz#680d1f6a5365d6d401974a8e949e05474e1fab7e" + integrity sha512-Fg5OX46pRdTgB7rKIUojkh9vXaVN6sGYCnEiJN1GYkb0RPwShZXp6KTDqmoMdQPKhcroOXh3fEzmkWmCYaKYhQ== dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/floating-point-hex-parser" "1.9.0" - "@webassemblyjs/helper-api-error" "1.9.0" - "@webassemblyjs/helper-code-frame" "1.9.0" - "@webassemblyjs/helper-fsm" "1.9.0" + "@webassemblyjs/ast" "1.11.0" "@xtuc/long" "4.2.2" -"@webassemblyjs/wast-printer@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.9.0.tgz#4935d54c85fef637b00ce9f52377451d00d47899" - integrity sha512-2J0nE95rHXHyQ24cWjMKJ1tqB/ds8z/cyeOZxJhcb+rW+SQASVjuznUSmdz5GpVJTzU8JkhYut0D3siFDD6wsA== - dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/wast-parser" "1.9.0" - "@xtuc/long" "4.2.2" +"@webpack-cli/configtest@^1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-1.0.3.tgz#204bcff87cda3ea4810881f7ea96e5f5321b87b9" + integrity sha512-WQs0ep98FXX2XBAfQpRbY0Ma6ADw8JR6xoIkaIiJIzClGOMqVRvPCWqndTxf28DgFopWan0EKtHtg/5W1h0Zkw== -"@webpack-cli/info@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@webpack-cli/info/-/info-1.1.0.tgz#c596d5bc48418b39df00c5ed7341bf0f102dbff1" - integrity sha512-uNWSdaYHc+f3LdIZNwhdhkjjLDDl3jP2+XBqAq9H8DjrJUvlOKdP8TNruy1yEaDfgpAIgbSAN7pye4FEHg9tYQ== +"@webpack-cli/info@^1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@webpack-cli/info/-/info-1.2.4.tgz#7381fd41c9577b2d8f6c2594fad397ef49ad5573" + integrity sha512-ogE2T4+pLhTTPS/8MM3IjHn0IYplKM4HbVNMCWA9N4NrdPzunwenpCsqKEXyejMfRu6K8mhauIPYf8ZxWG5O6g== dependencies: envinfo "^7.7.3" -"@webpack-cli/serve@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-1.1.0.tgz#13ad38f89b6e53d1133bac0006a128217a6ebf92" - integrity sha512-7RfnMXCpJ/NThrhq4gYQYILB18xWyoQcBey81oIyVbmgbc6m5ZHHyFK+DyH7pLHJf0p14MxL4mTsoPAgBSTpIg== +"@webpack-cli/serve@^1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-1.4.0.tgz#f84fd07bcacefe56ce762925798871092f0f228e" + integrity sha512-xgT/HqJ+uLWGX+Mzufusl3cgjAcnqYYskaB7o0vRcwOEfuu6hMzSILQpnIzFMGsTaeaX4Nnekl+6fadLbl1/Vg== "@xtuc/ieee754@^1.2.0": version "1.2.0" @@ -736,40 +681,38 @@ acorn-globals@^6.0.0: acorn "^7.1.1" acorn-walk "^7.1.1" -acorn-jsx@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.2.0.tgz#4c66069173d6fdd68ed85239fc256226182b2ebe" - integrity sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ== +acorn-jsx@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b" + integrity sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng== acorn-walk@^7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.1.1.tgz#345f0dffad5c735e7373d2fec9a1023e6a44b83e" - integrity sha512-wdlPY2tm/9XBr7QkKlq0WQVgiuGTX6YWPyRyBviSoScBuLfTVQhvwg6wJ369GJ/1nPfTLMfnrFIfjqVg6d+jQQ== + version "7.2.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc" + integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== acorn@^7.1.1, acorn@^7.4.0: - version "7.4.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.0.tgz#e1ad486e6c54501634c6c397c5c121daa383607c" - integrity sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w== + version "7.4.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" + integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== -acorn@^8.0.4: - version "8.0.4" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.0.4.tgz#7a3ae4191466a6984eee0fe3407a4f3aa9db8354" - integrity sha512-XNP0PqF1XD19ZlLKvB7cMmnZswW4C/03pRHgirB30uSJTaS3A3V1/P4sS3HPvFmjoriPCJQs+JDSbm4bL1TxGQ== +acorn@^8.1.0, acorn@^8.2.1: + version "8.2.4" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.2.4.tgz#caba24b08185c3b56e3168e97d15ed17f4d31fd0" + integrity sha512-Ibt84YwBDDA890eDiDCEqcbwvHlBvzzDkU2cGBBDDI1QWT12jTiXIOn2CIw5KK4i6N5Z2HUxwYjzriDyqaqqZg== + +agent-base@5: + version "5.1.1" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-5.1.1.tgz#e8fb3f242959db44d63be665db7a8e739537a32c" + integrity sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g== agent-base@6: - version "6.0.1" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.1.tgz#808007e4e5867decb0ab6ab2f928fbdb5a596db4" - integrity sha512-01q25QQDwLSsyfhrKbn8yuur+JNw0H+0Y4JiGIKd3z9aYk/w/2kxD/Upc+t2ZBBSUNff50VjPsSW2YxM8QYKVg== + version "6.0.2" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" + integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== dependencies: debug "4" -agent-base@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee" - integrity sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg== - dependencies: - es6-promisify "^5.0.0" - aggregate-error@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.0.1.tgz#db2fe7246e536f40d9b5442a39e117d7dd6a24e0" @@ -783,7 +726,17 @@ ajv-keywords@^3.5.2: resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== -ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.4, ajv@^6.12.5, ajv@^6.5.5: +ajv@^6.10.0, ajv@^6.12.3, ajv@^6.12.4: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ajv@^6.12.5: version "6.12.5" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.5.tgz#19b0e8bae8f476e5ba666300387775fb1a00a4da" integrity sha512-lRF8RORchjpKG50/WFf8xmg7sgCLFiYNNnqdKflk63whMQcWR5ngGjiSXkL9bjxy6B2npOK2HSMN49jEBMSkag== @@ -793,10 +746,15 @@ ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.4, ajv@^6.12.5, ajv@^6.5.5: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ansi-colors@3.2.3: - version "3.2.3" - resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.3.tgz#57d35b8686e851e2cc04c403f1c00203976a1813" - integrity sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw== +ajv@^8.0.1: + version "8.3.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.3.0.tgz#25ee7348e32cdc4a1dbb38256bf6bdc451dd577c" + integrity sha512-RYE7B5An83d7eWnDR8kbdaIFqmKCNsP16ay1hDbJEU+sa0e3H9SebskCt0Uufem6cfAVu7Col6ubcn/W+Sm8/Q== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" ansi-colors@4.1.1, ansi-colors@^4.1.1: version "4.1.1" @@ -813,17 +771,12 @@ ansi-regex@^3.0.0: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= -ansi-regex@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" - integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== - ansi-regex@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== -ansi-styles@^3.2.0, ansi-styles@^3.2.1: +ansi-styles@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== @@ -846,10 +799,10 @@ anymatch@~3.1.1: normalize-path "^3.0.0" picomatch "^2.0.4" -app-root-path@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/app-root-path/-/app-root-path-2.2.1.tgz#d0df4a682ee408273583d43f6f79e9892624bc9a" - integrity sha512-91IFKeKk7FjfmezPKkwtaRvSpnUc4gDwPAjA1YZ9Gn0q0PPeW+vbeUsZuyDwjI7+QTHhcLen2v25fi/AmhvbJA== +app-root-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/app-root-path/-/app-root-path-3.0.0.tgz#210b6f43873227e18a4b810a032283311555d5ad" + integrity sha512-qMcx+Gy2UZynHjOHOIXPNvpf+9cjvk3cWrBBK7zg4gH9+clobJRb9NGzcT7mQTcV/6Gm/1WelUtqxVXnNlrwcw== append-transform@^2.0.0: version "2.0.0" @@ -870,10 +823,10 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" -array-back@^4.0.0, array-back@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/array-back/-/array-back-4.0.1.tgz#9b80312935a52062e1a233a9c7abeb5481b30e90" - integrity sha512-Z/JnaVEXv+A9xabHzN43FiiiWEE7gPCRXMrVmRm00tWbjZRul1iHm7ECzlyNq1p4a4ATXz+G9FJ3GqGOkOV3fg== +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== array-filter@^1.0.0: version "1.0.0" @@ -907,29 +860,32 @@ assertion-error@^1.1.0: resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== -astral-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" - integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== +astral-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" + integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== async-limiter@~1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== -async@^2.6.1: - version "2.6.3" - resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" - integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg== - dependencies: - lodash "^4.17.14" +async@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720" + integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw== asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= -available-typed-arrays@^1.0.0, available-typed-arrays@^1.0.2: +at-least-node@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" + integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== + +available-typed-arrays@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.2.tgz#6b098ca9d8039079ee3f77f7b783c4480ba513f5" integrity sha512-XWX3OX8Onv97LMk/ftVyBibpGwY5a8SmuxZPzeOxqmuEqUCOM9ZE+uIaD1VNJ5QnvU2UQusvmKbuM1FR8QWGfQ== @@ -942,15 +898,20 @@ aws-sign2@~0.7.0: integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= aws4@^1.8.0: - version "1.9.1" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.9.1.tgz#7e33d8f7d449b3f673cd72deb9abdc552dbe528e" - integrity sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug== + version "1.11.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" + integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== balanced-match@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + bcrypt-pbkdf@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" @@ -958,16 +919,20 @@ bcrypt-pbkdf@^1.0.0: dependencies: tweetnacl "^0.14.3" -big.js@^5.2.2: - version "5.2.2" - resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" - integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== - binary-extensions@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.0.0.tgz#23c0df14f6a88077f5f986c0d167ec03c3d5537c" integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow== +bl@^4.0.3: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" + integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== + dependencies: + buffer "^5.5.0" + inherits "^2.0.4" + readable-stream "^3.4.0" + body-parser@1.19.0: version "1.19.0" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" @@ -1010,14 +975,15 @@ browser-stdout@1.3.1: integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== browserslist@^4.14.5: - version "4.14.5" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.14.5.tgz#1c751461a102ddc60e40993639b709be7f2c4015" - integrity sha512-Z+vsCZIvCBvqLoYkBFTwEYH3v5MCQbsAjp50ERycpOjnPmolg1Gjy4+KaWWpm8QOJt9GHkhdqAl14NpCX73CWA== + version "4.16.6" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.6.tgz#d7901277a5a88e554ed305b183ec9b0c08f66fa2" + integrity sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ== dependencies: - caniuse-lite "^1.0.30001135" - electron-to-chromium "^1.3.571" - escalade "^3.1.0" - node-releases "^1.1.61" + caniuse-lite "^1.0.30001219" + colorette "^1.2.2" + electron-to-chromium "^1.3.723" + escalade "^3.1.1" + node-releases "^1.1.71" buffer-crc32@~0.2.3: version "0.2.13" @@ -1029,6 +995,14 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== +buffer@^5.2.1, buffer@^5.5.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" + integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.1.13" + builtin-modules@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" @@ -1049,6 +1023,14 @@ caching-transform@^4.0.0: package-hash "^4.0.0" write-file-atomic "^3.0.0" +call-bind@^1.0.0, call-bind@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" + integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== + dependencies: + function-bind "^1.1.1" + get-intrinsic "^1.0.2" + callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -1064,29 +1046,29 @@ camelcase@^6.0.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.1.0.tgz#27dc176173725fb0adf8a48b647f4d7871944d78" integrity sha512-WCMml9ivU60+8rEJgELlFp1gxFcEGxwYleE3bziHEDeqsqAWGHdimB7beBFGjLzVNgPGyDsfgXLQEYMpmIFnVQ== -caniuse-lite@^1.0.30001135: - version "1.0.30001154" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001154.tgz#f3bbc245ce55e4c1cd20fa731b097880181a7f17" - integrity sha512-y9DvdSti8NnYB9Be92ddMZQrcOe04kcQtcxtBx4NkB04+qZ+JUWotnXBJTmxlKudhxNTQ3RRknMwNU2YQl/Org== +caniuse-lite@^1.0.30001219: + version "1.0.30001228" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001228.tgz#bfdc5942cd3326fa51ee0b42fbef4da9d492a7fa" + integrity sha512-QQmLOGJ3DEgokHbMSA8cj2a+geXqmnpyOFT0lhQV6P3/YOJvGDEwoedcwxEQ30gJIwIIunHIicunJ2rzK5gB2A== caseless@~0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= -chai@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/chai/-/chai-4.2.0.tgz#760aa72cf20e3795e84b12877ce0e83737aa29e5" - integrity sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw== +chai@^4.3.4: + version "4.3.4" + resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.4.tgz#b55e655b31e1eac7099be4c08c21964fce2e6c49" + integrity sha512-yS5H68VYOCtN1cjfwumDSuzn/9c+yza4f3reKXlE5rUg7SFcCEy90gJvydNgOYtblyf4Zi6jIWRnXOgErta0KA== dependencies: assertion-error "^1.1.0" check-error "^1.0.2" deep-eql "^3.0.1" get-func-name "^2.0.0" - pathval "^1.1.0" + pathval "^1.1.1" type-detect "^4.0.5" -chalk@^2.0.0, chalk@^2.0.1, chalk@^2.3.0, chalk@^2.4.2: +chalk@^2.0.0, chalk@^2.3.0: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -1103,15 +1085,23 @@ chalk@^4.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" +chalk@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.1.tgz#c80b3fab28bf6371e6863325eee67e618b77e6ad" + integrity sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + check-error@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" integrity sha1-V00xLt2Iu13YkS6Sht1sCu1KrII= -chokidar@3.4.3: - version "3.4.3" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.3.tgz#c1df38231448e45ca4ac588e6c79573ba6a57d5b" - integrity sha512-DtM3g7juCXQxFVSNPNByEC2+NImtBuxQQvWlHunpJIS5Ocr0lG306cC7FCi7cEA0fzmybPUIl4txBIobk1gGOQ== +chokidar@3.5.1: + version "3.5.1" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.1.tgz#ee9ce7bbebd2b79f49f304799d5468e31e14e68a" + integrity sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw== dependencies: anymatch "~3.1.1" braces "~3.0.2" @@ -1121,55 +1111,50 @@ chokidar@3.4.3: normalize-path "~3.0.0" readdirp "~3.5.0" optionalDependencies: - fsevents "~2.1.2" + fsevents "~2.3.1" + +chownr@^1.1.1: + version "1.1.4" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" + integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== chrome-devtools-frontend@1.0.445684: version "1.0.445684" resolved "https://registry.yarnpkg.com/chrome-devtools-frontend/-/chrome-devtools-frontend-1.0.445684.tgz#8540131836024df2b70fe90d0322af368931d762" integrity sha1-hUATGDYCTfK3D+kNAyKvNokx12I= -chrome-timeline@0.0.12: - version "0.0.12" - resolved "https://registry.yarnpkg.com/chrome-timeline/-/chrome-timeline-0.0.12.tgz#1516223b4bf289750b4b244c3f88d0095b10f914" - integrity sha512-lDVZGV2VYVS7kTmoLxwOmoTvKLxBHRiBQqGNvD87Bky+uIsnK6ThwESqIj+rsjx218Vq9tU5H6Bv2u6IsafS/g== +chrome-timeline@0.0.15: + version "0.0.15" + resolved "https://registry.yarnpkg.com/chrome-timeline/-/chrome-timeline-0.0.15.tgz#4fbc719d6abf08e0a0b1f0d5fea80ac461c4410b" + integrity sha512-6vc+MiyFsiOIAje+Z9MFurk9sDnKE5/WknPDpvPfApzApUxdhrml2ybTBRe2U3m2v0vYGis4VMLn9nuuoiofDA== dependencies: "@types/app-root-path" "^1.2.4" - "@types/fs-extra" "^7.0.0" - "@types/puppeteer" "^1.12.4" - app-root-path "^2.2.1" + "@types/fs-extra" "9.0.1" + "@types/puppeteer" "^5.4.3" + app-root-path "^3.0.0" devtools-timeline-model "^1.4.0" - puppeteer "^1.17.0" - simple-git "^1.113.0" - winston "^3.2.1" + fs-extra "9.0.1" + puppeteer "^5.5.0" + simple-git "^2.37.0" + winston "^3.3.3" chrome-trace-event@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz#234090ee97c7d4ad1a2c4beae27505deffc608a4" - integrity sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ== - dependencies: - tslib "^1.9.0" + version "1.0.3" + resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" + integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg== clean-stack@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== -cli-table@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.1.tgz#f53b05266a8b1a0b934b3d0821e6e2dc5914ae23" - integrity sha1-9TsFJmqLGguTSz0IIebi3FkUriM= +cli-table@^0.3.6: + version "0.3.6" + resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.6.tgz#e9d6aa859c7fe636981fd3787378c2a20bce92fc" + integrity sha512-ZkNZbnZjKERTY5NwC2SeMeLeifSPq/pubeRoTpdr3WchLlnZg6hEgvHkK5zL7KNFdd9PmHN8lxrENUwI3cE8vQ== dependencies: colors "1.0.3" -cliui@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" - integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA== - dependencies: - string-width "^3.1.0" - strip-ansi "^5.2.0" - wrap-ansi "^5.1.0" - cliui@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" @@ -1179,6 +1164,24 @@ cliui@^6.0.0: strip-ansi "^6.0.0" wrap-ansi "^6.2.0" +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + +clone-deep@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" + integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== + dependencies: + is-plain-object "^2.0.4" + kind-of "^6.0.2" + shallow-clone "^3.0.0" + clone@^1.0.2: version "1.0.4" resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" @@ -1209,9 +1212,9 @@ color-name@^1.0.0, color-name@~1.1.4: integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== color-string@^1.5.2: - version "1.5.3" - resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.3.tgz#c9bbc5f01b58b5492f3d6857459cb6590ce204cc" - integrity sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw== + version "1.5.5" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.5.tgz#65474a8f0e7439625f3d27a6a19d89fc45223014" + integrity sha512-jgIoum0OfQfq9Whcfc2z/VhCNcmQjWbey6qBX0vqt7YICflUmBCh9E9CiQD5GSJ+Uehixm3NUwHVhqUAWRivZg== dependencies: color-name "^1.0.0" simple-swizzle "^0.2.2" @@ -1224,15 +1227,10 @@ color@3.0.x: color-convert "^1.9.1" color-string "^1.5.2" -colorette@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b" - integrity sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw== - -colornames@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/colornames/-/colornames-1.1.1.tgz#f8889030685c7c4ff9e2a559f5077eb76a816f96" - integrity sha1-+IiQMGhcfE/54qVZ9Qd+t2qBb5Y= +colorette@^1.2.1, colorette@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" + integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w== colors@1.0.3: version "1.0.3" @@ -1267,51 +1265,36 @@ combined-stream@^1.0.6, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" -command-line-usage@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/command-line-usage/-/command-line-usage-6.1.0.tgz#f28376a3da3361ff3d36cfd31c3c22c9a64c7cb6" - integrity sha512-Ew1clU4pkUeo6AFVDFxCbnN7GIZfXl48HIOQeFQnkO3oOqvpI7wdqtLRwv9iOCZ/7A+z4csVZeiDdEcj8g6Wiw== - dependencies: - array-back "^4.0.0" - chalk "^2.4.2" - table-layout "^1.0.0" - typical "^5.2.0" - commander@^2.12.1, commander@^2.20.0: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== -commander@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.0.tgz#b990bfb8ac030aedc6d11bc04d1488ffef56db75" - integrity sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q== +commander@^6.1.0, commander@^6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" + integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== + +commander@^7.0.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" + integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== commondir@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= -complex.js@2.0.11: - version "2.0.11" - resolved "https://registry.yarnpkg.com/complex.js/-/complex.js-2.0.11.tgz#09a873fbf15ffd8c18c9c2201ccef425c32b8bf1" - integrity sha512-6IArJLApNtdg1P1dFtn3dnyzoZBEF0MwMnrfF1exSBRpZYoy4yieMkpZhQDC0uwctw48vii0CFVyHfpgZ/DfGw== +complex.js@^2.0.11: + version "2.0.12" + resolved "https://registry.yarnpkg.com/complex.js/-/complex.js-2.0.12.tgz#fa4df97d8928e5f7b6a86b35bdeecc3a3eda8a22" + integrity sha512-oQX99fwL6LrTVg82gDY1dIWXy6qZRnRL35N+YhIX0N7tSwsa0KFy6IEMHTNuCW4mP7FS7MEqZ/2I/afzYwPldw== concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= -concat-stream@^1.6.2: - version "1.6.2" - resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" - integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== - dependencies: - buffer-from "^1.0.0" - inherits "^2.0.3" - readable-stream "^2.2.2" - typedarray "^0.0.6" - content-disposition@0.5.3: version "0.5.3" resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" @@ -1346,7 +1329,14 @@ core-util-is@1.0.2, core-util-is@~1.0.0: resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= -cross-spawn@^7.0.0, cross-spawn@^7.0.2: +cross-env@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" + integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw== + dependencies: + cross-spawn "^7.0.1" + +cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== @@ -1365,10 +1355,10 @@ cssom@~0.3.6: resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a" integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg== -cssstyle@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-2.2.0.tgz#e4c44debccd6b7911ed617a4395e5754bba59992" - integrity sha512-sEb3XFPx3jNnCAMtqrXPDeSgQr+jojtCeNf8cvMNMh1cG970+lljssvQDzPq6lmmJu2Vhqood/gtEomBiHOGnA== +cssstyle@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-2.3.0.tgz#ff665a0ddbdc31864b09647f34163443d90b0852" + integrity sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A== dependencies: cssom "~0.3.6" @@ -1388,27 +1378,27 @@ data-urls@^2.0.0: whatwg-mimetype "^2.3.0" whatwg-url "^8.0.0" -debug@2.6.9, debug@^2.6.9: +debug@2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== dependencies: ms "2.0.0" -debug@3.2.6, debug@^3.1.0: - version "3.2.6" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" - integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== - dependencies: - ms "^2.1.1" - -debug@4, debug@4.2.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: +debug@4, debug@^4.1.0, debug@^4.1.1: version "4.2.0" resolved "https://registry.yarnpkg.com/debug/-/debug-4.2.0.tgz#7f150f93920e94c58f5574c2fd01a3110effe7f1" integrity sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg== dependencies: ms "2.1.2" +debug@4.3.1, debug@^4.0.1, debug@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" + integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== + dependencies: + ms "2.1.2" + decamelize@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" @@ -1419,10 +1409,10 @@ decamelize@^4.0.0: resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== -decimal.js@10.2.0, decimal.js@^10.0.0, decimal.js@^10.2.0: - version "10.2.0" - resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.2.0.tgz#39466113a9e036111d02f82489b5fd6b0b5ed231" - integrity sha512-vDPw+rDgn3bZe1+F/pyEwb1oMG2XTlRVgAa6B4KccTEpYgF8w6eQllVbQcfIJnZyvzFtFpxnpGtx8dd7DJp/Rw== +decimal.js@^10.0.0, decimal.js@^10.2.1: + version "10.2.1" + resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.2.1.tgz#238ae7b0f0c793d3e3cea410108b35a2c01426a3" + integrity sha512-KaL7+6Fw6i5A2XSnsbhm/6B+NuEA7TZ4vqxnd5tXz9sbKtrN9Srj8ab4vKVdK8YAqZO9P1kg45Y6YLoduPf+kw== deep-eql@^3.0.1: version "3.0.1" @@ -1431,31 +1421,27 @@ deep-eql@^3.0.1: dependencies: type-detect "^4.0.0" -deep-equal@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.0.4.tgz#6b0b407a074666033169df3acaf128e1c6f3eab6" - integrity sha512-BUfaXrVoCfgkOQY/b09QdO9L3XNoF2XH0A3aY9IQwQL/ZjLOe8FQgCNVl1wiolhsFo8kFdO9zdPViCPbmaJA5w== +deep-equal@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.0.5.tgz#55cd2fe326d83f9cbf7261ef0e060b3f724c5cb9" + integrity sha512-nPiRgmbAtm1a3JsnLCf6/SLfXcjyN5v8L1TXzdCmHrXJ4hx+gW/w1YCcn7z8gJtSiDArZCgYtbao3QqLm/N1Sw== dependencies: - es-abstract "^1.18.0-next.1" - es-get-iterator "^1.1.0" + call-bind "^1.0.0" + es-get-iterator "^1.1.1" + get-intrinsic "^1.0.1" is-arguments "^1.0.4" is-date-object "^1.0.2" is-regex "^1.1.1" isarray "^2.0.5" - object-is "^1.1.3" + object-is "^1.1.4" object-keys "^1.1.1" - object.assign "^4.1.1" + object.assign "^4.1.2" regexp.prototype.flags "^1.3.0" side-channel "^1.0.3" which-boxed-primitive "^1.0.1" which-collection "^1.0.1" which-typed-array "^1.1.2" -deep-extend@~0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" - integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== - deep-is@^0.1.3, deep-is@~0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" @@ -1475,7 +1461,7 @@ defaults@^1.0.3: dependencies: clone "^1.0.2" -define-properties@^1.1.2, define-properties@^1.1.3: +define-properties@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== @@ -1497,6 +1483,11 @@ destroy@~1.0.4: resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= +devtools-protocol@0.0.818844: + version "0.0.818844" + resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.818844.tgz#d1947278ec85b53e4c8ca598f607a28fa785ba9e" + integrity sha512-AD1hi7iVJ8OD0aMLQU5VK0XH9LDlA1+BcPIgrAxPfaibx2DbWucuyOhc4oyQCbnvDDO68nN6/LcKfqTP343Jjg== + devtools-timeline-model@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/devtools-timeline-model/-/devtools-timeline-model-1.4.0.tgz#91f9624fb0313fa3ebeda7bf99865357bc66c726" @@ -1505,21 +1496,12 @@ devtools-timeline-model@^1.4.0: chrome-devtools-frontend "1.0.445684" resolve "1.1.7" -diagnostics@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/diagnostics/-/diagnostics-1.1.1.tgz#cab6ac33df70c9d9a727490ae43ac995a769b22a" - integrity sha512-8wn1PmdunLJ9Tqbx+Fx/ZEuHfJf4NKSN2ZBj7SJC/OWRWha843+WsTjqMe1B5E3p28jqBlp+mJ2fPVxPyNgYKQ== - dependencies: - colorspace "1.1.x" - enabled "1.0.x" - kuler "1.0.x" +diff@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" + integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== -diff@3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" - integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== - -diff@4.0.2, diff@^4.0.1: +diff@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== @@ -1558,134 +1540,94 @@ ee-first@1.1.1: resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= -electron-to-chromium@^1.3.571: - version "1.3.585" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.585.tgz#71cdb722c73488b9475ad1c572cf43a763ef9081" - integrity sha512-xoeqjMQhgHDZM7FiglJAb2aeOxHZWFruUc3MbAGTgE7GB8rr5fTn1Sdh5THGuQtndU3GuXlu91ZKqRivxoCZ/A== - -emoji-regex@^7.0.1: - version "7.0.3" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" - integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== +electron-to-chromium@^1.3.723: + version "1.3.727" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.727.tgz#857e310ca00f0b75da4e1db6ff0e073cc4a91ddf" + integrity sha512-Mfz4FIB4FSvEwBpDfdipRIrwd6uo8gUDoRDF4QEYb4h4tSuI3ov594OrjU6on042UlFHouIJpClDODGkPcBSbg== emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== -emojis-list@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" - integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== - -enabled@1.0.x: - version "1.0.2" - resolved "https://registry.yarnpkg.com/enabled/-/enabled-1.0.2.tgz#965f6513d2c2d1c5f4652b64a2e3396467fc2f93" - integrity sha1-ll9lE9LC0cX0ZStkouM5ZGf8L5M= - dependencies: - env-variable "0.0.x" +enabled@2.0.x: + version "2.0.0" + resolved "https://registry.yarnpkg.com/enabled/-/enabled-2.0.0.tgz#f9dd92ec2d6f4bbc0d5d1e64e21d61cd4665e7c2" + integrity sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ== encodeurl@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= -end-of-stream@^1.1.0: +end-of-stream@^1.1.0, end-of-stream@^1.4.1: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== dependencies: once "^1.4.0" -enhanced-resolve@^4.0.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.3.0.tgz#3b806f3bfafc1ec7de69551ef93cca46c1704126" - integrity sha512-3e87LvavsdxyoCfGusJnrZ5G8SLPOFeHSNpZI/ATL9a5leXo2k0w6MKnbqhdBad9qTobSfB20Ld7UmgoNbAZkQ== - dependencies: - graceful-fs "^4.1.2" - memory-fs "^0.5.0" - tapable "^1.0.0" - -enhanced-resolve@^5.3.1: - version "5.3.1" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.3.1.tgz#3f988d0d7775bdc2d96ede321dc81f8249492f57" - integrity sha512-G1XD3MRGrGfNcf6Hg0LVZG7GIKcYkbfHa5QMxt1HDUTdYoXH0JR1xXyg+MaKLF73E9A27uWNVxvFivNRYeUB6w== +enhanced-resolve@^5.0.0, enhanced-resolve@^5.8.0: + version "5.8.2" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.8.2.tgz#15ddc779345cbb73e97c611cd00c01c1e7bf4d8b" + integrity sha512-F27oB3WuHDzvR2DOGNTaYy0D5o0cnrv8TeI482VM4kYgQd/FT9lUQwuNsJ0oOHtBUq7eiW5ytqzp7nBFknL+GA== dependencies: graceful-fs "^4.2.4" - tapable "^2.0.0" + tapable "^2.2.0" -enquirer@^2.3.5, enquirer@^2.3.6: +enquirer@^2.3.5: version "2.3.6" resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== dependencies: ansi-colors "^4.1.1" -env-variable@0.0.x: - version "0.0.6" - resolved "https://registry.yarnpkg.com/env-variable/-/env-variable-0.0.6.tgz#74ab20b3786c545b62b4a4813ab8cf22726c9808" - integrity sha512-bHz59NlBbtS0NhftmR8+ExBEekE7br0e01jw+kk0NDro7TtZzBYZ5ScGPs3OmwnpyfHTHOtr1Y6uedCdrIldtg== - envinfo@^7.7.3: - version "7.7.3" - resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.7.3.tgz#4b2d8622e3e7366afb8091b23ed95569ea0208cc" - integrity sha512-46+j5QxbPWza0PB1i15nZx0xQ4I/EfQxg9J8Had3b408SV63nEtor2e+oiY63amTo9KTuh2a3XLObNwduxYwwA== + version "7.8.1" + resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.8.1.tgz#06377e3e5f4d379fea7ac592d5ad8927e0c4d475" + integrity sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw== -errno@^0.1.3: - version "0.1.7" - resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618" - integrity sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg== - dependencies: - prr "~1.0.1" - -es-abstract@^1.17.0-next.1, es-abstract@^1.17.4, es-abstract@^1.17.5: - version "1.17.5" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.5.tgz#d8c9d1d66c8981fb9200e2251d799eee92774ae9" - integrity sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg== +es-abstract@^1.18.0-next.1, es-abstract@^1.18.0-next.2: + version "1.18.0" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0.tgz#ab80b359eecb7ede4c298000390bc5ac3ec7b5a4" + integrity sha512-LJzK7MrQa8TS0ja2w3YNLzUgJCGPdPOV1yVvezjNnS89D+VR08+Szt2mz3YB2Dck/+w5tfIq/RoUAFqJJGM2yw== dependencies: + call-bind "^1.0.2" es-to-primitive "^1.2.1" function-bind "^1.1.1" + get-intrinsic "^1.1.1" has "^1.0.3" - has-symbols "^1.0.1" - is-callable "^1.1.5" - is-regex "^1.0.5" - object-inspect "^1.7.0" + has-symbols "^1.0.2" + is-callable "^1.2.3" + is-negative-zero "^2.0.1" + is-regex "^1.1.2" + is-string "^1.0.5" + object-inspect "^1.9.0" object-keys "^1.1.1" - object.assign "^4.1.0" - string.prototype.trimleft "^2.1.1" - string.prototype.trimright "^2.1.1" + object.assign "^4.1.2" + string.prototype.trimend "^1.0.4" + string.prototype.trimstart "^1.0.4" + unbox-primitive "^1.0.0" -es-abstract@^1.18.0-next.0, es-abstract@^1.18.0-next.1: - version "1.18.0-next.1" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0-next.1.tgz#6e3a0a4bda717e5023ab3b8e90bec36108d22c68" - integrity sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA== +es-get-iterator@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.2.tgz#9234c54aba713486d7ebde0220864af5e2b283f7" + integrity sha512-+DTO8GYwbMCwbywjimwZMHp8AuYXOS2JZFWoi2AlPOS3ebnII9w/NLpNZtA7A0YLaVDw+O7KFCeoIV7OPvM7hQ== dependencies: - es-to-primitive "^1.2.1" - function-bind "^1.1.1" - has "^1.0.3" + call-bind "^1.0.2" + get-intrinsic "^1.1.0" has-symbols "^1.0.1" - is-callable "^1.2.2" - is-negative-zero "^2.0.0" - is-regex "^1.1.1" - object-inspect "^1.8.0" - object-keys "^1.1.1" - object.assign "^4.1.1" - string.prototype.trimend "^1.0.1" - string.prototype.trimstart "^1.0.1" - -es-get-iterator@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.0.tgz#bb98ad9d6d63b31aacdc8f89d5d0ee57bcb5b4c8" - integrity sha512-UfrmHuWQlNMTs35e1ypnvikg6jCz3SK8v8ImvmDsh36fCVUR1MqoFDiyn0/k52C8NqO3YsO8Oe0azeesNuqSsQ== - dependencies: - es-abstract "^1.17.4" - has-symbols "^1.0.1" - is-arguments "^1.0.4" - is-map "^2.0.1" - is-set "^2.0.1" + is-arguments "^1.1.0" + is-map "^2.0.2" + is-set "^2.0.2" is-string "^1.0.5" isarray "^2.0.5" +es-module-lexer@^0.4.0: + version "0.4.1" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.4.1.tgz#dda8c6a14d8f340a24e34331e0fab0cb50438e0e" + integrity sha512-ooYciCUtfw6/d2w56UVeqHPcoCFAiJdz5XOkYpv/Txl1HMUozpXjz/2RIQgqwKdXNDPSF1W7mJCFse3G+HDyAA== + es-to-primitive@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" @@ -1700,19 +1642,7 @@ es6-error@^4.0.1: resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d" integrity sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg== -es6-promise@^4.0.3: - version "4.2.8" - resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" - integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== - -es6-promisify@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" - integrity sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM= - dependencies: - es6-promise "^4.0.3" - -escalade@^3.1.0: +escalade@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== @@ -1722,28 +1652,33 @@ escape-html@~1.0.3: resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= -escape-latex@1.2.0: +escape-latex@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/escape-latex/-/escape-latex-1.2.0.tgz#07c03818cf7dac250cce517f4fda1b001ef2bca1" integrity sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw== -escape-string-regexp@1.0.5, escape-string-regexp@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= - escape-string-regexp@4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== -escodegen@^1.14.1: - version "1.14.1" - resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.1.tgz#ba01d0c8278b5e95a9a45350142026659027a457" - integrity sha512-Bmt7NcRySdIfNPfU2ZoXDrrXsG9ZjvDxcAlMfDUgRBjLOWTuIACXPBFJH7Z+cLb40JeQco5toikyc9t9P8E9SQ== +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + +escape-string-regexp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" + integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== + +escodegen@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.0.0.tgz#5e32b12833e8aa8fa35e1bf0befa89380484c7dd" + integrity sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw== dependencies: esprima "^4.0.1" - estraverse "^4.2.0" + estraverse "^5.2.0" esutils "^2.0.2" optionator "^0.8.1" optionalDependencies: @@ -1770,17 +1705,17 @@ eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.3.0: integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== eslint-visitor-keys@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz#21fdc8fbcd9c795cc0321f0563702095751511a8" - integrity sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ== + version "2.1.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" + integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== -eslint@^7.12.1: - version "7.12.1" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.12.1.tgz#bd9a81fa67a6cfd51656cdb88812ce49ccec5801" - integrity sha512-HlMTEdr/LicJfN08LB3nM1rRYliDXOmfoO4vj39xN6BLpFzF00hbwBoqHk8UcJ2M/3nlARZWy/mslvGEuZFvsg== +eslint@^7.26.0: + version "7.26.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.26.0.tgz#d416fdcdcb3236cd8f282065312813f8c13982f6" + integrity sha512-4R1ieRf52/izcZE7AlLy56uIHHDLT74Yzz2Iv2l6kDaYvEu9x+wMB5dZArVL8SYGXSYV2YAg70FcW5Y5nGGNIg== dependencies: - "@babel/code-frame" "^7.0.0" - "@eslint/eslintrc" "^0.2.1" + "@babel/code-frame" "7.12.11" + "@eslint/eslintrc" "^0.4.1" ajv "^6.10.0" chalk "^4.0.0" cross-spawn "^7.0.2" @@ -1790,13 +1725,13 @@ eslint@^7.12.1: eslint-scope "^5.1.1" eslint-utils "^2.1.0" eslint-visitor-keys "^2.0.0" - espree "^7.3.0" - esquery "^1.2.0" + espree "^7.3.1" + esquery "^1.4.0" esutils "^2.0.2" - file-entry-cache "^5.0.1" + file-entry-cache "^6.0.1" functional-red-black-tree "^1.0.1" glob-parent "^5.0.0" - globals "^12.1.0" + globals "^13.6.0" ignore "^4.0.6" import-fresh "^3.0.0" imurmurhash "^0.1.4" @@ -1804,7 +1739,7 @@ eslint@^7.12.1: js-yaml "^3.13.1" json-stable-stringify-without-jsonify "^1.0.1" levn "^0.4.1" - lodash "^4.17.19" + lodash "^4.17.21" minimatch "^3.0.4" natural-compare "^1.4.0" optionator "^0.9.1" @@ -1813,17 +1748,17 @@ eslint@^7.12.1: semver "^7.2.1" strip-ansi "^6.0.0" strip-json-comments "^3.1.0" - table "^5.2.3" + table "^6.0.4" text-table "^0.2.0" v8-compile-cache "^2.0.3" -espree@^7.3.0: - version "7.3.0" - resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.0.tgz#dc30437cf67947cf576121ebd780f15eeac72348" - integrity sha512-dksIWsvKCixn1yrEXO8UosNSxaDoSYpq9reEjZSbHLpT5hpaCAKTLBwq0RHtLrIr+c0ByiYzWT8KTMRzoRCNlw== +espree@^7.3.0, espree@^7.3.1: + version "7.3.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.1.tgz#f2df330b752c6f55019f8bd89b7660039c1bbbb6" + integrity sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g== dependencies: acorn "^7.4.0" - acorn-jsx "^5.2.0" + acorn-jsx "^5.3.1" eslint-visitor-keys "^1.3.0" esprima@^4.0.0, esprima@^4.0.1: @@ -1831,10 +1766,10 @@ esprima@^4.0.0, esprima@^4.0.1: resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== -esquery@^1.2.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.3.1.tgz#b78b5828aa8e214e29fb74c4d5b752e1c033da57" - integrity sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ== +esquery@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5" + integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w== dependencies: estraverse "^5.1.0" @@ -1845,7 +1780,7 @@ esrecurse@^4.3.0: dependencies: estraverse "^5.2.0" -estraverse@^4.1.1, estraverse@^4.2.0: +estraverse@^4.1.1: version "4.3.0" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== @@ -1866,23 +1801,23 @@ etag@~1.8.1: integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= events@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/events/-/events-3.2.0.tgz#93b87c18f8efcd4202a461aec4dfc0556b639379" - integrity sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg== + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== -execa@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-4.1.0.tgz#4e5491ad1572f2f17a77d388c6c857135b22847a" - integrity sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA== +execa@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-5.0.0.tgz#4029b0007998a841fbd1032e5f4de86a3c1e3376" + integrity sha512-ov6w/2LCiuyO4RLYGdpFGjkcs0wMTgGE8PrkTHikeUy5iJekXyPIKUjifk5CsE0pt7sMCrMZ3YNqoCj6idQOnQ== dependencies: - cross-spawn "^7.0.0" - get-stream "^5.0.0" - human-signals "^1.1.1" + cross-spawn "^7.0.3" + get-stream "^6.0.0" + human-signals "^2.1.0" is-stream "^2.0.0" merge-stream "^2.0.0" - npm-run-path "^4.0.0" - onetime "^5.1.0" - signal-exit "^3.0.2" + npm-run-path "^4.0.1" + onetime "^5.1.2" + signal-exit "^3.0.3" strip-final-newline "^2.0.0" express-ws@^4.0.0: @@ -1933,17 +1868,7 @@ extend@~3.0.2: resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== -extract-zip@^1.6.6: - version "1.7.0" - resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.7.0.tgz#556cc3ae9df7f452c493a0cfb51cc30277940927" - integrity sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA== - dependencies: - concat-stream "^1.6.2" - debug "^2.6.9" - mkdirp "^0.5.4" - yauzl "^2.10.0" - -extract-zip@^2.0.1: +extract-zip@^2.0.0, extract-zip@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a" integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg== @@ -1970,9 +1895,9 @@ fast-deep-equal@^3.1.1: integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA== fast-glob@^3.1.1: - version "3.2.4" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.4.tgz#d20aefbf99579383e7f3cc66529158c9b98554d3" - integrity sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ== + version "3.2.5" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.5.tgz#7939af2a656de79a4f1901903ee8adcaa7cb9661" + integrity sha512-2DtFcgT68wiTTiwZ2hNdJfcHNke9XOfnwmBRWXhmeKM8rF0TGwmC/Qto3S7RoZKp5cilZbxzO5iTNTQsJ+EeDg== dependencies: "@nodelib/fs.stat" "^2.0.2" "@nodelib/fs.walk" "^1.2.3" @@ -1996,10 +1921,15 @@ fast-safe-stringify@^2.0.4: resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz#124aa885899261f68aedb42a7c080de9da608743" integrity sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA== +fastest-levenshtein@^1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz#9990f7d3a88cc5a9ffd1f1745745251700d497e2" + integrity sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow== + fastq@^1.6.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.8.0.tgz#550e1f9f59bbc65fe185cb6a9b4d95357107f481" - integrity sha512-SMIZoZdLh/fgofivvIkmknUXyPnvxRE3DhtZ5Me3Mrsk5gyPL42F0xr51TdRXskBxHfMp+07bcYzfsYEsSQA9Q== + version "1.11.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.11.0.tgz#bb9fb955a07130a918eb63c1f5161cc32a5d0858" + integrity sha512-7Eczs8gIPDrVzT+EksYBcupqMyxSHXXrHOLRRxU2/DicV8789MRBRR8+Hc2uWzUupOs4YS4JzBmBxjjCVBxD/g== dependencies: reusify "^1.0.4" @@ -2010,17 +1940,17 @@ fd-slicer@~1.1.0: dependencies: pend "~1.2.0" -fecha@^2.3.3: - version "2.3.3" - resolved "https://registry.yarnpkg.com/fecha/-/fecha-2.3.3.tgz#948e74157df1a32fd1b12c3a3c3cdcb6ec9d96cd" - integrity sha512-lUGBnIamTAwk4znq5BcqsDaxSmZ9nDVJaij6NvRt/Tg4R69gERA+otPKbS86ROw9nxVMw2/mp1fnaiWqbs6Sdg== +fecha@^4.2.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/fecha/-/fecha-4.2.1.tgz#0a83ad8f86ef62a091e22bb5a039cd03d23eecce" + integrity sha512-MMMQ0ludy/nBs1/o0zVOiKTpG7qMbonKUzjJgQFEuvq6INZ1OraKPRAWkBq5vlKLOUMpmNYG1JoN3oDPUQ9m3Q== -file-entry-cache@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-5.0.1.tgz#ca0f6efa6dd3d561333fb14515065c2fafdf439c" - integrity sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g== +file-entry-cache@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" + integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== dependencies: - flat-cache "^2.0.1" + flat-cache "^3.0.4" fill-range@^7.0.1: version "7.0.1" @@ -2051,13 +1981,6 @@ find-cache-dir@^3.2.0: make-dir "^3.0.2" pkg-dir "^4.1.0" -find-up@3.0.0, find-up@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" - integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== - dependencies: - locate-path "^3.0.0" - find-up@5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" @@ -2074,31 +1997,28 @@ find-up@^4.0.0, find-up@^4.1.0: locate-path "^5.0.0" path-exists "^4.0.0" -flat-cache@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0" - integrity sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA== +flat-cache@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" + integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== dependencies: - flatted "^2.0.0" - rimraf "2.6.3" - write "1.0.3" - -flat@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/flat/-/flat-4.1.0.tgz#090bec8b05e39cba309747f1d588f04dbaf98db2" - integrity sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw== - dependencies: - is-buffer "~2.0.3" + flatted "^3.1.0" + rimraf "^3.0.2" flat@^5.0.2: version "5.0.2" resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== -flatted@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138" - integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA== +flatted@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.1.1.tgz#c4b489e80096d9df1dfc97c79871aea7c617c469" + integrity sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA== + +fn.name@1.x.x: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc" + integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw== foreach@^2.0.5: version "2.0.5" @@ -2132,10 +2052,10 @@ forwarded@~0.1.2: resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= -fraction.js@4.0.12: - version "4.0.12" - resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.0.12.tgz#0526d47c65a5fb4854df78bc77f7bec708d7b8c3" - integrity sha512-8Z1K0VTG4hzYY7kA/1sj4/r1/RWLBD3xwReT/RCrUCbzPszjNQCCsy3ktkU/eaEqX3MYa4pY37a52eiBlPMlhA== +fraction.js@^4.0.13: + version "4.0.13" + resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.0.13.tgz#3c1c315fa16b35c85fffa95725a36fa729c69dfe" + integrity sha512-E1fz2Xs9ltlUp+qbiyx9wmt2n9dRzPsS11Jtdb8D2o+cC7wr9xkkKsVKJuBX0ST+LVS+LhLO+SbLJNtfWcJvXA== fresh@0.5.2: version "0.5.2" @@ -2147,15 +2067,30 @@ fromentries@^1.2.0: resolved "https://registry.yarnpkg.com/fromentries/-/fromentries-1.2.0.tgz#e6aa06f240d6267f913cea422075ef88b63e7897" integrity sha512-33X7H/wdfO99GdRLLgkjUrD4geAFdq/Uv0kl3HD4da6HDixd2GUg8Mw7dahLCV9r/EARkmtYBB6Tch4EEokFTQ== +fs-constants@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" + integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== + +fs-extra@9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.0.1.tgz#910da0062437ba4c39fedd863f1675ccfefcb9fc" + integrity sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ== + dependencies: + at-least-node "^1.0.0" + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^1.0.0" + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= -fsevents@~2.1.2: - version "2.1.3" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e" - integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ== +fsevents@~2.3.1: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== function-bind@^1.1.1: version "1.1.1" @@ -2172,7 +2107,7 @@ gensync@^1.0.0-beta.1: resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269" integrity sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg== -get-caller-file@^2.0.1: +get-caller-file@^2.0.1, get-caller-file@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== @@ -2182,18 +2117,32 @@ get-func-name@^2.0.0: resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41" integrity sha1-6td0q+5y4gQJQzoGY2YCPdaIekE= +get-intrinsic@^1.0.1, get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6" + integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.1" + get-package-type@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== -get-stream@^5.0.0, get-stream@^5.1.0: +get-stream@^5.1.0: version "5.2.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== dependencies: pump "^3.0.0" +get-stream@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + getpass@^0.1.1: version "0.1.7" resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" @@ -2202,9 +2151,9 @@ getpass@^0.1.1: assert-plus "^1.0.0" glob-parent@^5.0.0, glob-parent@^5.1.0, glob-parent@~5.1.0: - version "5.1.1" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229" - integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ== + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== dependencies: is-glob "^4.0.1" @@ -2213,10 +2162,10 @@ glob-to-regexp@^0.4.1: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== -glob@7.1.3: - version "7.1.3" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1" - integrity sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ== +glob@7.1.6, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== dependencies: fs.realpath "^1.0.0" inflight "^1.0.4" @@ -2225,10 +2174,10 @@ glob@7.1.3: once "^1.3.0" path-is-absolute "^1.0.0" -glob@7.1.6, glob@^7.0.5, glob@^7.1.1, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: - version "7.1.6" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" - integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== +glob@^7.1.1, glob@^7.1.7: + version "7.1.7" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" + integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== dependencies: fs.realpath "^1.0.0" inflight "^1.0.4" @@ -2249,10 +2198,17 @@ globals@^12.1.0: dependencies: type-fest "^0.8.1" +globals@^13.6.0: + version "13.8.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.8.0.tgz#3e20f504810ce87a8d72e55aecf8435b50f4c1b3" + integrity sha512-rHtdA6+PDBIjeEvA91rpqzEvk/k3/i7EeNQiryiWuJH0Hw9cpyJMAt2jtbAwUaRdhD+573X4vWw6IcjKPasi9Q== + dependencies: + type-fest "^0.20.2" + globby@^11.0.1: - version "11.0.1" - resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.1.tgz#9a2bf107a068f3ffeabc49ad702c79ede8cfd357" - integrity sha512-iH9RmgwCmUJHi2z5o2l3eTtGBtXek1OYlHrbcxOYugyHLmAsZrPj43OtHThd62Buh/Vv6VyCBD2bdyWcGNQqoQ== + version "11.0.3" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.3.tgz#9b1f0cb523e171dd1ad8c7b2a9fb4b644b9593cb" + integrity sha512-ffdmosjA807y7+lA1NM0jELARVmYul/715xiILEjo3hBLPTcirgQNnXECn5g3mtR8TOLCVbkfua1Hpen25/Xcg== dependencies: array-union "^2.1.0" dir-glob "^3.0.1" @@ -2261,11 +2217,16 @@ globby@^11.0.1: merge2 "^1.3.0" slash "^3.0.0" -graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.2.4: +graceful-fs@^4.1.15, graceful-fs@^4.1.2: version "4.2.4" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== +graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4: + version "4.2.6" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" + integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== + growl@1.10.5: version "1.10.5" resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" @@ -2277,13 +2238,18 @@ har-schema@^2.0.0: integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= har-validator@~5.1.3: - version "5.1.3" - resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080" - integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g== + version "5.1.5" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.5.tgz#1f0803b9f8cb20c0fa13822df1ecddb36bde1efd" + integrity sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w== dependencies: - ajv "^6.5.5" + ajv "^6.12.3" har-schema "^2.0.0" +has-bigints@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113" + integrity sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA== + has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" @@ -2294,10 +2260,10 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -has-symbols@^1.0.0, has-symbols@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8" - integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg== +has-symbols@^1.0.1, has-symbols@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423" + integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== has@^1.0.3: version "1.0.3" @@ -2362,13 +2328,13 @@ http-signature@~1.2.0: jsprim "^1.2.2" sshpk "^1.7.0" -https-proxy-agent@^2.2.1: - version "2.2.4" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz#4ee7a737abd92678a293d9b34a1af4d0d08c787b" - integrity sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg== +https-proxy-agent@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz#702b71fb5520a132a66de1f67541d9e62154d82b" + integrity sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg== dependencies: - agent-base "^4.3.0" - debug "^3.1.0" + agent-base "5" + debug "4" https-proxy-agent@^5.0.0: version "5.0.0" @@ -2378,10 +2344,10 @@ https-proxy-agent@^5.0.0: agent-base "6" debug "4" -human-signals@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" - integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== +human-signals@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== iconv-lite@0.4.24: version "0.4.24" @@ -2397,6 +2363,11 @@ iconv-lite@^0.6.2: dependencies: safer-buffer ">= 2.1.2 < 3.0.0" +ieee754@^1.1.13: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + ignore@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" @@ -2408,9 +2379,9 @@ ignore@^5.1.4: integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== import-fresh@^3.0.0, import-fresh@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.2.1.tgz#633ff618506e793af5ac91bf48b72677e15cbe66" - integrity sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ== + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== dependencies: parent-module "^1.0.0" resolve-from "^4.0.0" @@ -2441,7 +2412,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@~2.0.3: +inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -2456,30 +2427,27 @@ interpret@^2.2.0: resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9" integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw== -ip-regex@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9" - integrity sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk= - ipaddr.js@1.9.1: version "1.9.1" resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== -is-arguments@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.0.4.tgz#3faf966c7cba0ff437fb31f6250082fcf0448cf3" - integrity sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA== +is-arguments@^1.0.4, is-arguments@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.0.tgz#62353031dfbee07ceb34656a6bde59efecae8dd9" + integrity sha512-1Ij4lOMPl/xB5kBDn7I+b2ttPMKa8szhEIrXDuXQD/oe3HJLTLhqhgGspwgyGd6MOywBUqVvYicF72lkgDnIHg== + dependencies: + call-bind "^1.0.0" is-arrayish@^0.3.1: version "0.3.2" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== -is-bigint@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.0.tgz#73da8c33208d00f130e9b5e15d23eac9215601c4" - integrity sha512-t5mGUXC/xRheCK431ylNiSkGGpBp8bHENBcENTkDT6ppwPzEVxNGZRvgvmOEfbWkFhA7D2GEuE2mmQTr78sl2g== +is-bigint@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.2.tgz#ffb381442503235ad245ea89e45b3dbff040ee5a" + integrity sha512-0JV5+SOCQkIdzjBK9buARcV804Ddu7A0Qet6sHi3FimE9ne6m4BGQZfRn+NZiXbBk4F4XmHfDZIipLj9pX8dSA== is-binary-path@~2.1.0: version "2.1.0" @@ -2488,30 +2456,29 @@ is-binary-path@~2.1.0: dependencies: binary-extensions "^2.0.0" -is-boolean-object@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.0.1.tgz#10edc0900dd127697a92f6f9807c7617d68ac48e" - integrity sha512-TqZuVwa/sppcrhUCAYkGBk7w0yxfQQnxq28fjkO53tnK9FQXmdwz2JS5+GjsWQ6RByES1K40nI+yDic5c9/aAQ== +is-boolean-object@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.1.tgz#3c0878f035cb821228d350d2e1e36719716a3de8" + integrity sha512-bXdQWkECBUIAcCkeH1unwJLIpZYaa5VvuygSyS/c2lf719mTKZDU5UdDRlpd01UjADgmW8RfqaP+mRaVPdr/Ng== + dependencies: + call-bind "^1.0.2" -is-buffer@~2.0.3: - version "2.0.4" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.4.tgz#3e572f23c8411a5cfd9557c849e3665e0b290623" - integrity sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A== +is-callable@^1.1.4, is-callable@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.3.tgz#8b1e0500b73a1d76c70487636f368e519de8db8e" + integrity sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ== -is-callable@^1.1.4, is-callable@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.5.tgz#f7e46b596890456db74e7f6e976cb3273d06faab" - integrity sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q== - -is-callable@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.2.tgz#c7c6715cd22d4ddb48d3e19970223aceabb080d9" - integrity sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA== +is-core-module@^2.2.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.4.0.tgz#8e9fc8e15027b011418026e98f0e6f4d86305cc1" + integrity sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A== + dependencies: + has "^1.0.3" is-date-object@^1.0.1, is-date-object@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e" - integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g== + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.4.tgz#550cfcc03afada05eea3dd30981c7b09551f73e5" + integrity sha512-/b4ZVsG7Z5XVtIxs/h9W8nvfLgSAyKYdtGWQLbqy6jA1icmgjf8WCoTKgeS4wy5tYaPePouzFMANbnj94c2Z+A== is-extglob@^2.1.1: version "2.1.1" @@ -2535,20 +2502,20 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: dependencies: is-extglob "^2.1.1" -is-map@^2.0.1: +is-map@^2.0.1, is-map@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.2.tgz#00922db8c9bf73e81b7a335827bc2a43f2b91127" + integrity sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg== + +is-negative-zero@^2.0.1: version "2.0.1" - resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.1.tgz#520dafc4307bb8ebc33b813de5ce7c9400d644a1" - integrity sha512-T/S49scO8plUiAOA2DBTBG3JHpn1yiw0kRp6dgiZ0v2/6twi5eiB0rHtHFH9ZIrvlWc6+4O+m4zg5+Z833aXgw== + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24" + integrity sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w== -is-negative-zero@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.0.tgz#9553b121b0fac28869da9ed459e20c7543788461" - integrity sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE= - -is-number-object@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.4.tgz#36ac95e741cf18b283fc1ddf5e83da798e3ec197" - integrity sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw== +is-number-object@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.5.tgz#6edfaeed7950cff19afedce9fbfca9ee6dd289eb" + integrity sha512-RU0lI/n95pMoUKu9v1BZP5MBcZuNSVJkMkAG2dJqC4z2GlkGUNeH68SuHuBKBD/XFe+LHZ+f9BKkLET60Niedw== is-number@^7.0.0: version "7.0.0" @@ -2560,52 +2527,56 @@ is-plain-obj@^2.1.0: resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== -is-potential-custom-element-name@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz#0c52e54bcca391bb2c494b21e8626d7336c6e397" - integrity sha1-DFLlS8yjkbssSUsh6GJtczbG45c= - -is-regex@^1.0.5, is-regex@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9" - integrity sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg== +is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== dependencies: - has-symbols "^1.0.1" + isobject "^3.0.1" -is-set@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.1.tgz#d1604afdab1724986d30091575f54945da7e5f43" - integrity sha512-eJEzOtVyenDs1TMzSQ3kU3K+E0GUS9sno+F0OBT97xsgcJsF9nXMBtkT9/kut5JEpM7oL7X/0qxR17K3mcwIAA== +is-potential-custom-element-name@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" + integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== -is-stream@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" - integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= +is-regex@^1.1.1, is-regex@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.3.tgz#d029f9aff6448b93ebbe3f33dac71511fdcbef9f" + integrity sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ== + dependencies: + call-bind "^1.0.2" + has-symbols "^1.0.2" + +is-set@^2.0.1, is-set@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.2.tgz#90755fa4c2562dc1c5d4024760d6119b94ca18ec" + integrity sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g== is-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3" integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw== -is-string@^1.0.4, is-string@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6" - integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ== +is-string@^1.0.5: + version "1.0.6" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.6.tgz#3fe5d5992fb0d93404f32584d4b0179a71b54a5f" + integrity sha512-2gdzbKUuqtQ3lYNrUTQYoClPhm7oQu4UdpSZMp1/DGgkHBT8E2Z1l0yMdb6D4zNAxwDiMv8MdulKROJGNl0Q0w== -is-symbol@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937" - integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ== +is-symbol@^1.0.2, is-symbol@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" + integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== dependencies: - has-symbols "^1.0.1" + has-symbols "^1.0.2" is-typed-array@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.3.tgz#a4ff5a5e672e1a55f99c7f54e59597af5c1df04d" - integrity sha512-BSYUBOK/HJibQ30wWkWold5txYwMUXQct9YHAQJr8fSwvZoiglcqB0pd7vEN23+Tsi9IUEjztdOSzl4qLVYGTQ== + version "1.1.5" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.5.tgz#f32e6e096455e329eb7b423862456aa213f0eb4e" + integrity sha512-S+GRDgJlR3PyEbsX/Fobd9cqpZBuvUS+8asRqYDMLCb2qMzt1oz5m5oxQCxOgUDxiWsOVNi4yaF+/uvdlHlYug== dependencies: - available-typed-arrays "^1.0.0" - es-abstract "^1.17.4" + available-typed-arrays "^1.0.2" + call-bind "^1.0.2" + es-abstract "^1.18.0-next.2" foreach "^2.0.5" has-symbols "^1.0.1" @@ -2644,6 +2615,11 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= +isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= + isstream@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" @@ -2713,39 +2689,38 @@ istanbul-reports@^3.0.2: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" -javascript-natural-sort@0.7.1: +javascript-natural-sort@^0.7.1: version "0.7.1" resolved "https://registry.yarnpkg.com/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz#f9e2303d4507f6d74355a73664d1440fb5a0ef59" integrity sha1-+eIwPUUH9tdDVac2ZNFED7Wg71k= -jest-worker@^26.6.1: - version "26.6.1" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.6.1.tgz#c2ae8cde6802cc14056043f997469ec170d9c32a" - integrity sha512-R5IE3qSGz+QynJx8y+ICEkdI2OJ3RJjRQVEyCcFAd3yVhQSEtquziPO29Mlzgn07LOVE8u8jhJ1FqcwegiXWOw== +jest-worker@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.6.2.tgz#7f72cbc4d643c365e27b9fd775f9d0eaa9c7a8ed" + integrity sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ== dependencies: "@types/node" "*" merge-stream "^2.0.0" supports-color "^7.0.0" jpeg-js@^0.4.2: - version "0.4.2" - resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.4.2.tgz#8b345b1ae4abde64c2da2fe67ea216a114ac279d" - integrity sha512-+az2gi/hvex7eLTMTlbRLOhH6P6WFdk2ITI8HJsaH2VqYO0I594zXSYEP+tf4FW+8Cy68ScDXoAsQdyQanv3sw== + version "0.4.3" + resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.4.3.tgz#6158e09f1983ad773813704be80680550eff977b" + integrity sha512-ru1HWKek8octvUHFHvE5ZzQ1yAsJmIvRdGWvSoKV52XKyuyYA437QWDttXT8eZXDSbuMpHlLzPDZUPd6idIz+Q== js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-yaml@3.13.1: - version "3.13.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" - integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw== +js-yaml@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.0.0.tgz#f426bc0ff4b4051926cd588c71113183409a121f" + integrity sha512-pqon0s+4ScYUvX30wxQi3PogGFAlUyH0awepWvwkj4jD4v+ova3RiYw8bmA6x2rDrEaj8i/oWKoRxpVNW+Re8Q== dependencies: - argparse "^1.0.7" - esprima "^4.0.0" + argparse "^2.0.1" -js-yaml@3.14.0, js-yaml@^3.13.1: +js-yaml@^3.13.1: version "3.14.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482" integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A== @@ -2758,36 +2733,36 @@ jsbn@~0.1.0: resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= -jsdom@^16.4.0: - version "16.4.0" - resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-16.4.0.tgz#36005bde2d136f73eee1a830c6d45e55408edddb" - integrity sha512-lYMm3wYdgPhrl7pDcRmvzPhhrGVBeVhPIqeHjzeiHN3DFmD1RBpbExbi8vU7BJdH8VAZYovR8DMt0PNNDM7k8w== +jsdom@^16.5.3: + version "16.5.3" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-16.5.3.tgz#13a755b3950eb938b4482c407238ddf16f0d2136" + integrity sha512-Qj1H+PEvUsOtdPJ056ewXM4UJPCi4hhLA8wpiz9F2YvsRBhuFsXxtrIFAgGBDynQA9isAMGE91PfUYbdMPXuTA== dependencies: - abab "^2.0.3" - acorn "^7.1.1" + abab "^2.0.5" + acorn "^8.1.0" acorn-globals "^6.0.0" cssom "^0.4.4" - cssstyle "^2.2.0" + cssstyle "^2.3.0" data-urls "^2.0.0" - decimal.js "^10.2.0" + decimal.js "^10.2.1" domexception "^2.0.1" - escodegen "^1.14.1" + escodegen "^2.0.0" html-encoding-sniffer "^2.0.1" is-potential-custom-element-name "^1.0.0" nwsapi "^2.2.0" - parse5 "5.1.1" + parse5 "6.0.1" request "^2.88.2" - request-promise-native "^1.0.8" - saxes "^5.0.0" + request-promise-native "^1.0.9" + saxes "^5.0.1" symbol-tree "^3.2.4" - tough-cookie "^3.0.1" + tough-cookie "^4.0.0" w3c-hr-time "^1.0.2" w3c-xmlserializer "^2.0.0" webidl-conversions "^6.1.0" whatwg-encoding "^1.0.5" whatwg-mimetype "^2.3.0" - whatwg-url "^8.0.0" - ws "^7.2.3" + whatwg-url "^8.5.0" + ws "^7.4.4" xml-name-validator "^3.0.0" jsesc@^2.5.1: @@ -2805,6 +2780,11 @@ json-schema-traverse@^0.4.1: resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + json-schema@0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" @@ -2820,13 +2800,6 @@ json-stringify-safe@~5.0.1: resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= -json5@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" - integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== - dependencies: - minimist "^1.2.0" - json5@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.2.tgz#43ef1f0af9835dd624751a6b7fa48874fb2d608e" @@ -2834,6 +2807,15 @@ json5@^2.1.2: dependencies: minimist "^1.2.5" +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + jsprim@^1.2.2: version "1.4.1" resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" @@ -2844,17 +2826,15 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" -kuler@1.0.x: - version "1.0.1" - resolved "https://registry.yarnpkg.com/kuler/-/kuler-1.0.1.tgz#ef7c784f36c9fb6e16dd3150d152677b2b0228a6" - integrity sha512-J9nVUucG1p/skKul6DU3PUZrhs0LPulNaeUOox0IyXDi8S4CztTHs1gQphhuZmzXG7VOQSf6NJfKuzteQLv9gQ== - dependencies: - colornames "^1.1.1" +kind-of@^6.0.2: + version "6.0.3" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== -leven@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" - integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== +kuler@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3" + integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A== levn@^0.4.1: version "0.4.1" @@ -2872,36 +2852,10 @@ levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" -loader-runner@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.1.0.tgz#f70bc0c29edbabdf2043e7ee73ccc3fe1c96b42d" - integrity sha512-oR4lB4WvwFoC70ocraKhn5nkKSs23t57h9udUgw8o0iH8hMXeEoRuUgfcvgUwAJ1ZpRqBvcou4N2SMvM1DwMrA== - -loader-utils@^1.0.2: - version "1.4.0" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613" - integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA== - dependencies: - big.js "^5.2.2" - emojis-list "^3.0.0" - json5 "^1.0.1" - -loader-utils@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.0.tgz#e4cace5b816d425a166b5f097e10cd12b36064b0" - integrity sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ== - dependencies: - big.js "^5.2.2" - emojis-list "^3.0.0" - json5 "^2.1.2" - -locate-path@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" - integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== - dependencies: - p-locate "^3.0.0" - path-exists "^3.0.0" +loader-runner@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.2.0.tgz#d7022380d66d14c5fb1d496b89864ebcfd478384" + integrity sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw== locate-path@^5.0.0: version "5.0.0" @@ -2917,27 +2871,25 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" +lodash.clonedeep@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" + integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= + lodash.flattendeep@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2" integrity sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI= -lodash.sortby@^4.7.0: - version "4.7.0" - resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" - integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg= +lodash.truncate@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" + integrity sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM= -lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19: - version "4.17.20" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" - integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== - -log-symbols@2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a" - integrity sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg== - dependencies: - chalk "^2.0.1" +lodash@^4.17.13, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.21, lodash@^4.7.0: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== log-symbols@4.0.0: version "4.0.0" @@ -2946,17 +2898,24 @@ log-symbols@4.0.0: dependencies: chalk "^4.0.0" -logform@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/logform/-/logform-2.1.2.tgz#957155ebeb67a13164069825ce67ddb5bb2dd360" - integrity sha512-+lZh4OpERDBLqjiwDLpAWNQu6KMjnlXH2ByZwCuSqVPJletw0kTWJf5CgSNAUKn1KUkv3m2cUz/LK8zyEy7wzQ== +logform@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/logform/-/logform-2.2.0.tgz#40f036d19161fc76b68ab50fdc7fe495544492f2" + integrity sha512-N0qPlqfypFx7UHNn4B3lzS/b0uLqt2hmuoa+PpuXNYgozdJYAyauF5Ky0BWVjrxDlMWiT3qN4zPq3vVAfZy7Yg== dependencies: colors "^1.2.1" fast-safe-stringify "^2.0.4" - fecha "^2.3.3" + fecha "^4.2.0" ms "^2.1.1" triple-beam "^1.3.0" +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + make-dir@^3.0.0, make-dir@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.0.2.tgz#04a1acbf22221e1d6ef43559f43e05a90dbb4392" @@ -2964,33 +2923,25 @@ make-dir@^3.0.0, make-dir@^3.0.2: dependencies: semver "^6.0.0" -mathjs@^5.10.3: - version "5.10.3" - resolved "https://registry.yarnpkg.com/mathjs/-/mathjs-5.10.3.tgz#e998885f932ea8886db8b40f7f5b199f89b427f1" - integrity sha512-ySjg30BC3dYjQm73ILZtwcWzFJde0VU6otkXW/57IjjuYRa3Qaf0Kb8pydEuBZYtqW2OxreAtsricrAmOj3jIw== +mathjs@^9.3.0: + version "9.3.2" + resolved "https://registry.yarnpkg.com/mathjs/-/mathjs-9.3.2.tgz#6523dd5c963d200ff1cea0ff7963b10521b82185" + integrity sha512-0YKSKAeN9OkbIQrxfxnBT4kk/KlH71piWOsvVvAasyRIj/Xd/zlpc5VP/aFxwr+llOq2F3f6booPEu2fWv3yjQ== dependencies: - complex.js "2.0.11" - decimal.js "10.2.0" - escape-latex "1.2.0" - fraction.js "4.0.12" - javascript-natural-sort "0.7.1" - seed-random "2.2.0" - tiny-emitter "2.1.0" - typed-function "1.1.0" + complex.js "^2.0.11" + decimal.js "^10.2.1" + escape-latex "^1.2.0" + fraction.js "^4.0.13" + javascript-natural-sort "^0.7.1" + seedrandom "^3.0.5" + tiny-emitter "^2.1.0" + typed-function "^2.0.0" media-typer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= -memory-fs@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.5.0.tgz#324c01288b88652966d161db77838720845a8e3c" - integrity sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA== - dependencies: - errno "^0.1.3" - readable-stream "^2.0.1" - merge-descriptors@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" @@ -3012,19 +2963,31 @@ methods@~1.1.2: integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= micromatch@^4.0.0, micromatch@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" - integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q== + version "4.0.4" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9" + integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg== dependencies: braces "^3.0.1" - picomatch "^2.0.5" + picomatch "^2.2.3" mime-db@1.44.0: version "1.44.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== -mime-types@^2.1.12, mime-types@^2.1.27, mime-types@~2.1.19, mime-types@~2.1.24: +mime-db@1.47.0: + version "1.47.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.47.0.tgz#8cb313e59965d3c05cfbf898915a267af46a335c" + integrity sha512-QBmA/G2y+IfeS4oktet3qRZ+P5kPhCKRXxXnQEudYqUaEioAU1/Lq2us3D/t1Jfo4hE9REQPrbB7K5sOczJVIw== + +mime-types@^2.1.12, mime-types@^2.1.27, mime-types@~2.1.19: + version "2.1.30" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.30.tgz#6e7be8b4c479825f85ed6326695db73f9305d62d" + integrity sha512-crmjA4bLtR8m9qLpHvgxSChT+XoSlZi8J4n/aIdn3z92e/U47Z0V/yl+Wh9W046GgFVAmoNR/fmdbZYcSSIUeg== + dependencies: + mime-db "1.47.0" + +mime-types@~2.1.24: version "2.1.27" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== @@ -3036,10 +2999,10 @@ mime@1.6.0: resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== -mime@^2.0.3, mime@^2.4.6: - version "2.4.6" - resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.6.tgz#e5b407c90db442f2beb5b162373d07b69affa4d1" - integrity sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA== +mime@^2.4.6: + version "2.5.2" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.5.2.tgz#6e3dc6cc2b9510643830e5f19d5cb753da5eeabe" + integrity sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg== mimic-fn@^2.1.0: version "2.1.0" @@ -3053,83 +3016,52 @@ minimatch@3.0.4, minimatch@^3.0.4: dependencies: brace-expansion "^1.1.7" -minimist@^1.2.0, minimist@^1.2.5: +minimist@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== -mkdirp@0.5.4: - version "0.5.4" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.4.tgz#fd01504a6797ec5c9be81ff43d204961ed64a512" - integrity sha512-iG9AK/dJLtJ0XNgTuDbSyNS3zECqDlAhnQW4CsNxBG3LQJBbHmRX1egw39DmtOdCAqY+dKXV+sgPgilNWUKMVw== - dependencies: - minimist "^1.2.5" +mkdirp-classic@^0.5.2: + version "0.5.3" + resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" + integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== -mkdirp@^0.5.1, mkdirp@^0.5.4: +mkdirp@^0.5.3: version "0.5.5" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== dependencies: minimist "^1.2.5" -mocha@^6.1.4: - version "6.2.3" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-6.2.3.tgz#e648432181d8b99393410212664450a4c1e31912" - integrity sha512-0R/3FvjIGH3eEuG17ccFPk117XL2rWxatr81a57D+r/x2uTYZRbdZ4oVidEUMh2W2TJDa7MdAb12Lm2/qrKajg== - dependencies: - ansi-colors "3.2.3" - browser-stdout "1.3.1" - debug "3.2.6" - diff "3.5.0" - escape-string-regexp "1.0.5" - find-up "3.0.0" - glob "7.1.3" - growl "1.10.5" - he "1.2.0" - js-yaml "3.13.1" - log-symbols "2.2.0" - minimatch "3.0.4" - mkdirp "0.5.4" - ms "2.1.1" - node-environment-flags "1.0.5" - object.assign "4.1.0" - strip-json-comments "2.0.1" - supports-color "6.0.0" - which "1.3.1" - wide-align "1.1.3" - yargs "13.3.2" - yargs-parser "13.1.2" - yargs-unparser "1.6.0" - -mocha@^8.2.1: - version "8.2.1" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-8.2.1.tgz#f2fa68817ed0e53343d989df65ccd358bc3a4b39" - integrity sha512-cuLBVfyFfFqbNR0uUKbDGXKGk+UDFe6aR4os78XIrMQpZl/nv7JYHcvP5MFIAb374b2zFXsdgEGwmzMtP0Xg8w== +mocha@^8.3.2, mocha@^8.4.0: + version "8.4.0" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-8.4.0.tgz#677be88bf15980a3cae03a73e10a0fc3997f0cff" + integrity sha512-hJaO0mwDXmZS4ghXsvPVriOhsxQ7ofcpQdm8dE+jISUOKopitvnXFQmpRR7jd2K6VBG6E26gU3IAbXXGIbu4sQ== dependencies: "@ungap/promise-all-settled" "1.1.2" ansi-colors "4.1.1" browser-stdout "1.3.1" - chokidar "3.4.3" - debug "4.2.0" - diff "4.0.2" + chokidar "3.5.1" + debug "4.3.1" + diff "5.0.0" escape-string-regexp "4.0.0" find-up "5.0.0" glob "7.1.6" growl "1.10.5" he "1.2.0" - js-yaml "3.14.0" + js-yaml "4.0.0" log-symbols "4.0.0" minimatch "3.0.4" - ms "2.1.2" - nanoid "3.1.12" + ms "2.1.3" + nanoid "3.1.20" serialize-javascript "5.0.1" strip-json-comments "3.1.1" - supports-color "7.2.0" + supports-color "8.1.1" which "2.0.2" wide-align "1.1.3" - workerpool "6.0.2" - yargs "13.3.2" - yargs-parser "13.1.2" + workerpool "6.1.0" + yargs "16.2.0" + yargs-parser "20.2.4" yargs-unparser "2.0.0" ms@2.0.0: @@ -3142,25 +3074,30 @@ ms@2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== -ms@2.1.2, ms@^2.1.1: +ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -mustache@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/mustache/-/mustache-4.0.1.tgz#d99beb031701ad433338e7ea65e0489416c854a2" - integrity sha512-yL5VE97+OXn4+Er3THSmTdCFCtx5hHWzrolvH+JObZnUYwuaG7XV+Ch4fR2cIrcYI0tFHxS7iyFYl14bW8y2sA== +ms@2.1.3, ms@^2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +mustache@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/mustache/-/mustache-4.2.0.tgz#e5892324d60a12ec9c2a73359edca52972bf6f64" + integrity sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ== nan@^2.14.0: - version "2.14.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" - integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== + version "2.14.2" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19" + integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ== -nanoid@3.1.12: - version "3.1.12" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.12.tgz#6f7736c62e8d39421601e4a0c77623a97ea69654" - integrity sha512-1qstj9z5+x491jfiC4Nelk+f8XBad7LN20PmyWINJEMRSf3wcAjAWysw1qaA8z6NSKe2sjq1hRSDpBH5paCb6A== +nanoid@3.1.20: + version "3.1.20" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.20.tgz#badc263c6b1dcf14b71efaa85f6ab4c1d6cfc788" + integrity sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw== natural-compare@^1.4.0: version "1.4.0" @@ -3177,13 +3114,10 @@ neo-async@^2.6.2: resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== -node-environment-flags@1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/node-environment-flags/-/node-environment-flags-1.0.5.tgz#fa930275f5bf5dae188d6192b24b4c8bbac3d76a" - integrity sha512-VNYPRfGfmZLx0Ye20jWzHUjyTW/c+6Wq+iLhDzUI4XmhrDd9l/FozXV3F2xOaXjvp0co0+v1YSR3CMP6g+VvLQ== - dependencies: - object.getownpropertydescriptors "^2.0.3" - semver "^5.7.0" +node-fetch@^2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" + integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== node-preload@^0.2.1: version "0.2.1" @@ -3192,24 +3126,24 @@ node-preload@^0.2.1: dependencies: process-on-spawn "^1.0.0" -node-pty@^0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-0.9.0.tgz#8f9bcc0d1c5b970a3184ffd533d862c7eb6590a6" - integrity sha512-MBnCQl83FTYOu7B4xWw10AW77AAh7ThCE1VXEv+JeWj8mSpGo+0bwgsV+b23ljBFwEM9OmsOv3kM27iUPPm84g== +node-pty@^0.10.1: + version "0.10.1" + resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-0.10.1.tgz#cd05d03a2710315ec40221232ec04186f6ac2c6d" + integrity sha512-JTdtUS0Im/yRsWJSx7yiW9rtpfmxqxolrtnyKwPLI+6XqTAPW/O2MjS8FYL4I5TsMbH2lVgDb2VMjp+9LoQGNg== dependencies: nan "^2.14.0" -node-releases@^1.1.61: - version "1.1.65" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.65.tgz#52d9579176bd60f23eba05c4438583f341944b81" - integrity sha512-YpzJOe2WFIW0V4ZkJQd/DGR/zdVwc/pI4Nl1CZrBO19FdRcSTmsuhdttw9rsTzzJLrNcSloLiBbEYx1C4f6gpA== +node-releases@^1.1.71: + version "1.1.71" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.71.tgz#cb1334b179896b1c89ecfdd4b725fb7bbdfc7dbb" + integrity sha512-zR6HoT6LrLCRBwukmrVbHv0EpEQjksO6GmFcZQQuCAy139BEsoVKPYnf3jongYW83fAa1torLGYwxxky/p28sg== normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== -npm-run-path@^4.0.0: +npm-run-path@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== @@ -3259,57 +3193,34 @@ oauth-sign@~0.9.0: resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== -object-inspect@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67" - integrity sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw== +object-inspect@^1.9.0: + version "1.10.3" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.10.3.tgz#c2aa7d2d09f50c99375704f7a0adf24c5782d369" + integrity sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw== -object-inspect@^1.8.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0" - integrity sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA== - -object-is@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.3.tgz#2e3b9e65560137455ee3bd62aec4d90a2ea1cc81" - integrity sha512-teyqLvFWzLkq5B9ki8FVWA902UER2qkxmdA4nLf+wjOLAWgxzCWZNCxpDq9MvE8MmhWNr+I8w3BN49Vx36Y6Xg== +object-is@^1.1.4: + version "1.1.5" + resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac" + integrity sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw== dependencies: + call-bind "^1.0.2" define-properties "^1.1.3" - es-abstract "^1.18.0-next.1" -object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1: +object-keys@^1.0.12, object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== -object.assign@4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da" - integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w== - dependencies: - define-properties "^1.1.2" - function-bind "^1.1.1" - has-symbols "^1.0.0" - object-keys "^1.0.11" - -object.assign@^4.1.0, object.assign@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.1.tgz#303867a666cdd41936ecdedfb1f8f3e32a478cdd" - integrity sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA== +object.assign@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940" + integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ== dependencies: + call-bind "^1.0.0" define-properties "^1.1.3" - es-abstract "^1.18.0-next.0" has-symbols "^1.0.1" object-keys "^1.1.1" -object.getownpropertydescriptors@^2.0.3: - version "2.1.0" - resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz#369bf1f9592d8ab89d712dced5cb81c7c5352649" - integrity sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.0-next.1" - on-finished@~2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" @@ -3324,12 +3235,14 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0: dependencies: wrappy "1" -one-time@0.0.4: - version "0.0.4" - resolved "https://registry.yarnpkg.com/one-time/-/one-time-0.0.4.tgz#f8cdf77884826fe4dff93e3a9cc37b1e4480742e" - integrity sha1-+M33eISCb+Tf+T46nMN7HkSAdC4= +one-time@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/one-time/-/one-time-1.0.0.tgz#e06bc174aed214ed58edede573b433bbf827cb45" + integrity sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g== + dependencies: + fn.name "1.x.x" -onetime@^5.1.0: +onetime@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== @@ -3360,7 +3273,7 @@ optionator@^0.9.1: type-check "^0.4.0" word-wrap "^1.2.3" -p-limit@^2.0.0, p-limit@^2.2.0: +p-limit@^2.2.0: version "2.2.2" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.2.tgz#61279b67721f5287aa1c13a9a7fbbc48c9291b1e" integrity sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ== @@ -3374,12 +3287,12 @@ p-limit@^3.0.2: dependencies: p-try "^2.0.0" -p-locate@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" - integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== +p-limit@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== dependencies: - p-limit "^2.0.0" + yocto-queue "^0.1.0" p-locate@^4.1.0: version "4.1.0" @@ -3424,21 +3337,16 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" -parse5@5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178" - integrity sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug== +parse5@6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" + integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== parseurl@~1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== -path-exists@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" - integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= - path-exists@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" @@ -3455,9 +3363,9 @@ path-key@^3.0.0, path-key@^3.1.0: integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== path-parse@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" - integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== path-to-regexp@0.1.7: version "0.1.7" @@ -3469,10 +3377,10 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== -pathval@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.0.tgz#b942e6d4bde653005ef6b71361def8727d0645e0" - integrity sha1-uULm1L3mUwBe9rcTYd74cn0GReA= +pathval@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.1.tgz#8534e77a77ce7ac5a2512ea21e0fdb8fcf6c3d8d" + integrity sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ== pend@~1.2.0: version "1.2.0" @@ -3484,11 +3392,16 @@ performance-now@^2.1.0: resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= -picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.2.1: +picomatch@^2.0.4, picomatch@^2.2.1: version "2.2.2" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== +picomatch@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.3.tgz#465547f359ccc206d3c48e46a1bcb89bf7ee619d" + integrity sha512-KpELjfwcCDUb9PeigTs2mBJzXUPzAuP2oPcA989He8Rte0+YUAjw1JVedDhuTKPkHjSYzMN3npC9luThGYEKdg== + pkg-dir@^4.1.0, pkg-dir@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" @@ -3496,11 +3409,12 @@ pkg-dir@^4.1.0, pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" -playwright@^1.5.2: - version "1.5.2" - resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.5.2.tgz#e127142cba86c918fad9f68315db5e79524af64c" - integrity sha512-on7IEui47bDZta0txL86QKMDSgjbxERkLc5N0+lU2zajIfN/Ld6vMl+xiROEUPlT/QtqVekq9pTDGdcc0yScMQ== +playwright@^1.11.0: + version "1.11.0" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.11.0.tgz#0796cf08f4756e8187e01c705315d8e1fb48e25f" + integrity sha512-s3FQBRpu/pW/vZ/lFYhG/Q3WBUbT2rvMgrgy1PHDA7QtPN910C2rj9Ovd6A/m8yxuLnltd/OKqvlAGevWISHKw== dependencies: + commander "^6.1.0" debug "^4.1.1" extract-zip "^2.0.1" https-proxy-agent "^5.0.0" @@ -3511,7 +3425,9 @@ playwright@^1.5.2: proper-lockfile "^4.1.1" proxy-from-env "^1.1.0" rimraf "^3.0.2" + stack-utils "^2.0.3" ws "^7.3.1" + yazl "^2.5.1" pngjs@^5.0.0: version "5.0.0" @@ -3546,11 +3462,11 @@ progress@^2.0.0, progress@^2.0.1, progress@^2.0.3: integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== proper-lockfile@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/proper-lockfile/-/proper-lockfile-4.1.1.tgz#284cf9db9e30a90e647afad69deb7cb06881262c" - integrity sha512-1w6rxXodisVpn7QYvLk706mzprPTAPCYAqxMvctmPN3ekuRk/kuGkGc82pangZiAt4R3lwSuUzheTTn0/Yb7Zg== + version "4.1.2" + resolved "https://registry.yarnpkg.com/proper-lockfile/-/proper-lockfile-4.1.2.tgz#c8b9de2af6b2f1601067f98e01ac66baa223141f" + integrity sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA== dependencies: - graceful-fs "^4.1.11" + graceful-fs "^4.2.4" retry "^0.12.0" signal-exit "^3.0.2" @@ -3567,12 +3483,7 @@ proxy-from-env@^1.0.0, proxy-from-env@^1.1.0: resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== -prr@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" - integrity sha1-0/wRS6BplaRexok/SEzrHXj19HY= - -psl@^1.1.28: +psl@^1.1.28, psl@^1.1.33: version "1.8.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ== @@ -3590,19 +3501,23 @@ punycode@^2.1.0, punycode@^2.1.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== -puppeteer@^1.17.0: - version "1.20.0" - resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-1.20.0.tgz#e3d267786f74e1d87cf2d15acc59177f471bbe38" - integrity sha512-bt48RDBy2eIwZPrkgbcwHtb51mj2nKvHOPMaSH2IsWiv7lOG9k9zhaRzpDZafrk05ajMc3cu+lSQYYOfH2DkVQ== +puppeteer@^5.5.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-5.5.0.tgz#331a7edd212ca06b4a556156435f58cbae08af00" + integrity sha512-OM8ZvTXAhfgFA7wBIIGlPQzvyEETzDjeRa4mZRCRHxYL+GNH5WAuYUQdja3rpWZvkX/JKqmuVgbsxDNsDFjMEg== dependencies: debug "^4.1.0" - extract-zip "^1.6.6" - https-proxy-agent "^2.2.1" - mime "^2.0.3" + devtools-protocol "0.0.818844" + extract-zip "^2.0.0" + https-proxy-agent "^4.0.0" + node-fetch "^2.6.1" + pkg-dir "^4.2.0" progress "^2.0.1" proxy-from-env "^1.0.0" - rimraf "^2.6.1" - ws "^6.1.0" + rimraf "^3.0.2" + tar-fs "^2.0.0" + unbzip2-stream "^1.3.3" + ws "^7.2.3" qs@6.7.0: version "6.7.0" @@ -3614,6 +3529,11 @@ qs@~6.5.2: resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" @@ -3636,7 +3556,7 @@ raw-body@2.4.0: iconv-lite "0.4.24" unpipe "1.0.0" -readable-stream@^2.0.1, readable-stream@^2.2.2, readable-stream@^2.3.6: +readable-stream@^2.3.7: version "2.3.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== @@ -3649,7 +3569,7 @@ readable-stream@^2.0.1, readable-stream@^2.2.2, readable-stream@^2.3.6: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.1.1: +readable-stream@^3.1.1, readable-stream@^3.4.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== @@ -3672,18 +3592,13 @@ rechoir@^0.7.0: dependencies: resolve "^1.9.0" -reduce-flatten@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/reduce-flatten/-/reduce-flatten-2.0.0.tgz#734fd84e65f375d7ca4465c69798c25c9d10ae27" - integrity sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w== - regexp.prototype.flags@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz#7aba89b3c13a64509dabcf3ca8d9fbb9bdf5cb75" - integrity sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ== + version "1.3.1" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.1.tgz#7ef352ae8d159e758c0eadca6f8fcb4eef07be26" + integrity sha512-JiBdRBq91WlY7uRJ0ds7R+dU02i6LKi8r3BuQhNXn+kmeLN+EfHhfjqMRis1zJxnlu88hq/4dx0P2OP3APRTOA== dependencies: + call-bind "^1.0.2" define-properties "^1.1.3" - es-abstract "^1.17.0-next.1" regexpp@^3.0.0, regexpp@^3.1.0: version "3.1.0" @@ -3697,19 +3612,19 @@ release-zalgo@^1.0.0: dependencies: es6-error "^4.0.1" -request-promise-core@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.3.tgz#e9a3c081b51380dfea677336061fea879a829ee9" - integrity sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ== +request-promise-core@1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.4.tgz#3eedd4223208d419867b78ce815167d10593a22f" + integrity sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw== dependencies: - lodash "^4.17.15" + lodash "^4.17.19" -request-promise-native@^1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.8.tgz#a455b960b826e44e2bf8999af64dff2bfe58cb36" - integrity sha512-dapwLGqkHtwL5AEbfenuzjTYg35Jd6KPytsC2/TLkVMz8rm+tNt72MGUWT1RP/aYawMpN6HqbNGBQaRcBtjQMQ== +request-promise-native@^1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.9.tgz#e407120526a5efdc9a39b28a5679bf47b9d9dc28" + integrity sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g== dependencies: - request-promise-core "1.1.3" + request-promise-core "1.1.4" stealthy-require "^1.1.1" tough-cookie "^2.3.3" @@ -3744,6 +3659,11 @@ require-directory@^2.1.1: resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + require-main-filename@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" @@ -3779,10 +3699,11 @@ resolve@^1.3.2: path-parse "^1.0.6" resolve@^1.9.0: - version "1.17.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444" - integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w== + version "1.20.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" + integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== dependencies: + is-core-module "^2.2.0" path-parse "^1.0.6" retry@^0.12.0: @@ -3795,20 +3716,6 @@ reusify@^1.0.4: resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== -rimraf@2.6.3: - version "2.6.3" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" - integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== - dependencies: - glob "^7.1.3" - -rimraf@^2.6.1: - version "2.7.1" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" - integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== - dependencies: - glob "^7.1.3" - rimraf@^3.0.0, rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" @@ -3817,16 +3724,23 @@ rimraf@^3.0.0, rimraf@^3.0.2: glob "^7.1.3" run-parallel@^1.1.9: - version "1.1.9" - resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.9.tgz#c9dd3a7cf9f4b2c4b6244e173a6ed866e61dd679" - integrity sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q== + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.2, safe-buffer@~5.2.0: +safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-buffer@^5.1.0: version "5.2.0" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== @@ -3836,10 +3750,10 @@ safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.2, safe-buffer@~5.2.0: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -saxes@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/saxes/-/saxes-5.0.0.tgz#b7d30284d7583a5ca6ad0248b56d8889da53788b" - integrity sha512-LXTZygxhf8lfwKaTP/8N9CsVdjTlea3teze4lL6u37ivbgGbV0GGMuNtS/I9rnD/HC2/txUM7Df4S2LVl1qhiA== +saxes@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/saxes/-/saxes-5.0.1.tgz#eebab953fa3b7608dbe94e5dadb15c888fa6696d" + integrity sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw== dependencies: xmlchars "^2.2.0" @@ -3852,12 +3766,12 @@ schema-utils@^3.0.0: ajv "^6.12.5" ajv-keywords "^3.5.2" -seed-random@2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/seed-random/-/seed-random-2.2.0.tgz#2a9b19e250a817099231a5b99a4daf80b7fbed54" - integrity sha1-KpsZ4lCoFwmSMaW5mk2vgLf77VQ= +seedrandom@^3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/seedrandom/-/seedrandom-3.0.5.tgz#54edc85c95222525b0c7a6f6b3543d8e0b3aa0a7" + integrity sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg== -semver@^5.3.0, semver@^5.4.1, semver@^5.7.0: +semver@^5.3.0, semver@^5.4.1: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== @@ -3867,10 +3781,12 @@ semver@^6.0.0, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== -semver@^7.2.1, semver@^7.3.2: - version "7.3.2" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938" - integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ== +semver@^7.2.1, semver@^7.3.2, semver@^7.3.4: + version "7.3.5" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" + integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== + dependencies: + lru-cache "^6.0.0" send@0.17.1: version "0.17.1" @@ -3918,6 +3834,13 @@ setprototypeof@1.1.1: resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== +shallow-clone@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" + integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== + dependencies: + kind-of "^6.0.2" + shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" @@ -3931,24 +3854,27 @@ shebang-regex@^3.0.0: integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== side-channel@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.3.tgz#cdc46b057550bbab63706210838df5d4c19519c3" - integrity sha512-A6+ByhlLkksFoUepsGxfj5x1gTSrs+OydsRptUxeNCabQpCFUvcwIczgOigI8vhY/OJCnPnyE9rGiwgvr9cS1g== + version "1.0.4" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" + integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== dependencies: - es-abstract "^1.18.0-next.0" - object-inspect "^1.8.0" + call-bind "^1.0.0" + get-intrinsic "^1.0.2" + object-inspect "^1.9.0" -signal-exit@^3.0.2: +signal-exit@^3.0.2, signal-exit@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== -simple-git@^1.113.0: - version "1.132.0" - resolved "https://registry.yarnpkg.com/simple-git/-/simple-git-1.132.0.tgz#53ac4c5ec9e74e37c2fd461e23309f22fcdf09b1" - integrity sha512-xauHm1YqCTom1sC9eOjfq3/9RKiUA9iPnxBbrY2DdL8l4ADMu0jjM5l5lphQP5YWNqAL2aXC/OeuQ76vHtW5fg== +simple-git@^2.37.0: + version "2.38.1" + resolved "https://registry.yarnpkg.com/simple-git/-/simple-git-2.38.1.tgz#6c3ce211777a65482598f4bd93c66443465228c0" + integrity sha512-SeMgUEA6Cmk7Ta57ZzMbkBcAfh8A+DZ6ACZ2onfCp/9UklC9yMJgvcH+GGI3QDTv0lTDIbWtl5LSbBk1UtEfeg== dependencies: - debug "^4.0.1" + "@kwsites/file-exists" "^1.1.1" + "@kwsites/promise-deferred" "^1.1.1" + debug "^4.3.1" simple-swizzle@^0.2.2: version "0.2.2" @@ -3962,33 +3888,35 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== -slice-ansi@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636" - integrity sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ== +slice-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" + integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== dependencies: - ansi-styles "^3.2.0" - astral-regex "^1.0.0" - is-fullwidth-code-point "^2.0.0" + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" source-list-map@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== -source-map-loader@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-1.1.2.tgz#5b782bf08496d3a7f355e1780df0e25190a80991" - integrity sha512-bjf6eSENOYBX4JZDfl9vVLNsGAQ6Uz90fLmOazcmMcyDYOBFsGxPNn83jXezWLY9bJsVAo1ObztxPcV8HAbjVA== +source-map-js@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-0.6.2.tgz#0bb5de631b41cfbda6cfba8bd05a80efdfd2385e" + integrity sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug== + +source-map-loader@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-2.0.1.tgz#b4fd0ae7fa7e7d3954300f383f2d6fcc230a4261" + integrity sha512-UzOTTQhoNPeTNzOxwFw220RSRzdGSyH4lpNyWjR7Qm34P4/N0W669YSUFdH07+YNeN75h765XLHmNsF/bm97RQ== dependencies: abab "^2.0.5" iconv-lite "^0.6.2" - loader-utils "^2.0.0" - schema-utils "^3.0.0" - source-map "^0.6.1" - whatwg-mimetype "^2.3.0" + source-map-js "^0.6.2" -source-map-support@~0.5.19: +source-map-support@^0.5.19, source-map-support@~0.5.19: version "0.5.19" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== @@ -4048,6 +3976,13 @@ stack-trace@0.0.x: resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" integrity sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA= +stack-utils@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.3.tgz#cd5f030126ff116b78ccb3c027fe302713b61277" + integrity sha512-gL//fkxfWUsIlFL2Tl42Cl6+HFALEaB1FU76I/Fy+oZjRreP7OPMXFlGbxM7NQsI0ZpUfw76sHnv0WNYuTb7Iw== + dependencies: + escape-string-regexp "^2.0.0" + "statuses@>= 1.5.0 < 2", statuses@~1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" @@ -4066,15 +4001,6 @@ stealthy-require@^1.1.1: is-fullwidth-code-point "^2.0.0" strip-ansi "^4.0.0" -string-width@^3.0.0, string-width@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" - integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== - dependencies: - emoji-regex "^7.0.1" - is-fullwidth-code-point "^2.0.0" - strip-ansi "^5.1.0" - string-width@^4.1.0, string-width@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" @@ -4084,37 +4010,21 @@ string-width@^4.1.0, string-width@^4.2.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.0" -string.prototype.trimend@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz#85812a6b847ac002270f5808146064c995fb6913" - integrity sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g== +string.prototype.trimend@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80" + integrity sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A== dependencies: + call-bind "^1.0.2" define-properties "^1.1.3" - es-abstract "^1.17.5" -string.prototype.trimleft@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz#9bdb8ac6abd6d602b17a4ed321870d2f8dcefc74" - integrity sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag== +string.prototype.trimstart@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz#b36399af4ab2999b4c9c648bd7a3fb2bb26feeed" + integrity sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw== dependencies: + call-bind "^1.0.2" define-properties "^1.1.3" - function-bind "^1.1.1" - -string.prototype.trimright@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz#440314b15996c866ce8a0341894d45186200c5d9" - integrity sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g== - dependencies: - define-properties "^1.1.3" - function-bind "^1.1.1" - -string.prototype.trimstart@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz#14af6d9f34b053f7cfc89b72f8f2ee14b9039a54" - integrity sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.5" string_decoder@^1.1.1: version "1.3.0" @@ -4144,13 +4054,6 @@ strip-ansi@^4.0.0: dependencies: ansi-regex "^3.0.0" -strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" - integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== - dependencies: - ansi-regex "^4.1.0" - strip-ansi@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" @@ -4168,27 +4071,15 @@ strip-final-newline@^2.0.0: resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== -strip-json-comments@2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" - integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= - strip-json-comments@3.1.1, strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== -supports-color@6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.0.0.tgz#76cfe742cf1f41bb9b1c29ad03068c05b4c0e40a" - integrity sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg== - dependencies: - has-flag "^3.0.0" - -supports-color@7.2.0, supports-color@^7.0.0, supports-color@^7.1.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" - integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== +supports-color@8.1.1: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== dependencies: has-flag "^4.0.0" @@ -4199,57 +4090,72 @@ supports-color@^5.3.0: dependencies: has-flag "^3.0.0" +supports-color@^7.0.0, supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + symbol-tree@^3.2.4: version "3.2.4" resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== -table-layout@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/table-layout/-/table-layout-1.0.1.tgz#8411181ee951278ad0638aea2f779a9ce42894f9" - integrity sha512-dEquqYNJiGwY7iPfZ3wbXDI944iqanTSchrACLL2nOB+1r+h1Nzu2eH+DuPPvWvm5Ry7iAPeFlgEtP5bIp5U7Q== +table@^6.0.4: + version "6.7.0" + resolved "https://registry.yarnpkg.com/table/-/table-6.7.0.tgz#26274751f0ee099c547f6cb91d3eff0d61d155b2" + integrity sha512-SAM+5p6V99gYiiy2gT5ArdzgM1dLDed0nkrWmG6Fry/bUS/m9x83BwpJUOf1Qj/x2qJd+thL6IkIx7qPGRxqBw== dependencies: - array-back "^4.0.1" - deep-extend "~0.6.0" - typical "^5.2.0" - wordwrapjs "^4.0.0" + ajv "^8.0.1" + lodash.clonedeep "^4.5.0" + lodash.truncate "^4.4.2" + slice-ansi "^4.0.0" + string-width "^4.2.0" + strip-ansi "^6.0.0" -table@^5.2.3: - version "5.4.6" - resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e" - integrity sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug== +tapable@^2.1.1, tapable@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.0.tgz#5c373d281d9c672848213d0e037d1c4165ab426b" + integrity sha512-FBk4IesMV1rBxX2tfiK8RAmogtWn53puLOQlvO8XuwlgxcYbP4mVPS9Ph4aeamSyyVjOl24aYWAuc8U5kCVwMw== + +tar-fs@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784" + integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng== dependencies: - ajv "^6.10.2" - lodash "^4.17.14" - slice-ansi "^2.1.0" - string-width "^3.0.0" + chownr "^1.1.1" + mkdirp-classic "^0.5.2" + pump "^3.0.0" + tar-stream "^2.1.4" -tapable@^1.0.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" - integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== - -tapable@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.0.0.tgz#a49c3d6a8a2bb606e7db372b82904c970d537a08" - integrity sha512-bjzn0C0RWoffnNdTzNi7rNDhs1Zlwk2tRXgk8EiHKAOX1Mag3d6T0Y5zNa7l9CJ+EoUne/0UHdwS8tMbkh9zDg== - -terser-webpack-plugin@^5.0.3: - version "5.0.3" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.0.3.tgz#ec60542db2421f45735c719d2e17dabfbb2e3e42" - integrity sha512-zFdGk8Lh9ZJGPxxPE6jwysOlATWB8GMW8HcfGULWA/nPal+3VdATflQvSBSLQJRCmYZnfFJl6vkRTiwJGNgPiQ== +tar-stream@^2.1.4: + version "2.2.0" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287" + integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== dependencies: - jest-worker "^26.6.1" - p-limit "^3.0.2" + bl "^4.0.3" + end-of-stream "^1.4.1" + fs-constants "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.1.1" + +terser-webpack-plugin@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.1.1.tgz#7effadee06f7ecfa093dbbd3e9ab23f5f3ed8673" + integrity sha512-5XNNXZiR8YO6X6KhSGXfY0QrGrCRlSwAEjIIrlRQR4W8nP69TaJUlh3bkuac6zzgspiGPfKEHcY295MMVExl5Q== + dependencies: + jest-worker "^26.6.2" + p-limit "^3.1.0" schema-utils "^3.0.0" serialize-javascript "^5.0.1" source-map "^0.6.1" - terser "^5.3.8" + terser "^5.5.1" -terser@^5.3.8: - version "5.3.8" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.3.8.tgz#991ae8ba21a3d990579b54aa9af11586197a75dd" - integrity sha512-zVotuHoIfnYjtlurOouTazciEfL7V38QMAOhGqpXDEg6yT13cF4+fEP9b0rrCEQTn+tT46uxgFsTZzhygk+CzQ== +terser@^5.5.1: + version "5.7.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.7.0.tgz#a761eeec206bc87b605ab13029876ead938ae693" + integrity sha512-HP5/9hp2UaZt5fYkuhNBR8YyRcT8juw8+uFbAme53iN9hblvKnLUTKkmwJG6ocWpIKf8UK4DoeWG4ty0J6S6/g== dependencies: commander "^2.20.0" source-map "~0.7.2" @@ -4274,7 +4180,12 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= -tiny-emitter@2.1.0: +through@^2.3.8: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= + +tiny-emitter@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423" integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q== @@ -4304,16 +4215,16 @@ tough-cookie@^2.3.3, tough-cookie@~2.5.0: psl "^1.1.28" punycode "^2.1.1" -tough-cookie@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-3.0.1.tgz#9df4f57e739c26930a018184887f4adb7dca73b2" - integrity sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg== +tough-cookie@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.0.0.tgz#d822234eeca882f991f0f908824ad2622ddbece4" + integrity sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg== dependencies: - ip-regex "^2.1.0" - psl "^1.1.28" + psl "^1.1.33" punycode "^2.1.1" + universalify "^0.1.2" -tr46@^2.0.0: +tr46@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/tr46/-/tr46-2.0.2.tgz#03273586def1595ae08fedb38d7733cee91d2479" integrity sha512-3n1qG+/5kg+jrbTzwAykB5yRYtQCTqOGKq5U5PE3b0a1/mzo6snDhjGS0zJVJunO0NrT3Dg1MLy5TjWP/UJppg== @@ -4325,31 +4236,30 @@ triple-beam@^1.2.0, triple-beam@^1.3.0: resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9" integrity sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw== -ts-loader@^8.0.8: - version "8.0.8" - resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-8.0.8.tgz#5c514c895d3b2462bf0148f63da0c22b227f7867" - integrity sha512-wihija1i2Ub9FxKFoEgx/phToJiDZKLIbTFURgf4Efxla2QuVucDO6ZpHf2jNJsRtDQfBId0g+1HF0biIjoT6Q== +ts-loader@^9.1.2: + version "9.1.2" + resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-9.1.2.tgz#ba9b9abb05a514e8ff825791a3f6fcf793272728" + integrity sha512-ryMgATvLLl+z8zQvdlm6Pep0slmwxFWIEnq/5VdiLVjqQXnFJgO+qNLGIIP+d2N2jsFZ9MibZCVDb2bSp7OmEA== dependencies: - chalk "^2.3.0" - enhanced-resolve "^4.0.0" - loader-utils "^1.0.2" + chalk "^4.1.0" + enhanced-resolve "^5.0.0" micromatch "^4.0.0" - semver "^6.0.0" + semver "^7.3.4" -tslib@^1.8.0, tslib@^1.8.1: +tslib@^1.13.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + +tslib@^1.8.1: version "1.11.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35" integrity sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA== -tslib@^1.9.0: - version "1.13.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043" - integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q== - -tslint@^5.17.0: - version "5.20.1" - resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.20.1.tgz#e401e8aeda0152bc44dd07e614034f3f80c67b7d" - integrity sha512-EcMxhzCFt8k+/UP5r8waCf/lzmeSyVlqxqMEDQE7rWYiQky8KpIBz1JAoYXfROHrPZ1XXd43q8yQnULOLiBRQg== +tslint@^6.1.3: + version "6.1.3" + resolved "https://registry.yarnpkg.com/tslint/-/tslint-6.1.3.tgz#5c23b2eccc32487d5523bd3a470e9aa31789d904" + integrity sha512-IbR4nkT96EQOvKE2PW/djGz8iGNeJ4rF2mBfiYaR/nvUWYKJhLwimoJKgjIFEIDibBtOevj7BqCRL4oHeWWUCg== dependencies: "@babel/code-frame" "^7.0.0" builtin-modules "^1.1.1" @@ -4359,10 +4269,10 @@ tslint@^5.17.0: glob "^7.1.1" js-yaml "^3.13.1" minimatch "^3.0.4" - mkdirp "^0.5.1" + mkdirp "^0.5.3" resolve "^1.3.2" semver "^5.3.0" - tslib "^1.8.0" + tslib "^1.13.0" tsutils "^2.29.0" tsutils@^2.29.0: @@ -4373,9 +4283,9 @@ tsutils@^2.29.0: tslib "^1.8.1" tsutils@^3.17.1: - version "3.17.1" - resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759" - integrity sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g== + version "3.21.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" + integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== dependencies: tslib "^1.8.1" @@ -4410,6 +4320,11 @@ type-detect@^4.0.0, type-detect@^4.0.5: resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== +type-fest@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" + integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== + type-fest@^0.8.0, type-fest@^0.8.1: version "0.8.1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" @@ -4423,10 +4338,10 @@ type-is@~1.6.17, type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" -typed-function@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/typed-function/-/typed-function-1.1.0.tgz#ea149706e0fb42aca1791c053a6d94ccd6c4fdcb" - integrity sha512-TuQzwiT4DDg19beHam3E66oRXhyqlyfgjHB/5fcvsRXbfmWPJfto9B4a0TBdTrQAPGlGmXh/k7iUI+WsObgORA== +typed-function@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/typed-function/-/typed-function-2.0.0.tgz#15ab3825845138a8b1113bd89e60cd6a435739e8" + integrity sha512-Hhy1Iwo/e4AtLZNK10ewVVcP2UEs408DS35ubP825w/YgSBK1KVLwALvvIG4yX75QJrxjCpcWkzkVRB0BwwYlA== typedarray-to-buffer@^3.1.5: version "3.1.5" @@ -4435,25 +4350,43 @@ typedarray-to-buffer@^3.1.5: dependencies: is-typedarray "^1.0.0" -typedarray@^0.0.6: - version "0.0.6" - resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" - integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= +typescript@^4.2.3, typescript@^4.2.4: + version "4.2.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.2.4.tgz#8610b59747de028fda898a8aef0e103f156d0961" + integrity sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg== -typescript@4.0: - version "4.0.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.5.tgz#ae9dddfd1069f1cb5beb3ef3b2170dd7c1332389" - integrity sha512-ywmr/VrTVCmNTJ6iV2LwIrfG1P+lv6luD8sUJs+2eI9NLGigaN+nUQc13iHqisq7bra9lnmUSYqbJvegraBOPQ== +unbox-primitive@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471" + integrity sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw== + dependencies: + function-bind "^1.1.1" + has-bigints "^1.0.1" + has-symbols "^1.0.2" + which-boxed-primitive "^1.0.2" -typescript@^3.5.1: - version "3.9.7" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.7.tgz#98d600a5ebdc38f40cb277522f12dc800e9e25fa" - integrity sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw== +unbzip2-stream@^1.3.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7" + integrity sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg== + dependencies: + buffer "^5.2.1" + through "^2.3.8" -typical@^5.0.0, typical@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/typical/-/typical-5.2.0.tgz#4daaac4f2b5315460804f0acf6cb69c52bb93066" - integrity sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg== +universalify@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" + integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== + +universalify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-1.0.0.tgz#b61a1da173e8435b2fe3c67d29b9adf8594bd16d" + integrity sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug== + +universalify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" + integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" @@ -4488,9 +4421,9 @@ uuid@^3.3.2, uuid@^3.3.3: integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== v8-compile-cache@^2.0.3, v8-compile-cache@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz#9471efa3ef9128d2f7c6a7ca39c4dd6b5055b132" - integrity sha512-gTpR5XQNKFwOd4clxfnhaqvfqMpqEwr4tOtCyz4MtYZX2JYhfr1JvBFKdS+7K/9rfpZR3VLX+YWBbKoxCgS43Q== + version "2.3.0" + resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" + integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== vary@~1.1.2: version "1.1.2" @@ -4521,9 +4454,9 @@ w3c-xmlserializer@^2.0.0: xml-name-validator "^3.0.0" watchpack@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.0.1.tgz#2f2192c542c82a3bcde76acd3411470c120426a8" - integrity sha512-vO8AKGX22ZRo6PiOFM9dC0re8IcKh8Kd/aH2zeqUc6w4/jBGlTy2P7fTC6ekT0NjVeGjgU2dGC5rNstKkeLEQg== + version "2.1.1" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.1.1.tgz#e99630550fca07df9f90a06056987baa40a689c7" + integrity sha512-Oo7LXCmc1eE1AjyuSBmtC3+Wy4HcV8PxWh2kP6fOl8yTlNS7r0K9l1ao2lrrUza7V39Y3D/BbJgY8VeSlc5JKw== dependencies: glob-to-regexp "^0.4.1" graceful-fs "^4.1.2" @@ -4545,31 +4478,32 @@ webidl-conversions@^6.1.0: resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-6.1.0.tgz#9111b4d7ea80acd40f5270d666621afa78b69514" integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w== -webpack-cli@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-4.2.0.tgz#10a09030ad2bd4d8b0f78322fba6ea43ec56aaaa" - integrity sha512-EIl3k88vaF4fSxWSgtAQR+VwicfLMTZ9amQtqS4o+TDPW9HGaEpbFBbAZ4A3ZOT5SOnMxNOzROsSTPiE8tBJPA== +webpack-cli@^4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-4.7.0.tgz#3195a777f1f802ecda732f6c95d24c0004bc5a35" + integrity sha512-7bKr9182/sGfjFm+xdZSwgQuFjgEcy0iCTIBxRUeteJ2Kr8/Wz0qNJX+jw60LU36jApt4nmMkep6+W5AKhok6g== dependencies: - "@webpack-cli/info" "^1.1.0" - "@webpack-cli/serve" "^1.1.0" + "@discoveryjs/json-ext" "^0.5.0" + "@webpack-cli/configtest" "^1.0.3" + "@webpack-cli/info" "^1.2.4" + "@webpack-cli/serve" "^1.4.0" colorette "^1.2.1" - command-line-usage "^6.1.0" - commander "^6.2.0" - enquirer "^2.3.6" - execa "^4.1.0" + commander "^7.0.0" + execa "^5.0.0" + fastest-levenshtein "^1.0.12" import-local "^3.0.2" interpret "^2.2.0" - leven "^3.1.0" rechoir "^0.7.0" v8-compile-cache "^2.2.0" - webpack-merge "^4.2.2" + webpack-merge "^5.7.3" -webpack-merge@^4.2.2: - version "4.2.2" - resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-4.2.2.tgz#a27c52ea783d1398afd2087f547d7b9d2f43634d" - integrity sha512-TUE1UGoTX2Cd42j3krGYqObZbOD+xF7u28WB7tfUordytSjbWTIjK/8V0amkBfTYN4/pB/GIDlJZZ657BGG19g== +webpack-merge@^5.7.3: + version "5.7.3" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.7.3.tgz#2a0754e1877a25a8bbab3d2475ca70a052708213" + integrity sha512-6/JUQv0ELQ1igjGDzHkXbVDRxkfA57Zw7PfiupdLFJYrgFqY5ZP8xxbpp2lU3EPwYx89ht5Z/aDkD40hFCm5AA== dependencies: - lodash "^4.17.15" + clone-deep "^4.0.1" + wildcard "^2.0.0" webpack-sources@^2.1.1: version "2.2.0" @@ -4579,33 +4513,32 @@ webpack-sources@^2.1.1: source-list-map "^2.0.1" source-map "^0.6.1" -webpack@^5.4.0: - version "5.4.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.4.0.tgz#4fdc6ec8a0ff9160701fb8f2eb8d06b33ecbae0f" - integrity sha512-udpYTyqz8toTTdaOsL2QKPLeZLt2IEm9qY7yTXuFEQhKu5bk0yQD9BtAdVQksmz4jFbbWOiWmm3NHarO0zr/ng== +webpack@^5, webpack@^5.37.0: + version "5.37.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.37.0.tgz#2ab00f613faf494504eb2beef278dab7493cc39d" + integrity sha512-yvdhgcI6QkQkDe1hINBAJ1UNevqNGTVaCkD2SSJcB8rcrNNl922RI8i2DXUAuNfANoxwsiXXEA4ZPZI9q2oGLA== dependencies: "@types/eslint-scope" "^3.7.0" - "@types/estree" "^0.0.45" - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/helper-module-context" "1.9.0" - "@webassemblyjs/wasm-edit" "1.9.0" - "@webassemblyjs/wasm-parser" "1.9.0" - acorn "^8.0.4" + "@types/estree" "^0.0.47" + "@webassemblyjs/ast" "1.11.0" + "@webassemblyjs/wasm-edit" "1.11.0" + "@webassemblyjs/wasm-parser" "1.11.0" + acorn "^8.2.1" browserslist "^4.14.5" chrome-trace-event "^1.0.2" - enhanced-resolve "^5.3.1" + enhanced-resolve "^5.8.0" + es-module-lexer "^0.4.0" eslint-scope "^5.1.1" events "^3.2.0" glob-to-regexp "^0.4.1" graceful-fs "^4.2.4" json-parse-better-errors "^1.0.2" - loader-runner "^4.1.0" + loader-runner "^4.2.0" mime-types "^2.1.27" neo-async "^2.6.2" - pkg-dir "^4.2.0" schema-utils "^3.0.0" - tapable "^2.0.0" - terser-webpack-plugin "^5.0.3" + tapable "^2.1.1" + terser-webpack-plugin "^5.1.1" watchpack "^2.0.0" webpack-sources "^2.1.1" @@ -4621,25 +4554,25 @@ whatwg-mimetype@^2.3.0: resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf" integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== -whatwg-url@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-8.0.0.tgz#37f256cb746398e19b107bd6ef820b4ae2d15871" - integrity sha512-41ou2Dugpij8/LPO5Pq64K5q++MnRCBpEHvQr26/mArEKTkCV5aoXIqyhuYtE0pkqScXwhf2JP57rkRTYM29lQ== +whatwg-url@^8.0.0, whatwg-url@^8.5.0: + version "8.5.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-8.5.0.tgz#7752b8464fc0903fec89aa9846fc9efe07351fd3" + integrity sha512-fy+R77xWv0AiqfLl4nuGUlQ3/6b5uNfQ4WAbGQVMYshCTCCPK9psC1nWh3XHuxGVCtlcDDQPQW1csmmIQo+fwg== dependencies: - lodash.sortby "^4.7.0" - tr46 "^2.0.0" - webidl-conversions "^5.0.0" + lodash "^4.7.0" + tr46 "^2.0.2" + webidl-conversions "^6.1.0" -which-boxed-primitive@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.1.tgz#cbe8f838ebe91ba2471bb69e9edbda67ab5a5ec1" - integrity sha512-7BT4TwISdDGBgaemWU0N0OU7FeAEJ9Oo2P1PHRm/FCWoEi2VLWC9b6xvxAA3C/NMpxg3HXVgi0sMmGbNUbNepQ== +which-boxed-primitive@^1.0.1, which-boxed-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" + integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== dependencies: - is-bigint "^1.0.0" - is-boolean-object "^1.0.0" - is-number-object "^1.0.3" - is-string "^1.0.4" - is-symbol "^1.0.2" + is-bigint "^1.0.1" + is-boolean-object "^1.1.0" + is-number-object "^1.0.4" + is-string "^1.0.5" + is-symbol "^1.0.3" which-collection@^1.0.1: version "1.0.1" @@ -4657,24 +4590,18 @@ which-module@^2.0.0: integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= which-typed-array@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.2.tgz#e5f98e56bda93e3dac196b01d47c1156679c00b2" - integrity sha512-KT6okrd1tE6JdZAy3o2VhMoYPh3+J6EMZLyrxBQsZflI1QCZIxMrIYLkosd8Twf+YfknVIHmYQPgJt238p8dnQ== + version "1.1.4" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.4.tgz#8fcb7d3ee5adf2d771066fba7cf37e32fe8711ff" + integrity sha512-49E0SpUe90cjpoc7BOJwyPHRqSAd12c10Qm2amdEZrJPCY2NDxaW01zHITrem+rnETY3dwrbH3UUrUwagfCYDA== dependencies: available-typed-arrays "^1.0.2" - es-abstract "^1.17.5" + call-bind "^1.0.0" + es-abstract "^1.18.0-next.1" foreach "^2.0.5" function-bind "^1.1.1" has-symbols "^1.0.1" is-typed-array "^1.1.3" -which@1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" - integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== - dependencies: - isexe "^2.0.0" - which@2.0.2, which@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" @@ -4689,55 +4616,43 @@ wide-align@1.1.3: dependencies: string-width "^1.0.2 || 2" -winston-transport@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.3.0.tgz#df68c0c202482c448d9b47313c07304c2d7c2c66" - integrity sha512-B2wPuwUi3vhzn/51Uukcao4dIduEiPOcOt9HJ3QeaXgkJ5Z7UwpBzxS4ZGNHtrxrUvTwemsQiSys0ihOf8Mp1A== +wildcard@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec" + integrity sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw== + +winston-transport@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.4.0.tgz#17af518daa690d5b2ecccaa7acf7b20ca7925e59" + integrity sha512-Lc7/p3GtqtqPBYYtS6KCN3c77/2QCev51DvcJKbkFPQNoj1sinkGwLGFDxkXY9J6p9+EPnYs+D90uwbnaiURTw== dependencies: - readable-stream "^2.3.6" + readable-stream "^2.3.7" triple-beam "^1.2.0" -winston@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/winston/-/winston-3.2.1.tgz#63061377976c73584028be2490a1846055f77f07" - integrity sha512-zU6vgnS9dAWCEKg/QYigd6cgMVVNwyTzKs81XZtTFuRwJOcDdBg7AU0mXVyNbs7O5RH2zdv+BdNZUlx7mXPuOw== +winston@^3.3.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/winston/-/winston-3.3.3.tgz#ae6172042cafb29786afa3d09c8ff833ab7c9170" + integrity sha512-oEXTISQnC8VlSAKf1KYSSd7J6IWuRPQqDdo8eoRNaYKLvwSb5+79Z3Yi1lrl6KDpU6/VWaxpakDAtb1oQ4n9aw== dependencies: - async "^2.6.1" - diagnostics "^1.1.1" - is-stream "^1.1.0" - logform "^2.1.1" - one-time "0.0.4" - readable-stream "^3.1.1" + "@dabh/diagnostics" "^2.0.2" + async "^3.1.0" + is-stream "^2.0.0" + logform "^2.2.0" + one-time "^1.0.0" + readable-stream "^3.4.0" stack-trace "0.0.x" triple-beam "^1.3.0" - winston-transport "^4.3.0" + winston-transport "^4.4.0" word-wrap@^1.2.3, word-wrap@~1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== -wordwrapjs@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/wordwrapjs/-/wordwrapjs-4.0.0.tgz#9aa9394155993476e831ba8e59fb5795ebde6800" - integrity sha512-Svqw723a3R34KvsMgpjFBYCgNOSdcW3mQFK4wIfhGQhtaFVOJmdYoXgi63ne3dTlWgatVcUc7t4HtQ/+bUVIzQ== - dependencies: - reduce-flatten "^2.0.0" - typical "^5.0.0" - -workerpool@6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.0.2.tgz#e241b43d8d033f1beb52c7851069456039d1d438" - integrity sha512-DSNyvOpFKrNusaaUwk+ej6cBj1bmhLcBfj80elGk+ZIo5JSkq+unB1dLKEOcNfJDZgjGICfhQ0Q5TbP0PvF4+Q== - -wrap-ansi@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" - integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q== - dependencies: - ansi-styles "^3.2.0" - string-width "^3.0.0" - strip-ansi "^5.0.0" +workerpool@6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.1.0.tgz#a8e038b4c94569596852de7a8ea4228eefdeb37b" + integrity sha512-toV7q9rWNYha963Pl/qyeZ6wG+3nnsyvolaNUS8+R5Wtw6qJPTxIlOP1ZSvcGhEJw+l3HMMmtiNo9Gl61G4GVg== wrap-ansi@^6.2.0: version "6.2.0" @@ -4748,6 +4663,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" @@ -4763,13 +4687,6 @@ write-file-atomic@^3.0.0: signal-exit "^3.0.2" typedarray-to-buffer "^3.1.5" -write@1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/write/-/write-1.0.3.tgz#0800e14523b923a387e415123c865616aae0f5c3" - integrity sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig== - dependencies: - mkdirp "^0.5.1" - ws@^5.2.0: version "5.2.2" resolved "https://registry.yarnpkg.com/ws/-/ws-5.2.2.tgz#dffef14866b8e8dc9133582514d1befaf96e980f" @@ -4777,17 +4694,10 @@ ws@^5.2.0: dependencies: async-limiter "~1.0.0" -ws@^6.1.0: - version "6.2.1" - resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.1.tgz#442fdf0a47ed64f59b6a5d8ff130f4748ed524fb" - integrity sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA== - dependencies: - async-limiter "~1.0.0" - -ws@^7.2.3, ws@^7.3.1: - version "7.3.1" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.3.1.tgz#d0547bf67f7ce4f12a72dfe31262c68d7dc551c8" - integrity sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA== +ws@^7.2.3, ws@^7.3.1, ws@^7.4.4, ws@^7.4.5: + version "7.4.6" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" + integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== xml-name-validator@^3.0.0: version "3.0.0" @@ -4799,38 +4709,46 @@ xmlchars@^2.2.0: resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== -xterm-benchmark@^0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/xterm-benchmark/-/xterm-benchmark-0.1.3.tgz#c637d078f7b73f77a4342299e706b4d0e52ab46a" - integrity sha512-HBSeUOFlr9JVMFNkL5w8EkuWccczkfZAX6adK5fSot1sRRAFJS9NZcXH/yRLZp9S24qOUBJznMZGa9CjWB3h3g== +xterm-benchmark@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/xterm-benchmark/-/xterm-benchmark-0.2.1.tgz#f7741127aa8eceaffbb0cbbca93d374075e44d64" + integrity sha512-QtfTIlCrjPlzmdjaxRXC0JeE1Dwk25WjNzHR1bTKRqSSQToVw+rcp53o5V5I9AaVWef8vCdJ7lVxXuuX4sphew== dependencies: "@types/app-root-path" "^1.2.4" "@types/cli-table" "^0.3.0" - "@types/mathjs" "^5.0.1" - "@types/mocha" "^5.2.7" - "@types/node" "^12.0.4" - app-root-path "^2.2.1" - chrome-timeline "0.0.12" - cli-table "^0.3.1" + "@types/mathjs" "^6.0.11" + "@types/mocha" "^8.2.1" + "@types/node" "^12.12.37" + "@types/puppeteer" "^5.4.3" + app-root-path "^3.0.0" + chrome-timeline "0.0.15" + cli-table "^0.3.6" columnify "^1.5.4" - commander "^2.20.0" - mathjs "^5.10.3" - mocha "^6.1.4" - tslint "^5.17.0" - typescript "^3.5.1" + commander "^6.2.1" + mathjs "^9.3.0" + mocha "^8.3.2" + tslint "^6.1.3" + typescript "^4.2.3" y18n@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" - integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== + version "4.0.1" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.1.tgz#8db2b83c31c5d75099bb890b23f3094891e247d4" + integrity sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ== -yargs-parser@13.1.2, yargs-parser@^13.1.2: - version "13.1.2" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38" - integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yargs-parser@20.2.4: + version "20.2.4" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54" + integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== yargs-parser@^18.1.1: version "18.1.2" @@ -4840,14 +4758,10 @@ yargs-parser@^18.1.1: camelcase "^5.0.0" decamelize "^1.2.0" -yargs-unparser@1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-1.6.0.tgz#ef25c2c769ff6bd09e4b0f9d7c605fb27846ea9f" - integrity sha512-W9tKgmSn0DpSatfri0nx52Joq5hVXgeLiqR/5G0sZNDoLZFOr/xjBUDcShCOGNsBnEMNo1KAMBkTej1Hm62HTw== - dependencies: - flat "^4.1.0" - lodash "^4.17.15" - yargs "^13.3.0" +yargs-parser@^20.2.2: + version "20.2.7" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.7.tgz#61df85c113edfb5a7a4e36eb8aa60ef423cbc90a" + integrity sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw== yargs-unparser@2.0.0: version "2.0.0" @@ -4859,21 +4773,18 @@ yargs-unparser@2.0.0: flat "^5.0.2" is-plain-obj "^2.1.0" -yargs@13.3.2, yargs@^13.3.0: - version "13.3.2" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd" - integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw== +yargs@16.2.0: + version "16.2.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== dependencies: - cliui "^5.0.0" - find-up "^3.0.0" - get-caller-file "^2.0.1" + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" require-directory "^2.1.1" - require-main-filename "^2.0.0" - set-blocking "^2.0.0" - string-width "^3.0.0" - which-module "^2.0.0" - y18n "^4.0.0" - yargs-parser "^13.1.2" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" yargs@^15.0.2: version "15.3.1" @@ -4899,3 +4810,15 @@ yauzl@^2.10.0: dependencies: buffer-crc32 "~0.2.3" fd-slicer "~1.1.0" + +yazl@^2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/yazl/-/yazl-2.5.1.tgz#a3d65d3dd659a5b0937850e8609f22fffa2b5c35" + integrity sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw== + dependencies: + buffer-crc32 "~0.2.3" + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==